@decocms/start 2.28.1 → 2.29.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,183 @@
1
+ /**
2
+ * URL-based head sampler — port of `deco-cx/deco/observability/otel/samplers/urlBased.ts`.
3
+ *
4
+ * Lets ops dial sampling rates per URL pattern without redeploying. Reads
5
+ * `OTEL_SAMPLING_CONFIG` (base64-encoded JSON) at boot and decides each
6
+ * trace's sample rate based on the matching pattern.
7
+ *
8
+ * Wrapped in `ParentBasedSampler` so a span inherits its parent's sampling
9
+ * decision when one exists (i.e. distributed traces are kept consistent end
10
+ * to end).
11
+ *
12
+ * @example
13
+ * ```jsonc
14
+ * // base64-encode this and set as OTEL_SAMPLING_CONFIG:
15
+ * {
16
+ * "default": 0.05,
17
+ * "rules": [
18
+ * { "pattern": "^/checkout", "ratio": 1.0 },
19
+ * { "pattern": "^/api/health", "ratio": 0.0 },
20
+ * { "pattern": "/p$", "ratio": 0.1 }
21
+ * ]
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ import { type Attributes, type Context, type Link, type SpanKind, trace } from "@opentelemetry/api";
27
+ import {
28
+ AlwaysOffSampler,
29
+ AlwaysOnSampler,
30
+ ParentBasedSampler,
31
+ type Sampler,
32
+ type SamplingResult,
33
+ TraceIdRatioBasedSampler,
34
+ } from "@opentelemetry/sdk-trace-base";
35
+
36
+ export interface SamplingRule {
37
+ /** ECMA RegExp pattern matched against the URL path. */
38
+ pattern: string;
39
+ /** Ratio in [0, 1]. */
40
+ ratio: number;
41
+ }
42
+
43
+ export interface SamplingConfig {
44
+ /** Default sample ratio applied when no rule matches. Defaults to 1.0 (always sample). */
45
+ default?: number;
46
+ /** Ordered list of rules. First match wins. */
47
+ rules?: SamplingRule[];
48
+ }
49
+
50
+ interface CompiledRule {
51
+ re: RegExp;
52
+ sampler: Sampler;
53
+ }
54
+
55
+ /**
56
+ * URL-pattern-driven head sampler. Implements the OTel `Sampler` interface
57
+ * directly so it can be plugged into `ParentBasedSampler`'s `root` slot.
58
+ */
59
+ export class URLBasedSampler implements Sampler {
60
+ private readonly defaultSampler: Sampler;
61
+ private readonly rules: CompiledRule[];
62
+
63
+ constructor(config: SamplingConfig = {}) {
64
+ this.defaultSampler = ratioToSampler(config.default ?? 1.0);
65
+ this.rules = (config.rules ?? []).map((rule) => ({
66
+ re: new RegExp(rule.pattern),
67
+ sampler: ratioToSampler(rule.ratio),
68
+ }));
69
+ }
70
+
71
+ shouldSample(
72
+ context: Context,
73
+ traceId: string,
74
+ spanName: string,
75
+ spanKind: SpanKind,
76
+ attributes: Attributes,
77
+ links: Link[],
78
+ ): SamplingResult {
79
+ const path = extractPath(attributes);
80
+ if (path) {
81
+ for (const rule of this.rules) {
82
+ if (rule.re.test(path)) {
83
+ return rule.sampler.shouldSample(context, traceId, spanName, spanKind, attributes, links);
84
+ }
85
+ }
86
+ }
87
+ return this.defaultSampler.shouldSample(
88
+ context,
89
+ traceId,
90
+ spanName,
91
+ spanKind,
92
+ attributes,
93
+ links,
94
+ );
95
+ }
96
+
97
+ toString(): string {
98
+ return `URLBasedSampler(${this.rules.length} rules)`;
99
+ }
100
+ }
101
+
102
+ function ratioToSampler(ratio: number): Sampler {
103
+ if (ratio >= 1) return new AlwaysOnSampler();
104
+ if (ratio <= 0) return new AlwaysOffSampler();
105
+ return new TraceIdRatioBasedSampler(ratio);
106
+ }
107
+
108
+ function extractPath(attrs: Attributes): string | null {
109
+ // Prefer the OTel-standard `url.path` (semconv >= 1.21), fall back to
110
+ // legacy `http.target` and `http.url`.
111
+ const direct = attrs["url.path"] ?? attrs["http.target"];
112
+ if (typeof direct === "string") return direct;
113
+
114
+ const httpUrl = attrs["http.url"];
115
+ if (typeof httpUrl === "string") {
116
+ try {
117
+ return new URL(httpUrl).pathname;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Boot helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Decode a base64-encoded `OTEL_SAMPLING_CONFIG` value into a `SamplingConfig`.
131
+ * Returns `null` (caller falls back to default ratio 1.0) on:
132
+ * - missing / empty input
133
+ * - invalid base64
134
+ * - JSON parse failure
135
+ * - schema-mismatched payload
136
+ *
137
+ * Logs a warning to console when the env var is set but unparseable so the
138
+ * mistake is visible in CF Logs without crashing the worker boot.
139
+ */
140
+ export function decodeSamplingConfig(raw: string | undefined): SamplingConfig | null {
141
+ if (!raw) return null;
142
+ try {
143
+ const json = atob(raw);
144
+ const parsed = JSON.parse(json) as unknown;
145
+ if (!parsed || typeof parsed !== "object") return null;
146
+ const obj = parsed as { default?: unknown; rules?: unknown };
147
+
148
+ const defaultRatio = typeof obj.default === "number" ? obj.default : undefined;
149
+ const rawRules = Array.isArray(obj.rules) ? obj.rules : [];
150
+ const rules: SamplingRule[] = [];
151
+ for (const r of rawRules) {
152
+ if (!r || typeof r !== "object") continue;
153
+ const rec = r as { pattern?: unknown; ratio?: unknown };
154
+ if (typeof rec.pattern !== "string" || typeof rec.ratio !== "number") continue;
155
+ try {
156
+ // Eagerly validate the regex so a bad pattern fails at boot, not
157
+ // on the first matching request.
158
+ new RegExp(rec.pattern);
159
+ rules.push({ pattern: rec.pattern, ratio: rec.ratio });
160
+ } catch {
161
+ console.warn(`[sampler] dropping invalid pattern: ${rec.pattern}`);
162
+ }
163
+ }
164
+
165
+ return { default: defaultRatio, rules };
166
+ } catch (err) {
167
+ console.warn(`[sampler] failed to decode OTEL_SAMPLING_CONFIG`, String(err));
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Build a `ParentBasedSampler` rooted at our URL-based sampler.
174
+ * Use as the `headSampler` for `@microlabs/otel-cf-workers`.
175
+ */
176
+ export function createUrlBasedHeadSampler(config: SamplingConfig | null): Sampler {
177
+ const root = new URLBasedSampler(config ?? {});
178
+ return new ParentBasedSampler({ root });
179
+ }
180
+
181
+ // Re-export OTel API helper so callers can read `traceId` / build tags off
182
+ // the active span without importing @opentelemetry/api directly.
183
+ export { trace as _otelTrace };