Skip to content

Commit 78ec3a0

Browse files
authored
Merge branch 'develop' into sig/nuxt-external-init
2 parents a0079a8 + 5eafa40 commit 78ec3a0

File tree

8 files changed

+151
-205
lines changed

8 files changed

+151
-205
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://[email protected]/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+
});

dev-packages/rollup-utils/plugins/npmPlugins.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import * as fs from 'fs';
1111
import * as path from 'path';
1212

13-
import { codecovRollupPlugin } from '@codecov/rollup-plugin';
1413
import json from '@rollup/plugin-json';
1514
import replace from '@rollup/plugin-replace';
1615
import cleanup from 'rollup-plugin-cleanup';

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@
9292
],
9393
"devDependencies": {
9494
"@biomejs/biome": "^1.4.0",
95-
"@codecov/rollup-plugin": "0.0.1-beta.5",
9695
"@rollup/plugin-commonjs": "^25.0.7",
9796
"@rollup/plugin-esm-shim": "^0.1.5",
9897
"@rollup/plugin-json": "^6.1.0",
@@ -112,7 +111,6 @@
112111
"codecov": "^3.6.5",
113112
"deepmerge": "^4.2.2",
114113
"downlevel-dts": "~0.11.0",
115-
"es-check": "7.1.0",
116114
"eslint": "7.32.0",
117115
"jest": "^27.5.1",
118116
"jest-environment-node": "^27.5.1",

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

packages/nextjs/src/config/types.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,22 @@ export type NextConfigFunction = (
404404
* Webpack config
405405
*/
406406

407+
// Note: The interface for `ignoreWarnings` is larger but we only need this. See https://webpack.js.org/configuration/other-options/#ignorewarnings
408+
export type IgnoreWarningsOption = (
409+
| { module?: RegExp; message?: RegExp }
410+
| ((
411+
webpackError: {
412+
module?: {
413+
readableIdentifier: (requestShortener: unknown) => string;
414+
};
415+
message: string;
416+
},
417+
compilation: {
418+
requestShortener: unknown;
419+
},
420+
) => boolean)
421+
)[];
422+
407423
// The two possible formats for providing custom webpack config in `next.config.js`
408424
export type WebpackConfigFunction = (config: WebpackConfigObject, options: BuildContext) => WebpackConfigObject;
409425
export type WebpackConfigObject = {
@@ -413,7 +429,7 @@ export type WebpackConfigObject = {
413429
output: { filename: string; path: string };
414430
target: string;
415431
context: string;
416-
ignoreWarnings?: { module?: RegExp }[]; // Note: The interface for `ignoreWarnings` is larger but we only need this. See https://webpack.js.org/configuration/other-options/#ignorewarnings
432+
ignoreWarnings?: IgnoreWarningsOption;
417433
resolve?: {
418434
modules?: string[];
419435
alias?: { [key: string]: string | boolean };

packages/nextjs/src/config/webpack.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { VercelCronsConfig } from '../common/types';
1414
import type {
1515
BuildContext,
1616
EntryPropertyObject,
17+
IgnoreWarningsOption,
1718
NextConfigObject,
1819
SentryBuildOptions,
1920
WebpackConfigFunction,
@@ -72,9 +73,7 @@ export function constructWebpackConfigFunction(
7273
// Add a loader which will inject code that sets global values
7374
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext);
7475

75-
if (isServer) {
76-
addOtelWarningIgnoreRule(newConfig);
77-
}
76+
addOtelWarningIgnoreRule(newConfig);
7877

7978
let pagesDirPath: string | undefined;
8079
const maybePagesDirPath = path.join(projectDir, 'pages');
@@ -668,9 +667,28 @@ function getRequestAsyncStorageModuleLocation(
668667

669668
function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules): void {
670669
const ignoreRules = [
670+
// Inspired by @matmannion: https://github.com/getsentry/sentry-javascript/issues/12077#issuecomment-2180307072
671+
(warning, compilation) => {
672+
// This is wapped in try-catch because we are vendoring types for this hook and we can't be 100% sure that we are accessing API that is there
673+
try {
674+
if (!warning.module) {
675+
return false;
676+
}
677+
678+
const isDependencyThatMayRaiseCriticalDependencyMessage =
679+
/@opentelemetry\/instrumentation/.test(warning.module.readableIdentifier(compilation.requestShortener)) ||
680+
/@prisma\/instrumentation/.test(warning.module.readableIdentifier(compilation.requestShortener));
681+
const isCriticalDependencyMessage = /Critical dependency/.test(warning.message);
682+
683+
return isDependencyThatMayRaiseCriticalDependencyMessage && isCriticalDependencyMessage;
684+
} catch {
685+
return false;
686+
}
687+
},
688+
// We provide these objects in addition to the hook above to provide redundancy in case the hook fails.
671689
{ module: /@opentelemetry\/instrumentation/, message: /Critical dependency/ },
672690
{ module: /@prisma\/instrumentation/, message: /Critical dependency/ },
673-
];
691+
] satisfies IgnoreWarningsOption;
674692

675693
if (newConfig.ignoreWarnings === undefined) {
676694
newConfig.ignoreWarnings = ignoreRules;

0 commit comments

Comments
 (0)