UNPKG

autotel

Version:
175 lines 7.78 kB
//#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 };