gradual-rollout-sdk
v0.1.16
Published
GradualRollout SDK is a lightweight, framework-agnostic JavaScript/TypeScript client for feature flagging and canary deployments. It enables instant, safe, and incremental feature rollouts by connecting to your GradualRollout backend API, evaluating user-
Downloads
48
Readme
GradualRollout SDK
A lightweight, framework-agnostic JavaScript and TypeScript SDK for feature flagging and canary deployments.
Enable safe, gradual feature rollouts with instant evaluation and real-time updates.
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, andinitializedevents - 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
npm install gradual-rollout-sdkUsage
import { GradualRolloutSDK } from 'gradual-rollout-sdk';
const sdk = new GradualRolloutSDK({
apiKey: 'your_api_key',
userId: 'user_123',
pollingIntervalMs: 30000, // optional, default 60000 (60s)
});
sdk.on('initialized', () => {
console.log('SDK initialized');
});
sdk.on('flagsUpdated', (flags) => {
console.log('Flags updated:', flags);
});
sdk.on('error', (error) => {
console.error('SDK error:', error);
});
await sdk.init();
const isNewFeatureEnabled = sdk.isFeatureEnabled('new-feature');
if (isNewFeatureEnabled) {
// Show new UI or enable feature
}
// Manually refresh flags anytime
await sdk.refreshFlags();
// When done with SDK (e.g., on app unload), clean up
sdk.destroy();React Integration Example
1. Create a GradualRolloutProvider and useGradualRollout hook
import React, { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
import { GradualRolloutSDK, FeatureFlag } from 'gradual-rollout-sdk';
import { v4 as uuidv4 } from 'uuid';
interface GradualRolloutContextType {
sdk: GradualRolloutSDK | null;
flags: FeatureFlag[];
isFeatureEnabled: (flagKey: string) => boolean;
refreshFlags: () => Promise<void>;
initialized: boolean;
error: Error | null;
}
const GradualRolloutContext = createContext<GradualRolloutContextType | undefined>(undefined);
function useAnonId() {
const [anonId, setAnonId] = useState<string | null>(null);
useEffect(() => {
let storedAnonId = localStorage.getItem('anon_id');
if (!storedAnonId) {
storedAnonId = uuidv4();
localStorage.setItem('anon_id', storedAnonId);
}
setAnonId(storedAnonId);
}, []);
return anonId;
}
export const GradualRolloutProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const anonId = useAnonId();
const [sdk, setSdk] = useState<GradualRolloutSDK | null>(null);
const [flags, setFlags] = useState<FeatureFlag[]>([]);
const [initialized, setInitialized] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!anonId) return;
const instance = new GradualRolloutSDK({
apiKey: 'YOUR_API_KEY', // Replace with your actual API key
anonId,
pollingIntervalMs: 60000,
});
setSdk(instance);
const handleFlagsUpdated = (newFlags: FeatureFlag[]) => setFlags(newFlags);
const handleInitialized = () => setInitialized(true);
const handleError = (err: Error) => setError(err);
instance.on('flagsUpdated', handleFlagsUpdated);
instance.on('initialized', handleInitialized);
instance.on('error', handleError);
instance.init();
return () => {
instance.off('flagsUpdated', handleFlagsUpdated);
instance.off('initialized', handleInitialized);
instance.off('error', handleError);
instance.destroy();
};
}, [anonId]);
const isFeatureEnabled = useCallback(
(flagKey: string) => (sdk ? sdk.isFeatureEnabled(flagKey) : false),
[sdk]
);
const refreshFlags = useCallback(async () => {
if (sdk) await sdk.refreshFlags();
}, [sdk]);
const value = useMemo(
() => ({ sdk, flags, isFeatureEnabled, refreshFlags, initialized, error }),
[sdk, flags, isFeatureEnabled, refreshFlags, initialized, error]
);
return (
<GradualRolloutContext.Provider value={value}>
{children}
</GradualRolloutContext.Provider>
);
};
export function useGradualRollout() {
const context = useContext(GradualRolloutContext);
if (context === undefined) {
throw new Error('useGradualRollout must be used within a GradualRolloutProvider');
}
return context;
}2. Wrap your app with the provider
import { GradualRolloutProvider } from './GradualRolloutProvider';
function App() {
return (
<GradualRolloutProvider>
{/* your app components */}
</GradualRolloutProvider>
);
}3. Use the hook in your components
import { useGradualRollout } from './GradualRolloutProvider';
function MyComponent() {
const { flags, isFeatureEnabled, refreshFlags, initialized, error } = useGradualRollout();
if (!initialized) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<div>Flags: {JSON.stringify(flags)}</div>
<div>Feature A enabled: {isFeatureEnabled('featureA') ? 'Yes' : 'No'}</div>
<button onClick={refreshFlags}>Refresh Flags</button>
</div>
);
}Multi-Framework Integration Guide
Extend your feature-flagging power beyond React! This guide walks you through integrating the GradualRollout SDK in Angular, Vue 3, Svelte, and Next.js. Each section covers:
- Introduction
- Installation & Setup
- Integration & Initialization
- Feature-Flag Usage Examples
- Advanced Configuration
- Troubleshooting & Common Issues
- FAQs
Angular Integration
Introduction
The GradualRollout SDK brings safe, gradual feature rollouts to your Angular app.
Benefits:
- Leverage Angular’s DI and lifecycle hooks for smooth initialization.
- Centralized flag state via an injectable service.
- Real-time updates via RxJS streams.
Installation & Setup
npm install gradual-rollout-sdk
# or
yarn add gradual-rollout-sdkPeer dependencies:
- Angular ≥11
- RxJS (bundled with Angular)
Integration & Initialization
- Create an Angular service wrapper:
// src/app/services/rollout.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { GradualRolloutSDK, FeatureFlag } from 'gradual-rollout-sdk';
@Injectable({ providedIn: 'root' })
export class RolloutService implements OnDestroy {
private sdk = new GradualRolloutSDK({ apiKey: 'YOUR_API_KEY', anonId: this.getAnonId() });
public flags$ = new BehaviorSubject<FeatureFlag[]>([]);
public ready$ = new BehaviorSubject<boolean>(false);
constructor() {
this.sdk.on('flagsUpdated', flags => this.flags$.next(flags));
this.sdk.on('initialized', () => this.ready$.next(true));
this.sdk.on('error', err => console.error('[Rollout]', err));
this.sdk.init();
}
private getAnonId(): string {
return localStorage.getItem('anon_id') || crypto.randomUUID();
}
isFeatureEnabled(key: string): boolean {
return this.sdk.isFeatureEnabled(key);
}
refresh(): Promise<void> {
return this.sdk.refreshFlags();
}
ngOnDestroy() {
this.sdk.destroy();
}
}- Inject and use in components:
// src/app/app.component.ts
import { Component } from '@angular/core';
import { RolloutService } from './services/rollout.service';
@Component({
selector: 'app-root',
template: `
<div *ngIf="!(rollout.ready$ | async)">Loading flags…</div>
<div *ngIf="rollout.isFeatureEnabled('new-ui')">
<!-- new UI here -->
</div>
<button (click)="rollout.refresh()">Refresh Flags</button>
`
})
export class AppComponent {
constructor(public rollout: RolloutService) {}
}Feature-Flag Usage Examples
- Toggle UI elements:
*ngIf="rollout.isFeatureEnabled('beta-banner')" - Reactive consumption:
rollout.flags$.subscribe(flags => /* update local state */)
Advanced Configuration
- Environment targeting: pass
environment: 'staging'to the SDK config. - Demo users: supply
demoUserIds: ['[email protected]']for 100% visibility. - Custom endpoints: override
apiBaseUrl.
Troubleshooting & Common Issues
- “SDK not initialized”: Ensure you wait on
ready$before checking flags. - CORS errors: Confirm your backend allows
X-API-KEYheader. - Duplicate anonId: Clear
localStorage.anon_idwhen switching projects.
FAQs
Q: Can I lazy-load the SDK?
A: Yes—inject your service only in lazy modules to defer initialization.
Q: How to switch user identity after login?
A: Call sdk.setIdentity(userId, anonId) in your service and it auto-refreshes.
Q: Does polling impact performance?
A: Default is 60 s. You can disable by omitting pollingIntervalMs.
Vue 3 Integration (Composition API)
Introduction
Vue 3’s Composition API and reactivity pairs perfectly with GradualRollout:
- Reactive
reforcomputedflags. - Plugin architecture for global access.
Installation & Setup
npm install gradual-rollout-sdk
# or
yarn add gradual-rollout-sdkIntegration & Initialization
- Create a plugin:
// src/plugins/rollout.ts
import { App, reactive } from 'vue';
import { GradualRolloutSDK, FeatureFlag } from 'gradual-rollout-sdk';
export interface RolloutState {
ready: boolean;
flags: FeatureFlag[];
}
export const rolloutState = reactive<RolloutState>({ ready: false, flags: [] });
export default {
install(app: App) {
const sdk = new GradualRolloutSDK({ apiKey: 'YOUR_API_KEY', anonId: crypto.randomUUID() });
sdk.on('initialized', () => (rolloutState.ready = true));
sdk.on('flagsUpdated', f => (rolloutState.flags = f));
sdk.on('error', console.error);
sdk.init();
app.config.globalProperties.$rollout = sdk;
}
};- Register in
main.ts:
import { createApp } from 'vue';
import App from './App.vue';
import rolloutPlugin from './plugins/rollout';
createApp(App).use(rolloutPlugin).mount('#app');- Use in components:
<template>
<div v-if="!state.ready">Loading…</div>
<div v-else>
<div v-if="isEnabled('new-feature')">🎉 New Feature!</div>
<button @click="refresh">Refresh Flags</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { rolloutState as state } from '@/plugins/rollout';
const sdk = getCurrentInstance()!.appContext.config.globalProperties.$rollout;
function isEnabled(key: string) {
return sdk.isFeatureEnabled(key);
}
async function refresh() {
await sdk.refreshFlags();
}
</script>Advanced Configuration
- Provide
demoUserIdsorenvironmentin plugin options. - Swap
apiBaseUrlfor self-hosted endpoints.
Troubleshooting & Common Issues
- Plugin not applied: Confirm
.use(rolloutPlugin)runs before.mount(). - Reactivity not updating: Always update a reactive property (e.g.,
rolloutState.flags = …).
FAQs
Q: How to access flags outside components?
A: Import rolloutState from your plugin file.
Q: Can I disable polling?
A: Pass pollingIntervalMs: 0 in SDK config.
Svelte Integration
Introduction
Svelte’s stores and automatic reactivity make feature-flag integration a breeze.
- Leverage writable stores for flag updates.
- Minimal boilerplate and straightforward reactive statements.
Installation & Setup
npm install gradual-rollout-sdk
# or
yarn add gradual-rollout-sdkIntegration & Initialization
- Create a store:
// src/lib/rollout.ts
import { writable } from 'svelte/store';
import { GradualRolloutSDK, FeatureFlag } from 'gradual-rollout-sdk';
export const flags = writable<FeatureFlag[]>([]);
export const ready = writable(false);
export const error = writable<Error | null>(null);
const sdk = new GradualRolloutSDK({ apiKey: 'YOUR_API_KEY', anonId: crypto.randomUUID() });
sdk.on('initialized', () => ready.set(true));
sdk.on('flagsUpdated', f => flags.set(f));
sdk.on('error', e => error.set(e));
sdk.init();
export function isFeatureEnabled(key: string) {
return sdk.isFeatureEnabled(key);
}
export function refreshFlags() {
return sdk.refreshFlags();
}
export function setIdentity(userId?: string, anonId?: string) {
sdk.setIdentity(userId, anonId);
}- Use in components:
<script lang="ts">
import { flags, ready, isFeatureEnabled, refreshFlags } from '$lib/rollout';
let isNewFeatureEnabled = false;
$: isNewFeatureEnabled = isFeatureEnabled('new-feature');
async function handleRefresh() {
await refreshFlags();
}
</script>
{#if $ready}
<div>
{#if isNewFeatureEnabled}
<p>🎉 New feature is enabled!</p>
{/if}
<button on:click={handleRefresh}>Refresh Flags</button>
</div>
{:else}
<p>Loading feature flags...</p>
{/if}Advanced Configuration
- Pass
environmentordemoUserIdsin SDK options. - Override
apiBaseUrlfor custom backends.
Troubleshooting & Common Issues
- Flags not updating: Ensure you’re using the
$prefix to subscribe to stores. - CORS issues: Check your browser’s console for CORS-related errors.
FAQs
Q: How to test different user roles?
A: Use setIdentity('user_id', 'anon_id') with known IDs.
Q: Can I use with SvelteKit?
A: Yes, just ensure the store is created in a module context (not in a component).
API
Constructor: new GradualRolloutSDK(config)
Creates a new SDK instance.
| Option | Type | Required | Description | |------------------|----------|----------|--------------------------------------------| | apiKey | string | Yes | API key for authenticating calls | | userId | string | Cond. | Unique identifier for logged-in user | | anonId | string | Cond. | Unique identifier for anonymous visitor | | pollingIntervalMs| number | No | Interval in ms for auto polling | | apiBaseUrl | string | No | Backend API base URL |
Methods
init(): Promise<void>
Initializes the SDK, fetches flags, and starts polling if enabled.isFeatureEnabled(flagKey: string): boolean
Returns true if the feature flag is enabled for the current user.refreshFlags(): Promise<void>
Manually fetches the latest flags from the backend.setIdentity(userId?: string, anonId?: string): void
Dynamically updates the user identity and refreshes flags accordingly.on(event: string, handler: Function)
Subscribes to events:flagsUpdated,error,initialized.off(event: string, handler: Function)
Unsubscribes a handler from an event.destroy(): void
Cleans up timers and event listeners (stop polling).
Events
| Event | Payload | Description | |--------------|----------------|----------------------------------------------| | flagsUpdated | FeatureFlag[] | Emitted whenever flags are fetched or updated| | error | Error | Emitted on API or internal errors | | initialized | void | Emitted once when SDK finishes initialization|
Dynamic User Identity Support
This SDK introduces support for dynamic user identity updates, enabling:
- Seamless A/B testing before and after login without losing rollout consistency
- Anonymous user bucketing using
anonId - Smooth transition to logged-in
userIdafter authentication
Why this is a big improvement
Traditional rollout systems often required:
- Backend config updates with manual user assignments
- New client versions to control phased rollouts
Our SDK simplifies this by:
- Using stateless, hash-based bucketing
- Allowing runtime identity updates without app reloads
Contributing
Contributions are welcome! Please open issues or pull requests on GitHub.
License
MIT © GradualRollout
