@vtex/api 7.4.2 → 7.5.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/CHANGELOG.md CHANGED
@@ -5,7 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [7.5.0]
9
+
10
+ ### Added
11
+
12
+ - Kubernetes cluster identity and role (`VTEX_CLUSTER_ID` → `cluster_id`,
13
+ `VTEX_CLUSTER_ROLE` → `cluster_role`) as shared diagnostics resource
14
+ attributes, applied centrally to diagnostics metrics and logs. Values are
15
+ trimmed and omitted independently when empty, enabling cross-cluster
16
+ dashboards and incident isolation without consuming per-call custom
17
+ attribute limits.
18
+
19
+ ### Fixed
20
+
21
+ - `runtime_http_*` metrics no longer emit samples without a `handler` label. Requests
22
+ that never reach a named handler (unmatched paths, replica-level rate limit
23
+ rejections, errors before the route pipeline) were counted with
24
+ `handler: undefined`; Node's cluster IPC serializes worker registries as JSON, which
25
+ drops `undefined` values, so the aggregated `/metrics` exposed a second, unnamed
26
+ series that Prometheus reads as `handler=""`. Those requests are now labelled
27
+ `handler="undefined"` — the same value prom-client rendered locally before cluster
28
+ aggregation — keeping dashboards and alerts that filter on it working.
29
+ - `/_status` requests are now reported as `handler="builtin:status-track"`, matching
30
+ the other builtin handlers, instead of falling into the unnamed bucket.
9
31
 
10
32
  ## [7.4.0] - 2026-06-22
11
33
  ### Changed
@@ -45,6 +45,8 @@ export declare const AttributeKeys: {
45
45
  VTEX_IO_WORKSPACE_TYPE: "vtex_io.workspace.type";
46
46
  VTEX_IO_APP_ID: "vtex_io.app.id";
47
47
  VTEX_IO_APP_AUTHOR_TYPE: "vtex_io.app.author_type";
48
+ VTEX_IO_CLUSTER_ID: "vtex_io.cluster.id";
49
+ VTEX_IO_CLUSTER_ROLE: "vtex_io.cluster.role";
48
50
  };
49
51
  /** @deprecated Use HeaderKeys.CACHE_CONTROL instead */
50
52
  export declare const CACHE_CONTROL_HEADER: string;
@@ -114,6 +116,8 @@ export declare const HTTP_SERVER_PORT = 5050;
114
116
  export declare const MAX_WORKERS = 4;
115
117
  export declare const LINKED: boolean;
116
118
  export declare const REGION: string;
119
+ export declare const CLUSTER_ID: string;
120
+ export declare const CLUSTER_ROLE: string;
117
121
  export declare const PUBLIC_ENDPOINT: string;
