From 303369c176dd18900a0216fdbd9e1dbd13445108 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 09:38:16 +0200 Subject: [PATCH 01/12] feat(browser): Add `onRequestSpanStart` hook to browser tracing integration --- .../src/tracing/browserTracingIntegration.ts | 7 +++- packages/browser/src/tracing/request.ts | 37 ++++++++++++++----- packages/core/src/fetch.ts | 6 +-- packages/core/src/types-hoist/instrument.ts | 2 + .../core/src/utils-hoist/instrument/fetch.ts | 28 +++++++++++++- packages/core/src/utils-hoist/is.ts | 9 +++++ 6 files changed, 72 insertions(+), 17 deletions(-) diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 062b308527d6..0834ddf0a853 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -9,7 +9,7 @@ import { startTrackingLongTasks, startTrackingWebVitals, } from '@sentry-internal/browser-utils'; -import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource } from '@sentry/core'; +import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, WebFetchHeaders } from '@sentry/core'; import { GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, @@ -195,6 +195,11 @@ export interface BrowserTracingOptions { * Default: (url: string) => true */ shouldCreateSpanForRequest?(this: void, url: string): boolean; + + /** + * Is called when spans are started for outgoing requests. + */ + onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; } const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 17dd71f0abba..5b3b1fbba3f7 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -5,7 +5,7 @@ import { extractNetworkProtocol, } from '@sentry-internal/browser-utils'; import type { XhrHint } from '@sentry-internal/browser-utils'; -import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/core'; +import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span, WebFetchHeaders } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -98,6 +98,11 @@ export interface RequestInstrumentationOptions { * Default: (url: string) => true */ shouldCreateSpanForRequest?(this: void, url: string): boolean; + + /** + * Is called when spans are started for outgoing requests. + */ + onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; } const responseToSpanId = new WeakMap(); @@ -108,6 +113,9 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions traceXHR: true, enableHTTPTimings: true, trackFetchStreamPerformance: false, + onRequestSpanStart() { + // noop + }, }; /** Registers span creators for xhr and fetch requests */ @@ -119,10 +127,9 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans); - if (enableHTTPTimings && createdSpan) { - addHTTPTimings(createdSpan); + if (createdSpan) { + if (enableHTTPTimings) { + addHTTPTimings(createdSpan); + } + + let headers; + try { + headers = new Headers(handlerData.xhr.__sentry_xhr_v3__?.request_headers); + } catch { + // noop + } + onRequestSpanStart(createdSpan, { headers }); } }); } diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 5f0c9cc30b56..379097936ef4 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -4,7 +4,7 @@ import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; import type { FetchBreadcrumbHint, HandlerDataFetch, Span, SpanAttributes, SpanOrigin } from './types-hoist'; import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; -import { isInstanceOf } from './utils-hoist/is'; +import { isInstanceOf, isRequest } from './utils-hoist/is'; import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils-hoist/url'; import { hasSpansEnabled } from './utils/hasSpansEnabled'; import { getActiveSpan } from './utils/spanUtils'; @@ -227,10 +227,6 @@ function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string ); } -function isRequest(request: unknown): request is Request { - return typeof Request !== 'undefined' && isInstanceOf(request, Request); -} - function isHeaders(headers: unknown): headers is Headers { return typeof Headers !== 'undefined' && isInstanceOf(headers, Headers); } diff --git a/packages/core/src/types-hoist/instrument.ts b/packages/core/src/types-hoist/instrument.ts index 420482579dd9..5eba6066432a 100644 --- a/packages/core/src/types-hoist/instrument.ts +++ b/packages/core/src/types-hoist/instrument.ts @@ -61,6 +61,8 @@ export interface HandlerDataFetch { error?: unknown; // This is to be consumed by the HttpClient integration virtualError?: unknown; + /** Headers that the user passed to the fetch request. */ + headers?: WebFetchHeaders; } export interface HandlerDataDom { diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/utils-hoist/instrument/fetch.ts index 71c5148fae9c..7d6185d00639 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/utils-hoist/instrument/fetch.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { HandlerDataFetch } from '../../types-hoist'; +import type { HandlerDataFetch, WebFetchHeaders } from '../../types-hoist'; -import { isError } from '../is'; +import { isError, isRequest } from '../is'; import { addNonEnumerableProperty, fill } from '../object'; import { supportsNativeFetch } from '../supports'; import { timestampInSeconds } from '../time'; @@ -67,6 +67,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat startTimestamp: timestampInSeconds() * 1000, // // Adding the error to be able to fingerprint the failed fetch event in HttpClient instrumentation virtualError, + headers: getHeadersFromFetchArgs(args), }; // if there is no callback, fetch is instrumented directly @@ -253,3 +254,26 @@ export function parseFetchArgs(fetchArgs: unknown[]): { method: string; url: str method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET', }; } + +function getHeadersFromFetchArgs(fetchArgs: unknown[]): WebFetchHeaders | undefined { + const [requestArgument, optionsArgument] = fetchArgs; + + try { + if ( + typeof optionsArgument === 'object' && + optionsArgument !== null && + 'headers' in optionsArgument && + optionsArgument.headers + ) { + return new Headers(optionsArgument.headers as any); + } + + if (isRequest(requestArgument)) { + return new Headers(requestArgument.headers); + } + } catch { + // noop + } + + return; +} diff --git a/packages/core/src/utils-hoist/is.ts b/packages/core/src/utils-hoist/is.ts index cfa9bc141e20..ab5e150e2394 100644 --- a/packages/core/src/utils-hoist/is.ts +++ b/packages/core/src/utils-hoist/is.ts @@ -201,3 +201,12 @@ export function isVueViewModel(wat: unknown): boolean { // Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property. return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue)); } + +/** + * Checks whether the given parameter is a Standard Web API Request instance. + * + * Returns false if Request is not available in the current runtime. + */ +export function isRequest(request: unknown): request is Request { + return typeof Request !== 'undefined' && isInstanceOf(request, Request); +} From 0cdb5685aa998ed73bd48ad2cd322bfe13d43715 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 09:41:46 +0200 Subject: [PATCH 02/12] feat(nextjs): Mark clientside prefetch request spans with `http.client.prefetch` op --- .../nextjs/src/client/browserTracingIntegration.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/nextjs/src/client/browserTracingIntegration.ts b/packages/nextjs/src/client/browserTracingIntegration.ts index 53cb3e4f6a3f..a305022fcfe5 100644 --- a/packages/nextjs/src/client/browserTracingIntegration.ts +++ b/packages/nextjs/src/client/browserTracingIntegration.ts @@ -1,4 +1,5 @@ import type { Integration } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; import { nextRouterInstrumentNavigation, nextRouterInstrumentPageLoad } from './routing/nextRoutingInstrumentation'; @@ -12,6 +13,16 @@ export function browserTracingIntegration( ...options, instrumentNavigation: false, instrumentPageLoad: false, + onRequestSpanStart(...args) { + const [span, { headers }] = args; + + // Next.js prefetch requests have a `next-router-prefetch` header + if (headers?.get('next-router-prefetch')) { + span?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.client.prefetch'); + } + + return options.onRequestSpanStart?.(...args); + }, }); const { instrumentPageLoad = true, instrumentNavigation = true } = options; From 9c357f5e9bdd7898e8dec32e38326a50f57d9579 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 14:19:03 +0200 Subject: [PATCH 03/12] Update packages/browser/src/tracing/browserTracingIntegration.ts Co-authored-by: Lukas Stracke --- packages/browser/src/tracing/browserTracingIntegration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 0834ddf0a853..3b192f00b728 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -197,7 +197,9 @@ export interface BrowserTracingOptions { shouldCreateSpanForRequest?(this: void, url: string): boolean; /** - * Is called when spans are started for outgoing requests. + * This callback is invoked directly after a span is started for an outgoing fetch or XHR request. + * You can use it to annotate the span with additional data or attributes, for example by setting + * attributes based on the passed request headers. */ onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; } From e11d453c9554de51c8f7ea6067947f0603ca5622 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 14:20:29 +0200 Subject: [PATCH 04/12] optional --- .../browser/src/tracing/browserTracingIntegration.ts | 2 +- packages/browser/src/tracing/request.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 3b192f00b728..90c97a314ddf 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -201,7 +201,7 @@ export interface BrowserTracingOptions { * You can use it to annotate the span with additional data or attributes, for example by setting * attributes based on the passed request headers. */ - onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; + onRequestSpanStart?(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; } const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 5b3b1fbba3f7..0a8e42547341 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -102,7 +102,7 @@ export interface RequestInstrumentationOptions { /** * Is called when spans are started for outgoing requests. */ - onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; + onRequestSpanStart?(span: Span, requestInformation: { headers?: WebFetchHeaders }): void; } const responseToSpanId = new WeakMap(); @@ -113,9 +113,6 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions traceXHR: true, enableHTTPTimings: true, trackFetchStreamPerformance: false, - onRequestSpanStart() { - // noop - }, }; /** Registers span creators for xhr and fetch requests */ @@ -191,7 +188,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial Date: Fri, 4 Apr 2025 14:41:01 +0200 Subject: [PATCH 05/12] size check --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index eed705e16da6..ca26288b07b3 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -139,7 +139,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '39.5 KB', + limit: '40 KB', }, // Svelte SDK (ESM) { From 3eea48cce43b337981b19d65fe85d48833656a1c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 14:41:06 +0200 Subject: [PATCH 06/12] test --- .../on-request-span-start/init.js | 16 ++++++ .../on-request-span-start/subject.js | 11 ++++ .../on-request-span-start/test.ts | 52 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js new file mode 100644 index 000000000000..e901e17dd087 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 1000, + onRequestSpanStart(span, { headers }) { + span.setAttribute('hook.called.headers', headers.get('foo')); + }, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js new file mode 100644 index 000000000000..494ce7d23a05 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js @@ -0,0 +1,11 @@ +fetch('http://sentry-test-site-fetch.example/', { + headers: { + foo: 'fetch', + }, +}); + +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://sentry-test-site-xhr.example/'); +xhr.setRequestHeader('foo', 'xhr'); +xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts new file mode 100644 index 000000000000..c4835484f9d4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts @@ -0,0 +1,52 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should call onRequestSpanStart hook', async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site-fetch.example/', async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: '', + }); + }); + await page.route('http://sentry-test-site-xhr.example/', async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: '', + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); + + const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers + + expect(tracingEvent.spans).toContainEqual( + expect.objectContaining({ + op: 'http.client', + data: { + 'hook.called.headers': 'fetch', + }, + }), + ); + + expect(tracingEvent.spans).toContainEqual( + expect.objectContaining({ + op: 'http.client', + data: { + 'hook.called.headers': 'xhr', + }, + }), + ); +}); From 94588fdcb676227c283e7711f4a5df583562acf5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 14:56:37 +0200 Subject: [PATCH 07/12] test --- .../nextjs-15/app/prefetching/page.tsx | 9 +++++++++ .../app/prefetching/to-be-prefetched/page.tsx | 3 +++ .../nextjs-15/tests/prefetch-spans.test.ts | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx new file mode 100644 index 000000000000..df55a712657c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx @@ -0,0 +1,9 @@ +import Link from 'next/link'; + +export default function Page() { + return ( + + link + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx new file mode 100644 index 000000000000..8db341ed627b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Hello

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts new file mode 100644 index 000000000000..494144224966 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Prefetch client spans should have the right op', async ({ page }) => { + const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === '/prefetching'; + }); + + await page.goto(`/prefetching`); + + // Make it more likely that nextjs prefetches + await page.hover('#prefetch-link'); + + expect((await pageloadTransactionPromise).spans).toContainEqual( + expect.objectContaining({ + op: 'http.client.prefetch', + }), + ); +}); From d23e28298d35e520ef7b712f719dd1f519b74ef1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 14:59:09 +0200 Subject: [PATCH 08/12] soy un idiota --- packages/browser/src/tracing/browserTracingIntegration.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 90c97a314ddf..fab45cd1ed4f 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -253,6 +253,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Fri, 4 Apr 2025 15:34:41 +0200 Subject: [PATCH 09/12] fix test --- .../on-request-span-start/init.js | 4 +++- .../on-request-span-start/test.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js index e901e17dd087..2c85bd05b765 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js @@ -8,7 +8,9 @@ Sentry.init({ Sentry.browserTracingIntegration({ idleTimeout: 1000, onRequestSpanStart(span, { headers }) { - span.setAttribute('hook.called.headers', headers.get('foo')); + if (headers) { + span.setAttribute('hook.called.headers', headers.get('foo')); + } }, }), ], diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts index c4835484f9d4..91b0c1333298 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts @@ -35,18 +35,18 @@ sentryTest('should call onRequestSpanStart hook', async ({ browserName, getLocal expect(tracingEvent.spans).toContainEqual( expect.objectContaining({ op: 'http.client', - data: { - 'hook.called.headers': 'fetch', - }, + data: expect.objectContaining({ + 'hook.called.headers': 'xhr', + }), }), ); expect(tracingEvent.spans).toContainEqual( expect.objectContaining({ op: 'http.client', - data: { - 'hook.called.headers': 'xhr', - }, + data: expect.objectContaining({ + 'hook.called.headers': 'fetch', + }), }), ); }); From 8addc769aa6d8c856b4485a16a48060c0945ac22 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Apr 2025 16:33:33 +0200 Subject: [PATCH 10/12] . --- dev-packages/e2e-tests/test-applications/nextjs-15/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json index 66b38e2e5cc0..a79d34746ee4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -18,7 +18,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "15.0.0-canary.182", + "next": "15.3.0-canary.33", "react": "beta", "react-dom": "beta", "typescript": "~5.0.0" From cf111cc094d8ed7046a5fc12ff543b2fef6fa01c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 7 Apr 2025 10:43:08 +0200 Subject: [PATCH 11/12] . --- .../test-applications/nextjs-15/app/prefetching/page.tsx | 2 +- .../nextjs-15/app/prefetching/to-be-prefetched/page.tsx | 2 ++ .../e2e-tests/test-applications/nextjs-15/next-env.d.ts | 2 +- .../test-applications/nextjs-15/tests/prefetch-spans.test.ts | 5 ++++- packages/nextjs/src/client/browserTracingIntegration.ts | 3 +-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx index df55a712657c..4cb811ecf1b4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; export default function Page() { return ( - + link ); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx index 8db341ed627b..83aac90d65cf 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/prefetching/to-be-prefetched/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = 'force-dynamic'; + export default function Page() { return

Hello

; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts index 40c3d68096c2..1b3be0840f3f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts index 494144224966..1d5480c65196 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts @@ -13,7 +13,10 @@ test('Prefetch client spans should have the right op', async ({ page }) => { expect((await pageloadTransactionPromise).spans).toContainEqual( expect.objectContaining({ - op: 'http.client.prefetch', + op: 'http.client', + data: expect.objectContaining({ + 'http.request.prefetch': true, + }), }), ); }); diff --git a/packages/nextjs/src/client/browserTracingIntegration.ts b/packages/nextjs/src/client/browserTracingIntegration.ts index a305022fcfe5..ab9ee6c43748 100644 --- a/packages/nextjs/src/client/browserTracingIntegration.ts +++ b/packages/nextjs/src/client/browserTracingIntegration.ts @@ -1,5 +1,4 @@ import type { Integration } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; import { nextRouterInstrumentNavigation, nextRouterInstrumentPageLoad } from './routing/nextRoutingInstrumentation'; @@ -18,7 +17,7 @@ export function browserTracingIntegration( // Next.js prefetch requests have a `next-router-prefetch` header if (headers?.get('next-router-prefetch')) { - span?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.client.prefetch'); + span?.setAttribute('http.request.prefetch', true); } return options.onRequestSpanStart?.(...args); From 1530703234ed536f6a586562096d0bf59a8b4dec Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 7 Apr 2025 10:47:24 +0200 Subject: [PATCH 12/12] . --- .../test-applications/nextjs-15/tests/prefetch-spans.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts index 1d5480c65196..b59a45f31f8b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts @@ -1,7 +1,9 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('Prefetch client spans should have the right op', async ({ page }) => { +test('Prefetch client spans should have a http.request.prefetch attribute', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', "Prefetch requests don't have the prefetch header in dev mode"); + const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent?.transaction === '/prefetching'; });