Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- `runtime_http_*` metrics no longer emit samples without a `handler` label. Requests
that never reach a named handler (unmatched paths, replica-level rate limit

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[general.documentation-gap] 🔵 SUGGEST

The changelog entry scopes the fix to runtime_http_* Prometheus metrics and justifies it entirely with Node cluster IPC JSON serialization, but requestHandlerLabel was also applied to otelRequestMetricsMiddleware.ts (aborted requests, response sizes, total requests, request timings). OpenTelemetry metrics do not go through the cluster IPC round-trip; for that exporter this silently changes the handler attribute from absent/undefined to the literal string "undefined", which is a series-identity change for any OTel-backed dashboard or alert. That impact is undocumented.

Action: Add a changelog bullet covering the OpenTelemetry request metrics as well, stating that the handler attribute is now always present and is "undefined" for unnamed handlers, so consumers of the OTel pipeline can adjust queries.

To dismiss: /dk-review dismiss 9a4d7e21-c3f6-4b58-8e0a-71d5c9f2b6e3 [reason]

rejections, errors before the route pipeline) were counted with
`handler: undefined`; Node's cluster IPC serializes worker registries as JSON, which
drops `undefined` values, so the aggregated `/metrics` exposed a second, unnamed
series that Prometheus reads as `handler=""`. Those requests are now labelled
`handler="undefined"` — the same value prom-client rendered locally before cluster
aggregation — keeping dashboards and alerts that filter on it working.
- `/_status` requests are now reported as `handler="builtin:status-track"`, matching
the other builtin handlers, instead of falling into the unnamed bucket.

## [7.4.0] - 2026-06-22
### Changed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[general.documentation-gap] 🔵 SUGGEST

The new ### Fixed block sits directly above ## [7.4.0] - 2026-06-22, but package.json declares version 7.4.2. Releases 7.4.1 and 7.4.2 have no changelog entries, so the Unreleased section is being appended to a changelog that is already two patch versions behind the published package — anyone cutting a release from this file will produce misleading release notes.

Action: Ask the author to confirm and backfill the missing [7.4.1] and [7.4.2] sections (or explain why they were intentionally omitted) before this Unreleased block is promoted to a version heading.

To dismiss: /dk-review dismiss b5417cae-2f68-4903-a7de-31c0d9b6e825 [reason]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ const parseSeries = (text: string, metric: string): Record<string, number> => {
return out
}

// Node's cluster IPC serializes messages as JSON, so worker registries reach the
// master through a JSON round-trip. Reproducing it here keeps these tests honest
// about what the master actually merges (notably: JSON drops `undefined` label
// values, which used to strip the `handler` label from the aggregated output).
const overClusterIpc = <T>(payload: T): T => JSON.parse(JSON.stringify(payload))

const aggregateRegistries = async (registries: Array<Registry>): Promise<string> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Evolvability.Organizational] 🔵 SUGGEST

overClusterIpc is defined verbatim in two test files (clusterMetricsAggregator.test.ts line 55 and requestHandlerLabel.test.ts line 9), each with its own copy of the explanatory comment. Duplicated test infrastructure drifts: the two comments already differ in wording, and a future correction to the IPC-fidelity model has to be applied twice.

Action: Extract the helper into a shared test utility (e.g. src/service/metrics/__tests__/helpers/overClusterIpc.ts) and import it from both files, keeping a single authoritative comment.

To dismiss: /dk-review dismiss a2d76b58-8c14-4e93-b7a0-5f39284ce671 [reason]

const jsons = await Promise.all(registries.map((r) => r.getMetricsAsJSON()))
const jsons = await Promise.all(registries.map(async (r) => overClusterIpc(await r.getMetricsAsJSON())))
const merged = AggregatorRegistry.aggregate(jsons)
return merged.metrics()
}
Expand Down
129 changes: 129 additions & 0 deletions src/service/metrics/__tests__/requestHandlerLabel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { EventEmitter } from 'events'
import { AggregatorRegistry, register } from 'prom-client'

