npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@grafana-experiments/sdk

v0.3.0

Published

Explicit experiment activation and analytics reporting with OpenFeature and Grafana Faro

Readme

grafana-experiments

Published package: @grafana-experiments/sdk (including /react and /grafana entry points). See RELEASING.md for the manual trusted-publishing workflow. Repository examples use the local package name; release artifacts use the scoped name.

A browser TypeScript SDK for explicitly activating OpenFeature experiments and reporting analytics to Grafana Faro and, optionally, an existing analytics reporter. It does not create flags, configure allocation, register Odin resources, or perform statistical analysis.

Quick start

import { createExperiments } from '@grafana-experiments/sdk';

const experiments = createExperiments({
  scope: 'my-application',
  client: openFeatureClient,
  faro: { instance: faro },
  reportAnalytics: existingReporter, // Optional: omit for Faro-only reporting.
  getSessionId: () => analyticsSessionId,
  contextKey: tenantContextKey,
  isEnabled: () => analyticsConsent,
  events: { guide_started: 'user-action' },
});

await experiments.ready;

const banner = experiments.defineExperiment({
  id: 'learning-banner-v1',
  flagKey: 'my-team.learning-banner',
  flag: { type: 'boolean' },
});

// Invoke only at the intended entry point, such as opening the panel.
const state = await banner.activate();
const showNewBanner = state.status === 'active' && state.variant === 'treatment';

const reportAnalytics = experiments.reportAnalytics;
reportAnalytics('guide_started', { guideId });
reportAnalytics('feedback', { helpful: true });

The original reporter receives the same name and properties plus the reserved experiments array. Each array entry identifies one activated experiment, its variant, session, flag and exposure. Before activation the array is empty. Supplying an existing experiments property does not override SDK attribution. Objects passed to the original reporter are not redacted; Faro receives a separate copy. Reporters may return promises; rejection is handled independently of Faro reporting.

scope must be stable and distinct for each application. Use a new experiment ID for a new experiment run or a changed definition. contextKey must be a stable, non-secret identity/tenant discriminator; it is included in the session-storage key. Never use credentials or email addresses as context keys.

Faro initialization and actions

Supply exactly one of faro: { instance } and faro: { config }. The latter accepts Faro's BrowserConfig, including collector, instrumentations, sampling and beforeSend. SDK-created instances are isolated, default to no global exposure, and include UserActionInstrumentation even when a custom instrumentation list is supplied. Call await experiments.ready before rendering instrumented surfaces. Reports made during initialization still reach the original reporter immediately; their Faro mirror waits for initialization.

An injected instance remains host-owned. Include Faro's UserActionInstrumentation when initializing it (Faro's default instrumentation list already includes it). If it is missing, action calls fall back to custom events with action_fallback: 'instrumentation-unavailable'; the SDK does not modify an injected instance's instrumentation list.

Select the representation through events or override it per call:

reportAnalytics(
  'guide_started',
  { guideId },
  {
    faro: { type: 'user-action' },
  },
);
reportAnalytics(
  'feedback',
  { helpful: true },
  {
    faro: { type: 'custom-event' },
  },
);
  • Unconfigured events default to custom events. Identical consecutive calls remain countable.
  • A user-action call starts a real Faro action; it does not also emit a custom-event mirror.
  • Starting another action ends a still-active SDK-owned action with end_reason: 'superseded'. This marks the observation boundary, not successful completion of the underlying operation.
  • A host-owned action is preserved. The call becomes a custom event with action_fallback: 'host-action-active', and Faro correlates it with the active action according to its normal lifecycle.
  • Faro ends actions naturally after browser activity. If no qualifying activity occurs, Faro may cancel the action. RudderStack call counts therefore need not equal completed Faro action counts.
  • Explicit completion uses Faro's internal lifecycle interface, isolated in src/faro.ts. Compatibility is restricted to Faro 2.12.x and tested against 2.12.0 and 2.12.1.

dispose() stops reporting, cancels pending activations, removes SDK listeners, and ends only a still-active SDK-owned action when reporting is enabled. If consent is disabled, both disposal and context reset cancel that action without emitting it; host-owned actions remain untouched. For an SDK-created Faro it also removes instrumentations and transports. It never pauses or disposes an injected Faro or an injected OpenFeature client. Disposing does not erase session deduplication state.

Activation and sessions

