From 5c2db746a55f264afa261eb695d4081c34dc600e Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 11:12:24 +0200 Subject: [PATCH 1/4] fix(metrics): label hosted project metrics with the runtime's service, release and deployment --- docs/guides/project-metrics.md | 5 +++- src/metrics/index.test.ts | 55 +++++++++++++++++++++++++++++----- src/metrics/index.ts | 28 ++++++++++------- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/docs/guides/project-metrics.md b/docs/guides/project-metrics.md index 1c456f25be..70d101030f 100644 --- a/docs/guides/project-metrics.md +++ b/docs/guides/project-metrics.md @@ -41,7 +41,10 @@ When code runs inside Veryfront, the SDK adds request-scoped labels for `project_id`, `project_slug`, `environment`, and `branch` for preview requests. Preview requests without an explicit branch use `branch="main"`. User code should not provide or trust those labels for isolation; the platform-owned -request context wins. +request context wins. In shared Veryfront runtimes, `service_name`, +`service_version` and `deployment_environment` name the runtime service, its +release and the platform deployment, such as `staging`, that exported the +sample. ## Emit eval metrics diff --git a/src/metrics/index.test.ts b/src/metrics/index.test.ts index 749b357797..e6c1f939e2 100644 --- a/src/metrics/index.test.ts +++ b/src/metrics/index.test.ts @@ -484,6 +484,46 @@ describe("metrics public SDK", () => { ); }); + it("labels hosted project metrics with the runtime's service, release and deployment", async () => { + const requests: RequestInit[] = []; + + await withEnv({ + OTEL_METRICS_ENABLED: "true", + OTEL_SERVICE_NAME: "veryfront-server", + OTEL_SERVICE_VERSION: "20260927085934-9420d79fc0e2", + OTEL_DEPLOYMENT_ENVIRONMENT: "staging", + VERYFRONT_API_BASE_URL: "http://veryfront-api:80", + VERYFRONT_API_INTERNAL_USER: "internal-user", + VERYFRONT_API_INTERNAL_PASS: "internal-pass", + }, async () => { + await withMockFetch( + ((_url: string | URL | Request, init?: RequestInit) => { + requests.push(init ?? {}); + return Promise.resolve(new Response("{}", { status: 200 })); + }) as typeof fetch, + async () => { + runWithTrustedProjectEnv( + { OTEL_SERVICE_NAME: "project-chosen", OTEL_DEPLOYMENT_ENVIRONMENT: "production" }, + { projectId: "project-123", environmentId: "env-1" }, + () => metrics.counter("vf_hosted_metric_total", 1), + ); + await metrics.__flushForTests(); + }, + ); + }); + + const resource = JSON.parse(String(requests[0]?.body)).resourceMetrics[0].resource; + const attributes = Object.fromEntries( + resource.attributes.map((attribute: { key: string; value: { stringValue: string } }) => [ + attribute.key, + attribute.value.stringValue, + ]), + ); + assertEquals(attributes["service.name"], "veryfront-server"); + assertEquals(attributes["service.version"], "20260927085934-9420d79fc0e2"); + assertEquals(attributes["deployment.environment"], "staging"); + }); + it("does not expose internal metrics credentials to a replaced Base64 encoder", async () => { const originalBtoa = Object.getOwnPropertyDescriptor(globalThis, "btoa"); const observedValues: string[] = []; @@ -1018,7 +1058,7 @@ describe("metrics public SDK", () => { assertEquals(metrics.__getDirectTargetCountForTests(), 17); }); - it("applies tenant target quotas to metrics routed through the internal proxy", async () => { + it("keeps one internal proxy target per project whatever telemetry env the project sets", async () => { await withEnv({ SERVER_ID: "server-1", ENVIRONMENT_IDS: "env-project", @@ -1054,7 +1094,7 @@ describe("metrics public SDK", () => { ); }); - assertEquals(metrics.__getDirectTargetCountForTests(), 17); + assertEquals(metrics.__getDirectTargetCountForTests(), 2); }); it("evicts credential-bearing targets without consulting Array species", async () => { @@ -1071,10 +1111,10 @@ describe("metrics public SDK", () => { await withMockFetch( (() => Promise.resolve(new Response("{}", { status: 200 }))) as typeof fetch, async () => { - for (let index = 0; index < 16; index++) { + for (let index = 0; index < 90; index++) { runWithTrustedProjectEnv( - { OTEL_METRICS_ENABLED: "true", OTEL_SERVICE_NAME: `project-${index}` }, - { projectId: "project-a", environmentId: "env-a" }, + { OTEL_METRICS_ENABLED: "true" }, + { projectId: `project-${index}`, environmentId: `env-${index}` }, () => metrics.counter("vf_project_metric_total", 1), ); } @@ -1089,8 +1129,8 @@ describe("metrics public SDK", () => { }); try { runWithTrustedProjectEnv( - { OTEL_METRICS_ENABLED: "true", OTEL_SERVICE_NAME: "project-new" }, - { projectId: "project-a", environmentId: "env-a" }, + { OTEL_METRICS_ENABLED: "true" }, + { projectId: "project-new", environmentId: "env-new" }, () => metrics.counter("vf_project_metric_total", 1), ); } finally { @@ -1101,6 +1141,7 @@ describe("metrics public SDK", () => { }); assertEquals(speciesCalls, 0); + assertEquals(metrics.__getDirectTargetCountForTests(), 90, "the oldest target was evicted"); }); it("dispatches every target without consulting Promise species", async () => { diff --git a/src/metrics/index.ts b/src/metrics/index.ts index 469b28af5c..3ef97b11a0 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -49,6 +49,7 @@ interface DirectMetricsTarget { headers: Record; serviceName: string; serviceVersion: string; + deploymentEnvironment: string | undefined; capacityScope: string; internal: boolean; tenantScoped: boolean; @@ -415,15 +416,18 @@ function parseHeaders(headerInput: string | undefined): Record { return result; } -function resolveDirectServiceIdentity(): Pick< - DirectMetricsTarget, - "serviceName" | "serviceVersion" -> { +// Shared runtimes read the identity from the host, so hosted project metrics +// name the platform service, release and deployment that emitted them. +function resolveDirectServiceIdentity( + read: (name: string) => string | undefined, +): Pick { return { - serviceName: readEnv("OTEL_SERVICE_NAME") ?? "veryfront", - serviceVersion: readEnv("VERYFRONT_VERSION") ?? - readEnv("RELEASE_VERSION") ?? + serviceName: read("OTEL_SERVICE_NAME") ?? "veryfront", + serviceVersion: read("OTEL_SERVICE_VERSION") ?? + read("VERYFRONT_VERSION") ?? + read("RELEASE_VERSION") ?? "unknown", + deploymentEnvironment: read("OTEL_DEPLOYMENT_ENVIRONMENT"), }; } @@ -467,7 +471,7 @@ function resolveDirectMetricsTarget(): DirectMetricsTarget | null { readProjectEnv("OTEL_EXPORTER_OTLP_METRICS_HEADERS") ?? readProjectEnv("OTEL_EXPORTER_OTLP_HEADERS"), ), - ...resolveDirectServiceIdentity(), + ...resolveDirectServiceIdentity(readEnv), capacityScope: resolveDirectCapacityScope(), internal: false, tenantScoped: true, @@ -485,7 +489,7 @@ function resolveDirectMetricsTarget(): DirectMetricsTarget | null { readHostEnv("VERYFRONT_API_INTERNAL_PASS") ?? "", ), }, - ...resolveDirectServiceIdentity(), + ...resolveDirectServiceIdentity(readHostEnv), capacityScope: tenantScoped ? resolveDirectCapacityScope() : "internal", internal: true, tenantScoped, @@ -501,7 +505,7 @@ function resolveDirectMetricsTarget(): DirectMetricsTarget | null { readEnv("OTEL_EXPORTER_OTLP_METRICS_HEADERS") ?? readEnv("OTEL_EXPORTER_OTLP_HEADERS"), ), - ...resolveDirectServiceIdentity(), + ...resolveDirectServiceIdentity(readEnv), capacityScope: tenantScoped ? resolveDirectCapacityScope() : "host", internal: false, tenantScoped, @@ -669,6 +673,7 @@ function retainDirectTarget(target: DirectMetricsTarget): string | null { interned.target.url === target.url && interned.target.serviceName === target.serviceName && interned.target.serviceVersion === target.serviceVersion && + interned.target.deploymentEnvironment === target.deploymentEnvironment && interned.target.capacityScope === target.capacityScope && interned.target.internal === target.internal && interned.target.tenantScoped === target.tenantScoped && @@ -887,6 +892,9 @@ function buildDirectOtlpBody( "service.name": target.serviceName, "service.version": target.serviceVersion, "service.instance.id": serviceInstanceId, + ...(target.deploymentEnvironment === undefined + ? {} + : { "deployment.environment": target.deploymentEnvironment }), }), }, scopeMetrics: [{ From 1b209174457bf9ccb1271b3763b40920d1e89082 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 11:16:14 +0200 Subject: [PATCH 2/4] fix(metrics): read the hosted identity from OTEL_RESOURCE_ATTRIBUTES too --- src/metrics/index.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/metrics/index.ts | 17 +++++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/metrics/index.test.ts b/src/metrics/index.test.ts index e6c1f939e2..a3cf42ca39 100644 --- a/src/metrics/index.test.ts +++ b/src/metrics/index.test.ts @@ -524,6 +524,41 @@ describe("metrics public SDK", () => { assertEquals(attributes["deployment.environment"], "staging"); }); + it("reads the hosted runtime identity from OTEL_RESOURCE_ATTRIBUTES", async () => { + const requests: RequestInit[] = []; + + await withEnv({ + OTEL_METRICS_ENABLED: "true", + OTEL_RESOURCE_ATTRIBUTES: + "service.name=veryfront-server,service.version=release-1,deployment.environment.name=staging", + VERYFRONT_API_BASE_URL: "http://veryfront-api:80", + VERYFRONT_API_INTERNAL_USER: "internal-user", + VERYFRONT_API_INTERNAL_PASS: "internal-pass", + }, async () => { + await withMockFetch( + ((_url: string | URL | Request, init?: RequestInit) => { + requests.push(init ?? {}); + return Promise.resolve(new Response("{}", { status: 200 })); + }) as typeof fetch, + async () => { + metrics.counter("vf_hosted_metric_total", 1); + await metrics.__flushForTests(); + }, + ); + }); + + const resource = JSON.parse(String(requests[0]?.body)).resourceMetrics[0].resource; + const attributes = Object.fromEntries( + resource.attributes.map((attribute: { key: string; value: { stringValue: string } }) => [ + attribute.key, + attribute.value.stringValue, + ]), + ); + assertEquals(attributes["service.name"], "veryfront-server"); + assertEquals(attributes["service.version"], "release-1"); + assertEquals(attributes["deployment.environment"], "staging"); + }); + it("does not expose internal metrics credentials to a replaced Base64 encoder", async () => { const originalBtoa = Object.getOwnPropertyDescriptor(globalThis, "btoa"); const observedValues: string[] = []; diff --git a/src/metrics/index.ts b/src/metrics/index.ts index 3ef97b11a0..b43e89cb6c 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -405,9 +405,14 @@ function parseHeaders(headerInput: string | undefined): Record { if (headerInput.startsWith("Authorization=")) { return { Authorization: headerInput.slice("Authorization=".length) }; } + return parseKeyValueList(headerInput); +} +/** Parse the OTel `key=value,key=value` env format. */ +function parseKeyValueList(input: string | undefined): Record { + if (!input) return {}; const result: Record = {}; - for (const part of headerInput.split(",")) { + for (const part of input.split(",")) { const [key, ...valueParts] = part.split("="); if (key && valueParts.length > 0) { result[key.trim()] = valueParts.join("=").trim(); @@ -421,13 +426,17 @@ function parseHeaders(headerInput: string | undefined): Record { function resolveDirectServiceIdentity( read: (name: string) => string | undefined, ): Pick { + const resource = parseKeyValueList(read("OTEL_RESOURCE_ATTRIBUTES")); return { - serviceName: read("OTEL_SERVICE_NAME") ?? "veryfront", - serviceVersion: read("OTEL_SERVICE_VERSION") ?? + serviceName: read("OTEL_SERVICE_NAME") ?? resource["service.name"] ?? "veryfront", + serviceVersion: resource["service.version"] ?? + read("OTEL_SERVICE_VERSION") ?? read("VERYFRONT_VERSION") ?? read("RELEASE_VERSION") ?? "unknown", - deploymentEnvironment: read("OTEL_DEPLOYMENT_ENVIRONMENT"), + deploymentEnvironment: resource["deployment.environment.name"] ?? + resource["deployment.environment"] ?? + read("OTEL_DEPLOYMENT_ENVIRONMENT"), }; } From 9617ffb49de28d1a2fe7aea256818cbbc3245f2c Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 11:30:02 +0200 Subject: [PATCH 3/4] fix(metrics): parse OTel key-value env into a null-prototype record --- src/metrics/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/metrics/index.ts b/src/metrics/index.ts index b43e89cb6c..a3324c3c8b 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -410,8 +410,9 @@ function parseHeaders(headerInput: string | undefined): Record { /** Parse the OTel `key=value,key=value` env format. */ function parseKeyValueList(input: string | undefined): Record { - if (!input) return {}; - const result: Record = {}; + // No prototype, so a value project code plants on Object.prototype is never read as a host key. + const result = apply(objectCreate, Object, [null]) as Record; + if (!input) return result; for (const part of input.split(",")) { const [key, ...valueParts] = part.split("="); if (key && valueParts.length > 0) { From df7bb5e07a49acd7ced51ba92b774ed95ac369de Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 11:40:37 +0200 Subject: [PATCH 4/4] fix(metrics): parse host telemetry env with captured string intrinsics --- src/metrics/index.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/metrics/index.ts b/src/metrics/index.ts index a3324c3c8b..6397a51888 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -143,6 +143,9 @@ const setHas = Set.prototype.has; const regExpTest = RegExp.prototype.test; const arrayIncludes = Array.prototype.includes; const stringStartsWith = String.prototype.startsWith; +const stringIndexOf = String.prototype.indexOf; +const stringSlice = String.prototype.slice; +const stringTrim = String.prototype.trim; const weakMapDelete = WeakMap.prototype.delete; const weakMapGet = WeakMap.prototype.get; const weakMapSet = WeakMap.prototype.set; @@ -410,14 +413,21 @@ function parseHeaders(headerInput: string | undefined): Record { /** Parse the OTel `key=value,key=value` env format. */ function parseKeyValueList(input: string | undefined): Record { - // No prototype, so a value project code plants on Object.prototype is never read as a host key. + // Host values are parsed with captured intrinsics into a record without a + // prototype, so project code that patched String or Object never sees them. const result = apply(objectCreate, Object, [null]) as Record; if (!input) return result; - for (const part of input.split(",")) { - const [key, ...valueParts] = part.split("="); - if (key && valueParts.length > 0) { - result[key.trim()] = valueParts.join("=").trim(); + let start = 0; + while (start <= input.length) { + let end = apply(stringIndexOf, input, [",", start]) as number; + if (end === -1) end = input.length; + const part = apply(stringSlice, input, [start, end]) as string; + const separator = apply(stringIndexOf, part, ["="]) as number; + if (separator > 0) { + const key = apply(stringTrim, apply(stringSlice, part, [0, separator]), []) as string; + result[key] = apply(stringTrim, apply(stringSlice, part, [separator + 1]), []) as string; } + start = end + 1; } return result; }