diff --git a/CHANGELOG.md b/CHANGELOG.md index f23fe92ce89b..10e4c48e0b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- **feat(core): Support array attributes for spans, logs, and metrics ([#20427](https://github.com/getsentry/sentry-javascript/pull/20427))** + + Arrays of primitive values (`string`, `number`, `boolean`) are now accepted as attribute values. Arrays containing non-primitive elements will be dropped and won't show up in Sentry. Note that array attributes on logs and metrics were previously stringified in certain cases and will now be sent as arrays instead. + ## 10.52.0 ### Important Changes 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 8312c2a13e4d..f16fc7170120 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 @@ -162,7 +162,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'Array: {}', type: 'string' }, - 'sentry.message.parameter.0': { value: '[1,2,3,"string"]', type: 'string' }, + 'sentry.message.parameter.0': { value: [1, 2, 3, 'string'], type: 'array' }, }, }, { @@ -179,7 +179,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: 'array' }, 'sentry.message.parameter.3': { value: 'suffix', 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 index 9bba2c222bdc..76377beb82a7 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 @@ -3,7 +3,6 @@ Sentry.logger.info('log_before_any_scope', { log_attr: 'log_attr_1' }); Sentry.getGlobalScope().setAttributes({ global_scope_attr: true }); -// this attribute will not be sent for now Sentry.getGlobalScope().setAttribute('array_attr', [1, 2, 3]); // global scope, log attribute 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 07af615712ff..5c3e4c7d2ec8 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 @@ -49,6 +49,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, log_attr: { value: 'log_attr_2', type: 'string' }, }, }, @@ -63,6 +64,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, log_attr: { value: 'log_attr_3', type: 'string' }, }, @@ -78,6 +80,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, 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' }, @@ -94,6 +97,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, 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/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts index 1f1e44e97c43..5eb989f8b268 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -209,6 +210,10 @@ sentryTest( type: 'string', value: SDK_VERSION, }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['SpanStreaming']), + }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts index 383ecade3530..b1aaa8c6dd42 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -80,6 +81,10 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: SDK_VERSION, }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), + }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: interactionSegmentSpan!.span_id, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts index c2dcad317a3f..0ea3bbe26eeb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -131,6 +132,10 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow type: 'string', value: SDK_VERSION, }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), + }, 'sentry.segment.id': { type: 'string', value: navigationSpan.span_id, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts index 2344e28c67d4..2068ffef5ef0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -138,6 +139,10 @@ sentryTest( type: 'string', value: SDK_VERSION, }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), + }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: pageloadSpan?.span_id, diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts index d9c18431202b..4ac80208eed2 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts @@ -2,13 +2,14 @@ import type { Envelope, SerializedStreamedSpanContainer } from '@sentry/core'; import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; import { expect, it } from 'vitest'; @@ -175,6 +176,10 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['SpanStreaming']), + }, [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.cloudflare' }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, diff --git a/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts index 15d7eaf99d9a..7ed898351fc2 100644 --- a/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts @@ -15,7 +15,10 @@ const SEGMENT_SPAN = { type: 'integer', value: expect.any(Number), }, - // TODO: 'device.archs' is set but arrays are not yet serialized in span attributes + 'device.archs': { + type: 'array', + value: expect.any(Array), + }, 'device.processor_count': { type: 'integer', value: expect.any(Number), @@ -92,6 +95,10 @@ const SEGMENT_SPAN = { type: 'string', value: expect.any(String), }, + 'sentry.sdk.integrations': { + type: 'array', + value: expect.arrayContaining(['SpanStreaming']), + }, 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/), 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 ee018e45e53b..98cbcdf0f9c8 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 @@ -3,6 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -128,6 +129,10 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['SpanStreaming']), + }, [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' }, @@ -143,8 +148,7 @@ test('sends a streamed span envelope with correct spans for a manually started s 'device.processor_frequency': { type: 'integer', value: expect.any(Number) }, 'culture.locale': { type: 'string', value: expect.any(String) }, 'culture.timezone': { type: 'string', value: expect.any(String) }, - // TODO: device.archs is an array and currently dropped during serialization - // 'device.archs': { type: 'array', value: [expect.any(String)] }, + 'device.archs': { type: 'array', value: expect.any(Array) }, }; // process.availableMemory is only available in Node 22+ diff --git a/dev-packages/node-integration-tests/suites/context-streamed/test.ts b/dev-packages/node-integration-tests/suites/context-streamed/test.ts index 9d1a6ca5099a..b33edf97dbbc 100644 --- a/dev-packages/node-integration-tests/suites/context-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/context-streamed/test.ts @@ -16,8 +16,7 @@ test('nodeContextIntegration sets context attributes on segment spans', async () // Static attributes expect(attrs['app.start_time']).toEqual({ type: 'string', value: expect.any(String) }); - // TODO: device.archs is an array and currently dropped during serialization - // expect(attrs['device.archs']).toEqual({ type: 'array', value: [expect.any(String)] }); + expect(attrs['device.archs']).toEqual({ type: 'array', value: expect.any(Array) }); expect(attrs['device.boot_time']).toEqual({ type: 'string', value: expect.any(String) }); expect(attrs['device.processor_count']).toEqual({ type: 'integer', value: expect.any(Number) }); expect(attrs['device.cpu_description']).toEqual({ type: 'string', value: expect.any(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 index c3be917706e7..c3ad84c1e146 100644 --- a/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts @@ -15,7 +15,6 @@ async function run(): Promise { Sentry.getGlobalScope().setAttribute('global_scope_attr', true); - // this attribute will not be sent for now Sentry.getGlobalScope().setAttributes({ array_attr: [1, 2, 3] }); // global scope, log attribute 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 index e992d70c4de3..d81b602e0527 100644 --- a/dev-packages/node-integration-tests/suites/public-api/logger/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/logger/test.ts @@ -61,6 +61,7 @@ describe('logs', () => { attributes: { ...commonAttributes, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, log_attr: { value: 'log_attr_2', type: 'string' }, }, }, @@ -73,6 +74,7 @@ describe('logs', () => { attributes: { ...commonAttributes, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, log_attr: { value: 'log_attr_3', type: 'string' }, }, @@ -86,6 +88,7 @@ describe('logs', () => { attributes: { ...commonAttributes, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, 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' }, @@ -100,6 +103,7 @@ describe('logs', () => { attributes: { ...commonAttributes, global_scope_attr: { value: true, type: 'boolean' }, + array_attr: { value: [1, 2, 3], type: 'array' }, 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/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 88e3f3686622..372ce9750d90 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 @@ -3,6 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -128,6 +129,10 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['SpanStreaming']), + }, [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' }, @@ -135,8 +140,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) }, 'app.memory': { type: 'integer', value: expect.any(Number) }, - // TODO: device.archs is an array and currently dropped during serialization - // 'device.archs': { type: 'array', value: [expect.any(String)] }, + 'device.archs': { type: 'array', value: expect.any(Array) }, 'device.boot_time': { type: 'string', value: expect.any(String) }, 'device.memory_size': { type: 'integer', value: expect.any(Number) }, 'device.free_memory': { type: 'integer', value: expect.any(Number) }, diff --git a/packages/browser/test/integrations/spanstreaming.test.ts b/packages/browser/test/integrations/spanstreaming.test.ts index 1d5d587290a3..53e879101d8a 100644 --- a/packages/browser/test/integrations/spanstreaming.test.ts +++ b/packages/browser/test/integrations/spanstreaming.test.ts @@ -1,5 +1,5 @@ import * as SentryCore from '@sentry/core'; -import { debug } from '@sentry/core'; +import { debug, SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient, spanStreamingIntegration } from '../../src'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; @@ -145,6 +145,10 @@ describe('spanStreamingIntegration', () => { type: 'string', value: expect.any(String), }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: ['SpanStreaming'], + }, 'sentry.segment.id': { type: 'string', value: span.spanContext().spanId, diff --git a/packages/core/src/attributes.ts b/packages/core/src/attributes.ts index 1f4a6638f577..2a365e93f37d 100644 --- a/packages/core/src/attributes.ts +++ b/packages/core/src/attributes.ts @@ -15,10 +15,7 @@ type AttributeTypeMap = { integer: number; double: number; boolean: boolean; - 'string[]': Array; - 'integer[]': Array; - 'double[]': Array; - 'boolean[]': Array; + array: Array | Array | Array; }; /* Generates a type from the AttributeTypeMap like: @@ -66,9 +63,9 @@ export function isAttributeObject(maybeObj: unknown): maybeObj is AttributeObjec /** * Converts an attribute value to a typed attribute value. * - * For now, we intentionally only support primitive values and attribute objects with primitive values. - * If @param useFallback is true, we stringify non-primitive values to a string attribute value. Otherwise - * we return `undefined` for unsupported values. + * For now, we support primitive values and arrays, either raw or inside attribute objects. + * If @param useFallback is true, we stringify other non-primitive values to a string attribute + * value. Otherwise we return `undefined` for unsupported values. * * @param value - The value of the passed attribute. * @param useFallback - If true, unsupported values will be stringified to a string attribute value. @@ -170,17 +167,15 @@ function estimatePrimitiveSizeInBytes(value: Primitive): number { } /** - * NOTE: We intentionally do not return anything for non-primitive values: - * - array support will come in the future but if we stringify arrays now, - * sending arrays (unstringified) later will be a subtle breaking change. + * NOTE: We return typed attributes for primitives and arrays: + * - Relay currently only supports arrays consisting of primitive values. Attributes with non-conforming arrays are dropped by Relay, so runtime type validation in the SDK is unnecessary. * - Objects are not supported yet and product support is still TBD. - * - We still keep the type signature for TypedAttributeValue wider to avoid a - * breaking change once we add support for non-primitive values. - * - Once we go back to supporting arrays and stringifying all other values, - * we already implemented the serialization logic here: - * https://github.com/getsentry/sentry-javascript/pull/18165 */ function getTypedAttributeValue(value: unknown): TypedAttributeValue | void { + if (Array.isArray(value)) { + return { value, type: 'array' }; + } + const primitiveType = typeof value === 'string' ? 'string' diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 02b6a4ec08a6..fff57045b65e 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -52,6 +52,8 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID = 'sentry.segment.id'; export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME = 'sentry.sdk.name'; /** The version of the Sentry SDK */ export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION = 'sentry.sdk.version'; +/** The list of integrations enabled in the Sentry SDK (e.g., ["InboundFilters", "BrowserTracing"]) */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS = 'sentry.sdk.integrations'; /** The user ID (gated by sendDefaultPii) */ export const SEMANTIC_ATTRIBUTE_USER_ID = 'user.id'; diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts index bed3f1790740..90cb699238ce 100644 --- a/packages/core/src/tracing/spans/captureSpan.ts +++ b/packages/core/src/tracing/spans/captureSpan.ts @@ -6,6 +6,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -64,6 +65,7 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW if (spanJSON.is_segment) { applyScopeToSegmentSpan(spanJSON, finalScopeData); + applySdkMetadataToSegmentSpan(spanJSON, client); // Allow hook subscribers to mutate the segment span JSON // This also invokes the `processSegmentSpan` hook of all integrations client.emit('processSegmentSpan', spanJSON); @@ -118,6 +120,15 @@ export function safeSetSpanJSONAttributes( }); } +function applySdkMetadataToSegmentSpan(segmentSpanJSON: StreamedSpanJSON, client: Client): void { + const integrationNames = client.getOptions().integrations.map(i => i.name); + if (!integrationNames.length) return; + + safeSetSpanJSONAttributes(segmentSpanJSON, { + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: integrationNames, + }); +} + function applyCommonSpanAttributes( spanJSON: StreamedSpanJSON, serializedSegmentSpan: StreamedSpanJSON, diff --git a/packages/core/test/lib/attributes.test.ts b/packages/core/test/lib/attributes.test.ts index 13b9e026e6e9..e7af21b9bada 100644 --- a/packages/core/test/lib/attributes.test.ts +++ b/packages/core/test/lib/attributes.test.ts @@ -76,33 +76,43 @@ describe('attributeValueToTypedAttributeValue', () => { ); }); - describe('invalid values (non-primitives)', () => { + // Element types are not validated at runtime by the SDK (Relay drops non-conforming arrays). + describe('arrays', () => { it.each([ - ['foo', 'bar'], - [1, 2, 3], - [true, false, true], - [1, 'foo', true], - { foo: 'bar' }, - () => 'test', - Symbol('test'), - ])('returns undefined for non-primitive raw values (%s)', value => { + [['foo', 'bar']], + [[1, 2, 3]], + [[true, false, true]], + [[1, 'foo', true]], + [[NaN, 1, 2]], + [{ value: ['foo', 'bar'] }], + ])('emits a typed array attribute for value %j', value => { const result = attributeValueToTypedAttributeValue(value); - expect(result).toBeUndefined(); + const expected = Array.isArray(value) ? value : (value as { value: unknown[] }).value; + expect(result).toStrictEqual({ value: expected, type: 'array' }); }); - it.each([ - ['foo', 'bar'], - [1, 2, 3], - [true, false, true], - [1, 'foo', true], - { foo: 'bar' }, - () => 'test', - Symbol('test'), - ])('returns undefined for non-primitive attribute object values (%s)', value => { - const result = attributeValueToTypedAttributeValue({ value }); - expect(result).toBeUndefined(); + it('emits a typed array attribute for empty arrays', () => { + expect(attributeValueToTypedAttributeValue([])).toStrictEqual({ value: [], type: 'array' }); }); }); + + describe('invalid values (non-primitives)', () => { + it.each([[{ foo: 'bar' }], [() => 'test'], [Symbol('test')]])( + 'returns undefined for non-primitive raw values (%s)', + value => { + const result = attributeValueToTypedAttributeValue(value); + expect(result).toBeUndefined(); + }, + ); + + it.each([[{ foo: 'bar' }], [() => 'test'], [Symbol('test')]])( + 'returns undefined for non-primitive attribute object values (%s)', + value => { + const result = attributeValueToTypedAttributeValue({ value }); + expect(result).toBeUndefined(); + }, + ); + }); }); describe('with fallback=true', () => { @@ -189,38 +199,6 @@ describe('attributeValueToTypedAttributeValue', () => { }); describe('invalid values (non-primitives) - stringified fallback', () => { - it('stringifies string arrays', () => { - const result = attributeValueToTypedAttributeValue(['foo', 'bar'], true); - expect(result).toStrictEqual({ - value: '["foo","bar"]', - type: 'string', - }); - }); - - it('stringifies number arrays', () => { - const result = attributeValueToTypedAttributeValue([1, 2, 3], true); - expect(result).toStrictEqual({ - value: '[1,2,3]', - type: 'string', - }); - }); - - it('stringifies boolean arrays', () => { - const result = attributeValueToTypedAttributeValue([true, false, true], true); - expect(result).toStrictEqual({ - value: '[true,false,true]', - type: 'string', - }); - }); - - it('stringifies mixed arrays', () => { - const result = attributeValueToTypedAttributeValue([1, 'foo', true], true); - expect(result).toStrictEqual({ - value: '[1,"foo",true]', - type: 'string', - }); - }); - it('stringifies objects', () => { const result = attributeValueToTypedAttributeValue({ foo: 'bar' }, true); expect(result).toStrictEqual({ @@ -425,15 +403,17 @@ describe('serializeAttributes', () => { describe('invalid (non-primitive) values', () => { it("doesn't fall back to stringification by default", () => { const result = serializeAttributes({ foo: { some: 'object' }, bar: [1, 2, 3], baz: () => {} }); - expect(result).toStrictEqual({}); + expect(result).toStrictEqual({ + bar: { type: 'array', value: [1, 2, 3] }, + }); }); it('falls back to stringification of unsupported non-primitive values if fallback is true', () => { const result = serializeAttributes({ foo: { some: 'object' }, bar: [1, 2, 3], baz: () => {} }, true); expect(result).toStrictEqual({ bar: { - type: 'string', - value: '[1,2,3]', + type: 'array', + value: [1, 2, 3], }, baz: { type: 'string', @@ -445,5 +425,12 @@ describe('serializeAttributes', () => { }, }); }); + + // Element types are not validated at runtime by the SDK (Relay drops non-conforming arrays). + it('accepts mixed-type arrays', () => { + expect(serializeAttributes({ mixed: ['a', 1] })).toStrictEqual({ + mixed: { type: 'array', value: ['a', 1] }, + }); + }); }); }); diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 360485f5ca84..48c93c7cf1d1 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -191,7 +191,6 @@ describe('_INTERNAL_captureLog', () => { scope.setAttribute('scope_2', { value: 38, unit: 'gigabyte' }); scope.setAttributes({ scope_3: true, - // these are invalid since for now we don't support arrays scope_4: [1, 2, 3], scope_5: { value: [true, false, true], unit: 'second' }, }); @@ -229,6 +228,15 @@ describe('_INTERNAL_captureLog', () => { type: 'boolean', value: true, }, + scope_4: { + type: 'array', + value: [1, 2, 3], + }, + scope_5: { + type: 'array', + value: [true, false, true], + unit: 'second', + }, 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 186f7f23a536..380229c80374 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -7,6 +7,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, @@ -291,6 +292,86 @@ describe('captureSpan', () => { }); }); + it('adds sentry.sdk.integrations to segment spans as an array attribute', () => { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://dsn@ingest.f00.f00/1', + tracesSampleRate: 1, + release: '1.0.0', + environment: 'staging', + integrations: [ + { name: 'InboundFilters', setupOnce: () => {} }, + { name: 'BrowserTracing', setupOnce: () => {} }, + ], + _metadata: { + sdk: { + name: 'sentry.javascript.browser', + version: '9.0.0', + }, + }, + }), + ); + + const span = withScope(scope => { + scope.setClient(client); + const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } }); + span.end(); + return span; + }); + + expect(captureSpan(span, client)).toStrictEqual({ + span_id: expect.stringMatching(/^[\da-f]{16}$/), + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + parent_span_id: undefined, + links: undefined, + start_timestamp: expect.any(Number), + name: 'my-span', + end_timestamp: expect.any(Number), + status: 'ok', + is_segment: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { value: 'my-span', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { value: span.spanContext().spanId, type: 'string' }, + 'sentry.span.source': { value: 'custom', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { value: 'custom', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { value: '1.0.0', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'staging', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { value: '9.0.0', type: 'string' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: ['InboundFilters', 'BrowserTracing'], + }, + }, + _segmentSpan: span, + }); + }); + + it('does not add sentry.sdk.integrations to non-segment child spans', () => { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://dsn@ingest.f00.f00/1', + tracesSampleRate: 1, + integrations: [{ name: 'InboundFilters', setupOnce: () => {} }], + }), + ); + + const serializedChild = withScope(scope => { + scope.setClient(client); + return startSpan({ name: 'segment' }, () => { + const childSpan = startInactiveSpan({ name: 'child' }); + childSpan.end(); + return captureSpan(childSpan, client); + }); + }); + + expect(serializedChild.is_segment).toBe(false); + expect(serializedChild.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]).toBeUndefined(); + }); + describe('client hooks', () => { it('calls processSpan and processSegmentSpan hooks for a segment span', () => { const client = new TestClient( diff --git a/packages/core/test/lib/tracing/spans/estimateSize.test.ts b/packages/core/test/lib/tracing/spans/estimateSize.test.ts index 35d569691dea..e92b260839f2 100644 --- a/packages/core/test/lib/tracing/spans/estimateSize.test.ts +++ b/packages/core/test/lib/tracing/spans/estimateSize.test.ts @@ -130,9 +130,9 @@ describe('estimateSerializedSpanSizeInBytes', () => { status: 'ok', is_segment: false, attributes: { - 'item.ids': { type: 'string[]', value: ['id-001', 'id-002', 'id-003', 'id-004', 'id-005'] }, - scores: { type: 'double[]', value: [1.1, 2.2, 3.3, 4.4] }, - flags: { type: 'boolean[]', value: [true, false, true] }, + 'item.ids': { type: 'array', value: ['id-001', 'id-002', 'id-003', 'id-004', 'id-005'] }, + scores: { type: 'array', value: [1.1, 2.2, 3.3, 4.4] }, + flags: { type: 'array', value: [true, false, true] }, }, }; diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index e4a0b31990d7..a2f2dbea7aba 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -622,11 +622,9 @@ describe('spanToJSON', () => { attr1: { type: 'string', value: 'value1' }, attr2: { type: 'integer', value: 2 }, attr3: { type: 'boolean', value: true }, + attr4: { type: 'array', value: [1, 2, 3] }, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test op' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, - // notice the absence of `attr4`! - // for now, we don't yet serialize array attributes. This test will fail - // once we allow serializing them. }, links: [ {