autotel 7.3.0 → 7.5.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.
- package/README.md +17 -0
- package/dist/auto.cjs +1 -1
- package/dist/auto.js +1 -1
- package/dist/{canonical-log-line-processor-DolzdawH.cjs → canonical-log-line-processor-B-tzezhk.cjs} +24 -10
- package/dist/{canonical-log-line-processor-l0Aqe6yp.d.cts → canonical-log-line-processor-C0Z_N4oL.d.cts} +17 -3
- package/dist/{canonical-log-line-processor-hFUmS__K.js → canonical-log-line-processor-CBv-OCp4.js} +24 -10
- package/dist/{canonical-log-line-processor-cPB4Axk-.d.ts → canonical-log-line-processor-Yq4cXcD5.d.ts} +17 -3
- package/dist/correlation-id.cjs +1 -1
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +1 -1
- package/dist/decorators.js +1 -1
- package/dist/event.cjs +1 -1
- package/dist/event.js +1 -1
- package/dist/feature-flags.cjs +204 -0
- package/dist/feature-flags.d.cts +175 -0
- package/dist/feature-flags.d.ts +175 -0
- package/dist/feature-flags.js +198 -0
- package/dist/{functional-C4js2vTM.js → functional-B86zszmZ.js} +1 -1
- package/dist/{functional-DHlD7V0x.cjs → functional-BOZ3me6w.cjs} +1 -1
- package/dist/functional.cjs +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/{init-CsKK9vZO.js → init-B2kv4bLO.js} +31 -12
- package/dist/{init-8GSy2bza.d.cts → init-BU7qa-Ii.d.cts} +67 -6
- package/dist/{init-DmM7dprX.cjs → init-CNUkdwke.cjs} +31 -12
- package/dist/{init-UnKrXhpq.d.ts → init-DDCgmfa4.d.ts} +67 -6
- package/dist/instrumentation.cjs +1 -1
- package/dist/instrumentation.js +1 -1
- package/dist/messaging.cjs +2 -2
- package/dist/messaging.js +2 -2
- package/dist/processors.cjs +1 -1
- package/dist/processors.d.cts +1 -1
- package/dist/processors.d.ts +1 -1
- package/dist/processors.js +1 -1
- package/dist/sampling.cjs +1 -1
- package/dist/sampling.js +1 -1
- package/dist/semantic-helpers.cjs +1 -1
- package/dist/semantic-helpers.js +1 -1
- package/dist/tail-sampling-processor.cjs +1 -1
- package/dist/tail-sampling-processor.js +1 -1
- package/dist/webhook.cjs +1 -1
- package/dist/webhook.js +1 -1
- package/dist/workflow-distributed.cjs +1 -1
- package/dist/workflow-distributed.js +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.js +1 -1
- package/dist/yaml-config.cjs +1 -1
- package/dist/yaml-config.d.cts +1 -1
- package/dist/yaml-config.d.ts +1 -1
- package/dist/yaml-config.js +1 -1
- package/package.json +6 -1
|
@@ -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 };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { l as asString, m as isDevelopment } from "./values-BenyvWKz.js";
|
|
2
2
|
import { getForceFlushableProvider } from "./tracer-provider.js";
|
|
3
3
|
import { getConfig } from "./config.js";
|
|
4
|
-
import { C as AUTOTEL_SAMPLING_RATE, D as AlwaysSampler, G as getActiveContextWithBaggage, I as debugCaptureRequested, J as getEventQueue, K as getContextStorage, T as AUTOTEL_SAMPLING_TAIL_KEEP, U as createTraceContext, h as runInOperationContext, i as getSdk, n as getConfiguredSampler, q as hasExplicitSpanStatus, t as getConfig$1, w as AUTOTEL_SAMPLING_TAIL_EVALUATED, z as isForceKept } from "./init-
|
|
4
|
+
import { C as AUTOTEL_SAMPLING_RATE, D as AlwaysSampler, G as getActiveContextWithBaggage, I as debugCaptureRequested, J as getEventQueue, K as getContextStorage, T as AUTOTEL_SAMPLING_TAIL_KEEP, U as createTraceContext, h as runInOperationContext, i as getSdk, n as getConfiguredSampler, q as hasExplicitSpanStatus, t as getConfig$1, w as AUTOTEL_SAMPLING_TAIL_EVALUATED, z as isForceKept } from "./init-B2kv4bLO.js";
|
|
5
5
|
import { evidenceAttribute } from "./evidence.js";
|
|
6
6
|
import { setSpanName } from "./trace-helpers.js";
|
|
7
7
|
import { SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
@@ -2,7 +2,7 @@ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
|
|
|
2
2
|
const require_values = require('./values-DApyq53y.cjs');
|
|
3
3
|
const require_tracer_provider = require('./tracer-provider.cjs');
|
|
4
4
|
const require_config = require('./config.cjs');
|
|
5
|
-
const require_init = require('./init-
|
|
5
|
+
const require_init = require('./init-CNUkdwke.cjs');
|
|
6
6
|
const require_evidence = require('./evidence.cjs');
|
|
7
7
|
const require_trace_helpers = require('./trace-helpers.cjs');
|
|
8
8
|
let _opentelemetry_api = require("@opentelemetry/api");
|
package/dist/functional.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_functional = require('./functional-
|
|
2
|
+
const require_functional = require('./functional-BOZ3me6w.cjs');
|
|
3
3
|
|
|
4
4
|
exports.ctx = require_functional.ctx;
|
|
5
5
|
exports.getActiveTraceContext = require_functional.getActiveTraceContext;
|
package/dist/functional.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as trace, c as withTracing, i as span, n as getActiveTraceContext, o as withBaggage, r as instrument, s as withNewContext, t as ctx } from "./functional-
|
|
1
|
+
import { a as trace, c as withTracing, i as span, n as getActiveTraceContext, o as withBaggage, r as instrument, s as withNewContext, t as ctx } from "./functional-B86zszmZ.js";
|
|
2
2
|
|
|
3
3
|
export { ctx, getActiveTraceContext, instrument, span, trace, withBaggage, withNewContext, withTracing };
|
package/dist/http.cjs
CHANGED
|
@@ -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-
|
|
4
|
+
const require_init = require('./init-CNUkdwke.cjs');
|
|
5
5
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
6
6
|
|
|
7
7
|
//#region src/http.ts
|
package/dist/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as readProperty, h as isFunction, i as asNumber, l as asString } from "./values-BenyvWKz.js";
|
|
2
2
|
import { getConfig } from "./config.js";
|
|
3
|
-
import { G as getActiveContextWithBaggage } from "./init-
|
|
3
|
+
import { G as getActiveContextWithBaggage } from "./init-B2kv4bLO.js";
|
|
4
4
|
import { SpanStatusCode, context, propagation } from "@opentelemetry/api";
|
|
5
5
|
|
|
6
6
|
//#region src/http.ts
|
package/dist/index.cjs
CHANGED
|
@@ -3,13 +3,13 @@ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
|
|
|
3
3
|
const require_values = require('./values-DApyq53y.cjs');
|
|
4
4
|
const require_tracer_provider = require('./tracer-provider.cjs');
|
|
5
5
|
const require_structured_error = require('./structured-error--_wXpd_C.cjs');
|
|
6
|
-
const require_init = require('./init-
|
|
6
|
+
const require_init = require('./init-CNUkdwke.cjs');
|
|
7
7
|
const require_filtering_span_processor = require('./filtering-span-processor.cjs');
|
|
8
8
|
const require_span_name_normalizer = require('./span-name-normalizer.cjs');
|
|
9
9
|
const require_attribute_redacting_processor = require('./attribute-redacting-processor.cjs');
|
|
10
|
-
const require_canonical_log_line_processor = require('./canonical-log-line-processor-
|
|
10
|
+
const require_canonical_log_line_processor = require('./canonical-log-line-processor-B-tzezhk.cjs');
|
|
11
11
|
const require_metric = require('./metric.cjs');
|
|
12
|
-
const require_functional = require('./functional-
|
|
12
|
+
const require_functional = require('./functional-BOZ3me6w.cjs');
|
|
13
13
|
const require_trace_helpers = require('./trace-helpers.cjs');
|
|
14
14
|
const require_stable_hash = require('./stable-hash-C0pOegH_.cjs');
|
|
15
15
|
const require_correlated_events = require('./correlated-events-CcuknWnU.cjs');
|
package/dist/index.d.cts
CHANGED
|
@@ -10,9 +10,9 @@ import { DrainPipelineOptions, PipelineDrainFn, createDrainPipeline } from "./dr
|
|
|
10
10
|
import { a as EventSchemaMetadata, c as FunnelStatus, l as OutcomeStatus, n as EventAttributeValue, o as EventSubscriber, r as EventAttributes, s as EventTrackingOptions, t as AutotelEventContext } from "./event-subscriber-DMACQA-1.cjs";
|
|
11
11
|
import { Event, EventsOptions, getEvents, resetEvents } from "./event.cjs";
|
|
12
12
|
import { n as FilteringSpanProcessorOptions, r as SpanFilterPredicate, t as FilteringSpanProcessor } from "./filtering-span-processor-BpDagWNa.cjs";
|
|
13
|
-
import { a as AutotelConfig, c as StringRedactor, i as lockLogger, l as createStringRedactor, n as isInitialized, r as isLoggerLocked, s as ProcessHandlersConfig, t as init } from "./init-
|
|
13
|
+
import { a as AutotelConfig, c as StringRedactor, i as lockLogger, l as createStringRedactor, n as isInitialized, r as isLoggerLocked, s as ProcessHandlersConfig, t as init } from "./init-BU7qa-Ii.cjs";
|
|
14
14
|
import { a as SpanNameNormalizerPreset, i as SpanNameNormalizerFn, n as NORMALIZER_PRESETS, o as SpanNameNormalizingProcessor, r as SpanNameNormalizerConfig, s as SpanNameNormalizingProcessorOptions, t as NORMALIZER_PATTERNS } from "./span-name-normalizer-CN1-8C76.cjs";
|
|
15
|
-
import "./canonical-log-line-processor-
|
|
15
|
+
import "./canonical-log-line-processor-C0Z_N4oL.cjs";
|
|
16
16
|
import { i as defineEvent, n as DefinedEvent, r as SchemaLike, t as DefineEventOptions } from "./define-event-BNeuKUPv.cjs";
|
|
17
17
|
import { ParsedError, parseError } from "./parse-error.cjs";
|
|
18
18
|
import { Metric, MetricsOptions, getMetrics, resetMetrics } from "./metric.cjs";
|
package/dist/index.d.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { DrainPipelineOptions, PipelineDrainFn, createDrainPipeline } from "./dr
|
|
|
10
10
|
import { a as EventSchemaMetadata, c as FunnelStatus, l as OutcomeStatus, n as EventAttributeValue, o as EventSubscriber, r as EventAttributes, s as EventTrackingOptions, t as AutotelEventContext } from "./event-subscriber-DMACQA-1.js";
|
|
11
11
|
import { Event, EventsOptions, getEvents, resetEvents } from "./event.js";
|
|
12
12
|
import { n as FilteringSpanProcessorOptions, r as SpanFilterPredicate, t as FilteringSpanProcessor } from "./filtering-span-processor-BpDagWNa.js";
|
|
13
|
-
import { a as AutotelConfig, c as StringRedactor, i as lockLogger, l as createStringRedactor, n as isInitialized, r as isLoggerLocked, s as ProcessHandlersConfig, t as init } from "./init-
|
|
13
|
+
import { a as AutotelConfig, c as StringRedactor, i as lockLogger, l as createStringRedactor, n as isInitialized, r as isLoggerLocked, s as ProcessHandlersConfig, t as init } from "./init-DDCgmfa4.js";
|
|
14
14
|
import { a as SpanNameNormalizerPreset, i as SpanNameNormalizerFn, n as NORMALIZER_PRESETS, o as SpanNameNormalizingProcessor, r as SpanNameNormalizerConfig, s as SpanNameNormalizingProcessorOptions, t as NORMALIZER_PATTERNS } from "./span-name-normalizer-CN1-8C76.js";
|
|
15
|
-
import "./canonical-log-line-processor-
|
|
15
|
+
import "./canonical-log-line-processor-Yq4cXcD5.js";
|
|
16
16
|
import { i as defineEvent, n as DefinedEvent, r as SchemaLike, t as DefineEventOptions } from "./define-event-Bd8f8mST.js";
|
|
17
17
|
import { ParsedError, parseError } from "./parse-error.js";
|
|
18
18
|
import { Metric, MetricsOptions, getMetrics, resetMetrics } from "./metric.js";
|