import { requestHandlerLabel, UNNAMED_REQUEST_HANDLER } from '../requestHandlerLabel'
import { addRequestMetricsMiddleware } from '../requestMetricsMiddleware'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Check] 🔵 SUGGEST

The new regression suite only exercises addRequestMetricsMiddleware (prom-client). otelRequestMetricsMiddleware.ts received the same four-call-site change in this PR and remains completely untested, so a future revert or a missed call site there would not be caught by CI.

Action: Add an equivalent test for addOtelRequestMetricsMiddleware with stubbed instruments, asserting that RequestsMetricLabels.REQUEST_HANDLER is always a non-empty string for aborted, sized, counted and timed requests.

To dismiss: /dk-review dismiss b7c4e290-15af-4c63-8f0d-2e6a9d31c085 [reason]

// Node's cluster IPC serializes messages as JSON, which drops properties whose
// value is `undefined`. This is what the master receives from each worker.
const overClusterIpc = <T>(payload: T): T => JSON.parse(JSON.stringify(payload))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Evolvability.Organizational] 🔵 SUGGEST

The overClusterIpc helper and its explanatory comment are duplicated verbatim between requestHandlerLabel.test.ts and clusterMetricsAggregator.test.ts. Since this helper encodes the central assumption of the whole fix (cluster IPC drops undefined label values), having two copies means a future correction to that assumption can be applied to only one of them.

Action: Extract overClusterIpc into a shared test helper (e.g. src/service/metrics/__tests__/helpers/clusterIpc.ts) and import it from both test files.

To dismiss: /dk-review dismiss 2c8f5b90-6a17-4d3e-b42c-0f9e7a1d84b6 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Evolvability.Organizational] 🔵 SUGGEST

The overClusterIpc helper and its explanatory comment are duplicated verbatim in requestHandlerLabel.test.ts and clusterMetricsAggregator.test.ts. Since this helper encodes a subtle, load-bearing assumption about Node cluster IPC JSON serialization, two independent copies will drift and one may silently stop reproducing the real transport.

Action: Extract overClusterIpc into a shared test helper (e.g. src/service/metrics/__tests__/helpers/clusterIpc.ts) and import it from both suites so the IPC-fidelity assumption is defined once.

To dismiss: /dk-review dismiss 9a58cf17-4b62-4e30-8f1c-7d34e6b902a5 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Evolvability.Organizational] 🔵 SUGGEST

overClusterIpc is defined identically (implementation plus explanatory comment) in both requestHandlerLabel.test.ts and clusterMetricsAggregator.test.ts. Since it encodes a non-obvious invariant about the cluster IPC JSON round-trip that both suites depend on, duplicating it means a future correction has to be found and applied twice.

Action: Extract overClusterIpc (with its comment) into a shared test helper under src/service/metrics/__tests__/ and import it from both suites.

To dismiss: /dk-review dismiss 0a5f7c18-3e6d-4a92-bb47-5c9e2d10f8a3 [reason]

const aggregatedMetrics = async (): Promise<string> => {
const workerRegistryJson = overClusterIpc(await register.getMetricsAsJSON())
return AggregatorRegistry.aggregate([workerRegistryJson]).metrics()
}

// Minimal ServiceContext stand-in for addRequestMetricsMiddleware: it only needs
// `req`/`res` emitters and a `response` with `length` and `status`.
const buildCtx = (requestHandlerName?: string) => {
const res = new EventEmitter()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality.new-logic-enforcement] 🔵 SUGGEST

buildCtx fixes requestHandlerName at context-construction time, so every test reads a handler name that already exists before the middleware runs. Real code — including statusTrack.ts in this very PR — assigns ctx.requestHandlerName during next(), and the middleware reads it afterwards in its finally block. That late-assignment ordering, the behaviour the statusTrack change depends on, is never exercised.

Action: Add a case where next() sets ctx.requestHandlerName = 'builtin:status-track' before emitting close, and assert the emitted series is handler="builtin:status-track" rather than handler="undefined".

To dismiss: /dk-review dismiss e50c8a19-7f43-4b2a-93d8-4c6e1b57a082 [reason]

