Skip to content

Commit 4e7c7ef

Browse files
feat(browser): Add detail to measure spans and add regression tests (#16557)
resolves #16237 In #16348 we had to revert the PR that added `detail` to `measure` spans as attributes. measure API: https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure detail: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure/detail This was [reverted](#16347) because it was causing issues in firefox, specifically this error was being thrown ``` Error: Permission denied to access object at _addMeasureSpans(../../node_modules/@sentry-internal/browser-utils/build/esm/metrics/browserMetrics.js:255:41) at X2e/<(../../node_modules/@sentry-internal/browser-utils/build/esm/metrics/browserMetrics.js:194:9) at addPerformanceEntries(../../node_modules/@sentry-internal/browser-utils/build/esm/metrics/browserMetrics.js:174:48) at idleSpan.beforeSpanEnd(../../node_modules/@sentry/browser/build/npm/esm/tracing/browserTracingIntegration.js:90:9) at span.endapply(../../node_modules/@sentry/browser/node_modules/@sentry/core/build/esm/tracing/idleSpan.js:52:9) at Coe/<(../../node_modules/@sentry/browser/node_modules/@sentry/core/build/esm/tracing/idleSpan.js:196:12) at sentryWrapped(../../node_modules/@sentry/browser/build/npm/esm/helpers.js:38:17) ``` From debugging, this seems to be coming from a `DOMException` being thrown @https://developer.mozilla.org/en-US/docs/Web/API/DOMException This was re-implemented, and then we added tests to validate that this wouldn't break on firefox. --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent d35030d commit 4e7c7ef

File tree

4 files changed

+171
-2
lines changed
  • dev-packages/browser-integration-tests/suites/tracing/metrics
  • packages/browser-utils/src/metrics

4 files changed

+171
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
// Create measures BEFORE SDK initializes
4+
5+
// Create a measure with detail
6+
const measure = performance.measure('restricted-test-measure', {
7+
start: performance.now(),
8+
end: performance.now() + 1,
9+
detail: { test: 'initial-value' },
10+
});
11+
12+
// Simulate Firefox's permission denial by overriding the detail getter
13+
// This mimics the actual Firefox behavior where accessing detail throws
14+
Object.defineProperty(measure, 'detail', {
15+
get() {
16+
throw new DOMException('Permission denied to access object', 'SecurityError');
17+
},
18+
configurable: false,
19+
enumerable: true,
20+
});
21+
22+
window.Sentry = Sentry;
23+
24+
Sentry.init({
25+
dsn: 'https://[email protected]/1337',
26+
integrations: [
27+
Sentry.browserTracingIntegration({
28+
idleTimeout: 9000,
29+
}),
30+
],
31+
tracesSampleRate: 1,
32+
});
33+
34+
// Also create a normal measure to ensure SDK still works
35+
performance.measure('normal-measure', {
36+
start: performance.now(),
37+
end: performance.now() + 50,
38+
detail: 'this-should-work',
39+
});
40+
41+
// Create a measure with complex detail object
42+
performance.measure('complex-detail-measure', {
43+
start: performance.now(),
44+
end: performance.now() + 25,
45+
detail: {
46+
nested: {
47+
array: [1, 2, 3],
48+
object: {
49+
key: 'value',
50+
},
51+
},
52+
metadata: {
53+
type: 'test',
54+
version: '1.0',
55+
tags: ['complex', 'nested', 'object'],
56+
},
57+
},
58+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/core';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
5+
6+
// This is a regression test for https://github.com/getsentry/sentry-javascript/issues/16347
7+
8+
sentryTest(
9+
'should handle permission denial gracefully and still create measure spans',
10+
async ({ getLocalTestUrl, page, browserName }) => {
11+
// Skip test on webkit because we can't validate the detail in the browser
12+
if (shouldSkipTracingTest() || browserName === 'webkit') {
13+
sentryTest.skip();
14+
}
15+
16+
const url = await getLocalTestUrl({ testDir: __dirname });
17+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
18+
19+
// Find all measure spans
20+
const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure');
21+
expect(measureSpans?.length).toBe(3); // All three measures should create spans
22+
23+
// Test 1: Verify the restricted-test-measure span exists but has no detail
24+
const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure');
25+
expect(restrictedMeasure).toBeDefined();
26+
expect(restrictedMeasure?.data).toMatchObject({
27+
'sentry.op': 'measure',
28+
'sentry.origin': 'auto.resource.browser.metrics',
29+
});
30+
31+
// Verify no detail attributes were added due to the permission error
32+
const restrictedDataKeys = Object.keys(restrictedMeasure?.data || {});
33+
const restrictedDetailKeys = restrictedDataKeys.filter(key => key.includes('detail'));
34+
expect(restrictedDetailKeys).toHaveLength(0);
35+
36+
// Test 2: Verify the normal measure still captures detail correctly
37+
const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure');
38+
expect(normalMeasure).toBeDefined();
39+
expect(normalMeasure?.data).toMatchObject({
40+
'sentry.browser.measure.detail': 'this-should-work',
41+
'sentry.op': 'measure',
42+
'sentry.origin': 'auto.resource.browser.metrics',
43+
});
44+
45+
// Test 3: Verify the complex detail object is captured correctly
46+
const complexMeasure = measureSpans?.find(span => span.description === 'complex-detail-measure');
47+
expect(complexMeasure).toBeDefined();
48+
expect(complexMeasure?.data).toMatchObject({
49+
'sentry.op': 'measure',
50+
'sentry.origin': 'auto.resource.browser.metrics',
51+
// The entire nested object is stringified as a single value
52+
'sentry.browser.measure.detail.nested': JSON.stringify({
53+
array: [1, 2, 3],
54+
object: {
55+
key: 'value',
56+
},
57+
}),
58+
'sentry.browser.measure.detail.metadata': JSON.stringify({
59+
type: 'test',
60+
version: '1.0',
61+
tags: ['complex', 'nested', 'object'],
62+
}),
63+
});
64+
},
65+
);

dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ performance.measure('Next.js-before-hydration', {
1010
window.Sentry = Sentry;
1111

1212
Sentry.init({
13-
debug: true,
1413
dsn: 'https://[email protected]/1337',
1514
integrations: [
1615
Sentry.browserTracingIntegration({

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/* eslint-disable max-lines */
2-
import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/core';
2+
import type { Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core';
33
import {
44
browserPerformanceTimeOrigin,
55
getActiveSpan,
66
getComponentName,
77
htmlTreeAsString,
8+
isPrimitive,
89
parseUrl,
910
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1011
setMeasurement,
@@ -483,6 +484,8 @@ export function _addMeasureSpans(
483484
attributes['sentry.browser.measure_start_time'] = measureStartTimestamp;
484485
}
485486

487+
_addDetailToSpanAttributes(attributes, entry as PerformanceMeasure);
488+
486489
// Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process.
487490
if (measureStartTimestamp <= measureEndTimestamp) {
488491
startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, {
@@ -493,6 +496,50 @@ export function _addMeasureSpans(
493496
}
494497
}
495498

499+
function _addDetailToSpanAttributes(attributes: SpanAttributes, performanceMeasure: PerformanceMeasure): void {
500+
try {
501+
// Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions
502+
const detail = performanceMeasure.detail;
503+
504+
if (!detail) {
505+
return;
506+
}
507+
508+
// Process detail based on its type
509+
if (typeof detail === 'object') {
510+
// Handle object details
511+
for (const [key, value] of Object.entries(detail)) {
512+
if (value && isPrimitive(value)) {
513+
attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue;
514+
} else if (value !== undefined) {
515+
try {
516+
// This is user defined so we can't guarantee it's serializable
517+
attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value);
518+
} catch {
519+
// Skip values that can't be stringified
520+
}
521+
}
522+
}
523+
return;
524+
}
525+
526+
if (isPrimitive(detail)) {
527+
// Handle primitive details
528+
attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue;
529+
return;
530+
}
531+
532+
try {
533+
attributes['sentry.browser.measure.detail'] = JSON.stringify(detail);
534+
} catch {
535+
// Skip if stringification fails
536+
}
537+
} catch {
538+
// Silently ignore any errors when accessing detail
539+
// This handles the Firefox "Permission denied to access object" error
540+
}
541+
}
542+
496543
/**
497544
* Instrument navigation entries
498545
* exported only for tests

0 commit comments

Comments
 (0)