@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,559 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createInstrumentedFetch } from "./instrumentedFetch";
|
|
3
|
+
import { configureLogger, defaultLoggerAdapter } from "./logger";
|
|
4
|
+
import type { Span, TracerAdapter } from "./observability";
|
|
5
|
+
import { configureTracer, setObservabilitySpanStore } from "./observability";
|
|
6
|
+
|
|
7
|
+
function makeFakeTracer(): {
|
|
8
|
+
tracer: TracerAdapter;
|
|
9
|
+
startSpan: ReturnType<typeof vi.fn>;
|
|
10
|
+
spans: Array<ReturnType<typeof makeFakeSpan>>;
|
|
11
|
+
} {
|
|
12
|
+
const spans: Array<ReturnType<typeof makeFakeSpan>> = [];
|
|
13
|
+
const startSpan = vi.fn((name: string, attrs?: Record<string, string | number | boolean>) => {
|
|
14
|
+
const s = makeFakeSpan(name, attrs);
|
|
15
|
+
spans.push(s);
|
|
16
|
+
return s.span;
|
|
17
|
+
});
|
|
18
|
+
return { tracer: { startSpan } as TracerAdapter, startSpan, spans };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeFakeSpan(
|
|
22
|
+
name: string,
|
|
23
|
+
initialAttrs?: Record<string, string | number | boolean>,
|
|
24
|
+
ctx?: { traceId: string; spanId: string; traceFlags: number },
|
|
25
|
+
) {
|
|
26
|
+
const attrs: Record<string, string | number | boolean> = { ...(initialAttrs ?? {}) };
|
|
27
|
+
const span: Span = {
|
|
28
|
+
end: vi.fn(),
|
|
29
|
+
setError: vi.fn(),
|
|
30
|
+
setAttribute: vi.fn((k: string, v: string | number | boolean) => {
|
|
31
|
+
attrs[k] = v;
|
|
32
|
+
}),
|
|
33
|
+
spanContext: ctx ? () => ctx : undefined,
|
|
34
|
+
};
|
|
35
|
+
return { name, span, attrs };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("createInstrumentedFetch — URL redaction", () => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
configureTracer({ startSpan: () => ({ end: () => {} }) });
|
|
41
|
+
setObservabilitySpanStore(undefined);
|
|
42
|
+
configureLogger(defaultLoggerAdapter);
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("stamps a redacted http.url on the span, not the raw URL", async () => {
|
|
47
|
+
const { tracer, spans } = makeFakeTracer();
|
|
48
|
+
configureTracer(tracer);
|
|
49
|
+
const baseFetch = vi.fn(async () => new Response("ok", { status: 200 }));
|
|
50
|
+
|
|
51
|
+
const f = createInstrumentedFetch({
|
|
52
|
+
name: "vtex",
|
|
53
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await f("https://api.test/search?token=SECRET123&page=2");
|
|
57
|
+
|
|
58
|
+
expect(spans).toHaveLength(1);
|
|
59
|
+
expect(spans[0].attrs["http.url"]).toBe("https://api.test/search?token=REDACTED&page=REDACTED");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("honors keepQueryKeys for benign query params", async () => {
|
|
63
|
+
const { tracer, spans } = makeFakeTracer();
|
|
64
|
+
configureTracer(tracer);
|
|
65
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
66
|
+
|
|
67
|
+
const f = createInstrumentedFetch({
|
|
68
|
+
name: "vtex",
|
|
69
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
70
|
+
keepQueryKeys: ["page", "sort"],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await f("https://api.test/search?token=SECRET&page=2&sort=name");
|
|
74
|
+
|
|
75
|
+
expect(spans[0].attrs["http.url"]).toBe(
|
|
76
|
+
"https://api.test/search?token=REDACTED&page=2&sort=name",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("emits the structured `outgoing fetch` log with host+path when OTEL_LOG_OUTGOING_FETCH=true", async () => {
|
|
81
|
+
// The breadcrumb is gated behind an env flag to avoid log explosion
|
|
82
|
+
// in production; we flip it on for the test and assert on the
|
|
83
|
+
// payload to keep this test honest about what it verifies.
|
|
84
|
+
const captured: Array<{
|
|
85
|
+
level: string;
|
|
86
|
+
msg: string;
|
|
87
|
+
attrs?: Record<string, unknown>;
|
|
88
|
+
}> = [];
|
|
89
|
+
configureLogger({
|
|
90
|
+
log: (level, msg, attrs) => {
|
|
91
|
+
captured.push({ level, msg, attrs });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const previous = process.env.OTEL_LOG_OUTGOING_FETCH;
|
|
96
|
+
process.env.OTEL_LOG_OUTGOING_FETCH = "true";
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const baseFetch = vi.fn(async () => new Response("ok", { status: 200 }));
|
|
100
|
+
const f = createInstrumentedFetch({
|
|
101
|
+
name: "vtex",
|
|
102
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
103
|
+
logging: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const res = await f("https://api.test/items?id=42");
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
|
|
109
|
+
const breadcrumb = captured.find((c) => c.msg === "outgoing fetch");
|
|
110
|
+
expect(breadcrumb).toBeDefined();
|
|
111
|
+
expect(breadcrumb?.level).toBe("info");
|
|
112
|
+
expect(breadcrumb?.attrs).toMatchObject({
|
|
113
|
+
app: "vtex",
|
|
114
|
+
host: "api.test",
|
|
115
|
+
path: "/items",
|
|
116
|
+
method: "GET",
|
|
117
|
+
status: 200,
|
|
118
|
+
ok: true,
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
if (previous === undefined) {
|
|
122
|
+
delete process.env.OTEL_LOG_OUTGOING_FETCH;
|
|
123
|
+
} else {
|
|
124
|
+
process.env.OTEL_LOG_OUTGOING_FETCH = previous;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("does NOT emit the `outgoing fetch` breadcrumb when the env flag is unset", async () => {
|
|
130
|
+
const captured: Array<{ msg: string }> = [];
|
|
131
|
+
configureLogger({
|
|
132
|
+
log: (_level, msg) => captured.push({ msg }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const previous = process.env.OTEL_LOG_OUTGOING_FETCH;
|
|
136
|
+
delete process.env.OTEL_LOG_OUTGOING_FETCH;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
140
|
+
const f = createInstrumentedFetch({
|
|
141
|
+
name: "vtex",
|
|
142
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
143
|
+
logging: false,
|
|
144
|
+
});
|
|
145
|
+
await f("https://api.test/items?id=42");
|
|
146
|
+
|
|
147
|
+
expect(captured.find((c) => c.msg === "outgoing fetch")).toBeUndefined();
|
|
148
|
+
} finally {
|
|
149
|
+
if (previous !== undefined) process.env.OTEL_LOG_OUTGOING_FETCH = previous;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("createInstrumentedFetch — traceparent injection", () => {
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
configureTracer({ startSpan: () => ({ end: () => {} }) });
|
|
157
|
+
setObservabilitySpanStore(undefined);
|
|
158
|
+
vi.restoreAllMocks();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("injects traceparent on outbound calls when a span is active", async () => {
|
|
162
|
+
// Install a tracer that creates a fake span whose spanContext()
|
|
163
|
+
// returns a known id, AND wire the spanStore so getActiveSpan()
|
|
164
|
+
// can find it across the await boundary inside withTracing.
|
|
165
|
+
const knownCtx = {
|
|
166
|
+
traceId: "0123456789abcdef0123456789abcdef",
|
|
167
|
+
spanId: "fedcba9876543210",
|
|
168
|
+
traceFlags: 1,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// The redacted "active span" is the one createInstrumentedFetch starts.
|
|
172
|
+
// injectTraceContext reads `getActiveSpan()`, which only works inside
|
|
173
|
+
// a `withTracing` / spanStore.run scope. The simplest stub: install a
|
|
174
|
+
// tracer that returns a span with spanContext, AND make the spanStore
|
|
175
|
+
// resolve that span when fetched.
|
|
176
|
+
const fakeSpan = makeFakeSpan("vtex.fetch", undefined, knownCtx);
|
|
177
|
+
configureTracer({
|
|
178
|
+
startSpan: () => fakeSpan.span,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Custom span store that returns the fake span on every get(). This
|
|
182
|
+
// models the host's ALS-backed store with a single active span.
|
|
183
|
+
setObservabilitySpanStore({
|
|
184
|
+
get: () => fakeSpan.span,
|
|
185
|
+
run: (_span, fn) => fn(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const baseFetch = vi.fn(async (_input: unknown, init?: RequestInit) => {
|
|
189
|
+
const h = new Headers(init?.headers);
|
|
190
|
+
return new Response(h.get("traceparent") ?? "<missing>", { status: 200 });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const f = createInstrumentedFetch({
|
|
194
|
+
name: "vtex",
|
|
195
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
196
|
+
logging: false,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const res = await f("https://api.test/x");
|
|
200
|
+
const body = await res.text();
|
|
201
|
+
expect(body).toBe(`00-${knownCtx.traceId}-${knownCtx.spanId}-01`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("does NOT inject traceparent when injectTraceparent: false", async () => {
|
|
205
|
+
const fakeSpan = makeFakeSpan("vtex.fetch", undefined, {
|
|
206
|
+
traceId: "11111111111111111111111111111111",
|
|
207
|
+
spanId: "2222222222222222",
|
|
208
|
+
traceFlags: 1,
|
|
209
|
+
});
|
|
210
|
+
configureTracer({ startSpan: () => fakeSpan.span });
|
|
211
|
+
setObservabilitySpanStore({
|
|
212
|
+
get: () => fakeSpan.span,
|
|
213
|
+
run: (_s, fn) => fn(),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const baseFetch = vi.fn(async (_i: unknown, init?: RequestInit) => {
|
|
217
|
+
const h = new Headers(init?.headers);
|
|
218
|
+
return new Response(JSON.stringify({ traceparent: h.get("traceparent") }), { status: 200 });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const f = createInstrumentedFetch({
|
|
222
|
+
name: "vtex",
|
|
223
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
224
|
+
logging: false,
|
|
225
|
+
injectTraceparent: false,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const res = await f("https://api.test/x");
|
|
229
|
+
const body = (await res.json()) as { traceparent: string | null };
|
|
230
|
+
expect(body.traceparent).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("is a safe no-op when no span is active", async () => {
|
|
234
|
+
// No tracer configured, no spanStore — injectTraceContext returns early.
|
|
235
|
+
configureTracer({ startSpan: () => ({ end: () => {} }) });
|
|
236
|
+
setObservabilitySpanStore(undefined);
|
|
237
|
+
|
|
238
|
+
const baseFetch = vi.fn(async (_i: unknown, init?: RequestInit) => {
|
|
239
|
+
const h = new Headers(init?.headers);
|
|
240
|
+
return new Response(h.get("traceparent") ?? "<missing>", { status: 200 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const f = createInstrumentedFetch({
|
|
244
|
+
name: "vtex",
|
|
245
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
246
|
+
logging: false,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const res = await f("https://api.test/x");
|
|
250
|
+
expect(await res.text()).toBe("<missing>");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("preserves caller-supplied headers when injecting traceparent", async () => {
|
|
254
|
+
const fakeSpan = makeFakeSpan("vtex.fetch", undefined, {
|
|
255
|
+
traceId: "33333333333333333333333333333333",
|
|
256
|
+
spanId: "4444444444444444",
|
|
257
|
+
traceFlags: 1,
|
|
258
|
+
});
|
|
259
|
+
configureTracer({ startSpan: () => fakeSpan.span });
|
|
260
|
+
setObservabilitySpanStore({
|
|
261
|
+
get: () => fakeSpan.span,
|
|
262
|
+
run: (_s, fn) => fn(),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const baseFetch = vi.fn(async (_i: unknown, init?: RequestInit) => {
|
|
266
|
+
const h = new Headers(init?.headers);
|
|
267
|
+
return new Response(
|
|
268
|
+
JSON.stringify({
|
|
269
|
+
auth: h.get("authorization"),
|
|
270
|
+
tp: h.get("traceparent"),
|
|
271
|
+
}),
|
|
272
|
+
{ status: 200 },
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const f = createInstrumentedFetch({
|
|
277
|
+
name: "vtex",
|
|
278
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
279
|
+
logging: false,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const res = await f("https://api.test/x", {
|
|
283
|
+
headers: { authorization: "Bearer abc" },
|
|
284
|
+
});
|
|
285
|
+
const body = (await res.json()) as { auth: string; tp: string };
|
|
286
|
+
expect(body.auth).toBe("Bearer abc");
|
|
287
|
+
expect(body.tp).toBe(
|
|
288
|
+
`00-${fakeSpan.span.spanContext!().traceId}-${fakeSpan.span.spanContext!().spanId}-01`,
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("honors Fetch-spec semantics: init.headers REPLACES Request headers (does not union)", async () => {
|
|
293
|
+
// Per the Fetch spec, when both a Request and `init.headers` are
|
|
294
|
+
// passed to `fetch()`, init.headers replace Request.headers — not
|
|
295
|
+
// union. Verify our wrapper preserves that contract: a Request with
|
|
296
|
+
// header `x-from-req` and an init with header `x-from-init` should
|
|
297
|
+
// surface only `x-from-init` on the wire, plus our injected
|
|
298
|
+
// traceparent.
|
|
299
|
+
const fakeSpan = makeFakeSpan("vtex.fetch", undefined, {
|
|
300
|
+
traceId: "55555555555555555555555555555555",
|
|
301
|
+
spanId: "6666666666666666",
|
|
302
|
+
traceFlags: 1,
|
|
303
|
+
});
|
|
304
|
+
configureTracer({ startSpan: () => fakeSpan.span });
|
|
305
|
+
setObservabilitySpanStore({
|
|
306
|
+
get: () => fakeSpan.span,
|
|
307
|
+
run: (_s, fn) => fn(),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const baseFetch = vi.fn(async (_i: unknown, init?: RequestInit) => {
|
|
311
|
+
const h = new Headers(init?.headers);
|
|
312
|
+
return new Response(
|
|
313
|
+
JSON.stringify({
|
|
314
|
+
fromReq: h.get("x-from-req"),
|
|
315
|
+
fromInit: h.get("x-from-init"),
|
|
316
|
+
tp: h.get("traceparent"),
|
|
317
|
+
}),
|
|
318
|
+
{ status: 200 },
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const f = createInstrumentedFetch({
|
|
323
|
+
name: "vtex",
|
|
324
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
325
|
+
logging: false,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const req = new Request("https://api.test/x", {
|
|
329
|
+
headers: { "x-from-req": "REQ" },
|
|
330
|
+
});
|
|
331
|
+
const res = await f(req, { headers: { "x-from-init": "INIT" } });
|
|
332
|
+
const body = (await res.json()) as {
|
|
333
|
+
fromReq: string | null;
|
|
334
|
+
fromInit: string | null;
|
|
335
|
+
tp: string | null;
|
|
336
|
+
};
|
|
337
|
+
// init.headers replaces, so x-from-req must NOT leak through.
|
|
338
|
+
expect(body.fromReq).toBeNull();
|
|
339
|
+
expect(body.fromInit).toBe("INIT");
|
|
340
|
+
expect(body.tp).toMatch(/^00-/);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("uses init.operation to name the span", async () => {
|
|
344
|
+
const { tracer, spans } = makeFakeTracer();
|
|
345
|
+
configureTracer(tracer);
|
|
346
|
+
const baseFetch = vi.fn(async () => new Response("ok", { status: 200 }));
|
|
347
|
+
|
|
348
|
+
const f = createInstrumentedFetch({
|
|
349
|
+
name: "vtex",
|
|
350
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
351
|
+
logging: false,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await f("https://api.test/whatever", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
operation: "intelligent-search.product_search",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(spans).toHaveLength(1);
|
|
360
|
+
expect(spans[0].name).toBe("vtex.intelligent-search.product_search");
|
|
361
|
+
expect(spans[0].attrs["fetch.operation"]).toBe("intelligent-search.product_search");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("strips `operation` from init before calling baseFetch", async () => {
|
|
365
|
+
const baseFetch = vi.fn(async (_input: unknown, init?: RequestInit) => {
|
|
366
|
+
// `operation` is an extension on InstrumentedFetchInit, NOT a
|
|
367
|
+
// valid RequestInit property. It must be removed before the
|
|
368
|
+
// underlying fetch sees the init.
|
|
369
|
+
expect(init && "operation" in init).toBe(false);
|
|
370
|
+
return new Response("ok");
|
|
371
|
+
});
|
|
372
|
+
const f = createInstrumentedFetch({
|
|
373
|
+
name: "vtex",
|
|
374
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
375
|
+
logging: false,
|
|
376
|
+
});
|
|
377
|
+
await f("https://api.test/x", {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "content-type": "application/json" },
|
|
380
|
+
operation: "checkout.orderForm",
|
|
381
|
+
});
|
|
382
|
+
expect(baseFetch).toHaveBeenCalledOnce();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("falls back to defaultOperation when init.operation is omitted", async () => {
|
|
386
|
+
const { tracer, spans } = makeFakeTracer();
|
|
387
|
+
configureTracer(tracer);
|
|
388
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
389
|
+
|
|
390
|
+
const f = createInstrumentedFetch({
|
|
391
|
+
name: "resend",
|
|
392
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
393
|
+
logging: false,
|
|
394
|
+
defaultOperation: "emails.send",
|
|
395
|
+
});
|
|
396
|
+
await f("https://api.resend.com/emails");
|
|
397
|
+
|
|
398
|
+
expect(spans[0].name).toBe("resend.emails.send");
|
|
399
|
+
expect(spans[0].attrs["fetch.operation"]).toBe("emails.send");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("falls back to resolveOperation when init.operation + defaultOperation are unset", async () => {
|
|
403
|
+
const { tracer, spans } = makeFakeTracer();
|
|
404
|
+
configureTracer(tracer);
|
|
405
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
406
|
+
|
|
407
|
+
const resolveOperation = vi.fn((url: string, _method: string) => {
|
|
408
|
+
const u = new URL(url);
|
|
409
|
+
// Toy router: `/api/checkout/...` → checkout.<segment>
|
|
410
|
+
const m = u.pathname.match(/^\/api\/checkout\/(\w+)/);
|
|
411
|
+
if (m) return `checkout.${m[1]}`;
|
|
412
|
+
return undefined;
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const f = createInstrumentedFetch({
|
|
416
|
+
name: "vtex",
|
|
417
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
418
|
+
logging: false,
|
|
419
|
+
resolveOperation,
|
|
420
|
+
});
|
|
421
|
+
await f("https://api.vtex.test/api/checkout/orderForm");
|
|
422
|
+
|
|
423
|
+
expect(resolveOperation).toHaveBeenCalledWith(
|
|
424
|
+
"https://api.vtex.test/api/checkout/orderForm",
|
|
425
|
+
"GET",
|
|
426
|
+
);
|
|
427
|
+
expect(spans[0].name).toBe("vtex.checkout.orderForm");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("falls back to the literal '.fetch' suffix when resolveOperation returns undefined", async () => {
|
|
431
|
+
const { tracer, spans } = makeFakeTracer();
|
|
432
|
+
configureTracer(tracer);
|
|
433
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
434
|
+
|
|
435
|
+
const f = createInstrumentedFetch({
|
|
436
|
+
name: "vtex",
|
|
437
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
438
|
+
logging: false,
|
|
439
|
+
resolveOperation: () => undefined,
|
|
440
|
+
});
|
|
441
|
+
await f("https://api.vtex.test/unknown");
|
|
442
|
+
|
|
443
|
+
expect(spans[0].name).toBe("vtex.fetch");
|
|
444
|
+
expect(spans[0].attrs["fetch.operation"]).toBe("fetch");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("explicit init.operation wins over defaultOperation AND resolveOperation", async () => {
|
|
448
|
+
const { tracer, spans } = makeFakeTracer();
|
|
449
|
+
configureTracer(tracer);
|
|
450
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
451
|
+
|
|
452
|
+
const f = createInstrumentedFetch({
|
|
453
|
+
name: "vtex",
|
|
454
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
455
|
+
logging: false,
|
|
456
|
+
defaultOperation: "from-default",
|
|
457
|
+
resolveOperation: () => "from-router",
|
|
458
|
+
});
|
|
459
|
+
await f("https://api.vtex.test/x", { operation: "from-call" });
|
|
460
|
+
|
|
461
|
+
expect(spans[0].name).toBe("vtex.from-call");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("derives method from a Request input when init.method is omitted", async () => {
|
|
465
|
+
// Without this, `fetch(new Request(url, { method: "POST" }))` would
|
|
466
|
+
// surface as GET on the span and in the URL-router callback —
|
|
467
|
+
// mislabeling POST traffic in dashboards.
|
|
468
|
+
const { tracer, spans } = makeFakeTracer();
|
|
469
|
+
configureTracer(tracer);
|
|
470
|
+
const resolveOperation = vi.fn(
|
|
471
|
+
(_url: string, method: string) => `inferred.${method.toLowerCase()}`,
|
|
472
|
+
);
|
|
473
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
474
|
+
|
|
475
|
+
const f = createInstrumentedFetch({
|
|
476
|
+
name: "vtex",
|
|
477
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
478
|
+
logging: false,
|
|
479
|
+
resolveOperation,
|
|
480
|
+
});
|
|
481
|
+
await f(new Request("https://api.test/x", { method: "POST" }));
|
|
482
|
+
|
|
483
|
+
expect(resolveOperation).toHaveBeenCalledWith("https://api.test/x", "POST");
|
|
484
|
+
expect(spans[0].attrs["http.method"]).toBe("POST");
|
|
485
|
+
expect(spans[0].name).toBe("vtex.inferred.post");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("init.method overrides the Request's method (Fetch spec)", async () => {
|
|
489
|
+
const { tracer, spans } = makeFakeTracer();
|
|
490
|
+
configureTracer(tracer);
|
|
491
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
492
|
+
|
|
493
|
+
const f = createInstrumentedFetch({
|
|
494
|
+
name: "vtex",
|
|
495
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
496
|
+
logging: false,
|
|
497
|
+
});
|
|
498
|
+
await f(new Request("https://api.test/x", { method: "POST" }), {
|
|
499
|
+
method: "DELETE",
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(spans[0].attrs["http.method"]).toBe("DELETE");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("passes the resolved operation to onComplete", async () => {
|
|
506
|
+
const baseFetch = vi.fn(async () => new Response("ok"));
|
|
507
|
+
const onComplete = vi.fn();
|
|
508
|
+
const f = createInstrumentedFetch({
|
|
509
|
+
name: "vtex",
|
|
510
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
511
|
+
logging: false,
|
|
512
|
+
defaultOperation: "catalog.fallback",
|
|
513
|
+
onComplete,
|
|
514
|
+
});
|
|
515
|
+
await f("https://api.vtex.test/x", { operation: "intelligent-search.product_search" });
|
|
516
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
517
|
+
expect.objectContaining({ operation: "intelligent-search.product_search" }),
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("preserves Request headers when init.headers is omitted", async () => {
|
|
522
|
+
const fakeSpan = makeFakeSpan("vtex.fetch", undefined, {
|
|
523
|
+
traceId: "77777777777777777777777777777777",
|
|
524
|
+
spanId: "8888888888888888",
|
|
525
|
+
traceFlags: 1,
|
|
526
|
+
});
|
|
527
|
+
configureTracer({ startSpan: () => fakeSpan.span });
|
|
528
|
+
setObservabilitySpanStore({
|
|
529
|
+
get: () => fakeSpan.span,
|
|
530
|
+
run: (_s, fn) => fn(),
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const baseFetch = vi.fn(async (_i: unknown, init?: RequestInit) => {
|
|
534
|
+
const h = new Headers(init?.headers);
|
|
535
|
+
return new Response(
|
|
536
|
+
JSON.stringify({
|
|
537
|
+
fromReq: h.get("x-from-req"),
|
|
538
|
+
tp: h.get("traceparent"),
|
|
539
|
+
}),
|
|
540
|
+
{ status: 200 },
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const f = createInstrumentedFetch({
|
|
545
|
+
name: "vtex",
|
|
546
|
+
baseFetch: baseFetch as unknown as typeof fetch,
|
|
547
|
+
logging: false,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const req = new Request("https://api.test/x", {
|
|
551
|
+
headers: { "x-from-req": "REQ" },
|
|
552
|
+
});
|
|
553
|
+
// No init.headers — Request headers must reach the wire.
|
|
554
|
+
const res = await f(req);
|
|
555
|
+
const body = (await res.json()) as { fromReq: string | null; tp: string | null };
|
|
556
|
+
expect(body.fromReq).toBe("REQ");
|
|
557
|
+
expect(body.tp).toMatch(/^00-/);
|
|
558
|
+
});
|
|
559
|
+
});
|