return {
req: new EventEmitter(),
requestHandlerName,
res,
response: { length: 128, status: 200 },
}
}

// Closing the response inside `next()` makes the middleware finish its timings
// synchronously, so no stream plumbing is needed.
const runRequest = async (middleware: any, ctx: any) => {
await middleware(ctx, async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Evolvability.SupportedByLanguage] 🔵 SUGGEST

runRequest(middleware: any, ctx: any) and the untyped object returned by buildCtx opt the whole suite out of type checking against ServiceContext and the middleware signature. The tests are specifically about a context property (ctx.requestHandlerName), so if that property is renamed or the middleware signature changes, these tests will compile cleanly and fail only at runtime — or, worse, keep passing against a stale shape.

Action: Type buildCtx as Partial<ServiceContext> cast once at the boundary and give runRequest the real middleware type ((ctx: ServiceContext, next: () => Promise<void>) => Promise<void>), keeping the any cast confined to the single stub construction.

To dismiss: /dk-review dismiss e21c9b46-77d0-4f35-a8e3-1b6f4c9d2057 [reason]

ctx.res.emit('close')
})
}

const samplesOf = (text: string, metric: string) =>
text
.split('\n')
.filter((line) => line.startsWith(metric) && !line.startsWith('# '))

describe('requestHandlerLabel', () => {
it('falls back to "undefined" so the label is never empty or missing', () => {
expect(UNNAMED_REQUEST_HANDLER).toBe('undefined')
expect(requestHandlerLabel(undefined)).toBe(UNNAMED_REQUEST_HANDLER)
expect(requestHandlerLabel('')).toBe(UNNAMED_REQUEST_HANDLER)
})

it('keeps a named handler untouched', () => {
expect(requestHandlerLabel('private-handler:ssr')).toBe('private-handler:ssr')
})
})