Boolean flags map false to control and true to treatment. A failed evaluation is never counted as control. Object-valued flags require a validator:

type BannerConfig = {
  variant: 'control' | 'treatment' | 'excluded';
  title: string;
};

const banner = experiments.defineExperiment<BannerConfig>({
  id: 'configured-banner-v1',
  flagKey: 'my-team.configured-banner',
  flag: {
    type: 'object',
    validate: (value): value is BannerConfig =>
      typeof value === 'object' && value !== null && 'title' in value && typeof value.title === 'string',
  },
});

Object flags may explicitly declare more than two active variants:

const nudge = experiments.defineExperiment({
  id: 'help-button-nudge-v1',
  flagKey: 'pathfinder.help-button-nudge-experiment',
  flag: {
    type: 'object',
    variants: ['control', 'glow', 'tooltip'],
    validate: (value): value is { variant: string } =>
      typeof value === 'object' && value !== null && 'variant' in value && typeof value.variant === 'string',
  },
});

Omitting variants preserves the control/treatment allowlist. Explicit lists must be non-empty, contain unique non-empty names without surrounding whitespace, and omit the reserved excluded value. excluded always means no exposure. Unknown variants remain unavailable even if the payload validator accepts them. Variant names pass unchanged through activation, exposure, session restore, journeys, analytics and Faro; boolean flags still map false/true to control/treatment. Use a new experiment ID when changing the variant definition. Variant and active assignment variants are now strings, so consumers needing an exhaustive union should validate against their own declared list.

This adds collection support, not multi-variant statistical analysis or remote allocation. Odin's current two-variant analysis contract still requires separate work before a three-way readout.

The SDK separately validates the variant. Excluded values emit no exposure. Malformed values, unavailable providers and missing flags produce status: 'unavailable'; keep the existing experience in that state. Readiness waits at most 10 seconds by default (readinessTimeoutMs). Subsequent explicit activation can retry; mounted React surfaces also retry after provider readiness/configuration recovery.

Each successful activation produces experiment_viewed in both destinations with:

| Field | Meaning | | ------------------ | -------------------------------------------------------- | | experiment_id | Stable experiment run identifier | | experiment_group | Stable audience label; defaults to rollout | | flag_key | Existing OpenFeature flag | | variant | The declared active variant | | session_id | Host analytics session, falling back to Faro's session | | exposure_id | Opaque random UUID reused for the same cached assignment |

Exposure is always a Faro custom event, independent of action completion. The SDK attempts reporting once; this is not a collector delivery acknowledgement. Analytics and Faro retain their existing sampling and filtering policies.

The storage key serializes application scope, context, session, experiment ID, flag key and flag type. The exposure ID itself contains none of those values. Within that cache, an unchanged variant/revision/allocation/population reuses its UUID; a changed assignment gets another UUID.

Connect a deployed flag to Odin

For an illustrative deployed boolean GOFF flag named my-team.learning-banner, use this explicit mapping:

| System | Field | Example | | -------------------------------- | --------------------------- | ------------------------- | | GOFF / OpenFeature | Deployed flag key | my-team.learning-banner | | SDK definition | flagKey | my-team.learning-banner | | SDK definition and Faro exposure | id / experiment_id | learning-banner-v1 | | Odin Experiment | metadata.name | learning-banner-v1 | | Odin Experiment spec | grafanaFeatureToggle.name | my-team.learning-banner |

The quick-start definition above uses these SDK values. Create the Odin resource separately with that name and flag reference, then configure its data source and measurement manifest. For this boolean example, false is control and true is treatment. defineExperiment() only registers an application-local handle; it does not create or validate the deployed flag, provision an Odin resource, or check names across systems. Odin's flag reference is documentation, not an assignment authority. Verify the emitted experiment_id and flag_key in the destination against the resource and deployed flag before collecting a run.

Verify assignment before declaring randomization

