mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(pricing): implement A/B experiment for pricing page flexibility/visibility (#44293)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES/NO ## What kind of change does this PR introduce? Includes 5 new variants to test pricing to solve cost flexibility and visibility concerns ## What is the current behavior? Current pricing page ## What is the new behavior? Variants: <img width="3840" height="2160" alt="CleanShot 2026-03-30 at 19 32 02@2x" src="https://github.com/user-attachments/assets/a4f3b048-fa0d-4137-9f24-2eb6d431f3c8" /> <img width="3840" height="2160" alt="CleanShot 2026-03-30 at 19 33 34@2x" src="https://github.com/user-attachments/assets/a9045a33-b8c3-4df1-a47d-00cf58495bb3" /> <img width="3840" height="2160" alt="CleanShot 2026-03-30 at 19 32 22@2x" src="https://github.com/user-attachments/assets/10511fc8-5e88-4e3c-baa1-56798009d86f" /> <img width="3840" height="2160" alt="CleanShot 2026-03-30 at 19 33 09@2x" src="https://github.com/user-attachments/assets/aa01aaf6-0e41-4466-bfc7-77ae912b6043" /> <img width="3840" height="2160" alt="CleanShot 2026-03-30 at 19 32 43@2x" src="https://github.com/user-attachments/assets/21834f7d-43cb-4981-9c29-934f44986714" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes **New Features** - Launched A/B testing variants on the pricing page featuring customized pricing plan layouts, descriptions, and billing options based on your assigned experiment group. - Certain plan tiers now display optional flexibility information, hourly billing rates, and project scaling details to help optimize your cost estimation and plan selection. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
83edf14a23
commit
f590319f0a
3 files changed
+169
-27
No files matched your search
@@ -4,13 +4,25 @@ import { ArrowDownIcon } from '@heroicons/react/outline'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import PricingPlans from '~/components/Pricing/PricingPlans'
|
||||
import { useOrganizations } from '~/data/organizations'
|
||||
import { hasConsented, posthogClient } from 'common'
|
||||
import { ArrowUpRight } from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
|
||||
const EXPERIMENT_ID = 'pricingPageExperiment' as const
|
||||
export type PricingPageExperimentVariant =
|
||||
| 'control'
|
||||
// GROWTH-694: flexibility/hourly billing
|
||||
| 'flexibility' // concrete FAQ example as section between cards and buttons
|
||||
| 'flexibility_card' // "no lock-in" messaging on Pro card warning
|
||||
| 'hourly_rate' // actual hourly compute rate on Pro card warning
|
||||
// GROWTH-697: pro project cost visibility
|
||||
| 'multi_project' // "first project included, additional from $10/mo" on Pro card
|
||||
| 'estimate_cta' // "estimate your cost" link on Pro card scrolling to calculator
|
||||
|
||||
const PricingComputeSection = dynamic(() => import('~/components/Pricing/PricingComputeSection'))
|
||||
const PricingAddons = dynamic(() => import('~/components/Pricing/PricingAddons'))
|
||||
const PricingComparisonTable = dynamic(() => import('~/components/Pricing/PricingComparisonTable'))
|
||||
@@ -45,6 +57,41 @@ export default function PricingContent() {
|
||||
const { isLoading, organizations } = useOrganizations()
|
||||
const hasExistingOrganizations = !isLoading && organizations.length > 0
|
||||
|
||||
// Pricing value/flexibility A/B experiment
|
||||
// Uses client-side PostHog directly — server-side evaluation lacks full person context for www pages.
|
||||
// DevToolbar overrides (x-ph-flag-overrides cookie) are respected in local dev via posthogClient.getFeatureFlag.
|
||||
const [flagValue, setFlagValue] = useState<PricingPageExperimentVariant | false | undefined>(
|
||||
() =>
|
||||
posthogClient.getFeatureFlag(EXPERIMENT_ID) as
|
||||
| PricingPageExperimentVariant
|
||||
| false
|
||||
| undefined
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return posthogClient.onFeatureFlags(() => {
|
||||
const value = posthogClient.getFeatureFlag(EXPERIMENT_ID)
|
||||
setFlagValue(value as PricingPageExperimentVariant | false | undefined)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const validVariants: PricingPageExperimentVariant[] = [
|
||||
'control',
|
||||
'flexibility',
|
||||
'flexibility_card',
|
||||
'hourly_rate',
|
||||
'multi_project',
|
||||
'estimate_cta',
|
||||
]
|
||||
const isInExperiment = validVariants.includes(flagValue as PricingPageExperimentVariant)
|
||||
const showFlexibilitySection = flagValue === 'flexibility'
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInExperiment) return
|
||||
|
||||
posthogClient.captureExperimentExposure(EXPERIMENT_ID, { variant: flagValue }, hasConsented())
|
||||
}, [isInExperiment, flagValue])
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<div className="relative z-10 pt-8 pb-4 xl:py-16">
|
||||
@@ -64,8 +111,20 @@ export default function PricingContent() {
|
||||
<PricingPlans
|
||||
organizations={organizations}
|
||||
hasExistingOrganizations={hasExistingOrganizations}
|
||||
experimentVariant={isInExperiment ? (flagValue as PricingPageExperimentVariant) : undefined}
|
||||
/>
|
||||
|
||||
{showFlexibilitySection && (
|
||||
<div className="mx-auto max-w-xl px-8 mt-10 xl:mt-16 text-center">
|
||||
<p className="text-foreground text-sm">
|
||||
Need more power for a launch? Scale up instantly.{' '}
|
||||
<span className="text-foreground-light">
|
||||
Scale back down after — you only pay for the hours you use.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mt-10 xl:mt-16 mx-auto max-w-lg flex flex-col gap-8">
|
||||
<div className="flex justify-center gap-2">
|
||||
<a href="#compare-plans">
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Check } from 'lucide-react'
|
||||
import { plans } from 'shared-data/plans'
|
||||
import { Button, cn } from 'ui'
|
||||
import type { PricingPageExperimentVariant } from '~/app/pricing/PricingContent'
|
||||
import { Organization } from '~/data/organizations'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import { Check } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { plans } from 'shared-data/plans'
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
import UpgradePlan from './UpgradePlan'
|
||||
|
||||
interface PricingPlansProps {
|
||||
organizations?: Organization[]
|
||||
hasExistingOrganizations?: boolean
|
||||
experimentVariant?: PricingPageExperimentVariant
|
||||
}
|
||||
|
||||
const PricingPlans = ({ organizations, hasExistingOrganizations }: PricingPlansProps) => {
|
||||
const PricingPlans = ({
|
||||
organizations,
|
||||
hasExistingOrganizations,
|
||||
experimentVariant,
|
||||
}: PricingPlansProps) => {
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
|
||||
return (
|
||||
@@ -66,14 +72,37 @@ const PricingPlans = ({ organizations, hasExistingOrganizations }: PricingPlansP
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
<div
|
||||
className={cn(
|
||||
'text-foreground-light mb-4 text-sm 2xl:pr-4',
|
||||
isProPlan && 'xl:mb-12'
|
||||
'flex flex-col',
|
||||
isProPlan &&
|
||||
(!experimentVariant || experimentVariant === 'control') &&
|
||||
'xl:mb-12'
|
||||
)}
|
||||
>
|
||||
{plan.description}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-light text-sm 2xl:pr-4',
|
||||
isUpgradablePlan &&
|
||||
(experimentVariant === 'flexibility_card' ||
|
||||
experimentVariant === 'hourly_rate')
|
||||
? 'mb-2'
|
||||
: 'mb-4'
|
||||
)}
|
||||
>
|
||||
{plan.description}
|
||||
</p>
|
||||
{isUpgradablePlan && experimentVariant === 'flexibility_card' && (
|
||||
<p className="text-brand text-xs mb-4">
|
||||
No lock-in. Scale up or down anytime — you only pay for the hours you use.
|
||||
</p>
|
||||
)}
|
||||
{isUpgradablePlan && experimentVariant === 'hourly_rate' && (
|
||||
<p className="text-brand text-xs mb-4">
|
||||
Compute from $0.01/hr. Scale anytime, pay only for what you use.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isUpgradablePlan && hasExistingOrganizations ? (
|
||||
<UpgradePlan
|
||||
planId={plan.planId}
|
||||
@@ -123,24 +152,48 @@ const PricingPlans = ({ organizations, hasExistingOrganizations }: PricingPlansP
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{plan.warning && (
|
||||
{isUpgradablePlan && experimentVariant === 'multi_project' ? (
|
||||
<div className="mt-4 flex flex-col gap-1">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[13px] leading-4 inline-flex gap-1 items-center'
|
||||
)}
|
||||
>
|
||||
{plan.warning}
|
||||
<span className="text-[13px] leading-4">
|
||||
First project included. Additional projects from $10/mo.
|
||||
</span>
|
||||
{(plan.name === 'Pro' || plan.name === 'Team') && (
|
||||
<Link
|
||||
href="#addon-compute"
|
||||
className="hover:underline text-foreground-lighter text-[13px] m-0 p-0 leading-3"
|
||||
>
|
||||
Need more compute?
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href="#addon-compute"
|
||||
className="hover:underline text-foreground-lighter text-[13px] m-0 p-0 leading-3"
|
||||
>
|
||||
See how pricing scales
|
||||
</Link>
|
||||
</div>
|
||||
) : isUpgradablePlan && experimentVariant === 'estimate_cta' ? (
|
||||
<div className="mt-4 flex flex-col gap-1">
|
||||
<span className="text-[13px] leading-4">{plan.warning}</span>
|
||||
<Link
|
||||
href="#addon-compute"
|
||||
className="hover:underline text-brand text-[13px] m-0 p-0 leading-3 font-medium"
|
||||
>
|
||||
Estimate your cost ↓
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
plan.warning && (
|
||||
<div className="mt-4 flex flex-col gap-1">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[13px] leading-4 inline-flex gap-1 items-center'
|
||||
)}
|
||||
>
|
||||
{plan.warning}
|
||||
</span>
|
||||
{(plan.name === 'Pro' || plan.name === 'Team') && (
|
||||
<Link
|
||||
href="#addon-compute"
|
||||
className="hover:underline text-foreground-lighter text-[13px] m-0 p-0 leading-3"
|
||||
>
|
||||
Need more compute?
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2992,6 +2992,35 @@ export interface StorageExplorerNavigateSubmittedEvent {
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User was exposed to the pricing value/flexibility experiment on the /pricing page.
|
||||
*
|
||||
* @group Events
|
||||
* @source www
|
||||
* @page /pricing
|
||||
*/
|
||||
export interface PricingPageExperimentExposedEvent {
|
||||
action: 'pricing_page_experiment_exposed'
|
||||
properties: {
|
||||
/**
|
||||
* Experiment identifier for tracking
|
||||
*/
|
||||
experiment_id: 'pricingPageExperiment'
|
||||
/**
|
||||
* Experiment variant.
|
||||
* GROWTH-694: flexibility (section), flexibility_card (on card), hourly_rate (on card)
|
||||
* GROWTH-697: multi_project (on card), estimate_cta (on card)
|
||||
*/
|
||||
variant:
|
||||
| 'control'
|
||||
| 'flexibility'
|
||||
| 'flexibility_card'
|
||||
| 'hourly_rate'
|
||||
| 'multi_project'
|
||||
| 'estimate_cta'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@@ -3160,3 +3189,4 @@ export type TelemetryEvent =
|
||||
| ComputeBadgeUpgradeClickedEvent
|
||||
| FreeMicroUpgradeBannerDismissedEvent
|
||||
| FreeMicroUpgradeBannerCtaClickedEvent
|
||||
| PricingPageExperimentExposedEvent
|
||||
Reference in new issue
Block a user