describe('addRequestMetricsMiddleware handler label', () => {
beforeEach(() => {
// The instruments register into the default registry on construction.
register.clear()
})

it('labels requests that never reached a named handler', async () => {
const middleware = addRequestMetricsMiddleware()
await runRequest(middleware, buildCtx(undefined))

const local = await register.metrics()
expect(samplesOf(local, 'runtime_http_requests_total')).toEqual([
'runtime_http_requests_total{handler="undefined",status_code="200"} 1',
])
})

// Regression: with `handler: undefined` the label key survived in-process but was
// dropped by the cluster IPC JSON serialization, so the aggregated /metrics
// exposed `runtime_http_requests_total{status_code="200"}` — a second, unnamed
// series (Prometheus reads an absent label as `handler=""`).
it('keeps the handler label through the cluster IPC round-trip', async () => {
const middleware = addRequestMetricsMiddleware()
await runRequest(middleware, buildCtx(undefined))

const aggregated = await aggregatedMetrics()
const samples = samplesOf(aggregated, 'runtime_http_requests_total')

expect(samples).toEqual(['runtime_http_requests_total{handler="undefined",status_code="200"} 1'])
expect(aggregated).not.toContain('runtime_http_requests_total{status_code=')
})

it('emits no sample with a missing or empty handler label', async () => {
const middleware = addRequestMetricsMiddleware()
await runRequest(middleware, buildCtx(undefined))
await runRequest(middleware, buildCtx('private-handler:ssr'))

const aggregated = await aggregatedMetrics()
const handlerLabelledMetrics = [
'runtime_http_requests_total',
'runtime_http_requests_duration_milliseconds',
'runtime_http_response_size_bytes',
]

handlerLabelledMetrics.forEach((metric) => {
samplesOf(aggregated, metric).forEach((sample) => {
expect(sample).toMatch(/handler="[^"]+"/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Check] 🔵 SUGGEST

The 'emits no sample with a missing or empty handler label' test nests its only assertion inside handlerLabelledMetrics.forEach(... samplesOf(...).forEach(...)). If a metric name drifts (e.g. runtime_http_response_size_bytes is renamed) or a sample stops being emitted, samplesOf returns an empty array, the inner forEach never runs, and the test passes vacuously — exactly the regression it is meant to guard against would go undetected.

Action: Assert the sample list is non-empty before iterating, e.g. const samples = samplesOf(aggregated, metric); expect(samples.length).toBeGreaterThan(0); samples.forEach(...).

To dismiss: /dk-review dismiss d1e6b230-8a47-4c9f-b0d3-6e2f5c81a904 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Evolvability.SolutionApproach] 🔵 SUGGEST

The emits no sample with a missing or empty handler label test asserts inside a nested forEach, so it passes vacuously whenever samplesOf returns an empty array — e.g. if a metric is renamed, if the histogram never observes (response length falsy, close never emitted), or if register.clear() wipes an instrument the test expected. A regression that stops emitting these series altogether would be reported as green.

Action: Assert expect(samples.length).toBeGreaterThan(0) for each metric before iterating, so the test fails when the expected samples are absent rather than silently passing.

To dismiss: /dk-review dismiss 6d9a8e33-42b7-4b1e-9c5f-8b21f047ad6e [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Check] 🟡 RESTRICT

The assertion in 'emits no sample with a missing or empty handler label' is vacuous: samplesOf(aggregated, metric).forEach(...) runs zero assertions when a metric produces no samples, so the test passes if the fix regresses to the point where the metric disappears from the aggregated output altogether. That is precisely the failure mode this PR is guarding against (a label/series vanishing through the cluster IPC round-trip).

Action: Assert the sample set is non-empty before iterating, e.g. const samples = samplesOf(aggregated, metric); expect(samples.length).toBeGreaterThan(0); samples.forEach(...), or use expect.hasAssertions() plus an explicit expected-series list per metric.

To dismiss: /dk-review dismiss b7e14d02-5a38-4c96-8f21-6d09c3ba7e58 [reason]

})
})
})

it('reports named and unnamed handlers as separate series', async () => {
const middleware = addRequestMetricsMiddleware()
await runRequest(middleware, buildCtx('private-handler:ssr'))
await runRequest(middleware, buildCtx(undefined))

const aggregated = await aggregatedMetrics()
expect(samplesOf(aggregated, 'runtime_http_requests_total').sort()).toEqual([
'runtime_http_requests_total{handler="private-handler:ssr",status_code="200"} 1',
'runtime_http_requests_total{handler="undefined",status_code="200"} 1',
])
})

it('labels aborted requests that never reached a named handler', async () => {
const middleware = addRequestMetricsMiddleware()
const ctx: any = buildCtx(undefined)

await middleware(ctx, async () => {
ctx.req.emit('aborted')
ctx.res.emit('close')
})

const aggregated = await aggregatedMetrics()
expect(samplesOf(aggregated, 'runtime_http_aborted_requests_total')).toEqual([
'runtime_http_aborted_requests_total{handler="undefined"} 1',
])
})
})
11 changes: 7 additions & 4 deletions src/service/metrics/otelRequestMetricsMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { finished as onStreamFinished } from 'stream'
import { hrToMillisFloat } from '../../utils'
import { ServiceContext } from '../worker/runtime/typings'
import { getOtelInstruments, OtelRequestInstruments, RequestsMetricLabels } from './metrics'
import { requestHandlerLabel } from './requestHandlerLabel'

const INSTRUMENTS_INITIALIZATION_TIMEOUT = 500

