From 593d5b3389cf326ca41a6f1242d2a7ffb683e680 Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Wed, 21 May 2025 10:05:10 +0200 Subject: [PATCH 01/26] test(react-router): Add and fix tests for pre rendered routes (#16346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was confirmed that static pre-renders don't pick up Sentry trace meta tags — this just adds a quick test. More context: https://linear.app/getsentry/issue/JS-392/rr7-evaluate-static-pre-rendering P.S. Not a priority, but for some reason, I had to add a trailing / to fetch the correct transactions and pass the tests. I’ll look into why this is happening for transaction route names and whether this behavior is expected in a later PR. --------- Co-authored-by: Charly Gomez --- .../react-router-7-framework/react-router.config.ts | 3 +-- .../tests/performance/pageload.client.test.ts | 9 ++++----- .../tests/performance/trace-propagation.test.ts | 7 +++++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts index 73b647e4eea6..bb1f96469dd2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts @@ -2,6 +2,5 @@ import type { Config } from '@react-router/dev/config'; export default { ssr: true, - // todo: check why this messes up client tracing in tests - // prerender: ['/performance/static'], + prerender: ['/performance/static'], } satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts index e283ea522c4a..b18ae44e0e71 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts @@ -5,7 +5,7 @@ import { APP_NAME } from '../constants'; test.describe('client - pageload performance', () => { test('should send pageload transaction', async ({ page }) => { const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance'; + return transactionEvent.transaction === '/performance/'; }); await page.goto(`/performance`); @@ -29,7 +29,7 @@ test.describe('client - pageload performance', () => { spans: expect.any(Array), start_timestamp: expect.any(Number), timestamp: expect.any(Number), - transaction: '/performance', + transaction: '/performance/', type: 'transaction', transaction_info: { source: 'url' }, measurements: expect.any(Object), @@ -103,10 +103,9 @@ test.describe('client - pageload performance', () => { }); }); - // todo: this page is currently not prerendered (see react-router.config.ts) test('should send pageload transaction for prerendered pages', async ({ page }) => { const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance/static'; + return transactionEvent.transaction === '/performance/static/'; }); await page.goto(`/performance/static`); @@ -114,7 +113,7 @@ test.describe('client - pageload performance', () => { const transaction = await txPromise; expect(transaction).toMatchObject({ - transaction: '/performance/static', + transaction: '/performance/static/', contexts: { trace: { span_id: expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts index 6a9623171236..7562297b2d4d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts @@ -33,4 +33,11 @@ test.describe('Trace propagation', () => { expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id); expect(clientTx.contexts?.trace?.parent_span_id).toBe(serverTx.contexts?.trace?.span_id); }); + + test('should not have trace connection for prerendered pages', async ({ page }) => { + await page.goto('/performance/static'); + + const sentryTraceElement = await page.$('meta[name="sentry-trace"]'); + expect(sentryTraceElement).toBeNull(); + }); }); From 1eecb02e320f4ff52ceceb092b8361d78cfa42d6 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 21 May 2025 15:04:23 +0200 Subject: [PATCH 02/26] doc(node): Fix `ignoreIncomingRequestBody` JSDoc (#16331) This PR just fixes incorrect JSDoc where I missed a mix-up of incoming and outgoing requests for the `ignoreIncomingRequestBody` option while reviewing. Raised in https://github.com/getsentry/sentry-docs/pull/13698/files#r2088760944 --- .../node/src/integrations/http/SentryHttpInstrumentation.ts | 4 ++-- packages/node/src/integrations/http/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 03c1260a2e5a..ac82134fd07f 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -77,8 +77,8 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & { * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`. * This can be useful for long running requests where the body is not needed and we want to avoid capturing it. * - * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request. - * @param request Contains the {@type RequestOptions} object used to make the outgoing request. + * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request. + * @param request Contains the {@type RequestOptions} object used to make the incoming request. */ ignoreIncomingRequestBody?: (url: string, request: http.RequestOptions) => boolean; diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 72326e21e6f1..7c49b6a6670c 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -85,8 +85,8 @@ interface HttpOptions { * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`. * This can be useful for long running requests where the body is not needed and we want to avoid capturing it. * - * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request. - * @param request Contains the {@type RequestOptions} object used to make the outgoing request. + * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request. + * @param request Contains the {@type RequestOptions} object used to make the incoming request. */ ignoreIncomingRequestBody?: (url: string, request: RequestOptions) => boolean; From 0a7053b4ae3e298e46244ba13f40de1fa8e1225b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 21 May 2025 16:36:13 +0200 Subject: [PATCH 03/26] test(remix): Pin shopify package versions for remix (#16357) --- .../e2e-tests/test-applications/remix-hydrogen/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 48f0becca3f7..6890e46da127 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -20,7 +20,7 @@ "@sentry/cloudflare": "latest || *", "@sentry/remix": "latest || *", "@sentry/vite-plugin": "^3.1.2", - "@shopify/hydrogen": "^2025.1.0", + "@shopify/hydrogen": "2025.4.0", "@shopify/remix-oxygen": "^2.0.10", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", @@ -34,7 +34,7 @@ "@remix-run/dev": "^2.15.2", "@remix-run/eslint-config": "^2.15.2", "@sentry-internal/test-utils": "link:../../../test-utils", - "@shopify/cli": "^3.74.1", + "@shopify/cli": "3.74.1", "@shopify/hydrogen-codegen": "^0.3.1", "@shopify/mini-oxygen": "3.2.0", "@shopify/oxygen-workers-types": "^4.1.2", From 9a185eec3b22426fde16814f817c88e8789f0290 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 21 May 2025 16:37:06 +0200 Subject: [PATCH 04/26] fix(browser): Ensure logs are flushed when sendClientReports=false (#16351) This was tied together and could theoretically result in logs not being flushed correctly, when users opt-out of client reports. Noticed this while scanning over the browser client code :) --- packages/browser/src/client.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 73cbd55d42db..c0841d161ed6 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -85,41 +85,41 @@ export class BrowserClient extends Client { super(opts); - // eslint-disable-next-line @typescript-eslint/no-this-alias - const client = this; - const { sendDefaultPii, _experiments } = client._options; + const { sendDefaultPii, sendClientReports, _experiments } = this._options; const enableLogs = _experiments?.enableLogs; - if (opts.sendClientReports && WINDOW.document) { + if (WINDOW.document && (sendClientReports || enableLogs)) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { - this._flushOutcomes(); + if (sendClientReports) { + this._flushOutcomes(); + } if (enableLogs) { - _INTERNAL_flushLogsBuffer(client); + _INTERNAL_flushLogsBuffer(this); } } }); } if (enableLogs) { - client.on('flush', () => { - _INTERNAL_flushLogsBuffer(client); + this.on('flush', () => { + _INTERNAL_flushLogsBuffer(this); }); - client.on('afterCaptureLog', () => { - if (client._logFlushIdleTimeout) { - clearTimeout(client._logFlushIdleTimeout); + this.on('afterCaptureLog', () => { + if (this._logFlushIdleTimeout) { + clearTimeout(this._logFlushIdleTimeout); } - client._logFlushIdleTimeout = setTimeout(() => { - _INTERNAL_flushLogsBuffer(client); + this._logFlushIdleTimeout = setTimeout(() => { + _INTERNAL_flushLogsBuffer(this); }, DEFAULT_FLUSH_INTERVAL); }); } if (sendDefaultPii) { - client.on('postprocessEvent', addAutoIpAddressToUser); - client.on('beforeSendSession', addAutoIpAddressToSession); + this.on('postprocessEvent', addAutoIpAddressToUser); + this.on('beforeSendSession', addAutoIpAddressToSession); } } From b4bd21fde0054285d6123e0c4ec351a28885e9b3 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 21 May 2025 16:38:21 +0200 Subject: [PATCH 05/26] feat(node): Do not add HTTP & fetch span instrumentation if tracing is disabled (#15730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of https://github.com/getsentry/sentry-javascript/issues/15725, this PR stops adding the `HttpInstrumentation` / `UndiciInstrumentation` if tracing is not enabled. I _think_ this should not really be breaking, as the integrations should not do anything else, if tracing is disabled 🤔 This only skips the http instrumentation without spans on node 22+, as only there our own diagnostics-channel based instrumentation can cover everything we need. --- packages/node/src/integrations/http/index.ts | 21 ++++++++++++++++--- .../node/src/integrations/node-fetch/index.ts | 8 ++++--- packages/node/test/integrations/http.test.ts | 15 +++++++++++-- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 7c49b6a6670c..96e9b84315be 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -3,7 +3,8 @@ import { diag } from '@opentelemetry/api'; import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import type { Span } from '@sentry/core'; -import { defineIntegration, getClient } from '@sentry/core'; +import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core'; +import { NODE_VERSION } from '../../nodeVersion'; import { generateInstrumentOnce } from '../../otel/instrument'; import type { NodeClient } from '../../sdk/client'; import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-module'; @@ -159,8 +160,22 @@ export const instrumentOtelHttp = generateInstrumentOnce = {}): boolean { // If `spans` is passed in, it takes precedence - // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` - return typeof options.spans === 'boolean' ? options.spans : !clientOptions.skipOpenTelemetrySetup; + // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` or spans are not enabled + if (typeof options.spans === 'boolean') { + return options.spans; + } + + if (clientOptions.skipOpenTelemetrySetup) { + return false; + } + + // IMPORTANT: We only disable span instrumentation when spans are not enabled _and_ we are on Node 22+, + // as otherwise the necessary diagnostics channel is not available yet + if (!hasSpansEnabled(clientOptions) && NODE_VERSION.major >= 22) { + return false; + } + + return true; } /** diff --git a/packages/node/src/integrations/node-fetch/index.ts b/packages/node/src/integrations/node-fetch/index.ts index d62d1e7c88c2..e85ce34dfb35 100644 --- a/packages/node/src/integrations/node-fetch/index.ts +++ b/packages/node/src/integrations/node-fetch/index.ts @@ -1,7 +1,7 @@ import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation-undici'; import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'; import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { defineIntegration, getClient, hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { generateInstrumentOnce } from '../../otel/instrument'; import type { NodeClient } from '../../sdk/client'; import type { NodeClientOptions } from '../../types'; @@ -86,8 +86,10 @@ function getAbsoluteUrl(origin: string, path: string = '/'): string { function _shouldInstrumentSpans(options: NodeFetchOptions, clientOptions: Partial = {}): boolean { // If `spans` is passed in, it takes precedence - // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` - return typeof options.spans === 'boolean' ? options.spans : !clientOptions.skipOpenTelemetrySetup; + // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` or spans are not enabled + return typeof options.spans === 'boolean' + ? options.spans + : !clientOptions.skipOpenTelemetrySetup && hasSpansEnabled(clientOptions); } function getConfigWithDefaults(options: Partial = {}): UndiciInstrumentationConfig { diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 6d4bea24661f..89052a348ea4 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,19 +1,30 @@ import { describe, expect, it } from 'vitest'; import { _shouldInstrumentSpans } from '../../src/integrations/http'; +import { conditionalTest } from '../helpers/conditional'; describe('httpIntegration', () => { describe('_shouldInstrumentSpans', () => { it.each([ - [{}, {}, true], [{ spans: true }, {}, true], [{ spans: false }, {}, false], [{ spans: true }, { skipOpenTelemetrySetup: true }, true], [{ spans: false }, { skipOpenTelemetrySetup: true }, false], [{}, { skipOpenTelemetrySetup: true }, false], - [{}, { skipOpenTelemetrySetup: false }, true], + [{}, { tracesSampleRate: 0, skipOpenTelemetrySetup: true }, false], + [{}, { tracesSampleRate: 0 }, true], ])('returns the correct value for options=%j and clientOptions=%j', (options, clientOptions, expected) => { const actual = _shouldInstrumentSpans(options, clientOptions); expect(actual).toBe(expected); }); + + conditionalTest({ min: 22 })('returns false without tracesSampleRate on Node >=22', () => { + const actual = _shouldInstrumentSpans({}, {}); + expect(actual).toBe(false); + }); + + conditionalTest({ max: 21 })('returns true without tracesSampleRate on Node <22', () => { + const actual = _shouldInstrumentSpans({}, {}); + expect(actual).toBe(true); + }); }); }); From 48f1a4f0c240d781bad3461bd26a683c5512e7ba Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 22 May 2025 09:13:51 +0200 Subject: [PATCH 06/26] fix(cloudflare): Capture exceptions thrown in hono (#16355) Our cloudflare SDK captures fetch exceptions in the catch block. But hono never reaches [this block](https://github.com/getsentry/sentry-javascript/blob/a874d763171289b5556973a4842f580d4bbb7ec0/packages/cloudflare/src/request.ts#L85-L90) where we send exceptions. hono processes errors with their `onError` function (or `errorHandler`) and we need to capture the exception there. This PR wraps the [`errorHandler` of hono](https://github.com/honojs/hono/blob/bb7afaccfd5b6b514da356e069f27eb5ccfc0e3b/src/hono-base.ts#L36 ). Will create an E2E test in another PR. --- packages/cloudflare/src/handler.ts | 21 ++++++ packages/cloudflare/test/handler.test.ts | 91 ++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 62956cff62cf..d3d1f80dbbd5 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -26,6 +26,7 @@ import { init } from './sdk'; * @param handler {ExportedHandler} The handler to wrap. * @returns The wrapped handler. */ +// eslint-disable-next-line complexity export function withSentry( optionsCallback: (env: Env) => CloudflareOptions, handler: ExportedHandler, @@ -47,6 +48,26 @@ export function withSentry>) { diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 6ae688f316f9..bced0fdbe277 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -7,6 +7,17 @@ import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../src/client'; import { withSentry } from '../src/handler'; +import { markAsInstrumented } from '../src/instrument'; + +// Custom type for hono-like apps (cloudflare handlers) that include errorHandler and onError +type HonoLikeApp = ExportedHandler< + Env, + QueueHandlerMessage, + CfHostMetadata +> & { + onError?: () => void; + errorHandler?: (err: Error) => Response; +}; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', @@ -931,6 +942,86 @@ describe('withSentry', () => { }); }); }); + + describe('hono errorHandler', () => { + test('captures errors handled by the errorHandler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test hono error'); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler(err: Error) { + return new Response(`Error: ${err.message}`, { status: 500 }); + }, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // simulates hono's error handling + const errorHandlerResponse = honoApp.errorHandler?.(error); + + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, { + mechanism: { handled: false, type: 'cloudflare' }, + }); + expect(errorHandlerResponse?.status).toBe(500); + }); + + test('preserves the original errorHandler functionality', async () => { + const originalErrorHandlerSpy = vi.fn().mockImplementation((err: Error) => { + return new Response(`Error: ${err.message}`, { status: 500 }); + }); + + const error = new Error('test hono error'); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler: originalErrorHandlerSpy, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // Call the errorHandler directly to simulate Hono's error handling + const errorHandlerResponse = honoApp.errorHandler?.(error); + + expect(originalErrorHandlerSpy).toHaveBeenCalledTimes(1); + expect(originalErrorHandlerSpy).toHaveBeenLastCalledWith(error); + expect(errorHandlerResponse?.status).toBe(500); + }); + + test('does not instrument an already instrumented errorHandler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test hono error'); + + // Create a handler with an errorHandler that's already been instrumented + const originalErrorHandler = (err: Error) => { + return new Response(`Error: ${err.message}`, { status: 500 }); + }; + + // Mark as instrumented before wrapping + markAsInstrumented(originalErrorHandler); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler: originalErrorHandler, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // The errorHandler should not have been wrapped again + honoApp.errorHandler?.(error); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + }); }); function createMockExecutionContext(): ExecutionContext { From 952373e43ba06f951dae87d7e9e06b4834e52e3b Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 23 May 2025 10:11:29 +0200 Subject: [PATCH 07/26] test(cloudflare): Add test for hono cloudflare (#16360) Adding a test to check that the build and types work (just like with cloudflare-wokers). --- .../cloudflare-hono/package.json | 29 ++++++++ .../cloudflare-hono/src/env.d.ts | 6 ++ .../cloudflare-hono/src/index.ts | 34 +++++++++ .../cloudflare-hono/test/env.d.ts | 4 + .../cloudflare-hono/test/index.test.ts | 74 +++++++++++++++++++ .../cloudflare-hono/test/tsconfig.json | 8 ++ .../cloudflare-hono/tsconfig.json | 16 ++++ .../cloudflare-hono/vitest.config.ts | 12 +++ .../cloudflare-hono/wrangler.toml | 7 ++ 9 files changed, 190 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json new file mode 100644 index 000000000000..b9667aeef85f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -0,0 +1,29 @@ +{ + "name": "cloudflare-hono", + "scripts": { + "dev": "wrangler dev", + "build": "wrangler deploy --dry-run --var E2E_TEST_DSN=$E2E_TEST_DSN", + "test": "vitest", + "typecheck": "tsc --noEmit", + "cf-typegen": "wrangler types --env-interface CloudflareBindings", + "test:build": "pnpm install && pnpm build", + "//": "Just checking if it builds correctly and types don't break", + "test:assert": "pnpm typecheck" + }, + "dependencies": { + "@sentry/cloudflare": "latest || *", + "hono": "4.7.10" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.8.31", + "@cloudflare/workers-types": "^4.20250521.0", + "vitest": "3.1.0", + "wrangler": "^4.16.0" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts new file mode 100644 index 000000000000..0c9e04919e42 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts @@ -0,0 +1,6 @@ +// Generated by Wrangler on Mon Jul 29 2024 21:44:31 GMT-0400 (Eastern Daylight Time) +// by running `wrangler types` + +interface Env { + E2E_TEST_DSN: ''; +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts new file mode 100644 index 000000000000..7cd667c72408 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts @@ -0,0 +1,34 @@ +import { Hono } from 'hono'; +import * as Sentry from '@sentry/cloudflare'; + +const app = new Hono(); + +app.get('/', ctx => { + return ctx.json({ message: 'Welcome to Hono API' }); +}); + +app.get('/hello/:name', ctx => { + const name = ctx.req.param('name'); + return ctx.json({ message: `Hello, ${name}!` }); +}); + +app.get('/error', () => { + throw new Error('This is a test error'); +}); + +app.onError((err, ctx) => { + console.error(`Error occured: ${err.message}`); + return ctx.json({ error: err.message }, 500); +}); + +app.notFound(ctx => { + return ctx.json({ message: 'Not Found' }, 404); +}); + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env?.E2E_TEST_DSN, + tracesSampleRate: 1.0, + }), + app, +); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts new file mode 100644 index 000000000000..3b9f82b9628f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts @@ -0,0 +1,4 @@ +declare module 'cloudflare:test' { + // ProvidedEnv controls the type of `import("cloudflare:test").env` + interface ProvidedEnv extends Env {} +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts new file mode 100644 index 000000000000..2ae93f9b1fd5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; +import app from '../src/index'; +import { SELF, createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test'; + +describe('Hono app on Cloudflare Workers', () => { + describe('Unit Tests', () => { + it('should return welcome message', async () => { + const res = await app.request('/', {}, env); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ message: 'Welcome to Hono API' }); + }); + + it('should greet a user with their name', async () => { + const res = await app.request('/hello/tester', {}, env); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ message: 'Hello, tester!' }); + }); + + it('should handle errors with custom error handler', async () => { + const res = await app.request('/error', {}, env); + expect(res.status).toBe(500); + const data = await res.json(); + expect(data).toHaveProperty('error', 'This is a test error'); + }); + + it('should handle 404 with custom not found handler', async () => { + const res = await app.request('/non-existent-route', {}, env); + expect(res.status).toBe(404); + const data = await res.json(); + expect(data).toEqual({ message: 'Not Found' }); + }); + }); + + // Integration test style with worker.fetch + describe('Integration Tests', () => { + it('should fetch the root endpoint', async () => { + // Create request and context + const request = new Request('http://localhost/'); + const ctx = createExecutionContext(); + + const response = await app.fetch(request, env, ctx); + + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toEqual({ message: 'Welcome to Hono API' }); + }); + + it('should handle a parameter route', async () => { + // Create request and context + const request = new Request('http://localhost/hello/cloudflare'); + const ctx = createExecutionContext(); + + const response = await app.fetch(request, env, ctx); + + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toEqual({ message: 'Hello, cloudflare!' }); + }); + + it('should handle errors gracefully', async () => { + const response = await SELF.fetch('http://localhost/error'); + + expect(response.status).toBe(500); + const data = await response.json(); + expect(data).toHaveProperty('error', 'This is a test error'); + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json new file mode 100644 index 000000000000..bc019a7e2bfb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] + }, + "include": ["./**/*.ts", "../src/env.d.ts"], + "exclude": [] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json new file mode 100644 index 000000000000..e2025cec5039 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "lib": [ + "ESNext" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, + "include": ["src/**/*"], + "exclude": ["test", "node_modules"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts new file mode 100644 index 000000000000..4466287fbe5b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config' + +export default defineWorkersProject(() => { + return { + test: { + globals: true, + poolOptions: { + workers: { wrangler: { configPath: './wrangler.toml' } }, + }, + }, + } +}) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml new file mode 100644 index 000000000000..9fdfb60c18b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml @@ -0,0 +1,7 @@ +name = "cloudflare-hono" +main = "src/index.ts" +compatibility_date = "2023-10-30" +compatibility_flags = ["nodejs_compat"] + +# [vars] +# E2E_TEST_DSN = "" From 3604a08a53d3127db1df9ea8b9d3df509a8c3f36 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 23 May 2025 16:47:02 +0100 Subject: [PATCH 08/26] feat(core): Allow re-use of `captureLog` (#16352) Co-authored-by: Abhijeet Prasad --- packages/core/src/logs/exports.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index a2451c6cc6c0..41f1155ee3e1 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -61,12 +61,25 @@ export function logAttributeToSerializedLogAttribute(value: unknown): Serialized } } +function defaultCaptureSerializedLog(client: Client, serializedLog: SerializedLog): void { + const logBuffer = _INTERNAL_getLogBuffer(client); + if (logBuffer === undefined) { + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [serializedLog]); + } else { + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [...logBuffer, serializedLog]); + if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) { + _INTERNAL_flushLogsBuffer(client, logBuffer); + } + } +} + /** * Captures a log event and sends it to Sentry. * * @param log - The log event to capture. * @param scope - A scope. Uses the current scope if not provided. * @param client - A client. Uses the current client if not provided. + * @param captureSerializedLog - A function to capture the serialized log. * * @experimental This method will experience breaking changes. This is not yet part of * the stable Sentry SDK API and can be changed or removed without warning. @@ -75,6 +88,7 @@ export function _INTERNAL_captureLog( beforeLog: Log, client: Client | undefined = getClient(), scope = getCurrentScope(), + captureSerializedLog: (client: Client, log: SerializedLog) => void = defaultCaptureSerializedLog, ): void { if (!client) { DEBUG_BUILD && logger.warn('No client available to capture log.'); @@ -151,15 +165,7 @@ export function _INTERNAL_captureLog( ), }; - const logBuffer = _INTERNAL_getLogBuffer(client); - if (logBuffer === undefined) { - GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [serializedLog]); - } else { - GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [...logBuffer, serializedLog]); - if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) { - _INTERNAL_flushLogsBuffer(client, logBuffer); - } - } + captureSerializedLog(client, serializedLog); client.emit('afterCaptureLog', log); } From 3d18c8e2f0bd6a6fe233a0ff6a450f9cb90cb901 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 24 May 2025 19:46:05 +0100 Subject: [PATCH 09/26] fix(node): Suppress Spotlight calls (#16380) Uses the established `suppressTracing` wrapper to hide Spotlight calls from OTEL too. --- packages/node/src/integrations/spotlight.ts | 85 ++++++++------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index 49a169076798..917fe073e194 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -1,6 +1,6 @@ import * as http from 'node:http'; import type { Client, Envelope, IntegrationFn } from '@sentry/core'; -import { defineIntegration, logger, serializeEnvelope } from '@sentry/core'; +import { defineIntegration, logger, serializeEnvelope, suppressTracing } from '@sentry/core'; type SpotlightConnectionOptions = { /** @@ -52,40 +52,40 @@ function connectToSpotlight(client: Client, options: Required { + const req = http.request( + { + method: 'POST', + path: spotlightUrl.pathname, + hostname: spotlightUrl.hostname, + port: spotlightUrl.port, + headers: { + 'Content-Type': 'application/x-sentry-envelope', + }, }, - }, - res => { - if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) { - // Reset failed requests counter on success - failedRequests = 0; - } - res.on('data', () => { - // Drain socket - }); - - res.on('end', () => { - // Drain socket - }); - res.setEncoding('utf8'); - }, - ); - - req.on('error', () => { - failedRequests++; - logger.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); + res => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) { + // Reset failed requests counter on success + failedRequests = 0; + } + res.on('data', () => { + // Drain socket + }); + + res.on('end', () => { + // Drain socket + }); + res.setEncoding('utf8'); + }, + ); + + req.on('error', () => { + failedRequests++; + logger.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); + }); + req.write(serializedEnvelope); + req.end(); }); - req.write(serializedEnvelope); - req.end(); }); } @@ -97,22 +97,3 @@ function parseSidecarUrl(url: string): URL | undefined { return undefined; } } - -type HttpRequestImpl = typeof http.request; -type WrappedHttpRequest = HttpRequestImpl & { __sentry_original__: HttpRequestImpl }; - -/** - * We want to get an unpatched http request implementation to avoid capturing our own calls. - */ -export function getNativeHttpRequest(): HttpRequestImpl { - const { request } = http; - if (isWrapped(request)) { - return request.__sentry_original__; - } - - return request; -} - -function isWrapped(impl: HttpRequestImpl): impl is WrappedHttpRequest { - return '__sentry_original__' in impl; -} From 72057b64864f0b9030266f28e465156424a06a94 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 26 May 2025 09:37:02 +0200 Subject: [PATCH 10/26] test(angular): Add Angular `20.0.0-rc.2` e2e test (#16364) adds an e2e test for Angular the latest Angular 20 RC -- to be promoted to Angular 20 stable once released --- .github/workflows/build.yml | 6 + .../angular-20/.editorconfig | 17 + .../test-applications/angular-20/.gitignore | 44 +++ .../test-applications/angular-20/.npmrc | 2 + .../test-applications/angular-20/README.md | 3 + .../test-applications/angular-20/angular.json | 87 +++++ .../test-applications/angular-20/package.json | 50 +++ .../angular-20/playwright.config.mjs | 8 + .../angular-20/public/favicon.ico | Bin 0 -> 15086 bytes .../angular-20/src/app/app.component.ts | 12 + .../angular-20/src/app/app.config.ts | 29 ++ .../angular-20/src/app/app.routes.ts | 42 +++ .../angular-20/src/app/cancel-guard.guard.ts | 5 + .../src/app/cancel/cancel.components.ts | 8 + .../component-tracking.components.ts | 21 ++ .../angular-20/src/app/home/home.component.ts | 26 ++ .../sample-component.components.ts | 12 + .../angular-20/src/app/user/user.component.ts | 25 ++ .../angular-20/src/index.html | 13 + .../test-applications/angular-20/src/main.ts | 15 + .../angular-20/src/styles.css | 1 + .../angular-20/start-event-proxy.mjs | 6 + .../angular-20/tests/errors.test.ts | 65 ++++ .../angular-20/tests/performance.test.ts | 326 ++++++++++++++++++ .../angular-20/tsconfig.app.json | 11 + .../angular-20/tsconfig.json | 27 ++ .../angular-20/tsconfig.spec.json | 10 + 27 files changed, 871 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/.editorconfig create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/README.md create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/angular.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/package.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/index.html create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/src/styles.css create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae4095e304ab..9577eb15aff7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -886,7 +886,13 @@ jobs: - uses: pnpm/action-setup@v4 with: version: 9.4.0 + - name: Set up Node for Angular 20 + if: matrix.test-application == 'angular-20' + uses: actions/setup-node@v4 + with: + node-version: '20.19.2' - name: Set up Node + if: matrix.test-application != 'angular-20' uses: actions/setup-node@v4 with: node-version-file: 'dev-packages/e2e-tests/package.json' diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.editorconfig b/dev-packages/e2e-tests/test-applications/angular-20/.editorconfig new file mode 100644 index 000000000000..f166060da1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.gitignore b/dev-packages/e2e-tests/test-applications/angular-20/.gitignore new file mode 100644 index 000000000000..315c644a53e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +test-results diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.npmrc b/dev-packages/e2e-tests/test-applications/angular-20/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/angular-20/README.md b/dev-packages/e2e-tests/test-applications/angular-20/README.md new file mode 100644 index 000000000000..5798a982a95c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/README.md @@ -0,0 +1,3 @@ +# Angular 20 + +E2E test app for Angular 20 and `@sentry/angular`. diff --git a/dev-packages/e2e-tests/test-applications/angular-20/angular.json b/dev-packages/e2e-tests/test-applications/angular-20/angular.json new file mode 100644 index 000000000000..09939b0f9b23 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/angular.json @@ -0,0 +1,87 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-20": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/angular-20", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-20:build:production" + }, + "development": { + "buildTarget": "angular-20:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"], + "scripts": [] + } + } + } + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json new file mode 100644 index 000000000000..a43fcaf412b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json @@ -0,0 +1,50 @@ +{ + "name": "angular-20", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "dev": "ng serve", + "proxy": "node start-event-proxy.mjs", + "preview": "http-server dist/angular-20/browser --port 8080", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "playwright test", + "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" + }, + "private": true, + "dependencies": { + "@angular/animations": "^20.0.0-rc.2", + "@angular/common": "^20.0.0-rc.2", + "@angular/compiler": "^20.0.0-rc.2", + "@angular/core": "^20.0.0-rc.2", + "@angular/forms": "^20.0.0-rc.2", + "@angular/platform-browser": "^20.0.0-rc.2", + "@angular/platform-browser-dynamic": "^20.0.0-rc.2", + "@angular/router": "^20.0.0-rc.2", + "@sentry/angular": "* || latest", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^20.0.0-rc.2", + "@angular/cli": "^20.0.0-rc.2", + "@angular/compiler-cli": "^20.0.0-rc.2", + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/jasmine": "~5.1.0", + "http-server": "^14.1.1", + "jasmine-core": "~5.4.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.8.3" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs new file mode 100644 index 000000000000..0845325879c9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm preview`, + port: 8080, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/public/favicon.ico b/dev-packages/e2e-tests/test-applications/angular-20/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts new file mode 100644 index 000000000000..e912fcc99b04 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, +}) +export class AppComponent { + title = 'angular-20'; +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts new file mode 100644 index 000000000000..f5cc30f3615b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts @@ -0,0 +1,29 @@ +import { + ApplicationConfig, + ErrorHandler, + inject, + provideAppInitializer, + provideZoneChangeDetection, +} from '@angular/core'; +import { Router, provideRouter } from '@angular/router'; + +import { TraceService, createErrorHandler } from '@sentry/angular'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + { + provide: ErrorHandler, + useValue: createErrorHandler(), + }, + { + provide: TraceService, + deps: [Router], + }, + provideAppInitializer(() => { + inject(TraceService); + }), + ], +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts new file mode 100644 index 000000000000..24bf8b769051 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts @@ -0,0 +1,42 @@ +import { Routes } from '@angular/router'; +import { cancelGuard } from './cancel-guard.guard'; +import { CancelComponent } from './cancel/cancel.components'; +import { ComponentTrackingComponent } from './component-tracking/component-tracking.components'; +import { HomeComponent } from './home/home.component'; +import { UserComponent } from './user/user.component'; + +export const routes: Routes = [ + { + path: 'users/:id', + component: UserComponent, + }, + { + path: 'home', + component: HomeComponent, + }, + { + path: 'cancel', + component: CancelComponent, + canActivate: [cancelGuard], + }, + { + path: 'component-tracking', + component: ComponentTrackingComponent, + }, + { + path: 'redirect1', + redirectTo: '/redirect2', + }, + { + path: 'redirect2', + redirectTo: '/redirect3', + }, + { + path: 'redirect3', + redirectTo: '/users/456', + }, + { + path: '**', + redirectTo: 'home', + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts new file mode 100644 index 000000000000..16ec4a2ab164 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts @@ -0,0 +1,5 @@ +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; + +export const cancelGuard: CanActivateFn = (_next: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { + return false; +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts new file mode 100644 index 000000000000..b6ee1876e035 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-cancel', + standalone: true, + template: `
`, +}) +export class CancelComponent {} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts new file mode 100644 index 000000000000..a82e5b1acce6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts @@ -0,0 +1,21 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular'; +import { SampleComponent } from '../sample-component/sample-component.components'; + +@Component({ + selector: 'app-component-tracking', + standalone: true, + imports: [TraceModule, SampleComponent], + template: ` + + + `, +}) +@TraceClass({ name: 'ComponentTrackingComponent' }) +export class ComponentTrackingComponent implements OnInit, AfterViewInit { + @TraceMethod({ name: 'ngOnInit' }) + ngOnInit() {} + + @TraceMethod() + ngAfterViewInit() {} +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts new file mode 100644 index 000000000000..033174fb0d8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [RouterLink], + template: ` +
+

Welcome to Sentry's Angular 20 E2E test app

+ + +
+ `, +}) +export class HomeComponent { + throwError() { + throw new Error('Error thrown from Angular 20 E2E test app'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts new file mode 100644 index 000000000000..da09425c7565 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-sample-component', + standalone: true, + template: `
Component
`, +}) +export class SampleComponent implements OnInit { + ngOnInit() { + console.log('SampleComponent'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts new file mode 100644 index 000000000000..db02568d395f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts @@ -0,0 +1,25 @@ +import { AsyncPipe } from '@angular/common'; +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, map } from 'rxjs'; + +@Component({ + selector: 'app-user', + standalone: true, + imports: [AsyncPipe], + template: ` +

Hello User {{ userId$ | async }}

+ + `, +}) +export class UserComponent { + public userId$: Observable; + + constructor(private route: ActivatedRoute) { + this.userId$ = this.route.paramMap.pipe(map(params => params.get('id') || 'UNKNOWN USER')); + } + + throwError() { + throw new Error('Error thrown from user page'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/index.html b/dev-packages/e2e-tests/test-applications/angular-20/src/index.html new file mode 100644 index 000000000000..0f546ff0114e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/index.html @@ -0,0 +1,13 @@ + + + + + Angular 20 + + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/main.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/main.ts new file mode 100644 index 000000000000..a0b841afc333 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/main.ts @@ -0,0 +1,15 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +import * as Sentry from '@sentry/angular'; + +Sentry.init({ + // Cannot use process.env here, so we hardcode the DSN + dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + tracesSampleRate: 1.0, + integrations: [Sentry.browserTracingIntegration({})], + tunnel: `http://localhost:3031/`, // proxy server +}); + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/styles.css b/dev-packages/e2e-tests/test-applications/angular-20/src/styles.css new file mode 100644 index 000000000000..90d4ee0072ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs new file mode 100644 index 000000000000..58559fb2f8b8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'angular-20', +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts new file mode 100644 index 000000000000..98ae26e195cc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('angular-20', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Angular 20 E2E test app', + mechanism: { + type: 'angular', + handled: false, + }, + }, + ], + }, + transaction: '/home/', + }); +}); + +test('assigns the correct transaction value after a navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorPromise = waitForError('angular-20', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + await page.locator('#navLink').click(); + + const [_, error] = await Promise.all([page.locator('#userErrorBtn').click(), errorPromise]); + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from user page', + mechanism: { + type: 'angular', + handled: false, + }, + }, + ], + }, + transaction: '/users/:id/', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts new file mode 100644 index 000000000000..f790cb10d180 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts @@ -0,0 +1,326 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, pageloadTxn, navigationTxn] = await Promise.all([ + page.locator('#navLink').click(), + pageloadTxnPromise, + navigationTxnPromise, + ]); + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('groups redirects within one navigation root span', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#redirectLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/redirect1'); +}); + +test.describe('finish routing span', () => { + test('finishes routing span on navigation cancel', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#cancelLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/cancel', + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/cancel'); + }); + + test('finishes routing span on navigation error', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#nonExistentLink').click(), navigationTxnPromise]); + + const nonExistentRoute = '/non-existent'; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: nonExistentRoute, + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe(nonExistentRoute); + }); +}); + +test.describe('TraceDirective', () => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const traceDirectiveSpans = navigationTxn.spans?.filter( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', + ); + + expect(traceDirectiveSpans).toHaveLength(2); + expect(traceDirectiveSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // custom component name passed to trace directive + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // fallback selector name + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ]), + ); + }); +}); + +test.describe('TraceClass Decorator', () => { + test('adds init span for decorated class', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const classDecoratorSpan = navigationTxn.spans?.find( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', + ); + + expect(classDecoratorSpan).toBeDefined(); + expect(classDecoratorSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + }, + description: '', + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_class_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); + +test.describe('TraceMethod Decorator', () => { + test('adds name to span description of decorated method `ngOnInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngOnInit'); + + expect(ngInitSpan).toBeDefined(); + expect(ngInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngOnInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngOnInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); + + test('adds fallback name to span description of decorated method `ngAfterViewInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngAfterViewInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngAfterViewInit'); + + expect(ngAfterViewInitSpan).toBeDefined(); + expect(ngAfterViewInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngAfterViewInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngAfterViewInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json new file mode 100644 index 000000000000..8886e903f8d0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json new file mode 100644 index 000000000000..5525117c6744 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json new file mode 100644 index 000000000000..e00e30e6d4fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} From 349d7d0ed03a9e087f507c7292b80e3601bab767 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 26 May 2025 13:18:08 +0200 Subject: [PATCH 11/26] fix(browser): Move `browserTracingIntegration` code to `setup` hook (#16386) This PR removes the warning for multiple browserTracingIntegrations being setup. We realised this warning was actually just a symptom of us running a bunch of the integration code in the function itself, not int `setup()`. This lead to stuff running twice when users passed in a custom integration in addition to a default one, because the logic is basically: ```js const defaultIntergations = [browserTracingIntegration()]; const usedDefinedIntegrations = [browserTracingIntegration()]; // at this point, the function was already executed twice, leading to side effects even without calling `init`! ``` Now, we move all the logic to setup/setupAfterAll, so this should be safe. This means that the integration will be deduped anyhow (I added a test to make sure this is the case), and we no longer need the warning. fixes https://github.com/getsentry/sentry-javascript/issues/16369 --- .../multiple-integrations/init.js | 9 --- .../multiple-integrations/test.ts | 21 ------ .../tracing/request/fetch-immediate/init.js | 14 ++++ .../tracing/request/fetch-immediate/test.ts | 40 ++++++++++ .../src/tracing/browserTracingIntegration.ts | 75 +++++++++---------- packages/core/test/lib/integration.test.ts | 31 +++++++- .../react/src/reactrouterv6-compat-utils.tsx | 4 +- 7 files changed, 118 insertions(+), 76 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js deleted file mode 100644 index 6d4dd43801b8..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.browserTracingIntegration()], - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts deleted file mode 100644 index 1789bdf76c12..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('warns if multiple integrations are used', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const msgs: string[] = []; - - page.on('console', msg => { - msgs.push(msg.text()); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.goto(url); - - expect(msgs).toEqual(['Multiple browserTracingIntegration instances are not supported.']); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js new file mode 100644 index 000000000000..0a301839c169 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + autoSessionTracking: false, +}); + +// fetch directly after init +fetch('http://sentry-test-site.example/0'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts new file mode 100644 index 000000000000..7d11168acb1a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest('should create spans for fetch requests called directly after init', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(1); + + expect(requestSpans![0]).toMatchObject({ + description: 'GET http://sentry-test-site.example/0', + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: { + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/0', + url: 'http://sentry-test-site.example/0', + 'server.address': 'sentry-test-site.example', + type: 'fetch', + }, + }); +}); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 0a4579f40774..d31fe41742f8 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -3,7 +3,6 @@ import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, import { addNonEnumerableProperty, browserPerformanceTimeOrigin, - consoleSandbox, generateTraceId, getClient, getCurrentScope, @@ -233,8 +232,6 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { ...defaultRequestInstrumentationOptions, }; -let _hasBeenInitialized = false; - /** * The Browser Tracing integration automatically instruments browser pageload/navigation * actions as transactions, and captures requests, metrics and errors as spans. @@ -245,14 +242,10 @@ let _hasBeenInitialized = false; * We explicitly export the proper type here, as this has to be extended in some cases. */ export const browserTracingIntegration = ((_options: Partial = {}) => { - if (_hasBeenInitialized) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('Multiple browserTracingIntegration instances are not supported.'); - }); - } - - _hasBeenInitialized = true; + const latestRoute: RouteInfo = { + name: undefined, + source: undefined, + }; /** * This is just a small wrapper that makes `document` optional. @@ -260,8 +253,6 @@ export const browserTracingIntegration = ((_options: Partial void); /** Create routing idle transaction. */ function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions): void { @@ -340,7 +307,9 @@ export const browserTracingIntegration = ((_options: Partial { - _collectWebVitals(); + // This will generally always be defined here, because it is set in `setup()` of the integration + // but technically, it is optional, so we guard here to be extra safe + _collectWebVitals?.(); addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans }); setActiveIdleSpan(client, undefined); @@ -378,8 +347,29 @@ export const browserTracingIntegration = ((_options: Partial {}); + public constructor(name: string, tag?: string) { this.name = name; this.tag = tag; } - - public setupOnce(): void { - // noop - } } type TestCase = [ @@ -74,6 +72,31 @@ describe('getIntegrationsToSetup', () => { }); expect(integrations.map(i => i.name)).toEqual(expected); }); + + test('it uses passed integration over default intergation', () => { + const integrationDefault = new MockIntegration('ChaseSquirrels'); + const integration1 = new MockIntegration('ChaseSquirrels'); + + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [integrationDefault], + integrations: [integration1], + }); + + expect(integrations).toEqual([integration1]); + }); + + test('it uses last passed integration only', () => { + const integrationDefault = new MockIntegration('ChaseSquirrels'); + const integration1 = new MockIntegration('ChaseSquirrels'); + const integration2 = new MockIntegration('ChaseSquirrels'); + + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [integrationDefault], + integrations: [integration1, integration2], + }); + + expect(integrations).toEqual([integration2]); + }); }); describe('deduping', () => { diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx index 97109e191fcf..d9d0cbe45e8c 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -236,7 +236,9 @@ export function createReactRouterV6CompatibleTracingIntegration( return { ...integration, - setup() { + setup(client) { + integration.setup(client); + _useEffect = useEffect; _useLocation = useLocation; _useNavigationType = useNavigationType; From 76b7f3a39d35acbd4c0c52eb1fd759fb12eb76a4 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 26 May 2025 13:19:50 +0200 Subject: [PATCH 12/26] feat(browser): Disable client when browser extension is detected in `init()` (#16354) Similar to https://github.com/getsentry/sentry-javascript/pull/16353, this changes how default options for BrowserClient are handled. Instead of building this in `init`, we now do (some) of it in BrowserClient. Additionally, this also adjusts what we do if we detect a browser extension: Instead of skipping the setup, we now just disable the client. This streamlines this a bit and also ensures that we actually always return a `Client` from init. It also fixes the type for `BrowserOption` to actually allow to configure `cdnBaseUrl`, which was actually only set for the ClientOptions, oops. The reason for this is to streamline the `init` code, making it easier to extend/adjust it. Right now, there is a lot going on in different places there. By moving as much as we can (and makes sense) to `BrowserClient` this becomes a bit easier. While playing with different ways to handle the browser extension stuff, I ended up landing on just disabling the SDK in this scenario. --- .../skip-init-browser-extension/test.ts | 7 +- .../skip-init-chrome-extension/test.ts | 7 +- packages/browser/src/client.ts | 40 ++++-- packages/browser/src/sdk.ts | 116 ++--------------- .../src/utils/detectBrowserExtension.ts | 65 ++++++++++ packages/browser/test/client.test.ts | 69 +++++++++- packages/browser/test/sdk.test.ts | 120 ++---------------- 7 files changed, 192 insertions(+), 232 deletions(-) create mode 100644 packages/browser/src/utils/detectBrowserExtension.ts diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts index 5098b4aa6552..d949e5d2b19e 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts @@ -18,7 +18,12 @@ sentryTest( return !!(window as any).Sentry.isInitialized(); }); - expect(isInitialized).toEqual(false); + const isEnabled = await page.evaluate(() => { + return !!(window as any).Sentry.getClient()?.getOptions().enabled; + }); + + expect(isInitialized).toEqual(true); + expect(isEnabled).toEqual(false); if (hasDebugLogs()) { expect(errorLogs.length).toEqual(1); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts index 411fbd2f9db8..83996a83aad7 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts @@ -16,7 +16,12 @@ sentryTest('should not initialize when inside a Chrome browser extension', async return !!(window as any).Sentry.isInitialized(); }); - expect(isInitialized).toEqual(false); + const isEnabled = await page.evaluate(() => { + return !!(window as any).Sentry.getClient()?.getOptions().enabled; + }); + + expect(isInitialized).toEqual(true); + expect(isEnabled).toEqual(false); if (hasDebugLogs()) { expect(errorLogs.length).toEqual(1); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index c0841d161ed6..5c50e53e708b 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -21,15 +21,24 @@ import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; +/** + * A magic string that build tooling can leverage in order to inject a release value into the SDK. + */ +declare const __SENTRY_RELEASE__: string | undefined; + const DEFAULT_FLUSH_INTERVAL = 5000; +type BrowserSpecificOptions = BrowserClientReplayOptions & + BrowserClientProfilingOptions & { + /** If configured, this URL will be used as base URL for lazy loading integration. */ + cdnBaseUrl?: string; + }; /** * Configuration options for the Sentry Browser SDK. * @see @sentry/core Options for more information. */ export type BrowserOptions = Options & - BrowserClientReplayOptions & - BrowserClientProfilingOptions & { + BrowserSpecificOptions & { /** * Important: Only set this option if you know what you are doing! * @@ -54,12 +63,7 @@ export type BrowserOptions = Options & * Configuration options for the Sentry Browser SDK Client class * @see BrowserClient for more information. */ -export type BrowserClientOptions = ClientOptions & - BrowserClientReplayOptions & - BrowserClientProfilingOptions & { - /** If configured, this URL will be used as base URL for lazy loading integration. */ - cdnBaseUrl?: string; - }; +export type BrowserClientOptions = ClientOptions & BrowserSpecificOptions; /** * The Sentry Browser SDK Client. @@ -75,11 +79,7 @@ export class BrowserClient extends Client { * @param options Configuration options for this SDK. */ public constructor(options: BrowserClientOptions) { - const opts = { - // We default this to true, as it is the safer scenario - parentSpanIsAlwaysRootSpan: true, - ...options, - }; + const opts = applyDefaultOptions(options); const sdkSource = WINDOW.SENTRY_SDK_SOURCE || getSDKSource(); applySdkMetadata(opts, 'browser', ['browser'], sdkSource); @@ -155,3 +155,17 @@ export class BrowserClient extends Client { return super._prepareEvent(event, hint, currentScope, isolationScope); } } + +/** Exported only for tests. */ +export function applyDefaultOptions>(optionsArg: T): T { + return { + release: + typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value + ? __SENTRY_RELEASE__ + : WINDOW.SENTRY_RELEASE?.id, // This supports the variable that sentry-webpack-plugin injects + sendClientReports: true, + // We default this to true, as it is the safer scenario + parentSpanIsAlwaysRootSpan: true, + ...optionsArg, + }; +} diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 56f3ace8f193..2101e4e36074 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,18 +1,14 @@ import type { Client, Integration, Options } from '@sentry/core'; import { - consoleSandbox, dedupeIntegration, functionToStringIntegration, getIntegrationsToSetup, - getLocationHref, inboundFiltersIntegration, initAndBind, stackParserFromStackParserOptions, } from '@sentry/core'; import type { BrowserClientOptions, BrowserOptions } from './client'; import { BrowserClient } from './client'; -import { DEBUG_BUILD } from './debug-build'; -import { WINDOW } from './helpers'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { browserApiErrorsIntegration } from './integrations/browserapierrors'; import { browserSessionIntegration } from './integrations/browsersession'; @@ -21,22 +17,7 @@ import { httpContextIntegration } from './integrations/httpcontext'; import { linkedErrorsIntegration } from './integrations/linkederrors'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; - -type ExtensionProperties = { - chrome?: Runtime; - browser?: Runtime; - nw?: unknown; -}; -type Runtime = { - runtime?: { - id?: string; - }; -}; - -/** - * A magic string that build tooling can leverage in order to inject a release value into the SDK. - */ -declare const __SENTRY_RELEASE__: string | undefined; +import { checkAndWarnIfIsEmbeddedBrowserExtension } from './utils/detectBrowserExtension'; /** Get the default integrations for the browser SDK. */ export function getDefaultIntegrations(_options: Options): Integration[] { @@ -59,40 +40,6 @@ export function getDefaultIntegrations(_options: Options): Integration[] { ]; } -/** Exported only for tests. */ -export function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { - const defaultOptions: BrowserOptions = { - defaultIntegrations: getDefaultIntegrations(optionsArg), - release: - typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value - ? __SENTRY_RELEASE__ - : WINDOW.SENTRY_RELEASE?.id, // This supports the variable that sentry-webpack-plugin injects - sendClientReports: true, - }; - - return { - ...defaultOptions, - ...dropTopLevelUndefinedKeys(optionsArg), - }; -} - -/** - * In contrast to the regular `dropUndefinedKeys` method, - * this one does not deep-drop keys, but only on the top level. - */ -function dropTopLevelUndefinedKeys(obj: T): Partial { - const mutatetedObj: Partial = {}; - - for (const k of Object.getOwnPropertyNames(obj)) { - const key = k as keyof T; - if (obj[key] !== undefined) { - mutatetedObj[key] = obj[key]; - } - } - - return mutatetedObj; -} - /** * The Sentry Browser SDK Client. * @@ -139,19 +86,21 @@ function dropTopLevelUndefinedKeys(obj: T): Partial { * * @see {@link BrowserOptions} for documentation on configuration options. */ -export function init(browserOptions: BrowserOptions = {}): Client | undefined { - if (!browserOptions.skipBrowserExtensionCheck && _checkForBrowserExtension()) { - return; - } +export function init(options: BrowserOptions = {}): Client | undefined { + const shouldDisableBecauseIsBrowserExtenstion = + !options.skipBrowserExtensionCheck && checkAndWarnIfIsEmbeddedBrowserExtension(); - const options = applyDefaultOptions(browserOptions); const clientOptions: BrowserClientOptions = { ...options, + enabled: shouldDisableBecauseIsBrowserExtenstion ? false : options.enabled, stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), + integrations: getIntegrationsToSetup({ + integrations: options.integrations, + defaultIntegrations: + options.defaultIntegrations == null ? getDefaultIntegrations(options) : options.defaultIntegrations, + }), transport: options.transport || makeFetchTransport, }; - return initAndBind(BrowserClient, clientOptions); } @@ -170,48 +119,3 @@ export function forceLoad(): void { export function onLoad(callback: () => void): void { callback(); } - -function _isEmbeddedBrowserExtension(): boolean { - if (typeof WINDOW.window === 'undefined') { - // No need to show the error if we're not in a browser window environment (e.g. service workers) - return false; - } - - const _window = WINDOW as typeof WINDOW & ExtensionProperties; - - // Running the SDK in NW.js, which appears like a browser extension but isn't, is also fine - // see: https://github.com/getsentry/sentry-javascript/issues/12668 - if (_window.nw) { - return false; - } - - const extensionObject = _window['chrome'] || _window['browser']; - - if (!extensionObject?.runtime?.id) { - return false; - } - - const href = getLocationHref(); - const extensionProtocols = ['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension']; - - // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage - const isDedicatedExtensionPage = - WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}://`)); - - return !isDedicatedExtensionPage; -} - -function _checkForBrowserExtension(): true | void { - if (_isEmbeddedBrowserExtension()) { - if (DEBUG_BUILD) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.error( - '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', - ); - }); - } - - return true; - } -} diff --git a/packages/browser/src/utils/detectBrowserExtension.ts b/packages/browser/src/utils/detectBrowserExtension.ts new file mode 100644 index 000000000000..52e667ccecf2 --- /dev/null +++ b/packages/browser/src/utils/detectBrowserExtension.ts @@ -0,0 +1,65 @@ +import { consoleSandbox, getLocationHref } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { WINDOW } from '../helpers'; + +type ExtensionRuntime = { + runtime?: { + id?: string; + }; +}; +type ExtensionProperties = { + chrome?: ExtensionRuntime; + browser?: ExtensionRuntime; + nw?: unknown; +}; + +/** + * Returns true if the SDK is running in an embedded browser extension. + * Stand-alone browser extensions (which do not share the same data as the main browser page) are fine. + */ +export function checkAndWarnIfIsEmbeddedBrowserExtension(): boolean { + if (_isEmbeddedBrowserExtension()) { + if (DEBUG_BUILD) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.error( + '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', + ); + }); + } + + return true; + } + + return false; +} + +function _isEmbeddedBrowserExtension(): boolean { + if (typeof WINDOW.window === 'undefined') { + // No need to show the error if we're not in a browser window environment (e.g. service workers) + return false; + } + + const _window = WINDOW as typeof WINDOW & ExtensionProperties; + + // Running the SDK in NW.js, which appears like a browser extension but isn't, is also fine + // see: https://github.com/getsentry/sentry-javascript/issues/12668 + if (_window.nw) { + return false; + } + + const extensionObject = _window['chrome'] || _window['browser']; + + if (!extensionObject?.runtime?.id) { + return false; + } + + const href = getLocationHref(); + const extensionProtocols = ['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension']; + + // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage + const isDedicatedExtensionPage = + WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}://`)); + + return !isDedicatedExtensionPage; +} diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index a90f8cdbc388..c0f4e649501a 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -4,7 +4,7 @@ import * as sentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { BrowserClient } from '../src/client'; +import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; @@ -118,3 +118,70 @@ describe('BrowserClient', () => { }); }); }); + +describe('applyDefaultOptions', () => { + it('works with empty options', () => { + const options = {}; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: undefined, + sendClientReports: true, + parentSpanIsAlwaysRootSpan: true, + }); + }); + + it('works with options', () => { + const options = { + tracesSampleRate: 0.5, + release: '1.0.0', + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + }); + + it('picks up release from WINDOW.SENTRY_RELEASE.id', () => { + const releaseBefore = WINDOW.SENTRY_RELEASE; + + WINDOW.SENTRY_RELEASE = { id: '1.0.0' }; + const options = { + tracesSampleRate: 0.5, + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + + WINDOW.SENTRY_RELEASE = releaseBefore; + }); + + it('passed in release takes precedence over WINDOW.SENTRY_RELEASE.id', () => { + const releaseBefore = WINDOW.SENTRY_RELEASE; + + WINDOW.SENTRY_RELEASE = { id: '1.0.0' }; + const options = { + release: '2.0.0', + tracesSampleRate: 0.5, + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '2.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + + WINDOW.SENTRY_RELEASE = releaseBefore; + }); +}); diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 342b008bfc18..b7972797182f 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -7,10 +7,10 @@ import type { Integration } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { createTransport, resolvedSyncPromise } from '@sentry/core'; import type { Mock } from 'vitest'; -import { afterAll, afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; +import { afterEach, describe, expect, it, test, vi } from 'vitest'; import type { BrowserOptions } from '../src'; import { WINDOW } from '../src'; -import { applyDefaultOptions, getDefaultIntegrations, init } from '../src/sdk'; +import { init } from '../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -32,15 +32,11 @@ export class MockIntegration implements Integration { } describe('init', () => { - beforeEach(() => { - vi.clearAllMocks(); + afterEach(() => { + vi.restoreAllMocks(); }); - afterAll(() => { - vi.resetAllMocks(); - }); - - test('installs default integrations', () => { + test('installs passed default integrations', () => { const DEFAULT_INTEGRATIONS: Integration[] = [ new MockIntegration('MockIntegration 0.1'), new MockIntegration('MockIntegration 0.2'), @@ -134,7 +130,7 @@ describe('init', () => { Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'nw', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'window', { value: WINDOW, writable: true }); - vi.clearAllMocks(); + vi.restoreAllMocks(); }); it('logs a browser extension error if executed inside a Chrome extension', () => { @@ -151,8 +147,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it('logs a browser extension error if executed inside a Firefox/Safari extension', () => { @@ -166,8 +160,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it.each(['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension'])( @@ -224,7 +216,7 @@ describe('init', () => { consoleErrorSpy.mockRestore(); }); - it("doesn't return a client on initialization error", () => { + it('returns a disabled client on initialization error', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'chrome', { @@ -234,7 +226,9 @@ describe('init', () => { const client = init(options); - expect(client).toBeUndefined(); + expect(client).toBeDefined(); + expect(SentryCore.isEnabled()).toBe(false); + expect(client!['_isEnabled']()).toBe(false); consoleErrorSpy.mockRestore(); }); @@ -245,97 +239,3 @@ describe('init', () => { expect(client).not.toBeUndefined(); }); }); - -describe('applyDefaultOptions', () => { - test('it works with empty options', () => { - const options = {}; - const actual = applyDefaultOptions(options); - - expect(actual).toEqual({ - defaultIntegrations: expect.any(Array), - release: undefined, - sendClientReports: true, - }); - - expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( - getDefaultIntegrations(options).map(i => i.name), - ); - }); - - test('it works with options', () => { - const options = { - tracesSampleRate: 0.5, - release: '1.0.0', - }; - const actual = applyDefaultOptions(options); - - expect(actual).toEqual({ - defaultIntegrations: expect.any(Array), - release: '1.0.0', - sendClientReports: true, - tracesSampleRate: 0.5, - }); - - expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( - getDefaultIntegrations(options).map(i => i.name), - ); - }); - - test('it works with defaultIntegrations=false', () => { - const options = { - defaultIntegrations: false, - } as const; - const actual = applyDefaultOptions(options); - - expect(actual.defaultIntegrations).toStrictEqual(false); - }); - - test('it works with defaultIntegrations=[]', () => { - const options = { - defaultIntegrations: [], - }; - const actual = applyDefaultOptions(options); - - expect(actual.defaultIntegrations).toEqual([]); - }); - - test('it works with tracesSampleRate=undefined', () => { - const options = { - tracesSampleRate: undefined, - } as const; - const actual = applyDefaultOptions(options); - - // Not defined, not even undefined - expect('tracesSampleRate' in actual).toBe(false); - }); - - test('it works with tracesSampleRate=null', () => { - const options = { - tracesSampleRate: null, - } as any; - const actual = applyDefaultOptions(options); - - expect(actual.tracesSampleRate).toStrictEqual(null); - }); - - test('it works with tracesSampleRate=0', () => { - const options = { - tracesSampleRate: 0, - } as const; - const actual = applyDefaultOptions(options); - - expect(actual.tracesSampleRate).toStrictEqual(0); - }); - - test('it does not deep-drop undefined keys', () => { - const options = { - obj: { - prop: undefined, - }, - } as any; - const actual = applyDefaultOptions(options) as any; - - expect('prop' in actual.obj).toBe(true); - expect(actual.obj.prop).toStrictEqual(undefined); - }); -}); From 2801d9b76a936d1d90e2f8fb81d800023f901ed8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 18:33:23 +0200 Subject: [PATCH 13/26] feat(deps): bump @opentelemetry/semantic-conventions from 1.32.0 to 1.34.0 (#16393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@opentelemetry/semantic-conventions](https://github.com/open-telemetry/opentelemetry-js) from 1.32.0 to 1.34.0.
Release notes

Sourced from @​opentelemetry/semantic-conventions's releases.

semconv/v1.34.0

1.34.0

:rocket: Features

  • feat: update semantic conventions to v1.34.0 #5703 @​trentm
    • Semantic Conventions v1.34.0: changelog | latest docs
    • @opentelemetry/semantic-conventions (stable) changes: none
    • @opentelemetry/semantic-conventions/incubating (unstable) changes: 12 added exports

Unstable changes in v1.34.0

ATTR_AWS_BEDROCK_GUARDRAIL_ID //
aws.bedrock.guardrail.id
ATTR_AWS_BEDROCK_KNOWLEDGE_BASE_ID // aws.bedrock.knowledge_base.id
ATTR_AWS_KINESIS_STREAM_NAME              // aws.kinesis.stream_name
ATTR_AWS_LAMBDA_RESOURCE_MAPPING_ID // aws.lambda.resource_mapping.id
ATTR_AWS_SECRETSMANAGER_SECRET_ARN // aws.secretsmanager.secret.arn
ATTR_AWS_SNS_TOPIC_ARN                    // aws.sns.topic.arn
ATTR_AWS_SQS_QUEUE_URL                    // aws.sqs.queue.url
ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN // aws.step_functions.activity.arn
ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN //
aws.step_functions.state_machine.arn

ATTR_FEATURE_FLAG_RESULT_VALUE // feature_flag.result.value

ATTR_GEN_AI_CONVERSATION_ID // gen_ai.conversation.id
ATTR_GEN_AI_DATA_SOURCE_ID // gen_ai.data_source.id

semconv/v1.33.1

1.33.1

:boom: Breaking Changes

  • fix: Remove the subset of DB_SYSTEM_NAME_VALUE_* exports that are unstable from the @opentelemetry/semantic-conventions entry point. #5690
    • Version 1.33.0 erroneously included all DB_SYSTEM_NAME_VALUE_* constants in the stable entry point. Some of those enum values are not yet stable. They have been moved back to the unstable @opentelemetry/semantic-conventions/incubating entry point. See the PR description for a full list.

semconv/v1.33.0

1.33.0

:rocket: Features

... (truncated)

Commits
  • a2e0f2c chore: prepare next release (#5705)
  • 019cb2c feat(semantic-conventions): update semantic conventions to v1.34.0 (#5703)
  • 7a9b049 chore: enable tsconfig isolatedModules (#5697)
  • 6d0d33b chore: prepare next release (#5700)
  • 280cd50 test(instrumentation-fetch): pin msw dep to avoid breakage in msw@2.8 (#5702)
  • 1c8ec3b fix(semantic-conventions): Remove the subset of DB_SYSTEM_NAME_VALUE_* expo...
  • 85f1bcc chore(opentelemetry-instrumentation-http): Fix link to docs (#5699)
  • f0ba942 chore(deps): update dependency @​babel/core to v7.27.1 (#5698)
  • af9c024 chore(deps): update all patch versions (#5694)
  • 9dbd1e4 chore: prepare next release (#5696)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@opentelemetry/semantic-conventions&package-manager=npm_and_yarn&previous-version=1.32.0&new-version=1.34.0)](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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/opentelemetry-v2-tests/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/node/package.json | 2 +- packages/opentelemetry/package.json | 4 ++-- packages/react-router/package.json | 2 +- packages/remix/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/vercel-edge/package.json | 2 +- yarn.lock | 8 ++++---- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 80f43c01c982..216989ffb022 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -16,7 +16,7 @@ "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.30.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index ca439183048c..e13dbd717cdc 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -48,7 +48,7 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/instrumentation-nestjs-core": "0.44.1", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0" }, diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 28e85accecf2..2678530141b3 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -77,7 +77,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "9.22.0", "@sentry/core": "9.22.0", diff --git a/packages/node/package.json b/packages/node/package.json index 88572cc9d500..5898f2d33496 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -93,7 +93,7 @@ "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.7.0", "@sentry/core": "9.22.0", "@sentry/opentelemetry": "9.22.0", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index b6b378d1d3da..b50d85981653 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -47,7 +47,7 @@ "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", - "@opentelemetry/semantic-conventions": "^1.30.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", @@ -55,7 +55,7 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.30.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index e488a2029acc..3ee797048354 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -37,7 +37,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/browser": "9.22.0", "@sentry/cli": "^2.43.0", "@sentry/core": "9.22.0", diff --git a/packages/remix/package.json b/packages/remix/package.json index be1bf30ad1d9..decdce331500 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -66,7 +66,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.57.2", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.43.0", "@sentry/core": "9.22.0", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index d8930ee14811..b95d4b34db00 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -51,7 +51,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry-internal/browser-utils": "9.22.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 2af29ebfa807..88be3c33b114 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -47,7 +47,7 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/opentelemetry": "9.22.0" }, "scripts": { diff --git a/yarn.lock b/yarn.lock index 67dbf24aac88..862445d20e69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5744,10 +5744,10 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0": - version "1.32.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz#a15e8f78f32388a7e4655e7f539570e40958ca3f" - integrity sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ== +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz#8b6a46681b38a4d5947214033ac48128328c1738" + integrity sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA== "@opentelemetry/sql-common@^0.40.1": version "0.40.1" From 5c056278ee77d45740505e4337adf2f64fec60c4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 26 May 2025 19:08:37 +0200 Subject: [PATCH 14/26] feat(core): Export `_INTERNAL_captureSerializedLog` (#16387) I renamed `defaultCaptureSerializedLog` to `_INTERNAL_captureSerializedLog` and exported it so it can be accessed from other SDKs. I will use this in the Electron SDK in the root main process to add logs passed by IPC from other processes to the queue. --- packages/core/src/index.ts | 2 +- packages/core/src/logs/exports.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a67f003aac56..986d18a972d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -119,7 +119,7 @@ export { trpcMiddleware } from './trpc'; export { wrapMcpServerWithSentry } from './mcp-server'; export { captureFeedback } from './feedback'; export type { ReportDialogOptions } from './report-dialog'; -export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs/exports'; +export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; export type { FeatureFlag } from './featureFlags'; diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 41f1155ee3e1..9a738d503a80 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -61,7 +61,16 @@ export function logAttributeToSerializedLogAttribute(value: unknown): Serialized } } -function defaultCaptureSerializedLog(client: Client, serializedLog: SerializedLog): void { +/** + * Captures a serialized log event and adds it to the log buffer for the given client. + * + * @param client - A client. Uses the current client if not provided. + * @param serializedLog - The serialized log event to capture. + * + * @experimental This method will experience breaking changes. This is not yet part of + * the stable Sentry SDK API and can be changed or removed without warning. + */ +export function _INTERNAL_captureSerializedLog(client: Client, serializedLog: SerializedLog): void { const logBuffer = _INTERNAL_getLogBuffer(client); if (logBuffer === undefined) { GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [serializedLog]); @@ -88,7 +97,7 @@ export function _INTERNAL_captureLog( beforeLog: Log, client: Client | undefined = getClient(), scope = getCurrentScope(), - captureSerializedLog: (client: Client, log: SerializedLog) => void = defaultCaptureSerializedLog, + captureSerializedLog: (client: Client, log: SerializedLog) => void = _INTERNAL_captureSerializedLog, ): void { if (!client) { DEBUG_BUILD && logger.warn('No client available to capture log.'); From e46c619474ddfbecf7c12c2a5630b5af70e66427 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 26 May 2025 19:10:48 +0200 Subject: [PATCH 15/26] fix(opentelemetry): Ensure `withScope` keeps span active & `_getTraceInfoFromScope` works (#16385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also ensure that `withScope(scope, callback)` maintains the active span from the passed in scope. Fixes https://github.com/getsentry/sentry-javascript/issues/16361 The problem here was that we used the `_getSpanForScope` method in core for this, but this only actually works for the core implementation, not the node one 😬 So I rewrote this to use more general utilities, which should work isomorphically. While doing this, I noticed an actual bug in the `withScope` implementation in otel, where we did not actually keep the active span correctly. --- packages/core/src/client.ts | 19 ++++++++++--------- packages/core/test/lib/tracing/trace.test.ts | 13 +++++++++++++ .../opentelemetry/src/asyncContextStrategy.ts | 4 ++-- packages/opentelemetry/test/trace.test.ts | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0dda6c86fd26..d9abd8f5d0d2 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { DEFAULT_ENVIRONMENT } from './constants'; -import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; +import { getCurrentScope, getIsolationScope, getTraceContextFromScope, withScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import type { IntegrationIndex } from './integration'; @@ -36,8 +36,7 @@ import { getPossibleEventMessages } from './utils/eventUtils'; import { merge } from './utils/merge'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; -import { _getSpanForScope } from './utils/spanOnScope'; -import { showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; +import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; import { createClientReportEnvelope } from './utils-hoist/clientreport'; import { dsnToString, makeDsn } from './utils-hoist/dsn'; @@ -1325,10 +1324,12 @@ export function _getTraceInfoFromScope( return [undefined, undefined]; } - const span = _getSpanForScope(scope); - const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); - const dynamicSamplingContext = span - ? getDynamicSamplingContextFromSpan(span) - : getDynamicSamplingContextFromScope(client, scope); - return [dynamicSamplingContext, traceContext]; + return withScope(scope, () => { + const span = getActiveSpan(); + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(client, scope); + return [dynamicSamplingContext, traceContext]; + }); } diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 0e0d16fb6ec4..83f4b150a6d6 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1781,6 +1781,19 @@ describe('getActiveSpan', () => { const result = getActiveSpan(); expect(result).toBe(staticSpan); }); + + it('handles active span when passing scopes to withScope', () => { + const [scope, span] = startSpan({ name: 'outer' }, span => { + return [getCurrentScope(), span]; + }); + + const spanOnScope = withScope(scope, () => { + return getActiveSpan(); + }); + + expect(spanOnScope).toBeDefined(); + expect(spanOnScope).toBe(span); + }); }); describe('withActiveSpan()', () => { diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index 695175bc3fa1..9f7b38d0b43d 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -8,7 +8,7 @@ import { } from './constants'; import { continueTrace, startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace'; import type { CurrentScopes } from './types'; -import { getScopesFromContext } from './utils/contextData'; +import { getContextFromScope, getScopesFromContext } from './utils/contextData'; import { getActiveSpan } from './utils/getActiveSpan'; import { getTraceData } from './utils/getTraceData'; import { suppressTracing } from './utils/suppressTracing'; @@ -48,7 +48,7 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { } function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const ctx = api.context.active(); + const ctx = getContextFromScope(scope) || api.context.active(); // We depend on the otelContextManager to handle the context/hub // We set the `SENTRY_FORK_SET_SCOPE_CONTEXT_KEY` context value, which is picked up by diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 74b37809f4ae..f9aed823a4a4 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1342,6 +1342,21 @@ describe('trace', () => { }); }); }); + + describe('scope passing', () => { + it('handles active span when passing scopes to withScope', () => { + const [scope, span] = startSpan({ name: 'outer' }, span => { + return [getCurrentScope(), span]; + }); + + const spanOnScope = withScope(scope, () => { + return getActiveSpan(); + }); + + expect(spanOnScope).toBeDefined(); + expect(spanOnScope).toBe(span); + }); + }); }); describe('trace (tracing disabled)', () => { From c297bf99ae0f9f8bf0d62d686c0f7735e249bc1a Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 26 May 2025 13:46:34 -0400 Subject: [PATCH 16/26] feat(browser): option to ignore certain resource types (#16389) Allows specific resource spans to be ignored by passing an array of strings to ignoreResourceSpans. Example values include, resource.css, resource.script. Co-authored-by: Lukas Stracke --- .../resource-spans-ignored/assets/script.js | 3 ++ .../resource-spans-ignored/init.js | 14 ++++++ .../resource-spans-ignored/test.ts | 20 ++++++++ .../src/metrics/browserMetrics.ts | 25 +++++++++- .../test/browser/browserMetrics.test.ts | 47 +++++++++++++++++++ .../src/tracing/browserTracingIntegration.ts | 11 ++++- 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js new file mode 100644 index 000000000000..eab583b75a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js @@ -0,0 +1,3 @@ +(() => { + // I do nothing. +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js new file mode 100644 index 000000000000..70c0b30a03a5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + ignoreResourceSpans: ['resource.script'], + idleTimeout: 9000, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts new file mode 100644 index 000000000000..4bc9621f8395 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts @@ -0,0 +1,20 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should allow specific types of resource spans to be ignored.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const allSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource.script')); + + expect(allSpans?.length).toBe(0); +}); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 71470a0d8706..d5ca039c65f0 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -300,6 +300,13 @@ interface AddPerformanceEntriesOptions { * sent as a standalone span instead. */ recordClsOnPageloadSpan: boolean; + + /** + * Resource spans with `op`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; } /** Add performance related spans to a transaction */ @@ -355,7 +362,15 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries break; } case 'resource': { - _addResourceSpans(span, entry as PerformanceResourceTiming, entry.name, startTime, duration, timeOrigin); + _addResourceSpans( + span, + entry as PerformanceResourceTiming, + entry.name, + startTime, + duration, + timeOrigin, + options.ignoreResourceSpans, + ); break; } // Ignore other entry types. @@ -568,6 +583,7 @@ export function _addResourceSpans( startTime: number, duration: number, timeOrigin: number, + ignoreResourceSpans?: Array, ): void { // we already instrument based on fetch and xhr, so we don't need to // duplicate spans here. @@ -575,6 +591,11 @@ export function _addResourceSpans( return; } + const op = entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other'; + if (ignoreResourceSpans?.includes(op)) { + return; + } + const parsedUrl = parseUrl(resourceUrl); const attributes: SpanAttributes = { @@ -616,7 +637,7 @@ export function _addResourceSpans( startAndEndSpan(span, startTimestamp, endTimestamp, { name: resourceUrl.replace(WINDOW.location.origin, ''), - op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other', + op, attributes, }); } diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 99cf451f824e..87646a690f0e 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -271,6 +271,53 @@ describe('_addResourceSpans', () => { } }); + it('allows resource spans to be ignored via ignoreResourceSpans', () => { + const spans: Span[] = []; + const ignoredResourceSpans = ['resource.other', 'resource.script']; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const table = [ + { + initiatorType: undefined, + op: 'resource.other', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'image', + op: 'resource.image', + }, + { + initiatorType: 'script', + op: 'resource.script', + }, + ]; + for (const row of table) { + const { initiatorType } = row; + const entry = mockPerformanceResourceTiming({ + initiatorType, + nextHopProtocol: 'http/1.1', + }); + _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465, ignoredResourceSpans); + } + expect(spans).toHaveLength(table.length - ignoredResourceSpans.length); + const spanOps = new Set( + spans.map(s => { + return spanToJSON(s).op; + }), + ); + expect(spanOps).toEqual(new Set(['resource.css', 'resource.image'])); + }); + it('allows for enter size of 0', () => { const spans: Span[] = []; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index d31fe41742f8..3f38bdb6a8be 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -144,6 +144,13 @@ export interface BrowserTracingOptions { */ enableHTTPTimings: boolean; + /** + * Resource spans with `op`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreResourceSpans: Array; + /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or * manually started span). When enabled, this option will allow you to navigate between traces @@ -226,6 +233,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, + ignoreResourceSpans: [], linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, @@ -268,6 +276,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Tue, 27 May 2025 08:00:29 +0100 Subject: [PATCH 17/26] fix(node): Don't warn about Spotlight on empty NODE_ENV (#16381) When running with `spotlight: true` I got a warning about not being in development mode when `NODE_ENV` was not set. This typically indicates dev mode so added a check for that. --- packages/node/src/integrations/spotlight.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index 917fe073e194..4e36f3692fb0 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -20,7 +20,12 @@ const _spotlightIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup(client) { - if (typeof process === 'object' && process.env && process.env.NODE_ENV !== 'development') { + if ( + typeof process === 'object' && + process.env && + process.env.NODE_ENV && + process.env.NODE_ENV !== 'development' + ) { logger.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); } connectToSpotlight(client, _options); From 1a72ed3bc7c6763235673eba38ad6527c111bdfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 09:39:46 +0200 Subject: [PATCH 18/26] feat(deps): bump @sentry/cli from 2.43.0 to 2.45.0 (#16395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@sentry/cli](https://github.com/getsentry/sentry-cli) from 2.43.0 to 2.45.0.
Release notes

Sourced from @​sentry/cli's releases.

2.45.0

New feature

  • feat(sourcemaps): Multi-project sourcemaps upload (#2497) by @​szokeasaurusrex
    • Sourcemaps can now be uploaded to multiple projects at once by passing each project to the sentry-cli sourcemaps upload command, like so:
      sentry-cli sourcemaps upload -p project1 -p
      project2 /path/to/sourcemaps
      
    • Note that users on old versions of self-hosted Sentry may need to upgrade their self-hosted server to a newer version to take advantage of multi-project uploads.

Various fixes & improvements

2.44.0

Various fixes & improvements

2.43.1

Various fixes & improvements

Changelog

Sourced from @​sentry/cli's changelog.

2.45.0

New feature

  • feat(sourcemaps): Multi-project sourcemaps upload (#2497) by @​szokeasaurusrex
    • Sourcemaps can now be uploaded to multiple projects at once by passing each project to the sentry-cli sourcemaps upload command, like so:
      sentry-cli sourcemaps upload -p project1 -p
      project2 /path/to/sourcemaps
      
    • Note that users on old versions of self-hosted Sentry may need to upgrade their self-hosted server to a newer version to take advantage of multi-project uploads.

Various fixes & improvements

2.44.0

Various fixes & improvements

2.43.1

Various fixes & improvements

Commits
  • bed438e meta: Update CHANGELOG.md
  • 7839225 release: 2.45.0
  • 47e426d ref: Rename fixup_js_file_end (#2475)
  • f5ea48c feat(sourcemaps): Multi-project sourcemaps upload (#2497)
  • f540dc1 ref: Use slice instead of vec for assemble artifact request
  • 8a63fec ref: Separate LegacyUploadContext for legacy uploads
  • b241c4a feat: Remove organization and project info log (#2490)
  • 046ab9d chore: update issue templates (#2493)
  • 4a0270f build(deps): bump actions/create-github-app-token from 2.0.2 to 2.0.6 (#2485)
  • 1571be7 build(deps): bump github/codeql-action from 3.28.16 to 3.28.17
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/cli&package-manager=npm_and_yarn&previous-version=2.43.0&new-version=2.45.0)](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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/react-router/package.json | 2 +- packages/remix/package.json | 2 +- yarn.lock | 88 +++++++++++++++--------------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 3ee797048354..ac3872ff45c0 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -39,7 +39,7 @@ "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/browser": "9.22.0", - "@sentry/cli": "^2.43.0", + "@sentry/cli": "^2.45.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0", "@sentry/vite-plugin": "^3.2.4", diff --git a/packages/remix/package.json b/packages/remix/package.json index decdce331500..52c657ab1999 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -68,7 +68,7 @@ "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", - "@sentry/cli": "^2.43.0", + "@sentry/cli": "^2.45.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0", "@sentry/opentelemetry": "9.22.0", diff --git a/yarn.lock b/yarn.lock index 862445d20e69..62182bda070f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6545,75 +6545,75 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz#a32a4f226e717122b37d9969e8d4d0e14779f720" integrity sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg== -"@sentry/cli-darwin@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.43.0.tgz#544caac85069a34d435b0723d8ac6bb6a530ab33" - integrity sha512-0MYvRHJowXOMNY5W6XF4p9GQNH3LuQ+IHAQwVbZOsfwnEv8e20rf9BiPPzmJ9sIjZSWYR4yIqm6dBp6ABJFbGQ== +"@sentry/cli-darwin@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.45.0.tgz#e3d6feae4fadcfdf91db9c7b9c4689a66d3d8d19" + integrity sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg== "@sentry/cli-linux-arm64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz#1c06c83ff21f51ec23acf5be3b1f8c7553bf86b1" integrity sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw== -"@sentry/cli-linux-arm64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.43.0.tgz#e110758c7cf6ced78d8b0b3c8c56802984b0fb98" - integrity sha512-7URSaNjbEJQZyYJ33XK3pVKl6PU2oO9ETF6R/4Cz2FmU3fecACLKVldv7+OuNl9aspLZ62mnPMDvT732/Fp2Ug== +"@sentry/cli-linux-arm64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.45.0.tgz#384c8e17f7e7dc007d164033d0e7c75aa83a2e9b" + integrity sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg== "@sentry/cli-linux-arm@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz#00cadc359ae3c051efb3e63873c033c61dbd1ca1" integrity sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg== -"@sentry/cli-linux-arm@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.43.0.tgz#00d36a7347480b8b693cf0ed5cff270ad40045e7" - integrity sha512-c2Fwb6HrFL1nbaGV4uRhHC1wEJPR+wfpKN5y06PgSNNbd10YrECAB3tqBHXC8CEmhuDyFR+ORGZ7VbswfCWEEQ== +"@sentry/cli-linux-arm@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.45.0.tgz#b9d6f86f3934b4d9ced5b45a8158ff2ac2bdd25d" + integrity sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ== "@sentry/cli-linux-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz#3b817b715dd806c20dfbffd539725ad8089c310a" integrity sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ== -"@sentry/cli-linux-i686@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.43.0.tgz#83a31f47a13d8b3fae2716f86412fae2ffec5cf4" - integrity sha512-bFo/tpMZeMJ275HPGmAENREchnBxhALOOpZAphSyalUu3pGZ+EETEtlSLrKyVNJo26Dye5W7GlrYUV9+rkyCtg== +"@sentry/cli-linux-i686@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.45.0.tgz#39e22beb84cfa26e11bdc198364315fdfb4da4d5" + integrity sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw== "@sentry/cli-linux-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz#ddf906bc3071cc79ce6e633eddcb76bb9068e688" integrity sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw== -"@sentry/cli-linux-x64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.43.0.tgz#dac1641c0c862d5d9f45c2b2c0d77b0281c53261" - integrity sha512-EbAmKXUNU/Ii4pNGVRCepU6ks1M43wStMKx3pibrUTllrrCwqYKyPxRRdoFYySHkduwCxnoKZcLEg9vWZ3qS6A== +"@sentry/cli-linux-x64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.45.0.tgz#25cd3699297f9433835fb5edd42dad722c11f041" + integrity sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg== -"@sentry/cli-win32-arm64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.43.0.tgz#ec499fc49c380252984ae5cde6b59ca0dbad79ef" - integrity sha512-KmJRCdQQGLSErJvrcGcN+yWo68m+5OdluhyJHsVYMOQknwu8YMOWLm12EIa+4t4GclDvwg5xcxLccCuiWMJUZw== +"@sentry/cli-win32-arm64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.45.0.tgz#50c7d29ea2169bdb4d98bbde81c5f7dac0dd3955" + integrity sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg== "@sentry/cli-win32-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz#9036085c7c6ce455ad45fda411c55ff39c06eb95" integrity sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw== -"@sentry/cli-win32-i686@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.43.0.tgz#bf686d387e91d7897cfbd96a681a52816f90fb0b" - integrity sha512-ZWxZdOyZX7NJ/CTskzg+dJ2xTpobFLXVNMOMq0HiwdhqXP2zYYJzKnIt3mHNJYA40zYFODGSgxIamodjpB8BuA== +"@sentry/cli-win32-i686@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.45.0.tgz#201075c4aec37a3e797160e0b468641245437f0c" + integrity sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w== "@sentry/cli-win32-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz#7d6464b63f32c9f97fff428f246b1f039b402233" integrity sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw== -"@sentry/cli-win32-x64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.43.0.tgz#9d5e7b9888c0e639d3e62e4dd7a58468103b6bce" - integrity sha512-S/IRQYAziEnjpyROhnqzTqShDq3m8jcevXx+q5f49uQnFbfYcTgS1sdrEPqqao/K2boOWbffxYtTkvBiB/piQQ== +"@sentry/cli-win32-x64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.45.0.tgz#2075e9e1ea3c3609e0fa1a758ca033e94e1c600f" + integrity sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw== "@sentry/cli@2.42.2": version "2.42.2" @@ -6634,10 +6634,10 @@ "@sentry/cli-win32-i686" "2.42.2" "@sentry/cli-win32-x64" "2.42.2" -"@sentry/cli@^2.36.1", "@sentry/cli@^2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.43.0.tgz#2b0dcb749a1529faeb2c239962307c5a2e33f817" - integrity sha512-gBE3bkx+PBJxopTrzIJLX4xHe5S0w87q5frIveWKDZ5ulVIU6YWnVumay0y07RIEweUEj3IYva1qH6HG2abfiA== +"@sentry/cli@^2.36.1", "@sentry/cli@^2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.45.0.tgz#35feed7a2fee54faf25daed73001a2a2a3143396" + integrity sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -6645,14 +6645,14 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.43.0" - "@sentry/cli-linux-arm" "2.43.0" - "@sentry/cli-linux-arm64" "2.43.0" - "@sentry/cli-linux-i686" "2.43.0" - "@sentry/cli-linux-x64" "2.43.0" - "@sentry/cli-win32-arm64" "2.43.0" - "@sentry/cli-win32-i686" "2.43.0" - "@sentry/cli-win32-x64" "2.43.0" + "@sentry/cli-darwin" "2.45.0" + "@sentry/cli-linux-arm" "2.45.0" + "@sentry/cli-linux-arm64" "2.45.0" + "@sentry/cli-linux-i686" "2.45.0" + "@sentry/cli-linux-x64" "2.45.0" + "@sentry/cli-win32-arm64" "2.45.0" + "@sentry/cli-win32-i686" "2.45.0" + "@sentry/cli-win32-x64" "2.45.0" "@sentry/rollup-plugin@3.4.0": version "3.4.0" From 33ef453e206d0ab6930566ed2d4daff237eee24f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 09:40:28 +0200 Subject: [PATCH 19/26] feat(deps): bump @sentry/webpack-plugin from 3.3.1 to 3.5.0 (#16394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@sentry/webpack-plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins) from 3.3.1 to 3.5.0.
Release notes

Sourced from @​sentry/webpack-plugin's releases.

3.5.0

  • feat(core): Add hook to customize source map file resolution (#732)
  • fix(core): Avoid console output and telemetry init when plugins are disabled (#741)

Work in this release was contributed by @​thecodewarrior. Thank you for your contribution!

3.4.0

  • fix: Replace existing debug ID comments (#730)
  • feat: Expose bundler plugin primitives via createSentryBuildPluginManager (#714)
Changelog

Sourced from @​sentry/webpack-plugin's changelog.

3.5.0

  • feat(core): Add hook to customize source map file resolution (#732)
  • fix(core): Avoid console output and telemetry init when plugins are disabled (#741)

Work in this release was contributed by @​thecodewarrior. Thank you for your contribution!

3.4.0

  • fix: Replace existing debug ID comments (#730)
  • feat: Expose bundler plugin primitives via createSentryBuildPluginManager (#714)
Commits
  • 20e59da release: 3.5.0
  • bab95d9 meta: Add Changelog entry for 3.5.0 (#743)
  • 4dcccca chore(deps-dev): bump rollup from 2.75.7 to 2.79.2 in /packages/webpack-plugi...
  • 7130a86 fix(core): Avoid console output and telemetry init when plugins are disabled ...
  • 3ba65eb chore: Add contributor attribution to changelog (#738)
  • b33725e feat(core): Add hook to customize source map file resolution (#732)
  • 7b097fc Merge branch 'release/3.4.0'
  • 0e15c25 release: 3.4.0
  • 30c15df meta: Update changelog for 3.4.0 (#733)
  • 7bc20a7 fix: Replace existing debug ID comments (#730)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/webpack-plugin&package-manager=npm_and_yarn&previous-version=3.3.1&new-version=3.5.0)](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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/gatsby/package.json | 2 +- packages/nextjs/package.json | 2 +- yarn.lock | 40 ++++++++++++++++++------------------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index e9da408c67b3..eb5c18783e24 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "9.22.0", "@sentry/react": "9.22.0", - "@sentry/webpack-plugin": "3.3.1" + "@sentry/webpack-plugin": "3.5.0" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 2678530141b3..a34c83f2f909 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -85,7 +85,7 @@ "@sentry/opentelemetry": "9.22.0", "@sentry/react": "9.22.0", "@sentry/vercel-edge": "9.22.0", - "@sentry/webpack-plugin": "3.3.1", + "@sentry/webpack-plugin": "3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "4.35.0", diff --git a/yarn.lock b/yarn.lock index 62182bda070f..a8d0c62e5467 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6474,16 +6474,16 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz#c0877df6e5ce227bf51754bf27da2fa5227af847" integrity sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA== -"@sentry/babel-plugin-component-annotate@3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.3.1.tgz#baecd89396cbb4659565a4e8efe7f0a71b19262a" - integrity sha512-5GOxGT7lZN+I8A7Vp0rWY+726FDKEw8HnFiebe51rQrMbfGfCu2Aw9uSM0nT9OG6xhV6WvGccIcCszTPs4fUZQ== - "@sentry/babel-plugin-component-annotate@3.4.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz#f47a7652e16f84556df82cbc38f0004bca1335d1" integrity sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg== +"@sentry/babel-plugin-component-annotate@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz#1b0d01f903b725da876117d551610085c3dd21c7" + integrity sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw== + "@sentry/bundler-plugin-core@2.22.6": version "2.22.6" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" @@ -6512,13 +6512,13 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.3.1.tgz#67c5017dc8a70f629c14e88420c6ede4e51c2047" - integrity sha512-Dd6xaWb293j9otEJ1yJqG2Ra6zB49OPzMNdIkdP8wdY+S9UFQE5PyKTyredmPY7hqCc005OrUQZolIIo9Zl13A== +"@sentry/bundler-plugin-core@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz#3a3459aba94cbeb093347f5730f15df25153fd0a" + integrity sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.3.1" + "@sentry/babel-plugin-component-annotate" "3.4.0" "@sentry/cli" "2.42.2" dotenv "^16.3.1" find-up "^5.0.0" @@ -6526,13 +6526,13 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz#3a3459aba94cbeb093347f5730f15df25153fd0a" - integrity sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw== +"@sentry/bundler-plugin-core@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz#b62af5be1b1a862e7062181655829c556c7d7c0b" + integrity sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.4.0" + "@sentry/babel-plugin-component-annotate" "3.5.0" "@sentry/cli" "2.42.2" dotenv "^16.3.1" find-up "^5.0.0" @@ -6678,12 +6678,12 @@ "@sentry/bundler-plugin-core" "3.2.4" unplugin "1.0.1" -"@sentry/webpack-plugin@3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.3.1.tgz#b257e1cb5f939b68f5050e9c4ea040d7366a55de" - integrity sha512-AFRnGNUnlIvq3M+ADdfWb+DIXWKK6yYEkVPAyOppkjO+cL/19gjXMdvAwv+CMFts28YCFKF8Kr3pamUiCmwodA== +"@sentry/webpack-plugin@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz#cde95534f1e945a4002d47465aeda01d382cd279" + integrity sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ== dependencies: - "@sentry/bundler-plugin-core" "3.3.1" + "@sentry/bundler-plugin-core" "3.5.0" unplugin "1.0.1" uuid "^9.0.0" From 56137a861c3238ab776fcb4b97ba04bdf5b746bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 09:41:33 +0200 Subject: [PATCH 20/26] feat(deps): bump @prisma/instrumentation from 6.7.0 to 6.8.2 (#16392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@prisma/instrumentation](https://github.com/prisma/prisma/tree/HEAD/packages/instrumentation) from 6.7.0 to 6.8.2.
Release notes

Sourced from @​prisma/instrumentation's releases.

6.8.2

Today, we are issuing the 6.8.2 patch release. It fully resolves an issue with the prisma init and prisma dev commands for some Windows users who were still facing problems after the previous incomplete fix in version 6.8.1.

Fixes:

6.8.1

Today, we are issuing the 6.8.1 patch release. It fixes an issue with the prisma init and prisma dev commands on Windows.

Fixes

6.8.0

Today, we are excited to share the 6.8.0 stable release 🎉 

🌟 Help us spread the word about Prisma by starring the repo ☝️ or posting on X about the release.

Highlights

Local development with Prisma Postgres via prisma dev (Early Access)

In this release, we're releasing a way to develop against Prisma Postgres locally — no Docker required!

To get started, run the new prisma dev command:

npx prisma dev # starts a local Prisma Postgres
server

This command spins up a local Prisma Postgres instance and prints the connection URL that you'll need to set as the url of your datasource block to point to a local Prisma Postgres instance. It looks similar to this:

datasource db {
  provider = "postgresql"
url = "prisma+postgres://localhost:51213/?api_key=ey..."
}

You can then run migrations and execute queries against this local Prisma Postgres instance as with any remote one. Note that you need to keep the prisma dev process running in order to interact with the local Prisma Postgres instance.

📚 Learn more in the docs.

Native Deno support in prisma-client generator (Preview)

In this release, we're removing the deno Preview feature from the prisma-client-js generator. If you want to use Prisma ORM with Deno, you can now do so with the new prisma-client generator:

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
</tr></table>

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@prisma/instrumentation&package-manager=npm_and_yarn&previous-version=6.7.0&new-version=6.8.2)](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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/node/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 5898f2d33496..12dccfc9555c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -94,7 +94,7 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@prisma/instrumentation": "6.7.0", + "@prisma/instrumentation": "6.8.2", "@sentry/core": "9.22.0", "@sentry/opentelemetry": "9.22.0", "import-in-the-middle": "^1.13.1", diff --git a/yarn.lock b/yarn.lock index a8d0c62e5467..1b5d8e707ce3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5879,10 +5879,10 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== -"@prisma/instrumentation@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.7.0.tgz#5fd97be1f89e9d9268148424a812deaea491f80a" - integrity sha512-3NuxWlbzYNevgPZbV0ktA2z6r0bfh0g22ONTxcK09a6+6MdIPjHsYx1Hnyu4yOq+j7LmupO5J69hhuOnuvj8oQ== +"@prisma/instrumentation@6.8.2": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.8.2.tgz#77a87a37f67ab35eaaf8ff629f889e9e11a465ac" + integrity sha512-5NCTbZjw7a+WIZ/ey6G8SY+YKcyM2zBF0hOT1muvqC9TbVtTCr5Qv3RL/2iNDOzLUHEvo4I1uEfioyfuNOGK8Q== dependencies: "@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" From bdbd54170b06b74bd231ee008d39d84095def308 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 28 May 2025 11:06:09 +0200 Subject: [PATCH 21/26] feat(nextjs): Include `static/chunks/main-*` files for `widenClientFileUpload` (#16406) --- packages/nextjs/src/config/webpackPluginOptions.ts | 8 ++++++-- .../test/config/webpack/webpackPluginOptions.test.ts | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts index 335c7bea8976..641efd77524c 100644 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ b/packages/nextjs/src/config/webpackPluginOptions.ts @@ -41,11 +41,15 @@ export function getWebpackPluginOptions( ); } - // TODO: We should think about uploading these when `widenClientFileUpload` is `true`. They may be useful in some situations. + // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful + if (!sentryBuildOptions.widenClientFileUpload) { + sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); + } + + // Always ignore framework, polyfills, and webpack files sourcemapUploadIgnore.push( path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*'), path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), ); diff --git a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts index 1dd0cfa95d5b..76ab58be9b64 100644 --- a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts +++ b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts @@ -153,9 +153,9 @@ describe('getWebpackPluginOptions()', () => { expect(generatedPluginOptions.sourcemaps).toMatchObject({ assets: ['/my/project/dir/.next/static/chunks/pages/**', '/my/project/dir/.next/static/chunks/app/**'], ignore: [ + '/my/project/dir/.next/static/chunks/main-*', '/my/project/dir/.next/static/chunks/framework-*', '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/main-*', '/my/project/dir/.next/static/chunks/polyfills-*', '/my/project/dir/.next/static/chunks/webpack-*', ], @@ -170,7 +170,6 @@ describe('getWebpackPluginOptions()', () => { ignore: [ '/my/project/dir/.next/static/chunks/framework-*', '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/main-*', '/my/project/dir/.next/static/chunks/polyfills-*', '/my/project/dir/.next/static/chunks/webpack-*', ], @@ -197,7 +196,6 @@ describe('getWebpackPluginOptions()', () => { ignore: [ 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework-*', 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework.*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/main-*', 'C:/my/windows/project/dir/.dist/v1/static/chunks/polyfills-*', 'C:/my/windows/project/dir/.dist/v1/static/chunks/webpack-*', ], From 90f6619ce33b7bec43de41cc64df5603417e1aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pe=C5=A1i=C4=87?= Date: Wed, 28 May 2025 11:26:45 +0200 Subject: [PATCH 22/26] feat(nuxt): Added support for nuxt layers (#16372) Added ability to detect and import config files from the **active** nuxt layer with strategy of the **most latest** layer taking precedence. Included fallback behavior for good measure. Rest of the config should be merged as it already is by c12 and defu. Don't think this PR covers the git:repo based layers but those are generally not full supported as well in Nuxt 3 ecosystem. Added basic tests Resolves [#14345](https://github.com/getsentry/sentry-javascript/issues/14345) --------- Co-authored-by: Ivan Pesic --- packages/nuxt/src/module.ts | 4 +- packages/nuxt/src/vite/utils.ts | 36 +++++++++++--- packages/nuxt/test/vite/utils.test.ts | 70 +++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index a7e81a49d045..377508e860a2 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -35,7 +35,7 @@ export default defineNuxtModule({ const moduleDirResolver = createResolver(import.meta.url); const buildDirResolver = createResolver(nuxt.options.buildDir); - const clientConfigFile = findDefaultSdkInitFile('client'); + const clientConfigFile = findDefaultSdkInitFile('client', nuxt); if (clientConfigFile) { // Inject the client-side Sentry config file with a side effect import @@ -59,7 +59,7 @@ export default defineNuxtModule({ addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), mode: 'client' }); } - const serverConfigFile = findDefaultSdkInitFile('server'); + const serverConfigFile = findDefaultSdkInitFile('server', nuxt); if (serverConfigFile) { addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index ea2db2bc21b8..f1ef1c9e4cf2 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -1,29 +1,49 @@ import { consoleSandbox } from '@sentry/core'; import * as fs from 'fs'; +import type { Nuxt } from 'nuxt/schema'; import * as path from 'path'; /** * Find the default SDK init file for the given type (client or server). * The sentry.server.config file is prioritized over the instrument.server file. */ -export function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined { +export function findDefaultSdkInitFile(type: 'server' | 'client', nuxt?: Nuxt): string | undefined { const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts']; - const cwd = process.cwd(); + const relativePaths: string[] = []; - const filePaths: string[] = []; if (type === 'server') { for (const ext of possibleFileExtensions) { - // order is important here - we want to prioritize the server.config file - filePaths.push(path.join(cwd, `sentry.${type}.config.${ext}`)); - filePaths.push(path.join(cwd, 'public', `instrument.${type}.${ext}`)); + relativePaths.push(`sentry.${type}.config.${ext}`); + relativePaths.push(path.join('public', `instrument.${type}.${ext}`)); } } else { for (const ext of possibleFileExtensions) { - filePaths.push(path.join(cwd, `sentry.${type}.config.${ext}`)); + relativePaths.push(`sentry.${type}.config.${ext}`); + } + } + + // Get layers from highest priority to lowest + const layers = [...(nuxt?.options._layers ?? [])].reverse(); + + for (const layer of layers) { + for (const relativePath of relativePaths) { + const fullPath = path.resolve(layer.cwd, relativePath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + } + + // As a fallback, also check CWD (left for pure compatibility) + const cwd = process.cwd(); + for (const relativePath of relativePaths) { + const fullPath = path.resolve(cwd, relativePath); + if (fs.existsSync(fullPath)) { + return fullPath; } } - return filePaths.find(filename => fs.existsSync(filename)); + return undefined; } /** diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 7ffd7654549e..f19ec98b4b64 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import type { Nuxt } from 'nuxt/schema'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { constructFunctionReExport, @@ -69,6 +70,75 @@ describe('findDefaultSdkInitFile', () => { const result = findDefaultSdkInitFile('server'); expect(result).toMatch('packages/nuxt/sentry.server.config.js'); }); + + it('should return the latest layer config file path if client config exists', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.includes('sentry.client.config.ts'); + }); + + const nuxtMock = { + options: { + _layers: [ + { + cwd: 'packages/nuxt/module', + }, + { + cwd: 'packages/nuxt', + }, + ], + }, + } as Nuxt; + + const result = findDefaultSdkInitFile('client', nuxtMock); + expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); + }); + + it('should return the latest layer config file path if server config exists', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return ( + !(filePath instanceof URL) && + (filePath.includes('sentry.server.config.ts') || filePath.includes('instrument.server.ts')) + ); + }); + + const nuxtMock = { + options: { + _layers: [ + { + cwd: 'packages/nuxt/module', + }, + { + cwd: 'packages/nuxt', + }, + ], + }, + } as Nuxt; + + const result = findDefaultSdkInitFile('server', nuxtMock); + expect(result).toMatch('packages/nuxt/sentry.server.config.ts'); + }); + + it('should return the latest layer config file path if client config exists in former layer', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.includes('nuxt/sentry.client.config.ts'); + }); + + const nuxtMock = { + options: { + _layers: [ + { + cwd: 'packages/nuxt/module', + }, + { + cwd: 'packages/nuxt', + }, + ], + }, + } as Nuxt; + + const result = findDefaultSdkInitFile('client', nuxtMock); + expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); + }); }); describe('getFilenameFromPath', () => { From 5bbe3754dcf946948bb0bce18b0e00f22b14e7ba Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 28 May 2025 11:56:27 +0200 Subject: [PATCH 23/26] feat: Export `isEnabled` from all SDKs (#16405) This was forgotten to re-export. We actually document this, but have not exported it, oops. --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/cloudflare/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/cloudflare/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/sveltekit/src/worker/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 14 files changed, 14 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 9e02765c66c3..29105cfb4b18 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -63,6 +63,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, kafkaIntegration, koaIntegration, knexIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 644d3345ec8f..942951c165da 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -14,6 +14,7 @@ export { createTransport, getClient, isInitialized, + isEnabled, generateInstrumentOnce, getCurrentScope, getGlobalScope, diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index b454f29556c2..27e1ea34b8b7 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -32,6 +32,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getIsolationScope, getGlobalScope, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b1ad295b1257..70104de6d7c3 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -33,6 +33,7 @@ export { createTransport, getClient, isInitialized, + isEnabled, generateInstrumentOnce, getCurrentScope, getGlobalScope, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 64a6f57fccc2..6754ffd04f7c 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -33,6 +33,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f95d36771b24..f388de7cb5ee 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -33,6 +33,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 7376978a5226..f5d593312743 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -14,6 +14,7 @@ export { createTransport, getClient, isInitialized, + isEnabled, generateInstrumentOnce, getCurrentScope, getGlobalScope, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 03705bda89ba..5a933002bc23 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -65,6 +65,7 @@ export { export { addBreadcrumb, isInitialized, + isEnabled, getGlobalScope, lastEventId, close, diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index f503adaf0711..3d3d17e1da27 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -55,6 +55,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index f90ff55eca6c..0aa30b3c93cc 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -52,6 +52,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, knexIntegration, kafkaIntegration, koaIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 06e97a20a96a..f11b9bb51077 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -55,6 +55,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, knexIntegration, kafkaIntegration, koaIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 5e49fa45fed3..8cc952bfb7e7 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -57,6 +57,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, knexIntegration, kafkaIntegration, koaIntegration, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 9fc8429e5864..3614922072ec 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -46,6 +46,7 @@ export { // eslint-disable-next-line deprecation/deprecation inboundFiltersIntegration, isInitialized, + isEnabled, lastEventId, linkedErrorsIntegration, requestDataIntegration, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index c98cf8ed253d..ff1231c8a1f8 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -33,6 +33,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, From a78868591c75954e4ccd2def9da6aa9561397c07 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Wed, 28 May 2025 12:03:12 +0200 Subject: [PATCH 24/26] chore: Add external contributor to CHANGELOG.md (#16408) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #16372 Co-authored-by: s1gr1d <32902192+s1gr1d@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb36dfdaffc..fd1a42eed45f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @Xenossolitarius. Thank you for your contribution! + ## 9.22.0 ### Important changes From 6e61f825abfff329f92ec30a97f3b49370683a2e Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 28 May 2025 14:28:48 +0200 Subject: [PATCH 25/26] fix(nuxt): Add `@sentry/nuxt` as external in Rollup (#16407) The Sentry Nuxt SDK emits the Sentry config file during the Rollup build. Users add this file to their application which includes an import from `@sentry/nuxt` and the init function. This file is then added as an `.mjs` file in the build output. However, when building the application, this `.mjs` file included a bunch of OpenTelemetry imports and not only the init code as every dependency from `@sentry/nuxt` was traced back and bundled into the build output. By adding `@sentry/nuxt` as external, the `.mjs` file really only contains the content like in the `.ts` file. **Could** fix https://github.com/getsentry/sentry-javascript/issues/15204 --------- Co-authored-by: Andrei Borza --- packages/nuxt/src/vite/addServerConfig.ts | 8 ++++ packages/nuxt/src/vite/utils.ts | 30 ++++++++++++ packages/nuxt/test/vite/utils.test.ts | 58 +++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 771c534705cb..83e59d439ecf 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -8,6 +8,7 @@ import type { SentryNuxtModuleOptions } from '../common/types'; import { constructFunctionReExport, constructWrappedFunctionExportQuery, + getExternalOptionsWithSentryNuxt, getFilenameFromNodeStartCommand, QUERY_END_INDICATOR, removeSentryQueryFromPath, @@ -130,6 +131,13 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, debug? return { name: 'rollup-plugin-inject-sentry-server-config', + options(opts) { + return { + ...opts, + external: getExternalOptionsWithSentryNuxt(opts.external), + }; + }, + buildStart() { const configPath = createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`); diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index f1ef1c9e4cf2..a0e88c892a25 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -2,6 +2,7 @@ import { consoleSandbox } from '@sentry/core'; import * as fs from 'fs'; import type { Nuxt } from 'nuxt/schema'; import * as path from 'path'; +import type { ExternalOption } from 'rollup'; /** * Find the default SDK init file for the given type (client or server). @@ -72,6 +73,35 @@ export function removeSentryQueryFromPath(url: string): string { return url.replace(regex, ''); } +/** + * Add @sentry/nuxt to the external options of the Rollup configuration to prevent Rollup bundling all dependencies + * that would result in adding imports from OpenTelemetry libraries etc. to the server build. + */ +export function getExternalOptionsWithSentryNuxt(previousExternal: ExternalOption | undefined): ExternalOption { + const sentryNuxt = /^@sentry\/nuxt$/; + let external: ExternalOption; + + if (typeof previousExternal === 'function') { + external = new Proxy(previousExternal, { + apply(target, thisArg, args: [string, string | undefined, boolean]) { + const [source] = args; + if (sentryNuxt.test(source)) { + return true; + } + return Reflect.apply(target, thisArg, args); + }, + }); + } else if (Array.isArray(previousExternal)) { + external = [sentryNuxt, ...previousExternal]; + } else if (previousExternal) { + external = [sentryNuxt, previousExternal]; + } else { + external = sentryNuxt; + } + + return external; +} + /** * Extracts and sanitizes function re-export and function wrap query parameters from a query string. * If it is a default export, it is not considered for re-exporting. diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index f19ec98b4b64..9419c5f0a545 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -6,6 +6,7 @@ import { constructWrappedFunctionExportQuery, extractFunctionReexportQueryParameters, findDefaultSdkInitFile, + getExternalOptionsWithSentryNuxt, getFilenameFromNodeStartCommand, QUERY_END_INDICATOR, removeSentryQueryFromPath, @@ -366,3 +367,60 @@ export { foo_sentryWrapped as foo }; expect(result).toBe(''); }); }); + +describe('getExternalOptionsWithSentryNuxt', () => { + it('should return sentryExternals when previousExternal is undefined', () => { + const result = getExternalOptionsWithSentryNuxt(undefined); + expect(result).toEqual(/^@sentry\/nuxt$/); + }); + + it('should merge sentryExternals with array previousExternal', () => { + const previousExternal = [/vue/, 'react']; + const result = getExternalOptionsWithSentryNuxt(previousExternal); + expect(result).toEqual([/^@sentry\/nuxt$/, /vue/, 'react']); + }); + + it('should create array with sentryExternals and non-array previousExternal', () => { + const previousExternal = 'vue'; + const result = getExternalOptionsWithSentryNuxt(previousExternal); + expect(result).toEqual([/^@sentry\/nuxt$/, 'vue']); + }); + + it('should create a proxy when previousExternal is a function', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + expect(typeof result).toBe('function'); + expect(result).toBeInstanceOf(Function); + }); + + it('should return true from proxied function when source is @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('@sentry/nuxt', undefined, false); + expect(output).toBe(true); + expect(mockExternalFn).not.toHaveBeenCalled(); + }); + + it('should return false from proxied function and call function when source just includes @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('@sentry/nuxt/dist/index.js', undefined, false); + expect(output).toBe(false); + expect(mockExternalFn).toHaveBeenCalledWith('@sentry/nuxt/dist/index.js', undefined, false); + }); + + it('should call original function when source does not include @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('vue', undefined, false); + expect(output).toBe(false); + expect(mockExternalFn).toHaveBeenCalledWith('vue', undefined, false); + }); +}); From 5eca4272b26178be98229a9040b655c309ea696a Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 28 May 2025 14:33:29 +0200 Subject: [PATCH 26/26] meta(changelog): Update changelog for 9.23.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1a42eed45f..1adce32abab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,43 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.23.0 + +### Important changes + +- **feat(browser): option to ignore certain resource types ([#16389](https://github.com/getsentry/sentry-javascript/pull/16389))** + +Adds an option to opt out of certain `resource.*` spans via `ignoreResourceSpans`. + +For example, to opt out of `resource.script` spans: + +```js +Sentry.browserTracingIntegration({ + ignoreResourceSpans: ['resource.script'], +}), +``` + +### Other changes + +- feat: Export `isEnabled` from all SDKs ([#16405](https://github.com/getsentry/sentry-javascript/pull/16405)) +- feat(browser): Disable client when browser extension is detected in `init()` ([#16354](https://github.com/getsentry/sentry-javascript/pull/16354)) +- feat(core): Allow re-use of `captureLog` ([#16352](https://github.com/getsentry/sentry-javascript/pull/16352)) +- feat(core): Export `_INTERNAL_captureSerializedLog` ([#16387](https://github.com/getsentry/sentry-javascript/pull/16387)) +- feat(deps): bump @opentelemetry/semantic-conventions from 1.32.0 to 1.34.0 ([#16393](https://github.com/getsentry/sentry-javascript/pull/16393)) +- feat(deps): bump @prisma/instrumentation from 6.7.0 to 6.8.2 ([#16392](https://github.com/getsentry/sentry-javascript/pull/16392)) +- feat(deps): bump @sentry/cli from 2.43.0 to 2.45.0 ([#16395](https://github.com/getsentry/sentry-javascript/pull/16395)) +- feat(deps): bump @sentry/webpack-plugin from 3.3.1 to 3.5.0 ([#16394](https://github.com/getsentry/sentry-javascript/pull/16394)) +- feat(nextjs): Include `static/chunks/main-*` files for `widenClientFileUpload` ([#16406](https://github.com/getsentry/sentry-javascript/pull/16406)) +- feat(node): Do not add HTTP & fetch span instrumentation if tracing is disabled ([#15730](https://github.com/getsentry/sentry-javascript/pull/15730)) +- feat(nuxt): Added support for nuxt layers ([#16372](https://github.com/getsentry/sentry-javascript/pull/16372)) +- fix(browser): Ensure logs are flushed when sendClientReports=false ([#16351](https://github.com/getsentry/sentry-javascript/pull/16351)) +- fix(browser): Move `browserTracingIntegration` code to `setup` hook ([#16386](https://github.com/getsentry/sentry-javascript/pull/16386)) +- fix(cloudflare): Capture exceptions thrown in hono ([#16355](https://github.com/getsentry/sentry-javascript/pull/16355)) +- fix(node): Don't warn about Spotlight on empty NODE_ENV ([#16381](https://github.com/getsentry/sentry-javascript/pull/16381)) +- fix(node): Suppress Spotlight calls ([#16380](https://github.com/getsentry/sentry-javascript/pull/16380)) +- fix(nuxt): Add `@sentry/nuxt` as external in Rollup ([#16407](https://github.com/getsentry/sentry-javascript/pull/16407)) +- fix(opentelemetry): Ensure `withScope` keeps span active & `_getTraceInfoFromScope` works ([#16385](https://github.com/getsentry/sentry-javascript/pull/16385)) + Work in this release was contributed by @Xenossolitarius. Thank you for your contribution! ## 9.22.0