autotel
Version:
Write Once, Observe Anywhere
175 lines • 7.78 kB
text/typescript
//#region src/feature-flags.d.ts
/**
* Feature flag evaluations, in the vocabulary OpenTelemetry already defined.
*
* A flagged rollout splits your traffic into two populations that share a
* service name, a route and a version. Without the flag on the span there is no
* way to ask the only question that matters during a rollout — is the new
* branch slower, or failing more, than the old one — and the usual workaround
* is to read it in the flag vendor's own dashboard, where the latency and the
* errors are not.
*
* The specification covers this: `feature_flag.key`, `.result.value`,
* `.result.variant`, `.result.reason`, `.provider.name`, `.context.id`, plus a
* `feature_flag.evaluation` event. Emitting those means any backend can split
* any metric by variant, with no vendor in the path.
*
* ## Attributes and events both
*
* Span attributes hold one flag: a second call overwrites the first. A request
* that branched on three flags needs one event each, which is what the
* `feature_flag.evaluation` event is for. Both are recorded — attributes so a
* single-flag span is filterable without unpacking events, events so a
* many-flag span keeps them all.
*
* ## Record what you branched on
*
* Record a flag where the code *reads* it, not where it is fetched. A flag
* evaluated and ignored explains nothing; the value the request actually took a
* branch on is the one that explains its behaviour.
*/
/** Canonical `feature_flag.*` attribute keys. */
declare const FEATURE_FLAG: {
readonly KEY: "feature_flag.key";
readonly RESULT_VALUE: "feature_flag.result.value";
readonly RESULT_VARIANT: "feature_flag.result.variant";
readonly RESULT_REASON: "feature_flag.result.reason";
readonly PROVIDER_NAME: "feature_flag.provider.name";
readonly CONTEXT_ID: "feature_flag.context.id";
readonly SET_ID: "feature_flag.set.id";
readonly VERSION: "feature_flag.version";
/**
* Why an evaluation failed. `feature_flag.evaluation.error.message` is the
* deprecated spelling of this and is deliberately not emitted — writing both
* would double the cardinality to no benefit.
*/
readonly ERROR_MESSAGE: "feature_flag.error.message";
};
/** Canonical event name for one evaluation. */
declare const FEATURE_FLAG_EVALUATION_EVENT = "feature_flag.evaluation";
/**
* Canonical `feature_flag.result.reason` values. The registry defines them in
* lower snake case; OpenFeature and most SDKs report them upper-cased, and
* forwarding that splits every group-by into two buckets meaning the same
* thing.
*/
declare const FEATURE_FLAG_REASON: readonly ["static", "default", "targeting_match", "split", "cached", "disabled", "unknown", "stale", "error"];
interface FeatureFlagEvaluation {
/** The flag's key, e.g. `new-checkout`. */
key: string;
/** The value the code branched on. Serialised if it is not a string. */
value: unknown;
/** Variant name, where the provider has one distinct from the value. */
variant?: string;
/**
* Why this value. Case-normalised to the registry's spelling, so a provider
* reporting `TARGETING_MATCH` and one reporting `targeting_match` land in the
* same bucket.
*/
reason?: string;
/** The provider that answered, e.g. `posthog`, `launchdarkly`, `flagd`. */
provider?: string;
/** Identifier of the evaluation context — the user or account keyed on. */
contextId?: string;
/** Identifier of the flag set this flag belongs to. */
setId?: string;
/** Version of the flag definition that produced this value. */
version?: string;
/** Why the evaluation failed, when it did. */
errorMessage?: string;
}
/** What an attribute can hold without being flattened to text. */
type FeatureFlagAttributeValue = string | number | boolean;
/**
* The smallest thing that can carry an evaluation.
*
* `track` is the correlated-log seam a `TraceContext` provides, and it is the
* only event seam offered: this repository emits events through the Logs API
* model, and a `Span.addEvent` fallback is how that direction quietly becomes
* optional. A caller holding a raw span supplies its own `track` — in the
* browser, `emitEvent` from `autotel-web` is one.
*
* A sink with no `track` still records the attributes, which covers the common
* single-flag span.
*/
interface FeatureFlagSink {
setAttributes(attributes: Record<string, FeatureFlagAttributeValue>): void;
track?(name: string, attributes?: Record<string, FeatureFlagAttributeValue>): void;
}
/** Canonical attributes for one evaluation. Absent fields are omitted. */
declare function featureFlagAttributes(evaluation: FeatureFlagEvaluation): Record<string, FeatureFlagAttributeValue>;
/**
* Record a flag evaluation on `sink` — as attributes and as a
* `feature_flag.evaluation` event.
*
* A missing sink is a no-op: instrumentation must never be the reason a branch
* throws, and a flag read outside any span is a legitimate thing to do.
*/
declare function recordFeatureFlag(sink: FeatureFlagSink | undefined, evaluation: FeatureFlagEvaluation): void;
/**
* The parts of an OpenFeature hook context this reads. Structurally typed on
* purpose: matching the shape rather than importing `@openfeature/server-sdk`
* keeps the SDK out of every bundle that imports a sibling of this module, and
* works against the web SDK, the server SDK and the React one alike — they
* agree on this shape and disagree on almost everything else.
*/
interface OpenFeatureHookContext {
flagKey: string;
defaultValue: unknown;
context?: {
targetingKey?: string;
};
providerMetadata?: {
name?: string;
};
clientMetadata?: {
name?: string;
};
}
/** The evaluation result an OpenFeature hook receives. */
interface OpenFeatureEvaluationDetails {
value: unknown;
variant?: string;
reason?: string;
}
/** Just enough of an OpenFeature hook to be registered as one. */
interface OpenFeatureHook {
after?(hookContext: OpenFeatureHookContext, details: OpenFeatureEvaluationDetails): void;
error?(hookContext: OpenFeatureHookContext, error: unknown): void;
}
interface OpenFeatureHookOptions {
/**
* Where to record. Defaults to the active span, which is what you want:
* the span that branched on the flag is the one whose latency and errors the
* flag explains.
*/
getSpan?: () => FeatureFlagSink | undefined;
/**
* Where the evaluation event goes when the sink brings no `track` of its own.
* Defaults to an OpenTelemetry log record. Injected for tests.
*/
emitLogRecord?: (attributes: Record<string, FeatureFlagAttributeValue>) => void;
}
/**
* An OpenFeature hook that records every evaluation under the canonical
* `feature_flag.*` convention.
*
* This is the zero-config path: OpenFeature already sits between the
* application and whichever flag vendor it uses, and it already fires on every
* evaluation — which is exactly the moment worth recording, because it is the
* moment the code branched.
*
* ```ts
* import { OpenFeature } from '@openfeature/server-sdk';
* import { autotelOpenFeatureHook } from 'autotel/feature-flags';
*
* OpenFeature.addHooks(autotelOpenFeatureHook());
* ```
*
* A failed evaluation is still recorded, with the default value the code
* actually used and `reason: 'ERROR'`. The default is what the request behaved
* as; the failure is why.
*/
declare function autotelOpenFeatureHook(options?: OpenFeatureHookOptions): OpenFeatureHook;
//#endregion
export { FEATURE_FLAG, FEATURE_FLAG_EVALUATION_EVENT, FEATURE_FLAG_REASON, FeatureFlagAttributeValue, FeatureFlagEvaluation, FeatureFlagSink, OpenFeatureEvaluationDetails, OpenFeatureHook, OpenFeatureHookContext, OpenFeatureHookOptions, autotelOpenFeatureHook, featureFlagAttributes, recordFeatureFlag };