Reference
Feature flags
Record flag assignments in Otis — manually or via OpenFeature for LaunchDarkly, Statsig, PostHog, GrowthBook, Unleash, Hypertune, and more.
Otis records feature-flag assignments as user properties so the observation agent can correlate a metric change with the flag flips that caused it. When the agent sees a frustration spike on Tuesday, it can ask: which variant of which flag was that cohort on?
There are two integration paths. Use whichever fits how flags flow through your app.
Path 1 — Manual exposure
Call setUserProperties with the flag. prefix anywhere you evaluate a flag:
import { setUserProperties } from "@runotis/sdk";
setUserProperties("user-123", {
"flag.checkout_v2": "variant_b",
"flag.new_onboarding": "control",
});Use this when you don't have a flag platform or you're rolling your own. Works in both server and browser runtimes.
Path 2 — OpenFeature hook (recommended)
If you use a flag platform with an OpenFeature provider — LaunchDarkly, Statsig, PostHog, GrowthBook, Unleash, Hypertune, ConfigCat, DevCycle, and more — register the OtisFlagHook. Every flag evaluation auto-records the assignment. No per-evaluation setUserProperties calls.
Install
@runotis/sdk/openfeature ships in the main SDK package. You also need OpenFeature itself and your vendor's provider.
# Server
npm install @openfeature/server-sdk
# Browser
npm install @openfeature/web-sdkRegister the hook
Once at app boot, before you evaluate any flags:
import { OpenFeature } from "@openfeature/server-sdk";
import { OtisFlagHook } from "@runotis/sdk/openfeature";
OpenFeature.addHooks(new OtisFlagHook());That's it. Every client.getBooleanValue(), getStringValue(), etc. will record a flag.X user property automatically.
Vendor examples
The Otis hook is vendor-neutral — it sits between OpenFeature and your provider. Setup of the provider itself comes from your vendor's docs.
import { OpenFeature } from "@openfeature/server-sdk";
import { LaunchDarklyServerProvider } from "@launchdarkly/openfeature-node-server";
import { OtisFlagHook } from "@runotis/sdk/openfeature";
await OpenFeature.setProviderAndWait(
new LaunchDarklyServerProvider("YOUR_LD_SDK_KEY"),
);
OpenFeature.addHooks(new OtisFlagHook());
// In your request handler:
const client = OpenFeature.getClient();
const value = await client.getStringValue(
"checkout_v2",
"control",
{ targetingKey: userId },
);
// → automatically records `flag.checkout_v2` on user `userId`import { OpenFeature } from "@openfeature/server-sdk";
import { StatsigProvider } from "@statsig/openfeature-provider";
import { OtisFlagHook } from "@runotis/sdk/openfeature";
await OpenFeature.setProviderAndWait(new StatsigProvider("YOUR_STATSIG_KEY"));
OpenFeature.addHooks(new OtisFlagHook());
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue(
"checkout_v2",
false,
{ targetingKey: userId },
);import { OpenFeature } from "@openfeature/server-sdk";
import { PostHogProvider } from "@posthog/openfeature-provider";
import { OtisFlagHook } from "@runotis/sdk/openfeature";
await OpenFeature.setProviderAndWait(
new PostHogProvider("YOUR_POSTHOG_KEY", { host: "https://us.i.posthog.com" }),
);
OpenFeature.addHooks(new OtisFlagHook());
const client = OpenFeature.getClient();
const variant = await client.getStringValue(
"checkout_v2",
"control",
{ targetingKey: userId },
);import { OpenFeature } from "@openfeature/server-sdk";
import { GrowthBookProvider } from "@growthbook/openfeature-provider";
import { OtisFlagHook } from "@runotis/sdk/openfeature";
await OpenFeature.setProviderAndWait(
new GrowthBookProvider({ clientKey: "YOUR_GB_KEY" }),
);
OpenFeature.addHooks(new OtisFlagHook());
const client = OpenFeature.getClient();
const value = await client.getStringValue(
"checkout_v2",
"control",
{ targetingKey: userId },
);Browser
For client-side flag evaluation, use @openfeature/web-sdk instead. The hook API is identical:
import { OpenFeature } from "@openfeature/web-sdk";
import { OtisFlagHook } from "@runotis/sdk/openfeature";
await OpenFeature.setProviderAndWait(/* your browser provider */);
OpenFeature.addHooks(new OtisFlagHook());Behavior
Targeting key required
Otis attaches each flag.X property to the user identified by targetingKey in the EvaluationContext. Evaluations without a targetingKey are skipped — there's no user to attach the assignment to.
If you're using the manual path, the userId argument to setUserProperties serves the same role.
Dedup
OpenFeature can evaluate the same flag many times per request. The hook keeps a small in-memory cache and emits a property write only when the assignment for a (user, flag) pair actually changes. You don't have to throttle calls yourself.
Default cache size: 10,000 entries. Tune with dedupCacheSize:
new OtisFlagHook({ dedupCacheSize: 50_000 });Selective tracking
For high-cardinality flag estates where you only care about a few canonical experiments:
new OtisFlagHook({
flagAllowlist: ["checkout_v2", "pricing_test", "new_onboarding"],
});Evaluations of any other flag are silently ignored.
Custom predicate
For arbitrary filtering:
new OtisFlagHook({
shouldEmit: (ctx, details) => {
// Skip evaluations that fell back to the default.
return details.reason !== "DEFAULT";
},
});Guidance on variant naming
Otis is optimized for canonical experiments. Two rules of thumb keep your data useful and your project tidy:
-
Variant values should be short, stable identifiers —
control,variant_b,treatment. Avoid UUIDs, JSON blobs, or per-user random strings as variants. Variant values longer than 128 characters are truncated automatically. -
Aim for fewer than 100 distinct variant values per flag. A flag with 5 stable variants is what cohort analysis is designed around; a flag with thousands of unique values per user is something else (a continuous configuration, an A/B/n with no replication).
If a flag exceeds 100 distinct variants, Otis auto-quarantines it: the existing assignments stay on each user, but the flag stops feeding cohort analysis and a notice appears in your project's recent-changes timeline. You can clear the quarantine after renaming the variants from your project settings.
What the observation agent does with this
Once flag assignments are flowing, the observation agent uses them to:
- Frame metric changes against flag rollouts in its narratives ("the Tuesday frustration spike was concentrated in the
flag.checkout_v2=variant_bcohort"). - Surface flag flips in the project's recent-changes timeline alongside deploys, incidents, and other events.
- Answer chat questions like "what flags ramped this week?" via the
query_recent_changestool.