Analytics & Experimentation Integration
Featureflip server SDKs can hand every flag evaluation to your own code as it happens, in-process, with no network egress owned by Featureflip. You decide where the data goes: an analytics pipeline, an experimentation tool, a trace span, an error report, or a log line for debugging.
You register one or more inspectors when you create the client. Each inspector is a plain function called synchronously on every boolVariation / stringVariation / numberVariation / jsonVariation / variationDetail call.
Available in the JavaScript (
@featureflip/js), Node (@featureflip/node), Python, C#, Go, Java, PHP, and Ruby server SDKs, and in every client SDK: Browser (@featureflip/browser), React (@featureflip/react), Swift, Android, and Flutter. Client SDKs receive pre-evaluated results from the Evaluation API rather than running a local evaluator, so their inspectors fire from a different point in the code — see Client SDKs below.
The evaluation event
Section titled “The evaluation event”Each inspector receives one object. The field names are the same in every SDK, adapted to each language’s naming convention (flagKey in JavaScript, flag_key in Python and Ruby, FlagKey in C# and Go):
interface EvaluationEvent { flagKey: string; // the flag that was evaluated context: EvaluationContext; // the full context you passed (a read-only copy) value: unknown; // the value your code received (default applied) variationKey?: string; // the served variation ("on", "control", ...) reason: string; // why this value was served (see below) ruleId?: string; // set only when a targeting rule matched prerequisiteKey?: string; // set only when a prerequisite was not met timestamp: string; // ISO-8601}Reason values differ by SDK
Section titled “Reason values differ by SDK”reason uses each SDK’s own native representation, because every SDK’s evaluator is the source of truth for its own runtime. The set of outcomes is identical everywhere; only the spelling changes:
| SDK | reason is | Example |
|---|---|---|
| JavaScript, Node, Ruby | a string | "RuleMatch", "PrerequisiteFailed" |
| Python, Java | the SDK’s EvaluationReason enum | EvaluationReason.RULE_MATCH |
| C# | the SDK’s EvaluationReason enum, PascalCase | EvaluationReason.RuleMatch |
| Go | the SDK’s EvaluationReason constant | featureflip.ReasonRuleMatch |
| PHP | a string, SCREAMING_SNAKE_CASE | 'RULE_MATCH', 'PREREQUISITE_FAILED' |
| Browser, React, Swift, Android, Flutter (client SDKs) | a string, the engine’s own kebab-case, forwarded verbatim | "fallthrough", "rule-match:rule-abc-123", "prerequisite-failed" |
Compare against your SDK’s own reason type rather than against a hardcoded string from another language’s example.
flag-not-found is the one reason a client SDK synthesizes rather than forwards from the server — it’s the client-side spelling of the same FlagNotFound / FLAG_NOT_FOUND outcome the server SDKs’ local evaluators already report (see the reason table above). A client SDK synthesizes it locally (never sent by the server) whenever the flag key is absent from the client’s snapshot — unknown key, client not yet initialized, or the flag isn’t marked clientSideVisible. See Client SDKs for the full behavior.
Register an inspector
Section titled “Register an inspector”JavaScript / Node
Section titled “JavaScript / Node”import { FeatureflipClient } from '@featureflip/node';
const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, inspectors: [ (event) => { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ],});Python
Section titled “Python”from featureflip import Config, FeatureflipClient
def log_exposure(event): print(f"[flag] {event.flag_key} -> {event.variation_key} ({event.reason})")
client = FeatureflipClient( sdk_key=os.environ["FEATUREFLIP_SDK_KEY"], config=Config(inspectors=[log_exposure]),)var options = new FeatureFlagOptions{ Inspectors = { e => Console.WriteLine($"[flag] {e.FlagKey} -> {e.VariationKey} ({e.Reason})"), },};
var client = FeatureflipClient.Get( Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY"), options);client, err := featureflip.Get( os.Getenv("FEATUREFLIP_SDK_KEY"), featureflip.WithInspectors(func(e featureflip.EvaluationEvent) { log.Printf("[flag] %s -> %s (%s)", e.FlagKey, e.VariationKey, e.Reason) }),)FeatureflipClient client = FeatureflipClient.builder(System.getenv("FEATUREFLIP_SDK_KEY")) .inspectors(List.of(event -> System.out.printf("[flag] %s -> %s (%s)%n", event.getFlagKey(), event.getVariationKey(), event.getReason()))) .build();The PHP SDK is transport-agnostic: cache, httpClient and requestFactory are required, and you supply your own PSR-16 / PSR-18 / PSR-17 implementations.
$httpFactory = new \GuzzleHttp\Psr7\HttpFactory();
$config = new Config( cache: $yourPsr16Cache, httpClient: new \GuzzleHttp\Client(['timeout' => 30]), requestFactory: $httpFactory, streamFactory: $httpFactory, inspectors: [ function (EvaluationEvent $event): void { error_log("[flag] {$event->flagKey} -> {$event->variationKey} ({$event->reason})"); }, ],);
$client = FeatureflipClient::get(getenv('FEATUREFLIP_SDK_KEY'), $config);config = Featureflip::Config.new( inspectors: [ ->(event) { puts "[flag] #{event.flag_key} -> #{event.variation_key} (#{event.reason})" } ])
client = Featureflip::Client.get(ENV["FEATUREFLIP_SDK_KEY"], config: config)Inspectors are honored on the first client created for a given SDK key. Pass a list; every callable in it is called on each evaluation, in registration order. Entries that aren’t callable are ignored rather than causing an error at evaluation time.
Client SDKs
Section titled “Client SDKs”Browser, React, Swift, Android, and Flutter also support inspectors, with the identical EvaluationEvent shape described above. Register one the same way you would on a server SDK:
Browser
Section titled “Browser”import { FeatureflipClient } from '@featureflip/browser';
const client = FeatureflipClient.get({ clientKey: 'your-client-sdk-key', inspectors: [ (event) => { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ],});import { FeatureflipProvider } from '@featureflip/react';
function App() { return ( <FeatureflipProvider clientKey="your-client-sdk-key" inspectors={[ (event) => { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ]} > {/* rest of your app, including components that call useFeatureFlag */} </FeatureflipProvider> );}import Featureflip
let config = FeatureflipConfig( clientKey: "your-client-sdk-key", inspectors: [ { event in print("[flag] \(event.flagKey) -> \(event.variationKey ?? "nil") (\(event.reason))") } ])let client = FeatureflipClient(config: config)Android
Section titled “Android”import dev.featureflip.android.FeatureflipClientimport dev.featureflip.android.FeatureflipConfig
val config = FeatureflipConfig( clientKey = "your-client-sdk-key", inspectors = listOf( { event -> println("[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})") }, ),)val client = FeatureflipClient.get(config)Flutter
Section titled “Flutter”import 'package:featureflip/featureflip.dart';
final config = FeatureflipConfig( clientKey: 'your-client-sdk-key', inspectors: [ (event) { print('[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})'); }, ],);final client = FeatureflipClient.get('your-client-sdk-key', config: config);A few things differ from the server SDKs described above.
A different insertion point
Section titled “A different insertion point”Server SDKs evaluate locally, so their inspectors fire from inside the evaluator itself. Client SDKs never evaluate — they hold a snapshot of pre-evaluated results pushed down from the Evaluation API, and the app just reads out of it. So a client-SDK inspector fires from the 4 variation accessors (boolVariation / stringVariation / numberVariation / jsonVariation), after the accessor has type-checked the stored value and decided what to hand back to your code. Whichever snapshot-reading accessor a given SDK exposes — flagDetail(key) on Browser/Swift/Android, allFlags() on Flutter, or the underlying Browser client’s flagDetail(key) reached via React’s useFeatureflipClient() — is deliberately silent: it’s a diagnostic read of the snapshot, not an evaluation your app acted on.
Reason representation, plus a synthesized flag-not-found
Section titled “Reason representation, plus a synthesized flag-not-found”Because the client SDK’s “evaluator” is just the server’s response, reason is the engine’s kebab-case string forwarded exactly as received: fallthrough, flag-disabled, rule-match:{id}, prerequisite-failed, error (see the reason table above). flag-not-found is the one reason a client SDK synthesizes rather than forwards from the server — it’s the client-side spelling of the same FlagNotFound / FLAG_NOT_FOUND outcome the server SDKs’ local evaluators already report — whenever the requested key isn’t in the client’s snapshot: an unknown key, a client that hasn’t finished initializing yet, or a flag that isn’t clientSideVisible (client SDKs only ever receive client-visible flags). On flag-not-found, variationKey, ruleId, and prerequisiteKey are all unset.
ruleId is derived the same way in every client SDK: it’s parsed out of a rule-match:{id} reason and left unset for every other reason — reason itself is never rewritten. Because the parsing rule and the event’s field names are identical to the server SDKs, one inspector function works unchanged whether it’s registered against a server SDK or a client SDK — including the PostHog/Amplitude/GrowthBook examples below and the prerequisiteKey debugging example at the end of this page.
Type mismatch: the one field combination that surprises people
Section titled “Type mismatch: the one field combination that surprises people”Reading a flag with the wrong accessor — say, boolVariation on a flag whose value is actually a string — makes the accessor fall back to your default. The inspector event reports that fallback as value, but reason and variationKey still describe what the server actually served, because the server-side evaluation genuinely happened; only the local accessor rejected the type. Concretely, for a snapshot flag { value: "hello", variation: "v1", reason: "rule-match:rule-abc-123" }:
client.boolVariation('string-flag', false);// returns false — your default, because the stored value isn't a boolean
// but the inspector event still shows what the server served:// {// flagKey: 'string-flag',// value: false, // <- your default, NOT the server's value// variationKey: 'v1', // <- a real variation WAS served// reason: 'rule-match:rule-abc-123', // <- a real rule DID match// }That combination — value equal to your default while variationKey/reason describe a live, non-off variation — is the type-mismatch signal. Don’t confuse it with flag-not-found: a missing flag also serves your default, but leaves variationKey unset and reports the synthesized flag-not-found reason instead of a real one.
The singleton foot-gun: inspectors is first-caller-wins
Section titled “The singleton foot-gun: inspectors is first-caller-wins”Every client SDK is singleton-by-construction: the underlying core is refcounted and shared per client key, so the first call to FeatureflipClient.get(...) (Swift: the first FeatureflipClient(config:); React: the first <FeatureflipProvider> mounted for a clientKey) constructs the shared core, and that call’s inspectors are the only ones that will ever fire for that key. A second get() (or provider mount) with the same client key — even one that passes different or additional inspectors — silently returns a handle to the already-constructed core and inherits whatever the first caller registered. On Browser, React, Android, and Flutter there is no warning specific to inspectors: config fields that are structurally compared (baseUrl, streaming, …) log a mismatch warning there, but inspectors is deliberately excluded from that comparison because functions aren’t structurally comparable. Swift is a step further removed: its core-acquisition path does no config comparison at all, for any field, so a repeat FeatureflipClient(config:) call for a live clientKey silently reuses the first config in every respect — not just inspectors — with no warning of any kind.
In practice: register every inspector your app needs at the one place that constructs the client for a given key. If your analytics wiring and your debug logging live in separate files, make sure only one of them actually calls get()/mounts the provider, and pass both inspectors there — a second, independent construction call for the same key will not add to the list.
Pipe exposures into your tools
Section titled “Pipe exposures into your tools”PostHog
Section titled “PostHog”inspectors: [ (event) => { posthog.capture({ distinctId: String(event.context.user_id ?? 'anonymous'), event: '$feature_flag_called', properties: { $feature_flag: event.flagKey, $feature_flag_response: event.value, variation: event.variationKey, reason: event.reason, }, }); },]Amplitude
Section titled “Amplitude”inspectors: [ (event) => { amplitude.track('flag_evaluated', { flag_key: event.flagKey, variation: event.variationKey, value: event.value, reason: event.reason, }, { user_id: String(event.context.user_id ?? '') }); },]GrowthBook (or any experiment tool)
Section titled “GrowthBook (or any experiment tool)”inspectors: [ (event) => { experimentTracker.recordExposure({ unit: String(event.context.user_id ?? ''), experiment: event.flagKey, variation: event.variationKey ?? 'default', }); },]Record evaluations on traces (OpenTelemetry)
Section titled “Record evaluations on traces (OpenTelemetry)”When a request fails, the first question on call is often which variation it got. An inspector can answer that on the trace itself. Each recipe below adds a feature_flag.evaluation event to the span that is active when the flag is read, using the attribute names from the OpenTelemetry feature flag semantic conventions. The event is at Release Candidate status there.
| Attribute | Value |
|---|---|
feature_flag.key | the flag key |
feature_flag.provider.name | featureflip |
feature_flag.result.variant | the served variation, when there is one |
feature_flag.result.reason | targeting_match, default (fallthrough), disabled, prerequisite_failed or error |
error.type | flag_not_found or general, on errors only |
The recipes leave out two optional attributes, feature_flag.context.id and feature_flag.result.value. A context id is usually a user id and a value can carry anything, so add them only if your traces may hold personal data. Outside a recording span every recipe returns before it allocates anything, and none of them makes a network call: the event travels with its span through the batching exporter you already run.
These recipes use span events because that’s what trace views such as Grafana Tempo and Jaeger show on a span today. OpenTelemetry plans to deprecate the span event API in favor of log-based events, and has said existing span events keep working until the next major SDK versions.
import { FeatureflipClient } from '@featureflip/node';import { trace } from '@opentelemetry/api';
const REASONS = { RuleMatch: 'targeting_match', Fallthrough: 'default', FlagDisabled: 'disabled', PrerequisiteFailed: 'prerequisite_failed',};const ERROR_TYPES = { FlagNotFound: 'flag_not_found', Error: 'general' };
function recordFlagEvaluation(event) { const span = trace.getActiveSpan(); if (!span?.isRecording()) return;
const errorType = ERROR_TYPES[event.reason]; span.addEvent('feature_flag.evaluation', { 'feature_flag.key': event.flagKey, 'feature_flag.provider.name': 'featureflip', 'feature_flag.result.reason': errorType ? 'error' : (REASONS[event.reason] ?? 'unknown'), ...(event.variationKey && { 'feature_flag.result.variant': event.variationKey }), ...(errorType && { 'error.type': errorType }), });}
const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY, inspectors: [recordFlagEvaluation],});trace.getActiveSpan() needs a context manager. NodeSDK from @opentelemetry/sdk-node registers one for you. With a hand-built tracer provider, register AsyncLocalStorageContextManager yourself, or every evaluation will look like it happened outside a span.
Python
Section titled “Python”from opentelemetry import tracefrom featureflip import Config, EvaluationReason as R, FeatureflipClient
REASONS = {R.RULE_MATCH: "targeting_match", R.FALLTHROUGH: "default", R.FLAG_DISABLED: "disabled", R.PREREQUISITE_FAILED: "prerequisite_failed"}ERRORS = {R.FLAG_NOT_FOUND: "flag_not_found", R.ERROR: "general"}
def otel_inspector(event): span = trace.get_current_span() if not span.is_recording(): return attrs = {"feature_flag.key": event.flag_key, "feature_flag.provider.name": "featureflip", "feature_flag.result.reason": "error" if event.reason in ERRORS else REASONS.get(event.reason, "unknown")} if event.variation_key: attrs["feature_flag.result.variant"] = event.variation_key if event.reason in ERRORS: attrs["error.type"] = ERRORS[event.reason] span.add_event("feature_flag.evaluation", attrs)
client = FeatureflipClient( sdk_key=os.environ["FEATUREFLIP_SDK_KEY"], config=Config(inspectors=[otel_inspector]),)import io.featureflip.client.EvaluationInspector;import io.opentelemetry.api.common.Attributes;import io.opentelemetry.api.common.AttributesBuilder;import io.opentelemetry.api.trace.Span;
static final EvaluationInspector OTEL = event -> { Span span = Span.current(); if (!span.isRecording()) return; AttributesBuilder attrs = Attributes.builder() .put("feature_flag.key", event.getFlagKey()) .put("feature_flag.provider.name", "featureflip"); if (event.getVariationKey() != null) { attrs.put("feature_flag.result.variant", event.getVariationKey()); } switch (event.getReason()) { case RULE_MATCH: attrs.put("feature_flag.result.reason", "targeting_match"); break; case FALLTHROUGH: attrs.put("feature_flag.result.reason", "default"); break; case FLAG_DISABLED: attrs.put("feature_flag.result.reason", "disabled"); break; case PREREQUISITE_FAILED: attrs.put("feature_flag.result.reason", "prerequisite_failed"); break; case FLAG_NOT_FOUND: attrs.put("feature_flag.result.reason", "error").put("error.type", "flag_not_found"); break; case ERROR: attrs.put("feature_flag.result.reason", "error").put("error.type", "general"); break; default: attrs.put("feature_flag.result.reason", "unknown"); } span.addEvent("feature_flag.evaluation", attrs.build());};
FeatureflipClient client = FeatureflipClient.builder(System.getenv("FEATUREFLIP_SDK_KEY")) .inspectors(List.of(OTEL)) .build();The classic switch keeps it compiling on Java 11, the SDK’s minimum.
.NET traces are System.Diagnostics.Activity objects, and the OpenTelemetry .NET SDK exports an activity’s events as span events. The inspector needs no OpenTelemetry package of its own.
using System.Diagnostics;using Featureflip.Client;
var options = new FeatureFlagOptions{ Inspectors = { e => { if (Activity.Current is not { IsAllDataRequested: true } activity) return; var (reason, error) = e.Reason switch { EvaluationReason.RuleMatch => ("targeting_match", null), EvaluationReason.Fallthrough => ("default", null), EvaluationReason.FlagDisabled => ("disabled", null), EvaluationReason.PrerequisiteFailed => ("prerequisite_failed", null), EvaluationReason.FlagNotFound => ("error", "flag_not_found"), EvaluationReason.Error => ("error", "general"), _ => ("unknown", (string?)null), }; var tags = new ActivityTagsCollection { ["feature_flag.key"] = e.FlagKey, ["feature_flag.provider.name"] = "featureflip", ["feature_flag.result.reason"] = reason, }; if (e.VariationKey is not null) tags["feature_flag.result.variant"] = e.VariationKey; if (error is not null) tags["error.type"] = error; activity.AddEvent(new ActivityEvent("feature_flag.evaluation", tags: tags)); }, },};
var client = FeatureflipClient.Get( Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY"), options);Featureflip’s own Management API runs this inspector in production, so its flag reads show up on our traces in Grafana Tempo.
Go’s variation methods take no context.Context, so an inspector has no way to find the active span. Use the Featureflip OpenFeature provider with OpenFeature’s own tracing hook, which reads the span from the ctx you evaluate with:
import ( featureflip "github.com/canopy-labs/featureflip-go-openfeature" otelhook "github.com/open-feature/go-sdk-contrib/hooks/open-telemetry/pkg" "github.com/open-feature/go-sdk/openfeature")
openfeature.AddHooks(otelhook.NewTracesHook())_ = openfeature.SetProviderAndWait(featureflip.NewProvider(os.Getenv("FEATUREFLIP_SDK_KEY")))client := openfeature.NewClient("my-app")
// ctx is the request context, carrying the active spanon, _ := client.BooleanValue(ctx, "new-checkout", false, evalCtx)The hook writes the same feature_flag.evaluation event with the same reason values, with three differences. feature_flag.provider.name is featureflip-go, an error adds error.message, and it always includes feature_flag.context.id (the targeting key) and feature_flag.result.value. If those count as personal data for you, remove them in your collector before export.
Attach evaluations to Sentry errors
Section titled “Attach evaluations to Sentry errors”Sentry can list the flags a request evaluated before it failed, on the error event itself. It records boolean flags only, so the recipes below skip every other type. Python, Java and browser JavaScript use Sentry’s OpenFeature integration with the matching Featureflip OpenFeature provider. Node has no OpenFeature integration in Sentry, so it uses an inspector instead.
Python
Section titled “Python”import sentry_sdkfrom sentry_sdk.integrations.openfeature import OpenFeatureIntegrationfrom openfeature import apifrom featureflip_openfeature import FeatureflipProvider
sentry_sdk.init(dsn=os.environ["SENTRY_DSN"], integrations=[OpenFeatureIntegration()])api.set_provider_and_wait(FeatureflipProvider(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]))Install featureflip-openfeature-provider, openfeature-sdk and sentry-sdk (2.19.2 or later).
import dev.openfeature.sdk.OpenFeatureAPI;import io.featureflip.openfeature.FeatureflipProvider;import io.sentry.Sentry;import io.sentry.openfeature.SentryOpenFeatureHook;
Sentry.init(options -> options.setDsn(System.getenv("SENTRY_DSN")));
OpenFeatureAPI api = OpenFeatureAPI.getInstance();api.addHooks(new SentryOpenFeatureHook());api.setProviderAndWait(new FeatureflipProvider(System.getenv("FEATUREFLIP_SDK_KEY")));This needs io.featureflip:featureflip-openfeature and io.sentry:sentry-openfeature (8.42.0 or later). Sentry’s Java hook records successful evaluations only, so a flag that couldn’t be found never reaches the event.
Browser
Section titled “Browser”import * as Sentry from '@sentry/browser';import { OpenFeature } from '@openfeature/web-sdk';import { FeatureflipProvider } from '@featureflip/openfeature-web';
Sentry.init({ dsn: 'your-sentry-dsn', integrations: [Sentry.openFeatureIntegration()],});OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook());
await OpenFeature.setProviderAndWait( new FeatureflipProvider({ clientKey: 'your-client-sdk-key' }), { targetingKey: 'user-42' },);The browser hook records every boolean evaluation, including a flag that wasn’t found, which shows up with the default your code passed.
import * as Sentry from '@sentry/node';import { FeatureflipClient } from '@featureflip/node';
const featureFlags = Sentry.featureFlagsIntegration();Sentry.init({ dsn: process.env.SENTRY_DSN, integrations: [featureFlags] });
const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, inspectors: [ (event) => { // Sentry records boolean evaluations only if (typeof event.value === 'boolean') { featureFlags.addFeatureFlag(event.flagKey, event.value); } }, ],});Keep a reference to the integration. addFeatureFlag is a method on it, not on the Sentry scope. The flags are stored on the current scope, so they’re kept apart per request only when Sentry’s request isolation is running, which its HTTP instrumentation sets up. Needs @sentry/node 9.31.0 or later.
Go and .NET
Section titled “Go and .NET”As of November 2026, Sentry’s Go and .NET SDKs have no feature-flag API, so there is no recipe for them yet. The OpenTelemetry recipes above work in both.
- Errors are isolated. If an inspector fails — a thrown exception, a Go panic, a Ruby raise — evaluation still returns the correct value, the other inspectors still run, and the SDK logs a warning. Your analytics failures never break a feature flag. The isolation guarantee holds identically on every client SDK, including Swift; the warning-on-throw specifically is Browser/React/Android/Flutter only — Swift’s
EvaluationInspectoris a non-throwing closure type, so an inspector cannot throw in the first place and there is nothing to catch or warn about. - Keep inspectors fast, and never make a network call from one. They run synchronously inside every evaluation, so a request per evaluation adds its latency to every flag read in your app. Hand the event to something that batches, the way the recipes above do: an analytics SDK’s queue, a span event that leaves with its span, a Sentry scope that is read only when an error is sent. For anything else slow, enqueue the event and process it off the hot path.
- Treat
contextas read-only. It is a shallow copy of the context you passed; mutating it has no effect on the SDK.
Debugging with inspectors
Section titled “Debugging with inspectors”Because reason, ruleId, and prerequisiteKey are on the event, an inspector answers “why did this user get variation X?” without any extra tooling. The example below is the JS/Node server SDK; use your SDK’s own reason type for the comparison (see Reason values differ by SDK) — note that even though Browser and React are also JavaScript, their reason is the engine’s kebab-case string ('prerequisite-failed', not 'PrerequisiteFailed'), because a client SDK forwards the server’s reason verbatim instead of running its own evaluator:
inspectors: [ (event) => { if (event.reason === 'PrerequisiteFailed') { console.warn(`${event.flagKey} served its off value: prerequisite "${event.prerequisiteKey}" was not met`); } },]