From 91b6f39d13ed79e777eb814cae0c4d644b069af8 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 9 Jun 2026 07:31:12 -0700 Subject: [PATCH 1/4] feat(instrumentation): Add deployment metadata to spans Attach the Vercel deployment ID and release version to every outgoing Sentry span so traces can be filtered by the exact deployment that emitted them. Document deployment.id in the tracing semantics specs alongside the existing deployment environment metadata. Co-Authored-By: GPT-5 Codex --- packages/junior/src/instrumentation.ts | 26 ++++++++++++++++++++++++++ specs/otel-semantics.md | 1 + specs/tracing.md | 1 + 3 files changed, 28 insertions(+) diff --git a/packages/junior/src/instrumentation.ts b/packages/junior/src/instrumentation.ts index 00e8eb509..b8964aad0 100644 --- a/packages/junior/src/instrumentation.ts +++ b/packages/junior/src/instrumentation.ts @@ -14,6 +14,19 @@ function getBoolean(value: string | undefined, fallback: boolean): boolean { return fallback; } +function getDeploymentSpanAttributes(): Record { + const attributes: Record = {}; + const serviceVersion = + process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA; + if (serviceVersion) { + attributes["service.version"] = serviceVersion; + } + if (process.env.VERCEL_DEPLOYMENT_ID) { + attributes["deployment.id"] = process.env.VERCEL_DEPLOYMENT_ID; + } + return attributes; +} + /** Initialize Sentry for the Junior runtime. Call at the top of your entry point. */ export function initSentry(): void { if (Sentry.getClient()) { @@ -22,6 +35,7 @@ export function initSentry(): void { const dsn = process.env.SENTRY_DSN; const enableLogs = getBoolean(process.env.SENTRY_ENABLE_LOGS, Boolean(dsn)); + const deploymentSpanAttributes = getDeploymentSpanAttributes(); Sentry.init({ dsn, @@ -36,6 +50,18 @@ export function initSentry(): void { enableLogs, registerEsmLoaderHooks: false, streamGenAiSpans: true, + beforeSendSpan(span) { + if (Object.keys(deploymentSpanAttributes).length === 0) { + return span; + } + + span.data = { + ...span.data, + ...deploymentSpanAttributes, + }; + + return span; + }, integrations: [ Sentry.vercelAIIntegration({ recordInputs: true, diff --git a/specs/otel-semantics.md b/specs/otel-semantics.md index 6fdac9601..f414a0714 100644 --- a/specs/otel-semantics.md +++ b/specs/otel-semantics.md @@ -33,6 +33,7 @@ This file is the canonical attribute and naming map for instrumentation in this - `service.name` - `service.version` +- `deployment.id` - `deployment.environment.name` - `trace_id` - `span_id` diff --git a/specs/tracing.md b/specs/tracing.md index e6b2cd08f..878cc2476 100644 --- a/specs/tracing.md +++ b/specs/tracing.md @@ -49,6 +49,7 @@ Define the canonical tracing contract for span naming, boundaries, attributes, a - `service.name` (when available) - `service.version` (when available) +- `deployment.id` (when available) - `deployment.environment.name` (when available) ### Correlation Context From fff878892ca437fb0a896c76c5cff8f5eb5a84c0 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 9 Jun 2026 07:38:42 -0700 Subject: [PATCH 2/4] feat(instrumentation): Add deployment metadata to logs Share Vercel deployment telemetry attributes between span enrichment and the logging facade so emitted log records carry the same deployment.id and service.version values. Cover the logging attribute contract with a focused unit test. Co-Authored-By: GPT-5 Codex --- .../junior/src/chat/deployment-attributes.ts | 13 ++++ packages/junior/src/chat/logging.ts | 3 + packages/junior/src/instrumentation.ts | 16 +---- .../logging/deployment-attributes.test.ts | 72 +++++++++++++++++++ 4 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 packages/junior/src/chat/deployment-attributes.ts create mode 100644 packages/junior/tests/unit/logging/deployment-attributes.test.ts diff --git a/packages/junior/src/chat/deployment-attributes.ts b/packages/junior/src/chat/deployment-attributes.ts new file mode 100644 index 000000000..932de566a --- /dev/null +++ b/packages/junior/src/chat/deployment-attributes.ts @@ -0,0 +1,13 @@ +/** Resolve deployment-scoped telemetry attributes from host environment. */ +export function getDeploymentTelemetryAttributes(): Record { + const attributes: Record = {}; + const serviceVersion = + process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA; + if (serviceVersion) { + attributes["service.version"] = serviceVersion; + } + if (process.env.VERCEL_DEPLOYMENT_ID) { + attributes["deployment.id"] = process.env.VERCEL_DEPLOYMENT_ID; + } + return attributes; +} diff --git a/packages/junior/src/chat/logging.ts b/packages/junior/src/chat/logging.ts index 31dce8625..3ca72d577 100644 --- a/packages/junior/src/chat/logging.ts +++ b/packages/junior/src/chat/logging.ts @@ -15,6 +15,7 @@ import type { LogLevel as ChatSdkLogLevel, } from "chat"; import { toOptionalNumber, toOptionalString } from "@/chat/coerce"; +import { getDeploymentTelemetryAttributes } from "@/chat/deployment-attributes"; import * as Sentry from "@/chat/sentry"; import type { AgentTurnUsage } from "@/chat/usage"; @@ -125,6 +126,7 @@ function normalizeGenAiFinishReasons(value: unknown): unknown { const contextStorage = new AsyncLocalStorage(); const logRecordSinks = new Set<(record: EmittedLogRecord) => void>(); +const deploymentLogAttributes = getDeploymentTelemetryAttributes(); type ConsoleTextStyle = Parameters[0]; const LOGTAPE_BODY_KEY = "__logtape_body"; const ROOT_LOGGER_CATEGORY = ["junior"] as const; @@ -1121,6 +1123,7 @@ function emitRecord( ? undefined : contextStorage.getStore(); const attributes = mergeAttributes(contextAttributes, traceAttributes, { + ...deploymentLogAttributes, "event.name": normalizedEventName, ...(source ? { "app.log.source": source } : {}), ...attrs, diff --git a/packages/junior/src/instrumentation.ts b/packages/junior/src/instrumentation.ts index b8964aad0..68dffe361 100644 --- a/packages/junior/src/instrumentation.ts +++ b/packages/junior/src/instrumentation.ts @@ -1,4 +1,5 @@ import * as Sentry from "@/chat/sentry"; +import { getDeploymentTelemetryAttributes } from "@/chat/deployment-attributes"; function getSampleRate(value: string | undefined, fallback: number): number { if (!value) return fallback; @@ -14,19 +15,6 @@ function getBoolean(value: string | undefined, fallback: boolean): boolean { return fallback; } -function getDeploymentSpanAttributes(): Record { - const attributes: Record = {}; - const serviceVersion = - process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA; - if (serviceVersion) { - attributes["service.version"] = serviceVersion; - } - if (process.env.VERCEL_DEPLOYMENT_ID) { - attributes["deployment.id"] = process.env.VERCEL_DEPLOYMENT_ID; - } - return attributes; -} - /** Initialize Sentry for the Junior runtime. Call at the top of your entry point. */ export function initSentry(): void { if (Sentry.getClient()) { @@ -35,7 +23,7 @@ export function initSentry(): void { const dsn = process.env.SENTRY_DSN; const enableLogs = getBoolean(process.env.SENTRY_ENABLE_LOGS, Boolean(dsn)); - const deploymentSpanAttributes = getDeploymentSpanAttributes(); + const deploymentSpanAttributes = getDeploymentTelemetryAttributes(); Sentry.init({ dsn, diff --git a/packages/junior/tests/unit/logging/deployment-attributes.test.ts b/packages/junior/tests/unit/logging/deployment-attributes.test.ts new file mode 100644 index 000000000..42299a08f --- /dev/null +++ b/packages/junior/tests/unit/logging/deployment-attributes.test.ts @@ -0,0 +1,72 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { EmittedLogRecord } from "@/chat/logging"; + +const ORIGINAL_SENTRY_RELEASE = process.env.SENTRY_RELEASE; +const ORIGINAL_VERCEL_DEPLOYMENT_ID = process.env.VERCEL_DEPLOYMENT_ID; +const ORIGINAL_VERCEL_GIT_COMMIT_SHA = process.env.VERCEL_GIT_COMMIT_SHA; + +async function loadLoggingModule() { + vi.resetModules(); + vi.doMock("@/chat/sentry", () => ({ + captureException: undefined, + captureMessage: undefined, + getActiveSpan: () => undefined, + logger: {}, + setTag: undefined, + setUser: undefined, + spanToJSON: () => ({}), + withScope: undefined, + })); + return await import("@/chat/logging"); +} + +function resetEnv(): void { + if (ORIGINAL_SENTRY_RELEASE === undefined) { + delete process.env.SENTRY_RELEASE; + } else { + process.env.SENTRY_RELEASE = ORIGINAL_SENTRY_RELEASE; + } + if (ORIGINAL_VERCEL_DEPLOYMENT_ID === undefined) { + delete process.env.VERCEL_DEPLOYMENT_ID; + } else { + process.env.VERCEL_DEPLOYMENT_ID = ORIGINAL_VERCEL_DEPLOYMENT_ID; + } + if (ORIGINAL_VERCEL_GIT_COMMIT_SHA === undefined) { + delete process.env.VERCEL_GIT_COMMIT_SHA; + } else { + process.env.VERCEL_GIT_COMMIT_SHA = ORIGINAL_VERCEL_GIT_COMMIT_SHA; + } +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); + vi.doUnmock("@/chat/sentry"); + resetEnv(); +}); + +describe("deployment log attributes", () => { + it("adds deployment metadata to emitted log records", async () => { + process.env.SENTRY_RELEASE = "release-abc123"; + process.env.VERCEL_DEPLOYMENT_ID = "dpl_123"; + process.env.VERCEL_GIT_COMMIT_SHA = "git-sha"; + + const { log, registerLogRecordSink } = await loadLoggingModule(); + const records: EmittedLogRecord[] = []; + const unregister = registerLogRecordSink((record) => { + records.push(record); + }); + + try { + log.warn("deployment_context_test", {}, "Deployment context test"); + } finally { + unregister(); + } + + expect(records).toHaveLength(1); + expect(records[0]?.attributes).toMatchObject({ + "deployment.id": "dpl_123", + "service.version": "release-abc123", + }); + }); +}); From c4012ac3a1bea325b5c28cc51e2cf92f0557a9ef Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 9 Jun 2026 07:53:50 -0700 Subject: [PATCH 3/4] fix(instrumentation): Harden deployment telemetry attributes Normalize deployment telemetry env values before emitting them on spans and logs. Keep deployment metadata authoritative over event-local log attributes, move the shared helper out of chat ownership, and document the logging contract. Add focused span enrichment coverage alongside the log record test. Co-Authored-By: GPT-5 Codex --- .../junior/src/chat/deployment-attributes.ts | 13 ---- packages/junior/src/chat/logging.ts | 19 +++-- packages/junior/src/deployment-telemetry.ts | 20 +++++ packages/junior/src/instrumentation.ts | 3 +- .../junior/tests/unit/instrumentation.test.ts | 77 +++++++++++++++++++ .../logging/deployment-attributes.test.ts | 13 +++- specs/instrumentation.md | 15 +++- specs/logging.md | 3 +- specs/otel-semantics.md | 2 +- specs/tracing.md | 2 +- 10 files changed, 139 insertions(+), 28 deletions(-) delete mode 100644 packages/junior/src/chat/deployment-attributes.ts create mode 100644 packages/junior/src/deployment-telemetry.ts create mode 100644 packages/junior/tests/unit/instrumentation.test.ts diff --git a/packages/junior/src/chat/deployment-attributes.ts b/packages/junior/src/chat/deployment-attributes.ts deleted file mode 100644 index 932de566a..000000000 --- a/packages/junior/src/chat/deployment-attributes.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** Resolve deployment-scoped telemetry attributes from host environment. */ -export function getDeploymentTelemetryAttributes(): Record { - const attributes: Record = {}; - const serviceVersion = - process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA; - if (serviceVersion) { - attributes["service.version"] = serviceVersion; - } - if (process.env.VERCEL_DEPLOYMENT_ID) { - attributes["deployment.id"] = process.env.VERCEL_DEPLOYMENT_ID; - } - return attributes; -} diff --git a/packages/junior/src/chat/logging.ts b/packages/junior/src/chat/logging.ts index 3ca72d577..42e7f738d 100644 --- a/packages/junior/src/chat/logging.ts +++ b/packages/junior/src/chat/logging.ts @@ -15,9 +15,9 @@ import type { LogLevel as ChatSdkLogLevel, } from "chat"; import { toOptionalNumber, toOptionalString } from "@/chat/coerce"; -import { getDeploymentTelemetryAttributes } from "@/chat/deployment-attributes"; import * as Sentry from "@/chat/sentry"; import type { AgentTurnUsage } from "@/chat/usage"; +import { getDeploymentTelemetryAttributes } from "@/deployment-telemetry"; type Primitive = string | number | boolean; type AttributeValue = Primitive | string[]; @@ -1122,12 +1122,17 @@ function emitRecord( const contextAttributes = ownsLogTapeBackend ? undefined : contextStorage.getStore(); - const attributes = mergeAttributes(contextAttributes, traceAttributes, { - ...deploymentLogAttributes, - "event.name": normalizedEventName, - ...(source ? { "app.log.source": source } : {}), - ...attrs, - }); + const attributes = mergeAttributes( + contextAttributes, + traceAttributes, + { + "event.name": normalizedEventName, + ...(source ? { "app.log.source": source } : {}), + ...attrs, + }, + // Deployment identity is process-owned and must win over event-local attrs. + deploymentLogAttributes, + ); if (usesDirectEmissionFallback) { emitDirect(level, normalizedEventName, message, attributes); diff --git a/packages/junior/src/deployment-telemetry.ts b/packages/junior/src/deployment-telemetry.ts new file mode 100644 index 000000000..b4ec59a45 --- /dev/null +++ b/packages/junior/src/deployment-telemetry.ts @@ -0,0 +1,20 @@ +function toOptionalTrimmed(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +/** Resolve deployment-scoped telemetry attributes from host environment. */ +export function getDeploymentTelemetryAttributes(): Record { + const attributes: Record = {}; + const serviceVersion = + toOptionalTrimmed(process.env.SENTRY_RELEASE) ?? + toOptionalTrimmed(process.env.VERCEL_GIT_COMMIT_SHA); + const deploymentId = toOptionalTrimmed(process.env.VERCEL_DEPLOYMENT_ID); + if (serviceVersion) { + attributes["service.version"] = serviceVersion; + } + if (deploymentId) { + attributes["deployment.id"] = deploymentId; + } + return attributes; +} diff --git a/packages/junior/src/instrumentation.ts b/packages/junior/src/instrumentation.ts index 68dffe361..7950ddeca 100644 --- a/packages/junior/src/instrumentation.ts +++ b/packages/junior/src/instrumentation.ts @@ -1,5 +1,5 @@ import * as Sentry from "@/chat/sentry"; -import { getDeploymentTelemetryAttributes } from "@/chat/deployment-attributes"; +import { getDeploymentTelemetryAttributes } from "@/deployment-telemetry"; function getSampleRate(value: string | undefined, fallback: number): number { if (!value) return fallback; @@ -38,6 +38,7 @@ export function initSentry(): void { enableLogs, registerEsmLoaderHooks: false, streamGenAiSpans: true, + // Keep deployment identity centralized so every emitted Sentry span carries it. beforeSendSpan(span) { if (Object.keys(deploymentSpanAttributes).length === 0) { return span; diff --git a/packages/junior/tests/unit/instrumentation.test.ts b/packages/junior/tests/unit/instrumentation.test.ts new file mode 100644 index 000000000..41057c6cf --- /dev/null +++ b/packages/junior/tests/unit/instrumentation.test.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const ORIGINAL_SENTRY_DSN = process.env.SENTRY_DSN; +const ORIGINAL_SENTRY_RELEASE = process.env.SENTRY_RELEASE; +const ORIGINAL_VERCEL_DEPLOYMENT_ID = process.env.VERCEL_DEPLOYMENT_ID; +const ORIGINAL_VERCEL_GIT_COMMIT_SHA = process.env.VERCEL_GIT_COMMIT_SHA; + +function resetEnv(): void { + if (ORIGINAL_SENTRY_DSN === undefined) { + delete process.env.SENTRY_DSN; + } else { + process.env.SENTRY_DSN = ORIGINAL_SENTRY_DSN; + } + if (ORIGINAL_SENTRY_RELEASE === undefined) { + delete process.env.SENTRY_RELEASE; + } else { + process.env.SENTRY_RELEASE = ORIGINAL_SENTRY_RELEASE; + } + if (ORIGINAL_VERCEL_DEPLOYMENT_ID === undefined) { + delete process.env.VERCEL_DEPLOYMENT_ID; + } else { + process.env.VERCEL_DEPLOYMENT_ID = ORIGINAL_VERCEL_DEPLOYMENT_ID; + } + if (ORIGINAL_VERCEL_GIT_COMMIT_SHA === undefined) { + delete process.env.VERCEL_GIT_COMMIT_SHA; + } else { + process.env.VERCEL_GIT_COMMIT_SHA = ORIGINAL_VERCEL_GIT_COMMIT_SHA; + } +} + +async function loadInstrumentationModule() { + vi.resetModules(); + const init = vi.fn(); + vi.doMock("@/chat/sentry", () => ({ + getClient: () => undefined, + init, + vercelAIIntegration: () => ({ name: "vercel-ai" }), + })); + const instrumentation = await import("@/instrumentation"); + return { init, instrumentation }; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); + vi.doUnmock("@/chat/sentry"); + resetEnv(); +}); + +describe("initSentry", () => { + it("adds deployment metadata to outgoing spans", async () => { + process.env.SENTRY_DSN = "https://public@example.com/1"; + process.env.SENTRY_RELEASE = " "; + process.env.VERCEL_DEPLOYMENT_ID = "dpl_123"; + process.env.VERCEL_GIT_COMMIT_SHA = "git-sha"; + + const { init, instrumentation } = await loadInstrumentationModule(); + instrumentation.initSentry(); + + expect(init).toHaveBeenCalledTimes(1); + const options = init.mock.calls[0]?.[0]; + expect(options?.beforeSendSpan).toBeTypeOf("function"); + + const span = { + data: { + "deployment.id": "span-deployment", + "service.version": "span-version", + }, + }; + + expect(options.beforeSendSpan(span)).toBe(span); + expect(span.data).toMatchObject({ + "deployment.id": "dpl_123", + "service.version": "git-sha", + }); + }); +}); diff --git a/packages/junior/tests/unit/logging/deployment-attributes.test.ts b/packages/junior/tests/unit/logging/deployment-attributes.test.ts index 42299a08f..7002be868 100644 --- a/packages/junior/tests/unit/logging/deployment-attributes.test.ts +++ b/packages/junior/tests/unit/logging/deployment-attributes.test.ts @@ -47,7 +47,7 @@ afterEach(() => { describe("deployment log attributes", () => { it("adds deployment metadata to emitted log records", async () => { - process.env.SENTRY_RELEASE = "release-abc123"; + process.env.SENTRY_RELEASE = " "; process.env.VERCEL_DEPLOYMENT_ID = "dpl_123"; process.env.VERCEL_GIT_COMMIT_SHA = "git-sha"; @@ -58,7 +58,14 @@ describe("deployment log attributes", () => { }); try { - log.warn("deployment_context_test", {}, "Deployment context test"); + log.warn( + "deployment_context_test", + { + "deployment.id": "caller-deployment", + "service.version": "caller-version", + }, + "Deployment context test", + ); } finally { unregister(); } @@ -66,7 +73,7 @@ describe("deployment log attributes", () => { expect(records).toHaveLength(1); expect(records[0]?.attributes).toMatchObject({ "deployment.id": "dpl_123", - "service.version": "release-abc123", + "service.version": "git-sha", }); }); }); diff --git a/specs/instrumentation.md b/specs/instrumentation.md index a46e9434a..9da1ac9ff 100644 --- a/specs/instrumentation.md +++ b/specs/instrumentation.md @@ -3,7 +3,7 @@ ## Metadata - Created: 2026-02-25 -- Last Edited: 2026-03-06 +- Last Edited: 2026-06-09 ## Purpose @@ -23,6 +23,19 @@ Define the canonical logging/tracing instrumentation contracts and shared policy - required aggregation cannot be recovered from existing span/log attributes, or - a critical SLO/SLA alert needs a dedicated low-latency metric path. +## Attribute Scope Policy + +Telemetry attributes that describe the emitting process or deployment may be +attached to every span and log record so each record is independently +queryable. Examples include `service.version`, `deployment.id`, and +`deployment.environment.name`. + +Operation-local attributes must stay on the span or log record that directly +observes them. For example, `http.response.status_code` belongs on the +corresponding `http.server`/`http.client` span and must not be copied to sibling +spans only to make cross-span queries easier. Treat spans like logs/events for +attribute scope: global context can be repeated, but local facts stay local. + ## Specs - [Structured Logging Spec](./logging.md) diff --git a/specs/logging.md b/specs/logging.md index 9729e172c..3be6dfac5 100644 --- a/specs/logging.md +++ b/specs/logging.md @@ -3,7 +3,7 @@ ## Metadata - Created: 2026-02-24 -- Last Edited: 2026-05-28 +- Last Edited: 2026-06-09 ## Purpose @@ -151,6 +151,7 @@ Rules: - `service.name` - `service.version` +- `deployment.id` - `deployment.environment.name` - `event.name` diff --git a/specs/otel-semantics.md b/specs/otel-semantics.md index f414a0714..8b0be1946 100644 --- a/specs/otel-semantics.md +++ b/specs/otel-semantics.md @@ -3,7 +3,7 @@ ## Metadata - Created: 2026-02-25 -- Last Edited: 2026-05-30 +- Last Edited: 2026-06-09 ## Purpose diff --git a/specs/tracing.md b/specs/tracing.md index 878cc2476..157808653 100644 --- a/specs/tracing.md +++ b/specs/tracing.md @@ -3,7 +3,7 @@ ## Metadata - Created: 2026-02-25 -- Last Edited: 2026-05-30 +- Last Edited: 2026-06-09 ## Purpose From d82a79409a588ec9be4e191dd387e32a12bf7838 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 9 Jun 2026 08:06:33 -0700 Subject: [PATCH 4/4] fix(instrumentation): Align release deployment metadata Use the existing deployment module for deployment telemetry helpers instead of a standalone telemetry module. Normalize the Sentry release value with the same deployment service version used on spans and logs so blank releases fall back to the Vercel commit SHA consistently. Co-Authored-By: GPT-5 Codex --- packages/junior/src/chat/logging.ts | 2 +- packages/junior/src/deployment-telemetry.ts | 20 -------------- packages/junior/src/deployment.ts | 27 +++++++++++++++++++ packages/junior/src/instrumentation.ts | 8 ++++-- .../junior/tests/unit/instrumentation.test.ts | 1 + 5 files changed, 35 insertions(+), 23 deletions(-) delete mode 100644 packages/junior/src/deployment-telemetry.ts diff --git a/packages/junior/src/chat/logging.ts b/packages/junior/src/chat/logging.ts index 42e7f738d..e16310b38 100644 --- a/packages/junior/src/chat/logging.ts +++ b/packages/junior/src/chat/logging.ts @@ -17,7 +17,7 @@ import type { import { toOptionalNumber, toOptionalString } from "@/chat/coerce"; import * as Sentry from "@/chat/sentry"; import type { AgentTurnUsage } from "@/chat/usage"; -import { getDeploymentTelemetryAttributes } from "@/deployment-telemetry"; +import { getDeploymentTelemetryAttributes } from "@/deployment"; type Primitive = string | number | boolean; type AttributeValue = Primitive | string[]; diff --git a/packages/junior/src/deployment-telemetry.ts b/packages/junior/src/deployment-telemetry.ts deleted file mode 100644 index b4ec59a45..000000000 --- a/packages/junior/src/deployment-telemetry.ts +++ /dev/null @@ -1,20 +0,0 @@ -function toOptionalTrimmed(value: string | undefined): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} - -/** Resolve deployment-scoped telemetry attributes from host environment. */ -export function getDeploymentTelemetryAttributes(): Record { - const attributes: Record = {}; - const serviceVersion = - toOptionalTrimmed(process.env.SENTRY_RELEASE) ?? - toOptionalTrimmed(process.env.VERCEL_GIT_COMMIT_SHA); - const deploymentId = toOptionalTrimmed(process.env.VERCEL_DEPLOYMENT_ID); - if (serviceVersion) { - attributes["service.version"] = serviceVersion; - } - if (deploymentId) { - attributes["deployment.id"] = deploymentId; - } - return attributes; -} diff --git a/packages/junior/src/deployment.ts b/packages/junior/src/deployment.ts index d20c380f4..44ea3ce63 100644 --- a/packages/junior/src/deployment.ts +++ b/packages/junior/src/deployment.ts @@ -4,3 +4,30 @@ export const JUNIOR_CONVERSATION_WORK_CALLBACK_ROUTE = "/api/internal/agent/continue"; export const LEGACY_JUNIOR_CONVERSATION_WORK_FUNCTION = "api/internal/agent/continue.ts"; + +function toOptionalTrimmed(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +/** Resolve the deployment version used for release and telemetry correlation. */ +export function getDeploymentServiceVersion(): string | undefined { + return ( + toOptionalTrimmed(process.env.SENTRY_RELEASE) ?? + toOptionalTrimmed(process.env.VERCEL_GIT_COMMIT_SHA) + ); +} + +/** Resolve deployment-scoped telemetry attributes from host environment. */ +export function getDeploymentTelemetryAttributes(): Record { + const attributes: Record = {}; + const serviceVersion = getDeploymentServiceVersion(); + const deploymentId = toOptionalTrimmed(process.env.VERCEL_DEPLOYMENT_ID); + if (serviceVersion) { + attributes["service.version"] = serviceVersion; + } + if (deploymentId) { + attributes["deployment.id"] = deploymentId; + } + return attributes; +} diff --git a/packages/junior/src/instrumentation.ts b/packages/junior/src/instrumentation.ts index 7950ddeca..56d713228 100644 --- a/packages/junior/src/instrumentation.ts +++ b/packages/junior/src/instrumentation.ts @@ -1,5 +1,8 @@ import * as Sentry from "@/chat/sentry"; -import { getDeploymentTelemetryAttributes } from "@/deployment-telemetry"; +import { + getDeploymentServiceVersion, + getDeploymentTelemetryAttributes, +} from "@/deployment"; function getSampleRate(value: string | undefined, fallback: number): number { if (!value) return fallback; @@ -23,6 +26,7 @@ export function initSentry(): void { const dsn = process.env.SENTRY_DSN; const enableLogs = getBoolean(process.env.SENTRY_ENABLE_LOGS, Boolean(dsn)); + const serviceVersion = getDeploymentServiceVersion(); const deploymentSpanAttributes = getDeploymentTelemetryAttributes(); Sentry.init({ @@ -31,7 +35,7 @@ export function initSentry(): void { process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV, - release: process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA, + release: serviceVersion, tracesSampleRate: getSampleRate(process.env.SENTRY_TRACES_SAMPLE_RATE, 1), sendDefaultPii: true, enabled: Boolean(dsn), diff --git a/packages/junior/tests/unit/instrumentation.test.ts b/packages/junior/tests/unit/instrumentation.test.ts index 41057c6cf..8d0908893 100644 --- a/packages/junior/tests/unit/instrumentation.test.ts +++ b/packages/junior/tests/unit/instrumentation.test.ts @@ -59,6 +59,7 @@ describe("initSentry", () => { expect(init).toHaveBeenCalledTimes(1); const options = init.mock.calls[0]?.[0]; + expect(options?.release).toBe("git-sha"); expect(options?.beforeSendSpan).toBeTypeOf("function"); const span = {