From b2a253951aa53af2c7b219fd0ac62cd2a1f065ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Jun 2025 18:17:08 +0000 Subject: [PATCH 1/8] Add support for capturing performance measure details as span attributes --- .../pageload-measure-spans-firefox/init.js | 38 ++++++++++++++++ .../pageload-measure-spans-firefox/test.ts | 42 ++++++++++++++++++ .../pageload-measure-spans-minimal/init.js | 16 +++++++ .../pageload-measure-spans-minimal/test.ts | 24 +++++++++++ .../metrics/pageload-measure-spans/init.js | 24 +++++++++++ .../metrics/pageload-measure-spans/test.ts | 34 +++++++++++++-- .../src/metrics/browserMetrics.ts | 43 ++++++++++++++++++- 7 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/init.js new file mode 100644 index 000000000000..7d32e3a4988e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/init.js @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/browser'; + +// Create measures BEFORE SDK initializes + +// Create a measure with detail +const measure = performance.measure('firefox-test-measure', { + duration: 100, + detail: { test: 'initial-value' }, +}); + +// Simulate Firefox's permission denial by overriding the detail getter +// This mimics the actual Firefox behavior where accessing detail throws +Object.defineProperty(measure, 'detail', { + get() { + throw new DOMException('Permission denied to access object', 'SecurityError'); + }, + configurable: false, + enumerable: true, +}); + +// Also create a normal measure to ensure SDK still works +performance.measure('normal-measure', { + duration: 50, + detail: 'this-should-work', +}); + +window.Sentry = Sentry; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts new file mode 100644 index 000000000000..e003b486d596 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should handle Firefox permission denial gracefully and still create measure spans', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + // Find all measure spans + const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); + expect(measureSpans?.length).toBe(2); // Both measures should create spans + + // Test 1: Verify the firefox-test-measure span exists but has no detail + const firefoxMeasure = measureSpans?.find(span => span.description === 'firefox-test-measure'); + expect(firefoxMeasure).toBeDefined(); + expect(firefoxMeasure?.data).toMatchObject({ + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + + // Verify no detail attributes were added due to the permission error + const firefoxDataKeys = Object.keys(firefoxMeasure?.data || {}); + const firefoxDetailKeys = firefoxDataKeys.filter(key => key.includes('detail')); + expect(firefoxDetailKeys).toHaveLength(0); + + // Test 2: Verify the normal measure still captures detail correctly + const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure'); + expect(normalMeasure).toBeDefined(); + expect(normalMeasure?.data).toMatchObject({ + 'sentry.browser.measure.detail': 'this-should-work', + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/init.js new file mode 100644 index 000000000000..fc4736e4537e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +// Create a simple measure with detail before SDK init +performance.measure('test-measure', { + duration: 100, + detail: { foo: 'bar' }, +}); + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/test.ts new file mode 100644 index 000000000000..837b657ba0ed --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/test.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture measure detail as span attributes', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const measureSpan = eventData.spans?.find(({ op }) => op === 'measure'); + expect(measureSpan).toBeDefined(); + expect(measureSpan?.description).toBe('test-measure'); + + // Verify detail was captured + expect(measureSpan?.data).toMatchObject({ + 'sentry.browser.measure.detail.foo': 'bar', + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index db9c448ed19b..01e4084793df 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -2,9 +2,33 @@ import * as Sentry from '@sentry/browser'; const end = performance.now(); + +// Test 1: Measure with object detail performance.measure('Next.js-before-hydration', { duration: 1000, end, + detail: { + component: 'HomePage', + renderTime: 123.45, + isSSR: true, + }, +}); + +// Test 2: Measure with primitive detail +performance.measure('custom-metric', { + duration: 500, + detail: 'simple-string-detail', +}); + +// Test 3: Measure with complex detail +performance.measure('complex-measure', { + duration: 200, + detail: { + nested: { + value: 'test', + array: [1, 2, 3], + }, + }, }); window.Sentry = Sentry; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts index 8d331118028e..3a31de39e2b1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts @@ -22,14 +22,40 @@ sentryTest('should add browser-related spans to pageload transaction', async ({ expect(requestSpan).toBeDefined(); expect(requestSpan?.description).toBe(page.url()); - const measureSpan = eventData.spans?.find(({ op }) => op === 'measure'); - expect(measureSpan).toBeDefined(); + // Find all measure spans + const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); + expect(measureSpans?.length).toBe(3); // We created 3 measures in init.js - expect(requestSpan!.start_timestamp).toBeLessThanOrEqual(measureSpan!.start_timestamp); - expect(measureSpan?.data).toEqual({ + // Test 1: Verify object detail is captured + const nextJsMeasure = measureSpans?.find(span => span.description === 'Next.js-before-hydration'); + expect(nextJsMeasure).toBeDefined(); + expect(nextJsMeasure?.data).toMatchObject({ 'sentry.browser.measure_happened_before_request': true, 'sentry.browser.measure_start_time': expect.any(Number), + 'sentry.browser.measure.detail.component': 'HomePage', + 'sentry.browser.measure.detail.renderTime': 123.45, + 'sentry.browser.measure.detail.isSSR': true, 'sentry.op': 'measure', 'sentry.origin': 'auto.resource.browser.metrics', }); + + // Test 2: Verify primitive detail is captured + const customMetricMeasure = measureSpans?.find(span => span.description === 'custom-metric'); + expect(customMetricMeasure).toBeDefined(); + expect(customMetricMeasure?.data).toMatchObject({ + 'sentry.browser.measure.detail': 'simple-string-detail', + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + + // Test 3: Verify complex detail is stringified + const complexMeasure = measureSpans?.find(span => span.description === 'complex-measure'); + expect(complexMeasure).toBeDefined(); + expect(complexMeasure?.data).toMatchObject({ + 'sentry.browser.measure.detail.nested': '{"value":"test","array":[1,2,3]}', + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + + expect(requestSpan!.start_timestamp).toBeLessThanOrEqual(nextJsMeasure!.start_timestamp); }); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 7695802941a6..8fa0b371b3f1 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,10 +1,11 @@ /* eslint-disable max-lines */ -import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/core'; +import type { Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core'; import { browserPerformanceTimeOrigin, getActiveSpan, getComponentName, htmlTreeAsString, + isPrimitive, parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, @@ -483,6 +484,46 @@ export function _addMeasureSpans( attributes['sentry.browser.measure_start_time'] = measureStartTimestamp; } + // Safely access and process detail property + // Cast to PerformanceMeasure to access detail property + const performanceMeasure = entry as PerformanceMeasure; + if (performanceMeasure.detail !== undefined) { + try { + // Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions + const detail = performanceMeasure.detail; + + // Process detail based on its type + if (detail && typeof detail === 'object') { + // Handle object details + for (const [key, value] of Object.entries(detail)) { + if (value && isPrimitive(value)) { + attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue; + } else if (value !== undefined) { + try { + // This is user defined so we can't guarantee it's serializable + attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); + } catch { + // Skip values that can't be stringified + } + } + } + } else if (isPrimitive(detail)) { + // Handle primitive details + attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; + } else if (detail !== null) { + // Handle non-primitive, non-object details + try { + attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); + } catch { + // Skip if stringification fails + } + } + } catch { + // Silently ignore any errors when accessing detail + // This handles the Firefox "Permission denied to access object" error + } + } + // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. if (measureStartTimestamp <= measureEndTimestamp) { startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { From b437a1334b10384ca47edb6044c59ac5fb26e4f8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jun 2025 15:08:07 -0400 Subject: [PATCH 2/8] cursor cleanup --- .../init.js | 0 .../test.ts | 0 .../init.js | 0 .../test.ts | 18 ++++++++++-------- 4 files changed, 10 insertions(+), 8 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{pageload-measure-spans-minimal => pageload-measure-spans-details}/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{pageload-measure-spans-minimal => pageload-measure-spans-details}/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{pageload-measure-spans-firefox => pageload-measure-spans-domexception-details}/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{pageload-measure-spans-firefox => pageload-measure-spans-domexception-details}/test.ts (65%) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-minimal/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts index e003b486d596..6635189a6bf6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-firefox/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts @@ -3,8 +3,10 @@ import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +// This is a regression test for https://github.com/getsentry/sentry-javascript/issues/16347 + sentryTest( - 'should handle Firefox permission denial gracefully and still create measure spans', + 'should handle permission denial gracefully and still create measure spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -17,18 +19,18 @@ sentryTest( const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); expect(measureSpans?.length).toBe(2); // Both measures should create spans - // Test 1: Verify the firefox-test-measure span exists but has no detail - const firefoxMeasure = measureSpans?.find(span => span.description === 'firefox-test-measure'); - expect(firefoxMeasure).toBeDefined(); - expect(firefoxMeasure?.data).toMatchObject({ + // Test 1: Verify the restricted-test-measure span exists but has no detail + const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure'); + expect(restrictedMeasure).toBeDefined(); + expect(restrictedMeasure?.data).toMatchObject({ 'sentry.op': 'measure', 'sentry.origin': 'auto.resource.browser.metrics', }); // Verify no detail attributes were added due to the permission error - const firefoxDataKeys = Object.keys(firefoxMeasure?.data || {}); - const firefoxDetailKeys = firefoxDataKeys.filter(key => key.includes('detail')); - expect(firefoxDetailKeys).toHaveLength(0); + const restrictedDataKeys = Object.keys(restrictedMeasure?.data || {}); + const restrictedDetailKeys = restrictedDataKeys.filter(key => key.includes('detail')); + expect(restrictedDetailKeys).toHaveLength(0); // Test 2: Verify the normal measure still captures detail correctly const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure'); From bda13796ff18283b08d727cb71e1a88183da342a Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jun 2025 16:35:52 -0400 Subject: [PATCH 3/8] revert previous changes --- .../metrics/pageload-measure-spans/init.js | 24 ------------- .../metrics/pageload-measure-spans/test.ts | 34 +++---------------- 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index 01e4084793df..db9c448ed19b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -2,33 +2,9 @@ import * as Sentry from '@sentry/browser'; const end = performance.now(); - -// Test 1: Measure with object detail performance.measure('Next.js-before-hydration', { duration: 1000, end, - detail: { - component: 'HomePage', - renderTime: 123.45, - isSSR: true, - }, -}); - -// Test 2: Measure with primitive detail -performance.measure('custom-metric', { - duration: 500, - detail: 'simple-string-detail', -}); - -// Test 3: Measure with complex detail -performance.measure('complex-measure', { - duration: 200, - detail: { - nested: { - value: 'test', - array: [1, 2, 3], - }, - }, }); window.Sentry = Sentry; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts index 3a31de39e2b1..8d331118028e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts @@ -22,40 +22,14 @@ sentryTest('should add browser-related spans to pageload transaction', async ({ expect(requestSpan).toBeDefined(); expect(requestSpan?.description).toBe(page.url()); - // Find all measure spans - const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); - expect(measureSpans?.length).toBe(3); // We created 3 measures in init.js + const measureSpan = eventData.spans?.find(({ op }) => op === 'measure'); + expect(measureSpan).toBeDefined(); - // Test 1: Verify object detail is captured - const nextJsMeasure = measureSpans?.find(span => span.description === 'Next.js-before-hydration'); - expect(nextJsMeasure).toBeDefined(); - expect(nextJsMeasure?.data).toMatchObject({ + expect(requestSpan!.start_timestamp).toBeLessThanOrEqual(measureSpan!.start_timestamp); + expect(measureSpan?.data).toEqual({ 'sentry.browser.measure_happened_before_request': true, 'sentry.browser.measure_start_time': expect.any(Number), - 'sentry.browser.measure.detail.component': 'HomePage', - 'sentry.browser.measure.detail.renderTime': 123.45, - 'sentry.browser.measure.detail.isSSR': true, 'sentry.op': 'measure', 'sentry.origin': 'auto.resource.browser.metrics', }); - - // Test 2: Verify primitive detail is captured - const customMetricMeasure = measureSpans?.find(span => span.description === 'custom-metric'); - expect(customMetricMeasure).toBeDefined(); - expect(customMetricMeasure?.data).toMatchObject({ - 'sentry.browser.measure.detail': 'simple-string-detail', - 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', - }); - - // Test 3: Verify complex detail is stringified - const complexMeasure = measureSpans?.find(span => span.description === 'complex-measure'); - expect(complexMeasure).toBeDefined(); - expect(complexMeasure?.data).toMatchObject({ - 'sentry.browser.measure.detail.nested': '{"value":"test","array":[1,2,3]}', - 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', - }); - - expect(requestSpan!.start_timestamp).toBeLessThanOrEqual(nextJsMeasure!.start_timestamp); }); From 5aa7e87953f8b9ddc50807ee4cce45e0d6666afd Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 12 Jun 2025 16:47:20 -0400 Subject: [PATCH 4/8] fix tests --- .../pageload-measure-spans-details/init.js | 16 ----- .../pageload-measure-spans-details/test.ts | 24 -------- .../init.js | 19 +++--- .../metrics/pageload-measure-spans/init.js | 3 +- .../src/metrics/browserMetrics.ts | 60 +++++++++---------- 5 files changed, 40 insertions(+), 82 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/init.js deleted file mode 100644 index fc4736e4537e..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/init.js +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -// Create a simple measure with detail before SDK init -performance.measure('test-measure', { - duration: 100, - detail: { foo: 'bar' }, -}); - -window.Sentry = Sentry; -window._testBaseTimestamp = performance.timeOrigin / 1000; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration()], - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/test.ts deleted file mode 100644 index 837b657ba0ed..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-details/test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; -import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('should capture measure detail as span attributes', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - const measureSpan = eventData.spans?.find(({ op }) => op === 'measure'); - expect(measureSpan).toBeDefined(); - expect(measureSpan?.description).toBe('test-measure'); - - // Verify detail was captured - expect(measureSpan?.data).toMatchObject({ - 'sentry.browser.measure.detail.foo': 'bar', - 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js index 7d32e3a4988e..cd9a4300b99a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js @@ -3,8 +3,9 @@ import * as Sentry from '@sentry/browser'; // Create measures BEFORE SDK initializes // Create a measure with detail -const measure = performance.measure('firefox-test-measure', { - duration: 100, +const measure = performance.measure('restricted-test-measure', { + start: performance.now(), + end: performance.now() + 1, detail: { test: 'initial-value' }, }); @@ -18,16 +19,9 @@ Object.defineProperty(measure, 'detail', { enumerable: true, }); -// Also create a normal measure to ensure SDK still works -performance.measure('normal-measure', { - duration: 50, - detail: 'this-should-work', -}); - window.Sentry = Sentry; Sentry.init({ - debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ @@ -36,3 +30,10 @@ Sentry.init({ ], tracesSampleRate: 1, }); + +// Also create a normal measure to ensure SDK still works +performance.measure('normal-measure', { + start: performance.now(), + end: performance.now() + 50, + detail: 'this-should-work', +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index db9c448ed19b..7c80335cf37d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -3,14 +3,13 @@ import * as Sentry from '@sentry/browser'; const end = performance.now(); performance.measure('Next.js-before-hydration', { - duration: 1000, + start: performance.now() - 1000, end, }); window.Sentry = Sentry; Sentry.init({ - debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 8fa0b371b3f1..fb0ea9994432 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -487,41 +487,39 @@ export function _addMeasureSpans( // Safely access and process detail property // Cast to PerformanceMeasure to access detail property const performanceMeasure = entry as PerformanceMeasure; - if (performanceMeasure.detail !== undefined) { - try { - // Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions - const detail = performanceMeasure.detail; - - // Process detail based on its type - if (detail && typeof detail === 'object') { - // Handle object details - for (const [key, value] of Object.entries(detail)) { - if (value && isPrimitive(value)) { - attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue; - } else if (value !== undefined) { - try { - // This is user defined so we can't guarantee it's serializable - attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); - } catch { - // Skip values that can't be stringified - } + try { + // Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions + const detail = performanceMeasure.detail; + + // Process detail based on its type + if (detail && typeof detail === 'object') { + // Handle object details + for (const [key, value] of Object.entries(detail)) { + if (value && isPrimitive(value)) { + attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue; + } else if (value !== undefined) { + try { + // This is user defined so we can't guarantee it's serializable + attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); + } catch { + // Skip values that can't be stringified } } - } else if (isPrimitive(detail)) { - // Handle primitive details - attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; - } else if (detail !== null) { - // Handle non-primitive, non-object details - try { - attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); - } catch { - // Skip if stringification fails - } } - } catch { - // Silently ignore any errors when accessing detail - // This handles the Firefox "Permission denied to access object" error + } else if (isPrimitive(detail)) { + // Handle primitive details + attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; + } else if (detail !== null) { + // Handle non-primitive, non-object details + try { + attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); + } catch { + // Skip if stringification fails + } } + } catch { + // Silently ignore any errors when accessing detail + // This handles the Firefox "Permission denied to access object" error } // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. From f7f0763d642c2ffb61b918145ee02a064ad35aaa Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 12 Jun 2025 16:57:51 -0400 Subject: [PATCH 5/8] add another test case --- .../init.js | 19 ++++++++++++++++ .../test.ts | 22 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js index cd9a4300b99a..f4df5dbe13e8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js @@ -37,3 +37,22 @@ performance.measure('normal-measure', { end: performance.now() + 50, detail: 'this-should-work', }); + +// Create a measure with complex detail object +performance.measure('complex-detail-measure', { + start: performance.now(), + end: performance.now() + 25, + detail: { + nested: { + array: [1, 2, 3], + object: { + key: 'value', + }, + }, + metadata: { + type: 'test', + version: '1.0', + tags: ['complex', 'nested', 'object'], + }, + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts index 6635189a6bf6..e37c8050fe8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts @@ -17,7 +17,7 @@ sentryTest( // Find all measure spans const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); - expect(measureSpans?.length).toBe(2); // Both measures should create spans + expect(measureSpans?.length).toBe(3); // All three measures should create spans // Test 1: Verify the restricted-test-measure span exists but has no detail const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure'); @@ -40,5 +40,25 @@ sentryTest( 'sentry.op': 'measure', 'sentry.origin': 'auto.resource.browser.metrics', }); + + // Test 3: Verify the complex detail object is captured correctly + const complexMeasure = measureSpans?.find(span => span.description === 'complex-detail-measure'); + expect(complexMeasure).toBeDefined(); + expect(complexMeasure?.data).toMatchObject({ + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + // The entire nested object is stringified as a single value + 'sentry.browser.measure.detail.nested': JSON.stringify({ + array: [1, 2, 3], + object: { + key: 'value', + }, + }), + 'sentry.browser.measure.detail.metadata': JSON.stringify({ + type: 'test', + version: '1.0', + tags: ['complex', 'nested', 'object'], + }), + }); }, ); From ea1831b121887eacd7e0a540e7babd17bf06d79a Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 12 Jun 2025 17:13:43 -0400 Subject: [PATCH 6/8] revert this test change --- .../suites/tracing/metrics/pageload-measure-spans/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index 7c80335cf37d..f3e6fa567911 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; const end = performance.now(); performance.measure('Next.js-before-hydration', { - start: performance.now() - 1000, + duration: 1000, end, }); From dabedbb937ba8e3977e9ec3cbf25f1b05dfda6a0 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 13 Jun 2025 09:58:18 -0400 Subject: [PATCH 7/8] don't attach null detail --- .../src/metrics/browserMetrics.ts | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index fb0ea9994432..4c5e78899b29 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -484,15 +484,29 @@ export function _addMeasureSpans( attributes['sentry.browser.measure_start_time'] = measureStartTimestamp; } - // Safely access and process detail property - // Cast to PerformanceMeasure to access detail property - const performanceMeasure = entry as PerformanceMeasure; + _addDetailToSpanAttributes(attributes, entry as PerformanceMeasure); + + // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. + if (measureStartTimestamp <= measureEndTimestamp) { + startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { + name: entry.name as string, + op: entry.entryType as string, + attributes, + }); + } +} + +function _addDetailToSpanAttributes(attributes: SpanAttributes, performanceMeasure: PerformanceMeasure): void { try { // Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions const detail = performanceMeasure.detail; + if (!detail) { + return; + } + // Process detail based on its type - if (detail && typeof detail === 'object') { + if (typeof detail === 'object') { // Handle object details for (const [key, value] of Object.entries(detail)) { if (value && isPrimitive(value)) { @@ -506,30 +520,24 @@ export function _addMeasureSpans( } } } - } else if (isPrimitive(detail)) { + return; + } + + if (isPrimitive(detail)) { // Handle primitive details attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; - } else if (detail !== null) { - // Handle non-primitive, non-object details - try { - attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); - } catch { - // Skip if stringification fails - } + return; + } + + try { + attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); + } catch { + // Skip if stringification fails } } catch { // Silently ignore any errors when accessing detail // This handles the Firefox "Permission denied to access object" error } - - // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. - if (measureStartTimestamp <= measureEndTimestamp) { - startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { - name: entry.name as string, - op: entry.entryType as string, - attributes, - }); - } } /** From 65efc548e23aa7ba44b80ae27dc46beedcc9662b Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 13 Jun 2025 10:49:06 -0400 Subject: [PATCH 8/8] special case webkit --- .../pageload-measure-spans-domexception-details/test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts index e37c8050fe8c..a990694b46bf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts @@ -7,8 +7,9 @@ import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../. sentryTest( 'should handle permission denial gracefully and still create measure spans', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { + async ({ getLocalTestUrl, page, browserName }) => { + // Skip test on webkit because we can't validate the detail in the browser + if (shouldSkipTracingTest() || browserName === 'webkit') { sentryTest.skip(); }