Skip to content

Commit f91b133

Browse files
AbhiPrasadmydea
andauthored
fix(browser): Make sure measure spans have valid start timestamps (#12648)
Co-authored-by: Francesco Novy <francesco.novy@sentry.io>
1 parent 36a15fa commit f91b133

File tree

3 files changed

+95
-27
lines changed

3 files changed

+95
-27
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Add measure before SDK initializes
2+
const end = performance.now();
3+
performance.measure('Next.js-before-hydration', {
4+
duration: 1000,
5+
end,
6+
});
7+
8+
import * as Sentry from '@sentry/browser';
9+
10+
window.Sentry = Sentry;
11+
12+
Sentry.init({
13+
debug: true,
14+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
15+
integrations: [
16+
Sentry.browserTracingIntegration({
17+
idleTimeout: 9000,
18+
}),
19+
],
20+
tracesSampleRate: 1,
21+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
6+
7+
// Validation test for https://github.com/getsentry/sentry-javascript/issues/12281
8+
sentryTest('should add browser-related spans to pageload transaction', async ({ getLocalTestPath, page }) => {
9+
if (shouldSkipTracingTest()) {
10+
sentryTest.skip();
11+
}
12+
13+
const url = await getLocalTestPath({ testDir: __dirname });
14+
15+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
16+
const browserSpans = eventData.spans?.filter(({ op }) => op === 'browser');
17+
18+
// Spans `domContentLoadedEvent`, `connect`, `cache` and `DNS` are not
19+
// always inside `pageload` transaction.
20+
expect(browserSpans?.length).toBeGreaterThanOrEqual(4);
21+
22+
const requestSpan = browserSpans!.find(({ description }) => description === 'request');
23+
expect(requestSpan).toBeDefined();
24+
25+
const measureSpan = eventData.spans?.find(({ op }) => op === 'measure');
26+
expect(measureSpan).toBeDefined();
27+
28+
expect(requestSpan!.start_timestamp).toBeLessThanOrEqual(measureSpan!.start_timestamp);
29+
expect(measureSpan?.data).toEqual({
30+
'sentry.browser.measure_happened_before_request': true,
31+
'sentry.browser.measure_start_time': expect.any(Number),
32+
'sentry.op': 'measure',
33+
'sentry.origin': 'auto.resource.browser.metrics',
34+
});
35+
});

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

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -342,15 +342,34 @@ export function _addMeasureSpans(
342342
duration: number,
343343
timeOrigin: number,
344344
): number {
345-
const measureStartTimestamp = timeOrigin + startTime;
346-
const measureEndTimestamp = measureStartTimestamp + duration;
345+
const navEntry = getNavigationEntry();
346+
const requestTime = msToSec(navEntry ? navEntry.requestStart : 0);
347+
// Because performance.measure accepts arbitrary timestamps it can produce
348+
// spans that happen before the browser even makes a request for the page.
349+
//
350+
// An example of this is the automatically generated Next.js-before-hydration
351+
// spans created by the Next.js framework.
352+
//
353+
// To prevent this we will pin the start timestamp to the request start time
354+
// This does make duration inaccruate, so if this does happen, we will add
355+
// an attribute to the span
356+
const measureStartTimestamp = timeOrigin + Math.max(startTime, requestTime);
357+
const startTimeStamp = timeOrigin + startTime;
358+
const measureEndTimestamp = startTimeStamp + duration;
359+
360+
const attributes: SpanAttributes = {
361+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
362+
};
363+
364+
if (measureStartTimestamp !== startTimeStamp) {
365+
attributes['sentry.browser.measure_happened_before_request'] = true;
366+
attributes['sentry.browser.measure_start_time'] = measureStartTimestamp;
367+
}
347368

348369
startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, {
349370
name: entry.name as string,
350371
op: entry.entryType as string,
351-
attributes: {
352-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
353-
},
372+
attributes,
354373
});
355374

356375
return measureStartTimestamp;
@@ -395,36 +414,29 @@ function _addPerformanceNavigationTiming(
395414
/** Create request and response related spans */
396415
// eslint-disable-next-line @typescript-eslint/no-explicit-any
397416
function _addRequest(span: Span, entry: Record<string, any>, timeOrigin: number): void {
417+
const requestStartTimestamp = timeOrigin + msToSec(entry.requestStart as number);
418+
const responseEndTimestamp = timeOrigin + msToSec(entry.responseEnd as number);
419+
const responseStartTimestamp = timeOrigin + msToSec(entry.responseStart as number);
398420
if (entry.responseEnd) {
399421
// It is possible that we are collecting these metrics when the page hasn't finished loading yet, for example when the HTML slowly streams in.
400422
// In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0.
401423
// In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect
402424
// these spans when the responseEnd value is available. The backend (Relay) would drop the entire span if it contained faulty spans.
403-
startAndEndSpan(
404-
span,
405-
timeOrigin + msToSec(entry.requestStart as number),
406-
timeOrigin + msToSec(entry.responseEnd as number),
407-
{
408-
op: 'browser',
409-
name: 'request',
410-
attributes: {
411-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
412-
},
425+
startAndEndSpan(span, requestStartTimestamp, responseEndTimestamp, {
426+
op: 'browser',
427+
name: 'request',
428+
attributes: {
429+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
413430
},
414-
);
431+
});
415432

416-
startAndEndSpan(
417-
span,
418-
timeOrigin + msToSec(entry.responseStart as number),
419-
timeOrigin + msToSec(entry.responseEnd as number),
420-
{
421-
op: 'browser',
422-
name: 'response',
423-
attributes: {
424-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
425-
},
433+
startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, {
434+
op: 'browser',
435+
name: 'response',
436+
attributes: {
437+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
426438
},
427-
);
439+
});
428440
}
429441
}
430442

0 commit comments

Comments
 (0)