Skip to main content

Experiments & Feature Flags

The @54vie/analytics package provides a complete experimentation platform including A/B testing, multivariate testing, and feature flags. This guide covers the experiment APIs, feature flags, and conversion tracking.

Installation

npm install @54vie/analytics
# or
yarn add @54vie/analytics

TypeScript Interfaces

Experiment Types

/**
* Configuration for an experiment
*/
interface Experiment {
id: string;
name: string;
description?: string;
status: ExperimentStatus;
type: ExperimentType;
variants: Variant[];
targetingRules?: TargetingRule[];
trafficAllocation: number;
startDate?: string;
endDate?: string;
primaryMetric?: string;
secondaryMetrics?: string[];
createdAt: string;
updatedAt: string;
}

enum ExperimentStatus {
DRAFT = 'draft',
RUNNING = 'running',
PAUSED = 'paused',
COMPLETED = 'completed',
ARCHIVED = 'archived',
}

enum ExperimentType {
AB_TEST = 'ab_test',
MULTIVARIATE = 'multivariate',
FEATURE_FLAG = 'feature_flag',
ROLLOUT = 'rollout',
}

interface Variant {
id: string;
name: string;
description?: string;
weight: number;
payload?: Record<string, unknown>;
isControl?: boolean;
}

interface TargetingRule {
id: string;
attribute: string;
operator: TargetingOperator;
value: string | number | boolean | string[];
}

enum TargetingOperator {
EQUALS = 'equals',
NOT_EQUALS = 'not_equals',
CONTAINS = 'contains',
NOT_CONTAINS = 'not_contains',
STARTS_WITH = 'starts_with',
ENDS_WITH = 'ends_with',
GREATER_THAN = 'greater_than',
LESS_THAN = 'less_than',
IN = 'in',
NOT_IN = 'not_in',
REGEX = 'regex',
IS_SET = 'is_set',
IS_NOT_SET = 'is_not_set',
}

Feature Flag Types

/**
* Configuration for a feature flag
*/
interface FeatureFlag {
id: string;
key: string;
name: string;
description?: string;
type: FeatureFlagType;
defaultValue: FlagValue;
enabled: boolean;
targetingRules?: TargetingRule[];
rolloutPercentage?: number;
variants?: FlagVariant[];
createdAt: string;
updatedAt: string;
}

enum FeatureFlagType {
BOOLEAN = 'boolean',
STRING = 'string',
NUMBER = 'number',
JSON = 'json',
}

type FlagValue = boolean | string | number | Record<string, unknown>;

interface FlagVariant {
value: FlagValue;
weight: number;
name?: string;
}

interface FeatureFlagEvaluation {
key: string;
value: FlagValue;
reason: EvaluationReason;
ruleId?: string;
variantKey?: string;
}

enum EvaluationReason {
DEFAULT = 'default',
TARGETING_MATCH = 'targeting_match',
ROLLOUT = 'rollout',
OVERRIDE = 'override',
ERROR = 'error',
}

Hook Return Types

/**
* Return type for useExperiment hook
*/
interface UseExperimentReturn<T = string> {
variant: T;
isLoading: boolean;
isActive: boolean;
error: Error | null;
trackConversion: (conversionName?: string, properties?: EventProperties) => void;
trackExposure: () => void;
metadata: ExperimentMetadata | null;
}

interface ExperimentMetadata {
experimentId: string;
experimentName: string;
variantId: string;
variantName: string;
startedAt: string;
endsAt?: string;
}

/**
* Return type for useFeatureFlag hook
*/
interface UseFeatureFlagReturn<T = boolean> {
value: T;
isLoading: boolean;
isEnabled: boolean;
error: Error | null;
evaluation: FeatureFlagEvaluation | null;
}

/**
* Return type for useFeatureFlags hook (batch)
*/
interface UseFeatureFlagsReturn {
flags: Record<string, FlagValue>;
isLoading: boolean;
error: Error | null;
getFlag: <T = boolean>(key: string, defaultValue?: T) => T;
isEnabled: (key: string) => boolean;
}

useExperiment Hook

The useExperiment hook provides access to A/B test variants and handles experiment tracking.

Import

import { useExperiment } from '@54vie/analytics';

Basic Usage

import { useExperiment } from '@54vie/analytics';

function PricingPage() {
const { variant, isLoading, trackConversion } = useExperiment<'control' | 'new_layout'>(
'pricing_page_experiment',
{ defaultVariant: 'control' }
);

if (isLoading) {
return <LoadingSpinner />;
}

const handlePurchase = (planId: string) => {
trackConversion('purchase', { planId });
};

if (variant === 'new_layout') {
return <NewPricingLayout onPurchase={handlePurchase} />;
}

return <ControlPricingLayout onPurchase={handlePurchase} />;
}

