@decocms/blocks 7.0.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/package.json +97 -0
- package/src/cms/applySectionConventions.ts +128 -0
- package/src/cms/blockSource.test.ts +67 -0
- package/src/cms/blockSource.ts +124 -0
- package/src/cms/index.ts +104 -0
- package/src/cms/layoutCacheRace.test.ts +146 -0
- package/src/cms/loadDecofileDirectory.test.ts +52 -0
- package/src/cms/loadDecofileDirectory.ts +48 -0
- package/src/cms/loader.test.ts +184 -0
- package/src/cms/loader.ts +284 -0
- package/src/cms/registry.test.ts +118 -0
- package/src/cms/registry.ts +252 -0
- package/src/cms/resolve.test.ts +547 -0
- package/src/cms/resolve.ts +2003 -0
- package/src/cms/schema.ts +993 -0
- package/src/cms/sectionLoaders.test.ts +409 -0
- package/src/cms/sectionLoaders.ts +562 -0
- package/src/cms/sectionMixins.test.ts +163 -0
- package/src/cms/sectionMixins.ts +153 -0
- package/src/hooks/LazySection.tsx +121 -0
- package/src/hooks/LiveControls.tsx +122 -0
- package/src/hooks/RenderSection.test.tsx +81 -0
- package/src/hooks/RenderSection.tsx +91 -0
- package/src/hooks/SectionErrorFallback.tsx +97 -0
- package/src/hooks/index.ts +4 -0
- package/src/index.ts +8 -0
- package/src/matchers/builtins.test.ts +251 -0
- package/src/matchers/builtins.ts +437 -0
- package/src/matchers/countryNames.ts +104 -0
- package/src/matchers/override.test.ts +205 -0
- package/src/matchers/override.ts +136 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +133 -0
- package/src/middleware/hydrationContext.test.ts +61 -0
- package/src/middleware/hydrationContext.ts +79 -0
- package/src/middleware/index.ts +88 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.test.ts +238 -0
- package/src/middleware/observability.ts +620 -0
- package/src/middleware/validateSection.test.ts +147 -0
- package/src/middleware/validateSection.ts +100 -0
- package/src/sdk/abTesting.test.ts +326 -0
- package/src/sdk/abTesting.ts +499 -0
- package/src/sdk/analytics.ts +77 -0
- package/src/sdk/cacheHeaders.test.ts +115 -0
- package/src/sdk/cacheHeaders.ts +424 -0
- package/src/sdk/cachedLoader.ts +364 -0
- package/src/sdk/clx.ts +5 -0
- package/src/sdk/cn.test.ts +34 -0
- package/src/sdk/cn.ts +28 -0
- package/src/sdk/composite.test.ts +121 -0
- package/src/sdk/composite.ts +114 -0
- package/src/sdk/cookie.test.ts +108 -0
- package/src/sdk/cookie.ts +129 -0
- package/src/sdk/crypto.ts +185 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/djb2.ts +20 -0
- package/src/sdk/encoding.test.ts +71 -0
- package/src/sdk/encoding.ts +47 -0
- package/src/sdk/env.ts +31 -0
- package/src/sdk/http.test.ts +71 -0
- package/src/sdk/http.ts +124 -0
- package/src/sdk/index.ts +90 -0
- package/src/sdk/inflightTimeout.test.ts +35 -0
- package/src/sdk/inflightTimeout.ts +53 -0
- package/src/sdk/instrumentedFetch.test.ts +559 -0
- package/src/sdk/instrumentedFetch.ts +339 -0
- package/src/sdk/invoke.test.ts +115 -0
- package/src/sdk/invoke.ts +260 -0
- package/src/sdk/logger.test.ts +432 -0
- package/src/sdk/logger.ts +304 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/normalizeUrls.ts +91 -0
- package/src/sdk/observability.ts +109 -0
- package/src/sdk/otel.test.ts +526 -0
- package/src/sdk/otel.ts +981 -0
- package/src/sdk/otelAdapters/clickhouseCollector.ts +65 -0
- package/src/sdk/otelAdapters.test.ts +89 -0
- package/src/sdk/otelAdapters.ts +144 -0
- package/src/sdk/otelHttpLog.test.ts +457 -0
- package/src/sdk/otelHttpLog.ts +419 -0
- package/src/sdk/otelHttpMeter.test.ts +292 -0
- package/src/sdk/otelHttpMeter.ts +506 -0
- package/src/sdk/otelHttpTracer.test.ts +474 -0
- package/src/sdk/otelHttpTracer.ts +543 -0
- package/src/sdk/redirects.ts +225 -0
- package/src/sdk/requestContext.ts +281 -0
- package/src/sdk/requestContextStorage.browser.test.ts +29 -0
- package/src/sdk/requestContextStorage.browser.ts +43 -0
- package/src/sdk/requestContextStorage.ts +35 -0
- package/src/sdk/retry.ts +45 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +42 -0
- package/src/sdk/sitemap.ts +160 -0
- package/src/sdk/urlRedaction.test.ts +73 -0
- package/src/sdk/urlRedaction.ts +82 -0
- package/src/sdk/urlUtils.ts +134 -0
- package/src/sdk/useDevice.test.ts +130 -0
- package/src/sdk/useDevice.ts +109 -0
- package/src/sdk/useDeviceContext.tsx +108 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.test.ts +128 -0
- package/src/sdk/useScript.ts +210 -0
- package/src/sdk/useSuggestions.test.ts +230 -0
- package/src/sdk/useSuggestions.ts +188 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/setup.ts +119 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +14 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createOtlpHttpMeterAdapter } from "./otelHttpMeter";
|
|
3
|
+
|
|
4
|
+
interface OtlpPayload {
|
|
5
|
+
resourceMetrics: Array<{
|
|
6
|
+
resource: {
|
|
7
|
+
attributes: Array<{ key: string; value: { stringValue: string } }>;
|
|
8
|
+
};
|
|
9
|
+
scopeMetrics: Array<{
|
|
10
|
+
scope: { name: string; version: string };
|
|
11
|
+
metrics: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
sum?: {
|
|
14
|
+
aggregationTemporality: number;
|
|
15
|
+
isMonotonic: boolean;
|
|
16
|
+
dataPoints: Array<{
|
|
17
|
+
attributes: Array<{ key: string; value: Record<string, unknown> }>;
|
|
18
|
+
startTimeUnixNano: string;
|
|
19
|
+
timeUnixNano: string;
|
|
20
|
+
asDouble: number;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
gauge?: {
|
|
24
|
+
dataPoints: Array<{
|
|
25
|
+
attributes: Array<{ key: string; value: Record<string, unknown> }>;
|
|
26
|
+
timeUnixNano: string;
|
|
27
|
+
asDouble: number;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
histogram?: {
|
|
31
|
+
aggregationTemporality: number;
|
|
32
|
+
dataPoints: Array<{
|
|
33
|
+
attributes: Array<{ key: string; value: Record<string, unknown> }>;
|
|
34
|
+
startTimeUnixNano: string;
|
|
35
|
+
timeUnixNano: string;
|
|
36
|
+
count: string;
|
|
37
|
+
sum: number;
|
|
38
|
+
min: number;
|
|
39
|
+
max: number;
|
|
40
|
+
bucketCounts: string[];
|
|
41
|
+
explicitBounds: number[];
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
}>;
|
|
45
|
+
}>;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function captureFetch() {
|
|
50
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
51
|
+
const impl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
52
|
+
calls.push({ url: String(url), init });
|
|
53
|
+
return new Response("{}", { status: 200 });
|
|
54
|
+
});
|
|
55
|
+
return { impl: impl as unknown as typeof fetch, calls };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildAdapter(
|
|
59
|
+
overrides: {
|
|
60
|
+
fetchImpl?: typeof fetch;
|
|
61
|
+
minFlushIntervalMs?: number;
|
|
62
|
+
maxBufferDatapoints?: number;
|
|
63
|
+
nowMs?: () => number;
|
|
64
|
+
onError?: (kind: "flush" | "overflow" | "kind-mismatch", err: unknown) => void;
|
|
65
|
+
histogramBounds?: number[];
|
|
66
|
+
} = {},
|
|
67
|
+
) {
|
|
68
|
+
return createOtlpHttpMeterAdapter({
|
|
69
|
+
endpoint: "https://ingest.test/v1/metrics",
|
|
70
|
+
resourceAttributes: {
|
|
71
|
+
"service.name": "smoke-site",
|
|
72
|
+
"service.version": "abc123",
|
|
73
|
+
},
|
|
74
|
+
scopeVersion: "5.0.0-test",
|
|
75
|
+
fetchImpl: overrides.fetchImpl,
|
|
76
|
+
minFlushIntervalMs: overrides.minFlushIntervalMs ?? 0,
|
|
77
|
+
maxBufferDatapoints: overrides.maxBufferDatapoints,
|
|
78
|
+
nowMs: overrides.nowMs,
|
|
79
|
+
onError: overrides.onError,
|
|
80
|
+
histogramBounds: overrides.histogramBounds,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("createOtlpHttpMeterAdapter — buffer + flush", () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
vi.useFakeTimers();
|
|
87
|
+
vi.setSystemTime(new Date("2026-05-18T16:00:00.000Z"));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
vi.useRealTimers();
|
|
92
|
+
vi.restoreAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("counterInc accumulates per attr-key and exports cumulative sum", async () => {
|
|
96
|
+
const { impl, calls } = captureFetch();
|
|
97
|
+
const meter = buildAdapter({ fetchImpl: impl });
|
|
98
|
+
|
|
99
|
+
meter.counterInc("deco.http.requests", 1, { method: "GET", status: "2xx" });
|
|
100
|
+
meter.counterInc("deco.http.requests", 1, { method: "GET", status: "2xx" });
|
|
101
|
+
meter.counterInc("deco.http.requests", 1, { method: "POST", status: "5xx" });
|
|
102
|
+
|
|
103
|
+
await meter.flush();
|
|
104
|
+
|
|
105
|
+
expect(calls).toHaveLength(1);
|
|
106
|
+
expect(calls[0].url).toBe("https://ingest.test/v1/metrics");
|
|
107
|
+
const payload = JSON.parse(calls[0].init!.body as string) as OtlpPayload;
|
|
108
|
+
const m = payload.resourceMetrics[0].scopeMetrics[0].metrics[0];
|
|
109
|
+
expect(m.name).toBe("deco.http.requests");
|
|
110
|
+
expect(m.sum?.isMonotonic).toBe(true);
|
|
111
|
+
expect(m.sum?.aggregationTemporality).toBe(2);
|
|
112
|
+
expect(m.sum?.dataPoints).toHaveLength(2);
|
|
113
|
+
|
|
114
|
+
// Sort for stable assertion order.
|
|
115
|
+
const points = [...(m.sum?.dataPoints ?? [])].sort((a, b) =>
|
|
116
|
+
JSON.stringify(a.attributes).localeCompare(JSON.stringify(b.attributes)),
|
|
117
|
+
);
|
|
118
|
+
expect(points[0].asDouble).toBe(2); // GET+2xx
|
|
119
|
+
expect(points[1].asDouble).toBe(1); // POST+5xx
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("histogramRecord assigns buckets correctly and reports count/sum/min/max", async () => {
|
|
123
|
+
const { impl, calls } = captureFetch();
|
|
124
|
+
const meter = buildAdapter({
|
|
125
|
+
fetchImpl: impl,
|
|
126
|
+
histogramBounds: [5, 10, 25, 50, 75, 100, 250, 500, 1000],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 12 samples across the [5,10,25,50,75,100,250,500,1000] bounds, with
|
|
130
|
+
// the `value <= bound[i]` lower-bucket convention used by the exporter.
|
|
131
|
+
// Distribution:
|
|
132
|
+
// bucket 0 (≤5): []
|
|
133
|
+
// bucket 1 (5..10]: [8.4]
|
|
134
|
+
// bucket 2 (10..25]: [12,14,20]
|
|
135
|
+
// bucket 3 (25..50]: [30,35,38,48]
|
|
136
|
+
// bucket 4 (50..75]: [60,70]
|
|
137
|
+
// bucket 5 (75..100]:[80,87.2]
|
|
138
|
+
// bucket 6+: []
|
|
139
|
+
const samples = [8.4, 12, 14, 20, 30, 35, 38, 48, 60, 70, 80, 87.2];
|
|
140
|
+
expect(samples).toHaveLength(12);
|
|
141
|
+
const sum = samples.reduce((a, b) => a + b, 0);
|
|
142
|
+
for (const v of samples) {
|
|
143
|
+
meter.histogramRecord("outbound_request_duration_ms", v, { provider: "vtex" });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await meter.flush();
|
|
147
|
+
|
|
148
|
+
const payload = JSON.parse(calls[0].init!.body as string) as OtlpPayload;
|
|
149
|
+
const m = payload.resourceMetrics[0].scopeMetrics[0].metrics[0];
|
|
150
|
+
expect(m.histogram?.aggregationTemporality).toBe(2);
|
|
151
|
+
expect(m.histogram?.dataPoints).toHaveLength(1);
|
|
152
|
+
const dp = m.histogram!.dataPoints[0];
|
|
153
|
+
expect(dp.count).toBe("12");
|
|
154
|
+
expect(dp.sum).toBeCloseTo(sum, 5);
|
|
155
|
+
expect(dp.min).toBe(8.4);
|
|
156
|
+
expect(dp.max).toBe(87.2);
|
|
157
|
+
expect(dp.bucketCounts).toEqual(["0", "1", "3", "4", "2", "2", "0", "0", "0", "0"]);
|
|
158
|
+
expect(dp.explicitBounds).toEqual([5, 10, 25, 50, 75, 100, 250, 500, 1000]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("gaugeSet keeps last write per attr-key", async () => {
|
|
162
|
+
const { impl, calls } = captureFetch();
|
|
163
|
+
const meter = buildAdapter({ fetchImpl: impl });
|
|
164
|
+
|
|
165
|
+
meter.gaugeSet("deco.metrics.buffer_size", 3);
|
|
166
|
+
meter.gaugeSet("deco.metrics.buffer_size", 7);
|
|
167
|
+
|
|
168
|
+
await meter.flush();
|
|
169
|
+
|
|
170
|
+
const payload = JSON.parse(calls[0].init!.body as string) as OtlpPayload;
|
|
171
|
+
const m = payload.resourceMetrics[0].scopeMetrics[0].metrics[0];
|
|
172
|
+
expect(m.gauge?.dataPoints).toHaveLength(1);
|
|
173
|
+
expect(m.gauge?.dataPoints[0].asDouble).toBe(7);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("resource attributes are stamped on every payload", async () => {
|
|
177
|
+
const { impl, calls } = captureFetch();
|
|
178
|
+
const meter = buildAdapter({ fetchImpl: impl });
|
|
179
|
+
meter.counterInc("x", 1);
|
|
180
|
+
await meter.flush();
|
|
181
|
+
|
|
182
|
+
const payload = JSON.parse(calls[0].init!.body as string) as OtlpPayload;
|
|
183
|
+
const attrs = payload.resourceMetrics[0].resource.attributes;
|
|
184
|
+
expect(attrs).toContainEqual({
|
|
185
|
+
key: "service.name",
|
|
186
|
+
value: { stringValue: "smoke-site" },
|
|
187
|
+
});
|
|
188
|
+
expect(attrs).toContainEqual({
|
|
189
|
+
key: "service.version",
|
|
190
|
+
value: { stringValue: "abc123" },
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("never throws when fetch rejects — surfaces via onError", async () => {
|
|
195
|
+
const onError = vi.fn();
|
|
196
|
+
const failing: typeof fetch = vi.fn(() =>
|
|
197
|
+
Promise.reject(new Error("network unreachable")),
|
|
198
|
+
) as unknown as typeof fetch;
|
|
199
|
+
const meter = buildAdapter({ fetchImpl: failing, onError });
|
|
200
|
+
|
|
201
|
+
meter.counterInc("x", 1);
|
|
202
|
+
await expect(meter.flush()).resolves.toBeUndefined();
|
|
203
|
+
expect(onError).toHaveBeenCalledWith("flush", expect.any(Error));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("non-200 HTTP response surfaces via onError but does not throw", async () => {
|
|
207
|
+
const onError = vi.fn();
|
|
208
|
+
const non200: typeof fetch = vi.fn(async () =>
|
|
209
|
+
new Response("oops", { status: 500 }),
|
|
210
|
+
) as unknown as typeof fetch;
|
|
211
|
+
const meter = buildAdapter({ fetchImpl: non200, onError });
|
|
212
|
+
meter.counterInc("x", 1);
|
|
213
|
+
await meter.flush();
|
|
214
|
+
expect(onError).toHaveBeenCalledWith("flush", expect.any(Error));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("registering a metric name as two different kinds drops the second + onError", () => {
|
|
218
|
+
const onError = vi.fn();
|
|
219
|
+
const meter = buildAdapter({ onError });
|
|
220
|
+
|
|
221
|
+
meter.counterInc("conflict", 1);
|
|
222
|
+
meter.gaugeSet?.("conflict", 7);
|
|
223
|
+
|
|
224
|
+
expect(onError).toHaveBeenCalledWith("kind-mismatch", expect.any(Error));
|
|
225
|
+
expect(meter.pendingDatapointCount()).toBe(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("cooldown gates flushes; cooldown is bypassed once buffer reaches the cap", async () => {
|
|
229
|
+
let mockNow = 1_000_000;
|
|
230
|
+
const { impl, calls } = captureFetch();
|
|
231
|
+
const meter = buildAdapter({
|
|
232
|
+
fetchImpl: impl,
|
|
233
|
+
minFlushIntervalMs: 5000,
|
|
234
|
+
maxBufferDatapoints: 3,
|
|
235
|
+
nowMs: () => mockNow,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 1st flush at t=0 — buffer has 1 entry, cooldown bypass via lastFlush=0
|
|
239
|
+
meter.counterInc("a", 1, { k: "1" });
|
|
240
|
+
await meter.flush();
|
|
241
|
+
expect(calls).toHaveLength(1);
|
|
242
|
+
|
|
243
|
+
// 2nd flush at t=2s — cooldown not elapsed AND buffer below cap → no-op
|
|
244
|
+
mockNow += 2000;
|
|
245
|
+
meter.counterInc("a", 1, { k: "2" });
|
|
246
|
+
await meter.flush();
|
|
247
|
+
expect(calls).toHaveLength(1);
|
|
248
|
+
|
|
249
|
+
// 3rd flush at t=2.5s — still under cooldown but buffer at cap (3) → flush
|
|
250
|
+
mockNow += 500;
|
|
251
|
+
meter.counterInc("a", 1, { k: "3" });
|
|
252
|
+
expect(meter.pendingDatapointCount()).toBe(3);
|
|
253
|
+
await meter.flush();
|
|
254
|
+
expect(calls).toHaveLength(2);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("overflow drops new attribute-keys when buffer is at cap (existing keys still update)", () => {
|
|
258
|
+
const onError = vi.fn();
|
|
259
|
+
const meter = buildAdapter({ maxBufferDatapoints: 2, onError });
|
|
260
|
+
|
|
261
|
+
meter.counterInc("a", 1, { k: "1" });
|
|
262
|
+
meter.counterInc("a", 1, { k: "2" });
|
|
263
|
+
expect(meter.pendingDatapointCount()).toBe(2);
|
|
264
|
+
|
|
265
|
+
meter.counterInc("a", 1, { k: "3" });
|
|
266
|
+
expect(onError).toHaveBeenCalledWith("overflow", expect.any(Error));
|
|
267
|
+
expect(meter.pendingDatapointCount()).toBe(2);
|
|
268
|
+
|
|
269
|
+
// Existing key still updates (no new datapoint, just bumps the existing value).
|
|
270
|
+
meter.counterInc("a", 5, { k: "1" });
|
|
271
|
+
expect(meter.pendingDatapointCount()).toBe(2);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("concurrent flushes share a single in-flight POST", async () => {
|
|
275
|
+
let releaseFetch: ((res: Response) => void) | undefined;
|
|
276
|
+
const slow: typeof fetch = vi.fn(
|
|
277
|
+
() =>
|
|
278
|
+
new Promise<Response>((resolve) => {
|
|
279
|
+
releaseFetch = resolve;
|
|
280
|
+
}),
|
|
281
|
+
) as unknown as typeof fetch;
|
|
282
|
+
const meter = buildAdapter({ fetchImpl: slow });
|
|
283
|
+
|
|
284
|
+
meter.counterInc("x", 1);
|
|
285
|
+
const a = meter.flush();
|
|
286
|
+
const b = meter.flush();
|
|
287
|
+
expect(slow).toHaveBeenCalledTimes(1);
|
|
288
|
+
releaseFetch?.(new Response("{}", { status: 200 }));
|
|
289
|
+
await Promise.all([a, b]);
|
|
290
|
+
expect(slow).toHaveBeenCalledTimes(1);
|
|
291
|
+
});
|
|
292
|
+
});
|