Standalone @flaggr/sdk package with pluggable OTEL instrumentation
Last updated October 3, 2026
TypeScript SDK (@flaggr/sdk)
The standalone Flaggr SDK for TypeScript. Includes a core client, React hooks, and pluggable OpenTelemetry instrumentation.
Installation
npm install @flaggr/sdkOr load it straight from the CDN — no bundler needed:
<script src="https://cdn.jsdelivr.net/npm/@flaggr/sdk@0/dist/browser.global.js"
data-service-id="web-app"
data-api-key="fgr_your_token"
data-environment="production"
defer></script>The data-* attributes auto-configure the global client — that script tag is
the entire setup (~6 KB gzipped). Anywhere later on the page:
<script type="module">
const enabled = await flaggr.flag('checkout-v2', false)
</script>ES modules via CDN work too:
<script type="module">
import { createFlaggr } from 'https://cdn.jsdelivr.net/npm/@flaggr/sdk@0.4.0/+esm'
const client = createFlaggr({ serviceId: 'web-app', apiKey: 'fgr_your_token' })
</script>Quick Start
import { createFlaggr } from '@flaggr/sdk'
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
})
const isEnabled = await client.getBooleanValue('checkout-v2', false)Remote Configuration (zero-config setup)
Set remoteConfig: true and the SDK fetches service-level settings from
GET /api/sdk-config — environment, update mode, cache TTL, and telemetry
opt-in all come from the service's settings.sdk instead of being repeated
in client code:
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
remoteConfig: true, // pulls environment/updateMode/telemetry from Flaggr
})Explicit fields always win — remote values only fill gaps. The settings
response is cached in localStorage (60s fresh window, stale-while-revalidate),
and the flag bootstrap is persisted as a separate snapshot — repeat page loads
evaluate instantly from disk with zero requests, revalidating in the
background. When the service config sets sdk.telemetry: true, the telemetry
plugin attaches itself.
Snapshot durability guarantees:
- Bustable:
client.clearPersistedConfig()drops settings + all flag snapshots for the service; schema-versioned records self-invalidate on SDK upgrades; snapshots expire after 7 days. - Isolated: snapshots are keyed
serviceId + environment + auth-scope(anonymous vs API-key hash) — an authed session's private flags can never leak into an anonymous session. - Managed: writes >4MB are skipped; quota errors evict oldest
flaggr:*snapshots and retry once; corrupt/stale records are detected and removed. - Fresh: the server returns a
bootstrapVersioncontent hash — identical payloads skip re-apply and re-persist entirely; SSEconfiguration_sync,configuration_delta, andflag-updatemessages keep the snapshot current.
The endpoint also returns a bootstrap flag payload — the service's flag
configurations for the environment — so the SDK evaluates locally with zero
additional round trips after the config fetch. Authenticated requests
(Authorization: Bearer) get the full flag set; unauthenticated requests get
isPublic flags only (same trust boundary as POST /api/flags/evaluate).
Script tag:
<script src="https://cdn.jsdelivr.net/npm/@flaggr/sdk@0/dist/browser.global.js"
data-service-id="web-app" data-api-key="fgr_..." data-remote-config defer></script>Browser Telemetry (opt-in)
Attach the telemetry plugin to stream evaluation stats, Core Web Vitals, and error correlation to the platform — same ingest surface as the hosted web provider, so CDN installs get identical analytics:
import { createFlaggr, flaggrTelemetry } from '@flaggr/sdk'
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
plugins: [flaggrTelemetry()],
})Via the script tag, add data-telemetry:
<script src="https://cdn.jsdelivr.net/npm/@flaggr/sdk@0/dist/browser.global.js"
data-service-id="web-app" data-api-key="fgr_..." data-telemetry defer></script>Options: { vitals, errors, beacon, flushIntervalMs } — vitals/errors/beacon
default on, flush every 30s plus sendBeacon on page hide.
Core Client API
Flag Evaluation Methods
// Boolean flag
const enabled: boolean = await client.getBooleanValue('my-flag', false)
// String flag
const variant: string = await client.getStringValue('button-color', 'blue')
// Number flag
const limit: number = await client.getNumberValue('rate-limit', 100)
// Object flag (typed)
interface BannerConfig {
text: string
color: string
dismissible: boolean
}
const banner: BannerConfig = await client.getObjectValue<BannerConfig>(
'banner-config',
{ text: '', color: 'blue', dismissible: true }
)Evaluation with Context
Pass targeting context for per-user or per-segment evaluation:
const result = await client.getBooleanValue('checkout-v2', false, {
targetingKey: 'user-456',
email: 'bob@example.com',
plan: 'enterprise',
country: 'AU',
})Detailed Evaluation
When you need the full evaluation result (reason, variant, metadata):
const detail = await client.evaluate<boolean>('checkout-v2', false)
// detail.value → true
// detail.reason → "TARGETING_MATCH"
// detail.variant → "enabled"
// detail.errorMessage → undefined (present on errors)With React
import { FlaggrProvider, useBooleanFlag } from '@flaggr/sdk/react'
function App() {
return (
<FlaggrProvider config={{
serviceId: 'web-app',
apiKey: 'fgr_your_token',
}}>
<Checkout />
</FlaggrProvider>
)
}
function Checkout() {
const useNewFlow = useBooleanFlag('checkout-v2', false)
return useNewFlow ? <CheckoutV2 /> : <CheckoutClassic />
}Available Hooks
| Hook | Returns | Description |
|---|---|---|
useBooleanFlag(key, default) | boolean | Boolean flag value |
useStringFlag(key, default) | string | String flag value |
useNumberFlag(key, default) | number | Number flag value |
useObjectFlag<T>(key, default) | T | Typed object flag value |
useConnectionState() | ConnectionState | Current connection state |
useRefreshFlags() | () => Promise<void> | Manual refresh function |
Hook Behavior
All flag hooks subscribe to real-time updates when streaming is enabled. When a flag value changes server-side, the hook re-renders the component automatically. If the SDK is disconnected, hooks return the default value you provided.
// ConnectionState is one of:
type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'
// Use in a status indicator
function ConnectionBadge() {
const state = useConnectionState()
return (
<span className={state === 'connected' ? 'text-green-500' : 'text-red-500'}>
{state}
</span>
)
}Error Handling
The SDK is designed to fail safely. Evaluation methods never throw -- they return the default value and report errors via the plugin system.
// This never throws, even if the server is unreachable
const value = await client.getBooleanValue('my-flag', false)
// → false (default) when an error occursEvaluation Reasons and Errors
| Reason | Meaning | Recovery |
|---|---|---|
STATIC | Flag returned its configured default or static value | No action needed |
DEFAULT | SDK returned a configured offline/default value | Check server reachability if unexpected |
TARGETING_MATCH | Targeting rules matched the supplied context | No action needed |
SPLIT | Rollout or variant allocation selected this value | No action needed |
DISABLED | Flag is disabled | Enable the flag or update the fallback |
NOT_FOUND | Flag key does not exist | Check the key spelling and service ID |
ERROR | Network/server error or invalid response | Check connectivity and errorMessage |
Custom Error Handling
Use the plugin system for logging and alerting on errors:
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
plugins: [{
name: 'error-reporter',
onEvaluateError(flagKey, error, durationMs) {
console.error(`Flag evaluation failed: ${flagKey}`, error)
// Send to your error tracking service
Sentry.captureException(error, {
tags: { flagKey, durationMs },
})
},
}],
})Streaming Reconnection
Evaluation methods fail safely by returning the provided default value and reporting errors through plugins. The SDK does not expose per-evaluation retry configuration. When streaming is enabled, browser EventSource reconnects automatically after connection loss:
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
enableStreaming: true,
})When streaming is enabled, the SDK updates its connection state to error on stream errors and relies on the browser's EventSource reconnection behavior.
With OpenTelemetry
The SDK supports pluggable OTEL instrumentation. Install the peer dependency:
npm install @opentelemetry/apiBasic OTEL Setup
import { createFlaggr } from '@flaggr/sdk'
import { otelPlugin } from '@flaggr/sdk/otel'
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
plugins: [otelPlugin()], // Uses global OTEL providers
})Custom Configuration
import { otelPlugin } from '@flaggr/sdk/otel'
plugins: [otelPlugin({
serviceName: 'checkout-service',
enableTracing: true,
enableMetrics: true,
metricPrefix: 'myapp.flags',
additionalAttributes: {
'deployment.environment': 'production',
'service.version': '1.2.3',
},
})]Bring Your Own Providers
import { MeterProvider } from '@opentelemetry/sdk-metrics'
import { otelPlugin } from '@flaggr/sdk/otel'
const meterProvider = new MeterProvider({
readers: [myMetricReader],
})
plugins: [otelPlugin({
meterProvider,
tracerProvider: myTracerProvider,
})]Exported Metrics
| Metric | Type | Description |
|---|---|---|
flaggr.evaluations.total | Counter | Total flag evaluations |
flaggr.evaluations.duration | Histogram | Evaluation latency (ms) |
flaggr.evaluations.errors | Counter | Evaluation errors |
flaggr.cache.hits | Counter | Cache hits |
flaggr.cache.misses | Counter | Cache misses |
flaggr.flag_changes.total | Counter | Flag change events |
flaggr.connections.active | UpDownCounter | Active connections |
Exported Spans
Each flag evaluation creates a span named flaggr.evaluate with attributes:
| Attribute | Description |
|---|---|
feature_flag.key | Flag key being evaluated |
feature_flag.provider_name | Always "flaggr" |
feature_flag.value | Evaluated value |
feature_flag.reason | Evaluation reason |
feature_flag.variant | Variant key (if applicable) |
Browser Analytics & Web Vitals Telemetry
The Flaggr client SDK (@flaggr/sdk / src/lib/client) provides built-in, zero-overhead browser analytics that link Core Web Vitals and client runtime errors directly to active feature flags.
Key Capabilities
- Zero-Allocation Evaluation Summaries: Tracks evaluation counts, cache hits, cache misses, and average latency locally without per-call network overhead.
- Automated Core Web Vitals Tracking: Uses browser
PerformanceObserverto track Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and First Contentful Paint (FCP), automatically tagging each metric with active flag keys and variants. - Client-Side Error Correlation: Records unhandled errors or caught UI exceptions alongside the user's active flag evaluations for fast regression attribution.
- Reliable Background Ingestion: Batches telemetry and flushes via
navigator.sendBeacon(withfetchkeepalivefallback), ensuring telemetry reaches the server even during page unloads without dropping frames. - Client Diagnostics & Profiling: Provides
getDiagnostics()to monitor evaluation throughput (over 7M+ ops/sec for L1 cache hits) and cache hit ratios.
Initializing Analytics
import { createFlaggr, AnalyticsClient } from '@flaggr/sdk'
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
analytics: {
enabled: true,
captureWebVitals: true, // Auto-tracks LCP, CLS, FCP via PerformanceObserver
captureErrors: true, // Auto-captures unhandled window errors
useBeacon: true, // Use navigator.sendBeacon during page unload
flushIntervalMs: 30000, // Flush batch every 30 seconds
maxBatchSize: 100, // Flush when 100 events accumulate
sampleRate: 1.0, // 100% of sessions (0.0 to 1.0)
},
})Manual Metric & Error Tracking
You can also explicitly track custom metrics, Core Web Vitals, or handled errors:
import { getFlaggrAnalytics } from '@flaggr/sdk'
const analytics = getFlaggrAnalytics()
// Track Web Vitals manually (e.g. via web-vitals package)
analytics.trackWebVital({ name: 'INP', value: 48, rating: 'good' })
// Correlate caught exceptions with active flags
try {
executePaymentFlow()
} catch (err: any) {
analytics.trackError(err.message, err.stack, { checkoutStep: 'payment' })
throw err
}SDK Diagnostics & Profiling
Inspect real-time evaluation throughput and cache performance at any time:
import { getFlaggrDiagnostics } from '@flaggr/sdk'
const diag = getFlaggrDiagnostics()
console.log(`Evaluations: ${diag.totalEvaluations}`)
console.log(`Cache Hit Ratio: ${(diag.cacheHitRatio * 100).toFixed(1)}%`)
console.log(`Active Flags in Memory: ${diag.trackedFlagsCount}`)Plugin System
The SDK supports a plugin architecture for extending behavior:
import type { FlaggrPlugin } from '@flaggr/sdk'
const myPlugin: FlaggrPlugin = {
name: 'my-plugin',
onInit(client) { /* SDK initialized */ },
onEvaluate(flagKey, context) { /* Before evaluation */ },
onEvaluateComplete(flagKey, result, durationMs) { /* After evaluation */ },
onEvaluateError(flagKey, error, durationMs) { /* On error */ },
onFlagChange(event) { /* Flag value changed */ },
onConnectionStateChange(state) { /* Connection state changed */ },
onDestroy() { /* SDK destroyed */ },
}
const client = createFlaggr({
serviceId: 'web-app',
plugins: [myPlugin],
})Plugin Lifecycle
createFlaggr()
├─ onInit() Called once after client creation
│
├─ evaluate('flag')
│ ├─ onEvaluate() Before evaluation
│ ├─ onEvaluateComplete() After successful evaluation
│ └─ onEvaluateError() On evaluation failure
│
├─ onFlagChange() When a flag value changes (streaming only)
├─ onConnectionStateChange() On connection state transitions
│
└─ client.destroy()
└─ onDestroy() Cleanup (close connections, flush metrics)
Configuration
interface FlaggrConfig {
apiUrl?: string // Default: 'https://api.flaggr.dev'
serviceId: string // Your service identifier
apiKey?: string // API token
environment?: string // Target environment
context?: EvaluationContext // Default evaluation context
plugins?: FlaggrPlugin[] // SDK plugins
cacheTtl?: number // Cache TTL in ms (default: 10000)
updateMode?: 'stream' | 'batch' | 'poll' // Update delivery (default: poll)
batchIntervalMs?: number // Batch refresh cadence (default: 2000)
enableStreaming?: boolean // Deprecated — use updateMode: 'stream'
defaults?: Record<string, FlagValue> // Offline defaults
}Update Modes
The SDK's update delivery can be switched at runtime — stream applies SSE pushes instantly with zero requests, batch revalidates all watched flags in one grouped request per interval, and poll relies on per-flag fetches when the cache expires:
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
updateMode: 'batch',
batchIntervalMs: 1500,
})
client.setUpdateMode('stream') // tears down batch, opens SSE
// Per-request telemetry for instrumentation
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
plugins: [{
name: 'telemetry',
onRequest: (info) => {
// info: { url, durationMs, ok, requestBytes, responseBytes }
metrics.record(info.durationMs)
},
}],
})Offline Defaults
Provide fallback values for when the SDK cannot reach the server:
const client = createFlaggr({
serviceId: 'web-app',
apiKey: 'fgr_your_token',
defaults: {
'checkout-v2': false,
'dark-mode': true,
'rate-limit': 100,
'banner-config': { text: 'Welcome', color: 'blue', dismissible: true },
},
})When the server is unreachable and the local cache is empty, the SDK returns values from defaults instead of the method-level default. This lets you centralize your fallback configuration.