population: 'randomized' and allocationUnit are host assertions, not SDK verification of GOFF. Before enabling randomized inference in Odin:

  1. Record the deployed flag revision, eligibility rule, randomized percentage split, and the exact evaluation-context field GOFF uses for assignment. A targeting rule or a local override alone is not evidence of randomization.
  2. Trace that field through the deployed provider's evaluation request. For a stack-assigned flag, every session on a stack must use the same authoritative stack identity, and the SDK's allocationUnit.id must identify that same entity. Do not substitute a browser session ID, or assume an organization ID means stack assignment.
  3. Evaluate known eligible entities through the actual provider. Check that repeated evaluations for the same entity agree across sessions, that independent entities can receive both variants, and that ineligible entities are excluded. Retain the rule/context evidence: an observed 50/50 split alone does not prove randomization.
  4. Inspect stored exposures and outcomes for matching experiment/revision, variant, allocation identity and manifest. Verify collection completeness independently of assignment: sampling, filtering, consent, failed delivery and missing exposure rows can invalidate the measurement population.
  5. Only then set the SDK population declaration and Odin's randomized-population declaration. Set Odin's expected allocation to the deployed eligible-population split; change the experiment run/revision when the assignment contract changes. Until verification is complete, omit the randomization declaration and use descriptive results.

These checks are an adoption contract; neither the SDK nor Odin introspects GOFF to enforce them. A sample-ratio check can detect some collection or assignment failures but cannot establish that the configured assignment rule was randomized.

Standards

The SDK follows the published standards where they exist and defines only what they leave out.

  • OpenFeature is the only assignment authority. The SDK reads variants through the evaluation API, so any provider works.
  • OpenTelemetry feature-flag semantic conventions (release candidate). Each exposure also carries the attributes of the evaluation that produced it, mapped as OpenFeature Appendix D recommends:

| Attribute | Source | | ----------------------------- | ------------------------------------------------------------------------------ | | feature_flag.key | Flag key | | feature_flag.result.variant | Provider's variant | | feature_flag.result.value | Evaluated value, only when the provider has no variant | | feature_flag.result.reason | OpenFeature reason in lower snake case (TARGETING_MATCH → targeting_match) | | feature_flag.provider.name | Provider metadata | | feature_flag.context.id | Flag metadata contextId | | feature_flag.set.id | Flag metadata flagSetId | | feature_flag.version | Flag metadata version |

  • The SDK does not emit OpenTelemetry's feature_flag.evaluation event, which is recorded on every evaluation. An exposure is reported once per session at the entry point. Add OpenFeature's OpenTelemetry hook if you also want every evaluation.
  • feature_flag.context.id falls back to the targeting key in the convention. The SDK omits that fallback so identity does not reach telemetry unless the provider supplies contextId. Set recordFlagValue: false when flag values are sensitive or large.
  • OpenFeature Tracking API. Supply openFeatureTracking to also send outcome events to client.track() for providers that attribute goals themselves. It is off by default because it shares business data with the flag provider. Return the tracking details to send, or null to skip:
openFeatureTracking: (name, properties) =>
  name === 'purchase_completed' ? { value: Number(properties.amount), currency: 'GBP' } : null,

To explicitly share the same attribution used by Faro and customer analytics, use the third callback argument and exported helper:

import { toOpenFeatureTrackingDetails } from '@grafana-experiments/sdk';

// Within createExperiments options:
openFeatureTracking: (name, properties, context) =>
  name === 'purchase_completed'
    ? { value: Number(properties.amount), ...toOpenFeatureTrackingDetails(context) }
    : null,

MeasurementContext contains the event ID, captured assignments, schema versions and optional journey ID. The helper returns an independent OpenFeature-compatible object, omitting undefined fields. Existing two-argument callbacks remain unchanged: no attribution is added to their return value. Select only the fields your provider should receive. Faro redaction does not redact provider Tracking or customer analytics; each destination has its own policy and receives independent copies.

The Web SDK supplies its current evaluation context to the provider's track() method. Our explicit tracking details retain the journey's original exposure snapshot; they neither change targeting nor guarantee that the provider uses those fields for its own attribution. Identity changes invalidate old journeys. Unsupported Tracking is a no-op, and a Tracking failure does not prevent Faro reporting. An attempted call is not a storage receipt.

Optional OpenTelemetry evaluation telemetry

Use the official @openfeature/open-telemetry-hooks package with an existing, host-configured OTel logs SDK and exporter. This is optional; the experiments package installs no OTel pipeline or additional queue.

import { EventHook } from '@openfeature/open-telemetry-hooks';

// Install once on the host's OpenFeature client after configuring OTel logs.
// Do not also install this hook globally.
client.addHooks(
  new EventHook({
    excludeAttributes: ['feature_flag.context.id', 'feature_flag.result.value'],
  }),
);