Hook Options

interface UseExperimentOptions<T = string> {
/**
* Default variant if experiment not found or user not in experiment
*/
defaultVariant?: T;

/**
* Automatically track exposure when variant is accessed
* @default true
*/
autoTrackExposure?: boolean;

/**
* Skip this experiment (user always gets default variant)
*/
skip?: boolean;

/**
* Custom attributes for targeting
*/
attributes?: Record<string, unknown>;

/**
* Override the variant for testing purposes
*/
overrideVariant?: T;
}

Advanced Usage

import { useExperiment } from '@54vie/analytics';

type CheckoutVariant = 'single_page' | 'multi_step' | 'express';

function CheckoutPage({ user }) {
const {
variant,
isLoading,
isActive,
trackConversion,
trackExposure,
metadata,
error,
} = useExperiment<CheckoutVariant>('checkout_flow_experiment', {
defaultVariant: 'multi_step',
autoTrackExposure: false, // Manual exposure tracking
skip: user.isEmployee, // Skip for employees
attributes: {
userTier: user.tier,
country: user.country,
deviceType: getDeviceType(),
},
});

// Manual exposure tracking when component becomes visible
useEffect(() => {
if (!isLoading && isActive) {
trackExposure();
}
}, [isLoading, isActive]);

// Track multiple conversion events
const handleCheckoutStart = () => {
trackConversion('checkout_started');
};

const handleStepComplete = (step: number) => {
trackConversion('step_completed', { step });
};

const handlePurchase = (order: Order) => {
trackConversion('purchase', {
orderId: order.id,
revenue: order.total,
currency: order.currency,
});
};

if (error) {
console.error('Experiment error:', error);
// Fall back to default experience
}

const checkoutComponents: Record<CheckoutVariant, React.FC> = {
single_page: SinglePageCheckout,
multi_step: MultiStepCheckout,
express: ExpressCheckout,
};

const CheckoutComponent = checkoutComponents[variant];

return (
<CheckoutComponent
onStart={handleCheckoutStart}
onStepComplete={handleStepComplete}
onPurchase={handlePurchase}
/>
);
}

With Variant Payloads

import { useExperiment } from '@54vie/analytics';

interface ButtonVariantPayload {
color: string;
text: string;
size: 'small' | 'medium' | 'large';
}

function CTAButton() {
const { variant, metadata } = useExperiment<'control' | 'variant_a' | 'variant_b'>(
'cta_button_test'
);

// Access variant payload from experiment configuration
const payload = metadata?.payload as ButtonVariantPayload | undefined;

return (
<button
style={{ backgroundColor: payload?.color ?? 'blue' }}
className={`btn-${payload?.size ?? 'medium'}`}
>
{payload?.text ?? 'Sign Up'}
</button>
);
}

useFeatureFlag Hook

The useFeatureFlag hook provides access to individual feature flags.

Import

import { useFeatureFlag } from '@54vie/analytics';

Basic Usage

import { useFeatureFlag } from '@54vie/analytics';

function Dashboard() {
const { value: showNewWidget, isLoading } = useFeatureFlag('new_dashboard_widget');

if (isLoading) {
return <LoadingSpinner />;
}

return (
<div>
<ExistingWidgets />
{showNewWidget && <NewWidget />}
</div>
);
}

Hook Options

interface UseFeatureFlagOptions<T = boolean> {
/**
* Default value if flag not found or evaluation fails
*/
defaultValue?: T;

/**
* Custom attributes for targeting
*/
attributes?: Record<string, unknown>;

/**
* Track flag evaluation as an event
* @default false
*/
trackEvaluation?: boolean;

/**
* Override the flag value for testing
*/
overrideValue?: T;
}

Typed Feature Flags

import { useFeatureFlag } from '@54vie/analytics';

// Boolean flag
function NewFeatureComponent() {
const { value: isEnabled } = useFeatureFlag<boolean>('new_feature_enabled', {
defaultValue: false,
});

if (!isEnabled) return null;
return <NewFeature />;
}

// String flag
function ThemeComponent() {
const { value: theme } = useFeatureFlag<'light' | 'dark' | 'system'>('default_theme', {
defaultValue: 'system',
});

return <ThemeProvider theme={theme}>{/* ... */}</ThemeProvider>;
}

// Number flag
function RateLimitedComponent() {
const { value: maxRequests } = useFeatureFlag<number>('api_rate_limit', {
defaultValue: 100,
});

return <RateLimiter maxRequests={maxRequests}>{/* ... */}</RateLimiter>;
}

// JSON flag
interface FeatureConfig {
maxItems: number;
showBanner: boolean;
bannerText: string;
}

