Features

  • Connects seamlessly with your GradualRollout backend API.
  • Framework agnostic: Use with React, Angular, Vue, or vanilla JS.
  • Consistent user bucketing via hashing for percentage rollouts.
  • Event-driven API with flagsUpdated, error, and initialized events.
  • Automatic polling with configurable interval and manual refresh support.
  • Simple to initialize and use with TypeScript support.
  • Dynamic user identity support: Works with anonymous users (anonId) before login and updates seamlessly post-login (userId).

Installation

Install the SDK using npm or yarn:

npm install gradual-rollout-sdk
# or
yarn add gradual-rollout-sdk

Basic Usage

Initialize the SDK and check feature flags:

import { GradualRolloutSDK } from 'gradual-rollout-sdk';

const sdk = new GradualRolloutSDK({
  apiKey: 'YOUR_API_KEY', // Required
  userId: 'user_123',     // Optional: for logged-in users
  anonId: 'some_uuid',     // Optional: for anonymous users
  pollingIntervalMs: 30000 // Optional: defaults to 60s
});

// Listen for SDK events
sdk.on('initialized', () => console.log('SDK ready!'));
sdk.on('flagsUpdated', (flags) => console.log('Flags:', flags));
sdk.on('error', (err) => console.error('SDK error:', err);

// Initialize the SDK and fetch flags
await sdk.init();

// Check if a feature is enabled
if (sdk.isFeatureEnabled('new-dashboard-ui')) {
  // Render new UI
}

// Manually refresh flags
await sdk.refreshFlags();

// Clean up SDK resources when no longer needed
sdk.destroy();

React Integration

For React applications, use a context provider and a custom hook for seamless integration.

1. Create your GradualRolloutProvider.tsx:

// GradualRolloutProvider.tsx
import React, { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
import { GradualRolloutSDK, FeatureFlag } from 'gradual-rollout-sdk';
import { v4 as uuidv4 } from 'uuid'; // npm install uuid

interface GradualRolloutContextType {
  isFeatureEnabled: (flagKey: string) => boolean;
  initialized: boolean;
  refreshFlags: () => Promise<void>;
  setIdentity: (userId?: string, anonId?: string) => Promise<void>;
}

const GradualRolloutContext = createContext<GradualRolloutContextType | undefined>(undefined);

function useAnonId() {
  const [anonId, setAnonId] = useState<string | null>(null);
  useEffect(() => {
    let storedAnonId = localStorage.getItem('gradual_rollout_anon_id'); // Unique key
    if (!storedAnonId) {
      storedAnonId = uuidv4();
      localStorage.setItem('gradual_rollout_anon_id', storedAnonId);
    }
    setAnonId(storedAnonId);
  }, []);
  return anonId;
}

export const GradualRolloutProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const anonId = useAnonId();
  const [sdkInstance, setSdkInstance] = useState<GradualRolloutSDK | null>(null);
  const [initialized, setInitialized] = useState(false);

  useEffect(() => {
    if (!anonId) return;
    const sdk = new GradualRolloutSDK({
      apiKey: 'YOUR_API_KEY', // <<< IMPORTANT: Replace with your actual API key
      anonId,
      pollingIntervalMs: 6000,
    });
    setSdkInstance(sdk);

    sdk.on('initialized', () => setInitialized(true));
    sdk.init();

    return () => {
      sdk.destroy();
    };
  }, [anonId]);

  const isFeatureEnabled = useCallback(
    (flagKey: string) => (sdkInstance ? sdkInstance.isFeatureEnabled(flagKey) : false),
    [sdkInstance]
  );
  const refreshFlags = useCallback(async () => {
    await sdkInstance?.refreshFlags();
  }, [sdkInstance]);
  const setIdentity = useCallback(async (userId?: string, anonId?: string) => {
    await sdkInstance?.setIdentity(userId, anonId);
  }, [sdkInstance]);

  const value = useMemo(
    () => ({ isFeatureEnabled, initialized, refreshFlags, setIdentity }),
    [isFeatureEnabled, initialized, refreshFlags, setIdentity]
  );

  return (
    <GradualRolloutContext.Provider value={value}>
      {children}
    </GradualRolloutContext.Provider>
  );
};

export function useGradualRollout() {
  const context = useContext(GradualRolloutContext);
  if (context === undefined) { // Added parenthesis for correct syntax
    throw new Error('useGradualRollout must be used within a GradualRolloutProvider');
  }
  return context;
}

2. Wrap your application with the provider (e.g., in App.tsx or Next.js's layout.tsx):

// App.tsx or layout.tsx
import { GradualRolloutProvider } from './GradualRolloutProvider'; // Adjust path as needed

function AppOrLayout({ children }: { children: React.ReactNode }) { // Added children prop type
  return (
    <GradualRolloutProvider>
      {/* Your application components */}
      {children} {/* Render children */}
    </GradualRolloutProvider>
  );
}
export default AppOrLayout;

3. Use the useGradualRollout hook in your components:

// MyFeatureComponent.tsx
import { useGradualRollout } from './GradualRolloutProvider'; // Adjust path as needed

function MyFeatureComponent() {
  const { isFeatureEnabled, initialized, refreshFlags, setIdentity } = useGradualRollout();

  if (!initialized) {
    return <div className="text-gray-500">Loading features...</div>;
  }

  return (
    <div>
      {isFeatureEnabled('my-new-feature') ? (
        <p className="text-green-400">New feature is enabled!</p>
      ) : (
        <p className="text-red-400">New feature is not enabled.</p>
      )}
      <button
        onClick={refreshFlags}
        className="mt-4 bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition-colors"
      >
        Refresh Flags
      </button>
      <button
        onClick={() => setIdentity('user-john-doe')}
        className="mt-4 ml-2 bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition-colors"
      >
        Set User ID
      </button>
    </div>
  );
}

API Reference

Constructor Options

OptionTypeRequiredDescription
apiKeystringYesYour project's API key for authentication.
userIdstringCond.Unique identifier for a logged-in user.
anonIdstringCond.Unique identifier for an anonymous visitor. Usually stored in local storage.
pollingIntervalMsnumberNoInterval in milliseconds for automatic flag polling (default: 60000ms).
apiBaseUrlstringNoBase URL for the GradualRollout backend API (default: SDK endpoint).

Methods

  • init(): Promise<void> – Initializes the SDK, fetches flags, and starts polling if configured. Must be called once.
  • isFeatureEnabled(flagKey: string): boolean – Checks if a specific feature flag is enabled for the current user.
  • refreshFlags(): Promise<void> – Manually fetches the latest flags from the backend.
  • setIdentity(userId?: string, anonId?: string): Promise<void> – Dynamically updates the user identity and refreshes flags. Call this on user login/logout.
  • on(event: string, handler: Function) – Subscribes to SDK events. See Events section below.
  • off(event: string, handler: Function) – Unsubscribes a handler from an event.
  • destroy(): void – Cleans up all SDK resources, including polling timers and event listeners. Call this on application unload or component unmount.

Events

The SDK emits the following events you can subscribe to:

Event NamePayload TypeDescription
flagsUpdatedFeatureFlag[]Fired when feature flags are successfully fetched or updated.
errorErrorFired if an API request fails or an internal SDK error occurs.
initializedvoidFired once the SDK has successfully completed its initial flag fetch and is ready.

Dynamic User Identity Support

  • Achieve seamless A/B testing before and after user login without losing rollout consistency.
  • Anonymous user bucketing using an automatically managed anonId.
  • Smooth transition to a logged-in userId after authentication, preserving feature experiences.

Why this is a Big Improvement

  • Stateless and Flexible: Unlike traditional systems that often require manual user assignments or backend configuration updates for phased rollouts, our SDK uses stateless, hash-based bucketing.
  • Runtime Updates: Allows for dynamic identity updates and flag refreshes at runtime, eliminating the need for app reloads or complex deployments.