@decocms/start 4.1.0 → 4.2.1
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/package.json +1 -1
- package/src/cms/resolve.ts +20 -0
- package/src/sdk/sampler.test.ts +49 -11
- package/src/sdk/sampler.ts +21 -3
package/package.json
CHANGED
package/src/cms/resolve.ts
CHANGED
|
@@ -1391,6 +1391,26 @@ async function resolveDecoPageImpl(
|
|
|
1391
1391
|
try {
|
|
1392
1392
|
const deferred = resolveSectionShallow(section, ctx);
|
|
1393
1393
|
if (deferred) {
|
|
1394
|
+
// Skip sections whose scheduling window has already closed (or
|
|
1395
|
+
// hasn't opened yet). Without this check a LoadingFallback
|
|
1396
|
+
// skeleton is rendered and immediately replaced by nothing once
|
|
1397
|
+
// the component's own scheduling guard returns null — producing
|
|
1398
|
+
// the "skeleton flashes then disappears" effect.
|
|
1399
|
+
const sched = deferred.rawProps?.scheduling as
|
|
1400
|
+
| { start?: string; end?: string }
|
|
1401
|
+
| undefined;
|
|
1402
|
+
if (sched) {
|
|
1403
|
+
const now = Date.now();
|
|
1404
|
+
if (sched.end && now > new Date(sched.end).getTime()) {
|
|
1405
|
+
flatIndex++;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (sched.start && now < new Date(sched.start).getTime()) {
|
|
1409
|
+
flatIndex++;
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1394
1414
|
deferred.index = currentFlatIndex;
|
|
1395
1415
|
|
|
1396
1416
|
// Cache rawProps server-side and strip from the deferred object
|
package/src/sdk/sampler.test.ts
CHANGED
|
@@ -5,14 +5,29 @@ import {
|
|
|
5
5
|
SamplingDecision,
|
|
6
6
|
} from "@opentelemetry/sdk-trace-base";
|
|
7
7
|
import { describe, expect, it } from "vitest";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createUrlBasedHeadSampler,
|
|
10
|
+
DEFAULT_SAMPLE_RATIO,
|
|
11
|
+
decodeSamplingConfig,
|
|
12
|
+
URLBasedSampler,
|
|
13
|
+
} from "./sampler";
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
// Two trace IDs at opposite ends of the TraceIdRatioBased accumulator.
|
|
16
|
+
// `accumulate(traceId)` xors the trace ID in 8-hex-char chunks; threshold at
|
|
17
|
+
// ratio R is `floor(R * 0xffffffff)`. See
|
|
18
|
+
// `@opentelemetry/sdk-trace-base/src/sampler/TraceIdRatioBasedSampler`.
|
|
19
|
+
//
|
|
20
|
+
// LOW: 0x00000000 ^ 0x00000000 ^ 0xffffffff ^ 0xffffffff = 0
|
|
21
|
+
// → 0 < threshold for any ratio > 0 → SAMPLED at any ratio > 0
|
|
22
|
+
// HIGH: 0xffffffff ^ 0x00000000 ^ 0x00000000 ^ 0x00000000 = 0xffffffff
|
|
23
|
+
// → never below threshold for ratio < 1.0 → DROPPED at any ratio < 1
|
|
24
|
+
const LOW_TRACE_ID = "0000000000000000ffffffffffffffff";
|
|
25
|
+
const HIGH_TRACE_ID = "ffffffff000000000000000000000000";
|
|
11
26
|
|
|
12
|
-
function decide(sampler: URLBasedSampler, path: string) {
|
|
27
|
+
function decide(sampler: URLBasedSampler, path: string, traceId = LOW_TRACE_ID) {
|
|
13
28
|
return sampler.shouldSample(
|
|
14
29
|
ROOT_CONTEXT,
|
|
15
|
-
|
|
30
|
+
traceId,
|
|
16
31
|
"span-name",
|
|
17
32
|
SpanKind.SERVER,
|
|
18
33
|
{ "url.path": path },
|
|
@@ -21,7 +36,28 @@ function decide(sampler: URLBasedSampler, path: string) {
|
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
describe("URLBasedSampler", () => {
|
|
24
|
-
it("
|
|
39
|
+
it("exposes 0.1 as the framework-wide default sample ratio", () => {
|
|
40
|
+
expect(DEFAULT_SAMPLE_RATIO).toBe(0.1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("defaults to DEFAULT_SAMPLE_RATIO (0.1) when config omits `default`", () => {
|
|
44
|
+
const s = new URLBasedSampler();
|
|
45
|
+
// LOW_TRACE_ID accumulates to 0 — sampled at any ratio > 0, including 0.1.
|
|
46
|
+
expect(decide(s, "/anything").decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
|
|
47
|
+
// HIGH_TRACE_ID accumulates to ~uint32 max — dropped at 0.1, would be
|
|
48
|
+
// kept only at ratio ~= 1. This is the assertion that catches an
|
|
49
|
+
// accidental revert to the old `?? 1.0` fallback.
|
|
50
|
+
expect(decide(s, "/anything", HIGH_TRACE_ID).decision).toBe(SamplingDecision.NOT_RECORD);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("explicit `default: 1` opts in to AlwaysOn (records every trace)", () => {
|
|
54
|
+
const s = new URLBasedSampler({ default: 1 });
|
|
55
|
+
expect(decide(s, "/anything", HIGH_TRACE_ID).decision).toBe(
|
|
56
|
+
SamplingDecision.RECORD_AND_SAMPLED,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("falls back to provided default ratio when no rule matches", () => {
|
|
25
61
|
const s = new URLBasedSampler({ default: 1.0 });
|
|
26
62
|
expect(decide(s, "/anything").decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
|
|
27
63
|
});
|
|
@@ -40,7 +76,7 @@ describe("URLBasedSampler", () => {
|
|
|
40
76
|
|
|
41
77
|
it("falls back to default when no path attribute is present", () => {
|
|
42
78
|
const s = new URLBasedSampler({ default: 1.0 });
|
|
43
|
-
const result = s.shouldSample(ROOT_CONTEXT,
|
|
79
|
+
const result = s.shouldSample(ROOT_CONTEXT, LOW_TRACE_ID, "noop", SpanKind.INTERNAL, {}, []);
|
|
44
80
|
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
|
|
45
81
|
});
|
|
46
82
|
|
|
@@ -50,7 +86,7 @@ describe("URLBasedSampler", () => {
|
|
|
50
86
|
rules: [{ pattern: "^/wanted", ratio: 1.0 }],
|
|
51
87
|
});
|
|
52
88
|
const ok = (attrs: Record<string, string>) =>
|
|
53
|
-
s.shouldSample(ROOT_CONTEXT,
|
|
89
|
+
s.shouldSample(ROOT_CONTEXT, LOW_TRACE_ID, "n", SpanKind.SERVER, attrs, []);
|
|
54
90
|
|
|
55
91
|
expect(ok({ "url.path": "/wanted/x" }).decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
|
|
56
92
|
expect(ok({ "http.target": "/wanted/y" }).decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
|
|
@@ -103,18 +139,20 @@ describe("createUrlBasedHeadSampler", () => {
|
|
|
103
139
|
expect(sampler).toBeInstanceOf(ParentBasedSampler);
|
|
104
140
|
});
|
|
105
141
|
|
|
106
|
-
it("applies
|
|
107
|
-
// Smoke test only — sampler internals are validated above.
|
|
142
|
+
it("applies DEFAULT_SAMPLE_RATIO when config is null", () => {
|
|
108
143
|
const sampler = createUrlBasedHeadSampler(null);
|
|
144
|
+
// High-accumulating trace ID is dropped at 0.1 — proves the ParentBased
|
|
145
|
+
// wrapper inherits the URLBasedSampler default and isn't accidentally
|
|
146
|
+
// forcing AlwaysOn.
|
|
109
147
|
const result = sampler.shouldSample(
|
|
110
148
|
ROOT_CONTEXT,
|
|
111
|
-
|
|
149
|
+
HIGH_TRACE_ID,
|
|
112
150
|
"n",
|
|
113
151
|
SpanKind.SERVER,
|
|
114
152
|
{ "url.path": "/" },
|
|
115
153
|
[],
|
|
116
154
|
);
|
|
117
|
-
expect(result.decision).toBe(SamplingDecision.
|
|
155
|
+
expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
|
|
118
156
|
});
|
|
119
157
|
});
|
|
120
158
|
|
package/src/sdk/sampler.ts
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* decision when one exists (i.e. distributed traces are kept consistent end
|
|
10
10
|
* to end).
|
|
11
11
|
*
|
|
12
|
+
* **Default ratio.** When no `default` is provided in the config (or the env
|
|
13
|
+
* var is unset entirely), the sampler keeps **10%** of traces. Production
|
|
14
|
+
* storefront traffic at full sampling will burn HyperDX ingest quotas
|
|
15
|
+
* quickly — for an unconfigured site we'd rather drop 90% than overspend by
|
|
16
|
+
* default. Sites that genuinely want every trace recorded must opt-in
|
|
17
|
+
* explicitly with `OTEL_SAMPLING_CONFIG` setting `default: 1`.
|
|
18
|
+
*
|
|
12
19
|
* @example
|
|
13
20
|
* ```jsonc
|
|
14
21
|
* // base64-encode this and set as OTEL_SAMPLING_CONFIG:
|
|
@@ -41,12 +48,23 @@ export interface SamplingRule {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
export interface SamplingConfig {
|
|
44
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Default sample ratio applied when no rule matches. Defaults to **0.1**
|
|
53
|
+
* (10% sampled) when omitted. Set to `1` to record every trace
|
|
54
|
+
* (only do this when you need a full debug stream and accept the cost).
|
|
55
|
+
*/
|
|
45
56
|
default?: number;
|
|
46
57
|
/** Ordered list of rules. First match wins. */
|
|
47
58
|
rules?: SamplingRule[];
|
|
48
59
|
}
|
|
49
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Default ratio applied by `URLBasedSampler` when neither the config nor a
|
|
63
|
+
* matching rule specifies one. Centralised so tests + docs share a single
|
|
64
|
+
* source of truth.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_SAMPLE_RATIO = 0.1;
|
|
67
|
+
|
|
50
68
|
interface CompiledRule {
|
|
51
69
|
re: RegExp;
|
|
52
70
|
sampler: Sampler;
|
|
@@ -61,7 +79,7 @@ export class URLBasedSampler implements Sampler {
|
|
|
61
79
|
private readonly rules: CompiledRule[];
|
|
62
80
|
|
|
63
81
|
constructor(config: SamplingConfig = {}) {
|
|
64
|
-
this.defaultSampler = ratioToSampler(config.default ??
|
|
82
|
+
this.defaultSampler = ratioToSampler(config.default ?? DEFAULT_SAMPLE_RATIO);
|
|
65
83
|
this.rules = (config.rules ?? []).map((rule) => ({
|
|
66
84
|
re: new RegExp(rule.pattern),
|
|
67
85
|
sampler: ratioToSampler(rule.ratio),
|
|
@@ -128,7 +146,7 @@ function extractPath(attrs: Attributes): string | null {
|
|
|
128
146
|
|
|
129
147
|
/**
|
|
130
148
|
* Decode a base64-encoded `OTEL_SAMPLING_CONFIG` value into a `SamplingConfig`.
|
|
131
|
-
* Returns `null` (caller falls back to
|
|
149
|
+
* Returns `null` (caller falls back to `DEFAULT_SAMPLE_RATIO`, currently 0.1) on:
|
|
132
150
|
* - missing / empty input
|
|
133
151
|
* - invalid base64
|
|
134
152
|
* - JSON parse failure
|