This example suppresses targeting identity and raw flag values. Review other provider metadata for sensitive data before enabling export. The host must enforce consent in its OTel collection/export pipeline, including queued records when consent changes. The SDK's isEnabled gate controls SDK reporting, not independently installed OpenFeature hooks or the host's OTel exporter.

Three distinct signals are involved:

| Signal | Meaning | Delivery | | ------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | | feature_flag.evaluation | A flag was evaluated, even if repeatedly | Official OTel hook through the host's OTel logs pipeline | | experiment_viewed | An eligible experience was explicitly activated | Faro and the configured exposure analytics destination | | Business outcome | An operation occurred, with captured exposure/journey attribution | Faro, optional customer analytics and opt-in OpenFeature Tracking |

OTel feature-flag evaluation attributes are currently release candidate within an in-development convention. Reusing their names on a Faro exposure does not export that exposure through OTel. Evaluation counts must not become the exposure denominator. Native Faro actions remain distinct from explicit business outcomes; automatic errors and measurements keep origin-aware Faro enrichment.

References: OpenFeature Tracking specification, official OTel hooks, and OTel evaluation conventions.

Neither standard defines an experiment, an exposure, attribution of outcomes to an exposure, or the allocation unit. Those remain this SDK's contract (experiment_id, exposure_id, experiments, allocation_unit_*), and the existing field names are unchanged.

Assignments remain stable until the session or identity/tenant context changes. Session storage preserves assignments and deduplication across reloads in the same tab. Blocked storage falls back to in-memory deduplication. Tab duplication, concurrent SDK instances and other devices can still produce duplicate exposures; use exposure_id for downstream deduplication. The browser is not an authoritative source of assignment persistence.

Faro session changes are observed automatically. Call experiments.refreshSession() when a host session ID or consent changes. Call experiments.resetContext(newContextKey) when switching identity/tenant, before reporting or activating there. Calling resetContext with the current key explicitly clears the current registered experiments' stored assignments. Neither operation changes the OpenFeature provider's context: the host must update that as well.

Without either session ID, activation remains unavailable. Exposure per session does not imply assignment per session: Grafana continues to allocate by its server-defined stack/org context.

React

import { ExperimentsProvider, useExperiment } from '@grafana-experiments/sdk/react';

// Define the SDK and experiment once in application setup, not during render.
function Panel({ eligible }: { eligible: boolean }) {
  const state = useExperiment(banner, eligible);
  return state.status === 'active' && state.variant === 'treatment' ? (
    <NewExperience />
  ) : (
    <ExistingExperience />
  );
}

<ExperimentsProvider experiments={experiments}>
  <Panel eligible={panelIsOpen} />
</ExperimentsProvider>;

The activation hook works with a stable experiment handle, with or without the context provider. useExperiments() accesses the SDK from that provider. Activation happens in an effect; abandoned renders and unmounted pending activations produce no exposure. Visibility is the application's responsibility: mount does not necessarily mean visible. Changing eligibility does not undo an already recorded exposure. The provider does not own or dispose the SDK; the application's bootstrap lifecycle does.

Grafana adapter

import { createGrafanaExperiments } from '@grafana-experiments/sdk/grafana';

const experiments = createGrafanaExperiments({
  scope: 'my-plugin',
  domain: 'my-plugin-experiments',
  faro: { instance: faro },
  getSessionId: () => analyticsSessionId,
});

Requires @grafana/runtime >=13.2.2 <14. The adapter composes Grafana's local-override and OFREP proxy providers in its own OpenFeature domain. It refuses to replace an existing named provider and does not alter Grafana's default provider or targeting context. Dispose releases only the adapter's provider registration.

Ordinary events use reportInteraction. Exposure uses reportExperimentView(id, group, variant) exactly once, with no duplicate interaction event. Grafana's exposure API accepts only those three fields; Grafana supplies its own analytics identity/session context. The full SDK exposure record is available in Faro and subsequent enriched outcome events. It is not injected into unsupported Grafana exposure fields.

Consent, filtering and failures

isEnabled gates SDK activation and reporting. Call refreshSession() when it changes; SDK-created Faro instances are paused on revocation and resumed only if the SDK paused them. The host remains responsible for pausing an injected Faro and its independently captured automatic telemetry. The SDK does not override the host's sampling or beforeSend filters.

