autotel 7.2.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.
Files changed (46) hide show
  1. package/README.md +16 -14
  2. package/dist/auto.cjs +1 -1
  3. package/dist/auto.js +1 -1
  4. package/dist/correlation-id.cjs +1 -1
  5. package/dist/correlation-id.js +1 -1
  6. package/dist/decorators.cjs +1 -1
  7. package/dist/decorators.js +1 -1
  8. package/dist/event.cjs +1 -1
  9. package/dist/event.js +1 -1
  10. package/dist/feature-flags.cjs +204 -0
  11. package/dist/feature-flags.d.cts +175 -0
  12. package/dist/feature-flags.d.ts +175 -0
  13. package/dist/feature-flags.js +198 -0
  14. package/dist/{functional-Doi0hFm3.js → functional-C4js2vTM.js} +5 -2
  15. package/dist/{functional-C925G95W.cjs → functional-DHlD7V0x.cjs} +5 -2
  16. package/dist/functional.cjs +1 -1
  17. package/dist/functional.js +1 -1
  18. package/dist/http.cjs +1 -1
  19. package/dist/http.js +1 -1
  20. package/dist/index.cjs +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/{init-DzAdgcFM.js → init-CsKK9vZO.js} +22 -2
  23. package/dist/{init-aTZKsAwy.cjs → init-DmM7dprX.cjs} +27 -1
  24. package/dist/instrumentation.cjs +1 -1
  25. package/dist/instrumentation.js +1 -1
  26. package/dist/messaging.cjs +2 -2
  27. package/dist/messaging.js +2 -2
  28. package/dist/sampling.cjs +1 -1
  29. package/dist/sampling.js +1 -1
  30. package/dist/semantic-helpers.cjs +1 -1
  31. package/dist/semantic-helpers.js +1 -1
  32. package/dist/tail-sampling-processor.cjs +1 -1
  33. package/dist/tail-sampling-processor.js +1 -1
  34. package/dist/testing.cjs +32 -0
  35. package/dist/testing.d.cts +26 -1
  36. package/dist/testing.d.ts +26 -1
  37. package/dist/testing.js +32 -1
  38. package/dist/webhook.cjs +1 -1
  39. package/dist/webhook.js +1 -1
  40. package/dist/workflow-distributed.cjs +1 -1
  41. package/dist/workflow-distributed.js +1 -1
  42. package/dist/workflow.cjs +1 -1
  43. package/dist/workflow.js +1 -1
  44. package/dist/yaml-config.cjs +1 -1
  45. package/dist/yaml-config.js +1 -1
  46. package/package.json +6 -1
package/README.md CHANGED
@@ -143,20 +143,20 @@ Replace `NODE_OPTIONS` and 30+ lines of SDK boilerplate with `init()`, wrap func
143
143
 
144
144
  `autotel` has ~35 subpath exports. For most scenarios you only need one or two. Cloudflare Workers and other edge runtimes should use [`autotel-cloudflare`](../autotel-cloudflare) or [`autotel-edge`](../autotel-edge), not `autotel` (which expects Node).
145
145
 
146
- | Scenario | Package | Import |
147
- | -------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------- |
148
- | Node service: init + spans | `autotel` | `import { init, trace, span, withTracing, span } from 'autotel'` |
149
- | Node service: pino-style logger with trace context | `autotel` | `import { createLogger } from 'autotel/logger'` |
150
- | Node service: class decorators | `autotel` | `import { Trace, Span } from 'autotel/decorators'` |
151
- | Node service: Drizzle ORM spans | `autotel-drizzle` | `import { instrumentDrizzleClient } from 'autotel-drizzle'` |
152
- | Node service: testing assertions | `autotel` | `import { createTraceCollector } from 'autotel/testing'` |
153
- | Cloudflare Worker: fetch with spans | `autotel-cloudflare` | `import { wrapModule, trace, span, withTracing } from 'autotel-cloudflare'` |
154
- | Cloudflare Worker: logs only, no `nodejs_compat` | `autotel-cloudflare` | `import { createEdgeLogger } from 'autotel-cloudflare/logger'` |
155
- | Cloudflare Worker: queue consumer | `autotel-cloudflare` | `import { wrapModule, getQueueLogger } from 'autotel-cloudflare'` |
156
- | Cloudflare Worker: Durable Object | `autotel-cloudflare` | `import { wrapDurableObject } from 'autotel-cloudflare'` |
157
- | Hono on Workers | `autotel-cloudflare` + `autotel-hono` | `wrapModule` from `autotel-cloudflare`, middleware from `autotel-hono` |
158
- | Vercel Edge / Netlify Edge / Deno Deploy | `autotel-edge` | `import { init, trace, span, withTracing } from 'autotel-edge'` |
159
- | Edge runtime: logs only | `autotel-edge` | `import { createEdgeLogger } from 'autotel-edge/logger'` |
146
+ | Scenario | Package | Import |
147
+ | -------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------ |
148
+ | Node service: init + spans | `autotel` | `import { init, trace, span, withTracing, span } from 'autotel'` |
149
+ | Node service: pino-style logger with trace context | `autotel` | `import { createLogger } from 'autotel/logger'` |
150
+ | Node service: class decorators | `autotel` | `import { Trace, Span } from 'autotel/decorators'` |
151
+ | Node service: Drizzle ORM spans | `autotel-drizzle` | `import { instrumentDrizzleClient } from 'autotel-drizzle'` |
152
+ | Node service: testing assertions | `autotel` | `import { createTraceCollector, createMemoryExporter } from 'autotel/testing'` |
153
+ | Cloudflare Worker: fetch with spans | `autotel-cloudflare` | `import { wrapModule, trace, span, withTracing } from 'autotel-cloudflare'` |
154
+ | Cloudflare Worker: logs only, no `nodejs_compat` | `autotel-cloudflare` | `import { createEdgeLogger } from 'autotel-cloudflare/logger'` |
155
+ | Cloudflare Worker: queue consumer | `autotel-cloudflare` | `import { wrapModule, getQueueLogger } from 'autotel-cloudflare'` |
156
+ | Cloudflare Worker: Durable Object | `autotel-cloudflare` | `import { wrapDurableObject } from 'autotel-cloudflare'` |
157
+ | Hono on Workers | `autotel-cloudflare` + `autotel-hono` | `wrapModule` from `autotel-cloudflare`, middleware from `autotel-hono` |
158
+ | Vercel Edge / Netlify Edge / Deno Deploy | `autotel-edge` | `import { init, trace, span, withTracing } from 'autotel-edge'` |
159
+ | Edge runtime: logs only | `autotel-edge` | `import { createEdgeLogger } from 'autotel-edge/logger'` |
160
160
 
