From 2ad7ca1fcd6f35b30b5a25ceb41cacfb4765bd5c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 10 Dec 2025 14:29:15 +0100 Subject: [PATCH 01/43] chore(bugbot): Add testing conventions code review rules (#18433) This PR adds some rules to bugbot's rulest to flag some testing-related issues we'd like to avoid. Like with all AI rules, there are for sure exceptions to the new rules, so no problem with us ignoring any of these flags. But I think having an additional reminder that testing is necessary would be a good change. LMK what you think! --- .cursor/BUGBOT.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 891b91d50f90..eac3a13be13a 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -45,3 +45,16 @@ Do not flag the issues below if they appear in tests. convention as the `SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN` value. - When calling `startSpan`, check if error cases are handled. If flag that it might make sense to try/catch and call `captureException`. - When calling `generateInstrumentationOnce`, the passed in name MUST match the name of the integration that uses it. If there are more than one instrumentations, they need to follow the pattern `${INSTRUMENTATION_NAME}.some-suffix`. + +## Testing Conventions + +- When reviewing a `feat` PR, check if the PR includes at least one integration or E2E test. + If neither of the two are present, add a comment, recommending to add one. +- When reviewing a `fix` PR, check if the PR includes at least one unit, integration or e2e test that tests the regression this PR fixes. + Usually this means the test failed prior to the fix and passes with the fix. + If no tests are present, add a comment recommending to add one. +- Check that tests actually test the newly added behaviour. + For instance, when checking on sent payloads by the SDK, ensure that the newly added data is asserted thoroughly. +- Flag usage of `expect.objectContaining` and other relaxed assertions, when a test expects something NOT to be included in a payload but there's no respective assertion. +- Flag usage of conditionals in one test and recommend splitting up the test for the different paths. +- Flag usage of loops testing multiple scenarios in one test and recommend using `(it)|(test).each` instead. From ec8c8c64cde6001123db0199a8ca017b8863eac8 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:07:57 +0100 Subject: [PATCH 02/43] feat(core): Parse individual cookies from cookie header (#18325) Parse each individual cookie header and filter sensitive cookies to at least know which keys the cookie string included. Follow-up on https://github.com/getsentry/sentry-javascript/pull/18311 Closes #18441 --- packages/astro/src/server/middleware.ts | 5 +- packages/bun/src/integrations/bunserver.ts | 6 +- packages/cloudflare/src/request.ts | 9 +- packages/core/src/utils/request.ts | 87 +++++++++++++-- packages/core/test/lib/utils/request.test.ts | 105 ++++++++++++++++-- .../common/utils/addHeadersAsAttributes.ts | 4 +- .../http/httpServerSpansIntegration.ts | 5 +- .../runtime/hooks/wrapMiddlewareHandler.ts | 3 +- packages/remix/src/server/instrumentServer.ts | 5 +- .../sveltekit/src/server-common/handle.ts | 11 +- 10 files changed, 209 insertions(+), 31 deletions(-) diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 64fde266a3f8..a12c25ff6045 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -219,7 +219,10 @@ async function instrumentRequestStartHttpServerSpan( // This is here for backwards compatibility, we used to set this here before method, url: stripUrlQueryAndFragment(ctx.url.href), - ...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)), + ...httpHeadersToSpanAttributes( + winterCGHeadersToDict(request.headers), + getClient()?.getOptions().sendDefaultPii ?? false, + ), }; if (parametrizedRoute) { diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 73998e529349..83e7f5ff4967 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -3,6 +3,7 @@ import { captureException, continueTrace, defineIntegration, + getClient, httpHeadersToSpanAttributes, isURLObjectRelative, parseStringToURLObject, @@ -206,7 +207,10 @@ function wrapRequestHandler( routeName = route; } - Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON())); + Object.assign( + attributes, + httpHeadersToSpanAttributes(request.headers.toJSON(), getClient()?.getOptions().sendDefaultPii ?? false), + ); isolationScope.setSDKProcessingMetadata({ normalizedRequest: { diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 20706e8b9146..c404e57d01d8 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -3,6 +3,7 @@ import { captureException, continueTrace, flush, + getClient, getHttpSpanDetailsFromUrlObject, httpHeadersToSpanAttributes, parseStringToURLObject, @@ -67,7 +68,13 @@ export function wrapRequestHandler( attributes['user_agent.original'] = userAgentHeader; } - Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers))); + Object.assign( + attributes, + httpHeadersToSpanAttributes( + winterCGHeadersToDict(request.headers), + getClient()?.getOptions().sendDefaultPii ?? false, + ), + ); attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 1d3985dd8479..d328a16e05d9 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -128,21 +128,29 @@ function getAbsoluteUrl({ return undefined; } -// "-user" because otherwise it would match "user-agent" const SENSITIVE_HEADER_SNIPPETS = [ 'auth', 'token', 'secret', - 'cookie', - '-user', + 'session', // for the user_session cookie 'password', + 'passwd', + 'pwd', 'key', 'jwt', 'bearer', 'sso', 'saml', + 'csrf', + 'xsrf', + 'credentials', + // Always treat cookie headers as sensitive in case individual key-value cookie pairs cannot properly be extracted + 'set-cookie', + 'cookie', ]; +const PII_HEADER_SNIPPETS = ['x-forwarded-', '-user']; + /** * Converts incoming HTTP request headers to OpenTelemetry span attributes following semantic conventions. * Header names are converted to the format: http.request.header. @@ -152,6 +160,7 @@ const SENSITIVE_HEADER_SNIPPETS = [ */ export function httpHeadersToSpanAttributes( headers: Record, + sendDefaultPii: boolean = false, ): Record { const spanAttributes: Record = {}; @@ -161,16 +170,29 @@ export function httpHeadersToSpanAttributes( return; } - const lowerCasedKey = key.toLowerCase(); - const isSensitive = SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet)); - const normalizedKey = `http.request.header.${lowerCasedKey.replace(/-/g, '_')}`; + const lowerCasedHeaderKey = key.toLowerCase(); + const isCookieHeader = lowerCasedHeaderKey === 'cookie' || lowerCasedHeaderKey === 'set-cookie'; + + if (isCookieHeader && typeof value === 'string' && value !== '') { + // Set-Cookie: single cookie with attributes ("name=value; HttpOnly; Secure") + // Cookie: multiple cookies separated by "; " ("cookie1=value1; cookie2=value2") + const isSetCookie = lowerCasedHeaderKey === 'set-cookie'; + const semicolonIndex = value.indexOf(';'); + const cookieString = isSetCookie && semicolonIndex !== -1 ? value.substring(0, semicolonIndex) : value; + const cookies = isSetCookie ? [cookieString] : cookieString.split('; '); + + for (const cookie of cookies) { + // Split only at the first '=' to preserve '=' characters in cookie values + const equalSignIndex = cookie.indexOf('='); + const cookieKey = equalSignIndex !== -1 ? cookie.substring(0, equalSignIndex) : cookie; + const cookieValue = equalSignIndex !== -1 ? cookie.substring(equalSignIndex + 1) : ''; - if (isSensitive) { - spanAttributes[normalizedKey] = '[Filtered]'; - } else if (Array.isArray(value)) { - spanAttributes[normalizedKey] = value.map(v => (v != null ? String(v) : v)).join(';'); - } else if (typeof value === 'string') { - spanAttributes[normalizedKey] = value; + const lowerCasedCookieKey = cookieKey.toLowerCase(); + + addSpanAttribute(spanAttributes, lowerCasedHeaderKey, lowerCasedCookieKey, cookieValue, sendDefaultPii); + } + } else { + addSpanAttribute(spanAttributes, lowerCasedHeaderKey, '', value, sendDefaultPii); } }); } catch { @@ -180,6 +202,47 @@ export function httpHeadersToSpanAttributes( return spanAttributes; } +function normalizeAttributeKey(key: string): string { + return key.replace(/-/g, '_'); +} + +function addSpanAttribute( + spanAttributes: Record, + headerKey: string, + cookieKey: string, + value: string | string[] | undefined, + sendPii: boolean, +): void { + const normalizedKey = cookieKey + ? `http.request.header.${normalizeAttributeKey(headerKey)}.${normalizeAttributeKey(cookieKey)}` + : `http.request.header.${normalizeAttributeKey(headerKey)}`; + + const headerValue = handleHttpHeader(cookieKey || headerKey, value, sendPii); + if (headerValue !== undefined) { + spanAttributes[normalizedKey] = headerValue; + } +} + +function handleHttpHeader( + lowerCasedKey: string, + value: string | string[] | undefined, + sendPii: boolean, +): string | undefined { + const isSensitive = sendPii + ? SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet)) + : [...PII_HEADER_SNIPPETS, ...SENSITIVE_HEADER_SNIPPETS].some(snippet => lowerCasedKey.includes(snippet)); + + if (isSensitive) { + return '[Filtered]'; + } else if (Array.isArray(value)) { + return value.map(v => (v != null ? String(v) : v)).join(';'); + } else if (typeof value === 'string') { + return value; + } + + return undefined; +} + /** Extract the query params from an URL. */ export function extractQueryParamsFromUrl(url: string): string | undefined { // url is path and query string diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts index 328aebf29209..c17c25802599 100644 --- a/packages/core/test/lib/utils/request.test.ts +++ b/packages/core/test/lib/utils/request.test.ts @@ -527,7 +527,7 @@ describe('request utils', () => { 'X-Forwarded-For': '192.168.1.1', }; - const result = httpHeadersToSpanAttributes(headers); + const result = httpHeadersToSpanAttributes(headers, true); expect(result).toEqual({ 'http.request.header.host': 'example.com', @@ -612,7 +612,7 @@ describe('request utils', () => { }); }); - describe('PII filtering', () => { + describe('PII/Sensitive data filtering', () => { it('filters sensitive headers case-insensitively', () => { const headers = { AUTHORIZATION: 'Bearer secret-token', @@ -625,12 +625,99 @@ describe('request utils', () => { expect(result).toEqual({ 'http.request.header.content_type': 'application/json', - 'http.request.header.cookie': '[Filtered]', + 'http.request.header.cookie.session': '[Filtered]', 'http.request.header.x_api_key': '[Filtered]', 'http.request.header.authorization': '[Filtered]', }); }); + it('attaches and filters sensitive cookie headers', () => { + const headers = { + Cookie: + 'session=abc123; tracking=enabled; cookie-authentication-key-without-value; theme=dark; lang=en; user_session=xyz789; pref=1', + }; + + const result = httpHeadersToSpanAttributes(headers); + + expect(result).toEqual({ + 'http.request.header.cookie.session': '[Filtered]', + 'http.request.header.cookie.tracking': 'enabled', + 'http.request.header.cookie.theme': 'dark', + 'http.request.header.cookie.lang': 'en', + 'http.request.header.cookie.user_session': '[Filtered]', + 'http.request.header.cookie.cookie_authentication_key_without_value': '[Filtered]', + 'http.request.header.cookie.pref': '1', + }); + }); + + it('adds a filtered cookie header when cookie header is present, but has no valid key=value pairs', () => { + const headers1 = { Cookie: ['key', 'val'] }; + const result1 = httpHeadersToSpanAttributes(headers1); + expect(result1).toEqual({ 'http.request.header.cookie': '[Filtered]' }); + + const headers3 = { Cookie: '' }; + const result3 = httpHeadersToSpanAttributes(headers3); + expect(result3).toEqual({ 'http.request.header.cookie': '[Filtered]' }); + }); + + it.each([ + ['preferred-color-mode=light', { 'http.request.header.set_cookie.preferred_color_mode': 'light' }], + ['theme=dark; HttpOnly', { 'http.request.header.set_cookie.theme': 'dark' }], + ['session=abc123; Domain=example.com; HttpOnly', { 'http.request.header.set_cookie.session': '[Filtered]' }], + ['lang=en; Expires=Wed, 21 Oct 2025 07:28:00 GMT', { 'http.request.header.set_cookie.lang': 'en' }], + ['pref=1; Max-Age=3600', { 'http.request.header.set_cookie.pref': '1' }], + ['color=blue; Path=/dashboard', { 'http.request.header.set_cookie.color': 'blue' }], + ['token=eyJhbGc=.eyJzdWI=.SflKxw; Secure', { 'http.request.header.set_cookie.token': '[Filtered]' }], + ['auth_required; HttpOnly', { 'http.request.header.set_cookie.auth_required': '[Filtered]' }], + ['empty=; Secure', { 'http.request.header.set_cookie.empty': '' }], + ])('should parse and filter Set-Cookie header: %s', (setCookieValue, expected) => { + const headers = { 'Set-Cookie': setCookieValue }; + const result = httpHeadersToSpanAttributes(headers); + expect(result).toEqual(expected); + }); + + it('only splits cookies once between key and value, even when more equals signs are present', () => { + const headers = { Cookie: 'random-string=eyJhbGc=.eyJzdWI=.SflKxw' }; + const result = httpHeadersToSpanAttributes(headers); + expect(result).toEqual({ 'http.request.header.cookie.random_string': 'eyJhbGc=.eyJzdWI=.SflKxw' }); + }); + + it.each([ + { sendDefaultPii: false, description: 'sendDefaultPii is false (default)' }, + { sendDefaultPii: true, description: 'sendDefaultPii is true' }, + ])('does not include PII headers when $description', ({ sendDefaultPii }) => { + const headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + 'x-user': 'my-personal-username', + 'X-Forwarded-For': '192.168.1.1', + 'X-Forwarded-Host': 'example.com', + 'X-Forwarded-Proto': 'https', + }; + + const result = httpHeadersToSpanAttributes(headers, sendDefaultPii); + + if (sendDefaultPii) { + expect(result).toEqual({ + 'http.request.header.content_type': 'application/json', + 'http.request.header.user_agent': 'Mozilla/5.0', + 'http.request.header.x_user': 'my-personal-username', + 'http.request.header.x_forwarded_for': '192.168.1.1', + 'http.request.header.x_forwarded_host': 'example.com', + 'http.request.header.x_forwarded_proto': 'https', + }); + } else { + expect(result).toEqual({ + 'http.request.header.content_type': 'application/json', + 'http.request.header.user_agent': 'Mozilla/5.0', + 'http.request.header.x_user': '[Filtered]', + 'http.request.header.x_forwarded_for': '[Filtered]', + 'http.request.header.x_forwarded_host': '[Filtered]', + 'http.request.header.x_forwarded_proto': '[Filtered]', + }); + } + }); + it('always filters comprehensive list of sensitive headers', () => { const headers = { 'Content-Type': 'application/json', @@ -649,8 +736,8 @@ describe('request utils', () => { 'WWW-Authenticate': 'Basic', 'Proxy-Authorization': 'Basic auth', 'X-Access-Token': 'access', - 'X-CSRF-Token': 'csrf', - 'X-XSRF-Token': 'xsrf', + 'X-CSRF': 'csrf', + 'X-XSRF': 'xsrf', 'X-Session-Token': 'session', 'X-Password': 'password', 'X-Private-Key': 'private', @@ -671,8 +758,8 @@ describe('request utils', () => { 'http.request.header.accept': 'application/json', 'http.request.header.host': 'example.com', 'http.request.header.authorization': '[Filtered]', - 'http.request.header.cookie': '[Filtered]', - 'http.request.header.set_cookie': '[Filtered]', + 'http.request.header.cookie.session': '[Filtered]', + 'http.request.header.set_cookie.session': '[Filtered]', 'http.request.header.x_api_key': '[Filtered]', 'http.request.header.x_auth_token': '[Filtered]', 'http.request.header.x_secret': '[Filtered]', @@ -680,8 +767,8 @@ describe('request utils', () => { 'http.request.header.www_authenticate': '[Filtered]', 'http.request.header.proxy_authorization': '[Filtered]', 'http.request.header.x_access_token': '[Filtered]', - 'http.request.header.x_csrf_token': '[Filtered]', - 'http.request.header.x_xsrf_token': '[Filtered]', + 'http.request.header.x_csrf': '[Filtered]', + 'http.request.header.x_xsrf': '[Filtered]', 'http.request.header.x_session_token': '[Filtered]', 'http.request.header.x_password': '[Filtered]', 'http.request.header.x_private_key': '[Filtered]', diff --git a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts index ff025fc3ecc7..8d4b0eca3724 100644 --- a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts +++ b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts @@ -1,5 +1,5 @@ import type { Span, WebFetchHeaders } from '@sentry/core'; -import { httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core'; +import { getClient, httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core'; /** * Extracts HTTP request headers as span attributes and optionally applies them to a span. @@ -17,7 +17,7 @@ export function addHeadersAsAttributes( ? winterCGHeadersToDict(headers as Headers) : headers; - const headerAttributes = httpHeadersToSpanAttributes(headersDict); + const headerAttributes = httpHeadersToSpanAttributes(headersDict, getClient()?.getOptions().sendDefaultPii ?? false); if (span) { span.setAttributes(headerAttributes); diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index 34741e95c912..7909482a5923 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -157,7 +157,10 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions 'http.flavor': httpVersion, 'net.transport': httpVersion?.toUpperCase() === 'QUIC' ? 'ip_udp' : 'ip_tcp', ...getRequestContentLengthAttribute(request), - ...httpHeadersToSpanAttributes(normalizedRequest.headers || {}), + ...httpHeadersToSpanAttributes( + normalizedRequest.headers || {}, + client.getOptions().sendDefaultPii ?? false, + ), }, }); diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index 84d80a7c6f80..b257d70b72d7 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -2,6 +2,7 @@ import { captureException, debug, flushIfServerless, + getClient, httpHeadersToSpanAttributes, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -172,7 +173,7 @@ function getSpanAttributes( // Get headers from the Node.js request object const headers = event.node?.req?.headers || {}; - const headerAttributes = httpHeadersToSpanAttributes(headers); + const headerAttributes = httpHeadersToSpanAttributes(headers, getClient()?.getOptions().sendDefaultPii ?? false); // Merge header attributes with existing attributes Object.assign(attributes, headerAttributes); diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index 273f72d809c1..d8864d254a99 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -359,7 +359,10 @@ function wrapRequestHandler ServerBuild | Promise [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', method: request.method, - ...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)), + ...httpHeadersToSpanAttributes( + winterCGHeadersToDict(request.headers), + clientOptions.sendDefaultPii ?? false, + ), }, }, async span => { diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 3d9963bd1056..26872a0f6f24 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -3,6 +3,7 @@ import { continueTrace, debug, flushIfServerless, + getClient, getCurrentScope, getDefaultIsolationScope, getIsolationScope, @@ -178,7 +179,10 @@ async function instrumentHandle( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url', 'sveltekit.tracing.original_name': originalName, - ...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)), + ...httpHeadersToSpanAttributes( + winterCGHeadersToDict(event.request.headers), + getClient()?.getOptions().sendDefaultPii ?? false, + ), }); } @@ -204,7 +208,10 @@ async function instrumentHandle( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', 'http.method': event.request.method, - ...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)), + ...httpHeadersToSpanAttributes( + winterCGHeadersToDict(event.request.headers), + getClient()?.getOptions().sendDefaultPii ?? false, + ), }, name: routeName, }, From 5c5c7d4046f24aac338691cfeb2c5843312f886c Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Wed, 10 Dec 2025 15:35:00 +0100 Subject: [PATCH 03/43] feat(node): Add instrument OpenAI export to node (#18461) Until we find a way to automatically instrument OpenAI in Nextjs, we shouldn't block users from using the manual instrumentation. Docs for this are in review already. Closes #18462 (added automatically) --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 7 files changed, 7 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 2913022c816b..d5cfed7fae7d 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -147,6 +147,7 @@ export { withScope, supabaseIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, profiler, logger, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 6b36930265ca..b9ab9b013925 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -129,6 +129,7 @@ export { updateSpanName, supabaseIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d3d266b4dfc1..09fff4dc5f6a 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -147,6 +147,7 @@ export { updateSpanName, supabaseIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index d7cd08e5b14e..72fd0fa3a12d 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -127,6 +127,7 @@ export { supabaseIntegration, systemErrorIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index a03b31619472..9d5941c41c8e 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -129,6 +129,7 @@ export { updateSpanName, supabaseIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, profiler, consoleLoggingIntegration, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 88254807ec05..181c9fd36d16 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -121,6 +121,7 @@ export { withScope, supabaseIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 9c2e4cd82a34..47c4cfa7d3f8 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -123,6 +123,7 @@ export { withScope, supabaseIntegration, instrumentSupabaseClient, + instrumentOpenAiClient, zodErrorsIntegration, logger, consoleLoggingIntegration, From 847daf0a0df1e2010d1192f58635dd1bd797d19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Wed, 10 Dec 2025 17:14:02 +0100 Subject: [PATCH 04/43] feat(profiling): Add support for Node v24 in the prune script (#18447) (closes #18428) (closes [JS-1266](https://linear.app/getsentry/issue/JS-1266/sentry-prune-profiler-binaries-does-not-recognize-nodejs-24-as-a-valid)) This adds support for Node v24 in the prune script. On top this also adds a test that is testing against the current Node version (as suggested in https://github.com/getsentry/sentry-javascript/pull/14491#pullrequestreview-2468295980). Since we have [a matrix](https://github.com/getsentry/sentry-javascript/blob/a906759fd8769d264498598dc16dab8af26377ea/.github/workflows/build.yml#L747) for our integration tests, this test would fail once we add Node v26 - where we are forced to update the ABI manually. Theoretically we could also use [node-abi](https://www.npmjs.com/package/node-abi), but decided against it to keep the dependencies low. --- .../scripts/prune-profiler-binaries.js | 3 +++ .../test/prune-profiler-binaries.test.ts | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 packages/profiling-node/test/prune-profiler-binaries.test.ts diff --git a/packages/profiling-node/scripts/prune-profiler-binaries.js b/packages/profiling-node/scripts/prune-profiler-binaries.js index a0c5ce7fc33d..9ccea8266e7d 100755 --- a/packages/profiling-node/scripts/prune-profiler-binaries.js +++ b/packages/profiling-node/scripts/prune-profiler-binaries.js @@ -63,6 +63,7 @@ const NODE_TO_ABI = { 18: '108', 20: '115', 22: '127', + 24: '134', }; if (NODE) { @@ -76,6 +77,8 @@ if (NODE) { NODE = NODE_TO_ABI['20']; } else if (NODE.startsWith('22')) { NODE = NODE_TO_ABI['22']; + } else if (NODE.startsWith('24')) { + NODE = NODE_TO_ABI['24']; } else { ARGV_ERRORS.push( `❌ Sentry: Invalid node version passed as argument, please make sure --target_node is a valid major node version. Supported versions are ${Object.keys( diff --git a/packages/profiling-node/test/prune-profiler-binaries.test.ts b/packages/profiling-node/test/prune-profiler-binaries.test.ts new file mode 100644 index 000000000000..174eab6a9879 --- /dev/null +++ b/packages/profiling-node/test/prune-profiler-binaries.test.ts @@ -0,0 +1,24 @@ +import { spawnSync } from 'node:child_process'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +describe('prune-profiler-binaries', () => { + it('should check if the node version is valid', () => { + const currentNode = process.version.split('v')[1]; + const result = spawnSync( + 'node', + [ + path.join(__dirname, '../scripts/prune-profiler-binaries.js'), + '--target_platform=linux', + '--target_arch=x64', + '--target_stdlib=glibc', + `--target_dir_path=${os.tmpdir()}`, + `--target_node=${currentNode}`, + ], + { encoding: 'utf8' }, + ); + + expect(result.stdout).not.toContain('Invalid node version passed as argument'); + }); +}); From 0591b1bdb319d97f37901e79b7387924236c0f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Wed, 10 Dec 2025 17:14:33 +0100 Subject: [PATCH 05/43] feat(bun): Expose spotlight option in TypeScript (#18436) (closes #18419) (closes [JS-1261](https://linear.app/getsentry/issue/JS-1261/add-support-for-spotlight-in-sentrybun)) It seems that for Bun we are already using the init function of `@sentry/node`, so all the options are passed do satisfy the `NodeOptions`. This is now re-exporting `spotlight` as an option. (related: #17349) --- packages/bun/src/types.ts | 12 ++++++++++++ packages/bun/test/init.test.ts | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/bun/src/types.ts b/packages/bun/src/types.ts index afec75d1ee8d..91686f9cf8c3 100644 --- a/packages/bun/src/types.ts +++ b/packages/bun/src/types.ts @@ -22,6 +22,18 @@ export interface BaseBunOptions { /** Sets an optional server name (device name) */ serverName?: string; + /** + * If you use Spotlight by Sentry during development, use + * this option to forward captured Sentry events to Spotlight. + * + * Either set it to true, or provide a specific Spotlight Sidecar URL. + * + * More details: https://spotlightjs.com/ + * + * IMPORTANT: Only set this option to `true` while developing, not in production! + */ + spotlight?: boolean | string; + /** * If this is set to true, the SDK will not set up OpenTelemetry automatically. * In this case, you _have_ to ensure to set it up correctly yourself, including: diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts index 4b2ddd452713..0dd76a3a4072 100644 --- a/packages/bun/test/init.test.ts +++ b/packages/bun/test/init.test.ts @@ -38,6 +38,20 @@ describe('init()', () => { expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); }); + it('enables spotlight with default URL from config `true`', () => { + const client = init({ dsn: PUBLIC_DSN, spotlight: true }); + + expect(client?.getOptions().spotlight).toBe(true); + expect(client?.getOptions().integrations.some(integration => integration.name === 'Spotlight')).toBe(true); + }); + + it('disables spotlight from config `false`', () => { + const client = init({ dsn: PUBLIC_DSN, spotlight: false }); + + expect(client?.getOptions().spotlight).toBe(false); + expect(client?.getOptions().integrations.some(integration => integration.name === 'Spotlight')).toBe(false); + }); + it('installs merged default integrations, with overrides provided through options', () => { const mockDefaultIntegrations = [ new MockIntegration('Some mock integration 2.1'), From 5affac5fd580eb738a9cce3503807f63251b3f97 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 11 Dec 2025 13:17:48 +0200 Subject: [PATCH 06/43] fix(tanstack-router): Check for `fromLocation` existence before reporting pageload (#18463) Bad condition which short circuits the web vitals from being reported, I included a test anyways even though it doesn't reproduce it. closes #18425 Closes #18464 (added automatically)??? --- .../tests/routing-instrumentation.test.ts | 41 +++++++++++++++++++ packages/react/src/tanstackrouter.ts | 9 +++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts index 59d15b989dd4..bfb7dde10865 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts @@ -35,6 +35,47 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = }); }); +test('sends pageload transaction with web vitals measurements', async ({ page }) => { + const transactionPromise = waitForTransaction('tanstack-router', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.tanstack_router', + }, + }, + transaction: '/', + transaction_info: { + source: 'route', + }, + measurements: expect.objectContaining({ + ttfb: expect.objectContaining({ + value: expect.any(Number), + unit: 'millisecond', + }), + lcp: expect.objectContaining({ + value: expect.any(Number), + unit: 'millisecond', + }), + fp: expect.objectContaining({ + value: expect.any(Number), + unit: 'millisecond', + }), + fcp: expect.objectContaining({ + value: expect.any(Number), + unit: 'millisecond', + }), + }), + }); +}); + test('sends a navigation transaction with a parameterized URL', async ({ page }) => { const pageloadTxnPromise = waitForTransaction('tanstack-router', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 49c3a8984443..0eba31722819 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -64,8 +64,13 @@ export function tanstackRouterBrowserTracingIntegration( if (instrumentNavigation) { // The onBeforeNavigate hook is called at the very beginning of a navigation and is only called once per navigation, even when the user is redirected castRouterInstance.subscribe('onBeforeNavigate', onBeforeNavigateArgs => { - // onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by comparing the states of the to and from arguments. - if (onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation?.state) { + // onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by: + // 1. Checking if there's no fromLocation (initial pageload) + // 2. Comparing the states of the to and from arguments + if ( + !onBeforeNavigateArgs.fromLocation || + onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation.state + ) { return; } From 065ce1acabf22209ec6e9ba7f7d91a77d7c93622 Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Thu, 11 Dec 2025 13:00:34 +0100 Subject: [PATCH 07/43] fix(tracing): Set span operations for AI spans with model ID only (#18471) Fixes an issue where VercelAI integration span operations were not being set correctly because the validation was too restrictive. I relaxed the condition to only require `ai.model.id` attribute instead of both `ai.model.id` and `ai.model.provider` as provider attribute is optional and may not always be present on spans Closes https://linear.app/getsentry/issue/JS-1280 --- packages/core/src/tracing/vercel-ai/index.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index 62272d222fd5..93be1ca33423 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -24,7 +24,6 @@ import { import type { ProviderMetadata } from './vercel-ai-attributes'; import { AI_MODEL_ID_ATTRIBUTE, - AI_MODEL_PROVIDER_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE, AI_PROMPT_TOOLS_ATTRIBUTE, AI_RESPONSE_OBJECT_ATTRIBUTE, @@ -65,12 +64,10 @@ function onVercelAiSpanStart(span: Span): void { return; } - // The AI and Provider must be defined for generate, stream, and embed spans. - // The id of the model + // The AI model ID must be defined for generate, stream, and embed spans. + // The provider is optional and may not always be present. const aiModelId = attributes[AI_MODEL_ID_ATTRIBUTE]; - // the provider of the model - const aiModelProvider = attributes[AI_MODEL_PROVIDER_ATTRIBUTE]; - if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) { + if (typeof aiModelId !== 'string' || !aiModelId) { return; } From 4b18ffd968f7bafdfbeac9ca11f3124d47c75eae Mon Sep 17 00:00:00 2001 From: sebws <53290489+sebws@users.noreply.github.com> Date: Fri, 12 Dec 2025 00:07:42 +1100 Subject: [PATCH 08/43] fix(node-core): passthrough node-cron context (#17835) --- packages/node-core/src/cron/node-cron.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/node-core/src/cron/node-cron.ts b/packages/node-core/src/cron/node-cron.ts index a2374b06d4b5..0c6d2a8e5ca1 100644 --- a/packages/node-core/src/cron/node-cron.ts +++ b/packages/node-core/src/cron/node-cron.ts @@ -7,7 +7,11 @@ export interface NodeCronOptions { } export interface NodeCron { - schedule: (cronExpression: string, callback: () => void, options: NodeCronOptions | undefined) => unknown; + schedule: ( + cronExpression: string, + callback: (context?: unknown) => void, + options: NodeCronOptions | undefined, + ) => unknown; } /** @@ -44,14 +48,14 @@ export function instrumentNodeCron(lib: Partial & T): T { throw new Error('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); } - const monitoredCallback = async (): Promise => { + const monitoredCallback = async (...args: Parameters): Promise => { return withMonitor( name, async () => { // We have to manually catch here and capture the exception because node-cron swallows errors // https://github.com/node-cron/node-cron/issues/399 try { - return await callback(); + return await callback(...args); } catch (e) { captureException(e, { mechanism: { From 907d13f972cc9e37bd0ab02cbb6e201f097e0cb1 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Thu, 11 Dec 2025 14:29:29 +0100 Subject: [PATCH 09/43] chore: Add external contributor to CHANGELOG.md (#18473) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #17835 Co-authored-by: andreiborza <168741329+andreiborza@users.noreply.github.com> Co-authored-by: Nicolas Hrubec --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d967a7c39408..4c903d7df829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @sebws. Thank you for your contribution! + ## 10.30.0 - feat(nextjs): Deprecate Webpack top-level options ([#18343](https://github.com/getsentry/sentry-javascript/pull/18343)) From d4301fd6fbc86bd4592cbc9efc2051fc1965d314 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 11 Dec 2025 13:39:44 +0000 Subject: [PATCH 10/43] chore: upgrade Playwright to ~1.56.0 for WSL2 compatibility (#18468) Closes #18469 (added automatically) --- .../noOnLoad/sdkLoadedInMeanwhile/test.ts | 2 +- .../browser-integration-tests/package.json | 2 +- .../featureFlags/onError/basic/test.ts | 2 +- .../featureFlags/onError/withScope/test.ts | 2 +- .../featureFlags/featureFlags/onSpan/test.ts | 2 +- .../growthbook/onError/basic/test.ts | 2 +- .../growthbook/onError/withScope/test.ts | 2 +- .../featureFlags/growthbook/onSpan/test.ts | 2 +- .../launchdarkly/onError/basic/test.ts | 2 +- .../launchdarkly/onError/withScope/test.ts | 2 +- .../featureFlags/launchdarkly/onSpan/test.ts | 2 +- .../openfeature/onError/basic/test.ts | 2 +- .../openfeature/onError/errorHook/test.ts | 2 +- .../openfeature/onError/withScope/test.ts | 2 +- .../featureFlags/openfeature/onSpan/test.ts | 2 +- .../statsig/onError/basic/test.ts | 2 +- .../statsig/onError/withScope/test.ts | 2 +- .../featureFlags/statsig/onSpan/test.ts | 2 +- .../featureFlags/unleash/badSignature/test.ts | 2 +- .../unleash/onError/basic/test.ts | 2 +- .../unleash/onError/withScope/test.ts | 2 +- .../featureFlags/unleash/onSpan/test.ts | 2 +- .../suites/integrations/supabase/auth/test.ts | 4 +-- .../suites/replay/bufferModeManual/test.ts | 6 ++-- .../replay/bufferStalledRequests/test.ts | 6 ++-- .../suites/replay/errorResponse/test.ts | 2 +- .../suites/replay/errors/droppedError/test.ts | 2 +- .../suites/replay/errors/errorMode/test.ts | 2 +- .../suites/replay/errors/errorNotSent/test.ts | 2 +- .../replay/errors/errorsInSession/test.ts | 2 +- .../suites/replay/eventBufferError/test.ts | 2 +- .../fetch/captureTimestamps/test.ts | 2 +- .../xhr/captureTimestamps/test.ts | 2 +- .../replay/replayIntegrationShim/test.ts | 2 +- .../suites/replay/replayShim/test.ts | 2 +- .../suites/replay/sampling/test.ts | 2 +- .../suites/sessions/initial-scope/test.ts | 3 +- .../browserTracingIntegrationShim/test.ts | 2 +- .../utils/fixtures.ts | 2 +- .../test-applications/angular-17/package.json | 2 +- .../test-applications/angular-18/package.json | 2 +- .../test-applications/angular-19/package.json | 2 +- .../test-applications/angular-20/package.json | 2 +- .../test-applications/angular-21/package.json | 2 +- .../test-applications/astro-4/package.json | 2 +- .../astro-4/tests/errors.server.test.ts | 5 +++- .../test-applications/astro-5/package.json | 2 +- .../astro-5/tests/errors.server.test.ts | 5 +++- .../aws-serverless/package.json | 2 +- .../browser-webworker-vite/package.json | 2 +- .../cloudflare-mcp/package.json | 2 +- .../cloudflare-workers/package.json | 2 +- .../create-next-app/package.json | 2 +- .../package.json | 2 +- .../create-remix-app-express/package.json | 2 +- .../create-remix-app-v2-non-vite/package.json | 2 +- .../create-remix-app-v2/package.json | 2 +- .../default-browser/package.json | 2 +- .../ember-classic/package.json | 2 +- .../ember-embroider/package.json | 2 +- .../hydrogen-react-router-7/package.json | 2 +- .../test-applications/nestjs-11/package.json | 2 +- .../test-applications/nestjs-8/package.json | 2 +- .../nestjs-basic-with-graphql/package.json | 2 +- .../nestjs-basic/package.json | 2 +- .../nestjs-distributed-tracing/package.json | 2 +- .../nestjs-fastify/package.json | 2 +- .../nestjs-graphql/package.json | 2 +- .../package.json | 2 +- .../nestjs-with-submodules/package.json | 2 +- .../test-applications/nextjs-13/package.json | 2 +- .../nextjs-13/tests/client/fetch.test.ts | 2 +- .../tests/client/pages-dir-pageload.test.ts | 5 +++- .../tests/server/getServerSideProps.test.ts | 10 +++++-- .../server/server-component-error.test.ts | 5 +++- .../test-applications/nextjs-14/package.json | 2 +- .../nextjs-15-basepath/package.json | 2 +- .../nextjs-15-intl/package.json | 2 +- .../test-applications/nextjs-15/package.json | 2 +- .../nextjs-16-cacheComponents/package.json | 2 +- .../nextjs-16-tunnel/package.json | 2 +- .../test-applications/nextjs-16/package.json | 2 +- .../nextjs-app-dir/package.json | 2 +- .../nextjs-orpc/package.json | 2 +- .../nextjs-pages-dir/package.json | 2 +- .../test-applications/nextjs-t3/package.json | 2 +- .../nextjs-turbo/package.json | 2 +- .../node-connect/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../node-core-express-otel-v1/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../node-core-express-otel-v2/package.json | 2 +- .../node-express-cjs-preload/package.json | 2 +- .../node-express-esm-loader/package.json | 2 +- .../node-express-esm-preload/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../node-express-send-to-sentry/package.json | 2 +- .../node-express-v5/package.json | 2 +- .../node-express/package.json | 2 +- .../node-fastify-3/package.json | 2 +- .../node-fastify-4/package.json | 2 +- .../node-fastify-5/package.json | 2 +- .../node-firebase/package.json | 2 +- .../test-applications/node-hapi/package.json | 2 +- .../test-applications/node-koa/package.json | 2 +- .../node-otel-custom-sampler/package.json | 2 +- .../node-otel-sdk-node/package.json | 2 +- .../node-otel-without-tracing/package.json | 2 +- .../test-applications/node-otel/package.json | 2 +- .../node-profiling-cjs/package.json | 2 +- .../node-profiling-electron/package.json | 2 +- .../node-profiling-esm/package.json | 2 +- .../nuxt-3-dynamic-import/package.json | 2 +- .../test-applications/nuxt-3-min/package.json | 2 +- .../nuxt-3-top-level-import/package.json | 2 +- .../test-applications/nuxt-3/package.json | 2 +- .../test-applications/nuxt-4/package.json | 2 +- .../test-applications/react-17/package.json | 2 +- .../test-applications/react-19/package.json | 2 +- .../react-create-browser-router/package.json | 2 +- .../tests/transactions.test.ts | 4 ++- .../react-create-hash-router/package.json | 2 +- .../tests/transactions.test.ts | 4 ++- .../react-create-memory-router/package.json | 2 +- .../react-router-5/package.json | 2 +- .../package.json | 2 +- .../react-router-6-use-routes/package.json | 2 +- .../react-router-6/package.json | 2 +- .../react-router-7-cross-usage/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../react-router-7-framework-spa/package.json | 2 +- .../react-router-7-framework/package.json | 2 +- .../react-router-7-lazy-routes/package.json | 2 +- .../react-router-7-spa/package.json | 2 +- .../react-send-to-sentry/package.json | 2 +- .../remix-hydrogen/package.json | 2 +- .../solid-tanstack-router/package.json | 2 +- .../test-applications/solid/package.json | 2 +- .../solidstart-dynamic-import/package.json | 2 +- .../solidstart-spa/package.json | 2 +- .../solidstart-top-level-import/package.json | 2 +- .../test-applications/solidstart/package.json | 2 +- .../supabase-nextjs/package.json | 2 +- .../test-applications/svelte-5/package.json | 2 +- .../sveltekit-2-kit-tracing/package.json | 2 +- .../sveltekit-2-svelte-5/package.json | 2 +- .../sveltekit-2.5.0-twp/package.json | 2 +- .../sveltekit-2/package.json | 2 +- .../sveltekit-cloudflare-pages/package.json | 2 +- .../tanstack-router/package.json | 2 +- .../tanstackstart-react/package.json | 2 +- .../tsx-express/package.json | 2 +- .../test-applications/vue-3/package.json | 2 +- .../test-applications/webpack-4/package.json | 2 +- .../test-applications/webpack-5/package.json | 2 +- dev-packages/test-utils/package.json | 4 +-- .../test-utils/src/playwright-config.ts | 4 +-- yarn.lock | 28 +++++++++---------- 163 files changed, 207 insertions(+), 184 deletions(-) diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts index 844b5f1d7169..132281668fda 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts @@ -17,7 +17,7 @@ sentryTest('it does not download the SDK if the SDK was loaded in the meanwhile' let cdnLoadedCount = 0; let sentryEventCount = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { sentryEventCount++; return route.fulfill({ diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index a42cd499956a..a5c3351dbb03 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -41,7 +41,7 @@ "dependencies": { "@babel/core": "^7.27.7", "@babel/preset-typescript": "^7.16.7", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/rrweb": "2.34.0", "@sentry/browser": "10.30.0", "@supabase/supabase-js": "2.49.3", diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts index 3233c9047649..e0b96c22e118 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts @@ -12,7 +12,7 @@ sentryTest('Basic test with eviction, update, and no async tasks', async ({ getL sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts index fecc762d4c99..b7e8a79b2410 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts @@ -12,7 +12,7 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index 6516d8e36abb..ee132346d51c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -14,7 +14,7 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts index fc23f80927ff..b1d818815315 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts @@ -12,7 +12,7 @@ sentryTest('GrowthBook onError: basic eviction/update and no async tasks', async sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'test-id' }) }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts index 48fa4718b856..de9382bd547f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts @@ -12,7 +12,7 @@ sentryTest('GrowthBook onError: forked scopes are isolated', async ({ getLocalTe sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'test-id' }) }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts index 6661edc9723d..b93047ed01d3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts @@ -16,7 +16,7 @@ sentryTest( sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts index bc3e0afdc292..a837300d90d4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts @@ -12,7 +12,7 @@ sentryTest('Basic test with eviction, update, and no async tasks', async ({ getL sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts index 2efb3fdc9ad0..9e81822fff57 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts @@ -12,7 +12,7 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts index eb7eb003c838..f6f243262f49 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -14,7 +14,7 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts index 5953f1e0b087..77cd890e7d2e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts @@ -12,7 +12,7 @@ sentryTest('Basic test with eviction, update, and no async tasks', async ({ getL sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index 89654c82eda1..c77db68a235e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts @@ -12,7 +12,7 @@ sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts index 14cc072af30d..fac506e0f320 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts @@ -12,7 +12,7 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts index 5ade5d01b3d5..ff03be5a972b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -14,7 +14,7 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts index 134b29417d53..fdd5cfbcd975 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts @@ -12,7 +12,7 @@ sentryTest('Basic test with eviction, update, and no async tasks', async ({ getL sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts index e80c6dbfc5fa..4805b2f53358 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts @@ -12,7 +12,7 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts index 1ea192f98850..34a760a02e61 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -14,7 +14,7 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts index 8fe5729f53c2..11b91fa89f4e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts @@ -9,7 +9,7 @@ sentryTest('Logs and returns if isEnabled does not match expected signature', as const bundleKey = process.env.PW_BUNDLE || ''; const hasDebug = !bundleKey.includes('_min'); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts index 6e2760b69600..4a75a8e189a9 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts @@ -12,7 +12,7 @@ sentryTest('Basic test with eviction, update, and no async tasks', async ({ getL sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts index fe3aec3ff188..399c1cbc5f2e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts @@ -12,7 +12,7 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts index 984bba3bc0e3..cf92c32f2a8f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -14,7 +14,7 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index b37fa79ed97e..fa1353f64c5d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -9,7 +9,7 @@ import { } from '../../../../utils/helpers'; async function mockSupabaseAuthRoutesSuccess(page: Page) { - await page.route('**/auth/v1/token?grant_type=password**', route => { + await page.route(/\/auth\/v1\/token\?grant_type=password/, route => { return route.fulfill({ status: 200, body: JSON.stringify({ @@ -38,7 +38,7 @@ async function mockSupabaseAuthRoutesSuccess(page: Page) { } async function mockSupabaseAuthRoutesFailure(page: Page) { - await page.route('**/auth/v1/token?grant_type=password**', route => { + await page.route(/\/auth\/v1\/token\?grant_type=password/, route => { return route.fulfill({ status: 400, body: JSON.stringify({ diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts index a9e7ad0240be..e6c85f672966 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts @@ -26,7 +26,7 @@ sentryTest( const reqPromise1 = waitForReplayRequest(page, 1); const reqErrorPromise = waitForErrorRequest(page); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const event = envelopeRequestParser(route.request()); // error events have no type field if (event && !event.type && event.event_id) { @@ -171,7 +171,7 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqErrorPromise = waitForErrorRequest(page); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const event = envelopeRequestParser(route.request()); // error events have no type field if (event && !event.type && event.event_id) { @@ -405,7 +405,7 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqErrorPromise0 = waitForErrorRequest(page); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const event = envelopeRequestParser(route.request()); // error events have no type field if (event && !event.type && event.event_id) { diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/test.ts b/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/test.ts index 11154caaaa8b..ea106dfcd7ff 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/test.ts @@ -27,7 +27,7 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); - await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, async route => { const event = envelopeRequestParser(route.request()); // Track error events @@ -106,7 +106,7 @@ sentryTest('buffer mode remains after interrupting replay flush', async ({ getLo const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); - await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, async route => { const event = envelopeRequestParser(route.request()); // Track error events @@ -186,7 +186,7 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); - await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, async route => { const event = envelopeRequestParser(route.request()); // Track error events diff --git a/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts b/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts index 49d88bfa67a5..7ba979cfdb93 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts @@ -13,7 +13,7 @@ sentryTest('should stop recording after receiving an error response', async ({ g } let called = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { called++; return route.fulfill({ diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts index e202f6f63faf..d4f03c51e881 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts @@ -12,7 +12,7 @@ sentryTest( let callsToSentry = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const req = route.request(); const event = envelopeRequestParser(req); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts index 8970466ad174..758fac370c15 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts @@ -29,7 +29,7 @@ sentryTest( const reqPromise2 = waitForReplayRequest(page, 2); const reqErrorPromise = waitForErrorRequest(page); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const event = envelopeRequestParser(route.request()); // error events have no type field if (event && !event.type && event.event_id) { diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts index 6f29f2295303..af4803409249 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts @@ -11,7 +11,7 @@ sentryTest( let callsToSentry = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { callsToSentry++; return route.fulfill({ diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts index d3616b8c61bd..218e6df5a124 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts @@ -22,7 +22,7 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const event = envelopeRequestParser(route.request()); // error events have no type field if (event && !event.type && event.event_id) { diff --git a/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts b/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts index d1ac87454b03..213868137f20 100644 --- a/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts @@ -29,7 +29,7 @@ sentryTest( let called = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { const event = envelopeRequestParser(route.request()); // We only want to count replays here diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts index 715fddecfd34..c3848a1de65e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts @@ -19,7 +19,7 @@ sentryTest('captures correct timestamps', async ({ getLocalTestUrl, page, browse }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, async route => { await new Promise(resolve => setTimeout(resolve, 10)); return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts index 40c9462fff21..349494a8cbcb 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts @@ -19,7 +19,7 @@ sentryTest('captures correct timestamps', async ({ getLocalTestUrl, page, browse }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, async route => { await new Promise(resolve => setTimeout(resolve, 10)); return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts index 18a020dc3e2f..0ce5887bf6ee 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts @@ -14,7 +14,7 @@ sentryTest( page.on('console', msg => consoleMessages.push(msg.text())); let requestCount = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { requestCount++; return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts index 7291ae1644ba..d0d64c8c75a0 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts @@ -14,7 +14,7 @@ sentryTest( page.on('console', msg => consoleMessages.push(msg.text())); let requestCount = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { requestCount++; return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts b/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts index 4d8e856b3bd8..886f7b871abd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts @@ -7,7 +7,7 @@ sentryTest('should not send replays if both sample rates are 0', async ({ getLoc sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { // This should never be called! expect(true).toBe(false); diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts index e2e1d29717fe..781e22c28f40 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts @@ -18,11 +18,12 @@ sentryTest('should start a new session on pageload.', async ({ getLocalTestUrl, sentryTest('should start a new session with navigation.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); + // Route must be set up before any navigation to avoid race conditions await page.route('**/foo', (route: Route) => route.continue({ url })); const initSession = await getFirstSentryEnvelopeRequest(page, url); - await page.click('#navigate'); + await page.locator('#navigate').click(); const newSession = await getFirstSentryEnvelopeRequest(page, url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts index 5c0aa7c62b97..1dde1534c999 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts @@ -14,7 +14,7 @@ sentryTest( page.on('console', msg => consoleMessages.push(msg.text())); let requestCount = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { requestCount++; return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/utils/fixtures.ts b/dev-packages/browser-integration-tests/utils/fixtures.ts index 7cedc1e4001a..f856fa64ce23 100644 --- a/dev-packages/browser-integration-tests/utils/fixtures.ts +++ b/dev-packages/browser-integration-tests/utils/fixtures.ts @@ -80,7 +80,7 @@ const sentryTest = base.extend({ } if (!skipDsnRouteHandler) { - await page.route('https://dsn.ingest.sentry.io/**/*', route => { + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json index 74aeab9e8cd8..0d27f73f94e8 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -29,7 +29,7 @@ "zone.js": "~0.14.3" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@angular-devkit/build-angular": "^17.1.1", diff --git a/dev-packages/e2e-tests/test-applications/angular-18/package.json b/dev-packages/e2e-tests/test-applications/angular-18/package.json index bbdb42a84052..a32d3f5de99f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-18/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-18/package.json @@ -29,7 +29,7 @@ "zone.js": "~0.14.3" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@angular-devkit/build-angular": "^18.0.0", diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json index b16b1be7384b..1e02f440b0a9 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -32,7 +32,7 @@ "@angular-devkit/build-angular": "^19.0.0", "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@types/jasmine": "~5.1.0", diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json index 5488a1fef56c..02a333094158 100644 --- a/dev-packages/e2e-tests/test-applications/angular-20/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json @@ -33,7 +33,7 @@ "@angular-devkit/build-angular": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@types/jasmine": "~5.1.0", diff --git a/dev-packages/e2e-tests/test-applications/angular-21/package.json b/dev-packages/e2e-tests/test-applications/angular-21/package.json index 315f7eea0492..f1e9f4d0e871 100644 --- a/dev-packages/e2e-tests/test-applications/angular-21/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-21/package.json @@ -33,7 +33,7 @@ "@angular-devkit/build-angular": "^21.0.0", "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@types/jasmine": "~5.1.0", diff --git a/dev-packages/e2e-tests/test-applications/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json index df0750ee226c..339f5fd18c7d 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json @@ -14,7 +14,7 @@ "dependencies": { "@astrojs/check": "0.9.2", "@astrojs/node": "8.3.4", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry/astro": "* || latest", "@sentry-internal/test-utils": "link:../../../test-utils", "@spotlightjs/astro": "2.1.6", diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.server.test.ts index f3b0e9a189de..bfa4c2ca3884 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.server.test.ts @@ -11,7 +11,10 @@ test.describe('server-side errors', () => { return transactionEvent.transaction === 'GET /ssr-error'; }); - await page.goto('/ssr-error'); + // This page returns an error status code, so we need to catch the navigation error + await page.goto('/ssr-error').catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); const errorEvent = await errorEventPromise; const transactionEvent = await transactionEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json index 6695d3c9434c..f42b22b3d07f 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json @@ -14,7 +14,7 @@ "dependencies": { "@astrojs/internal-helpers": "^0.4.2", "@astrojs/node": "^9.0.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/astro": "latest || *", "astro": "^5.0.3" diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts index 77d515ad2a24..2809670ff46d 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts @@ -11,7 +11,10 @@ test.describe('server-side errors', () => { return transactionEvent.transaction === 'GET /ssr-error'; }); - await page.goto('/ssr-error'); + // This page returns an error status code, so we need to catch the navigation error + await page.goto('/ssr-error').catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); const errorEvent = await errorEventPromise; const transactionEvent = await transactionEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json index d24bc1b78805..a3d164e15813 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json @@ -12,7 +12,7 @@ "//": "We just need the @sentry/aws-serverless layer zip file, not the NPM package", "devDependencies": { "@aws-sdk/client-lambda": "^3.863.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/", "@types/tmp": "^0.2.6", diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json index 3321552a5442..37954bd3cbbc 100644 --- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json @@ -12,7 +12,7 @@ "test:assert": "pnpm test" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "~5.8.3", "vite": "^7.0.4" diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json index 686e747422fa..9ec5f1ab2e7f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json @@ -23,7 +23,7 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.19", "@cloudflare/workers-types": "^4.20240725.0", - "@playwright/test": "~1.50.0", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.5.2", "vitest": "~3.2.0", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json index 5d7cfc35e469..112549476aed 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json @@ -18,7 +18,7 @@ "@sentry/cloudflare": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.50.0", + "@playwright/test": "~1.56.0", "@cloudflare/vitest-pool-workers": "^0.8.19", "@cloudflare/workers-types": "^4.20240725.0", "@sentry-internal/test-utils": "link:../../../test-utils", diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json index d87aed0de03e..3a17f479cd16 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json @@ -22,7 +22,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index b4e96beffd86..6ad073cf10ee 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -25,7 +25,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@remix-run/dev": "^2.7.2", "@types/compression": "^1.7.5", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json index f47bfd9a170e..9e0c48265336 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json @@ -28,7 +28,7 @@ "source-map-support": "^0.5.21" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@remix-run/dev": "^2.7.2", "@types/compression": "^1.7.2", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json index 57b106540701..b3543da03eb8 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json @@ -21,7 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@remix-run/dev": "2.7.2", "@remix-run/eslint-config": "2.7.2", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index 535264a09978..fb777a69d8f6 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -21,7 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@remix-run/dev": "2.16.7", "@remix-run/eslint-config": "2.16.7", diff --git a/dev-packages/e2e-tests/test-applications/default-browser/package.json b/dev-packages/e2e-tests/test-applications/default-browser/package.json index 8dc6d7f28334..f181008e0427 100644 --- a/dev-packages/e2e-tests/test-applications/default-browser/package.json +++ b/dev-packages/e2e-tests/test-applications/default-browser/package.json @@ -28,7 +28,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "webpack": "^5.91.0", "serve": "14.0.1", diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/package.json b/dev-packages/e2e-tests/test-applications/ember-classic/package.json index 949b2b05f816..5a0b49c0972c 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json @@ -24,7 +24,7 @@ "@ember/optional-features": "~2.0.0", "@glimmer/component": "~1.1.2", "@glimmer/tracking": "~1.1.2", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@ember/string": "~3.1.1", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/ember": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json index b7a102917e80..e451a5da9db7 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json @@ -50,7 +50,7 @@ "loader.js": "^4.7.0", "tracked-built-ins": "^3.3.0", "webpack": "^5.91.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry/ember": "latest || *", "@sentry-internal/test-utils": "link:../../../test-utils", "@tsconfig/ember": "^3.0.6", diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index d714373b3837..92bf1dc70c14 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@react-router/dev": "7.9.6", "@react-router/fs-routes": "7.9.6", "@sentry-internal/test-utils": "link:../../../test-utils", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json index 50ef252865be..b59ad9b2245e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -24,7 +24,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json index aa17d718c01d..4a21f67e908a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json @@ -24,7 +24,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json index f11ff272b072..3128d2f7ae51 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json @@ -26,7 +26,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json index ba3535042af8..2c142b5c6f90 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json @@ -24,7 +24,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json index 8ed28dd45430..d15679556bad 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json @@ -23,7 +23,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json index 9553a3b4e115..d5cecac78725 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json @@ -25,7 +25,7 @@ "fastify": "^4.28.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json index d6a501596b35..be8ed9d58533 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json @@ -26,7 +26,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json index 1b8fd5b69c7f..f19782b24a7d 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json @@ -22,7 +22,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json index e17b1d3a5cbd..297555d6802f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json @@ -22,7 +22,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json index adb8005d1a9d..29270d71da6a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -22,7 +22,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts index bd091fcfd354..44ab9f36173f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('should correctly instrument `fetch` for performance tracing', async ({ page }) => { - await page.route('https://example.com/**/*', route => { + await page.route(/^https:\/\/example\.com\//, route => { return route.fulfill({ status: 200, body: JSON.stringify({ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts index 295fa762f636..313fe49a9a83 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts @@ -57,7 +57,10 @@ test('should create a pageload transaction with correct name when an error occur ); }); - await page.goto(`/something/error-getServerSideProps`, { waitUntil: 'networkidle' }); + // This page returns an error status code, so we need to catch the navigation error + await page.goto(`/something/error-getServerSideProps`, { waitUntil: 'networkidle' }).catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); const transaction = await transactionPromise; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts index 3338d8e28595..bc7138e5c602 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts @@ -13,7 +13,10 @@ test('Should report an error event for errors thrown in getServerSideProps', asy ); }); - await page.goto('/dogsaregreat/error-getServerSideProps'); + // This page returns an error status code, so we need to catch the navigation error + await page.goto('/dogsaregreat/error-getServerSideProps').catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); expect(await errorEventPromise).toMatchObject({ contexts: { @@ -100,7 +103,10 @@ test('Should report an error event for errors thrown in getServerSideProps in pa ); }); - await page.goto('/123/customPageExtension'); + // This page returns an error status code, so we need to catch the navigation error + await page.goto('/123/customPageExtension').catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); expect(await errorEventPromise).toMatchObject({ contexts: { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts index c485c02716a3..5412cc000694 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts @@ -6,7 +6,10 @@ test('Should capture an error thrown in a server component', async ({ page }) => return errorEvent.exception?.values?.[0].value === 'RSC error'; }); - await page.goto('/rsc-error'); + // This page returns an error status code, so we need to catch the navigation error + await page.goto('/rsc-error').catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); expect(await errorEventPromise).toMatchObject({ contexts: { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index fc6b31591bfa..6a9df31078f0 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -23,7 +23,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json index 7481ea0fca7a..01d3747009c7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json @@ -21,7 +21,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json index af7863b46e81..f336c808668a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json @@ -22,7 +22,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { 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 2c0a4956e34a..cef68807cf7f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -27,7 +27,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index bbd1573fc5be..0cd6c237a5af 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json @@ -33,7 +33,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@types/node": "^20", "@types/react": "^19", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json index 724dc9e58e4d..60e78865f0f7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -34,7 +34,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@types/node": "^20", "@types/react": "^19", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index 54e3fc3eddeb..782256f87ee3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -34,7 +34,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@types/node": "^20", "@types/react": "^19", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 5502ab95e012..534fb93b9521 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -25,7 +25,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json index 0a15260db4e8..6496e6c60343 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json @@ -25,7 +25,7 @@ "server-only": "^0.0.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@types/eslint": "^8.56.10", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json index e236484bf51c..30da3eef99c1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json @@ -26,7 +26,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json index f6e3613b379d..3de6bc858768 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json @@ -28,7 +28,7 @@ "zod": "^3.23.3" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@types/eslint": "^8.56.10", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index e28db5352884..6dd6a6e8e279 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -23,7 +23,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *" }, diff --git a/dev-packages/e2e-tests/test-applications/node-connect/package.json b/dev-packages/e2e-tests/test-applications/node-connect/package.json index 323f2b32befb..db3979ac7a94 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/package.json +++ b/dev-packages/e2e-tests/test-applications/node-connect/package.json @@ -19,7 +19,7 @@ "ts-node": "10.9.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json index d445419ece51..42c05b104b76 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json @@ -27,7 +27,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json index 868334df93fa..02457a817a33 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json @@ -29,7 +29,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json index 67351f3ab187..abb49f748d96 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json @@ -27,7 +27,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.50.0", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "resolutions": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json index 5710105d4ab8..d7c83c773514 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json @@ -27,7 +27,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json index f6074d159bbe..c8987cd84f39 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json @@ -29,7 +29,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json index b9ba557d67b5..c532f50e5ef9 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json @@ -27,7 +27,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.50.0", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "resolutions": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json index 3f0dc21a2f08..08d51754ed21 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json @@ -14,7 +14,7 @@ "express": "4.20.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json index ffc25e76d2fe..82ace8aa2133 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json @@ -14,7 +14,7 @@ "express": "4.20.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json index 9489b861ce6d..a570df2c60b7 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json @@ -14,7 +14,7 @@ "express": "4.20.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json index 546eb4a4cb11..6ce1390487c4 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json @@ -14,7 +14,7 @@ "express": "4.20.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json index b430779b6f4f..624995e66987 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json @@ -21,7 +21,7 @@ "zod": "~3.22.4" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json index 070644cce16f..5ba79164fa21 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json @@ -18,7 +18,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2" + "@playwright/test": "~1.56.0" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/package.json b/dev-packages/e2e-tests/test-applications/node-express-v5/package.json index 0890fec0f10d..ecd06aec07cf 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/package.json @@ -22,7 +22,7 @@ "zod": "~3.25.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *" }, diff --git a/dev-packages/e2e-tests/test-applications/node-express/package.json b/dev-packages/e2e-tests/test-applications/node-express/package.json index d5bba8591164..d0fbe7df5ff0 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express/package.json @@ -22,7 +22,7 @@ "zod": "~3.25.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *" }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json index 006a33585fd3..663268c466a9 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json @@ -20,7 +20,7 @@ "ts-node": "10.9.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json index 01f4879d5a68..389aa9ff677b 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json @@ -20,7 +20,7 @@ "ts-node": "10.9.2" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json index 0b03a26eca47..e25531d0f575 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json @@ -20,7 +20,7 @@ "ts-node": "10.9.2" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json index 41eb0ce085d4..a1d4965e9745 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -20,7 +20,7 @@ "typescript": "5.9.3" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "firebase-tools": "^14.20.0" }, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/package.json b/dev-packages/e2e-tests/test-applications/node-hapi/package.json index cf83acd72573..b735268d901d 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi/package.json @@ -16,7 +16,7 @@ "@sentry/node": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-koa/package.json b/dev-packages/e2e-tests/test-applications/node-koa/package.json index 10009bed3824..0d993990730b 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/package.json +++ b/dev-packages/e2e-tests/test-applications/node-koa/package.json @@ -18,7 +18,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json index 9ba6f629e78c..85fc9b85de85 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json @@ -21,7 +21,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json index 1eb93f281cf8..709d597b19af 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json @@ -20,7 +20,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json index 736fdb9dfc33..a66c60146c65 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json @@ -24,7 +24,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json index e2b7086f23ba..0b202bd80556 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel/package.json @@ -20,7 +20,7 @@ "typescript": "~5.0.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json index 06b1108f4b78..d217090e80fb 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json @@ -10,7 +10,7 @@ "test:assert": "pnpm run typecheck && pnpm run test" }, "dependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry/node": "latest || *", "@sentry/profiling-node": "latest || *", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json index 80982adf51ef..5e87a3ca8002 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@electron/rebuild": "^3.7.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry/electron": "latest || *", "@sentry/node": "latest || *", "@sentry/profiling-node": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json index d3dc41d0ff44..fb1f3bf055b8 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json @@ -10,7 +10,7 @@ "test:assert": "pnpm run typecheck && pnpm run test" }, "dependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry/node": "latest || *", "@sentry/profiling-node": "latest || *", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json index b776fa5aee68..e146587fd08e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json @@ -18,7 +18,7 @@ "nuxt": "^3.14.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json index 1d569cf35597..8160b472f57e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json @@ -22,7 +22,7 @@ "vue-router": "4.2.4" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "pnpm": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json index 61bb3b7f5e11..9e6fedb17838 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json @@ -19,7 +19,7 @@ "nuxt": "^3.14.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index bbf0ced23c12..2a2ab10334f1 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -21,7 +21,7 @@ "nuxt": "^3.14.0" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index a5d36c1f6a61..eb28e69b0633 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -21,7 +21,7 @@ "nuxt": "^4.1.2" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/react-17/package.json b/dev-packages/e2e-tests/test-applications/react-17/package.json index c4702bba86a6..7c11ea63d039 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/package.json +++ b/dev-packages/e2e-tests/test-applications/react-17/package.json @@ -42,7 +42,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json index 3e35f48a5fcc..08ef823e2bfe 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/package.json +++ b/dev-packages/e2e-tests/test-applications/react-19/package.json @@ -42,7 +42,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json index 6b0e2a2d97c9..8bb20583f5aa 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json @@ -41,7 +41,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts index 4af30d8e139d..362119d1da13 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts @@ -86,7 +86,9 @@ test('Captures a navigation transaction', async ({ page }) => { }), ); - expect(transactionEvent.spans).toEqual([]); + // Filter out favicon spans which may or may not be present depending on the browser version + const spans = (transactionEvent.spans || []).filter(span => !span.description?.includes('favicon')); + expect(spans).toEqual([]); }); test('Captures a lazy pageload transaction', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json index afe9486eeebf..acbf8d00ef2d 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json @@ -42,7 +42,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts index 15cab5e8569e..e1e06e9bedae 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts @@ -150,7 +150,9 @@ test('Captures a navigation transaction', async ({ page }) => { }), ); - expect(transactionEvent.spans).toEqual([]); + // Filter out favicon spans which may or may not be present depending on the browser version + const spans = (transactionEvent.spans || []).filter(span => !span.description?.includes('favicon')); + expect(spans).toEqual([]); }); test('Captures a parameterized path pageload transaction', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json index 1263c07efc16..119fde33b8e8 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json @@ -41,7 +41,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/package.json b/dev-packages/e2e-tests/test-applications/react-router-5/package.json index f90fe9cb54dc..973d87e057e5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-5/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-5/package.json @@ -44,7 +44,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json index 38110810bbf0..394f631ffb2c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json @@ -44,7 +44,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1", "npm-run-all2": "^6.2.0" diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json index 1e8294cc0139..fe4494775753 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json @@ -41,7 +41,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index bcab38dad727..20d9b4f85f18 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json @@ -44,7 +44,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1", "npm-run-all2": "^6.2.0" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json index dc61a85199e9..ca99556b6c4f 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json @@ -42,7 +42,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1", "npm-run-all2": "^6.2.0" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json index d4931faa13e5..52bec0f72e13 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json @@ -17,7 +17,7 @@ "@types/react-dom": "18.3.1", "@types/node": "^20", "@react-router/dev": "^7.1.5", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.6.3", "vite": "^5.4.11" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json index bb211d225f0a..9fa48d9d12fd 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json @@ -17,7 +17,7 @@ "@types/react-dom": "18.3.1", "@types/node": "^20", "@react-router/dev": "7.9.6", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.6.3", "vite": "^5.4.11" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json index 04c460363320..eae71de9c9e5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json @@ -26,7 +26,7 @@ "react-router": "7.9.6" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@react-router/dev": "7.9.6", "@sentry-internal/test-utils": "link:../../../test-utils", "@tailwindcss/vite": "^4.1.4", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json index 3a71edfecc68..50dbbe4177d2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json @@ -26,7 +26,7 @@ "react-router": "^7.1.5" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@react-router/dev": "^7.5.3", "@sentry-internal/test-utils": "link:../../../test-utils", "@tailwindcss/vite": "^4.1.4", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json index 1ec3da8da47a..3f4b5e9fd3b3 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json @@ -17,7 +17,7 @@ "@types/react-dom": "18.3.1", "@types/node": "^20", "@react-router/dev": "^7.1.5", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.6.3", "vite": "^5.4.11" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json index 1c38ac1468ec..c9cccd09f60e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json @@ -42,7 +42,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "serve": "14.0.1", "npm-run-all2": "^6.2.0" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json index 2c6bf1654cae..aadbe6b3d736 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json @@ -11,7 +11,7 @@ "react-router": "^7.0.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "vite": "^6.0.1", "@vitejs/plugin-react": "^4.3.4", diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json index a38736d66546..a256cd9f29ae 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json @@ -42,7 +42,7 @@ ] }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "serve": "14.0.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json index 7b121ac6d9df..4314393034bb 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@remix-run/dev": "^2.15.2", "@remix-run/eslint-config": "^2.15.2", "@sentry-internal/test-utils": "link:../../../test-utils", diff --git a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json index 5dc35acaf095..8b2cceecb30a 100644 --- a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json @@ -22,7 +22,7 @@ "tailwindcss": "^4.0.6" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.7.2", "vite": "^7.1.7", diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json index 32b7b6dc500e..3551001d5dd1 100644 --- a/dev-packages/e2e-tests/test-applications/solid/package.json +++ b/dev-packages/e2e-tests/test-applications/solid/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "autoprefixer": "^10.4.17", "postcss": "^8.4.33", diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json index 3c896501e487..1dbca26fc50c 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json @@ -15,7 +15,7 @@ "@sentry/solidstart": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index f1638f71866b..cc4549c12c47 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -15,7 +15,7 @@ "@sentry/solidstart": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json index a5dc0d9dc5c9..35a20e4e64a7 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json @@ -15,7 +15,7 @@ "@sentry/solidstart": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index 3b5a2d2b51c8..bb2f5b85134c 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -15,7 +15,7 @@ "@sentry/solidstart": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 05c8f96b2cae..13aeda4135d9 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -29,7 +29,7 @@ "typescript": "4.9.5" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "eslint": "8.34.0", "eslint-config-next": "14.2.25" diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json index 4d1b6854cb70..1cfa4f510219 100644 --- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json @@ -13,7 +13,7 @@ "test:assert": "pnpm test:prod" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sveltejs/vite-plugin-svelte": "^3.0.2", "@tsconfig/svelte": "^5.0.2", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json index 2b6184b8cb28..ea07b939dfa5 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json @@ -19,7 +19,7 @@ "@spotlightjs/spotlight": "2.0.0-alpha.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sveltejs/adapter-node": "^5.3.1", "@sveltejs/kit": "2.31.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index 3c5dcfd8f279..0d64ea954f1c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -19,7 +19,7 @@ "@spotlightjs/spotlight": "2.0.0-alpha.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "2.41.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json index abcb24f3288d..fc691d4acebf 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json @@ -18,7 +18,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "2.8.3", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 1f73b47bc10b..32cfeada718c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -18,7 +18,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json index b974e52b7ea6..81c06113d6bd 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json @@ -18,7 +18,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sveltejs/adapter-cloudflare": "^5.0.3", "@sveltejs/kit": "^2.21.3", "@sveltejs/vite-plugin-svelte": "^5.0.3", diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json index 37ac65c52206..64f92e662ae0 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json @@ -25,7 +25,7 @@ "@vitejs/plugin-react-swc": "^3.5.0", "typescript": "^5.2.2", "vite": "^5.4.11", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json index e44229cce78f..5df2d237445e 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -29,7 +29,7 @@ "vite": "7.2.0", "vite-tsconfig-paths": "^5.1.4", "nitro": "^3.0.0", - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/package.json b/dev-packages/e2e-tests/test-applications/tsx-express/package.json index d99a88bfa3b8..32c8a5668f63 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/package.json +++ b/dev-packages/e2e-tests/test-applications/tsx-express/package.json @@ -22,7 +22,7 @@ "zod": "~3.25.0" }, "devDependencies": { - "@playwright/test": "~1.50.0", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "tsx": "^4.20.3" }, diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index 4ac2e160bb02..1dc469b50ca1 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -21,7 +21,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@tsconfig/node20": "^20.1.2", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/package.json b/dev-packages/e2e-tests/test-applications/webpack-4/package.json index 615cb29dac82..d4f59a0d0511 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-4/package.json +++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json @@ -8,7 +8,7 @@ "test:assert": "playwright test" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/browser": "latest || *", "babel-loader": "^8.0.0", diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/package.json b/dev-packages/e2e-tests/test-applications/webpack-5/package.json index 3c6297e511a8..4378532be1b9 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-5/package.json +++ b/dev-packages/e2e-tests/test-applications/webpack-5/package.json @@ -8,7 +8,7 @@ "test:assert": "playwright test" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/browser": "latest || *", "webpack": "^5.91.0", diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index fd5d0b44dbc5..04155e156b03 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -41,13 +41,13 @@ "clean": "rimraf -g ./node_modules ./build" }, "peerDependencies": { - "@playwright/test": "~1.53.2" + "@playwright/test": "~1.56.0" }, "dependencies": { "express": "^4.21.1" }, "devDependencies": { - "@playwright/test": "~1.53.2", + "@playwright/test": "~1.56.0", "@sentry/core": "10.30.0", "eslint-plugin-regexp": "^1.15.0" }, diff --git a/dev-packages/test-utils/src/playwright-config.ts b/dev-packages/test-utils/src/playwright-config.ts index c380c9547ec0..fb15fc325232 100644 --- a/dev-packages/test-utils/src/playwright-config.ts +++ b/dev-packages/test-utils/src/playwright-config.ts @@ -54,11 +54,11 @@ export function getPlaywrightConfig( { name: 'chromium', use: { - // This comes from `devices["Desktop Chrome"] + // This comes from `devices["Desktop Chrome"]` in Playwright 1.56.0 // We inline this instead of importing this, // because playwright otherwise complains that it was imported twice :( userAgent: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.37 Safari/537.36', viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1, isMobile: false, diff --git a/yarn.lock b/yarn.lock index a0d0b72f6718..a332772b21e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6352,12 +6352,12 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@~1.53.2": - version "1.53.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.53.2.tgz#fafb8dd5e109fc238c4580f82bebc2618f929f77" - integrity sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw== +"@playwright/test@~1.56.0": + version "1.56.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" + integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== dependencies: - playwright "1.53.2" + playwright "1.56.1" "@polka/url@^1.0.0-next.24": version "1.0.0-next.28" @@ -25046,17 +25046,17 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright-core@1.53.2: - version "1.53.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.53.2.tgz#78f71e2f727713daa8d360dc11c460022c13cf91" - integrity sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw== +playwright-core@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" + integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== -playwright@1.53.2: - version "1.53.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.53.2.tgz#cc2ef4a22da1ae562e0ed91edb9e22a7c4371305" - integrity sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A== +playwright@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" + integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== dependencies: - playwright-core "1.53.2" + playwright-core "1.56.1" optionalDependencies: fsevents "2.3.2" From 9c2ebf32f24e4b47d3d905a97abfe3bcb351a7e1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 11 Dec 2025 15:02:42 +0100 Subject: [PATCH 11/43] feat(node): Support `propagateTraceparent` --- .../requests/fetch-sampled/instrument.mjs | 13 ++++++++ .../requests/fetch-sampled/scenario.mjs | 10 +++++++ .../tracing/requests/fetch-sampled/test.ts | 29 ++++++++++++++++++ packages/browser/src/client.ts | 14 --------- packages/core/src/index.ts | 3 +- packages/core/src/types-hoist/options.ts | 14 +++++++++ packages/core/src/utils/traceData.ts | 10 +++---- .../integrations/http/outgoing-requests.ts | 21 +++++++++++-- .../SentryNodeFetchInstrumentation.ts | 15 ++++++++-- .../opentelemetry/src/utils/getTraceData.ts | 30 ++++++++++++------- 10 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs new file mode 100644 index 000000000000..bd16f7b0315c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + propagateTraceparent: true, + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs new file mode 100644 index 000000000000..cda662214b4d --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + }); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts new file mode 100644 index 000000000000..ec8b6a0b1290 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts @@ -0,0 +1,29 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch traceparent', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests are correctly instrumented when not sampled', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: {}, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 65fcdf24734a..4ffc85b07762 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -50,20 +50,6 @@ type BrowserSpecificOptions = BrowserClientReplayOptions & */ skipBrowserExtensionCheck?: boolean; - /** - * If set to `true`, the SDK propagates the W3C `traceparent` header to any outgoing requests, - * in addition to the `sentry-trace` and `baggage` headers. Use the {@link CoreOptions.tracePropagationTargets} - * option to control to which outgoing requests the header will be attached. - * - * **Important:** If you set this option to `true`, make sure that you configured your servers' - * CORS settings to allow the `traceparent` header. Otherwise, requests might get blocked. - * - * @see https://www.w3.org/TR/trace-context/ - * - * @default false - */ - propagateTraceparent?: boolean; - /** * If you use Spotlight by Sentry during development, use * this option to forward captured Sentry events to Spotlight. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 387ba0aba4a2..a38e841da6c1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -74,6 +74,7 @@ export { addAutoIpAddressToSession } from './utils/ipAddress'; export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, + spanToTraceparentHeader, spanToTraceHeader, spanToJSON, spanIsSampled, @@ -89,7 +90,7 @@ export { export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; -export { getTraceData } from './utils/traceData'; +export { getTraceData, scopeToTraceparentHeader } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; export { debounce } from './utils/debounce'; export { diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index c33d0107df5f..3d4ad7b67ea5 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -369,6 +369,20 @@ export interface ClientOptions, @@ -53,16 +54,16 @@ export function addTracePropagationHeadersToOutgoingRequest( // Manually add the trace headers, if it applies // Note: We do not use `propagation.inject()` here, because our propagator relies on an active span // Which we do not have in this case - const tracePropagationTargets = getClient()?.getOptions().tracePropagationTargets; + const { tracePropagationTargets, propagateTraceparent } = getClient()?.getOptions() || {}; const headersToAdd = shouldPropagateTraceForUrl(url, tracePropagationTargets, propagationDecisionMap) - ? getTraceData() + ? getTraceData({ propagateTraceparent }) : undefined; if (!headersToAdd) { return; } - const { 'sentry-trace': sentryTrace, baggage } = headersToAdd; + const { 'sentry-trace': sentryTrace, baggage, traceparent } = headersToAdd; // We do not want to overwrite existing header here, if it was already set if (sentryTrace && !request.getHeader('sentry-trace')) { @@ -79,6 +80,20 @@ export function addTracePropagationHeadersToOutgoingRequest( } } + if (traceparent && !request.getHeader('traceparent')) { + try { + request.setHeader('traceparent', traceparent); + DEBUG_BUILD && debug.log(INSTRUMENTATION_NAME, 'Added traceparent header to outgoing request'); + } catch (error) { + DEBUG_BUILD && + debug.error( + INSTRUMENTATION_NAME, + 'Failed to add traceparent header to outgoing request:', + isError(error) ? error.message : 'Unknown error', + ); + } + } + if (baggage) { // For baggage, we make sure to merge this into a possibly existing header const newBaggage = mergeBaggageHeaders(request.getHeader('baggage'), baggage); diff --git a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 3b7b745077be..f3bb8ca1e15a 100644 --- a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -114,6 +114,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase header === SENTRY_BAGGAGE_HEADER); if (baggage && existingBaggagePos === -1) { @@ -177,6 +182,10 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase Date: Thu, 11 Dec 2025 19:01:11 +0200 Subject: [PATCH 12/43] chore(tests): Added test variant flag (#18458) I have been running a lot of Next.js tests and needed to run specific variants against my changes, so I made these changes to our test runner script and thought to share it with everyone. ``` yarn test:run --variant ``` Closes #18459 (added automatically) --- dev-packages/e2e-tests/README.md | 31 +++++++ dev-packages/e2e-tests/run.ts | 142 +++++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 7 deletions(-) diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 2c793fa05df0..133b53268d52 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -25,6 +25,37 @@ Or run only a single E2E test app: yarn test:run ``` +Or you can run a single E2E test app with a specific variant: + +```bash +yarn test:run --variant +``` + +Variant name matching is case-insensitive and partial. For example, `--variant 13` will match `nextjs-pages-dir (next@13)` if a matching variant is present in the test app's `package.json`. + +For example, if you have the following variants in your test app's `package.json`: + +```json +"sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build-13", + "label": "nextjs-pages-dir (next@13)" + }, + { + "build-command": "pnpm test:build-13-canary", + "label": "nextjs-pages-dir (next@13-canary)" + }, + { + "build-command": "pnpm test:build-15", + "label": "nextjs-pages-dir (next@15)" + } + ] +} +``` + +If you run `yarn test:run nextjs-pages-dir --variant 13`, it will match against the very first matching variant, which is `nextjs-pages-dir (next@13)`. If you need to target the second variant in the example, you need to be more specific and use `--variant 13-canary`. + ## How they work Before running any tests we launch a fake test registry (in our case [Verdaccio](https://verdaccio.org/docs/e2e/)), we diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index e0331f0694f8..443ccf806b73 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -1,13 +1,26 @@ /* eslint-disable no-console */ import { spawn } from 'child_process'; import * as dotenv from 'dotenv'; -import { mkdtemp, rm } from 'fs/promises'; +import { mkdtemp, readFile, rm } from 'fs/promises'; import { sync as globSync } from 'glob'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; import { copyToTemp } from './lib/copyToTemp'; import { registrySetup } from './registrySetup'; +interface SentryTestVariant { + 'build-command': string; + 'assert-command'?: string; + label?: string; +} + +interface PackageJson { + sentryTest?: { + variants?: SentryTestVariant[]; + optionalVariants?: SentryTestVariant[]; + }; +} + const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; @@ -58,6 +71,49 @@ function asyncExec( }); } +function findMatchingVariant(variants: SentryTestVariant[], variantLabel: string): SentryTestVariant | undefined { + const variantLabelLower = variantLabel.toLowerCase(); + + return variants.find(variant => variant.label?.toLowerCase().includes(variantLabelLower)); +} + +async function getVariantBuildCommand( + packageJsonPath: string, + variantLabel: string, + testAppPath: string, +): Promise<{ buildCommand: string; assertCommand: string; testLabel: string; matchedVariantLabel?: string }> { + try { + const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); + const packageJson: PackageJson = JSON.parse(packageJsonContent); + + const allVariants = [ + ...(packageJson.sentryTest?.variants || []), + ...(packageJson.sentryTest?.optionalVariants || []), + ]; + + const matchingVariant = findMatchingVariant(allVariants, variantLabel); + + if (matchingVariant) { + return { + buildCommand: matchingVariant['build-command'] || 'pnpm test:build', + assertCommand: matchingVariant['assert-command'] || 'pnpm test:assert', + testLabel: matchingVariant.label || testAppPath, + matchedVariantLabel: matchingVariant.label, + }; + } + + console.log(`No matching variant found for "${variantLabel}" in ${testAppPath}, using default build`); + } catch { + console.log(`Could not read variants from package.json for ${testAppPath}, using default build`); + } + + return { + buildCommand: 'pnpm test:build', + assertCommand: 'pnpm test:assert', + testLabel: testAppPath, + }; +} + async function run(): Promise { // Load environment variables from .env file locally dotenv.config(); @@ -65,7 +121,50 @@ async function run(): Promise { // Allow to run a single app only via `yarn test:run ` const appName = process.argv[2] || ''; // Forward any additional flags to the test command - const testFlags = process.argv.slice(3); + const allTestFlags = process.argv.slice(3); + + // Check for --variant flag + let variantLabel: string | undefined; + let skipNextFlag = false; + + const testFlags = allTestFlags.filter((flag, index) => { + // Skip this flag if it was marked to skip (variant value after --variant) + if (skipNextFlag) { + skipNextFlag = false; + return false; + } + + // Handle --variant= format + if (flag.startsWith('--variant=')) { + const value = flag.slice('--variant='.length); + const trimmedValue = value?.trim(); + if (trimmedValue) { + variantLabel = trimmedValue; + } else { + console.warn('Warning: --variant= specified but no value provided. Ignoring variant flag.'); + } + return false; // Remove this flag from testFlags + } + + // Handle --variant format + if (flag === '--variant') { + if (index + 1 < allTestFlags.length) { + const value = allTestFlags[index + 1]; + const trimmedValue = value?.trim(); + if (trimmedValue) { + variantLabel = trimmedValue; + skipNextFlag = true; // Mark next flag to be skipped + } else { + console.warn('Warning: --variant specified but no value provided. Ignoring variant flag.'); + } + } else { + console.warn('Warning: --variant specified but no value provided. Ignoring variant flag.'); + } + return false; + } + + return true; + }); const dsn = process.env.E2E_TEST_DSN || DEFAULT_DSN; @@ -107,13 +206,42 @@ async function run(): Promise { await copyToTemp(originalPath, tmpDirPath); const cwd = tmpDirPath; + // Resolve variant if needed + const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel + ? await getVariantBuildCommand(join(tmpDirPath, 'package.json'), variantLabel, testAppPath) + : { + buildCommand: 'pnpm test:build', + assertCommand: 'pnpm test:assert', + testLabel: testAppPath, + }; + + // Print which variant we're using if found + if (matchedVariantLabel) { + console.log(`\n\nUsing variant: "${matchedVariantLabel}"\n\n`); + } - console.log(`Building ${testAppPath} in ${tmpDirPath}...`); - await asyncExec('volta run pnpm test:build', { env, cwd }); + console.log(`Building ${testLabel} in ${tmpDirPath}...`); + await asyncExec(`volta run ${buildCommand}`, { env, cwd }); - console.log(`Testing ${testAppPath}...`); - // Pass command and arguments as an array to prevent command injection - const testCommand = ['volta', 'run', 'pnpm', 'test:assert', ...testFlags]; + console.log(`Testing ${testLabel}...`); + // Pass command as a string to support shell features (env vars, operators like &&) + // This matches how buildCommand is handled for consistency + // Properly quote test flags to preserve spaces and special characters + const quotedTestFlags = testFlags.map(flag => { + // If flag contains spaces or special shell characters, quote it + if ( + flag.includes(' ') || + flag.includes('"') || + flag.includes("'") || + flag.includes('$') || + flag.includes('`') + ) { + // Escape single quotes and wrap in single quotes (safest for shell) + return `'${flag.replace(/'/g, "'\\''")}'`; + } + return flag; + }); + const testCommand = `volta run ${assertCommand}${quotedTestFlags.length > 0 ? ` ${quotedTestFlags.join(' ')}` : ''}`; await asyncExec(testCommand, { env, cwd }); // clean up (although this is tmp, still nice to do) From eaa3c56246dfc3ffa38d9d2363c4a383506dce27 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 8 Dec 2025 13:12:45 -0800 Subject: [PATCH 13/43] fix(tracing): add system prompt, model to google genai (#18424) Move the message reformatting into a separate util for google-genai, and add unit test coverage for that file. Add an integration test scenario to ensure that the system message will be included if provided in the config params. Related to https://github.com/getsentry/testing-ai-sdk-integrations/pull/10 Fix JS-1218 --- .../suites/tracing/google-genai/scenario.mjs | 1 + .../suites/tracing/google-genai/test.ts | 4 +- .../core/src/tracing/google-genai/index.ts | 51 +++++++---- .../core/src/tracing/google-genai/utils.ts | 51 +++++++++-- .../test/lib/utils/google-genai-utils.test.ts | 85 +++++++++++++++++++ 5 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 packages/core/test/lib/utils/google-genai-utils.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs index 91c75886e410..40f8af031f5a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs @@ -61,6 +61,7 @@ async function run() { temperature: 0.8, topP: 0.9, maxOutputTokens: 150, + systemInstruction: 'You are a friendly robot who likes to be funny.', }, history: [ { diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 8b2b04137fff..486b71dfedc7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -94,7 +94,9 @@ describe('Google GenAI integration', () => { 'gen_ai.request.temperature': 0.8, 'gen_ai.request.top_p': 0.9, 'gen_ai.request.max_tokens': 150, - 'gen_ai.request.messages': expect.any(String), // Should include history when recordInputs: true + 'gen_ai.request.messages': expect.stringMatching( + /\[\{"role":"system","content":"You are a friendly robot who likes to be funny."\},/, + ), // Should include history when recordInputs: true }), description: 'chat gemini-1.5-pro create', op: 'gen_ai.chat', diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index cc0226c5cb37..9c53e09fd1ca 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -16,6 +16,7 @@ import { GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, GEN_AI_REQUEST_TOP_K_ATTRIBUTE, GEN_AI_REQUEST_TOP_P_ATTRIBUTE, + GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, @@ -23,7 +24,8 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { buildMethodPath, getFinalOperationName, getSpanOperation, getTruncatedJsonString } from '../ai/utils'; +import { truncateGenAiMessages } from '../ai/messageTruncation'; +import { buildMethodPath, getFinalOperationName, getSpanOperation } from '../ai/utils'; import { CHAT_PATH, CHATS_CREATE_METHOD, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { @@ -33,7 +35,8 @@ import type { GoogleGenAIOptions, GoogleGenAIResponse, } from './types'; -import { isStreamingMethod, shouldInstrument } from './utils'; +import type { ContentListUnion, ContentUnion, Message, PartListUnion } from './utils'; +import { contentUnionToMessages, isStreamingMethod, shouldInstrument } from './utils'; /** * Extract model from parameters or chat context object @@ -134,26 +137,38 @@ function extractRequestAttributes( * Handles different parameter formats for different Google GenAI methods. */ function addPrivateRequestAttributes(span: Span, params: Record): void { - // For models.generateContent: ContentListUnion: Content | Content[] | PartUnion | PartUnion[] + const messages: Message[] = []; + + // config.systemInstruction: ContentUnion + if ( + 'config' in params && + params.config && + typeof params.config === 'object' && + 'systemInstruction' in params.config && + params.config.systemInstruction + ) { + messages.push(...contentUnionToMessages(params.config.systemInstruction as ContentUnion, 'system')); + } + + // For chats.create: history contains the conversation history + if ('history' in params) { + messages.push(...contentUnionToMessages(params.history as PartListUnion, 'user')); + } + + // For models.generateContent: ContentListUnion if ('contents' in params) { - const contents = params.contents; - // For models.generateContent: ContentListUnion: Content | Content[] | PartUnion | PartUnion[] - const truncatedContents = getTruncatedJsonString(contents); - span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: truncatedContents }); + messages.push(...contentUnionToMessages(params.contents as ContentListUnion, 'user')); } - // For chat.sendMessage: message can be string or Part[] + // For chat.sendMessage: message can be PartListUnion if ('message' in params) { - const message = params.message; - const truncatedMessage = getTruncatedJsonString(message); - span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: truncatedMessage }); + messages.push(...contentUnionToMessages(params.message as PartListUnion, 'user')); } - // For chats.create: history contains the conversation history - if ('history' in params) { - const history = params.history; - const truncatedHistory = getTruncatedJsonString(history); - span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: truncatedHistory }); + if (messages.length) { + span.setAttributes({ + [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(truncateGenAiMessages(messages)), + }); } } @@ -164,6 +179,10 @@ function addPrivateRequestAttributes(span: Span, params: Record function addResponseAttributes(span: Span, response: GoogleGenAIResponse, recordOutputs?: boolean): void { if (!response || typeof response !== 'object') return; + if (response.modelVersion) { + span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, response.modelVersion); + } + // Add usage metadata if present if (response.usageMetadata && typeof response.usageMetadata === 'object') { const usage = response.usageMetadata; diff --git a/packages/core/src/tracing/google-genai/utils.ts b/packages/core/src/tracing/google-genai/utils.ts index a394ed64a1bb..4280957ce43f 100644 --- a/packages/core/src/tracing/google-genai/utils.ts +++ b/packages/core/src/tracing/google-genai/utils.ts @@ -19,9 +19,50 @@ export function shouldInstrument(methodPath: string): methodPath is GoogleGenAII * Check if a method is a streaming method */ export function isStreamingMethod(methodPath: string): boolean { - return ( - methodPath.includes('Stream') || - methodPath.endsWith('generateContentStream') || - methodPath.endsWith('sendMessageStream') - ); + return methodPath.includes('Stream'); +} + +// Copied from https://googleapis.github.io/js-genai/release_docs/index.html +export type ContentListUnion = Content | Content[] | PartListUnion; +export type ContentUnion = Content | PartUnion[] | PartUnion; +export type Content = { + parts?: Part[]; + role?: string; +}; +export type PartUnion = Part | string; +export type Part = Record & { + inlineData?: { + data?: string; + displayName?: string; + mimeType?: string; + }; + text?: string; +}; +export type PartListUnion = PartUnion[] | PartUnion; + +// our consistent span message shape +export type Message = Record & { + role: string; + content?: PartListUnion; + parts?: PartListUnion; +}; + +/** + * + */ +export function contentUnionToMessages(content: ContentListUnion, role = 'user'): Message[] { + if (typeof content === 'string') { + return [{ role, content }]; + } + if (Array.isArray(content)) { + return content.flatMap(content => contentUnionToMessages(content, role)); + } + if (typeof content !== 'object' || !content) return []; + if ('role' in content && typeof content.role === 'string') { + return [content as Message]; + } + if ('parts' in content) { + return [{ ...content, role } as Message]; + } + return [{ role, content }]; } diff --git a/packages/core/test/lib/utils/google-genai-utils.test.ts b/packages/core/test/lib/utils/google-genai-utils.test.ts new file mode 100644 index 000000000000..7b9c6d80c773 --- /dev/null +++ b/packages/core/test/lib/utils/google-genai-utils.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import type { ContentListUnion } from '../../../src/tracing/google-genai/utils'; +import { contentUnionToMessages, isStreamingMethod, shouldInstrument } from '../../../src/tracing/google-genai/utils'; + +describe('isStreamingMethod', () => { + it('detects streaming methods', () => { + expect(isStreamingMethod('messageStreamBlah')).toBe(true); + expect(isStreamingMethod('blahblahblah generateContentStream')).toBe(true); + expect(isStreamingMethod('blahblahblah sendMessageStream')).toBe(true); + expect(isStreamingMethod('blahblahblah generateContentStream')).toBe(true); + expect(isStreamingMethod('blahblahblah sendMessageStream')).toBe(true); + expect(isStreamingMethod('blahblahblah generateContent')).toBe(false); + expect(isStreamingMethod('blahblahblah sendMessage')).toBe(false); + }); +}); + +describe('shouldInstrument', () => { + it('detects which methods to instrument', () => { + expect(shouldInstrument('models.generateContent')).toBe(true); + expect(shouldInstrument('some.path.to.sendMessage')).toBe(true); + expect(shouldInstrument('unknown')).toBe(false); + }); +}); + +describe('convert google-genai messages to consistent message', () => { + it('converts strings to messages', () => { + expect(contentUnionToMessages('hello', 'system')).toStrictEqual([{ role: 'system', content: 'hello' }]); + expect(contentUnionToMessages('hello')).toStrictEqual([{ role: 'user', content: 'hello' }]); + }); + + it('converts arrays of strings to messages', () => { + expect(contentUnionToMessages(['hello', 'goodbye'], 'system')).toStrictEqual([ + { role: 'system', content: 'hello' }, + { role: 'system', content: 'goodbye' }, + ]); + expect(contentUnionToMessages(['hello', 'goodbye'])).toStrictEqual([ + { role: 'user', content: 'hello' }, + { role: 'user', content: 'goodbye' }, + ]); + }); + + it('converts PartUnion to messages', () => { + expect(contentUnionToMessages(['hello', { parts: ['i am here', { text: 'goodbye' }] }], 'system')).toStrictEqual([ + { role: 'system', content: 'hello' }, + { role: 'system', parts: ['i am here', { text: 'goodbye' }] }, + ]); + + expect(contentUnionToMessages(['hello', { parts: ['i am here', { text: 'goodbye' }] }])).toStrictEqual([ + { role: 'user', content: 'hello' }, + { role: 'user', parts: ['i am here', { text: 'goodbye' }] }, + ]); + }); + + it('converts ContentUnion to messages', () => { + expect( + contentUnionToMessages( + { + parts: ['hello', 'goodbye'], + role: 'agent', + }, + 'user', + ), + ).toStrictEqual([{ parts: ['hello', 'goodbye'], role: 'agent' }]); + }); + + it('handles unexpected formats safely', () => { + expect( + contentUnionToMessages( + [ + { + parts: ['hello', 'goodbye'], + role: 'agent', + }, + null, + 21345, + { data: 'this is content' }, + ] as unknown as ContentListUnion, + 'user', + ), + ).toStrictEqual([ + { parts: ['hello', 'goodbye'], role: 'agent' }, + { role: 'user', content: { data: 'this is content' } }, + ]); + }); +}); From 80d5f5e3c3f891863b38d0a1a4245205f4495760 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 10:43:57 +0100 Subject: [PATCH 14/43] less otel changes --- .../opentelemetry/src/utils/getTraceData.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 4de14aab1bb3..7c25b6b11a6b 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -3,41 +3,51 @@ import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, - getActiveSpan, getCapturedScopesOnSpan, - getCurrentScope, scopeToTraceparentHeader, spanToTraceparentHeader, } from '@sentry/core'; import { getInjectionData } from '../propagator'; -import { getContextFromScope } from './contextData'; +import { getContextFromScope, getScopesFromContext } from './contextData'; /** * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData( - options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}, -): SerializedTraceData { - const span = options.span || getActiveSpan(); - const scope = options.scope || (span && getCapturedScopesOnSpan(span).scope) || getCurrentScope(); +export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData { + const { client, propagateTraceparent } = options; + let { span, scope } = options; - let ctx = getContextFromScope(scope) ?? api.context.active(); + let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { + const { scope } = getCapturedScopesOnSpan(span); // fall back to current context if for whatever reason we can't find the one of the span - ctx = getContextFromScope(scope) || api.trace.setSpan(api.context.active(), span); + ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); + } else { + span = api.trace.getSpan(ctx); } - const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client: options.client }); + if (!scope) { + const scopes = getScopesFromContext(ctx); + if (scopes) { + scope = scopes.scope; + } + } + + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); const traceData: SerializedTraceData = { 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), baggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), }; - if (options.propagateTraceparent) { - traceData.traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope); + if (propagateTraceparent) { + if (span) { + traceData.traceparent = spanToTraceparentHeader(span); + } else if (scope) { + traceData.traceparent = scopeToTraceparentHeader(scope); + } } return traceData; From dc8d7810fb9d7f5c66bcd34a336d40867dc500e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:44:10 +0100 Subject: [PATCH 15/43] chore(deps): bump next from 15.5.7 to 15.5.9 in /dev-packages/e2e-tests/test-applications/nextjs-15-intl (#18483) Bumps [next](https://github.com/vercel/next.js) from 15.5.7 to 15.5.9.
Release notes

Sourced from next's releases.

v15.5.9

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=15.5.7&new-version=15.5.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/nextjs-15-intl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json index f336c808668a..46bbc75d7020 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json @@ -15,7 +15,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "15.5.7", + "next": "15.5.9", "next-intl": "^4.3.12", "react": "latest", "react-dom": "latest", From 3bc1437f252eb41fcc09f180c13a220fa63e6b8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:44:35 +0100 Subject: [PATCH 16/43] chore(deps): bump next from 15.5.7 to 15.5.9 in /dev-packages/e2e-tests/test-applications/nextjs-15 (#18482) Bumps [next](https://github.com/vercel/next.js) from 15.5.7 to 15.5.9.
Release notes

Sourced from next's releases.

v15.5.9

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=15.5.7&new-version=15.5.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- 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 cef68807cf7f..45e72e4c3920 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -20,7 +20,7 @@ "@types/react": "18.0.26", "@types/react-dom": "18.0.9", "ai": "^3.0.0", - "next": "15.5.7", + "next": "15.5.9", "react": "latest", "react-dom": "latest", "typescript": "~5.0.0", From 529f933fe0cf9bdbc6bb93585ccbf442d35ef03a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:44:48 +0100 Subject: [PATCH 17/43] chore(deps): bump next from 16.0.7 to 16.0.9 in /dev-packages/e2e-tests/test-applications/nextjs-16-tunnel (#18481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [next](https://github.com/vercel/next.js) from 16.0.7 to 16.0.9.
Release notes

Sourced from next's releases.

v16.0.8

[!NOTE] This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

  • Update react version in cna templates (#86950)

Credits

Huge thanks to @​huozhi for helping!

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.0.7&new-version=16.0.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/nextjs-16-tunnel/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json index 60e78865f0f7..2e69ba66d716 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -27,7 +27,7 @@ "@sentry/core": "latest || *", "ai": "^3.0.0", "import-in-the-middle": "^1", - "next": "16.0.7", + "next": "16.0.9", "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^7", From 053487f59016a0f4a8220e2ca1b8e03e73676c57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:45:03 +0100 Subject: [PATCH 18/43] chore(deps): bump next from 16.0.7 to 16.0.9 in /dev-packages/e2e-tests/test-applications/nextjs-16 (#18480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [next](https://github.com/vercel/next.js) from 16.0.7 to 16.0.9.
Release notes

Sourced from next's releases.

v16.0.8

[!NOTE] This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

  • Update react version in cna templates (#86950)

Credits

Huge thanks to @​huozhi for helping!

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.0.7&new-version=16.0.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/e2e-tests/test-applications/nextjs-16/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index 782256f87ee3..45af070a0ea9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -27,7 +27,7 @@ "@sentry/core": "latest || *", "ai": "^3.0.0", "import-in-the-middle": "^2", - "next": "16.0.7", + "next": "16.0.9", "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^8", From f1f2604200c7afe2338fe92905b599c3247fbccb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:45:18 +0100 Subject: [PATCH 19/43] chore(deps): bump next from 16.0.7 to 16.0.9 in /dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents (#18479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [next](https://github.com/vercel/next.js) from 16.0.7 to 16.0.9.
Release notes

Sourced from next's releases.

v16.0.8

[!NOTE] This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

  • Update react version in cna templates (#86950)

Credits

Huge thanks to @​huozhi for helping!

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.0.7&new-version=16.0.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../test-applications/nextjs-16-cacheComponents/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index 0cd6c237a5af..b07203d86b0d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json @@ -26,7 +26,7 @@ "@sentry/nextjs": "latest || *", "@sentry/core": "latest || *", "import-in-the-middle": "^1", - "next": "16.0.7", + "next": "16.0.9", "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^7", From c448386b6ed23029097aaf10f80878b2168ed0c1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 11:02:10 +0100 Subject: [PATCH 20/43] Revert --- packages/core/src/index.ts | 4 +-- packages/core/src/utils/traceData.ts | 5 +--- .../opentelemetry/src/utils/getTraceData.ts | 30 ++++++------------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a38e841da6c1..5e414f76c341 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -74,7 +74,6 @@ export { addAutoIpAddressToSession } from './utils/ipAddress'; export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, - spanToTraceparentHeader, spanToTraceHeader, spanToJSON, spanIsSampled, @@ -90,7 +89,7 @@ export { export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; -export { getTraceData, scopeToTraceparentHeader } from './utils/traceData'; +export { getTraceData } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; export { debounce } from './utils/debounce'; export { @@ -270,6 +269,7 @@ export { generateSentryTraceHeader, propagationContextFromHeaders, shouldContinueTrace, + generateTraceparentHeader, } from './utils/tracing'; export { getSDKSource, isBrowserBundle } from './utils/env'; export type { SdkSource } from './utils/env'; diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 22358d750f14..9958e2761960 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -72,10 +72,7 @@ function scopeToTraceHeader(scope: Scope): string { return generateSentryTraceHeader(traceId, propagationSpanId, sampled); } -/** - * Get a traceparent header value for the given scope. - */ -export function scopeToTraceparentHeader(scope: Scope): string { +function scopeToTraceparentHeader(scope: Scope): string { const { traceId, sampled, propagationSpanId } = scope.getPropagationContext(); return generateTraceparentHeader(traceId, propagationSpanId, sampled); } diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 7c25b6b11a6b..cae4059cf9e1 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -3,36 +3,28 @@ import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, + generateTraceparentHeader, getCapturedScopesOnSpan, - scopeToTraceparentHeader, - spanToTraceparentHeader, } from '@sentry/core'; import { getInjectionData } from '../propagator'; -import { getContextFromScope, getScopesFromContext } from './contextData'; +import { getContextFromScope } from './contextData'; /** * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData { - const { client, propagateTraceparent } = options; - let { span, scope } = options; - +export function getTraceData({ + span, + scope, + client, + propagateTraceparent, +}: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData { let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { const { scope } = getCapturedScopesOnSpan(span); // fall back to current context if for whatever reason we can't find the one of the span ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); - } else { - span = api.trace.getSpan(ctx); - } - - if (!scope) { - const scopes = getScopesFromContext(ctx); - if (scopes) { - scope = scopes.scope; - } } const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); @@ -43,11 +35,7 @@ export function getTraceData(options: { span?: Span; scope?: Scope; client?: Cli }; if (propagateTraceparent) { - if (span) { - traceData.traceparent = spanToTraceparentHeader(span); - } else if (scope) { - traceData.traceparent = scopeToTraceparentHeader(scope); - } + traceData.traceparent = generateTraceparentHeader(traceId, spanId, sampled); } return traceData; From 89f1ee2b3d94be466c8f2e7075727638ac0e3650 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 11:38:43 +0100 Subject: [PATCH 21/43] Test http --- .../tracing/requests/fetch-sampled/test.ts | 29 ---------- .../instrument.mjs | 0 .../scenario-fetch.mjs} | 0 .../requests/traceparent/scenario-http.mjs | 21 +++++++ .../tracing/requests/traceparent/test.ts | 57 +++++++++++++++++++ 5 files changed, 78 insertions(+), 29 deletions(-) delete mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts rename dev-packages/node-core-integration-tests/suites/tracing/requests/{fetch-sampled => traceparent}/instrument.mjs (100%) rename dev-packages/node-core-integration-tests/suites/tracing/requests/{fetch-sampled/scenario.mjs => traceparent/scenario-fetch.mjs} (100%) create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts deleted file mode 100644 index ec8b6a0b1290..000000000000 --- a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect } from 'vitest'; -import { createEsmAndCjsTests } from '../../../../utils/runner'; -import { createTestServer } from '../../../../utils/server'; - -describe('outgoing fetch traceparent', () => { - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { - test('outgoing fetch requests are correctly instrumented when not sampled', async () => { - expect.assertions(5); - - const [SERVER_URL, closeTestServer] = await createTestServer() - .get('/api/v1', headers => { - expect(headers['baggage']).toEqual(expect.any(String)); - expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); - expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); - expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); - }) - .start(); - - await createRunner() - .withEnv({ SERVER_URL }) - .expect({ - transaction: {}, - }) - .start() - .completed(); - closeTestServer(); - }); - }); -}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/instrument.mjs similarity index 100% rename from dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs rename to dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/instrument.mjs diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs similarity index 100% rename from dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs rename to dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs new file mode 100644 index 000000000000..4ddeb450f76d --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +await Sentry.startSpan({ name: 'outer' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts new file mode 100644 index 000000000000..2cdb4cfd1aa7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts @@ -0,0 +1,57 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing traceparent', () => { + createEsmAndCjsTests(__dirname, 'scenario-fetch.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-http.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing http requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); From d9fa8ba63478ce0c6b99570fba8e6ad17ec14a88 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 11:57:16 +0100 Subject: [PATCH 22/43] More tests and bump size limit --- .size-limit.js | 2 +- .../requests/traceparent/instrument.mjs | 10 ++++ .../requests/traceparent/scenario-fetch.mjs | 10 ++++ .../requests/traceparent/scenario-http.mjs | 21 +++++++ .../tracing/requests/traceparent/test.ts | 57 +++++++++++++++++++ 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts diff --git a/.size-limit.js b/.size-limit.js index 6e6ee0f68303..00b4bdbfd4d8 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -240,7 +240,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '160 KB', + limit: '161 KB', }, { name: '@sentry/node - without tracing', diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs new file mode 100644 index 000000000000..886377ef59e7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + propagateTraceparent: true, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs new file mode 100644 index 000000000000..84203bb8843e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; + +async function run() { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs new file mode 100644 index 000000000000..35805293a797 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/node'; +import * as http from 'http'; + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +await Sentry.startSpan({ name: 'outer' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts new file mode 100644 index 000000000000..2cdb4cfd1aa7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts @@ -0,0 +1,57 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing traceparent', () => { + createEsmAndCjsTests(__dirname, 'scenario-fetch.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-http.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing http requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); From e9b2df98ac6f4b3283d179c8cf9a1396e7b852a7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 12 Dec 2025 15:06:33 +0100 Subject: [PATCH 23/43] feat(nuxt): Bump `@sentry/vite-plugin` and `@sentry/rollup-plugin` to 4.6.1 (#18349) https://github.com/getsentry/sentry-javascript/pull/18270 bumps just the rollup plugin but we should bump both simultaneously. --- packages/nuxt/package.json | 6 +- packages/react-router/package.json | 4 +- .../src/vite/buildEnd/handleOnBuildEnd.ts | 16 +- .../vite/buildEnd/handleOnBuildEnd.test.ts | 36 +++++ yarn.lock | 147 ++++++++---------- 5 files changed, 117 insertions(+), 92 deletions(-) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index f17461a38171..ae78bac28ff7 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -53,9 +53,9 @@ "@sentry/cloudflare": "10.30.0", "@sentry/core": "10.30.0", "@sentry/node": "10.30.0", - "@sentry/rollup-plugin": "^4.3.0", - "@sentry/vite-plugin": "^4.3.0", - "@sentry/vue": "10.30.0" + "@sentry/vue": "10.30.0", + "@sentry/rollup-plugin": "^4.6.1", + "@sentry/vite-plugin": "^4.6.1" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index f971952daa61..cb62cc4fd1ce 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -50,11 +50,11 @@ "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/browser": "10.30.0", - "@sentry/cli": "^2.58.2", + "@sentry/cli": "^2.58.4", "@sentry/core": "10.30.0", "@sentry/node": "10.30.0", "@sentry/react": "10.30.0", - "@sentry/vite-plugin": "^4.1.0", + "@sentry/vite-plugin": "^4.6.1", "glob": "11.1.0" }, "devDependencies": { diff --git a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts index a3d1e78cb285..2ee57bdc717e 100644 --- a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts +++ b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts @@ -30,6 +30,8 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo ...sentryConfigWithoutDeprecatedSourceMapOption } = sentryConfig; + const unstableSentryVitePluginOptions = sentryConfig.unstable_sentryVitePluginOptions; + const { authToken, org, @@ -40,26 +42,32 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo }: Omit & // Pick 'sourcemaps' from Vite plugin options as the types allow more (e.g. Promise values for `deleteFilesAfterUpload`) Pick = { - ...sentryConfig.unstable_sentryVitePluginOptions, + ...unstableSentryVitePluginOptions, ...sentryConfigWithoutDeprecatedSourceMapOption, // spread in the config without the deprecated sourceMapsUploadOptions sourcemaps: { - ...sentryConfig.unstable_sentryVitePluginOptions?.sourcemaps, + ...unstableSentryVitePluginOptions?.sourcemaps, ...sentryConfig.sourcemaps, ...sourceMapsUploadOptions, // eslint-disable-next-line deprecation/deprecation disable: sourceMapsUploadOptions?.enabled === false ? true : sentryConfig.sourcemaps?.disable, }, release: { - ...sentryConfig.unstable_sentryVitePluginOptions?.release, + ...unstableSentryVitePluginOptions?.release, ...sentryConfig.release, }, + project: unstableSentryVitePluginOptions?.project + ? Array.isArray(unstableSentryVitePluginOptions?.project) + ? unstableSentryVitePluginOptions?.project[0] + : unstableSentryVitePluginOptions?.project + : sentryConfigWithoutDeprecatedSourceMapOption.project, }; const cliInstance = new SentryCli(null, { authToken, org, - project, ...sentryConfig.unstable_sentryVitePluginOptions, + // same handling as in bundler plugins: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/05084f214c763a05137d863ff5a05ef38254f68d/packages/bundler-plugin-core/src/build-plugin-manager.ts#L102-L103 + project: Array.isArray(project) ? project[0] : project, }); // check if release should be created diff --git a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts index 8be1d7764219..a607ff3ccfc6 100644 --- a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts +++ b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts @@ -329,4 +329,40 @@ describe('sentryOnBuildEnd', () => { expect(SentryCli).toHaveBeenCalledWith(null, expect.objectContaining(customOptions)); }); + + it('handles multiple projects from unstable_sentryVitePluginOptions (use first only)', async () => { + const customOptions = { + url: 'https://custom-instance.ejemplo.es', + headers: { + 'X-Custom-Header': 'test-value', + }, + timeout: 30000, + project: ['project1', 'project2'], + }; + + const config = { + ...defaultConfig, + viteConfig: { + ...defaultConfig.viteConfig, + sentryConfig: { + ...defaultConfig.viteConfig.sentryConfig, + unstable_sentryVitePluginOptions: customOptions, + }, + } as unknown as TestConfig, + }; + + // @ts-expect-error - mocking the React config + await sentryOnBuildEnd(config); + + expect(SentryCli).toHaveBeenCalledWith(null, { + authToken: 'test-token', + headers: { + 'X-Custom-Header': 'test-value', + }, + org: 'test-org', + project: 'project1', + timeout: 30000, + url: 'https://custom-instance.ejemplo.es', + }); + }); }); diff --git a/yarn.lock b/yarn.lock index a332772b21e9..324b3a726ba4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7069,30 +7069,11 @@ fflate "^0.4.4" mitt "^3.0.0" -"@sentry/babel-plugin-component-annotate@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz#c5b6cbb986952596d3ad233540a90a1fd18bad80" - integrity sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw== - "@sentry/babel-plugin-component-annotate@4.6.1": version "4.6.1" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz#94eec0293be8289daa574e18783e64d29203c236" integrity sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA== -"@sentry/bundler-plugin-core@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.3.0.tgz#cf302522a3e5b8a3bf727635d0c6a7bece981460" - integrity sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "4.3.0" - "@sentry/cli" "^2.51.0" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - "@sentry/bundler-plugin-core@4.6.1", "@sentry/bundler-plugin-core@^4.6.1": version "4.6.1" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.1.tgz#d6013e6233bf663114f581bbd3c3a380ff9311d4" @@ -7107,50 +7088,50 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz#61f6f836de8ac2e1992ccadc0368bc403f23c609" - integrity sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w== - -"@sentry/cli-linux-arm64@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz#3a7a9c83e31b482599ce08d93d5ba6c8a1a44c7f" - integrity sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g== - -"@sentry/cli-linux-arm@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz#f9bef6802cb707d1603a02e0727fed22d834e133" - integrity sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg== - -"@sentry/cli-linux-i686@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz#a3e6cb24d314f2d948b96457731f9345dc8370f9" - integrity sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA== - -"@sentry/cli-linux-x64@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz#8e071e11b03524b08d369075f3203b05529ca233" - integrity sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A== - -"@sentry/cli-win32-arm64@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz#af109a165c25245458a6c58b79a91c639b1df1b0" - integrity sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w== - -"@sentry/cli-win32-i686@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz#53038b43b2c14c419fb71586f7448e7580ed4e39" - integrity sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q== - -"@sentry/cli-win32-x64@2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz#b4c81a3c163344ae8b27523a0391e7f99c533f41" - integrity sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA== - -"@sentry/cli@^2.51.0", "@sentry/cli@^2.57.0", "@sentry/cli@^2.58.2": - version "2.58.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.58.2.tgz#0d6e19a1771d27aae8b2765a6f3e96062e2c7502" - integrity sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw== +"@sentry/cli-darwin@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz#5e3005c1f845acac243e8dcb23bef17337924768" + integrity sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ== + +"@sentry/cli-linux-arm64@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz#69da57656fda863f255d92123c3a3437e470408e" + integrity sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig== + +"@sentry/cli-linux-arm@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz#869ddab30f0dcebc0e61cff2f3ff47dcd40f8abe" + integrity sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA== + +"@sentry/cli-linux-i686@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz#e30ca6b897147b3fb7b2e8684b139183d55e21c6" + integrity sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA== + +"@sentry/cli-linux-x64@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz#f667e1fcaf0860f15401af8e0ee72f5013d84458" + integrity sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q== + +"@sentry/cli-win32-arm64@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz#f612c5788954e2a97b6626e9e46fa9a41cb049c1" + integrity sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w== + +"@sentry/cli-win32-i686@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz#5611c05499f1b959d23e37650d0621d299c49cfc" + integrity sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug== + +"@sentry/cli-win32-x64@2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz#3290c59399579e8d484c97246cfa720171241061" + integrity sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w== + +"@sentry/cli@^2.57.0", "@sentry/cli@^2.58.2", "@sentry/cli@^2.58.4": + version "2.58.4" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.58.4.tgz#eb8792600cdf956cc4fe2bf51380ea1682327411" + integrity sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -7158,29 +7139,29 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.58.2" - "@sentry/cli-linux-arm" "2.58.2" - "@sentry/cli-linux-arm64" "2.58.2" - "@sentry/cli-linux-i686" "2.58.2" - "@sentry/cli-linux-x64" "2.58.2" - "@sentry/cli-win32-arm64" "2.58.2" - "@sentry/cli-win32-i686" "2.58.2" - "@sentry/cli-win32-x64" "2.58.2" - -"@sentry/rollup-plugin@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-4.3.0.tgz#d23fe49e48fa68dafa2b0933a8efabcc964b1df9" - integrity sha512-Ebk6cTGTNohnLEvHtwDKYlMRs8Qit/ybOflIKlQziBHjd51GtxG9TPIu9NYU0fJXa428aYNluto3BfgdMp+c+Q== + "@sentry/cli-darwin" "2.58.4" + "@sentry/cli-linux-arm" "2.58.4" + "@sentry/cli-linux-arm64" "2.58.4" + "@sentry/cli-linux-i686" "2.58.4" + "@sentry/cli-linux-x64" "2.58.4" + "@sentry/cli-win32-arm64" "2.58.4" + "@sentry/cli-win32-i686" "2.58.4" + "@sentry/cli-win32-x64" "2.58.4" + +"@sentry/rollup-plugin@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-4.6.1.tgz#28dece8d6fad3044fd634724f6334f6b9b8f3ded" + integrity sha512-4G4oo05BhP7CjXdpTVFPInBgDNcuE5WKglALbCa2H9CY4ta8nHHPn2ni+d0WjhUIp6m5E1e+0NQ+0SxuFTCHVw== dependencies: - "@sentry/bundler-plugin-core" "4.3.0" + "@sentry/bundler-plugin-core" "4.6.1" unplugin "1.0.1" -"@sentry/vite-plugin@^4.1.0", "@sentry/vite-plugin@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-4.3.0.tgz#ced993a1f59046404aa26fb57b12078d13680ffa" - integrity sha512-MeTAHMmTOgBPMAjeW7/ONyXwgScZdaFFtNiALKcAODnVqC7eoHdSRIWeH5mkLr2Dvs7nqtBaDpKxRjUBgfm9LQ== +"@sentry/vite-plugin@^4.1.0", "@sentry/vite-plugin@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-4.6.1.tgz#883d8448c033b309528985e12e0d5d1af99ee1c6" + integrity sha512-Qvys1y3o8/bfL3ikrHnJS9zxdjt0z3POshdBl3967UcflrTqBmnGNkcVk53SlmtJWIfh85fgmrLvGYwZ2YiqNg== dependencies: - "@sentry/bundler-plugin-core" "4.3.0" + "@sentry/bundler-plugin-core" "4.6.1" unplugin "1.0.1" "@sentry/webpack-plugin@^4.6.1": @@ -18079,7 +18060,7 @@ glob@^8.0.0, glob@^8.0.1, glob@^8.0.3, glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" -glob@^9.2.0, glob@^9.3.2, glob@^9.3.3: +glob@^9.2.0, glob@^9.3.3: version "9.3.5" resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== From 6fd995e814c5c84a1c94baae769c58a1146eb401 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:30:57 +0000 Subject: [PATCH 24/43] chore(deps): bump next from 16.0.9 to 16.0.10 in /dev-packages/e2e-tests/test-applications/nextjs-16-tunnel (#18487) Bumps [next](https://github.com/vercel/next.js) from 16.0.9 to 16.0.10.
Release notes

Sourced from next's releases.

v16.0.10

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.0.9&new-version=16.0.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/nextjs-16-tunnel/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json index 2e69ba66d716..8ebdc91a6e65 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -27,7 +27,7 @@ "@sentry/core": "latest || *", "ai": "^3.0.0", "import-in-the-middle": "^1", - "next": "16.0.9", + "next": "16.0.10", "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^7", From 2e7a29ed969b82c6f5d36c9bf7f54bba3a108a51 Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 4 Dec 2025 14:06:53 -0800 Subject: [PATCH 25/43] feat(tracing): strip inline media from messages (#18413) This is the functional portion addressing JS-1002. Prior to truncating text messages for their byte length, any inline base64-encoded media properties are filtered out. This allows the message to possibly be included in the span, indicating to the user that a media object was present, without overflowing the allotted buffer for sending data. If a media message is not removed, the fallback is still to simply remove it if its overhead grows too large. Re JS-1002 Re GH-17810 --- .size-limit.js | 2 +- .../anthropic/scenario-media-truncation.mjs | 79 ++++ .../suites/tracing/anthropic/test.ts | 48 +++ .../core/src/tracing/ai/messageTruncation.ts | 184 +++++++++- .../lib/tracing/ai-message-truncation.test.ts | 336 ++++++++++++++++++ 5 files changed, 633 insertions(+), 16 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs create mode 100644 packages/core/test/lib/tracing/ai-message-truncation.test.ts diff --git a/.size-limit.js b/.size-limit.js index 6e6ee0f68303..00b4bdbfd4d8 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -240,7 +240,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '160 KB', + limit: '161 KB', }, { name: '@sentry/node - without tracing', diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs new file mode 100644 index 000000000000..73891ad30b6f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs @@ -0,0 +1,79 @@ +import { instrumentAnthropicAiClient } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +class MockAnthropic { + constructor(config) { + this.apiKey = config.apiKey; + this.baseURL = config.baseURL; + + // Create messages object with create method + this.messages = { + create: this._messagesCreate.bind(this), + }; + } + + /** + * Create a mock message + */ + async _messagesCreate(params) { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + return { + id: 'msg-truncation-test', + type: 'message', + role: 'assistant', + content: [ + { + type: 'text', + text: 'This is the number **3**.', + }, + ], + model: params.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 15, + }, + }; + } +} + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const mockClient = new MockAnthropic({ + apiKey: 'mock-api-key', + }); + + const client = instrumentAnthropicAiClient(mockClient); + + // Send the image showing the number 3 + await client.messages.create({ + model: 'claude-3-haiku-20240307', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: 'base64-mumbo-jumbo'.repeat(100), + }, + }, + ], + }, + { + role: 'user', + content: 'what number is this?', + }, + ], + temperature: 0.7, + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index ebebf60db042..f62975dafb71 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -661,4 +661,52 @@ describe('Anthropic integration', () => { }); }, ); + + createEsmAndCjsTests(__dirname, 'scenario-media-truncation.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('truncates media attachment, keeping all other details', async () => { + await createRunner() + .ignore('event') + .expect({ + transaction: { + transaction: 'main', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'gen_ai.operation.name': 'messages', + 'sentry.op': 'gen_ai.messages', + 'sentry.origin': 'auto.ai.anthropic', + 'gen_ai.system': 'anthropic', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.request.messages': JSON.stringify([ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: '[Filtered]', + }, + }, + ], + }, + { + role: 'user', + content: 'what number is this?', + }, + ]), + }), + description: 'messages claude-3-haiku-20240307', + op: 'gen_ai.messages', + origin: 'auto.ai.anthropic', + status: 'ok', + }), + ]), + }, + }) + .start() + .completed(); + }); + }); }); diff --git a/packages/core/src/tracing/ai/messageTruncation.ts b/packages/core/src/tracing/ai/messageTruncation.ts index 945761f6220c..9c8718387404 100644 --- a/packages/core/src/tracing/ai/messageTruncation.ts +++ b/packages/core/src/tracing/ai/messageTruncation.ts @@ -12,13 +12,49 @@ type ContentMessage = { content: string; }; +/** + * Message format used by OpenAI and Anthropic APIs for media. + */ +type ContentArrayMessage = { + [key: string]: unknown; + content: { + [key: string]: unknown; + type: string; + }[]; +}; + +/** + * Inline media content source, with a potentially very large base64 + * blob or data: uri. + */ +type ContentMedia = Record & + ( + | { + media_type: string; + data: string; + } + | { + image_url: `data:${string}`; + } + | { + type: 'blob' | 'base64'; + content: string; + } + | { + b64_json: string; + } + | { + uri: `data:${string}`; + } + ); + /** * Message format used by Google GenAI API. * Parts can be strings or objects with a text property. */ type PartsMessage = { [key: string]: unknown; - parts: Array; + parts: Array; }; /** @@ -26,6 +62,14 @@ type PartsMessage = { */ type TextPart = string | { text: string }; +/** + * A part in a Google GenAI that contains media. + */ +type MediaPart = { + type: string; + content: string; +}; + /** * Calculate the UTF-8 byte length of a string. */ @@ -79,11 +123,12 @@ function truncateTextByBytes(text: string, maxBytes: number): string { * * @returns The text content */ -function getPartText(part: TextPart): string { +function getPartText(part: TextPart | MediaPart): string { if (typeof part === 'string') { return part; } - return part.text; + if ('text' in part) return part.text; + return ''; } /** @@ -93,7 +138,7 @@ function getPartText(part: TextPart): string { * @param text - New text content * @returns New part with updated text */ -function withPartText(part: TextPart, text: string): TextPart { +function withPartText(part: TextPart | MediaPart, text: string): TextPart { if (typeof part === 'string') { return text; } @@ -112,6 +157,43 @@ function isContentMessage(message: unknown): message is ContentMessage { ); } +/** + * Check if a message has the OpenAI/Anthropic content array format. + */ +function isContentArrayMessage(message: unknown): message is ContentArrayMessage { + return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content); +} + +/** + * Check if a content part is an OpenAI/Anthropic media source + */ +function isContentMedia(part: unknown): part is ContentMedia { + if (!part || typeof part !== 'object') return false; + + return ( + isContentMediaSource(part) || + hasInlineData(part) || + ('media_type' in part && typeof part.media_type === 'string' && 'data' in part) || + ('image_url' in part && typeof part.image_url === 'string' && part.image_url.startsWith('data:')) || + ('type' in part && (part.type === 'blob' || part.type === 'base64')) || + 'b64_json' in part || + ('type' in part && 'result' in part && part.type === 'image_generation') || + ('uri' in part && typeof part.uri === 'string' && part.uri.startsWith('data:')) + ); +} +function isContentMediaSource(part: NonNullable): boolean { + return 'type' in part && typeof part.type === 'string' && 'source' in part && isContentMedia(part.source); +} +function hasInlineData(part: NonNullable): part is { inlineData: { data?: string } } { + return ( + 'inlineData' in part && + !!part.inlineData && + typeof part.inlineData === 'object' && + 'data' in part.inlineData && + typeof part.inlineData.data === 'string' + ); +} + /** * Check if a message has the Google GenAI parts format. */ @@ -167,7 +249,7 @@ function truncatePartsMessage(message: PartsMessage, maxBytes: number): unknown[ } // Include parts until we run out of space - const includedParts: TextPart[] = []; + const includedParts: (TextPart | MediaPart)[] = []; for (const part of parts) { const text = getPartText(part); @@ -190,7 +272,14 @@ function truncatePartsMessage(message: PartsMessage, maxBytes: number): unknown[ } } - return includedParts.length > 0 ? [{ ...message, parts: includedParts }] : []; + /* c8 ignore start + * for type safety only, algorithm guarantees SOME text included */ + if (includedParts.length <= 0) { + return []; + } else { + /* c8 ignore stop */ + return [{ ...message, parts: includedParts }]; + } } /** @@ -205,9 +294,11 @@ function truncatePartsMessage(message: PartsMessage, maxBytes: number): unknown[ * @returns Array containing the truncated message, or empty array if truncation fails */ function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { + /* c8 ignore start - unreachable */ if (!message || typeof message !== 'object') { return []; } + /* c8 ignore stop */ if (isContentMessage(message)) { return truncateContentMessage(message, maxBytes); @@ -221,6 +312,64 @@ function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { return []; } +const REMOVED_STRING = '[Filtered]'; + +const MEDIA_FIELDS = ['image_url', 'data', 'content', 'b64_json', 'result', 'uri'] as const; + +function stripInlineMediaFromSingleMessage(part: ContentMedia): ContentMedia { + const strip = { ...part }; + if (isContentMedia(strip.source)) { + strip.source = stripInlineMediaFromSingleMessage(strip.source); + } + // google genai inline data blob objects + if (hasInlineData(part)) { + strip.inlineData = { ...part.inlineData, data: REMOVED_STRING }; + } + for (const field of MEDIA_FIELDS) { + if (typeof strip[field] === 'string') strip[field] = REMOVED_STRING; + } + return strip; +} + +/** + * Strip the inline media from message arrays. + * + * This returns a stripped message. We do NOT want to mutate the data in place, + * because of course we still want the actual API/client to handle the media. + */ +function stripInlineMediaFromMessages(messages: unknown[]): unknown[] { + const stripped = messages.map(message => { + let newMessage: Record | undefined = undefined; + if (!!message && typeof message === 'object') { + if (isContentArrayMessage(message)) { + newMessage = { + ...message, + content: stripInlineMediaFromMessages(message.content), + }; + } else if ('content' in message && isContentMedia(message.content)) { + newMessage = { + ...message, + content: stripInlineMediaFromSingleMessage(message.content), + }; + } + if (isPartsMessage(message)) { + newMessage = { + // might have to strip content AND parts + ...(newMessage ?? message), + parts: stripInlineMediaFromMessages(message.parts), + }; + } + if (isContentMedia(newMessage)) { + newMessage = stripInlineMediaFromSingleMessage(newMessage); + } else if (isContentMedia(message)) { + newMessage = stripInlineMediaFromSingleMessage(message); + } + } + return newMessage ?? message; + }); + return stripped; +} + /** * Truncate an array of messages to fit within a byte limit. * @@ -240,26 +389,30 @@ function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { * // Returns [msg3, msg4] if they fit, or [msg4] if only it fits, etc. * ``` */ -export function truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] { +function truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] { // Early return for empty or invalid input if (!Array.isArray(messages) || messages.length === 0) { return messages; } + // strip inline media first. This will often get us below the threshold, + // while preserving human-readable information about messages sent. + const stripped = stripInlineMediaFromMessages(messages); + // Fast path: if all messages fit, return as-is - const totalBytes = jsonBytes(messages); + const totalBytes = jsonBytes(stripped); if (totalBytes <= maxBytes) { - return messages; + return stripped; } // Precompute each message's JSON size once for efficiency - const messageSizes = messages.map(jsonBytes); + const messageSizes = stripped.map(jsonBytes); // Find the largest suffix (newest messages) that fits within the budget let bytesUsed = 0; - let startIndex = messages.length; // Index where the kept suffix starts + let startIndex = stripped.length; // Index where the kept suffix starts - for (let i = messages.length - 1; i >= 0; i--) { + for (let i = stripped.length - 1; i >= 0; i--) { const messageSize = messageSizes[i]; if (messageSize && bytesUsed + messageSize > maxBytes) { @@ -274,13 +427,14 @@ export function truncateMessagesByBytes(messages: unknown[], maxBytes: number): } // If no complete messages fit, try truncating just the newest message - if (startIndex === messages.length) { - const newestMessage = messages[messages.length - 1]; + if (startIndex === stripped.length) { + // we're truncating down to one message, so all others dropped. + const newestMessage = stripped[stripped.length - 1]; return truncateSingleMessage(newestMessage, maxBytes); } // Return the suffix that fits - return messages.slice(startIndex); + return stripped.slice(startIndex); } /** diff --git a/packages/core/test/lib/tracing/ai-message-truncation.test.ts b/packages/core/test/lib/tracing/ai-message-truncation.test.ts new file mode 100644 index 000000000000..968cd2308bb7 --- /dev/null +++ b/packages/core/test/lib/tracing/ai-message-truncation.test.ts @@ -0,0 +1,336 @@ +import { describe, expect, it } from 'vitest'; +import { truncateGenAiMessages, truncateGenAiStringInput } from '../../../src/tracing/ai/messageTruncation'; + +describe('message truncation utilities', () => { + describe('truncateGenAiMessages', () => { + it('leaves empty/non-array/small messages alone', () => { + // @ts-expect-error - exercising invalid type code path + expect(truncateGenAiMessages(null)).toBe(null); + expect(truncateGenAiMessages([])).toStrictEqual([]); + expect(truncateGenAiMessages([{ text: 'hello' }])).toStrictEqual([{ text: 'hello' }]); + expect(truncateGenAiStringInput('hello')).toBe('hello'); + }); + + it('strips inline media from messages', () => { + const b64 = Buffer.from('lots of data\n').toString('base64'); + const removed = '[Filtered]'; + const messages = [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: b64, + }, + }, + ], + }, + { + role: 'user', + content: { + image_url: `data:image/png;base64,${b64}`, + }, + }, + { + role: 'agent', + type: 'image', + content: { + b64_json: b64, + }, + }, + { + role: 'system', + inlineData: { + mimeType: 'kiki/booba', + data: 'booboobooboobooba', + }, + content: [ + 'this one has content AND parts and has inline data', + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: b64, + }, + }, + ], + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: 'bloobloobloo', + }, + }, + { + image_url: `data:image/png;base64,${b64}`, + }, + { + type: 'image_generation', + result: b64, + }, + { + uri: `data:image/png;base64,${b64}`, + mediaType: 'image/png', + }, + { + type: 'blob', + mediaType: 'image/png', + content: b64, + }, + { + type: 'text', + text: 'just some text!', + }, + 'unadorned text', + ], + }, + ]; + + // indented json makes for better diffs in test output + const messagesJson = JSON.stringify(messages, null, 2); + const result = truncateGenAiMessages(messages); + + // original messages objects must not be mutated + expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); + expect(result).toStrictEqual([ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: removed, + }, + }, + ], + }, + { + role: 'user', + content: { + image_url: removed, + }, + }, + { + role: 'agent', + type: 'image', + content: { + b64_json: removed, + }, + }, + { + role: 'system', + inlineData: { + mimeType: 'kiki/booba', + data: removed, + }, + content: [ + 'this one has content AND parts and has inline data', + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: removed, + }, + }, + ], + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: removed, + }, + }, + { + image_url: removed, + }, + { + type: 'image_generation', + result: removed, + }, + { + uri: removed, + mediaType: 'image/png', + }, + { + type: 'blob', + mediaType: 'image/png', + content: removed, + }, + { + type: 'text', + text: 'just some text!', + }, + 'unadorned text', + ], + }, + ]); + }); + + const humongous = 'this is a long string '.repeat(10_000); + const giant = 'this is a long string '.repeat(1_000); + const big = 'this is a long string '.repeat(100); + + it('drops older messages to fit in the limit', () => { + const messages = [ + `0 ${giant}`, + { type: 'text', content: `1 ${big}` }, + { type: 'text', content: `2 ${big}` }, + { type: 'text', content: `3 ${giant}` }, + { type: 'text', content: `4 ${big}` }, + `5 ${big}`, + { type: 'text', content: `6 ${big}` }, + { type: 'text', content: `7 ${big}` }, + { type: 'text', content: `8 ${big}` }, + { type: 'text', content: `9 ${big}` }, + { type: 'text', content: `10 ${big}` }, + { type: 'text', content: `11 ${big}` }, + { type: 'text', content: `12 ${big}` }, + ]; + + const messagesJson = JSON.stringify(messages, null, 2); + const result = truncateGenAiMessages(messages); + // should not mutate original messages list + expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); + + // just retain the messages that fit in the budget + expect(result).toStrictEqual([ + `5 ${big}`, + { type: 'text', content: `6 ${big}` }, + { type: 'text', content: `7 ${big}` }, + { type: 'text', content: `8 ${big}` }, + { type: 'text', content: `9 ${big}` }, + { type: 'text', content: `10 ${big}` }, + { type: 'text', content: `11 ${big}` }, + { type: 'text', content: `12 ${big}` }, + ]); + }); + + it('fully drops message if content cannot be made to fit', () => { + const messages = [{ some_other_field: humongous, content: 'hello' }]; + expect(truncateGenAiMessages(messages)).toStrictEqual([]); + }); + + it('truncates if the message content string will not fit', () => { + const messages = [{ content: `2 ${humongous}` }]; + const result = truncateGenAiMessages(messages); + const truncLen = 20_000 - JSON.stringify({ content: '' }).length; + expect(result).toStrictEqual([{ content: `2 ${humongous}`.substring(0, truncLen) }]); + }); + + it('fully drops message if first part overhead does not fit', () => { + const messages = [ + { + parts: [{ some_other_field: humongous }], + }, + ]; + expect(truncateGenAiMessages(messages)).toStrictEqual([]); + }); + + it('fully drops message if overhead too large', () => { + const messages = [ + { + some_other_field: humongous, + parts: [], + }, + ]; + expect(truncateGenAiMessages(messages)).toStrictEqual([]); + }); + + it('truncates if the first message part will not fit', () => { + const messages = [ + { + parts: [`2 ${humongous}`, { some_other_field: 'no text here' }], + }, + ]; + + const result = truncateGenAiMessages(messages); + + // interesting (unexpected?) edge case effect of this truncation. + // subsequent messages count towards truncation overhead limit, + // but are not included, even without their text. This is an edge + // case that seems unlikely in normal usage. + const truncLen = + 20_000 - + JSON.stringify({ + parts: ['', { some_other_field: 'no text here', text: '' }], + }).length; + + expect(result).toStrictEqual([ + { + parts: [`2 ${humongous}`.substring(0, truncLen)], + }, + ]); + }); + + it('truncates if the first message part will not fit, text object', () => { + const messages = [ + { + parts: [{ text: `2 ${humongous}` }], + }, + ]; + const result = truncateGenAiMessages(messages); + const truncLen = + 20_000 - + JSON.stringify({ + parts: [{ text: '' }], + }).length; + expect(result).toStrictEqual([ + { + parts: [ + { + text: `2 ${humongous}`.substring(0, truncLen), + }, + ], + }, + ]); + }); + + it('drops if subsequent message part will not fit, text object', () => { + const messages = [ + { + parts: [ + { text: `1 ${big}` }, + { some_other_field: 'ok' }, + { text: `2 ${big}` }, + { text: `3 ${big}` }, + { text: `4 ${giant}` }, + { text: `5 ${giant}` }, + { text: `6 ${big}` }, + { text: `7 ${big}` }, + { text: `8 ${big}` }, + ], + }, + ]; + const result = truncateGenAiMessages(messages); + expect(result).toStrictEqual([ + { + parts: [{ text: `1 ${big}` }, { some_other_field: 'ok' }, { text: `2 ${big}` }, { text: `3 ${big}` }], + }, + ]); + }); + + it('truncates first message if none fit', () => { + const messages = [{ content: `1 ${humongous}` }, { content: `2 ${humongous}` }, { content: `3 ${humongous}` }]; + const result = truncateGenAiMessages(messages); + const truncLen = 20_000 - JSON.stringify({ content: '' }).length; + expect(result).toStrictEqual([{ content: `3 ${humongous}`.substring(0, truncLen) }]); + }); + + it('drops if first message cannot be safely truncated', () => { + const messages = [ + { content: `1 ${humongous}` }, + { content: `2 ${humongous}` }, + { what_even_is_this: `? ${humongous}` }, + ]; + const result = truncateGenAiMessages(messages); + expect(result).toStrictEqual([]); + }); + }); +}); From 356776c13e338af6599042b0b453924ceb216620 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:51:03 +0100 Subject: [PATCH 26/43] chore(deps): bump next from 14.2.32 to 14.2.35 in /dev-packages/e2e-tests/test-applications/nextjs-pages-dir (#18496) Bumps [next](https://github.com/vercel/next.js) from 14.2.32 to 14.2.35.
Release notes

Sourced from next's releases.

v14.2.35

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=14.2.32&new-version=14.2.35)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/nextjs-pages-dir/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json index 30da3eef99c1..eda574954224 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json @@ -20,7 +20,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "14.2.32", + "next": "14.2.35", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "~5.0.0" From f78aa3f754f18d6713882aeb8f2d90dc032451e7 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:13:11 +0100 Subject: [PATCH 27/43] fix(browser): Stringify span context in linked traces log statement (#18376) I came upon this log `Sentry Logger [log]: Adding previous_trace [object Object] link to span [object Object]` and this PR fixes this by stringifying the context. One concern I have with that is that the object could be too large (stringifying takes too long) or circular. But this should be very unlikely in this case. However, if someone else shares this concerns we might change the log to either limit the depth or to only log specific entries of the object (might add bundle size). Closes #18377 --- packages/browser/src/tracing/linkedTraces.ts | 4 +- .../browser/test/tracing/linkedTraces.test.ts | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/tracing/linkedTraces.ts b/packages/browser/src/tracing/linkedTraces.ts index 3a67e1cf61ac..4768ac7c66a1 100644 --- a/packages/browser/src/tracing/linkedTraces.ts +++ b/packages/browser/src/tracing/linkedTraces.ts @@ -176,10 +176,10 @@ export function addPreviousTraceSpanLink( if (Date.now() / 1000 - previousTraceInfo.startTimestamp <= PREVIOUS_TRACE_MAX_DURATION) { if (DEBUG_BUILD) { debug.log( - `Adding previous_trace ${previousTraceSpanCtx} link to span ${{ + `Adding previous_trace \`${JSON.stringify(previousTraceSpanCtx)}\` link to span \`${JSON.stringify({ op: spanJson.op, ...span.spanContext(), - }}`, + })}\``, ); } diff --git a/packages/browser/test/tracing/linkedTraces.test.ts b/packages/browser/test/tracing/linkedTraces.test.ts index 7c075da588ef..56616c6a7692 100644 --- a/packages/browser/test/tracing/linkedTraces.test.ts +++ b/packages/browser/test/tracing/linkedTraces.test.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { addChildSpanToSpan, SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; +import { addChildSpanToSpan, debug, SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../../src'; import type { PreviousTraceInfo } from '../../src/tracing/linkedTraces'; @@ -201,6 +201,47 @@ describe('addPreviousTraceSpanLink', () => { }); }); + it('logs a debug message when adding a previous trace link (with stringified context)', () => { + const debugLogSpy = vi.spyOn(debug, 'log'); + + const currentSpanStart = timestampInSeconds(); + + const previousTraceInfo: PreviousTraceInfo = { + spanContext: { traceId: '123', spanId: '456', traceFlags: 1 }, + startTimestamp: currentSpanStart - PREVIOUS_TRACE_MAX_DURATION + 1, + sampleRand: 0.0126, + sampleRate: 0.5, + }; + + const currentSpan = new SentrySpan({ + name: 'test', + op: 'navigation', + startTimestamp: currentSpanStart, + parentSpanId: '789', + spanId: 'abc', + traceId: 'def', + sampled: true, + }); + + const oldPropagationContext = { + sampleRand: 0.0126, + traceId: '123', + sampled: true, + dsc: { sample_rand: '0.0126', sample_rate: '0.5' }, + }; + + addPreviousTraceSpanLink(previousTraceInfo, currentSpan, oldPropagationContext); + + expect(debugLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('[object Object]')); + expect(debugLogSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'Adding previous_trace `{"traceId":"123","spanId":"456","traceFlags":1}` link to span `{"op":"navigation","spanId":"abc","traceId":"def","traceFlags":1}`', + ), + ); + + debugLogSpy.mockRestore(); + }); + it(`doesn't add a previous_trace span link if the previous trace was created more than ${PREVIOUS_TRACE_MAX_DURATION}s ago`, () => { const currentSpanStart = timestampInSeconds(); From 4132f4a62971fb420c771c7f6056c053ae8dbd82 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 15 Dec 2025 15:00:55 +0100 Subject: [PATCH 28/43] Working? --- packages/opentelemetry/src/propagator.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 7dc005521aa7..73c0629f6f0c 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -7,6 +7,7 @@ import { baggageHeaderToDynamicSamplingContext, debug, generateSentryTraceHeader, + generateTraceparentHeader, getClient, getCurrentScope, getDynamicSamplingContextFromScope, @@ -54,7 +55,7 @@ export class SentryPropagator extends W3CBaggagePropagator { const activeSpan = trace.getSpan(context); const url = activeSpan && getCurrentURL(activeSpan); - const tracePropagationTargets = getClient()?.getOptions()?.tracePropagationTargets; + const { tracePropagationTargets, propagateTraceparent } = getClient()?.getOptions() || {}; if (!shouldPropagateTraceForUrl(url, tracePropagationTargets, this._urlMatchesTargetsMap)) { DEBUG_BUILD && debug.log('[Tracing] Not injecting trace data for url because it does not match tracePropagationTargets:', url); @@ -85,6 +86,10 @@ export class SentryPropagator extends W3CBaggagePropagator { }, baggage); } + if (propagateTraceparent) { + setter.set(carrier, 'traceparent', generateTraceparentHeader(traceId, spanId, sampled)); + } + // We also want to avoid setting the default OTEL trace ID, if we get that for whatever reason if (traceId && traceId !== INVALID_TRACEID) { setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled)); From d1b86bd64b25435413ad62476b605d484e4337dc Mon Sep 17 00:00:00 2001 From: Tim Beeren <36151761+TBeeren@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:10:42 +0100 Subject: [PATCH 29/43] feat(browser): Add support for GraphQL persisted operations (#18505) Closes: #18499 --------- Co-authored-by: tbeeren Co-authored-by: Claude Sonnet 4.5 Co-authored-by: Andrei Borza --- CHANGELOG.md | 25 +++ .../integrations/graphqlClient/fetch/test.ts | 3 +- .../suites/integrations/graphqlClient/init.js | 2 +- .../persistedQuery-fetch/subject.js | 19 ++ .../persistedQuery-fetch/test.ts | 102 ++++++++++ .../persistedQuery-xhr/subject.js | 18 ++ .../graphqlClient/persistedQuery-xhr/test.ts | 98 ++++++++++ .../integrations/graphqlClient/xhr/test.ts | 3 +- .../browser/src/integrations/graphqlClient.ts | 109 +++++++++-- .../test/integrations/graphqlClient.test.ts | 174 +++++++++++++++++- 10 files changed, 527 insertions(+), 26 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c903d7df829..14a4f2bc7e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ ## Unreleased +### Important Changes + +- **feat(browser): Add support for GraphQL persisted operations ([#18505](https://github.com/getsentry/sentry-javascript/pull/18505))** + +The `graphqlClientIntegration` now supports GraphQL persisted operations (queries). When a persisted query is detected, the integration will capture the operation hash and version as span attributes: + +- `graphql.persisted_query.hash.sha256` - The SHA-256 hash of the persisted query +- `graphql.persisted_query.version` - The version of the persisted query protocol + +Additionally, the `graphql.document` attribute format has changed to align with OpenTelemetry semantic conventions. It now contains only the GraphQL query string instead of the full JSON request payload. + +**Before:** + +```javascript +"graphql.document": "{\"query\":\"query Test { user { id } }\"}" +``` + +**After:** + +```javascript +"graphql.document": "query Test { user { id } }" +``` + +### Other Changes + - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott Work in this release was contributed by @sebws. Thank you for your contribution! diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts index f3fd78bc0b94..db3758706737 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts @@ -10,7 +10,6 @@ const query = `query Test{ pet } }`; -const queryPayload = JSON.stringify({ query }); sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -55,7 +54,7 @@ sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTe 'server.address': 'sentry-test.io', 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.browser', - 'graphql.document': queryPayload, + 'graphql.document': query, }), }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js index ec5f5b76cd44..ef8d5fa541e4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js @@ -9,7 +9,7 @@ Sentry.init({ integrations: [ Sentry.browserTracingIntegration(), graphqlClientIntegration({ - endpoints: ['http://sentry-test.io/foo'], + endpoints: ['http://sentry-test.io/foo', 'http://sentry-test.io/graphql'], }), ], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/subject.js b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/subject.js new file mode 100644 index 000000000000..f6da78b0abd6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/subject.js @@ -0,0 +1,19 @@ +const requestBody = JSON.stringify({ + operationName: 'GetUser', + variables: { id: '123' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + }, + }, +}); + +fetch('http://sentry-test.io/graphql', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: requestBody, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts new file mode 100644 index 000000000000..14003c708574 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts @@ -0,0 +1,102 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should update spans for GraphQL persisted query fetch requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/graphql', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + data: { + user: { + id: '123', + name: 'Test User', + }, + }, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(1); + + expect(requestSpans![0]).toMatchObject({ + description: 'POST http://sentry-test.io/graphql (persisted GetUser)', + parent_span_id: eventData.contexts?.trace?.span_id, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: eventData.contexts?.trace?.trace_id, + status: 'ok', + data: expect.objectContaining({ + type: 'fetch', + 'http.method': 'POST', + 'http.url': 'http://sentry-test.io/graphql', + url: 'http://sentry-test.io/graphql', + 'server.address': 'sentry-test.io', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + 'graphql.persisted_query.version': 1, + }), + }); +}); + +sentryTest( + 'should update breadcrumbs for GraphQL persisted query fetch requests', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/graphql', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + data: { + user: { + id: '123', + name: 'Test User', + }, + }, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData?.breadcrumbs?.length).toBe(1); + + expect(eventData.breadcrumbs![0]).toEqual({ + timestamp: expect.any(Number), + category: 'fetch', + type: 'http', + data: { + method: 'POST', + status_code: 200, + url: 'http://sentry-test.io/graphql', + __span: expect.any(String), + 'graphql.operation': 'persisted GetUser', + 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + 'graphql.persisted_query.version': 1, + }, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/subject.js b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/subject.js new file mode 100644 index 000000000000..7468bd27a244 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/subject.js @@ -0,0 +1,18 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('POST', 'http://sentry-test.io/graphql'); +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); + +const requestBody = JSON.stringify({ + operationName: 'GetUser', + variables: { id: '123' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + }, + }, +}); + +xhr.send(requestBody); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts new file mode 100644 index 000000000000..ad62475fa841 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts @@ -0,0 +1,98 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should update spans for GraphQL persisted query XHR requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/graphql', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + data: { + user: { + id: '123', + name: 'Test User', + }, + }, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(1); + + expect(requestSpans![0]).toMatchObject({ + description: 'POST http://sentry-test.io/graphql (persisted GetUser)', + parent_span_id: eventData.contexts?.trace?.span_id, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: eventData.contexts?.trace?.trace_id, + status: 'ok', + data: { + type: 'xhr', + 'http.method': 'POST', + 'http.url': 'http://sentry-test.io/graphql', + url: 'http://sentry-test.io/graphql', + 'server.address': 'sentry-test.io', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + 'graphql.persisted_query.version': 1, + }, + }); +}); + +sentryTest('should update breadcrumbs for GraphQL persisted query XHR requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/graphql', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + data: { + user: { + id: '123', + name: 'Test User', + }, + }, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData?.breadcrumbs?.length).toBe(1); + + expect(eventData.breadcrumbs![0]).toEqual({ + timestamp: expect.any(Number), + category: 'xhr', + type: 'http', + data: { + method: 'POST', + status_code: 200, + url: 'http://sentry-test.io/graphql', + 'graphql.operation': 'persisted GetUser', + 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + 'graphql.persisted_query.version': 1, + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts index ca9704cc48fe..3f0a37771c66 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts @@ -10,7 +10,6 @@ const query = `query Test{ pet } }`; -const queryPayload = JSON.stringify({ query }); sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -55,7 +54,7 @@ sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTest 'server.address': 'sentry-test.io', 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.browser', - 'graphql.document': queryPayload, + 'graphql.document': query, }, }); }); diff --git a/packages/browser/src/integrations/graphqlClient.ts b/packages/browser/src/integrations/graphqlClient.ts index a467a4a70ff4..d256fa6b72e1 100644 --- a/packages/browser/src/integrations/graphqlClient.ts +++ b/packages/browser/src/integrations/graphqlClient.ts @@ -16,13 +16,27 @@ interface GraphQLClientOptions { } /** Standard graphql request shape: https://graphql.org/learn/serving-over-http/#post-request-and-body */ -interface GraphQLRequestPayload { +interface GraphQLStandardRequest { query: string; operationName?: string; variables?: Record; extensions?: Record; } +/** Persisted operation request */ +interface GraphQLPersistedRequest { + operationName: string; + variables?: Record; + extensions: { + persistedQuery: { + version: number; + sha256Hash: string; + }; + } & Record; +} + +type GraphQLRequestPayload = GraphQLStandardRequest | GraphQLPersistedRequest; + interface GraphQLOperation { operationType?: string; operationName?: string; @@ -33,7 +47,7 @@ const INTEGRATION_NAME = 'GraphQLClient'; const _graphqlClientIntegration = ((options: GraphQLClientOptions) => { return { name: INTEGRATION_NAME, - setup(client) { + setup(client: Client) { _updateSpanWithGraphQLData(client, options); _updateBreadcrumbWithGraphQLData(client, options); }, @@ -70,7 +84,17 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption if (graphqlBody) { const operationInfo = _getGraphQLOperation(graphqlBody); span.updateName(`${httpMethod} ${httpUrl} (${operationInfo})`); - span.setAttribute('graphql.document', payload); + + // Handle standard requests - always capture the query document + if (isStandardRequest(graphqlBody)) { + span.setAttribute('graphql.document', graphqlBody.query); + } + + // Handle persisted operations - capture hash for debugging + if (isPersistedRequest(graphqlBody)) { + span.setAttribute('graphql.persisted_query.hash.sha256', graphqlBody.extensions.persistedQuery.sha256Hash); + span.setAttribute('graphql.persisted_query.version', graphqlBody.extensions.persistedQuery.version); + } } } }); @@ -96,8 +120,17 @@ function _updateBreadcrumbWithGraphQLData(client: Client, options: GraphQLClient if (!data.graphql && graphqlBody) { const operationInfo = _getGraphQLOperation(graphqlBody); - data['graphql.document'] = graphqlBody.query; + data['graphql.operation'] = operationInfo; + + if (isStandardRequest(graphqlBody)) { + data['graphql.document'] = graphqlBody.query; + } + + if (isPersistedRequest(graphqlBody)) { + data['graphql.persisted_query.hash.sha256'] = graphqlBody.extensions.persistedQuery.sha256Hash; + data['graphql.persisted_query.version'] = graphqlBody.extensions.persistedQuery.version; + } } } } @@ -106,15 +139,24 @@ function _updateBreadcrumbWithGraphQLData(client: Client, options: GraphQLClient /** * @param requestBody - GraphQL request - * @returns A formatted version of the request: 'TYPE NAME' or 'TYPE' + * @returns A formatted version of the request: 'TYPE NAME' or 'TYPE' or 'persisted NAME' */ -function _getGraphQLOperation(requestBody: GraphQLRequestPayload): string { - const { query: graphqlQuery, operationName: graphqlOperationName } = requestBody; +export function _getGraphQLOperation(requestBody: GraphQLRequestPayload): string { + // Handle persisted operations + if (isPersistedRequest(requestBody)) { + return `persisted ${requestBody.operationName}`; + } - const { operationName = graphqlOperationName, operationType } = parseGraphQLQuery(graphqlQuery); - const operationInfo = operationName ? `${operationType} ${operationName}` : `${operationType}`; + // Handle standard GraphQL requests + if (isStandardRequest(requestBody)) { + const { query: graphqlQuery, operationName: graphqlOperationName } = requestBody; + const { operationName = graphqlOperationName, operationType } = parseGraphQLQuery(graphqlQuery); + const operationInfo = operationName ? `${operationType} ${operationName}` : `${operationType}`; + return operationInfo; + } - return operationInfo; + // Fallback for unknown request types + return 'unknown'; } /** @@ -168,6 +210,34 @@ export function parseGraphQLQuery(query: string): GraphQLOperation { }; } +/** + * Helper to safely check if a value is a non-null object + */ +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +/** + * Type guard to check if a request is a standard GraphQL request + */ +function isStandardRequest(payload: unknown): payload is GraphQLStandardRequest { + return isObject(payload) && typeof payload.query === 'string'; +} + +/** + * Type guard to check if a request is a persisted operation request + */ +function isPersistedRequest(payload: unknown): payload is GraphQLPersistedRequest { + return ( + isObject(payload) && + typeof payload.operationName === 'string' && + isObject(payload.extensions) && + isObject(payload.extensions.persistedQuery) && + typeof payload.extensions.persistedQuery.sha256Hash === 'string' && + typeof payload.extensions.persistedQuery.version === 'number' + ); +} + /** * Extract the payload of a request if it's GraphQL. * Exported for tests only. @@ -175,20 +245,19 @@ export function parseGraphQLQuery(query: string): GraphQLOperation { * @returns A POJO or undefined */ export function getGraphQLRequestPayload(payload: string): GraphQLRequestPayload | undefined { - let graphqlBody = undefined; try { - const requestBody = JSON.parse(payload) satisfies GraphQLRequestPayload; + const requestBody = JSON.parse(payload); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const isGraphQLRequest = !!requestBody['query']; - if (isGraphQLRequest) { - graphqlBody = requestBody; + // Return any valid GraphQL request (standard, persisted, or APQ retry with both) + if (isStandardRequest(requestBody) || isPersistedRequest(requestBody)) { + return requestBody; } - } finally { - // Fallback to undefined if payload is an invalid JSON (SyntaxError) - /* eslint-disable no-unsafe-finally */ - return graphqlBody; + // Not a GraphQL request + return undefined; + } catch { + // Invalid JSON + return undefined; } } diff --git a/packages/browser/test/integrations/graphqlClient.test.ts b/packages/browser/test/integrations/graphqlClient.test.ts index 7a647b776e69..a90b62ee13d5 100644 --- a/packages/browser/test/integrations/graphqlClient.test.ts +++ b/packages/browser/test/integrations/graphqlClient.test.ts @@ -6,6 +6,7 @@ import type { FetchHint, XhrHint } from '@sentry-internal/browser-utils'; import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; import { describe, expect, test } from 'vitest'; import { + _getGraphQLOperation, getGraphQLRequestPayload, getRequestPayloadXhrOrFetch, parseGraphQLQuery, @@ -57,7 +58,8 @@ describe('GraphqlClient', () => { expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toBeUndefined(); }); - test('should return the payload object for GraphQL request', () => { + + test('should return the payload object for standard GraphQL request', () => { const requestBody = { query: 'query Test {\r\n items {\r\n id\r\n }\r\n }', operationName: 'Test', @@ -67,6 +69,89 @@ describe('GraphqlClient', () => { expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toEqual(requestBody); }); + + test('should return the payload object for persisted operation request', () => { + const requestBody = { + operationName: 'GetUser', + variables: { id: '123' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'abc123def456...', + }, + }, + }; + + expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toEqual(requestBody); + }); + + test('should return undefined for persisted operation without operationName', () => { + const requestBody = { + variables: { id: '123' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'abc123def456...', + }, + }, + }; + + expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toBeUndefined(); + }); + + test('should return undefined for request with extensions but no persistedQuery', () => { + const requestBody = { + operationName: 'GetUser', + variables: { id: '123' }, + extensions: { + someOtherExtension: true, + }, + }; + + expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toBeUndefined(); + }); + + test('should return undefined for persisted operation with incomplete persistedQuery object', () => { + const requestBody = { + operationName: 'GetUser', + variables: { id: '123' }, + extensions: { + persistedQuery: {}, + }, + }; + + expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toBeUndefined(); + }); + + test('should return undefined for persisted operation missing sha256Hash', () => { + const requestBody = { + operationName: 'GetUser', + extensions: { + persistedQuery: { + version: 1, + }, + }, + }; + + expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toBeUndefined(); + }); + + test('should return undefined for persisted operation missing version', () => { + const requestBody = { + operationName: 'GetUser', + extensions: { + persistedQuery: { + sha256Hash: 'abc123', + }, + }, + }; + + expect(getGraphQLRequestPayload(JSON.stringify(requestBody))).toBeUndefined(); + }); + + test('should return undefined for invalid JSON', () => { + expect(getGraphQLRequestPayload('not valid json {')).toBeUndefined(); + }); }); describe('getRequestPayloadXhrOrFetch', () => { @@ -136,4 +221,91 @@ describe('GraphqlClient', () => { expect(result).toBeUndefined(); }); }); + + describe('_getGraphQLOperation', () => { + test('should format standard GraphQL query with operation name', () => { + const requestBody = { + query: 'query GetUser { user { id } }', + operationName: 'GetUser', + }; + + expect(_getGraphQLOperation(requestBody)).toBe('query GetUser'); + }); + + test('should format standard GraphQL mutation with operation name', () => { + const requestBody = { + query: 'mutation CreateUser($input: UserInput!) { createUser(input: $input) { id } }', + operationName: 'CreateUser', + }; + + expect(_getGraphQLOperation(requestBody)).toBe('mutation CreateUser'); + }); + + test('should format standard GraphQL subscription with operation name', () => { + const requestBody = { + query: 'subscription OnUserCreated { userCreated { id } }', + operationName: 'OnUserCreated', + }; + + expect(_getGraphQLOperation(requestBody)).toBe('subscription OnUserCreated'); + }); + + test('should format standard GraphQL query without operation name', () => { + const requestBody = { + query: 'query { users { id } }', + }; + + expect(_getGraphQLOperation(requestBody)).toBe('query'); + }); + + test('should use query operation name when provided in request body', () => { + const requestBody = { + query: 'query { users { id } }', + operationName: 'GetAllUsers', + }; + + expect(_getGraphQLOperation(requestBody)).toBe('query GetAllUsers'); + }); + + test('should format persisted operation request', () => { + const requestBody = { + operationName: 'GetUser', + variables: { id: '123' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'abc123def456', + }, + }, + }; + + expect(_getGraphQLOperation(requestBody)).toBe('persisted GetUser'); + }); + + test('should handle persisted operation with additional extensions', () => { + const requestBody = { + operationName: 'GetUser', + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'abc123def456', + }, + tracing: true, + customExtension: 'value', + }, + }; + + expect(_getGraphQLOperation(requestBody)).toBe('persisted GetUser'); + }); + + test('should return "unknown" for unrecognized request format', () => { + const requestBody = { + variables: { id: '123' }, + }; + + // This shouldn't happen in practice since getGraphQLRequestPayload filters, + // but test the fallback behavior + expect(_getGraphQLOperation(requestBody as any)).toBe('unknown'); + }); + }); }); From a8490c9e99cf07b4c6fcf2e8864a63577c7c0e10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:45:30 +0100 Subject: [PATCH 30/43] chore(deps): bump next from 14.2.25 to 14.2.35 in /dev-packages/e2e-tests/test-applications/create-next-app (#18494) Bumps [next](https://github.com/vercel/next.js) from 14.2.25 to 14.2.35.
Release notes

Sourced from next's releases.

v14.2.35

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=14.2.25&new-version=14.2.35)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/create-next-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json index 3a17f479cd16..b484016785b9 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json @@ -16,7 +16,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "14.2.25", + "next": "14.2.35", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "~5.0.0" From 51b1b89261730d0924a2f83ac58e7dd8a478770a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 15 Dec 2025 17:59:00 +0100 Subject: [PATCH 31/43] Fix --- .../suites/tracing/requests/traceparent/scenario-http.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs index 35805293a797..3d085780d218 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs @@ -16,6 +16,6 @@ function makeHttpRequest(url) { }); } -await Sentry.startSpan({ name: 'outer' }, async () => { +Sentry.startSpan({ name: 'outer' }, async () => { await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); }); From 4134dabf7029cd1d3158db269cd9dcb2b677213a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 15 Dec 2025 18:06:39 +0100 Subject: [PATCH 32/43] Improve logic --- packages/opentelemetry/src/propagator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 73c0629f6f0c..e32f8cd75ce9 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -86,13 +86,13 @@ export class SentryPropagator extends W3CBaggagePropagator { }, baggage); } - if (propagateTraceparent) { - setter.set(carrier, 'traceparent', generateTraceparentHeader(traceId, spanId, sampled)); - } - // We also want to avoid setting the default OTEL trace ID, if we get that for whatever reason if (traceId && traceId !== INVALID_TRACEID) { setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled)); + + if (propagateTraceparent) { + setter.set(carrier, 'traceparent', generateTraceparentHeader(traceId, spanId, sampled)); + } } super.inject(propagation.setBaggage(context, baggage), carrier, setter); From 3cd6b367f40cbe842542f8ac4d6dfdc4620d87c0 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 15 Dec 2025 18:59:39 +0100 Subject: [PATCH 33/43] Include in fields --- packages/opentelemetry/src/propagator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index e32f8cd75ce9..458fc7f2ac94 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -120,7 +120,7 @@ export class SentryPropagator extends W3CBaggagePropagator { * @inheritDoc */ public fields(): string[] { - return [SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]; + return [SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER, 'traceparent']; } } From 8ce9ede0c0ce418d3b79256be15c87c275490063 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 15 Dec 2025 19:44:36 +0100 Subject: [PATCH 34/43] Oops --- packages/opentelemetry/test/propagator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index 46d4be5d8275..ef8223b5974a 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -35,7 +35,7 @@ describe('SentryPropagator', () => { }); it('returns fields set', () => { - expect(propagator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]); + expect(propagator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER, 'traceparent']); }); describe('inject', () => { From c73437f36f8d8914eeb72edc9223fe849a51005f Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:55:37 +0100 Subject: [PATCH 35/43] ref(browser): Improve profiling debug statement (#18507) Small log improvement as the log message was not quite clear. Before, it read like it's related to the whole session, but this log is about each span. Closes #18508 (added automatically) --- packages/browser/src/profiling/UIProfiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/profiling/UIProfiler.ts b/packages/browser/src/profiling/UIProfiler.ts index fc735e871e29..89edb4899a6a 100644 --- a/packages/browser/src/profiling/UIProfiler.ts +++ b/packages/browser/src/profiling/UIProfiler.ts @@ -199,7 +199,8 @@ export class UIProfiler implements ContinuousProfiler { private _setupTraceLifecycleListeners(client: Client): void { client.on('spanStart', span => { if (!this._sessionSampled) { - DEBUG_BUILD && debug.log('[Profiling] Session not sampled because of negative sampling decision.'); + DEBUG_BUILD && + debug.log('[Profiling] Span not profiled because of negative sampling decision for user session.'); return; } if (span !== getRootSpan(span)) { From 861ed3fd9fc2d08137e1f2548eae3dc5dbb6a6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 16 Dec 2025 11:09:54 +0100 Subject: [PATCH 36/43] fix(google-cloud-serverless): Move @types/express to optional peerDeps (#18452) (closes #18438) (closes [JS-1273](https://linear.app/getsentry/issue/JS-1273/bump-typesexpress-to-v5-for-sentrygoogle-cloud-serverless)) This moves the express types as optional peerDependencies. Since we are only relying on `Request` and `Response` and nothing more, this should be a save update. In order to also test against this - I updated the the local types to v5. The pattern of having the types as optional peerDependencies is already given for our Cloudflare SDK: https://github.com/getsentry/sentry-javascript/blob/2ef3938fecf872b3d09006538484e5de97123ac5/packages/cloudflare/package.json#L55-L62 --- packages/google-cloud-serverless/package.json | 12 ++++- yarn.lock | 51 ++++++++++++++----- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index f1dabb7901bd..0b603321fc39 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -49,15 +49,23 @@ }, "dependencies": { "@sentry/core": "10.30.0", - "@sentry/node": "10.30.0", - "@types/express": "^4.17.14" + "@sentry/node": "10.30.0" }, "devDependencies": { "@google-cloud/bigquery": "^5.3.0", "@google-cloud/common": "^3.4.1", + "@types/express": "^5.0.6", "@types/node": "^18.19.1", "nock": "^13.5.5" }, + "peerDependencies": { + "@types/express": "^4.17.14 || ^5.x" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + }, "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", diff --git a/yarn.lock b/yarn.lock index 324b3a726ba4..b26b3f069801 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8597,10 +8597,10 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18", "@types/express-serve-static-core@^4.17.33": - version "4.17.43" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" - integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b" + integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA== dependencies: "@types/node" "*" "@types/qs" "*" @@ -8616,15 +8616,24 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.2": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== +"@types/express-serve-static-core@^4.17.18", "@types/express-serve-static-core@^4.17.33": + version "4.17.43" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" + integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" + "@types/node" "*" "@types/qs" "*" - "@types/serve-static" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^5.0.6": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc" + integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "^2" "@types/express@4.17.14": version "4.17.14" @@ -8636,6 +8645,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.2": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@^5.0.5": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" @@ -9054,7 +9073,15 @@ dependencies: "@types/express" "*" -"@types/serve-static@*", "@types/serve-static@^1.13.10": +"@types/serve-static@*", "@types/serve-static@^2": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-2.2.0.tgz#d4a447503ead0d1671132d1ab6bd58b805d8de6a" + integrity sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + +"@types/serve-static@^1.13.10": version "1.15.5" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033" integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== From 0cd1d4a4abb328143b271e7ad963f116ecef66e2 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 16 Dec 2025 10:29:43 +0000 Subject: [PATCH 37/43] feat(node): Add ESM support for postgres.js instrumentation (#17961) Rewrite the `postgresjs` instrumentation with a new architecture: - Added ESM support via `replaceExports` - Moved to main export wrapping instead of internal module patching - Previously, we were patching `connection.js` and `query.js` internal modules - New approach: We are wrapping the main postgres module export to intercept sql instance creation - Connection context is now stored directly on sql instances using `CONNECTION_CONTEXT_SYMBOL` - `Query.prototype` fallback (CJS only) - Patches `Query.prototype.handle` as a fallback for pre-existing sql instances - Uses `QUERY_FROM_INSTRUMENTED_SQL` marker to prevent duplicate spans Also, - Improved SQL sanitization - `port` attribute is now stored as a number per OTEL semantic conventions - Added fallback regex extraction for operation name when `command` isn't available --- .size-limit.js | 2 +- .../postgresjs/instrument-requestHook.cjs | 25 + .../postgresjs/instrument-requestHook.mjs | 25 + .../suites/tracing/postgresjs/instrument.cjs | 9 + .../suites/tracing/postgresjs/instrument.mjs | 9 + .../postgresjs/scenario-requestHook.js | 39 + .../postgresjs/scenario-requestHook.mjs | 39 + .../tracing/postgresjs/scenario-unsafe.cjs | 46 + .../tracing/postgresjs/scenario-unsafe.mjs | 36 + .../tracing/postgresjs/scenario-url.cjs | 63 ++ .../tracing/postgresjs/scenario-url.mjs | 74 ++ .../suites/tracing/postgresjs/scenario.cjs | 62 ++ .../suites/tracing/postgresjs/scenario.js | 21 + .../suites/tracing/postgresjs/scenario.mjs | 73 ++ .../suites/tracing/postgresjs/test.ts | 799 +++++++++++++++++- .../src/integrations/tracing/postgresjs.ts | 748 ++++++++++++---- .../integrations/tracing/postgresjs.test.ts | 411 +++++++++ 17 files changed, 2271 insertions(+), 210 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.cjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.cjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.js create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.cjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.cjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.cjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.mjs create mode 100644 packages/node/test/integrations/tracing/postgresjs.test.ts diff --git a/.size-limit.js b/.size-limit.js index 00b4bdbfd4d8..880f91cbeb54 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -240,7 +240,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '161 KB', + limit: '162 KB', }, { name: '@sentry/node - without tracing', diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.cjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.cjs new file mode 100644 index 000000000000..0cf4c6185ef3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.cjs @@ -0,0 +1,25 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [ + Sentry.postgresJsIntegration({ + requestHook: (span, sanitizedSqlQuery, connectionContext) => { + // Add custom attributes to demonstrate requestHook functionality + span.setAttribute('custom.requestHook', 'called'); + + // Set context information as extras for test validation + Sentry.setExtra('requestHookCalled', { + sanitizedQuery: sanitizedSqlQuery, + database: connectionContext?.ATTR_DB_NAMESPACE, + host: connectionContext?.ATTR_SERVER_ADDRESS, + port: connectionContext?.ATTR_SERVER_PORT, + }); + }, + }), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.mjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.mjs new file mode 100644 index 000000000000..885c6198100b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument-requestHook.mjs @@ -0,0 +1,25 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [ + Sentry.postgresJsIntegration({ + requestHook: (span, sanitizedSqlQuery, connectionContext) => { + // Add custom attributes to demonstrate requestHook functionality + span.setAttribute('custom.requestHook', 'called'); + + // Set context information as extras for test validation + Sentry.setExtra('requestHookCalled', { + sanitizedQuery: sanitizedSqlQuery, + database: connectionContext?.ATTR_DB_NAMESPACE, + host: connectionContext?.ATTR_SERVER_ADDRESS, + port: connectionContext?.ATTR_SERVER_PORT, + }); + }, + }), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.cjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.cjs new file mode 100644 index 000000000000..6aec5f1f9384 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.cjs @@ -0,0 +1,9 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.mjs new file mode 100644 index 000000000000..46a27dd03b74 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.js b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.js new file mode 100644 index 000000000000..a2b405d71f60 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.js @@ -0,0 +1,39 @@ +const Sentry = require('@sentry/node'); +const postgres = require('postgres'); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await sql` + CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id")); + `; + + await sql` + INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com'); + `; + + await sql` + SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + await sql` + DROP TABLE "User"; + `; + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.mjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.mjs new file mode 100644 index 000000000000..f6e69354ccbc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-requestHook.mjs @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/node'; +import postgres from 'postgres'; + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await sql` + CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id")); + `; + + await sql` + INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com'); + `; + + await sql` + SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + await sql` + DROP TABLE "User"; + `; + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.cjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.cjs new file mode 100644 index 000000000000..0ee537052a4a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.cjs @@ -0,0 +1,46 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Import postgres AFTER Sentry.init() so instrumentation is set up +const postgres = require('postgres'); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +// Test with plain object options +const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + // Test sql.unsafe() - this was not being instrumented before the fix + await sql.unsafe('CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))'); + + await sql.unsafe('INSERT INTO "User" ("email") VALUES ($1)', ['test@example.com']); + + await sql.unsafe('SELECT * FROM "User" WHERE "email" = $1', ['test@example.com']); + + await sql.unsafe('DROP TABLE "User"'); + + // This will be captured as an error as the table no longer exists + await sql.unsafe('SELECT * FROM "User"'); + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.mjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.mjs new file mode 100644 index 000000000000..9d2e7de99e51 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-unsafe.mjs @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node'; +import postgres from 'postgres'; + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +// Test with plain object options +const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + // Test sql.unsafe() - this was not being instrumented before the fix + await sql.unsafe('CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))'); + + await sql.unsafe('INSERT INTO "User" ("email") VALUES ($1)', ['test@example.com']); + + await sql.unsafe('SELECT * FROM "User" WHERE "email" = $1', ['test@example.com']); + + await sql.unsafe('DROP TABLE "User"'); + + // This will be captured as an error as the table no longer exists + await sql.unsafe('SELECT * FROM "User"'); + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.cjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.cjs new file mode 100644 index 000000000000..1a5cc93e2261 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.cjs @@ -0,0 +1,63 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Import postgres AFTER Sentry.init() so instrumentation is set up +const postgres = require('postgres'); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +// Test URL-based initialization - this is the common pattern that was causing the regression +const sql = postgres('postgres://test:test@localhost:5444/test_db'); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await sql` + CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id")); + `; + + await sql` + INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com'); + `; + + await sql` + UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com'; + `; + + await sql` + SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => { + await Promise.all(rows); + }); + + await sql` + DROP TABLE "User"; + `; + + // This will be captured as an error as the table no longer exists + await sql` + SELECT * FROM "User" WHERE "email" = 'foo@baz.com'; + `; + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.mjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.mjs new file mode 100644 index 000000000000..2694bca96569 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario-url.mjs @@ -0,0 +1,74 @@ +import * as Sentry from '@sentry/node'; +import postgres from 'postgres'; + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +// Test URL-based initialization - this is the common pattern that was causing the regression +const sql = postgres('postgres://test:test@localhost:5444/test_db'); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await sql` + CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id")); + `; + + await sql` + INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com'); + `; + + await sql` + UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com'; + `; + + await sql` + SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + // Test parameterized queries + await sql` + SELECT * FROM "User" WHERE "email" = ${'bar@baz.com'} AND "name" = ${'Foo'}; + `; + + // Test DELETE operation + await sql` + DELETE FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + // Test INSERT with RETURNING + await sql` + INSERT INTO "User" ("email", "name") VALUES ('test@example.com', 'Test User') RETURNING *; + `; + + // Test cursor-based queries + await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => { + await Promise.all(rows); + }); + + // Test multiple rows at once + await sql` + SELECT * FROM "User" LIMIT 10; + `; + + await sql` + DROP TABLE "User"; + `; + + // This will be captured as an error as the table no longer exists + await sql` + SELECT * FROM "User" WHERE "email" = 'foo@baz.com'; + `; + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.cjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.cjs new file mode 100644 index 000000000000..d19b412dcbec --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.cjs @@ -0,0 +1,62 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Import postgres AFTER Sentry.init() so instrumentation is set up +const postgres = require('postgres'); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await sql` + CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id")); + `; + + await sql` + INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com'); + `; + + await sql` + UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com'; + `; + + await sql` + SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => { + await Promise.all(rows); + }); + + await sql` + DROP TABLE "User"; + `; + + // This will be captured as an error as the table no longer exists + await sql` + SELECT * FROM "User" WHERE "email" = 'foo@baz.com'; + `; + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js index f1010281f904..d9049353f6eb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js @@ -39,10 +39,31 @@ async function run() { SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; `; + // Test parameterized queries + await sql` + SELECT * FROM "User" WHERE "email" = ${'bar@baz.com'} AND "name" = ${'Foo'}; + `; + + // Test DELETE operation + await sql` + DELETE FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + // Test INSERT with RETURNING + await sql` + INSERT INTO "User" ("email", "name") VALUES ('test@example.com', 'Test User') RETURNING *; + `; + + // Test cursor-based queries await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => { await Promise.all(rows); }); + // Test multiple rows at once + await sql` + SELECT * FROM "User" LIMIT 10; + `; + await sql` DROP TABLE "User"; `; diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.mjs new file mode 100644 index 000000000000..7d62c8d52dde --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.mjs @@ -0,0 +1,73 @@ +import * as Sentry from '@sentry/node'; +import postgres from 'postgres'; + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await sql` + CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id")); + `; + + await sql` + INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com'); + `; + + await sql` + UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com'; + `; + + await sql` + SELECT * FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + // Test parameterized queries + await sql` + SELECT * FROM "User" WHERE "email" = ${'bar@baz.com'} AND "name" = ${'Foo'}; + `; + + // Test DELETE operation + await sql` + DELETE FROM "User" WHERE "email" = 'bar@baz.com'; + `; + + // Test INSERT with RETURNING + await sql` + INSERT INTO "User" ("email", "name") VALUES ('test@example.com', 'Test User') RETURNING *; + `; + + // Test cursor-based queries + await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => { + await Promise.all(rows); + }); + + // Test multiple rows at once + await sql` + SELECT * FROM "User" LIMIT 10; + `; + + await sql` + DROP TABLE "User"; + `; + + // This will be captured as an error as the table no longer exists + await sql` + SELECT * FROM "User" WHERE "email" = 'foo@baz.com'; + `; + } finally { + await sql.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts index 99203fd75ae6..2dfbc020966b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts @@ -1,11 +1,12 @@ -import { describe, expect, test } from 'vitest'; -import { createRunner } from '../../../utils/runner'; - -const EXISTING_TEST_EMAIL = 'bar@baz.com'; -const NON_EXISTING_TEST_EMAIL = 'foo@baz.com'; +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; describe('postgresjs auto instrumentation', () => { - test('should auto-instrument `postgres` package', { timeout: 60_000 }, async () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should auto-instrument `postgres` package (CJS)', { timeout: 60_000 }, async () => { const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', spans: expect.arrayContaining([ @@ -17,7 +18,7 @@ describe('postgresjs auto instrumentation', () => { 'db.query.text': 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), @@ -25,7 +26,136 @@ describe('postgresjs auto instrumentation', () => { 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + 'sentry.origin': 'auto.db.postgresjs', + 'sentry.op': 'db', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'UPDATE', + 'db.query.text': 'UPDATE "User" SET "name" = ? WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'UPDATE "User" SET "name" = ? WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + // Parameterized query test - verifies that tagged template queries with interpolations + // are properly reconstructed with $1, $2 placeholders which are PRESERVED per OTEL spec + // (PostgreSQL $n placeholders indicate parameterized queries that don't leak sensitive data) + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = $1 AND "name" = $2', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = $1 AND "name" = $2', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * from generate_series(?,?) as x', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * from generate_series(?,?) as x', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'DROP TABLE', + 'db.query.text': 'DROP TABLE "User"', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'DROP TABLE "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -37,18 +167,85 @@ describe('postgresjs auto instrumentation', () => { 'db.namespace': 'test_db', 'db.system.name': 'postgres', 'db.operation.name': 'SELECT', + 'db.response.status_code': '42P01', + 'error.type': 'PostgresError', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = ?', + op: 'db', + status: 'internal_error', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + ]), + }; + + const EXPECTED_ERROR_EVENT = { + event_id: expect.any(String), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + exception: { + values: [ + { + type: 'PostgresError', + value: 'relation "User" does not exist', + stacktrace: expect.objectContaining({ + frames: expect.arrayContaining([ + expect.objectContaining({ + function: 'handle', + module: 'postgres.cjs.src:connection', + filename: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }), + }, + ], + }, + }; + + await createRunner(__dirname, 'scenario.js') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ event: EXPECTED_ERROR_EVENT }) + .start() + .completed(); + }); + + test('should auto-instrument `postgres` package (ESM)', { timeout: 60_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', 'db.query.text': - "select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid", + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), description: - "select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid", + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -60,16 +257,16 @@ describe('postgresjs auto instrumentation', () => { 'db.namespace': 'test_db', 'db.system.name': 'postgres', 'db.operation.name': 'INSERT', - 'db.query.text': `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`, - 'sentry.origin': 'auto.db.otel.postgres', + 'db.query.text': 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + 'sentry.origin': 'auto.db.postgresjs', 'sentry.op': 'db', 'server.address': 'localhost', 'server.port': 5444, }), - description: `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`, + description: 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -81,37 +278,61 @@ describe('postgresjs auto instrumentation', () => { 'db.namespace': 'test_db', 'db.system.name': 'postgres', 'db.operation.name': 'UPDATE', - 'db.query.text': `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`, + 'db.query.text': 'UPDATE "User" SET "name" = ? WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'UPDATE "User" SET "name" = ? WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), - description: `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`, + description: 'SELECT * FROM "User" WHERE "email" = ?', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), timestamp: expect.any(Number), trace_id: expect.any(String), }), + // Parameterized query test - verifies that tagged template queries with interpolations + // are properly reconstructed with $1, $2 placeholders which are PRESERVED per OTEL spec + // (PostgreSQL $n placeholders indicate parameterized queries that don't leak sensitive data) expect.objectContaining({ data: expect.objectContaining({ 'db.namespace': 'test_db', 'db.system.name': 'postgres', 'db.operation.name': 'SELECT', - 'db.query.text': `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`, + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = $1 AND "name" = $2', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), - description: `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`, + description: 'SELECT * FROM "User" WHERE "email" = $1 AND "name" = $2', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -125,14 +346,14 @@ describe('postgresjs auto instrumentation', () => { 'db.operation.name': 'SELECT', 'db.query.text': 'SELECT * from generate_series(?,?) as x', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), description: 'SELECT * from generate_series(?,?) as x', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -146,14 +367,14 @@ describe('postgresjs auto instrumentation', () => { 'db.operation.name': 'DROP TABLE', 'db.query.text': 'DROP TABLE "User"', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), description: 'DROP TABLE "User"', op: 'db', status: 'ok', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -164,19 +385,19 @@ describe('postgresjs auto instrumentation', () => { data: expect.objectContaining({ 'db.namespace': 'test_db', 'db.system.name': 'postgres', - // No db.operation.name here, as this is an errored span + 'db.operation.name': 'SELECT', 'db.response.status_code': '42P01', 'error.type': 'PostgresError', - 'db.query.text': `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`, + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', 'sentry.op': 'db', - 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.origin': 'auto.db.postgresjs', 'server.address': 'localhost', 'server.port': 5444, }), - description: `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`, + description: 'SELECT * FROM "User" WHERE "email" = ?', op: 'db', status: 'internal_error', - origin: 'auto.db.otel.postgres', + origin: 'auto.db.postgresjs', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -203,7 +424,7 @@ describe('postgresjs auto instrumentation', () => { frames: expect.arrayContaining([ expect.objectContaining({ function: 'handle', - module: 'postgres.cjs.src:connection', + module: 'postgres.src:connection', filename: expect.any(String), lineno: expect.any(Number), colno: expect.any(Number), @@ -215,11 +436,519 @@ describe('postgresjs auto instrumentation', () => { }, }; - await createRunner(__dirname, 'scenario.js') + await createRunner(__dirname, 'scenario.mjs') + .withFlags('--import', `${__dirname}/instrument.mjs`) .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) .expect({ transaction: EXPECTED_TRANSACTION }) .expect({ event: EXPECTED_ERROR_EVENT }) .start() .completed(); }); + + test('should call requestHook when provided (CJS)', { timeout: 60_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', + 'db.query.text': + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'DROP TABLE', + 'db.query.text': 'DROP TABLE "User"', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'DROP TABLE "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + ]), + extra: expect.objectContaining({ + requestHookCalled: expect.objectContaining({ + database: 'test_db', + host: 'localhost', + port: '5444', + sanitizedQuery: expect.any(String), + }), + }), + }; + + await createRunner(__dirname, 'scenario-requestHook.js') + .withFlags('--require', `${__dirname}/instrument-requestHook.cjs`) + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); + + test('should call requestHook when provided (ESM)', { timeout: 60_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', + 'db.query.text': + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'DROP TABLE', + 'db.query.text': 'DROP TABLE "User"', + 'custom.requestHook': 'called', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'DROP TABLE "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + ]), + extra: expect.objectContaining({ + requestHookCalled: expect.objectContaining({ + database: 'test_db', + host: 'localhost', + port: '5444', + sanitizedQuery: expect.any(String), + }), + }), + }; + + await createRunner(__dirname, 'scenario-requestHook.mjs') + .withFlags('--import', `${__dirname}/instrument-requestHook.mjs`) + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); + + // Tests for URL-based initialization pattern (regression prevention) + test('should instrument postgres package with URL initialization (CJS)', { timeout: 90_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', + 'db.query.text': + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'UPDATE', + 'db.query.text': 'UPDATE "User" SET "name" = ? WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'UPDATE "User" SET "name" = ? WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + ]), + }; + + await createRunner(__dirname, 'scenario-url.cjs') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); + + test('should instrument postgres package with URL initialization (ESM)', { timeout: 90_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', + 'db.query.text': + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email", "name") VALUES (?, ?)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'DELETE', + 'db.query.text': 'DELETE FROM "User" WHERE "email" = ?', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'DELETE FROM "User" WHERE "email" = ?', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + ]), + }; + + await createRunner(__dirname, 'scenario-url.mjs') + .withFlags('--import', `${__dirname}/instrument.mjs`) + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); + + test('should instrument sql.unsafe() queries (CJS)', { timeout: 90_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', + 'db.query.text': 'CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + // sql.unsafe() with $1 placeholders - preserved per OTEL spec + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email") VALUES ($1)', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email") VALUES ($1)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = $1', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = $1', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'DROP TABLE', + 'db.query.text': 'DROP TABLE "User"', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'DROP TABLE "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + ]), + }; + + await createRunner(__dirname, 'scenario-unsafe.cjs') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); + + test('should instrument sql.unsafe() queries (ESM)', { timeout: 90_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'CREATE TABLE', + 'db.query.text': 'CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + // sql.unsafe() with $1 placeholders - preserved per OTEL spec + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'INSERT', + 'db.query.text': 'INSERT INTO "User" ("email") VALUES ($1)', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'INSERT INTO "User" ("email") VALUES ($1)', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'SELECT', + 'db.query.text': 'SELECT * FROM "User" WHERE "email" = $1', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'SELECT * FROM "User" WHERE "email" = $1', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.namespace': 'test_db', + 'db.system.name': 'postgres', + 'db.operation.name': 'DROP TABLE', + 'db.query.text': 'DROP TABLE "User"', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.postgresjs', + 'server.address': 'localhost', + 'server.port': 5444, + }), + description: 'DROP TABLE "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.postgresjs', + }), + ]), + }; + + await createRunner(__dirname, 'scenario-unsafe.mjs') + .withFlags('--import', `${__dirname}/instrument.mjs`) + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); }); diff --git a/packages/node/src/integrations/tracing/postgresjs.ts b/packages/node/src/integrations/tracing/postgresjs.ts index 438a63c804c6..55ee90444b47 100644 --- a/packages/node/src/integrations/tracing/postgresjs.ts +++ b/packages/node/src/integrations/tracing/postgresjs.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // Instrumentation for https://github.com/porsager/postgres import { context, trace } from '@opentelemetry/api'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; @@ -21,15 +22,17 @@ import type { IntegrationFn, Span } from '@sentry/core'; import { debug, defineIntegration, - getCurrentScope, + replaceExports, SDK_VERSION, SPAN_STATUS_ERROR, startSpanManual, } from '@sentry/core'; import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; +import { DEBUG_BUILD } from '../../debug-build'; const INTEGRATION_NAME = 'PostgresJs'; const SUPPORTED_VERSIONS = ['>=3.0.0 <4']; +const SQL_OPERATION_REGEX = /^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i; type PostgresConnectionContext = { ATTR_DB_NAMESPACE?: string; // Database name @@ -37,6 +40,12 @@ type PostgresConnectionContext = { ATTR_SERVER_PORT?: string; // Port number of the database server }; +const CONNECTION_CONTEXT_SYMBOL = Symbol('sentryPostgresConnectionContext'); +const INSTRUMENTED_MARKER = Symbol.for('sentry.instrumented.postgresjs'); +// Marker to track if a query was created from an instrumented sql instance +// This prevents double-spanning when both wrapper and prototype patches are active +const QUERY_FROM_INSTRUMENTED_SQL = Symbol.for('sentry.query.from.instrumented.sql'); + type PostgresJsInstrumentationConfig = InstrumentationConfig & { /** * Whether to require a parent span for the instrumentation. @@ -63,7 +72,9 @@ export const instrumentPostgresJs = generateInstrumentOnce( /** * Instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library. - * This instrumentation captures postgresjs queries and their attributes, + * This instrumentation captures postgresjs queries and their attributes. + * + * Uses internal Sentry patching patterns to support both CommonJS and ESM environments. */ export class PostgresJsInstrumentation extends InstrumentationBase { public constructor(config: PostgresJsInstrumentationConfig) { @@ -71,210 +82,443 @@ export class PostgresJsInstrumentation extends InstrumentationBase { + try { + return this._patchPostgres(exports); + } catch (e) { + DEBUG_BUILD && debug.error('Failed to patch postgres module:', e); + return exports; + } + }, + exports => exports, + ); + // Add fallback Query.prototype patching for pre-existing sql instances (CJS only) + // This catches queries from sql instances created before Sentry was initialized ['src', 'cf/src', 'cjs/src'].forEach(path => { - instrumentationModule.files.push( - new InstrumentationNodeModuleFile( - `postgres/${path}/connection.js`, - ['*'], - this._patchConnection.bind(this), - this._unwrap.bind(this), - ), - ); - - instrumentationModule.files.push( + module.files.push( new InstrumentationNodeModuleFile( `postgres/${path}/query.js`, SUPPORTED_VERSIONS, - this._patchQuery.bind(this), - this._unwrap.bind(this), + this._patchQueryPrototype.bind(this), + this._unpatchQueryPrototype.bind(this), ), ); }); - return [instrumentationModule]; + return module; } /** - * Determines whether a span should be created based on the current context. - * If `requireParentSpan` is set to true in the configuration, a span will - * only be created if there is a parent span available. + * Patches the postgres module by wrapping the main export function. + * This intercepts the creation of sql instances and instruments them. */ - private _shouldCreateSpans(): boolean { - const config = this.getConfig(); - const hasParentSpan = trace.getSpan(context.active()) !== undefined; - return hasParentSpan || !config.requireParentSpan; + private _patchPostgres(exports: { [key: string]: unknown }): { [key: string]: unknown } { + // In CJS: exports is the function itself + // In ESM: exports.default is the function + const isFunction = typeof exports === 'function'; + const Original = isFunction ? exports : exports.default; + + if (typeof Original !== 'function') { + DEBUG_BUILD && debug.warn('postgres module does not export a function. Skipping instrumentation.'); + return exports; + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + const WrappedPostgres = function (this: unknown, ...args: unknown[]): unknown { + const sql = Reflect.construct(Original as (...args: unknown[]) => unknown, args); + + // Validate that construction succeeded and returned a valid function object + if (!sql || typeof sql !== 'function') { + DEBUG_BUILD && debug.warn('postgres() did not return a valid instance'); + return sql; + } + + return self._instrumentSqlInstance(sql); + }; + + Object.setPrototypeOf(WrappedPostgres, Original); + Object.setPrototypeOf(WrappedPostgres.prototype, (Original as { prototype: object }).prototype); + + for (const key of Object.getOwnPropertyNames(Original)) { + if (!['length', 'name', 'prototype'].includes(key)) { + const descriptor = Object.getOwnPropertyDescriptor(Original, key); + if (descriptor) { + Object.defineProperty(WrappedPostgres, key, descriptor); + } + } + } + + // For CJS: the exports object IS the function, so return the wrapped function + // For ESM: replace the default export + if (isFunction) { + return WrappedPostgres as unknown as { [key: string]: unknown }; + } else { + replaceExports(exports, 'default', WrappedPostgres); + return exports; + } } /** - * Patches the reject method of the Query class to set the span status and end it + * Wraps query-returning methods (unsafe, file) to ensure their queries are instrumented. */ - private _patchReject(rejectTarget: any, span: Span): any { - return new Proxy(rejectTarget, { - apply: ( - rejectTarget, - rejectThisArg, - rejectArgs: { - message?: string; - code?: string; - name?: string; - }[], - ) => { - span.setStatus({ - code: SPAN_STATUS_ERROR, - // This message is the error message from the rejectArgs, when available - // e.g "relation 'User' does not exist" - message: rejectArgs?.[0]?.message || 'internal_error', - }); - - const result = Reflect.apply(rejectTarget, rejectThisArg, rejectArgs); - - // This status code is PG error code, e.g. '42P01' for "relation does not exist" - // https://www.postgresql.org/docs/current/errcodes-appendix.html - span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'Unknown error'); - // This is the error type, e.g. 'PostgresError' for a Postgres error - span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'Unknown error'); - - span.end(); - return result; - }, - }); + private _wrapQueryMethod( + original: (...args: unknown[]) => unknown, + target: unknown, + proxiedSql: unknown, + ): (...args: unknown[]) => unknown { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + return function (this: unknown, ...args: unknown[]): unknown { + const query = Reflect.apply(original, target, args); + + if (query && typeof query === 'object' && 'handle' in query) { + self._wrapSingleQueryHandle(query as { handle: unknown; strings?: string[] }, proxiedSql); + } + + return query; + }; } /** - * Patches the resolve method of the Query class to end the span when the query is resolved. + * Wraps callback-based methods (begin, reserve) to recursively instrument Sql instances. + * Note: These methods can also be used as tagged templates, which we pass through unchanged. + * + * Savepoint is not wrapped to avoid complex nested transaction instrumentation issues. + * Queries within savepoint callbacks are still instrumented through the parent transaction's Sql instance. */ - private _patchResolve(resolveTarget: any, span: Span): any { - return new Proxy(resolveTarget, { - apply: (resolveTarget, resolveThisArg, resolveArgs: [{ command?: string }]) => { - const result = Reflect.apply(resolveTarget, resolveThisArg, resolveArgs); - const sqlCommand = resolveArgs?.[0]?.command; - - if (sqlCommand) { - // SQL command is only available when the query is resolved successfully - span.setAttribute(ATTR_DB_OPERATION_NAME, sqlCommand); + private _wrapCallbackMethod( + original: (...args: unknown[]) => unknown, + target: unknown, + parentSqlInstance: unknown, + ): (...args: unknown[]) => unknown { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + return function (this: unknown, ...args: unknown[]): unknown { + // Extract parent context to propagate to child instances + const parentContext = (parentSqlInstance as Record)[CONNECTION_CONTEXT_SYMBOL] as + | PostgresConnectionContext + | undefined; + + // Check if this is a callback-based call by verifying the last argument is a function + const isCallbackBased = typeof args[args.length - 1] === 'function'; + + if (!isCallbackBased) { + // Not a callback-based call - could be tagged template or promise-based + const result = Reflect.apply(original, target, args); + // If result is a Promise (e.g., reserve() without callback), instrument the resolved Sql instance + if (result && typeof (result as Promise).then === 'function') { + return (result as Promise).then((sqlInstance: unknown) => { + return self._instrumentSqlInstance(sqlInstance, parentContext); + }); } - span.end(); return result; - }, - }); + } + + // Callback-based call: wrap the callback to instrument the Sql instance + const callback = (args.length === 1 ? args[0] : args[1]) as (sql: unknown) => unknown; + const wrappedCallback = function (sqlInstance: unknown): unknown { + const instrumentedSql = self._instrumentSqlInstance(sqlInstance, parentContext); + return callback(instrumentedSql); + }; + + const newArgs = args.length === 1 ? [wrappedCallback] : [args[0], wrappedCallback]; + return Reflect.apply(original, target, newArgs); + }; } /** - * Patches the Query class to instrument the handle method. + * Sets connection context attributes on a span. */ - private _patchQuery(moduleExports: { - Query: { - prototype: { - handle: any; - }; + private _setConnectionAttributes(span: Span, connectionContext: PostgresConnectionContext | undefined): void { + if (!connectionContext) { + return; + } + if (connectionContext.ATTR_DB_NAMESPACE) { + span.setAttribute(ATTR_DB_NAMESPACE, connectionContext.ATTR_DB_NAMESPACE); + } + if (connectionContext.ATTR_SERVER_ADDRESS) { + span.setAttribute(ATTR_SERVER_ADDRESS, connectionContext.ATTR_SERVER_ADDRESS); + } + if (connectionContext.ATTR_SERVER_PORT !== undefined) { + // Port is stored as string in PostgresConnectionContext for requestHook backwards compatibility, + // but OTEL semantic conventions expect port as a number for span attributes + const portNumber = parseInt(connectionContext.ATTR_SERVER_PORT, 10); + if (!isNaN(portNumber)) { + span.setAttribute(ATTR_SERVER_PORT, portNumber); + } + } + } + + /** + * Extracts DB operation name from SQL query and sets it on the span. + */ + private _setOperationName(span: Span, sanitizedQuery: string | undefined, command?: string): void { + if (command) { + span.setAttribute(ATTR_DB_OPERATION_NAME, command); + return; + } + // Fallback: extract operation from the SQL query + const operationMatch = sanitizedQuery?.match(SQL_OPERATION_REGEX); + if (operationMatch?.[1]) { + span.setAttribute(ATTR_DB_OPERATION_NAME, operationMatch[1].toUpperCase()); + } + } + + /** + * Extracts and stores connection context from sql.options. + */ + private _attachConnectionContext(sql: unknown, proxiedSql: Record): void { + const sqlInstance = sql as { options?: { host?: string[]; port?: number[]; database?: string } }; + if (!sqlInstance.options || typeof sqlInstance.options !== 'object') { + return; + } + + const opts = sqlInstance.options; + // postgres.js stores parsed options with host and port as arrays + // The library defaults to 'localhost' and 5432 if not specified, but we're defensive here + const host = opts.host?.[0] || 'localhost'; + const port = opts.port?.[0] || 5432; + + const connectionContext: PostgresConnectionContext = { + ATTR_DB_NAMESPACE: typeof opts.database === 'string' && opts.database !== '' ? opts.database : undefined, + ATTR_SERVER_ADDRESS: host, + ATTR_SERVER_PORT: String(port), }; - }): any { - moduleExports.Query.prototype.handle = new Proxy(moduleExports.Query.prototype.handle, { - apply: async ( - handleTarget, - handleThisArg: { - resolve: any; - reject: any; - strings?: string[]; - }, - handleArgs, - ) => { - if (!this._shouldCreateSpans()) { - // If we don't need to create spans, just call the original method - return Reflect.apply(handleTarget, handleThisArg, handleArgs); + + proxiedSql[CONNECTION_CONTEXT_SYMBOL] = connectionContext; + } + + /** + * Instruments a sql instance by wrapping its query execution methods. + */ + private _instrumentSqlInstance(sql: unknown, parentConnectionContext?: PostgresConnectionContext): unknown { + // Check if already instrumented to prevent double-wrapping + // Using Symbol.for() ensures the marker survives proxying + if ((sql as Record)[INSTRUMENTED_MARKER]) { + return sql; + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + // Wrap the sql function to intercept query creation + const proxiedSql: unknown = new Proxy(sql as (...args: unknown[]) => unknown, { + apply(target, thisArg, argumentsList: unknown[]) { + const query = Reflect.apply(target, thisArg, argumentsList); + + if (query && typeof query === 'object' && 'handle' in query) { + self._wrapSingleQueryHandle(query as { handle: unknown; strings?: string[] }, proxiedSql); } - const sanitizedSqlQuery = this._sanitizeSqlQuery(handleThisArg.strings?.[0]); - - return startSpanManual( - { - name: sanitizedSqlQuery || 'postgresjs.query', - op: 'db', - }, - (span: Span) => { - const scope = getCurrentScope(); - const postgresConnectionContext = scope.getScopeData().contexts['postgresjsConnection'] as - | PostgresConnectionContext - | undefined; - - addOriginToSpan(span, 'auto.db.otel.postgres'); - - const { requestHook } = this.getConfig(); - - if (requestHook) { - safeExecuteInTheMiddle( - () => requestHook(span, sanitizedSqlQuery, postgresConnectionContext), - error => { - if (error) { - debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, error); - } - }, - ); - } - - // ATTR_DB_NAMESPACE is used to indicate the database name and the schema name - // It's only the database name as we don't have the schema information - const databaseName = postgresConnectionContext?.ATTR_DB_NAMESPACE || ''; - const databaseHost = postgresConnectionContext?.ATTR_SERVER_ADDRESS || ''; - const databasePort = postgresConnectionContext?.ATTR_SERVER_PORT || ''; - - span.setAttribute(ATTR_DB_SYSTEM_NAME, 'postgres'); - span.setAttribute(ATTR_DB_NAMESPACE, databaseName); - span.setAttribute(ATTR_SERVER_ADDRESS, databaseHost); - span.setAttribute(ATTR_SERVER_PORT, databasePort); - span.setAttribute(ATTR_DB_QUERY_TEXT, sanitizedSqlQuery); - - handleThisArg.resolve = this._patchResolve(handleThisArg.resolve, span); - handleThisArg.reject = this._patchReject(handleThisArg.reject, span); - - try { - return Reflect.apply(handleTarget, handleThisArg, handleArgs); - } catch (error) { - span.setStatus({ - code: SPAN_STATUS_ERROR, - }); - span.end(); - throw error; // Re-throw the error to propagate it - } - }, - ); + return query; + }, + get(target, prop) { + const original = (target as unknown as Record)[prop]; + + if (typeof prop !== 'string' || typeof original !== 'function') { + return original; + } + + // Wrap methods that return PendingQuery objects (unsafe, file) + if (prop === 'unsafe' || prop === 'file') { + return self._wrapQueryMethod(original as (...args: unknown[]) => unknown, target, proxiedSql); + } + + // Wrap begin and reserve (not savepoint to avoid duplicate spans) + if (prop === 'begin' || prop === 'reserve') { + return self._wrapCallbackMethod(original as (...args: unknown[]) => unknown, target, proxiedSql); + } + + return original; }, }); - return moduleExports; + // Use provided parent context if available, otherwise extract from sql.options + if (parentConnectionContext) { + (proxiedSql as Record)[CONNECTION_CONTEXT_SYMBOL] = parentConnectionContext; + } else { + this._attachConnectionContext(sql, proxiedSql as Record); + } + + // Mark both the original and proxy as instrumented to prevent double-wrapping + // The proxy might be passed to other methods, or the original + // might be accessed directly, so we need to mark both + (sql as Record)[INSTRUMENTED_MARKER] = true; + (proxiedSql as Record)[INSTRUMENTED_MARKER] = true; + + return proxiedSql; } /** - * Patches the Connection class to set the database, host, and port attributes - * when a new connection is created. + * Wraps a single query's handle method to create spans. */ - private _patchConnection(Connection: any): any { - return new Proxy(Connection, { - apply: (connectionTarget, thisArg, connectionArgs: { database: string; host: string[]; port: number[] }[]) => { - const databaseName = connectionArgs[0]?.database || ''; - const databaseHost = connectionArgs[0]?.host?.[0] || ''; - const databasePort = connectionArgs[0]?.port?.[0] || ''; - - const scope = getCurrentScope(); - scope.setContext('postgresjsConnection', { - ATTR_DB_NAMESPACE: databaseName, - ATTR_SERVER_ADDRESS: databaseHost, - ATTR_SERVER_PORT: databasePort, - }); - - return Reflect.apply(connectionTarget, thisArg, connectionArgs); - }, - }); + private _wrapSingleQueryHandle( + query: { handle: unknown; strings?: string[]; __sentryWrapped?: boolean }, + sqlInstance: unknown, + ): void { + // Prevent double wrapping - check if the handle itself is already wrapped + if ((query.handle as { __sentryWrapped?: boolean })?.__sentryWrapped) { + return; + } + + // Mark this query as coming from an instrumented sql instance + // This prevents the Query.prototype fallback patch from double-spanning + (query as Record)[QUERY_FROM_INSTRUMENTED_SQL] = true; + + const originalHandle = query.handle as (...args: unknown[]) => Promise; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + // IMPORTANT: We must replace the handle function directly, not use a Proxy, + // because Query.then() internally calls this.handle(), which would bypass a Proxy wrapper. + const wrappedHandle = async function (this: unknown, ...args: unknown[]): Promise { + if (!self._shouldCreateSpans()) { + return originalHandle.apply(this, args); + } + + const fullQuery = self._reconstructQuery(query.strings); + const sanitizedSqlQuery = self._sanitizeSqlQuery(fullQuery); + + return startSpanManual( + { + name: sanitizedSqlQuery || 'postgresjs.query', + op: 'db', + }, + (span: Span) => { + addOriginToSpan(span, 'auto.db.postgresjs'); + + span.setAttributes({ + [ATTR_DB_SYSTEM_NAME]: 'postgres', + [ATTR_DB_QUERY_TEXT]: sanitizedSqlQuery, + }); + + const connectionContext = sqlInstance + ? ((sqlInstance as Record)[CONNECTION_CONTEXT_SYMBOL] as + | PostgresConnectionContext + | undefined) + : undefined; + + self._setConnectionAttributes(span, connectionContext); + + const config = self.getConfig(); + const { requestHook } = config; + if (requestHook) { + safeExecuteInTheMiddle( + () => requestHook(span, sanitizedSqlQuery, connectionContext), + e => { + if (e) { + span.setAttribute('sentry.hook.error', 'requestHook failed'); + DEBUG_BUILD && debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, e); + } + }, + true, + ); + } + + const queryWithCallbacks = this as { + resolve: unknown; + reject: unknown; + }; + + queryWithCallbacks.resolve = new Proxy(queryWithCallbacks.resolve as (...args: unknown[]) => unknown, { + apply: (resolveTarget, resolveThisArg, resolveArgs: [{ command?: string }]) => { + try { + self._setOperationName(span, sanitizedSqlQuery, resolveArgs?.[0]?.command); + span.end(); + } catch (e) { + DEBUG_BUILD && debug.error('Error ending span in resolve callback:', e); + } + + return Reflect.apply(resolveTarget, resolveThisArg, resolveArgs); + }, + }); + + queryWithCallbacks.reject = new Proxy(queryWithCallbacks.reject as (...args: unknown[]) => unknown, { + apply: (rejectTarget, rejectThisArg, rejectArgs: { message?: string; code?: string; name?: string }[]) => { + try { + span.setStatus({ + code: SPAN_STATUS_ERROR, + message: rejectArgs?.[0]?.message || 'unknown_error', + }); + + span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'unknown'); + span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'unknown'); + + self._setOperationName(span, sanitizedSqlQuery); + span.end(); + } catch (e) { + DEBUG_BUILD && debug.error('Error ending span in reject callback:', e); + } + return Reflect.apply(rejectTarget, rejectThisArg, rejectArgs); + }, + }); + + // Handle synchronous errors that might occur before promise is created + try { + return originalHandle.apply(this, args); + } catch (e) { + span.setStatus({ + code: SPAN_STATUS_ERROR, + message: e instanceof Error ? e.message : 'unknown_error', + }); + span.end(); + throw e; + } + }, + ); + }; + + (wrappedHandle as { __sentryWrapped?: boolean }).__sentryWrapped = true; + query.handle = wrappedHandle; + } + + /** + * Determines whether a span should be created based on the current context. + * If `requireParentSpan` is set to true in the configuration, a span will + * only be created if there is a parent span available. + */ + private _shouldCreateSpans(): boolean { + const config = this.getConfig(); + const hasParentSpan = trace.getSpan(context.active()) !== undefined; + return hasParentSpan || !config.requireParentSpan; + } + + /** + * Reconstructs the full SQL query from template strings with PostgreSQL placeholders. + * + * For sql`SELECT * FROM users WHERE id = ${123} AND name = ${'foo'}`: + * strings = ["SELECT * FROM users WHERE id = ", " AND name = ", ""] + * returns: "SELECT * FROM users WHERE id = $1 AND name = $2" + */ + private _reconstructQuery(strings: string[] | undefined): string | undefined { + if (!strings?.length) { + return undefined; + } + if (strings.length === 1) { + return strings[0] || undefined; + } + // Join template parts with PostgreSQL placeholders ($1, $2, etc.) + return strings.reduce((acc, str, i) => (i === 0 ? str : `${acc}$${i}${str}`), ''); } /** * Sanitize SQL query as per the OTEL semantic conventions * https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sanitization-of-dbquerytext + * + * PostgreSQL $n placeholders are preserved per OTEL spec - they're parameterized queries, + * not sensitive literals. Only actual values (strings, numbers, booleans) are sanitized. */ private _sanitizeSqlQuery(sqlQuery: string | undefined): string { if (!sqlQuery) { @@ -283,27 +527,183 @@ export class PostgresJsInstrumentation extends InstrumentationBase Promise) & { + __sentry_original__?: (...args: unknown[]) => Promise; + }; + }; + }; + }): typeof moduleExports { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + const originalHandle = moduleExports.Query.prototype.handle; + + moduleExports.Query.prototype.handle = async function ( + this: { + resolve: unknown; + reject: unknown; + strings?: string[]; + }, + ...args: unknown[] + ): Promise { + // Skip if this query came from an instrumented sql instance (already handled by wrapper) + if ((this as Record)[QUERY_FROM_INSTRUMENTED_SQL]) { + return originalHandle.apply(this, args); + } + + // Skip if we shouldn't create spans + if (!self._shouldCreateSpans()) { + return originalHandle.apply(this, args); + } + + const fullQuery = self._reconstructQuery(this.strings); + const sanitizedSqlQuery = self._sanitizeSqlQuery(fullQuery); + + return startSpanManual( + { + name: sanitizedSqlQuery || 'postgresjs.query', + op: 'db', + }, + (span: Span) => { + addOriginToSpan(span, 'auto.db.postgresjs'); + + span.setAttributes({ + [ATTR_DB_SYSTEM_NAME]: 'postgres', + [ATTR_DB_QUERY_TEXT]: sanitizedSqlQuery, + }); + + // Note: No connection context available for pre-existing instances + // because the sql instance wasn't created through our instrumented wrapper + + const config = self.getConfig(); + const { requestHook } = config; + if (requestHook) { + safeExecuteInTheMiddle( + () => requestHook(span, sanitizedSqlQuery, undefined), + e => { + if (e) { + span.setAttribute('sentry.hook.error', 'requestHook failed'); + DEBUG_BUILD && debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, e); + } + }, + true, + ); + } + + // Wrap resolve to end span on success + const originalResolve = this.resolve; + this.resolve = new Proxy(originalResolve as (...args: unknown[]) => unknown, { + apply: (resolveTarget, resolveThisArg, resolveArgs: [{ command?: string }]) => { + try { + self._setOperationName(span, sanitizedSqlQuery, resolveArgs?.[0]?.command); + span.end(); + } catch (e) { + DEBUG_BUILD && debug.error('Error ending span in resolve callback:', e); + } + return Reflect.apply(resolveTarget, resolveThisArg, resolveArgs); + }, + }); + + // Wrap reject to end span on error + const originalReject = this.reject; + this.reject = new Proxy(originalReject as (...args: unknown[]) => unknown, { + apply: (rejectTarget, rejectThisArg, rejectArgs: { message?: string; code?: string; name?: string }[]) => { + try { + span.setStatus({ + code: SPAN_STATUS_ERROR, + message: rejectArgs?.[0]?.message || 'unknown_error', + }); + span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'unknown'); + span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'unknown'); + self._setOperationName(span, sanitizedSqlQuery); + span.end(); + } catch (e) { + DEBUG_BUILD && debug.error('Error ending span in reject callback:', e); + } + return Reflect.apply(rejectTarget, rejectThisArg, rejectArgs); + }, + }); + + try { + return originalHandle.apply(this, args); + } catch (e) { + span.setStatus({ + code: SPAN_STATUS_ERROR, + message: e instanceof Error ? e.message : 'unknown_error', + }); + span.end(); + throw e; + } + }, + ); + }; + + // Store original for unpatch - must be set on the NEW patched function + moduleExports.Query.prototype.handle.__sentry_original__ = originalHandle; + + return moduleExports; + } + + /** + * Restores the original Query.prototype.handle method. + */ + private _unpatchQueryPrototype(moduleExports: { + Query: { + prototype: { + handle: ((...args: unknown[]) => Promise) & { + __sentry_original__?: (...args: unknown[]) => Promise; + }; + }; + }; + }): typeof moduleExports { + if (moduleExports.Query.prototype.handle.__sentry_original__) { + moduleExports.Query.prototype.handle = moduleExports.Query.prototype.handle.__sentry_original__; + } + return moduleExports; + } } -const _postgresJsIntegration = (() => { +const _postgresJsIntegration = ((options?: PostgresJsInstrumentationConfig) => { return { name: INTEGRATION_NAME, setupOnce() { - instrumentPostgresJs(); + instrumentPostgresJs(options); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/test/integrations/tracing/postgresjs.test.ts b/packages/node/test/integrations/tracing/postgresjs.test.ts new file mode 100644 index 000000000000..a20b1941bb28 --- /dev/null +++ b/packages/node/test/integrations/tracing/postgresjs.test.ts @@ -0,0 +1,411 @@ +import { describe, expect, it } from 'vitest'; +import { PostgresJsInstrumentation } from '../../../src/integrations/tracing/postgresjs'; + +describe('PostgresJs', () => { + const instrumentation = new PostgresJsInstrumentation({ requireParentSpan: true }); + + describe('_reconstructQuery', () => { + const reconstruct = (strings: string[] | undefined) => + ( + instrumentation as unknown as { _reconstructQuery: (s: string[] | undefined) => string | undefined } + )._reconstructQuery(strings); + + describe('empty input handling', () => { + it.each([ + [undefined, undefined], + [null as unknown as undefined, undefined], + [[], undefined], + [[''], undefined], + ])('returns undefined for %p', (input, expected) => { + expect(reconstruct(input)).toBe(expected); + }); + + it('returns whitespace-only string as-is', () => { + expect(reconstruct([' '])).toBe(' '); + }); + }); + + describe('single-element array (non-parameterized)', () => { + it.each([ + ['SELECT * FROM users', 'SELECT * FROM users'], + ['SELECT * FROM users WHERE id = $1', 'SELECT * FROM users WHERE id = $1'], + ['INSERT INTO users (email, name) VALUES ($1, $2)', 'INSERT INTO users (email, name) VALUES ($1, $2)'], + ])('returns %p as-is', (input, expected) => { + expect(reconstruct([input])).toBe(expected); + }); + }); + + describe('multi-element array (parameterized)', () => { + it.each([ + [['SELECT * FROM users WHERE id = ', ''], 'SELECT * FROM users WHERE id = $1'], + [['SELECT * FROM users WHERE id = ', ' AND name = ', ''], 'SELECT * FROM users WHERE id = $1 AND name = $2'], + [['INSERT INTO t VALUES (', ', ', ', ', ')'], 'INSERT INTO t VALUES ($1, $2, $3)'], + [['', ' WHERE id = ', ''], '$1 WHERE id = $2'], + [ + ['SELECT * FROM ', ' WHERE id = ', ' AND status IN (', ', ', ') ORDER BY ', ''], + 'SELECT * FROM $1 WHERE id = $2 AND status IN ($3, $4) ORDER BY $5', + ], + ])('reconstructs %p to %p', (input, expected) => { + expect(reconstruct(input)).toBe(expected); + }); + }); + + describe('edge cases', () => { + it('handles 10+ parameters', () => { + const strings = ['INSERT INTO t VALUES (', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ')']; + expect(reconstruct(strings)).toBe('INSERT INTO t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)'); + }); + + it.each([ + [['SELECT * FROM users WHERE id = ', ' ', ''], 'SELECT * FROM users WHERE id = $1 $2'], + [['SELECT * FROM users WHERE id = ', ' LIMIT 10'], 'SELECT * FROM users WHERE id = $1 LIMIT 10'], + [['SELECT *\nFROM users\nWHERE id = ', ''], 'SELECT *\nFROM users\nWHERE id = $1'], + [['SELECT * FROM "User" WHERE "email" = ', ''], 'SELECT * FROM "User" WHERE "email" = $1'], + [['SELECT ', '', '', ''], 'SELECT $1$2$3'], + [['', ''], '$1'], + ])('handles edge case %p', (input, expected) => { + expect(reconstruct(input)).toBe(expected); + }); + }); + + describe('integration with _sanitizeSqlQuery', () => { + const sanitize = (query: string | undefined) => + (instrumentation as unknown as { _sanitizeSqlQuery: (q: string | undefined) => string })._sanitizeSqlQuery( + query, + ); + + it('preserves $n placeholders per OTEL spec', () => { + const strings = ['SELECT * FROM users WHERE id = ', ' AND name = ', '']; + expect(sanitize(reconstruct(strings))).toBe('SELECT * FROM users WHERE id = $1 AND name = $2'); + }); + + it('collapses IN clause with $n to IN ($?)', () => { + const strings = ['SELECT * FROM users WHERE id = ', ' AND status IN (', ', ', ', ', ')']; + expect(sanitize(reconstruct(strings))).toBe('SELECT * FROM users WHERE id = $1 AND status IN ($?)'); + }); + + it('returns Unknown SQL Query for undefined input', () => { + expect(sanitize(reconstruct(undefined))).toBe('Unknown SQL Query'); + }); + + it('normalizes whitespace and removes trailing semicolon', () => { + const strings = ['SELECT *\n FROM users\n WHERE id = ', ';']; + expect(sanitize(reconstruct(strings))).toBe('SELECT * FROM users WHERE id = $1'); + }); + }); + }); + + describe('_sanitizeSqlQuery', () => { + const sanitize = (query: string | undefined) => + (instrumentation as unknown as { _sanitizeSqlQuery: (q: string | undefined) => string })._sanitizeSqlQuery(query); + + describe('passthrough (no literals)', () => { + it.each([ + ['SELECT * FROM users', 'SELECT * FROM users'], + ['INSERT INTO users (a, b) SELECT a, b FROM other', 'INSERT INTO users (a, b) SELECT a, b FROM other'], + [ + 'SELECT col1, col2 FROM table1 JOIN table2 ON table1.id = table2.id', + 'SELECT col1, col2 FROM table1 JOIN table2 ON table1.id = table2.id', + ], + ])('passes through %p unchanged', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('comment removal', () => { + it.each([ + ['SELECT * FROM users -- comment', 'SELECT * FROM users'], + ['SELECT * -- comment\nFROM users', 'SELECT * FROM users'], + ['SELECT /* comment */ * FROM users', 'SELECT * FROM users'], + ['SELECT /* multi\nline */ * FROM users', 'SELECT * FROM users'], + ['SELECT /* c1 */ * FROM /* c2 */ users -- c3', 'SELECT * FROM users'], + ])('removes comments: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('whitespace normalization', () => { + it.each([ + ['SELECT * FROM users', 'SELECT * FROM users'], + ['SELECT *\n\tFROM\n\tusers', 'SELECT * FROM users'], + [' SELECT * FROM users ', 'SELECT * FROM users'], + [' SELECT \n\t * \r\n FROM \t\t users ', 'SELECT * FROM users'], + ])('normalizes %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('trailing semicolon removal', () => { + it.each([ + ['SELECT * FROM users;', 'SELECT * FROM users'], + ['SELECT * FROM users; ', 'SELECT * FROM users'], + ])('removes trailing semicolon: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('$n placeholder preservation (OTEL compliance)', () => { + it.each([ + ['SELECT * FROM users WHERE id = $1', 'SELECT * FROM users WHERE id = $1'], + ['SELECT * FROM users WHERE id = $1 AND name = $2', 'SELECT * FROM users WHERE id = $1 AND name = $2'], + ['INSERT INTO t VALUES ($1, $10, $100)', 'INSERT INTO t VALUES ($1, $10, $100)'], + ['$1 UNION SELECT * FROM users', '$1 UNION SELECT * FROM users'], + ['SELECT * FROM users LIMIT $1', 'SELECT * FROM users LIMIT $1'], + ['SELECT $1$2$3', 'SELECT $1$2$3'], + ['SELECT generate_series($1, $2)', 'SELECT generate_series($1, $2)'], + ])('preserves $n: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('string literal sanitization', () => { + it.each([ + ["SELECT * FROM users WHERE name = 'John'", 'SELECT * FROM users WHERE name = ?'], + ["SELECT * FROM users WHERE a = 'x' AND b = 'y'", 'SELECT * FROM users WHERE a = ? AND b = ?'], + ["SELECT * FROM users WHERE name = ''", 'SELECT * FROM users WHERE name = ?'], + ["SELECT * FROM users WHERE name = 'it''s'", 'SELECT * FROM users WHERE name = ?'], + ["SELECT * FROM users WHERE data = 'a''b''c'", 'SELECT * FROM users WHERE data = ?'], + ["SELECT * FROM t WHERE desc = 'Use $1 for param'", 'SELECT * FROM t WHERE desc = ?'], + ["SELECT * FROM users WHERE name = '日本語'", 'SELECT * FROM users WHERE name = ?'], + ])('sanitizes string: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('numeric literal sanitization', () => { + it.each([ + ['SELECT * FROM users WHERE id = 123', 'SELECT * FROM users WHERE id = ?'], + ['SELECT * FROM users WHERE count = 0', 'SELECT * FROM users WHERE count = ?'], + ['SELECT * FROM products WHERE price = 19.99', 'SELECT * FROM products WHERE price = ?'], + ['SELECT * FROM products WHERE discount = .5', 'SELECT * FROM products WHERE discount = ?'], + ['SELECT * FROM accounts WHERE balance = -500', 'SELECT * FROM accounts WHERE balance = ?'], + ['SELECT * FROM accounts WHERE rate = -0.05', 'SELECT * FROM accounts WHERE rate = ?'], + ['SELECT * FROM data WHERE value = 1e10', 'SELECT * FROM data WHERE value = ?'], + ['SELECT * FROM data WHERE value = 1.5e-3', 'SELECT * FROM data WHERE value = ?'], + ['SELECT * FROM data WHERE value = 2.5E+10', 'SELECT * FROM data WHERE value = ?'], + ['SELECT * FROM data WHERE value = -1e10', 'SELECT * FROM data WHERE value = ?'], + ['SELECT * FROM users LIMIT 10 OFFSET 20', 'SELECT * FROM users LIMIT ? OFFSET ?'], + ])('sanitizes number: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + + it('preserves numbers in identifiers', () => { + expect(sanitize('SELECT * FROM users2 WHERE col1 = 5')).toBe('SELECT * FROM users2 WHERE col1 = ?'); + expect(sanitize('SELECT * FROM "table1" WHERE "col2" = 5')).toBe('SELECT * FROM "table1" WHERE "col2" = ?'); + }); + }); + + describe('hex and binary literal sanitization', () => { + it.each([ + ["SELECT * FROM t WHERE data = X'1A2B'", 'SELECT * FROM t WHERE data = ?'], + ["SELECT * FROM t WHERE data = x'ff'", 'SELECT * FROM t WHERE data = ?'], + ["SELECT * FROM t WHERE data = X''", 'SELECT * FROM t WHERE data = ?'], + ['SELECT * FROM t WHERE flags = 0x1A2B', 'SELECT * FROM t WHERE flags = ?'], + ['SELECT * FROM t WHERE flags = 0XFF', 'SELECT * FROM t WHERE flags = ?'], + ["SELECT * FROM t WHERE bits = B'1010'", 'SELECT * FROM t WHERE bits = ?'], + ["SELECT * FROM t WHERE bits = b'1111'", 'SELECT * FROM t WHERE bits = ?'], + ["SELECT * FROM t WHERE bits = B''", 'SELECT * FROM t WHERE bits = ?'], + ])('sanitizes hex/binary: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('boolean literal sanitization', () => { + it.each([ + ['SELECT * FROM users WHERE active = TRUE', 'SELECT * FROM users WHERE active = ?'], + ['SELECT * FROM users WHERE active = FALSE', 'SELECT * FROM users WHERE active = ?'], + ['SELECT * FROM users WHERE a = true AND b = false', 'SELECT * FROM users WHERE a = ? AND b = ?'], + ['SELECT * FROM users WHERE a = True AND b = False', 'SELECT * FROM users WHERE a = ? AND b = ?'], + ])('sanitizes boolean: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + + it('does not affect identifiers containing TRUE/FALSE', () => { + expect(sanitize('SELECT TRUE_FLAG FROM users WHERE active = TRUE')).toBe( + 'SELECT TRUE_FLAG FROM users WHERE active = ?', + ); + }); + }); + + describe('IN clause collapsing', () => { + it.each([ + ['SELECT * FROM users WHERE id IN (?, ?, ?)', 'SELECT * FROM users WHERE id IN (?)'], + ['SELECT * FROM users WHERE id IN ($1, $2, $3)', 'SELECT * FROM users WHERE id IN ($?)'], + ['SELECT * FROM users WHERE id in ($1, $2)', 'SELECT * FROM users WHERE id IN ($?)'], + ['SELECT * FROM users WHERE id IN ( $1 , $2 , $3 )', 'SELECT * FROM users WHERE id IN ($?)'], + [ + 'SELECT * FROM users WHERE id IN ($1, $2) AND status IN ($3, $4)', + 'SELECT * FROM users WHERE id IN ($?) AND status IN ($?)', + ], + ['SELECT * FROM users WHERE id NOT IN ($1, $2)', 'SELECT * FROM users WHERE id NOT IN ($?)'], + ['SELECT * FROM users WHERE id NOT IN (?, ?)', 'SELECT * FROM users WHERE id NOT IN (?)'], + ['SELECT * FROM users WHERE id IN ($1)', 'SELECT * FROM users WHERE id IN ($?)'], + ['SELECT * FROM users WHERE id IN (1, 2, 3)', 'SELECT * FROM users WHERE id IN (?)'], + ])('collapses IN clause: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('mixed scenarios (params + literals)', () => { + it.each([ + ["SELECT * FROM users WHERE id = $1 AND status = 'active'", 'SELECT * FROM users WHERE id = $1 AND status = ?'], + ['SELECT * FROM users WHERE id = $1 AND limit = 100', 'SELECT * FROM users WHERE id = $1 AND limit = ?'], + [ + "SELECT * FROM t WHERE a = $1 AND b = 'foo' AND c = 123 AND d = TRUE AND e IN ($2, $3)", + 'SELECT * FROM t WHERE a = $1 AND b = ? AND c = ? AND d = ? AND e IN ($?)', + ], + ])('handles mixed: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('PostgreSQL-specific syntax', () => { + it.each([ + ['SELECT $1::integer', 'SELECT $1::integer'], + ['SELECT $1::text', 'SELECT $1::text'], + ['SELECT * FROM t WHERE tags = ARRAY[1, 2, 3]', 'SELECT * FROM t WHERE tags = ARRAY[?, ?, ?]'], + ['SELECT * FROM t WHERE tags = ARRAY[$1, $2]', 'SELECT * FROM t WHERE tags = ARRAY[$1, $2]'], + ["SELECT data->'key' FROM t WHERE id = $1", 'SELECT data->? FROM t WHERE id = $1'], + ["SELECT data->>'key' FROM t WHERE id = $1", 'SELECT data->>? FROM t WHERE id = $1'], + ["SELECT * FROM t WHERE data @> '{}'", 'SELECT * FROM t WHERE data @> ?'], + [ + "SELECT * FROM t WHERE created_at > NOW() - INTERVAL '7 days'", + 'SELECT * FROM t WHERE created_at > NOW() - INTERVAL ?', + ], + ['CREATE TABLE t (created_at TIMESTAMP(3))', 'CREATE TABLE t (created_at TIMESTAMP(?))'], + ['CREATE TABLE t (price NUMERIC(10, 2))', 'CREATE TABLE t (price NUMERIC(?, ?))'], + ])('handles PostgreSQL syntax: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('empty/undefined input', () => { + it.each([ + [undefined, 'Unknown SQL Query'], + ['', 'Unknown SQL Query'], + [' ', ''], + [' \n\t ', ''], + ])('handles empty input %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('complex real-world queries', () => { + it('handles query with comments, whitespace, and IN clause', () => { + const input = ` + SELECT * FROM users -- fetch all users + WHERE id = $1 + AND status IN ($2, $3, $4); + `; + expect(sanitize(input)).toBe('SELECT * FROM users WHERE id = $1 AND status IN ($?)'); + }); + + it('handles Prisma-style query', () => { + const input = ` + SELECT "User"."id", "User"."email", "User"."name" + FROM "User" + WHERE "User"."email" = $1 + AND "User"."deleted_at" IS NULL + LIMIT $2; + `; + expect(sanitize(input)).toBe( + 'SELECT "User"."id", "User"."email", "User"."name" FROM "User" WHERE "User"."email" = $1 AND "User"."deleted_at" IS NULL LIMIT $2', + ); + }); + + it('handles CREATE TABLE with various types', () => { + const input = ` + CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" TEXT NOT NULL, + "balance" NUMERIC(10, 2) DEFAULT 0.00, + CONSTRAINT "User_pkey" PRIMARY KEY ("id") + ); + `; + expect(sanitize(input)).toBe( + 'CREATE TABLE "User" ( "id" SERIAL NOT NULL, "createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP, "email" TEXT NOT NULL, "balance" NUMERIC(?, ?) DEFAULT ?, CONSTRAINT "User_pkey" PRIMARY KEY ("id") )', + ); + }); + + it('handles INSERT/UPDATE with mixed literals and params', () => { + expect(sanitize("INSERT INTO users (name, age, active) VALUES ('John', 30, TRUE)")).toBe( + 'INSERT INTO users (name, age, active) VALUES (?, ?, ?)', + ); + expect(sanitize("UPDATE users SET name = $1, updated_at = '2024-01-01' WHERE id = 123")).toBe( + 'UPDATE users SET name = $1, updated_at = ? WHERE id = ?', + ); + }); + }); + + describe('edge cases', () => { + it.each([ + ['SELECT * FROM "my-table" WHERE "my-column" = $1', 'SELECT * FROM "my-table" WHERE "my-column" = $1'], + ['SELECT * FROM t WHERE big_id = 99999999999999999999', 'SELECT * FROM t WHERE big_id = ?'], + ['SELECT * FROM t WHERE val > -5', 'SELECT * FROM t WHERE val > ?'], + ['SELECT * FROM t WHERE id IN (1, -2, 3)', 'SELECT * FROM t WHERE id IN (?)'], + ['SELECT 1+2*3', 'SELECT ?+?*?'], + ["SELECT * FROM users WHERE name LIKE '%john%'", 'SELECT * FROM users WHERE name LIKE ?'], + ['SELECT * FROM t WHERE age BETWEEN 18 AND 65', 'SELECT * FROM t WHERE age BETWEEN ? AND ?'], + ['SELECT * FROM t WHERE age BETWEEN $1 AND $2', 'SELECT * FROM t WHERE age BETWEEN $1 AND $2'], + [ + "SELECT CASE WHEN status = 'active' THEN 1 ELSE 0 END FROM users", + 'SELECT CASE WHEN status = ? THEN ? ELSE ? END FROM users', + ], + [ + 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)', + 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > ?)', + ], + [ + "WITH cte AS (SELECT * FROM users WHERE status = 'active') SELECT * FROM cte WHERE id = $1", + 'WITH cte AS (SELECT * FROM users WHERE status = ?) SELECT * FROM cte WHERE id = $1', + ], + [ + 'SELECT COUNT(*), SUM(amount), AVG(price) FROM orders WHERE status = $1', + 'SELECT COUNT(*), SUM(amount), AVG(price) FROM orders WHERE status = $1', + ], + [ + 'SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 10', + 'SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > ?', + ], + [ + 'SELECT ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) FROM orders', + 'SELECT ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) FROM orders', + ], + ])('handles edge case: %p', (input, expected) => { + expect(sanitize(input)).toBe(expected); + }); + }); + + describe('regression tests', () => { + it('does not replace $n with ? (OTEL compliance)', () => { + const result = sanitize('SELECT * FROM users WHERE id = $1'); + expect(result).not.toContain('?'); + expect(result).toBe('SELECT * FROM users WHERE id = $1'); + }); + + it('does not split decimal numbers into ?.?', () => { + const result = sanitize('SELECT * FROM t WHERE price = 19.99'); + expect(result).not.toBe('SELECT * FROM t WHERE price = ?.?'); + expect(result).toBe('SELECT * FROM t WHERE price = ?'); + }); + + it('does not leave minus sign when sanitizing negative numbers', () => { + const result = sanitize('SELECT * FROM t WHERE val = -500'); + expect(result).not.toBe('SELECT * FROM t WHERE val = -?'); + expect(result).toBe('SELECT * FROM t WHERE val = ?'); + }); + + it('handles exact queries from integration tests', () => { + expect( + sanitize( + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + ), + ).toBe( + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + ); + expect(sanitize('SELECT * from generate_series(1,1000) as x')).toBe('SELECT * from generate_series(?,?) as x'); + }); + }); + }); +}); From ffe161469cc3d6a630b3b1b1792ae4a1a9cfa16c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:30:08 +0100 Subject: [PATCH 38/43] chore(deps): bump next from 16.0.9 to 16.0.10 in /dev-packages/e2e-tests/test-applications/nextjs-16 (#18514) Bumps [next](https://github.com/vercel/next.js) from 16.0.9 to 16.0.10.
Release notes

Sourced from next's releases.

v16.0.10

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.0.9&new-version=16.0.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/e2e-tests/test-applications/nextjs-16/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index 45af070a0ea9..2357595bfea9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -27,7 +27,7 @@ "@sentry/core": "latest || *", "ai": "^3.0.0", "import-in-the-middle": "^2", - "next": "16.0.9", + "next": "16.0.10", "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^8", From e42e912c04d92a48d664c97e3f530f3fa18a6fbd Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 16 Dec 2025 11:39:52 +0100 Subject: [PATCH 39/43] feat(core): Export `captureException` and `captureMessage` parameter types (#18509) Add exports for `ExclusiveEventHintOrCaptureContext` and `CaptureContext` closes https://github.com/getsentry/sentry-javascript/issues/18503 --- packages/browser/src/exports.ts | 2 ++ packages/core/src/index.ts | 1 + packages/node-core/src/index.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 1b46687194da..3f25eda87fe5 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -16,6 +16,8 @@ export type { User, Session, ReportDialogOptions, + CaptureContext, + ExclusiveEventHintOrCaptureContext, } from '@sentry/core'; export type { BrowserOptions } from './client'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 387ba0aba4a2..b32568562e77 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -63,6 +63,7 @@ export { } from './utils/ai/providerSkip'; export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; +export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasSpansEnabled } from './utils/hasSpansEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index 7557d73c74a2..8ab20e9dfd4c 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -156,6 +156,8 @@ export type { User, Span, FeatureFlagsIntegration, + ExclusiveEventHintOrCaptureContext, + CaptureContext, } from '@sentry/core'; export { logger }; From 4068b570e06208a791f1e885680747694e13a73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 16 Dec 2025 12:28:50 +0100 Subject: [PATCH 40/43] test(cloudflare-mcp): Pin mcp sdk to 1.24.0 (#18524) Our CI was failing with v1.25.0. Pinned to 1.24.0. More info: https://github.com/modelcontextprotocol/typescript-sdk/issues/1302 Closes #18525 (added automatically) --- .../e2e-tests/test-applications/cloudflare-mcp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json index 9ec5f1ab2e7f..ada6cf527f9b 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json @@ -15,7 +15,7 @@ "test:dev": "TEST_ENV=development playwright test" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.22.0", + "@modelcontextprotocol/sdk": "1.24.0", "@sentry/cloudflare": "latest || *", "agents": "^0.2.23", "zod": "^3.25.76" From a88286282c2b8f9a206f441e933a35581607f700 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:37:08 +0100 Subject: [PATCH 41/43] chore(deps): bump next from 14.2.32 to 14.2.35 in /dev-packages/e2e-tests/test-applications/nextjs-orpc (#18520) Bumps [next](https://github.com/vercel/next.js) from 14.2.32 to 14.2.35.
Release notes

Sourced from next's releases.

v14.2.35

Please see the Next.js Security Update for information about this security patch.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=14.2.32&new-version=14.2.35)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/nextjs-orpc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json index 6496e6c60343..7e32f562916a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json @@ -19,7 +19,7 @@ "@sentry/nextjs": "latest || *", "@orpc/server": "latest", "@orpc/client": "latest", - "next": "14.2.32", + "next": "14.2.35", "react": "18.3.1", "react-dom": "18.3.1", "server-only": "^0.0.1" From 6f0b8b427ad76b181c94a1b68a068557ddaba1e0 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 16 Dec 2025 14:46:44 +0100 Subject: [PATCH 42/43] feat(core): Add additional exports for `captureException` and `captureMessage` parameter types (#18521) Extends these exports to node, cf and bun ref https://github.com/getsentry/sentry-javascript/pull/18509 Closes #18522 (added automatically) --- packages/bun/src/index.ts | 2 ++ packages/cloudflare/src/index.ts | 2 ++ packages/node/src/index.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 09fff4dc5f6a..835105527b1e 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -17,6 +17,8 @@ export type { User, FeatureFlagsIntegration, Metric, + ExclusiveEventHintOrCaptureContext, + CaptureContext, } from '@sentry/core'; export { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 827c45327689..33572c81714d 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -17,6 +17,8 @@ export type { Thread, User, Metric, + ExclusiveEventHintOrCaptureContext, + CaptureContext, } from '@sentry/core'; export type { CloudflareOptions } from './client'; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9d5941c41c8e..bb655b87fc42 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -159,6 +159,8 @@ export type { Span, Metric, FeatureFlagsIntegration, + ExclusiveEventHintOrCaptureContext, + CaptureContext, } from '@sentry/core'; export { From 47e82e330c9ee086ab022a9cdf0291eac2070888 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 16 Dec 2025 15:29:19 +0100 Subject: [PATCH 43/43] meta(changelog): Update changelog for 10.31.0 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a4f2bc7e65..d686b7b33ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott + +## 10.31.0 + ### Important Changes - **feat(browser): Add support for GraphQL persisted operations ([#18505](https://github.com/getsentry/sentry-javascript/pull/18505))** @@ -27,9 +31,46 @@ Additionally, the `graphql.document` attribute format has changed to align with ### Other Changes -- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- feat(node): Support `propagateTraceparent` option ([#18476](https://github.com/getsentry/sentry-javascript/pull/18476)) +- feat(bun): Expose spotlight option in TypeScript ([#18436](https://github.com/getsentry/sentry-javascript/pull/18436)) +- feat(core): Add additional exports for `captureException` and `captureMessage` parameter types ([#18521](https://github.com/getsentry/sentry-javascript/pull/18521)) +- feat(core): Export `captureException` and `captureMessage` parameter types ([#18509](https://github.com/getsentry/sentry-javascript/pull/18509)) +- feat(core): Parse individual cookies from cookie header ([#18325](https://github.com/getsentry/sentry-javascript/pull/18325)) +- feat(node): Add instrument OpenAI export to node ([#18461](https://github.com/getsentry/sentry-javascript/pull/18461)) +- feat(nuxt): Bump `@sentry/vite-plugin` and `@sentry/rollup-plugin` to 4.6.1 ([#18349](https://github.com/getsentry/sentry-javascript/pull/18349)) +- feat(profiling): Add support for Node v24 in the prune script ([#18447](https://github.com/getsentry/sentry-javascript/pull/18447)) +- feat(tracing): strip inline media from messages ([#18413](https://github.com/getsentry/sentry-javascript/pull/18413)) +- feat(node): Add ESM support for postgres.js instrumentation ([#17961](https://github.com/getsentry/sentry-javascript/pull/17961)) +- fix(browser): Stringify span context in linked traces log statement ([#18376](https://github.com/getsentry/sentry-javascript/pull/18376)) +- fix(google-cloud-serverless): Move @types/express to optional peerDeps ([#18452](https://github.com/getsentry/sentry-javascript/pull/18452)) +- fix(node-core): passthrough node-cron context ([#17835](https://github.com/getsentry/sentry-javascript/pull/17835)) +- fix(tanstack-router): Check for `fromLocation` existence before reporting pageload ([#18463](https://github.com/getsentry/sentry-javascript/pull/18463)) +- fix(tracing): add system prompt, model to google genai ([#18424](https://github.com/getsentry/sentry-javascript/pull/18424)) +- fix(tracing): Set span operations for AI spans with model ID only ([#18471](https://github.com/getsentry/sentry-javascript/pull/18471)) +- ref(browser): Improve profiling debug statement ([#18507](https://github.com/getsentry/sentry-javascript/pull/18507)) + +
+ Internal Changes + +- chore: Add external contributor to CHANGELOG.md ([#18473](https://github.com/getsentry/sentry-javascript/pull/18473)) +- chore: upgrade Playwright to ~1.56.0 for WSL2 compatibility ([#18468](https://github.com/getsentry/sentry-javascript/pull/18468)) +- chore(bugbot): Add testing conventions code review rules ([#18433](https://github.com/getsentry/sentry-javascript/pull/18433)) +- chore(deps): bump next from 14.2.25 to 14.2.35 in /dev-packages/e2e-tests/test-applications/create-next-app ([#18494](https://github.com/getsentry/sentry-javascript/pull/18494)) +- chore(deps): bump next from 14.2.32 to 14.2.35 in /dev-packages/e2e-tests/test-applications/nextjs-orpc ([#18520](https://github.com/getsentry/sentry-javascript/pull/18520)) +- chore(deps): bump next from 14.2.32 to 14.2.35 in /dev-packages/e2e-tests/test-applications/nextjs-pages-dir ([#18496](https://github.com/getsentry/sentry-javascript/pull/18496)) +- chore(deps): bump next from 15.5.7 to 15.5.9 in /dev-packages/e2e-tests/test-applications/nextjs-15 ([#18482](https://github.com/getsentry/sentry-javascript/pull/18482)) +- chore(deps): bump next from 15.5.7 to 15.5.9 in /dev-packages/e2e-tests/test-applications/nextjs-15-intl ([#18483](https://github.com/getsentry/sentry-javascript/pull/18483)) +- chore(deps): bump next from 16.0.7 to 16.0.9 in /dev-packages/e2e-tests/test-applications/nextjs-16 ([#18480](https://github.com/getsentry/sentry-javascript/pull/18480)) +- chore(deps): bump next from 16.0.7 to 16.0.9 in /dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents ([#18479](https://github.com/getsentry/sentry-javascript/pull/18479)) +- chore(deps): bump next from 16.0.7 to 16.0.9 in /dev-packages/e2e-tests/test-applications/nextjs-16-tunnel ([#18481](https://github.com/getsentry/sentry-javascript/pull/18481)) +- chore(deps): bump next from 16.0.9 to 16.0.10 in /dev-packages/e2e-tests/test-applications/nextjs-16 ([#18514](https://github.com/getsentry/sentry-javascript/pull/18514)) +- chore(deps): bump next from 16.0.9 to 16.0.10 in /dev-packages/e2e-tests/test-applications/nextjs-16-tunnel ([#18487](https://github.com/getsentry/sentry-javascript/pull/18487)) +- chore(tests): Added test variant flag ([#18458](https://github.com/getsentry/sentry-javascript/pull/18458)) +- test(cloudflare-mcp): Pin mcp sdk to 1.24.0 ([#18524](https://github.com/getsentry/sentry-javascript/pull/18524)) + +
-Work in this release was contributed by @sebws. Thank you for your contribution! +Work in this release was contributed by @sebws and @TBeeren. Thank you for your contributions! ## 10.30.0