Use transformFaroProperties(name, properties) to redact the Faro copy, or return null to suppress it. Faro values are converted to strings; object values are JSON encoded. Uncloneable/unserializable properties are omitted from the Faro copy without dropping other attributes. Original-reporter and Faro failures are isolated and can be observed through onError. No transport retries or durable queues are added.

Development

npm ci --legacy-peer-deps
npm run check
npm run test:mutation
npm pack --dry-run

The standalone package lives outside Odin's plugin build. Development uses --legacy-peer-deps because Grafana runtime is an optional host integration and its full UI peer dependency graph is unnecessary for adapter-boundary tests. Real OpenFeature clients and Faro recording transports exercise the core; only Grafana's host boundary is substituted. A/A and Pathfinder-style examples are in examples/. No package is published or production application migrated by these commands.

Version 2 measurement contract

OpenFeature remains the only assignment authority. Define metadata for declared allocation identity and population, or mapAssignment(details) to map provider results explicitly. The SDK never infers that a browser session represents a stack or organization. Missing allocation metadata is valid descriptive telemetry; Odin withholds allocation-level inference.

const experiment = sdk.defineExperiment({
  id: 'checkout',
  flagKey: 'checkout-recommendations',
  flag: { type: 'boolean' },
  metadata: {
    revision: 'recommendations-v2',
    allocationUnit: { type: 'stack', id: stackId },
    population: 'randomized',
  },
});
await experiment.activate({ entryId: 'checkout-entry-1' });
const journey = sdk.beginJourney();
await addProduct();
journey.reportAnalytics('product_added', { product: 'trail-pack' });
journey.reportAnalytics('purchase_completed', { amount: 48, currency: 'GBP' });
journey.end();

Use one stable entryId for repeated activation of a committed entry; use a new ID for a new entry. React's useExperiment(experiment, eligible, entryId) follows the same rule. Provider updates do not change an active journey. Re-entry evaluates again: an unchanged assignment reuses its session exposure, while variant/revision/allocation changes receive their own exposure. Omitting entryId preserves the original session-stable API for existing integrations.

Journey handles snapshot active assignments and are invalidated on session/identity reset or end(). Consent is checked when events are sent. getTelemetryContext() supplies captured experiments and journey_id strings for explicit Faro error/measurement calls. Ended or invalidated handles return no experiment assignments. Do not use journey handles to bypass the host's Faro consent policy.

Each logical call receives a unique event_id; repeated calls remain distinct, while transport retries reuse the same payload. Exposure IDs are opaque random identifiers. Assignment arrays retain every active experiment. The v2 storage namespace deliberately does not reuse v1 cache entries; existing v1 warehouse records remain readable as legacy session records without invented allocation metadata.

Typed events and manifests

Pass eventDefinitions into createExperiments. Each definition has a positive integer version and properties mapping to {type: 'string' | 'number' | 'boolean', required?: boolean}. Use defineEvent and createTypedReporter to preserve TypeScript inference. getEventManifest() returns a JSON-serializable versioned manifest to import into Odin. Event names not in the manifest retain the original permissive reporting behavior.

Malformed declared measurements still attempt the existing customer reporter but emit an experiment_diagnostic to Faro rather than a valid business outcome. Event manifests, including schema versions, must match Odin's analysis configuration. Exporting a manifest does not provision flags, mutate Odin, or transmit credentials.

Diagnostics and Faro enrichment

getDiagnostics() returns a bounded snapshot (latest 200 entries); subscribeDiagnostics(listener) observes evaluation, activation, suppression, transitions, validation and reporting attempts/failures. Observer failures cannot break application reporting. Diagnostic messages are local developer information and should not be copied into public telemetry without redaction. A reporting attempt is never represented as confirmed storage.

Use enrichFaro: true with SDK-owned initialization options. For an existing Faro instance, install createExperimentEnricher({getAssignments: () => sdk?.getActiveAssignments() ?? [], isEnabled, beforeSend: existingHook}) when the host initializes Faro. The host hook runs last so its filtering/redaction remains authoritative. The SDK does not replace another integration's hooks or sampling configuration.

Measurement emission time does not prove when measured activity happened. Measurements require context.experiment_started_at (epoch milliseconds), or an explicit journey snapshot, to receive attribution. Pre-exposure and unscoped Web Vitals remain unattributed. The SDK never installs a second replay recorder. Native Faro user-action completion is not a business conversion.

Storage ownership

The SDK emits the standard Faro envelope with custom attributes. Storage provisioning and database ownership remain outside the SDK.