161
161
  ## Quick Start
162
162
 
@@ -434,6 +434,8 @@ calling a model, this is why.
434
434
 
435
435
  Autotel defaults to production-ready adaptive sampling: a 10% baseline, with errors and slow requests kept automatically.
436
436
 
437
+ A sampler configured here also governs the functions you wrap. `trace()`, `withTracing()` and `instrument()` resolve their sampler per call as `options.sampler ?? init()'s sampler ?? keep everything`, so `sampling: 'production'` applies to wrapped spans and not only to the SDK. Resolution is per call because a wrapper is usually created at module load, before `init()` has run. A sampler passed at the wrapper still wins.
438
+
437
439
  ### Preset Shorthand
438
440
 
439
441
  Use the `sampling` field on `init()` when you want the shortest path:
package/dist/auto.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
2
- const require_init = require('./init-aTZKsAwy.cjs');
2
+ const require_init = require('./init-DmM7dprX.cjs');
3
3
  let node_module = require("node:module");
4
4
  node_module = require_rolldown_runtime.__toESM(node_module, 1);
5
5
  let import_in_the_middle = require("import-in-the-middle");
package/dist/auto.js CHANGED
@@ -1,4 +1,4 @@
1
- import { g as loadYamlConfig, i as init } from "./init-DzAdgcFM.js";
1
+ import { _ as loadYamlConfig, a as init } from "./init-CsKK9vZO.js";
2
2
  import * as nodeModule from "node:module";
3
3
  import { createAddHookMessageChannel } from "import-in-the-middle";
4
4
 
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_init = require('./init-aTZKsAwy.cjs');
2
+ const require_init = require('./init-DmM7dprX.cjs');
3
3
 
4
4
  exports.CORRELATION_ID_BAGGAGE_KEY = require_init.CORRELATION_ID_BAGGAGE_KEY;
5
5
  exports.generateCorrelationId = require_init.generateCorrelationId;
@@ -1,3 +1,3 @@
1
- import { $ as getOrCreateCorrelationId, Q as getCorrelationStorage, X as generateCorrelationId, Y as CORRELATION_ID_BAGGAGE_KEY, Z as getCorrelationId, et as runWithCorrelationId, nt as setCorrelationIdInBaggage, tt as setCorrelationId } from "./init-DzAdgcFM.js";
1
+ import { $ as getCorrelationStorage, Q as getCorrelationId, X as CORRELATION_ID_BAGGAGE_KEY, Z as generateCorrelationId, et as getOrCreateCorrelationId, nt as setCorrelationId, rt as setCorrelationIdInBaggage, tt as runWithCorrelationId } from "./init-CsKK9vZO.js";
2
2
 
3
3
  export { CORRELATION_ID_BAGGAGE_KEY, generateCorrelationId, getCorrelationId, getCorrelationStorage, getOrCreateCorrelationId, runWithCorrelationId, setCorrelationId, setCorrelationIdInBaggage };
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_values = require('./values-DApyq53y.cjs');
3
3
  const require_config = require('./config.cjs');
4
- const require_init = require('./init-aTZKsAwy.cjs');
4
+ const require_init = require('./init-DmM7dprX.cjs');
5
5
  let _opentelemetry_api = require("@opentelemetry/api");
6
6
 
7
7
  //#region src/decorators.ts
@@ -1,6 +1,6 @@
1
1
  import { y as splitNameOrOptions } from "./values-BenyvWKz.js";
2
2
  import { getConfig } from "./config.js";
3
- import { H as createTraceContext } from "./init-DzAdgcFM.js";
3
+ import { U as createTraceContext } from "./init-CsKK9vZO.js";
4
4
  import { SpanStatusCode } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/decorators.ts
package/dist/event.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_init = require('./init-aTZKsAwy.cjs');
2
+ const require_init = require('./init-DmM7dprX.cjs');
3
3
 
4
4
  exports.Event = require_init.Event;
5
5
  exports.getEvents = require_init.getEvents;
package/dist/event.js CHANGED
@@ -1,3 +1,3 @@
1
- import { d as getEvents, f as resetEvents, u as Event } from "./init-DzAdgcFM.js";
1
+ import { d as Event, f as getEvents, p as resetEvents } from "./init-CsKK9vZO.js";
2
2
 
3
3
  export { Event, getEvents, resetEvents };
@@ -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 };