function ConfigurableFeature() {
const { value: config } = useFeatureFlag<FeatureConfig>('feature_config', {
defaultValue: {
maxItems: 10,
showBanner: false,
bannerText: '',
},
});

return (
<div>
{config.showBanner && <Banner text={config.bannerText} />}
<ItemList maxItems={config.maxItems} />
</div>
);
}

With Targeting Attributes

import { useFeatureFlag } from '@54vie/analytics';

function PremiumFeature({ user }) {
const { value: hasAccess, evaluation } = useFeatureFlag('premium_feature', {
defaultValue: false,
attributes: {
userId: user.id,
plan: user.subscription.plan,
country: user.country,
accountAge: calculateAccountAge(user.createdAt),
},
trackEvaluation: true,
});

// Log evaluation reason for debugging
useEffect(() => {
if (evaluation) {
console.log(`Flag evaluated: ${evaluation.reason}`, evaluation);
}
}, [evaluation]);

if (!hasAccess) {
return <UpgradePrompt />;
}

return <PremiumFeatureContent />;
}

useFeatureFlags Hook (Batch)

For accessing multiple feature flags efficiently.

Import

import { useFeatureFlags } from '@54vie/analytics';

Usage

import { useFeatureFlags } from '@54vie/analytics';

function App() {
const { flags, isLoading, getFlag, isEnabled } = useFeatureFlags([
'new_navigation',
'dark_mode',
'beta_features',
'api_v2',
]);

if (isLoading) {
return <LoadingSpinner />;
}

return (
<div>
{isEnabled('new_navigation') ? <NewNavigation /> : <LegacyNavigation />}

<ThemeProvider darkMode={getFlag('dark_mode', false)}>
<MainContent />
</ThemeProvider>

{isEnabled('beta_features') && <BetaFeatures />}
</div>
);
}

Conversion Tracking

Basic Conversion Tracking

import { useExperiment } from '@54vie/analytics';

function SignupForm() {
const { variant, trackConversion } = useExperiment('signup_flow');

const handleSignup = async (formData: FormData) => {
const user = await createUser(formData);

// Track primary conversion
trackConversion('signup_completed', {
userId: user.id,
signupMethod: formData.method,
});
};

return <form onSubmit={handleSignup}>{/* form fields */}</form>;
}

Multiple Conversion Points

import { useExperiment } from '@54vie/analytics';

function OnboardingFlow() {
const { trackConversion } = useExperiment('onboarding_experiment');

// Track funnel progression
const trackStep = (step: number, data?: Record<string, unknown>) => {
trackConversion(`onboarding_step_${step}`, data);
};

// Track completion
const trackComplete = () => {
trackConversion('onboarding_completed', {
totalTime: calculateElapsedTime(),
});
};

// Track drop-off
const trackDropoff = (step: number, reason?: string) => {
trackConversion('onboarding_dropoff', {
step,
reason,
});
};

return <OnboardingUI onStep={trackStep} onComplete={trackComplete} />;
}

Revenue Tracking

import { useExperiment } from '@54vie/analytics';

function CheckoutComplete({ order }) {
const { trackConversion } = useExperiment('checkout_experiment');

useEffect(() => {
// Track purchase with revenue
trackConversion('purchase', {
revenue: order.total,
currency: order.currency,
orderId: order.id,
itemCount: order.items.length,
});

// Track per-item revenue
order.items.forEach((item) => {
trackConversion('item_purchased', {
itemId: item.id,
itemName: item.name,
price: item.price,
quantity: item.quantity,
category: item.category,
});
});
}, [order.id]);

return <OrderConfirmation order={order} />;
}

useExperiments Hook (Multiple Experiments)

For components participating in multiple experiments.

Import

import { useExperiments } from '@54vie/analytics';

Usage

import { useExperiments } from '@54vie/analytics';

function ProductPage({ product }) {
const experiments = useExperiments({
layout: 'product_page_layout',
pricing: 'pricing_display_test',
recommendations: 'recommendation_algorithm',
});

if (experiments.isLoading) {
return <LoadingSpinner />;
}

const layoutVariant = experiments.getVariant('layout', 'control');
const pricingVariant = experiments.getVariant('pricing', 'standard');
const recsVariant = experiments.getVariant('recommendations', 'collaborative');

const handlePurchase = () => {
// Track conversion for all active experiments
experiments.trackConversion('purchase', {
productId: product.id,
price: product.price,
});
};

return (
<ProductLayout variant={layoutVariant}>
<ProductDetails product={product} />
<PriceDisplay product={product} variant={pricingVariant} />
<Recommendations variant={recsVariant} />
<BuyButton onClick={handlePurchase} />
</ProductLayout>
);
}