Delivery boundaries

An activated diagnostic records an exposure decision. analytics-attempted and faro-attempted record calls to those destinations, not acknowledgements of storage. faro-filtered describes the SDK's redaction transform; filtering inside a host Faro hook or transport may not be observable by this SDK. Observable synchronous and asynchronous reporter failures produce reporting-error diagnostics.

Faro's transport owns batching and retries. Retry attempts retain the original event ID; separate legitimate reporting calls receive different IDs. The SDK does not replace caller-owned transports or add a second delivery queue. Transport retries can be exhausted, and closing a browser can lose buffered telemetry. Session-persisted exposure deduplication is not a delivery receipt: reloading does not replay an exposure that has already been activated. Verify ingestion in the destination when completeness matters. Sampling and filtering remain controlled by the host; a complete-collection declaration in Odin is an operational assertion, not a guarantee from this SDK.

Inspecting activation and previewing variations

experiments.inspect() returns each registered experiment's id, flag key, status, unavailable reason and selected variant. It does not evaluate a flag or activate an experiment, and omits configuration payloads, identities and exposure IDs. inactive means no activation is currently committed, including after a reset. Use getDiagnostics() or subscribeDiagnostics() for the bounded transition history, including aborted and invalidated activation attempts. Diagnostics are local observations, not delivery receipts. A dismissed tooltip, hidden button, or already-open panel is host state: inspect that in the application rather than expecting the SDK to infer it.

For local UI testing, opt in when constructing a separate SDK instance:

const experiments = createExperiments({
  scope: 'learning-preview',
  client,
  faro: { instance: faro },
  development: true,
  isEnabled: hasAnalyticsConsent,
});
const help = experiments.defineExperiment({
  id: 'help-nudge',
  flagKey: 'help-nudge',
  flag: {
    type: 'object',
    variants: ['control', 'glow', 'tooltip'],
    validate: (value): value is { variant: string } =>
      typeof value === 'object' && value !== null && 'variant' in value && typeof value.variant === 'string',
  },
});
experiments.development!.setOverride('help-nudge', { variant: 'tooltip' });
await help.activate(); // Call from the same visible entry point used by the application.
console.table(experiments.inspect());
experiments.development!.reset(); // Preserve overrides, discard preview assignments and journeys.
experiments.development!.clearOverride('help-nudge'); // Next activation evaluates the provider again.

The development controls are absent unless explicitly enabled. Overrides accept complete flag values, use the experiment's normal validator and active-variant allowlist, and never mutate the OpenFeature provider or remote flags. Setting or clearing an override resets all assignments and invalidates journeys in this preview instance; activate again through the normal UI entry point. Overrides are cloned and held in memory. Preview storage never reads, writes or deletes the host's production sessionStorage, even when a custom storage adapter is supplied.

Preview assignments are marked population: 'test'. The SDK suppresses exposure callbacks, analytics reporters, Faro experiment/outcome events and OpenFeature tracking for the entire instance. It still checks consent and requires a session. Faro's own host instrumentation is independent and may still send telemetry; disable host enrichment/reporting of preview assignment snapshots as well. Do not expose these controls globally or enable preview mode in a production build. Host-owned UI dismissal keys must be reset by the host; this API deliberately cannot delete them. A reload removes overrides, so development tooling must reapply them explicitly.

Evolving a running experiment

Choose variation names, their meaning, allocation unit, eligibility and weights before launch. The SDK allows any declared variation list but does not own bucketing or guarantee sticky provider assignment. Provider changes to weights, ordering, targeting or hashing can reassign entities. Renaming a variant creates a different analysis label; adding or removing one changes the design. Do not pool these changes silently into the original comparison.

For a materially different design, use a new experiment id and an independently configured flag. For a planned allocation revision, record an explicit metadata.revision (or map the provider's revision with mapAssignment), preserve variant meanings, and analyse revisions separately. Record the allocation unit and whether the population is randomized, targeted or test. A revision is attribution metadata, not a remote configuration operation or a force-refresh switch.

Session restoration retains the saved assignment for ordinary activation; provider updates alone do not replace it. Entry-based re-evaluation with a new entryId can observe a new assignment. Audit assignment-transition records and the actual bucketing contract before interpreting results. Never use development resets to rebucket real participants. Deduplicate ingestion by event identity, and verify both exposure and outcome delivery at your destination before launching analysis.