118
122
  export declare const APP: {
119
123
  ID: string;
package/lib/constants.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LOG_CLIENT_INIT_TIMEOUT_MS = exports.cancellableMethods = exports.INSPECT_DEBUGGER_PORT = exports.PRODUCTION = exports.WORKSPACE = exports.ACCOUNT = exports.NODE_ENV = exports.APP = exports.PUBLIC_ENDPOINT = exports.REGION = exports.LINKED = exports.MAX_WORKERS = exports.HTTP_SERVER_PORT = exports.MAX_AGE = exports.UP_SIGNAL = exports.BODY_HASH = exports.PROVIDER_HEADER = exports.TRACE_ID_HEADER = exports.COLOSSUS_PARAMS_HEADER = exports.COLOSSUS_ROUTE_ID_HEADER = exports.COLOSSUS_ROUTE_DECLARER_HEADER = exports.EVENT_HANDLER_ID_HEADER = exports.EVENT_SUBJECT_HEADER = exports.EVENT_SENDER_HEADER = exports.EVENT_KEY_HEADER = exports.WORKSPACE_HEADER = exports.WORKSPACE_IS_PRODUCTION_HEADER = exports.PLATFORM_HEADER = exports.OPERATION_ID_HEADER = exports.ROUTER_CACHE_HEADER = exports.REQUEST_ID_HEADER = exports.CREDENTIAL_HEADER = exports.ACCOUNT_HEADER = exports.ETAG_HEADER = exports.META_HEADER_BUCKET = exports.META_HEADER = exports.BINDING_HEADER = exports.TENANT_HEADER = exports.FORWARDED_HOST_HEADER = exports.LOCALE_HEADER = exports.PRODUCT_HEADER = exports.SESSION_HEADER = exports.SEGMENT_HEADER = exports.CACHE_CONTROL_HEADER = exports.AttributeKeys = exports.HeaderKeys = exports.PID = exports.IS_IO = exports.DEFAULT_WORKSPACE = exports.NODE_VTEX_API_VERSION = void 0;
4
- exports.DIAGNOSTICS_TELEMETRY_ENABLED = exports.DK_APP_ID = exports.OTEL_EXPORTER_OTLP_ENDPOINT = exports.METRIC_CLIENT_INIT_TIMEOUT_MS = void 0;
3
+ exports.INSPECT_DEBUGGER_PORT = exports.PRODUCTION = exports.WORKSPACE = exports.ACCOUNT = exports.NODE_ENV = exports.APP = exports.PUBLIC_ENDPOINT = exports.CLUSTER_ROLE = exports.CLUSTER_ID = exports.REGION = exports.LINKED = exports.MAX_WORKERS = exports.HTTP_SERVER_PORT = exports.MAX_AGE = exports.UP_SIGNAL = exports.BODY_HASH = exports.PROVIDER_HEADER = exports.TRACE_ID_HEADER = exports.COLOSSUS_PARAMS_HEADER = exports.COLOSSUS_ROUTE_ID_HEADER = exports.COLOSSUS_ROUTE_DECLARER_HEADER = exports.EVENT_HANDLER_ID_HEADER = exports.EVENT_SUBJECT_HEADER = exports.EVENT_SENDER_HEADER = exports.EVENT_KEY_HEADER = exports.WORKSPACE_HEADER = exports.WORKSPACE_IS_PRODUCTION_HEADER = exports.PLATFORM_HEADER = exports.OPERATION_ID_HEADER = exports.ROUTER_CACHE_HEADER = exports.REQUEST_ID_HEADER = exports.CREDENTIAL_HEADER = exports.ACCOUNT_HEADER = exports.ETAG_HEADER = exports.META_HEADER_BUCKET = exports.META_HEADER = exports.BINDING_HEADER = exports.TENANT_HEADER = exports.FORWARDED_HOST_HEADER = exports.LOCALE_HEADER = exports.PRODUCT_HEADER = exports.SESSION_HEADER = exports.SEGMENT_HEADER = exports.CACHE_CONTROL_HEADER = exports.AttributeKeys = exports.HeaderKeys = exports.PID = exports.IS_IO = exports.DEFAULT_WORKSPACE = exports.NODE_VTEX_API_VERSION = void 0;
4
+ exports.DIAGNOSTICS_TELEMETRY_ENABLED = exports.DK_APP_ID = exports.OTEL_EXPORTER_OTLP_ENDPOINT = exports.METRIC_CLIENT_INIT_TIMEOUT_MS = exports.LOG_CLIENT_INIT_TIMEOUT_MS = exports.cancellableMethods = void 0;
5
5
  const app_1 = require("./utils/app");
6
6
  const diagnostics_semconv_1 = require("@vtex/diagnostics-semconv");
7
7
  // tslint:disable-next-line
@@ -55,6 +55,8 @@ exports.AttributeKeys = {
55
55
  VTEX_IO_WORKSPACE_TYPE: diagnostics_semconv_1.ATTR_VTEX_IO_WORKSPACE_TYPE,
56
56
  VTEX_IO_APP_ID: diagnostics_semconv_1.ATTR_VTEX_IO_APP_ID,
57
57
  VTEX_IO_APP_AUTHOR_TYPE: diagnostics_semconv_1.ATTR_VTEX_IO_APP_AUTHOR_TYPE,
58
+ VTEX_IO_CLUSTER_ID: diagnostics_semconv_1.ATTR_VTEX_IO_CLUSTER_ID,
59
+ VTEX_IO_CLUSTER_ROLE: diagnostics_semconv_1.ATTR_VTEX_IO_CLUSTER_ROLE,
58
60
  };
59
61
  /** @deprecated Use HeaderKeys.CACHE_CONTROL instead */
60
62
  exports.CACHE_CONTROL_HEADER = exports.HeaderKeys.CACHE_CONTROL;
@@ -123,6 +125,8 @@ exports.HTTP_SERVER_PORT = 5050;
123
125
  exports.MAX_WORKERS = 4;
124
126
  exports.LINKED = !!process.env.VTEX_APP_LINK;
125
127
  exports.REGION = process.env.VTEX_REGION;
128
+ exports.CLUSTER_ID = process.env.VTEX_CLUSTER_ID;
129
+ exports.CLUSTER_ROLE = process.env.VTEX_CLUSTER_ROLE;
126
130
  exports.PUBLIC_ENDPOINT = process.env.VTEX_PUBLIC_ENDPOINT || 'myvtex.com';
127
131
  exports.APP = {
128
132
  ID: process.env.VTEX_APP_ID,
@@ -4,6 +4,7 @@ exports.addOtelRequestMetricsMiddleware = void 0;
4
4
  const stream_1 = require("stream");
5
5
  const utils_1 = require("../../utils");
6
6
  const metrics_1 = require("./metrics");
7
+ const requestHandlerLabel_1 = require("./requestHandlerLabel");
7
8
  const INSTRUMENTS_INITIALIZATION_TIMEOUT = 500;
8
9
  const addOtelRequestMetricsMiddleware = () => {
9
10
  let instruments;
@@ -31,7 +32,9 @@ const addOtelRequestMetricsMiddleware = () => {
31
32
  instruments.concurrentRequests.add(1);
32
33
  ctx.req.once('aborted', () => {
33
34
  if (instruments) {
34
- instruments.abortedRequests.add(1, { [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName });
35
+ instruments.abortedRequests.add(1, {
36
+ [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
37
+ });
35
38
  }
36
39
  });
37
40
  let responseClosed = false;
@@ -43,14 +46,14 @@ const addOtelRequestMetricsMiddleware = () => {
43
46
  const responseLength = ctx.response.length;
44
47
  if (responseLength && instruments) {
45
48
  instruments.responseSizes.record(responseLength, {
46
- [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
49
+ [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
47
50
  [metrics_1.RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
48
51
  [metrics_1.RequestsMetricLabels.ACCOUNT_NAME]: ((_a = ctx.vtex) === null || _a === void 0 ? void 0 : _a.account) || 'unknown',
49
52
  });
50
53
  }
51
54
  if (instruments) {
52
55
  instruments.totalRequests.add(1, {
53
- [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
56
+ [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
54
57
  [metrics_1.RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
55
58
  [metrics_1.RequestsMetricLabels.ACCOUNT_NAME]: ((_b = ctx.vtex) === null || _b === void 0 ? void 0 : _b.account) || 'unknown',
56
59
  });
@@ -59,7 +62,7 @@ const addOtelRequestMetricsMiddleware = () => {
59
62
  var _a;
60
63
  if (instruments) {
61
64
  instruments.requestTimings.record((0, utils_1.hrToMillisFloat)(process.hrtime(start)), {
62
- [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName,
65
+ [metrics_1.RequestsMetricLabels.REQUEST_HANDLER]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
63
66
  [metrics_1.RequestsMetricLabels.STATUS_CODE]: ctx.response.status,
64
67
  [metrics_1.RequestsMetricLabels.ACCOUNT_NAME]: ((_a = ctx.vtex) === null || _a === void 0 ? void 0 : _a.account) || 'unknown',
65
68
  });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Label value reported for requests that never reached a named handler: unmatched
3
+ * paths (Koa answers its default 404), requests rejected before the route pipeline
4
+ * (replica-level rate limiter, errors in compress/recorder), and handlers that
5
+ * don't set `ctx.requestHandlerName`.
6
+ *
7
+ * The value must be a non-empty string. `ctx.requestHandlerName` is `undefined`
8
+ * for those requests, and Node's cluster IPC serializes each worker's registry as
9
+ * JSON, which drops properties whose value is `undefined`. The sample then reaches
10
+ * the aggregated `/metrics` with the `handler` label missing altogether, which
11
+ * Prometheus reads as `handler=""` — a distinct, unnamed series that shows up as
12
+ * an extra "Value" line in dashboards.
13
+ *
14
+ * `'undefined'` is deliberate rather than a nicer word: prom-client's local
15
+ * exposition already rendered `handler: undefined` as `handler="undefined"` before
16
+ * the cluster aggregation was introduced, so keeping that value makes the
17
+ * aggregated output match the historical series identity and keeps existing
18
+ * dashboards, filters and alerts (e.g. `handler!~"builtin:.*|undefined"`) working.
19
+ */
20
+ export declare const UNNAMED_REQUEST_HANDLER = "undefined";
21
+ /**
22
+ * Resolves the `handler` label value for a request, falling back to
23
+ * {@link UNNAMED_REQUEST_HANDLER} when the pipeline never named the handler.
24
+ * Empty strings fall back too, so the label is never emitted empty.
25
+ */
26
+ export declare const requestHandlerLabel: (requestHandlerName?: string) => string;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestHandlerLabel = exports.UNNAMED_REQUEST_HANDLER = void 0;
4
+ /**
5
+ * Label value reported for requests that never reached a named handler: unmatched
6
+ * paths (Koa answers its default 404), requests rejected before the route pipeline
7
+ * (replica-level rate limiter, errors in compress/recorder), and handlers that
8
+ * don't set `ctx.requestHandlerName`.
9
+ *
10
+ * The value must be a non-empty string. `ctx.requestHandlerName` is `undefined`
11
+ * for those requests, and Node's cluster IPC serializes each worker's registry as
12
+ * JSON, which drops properties whose value is `undefined`. The sample then reaches
13
+ * the aggregated `/metrics` with the `handler` label missing altogether, which
14
+ * Prometheus reads as `handler=""` — a distinct, unnamed series that shows up as
15
+ * an extra "Value" line in dashboards.
16
+ *
17
+ * `'undefined'` is deliberate rather than a nicer word: prom-client's local
18
+ * exposition already rendered `handler: undefined` as `handler="undefined"` before
19
+ * the cluster aggregation was introduced, so keeping that value makes the
20
+ * aggregated output match the historical series identity and keeps existing
21
+ * dashboards, filters and alerts (e.g. `handler!~"builtin:.*|undefined"`) working.
22
+ */
23
+ exports.UNNAMED_REQUEST_HANDLER = 'undefined';
24
+ /**
25
+ * Resolves the `handler` label value for a request, falling back to
26
+ * {@link UNNAMED_REQUEST_HANDLER} when the pipeline never named the handler.
27
+ * Empty strings fall back too, so the label is never emitted empty.
28
+ */
29
+ const requestHandlerLabel = (requestHandlerName) => requestHandlerName || exports.UNNAMED_REQUEST_HANDLER;
30
+ exports.requestHandlerLabel = requestHandlerLabel;
@@ -4,6 +4,7 @@ exports.addRequestMetricsMiddleware = void 0;
4
4
  const stream_1 = require("stream");
5
5
  const utils_1 = require("../../utils");
6
6
  const instruments_1 = require("../tracing/metrics/instruments");
7
+ const requestHandlerLabel_1 = require("./requestHandlerLabel");
7
8
  const addRequestMetricsMiddleware = () => {
8
9
  const concurrentRequests = (0, instruments_1.createConcurrentRequestsInstrument)();
9
10
  const requestTimings = (0, instruments_1.createRequestsTimingsInstrument)();
@@ -13,7 +14,7 @@ const addRequestMetricsMiddleware = () => {
13
14
  return async function addRequestMetrics(ctx, next) {
14
15
  const start = process.hrtime();
15
16
  concurrentRequests.inc(1);
16
- ctx.req.once('aborted', () => abortedRequests.inc({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName }, 1));
17
+ ctx.req.once('aborted', () => abortedRequests.inc({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName) }, 1));
17
18
  let responseClosed = false;
18
19
  ctx.res.once('close', () => (responseClosed = true));
19
20
  try {
@@ -22,15 +23,15 @@ const addRequestMetricsMiddleware = () => {
22
23
  finally {
23
24
  const responseLength = ctx.response.length;
24
25
  if (responseLength) {
25
- responseSizes.observe({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName }, responseLength);
26
+ responseSizes.observe({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName) }, responseLength);
26
27
  }
27
28
  totalRequests.inc({
28
- ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName,
29
+ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
29
30
  ["status_code" /* RequestsMetricLabels.STATUS_CODE */]: ctx.response.status,
30
31
  }, 1);
31
32
  const onResFinished = () => {
32
33
  requestTimings.observe({
33
- ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName,
34
+ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
34
35
  }, (0, utils_1.hrToMillisFloat)(process.hrtime(start)));
35
36
  concurrentRequests.dec(1);
36
37
  };
@@ -5,6 +5,7 @@ const instrumentation_koa_1 = require("@opentelemetry/instrumentation-koa");
5
5
  const diagnostics_nodejs_1 = require("@vtex/diagnostics-nodejs");
6
6
  const constants_1 = require("../../constants");
7
7
  const hostMetrics_1 = require("../metrics/instruments/hostMetrics");
8
+ const resourceAttributes_1 = require("./resourceAttributes");
8
9
  const APPLICATION_ID = constants_1.APP.ID || 'vtex-io-app';
9
10
  class TelemetryClientSingleton {
10
11
  static getInstance() {
@@ -58,6 +59,7 @@ class TelemetryClientSingleton {
58
59
  'version': constants_1.APP.VERSION || '',
59
60
  [constants_1.AttributeKeys.VTEX_IO_WORKSPACE_NAME]: constants_1.WORKSPACE,
60
61
  [constants_1.AttributeKeys.VTEX_IO_WORKSPACE_TYPE]: constants_1.PRODUCTION ? 'production' : 'development',
62
+ ...(0, resourceAttributes_1.getClusterResourceAttributes)(constants_1.CLUSTER_ID, constants_1.CLUSTER_ROLE),
61
63
  },
62
64
  // Use built-in no-op functionality when telemetry is disabled
63
65
  noop: !constants_1.DIAGNOSTICS_TELEMETRY_ENABLED,
@@ -0,0 +1 @@
1
+ export declare const getClusterResourceAttributes: (clusterId?: string, clusterRole?: string) => Record<string, string>;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getClusterResourceAttributes = void 0;
4
+ const constants_1 = require("../../constants");
5
+ const normalizeAttribute = (value) => {
6
+ const normalized = value === null || value === void 0 ? void 0 : value.trim();
7
+ return normalized || undefined;
8
+ };
9
+ const getClusterResourceAttributes = (clusterId, clusterRole) => {
10
+ const attributes = {};
11
+ const normalizedClusterId = normalizeAttribute(clusterId);
12
+ const normalizedClusterRole = normalizeAttribute(clusterRole);
13
+ if (normalizedClusterId) {
14
+ attributes[constants_1.AttributeKeys.VTEX_IO_CLUSTER_ID] = normalizedClusterId;
15
+ }
16
+ if (normalizedClusterRole) {
17
+ attributes[constants_1.AttributeKeys.VTEX_IO_CLUSTER_ROLE] = normalizedClusterRole;
18
+ }
19
+ return attributes;
20
+ };
21
+ exports.getClusterResourceAttributes = getClusterResourceAttributes;
@@ -15,7 +15,10 @@ const isStatusTrackBroadcast = (message) => message === BROADCAST_STATUS_TRACK;
15
15
  exports.isStatusTrackBroadcast = isStatusTrackBroadcast;
16
16
  const statusTrackHandler = async (ctx) => {
17
17
  var _a, _b, _c;
18
- (_b = (_a = ctx.tracing) === null || _a === void 0 ? void 0 : _a.currentSpan) === null || _b === void 0 ? void 0 : _b.setOperationName('builtin:status-track');
18
+ // Parity with the other builtin handlers: name the request so its samples don't
19
+ // land in the catch-all `handler="undefined"` bucket.
20
+ ctx.requestHandlerName = 'builtin:status-track';
21
+ (_b = (_a = ctx.tracing) === null || _a === void 0 ? void 0 : _a.currentSpan) === null || _b === void 0 ? void 0 : _b.setOperationName(ctx.requestHandlerName);
19
22
  if (!constants_1.LINKED) {
20
23
  (_c = process.send) === null || _c === void 0 ? void 0 : _c.call(process, BROADCAST_STATUS_TRACK);
21
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/api",
3
- "version": "7.4.2",
3
+ "version": "7.5.0",
4
4
  "description": "VTEX I/O API client",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -54,7 +54,7 @@
54
54
  "@types/koa": "^2.11.0",
55
55
  "@types/koa-compose": "^3.2.3",
56
56
  "@vtex/diagnostics-nodejs": "0.1.8-io",
57
- "@vtex/diagnostics-semconv": "^1.1.2",
57
+ "@vtex/diagnostics-semconv": "5.5.2",
58
58
  "@vtex/node-error-report": "^0.0.3",
59
59
  "@wry/equality": "^0.1.9",
60
60
  "agentkeepalive": "^4.0.2",