From 5e7c7ac69c4ff530fba0dd6021ddb6f3489a84bc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 12 Nov 2025 12:42:08 +0100 Subject: [PATCH 1/6] feat(core): Apply scope attributes to logs --- packages/core/src/logs/internal.ts | 62 +++++----------------------- packages/core/src/types-hoist/log.ts | 9 +--- 2 files changed, 12 insertions(+), 59 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 819c51c7e3f1..19e4f3871355 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -1,10 +1,12 @@ +import type { TypedAttributes } from '../attributes'; +import { attributeValueToTypedAttributeValue } from '../attributes'; import { getGlobalSingleton } from '../carrier'; import type { Client } from '../client'; import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope, ScopeData } from '../scope'; import type { Integration } from '../types-hoist/integration'; -import type { Log, SerializedLog, SerializedLogAttributeValue } from '../types-hoist/log'; +import type { Log, SerializedLog } from '../types-hoist/log'; import { mergeScopeData } from '../utils/applyScopeDataToEvent'; import { consoleSandbox, debug } from '../utils/debug-logger'; import { isParameterizedString } from '../utils/is'; @@ -16,51 +18,6 @@ import { createLogEnvelope } from './envelope'; const MAX_LOG_BUFFER_SIZE = 100; -/** - * Converts a log attribute to a serialized log attribute. - * - * @param key - The key of the log attribute. - * @param value - The value of the log attribute. - * @returns The serialized log attribute. - */ -export function logAttributeToSerializedLogAttribute(value: unknown): SerializedLogAttributeValue { - switch (typeof value) { - case 'number': - if (Number.isInteger(value)) { - return { - value, - type: 'integer', - }; - } - return { - value, - type: 'double', - }; - case 'boolean': - return { - value, - type: 'boolean', - }; - case 'string': - return { - value, - type: 'string', - }; - default: { - let stringValue = ''; - try { - stringValue = JSON.stringify(value) ?? ''; - } catch { - // Do nothing - } - return { - value: stringValue, - type: 'string', - }; - } - } -} - /** * Sets a log attribute if the value exists and the attribute key is not already present. * @@ -141,6 +98,7 @@ export function _INTERNAL_captureLog( const { user: { id, email, username }, + attributes: scopeAttributes, } = getMergedScopeData(currentScope); setLogAttribute(processedLogAttributes, 'user.id', id, false); setLogAttribute(processedLogAttributes, 'user.email', email, false); @@ -203,13 +161,13 @@ export function _INTERNAL_captureLog( body: message, trace_id: traceContext?.trace_id, severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], - attributes: Object.keys(attributes).reduce( - (acc, key) => { - acc[key] = logAttributeToSerializedLogAttribute(attributes[key]); + attributes: { + ...scopeAttributes, + ...Object.keys(attributes).reduce((acc, key) => { + acc[key] = attributeValueToTypedAttributeValue(attributes[key]); return acc; - }, - {} as Record, - ), + }, {} as TypedAttributes), + }, }; captureSerializedLog(client, serializedLog); diff --git a/packages/core/src/types-hoist/log.ts b/packages/core/src/types-hoist/log.ts index 1a6e3974e91e..04dbb4f3a149 100644 --- a/packages/core/src/types-hoist/log.ts +++ b/packages/core/src/types-hoist/log.ts @@ -1,3 +1,4 @@ +import type { TypedAttributes } from '../attributes'; import type { ParameterizedString } from './parameterize'; export type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; @@ -30,12 +31,6 @@ export interface Log { severityNumber?: number; } -export type SerializedLogAttributeValue = - | { value: string; type: 'string' } - | { value: number; type: 'integer' } - | { value: number; type: 'double' } - | { value: boolean; type: 'boolean' }; - export interface SerializedLog { /** * Timestamp in seconds (epoch time) indicating when the log occurred. @@ -60,7 +55,7 @@ export interface SerializedLog { /** * Arbitrary structured data that stores information about the log - e.g., userId: 100. */ - attributes?: Record; + attributes?: TypedAttributes; /** * The severity number. From 9777a6412fc7179842d2e3a8e67cbcb7acfdc772 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 13 Nov 2025 11:00:21 +0100 Subject: [PATCH 2/6] wip --- packages/core/src/logs/internal.ts | 1 + packages/core/src/types-hoist/options.ts | 19 ++ .../core/src/utils/applyScopeDataToEvent.ts | 4 +- packages/core/test/lib/logs/internal.test.ts | 192 +++++++++--------- .../lib/utils/applyScopeDataToEvent.test.ts | 10 + 5 files changed, 127 insertions(+), 99 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 19e4f3871355..e9e8395c0fdc 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -162,6 +162,7 @@ export function _INTERNAL_captureLog( trace_id: traceContext?.trace_id, severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], attributes: { + // TODO: This is too late to apply scope attributes because we already invoked beforeSendLog earlier. ...scopeAttributes, ...Object.keys(attributes).reduce((acc, key) => { acc[key] = attributeValueToTypedAttributeValue(attributes[key]); diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index ccdc3b180e15..3f54c80f510c 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -518,3 +518,22 @@ export interface CoreOptions(data: Data, prop: Prop, mergeVal: Data[Prop]): void { data[prop] = merge(data[prop], mergeVal, 1); diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 563139aba36d..2fb42aa8a3e6 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -1,85 +1,12 @@ import { describe, expect, it, vi } from 'vitest'; import { fmt, Scope } from '../../../src'; -import { - _INTERNAL_captureLog, - _INTERNAL_flushLogsBuffer, - _INTERNAL_getLogBuffer, - logAttributeToSerializedLogAttribute, -} from '../../../src/logs/internal'; +import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_getLogBuffer } from '../../../src/logs/internal'; import type { Log } from '../../../src/types-hoist/log'; import * as loggerModule from '../../../src/utils/debug-logger'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; -describe('logAttributeToSerializedLogAttribute', () => { - it('serializes integer values', () => { - const result = logAttributeToSerializedLogAttribute(42); - expect(result).toEqual({ - value: 42, - type: 'integer', - }); - }); - - it('serializes double values', () => { - const result = logAttributeToSerializedLogAttribute(42.34); - expect(result).toEqual({ - value: 42.34, - type: 'double', - }); - }); - - it('serializes boolean values', () => { - const result = logAttributeToSerializedLogAttribute(true); - expect(result).toEqual({ - value: true, - type: 'boolean', - }); - }); - - it('serializes string values', () => { - const result = logAttributeToSerializedLogAttribute('username'); - expect(result).toEqual({ - value: 'username', - type: 'string', - }); - }); - - it('serializes object values as JSON strings', () => { - const obj = { name: 'John', age: 30 }; - const result = logAttributeToSerializedLogAttribute(obj); - expect(result).toEqual({ - value: JSON.stringify(obj), - type: 'string', - }); - }); - - it('serializes array values as JSON strings', () => { - const array = [1, 2, 3, 'test']; - const result = logAttributeToSerializedLogAttribute(array); - expect(result).toEqual({ - value: JSON.stringify(array), - type: 'string', - }); - }); - - it('serializes undefined values as empty strings', () => { - const result = logAttributeToSerializedLogAttribute(undefined); - expect(result).toEqual({ - value: '', - type: 'string', - }); - }); - - it('serializes null values as JSON strings', () => { - const result = logAttributeToSerializedLogAttribute(null); - expect(result).toEqual({ - value: 'null', - type: 'string', - }); - }); -}); - describe('_INTERNAL_captureLog', () => { it('captures and sends logs', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); @@ -215,31 +142,92 @@ describe('_INTERNAL_captureLog', () => { ); }); - it('includes custom attributes in log', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); - const client = new TestClient(options); - const scope = new Scope(); - scope.setClient(client); + describe('attributes', () => { + it('includes custom attributes in log', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); - _INTERNAL_captureLog( - { - level: 'info', - message: 'test log with custom attributes', - attributes: { userId: '123', component: 'auth' }, - }, - scope, - ); + _INTERNAL_captureLog( + { + level: 'info', + message: 'test log with custom attributes', + attributes: { userId: '123', component: 'auth' }, + }, + scope, + ); - const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - expect(logAttributes).toEqual({ - userId: { - value: '123', - type: 'string', - }, - component: { - value: 'auth', - type: 'string', - }, + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({ + userId: { + value: '123', + type: 'string', + }, + component: { + value: 'auth', + type: 'string', + }, + }); + }); + + it('applies scope attributes attributes to log', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + scope.setAttribute('scope_1', 'attribute_value'); + scope.setAttribute('scope_2', { value: 143.5, type: 'double', unit: 'bytes' }); + scope.setAttributes({ + scope_3: true, + scope_4: [1, 2, 3], + scope_5: { value: [true, false, true], type: 'boolean[]', unit: 's' }, + }); + + _INTERNAL_captureLog( + { + level: 'info', + message: 'test log with custom attributes', + attributes: { userId: '123', component: 'auth' }, + }, + scope, + ); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + + expect(logAttributes).toEqual({ + userId: { + value: '123', + type: 'string', + }, + component: { + value: 'auth', + type: 'string', + }, + scope_1: { + type: 'string', + value: 'attribute_value', + }, + scope_2: { + type: 'double', + unit: 'bytes', + value: 143.5, + }, + scope_3: { + type: 'boolean', + value: true, + }, + scope_4: { + type: 'integer[]', + value: [1, 2, 3], + }, + scope_5: { + type: 'boolean[]', + value: [true, false, true], + unit: 's', + }, + }); }); }); @@ -329,6 +317,9 @@ describe('_INTERNAL_captureLog', () => { const scope = new Scope(); scope.setClient(client); + scope.setAttribute('scope_1', 'attribute_value'); + scope.setAttribute('scope_2', { value: 143.5, type: 'double', unit: 'bytes' }); + _INTERNAL_captureLog( { level: 'info', @@ -341,7 +332,12 @@ describe('_INTERNAL_captureLog', () => { expect(beforeSendLog).toHaveBeenCalledWith({ level: 'info', message: 'original message', - attributes: { original: true }, + attributes: { + original: true, + // scope attributes should already be applied prior to beforeSendLog + scope_1: 'attribute_value', + scope_2: { value: 143.5, type: 'double', unit: 'bytes' }, + }, }); const logBuffer = _INTERNAL_getLogBuffer(client); diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts index f85fa0f02617..a23404eaf70f 100644 --- a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -83,6 +83,7 @@ describe('mergeScopeData', () => { breadcrumbs: [], user: {}, tags: {}, + attributes: {}, extra: {}, contexts: {}, attachments: [], @@ -95,6 +96,7 @@ describe('mergeScopeData', () => { breadcrumbs: [], user: {}, tags: {}, + attributes: {}, extra: {}, contexts: {}, attachments: [], @@ -108,6 +110,7 @@ describe('mergeScopeData', () => { breadcrumbs: [], user: {}, tags: {}, + attributes: {}, extra: {}, contexts: {}, attachments: [], @@ -135,6 +138,7 @@ describe('mergeScopeData', () => { breadcrumbs: [breadcrumb1], user: { id: '1', email: 'test@example.com' }, tags: { tag1: 'aa', tag2: 'aa' }, + attributes: { attr1: { value: 'value1', type: 'string' }, attr2: { value: 123, type: 'integer' } }, extra: { extra1: 'aa', extra2: 'aa' }, contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, attachments: [attachment1], @@ -155,6 +159,7 @@ describe('mergeScopeData', () => { breadcrumbs: [breadcrumb2, breadcrumb3], user: { id: '2', name: 'foo' }, tags: { tag2: 'bb', tag3: 'bb' }, + attributes: { attr2: { value: 456, type: 'integer' }, attr3: { value: 'value3', type: 'string' } }, extra: { extra2: 'bb', extra3: 'bb' }, contexts: { os: { name: 'os2' } }, attachments: [attachment2, attachment3], @@ -176,6 +181,11 @@ describe('mergeScopeData', () => { breadcrumbs: [breadcrumb1, breadcrumb2, breadcrumb3], user: { id: '2', name: 'foo', email: 'test@example.com' }, tags: { tag1: 'aa', tag2: 'bb', tag3: 'bb' }, + attributes: { + attr1: { value: 'value1', type: 'string' }, + attr2: { value: 456, type: 'integer' }, + attr3: { value: 'value3', type: 'string' }, + }, extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, attachments: [attachment1, attachment2, attachment3], From f4441a212798057593915ef8a3a1af1341cfb4ee Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 17 Nov 2025 17:32:07 +0100 Subject: [PATCH 3/6] adjust after scope attribute changes --- .../public-api/logger/integration/test.ts | 4 +-- packages/core/src/attributes.ts | 2 +- packages/core/src/logs/internal.ts | 15 +++++------ packages/core/test/lib/logs/internal.test.ts | 26 +++++++++++++------ 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts index dd4bd7e8ebc3..fc3fa61163c6 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts @@ -166,7 +166,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.message.template': { value: 'Mixed: {} {} {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 'prefix', type: 'string' }, 'sentry.message.parameter.1': { value: '{"obj":true}', type: 'string' }, - 'sentry.message.parameter.2': { value: '[4,5,6]', type: 'string' }, + 'sentry.message.parameter.2': { value: [4, 5, 6], type: 'integer[]' }, 'sentry.message.parameter.3': { value: 'suffix', type: 'string' }, }, }, @@ -235,7 +235,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.message.template': { value: 'hello {} {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: true, type: 'boolean' }, 'sentry.message.parameter.1': { value: 'null', type: 'string' }, - 'sentry.message.parameter.2': { value: '', type: 'string' }, + 'sentry.message.parameter.2': { value: 'undefined', type: 'string' }, }, }, ], diff --git a/packages/core/src/attributes.ts b/packages/core/src/attributes.ts index d979d5c4350f..28f2f80a63f6 100644 --- a/packages/core/src/attributes.ts +++ b/packages/core/src/attributes.ts @@ -6,7 +6,7 @@ export type RawAttributes = T & ValidatedAttributes; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type RawAttribute = T extends { value: any } | { unit: any } ? AttributeObject : T; -export type Attributes = Record; +export type TypedAttributes = Record; export type AttributeValueType = string | number | boolean | Array | Array | Array; diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index e9e8395c0fdc..ac17d036ca5e 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -1,4 +1,4 @@ -import type { TypedAttributes } from '../attributes'; +import type { TypedAttributeValue } from '../attributes'; import { attributeValueToTypedAttributeValue } from '../attributes'; import { getGlobalSingleton } from '../carrier'; import type { Client } from '../client'; @@ -141,7 +141,7 @@ export function _INTERNAL_captureLog( // Add the parent span ID to the log attributes for trace context setLogAttribute(processedLogAttributes, 'sentry.trace.parent_span_id', span?.spanContext().spanId); - const processedLog = { ...beforeLog, attributes: processedLogAttributes }; + const processedLog = { ...beforeLog, attributes: { ...scopeAttributes, ...processedLogAttributes } }; client.emit('beforeCaptureLog', processedLog); @@ -161,14 +161,13 @@ export function _INTERNAL_captureLog( body: message, trace_id: traceContext?.trace_id, severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], - attributes: { - // TODO: This is too late to apply scope attributes because we already invoked beforeSendLog earlier. - ...scopeAttributes, - ...Object.keys(attributes).reduce((acc, key) => { + attributes: Object.keys(attributes).reduce( + (acc, key) => { acc[key] = attributeValueToTypedAttributeValue(attributes[key]); return acc; - }, {} as TypedAttributes), - }, + }, + {} as Record, + ), }; captureSerializedLog(client, serializedLog); diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 2fb42aa8a3e6..0c6cae44e1a7 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -178,11 +178,11 @@ describe('_INTERNAL_captureLog', () => { scope.setClient(client); scope.setAttribute('scope_1', 'attribute_value'); - scope.setAttribute('scope_2', { value: 143.5, type: 'double', unit: 'bytes' }); + scope.setAttribute('scope_2', { value: 38, unit: 'gigabytes' }); scope.setAttributes({ scope_3: true, scope_4: [1, 2, 3], - scope_5: { value: [true, false, true], type: 'boolean[]', unit: 's' }, + scope_5: { value: [true, false, true], unit: 's' }, }); _INTERNAL_captureLog( @@ -210,9 +210,9 @@ describe('_INTERNAL_captureLog', () => { value: 'attribute_value', }, scope_2: { - type: 'double', - unit: 'bytes', - value: 143.5, + type: 'integer', + unit: 'gigabytes', + value: 38, }, scope_3: { type: 'boolean', @@ -318,7 +318,7 @@ describe('_INTERNAL_captureLog', () => { scope.setClient(client); scope.setAttribute('scope_1', 'attribute_value'); - scope.setAttribute('scope_2', { value: 143.5, type: 'double', unit: 'bytes' }); + scope.setAttribute('scope_2', { value: 38, unit: 'gigabytes' }); _INTERNAL_captureLog( { @@ -334,9 +334,9 @@ describe('_INTERNAL_captureLog', () => { message: 'original message', attributes: { original: true, - // scope attributes should already be applied prior to beforeSendLog + // attributes here still have the same form as originally set on the scope or log scope_1: 'attribute_value', - scope_2: { value: 143.5, type: 'double', unit: 'bytes' }, + scope_2: { value: 38, unit: 'gigabytes' }, }, }); @@ -354,6 +354,16 @@ describe('_INTERNAL_captureLog', () => { value: true, type: 'boolean', }, + // during serialization, they're converted to the typed attribute format + scope_1: { + value: 'attribute_value', + type: 'string', + }, + scope_2: { + value: 38, + unit: 'gigabytes', + type: 'integer', + }, }, }), ); From d6c43c2c15b9ac486ff64a35ff39fbebdfc75a90 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 18 Nov 2025 13:15:09 +0100 Subject: [PATCH 4/6] add browser integration test --- .../logger/scopeAttributes/subject.js | 34 +++++++ .../public-api/logger/scopeAttributes/test.ts | 98 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js new file mode 100644 index 000000000000..54b0dcf5ce29 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js @@ -0,0 +1,34 @@ +// only log attribute +Sentry.logger.info('log_before_any_scope', { log_attr: 'scope_attr_1' }); + +Sentry.getGlobalScope().setAttribute('global_scope_attr', true); + +// global scope, log attribute +Sentry.logger.info('log_after_global_scope', { log_attr: 'scope_attr_2' }); + +let isolScope = null; +let isolScope2 = null; + +Sentry.withIsolationScope(isolationScope => { + isolScope = isolationScope; + isolationScope.setAttribute('isolation_scope_1_attr', { value: 100, unit: 'ms' }); + + // global scope, isolation scope, log attribute + Sentry.logger.info('log_with_isolation_scope', { log_attr: 'scope_attr_3' }); + + Sentry.withScope(scope => { + scope.setAttribute('scope_attr', { value: 200, unit: 'ms' }); + + // global scope, isolation scope, current scope attribute, log attribute + Sentry.logger.info('log_with_scope', { log_attr: 'scope_attr_4' }); + }); + + Sentry.withScope(scope2 => { + scope2.setAttribute('scope_2_attr', { value: 300, unit: 'ms' }); + + // global scope, isolation scope, current scope attribute, log attribute + Sentry.logger.info('log_with_scope_2', { log_attr: 'scope_attr_5' }); + }); +}); + +Sentry.flush(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts new file mode 100644 index 000000000000..fbf268771996 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts @@ -0,0 +1,98 @@ +import { expect } from '@playwright/test'; +import type { LogEnvelope } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../../utils/helpers'; + +sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE || ''; + // Only run this for npm package exports + if (bundle.startsWith('bundle') || bundle.startsWith('loader')) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const event = await getFirstSentryEnvelopeRequest(page, url, properFullEnvelopeRequestParser); + const envelopeItems = event[1]; + + expect(envelopeItems[0]).toEqual([ + { + type: 'log', + item_count: 5, + content_type: 'application/vnd.sentry.items.log+json', + }, + { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_before_any_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + log_attr: { value: 'scope_attr_1', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_after_global_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + global_scope_attr: { value: true, type: 'boolean' }, + log_attr: { value: 'scope_attr_2', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_with_isolation_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + global_scope_attr: { value: true, type: 'boolean' }, + isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, + log_attr: { value: 'scope_attr_3', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_with_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + global_scope_attr: { value: true, type: 'boolean' }, + isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, + scope_attr: { value: 200, unit: 'ms', type: 'integer' }, + log_attr: { value: 'scope_attr_4', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_with_scope_2', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + global_scope_attr: { value: true, type: 'boolean' }, + isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, + scope_2_attr: { value: 300, unit: 'ms', type: 'integer' }, + log_attr: { value: 'scope_attr_5', type: 'string' }, + }, + }, + ], + }, + ]); +}); From bba3ece80978df42227f40e75dd295d1c0d8d4ce Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 18 Nov 2025 14:04:46 +0100 Subject: [PATCH 5/6] adjust browser integration test --- .../public-api/logger/scopeAttributes/subject.js | 14 +++++--------- .../public-api/logger/scopeAttributes/test.ts | 10 +++++----- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js index 54b0dcf5ce29..1bc9c9a62f0a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js @@ -1,33 +1,29 @@ // only log attribute -Sentry.logger.info('log_before_any_scope', { log_attr: 'scope_attr_1' }); +Sentry.logger.info('log_before_any_scope', { log_attr: 'log_attr_1' }); Sentry.getGlobalScope().setAttribute('global_scope_attr', true); // global scope, log attribute -Sentry.logger.info('log_after_global_scope', { log_attr: 'scope_attr_2' }); - -let isolScope = null; -let isolScope2 = null; +Sentry.logger.info('log_after_global_scope', { log_attr: 'log_attr_2' }); Sentry.withIsolationScope(isolationScope => { - isolScope = isolationScope; isolationScope.setAttribute('isolation_scope_1_attr', { value: 100, unit: 'ms' }); // global scope, isolation scope, log attribute - Sentry.logger.info('log_with_isolation_scope', { log_attr: 'scope_attr_3' }); + Sentry.logger.info('log_with_isolation_scope', { log_attr: 'log_attr_3' }); Sentry.withScope(scope => { scope.setAttribute('scope_attr', { value: 200, unit: 'ms' }); // global scope, isolation scope, current scope attribute, log attribute - Sentry.logger.info('log_with_scope', { log_attr: 'scope_attr_4' }); + Sentry.logger.info('log_with_scope', { log_attr: 'log_attr_4' }); }); Sentry.withScope(scope2 => { scope2.setAttribute('scope_2_attr', { value: 300, unit: 'ms' }); // global scope, isolation scope, current scope attribute, log attribute - Sentry.logger.info('log_with_scope_2', { log_attr: 'scope_attr_5' }); + Sentry.logger.info('log_with_scope_2', { log_attr: 'log_attr_5' }); }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts index fbf268771996..6015a952b939 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts @@ -32,7 +32,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, - log_attr: { value: 'scope_attr_1', type: 'string' }, + log_attr: { value: 'log_attr_1', type: 'string' }, }, }, { @@ -45,7 +45,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, global_scope_attr: { value: true, type: 'boolean' }, - log_attr: { value: 'scope_attr_2', type: 'string' }, + log_attr: { value: 'log_attr_2', type: 'string' }, }, }, { @@ -59,7 +59,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, global_scope_attr: { value: true, type: 'boolean' }, isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, - log_attr: { value: 'scope_attr_3', type: 'string' }, + log_attr: { value: 'log_attr_3', type: 'string' }, }, }, { @@ -74,7 +74,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page global_scope_attr: { value: true, type: 'boolean' }, isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, scope_attr: { value: 200, unit: 'ms', type: 'integer' }, - log_attr: { value: 'scope_attr_4', type: 'string' }, + log_attr: { value: 'log_attr_4', type: 'string' }, }, }, { @@ -89,7 +89,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page global_scope_attr: { value: true, type: 'boolean' }, isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, scope_2_attr: { value: 300, unit: 'ms', type: 'integer' }, - log_attr: { value: 'scope_attr_5', type: 'string' }, + log_attr: { value: 'log_attr_5', type: 'string' }, }, }, ], From e5785eab2c731b3eb1b28595c7a86907ed6d8c69 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 18 Nov 2025 14:19:33 +0100 Subject: [PATCH 6/6] add node integration test --- .../logger/scopeAttributes/subject.js | 8 +- .../public-api/logger/scopeAttributes/test.ts | 10 +- .../suites/public-api/logger/scenario.ts | 46 ++++++++ .../suites/public-api/logger/test.ts | 109 ++++++++++++++++++ 4 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/logger/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js index 1bc9c9a62f0a..f7696b0ff458 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js @@ -1,26 +1,26 @@ // only log attribute Sentry.logger.info('log_before_any_scope', { log_attr: 'log_attr_1' }); -Sentry.getGlobalScope().setAttribute('global_scope_attr', true); +Sentry.getGlobalScope().setAttributes({ global_scope_attr: true }); // global scope, log attribute Sentry.logger.info('log_after_global_scope', { log_attr: 'log_attr_2' }); Sentry.withIsolationScope(isolationScope => { - isolationScope.setAttribute('isolation_scope_1_attr', { value: 100, unit: 'ms' }); + isolationScope.setAttribute('isolation_scope_1_attr', { value: 100, unit: 'millisecond' }); // global scope, isolation scope, log attribute Sentry.logger.info('log_with_isolation_scope', { log_attr: 'log_attr_3' }); Sentry.withScope(scope => { - scope.setAttribute('scope_attr', { value: 200, unit: 'ms' }); + scope.setAttributes({ scope_attr: { value: 200, unit: 'millisecond' } }); // global scope, isolation scope, current scope attribute, log attribute Sentry.logger.info('log_with_scope', { log_attr: 'log_attr_4' }); }); Sentry.withScope(scope2 => { - scope2.setAttribute('scope_2_attr', { value: 300, unit: 'ms' }); + scope2.setAttribute('scope_2_attr', { value: 300, unit: 'millisecond' }); // global scope, isolation scope, current scope attribute, log attribute Sentry.logger.info('log_with_scope_2', { log_attr: 'log_attr_5' }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts index 6015a952b939..b9ad6c9c8a72 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts @@ -58,7 +58,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, global_scope_attr: { value: true, type: 'boolean' }, - isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, + isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, log_attr: { value: 'log_attr_3', type: 'string' }, }, }, @@ -72,8 +72,8 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, global_scope_attr: { value: true, type: 'boolean' }, - isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, - scope_attr: { value: 200, unit: 'ms', type: 'integer' }, + isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, + scope_attr: { value: 200, unit: 'millisecond', type: 'integer' }, log_attr: { value: 'log_attr_4', type: 'string' }, }, }, @@ -87,8 +87,8 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, global_scope_attr: { value: true, type: 'boolean' }, - isolation_scope_1_attr: { value: 100, unit: 'ms', type: 'integer' }, - scope_2_attr: { value: 300, unit: 'ms', type: 'integer' }, + isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, + scope_2_attr: { value: 300, unit: 'millisecond', type: 'integer' }, log_attr: { value: 'log_attr_5', type: 'string' }, }, }, diff --git a/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts new file mode 100644 index 000000000000..c5ba3ed29eb7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts @@ -0,0 +1,46 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0.0', + environment: 'test', + enableLogs: true, + transport: loggingTransport, +}); + +async function run(): Promise { + // only log attribute + Sentry.logger.info('log_before_any_scope', { log_attr: 'log_attr_1' }); + + Sentry.getGlobalScope().setAttribute('global_scope_attr', true); + + // global scope, log attribute + Sentry.logger.info('log_after_global_scope', { log_attr: 'log_attr_2' }); + + Sentry.withIsolationScope(isolationScope => { + isolationScope.setAttribute('isolation_scope_1_attr', { value: 100, unit: 'millisecond' }); + + // global scope, isolation scope, log attribute + Sentry.logger.info('log_with_isolation_scope', { log_attr: 'log_attr_3' }); + + Sentry.withScope(scope => { + scope.setAttribute('scope_attr', { value: 200, unit: 'millisecond' }); + + // global scope, isolation scope, current scope attribute, log attribute + Sentry.logger.info('log_with_scope', { log_attr: 'log_attr_4' }); + }); + + Sentry.withScope(scope2 => { + scope2.setAttribute('scope_2_attr', { value: 300, unit: 'millisecond' }); + + // global scope, isolation scope, current scope attribute, log attribute + Sentry.logger.info('log_with_scope_2', { log_attr: 'log_attr_5' }); + }); + }); + + await Sentry.flush(); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/logger/test.ts b/dev-packages/node-integration-tests/suites/public-api/logger/test.ts new file mode 100644 index 000000000000..661ea0436acc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/logger/test.ts @@ -0,0 +1,109 @@ +import type { SerializedLog } from '@sentry/core'; +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +const commonAttributes: SerializedLog['attributes'] = { + 'sentry.environment': { + type: 'string', + value: 'test', + }, + 'sentry.release': { + type: 'string', + value: '1.0.0', + }, + 'sentry.sdk.name': { + type: 'string', + value: 'sentry.javascript.node', + }, + 'sentry.sdk.version': { + type: 'string', + value: expect.any(String), + }, + 'server.address': { + type: 'string', + value: expect.any(String), + }, +}; + +describe('metrics', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should capture all metric types', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_before_any_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + ...commonAttributes, + log_attr: { value: 'log_attr_1', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_after_global_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + ...commonAttributes, + global_scope_attr: { value: true, type: 'boolean' }, + log_attr: { value: 'log_attr_2', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_with_isolation_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + ...commonAttributes, + global_scope_attr: { value: true, type: 'boolean' }, + isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, + log_attr: { value: 'log_attr_3', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_with_scope', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + ...commonAttributes, + global_scope_attr: { value: true, type: 'boolean' }, + isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, + scope_attr: { value: 200, unit: 'millisecond', type: 'integer' }, + log_attr: { value: 'log_attr_4', type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'log_with_scope_2', + severity_number: 9, + trace_id: expect.any(String), + attributes: { + ...commonAttributes, + global_scope_attr: { value: true, type: 'boolean' }, + isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, + scope_2_attr: { value: 300, unit: 'millisecond', type: 'integer' }, + log_attr: { value: 'log_attr_5', type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); +});