Server-Side Rendering

Next.js App Router

// app/page.tsx
import { ExperimentProvider, getExperimentVariant } from '@54vie/analytics/server';

export default async function Page() {
// Fetch variant on server
const variant = await getExperimentVariant('homepage_experiment', {
defaultVariant: 'control',
});

return (
<ExperimentProvider
experimentKey="homepage_experiment"
initialVariant={variant}
>
<HomePageContent />
</ExperimentProvider>
);
}

// Client component
'use client';
import { useExperiment } from '@54vie/analytics';

function HomePageContent() {
const { variant, trackConversion } = useExperiment('homepage_experiment');

// variant is hydrated from server, no loading state
return variant === 'new_hero' ? <NewHero /> : <ControlHero />;
}

Feature Flags with SSR

// app/layout.tsx
import { FeatureFlagProvider, evaluateFlags } from '@54vie/analytics/server';

export default async function RootLayout({ children }) {
const flags = await evaluateFlags(['new_navigation', 'dark_mode']);

return (
<html>
<body>
<FeatureFlagProvider initialFlags={flags}>
{children}
</FeatureFlagProvider>
</body>
</html>
);
}

Testing & Development

Override Variants in Development

import { AnalyticsProvider } from '@54vie/analytics';

function App() {
return (
<AnalyticsProvider
apiKey={process.env.NEXT_PUBLIC_ANALYTICS_KEY!}
projectId={process.env.NEXT_PUBLIC_PROJECT_ID!}
config={{
experiments: {
// Force specific variants in development
overrides: process.env.NODE_ENV === 'development' ? {
'checkout_experiment': 'express',
'pricing_test': 'variant_b',
} : undefined,
},
}}
>
<YourApp />
</AnalyticsProvider>
);
}

URL-Based Overrides

// Enable via URL: ?experiment_checkout=express
import { useExperiment } from '@54vie/analytics';

function Checkout() {
const searchParams = new URLSearchParams(window.location.search);
const urlOverride = searchParams.get('experiment_checkout');

const { variant } = useExperiment('checkout_experiment', {
overrideVariant: urlOverride as any,
});

// ...
}

Mock Experiments in Tests

import { render } from '@testing-library/react';
import { MockAnalyticsProvider } from '@54vie/analytics/testing';

describe('PricingPage', () => {
it('renders control variant', () => {
render(
<MockAnalyticsProvider
experiments={{
pricing_test: { variant: 'control', isActive: true },
}}
>
<PricingPage />
</MockAnalyticsProvider>
);

expect(screen.getByText('Standard Pricing')).toBeInTheDocument();
});

it('renders new pricing variant', () => {
render(
<MockAnalyticsProvider
experiments={{
pricing_test: { variant: 'new_pricing', isActive: true },
}}
>
<PricingPage />
</MockAnalyticsProvider>
);

expect(screen.getByText('New Pricing Layout')).toBeInTheDocument();
});
});

Best Practices

Experiment Design

// Good - clear, measurable experiment
const experiment = {
name: 'checkout_simplification',
hypothesis: 'Reducing checkout steps will increase conversion rate',
primaryMetric: 'checkout_completed',
secondaryMetrics: ['cart_abandonment', 'average_order_value'],
minimumSampleSize: 10000,
duration: '14 days',
};

// Define clear variants
type CheckoutVariant = 'control' | 'simplified' | 'express';

Gradual Rollouts

import { useFeatureFlag } from '@54vie/analytics';

function NewFeature() {
const { value: rolloutPercentage } = useFeatureFlag<number>('new_feature_rollout', {
defaultValue: 0,
});

// Feature is gradually rolled out via dashboard
// 0% -> 5% -> 25% -> 50% -> 100%

if (rolloutPercentage === 0) {
return null;
}

return <FeatureContent />;
}

Cleanup After Experiments

// Before experiment ends
function ComponentWithExperiment() {
const { variant } = useExperiment('button_color_test');

return variant === 'green' ? <GreenButton /> : <BlueButton />;
}

// After experiment concludes with 'green' as winner
function ComponentAfterExperiment() {
// Remove experiment code, ship winning variant
return <GreenButton />;
}

Avoiding Flickering

import { useExperiment } from '@54vie/analytics';

function HeroSection() {
const { variant, isLoading } = useExperiment('hero_test');

// Show skeleton while loading to prevent layout shift
if (isLoading) {
return <HeroSkeleton />;
}

return variant === 'video_hero' ? <VideoHero /> : <ImageHero />;
}
  • Provider - AnalyticsProvider setup and configuration
  • Hooks - useAnalytics, useUserProperties, useExperiment
  • Events - StandardEvents and custom event tracking