@decocms/blocks 7.37.4 → 7.37.5
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
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
8
|
import {
|
|
9
9
|
configureMeter,
|
|
10
|
-
|
|
10
|
+
DURATION_BUCKET_BOUNDARIES_SECONDS,
|
|
11
|
+
METRIC_METADATA,
|
|
11
12
|
type MeterAdapter,
|
|
12
13
|
MetricNames,
|
|
14
|
+
normalizePath,
|
|
13
15
|
recordCacheMetric,
|
|
14
16
|
recordCommerceMetric,
|
|
15
17
|
recordRequestMetric,
|
|
@@ -258,6 +260,46 @@ describe("recordCommerceMetric (D-11)", () => {
|
|
|
258
260
|
});
|
|
259
261
|
});
|
|
260
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Guards the unit/bucket agreement. Durations are recorded in seconds, but the
|
|
265
|
+
* OTel SDK's default explicit buckets are milliseconds
|
|
266
|
+
* (`[0, 5, 10, 25, 50, 75, 100, 250, 500, 1000]`). A duration metric that ships
|
|
267
|
+
* without `boundaries` inherits those defaults and files ~100% of real traffic
|
|
268
|
+
* into the first bucket, which silently destroys every quantile computed from
|
|
269
|
+
* it. These assertions exist so that regression fails here instead of in a
|
|
270
|
+
* dashboard six months later.
|
|
271
|
+
*/
|
|
272
|
+
describe("METRIC_METADATA duration buckets", () => {
|
|
273
|
+
it("declares second-scale boundaries for every metric measured in seconds", () => {
|
|
274
|
+
const secondValued = Object.entries(METRIC_METADATA).filter(
|
|
275
|
+
([, meta]) => meta.unit === "s",
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(secondValued.length).toBeGreaterThan(0);
|
|
279
|
+
|
|
280
|
+
for (const [name, meta] of secondValued) {
|
|
281
|
+
expect(meta.boundaries, `${name} must declare boundaries`).toBeDefined();
|
|
282
|
+
// A millisecond-scaled bucket set starts at 5; a second-scaled one starts
|
|
283
|
+
// well below 1. This is the exact mistake being guarded against.
|
|
284
|
+
expect(meta.boundaries![0], `${name} boundaries look like milliseconds`)
|
|
285
|
+
.toBeLessThan(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("keeps boundaries sorted ascending and free of duplicates", () => {
|
|
290
|
+
const b = DURATION_BUCKET_BOUNDARIES_SECONDS;
|
|
291
|
+
expect([...b]).toEqual([...new Set(b)]);
|
|
292
|
+
expect([...b]).toEqual([...b].sort((x, y) => x - y));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("covers the latency range real traffic falls in", () => {
|
|
296
|
+
// Observed production averages sat between 0.14s and 0.49s, with a tail
|
|
297
|
+
// past 10s on uncached loader paths. Buckets must resolve both ends.
|
|
298
|
+
expect(DURATION_BUCKET_BOUNDARIES_SECONDS.some((x) => x <= 0.05)).toBe(true);
|
|
299
|
+
expect(DURATION_BUCKET_BOUNDARIES_SECONDS.some((x) => x >= 10)).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
261
303
|
/**
|
|
262
304
|
* `http.route` is the highest-cardinality risk on the highest-volume metric.
|
|
263
305
|
* These cases are the real paths measured on production when the label reached
|
|
@@ -27,7 +27,23 @@
|
|
|
27
27
|
*
|
|
28
28
|
* configureMeter({
|
|
29
29
|
* counterInc: (name, value, labels) => metrics.getMeter("deco").createCounter(name).add(value, labels),
|
|
30
|
-
* histogramRecord: (name, value, labels) =>
|
|
30
|
+
* histogramRecord: (name, value, labels) => {
|
|
31
|
+
* // METRIC_METADATA carries the unit AND the bucket boundaries. Forwarding
|
|
32
|
+
* // `boundaries` to `advice` is required, not optional: durations are
|
|
33
|
+
* // recorded in seconds and the SDK's default buckets are milliseconds, so
|
|
34
|
+
* // an adapter that omits it files everything into the first bucket and
|
|
35
|
+
* // every quantile reads back as ~0.
|
|
36
|
+
* const meta = METRIC_METADATA[name];
|
|
37
|
+
* metrics.getMeter("deco")
|
|
38
|
+
* .createHistogram(name, {
|
|
39
|
+
* unit: meta?.unit,
|
|
40
|
+
* description: meta?.description,
|
|
41
|
+
* advice: meta?.boundaries
|
|
42
|
+
* ? { explicitBucketBoundaries: [...meta.boundaries] }
|
|
43
|
+
* : undefined,
|
|
44
|
+
* })
|
|
45
|
+
* .record(value, labels);
|
|
46
|
+
* },
|
|
31
47
|
* });
|
|
32
48
|
* ```
|
|
33
49
|
*/
|
|
@@ -271,23 +287,69 @@ export const MetricNames = {
|
|
|
271
287
|
LOADER_ERRORS: "deco.loader.errors",
|
|
272
288
|
} as const;
|
|
273
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Explicit bucket boundaries, in SECONDS, for every duration histogram declared
|
|
292
|
+
* in {@link METRIC_METADATA}.
|
|
293
|
+
*
|
|
294
|
+
* These exist because the unit and the buckets are configured in two different
|
|
295
|
+
* places and had drifted apart. The record helpers divide by 1000, so values
|
|
296
|
+
* arrive in seconds — correct per SemConv. But the OTel SDK's *default*
|
|
297
|
+
* boundaries are `[0, 5, 10, 25, 50, 75, 100, 250, 500, 1000]`, which are
|
|
298
|
+
* milliseconds. An adapter that calls `createHistogram(name)` without an
|
|
299
|
+
* `advice` therefore files every sub-5-second observation into the first bucket.
|
|
300
|
+
*
|
|
301
|
+
* Measured on the production ClickHouse before this change: 99.8%–99.95% of all
|
|
302
|
+
* observations on `http.server.request.duration` (78.7M/hour),
|
|
303
|
+
* `http.client.request.duration`, `deco.loader.duration` and
|
|
304
|
+
* `deco.cms.resolve.duration` sat in bucket 1, so every quantile read back as
|
|
305
|
+
* ~0 and the latency dashboards had to fall back to tail-sampled traces.
|
|
306
|
+
*
|
|
307
|
+
* Values are the SemConv-recommended defaults for HTTP duration histograms.
|
|
308
|
+
*/
|
|
309
|
+
export const DURATION_BUCKET_BOUNDARIES_SECONDS: readonly number[] = [
|
|
310
|
+
0.005,
|
|
311
|
+
0.01,
|
|
312
|
+
0.025,
|
|
313
|
+
0.05,
|
|
314
|
+
0.075,
|
|
315
|
+
0.1,
|
|
316
|
+
0.25,
|
|
317
|
+
0.5,
|
|
318
|
+
0.75,
|
|
319
|
+
1,
|
|
320
|
+
2.5,
|
|
321
|
+
5,
|
|
322
|
+
7.5,
|
|
323
|
+
10,
|
|
324
|
+
];
|
|
325
|
+
|
|
274
326
|
/**
|
|
275
327
|
* Per-metric metadata emitted in the OTLP payload's `description` and
|
|
276
328
|
* `unit` fields. Durations are normalized to **seconds at the source** (OTel
|
|
277
329
|
* semconv), matching the deco-cx/deco framework — NOT converted downstream in
|
|
278
330
|
* the collector. Callers still pass milliseconds; the record helpers divide.
|
|
279
331
|
*
|
|
332
|
+
* `boundaries`, when present, MUST be forwarded to the histogram's `advice` at
|
|
333
|
+
* creation time — the unit alone does not tell the SDK how to bucket, and its
|
|
334
|
+
* defaults assume milliseconds. See
|
|
335
|
+
* {@link DURATION_BUCKET_BOUNDARIES_SECONDS}.
|
|
336
|
+
*
|
|
280
337
|
* Keep keys aligned with `MetricNames` values so a missing entry is a
|
|
281
338
|
* type error at compile time when a new metric ships without metadata.
|
|
282
339
|
*/
|
|
283
|
-
export const METRIC_METADATA: Record<
|
|
340
|
+
export const METRIC_METADATA: Record<
|
|
341
|
+
string,
|
|
342
|
+
{ description: string; unit: string; boundaries?: readonly number[] }
|
|
343
|
+
> = {
|
|
284
344
|
[MetricNames.HTTP_SERVER_REQUEST_DURATION]: {
|
|
285
345
|
description: "Duration of HTTP server requests handled at the Worker entry point.",
|
|
286
346
|
unit: "s",
|
|
347
|
+
boundaries: DURATION_BUCKET_BOUNDARIES_SECONDS,
|
|
287
348
|
},
|
|
288
349
|
[MetricNames.HTTP_CLIENT_REQUEST_DURATION]: {
|
|
289
350
|
description: "Duration of outbound HTTP client requests (commerce, generic fetch).",
|
|
290
351
|
unit: "s",
|
|
352
|
+
boundaries: DURATION_BUCKET_BOUNDARIES_SECONDS,
|
|
291
353
|
},
|
|
292
354
|
[MetricNames.CACHE_REQUESTS]: {
|
|
293
355
|
description: "Cache lookups, dimensioned by status (hit/stale/miss/bypass).",
|
|
@@ -296,10 +358,12 @@ export const METRIC_METADATA: Record<string, { description: string; unit: string
|
|
|
296
358
|
[MetricNames.RESOLVE_DURATION]: {
|
|
297
359
|
description: "Duration of `deco.cms.resolvePage` — CMS route to block tree resolution.",
|
|
298
360
|
unit: "s",
|
|
361
|
+
boundaries: DURATION_BUCKET_BOUNDARIES_SECONDS,
|
|
299
362
|
},
|
|
300
363
|
[MetricNames.LOADER_DURATION]: {
|
|
301
364
|
description: "Per-loader execution duration, emitted by cachedLoader.",
|
|
302
365
|
unit: "s",
|
|
366
|
+
boundaries: DURATION_BUCKET_BOUNDARIES_SECONDS,
|
|
303
367
|
},
|
|
304
368
|
[MetricNames.LOADER_ERRORS]: {
|
|
305
369
|
description: "Per-loader error count.",
|