Expand Down Expand Up @@ -38,7 +39,9 @@ export const addOtelRequestMetricsMiddleware = () => {

ctx.req.once('aborted', () => {
if (instruments) {
instruments.abortedRequests.add(1, { [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName })
instruments.abortedRequests.add(1, {
[RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality.new-logic-enforcement] 🔵 SUGGEST

addOtelRequestMetricsMiddleware gains the same requestHandlerLabel(...) fallback in four places (aborted, response sizes, total requests, timings), but no test in this PR exercises the OTel middleware. The new suite requestHandlerLabel.test.ts only covers the prom-client addRequestMetricsMiddleware, so the OTel branch of the fix — including the aborted-request path — ships without coverage and could silently regress.

Action: Add a test that drives addOtelRequestMetricsMiddleware with a mocked getOtelInstruments() and asserts the handler attribute equals 'undefined' when ctx.requestHandlerName is unset, mirroring the prom-client cases.

To dismiss: /dk-review dismiss b7c04e58-2f93-4a61-8de2-5c9b1a03f742 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality.new-logic-enforcement] 🔵 SUGGEST

otelRequestMetricsMiddleware.ts gains the same requestHandlerLabel(...) fallback at four instrument call sites (abortedRequests, responseSizes, totalRequests, requestTimings) but no test accompanies it. The new requestHandlerLabel.test.ts covers only the prom-client path via addRequestMetricsMiddleware; the OTel middleware's attribute handling stays uncovered, so a future call site added without the helper would not be caught.

Action: Add a test for addOtelRequestMetricsMiddleware with a mocked getOtelInstruments that asserts every recorded attribute set carries handler: 'undefined' when ctx.requestHandlerName is unset, mirroring the prom-client tests.

To dismiss: /dk-review dismiss c94a6f37-2b81-40de-9a53-18f7ce20b4d6 [reason]

})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Check] 🔵 SUGGEST

otelRequestMetricsMiddleware.ts is changed in four places but has no test coverage in this PR — the new requestHandlerLabel.test.ts exercises only addRequestMetricsMiddleware (prom-client). A regression that drops the fallback on the OTel path (or a future refactor of the attribute objects) would not be caught.

Action: Add at least one case driving addOtelRequestMetricsMiddleware with a stubbed getOtelInstruments, asserting that every recorded instrument receives a non-empty handler attribute for a ctx with requestHandlerName === undefined.

To dismiss: /dk-review dismiss 7d0e6431-b9c2-45a8-9f13-6e2b8c5a0f47 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Interface] 🟡 RESTRICT

The handler="undefined" fallback is also applied to the OpenTelemetry instruments, but the justification documented in requestHandlerLabel.ts (Node cluster IPC serializes the prom-client worker registry as JSON and drops undefined values) only holds for the prom-client cluster aggregation path. OTel instruments export per-process and never traverse the cluster IPC JSON round-trip, so this changes the attribute set — and therefore the time-series identity — of the existing diagnostics abortedRequests/responseSizes/totalRequests/requestTimings series from "handler attribute absent" to handler="undefined". The CHANGELOG documents the change only for runtime_http_*, so consumers of the OTel/diagnostics metrics get an undocumented breaking change to their dashboards and alerts at the 7.5.0 boundary.

Action: Either state explicitly in the CHANGELOG that the diagnostics/OTel handler attribute also changes from absent to undefined (and why consistency with the Prometheus series is desired), or keep the OTel call sites unchanged if the diagnostics backend already renders the missing attribute in a way existing dashboards depend on.

To dismiss: /dk-review dismiss 3f2b6c1a-9d47-4e58-b1c2-7a0d5e83f914 [reason]

}
})

