From f2e8a6d90534e1d1f83c22d0cd487bb6de970671 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 5 May 2026 14:55:38 +0200 Subject: [PATCH 1/4] add processSegmentSpan for integration --- .../suites/context-streamed/scenario.ts | 15 ++++ .../suites/context-streamed/test.ts | 25 ++++++ .../node-core/src/integrations/context.ts | 48 +++++++++++- .../test/integrations/context.test.ts | 76 ++++++++++++++++++- 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/context-streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/context-streamed/test.ts diff --git a/dev-packages/node-integration-tests/suites/context-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/context-streamed/scenario.ts new file mode 100644 index 000000000000..7f1b5ddd053f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/context-streamed/scenario.ts @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +Sentry.startSpan({ name: 'test-span' }, () => { + // noop +}); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/context-streamed/test.ts b/dev-packages/node-integration-tests/suites/context-streamed/test.ts new file mode 100644 index 000000000000..8aa779a7ac22 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/context-streamed/test.ts @@ -0,0 +1,25 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('nodeContextIntegration sets context attributes on segment spans', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: container => { + const segmentSpan = container.items.find(s => !!s.is_segment); + expect(segmentSpan).toBeDefined(); + + const attrs = segmentSpan!.attributes!; + + expect(attrs['app.start_time']).toEqual({ type: 'string', value: expect.any(String) }); + expect(attrs['device.processor_count']).toEqual({ type: 'integer', value: expect.any(Number) }); + expect(attrs['process.runtime.engine.name']).toEqual({ type: 'string', value: 'v8' }); + expect(attrs['process.runtime.engine.version']).toEqual({ type: 'string', value: expect.any(String) }); + }, + }) + .start() + .completed(); +}); diff --git a/packages/node-core/src/integrations/context.ts b/packages/node-core/src/integrations/context.ts index a39f75bfa2a9..c41aeb816ec5 100644 --- a/packages/node-core/src/integrations/context.ts +++ b/packages/node-core/src/integrations/context.ts @@ -15,7 +15,7 @@ import type { IntegrationFn, OsContext, } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; +import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core'; export const readFileAsync = promisify(readFile); export const readDirAsync = promisify(readdir); @@ -53,6 +53,45 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => { ...options, }; + const cachedSpanAttributes: Record = { + 'process.runtime.engine.name': 'v8', + 'process.runtime.engine.version': process.versions.v8, + }; + + if (_options.app) { + // oxlint-disable-next-line sdk/no-unsafe-random-apis + cachedSpanAttributes['app.start_time'] = new Date(Date.now() - process.uptime() * 1000).toISOString(); + } + + if (_options.device) { + const deviceOpt = _options.device; + // Convention uses 'device.archs' (string[]), but array attributes are not yet serialized. + cachedSpanAttributes['device.archs'] = [os.arch()]; + if (deviceOpt === true || (typeof deviceOpt === 'object' && deviceOpt.cpu)) { + const cpuInfo = os.cpus() as os.CpuInfo[] | undefined; + if (cpuInfo?.[0]) { + cachedSpanAttributes['device.processor_count'] = cpuInfo.length; + } + } + } + + const osContextPromise = _options.os ? getOsContext() : undefined; + + if (osContextPromise) { + osContextPromise + .then(osContext => { + if (osContext.name) { + cachedSpanAttributes['os.name'] = osContext.name; + } + if (osContext.version) { + cachedSpanAttributes['os.version'] = osContext.version; + } + }) + .catch(() => { + // Ignore - os attributes will be undefined + }); + } + /** Add contexts to the event. Caches the context so we only look it up once. */ async function addContext(event: Event): Promise { if (cachedContext === undefined) { @@ -78,8 +117,8 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => { async function _getContexts(): Promise { const contexts: Contexts = {}; - if (_options.os) { - contexts.os = await getOsContext(); + if (osContextPromise) { + contexts.os = await osContextPromise; } if (_options.app) { @@ -110,6 +149,9 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => { processEvent(event) { return addContext(event); }, + processSegmentSpan(span) { + safeSetSpanJSONAttributes(span, cachedSpanAttributes); + }, }; }) satisfies IntegrationFn; diff --git a/packages/node-core/test/integrations/context.test.ts b/packages/node-core/test/integrations/context.test.ts index b8c3f8e3d49b..6a84ad870629 100644 --- a/packages/node-core/test/integrations/context.test.ts +++ b/packages/node-core/test/integrations/context.test.ts @@ -1,6 +1,7 @@ import * as os from 'node:os'; +import type { StreamedSpanJSON } from '@sentry/core'; import { afterAll, describe, expect, it, vi } from 'vitest'; -import { getAppContext, getDeviceContext } from '../../src/integrations/context'; +import { getAppContext, getDeviceContext, nodeContextIntegration } from '../../src/integrations/context'; import { conditionalTest } from '../helpers/conditional'; vi.mock('node:os', async () => { @@ -53,4 +54,77 @@ describe('Context', () => { expect(deviceCtx.boot_time).toBeUndefined(); }); }); + + describe('processSegmentSpan', () => { + it('sets context attributes on segment span', () => { + const integration = nodeContextIntegration(); + + const span: StreamedSpanJSON = { + trace_id: 'abc123', + span_id: 'def456', + name: 'test-span', + start_timestamp: Date.now(), + end_timestamp: Date.now(), + status: 'ok', + is_segment: true, + attributes: {}, + }; + + integration.processSegmentSpan!(span, {} as any); + + expect(span.attributes).toMatchObject({ + 'app.start_time': expect.any(String), + 'device.archs': [os.arch()], + 'device.processor_count': expect.any(Number), + 'process.runtime.engine.name': 'v8', + 'process.runtime.engine.version': process.versions.v8, + }); + }); + + it('does not overwrite existing attributes', () => { + const integration = nodeContextIntegration(); + + const span: StreamedSpanJSON = { + trace_id: 'abc123', + span_id: 'def456', + name: 'test-span', + start_timestamp: Date.now(), + end_timestamp: Date.now(), + status: 'ok', + is_segment: true, + attributes: { + 'process.runtime.engine.name': 'custom-engine', + }, + }; + + integration.processSegmentSpan!(span, {} as any); + + expect(span.attributes!['process.runtime.engine.name']).toBe('custom-engine'); + }); + + it('respects disabled options', () => { + const integration = nodeContextIntegration({ app: false, device: false, os: false }); + + const span: StreamedSpanJSON = { + trace_id: 'abc123', + span_id: 'def456', + name: 'test-span', + start_timestamp: Date.now(), + end_timestamp: Date.now(), + status: 'ok', + is_segment: true, + attributes: {}, + }; + + integration.processSegmentSpan!(span, {} as any); + + expect(span.attributes).toMatchObject({ + 'process.runtime.engine.name': 'v8', + 'process.runtime.engine.version': process.versions.v8, + }); + expect(span.attributes!['app.start_time']).toBeUndefined(); + expect(span.attributes!['device.archs']).toBeUndefined(); + expect(span.attributes!['device.processor_count']).toBeUndefined(); + }); + }); }); From 26f39fdc618a2431a5446c63284f4367a3b7183d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 5 May 2026 15:45:43 +0200 Subject: [PATCH 2/4] fix test --- .../public-api/startSpan/basic-usage-streamed/test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index b31ca320df53..6044c04e761f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -124,7 +124,7 @@ test('sends a streamed span envelope with correct spans for a manually started s }); expect(segmentSpan).toEqual({ - attributes: { + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, @@ -132,7 +132,11 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - }, + 'process.runtime.engine.name': { type: 'string', value: 'v8' }, + 'process.runtime.engine.version': { type: 'string', value: expect.any(String) }, + 'app.start_time': { type: 'string', value: expect.any(String) }, + 'device.processor_count': { type: 'integer', value: expect.any(Number) }, + }), name: 'test-span', is_segment: true, trace_id: traceId, From e5c90cdefa2b75e3e70c89bfdf6a42d584f3d884 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 5 May 2026 17:11:27 +0200 Subject: [PATCH 3/4] fix(node-core): Update basic-usage-streamed test for context span attributes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../public-api/startSpan/basic-usage-streamed/test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 3184aae69d64..1b50691e53d7 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -124,7 +124,7 @@ test('sends a streamed span envelope with correct spans for a manually started s }); expect(segmentSpan).toEqual({ - attributes: { + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, @@ -132,7 +132,11 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - }, + 'process.runtime.engine.name': { type: 'string', value: 'v8' }, + 'process.runtime.engine.version': { type: 'string', value: expect.any(String) }, + 'app.start_time': { type: 'string', value: expect.any(String) }, + 'device.processor_count': { type: 'integer', value: expect.any(Number) }, + }), name: 'test-span', is_segment: true, trace_id: traceId, From 9a7069fc372695e523980dab95c8eb2e9654c22a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 5 May 2026 17:17:19 +0200 Subject: [PATCH 4/4] fix: Use strict attribute checks in basic-usage-streamed tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../suites/public-api/startSpan/basic-usage-streamed/test.ts | 4 ++-- .../suites/public-api/startSpan/basic-usage-streamed/test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 1b50691e53d7..ed399914dde0 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -124,7 +124,7 @@ test('sends a streamed span envelope with correct spans for a manually started s }); expect(segmentSpan).toEqual({ - attributes: expect.objectContaining({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, @@ -136,7 +136,7 @@ test('sends a streamed span envelope with correct spans for a manually started s 'process.runtime.engine.version': { type: 'string', value: expect.any(String) }, 'app.start_time': { type: 'string', value: expect.any(String) }, 'device.processor_count': { type: 'integer', value: expect.any(Number) }, - }), + }, name: 'test-span', is_segment: true, trace_id: traceId, diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 6044c04e761f..ef0c51803280 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -124,7 +124,7 @@ test('sends a streamed span envelope with correct spans for a manually started s }); expect(segmentSpan).toEqual({ - attributes: expect.objectContaining({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, @@ -136,7 +136,7 @@ test('sends a streamed span envelope with correct spans for a manually started s 'process.runtime.engine.version': { type: 'string', value: expect.any(String) }, 'app.start_time': { type: 'string', value: expect.any(String) }, 'device.processor_count': { type: 'integer', value: expect.any(Number) }, - }), + }, name: 'test-span', is_segment: true, trace_id: traceId,