autotel 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _opentelemetry_api = require("@opentelemetry/api");
3
+ let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
4
+
5
+ //#region src/feature-flags.ts
6
+ /**
7
+ * Feature flag evaluations, in the vocabulary OpenTelemetry already defined.
8
+ *
9
+ * A flagged rollout splits your traffic into two populations that share a
10
+ * service name, a route and a version. Without the flag on the span there is no
11
+ * way to ask the only question that matters during a rollout — is the new
12
+ * branch slower, or failing more, than the old one — and the usual workaround
13
+ * is to read it in the flag vendor's own dashboard, where the latency and the
14
+ * errors are not.
15
+ *
16
+ * The specification covers this: `feature_flag.key`, `.result.value`,
17
+ * `.result.variant`, `.result.reason`, `.provider.name`, `.context.id`, plus a
18
+ * `feature_flag.evaluation` event. Emitting those means any backend can split
19
+ * any metric by variant, with no vendor in the path.
20
+ *
21
+ * ## Attributes and events both
22
+ *
23
+ * Span attributes hold one flag: a second call overwrites the first. A request
24
+ * that branched on three flags needs one event each, which is what the
25
+ * `feature_flag.evaluation` event is for. Both are recorded — attributes so a
26
+ * single-flag span is filterable without unpacking events, events so a
27
+ * many-flag span keeps them all.
28
+ *
29
+ * ## Record what you branched on
30
+ *
31
+ * Record a flag where the code *reads* it, not where it is fetched. A flag
32
+ * evaluated and ignored explains nothing; the value the request actually took a
33
+ * branch on is the one that explains its behaviour.
34
+ */
35
+ /** Canonical `feature_flag.*` attribute keys. */
36
+ const FEATURE_FLAG = {
37
+ KEY: "feature_flag.key",
38
+ RESULT_VALUE: "feature_flag.result.value",
39
+ RESULT_VARIANT: "feature_flag.result.variant",
40
+ RESULT_REASON: "feature_flag.result.reason",
41
+ PROVIDER_NAME: "feature_flag.provider.name",
42
+ CONTEXT_ID: "feature_flag.context.id",
43
+ SET_ID: "feature_flag.set.id",
44
+ VERSION: "feature_flag.version",
45
+ /**
46
+ * Why an evaluation failed. `feature_flag.evaluation.error.message` is the
47
+ * deprecated spelling of this and is deliberately not emitted — writing both
48
+ * would double the cardinality to no benefit.
49
+ */
50
+ ERROR_MESSAGE: "feature_flag.error.message"
51
+ };
52
+ /** Canonical event name for one evaluation. */
53
+ const FEATURE_FLAG_EVALUATION_EVENT = "feature_flag.evaluation";
54
+ /**
55
+ * Canonical `feature_flag.result.reason` values. The registry defines them in
56
+ * lower snake case; OpenFeature and most SDKs report them upper-cased, and
57
+ * forwarding that splits every group-by into two buckets meaning the same
58
+ * thing.
59
+ */
60
+ const FEATURE_FLAG_REASON = [
61
+ "static",
62
+ "default",
63
+ "targeting_match",
64
+ "split",
65
+ "cached",
66
+ "disabled",
67
+ "unknown",
68
+ "stale",
69
+ "error"
70
+ ];
71
+ /**
72
+ * A flag value as an attribute.
73
+ *
74
+ * Booleans and numbers are kept as they are: `feature_flag.result.value`
75
+ * permits typed values, and stringifying `true` makes a boolean flag
76
+ * uncomparable with the numeric one next to it. Only a structured value — which
77
+ * no attribute type can hold — is serialised.
78
+ */
79
+ function asAttributeValue(value) {
80
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return value;
81
+ try {
82
+ return JSON.stringify(value) ?? String(value);
83
+ } catch {
84
+ return String(value);
85
+ }
86
+ }
87
+ /** Canonical attributes for one evaluation. Absent fields are omitted. */
88
+ function featureFlagAttributes(evaluation) {
89
+ const attributes = {
90
+ [FEATURE_FLAG.KEY]: evaluation.key,
91
+ [FEATURE_FLAG.RESULT_VALUE]: asAttributeValue(evaluation.value)
92
+ };
93
+ const optional = [
94
+ [FEATURE_FLAG.RESULT_VARIANT, evaluation.variant],
95
+ [FEATURE_FLAG.RESULT_REASON, evaluation.reason === void 0 ? void 0 : evaluation.reason.toLowerCase()],
96
+ [FEATURE_FLAG.PROVIDER_NAME, evaluation.provider],
97
+ [FEATURE_FLAG.CONTEXT_ID, evaluation.contextId],
98
+ [FEATURE_FLAG.SET_ID, evaluation.setId],
99
+ [FEATURE_FLAG.VERSION, evaluation.version],
100
+ [FEATURE_FLAG.ERROR_MESSAGE, evaluation.errorMessage]
101
+ ];
102
+ for (const [key, value] of optional) if (value !== void 0) attributes[key] = value;
103
+ return attributes;
104
+ }
105
+ /**
106
+ * Record a flag evaluation on `sink` — as attributes and as a
107
+ * `feature_flag.evaluation` event.
108
+ *
109
+ * A missing sink is a no-op: instrumentation must never be the reason a branch
110
+ * throws, and a flag read outside any span is a legitimate thing to do.
111
+ */
112
+ function recordFeatureFlag(sink, evaluation) {
113
+ if (!sink) return;
114
+ const attributes = featureFlagAttributes(evaluation);
115
+ sink.setAttributes(attributes);
116
+ sink.track?.(FEATURE_FLAG_EVALUATION_EVENT, attributes);
117
+ }
118
+ /**
119
+ * Emit the evaluation as an OpenTelemetry **log record**.
120
+ *
121
+ * This is the Logs API model the repository emits events through. The obvious
122
+ * shortcut — `Span.addEvent` on the active span — puts the evaluation somewhere
123
+ * no log or event pipeline will ever look, which is exactly the failure the
124
+ * invariant exists to prevent. `correlated-events` is not a way around it
125
+ * either: that helper prefers `addEvent` when the target has one, so routing
126
+ * through it lands in the same place with more indirection.
127
+ *
128
+ * `logs.getLogger` returns a no-op logger when no `LoggerProvider` is
129
+ * registered, so this costs nothing in an application that exports no logs.
130
+ */
131
+ function emitEvaluationRecord(attributes) {
132
+ _opentelemetry_api_logs.logs.getLogger("autotel").emit({
133
+ eventName: FEATURE_FLAG_EVALUATION_EVENT,
134
+ attributes
135
+ });
136
+ }
137
+ function evaluationFrom(hookContext, value, extra) {
138
+ return {
139
+ key: hookContext.flagKey,
140
+ value,
141
+ provider: hookContext.providerMetadata?.name,
142
+ contextId: hookContext.context?.targetingKey,
143
+ setId: hookContext.clientMetadata?.name,
144
+ ...extra
145
+ };
146
+ }
147
+ /**
148
+ * An OpenFeature hook that records every evaluation under the canonical
149
+ * `feature_flag.*` convention.
150
+ *
151
+ * This is the zero-config path: OpenFeature already sits between the
152
+ * application and whichever flag vendor it uses, and it already fires on every
153
+ * evaluation — which is exactly the moment worth recording, because it is the
154
+ * moment the code branched.
155
+ *
156
+ * ```ts
157
+ * import { OpenFeature } from '@openfeature/server-sdk';
158
+ * import { autotelOpenFeatureHook } from 'autotel/feature-flags';
159
+ *
160
+ * OpenFeature.addHooks(autotelOpenFeatureHook());
161
+ * ```
162
+ *
163
+ * A failed evaluation is still recorded, with the default value the code
164
+ * actually used and `reason: 'ERROR'`. The default is what the request behaved
165
+ * as; the failure is why.
166
+ */
167
+ function autotelOpenFeatureHook(options = {}) {
168
+ const getSpan = options.getSpan ?? (() => _opentelemetry_api.trace.getActiveSpan());
169
+ const emitLogRecord = options.emitLogRecord ?? emitEvaluationRecord;
170
+ /**
171
+ * Attributes onto the span that branched, and the event wherever the sink
172
+ * says. A sink with no `track` of its own gets a log record — including when
173
+ * there is no span at all, because the evaluation still happened.
174
+ */
175
+ const record = (evaluation) => {
176
+ const sink = getSpan();
177
+ const attributes = featureFlagAttributes(evaluation);
178
+ sink?.setAttributes(attributes);
179
+ if (sink?.track) sink.track(FEATURE_FLAG_EVALUATION_EVENT, attributes);
180
+ else emitLogRecord(attributes);
181
+ };
182
+ return {
183
+ after(hookContext, details) {
184
+ record(evaluationFrom(hookContext, details.value, {
185
+ variant: details.variant,
186
+ reason: details.reason
187
+ }));
188
+ },
189
+ error(hookContext, error) {
190
+ record(evaluationFrom(hookContext, hookContext.defaultValue, {
191
+ reason: "error",
192
+ errorMessage: error instanceof Error ? error.message : String(error)
193
+ }));
194
+ }
195
+ };
196
+ }
197
+
198
+ //#endregion
199
+ exports.FEATURE_FLAG = FEATURE_FLAG;
200
+ exports.FEATURE_FLAG_EVALUATION_EVENT = FEATURE_FLAG_EVALUATION_EVENT;
201
+ exports.FEATURE_FLAG_REASON = FEATURE_FLAG_REASON;
202
+ exports.autotelOpenFeatureHook = autotelOpenFeatureHook;
203
+ exports.featureFlagAttributes = featureFlagAttributes;
204
+ exports.recordFeatureFlag = recordFeatureFlag;
@@ -0,0 +1,175 @@
1
+ //#region src/feature-flags.d.ts
2
+ /**
3
+ * Feature flag evaluations, in the vocabulary OpenTelemetry already defined.
4
+ *
5
+ * A flagged rollout splits your traffic into two populations that share a
6
+ * service name, a route and a version. Without the flag on the span there is no
7
+ * way to ask the only question that matters during a rollout — is the new
8
+ * branch slower, or failing more, than the old one — and the usual workaround
9
+ * is to read it in the flag vendor's own dashboard, where the latency and the
10
+ * errors are not.
11
+ *
12
+ * The specification covers this: `feature_flag.key`, `.result.value`,
13
+ * `.result.variant`, `.result.reason`, `.provider.name`, `.context.id`, plus a
14
+ * `feature_flag.evaluation` event. Emitting those means any backend can split
15
+ * any metric by variant, with no vendor in the path.
16
+ *
17
+ * ## Attributes and events both
18
+ *
19
+ * Span attributes hold one flag: a second call overwrites the first. A request
20
+ * that branched on three flags needs one event each, which is what the
21
+ * `feature_flag.evaluation` event is for. Both are recorded — attributes so a
22
+ * single-flag span is filterable without unpacking events, events so a
23
+ * many-flag span keeps them all.
24
+ *
25
+ * ## Record what you branched on
26
+ *
27
+ * Record a flag where the code *reads* it, not where it is fetched. A flag
28
+ * evaluated and ignored explains nothing; the value the request actually took a
29
+ * branch on is the one that explains its behaviour.
30
+ */
31
+ /** Canonical `feature_flag.*` attribute keys. */
32
+ declare const FEATURE_FLAG: {
33
+ readonly KEY: "feature_flag.key";
34
+ readonly RESULT_VALUE: "feature_flag.result.value";
35
+ readonly RESULT_VARIANT: "feature_flag.result.variant";
36
+ readonly RESULT_REASON: "feature_flag.result.reason";
37
+ readonly PROVIDER_NAME: "feature_flag.provider.name";
38
+ readonly CONTEXT_ID: "feature_flag.context.id";
39
+ readonly SET_ID: "feature_flag.set.id";
40
+ readonly VERSION: "feature_flag.version";
41
+ /**
42
+ * Why an evaluation failed. `feature_flag.evaluation.error.message` is the
43
+ * deprecated spelling of this and is deliberately not emitted — writing both
44
+ * would double the cardinality to no benefit.
45
+ */
46
+ readonly ERROR_MESSAGE: "feature_flag.error.message";
47
+ };
48
+ /** Canonical event name for one evaluation. */
49
+ declare const FEATURE_FLAG_EVALUATION_EVENT = "feature_flag.evaluation";
50
+ /**
51
+ * Canonical `feature_flag.result.reason` values. The registry defines them in
52
+ * lower snake case; OpenFeature and most SDKs report them upper-cased, and
53
+ * forwarding that splits every group-by into two buckets meaning the same
54
+ * thing.
55
+ */
56
+ declare const FEATURE_FLAG_REASON: readonly ["static", "default", "targeting_match", "split", "cached", "disabled", "unknown", "stale", "error"];
57
+ interface FeatureFlagEvaluation {
58
+ /** The flag's key, e.g. `new-checkout`. */
59
+ key: string;
60
+ /** The value the code branched on. Serialised if it is not a string. */
61
+ value: unknown;
62
+ /** Variant name, where the provider has one distinct from the value. */
63
+ variant?: string;
64
+ /**
65
+ * Why this value. Case-normalised to the registry's spelling, so a provider
66
+ * reporting `TARGETING_MATCH` and one reporting `targeting_match` land in the
67
+ * same bucket.
68
+ */
69
+ reason?: string;
70
+ /** The provider that answered, e.g. `posthog`, `launchdarkly`, `flagd`. */
71
+ provider?: string;
72
+ /** Identifier of the evaluation context — the user or account keyed on. */
73
+ contextId?: string;
74
+ /** Identifier of the flag set this flag belongs to. */
75
+ setId?: string;
76
+ /** Version of the flag definition that produced this value. */
77
+ version?: string;
78
+ /** Why the evaluation failed, when it did. */
79
+ errorMessage?: string;
80
+ }
81
+ /** What an attribute can hold without being flattened to text. */
82
+ type FeatureFlagAttributeValue = string | number | boolean;
83
+ /**
84
+ * The smallest thing that can carry an evaluation.
85
+ *
86
+ * `track` is the correlated-log seam a `TraceContext` provides, and it is the
87
+ * only event seam offered: this repository emits events through the Logs API
88
+ * model, and a `Span.addEvent` fallback is how that direction quietly becomes
89
+ * optional. A caller holding a raw span supplies its own `track` — in the
90
+ * browser, `emitEvent` from `autotel-web` is one.
91
+ *
92
+ * A sink with no `track` still records the attributes, which covers the common
93
+ * single-flag span.
94
+ */
95
+ interface FeatureFlagSink {
96
+ setAttributes(attributes: Record<string, FeatureFlagAttributeValue>): void;
97
+ track?(name: string, attributes?: Record<string, FeatureFlagAttributeValue>): void;
98
+ }
99
+ /** Canonical attributes for one evaluation. Absent fields are omitted. */
100
+ declare function featureFlagAttributes(evaluation: FeatureFlagEvaluation): Record<string, FeatureFlagAttributeValue>;
101
+ /**
102
+ * Record a flag evaluation on `sink` — as attributes and as a
103
+ * `feature_flag.evaluation` event.
104
+ *
105
+ * A missing sink is a no-op: instrumentation must never be the reason a branch
106
+ * throws, and a flag read outside any span is a legitimate thing to do.
107
+ */
108
+ declare function recordFeatureFlag(sink: FeatureFlagSink | undefined, evaluation: FeatureFlagEvaluation): void;
109
+ /**
110
+ * The parts of an OpenFeature hook context this reads. Structurally typed on
111
+ * purpose: matching the shape rather than importing `@openfeature/server-sdk`
112
+ * keeps the SDK out of every bundle that imports a sibling of this module, and
113
+ * works against the web SDK, the server SDK and the React one alike — they
114
+ * agree on this shape and disagree on almost everything else.
115
+ */
116
+ interface OpenFeatureHookContext {
117
+ flagKey: string;
118
+ defaultValue: unknown;
119
+ context?: {
120
+ targetingKey?: string;
121
+ };
122
+ providerMetadata?: {
123
+ name?: string;
124
+ };
125
+ clientMetadata?: {
126
+ name?: string;
127
+ };
128
+ }
129
+ /** The evaluation result an OpenFeature hook receives. */
130
+ interface OpenFeatureEvaluationDetails {
131
+ value: unknown;
132
+ variant?: string;
133
+ reason?: string;
134
+ }
135
+ /** Just enough of an OpenFeature hook to be registered as one. */
136
+ interface OpenFeatureHook {
137
+ after?(hookContext: OpenFeatureHookContext, details: OpenFeatureEvaluationDetails): void;
138
+ error?(hookContext: OpenFeatureHookContext, error: unknown): void;
139
+ }
140
+ interface OpenFeatureHookOptions {
141
+ /**
142
+ * Where to record. Defaults to the active span, which is what you want:
143
+ * the span that branched on the flag is the one whose latency and errors the
144
+ * flag explains.
145
+ */
146
+ getSpan?: () => FeatureFlagSink | undefined;
147
+ /**
148
+ * Where the evaluation event goes when the sink brings no `track` of its own.
149
+ * Defaults to an OpenTelemetry log record. Injected for tests.
150
+ */
151
+ emitLogRecord?: (attributes: Record<string, FeatureFlagAttributeValue>) => void;
152
+ }
153
+ /**
154
+ * An OpenFeature hook that records every evaluation under the canonical
155
+ * `feature_flag.*` convention.
156
+ *
157
+ * This is the zero-config path: OpenFeature already sits between the
158
+ * application and whichever flag vendor it uses, and it already fires on every
159
+ * evaluation — which is exactly the moment worth recording, because it is the
160
+ * moment the code branched.
161
+ *
162
+ * ```ts
163
+ * import { OpenFeature } from '@openfeature/server-sdk';
164
+ * import { autotelOpenFeatureHook } from 'autotel/feature-flags';
165
+ *
166
+ * OpenFeature.addHooks(autotelOpenFeatureHook());
167
+ * ```
168
+ *
169
+ * A failed evaluation is still recorded, with the default value the code
170
+ * actually used and `reason: 'ERROR'`. The default is what the request behaved
171
+ * as; the failure is why.
172
+ */
173
+ declare function autotelOpenFeatureHook(options?: OpenFeatureHookOptions): OpenFeatureHook;
174
+ //#endregion
175
+ export { FEATURE_FLAG, FEATURE_FLAG_EVALUATION_EVENT, FEATURE_FLAG_REASON, FeatureFlagAttributeValue, FeatureFlagEvaluation, FeatureFlagSink, OpenFeatureEvaluationDetails, OpenFeatureHook, OpenFeatureHookContext, OpenFeatureHookOptions, autotelOpenFeatureHook, featureFlagAttributes, recordFeatureFlag };
@@ -0,0 +1,175 @@
1
+ //#region src/feature-flags.d.ts
2
+ /**
3
+ * Feature flag evaluations, in the vocabulary OpenTelemetry already defined.
4
+ *
5
+ * A flagged rollout splits your traffic into two populations that share a
6
+ * service name, a route and a version. Without the flag on the span there is no
7
+ * way to ask the only question that matters during a rollout — is the new
8
+ * branch slower, or failing more, than the old one — and the usual workaround
9
+ * is to read it in the flag vendor's own dashboard, where the latency and the
10
+ * errors are not.
11
+ *
12
+ * The specification covers this: `feature_flag.key`, `.result.value`,
13
+ * `.result.variant`, `.result.reason`, `.provider.name`, `.context.id`, plus a
14
+ * `feature_flag.evaluation` event. Emitting those means any backend can split
15
+ * any metric by variant, with no vendor in the path.
16
+ *
17
+ * ## Attributes and events both
18
+ *
19
+ * Span attributes hold one flag: a second call overwrites the first. A request
20
+ * that branched on three flags needs one event each, which is what the
21
+ * `feature_flag.evaluation` event is for. Both are recorded — attributes so a
22
+ * single-flag span is filterable without unpacking events, events so a
23
+ * many-flag span keeps them all.
24
+ *
25
+ * ## Record what you branched on
26
+ *
27
+ * Record a flag where the code *reads* it, not where it is fetched. A flag
28
+ * evaluated and ignored explains nothing; the value the request actually took a
29
+ * branch on is the one that explains its behaviour.
30
+ */
31
+ /** Canonical `feature_flag.*` attribute keys. */
32
+ declare const FEATURE_FLAG: {
33
+ readonly KEY: "feature_flag.key";
34
+ readonly RESULT_VALUE: "feature_flag.result.value";
35
+ readonly RESULT_VARIANT: "feature_flag.result.variant";
36
+ readonly RESULT_REASON: "feature_flag.result.reason";
37
+ readonly PROVIDER_NAME: "feature_flag.provider.name";
38
+ readonly CONTEXT_ID: "feature_flag.context.id";
39
+ readonly SET_ID: "feature_flag.set.id";
40
+ readonly VERSION: "feature_flag.version";
41
+ /**
42
+ * Why an evaluation failed. `feature_flag.evaluation.error.message` is the
43
+ * deprecated spelling of this and is deliberately not emitted — writing both
44
+ * would double the cardinality to no benefit.
45
+ */
46
+ readonly ERROR_MESSAGE: "feature_flag.error.message";
47
+ };
48
+ /** Canonical event name for one evaluation. */
49
+ declare const FEATURE_FLAG_EVALUATION_EVENT = "feature_flag.evaluation";
50
+ /**
51
+ * Canonical `feature_flag.result.reason` values. The registry defines them in
52
+ * lower snake case; OpenFeature and most SDKs report them upper-cased, and
53
+ * forwarding that splits every group-by into two buckets meaning the same
54
+ * thing.
55
+ */
56
+ declare const FEATURE_FLAG_REASON: readonly ["static", "default", "targeting_match", "split", "cached", "disabled", "unknown", "stale", "error"];
57
+ interface FeatureFlagEvaluation {
58
+ /** The flag's key, e.g. `new-checkout`. */
59
+ key: string;
60
+ /** The value the code branched on. Serialised if it is not a string. */
61
+ value: unknown;
62
+ /** Variant name, where the provider has one distinct from the value. */
63
+ variant?: string;
64
+ /**
65
+ * Why this value. Case-normalised to the registry's spelling, so a provider
66
+ * reporting `TARGETING_MATCH` and one reporting `targeting_match` land in the
67
+ * same bucket.
68
+ */
69
+ reason?: string;
70
+ /** The provider that answered, e.g. `posthog`, `launchdarkly`, `flagd`. */
71
+ provider?: string;
72
+ /** Identifier of the evaluation context — the user or account keyed on. */
73
+ contextId?: string;
74
+ /** Identifier of the flag set this flag belongs to. */
75
+ setId?: string;
76
+ /** Version of the flag definition that produced this value. */
77
+ version?: string;
78
+ /** Why the evaluation failed, when it did. */
79
+ errorMessage?: string;
80
+ }
81
+ /** What an attribute can hold without being flattened to text. */
82
+ type FeatureFlagAttributeValue = string | number | boolean;
83
+ /**
84
+ * The smallest thing that can carry an evaluation.
85
+ *
86
+ * `track` is the correlated-log seam a `TraceContext` provides, and it is the
87
+ * only event seam offered: this repository emits events through the Logs API
88
+ * model, and a `Span.addEvent` fallback is how that direction quietly becomes
89
+ * optional. A caller holding a raw span supplies its own `track` — in the
90
+ * browser, `emitEvent` from `autotel-web` is one.
91
+ *
92
+ * A sink with no `track` still records the attributes, which covers the common
93
+ * single-flag span.
94
+ */
95
+ interface FeatureFlagSink {
96
+ setAttributes(attributes: Record<string, FeatureFlagAttributeValue>): void;
97
+ track?(name: string, attributes?: Record<string, FeatureFlagAttributeValue>): void;
98
+ }
99
+ /** Canonical attributes for one evaluation. Absent fields are omitted. */
100
+ declare function featureFlagAttributes(evaluation: FeatureFlagEvaluation): Record<string, FeatureFlagAttributeValue>;
101
+ /**
102
+ * Record a flag evaluation on `sink` — as attributes and as a
103
+ * `feature_flag.evaluation` event.
104
+ *
105
+ * A missing sink is a no-op: instrumentation must never be the reason a branch
106
+ * throws, and a flag read outside any span is a legitimate thing to do.
107
+ */
108
+ declare function recordFeatureFlag(sink: FeatureFlagSink | undefined, evaluation: FeatureFlagEvaluation): void;
109
+ /**
110
+ * The parts of an OpenFeature hook context this reads. Structurally typed on
111
+ * purpose: matching the shape rather than importing `@openfeature/server-sdk`
112
+ * keeps the SDK out of every bundle that imports a sibling of this module, and
113
+ * works against the web SDK, the server SDK and the React one alike — they
114
+ * agree on this shape and disagree on almost everything else.
115
+ */
116
+ interface OpenFeatureHookContext {
117
+ flagKey: string;
118
+ defaultValue: unknown;
119
+ context?: {
120
+ targetingKey?: string;
121
+ };
122
+ providerMetadata?: {
123
+ name?: string;
124
+ };
125
+ clientMetadata?: {
126
+ name?: string;
127
+ };
128
+ }
129
+ /** The evaluation result an OpenFeature hook receives. */
130
+ interface OpenFeatureEvaluationDetails {
131
+ value: unknown;
132
+ variant?: string;
133
+ reason?: string;
134
+ }
135
+ /** Just enough of an OpenFeature hook to be registered as one. */
136
+ interface OpenFeatureHook {
137
+ after?(hookContext: OpenFeatureHookContext, details: OpenFeatureEvaluationDetails): void;
138
+ error?(hookContext: OpenFeatureHookContext, error: unknown): void;
139
+ }
140
+ interface OpenFeatureHookOptions {
141
+ /**
142
+ * Where to record. Defaults to the active span, which is what you want:
143
+ * the span that branched on the flag is the one whose latency and errors the
144
+ * flag explains.
145
+ */
146
+ getSpan?: () => FeatureFlagSink | undefined;
147
+ /**
148
+ * Where the evaluation event goes when the sink brings no `track` of its own.
149
+ * Defaults to an OpenTelemetry log record. Injected for tests.
150
+ */
151
+ emitLogRecord?: (attributes: Record<string, FeatureFlagAttributeValue>) => void;
152
+ }
153
+ /**
154
+ * An OpenFeature hook that records every evaluation under the canonical
155
+ * `feature_flag.*` convention.
156
+ *
157
+ * This is the zero-config path: OpenFeature already sits between the
158
+ * application and whichever flag vendor it uses, and it already fires on every
159
+ * evaluation — which is exactly the moment worth recording, because it is the
160
+ * moment the code branched.
161
+ *
162
+ * ```ts
163
+ * import { OpenFeature } from '@openfeature/server-sdk';
164
+ * import { autotelOpenFeatureHook } from 'autotel/feature-flags';
165
+ *
166
+ * OpenFeature.addHooks(autotelOpenFeatureHook());
167
+ * ```
168
+ *
169
+ * A failed evaluation is still recorded, with the default value the code
170
+ * actually used and `reason: 'ERROR'`. The default is what the request behaved
171
+ * as; the failure is why.
172
+ */
173
+ declare function autotelOpenFeatureHook(options?: OpenFeatureHookOptions): OpenFeatureHook;
174
+ //#endregion
175
+ export { FEATURE_FLAG, FEATURE_FLAG_EVALUATION_EVENT, FEATURE_FLAG_REASON, FeatureFlagAttributeValue, FeatureFlagEvaluation, FeatureFlagSink, OpenFeatureEvaluationDetails, OpenFeatureHook, OpenFeatureHookContext, OpenFeatureHookOptions, autotelOpenFeatureHook, featureFlagAttributes, recordFeatureFlag };
@@ -0,0 +1,198 @@
1
+ import { trace } from "@opentelemetry/api";
2
+ import { logs } from "@opentelemetry/api-logs";
3
+
4
+ //#region src/feature-flags.ts
5
+ /**
6
+ * Feature flag evaluations, in the vocabulary OpenTelemetry already defined.
7
+ *
8
+ * A flagged rollout splits your traffic into two populations that share a
9
+ * service name, a route and a version. Without the flag on the span there is no
10
+ * way to ask the only question that matters during a rollout — is the new
11
+ * branch slower, or failing more, than the old one — and the usual workaround
12
+ * is to read it in the flag vendor's own dashboard, where the latency and the
13
+ * errors are not.
14
+ *
15
+ * The specification covers this: `feature_flag.key`, `.result.value`,
16
+ * `.result.variant`, `.result.reason`, `.provider.name`, `.context.id`, plus a
17
+ * `feature_flag.evaluation` event. Emitting those means any backend can split
18
+ * any metric by variant, with no vendor in the path.
19
+ *
20
+ * ## Attributes and events both
21
+ *
22
+ * Span attributes hold one flag: a second call overwrites the first. A request
23
+ * that branched on three flags needs one event each, which is what the
24
+ * `feature_flag.evaluation` event is for. Both are recorded — attributes so a
25
+ * single-flag span is filterable without unpacking events, events so a
26
+ * many-flag span keeps them all.
27
+ *
28
+ * ## Record what you branched on
29
+ *
30
+ * Record a flag where the code *reads* it, not where it is fetched. A flag
31
+ * evaluated and ignored explains nothing; the value the request actually took a
32
+ * branch on is the one that explains its behaviour.
33
+ */
34
+ /** Canonical `feature_flag.*` attribute keys. */
35
+ const FEATURE_FLAG = {
36
+ KEY: "feature_flag.key",
37
+ RESULT_VALUE: "feature_flag.result.value",
38
+ RESULT_VARIANT: "feature_flag.result.variant",
39
+ RESULT_REASON: "feature_flag.result.reason",
40
+ PROVIDER_NAME: "feature_flag.provider.name",
41
+ CONTEXT_ID: "feature_flag.context.id",
42
+ SET_ID: "feature_flag.set.id",
43
+ VERSION: "feature_flag.version",
44
+ /**
45
+ * Why an evaluation failed. `feature_flag.evaluation.error.message` is the
46
+ * deprecated spelling of this and is deliberately not emitted — writing both
47
+ * would double the cardinality to no benefit.
48
+ */
49
+ ERROR_MESSAGE: "feature_flag.error.message"
50
+ };
51
+ /** Canonical event name for one evaluation. */
52
+ const FEATURE_FLAG_EVALUATION_EVENT = "feature_flag.evaluation";
53
+ /**
54
+ * Canonical `feature_flag.result.reason` values. The registry defines them in
55
+ * lower snake case; OpenFeature and most SDKs report them upper-cased, and
56
+ * forwarding that splits every group-by into two buckets meaning the same
57
+ * thing.
58
+ */
59
+ const FEATURE_FLAG_REASON = [
60
+ "static",
61
+ "default",
62
+ "targeting_match",
63
+ "split",
64
+ "cached",
65
+ "disabled",
66
+ "unknown",
67
+ "stale",
68
+ "error"
69
+ ];
70
+ /**
71
+ * A flag value as an attribute.
72
+ *
73
+ * Booleans and numbers are kept as they are: `feature_flag.result.value`
74
+ * permits typed values, and stringifying `true` makes a boolean flag
75
+ * uncomparable with the numeric one next to it. Only a structured value — which
76
+ * no attribute type can hold — is serialised.
77
+ */
78
+ function asAttributeValue(value) {
79
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return value;
80
+ try {
81
+ return JSON.stringify(value) ?? String(value);
82
+ } catch {
83
+ return String(value);
84
+ }
85
+ }
86
+ /** Canonical attributes for one evaluation. Absent fields are omitted. */
87
+ function featureFlagAttributes(evaluation) {
88
+ const attributes = {
89
+ [FEATURE_FLAG.KEY]: evaluation.key,
90
+ [FEATURE_FLAG.RESULT_VALUE]: asAttributeValue(evaluation.value)
91
+ };
92
+ const optional = [
93
+ [FEATURE_FLAG.RESULT_VARIANT, evaluation.variant],
94
+ [FEATURE_FLAG.RESULT_REASON, evaluation.reason === void 0 ? void 0 : evaluation.reason.toLowerCase()],
95
+ [FEATURE_FLAG.PROVIDER_NAME, evaluation.provider],
96
+ [FEATURE_FLAG.CONTEXT_ID, evaluation.contextId],
97
+ [FEATURE_FLAG.SET_ID, evaluation.setId],
98
+ [FEATURE_FLAG.VERSION, evaluation.version],
99
+ [FEATURE_FLAG.ERROR_MESSAGE, evaluation.errorMessage]
100
+ ];
101
+ for (const [key, value] of optional) if (value !== void 0) attributes[key] = value;
102
+ return attributes;
103
+ }
104
+ /**
105
+ * Record a flag evaluation on `sink` — as attributes and as a
106
+ * `feature_flag.evaluation` event.
107
+ *
108
+ * A missing sink is a no-op: instrumentation must never be the reason a branch
109
+ * throws, and a flag read outside any span is a legitimate thing to do.
110
+ */
111
+ function recordFeatureFlag(sink, evaluation) {
112
+ if (!sink) return;
113
+ const attributes = featureFlagAttributes(evaluation);
114
+ sink.setAttributes(attributes);
115
+ sink.track?.(FEATURE_FLAG_EVALUATION_EVENT, attributes);
116
+ }
117
+ /**
118
+ * Emit the evaluation as an OpenTelemetry **log record**.
119
+ *
120
+ * This is the Logs API model the repository emits events through. The obvious
121
+ * shortcut — `Span.addEvent` on the active span — puts the evaluation somewhere
122
+ * no log or event pipeline will ever look, which is exactly the failure the
123
+ * invariant exists to prevent. `correlated-events` is not a way around it
124
+ * either: that helper prefers `addEvent` when the target has one, so routing
125
+ * through it lands in the same place with more indirection.
126
+ *
127
+ * `logs.getLogger` returns a no-op logger when no `LoggerProvider` is
128
+ * registered, so this costs nothing in an application that exports no logs.
129
+ */
130
+ function emitEvaluationRecord(attributes) {
131
+ logs.getLogger("autotel").emit({
132
+ eventName: FEATURE_FLAG_EVALUATION_EVENT,
133
+ attributes
134
+ });
135
+ }
136
+ function evaluationFrom(hookContext, value, extra) {
137
+ return {
138
+ key: hookContext.flagKey,
139
+ value,
140
+ provider: hookContext.providerMetadata?.name,
141
+ contextId: hookContext.context?.targetingKey,
142
+ setId: hookContext.clientMetadata?.name,
143
+ ...extra
144
+ };
145
+ }
146
+ /**
147
+ * An OpenFeature hook that records every evaluation under the canonical
148
+ * `feature_flag.*` convention.
149
+ *
150
+ * This is the zero-config path: OpenFeature already sits between the
151
+ * application and whichever flag vendor it uses, and it already fires on every
152
+ * evaluation — which is exactly the moment worth recording, because it is the
153
+ * moment the code branched.
154
+ *
155
+ * ```ts
156
+ * import { OpenFeature } from '@openfeature/server-sdk';
157
+ * import { autotelOpenFeatureHook } from 'autotel/feature-flags';
158
+ *
159
+ * OpenFeature.addHooks(autotelOpenFeatureHook());
160
+ * ```
161
+ *
162
+ * A failed evaluation is still recorded, with the default value the code
163
+ * actually used and `reason: 'ERROR'`. The default is what the request behaved
164
+ * as; the failure is why.
165
+ */
166
+ function autotelOpenFeatureHook(options = {}) {
167
+ const getSpan = options.getSpan ?? (() => trace.getActiveSpan());
168
+ const emitLogRecord = options.emitLogRecord ?? emitEvaluationRecord;
169
+ /**
170
+ * Attributes onto the span that branched, and the event wherever the sink
171
+ * says. A sink with no `track` of its own gets a log record — including when
172
+ * there is no span at all, because the evaluation still happened.
173
+ */
174
+ const record = (evaluation) => {
175
+ const sink = getSpan();
176
+ const attributes = featureFlagAttributes(evaluation);
177
+ sink?.setAttributes(attributes);
178
+ if (sink?.track) sink.track(FEATURE_FLAG_EVALUATION_EVENT, attributes);
179
+ else emitLogRecord(attributes);
180
+ };
181
+ return {
182
+ after(hookContext, details) {
183
+ record(evaluationFrom(hookContext, details.value, {
184
+ variant: details.variant,
185
+ reason: details.reason
186
+ }));
187
+ },
188
+ error(hookContext, error) {
189
+ record(evaluationFrom(hookContext, hookContext.defaultValue, {
190
+ reason: "error",
191
+ errorMessage: error instanceof Error ? error.message : String(error)
192
+ }));
193
+ }
194
+ };
195
+ }
196
+
197
+ //#endregion
198
+ export { FEATURE_FLAG, FEATURE_FLAG_EVALUATION_EVENT, FEATURE_FLAG_REASON, autotelOpenFeatureHook, featureFlagAttributes, recordFeatureFlag };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Write Once, Observe Anywhere",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -258,6 +258,11 @@
258
258
  "import": "./dist/drain-pipeline.js",
259
259
  "require": "./dist/drain-pipeline.cjs"
260
260
  },
261
+ "./feature-flags": {
262
+ "types": "./dist/feature-flags.d.ts",
263
+ "import": "./dist/feature-flags.js",
264
+ "require": "./dist/feature-flags.cjs"
265
+ },
261
266
  "./evidence": {
262
267
  "types": "./dist/evidence.d.ts",
263
268
  "import": "./dist/evidence.js",