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/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..f7696b0ff458 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/subject.js @@ -0,0 +1,30 @@ +// only log attribute +Sentry.logger.info('log_before_any_scope', { log_attr: 'log_attr_1' }); + +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: 'millisecond' }); + + // global scope, isolation scope, log attribute + Sentry.logger.info('log_with_isolation_scope', { log_attr: 'log_attr_3' }); + + Sentry.withScope(scope => { + 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: 'millisecond' }); + + // global scope, isolation scope, current scope attribute, log attribute + Sentry.logger.info('log_with_scope_2', { log_attr: 'log_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..b9ad6c9c8a72 --- /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: '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: { + '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: '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: { + '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: '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: { + '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: '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: { + '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: '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(); + }); +}); 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 819c51c7e3f1..ac17d036ca5e 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -1,10 +1,12 @@ +import type { TypedAttributeValue } 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); @@ -183,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); @@ -205,10 +163,10 @@ export function _INTERNAL_captureLog( severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], attributes: Object.keys(attributes).reduce( (acc, key) => { - acc[key] = logAttributeToSerializedLogAttribute(attributes[key]); + acc[key] = attributeValueToTypedAttributeValue(attributes[key]); return acc; }, - {} as Record, + {} as Record, ), }; 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. 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..0c6cae44e1a7 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: 38, unit: 'gigabytes' }); + scope.setAttributes({ + scope_3: true, + scope_4: [1, 2, 3], + scope_5: { value: [true, false, true], 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: 'integer', + unit: 'gigabytes', + value: 38, + }, + 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: 38, unit: 'gigabytes' }); + _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, + // attributes here still have the same form as originally set on the scope or log + scope_1: 'attribute_value', + scope_2: { value: 38, unit: 'gigabytes' }, + }, }); const logBuffer = _INTERNAL_getLogBuffer(client); @@ -358,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', + }, }, }), ); 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],