Expand All @@ -53,7 +56,7 @@ export const addOtelRequestMetricsMiddleware = () => {
instruments.responseSizes.record(
responseLength,
{
[RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
[RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName),
[RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
[RequestsMetricLabels.ACCOUNT_NAME]: ctx.vtex?.account || 'unknown',
}
Expand All @@ -64,7 +67,7 @@ export const addOtelRequestMetricsMiddleware = () => {
instruments.totalRequests.add(
1,
{
[RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
[RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName),
[RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
[RequestsMetricLabels.ACCOUNT_NAME]: ctx.vtex?.account || 'unknown',
}
Expand All @@ -76,7 +79,7 @@ export const addOtelRequestMetricsMiddleware = () => {
instruments.requestTimings.record(
hrToMillisFloat(process.hrtime(start)),
{
[RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
[RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName),
[RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
[RequestsMetricLabels.ACCOUNT_NAME]: ctx.vtex?.account || 'unknown',
}
Expand Down
28 changes: 28 additions & 0 deletions src/service/metrics/requestHandlerLabel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Label value reported for requests that never reached a named handler: unmatched
* paths (Koa answers its default 404), requests rejected before the route pipeline
* (replica-level rate limiter, errors in compress/recorder), and handlers that
* don't set `ctx.requestHandlerName`.
*
* The value must be a non-empty string. `ctx.requestHandlerName` is `undefined`
* for those requests, and Node's cluster IPC serializes each worker's registry as
* JSON, which drops properties whose value is `undefined`. The sample then reaches
* the aggregated `/metrics` with the `handler` label missing altogether, which
* Prometheus reads as `handler=""` — a distinct, unnamed series that shows up as
* an extra "Value" line in dashboards.
*
* `'undefined'` is deliberate rather than a nicer word: prom-client's local
* exposition already rendered `handler: undefined` as `handler="undefined"` before
* the cluster aggregation was introduced, so keeping that value makes the
* aggregated output match the historical series identity and keeps existing
* dashboards, filters and alerts (e.g. `handler!~"builtin:.*|undefined"`) working.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[general.broken-references] 🔵 SUGGEST

The whole design decision — choosing the literal string 'undefined' over a clearer value such as 'unnamed' — rests on two unverifiable external claims in the doc comment: that prom-client's local exposition historically rendered handler: undefined as handler="undefined", and that existing dashboards/alerts filter on it (handler!~"builtin:.*|undefined"). Neither can be confirmed from this repository.

Action: Ask the author to double-check both references — the prom-client behaviour for the pinned version and at least one real dashboard/alert using that filter — and cite them (dashboard URL or alert rule) in the comment so a future reader can safely change the value.

To dismiss: /dk-review dismiss d5170a8c-2f4b-49e6-b3c1-6e8a95d24f07 [reason]

*/
export const UNNAMED_REQUEST_HANDLER = 'undefined'

/**
* Resolves the `handler` label value for a request, falling back to
* {@link UNNAMED_REQUEST_HANDLER} when the pipeline never named the handler.
* Empty strings fall back too, so the label is never emitted empty.
*/
export const requestHandlerLabel = (requestHandlerName?: string): string =>
requestHandlerName || UNNAMED_REQUEST_HANDLER
9 changes: 5 additions & 4 deletions src/service/metrics/requestMetricsMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
RequestsMetricLabels,
} from '../tracing/metrics/instruments'
import { ServiceContext } from '../worker/runtime/typings'
import { requestHandlerLabel } from './requestHandlerLabel'


export const addRequestMetricsMiddleware = () => {
Expand All @@ -23,7 +24,7 @@ export const addRequestMetricsMiddleware = () => {
concurrentRequests.inc(1)

ctx.req.once('aborted', () =>
abortedRequests.inc({ [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName }, 1)
abortedRequests.inc({ [RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName) }, 1)
)

let responseClosed = false
Expand All @@ -35,14 +36,14 @@ export const addRequestMetricsMiddleware = () => {
const responseLength = ctx.response.length
if (responseLength) {
responseSizes.observe(
{ [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName },
{ [RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName) },
responseLength
)
}

totalRequests.inc(
{
[RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
[RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName),
[RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
},
1
Expand All @@ -51,7 +52,7 @@ export const addRequestMetricsMiddleware = () => {
const onResFinished = () => {
requestTimings.observe(
{
[RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
[RequestsMetricLabels.REQUEST_HANDLER]: requestHandlerLabel(ctx.requestHandlerName),
},
hrToMillisFloat(process.hrtime(start))
)
Expand Down
29 changes: 29 additions & 0 deletions src/service/worker/runtime/__tests__/statusTrack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { statusTrackHandler } from '../statusTrack'
import { ServiceContext } from '../typings'

describe('statusTrackHandler', () => {
// /_status is served by a handler that answers 200, so its samples must carry a
// handler label like every other builtin (healthcheck, whoami, metrics-logger)
// instead of landing in the catch-all `handler="undefined"` bucket.
it('names the request so metrics are not reported as unnamed', async () => {
const setOperationName = jest.fn()
const ctx: any = {
body: undefined,
tracing: { currentSpan: { setOperationName } },
}

await statusTrackHandler(ctx as ServiceContext)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Resource] 🔴 BLOCK

The new test calls statusTrackHandler without stubbing process.send. LINKED is !!process.env.VTEX_APP_LINK, which is false under Jest, so the handler executes process.send?.('broadcastStatusTrack'). Jest runs test files in jest-worker child processes that have a live IPC channel, so this sends a raw string message to the Jest parent; jest-worker's _onMessage switches on response[0] and throws TypeError: Unexpected response from worker: b for unrecognised payloads, which can abort the test run. Both it blocks trigger it. The sibling test file src/service/metrics/__tests__/clusterMetricsAggregator.test.ts already guards against exactly this by assigning and then delete (process as any).send in afterEach.

Action: Stub the IPC channel in the test, e.g. beforeEach(() => { (process as any).send = jest.fn() }) and afterEach(() => { delete (process as any).send }), following the pattern already used in clusterMetricsAggregator.test.ts. Asserting that the broadcast was sent would also cover the !LINKED branch that is currently exercised by accident.

To dismiss: /dk-review dismiss f3b1c2a7-5d84-4e19-9c07-2a6e8b41d0f5 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Resource] 🟡 RESTRICT

The new test calls statusTrackHandler without stubbing process.send. The handler runs process.send?.(BROADCAST_STATUS_TRACK) whenever LINKED is false (the default in CI), and Jest executes test files inside jest-worker child processes where process.send is a real IPC channel. The test therefore emits the raw string 'broadcastStatusTrack' onto Jest's worker protocol channel on every run — an uncontrolled side effect that can surface as worker protocol noise or flaky runs, and the broadcast branch is asserted nowhere.

Action: Stub the channel in the test (e.g. const send = jest.fn(); (process as any).send = send) and restore it in afterEach, then assert the broadcast behaviour explicitly for both LINKED states instead of letting the real IPC call escape.

To dismiss: /dk-review dismiss 3f2a91c4-6d18-4b7e-9c05-1a8e2f7d4b31 [reason]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functional.Resource] 🔴 BLOCK

The new test invokes statusTrackHandler without stubbing process.send. LINKED is !!process.env.VTEX_APP_LINK, which is false under Jest, so the handler executes process.send?.('broadcastStatusTrack') for real. When Jest runs this file in a child-process worker (the default whenever more than one test file runs, including yarn ci:test), process.send is the jest-worker IPC channel; jest-worker's parent _onMessage switches on response[0] and throws TypeError: Unexpected response from worker: b for an unrecognized string message, aborting the run. Both tests in the file trigger this.

Action: Stub the IPC channel in the test, following the convention already used in src/service/metrics/__tests__/clusterMetricsAggregator.test.ts: set (process as any).send = jest.fn() in beforeEach and delete (process as any).send in afterEach. Assert the broadcast while you are there — expect(sendMock).toHaveBeenCalledWith('broadcastStatusTrack') — so the side effect is covered rather than merely leaked.

To dismiss: /dk-review dismiss 3f2b8c41-9d6e-4a17-b0c5-7e21a4f8d093 [reason]

expect(ctx.requestHandlerName).toBe('builtin:status-track')
expect(setOperationName).toHaveBeenCalledWith('builtin:status-track')
expect(ctx.body).toEqual([])
})

it('works when tracing is disabled for the path', async () => {
const ctx: any = { body: undefined, tracing: undefined }

await statusTrackHandler(ctx as ServiceContext)

expect(ctx.requestHandlerName).toBe('builtin:status-track')
})
})
5 changes: 4 additions & 1 deletion src/service/worker/runtime/statusTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export const isStatusTrackBroadcast = (message: any): message is typeof BROADCAS
message === BROADCAST_STATUS_TRACK

export const statusTrackHandler = async (ctx: ServiceContext) => {
ctx.tracing?.currentSpan?.setOperationName('builtin:status-track')
// Parity with the other builtin handlers: name the request so its samples don't
// land in the catch-all `handler="undefined"` bucket.
ctx.requestHandlerName = 'builtin:status-track'
ctx.tracing?.currentSpan?.setOperationName(ctx.requestHandlerName)
if (!LINKED) {
process.send?.(BROADCAST_STATUS_TRACK)
}
Expand Down
Loading