diff --git a/.craft.yml b/.craft.yml index a5df75a24519..deb38bf0c40d 100644 --- a/.craft.yml +++ b/.craft.yml @@ -193,8 +193,12 @@ targets: onlyIfPresent: /^sentry-gatsby-\d.*\.tgz$/ 'npm:@sentry/google-cloud-serverless': onlyIfPresent: /^sentry-google-cloud-serverless-\d.*\.tgz$/ + 'npm:@sentry/nestjs': + onlyIfPresent: /^sentry-nestjs-\d.*\.tgz$/ 'npm:@sentry/nextjs': onlyIfPresent: /^sentry-nextjs-\d.*\.tgz$/ + 'npm:@sentry/nuxt': + onlyIfPresent: /^sentry-nuxt-\d.*\.tgz$/ 'npm:@sentry/node': onlyIfPresent: /^sentry-node-\d.*\.tgz$/ 'npm:@sentry/react': @@ -211,6 +215,8 @@ targets: onlyIfPresent: /^sentry-svelte-\d.*\.tgz$/ 'npm:@sentry/sveltekit': onlyIfPresent: /^sentry-sveltekit-\d.*\.tgz$/ + 'npm:@sentry/tanstackstart-react': + onlyIfPresent: /^sentry-tanstackstart-react-\d.*\.tgz$/ 'npm:@sentry/vercel-edge': onlyIfPresent: /^sentry-vercel-edge-\d.*\.tgz$/ 'npm:@sentry/vue': diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 037d063f37a1..5e172393dc37 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aad0f9057953..978e728e1f56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,7 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dev-packages/*/build ${{ github.workspace }}/packages/*/build + ${{ github.workspace }}/packages/*/lib ${{ github.workspace }}/packages/ember/*.d.ts ${{ github.workspace }}/packages/gatsby/*.d.ts ${{ github.workspace }}/packages/utils/cjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c64738032e43..af083427288e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} diff --git a/.size-limit.js b/.size-limit.js index 7bab8160966b..eed705e16da6 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -79,7 +79,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '80.5 KB', + limit: '81 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -210,7 +210,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '41 KB', + limit: '42 KB', }, // SvelteKit SDK (ESM) { @@ -219,7 +219,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '38 KB', + limit: '38.5 KB', }, // Node SDK (ESM) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f43d0a78d30..266092c9064e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,21 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.11.0 + +- feat(browser): Add `http.redirect_count` attribute to `browser.redirect` span ([#15943](https://github.com/getsentry/sentry-javascript/pull/15943)) +- feat(core): Add `consoleLoggingIntegration` for logs ([#15955](https://github.com/getsentry/sentry-javascript/pull/15955)) +- feat(core): Don't truncate error messages ([#15818](https://github.com/getsentry/sentry-javascript/pull/15818)) +- feat(nextjs): Add release injection in Turbopack ([#15958](https://github.com/getsentry/sentry-javascript/pull/15958)) +- feat(nextjs): Record `turbopack` as tag ([#15928](https://github.com/getsentry/sentry-javascript/pull/15928)) +- feat(nuxt): Base decision on source maps upload only on Nuxt source map settings ([#15859](https://github.com/getsentry/sentry-javascript/pull/15859)) +- feat(react-router): Add `sentryHandleRequest` ([#15787](https://github.com/getsentry/sentry-javascript/pull/15787)) +- fix(node): Use `module` instead of `require` for CJS check ([#15927](https://github.com/getsentry/sentry-javascript/pull/15927)) +- fix(remix): Remove mentions of deprecated `ErrorBoundary` wrapper ([#15930](https://github.com/getsentry/sentry-javascript/pull/15930)) +- ref(browser): Temporarily add `sentry.previous_trace` span attribute ([#15957](https://github.com/getsentry/sentry-javascript/pull/15957)) +- ref(browser/core): Move all log flushing logic into clients ([#15831](https://github.com/getsentry/sentry-javascript/pull/15831)) +- ref(core): Improve URL parsing utilities ([#15882](https://github.com/getsentry/sentry-javascript/pull/15882)) + ## 9.10.1 - fix: Correct @sentry-internal/feedback docs to match the code ([#15874](https://github.com/getsentry/sentry-javascript/pull/15874)) diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/subject.js index f7eb81412b13..1fd43c508071 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/subject.js @@ -1,13 +1,13 @@ -const wat = new Error(`This is a very long message that should be truncated and will be, -this is a very long message that should be truncated and will be, -this is a very long message that should be truncated and will be, -this is a very long message that should be truncated and will be, -this is a very long message that should be truncated and will be`); +const wat = new Error(`This is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be`); -wat.cause = new Error(`This is a very long message that should be truncated and hopefully will be, -this is a very long message that should be truncated and hopefully will be, -this is a very long message that should be truncated and hopefully will be, -this is a very long message that should be truncated and hopefully will be, -this is a very long message that should be truncated and hopefully will be,`); +wat.cause = new Error(`This is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be`); Sentry.captureException(wat); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/test.ts index 61dc0e3418f6..2b9910e38b1d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/test.ts @@ -12,10 +12,11 @@ sentryTest('should capture a linked error with messages', async ({ getLocalTestU expect(eventData.exception?.values).toHaveLength(2); expect(eventData.exception?.values?.[0]).toMatchObject({ type: 'Error', - value: `This is a very long message that should be truncated and hopefully will be, -this is a very long message that should be truncated and hopefully will be, -this is a very long message that should be truncated and hopefully will be, -this is a very long me...`, + value: `This is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be, +this is a very long message that should not be truncated and hopefully won't be`, mechanism: { type: 'chained', handled: true, @@ -26,10 +27,11 @@ this is a very long me...`, }); expect(eventData.exception?.values?.[1]).toMatchObject({ type: 'Error', - value: `This is a very long message that should be truncated and will be, -this is a very long message that should be truncated and will be, -this is a very long message that should be truncated and will be, -this is a very long message that should be truncated...`, + value: `This is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be, +this is a very long message that should not be truncated and won't be`, mechanism: { type: 'generic', handled: true, diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/thrown-string-large/subject.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/thrown-string-large/subject.js deleted file mode 100644 index aae0c81d4df9..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/thrown-string-large/subject.js +++ /dev/null @@ -1,9 +0,0 @@ -function run() { - const reason = 'stringError'.repeat(100); - const promise = Promise.reject(reason); - const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason }); - // simulate window.onunhandledrejection without generating a Script error - window.onunhandledrejection(event); -} - -run(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/thrown-string-large/test.ts b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/thrown-string-large/test.ts deleted file mode 100644 index e198d6f944f0..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/thrown-string-large/test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; - -import { sentryTest } from '../../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; - -sentryTest('should capture unhandledrejection with a large string', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.exception?.values).toHaveLength(1); - expect(eventData.exception?.values?.[0]).toMatchObject({ - type: 'UnhandledRejection', - value: - 'Non-Error promise rejection captured with value: stringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstr...', - mechanism: { - type: 'onunhandledrejection', - handled: false, - }, - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js new file mode 100644 index 000000000000..e0ceaaebf017 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + _experiments: { + enableLogs: true, + }, + integrations: [Sentry.consoleLoggingIntegration()], +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js new file mode 100644 index 000000000000..6c2e9cfdde7a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js @@ -0,0 +1,11 @@ +console.trace('console.trace', 123, false); +console.debug('console.debug', 123, false); +console.log('console.log', 123, false); +console.info('console.info', 123, false); +console.warn('console.warn', 123, false); +console.error('console.error', 123, false); +console.assert(false, 'console.assert', 123, false); + +console.log(''); + +Sentry.flush(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts new file mode 100644 index 000000000000..00e918ce9719 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts @@ -0,0 +1,116 @@ +import { expect } from '@playwright/test'; +import type { OtelLogEnvelope } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../../utils/helpers'; + +sentryTest('should capture console object calls', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE || ''; + // Only run this for npm package exports + if (bundle.startsWith('bundle') || bundle.startsWith('loader')) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const event = await getFirstSentryEnvelopeRequest(page, url, properFullEnvelopeRequestParser); + const envelopeItems = event[1]; + + expect(envelopeItems[0]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'trace', + body: { stringValue: 'console.trace 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 1, + }, + ]); + + expect(envelopeItems[1]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'debug', + body: { stringValue: 'console.debug 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 5, + }, + ]); + + expect(envelopeItems[2]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'info', + body: { stringValue: 'console.log 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 10, + }, + ]); + + expect(envelopeItems[3]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'info', + body: { stringValue: 'console.info 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 9, + }, + ]); + + expect(envelopeItems[4]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'warn', + body: { stringValue: 'console.warn 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 13, + }, + ]); + + expect(envelopeItems[5]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'error', + body: { stringValue: 'console.error 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 17, + }, + ]); + + expect(envelopeItems[6]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'error', + body: { stringValue: 'Assertion failed: console.assert 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 17, + }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/logger/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/logger/simple/subject.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts similarity index 98% rename from dev-packages/browser-integration-tests/suites/public-api/logger/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts index 53a5f31ffcbb..84f2a34e8fe5 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test'; import type { OtelLogEnvelope } from '@sentry/core'; -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../utils/helpers'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../../utils/helpers'; sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page }) => { const bundle = process.env.PW_BUNDLE || ''; diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/default/test.ts index bf1d9f78e308..f36aa22e6a12 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/default/test.ts @@ -49,6 +49,10 @@ sentryTest("navigation spans link back to previous trace's root span", async ({ }, ]); + expect(navigation1TraceContext?.data).toMatchObject({ + 'sentry.previous_trace': `${pageloadTraceId}-${pageloadTraceContext?.span_id}-1`, + }); + expect(navigation2TraceContext?.links).toEqual([ { trace_id: navigation1TraceId, @@ -60,6 +64,10 @@ sentryTest("navigation spans link back to previous trace's root span", async ({ }, ]); + expect(navigation2TraceContext?.data).toMatchObject({ + 'sentry.previous_trace': `${navigation1TraceId}-${navigation1TraceContext?.span_id}-1`, + }); + expect(pageloadTraceId).not.toEqual(navigation1TraceId); expect(navigation1TraceId).not.toEqual(navigation2TraceId); expect(pageloadTraceId).not.toEqual(navigation2TraceId); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/test.ts index 2563b22ad701..6c0ec874684b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/test.ts @@ -34,6 +34,10 @@ sentryTest('includes a span link to a previously negatively sampled span', async }, ]); + expect(navigationTraceContext?.data).toMatchObject({ + 'sentry.previous_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-0/), + }); + expect(navigationTraceContext?.trace_id).not.toEqual(navigationTraceContext?.links![0].trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts index 4bdafbc7ed10..bd091fcfd354 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts @@ -43,7 +43,7 @@ test('should correctly instrument `fetch` for performance tracing', async ({ pag 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.browser', }, - description: 'GET https://example.com', + description: 'GET https://example.com/', op: 'http.client', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts index 725dd6f24515..3cd7048ed947 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts @@ -3,4 +3,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js index e09e64bac6a2..a0d2b254bc42 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js @@ -9,4 +9,7 @@ const nextConfig = { module.exports = withSentryConfig(nextConfig, { silent: true, + release: { + name: 'foobar123', + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index 243c719da4c2..94e762a859a9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -17,7 +17,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "15.3.0-canary.8", + "next": "15.3.0-canary.26", "react": "rc", "react-dom": "rc", "typescript": "~5.0.0" diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts index 52e492b3f234..032aa55b3116 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts @@ -28,4 +28,14 @@ test('Should propagate traces from server to client in pages router', async ({ p expect(serverTransaction.contexts?.trace?.trace_id).toBeDefined(); expect(pageloadTransaction.contexts?.trace?.trace_id).toBe(serverTransaction.contexts?.trace?.trace_id); + + await test.step('release was successfully injected on the serverside', () => { + // Release as defined in next.config.js + expect(serverTransaction.release).toBe('foobar123'); + }); + + await test.step('release was successfully injected on the clientside', () => { + // Release as defined in next.config.js + expect(pageloadTransaction.release).toBe('foobar123'); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx index faa62bd97197..567edfe4e032 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx @@ -9,7 +9,7 @@ import type { AppLoadContext, EntryContext } from 'react-router'; import { ServerRouter } from 'react-router'; const ABORT_DELAY = 5_000; -export default function handleRequest( +function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, @@ -60,6 +60,8 @@ export default function handleRequest( }); } +export default Sentry.sentryHandleRequest(handleRequest); + import { type HandleErrorFunction } from 'react-router'; export const handleError: HandleErrorFunction = (error, { request }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json index cdd96f39569e..a9afbbfcd07b 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json @@ -10,6 +10,15 @@ "@react-router/node": "^7.1.5", "@react-router/serve": "^7.1.5", "@sentry/react-router": "latest || *", + "@sentry-internal/feedback": "latest || *", + "@sentry-internal/replay-canvas": "latest || *", + "@sentry-internal/browser-utils": "latest || *", + "@sentry/browser": "latest || *", + "@sentry/core": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@sentry/react": "latest || *", + "@sentry-internal/replay": "latest || *", "isbot": "^5.1.17" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts index f080d01064ea..4f570beca144 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts @@ -5,8 +5,7 @@ import { APP_NAME } from '../constants'; test.describe('servery - performance', () => { test('should send server transaction on pageload', async ({ page }) => { const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - // todo: should be GET /performance - return transactionEvent.transaction === 'GET *'; + return transactionEvent.transaction === 'GET /performance'; }); await page.goto(`/performance`); @@ -30,8 +29,7 @@ test.describe('servery - performance', () => { spans: expect.any(Array), start_timestamp: expect.any(Number), timestamp: expect.any(Number), - // todo: should be GET /performance - transaction: 'GET *', + transaction: 'GET /performance', type: 'transaction', transaction_info: { source: 'route' }, platform: 'node', @@ -58,8 +56,7 @@ test.describe('servery - performance', () => { test('should send server transaction on parameterized route', async ({ page }) => { const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - // todo: should be GET /performance/with/:param - return transactionEvent.transaction === 'GET *'; + return transactionEvent.transaction === 'GET /performance/with/:param'; }); await page.goto(`/performance/with/some-param`); @@ -83,8 +80,7 @@ test.describe('servery - performance', () => { spans: expect.any(Array), start_timestamp: expect.any(Number), timestamp: expect.any(Number), - // todo: should be GET /performance/with/:param - transaction: 'GET *', + transaction: 'GET /performance/with/:param', type: 'transaction', transaction_info: { source: 'route' }, platform: 'node', diff --git a/package.json b/package.json index 8985b40af8d0..a3fb0123a5ae 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "lint": "run-s lint:prettier lint:lerna", "lint:lerna": "lerna run lint", "lint:prettier": "prettier \"**/*.{md,css,yml,yaml}\" \"packages/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" --check", - "lint:es-compatibility": "es-check es2020 ./packages/*/build/{bundles,npm/cjs,cjs}/*.js && es-check es2020 ./packages/*/build/{npm/esm,esm}/*.js --module", + "lint:es-compatibility": "lerna run lint:es-compatibility", "dedupe-deps:check": "yarn-deduplicate yarn.lock --list --fail", "dedupe-deps:fix": "yarn-deduplicate yarn.lock", "postpublish": "lerna run --stream --concurrency 1 postpublish", diff --git a/packages/angular/package.json b/packages/angular/package.json index 3568f8eec2ec..0d2088bd31cb 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -53,6 +53,7 @@ "clean": "rimraf build coverage sentry-angular-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/{esm2020,fesm2015,fesm2020}/*.mjs --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:unit:watch": "vitest --watch", diff --git a/packages/astro/package.json b/packages/astro/package.json index 94ba9f5a2be4..40f9e60f9717 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -79,6 +79,7 @@ "clean": "rimraf build coverage sentry-astro-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 6467affd841c..d89503eb9dfb 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -128,6 +128,7 @@ export { zodErrorsIntegration, profiler, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 14844c374819..443d5b9c45f7 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -93,6 +93,7 @@ "clean": "rimraf build dist-awslambda-layer coverage sentry-serverless-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/npm/cjs/*.js && es-check es2022 ./build/npm/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 473b87c793d9..59465831a734 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -114,6 +114,7 @@ export { amqplibIntegration, vercelAIIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index e16c6b5b11a9..9bef36f6caa2 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -56,6 +56,7 @@ "clean": "rimraf build coverage sentry-internal-browser-utils-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test:unit": "vitest run", "test": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 62201a72ceab..dbdc54dca512 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -460,8 +460,11 @@ export function _addMeasureSpans( } } -/** Instrument navigation entries */ -function _addNavigationSpans(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void { +/** + * Instrument navigation entries + * exported only for tests + */ +export function _addNavigationSpans(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void { (['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'] as const).forEach(event => { _addPerformanceNavigationTiming(span, entry, event, timeOrigin); }); @@ -511,6 +514,7 @@ function _addPerformanceNavigationTiming( name: entry.name, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + ...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}), }, }); } diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 69169e01af67..685f7e2c41ed 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -11,7 +11,7 @@ import { import type { Span } from '@sentry/core'; import { describe, beforeEach, it, expect, beforeAll, afterAll } from 'vitest'; -import { _addMeasureSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; +import { _addMeasureSpans, _addNavigationSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; import { WINDOW } from '../../src/types'; import { TestClient, getDefaultClientOptions } from '../utils/TestClient'; @@ -416,6 +416,188 @@ describe('_addResourceSpans', () => { ); }); +describe('_addNavigationSpans', () => { + const pageloadSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + + beforeAll(() => { + setGlobalLocation(mockWindowLocation); + }); + + afterAll(() => { + resetGlobalLocation(); + }); + + beforeEach(() => { + getCurrentScope().clear(); + getIsolationScope().clear(); + + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + client.init(); + }); + + it('adds navigation spans based on the navigation performance entry', () => { + // entry taken from a real entry via browser dev tools + const entry: PerformanceNavigationTiming = { + name: 'https://santry.com/test', + entryType: 'navigation', + startTime: 0, + duration: 546.1000000014901, + initiatorType: 'navigation', + nextHopProtocol: 'h2', + workerStart: 0, + redirectStart: 7.5, + redirectEnd: 20.5, + redirectCount: 2, + fetchStart: 4.9000000059604645, + domainLookupStart: 4.9000000059604645, + domainLookupEnd: 4.9000000059604645, + connectStart: 4.9000000059604645, + secureConnectionStart: 4.9000000059604645, + connectEnd: 4.9000000059604645, + requestStart: 7.9000000059604645, + responseStart: 396.80000000447035, + responseEnd: 416.40000000596046, + transferSize: 14726, + encodedBodySize: 14426, + decodedBodySize: 67232, + responseStatus: 200, + serverTiming: [], + unloadEventStart: 0, + unloadEventEnd: 0, + domInteractive: 473.20000000298023, + domContentLoadedEventStart: 480.1000000014901, + domContentLoadedEventEnd: 480.30000000447035, + domComplete: 546, + loadEventStart: 546, + loadEventEnd: 546.1000000014901, + type: 'navigate', + activationStart: 0, + toJSON: () => ({}), + }; + const spans: Span[] = []; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + _addNavigationSpans(pageloadSpan, entry, 999); + + const trace_id = pageloadSpan.spanContext().traceId; + const parent_span_id = pageloadSpan.spanContext().spanId; + + expect(spans).toHaveLength(9); + expect(spans.map(spanToJSON)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + 'sentry.op': 'browser.domContentLoadedEvent', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.domContentLoadedEvent', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.loadEvent', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.loadEvent', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.connect', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.connect', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.TLS/SSL', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.TLS/SSL', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.cache', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.cache', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.DNS', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.DNS', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.request', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.request', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'sentry.op': 'browser.response', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.response', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + expect.objectContaining({ + data: { + 'http.redirect_count': 2, + 'sentry.op': 'browser.redirect', + 'sentry.origin': 'auto.ui.browser.metrics', + }, + description: 'https://santry.com/test', + op: 'browser.redirect', + origin: 'auto.ui.browser.metrics', + parent_span_id, + trace_id, + }), + ]), + ); + }); +}); + const setGlobalLocation = (location: Location) => { // @ts-expect-error need to delete this in order to set to new value delete WINDOW.location; diff --git a/packages/browser/package.json b/packages/browser/package.json index b2eb2bd9376a..2cb52ce71e5a 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -67,6 +67,7 @@ "clean": "rimraf build coverage .rpt2_cache sentry-browser-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/{bundles,npm/cjs}/*.js && es-check es2020 ./build/npm/esm/*.js --module", "size:check": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES2017: \",$1,\"kB\";}'", "test": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 0e5b3fb6214c..4ae9562f7ea3 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -15,11 +15,14 @@ import { addAutoIpAddressToUser, applySdkMetadata, getSDKSource, + _INTERNAL_flushLogsBuffer, } from '@sentry/core'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; +const DEFAULT_FLUSH_INTERVAL = 5000; + /** * Configuration options for the Sentry Browser SDK. * @see @sentry/core Options for more information. @@ -65,6 +68,7 @@ export type BrowserClientOptions = ClientOptions & * @see SentryClient for usage documentation. */ export class BrowserClient extends Client { + private _logFlushIdleTimeout: ReturnType | undefined; /** * Creates a new Browser SDK instance. * @@ -81,17 +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 enableLogs = _experiments?.enableLogs; + if (opts.sendClientReports && WINDOW.document) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { this._flushOutcomes(); + if (enableLogs) { + _INTERNAL_flushLogsBuffer(client); + } } }); } - if (this._options.sendDefaultPii) { - this.on('postprocessEvent', addAutoIpAddressToUser); - this.on('beforeSendSession', addAutoIpAddressToSession); + if (enableLogs) { + client.on('flush', () => { + _INTERNAL_flushLogsBuffer(client); + }); + + client.on('afterCaptureLog', () => { + if (client._logFlushIdleTimeout) { + clearTimeout(client._logFlushIdleTimeout); + } + + client._logFlushIdleTimeout = setTimeout(() => { + _INTERNAL_flushLogsBuffer(client); + }, DEFAULT_FLUSH_INTERVAL); + }); + } + + if (sendDefaultPii) { + client.on('postprocessEvent', addAutoIpAddressToUser); + client.on('beforeSendSession', addAutoIpAddressToSession); } } diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 9103bbab99b5..275144cd280c 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -14,6 +14,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, captureFeedback, + consoleLoggingIntegration, } from '@sentry/core'; export { replayIntegration, getReplay } from '@sentry-internal/replay'; diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 4b99ac876386..d0eace11aadb 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -25,7 +25,6 @@ const _linkedErrorsIntegration = ((options: LinkedErrorsOptions = {}) => { // This differs from the LinkedErrors integration in core by using a different exceptionFromError function exceptionFromError, options.stackParser, - options.maxValueLength, key, limit, event, diff --git a/packages/browser/src/log.ts b/packages/browser/src/log.ts index 23322c168a67..ba4783e0c1c4 100644 --- a/packages/browser/src/log.ts +++ b/packages/browser/src/log.ts @@ -1,53 +1,5 @@ -import type { LogSeverityLevel, Log, Client, ParameterizedString } from '@sentry/core'; -import { getClient, _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '@sentry/core'; - -import { WINDOW } from './helpers'; - -/** - * TODO: Make this configurable - */ -const DEFAULT_FLUSH_INTERVAL = 5000; - -let timeout: ReturnType | undefined; - -/** - * This is a global timeout that is used to flush the logs buffer. - * It is used to ensure that logs are flushed even if the client is not flushed. - */ -function startFlushTimeout(client: Client): void { - if (timeout) { - clearTimeout(timeout); - } - - timeout = setTimeout(() => { - _INTERNAL_flushLogsBuffer(client); - }, DEFAULT_FLUSH_INTERVAL); -} - -let isClientListenerAdded = false; -/** - * This is a function that is used to add a flush listener to the client. - * It is used to ensure that the logger buffer is flushed when the client is flushed. - */ -function addFlushingListeners(client: Client): void { - if (isClientListenerAdded || !client.getOptions()._experiments?.enableLogs) { - return; - } - - isClientListenerAdded = true; - - if (WINDOW.document) { - WINDOW.document.addEventListener('visibilitychange', () => { - if (WINDOW.document.visibilityState === 'hidden') { - _INTERNAL_flushLogsBuffer(client); - } - }); - } - - client.on('flush', () => { - _INTERNAL_flushLogsBuffer(client); - }); -} +import type { LogSeverityLevel, Log, ParameterizedString } from '@sentry/core'; +import { _INTERNAL_captureLog } from '@sentry/core'; /** * Capture a log with the given level. @@ -63,14 +15,7 @@ function captureLog( attributes?: Log['attributes'], severityNumber?: Log['severityNumber'], ): void { - const client = getClient(); - if (client) { - addFlushingListeners(client); - - startFlushTimeout(client); - } - - _INTERNAL_captureLog({ level, message, attributes, severityNumber }, client, undefined); + _INTERNAL_captureLog({ level, message, attributes, severityNumber }); } /** diff --git a/packages/browser/src/tracing/previousTrace.ts b/packages/browser/src/tracing/previousTrace.ts index fd968c5a5cc3..91e52b519dad 100644 --- a/packages/browser/src/tracing/previousTrace.ts +++ b/packages/browser/src/tracing/previousTrace.ts @@ -21,6 +21,8 @@ export const PREVIOUS_TRACE_MAX_DURATION = 3600; // session storage key export const PREVIOUS_TRACE_KEY = 'sentry_previous_trace'; +export const PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE = 'sentry.previous_trace'; + /** * Adds a previous_trace span link to the passed span if the passed * previousTraceInfo is still valid. @@ -41,7 +43,8 @@ export function addPreviousTraceSpanLink( }; } - if (previousTraceInfo.spanContext.traceId === spanJson.trace_id) { + const previousTraceSpanCtx = previousTraceInfo.spanContext; + if (previousTraceSpanCtx.traceId === spanJson.trace_id) { // This means, we're still in the same trace so let's not update the previous trace info // or add a link to the current span. // Once we move away from the long-lived, route-based trace model, we can remove this cases @@ -56,7 +59,7 @@ export function addPreviousTraceSpanLink( if (Date.now() / 1000 - previousTraceInfo.startTimestamp <= PREVIOUS_TRACE_MAX_DURATION) { if (DEBUG_BUILD) { logger.info( - `Adding previous_trace ${previousTraceInfo.spanContext} link to span ${{ + `Adding previous_trace ${previousTraceSpanCtx} link to span ${{ op: spanJson.op, ...span.spanContext(), }}`, @@ -64,11 +67,22 @@ export function addPreviousTraceSpanLink( } span.addLink({ - context: previousTraceInfo.spanContext, + context: previousTraceSpanCtx, attributes: { [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace', }, }); + + // TODO: Remove this once EAP can store span links. We currently only set this attribute so that we + // can obtain the previous trace information from the EAP store. Long-term, EAP will handle + // span links and then we should remove this again. Also throwing in a TODO(v10), to remind us + // to check this at v10 time :) + span.setAttribute( + PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE, + `${previousTraceSpanCtx.traceId}-${previousTraceSpanCtx.spanId}-${ + previousTraceSpanCtx.traceFlags === 0x1 ? 1 : 0 + }`, + ); } return { diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts new file mode 100644 index 000000000000..66d825eea892 --- /dev/null +++ b/packages/browser/test/client.test.ts @@ -0,0 +1,120 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as sentryCore from '@sentry/core'; +import { BrowserClient } from '../src/client'; +import { WINDOW } from '../src/helpers'; +import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; + +vi.mock('@sentry/core', async requireActual => { + return { + ...((await requireActual()) as any), + _INTERNAL_flushLogsBuffer: vi.fn(), + }; +}); + +describe('BrowserClient', () => { + let client: BrowserClient; + const DEFAULT_FLUSH_INTERVAL = 5000; + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + it('does not flush logs when logs are disabled', () => { + client = new BrowserClient( + getDefaultBrowserClientOptions({ + _experiments: { enableLogs: false }, + sendClientReports: true, + }), + ); + + // Add some logs + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client); + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client); + + // Simulate visibility change to hidden + if (WINDOW.document) { + Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden' }); + WINDOW.document.dispatchEvent(new Event('visibilitychange')); + } + + expect(sentryCore._INTERNAL_flushLogsBuffer).not.toHaveBeenCalled(); + }); + + describe('log flushing', () => { + beforeEach(() => { + vi.useFakeTimers(); + client = new BrowserClient( + getDefaultBrowserClientOptions({ + _experiments: { enableLogs: true }, + sendClientReports: true, + }), + ); + }); + + it('flushes logs when page visibility changes to hidden', () => { + const flushOutcomesSpy = vi.spyOn(client as any, '_flushOutcomes'); + + // Add some logs + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client); + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client); + + // Simulate visibility change to hidden + if (WINDOW.document) { + Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden' }); + WINDOW.document.dispatchEvent(new Event('visibilitychange')); + } + + expect(flushOutcomesSpy).toHaveBeenCalled(); + expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); + }); + + it('flushes logs on flush event', () => { + // Add some logs + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client); + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client); + + // Trigger flush event + client.emit('flush'); + + expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); + }); + + it('flushes logs after idle timeout', () => { + // Add a log which will trigger afterCaptureLog event + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log' }, client); + + // Fast forward the idle timeout + vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL); + + expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); + }); + + it('resets idle timeout when new logs are captured', () => { + // Add initial log + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client); + + // Fast forward part of the idle timeout + vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL / 2); + + // Add another log which should reset the timeout + sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client); + + // Fast forward the remaining time + vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL / 2); + + // Should not have flushed yet since timeout was reset + expect(sentryCore._INTERNAL_flushLogsBuffer).not.toHaveBeenCalled(); + + // Fast forward the full timeout + vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL); + + // Now should have flushed both logs + expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); + }); + }); +}); diff --git a/packages/browser/test/log.test.ts b/packages/browser/test/log.test.ts index 9cddc3ecfc71..d9bf63ce5fce 100644 --- a/packages/browser/test/log.test.ts +++ b/packages/browser/test/log.test.ts @@ -68,151 +68,87 @@ describe('Logger', () => { it('should call _INTERNAL_captureLog with trace level', () => { logger.trace('Test trace message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'trace', - message: 'Test trace message', - attributes: { key: 'value' }, - severityNumber: undefined, - }, - expect.any(Object), - undefined, - ); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'trace', + message: 'Test trace message', + attributes: { key: 'value' }, + severityNumber: undefined, + }); }); it('should call _INTERNAL_captureLog with debug level', () => { logger.debug('Test debug message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'debug', - message: 'Test debug message', - attributes: { key: 'value' }, - severityNumber: undefined, - }, - expect.any(Object), - undefined, - ); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'debug', + message: 'Test debug message', + attributes: { key: 'value' }, + severityNumber: undefined, + }); }); it('should call _INTERNAL_captureLog with info level', () => { logger.info('Test info message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'info', - message: 'Test info message', - attributes: { key: 'value' }, - severityNumber: undefined, - }, - expect.any(Object), - undefined, - ); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'info', + message: 'Test info message', + attributes: { key: 'value' }, + severityNumber: undefined, + }); }); it('should call _INTERNAL_captureLog with warn level', () => { logger.warn('Test warn message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'warn', - message: 'Test warn message', - attributes: { key: 'value' }, - severityNumber: undefined, - }, - expect.any(Object), - undefined, - ); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'warn', + message: 'Test warn message', + attributes: { key: 'value' }, + severityNumber: undefined, + }); }); it('should call _INTERNAL_captureLog with error level', () => { logger.error('Test error message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'error', - message: 'Test error message', - attributes: { key: 'value' }, - severityNumber: undefined, - }, - expect.any(Object), - undefined, - ); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'error', + message: 'Test error message', + attributes: { key: 'value' }, + severityNumber: undefined, + }); }); it('should call _INTERNAL_captureLog with fatal level', () => { logger.fatal('Test fatal message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'fatal', - message: 'Test fatal message', - attributes: { key: 'value' }, - severityNumber: undefined, - }, - expect.any(Object), - undefined, - ); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'fatal', + message: 'Test fatal message', + attributes: { key: 'value' }, + severityNumber: undefined, + }); }); }); - describe('Automatic flushing', () => { - it('should flush logs after timeout', () => { - logger.info('Test message'); - expect(mockFlushLogsBuffer).not.toHaveBeenCalled(); - - // Fast-forward time by 5000ms (the default flush interval) - vi.advanceTimersByTime(5000); - - expect(mockFlushLogsBuffer).toHaveBeenCalledTimes(1); - expect(mockFlushLogsBuffer).toHaveBeenCalledWith(expect.any(Object)); - }); - - it('should restart the flush timeout when a new log is captured', () => { - logger.info('First message'); - - // Advance time by 3000ms (not enough to trigger flush) - vi.advanceTimersByTime(3000); - expect(mockFlushLogsBuffer).not.toHaveBeenCalled(); - - // Log another message, which should reset the timer - logger.info('Second message'); - - // Advance time by 3000ms again (should be 6000ms total, but timer was reset) - vi.advanceTimersByTime(3000); - expect(mockFlushLogsBuffer).not.toHaveBeenCalled(); - - // Advance time to complete the 5000ms after the second message - vi.advanceTimersByTime(2000); - expect(mockFlushLogsBuffer).toHaveBeenCalledTimes(1); - }); - - it('should handle parameterized strings with parameters', () => { - logger.info(logger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 }); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'info', - message: expect.objectContaining({ - __sentry_template_string__: 'Hello %s, your balance is %s', - __sentry_template_values__: ['John', 100], - }), - attributes: { - userId: 123, - }, - }, - expect.any(Object), - undefined, - ); + it('should handle parameterized strings with parameters', () => { + logger.info(logger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 }); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'info', + message: expect.objectContaining({ + __sentry_template_string__: 'Hello %s, your balance is %s', + __sentry_template_values__: ['John', 100], + }), + attributes: { + userId: 123, + }, }); + }); - it('should handle parameterized strings without additional attributes', () => { - logger.debug(logger.fmt`User ${'Alice'} logged in from ${'mobile'}`); - expect(mockCaptureLog).toHaveBeenCalledWith( - { - level: 'debug', - message: expect.objectContaining({ - __sentry_template_string__: 'User %s logged in from %s', - __sentry_template_values__: ['Alice', 'mobile'], - }), - }, - expect.any(Object), - undefined, - ); + it('should handle parameterized strings without additional attributes', () => { + logger.debug(logger.fmt`User ${'Alice'} logged in from ${'mobile'}`); + expect(mockCaptureLog).toHaveBeenCalledWith({ + level: 'debug', + message: expect.objectContaining({ + __sentry_template_string__: 'User %s logged in from %s', + __sentry_template_values__: ['Alice', 'mobile'], + }), }); }); }); diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index fe9005d47215..6fd155b36e35 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -40,6 +40,7 @@ import { startBrowserTracingPageLoadSpan, } from '../../src/tracing/browserTracingIntegration'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; +import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/previousTrace'; // We're setting up JSDom here because the Next.js routing instrumentations requires a few things to be present on pageload: // 1. Access to window.document API for `window.document.getElementById` @@ -202,6 +203,7 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: `${span?.spanContext().traceId}-${span?.spanContext().spanId}-1`, }, links: [ { @@ -239,6 +241,7 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: `${span2?.spanContext().traceId}-${span2?.spanContext().spanId}-1`, }, links: [ { @@ -502,6 +505,7 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), }, links: [ { diff --git a/packages/browser/test/tracing/previousTrace.test.ts b/packages/browser/test/tracing/previousTrace.test.ts index f5815cbedc68..e3e3e3cc597e 100644 --- a/packages/browser/test/tracing/previousTrace.test.ts +++ b/packages/browser/test/tracing/previousTrace.test.ts @@ -5,6 +5,7 @@ import { getPreviousTraceFromSessionStorage, PREVIOUS_TRACE_KEY, PREVIOUS_TRACE_MAX_DURATION, + PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE, } from '../../src/tracing/previousTrace'; import { SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; import { storePreviousTraceInSessionStorage } from '../../src/tracing/previousTrace'; @@ -34,7 +35,9 @@ describe('addPreviousTraceSpanLink', () => { const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); - expect(spanToJSON(currentSpan).links).toEqual([ + const spanJson = spanToJSON(currentSpan); + + expect(spanJson.links).toEqual([ { attributes: { 'sentry.link.type': 'previous_trace', @@ -45,6 +48,10 @@ describe('addPreviousTraceSpanLink', () => { }, ]); + expect(spanJson.data).toMatchObject({ + [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: '123-456-1', + }); + expect(updatedPreviousTraceInfo).toEqual({ spanContext: currentSpan.spanContext(), startTimestamp: currentSpanStart, @@ -70,7 +77,11 @@ describe('addPreviousTraceSpanLink', () => { const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); - expect(spanToJSON(currentSpan).links).toBeUndefined(); + const spanJson = spanToJSON(currentSpan); + + expect(spanJson.links).toBeUndefined(); + + expect(Object.keys(spanJson.data)).not.toContain(PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE); // but still updates the previousTraceInfo to the current span expect(updatedPreviousTraceInfo).toEqual({ @@ -141,7 +152,9 @@ describe('addPreviousTraceSpanLink', () => { const updatedPreviousTraceInfo = addPreviousTraceSpanLink(undefined, currentSpan); - expect(spanToJSON(currentSpan).links).toBeUndefined(); + const spanJson = spanToJSON(currentSpan); + expect(spanJson.links).toBeUndefined(); + expect(Object.keys(spanJson.data)).not.toContain(PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE); expect(updatedPreviousTraceInfo).toEqual({ spanContext: currentSpan.spanContext(), @@ -169,7 +182,9 @@ describe('addPreviousTraceSpanLink', () => { const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); - expect(spanToJSON(currentSpan).links).toBeUndefined(); + const spanJson = spanToJSON(currentSpan); + expect(spanJson.links).toBeUndefined(); + expect(Object.keys(spanJson.data)).not.toContain(PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE); expect(updatedPreviousTraceInfo).toBe(previousTraceInfo); }); diff --git a/packages/bun/package.json b/packages/bun/package.json index 74a546cba815..a5074ed05a75 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -62,6 +62,7 @@ "clean": "rimraf build coverage sentry-bun-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "install:bun": "node ./scripts/install-bun.js", "test": "run-s install:bun test:bun", "test:bun": "bun test", diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index c2aff0f1ca52..a1c26d5a2819 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -133,6 +133,7 @@ export { amqplibIntegration, vercelAIIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 06386bb10aca..66bb26ff05ed 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -80,6 +80,7 @@ "clean": "rimraf build coverage sentry-cloudflare-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/core/package.json b/packages/core/package.json index 135b38526cb3..12868d6183d8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,6 +54,7 @@ "clean": "rimraf build coverage sentry-core-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 650cad83effa..5f0c9cc30b56 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -1,12 +1,11 @@ -/* eslint-disable complexity */ import { getClient } from './currentScopes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; -import type { FetchBreadcrumbHint, HandlerDataFetch, Span, SpanOrigin } from './types-hoist'; +import type { FetchBreadcrumbHint, HandlerDataFetch, Span, SpanAttributes, SpanOrigin } from './types-hoist'; import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; -import { parseStringToURL, stripUrlQueryAndFragment } from './utils-hoist/url'; +import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils-hoist/url'; import { hasSpansEnabled } from './utils/hasSpansEnabled'; import { getActiveSpan } from './utils/spanUtils'; import { getTraceData } from './utils/traceData'; @@ -54,40 +53,11 @@ export function instrumentFetchRequest( return undefined; } - // Curious about `thismessage:/`? See: https://www.rfc-editor.org/rfc/rfc2557.html - // > When the methods above do not yield an absolute URI, a base URL - // > of "thismessage:/" MUST be employed. This base URL has been - // > defined for the sole purpose of resolving relative references - // > within a multipart/related structure when no other base URI is - // > specified. - // - // We need to provide a base URL to `parseStringToURL` because the fetch API gives us a - // relative URL sometimes. - // - // This is the only case where we need to provide a base URL to `parseStringToURL` - // because the relative URL is not valid on its own. - const parsedUrl = url.startsWith('/') ? parseStringToURL(url, 'thismessage:/') : parseStringToURL(url); - const fullUrl = url.startsWith('/') ? undefined : parsedUrl?.href; - const hasParent = !!getActiveSpan(); const span = shouldCreateSpanResult && hasParent - ? startInactiveSpan({ - name: `${method} ${stripUrlQueryAndFragment(url)}`, - attributes: { - url, - type: 'fetch', - 'http.method': method, - 'http.url': parsedUrl?.href, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - ...(fullUrl && { 'http.url': fullUrl }), - ...(fullUrl && parsedUrl?.host && { 'server.address': parsedUrl.host }), - ...(parsedUrl?.search && { 'http.query': parsedUrl.search }), - ...(parsedUrl?.hash && { 'http.fragment': parsedUrl.hash }), - }, - }) + ? startInactiveSpan(getSpanStartOptions(url, method, spanOrigin)) : new SentryNonRecordingSpan(); handlerData.fetchData.__span = span.spanContext().spanId; @@ -264,3 +234,43 @@ function isRequest(request: unknown): request is Request { function isHeaders(headers: unknown): headers is Headers { return typeof Headers !== 'undefined' && isInstanceOf(headers, Headers); } + +function getSpanStartOptions( + url: string, + method: string, + spanOrigin: SpanOrigin, +): Parameters[0] { + const parsedUrl = parseStringToURLObject(url); + return { + name: parsedUrl ? `${method} ${getSanitizedUrlStringFromUrlObject(parsedUrl)}` : method, + attributes: getFetchSpanAttributes(url, parsedUrl, method, spanOrigin), + }; +} + +function getFetchSpanAttributes( + url: string, + parsedUrl: ReturnType, + method: string, + spanOrigin: SpanOrigin, +): SpanAttributes { + const attributes: SpanAttributes = { + url, + type: 'fetch', + 'http.method': method, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + }; + if (parsedUrl) { + if (!isURLObjectRelative(parsedUrl)) { + attributes['http.url'] = parsedUrl.href; + attributes['server.address'] = parsedUrl.host; + } + if (parsedUrl.search) { + attributes['http.query'] = parsedUrl.search; + } + if (parsedUrl.hash) { + attributes['http.fragment'] = parsedUrl.hash; + } + } + return attributes; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 800df99e1c0e..6c9a7fdde82e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -113,7 +113,8 @@ export { instrumentFetchRequest } from './fetch'; export { trpcMiddleware } from './trpc'; export { captureFeedback } from './feedback'; export type { ReportDialogOptions } from './report-dialog'; -export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs'; +export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs/exports'; +export { consoleLoggingIntegration } from './logs/console-integration'; // TODO: Make this structure pretty again and don't do "export *" export * from './utils-hoist/index'; diff --git a/packages/core/src/integrations/linkederrors.ts b/packages/core/src/integrations/linkederrors.ts index caaefae1a6d5..e5a6b84918c2 100644 --- a/packages/core/src/integrations/linkederrors.ts +++ b/packages/core/src/integrations/linkederrors.ts @@ -22,15 +22,7 @@ const _linkedErrorsIntegration = ((options: LinkedErrorsOptions = {}) => { preprocessEvent(event, hint, client) { const options = client.getOptions(); - applyAggregateErrorsToEvent( - exceptionFromError, - options.stackParser, - options.maxValueLength, - key, - limit, - event, - hint, - ); + applyAggregateErrorsToEvent(exceptionFromError, options.stackParser, key, limit, event, hint); }, }; }) satisfies IntegrationFn; diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts new file mode 100644 index 000000000000..6dbb829db5a6 --- /dev/null +++ b/packages/core/src/logs/console-integration.ts @@ -0,0 +1,82 @@ +import { getClient } from '../currentScopes'; +import { DEBUG_BUILD } from '../debug-build'; +import { defineIntegration } from '../integration'; +import type { ConsoleLevel, IntegrationFn } from '../types-hoist'; +import { CONSOLE_LEVELS, GLOBAL_OBJ, addConsoleInstrumentationHandler, logger, safeJoin } from '../utils-hoist'; +import { _INTERNAL_captureLog } from './exports'; + +interface CaptureConsoleOptions { + levels: ConsoleLevel[]; +} + +type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { + util: { + format: (...args: unknown[]) => string; + }; +}; + +const INTEGRATION_NAME = 'ConsoleLogs'; + +const _consoleLoggingIntegration = ((options: Partial = {}) => { + const levels = options.levels || CONSOLE_LEVELS; + + return { + name: INTEGRATION_NAME, + setup(client) { + if (!client.getOptions()._experiments?.enableLogs) { + DEBUG_BUILD && logger.warn('`_experiments.enableLogs` is not enabled, ConsoleLogs integration disabled'); + return; + } + + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client || !levels.includes(level)) { + return; + } + + if (level === 'assert') { + if (!args[0]) { + const followingArgs = args.slice(1); + const message = + followingArgs.length > 0 ? `Assertion failed: ${formatConsoleArgs(followingArgs)}` : 'Assertion failed'; + _INTERNAL_captureLog({ level: 'error', message }); + } + return; + } + + const isLevelLog = level === 'log'; + _INTERNAL_captureLog({ + level: isLevelLog ? 'info' : level, + message: formatConsoleArgs(args), + severityNumber: isLevelLog ? 10 : undefined, + }); + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Captures calls to the `console` API as logs in Sentry. Requires `_experiments.enableLogs` to be enabled. + * + * @experimental This feature is experimental and may be changed or removed in future versions. + * + * By default the integration instruments `console.debug`, `console.info`, `console.warn`, `console.error`, + * `console.log`, `console.trace`, and `console.assert`. You can use the `levels` option to customize which + * levels are captured. + * + * @example + * + * ```ts + * import * as Sentry from '@sentry/browser'; + * + * Sentry.init({ + * integrations: [Sentry.consoleLoggingIntegration({ levels: ['error', 'warn'] })], + * }); + * ``` + */ +export const consoleLoggingIntegration = defineIntegration(_consoleLoggingIntegration); + +function formatConsoleArgs(values: unknown[]): string { + return 'util' in GLOBAL_OBJ && typeof (GLOBAL_OBJ as GlobalObjectWithUtil).util.format === 'function' + ? (GLOBAL_OBJ as GlobalObjectWithUtil).util.format(...values) + : safeJoin(values, ' '); +} diff --git a/packages/core/src/logs/index.ts b/packages/core/src/logs/exports.ts similarity index 94% rename from packages/core/src/logs/index.ts rename to packages/core/src/logs/exports.ts index fbe1b40493c3..5e12f5739729 100644 --- a/packages/core/src/logs/index.ts +++ b/packages/core/src/logs/exports.ts @@ -41,7 +41,7 @@ export function logAttributeToSerializedLogAttribute(key: string, value: unknown let stringValue = ''; try { stringValue = JSON.stringify(value) ?? ''; - } catch (_) { + } catch { // Do nothing } return { @@ -62,7 +62,11 @@ export function logAttributeToSerializedLogAttribute(key: string, value: unknown * @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_captureLog(beforeLog: Log, client = getClient(), scope = getCurrentScope()): void { +export function _INTERNAL_captureLog( + beforeLog: Log, + client: Client | undefined = getClient(), + scope = getCurrentScope(), +): void { if (!client) { DEBUG_BUILD && logger.warn('No client available to capture log.'); return; @@ -143,6 +147,9 @@ export function _INTERNAL_captureLog(beforeLog: Log, client = getClient(), scope * * @param client - A client. * @param maybeLogBuffer - A log buffer. Uses the log buffer for the given client if not provided. + * + * @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_flushLogsBuffer(client: Client, maybeLogBuffer?: Array): void { const logBuffer = maybeLogBuffer ?? CLIENT_TO_LOG_BUFFER_MAP.get(client) ?? []; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 16af7b57f7c4..dfada209fcfd 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -22,7 +22,7 @@ import { eventFromMessage, eventFromUnknownInput } from './utils-hoist/eventbuil import { logger } from './utils-hoist/logger'; import { uuid4 } from './utils-hoist/misc'; import { resolvedSyncPromise } from './utils-hoist/syncpromise'; -import { _INTERNAL_flushLogsBuffer } from './logs'; +import { _INTERNAL_flushLogsBuffer } from './logs/exports'; import { isPrimitive } from './utils-hoist'; export interface ServerRuntimeClientOptions extends ClientOptions { @@ -51,23 +51,26 @@ export class ServerRuntimeClient< this._logWeight = 0; - // eslint-disable-next-line @typescript-eslint/no-this-alias - const client = this; - this.on('flush', () => { - _INTERNAL_flushLogsBuffer(client); - }); - - this.on('afterCaptureLog', log => { - client._logWeight += estimateLogSizeInBytes(log); - - // We flush the logs buffer if it exceeds 0.8 MB - // The log weight is a rough estimate, so we flush way before - // the payload gets too big. - if (client._logWeight > 800_000) { + if (this._options._experiments?.enableLogs) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const client = this; + client.on('flush', () => { _INTERNAL_flushLogsBuffer(client); client._logWeight = 0; - } - }); + }); + + client.on('afterCaptureLog', log => { + client._logWeight += estimateLogSizeInBytes(log); + + // We flush the logs buffer if it exceeds 0.8 MB + // The log weight is a rough estimate, so we flush way before + // the payload gets too big. + if (client._logWeight >= 800_000) { + _INTERNAL_flushLogsBuffer(client); + client._logWeight = 0; + } + }); + } } /** diff --git a/packages/core/src/types-hoist/log.ts b/packages/core/src/types-hoist/log.ts index 35e30ce42b65..05f8e05ad228 100644 --- a/packages/core/src/types-hoist/log.ts +++ b/packages/core/src/types-hoist/log.ts @@ -36,7 +36,7 @@ export interface Log { level: LogSeverityLevel; /** - * The message to be logged - for example, 'hello world' would become a log like '[INFO] hello world' + * The message to be logged. */ message: ParameterizedString; diff --git a/packages/core/src/utils-hoist/aggregate-errors.ts b/packages/core/src/utils-hoist/aggregate-errors.ts index 606b2d12161e..33728c03edba 100644 --- a/packages/core/src/utils-hoist/aggregate-errors.ts +++ b/packages/core/src/utils-hoist/aggregate-errors.ts @@ -1,7 +1,6 @@ import type { Event, EventHint, Exception, ExtendedError, StackParser } from '../types-hoist'; import { isInstanceOf } from './is'; -import { truncate } from './string'; /** * Creates exceptions inside `event.exception.values` for errors that are nested on properties based on the `key` parameter. @@ -9,7 +8,6 @@ import { truncate } from './string'; export function applyAggregateErrorsToEvent( exceptionFromErrorImplementation: (stackParser: StackParser, ex: Error) => Exception, parser: StackParser, - maxValueLimit: number = 250, key: string, limit: number, event: Event, @@ -25,18 +23,15 @@ export function applyAggregateErrorsToEvent( // We only create exception grouping if there is an exception in the event. if (originalException) { - event.exception.values = truncateAggregateExceptions( - aggregateExceptionsFromError( - exceptionFromErrorImplementation, - parser, - limit, - hint.originalException as ExtendedError, - key, - event.exception.values, - originalException, - 0, - ), - maxValueLimit, + event.exception.values = aggregateExceptionsFromError( + exceptionFromErrorImplementation, + parser, + limit, + hint.originalException as ExtendedError, + key, + event.exception.values, + originalException, + 0, ); } } @@ -129,17 +124,3 @@ function applyExceptionGroupFieldsForChildException( parent_id: parentId, }; } - -/** - * Truncate the message (exception.value) of all exceptions in the event. - * Because this event processor is ran after `applyClientOptions`, - * we need to truncate the message of the added exceptions here. - */ -function truncateAggregateExceptions(exceptions: Exception[], maxValueLength: number): Exception[] { - return exceptions.map(exception => { - if (exception.value) { - exception.value = truncate(exception.value, maxValueLength); - } - return exception; - }); -} diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 990ad55fcc8e..2bb15f1423dc 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -122,7 +122,14 @@ export { objectToBaggageHeader, } from './baggage'; -export { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; +export { + getSanitizedUrlString, + parseUrl, + stripUrlQueryAndFragment, + parseStringToURLObject, + isURLObjectRelative, + getSanitizedUrlStringFromUrlObject, +} from './url'; export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; export { callFrameToStackFrame, watchdogTimer } from './anr'; export { LRUMap } from './lru'; diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils-hoist/url.ts index 0b542cf14d6c..7a7893a36b68 100644 --- a/packages/core/src/utils-hoist/url.ts +++ b/packages/core/src/utils-hoist/url.ts @@ -11,13 +11,50 @@ interface URLwithCanParse extends URL { canParse: (url: string, base?: string | URL | undefined) => boolean; } +// A subset of the URL object that is valid for relative URLs +// The URL object cannot handle relative URLs, so we need to handle them separately +type RelativeURL = { + isRelative: true; + pathname: URL['pathname']; + search: URL['search']; + hash: URL['hash']; +}; + +type URLObject = RelativeURL | URL; + +// Curious about `thismessage:/`? See: https://www.rfc-editor.org/rfc/rfc2557.html +// > When the methods above do not yield an absolute URI, a base URL +// > of "thismessage:/" MUST be employed. This base URL has been +// > defined for the sole purpose of resolving relative references +// > within a multipart/related structure when no other base URI is +// > specified. +// +// We need to provide a base URL to `parseStringToURLObject` because the fetch API gives us a +// relative URL sometimes. +// +// This is the only case where we need to provide a base URL to `parseStringToURLObject` +// because the relative URL is not valid on its own. +const DEFAULT_BASE_URL = 'thismessage:/'; + +/** + * Checks if the URL object is relative + * + * @param url - The URL object to check + * @returns True if the URL object is relative, false otherwise + */ +export function isURLObjectRelative(url: URLObject): url is RelativeURL { + return 'isRelative' in url; +} + /** * Parses string to a URL object * * @param url - The URL to parse * @returns The parsed URL object or undefined if the URL is invalid */ -export function parseStringToURL(url: string, base?: string | URL | undefined): URL | undefined { +export function parseStringToURLObject(url: string, urlBase?: string | URL | undefined): URLObject | undefined { + const isRelative = url.startsWith('/'); + const base = urlBase ?? (isRelative ? DEFAULT_BASE_URL : undefined); try { // Use `canParse` to short-circuit the URL constructor if it's not a valid URL // This is faster than trying to construct the URL and catching the error @@ -26,7 +63,18 @@ export function parseStringToURL(url: string, base?: string | URL | undefined): return undefined; } - return new URL(url, base); + const fullUrlObject = new URL(url, base); + if (isRelative) { + // Because we used a fake base URL, we need to return a relative URL object. + // We cannot return anything about the origin, host, etc. because it will refer to the fake base URL. + return { + isRelative, + pathname: fullUrlObject.pathname, + search: fullUrlObject.search, + hash: fullUrlObject.hash, + }; + } + return fullUrlObject; } catch { // empty body } @@ -34,6 +82,31 @@ export function parseStringToURL(url: string, base?: string | URL | undefined): return undefined; } +/** + * Takes a URL object and returns a sanitized string which is safe to use as span name + * see: https://develop.sentry.dev/sdk/data-handling/#structuring-data + */ +export function getSanitizedUrlStringFromUrlObject(url: URLObject): string { + if (isURLObjectRelative(url)) { + return url.pathname; + } + + const newUrl = new URL(url); + newUrl.search = ''; + newUrl.hash = ''; + if (['80', '443'].includes(newUrl.port)) { + newUrl.port = ''; + } + if (newUrl.password) { + newUrl.password = '%filtered%'; + } + if (newUrl.username) { + newUrl.username = '%filtered%'; + } + + return newUrl.toString(); +} + /** * Parses string form of URL into an object * // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index e417640a387a..edb4e8d2eac4 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -144,15 +144,6 @@ export function applyClientOptions(event: Event, options: ClientOptions): void { event.dist = dist; } - if (event.message) { - event.message = truncate(event.message, maxValueLength); - } - - const exception = event.exception?.values?.[0]; - if (exception?.value) { - exception.value = truncate(exception.value, maxValueLength); - } - const request = event.request; if (request?.url) { request.url = truncate(request.url, maxValueLength); diff --git a/packages/core/test/lib/logs/index.test.ts b/packages/core/test/lib/logs/exports.test.ts similarity index 99% rename from packages/core/test/lib/logs/index.test.ts rename to packages/core/test/lib/logs/exports.test.ts index e2b7eba781d2..acc40ba0c361 100644 --- a/packages/core/test/lib/logs/index.test.ts +++ b/packages/core/test/lib/logs/exports.test.ts @@ -4,7 +4,7 @@ import { _INTERNAL_getLogBuffer, _INTERNAL_captureLog, logAttributeToSerializedLogAttribute, -} from '../../../src/logs'; +} from '../../../src/logs/exports'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; import * as loggerModule from '../../../src/utils-hoist/logger'; import { Scope, fmt } from '../../../src'; diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index bbe1e441cad4..87c62b1567d4 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it, test, vi } from 'vitest'; import { Scope, createTransport } from '../../src'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; +import { _INTERNAL_captureLog } from '../../src/logs/exports'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -206,4 +207,77 @@ describe('ServerRuntimeClient', () => { ]); }); }); + + describe('log weight-based flushing', () => { + it('flushes logs when weight exceeds 800KB', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true }, + }); + client = new ServerRuntimeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a large log message that will exceed the 800KB threshold + const largeMessage = 'x'.repeat(400_000); // 400KB string + _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, client); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(client['_logWeight']).toBe(0); // Weight should be reset after flush + }); + + it('accumulates log weight without flushing when under threshold', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true }, + }); + client = new ServerRuntimeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a log message that won't exceed the threshold + const message = 'x'.repeat(100_000); // 100KB string + _INTERNAL_captureLog({ message, level: 'info' }, client); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + expect(client['_logWeight']).toBeGreaterThan(0); + }); + + it('flushes logs on flush event', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true }, + }); + client = new ServerRuntimeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add some logs + _INTERNAL_captureLog({ message: 'test1', level: 'info' }, client); + _INTERNAL_captureLog({ message: 'test2', level: 'info' }, client); + + // Trigger flush event + client.emit('flush'); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(client['_logWeight']).toBe(0); // Weight should be reset after flush + }); + + it('does not flush logs when logs are disabled', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: false }, + }); + client = new ServerRuntimeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a large log message + const largeMessage = 'x'.repeat(400_000); + _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, client); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + expect(client['_logWeight']).toBe(0); + }); + }); }); diff --git a/packages/core/test/utils-hoist/aggregate-errors.test.ts b/packages/core/test/utils-hoist/aggregate-errors.test.ts index bc6aa2a99d19..2ec932cbd3a9 100644 --- a/packages/core/test/utils-hoist/aggregate-errors.test.ts +++ b/packages/core/test/utils-hoist/aggregate-errors.test.ts @@ -21,7 +21,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if event does not contain an exception', () => { const event: Event = { exception: undefined }; const eventHint: EventHint = { originalException: new Error() }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: undefined }); @@ -30,7 +30,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if event does not contain exception values', () => { const event: Event = { exception: { values: undefined } }; const eventHint: EventHint = { originalException: new Error() }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: { values: undefined } }); @@ -38,7 +38,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if event does not contain an event hint', () => { const event: Event = { exception: { values: [] } }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, undefined); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, undefined); // no changes expect(event).toStrictEqual({ exception: { values: [] } }); @@ -47,7 +47,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if the event hint does not contain an original exception', () => { const event: Event = { exception: { values: [] } }; const eventHint: EventHint = { originalException: undefined }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: { values: [] } }); @@ -62,7 +62,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint); expect(event).toStrictEqual({ exception: { values: [ @@ -108,7 +108,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: { values: [exceptionFromError(stackParser, originalException)] } }); @@ -127,7 +127,7 @@ describe('applyAggregateErrorsToEvent()', () => { } const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 5, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 5, event, eventHint); // 6 -> one for original exception + 5 linked expect(event.exception?.values).toHaveLength(5 + 1); @@ -153,7 +153,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError)] } }; const eventHint: EventHint = { originalException: fakeAggregateError }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); expect(event.exception?.values?.[event.exception.values.length - 1]?.mechanism?.type).toBe('instrument'); }); @@ -170,7 +170,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError1)] } }; const eventHint: EventHint = { originalException: fakeAggregateError1 }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); expect(event).toStrictEqual({ exception: { values: [ @@ -254,7 +254,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint); expect(event).toStrictEqual({ exception: { values: [ @@ -293,53 +293,4 @@ describe('applyAggregateErrorsToEvent()', () => { }, }); }); - - test('should truncate the exception values if they exceed the `maxValueLength` option', () => { - const originalException: ExtendedError = new Error('Root Error with long message'); - originalException.cause = new Error('Nested Error 1 with longer message'); - originalException.cause.cause = new Error('Nested Error 2 with longer message with longer message'); - - const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; - const eventHint: EventHint = { originalException }; - - const maxValueLength = 15; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, maxValueLength, 'cause', 10, event, eventHint); - expect(event).toStrictEqual({ - exception: { - values: [ - { - type: 'Error', - value: 'Nested Error 2 ...', - mechanism: { - exception_id: 2, - handled: true, - parent_id: 1, - source: 'cause', - type: 'chained', - }, - }, - { - type: 'Error', - value: 'Nested Error 1 ...', - mechanism: { - exception_id: 1, - handled: true, - parent_id: 0, - source: 'cause', - type: 'chained', - }, - }, - { - type: 'Error', - value: 'Root Error with...', - mechanism: { - exception_id: 0, - handled: true, - type: 'instrument', - }, - }, - ], - }, - }); - }); }); diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/utils-hoist/url.test.ts index 8cb0a945c2d5..14af8c5c2403 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/utils-hoist/url.test.ts @@ -1,5 +1,12 @@ import { describe, expect, it } from 'vitest'; -import { getSanitizedUrlString, parseStringToURL, parseUrl, stripUrlQueryAndFragment } from '../../src/utils-hoist/url'; +import { + getSanitizedUrlString, + parseUrl, + stripUrlQueryAndFragment, + parseStringToURLObject, + isURLObjectRelative, + getSanitizedUrlStringFromUrlObject, +} from '../../src/utils-hoist/url'; describe('stripQueryStringAndFragment', () => { const urlString = 'http://dogs.are.great:1231/yay/'; @@ -62,8 +69,6 @@ describe('getSanitizedUrlString', () => { 'https://[filtered]:[filtered]@somedomain.com', ], ['same-origin url', '/api/v4/users?id=123', '/api/v4/users'], - ['url without a protocol', 'example.com', 'example.com'], - ['url without a protocol with a path', 'example.com/sub/path?id=123', 'example.com/sub/path'], ['url with port 8080', 'http://172.31.12.144:8080/test', 'http://172.31.12.144:8080/test'], ['url with port 4433', 'http://172.31.12.144:4433/test', 'http://172.31.12.144:4433/test'], ['url with port 443', 'http://172.31.12.144:443/test', 'http://172.31.12.144/test'], @@ -197,19 +202,95 @@ describe('parseUrl', () => { }); }); -describe('parseStringToURL', () => { +describe('parseStringToURLObject', () => { it('returns undefined for invalid URLs', () => { - expect(parseStringToURL('invalid-url')).toBeUndefined(); + expect(parseStringToURLObject('invalid-url')).toBeUndefined(); }); it('returns a URL object for valid URLs', () => { - expect(parseStringToURL('https://somedomain.com')).toBeInstanceOf(URL); + expect(parseStringToURLObject('https://somedomain.com')).toBeInstanceOf(URL); + }); + + it('returns a URL object for valid URLs with a base URL', () => { + expect(parseStringToURLObject('https://somedomain.com', 'https://base.com')).toBeInstanceOf(URL); + }); + + it('returns a relative URL object for relative URLs', () => { + expect(parseStringToURLObject('/path/to/happiness')).toEqual({ + isRelative: true, + pathname: '/path/to/happiness', + search: '', + hash: '', + }); }); it('does not throw an error if URl.canParse is not defined', () => { const canParse = (URL as any).canParse; delete (URL as any).canParse; - expect(parseStringToURL('https://somedomain.com')).toBeInstanceOf(URL); + expect(parseStringToURLObject('https://somedomain.com')).toBeInstanceOf(URL); (URL as any).canParse = canParse; }); }); + +describe('isURLObjectRelative', () => { + it('returns true for relative URLs', () => { + expect(isURLObjectRelative(parseStringToURLObject('/path/to/happiness')!)).toBe(true); + }); + + it('returns false for absolute URLs', () => { + expect(isURLObjectRelative(parseStringToURLObject('https://somedomain.com')!)).toBe(false); + }); +}); + +describe('getSanitizedUrlStringFromUrlObject', () => { + it.each([ + ['regular url', 'https://somedomain.com', 'https://somedomain.com/'], + ['regular url with a path', 'https://somedomain.com/path/to/happiness', 'https://somedomain.com/path/to/happiness'], + [ + 'url with standard http port 80', + 'http://somedomain.com:80/path/to/happiness', + 'http://somedomain.com/path/to/happiness', + ], + [ + 'url with standard https port 443', + 'https://somedomain.com:443/path/to/happiness', + 'https://somedomain.com/path/to/happiness', + ], + [ + 'url with non-standard port', + 'https://somedomain.com:4200/path/to/happiness', + 'https://somedomain.com:4200/path/to/happiness', + ], + [ + 'url with query params', + 'https://somedomain.com:4200/path/to/happiness?auhtToken=abc123¶m2=bar', + 'https://somedomain.com:4200/path/to/happiness', + ], + [ + 'url with a fragment', + 'https://somedomain.com/path/to/happiness#somewildfragment123', + 'https://somedomain.com/path/to/happiness', + ], + [ + 'url with a fragment and query params', + 'https://somedomain.com/path/to/happiness#somewildfragment123?auhtToken=abc123¶m2=bar', + 'https://somedomain.com/path/to/happiness', + ], + [ + 'url with authorization', + 'https://username:password@somedomain.com', + 'https://%filtered%:%filtered%@somedomain.com/', + ], + ['same-origin url', '/api/v4/users?id=123', '/api/v4/users'], + ['url with port 8080', 'http://172.31.12.144:8080/test', 'http://172.31.12.144:8080/test'], + ['url with port 4433', 'http://172.31.12.144:4433/test', 'http://172.31.12.144:4433/test'], + ['url with port 443', 'http://172.31.12.144:443/test', 'http://172.31.12.144/test'], + ['url with IP and port 80', 'http://172.31.12.144:80/test', 'http://172.31.12.144/test'], + ])('returns a sanitized URL for a %s', (_, rawUrl: string, sanitizedURL: string) => { + const urlObject = parseStringToURLObject(rawUrl); + if (!urlObject) { + throw new Error('Invalid URL'); + } + expect(getSanitizedUrlStringFromUrlObject(urlObject)).toEqual(sanitizedURL); + }); +}); diff --git a/packages/deno/package.json b/packages/deno/package.json index a1355995ec19..0fefc166fa02 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -39,6 +39,7 @@ "fix": "eslint . --format stylish --fix", "prelint": "yarn deno-types", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/esm/*.js --module", "install:deno": "node ./scripts/install-deno.mjs", "test": "run-s install:deno deno-types test:unit", "test:unit": "deno test --allow-read --allow-run --no-check", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 5324f4cef94e..c16a1e297e25 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -62,6 +62,7 @@ "clean": "rimraf build sentry-internal-feedback-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/{bundles,npm/cjs}/*.js && es-check es2020 ./build/npm/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 846a1187af17..50c3c6c3e297 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -75,6 +75,7 @@ "clean": "rimraf build coverage *.d.ts sentry-gatsby-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index c58070a24bc1..699a80118cdc 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -74,6 +74,7 @@ "clean": "rimraf build coverage sentry-google-cloud-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 83a18f62c6df..54ae30fb5c8c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -114,6 +114,7 @@ export { childProcessIntegration, vercelAIIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index c79016eb2ae1..5cf41c378584 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -43,7 +43,8 @@ "build:types:watch": "tsc -p tsconfig.types.json --watch", "clean": "rimraf build", "fix": "eslint . --format stylish --fix", - "lint": "eslint . --format stylish" + "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module" }, "repository": { "type": "git", diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 3e67a79c1546..869be6835122 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -77,6 +77,7 @@ "clean": "rimraf build coverage sentry-nestjs-*.tgz ./*.d.ts ./*.d.ts.map", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index f2acca4d7be8..08b715097105 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -115,6 +115,7 @@ "clean": "rimraf build coverage sentry-nextjs-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:all": "run-s test:unit", "test:unit": "vitest run", diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 9b1d58610e0d..37589467f5d6 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,4 +1,5 @@ import type { Client, EventProcessor, Integration } from '@sentry/core'; +import { getGlobalScope } from '@sentry/core'; import { GLOBAL_OBJ, addEventProcessor, applySdkMetadata } from '@sentry/core'; import type { BrowserOptions } from '@sentry/react'; import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react'; @@ -19,6 +20,7 @@ const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesAssetPrefixPath: string; _sentryAssetPrefix?: string; _sentryBasePath?: string; + _sentryRelease?: string; _experimentalThirdPartyOriginStackFrames?: string; }; @@ -30,6 +32,7 @@ export function init(options: BrowserOptions): Client | undefined { const opts = { environment: getVercelEnv(true) || process.env.NODE_ENV, defaultIntegrations: getDefaultIntegrations(options), + release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, ...options, } satisfies BrowserOptions; @@ -61,6 +64,16 @@ export function init(options: BrowserOptions): Client | undefined { addEventProcessor(devErrorSymbolicationEventProcessor); } + try { + // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js + if (process.turbopack) { + getGlobalScope().setTag('turbopack', true); + } + } catch (e) { + // Noop + // The statement above can throw because process is not defined on the client + } + return client; } diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 7250af15ff8e..4b24d8370393 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -50,6 +50,8 @@ function getFinalConfigObject( incomingUserNextConfigObject: NextConfigObject, userSentryOptions: SentryBuildOptions, ): NextConfigObject { + const releaseName = userSentryOptions.release?.name ?? getSentryRelease() ?? getGitRevision(); + if (userSentryOptions?.tunnelRoute) { if (incomingUserNextConfigObject.output === 'export') { if (!showedExportModeTunnelWarning) { @@ -64,7 +66,7 @@ function getFinalConfigObject( } } - setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions); + setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions, releaseName); const nextJsVersion = getNextjsVersion(); @@ -207,8 +209,6 @@ function getFinalConfigObject( ); } - const releaseName = userSentryOptions.release?.name ?? getSentryRelease() ?? getGitRevision(); - return { ...incomingUserNextConfigObject, webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName), @@ -291,8 +291,11 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s }; } -// TODO: For Turbopack we need to pass the release name here and pick it up in the SDK -function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOptions: SentryBuildOptions): void { +function setUpBuildTimeVariables( + userNextConfig: NextConfigObject, + userSentryOptions: SentryBuildOptions, + releaseName: string | undefined, +): void { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const basePath = userNextConfig.basePath ?? ''; const rewritesTunnelPath = @@ -335,6 +338,10 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true'; } + if (releaseName) { + buildTimeVariables._sentryRelease = releaseName; + } + if (typeof userNextConfig.env === 'object') { userNextConfig.env = { ...buildTimeVariables, ...userNextConfig.env }; } else if (userNextConfig.env === undefined) { diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 77469cfdf9dc..82eb4f597ba6 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata, + getGlobalScope, getRootSpan, registerSpanErrorInstrumentation, spanToJSON, @@ -25,6 +26,7 @@ export type EdgeOptions = VercelEdgeOptions; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir?: string; + _sentryRelease?: string; }; /** Inits the Sentry NextJS SDK on the Edge Runtime. */ @@ -47,6 +49,7 @@ export function init(options: VercelEdgeOptions = {}): void { const opts = { defaultIntegrations: customDefaultIntegrations, + release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, ...options, }; @@ -90,6 +93,16 @@ export function init(options: VercelEdgeOptions = {}): void { vercelWaitUntil(flushSafelyWithTimeout()); } }); + + try { + // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js + if (process.turbopack) { + getGlobalScope().setTag('turbopack', true); + } + } catch { + // Noop + // The statement above can throw because process is not defined on the client + } } /** diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index bc5cbb50893d..da18f0fa459c 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -43,6 +43,7 @@ export { captureUnderscoreErrorException } from '../common/pages-router-instrume const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir?: string; _sentryRewritesTunnelPath?: string; + _sentryRelease?: string; }; /** @@ -115,6 +116,7 @@ export function init(options: NodeOptions): NodeClient | undefined { const opts: NodeOptions = { environment: process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV, + release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, defaultIntegrations: customDefaultIntegrations, ...options, }; @@ -357,6 +359,16 @@ export function init(options: NodeOptions): NodeClient | undefined { getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); } + try { + // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js + if (process.turbopack) { + getGlobalScope().setTag('turbopack', true); + } + } catch { + // Noop + // The statement above can throw because process is not defined on the client + } + DEBUG_BUILD && logger.log('SDK successfully initialized'); return client; diff --git a/packages/node/package.json b/packages/node/package.json index 866a2810db0e..c0719b7300e4 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -119,6 +119,7 @@ "clean": "rimraf build coverage sentry-node-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index decfbd578c68..31e383040f70 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -130,6 +130,7 @@ export { updateSpanName, zodErrorsIntegration, profiler, + consoleLoggingIntegration, } from '@sentry/core'; export type { diff --git a/packages/node/src/utils/commonjs.ts b/packages/node/src/utils/commonjs.ts index 9b5e5b531d73..23a9b97f9fc1 100644 --- a/packages/node/src/utils/commonjs.ts +++ b/packages/node/src/utils/commonjs.ts @@ -1,4 +1,8 @@ /** Detect CommonJS. */ export function isCjs(): boolean { - return typeof require !== 'undefined'; + try { + return typeof module !== 'undefined' && typeof module.exports !== 'undefined'; + } catch { + return false; + } } diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 8d3f5918573b..93ee5106d82f 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -70,6 +70,7 @@ "clean": "rimraf build coverage sentry-nuxt-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module && es-check es2020 ./build/module/*.cjs && es-check es2020 ./build/module/*.mjs --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 1372db47889e..176690734339 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -3,7 +3,6 @@ import { consoleSandbox } from '@sentry/core'; import { type SentryRollupPluginOptions, sentryRollupPlugin } from '@sentry/rollup-plugin'; import { type SentryVitePluginOptions, sentryVitePlugin } from '@sentry/vite-plugin'; import type { NitroConfig } from 'nitropack'; -import type { OutputOptions } from 'rollup'; import type { SentryNuxtModuleOptions } from '../common/types'; /** @@ -11,28 +10,84 @@ import type { SentryNuxtModuleOptions } from '../common/types'; */ export type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined; +/** A valid source map setting */ +export type SourceMapSetting = boolean | 'hidden' | 'inline'; + /** * Setup source maps for Sentry inside the Nuxt module during build time (in Vite for Nuxt and Rollup for Nitro). */ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nuxt): void { + const isDebug = moduleOptions.debug; + const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; const sourceMapsEnabled = sourceMapsUploadOptions.enabled ?? true; + // In case we overwrite the source map settings, we default to deleting the files + let shouldDeleteFilesFallback = { client: true, server: true }; + nuxt.hook('modules:done', () => { if (sourceMapsEnabled && !nuxt.options.dev) { - changeNuxtSourceMapSettings(nuxt, moduleOptions); + // Changing this setting will propagate: + // - for client to viteConfig.build.sourceMap + // - for server to viteConfig.build.sourceMap and nitro.sourceMap + // On server, nitro.rollupConfig.output.sourcemap remains unaffected from this change. + + // ONLY THIS nuxt.sourcemap.(server/client) setting is the one Sentry will eventually overwrite with 'hidden' + const previousSourceMapSettings = changeNuxtSourceMapSettings(nuxt, moduleOptions); + + shouldDeleteFilesFallback = { + client: previousSourceMapSettings.client === 'unset', + server: previousSourceMapSettings.server === 'unset', + }; + + if ( + isDebug && + !sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload && + (shouldDeleteFilesFallback.client || shouldDeleteFilesFallback.server) + ) { + consoleSandbox(() => + // eslint-disable-next-line no-console + console.log( + "[Sentry] As Sentry enabled `'hidden'` source maps, source maps will be automatically deleted after uploading them to Sentry.", + ), + ); + } } }); - nuxt.hook('vite:extendConfig', async (viteConfig, _env) => { + nuxt.hook('vite:extendConfig', async (viteConfig, env) => { if (sourceMapsEnabled && viteConfig.mode !== 'development') { - const previousUserSourceMapSetting = changeViteSourceMapSettings(viteConfig, moduleOptions); + const runtime = env.isServer ? 'server' : env.isClient ? 'client' : undefined; + const nuxtSourceMapSetting = extractNuxtSourceMapSetting(nuxt, runtime); + + viteConfig.build = viteConfig.build || {}; + const viteSourceMap = viteConfig.build.sourcemap; + + // Vite source map options are the same as the Nuxt source map config options (unless overwritten) + validateDifferentSourceMapSettings({ + nuxtSettingKey: `sourcemap.${runtime}`, + nuxtSettingValue: nuxtSourceMapSetting, + otherSettingKey: 'viteConfig.build.sourcemap', + otherSettingValue: viteSourceMap, + }); + + if (isDebug) { + consoleSandbox(() => { + if (!runtime) { + // eslint-disable-next-line no-console + console.log("[Sentry] Cannot detect runtime (client/server) inside hook 'vite:extendConfig'."); + } else { + // eslint-disable-next-line no-console + console.log(`[Sentry] Adding Sentry Vite plugin to the ${runtime} runtime.`); + } + }); + } - // Add Sentry plugin + // Add Sentry plugin + // Vite plugin is added on the client and server side (hook runs twice) + // Nuxt client source map is 'false' by default. Warning about this will be shown already in an earlier step, and it's also documented that `nuxt.sourcemap.client` needs to be enabled. viteConfig.plugins = viteConfig.plugins || []; - viteConfig.plugins.push( - sentryVitePlugin(getPluginOptions(moduleOptions, previousUserSourceMapSetting === 'unset')), - ); + viteConfig.plugins.push(sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))); } }); @@ -49,11 +104,19 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu nitroConfig.rollupConfig.plugins = [nitroConfig.rollupConfig.plugins]; } - const previousUserSourceMapSetting = changeRollupSourceMapSettings(nitroConfig, moduleOptions); + validateNitroSourceMapSettings(nuxt, nitroConfig, moduleOptions); + + if (isDebug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log('[Sentry] Adding Sentry Rollup plugin to the server runtime.'); + }); + } // Add Sentry plugin + // Runs only on server-side (Nitro) nitroConfig.rollupConfig.plugins.push( - sentryRollupPlugin(getPluginOptions(moduleOptions, previousUserSourceMapSetting === 'unset')), + sentryRollupPlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)), ); } }); @@ -73,15 +136,29 @@ function normalizePath(path: string): string { */ export function getPluginOptions( moduleOptions: SentryNuxtModuleOptions, - deleteFilesAfterUpload: boolean, + shouldDeleteFilesFallback?: { client: boolean; server: boolean }, ): SentryVitePluginOptions | SentryRollupPluginOptions { const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; - if (typeof sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload === 'undefined' && deleteFilesAfterUpload) { + const shouldDeleteFilesAfterUpload = shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server; + const fallbackFilesToDelete = [ + ...(shouldDeleteFilesFallback?.client ? ['.*/**/public/**/*.map'] : []), + ...(shouldDeleteFilesFallback?.server + ? ['.*/**/server/**/*.map', '.*/**/output/**/*.map', '.*/**/function/**/*.map'] + : []), + ]; + + if ( + typeof sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload === 'undefined' && + shouldDeleteFilesAfterUpload + ) { consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - '[Sentry] Setting `sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [".*/**/public/**/*.map"]` to delete generated source maps after they were uploaded to Sentry.', + `[Sentry] Setting \`sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [${fallbackFilesToDelete + // Logging it as strings in the array + .map(path => `"${path}"`) + .join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`, ); }); } @@ -114,8 +191,8 @@ export function getPluginOptions( ignore: sourceMapsUploadOptions.sourcemaps?.ignore ?? undefined, filesToDeleteAfterUpload: sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload ? sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload - : deleteFilesAfterUpload - ? ['.*/**/public/**/*.map'] + : shouldDeleteFilesFallback?.server || shouldDeleteFilesFallback?.client + ? fallbackFilesToDelete : undefined, rewriteSources: (source: string) => normalizePath(source), ...moduleOptions?.unstable_sentryBundlerPluginOptions?.sourcemaps, @@ -123,7 +200,7 @@ export function getPluginOptions( }; } -/* There are 3 ways to set up source maps (https://github.com/getsentry/sentry-javascript/issues/13993) +/* There are multiple ways to set up source maps (https://github.com/getsentry/sentry-javascript/issues/13993 and https://github.com/getsentry/sentry-javascript/pull/15859) 1. User explicitly disabled source maps - keep this setting (emit a warning that errors won't be unminified in Sentry) - We will not upload anything @@ -133,11 +210,24 @@ export function getPluginOptions( - we enable 'hidden' source maps generation - configure `filesToDeleteAfterUpload` to delete all .map files (we emit a log about this) - Nuxt has 3 places to set source maps: vite options, rollup options, nuxt itself - Ideally, all 3 are enabled to get all source maps. + Users only have to explicitly enable client source maps. Sentry only overwrites the base Nuxt source map settings as they propagate. */ -/** only exported for testing */ +/** only exported for tests */ +export function extractNuxtSourceMapSetting( + nuxt: { options: { sourcemap?: SourceMapSetting | { server?: SourceMapSetting; client?: SourceMapSetting } } }, + runtime: 'client' | 'server' | undefined, +): SourceMapSetting | undefined { + if (!runtime) { + return undefined; + } else { + return typeof nuxt.options?.sourcemap === 'boolean' || typeof nuxt.options?.sourcemap === 'string' + ? nuxt.options.sourcemap + : nuxt.options?.sourcemap?.[runtime]; + } +} + +/** only exported for testing */ export function changeNuxtSourceMapSettings( nuxt: Nuxt, sentryModuleOptions: SentryNuxtModuleOptions, @@ -160,7 +250,7 @@ export function changeNuxtSourceMapSettings( case 'hidden': case true: - logKeepSourceMapSetting(sentryModuleOptions, 'sourcemap', (nuxtSourceMap as true).toString()); + logKeepEnabledSourceMapSetting(sentryModuleOptions, 'sourcemap', (nuxtSourceMap as true).toString()); previousUserSourceMapSetting = { client: 'enabled', server: 'enabled' }; break; case undefined: @@ -175,7 +265,7 @@ export function changeNuxtSourceMapSettings( warnExplicitlyDisabledSourceMap('sourcemap.client'); previousUserSourceMapSetting.client = 'disabled'; } else if (['hidden', true].includes(nuxtSourceMap.client)) { - logKeepSourceMapSetting(sentryModuleOptions, 'sourcemap.client', nuxtSourceMap.client.toString()); + logKeepEnabledSourceMapSetting(sentryModuleOptions, 'sourcemap.client', nuxtSourceMap.client.toString()); previousUserSourceMapSetting.client = 'enabled'; } else { nuxt.options.sourcemap.client = 'hidden'; @@ -187,7 +277,7 @@ export function changeNuxtSourceMapSettings( warnExplicitlyDisabledSourceMap('sourcemap.server'); previousUserSourceMapSetting.server = 'disabled'; } else if (['hidden', true].includes(nuxtSourceMap.server)) { - logKeepSourceMapSetting(sentryModuleOptions, 'sourcemap.server', nuxtSourceMap.server.toString()); + logKeepEnabledSourceMapSetting(sentryModuleOptions, 'sourcemap.server', nuxtSourceMap.server.toString()); previousUserSourceMapSetting.server = 'enabled'; } else { nuxt.options.sourcemap.server = 'hidden'; @@ -199,74 +289,79 @@ export function changeNuxtSourceMapSettings( return previousUserSourceMapSetting; } -/** only exported for testing */ -export function changeViteSourceMapSettings( - viteConfig: { build?: { sourcemap?: boolean | 'inline' | 'hidden' } }, +/** Logs warnings about potentially conflicting source map settings. + * Configures `sourcemapExcludeSources` in Nitro to make source maps usable in Sentry. + * + * only exported for testing + */ +export function validateNitroSourceMapSettings( + nuxt: { options: { sourcemap?: SourceMapSetting | { server?: SourceMapSetting } } }, + nitroConfig: NitroConfig, sentryModuleOptions: SentryNuxtModuleOptions, -): UserSourceMapSetting { - viteConfig.build = viteConfig.build || {}; - const viteSourceMap = viteConfig.build.sourcemap; - - let previousUserSourceMapSetting: UserSourceMapSetting; - - if (viteSourceMap === false) { - warnExplicitlyDisabledSourceMap('vite.build.sourcemap'); - previousUserSourceMapSetting = 'disabled'; - } else if (viteSourceMap && ['hidden', 'inline', true].includes(viteSourceMap)) { - logKeepSourceMapSetting(sentryModuleOptions, 'vite.build.sourcemap', viteSourceMap.toString()); - previousUserSourceMapSetting = 'enabled'; - } else { - viteConfig.build.sourcemap = 'hidden'; - logSentryEnablesSourceMap('vite.build.sourcemap', 'hidden'); - previousUserSourceMapSetting = 'unset'; - } +): void { + const isDebug = sentryModuleOptions.debug; + const nuxtSourceMap = extractNuxtSourceMapSetting(nuxt, 'server'); - return previousUserSourceMapSetting; -} + // NITRO CONFIG --- + + validateDifferentSourceMapSettings({ + nuxtSettingKey: 'sourcemap.server', + nuxtSettingValue: nuxtSourceMap, + otherSettingKey: 'nitro.sourceMap', + otherSettingValue: nitroConfig.sourceMap, + }); + + // ROLLUP CONFIG --- -/** only exported for testing */ -export function changeRollupSourceMapSettings( - nitroConfig: { - rollupConfig?: { - output?: { - sourcemap?: OutputOptions['sourcemap']; - sourcemapExcludeSources?: OutputOptions['sourcemapExcludeSources']; - }; - }; - }, - sentryModuleOptions: SentryNuxtModuleOptions, -): UserSourceMapSetting { nitroConfig.rollupConfig = nitroConfig.rollupConfig || {}; nitroConfig.rollupConfig.output = nitroConfig.rollupConfig.output || { sourcemap: undefined }; + const nitroRollupSourceMap = nitroConfig.rollupConfig.output.sourcemap; - let previousUserSourceMapSetting: UserSourceMapSetting; - - const nitroSourceMap = nitroConfig.rollupConfig.output.sourcemap; + // We don't override nitro.rollupConfig.output.sourcemap (undefined by default, but overrides all other server-side source map settings) + if (typeof nitroRollupSourceMap !== 'undefined' && ['hidden', 'inline', true, false].includes(nitroRollupSourceMap)) { + const settingKey = 'nitro.rollupConfig.output.sourcemap'; - if (nitroSourceMap === false) { - warnExplicitlyDisabledSourceMap('nitro.rollupConfig.output.sourcemap'); - previousUserSourceMapSetting = 'disabled'; - } else if (nitroSourceMap && ['hidden', 'inline', true].includes(nitroSourceMap)) { - logKeepSourceMapSetting(sentryModuleOptions, 'nitro.rollupConfig.output.sourcemap', nitroSourceMap.toString()); - previousUserSourceMapSetting = 'enabled'; - } else { - nitroConfig.rollupConfig.output.sourcemap = 'hidden'; - logSentryEnablesSourceMap('nitro.rollupConfig.output.sourcemap', 'hidden'); - previousUserSourceMapSetting = 'unset'; + validateDifferentSourceMapSettings({ + nuxtSettingKey: 'sourcemap.server', + nuxtSettingValue: nuxtSourceMap, + otherSettingKey: settingKey, + otherSettingValue: nitroRollupSourceMap, + }); } nitroConfig.rollupConfig.output.sourcemapExcludeSources = false; - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.log( - '[Sentry] Disabled source map setting in the Nuxt config: `nitro.rollupConfig.output.sourcemapExcludeSources`. Source maps will include the actual code to be able to un-minify code snippets in Sentry.', - ); - }); + if (isDebug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + '[Sentry] Set `sourcemapExcludeSources: false` in the Nuxt config (`nitro.rollupConfig.output`). Source maps will now include the actual code to be able to un-minify code snippets in Sentry.', + ); + }); + } +} - return previousUserSourceMapSetting; +function validateDifferentSourceMapSettings({ + nuxtSettingKey, + nuxtSettingValue, + otherSettingKey, + otherSettingValue, +}: { + nuxtSettingKey: string; + nuxtSettingValue?: SourceMapSetting; + otherSettingKey: string; + otherSettingValue?: SourceMapSetting; +}): void { + if (nuxtSettingValue !== otherSettingValue) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] Source map generation settings are conflicting. Sentry uses \`${nuxtSettingKey}: ${nuxtSettingValue}\`. However, a conflicting setting was discovered (\`${otherSettingKey}: ${otherSettingValue}\`). This setting was probably explicitly set in your configuration. Sentry won't override this setting but it may affect source maps generation and upload. Without source maps, code snippets on the Sentry Issues page will remain minified.`, + ); + }); + } } -function logKeepSourceMapSetting( +function logKeepEnabledSourceMapSetting( sentryNuxtModuleOptions: SentryNuxtModuleOptions, settingKey: string, settingValue: string, @@ -275,7 +370,7 @@ function logKeepSourceMapSetting( consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - `[Sentry] We discovered \`${settingKey}\` is set to \`${settingValue}\`. Sentry will keep this source map setting. This will un-minify the code snippet on the Sentry Issue page.`, + `[Sentry] \`${settingKey}\` is enabled with \`${settingValue}\`. This will correctly un-minify the code snippet on the Sentry Issue Details page.`, ); }); } @@ -283,16 +378,16 @@ function logKeepSourceMapSetting( function warnExplicitlyDisabledSourceMap(settingKey: string): void { consoleSandbox(() => { - // eslint-disable-next-line no-console + // eslint-disable-next-line no-console console.warn( - `[Sentry] Parts of source map generation are currently disabled in your Nuxt configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`, + `[Sentry] We discovered \`${settingKey}\` is set to \`false\`. This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`'hidden'\`).`, ); }); } function logSentryEnablesSourceMap(settingKey: string, settingValue: string): void { consoleSandbox(() => { - // eslint-disable-next-line no-console + // eslint-disable-next-line no-console console.log(`[Sentry] Enabled source map generation in the build options with \`${settingKey}: ${settingValue}\`.`); }); } diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts index b33d314f5166..ab8fdcb3e385 100644 --- a/packages/nuxt/test/vite/sourceMaps.test.ts +++ b/packages/nuxt/test/vite/sourceMaps.test.ts @@ -1,14 +1,17 @@ import type { Nuxt } from '@nuxt/schema'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import type { SentryNuxtModuleOptions } from '../../src/common/types'; -import type { UserSourceMapSetting } from '../../src/vite/sourceMaps'; +import type { SourceMapSetting } from '../../src/vite/sourceMaps'; import { changeNuxtSourceMapSettings, - changeRollupSourceMapSettings, - changeViteSourceMapSettings, + validateNitroSourceMapSettings, getPluginOptions, } from '../../src/vite/sourceMaps'; +vi.mock('@sentry/core', () => ({ + consoleSandbox: (callback: () => void) => callback(), +})); + describe('getPluginOptions', () => { beforeEach(() => { vi.resetModules(); @@ -25,7 +28,7 @@ describe('getPluginOptions', () => { process.env = { ...defaultEnv }; - const options = getPluginOptions({} as SentryNuxtModuleOptions, false); + const options = getPluginOptions({} as SentryNuxtModuleOptions); expect(options).toEqual( expect.objectContaining({ @@ -48,7 +51,7 @@ describe('getPluginOptions', () => { }); it('returns default options when no moduleOptions are provided', () => { - const options = getPluginOptions({} as SentryNuxtModuleOptions, false); + const options = getPluginOptions({} as SentryNuxtModuleOptions); expect(options.org).toBeUndefined(); expect(options.project).toBeUndefined(); @@ -84,7 +87,7 @@ describe('getPluginOptions', () => { }, debug: true, }; - const options = getPluginOptions(customOptions, true); + const options = getPluginOptions(customOptions, { client: true, server: false }); expect(options).toEqual( expect.objectContaining({ org: 'custom-org', @@ -130,7 +133,7 @@ describe('getPluginOptions', () => { url: 'https://suntry.io', }, }; - const options = getPluginOptions(customOptions, false); + const options = getPluginOptions(customOptions); expect(options).toEqual( expect.objectContaining({ debug: true, @@ -148,146 +151,187 @@ describe('getPluginOptions', () => { }), ); }); + + it('sets filesToDeleteAfterUpload correctly based on fallback options', () => { + // Scenario 1: Both client and server fallback are true + const optionsBothTrue = getPluginOptions({}, { client: true, server: true }); + expect(optionsBothTrue?.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '.*/**/public/**/*.map', + '.*/**/server/**/*.map', + '.*/**/output/**/*.map', + '.*/**/function/**/*.map', + ]); + + // Scenario 2: Only client fallback is true + const optionsClientTrue = getPluginOptions({}, { client: true, server: false }); + expect(optionsClientTrue?.sourcemaps?.filesToDeleteAfterUpload).toEqual(['.*/**/public/**/*.map']); + + // Scenario 3: Only server fallback is true + const optionsServerTrue = getPluginOptions({}, { client: false, server: true }); + expect(optionsServerTrue?.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '.*/**/server/**/*.map', + '.*/**/output/**/*.map', + '.*/**/function/**/*.map', + ]); + + // Scenario 4: No fallback, but custom filesToDeleteAfterUpload is provided + const customDeleteFiles = ['custom/path/**/*.map']; + const optionsWithCustomDelete = getPluginOptions( + { + sourceMapsUploadOptions: { + sourcemaps: { + filesToDeleteAfterUpload: customDeleteFiles, + }, + }, + }, + { client: false, server: false }, + ); + expect(optionsWithCustomDelete?.sourcemaps?.filesToDeleteAfterUpload).toEqual(customDeleteFiles); + + // Scenario 5: No fallback, both source maps explicitly false and no custom filesToDeleteAfterUpload + const optionsNoDelete = getPluginOptions({}, { client: false, server: false }); + expect(optionsNoDelete?.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); }); -describe('change sourcemap settings', () => { - describe('changeViteSourcemapSettings', () => { - let viteConfig: { build?: { sourcemap?: boolean | 'inline' | 'hidden' } }; - let sentryModuleOptions: SentryNuxtModuleOptions; +describe('validate sourcemap settings', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - beforeEach(() => { - viteConfig = {}; - sentryModuleOptions = {}; - }); + beforeEach(() => { + consoleLogSpy.mockClear(); + consoleWarnSpy.mockClear(); + }); - it('should handle viteConfig.build.sourcemap settings', () => { - const cases: { - sourcemap?: boolean | 'hidden' | 'inline'; - expectedSourcemap: boolean | string; - expectedReturn: UserSourceMapSetting; - }[] = [ - { sourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, - { sourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, - { sourcemap: 'inline', expectedSourcemap: 'inline', expectedReturn: 'enabled' }, - { sourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, - { sourcemap: undefined, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, - ]; + afterEach(() => { + vi.clearAllMocks(); + }); - cases.forEach(({ sourcemap, expectedSourcemap, expectedReturn }) => { - viteConfig.build = { sourcemap }; - const previousUserSourcemapSetting = changeViteSourceMapSettings(viteConfig, sentryModuleOptions); - expect(viteConfig.build.sourcemap).toBe(expectedSourcemap); - expect(previousUserSourcemapSetting).toBe(expectedReturn); - }); + describe('should handle nitroConfig.rollupConfig.output.sourcemap settings', () => { + afterEach(() => { + vi.clearAllMocks(); }); - }); - describe('changeRollupSourcemapSettings', () => { - let nitroConfig: { - rollupConfig?: { output?: { sourcemap?: boolean | 'hidden' | 'inline'; sourcemapExcludeSources?: boolean } }; + type MinimalNitroConfig = { + sourceMap?: SourceMapSetting; + rollupConfig?: { + output?: { sourcemap?: SourceMapSetting; sourcemapExcludeSources?: boolean }; + }; + }; + type MinimalNuxtConfig = { + options: { sourcemap?: SourceMapSetting | { server?: SourceMapSetting; client?: SourceMapSetting } }; }; - let sentryModuleOptions: SentryNuxtModuleOptions; - beforeEach(() => { - nitroConfig = {}; - sentryModuleOptions = {}; + const getNitroConfig = ( + nitroSourceMap?: SourceMapSetting, + rollupSourceMap?: SourceMapSetting, + ): MinimalNitroConfig => ({ + sourceMap: nitroSourceMap, + rollupConfig: { output: { sourcemap: rollupSourceMap } }, }); - it('should handle nitroConfig.rollupConfig.output.sourcemap settings', () => { - const cases: { - output?: { sourcemap?: boolean | 'hidden' | 'inline' }; - expectedSourcemap: boolean | string; - expectedReturn: UserSourceMapSetting; - }[] = [ - { output: { sourcemap: false }, expectedSourcemap: false, expectedReturn: 'disabled' }, - { output: { sourcemap: 'hidden' }, expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, - { output: { sourcemap: 'inline' }, expectedSourcemap: 'inline', expectedReturn: 'enabled' }, - { output: { sourcemap: true }, expectedSourcemap: true, expectedReturn: 'enabled' }, - { output: { sourcemap: undefined }, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, - { output: undefined, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, - ]; + const getNuxtConfig = (nuxtSourceMap?: SourceMapSetting): MinimalNuxtConfig => ({ + options: { sourcemap: { server: nuxtSourceMap } }, + }); - cases.forEach(({ output, expectedSourcemap, expectedReturn }) => { - nitroConfig.rollupConfig = { output }; - const previousUserSourceMapSetting = changeRollupSourceMapSettings(nitroConfig, sentryModuleOptions); - expect(nitroConfig.rollupConfig?.output?.sourcemap).toBe(expectedSourcemap); - expect(previousUserSourceMapSetting).toBe(expectedReturn); - expect(nitroConfig.rollupConfig?.output?.sourcemapExcludeSources).toBe(false); - }); + it('should log a warning when Nuxt and Nitro source map settings differ', () => { + const nuxt = getNuxtConfig(true); + const nitroConfig = getNitroConfig(false); + + validateNitroSourceMapSettings(nuxt, nitroConfig, { debug: true }); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[Sentry] Source map generation settings are conflicting. Sentry uses `sourcemap.server: true`. However, a conflicting setting was discovered (`nitro.sourceMap: false`). This setting was probably explicitly set in your configuration. Sentry won't override this setting but it may affect source maps generation and upload. Without source maps, code snippets on the Sentry Issues page will remain minified.", + ); + }); + + it('should set sourcemapExcludeSources to false', () => { + const nitroConfig = getNitroConfig(true); + validateNitroSourceMapSettings(getNuxtConfig(true), nitroConfig, { debug: true }); + + expect(nitroConfig?.rollupConfig?.output?.sourcemapExcludeSources).toBe(false); }); + + it('should not show console.warn when rollup sourcemap is undefined', () => { + const nitroConfig = getNitroConfig(true); + + validateNitroSourceMapSettings(getNuxtConfig(true), nitroConfig, { debug: true }); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + }); +}); + +describe('change Nuxt source map settings', () => { + let nuxt: { options: { sourcemap: { client: boolean | 'hidden'; server: boolean | 'hidden' } } }; + let sentryModuleOptions: SentryNuxtModuleOptions; + + beforeEach(() => { + // @ts-expect-error - Nuxt types don't accept `undefined` but we want to test this case + nuxt = { options: { sourcemap: { client: undefined } } }; + sentryModuleOptions = {}; }); - describe('changeNuxtSourcemapSettings', () => { - let nuxt: { options: { sourcemap: { client: boolean | 'hidden'; server: boolean | 'hidden' } } }; - let sentryModuleOptions: SentryNuxtModuleOptions; + it('should handle nuxt.options.sourcemap.client settings', () => { + const cases = [ + { clientSourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, + { clientSourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, + { clientSourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, + { clientSourcemap: undefined, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, + ]; - beforeEach(() => { + cases.forEach(({ clientSourcemap, expectedSourcemap, expectedReturn }) => { // @ts-expect-error - Nuxt types don't accept `undefined` but we want to test this case - nuxt = { options: { sourcemap: { client: undefined } } }; - sentryModuleOptions = {}; + nuxt.options.sourcemap.client = clientSourcemap; + const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); + expect(nuxt.options.sourcemap.client).toBe(expectedSourcemap); + expect(previousUserSourcemapSetting.client).toBe(expectedReturn); }); + }); - it('should handle nuxt.options.sourcemap.client settings', () => { - const cases = [ - // { clientSourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, - // { clientSourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, - { clientSourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, - { clientSourcemap: undefined, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, - ]; + it('should handle nuxt.options.sourcemap.server settings', () => { + const cases = [ + { serverSourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, + { serverSourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, + { serverSourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, + { serverSourcemap: undefined, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, + ]; - cases.forEach(({ clientSourcemap, expectedSourcemap, expectedReturn }) => { - // @ts-expect-error - Nuxt types don't accept `undefined` but we want to test this case - nuxt.options.sourcemap.client = clientSourcemap; - const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); - expect(nuxt.options.sourcemap.client).toBe(expectedSourcemap); - expect(previousUserSourcemapSetting.client).toBe(expectedReturn); - }); + cases.forEach(({ serverSourcemap, expectedSourcemap, expectedReturn }) => { + // @ts-expect-error server available + nuxt.options.sourcemap.server = serverSourcemap; + const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); + expect(nuxt.options.sourcemap.server).toBe(expectedSourcemap); + expect(previousUserSourcemapSetting.server).toBe(expectedReturn); }); + }); - it('should handle nuxt.options.sourcemap.server settings', () => { + describe('should handle nuxt.options.sourcemap as a boolean', () => { + it('keeps setting of nuxt.options.sourcemap if it is set', () => { const cases = [ - { serverSourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, - { serverSourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, - { serverSourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, - { serverSourcemap: undefined, expectedSourcemap: 'hidden', expectedReturn: 'unset' }, + { sourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, + { sourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, + { sourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, ]; - cases.forEach(({ serverSourcemap, expectedSourcemap, expectedReturn }) => { - // @ts-expect-error server available - nuxt.options.sourcemap.server = serverSourcemap; + cases.forEach(({ sourcemap, expectedSourcemap, expectedReturn }) => { + // @ts-expect-error string type is possible in Nuxt (but type says differently) + nuxt.options.sourcemap = sourcemap; const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); - expect(nuxt.options.sourcemap.server).toBe(expectedSourcemap); + expect(nuxt.options.sourcemap).toBe(expectedSourcemap); + expect(previousUserSourcemapSetting.client).toBe(expectedReturn); expect(previousUserSourcemapSetting.server).toBe(expectedReturn); }); }); - describe('should handle nuxt.options.sourcemap as a boolean', () => { - it('keeps setting of nuxt.options.sourcemap if it is set', () => { - const cases = [ - { sourcemap: false, expectedSourcemap: false, expectedReturn: 'disabled' }, - { sourcemap: true, expectedSourcemap: true, expectedReturn: 'enabled' }, - { sourcemap: 'hidden', expectedSourcemap: 'hidden', expectedReturn: 'enabled' }, - ]; - - cases.forEach(({ sourcemap, expectedSourcemap, expectedReturn }) => { - // @ts-expect-error string type is possible in Nuxt (but type says differently) - nuxt.options.sourcemap = sourcemap; - const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); - expect(nuxt.options.sourcemap).toBe(expectedSourcemap); - expect(previousUserSourcemapSetting.client).toBe(expectedReturn); - expect(previousUserSourcemapSetting.server).toBe(expectedReturn); - }); - }); - - it("sets client and server to 'hidden' if nuxt.options.sourcemap not set", () => { - // @ts-expect-error - Nuxt types don't accept `undefined` but we want to test this case - nuxt.options.sourcemap = undefined; - const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); - expect(nuxt.options.sourcemap.client).toBe('hidden'); - expect(nuxt.options.sourcemap.server).toBe('hidden'); - expect(previousUserSourcemapSetting.client).toBe('unset'); - expect(previousUserSourcemapSetting.server).toBe('unset'); - }); + it("sets client and server to 'hidden' if nuxt.options.sourcemap not set", () => { + // @ts-expect-error - Nuxt types don't accept `undefined` but we want to test this case + nuxt.options.sourcemap = undefined; + const previousUserSourcemapSetting = changeNuxtSourceMapSettings(nuxt as Nuxt, sentryModuleOptions); + expect(nuxt.options.sourcemap.client).toBe('hidden'); + expect(nuxt.options.sourcemap.server).toBe('hidden'); + expect(previousUserSourcemapSetting.client).toBe('unset'); + expect(previousUserSourcemapSetting.server).toBe('unset'); }); }); }); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index a9460e20c248..0fa4926756c7 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -73,6 +73,7 @@ "clean": "rimraf build coverage sentry-opentelemetry-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/profiling-node/.eslintignore b/packages/profiling-node/.eslintignore index 0deb19641d74..ed07a0c4546a 100644 --- a/packages/profiling-node/.eslintignore +++ b/packages/profiling-node/.eslintignore @@ -1,4 +1,4 @@ node_modules/ build/ -lib/ +build/ coverage/ diff --git a/packages/profiling-node/.eslintrc.js b/packages/profiling-node/.eslintrc.js index 22c20a12dd77..ab3515d9ad37 100644 --- a/packages/profiling-node/.eslintrc.js +++ b/packages/profiling-node/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { }, extends: ['../../.eslintrc.js'], - ignorePatterns: ['lib/**/*', 'examples/**/*', 'vitest.config.ts'], + ignorePatterns: ['build/**/*', 'examples/**/*', 'vitest.config.ts'], rules: { '@sentry-internal/sdk/no-class-field-initializers': 'off', }, diff --git a/packages/profiling-node/.gitignore b/packages/profiling-node/.gitignore index 7a329e70a46c..a06983d7a102 100644 --- a/packages/profiling-node/.gitignore +++ b/packages/profiling-node/.gitignore @@ -2,5 +2,5 @@ # compiled output /node_modules/ -/lib/ +/build/ diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index bd0077f68249..6de0112b9521 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -6,26 +6,26 @@ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", "author": "Sentry", "license": "MIT", - "main": "lib/cjs/index.js", - "module": "lib/esm/index.js", - "types": "lib/types/index.d.ts", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "import": { - "types": "./lib/types/index.d.ts", - "default": "./lib/esm/index.js" + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" }, "require": { - "types": "./lib/types/index.d.ts", - "default": "./lib/cjs/index.js" + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" } } }, "typesVersions": { "<5.0": { - "lib/types/index.d.ts": [ - "lib/types-ts3.8/index.d.ts" + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" ] } }, @@ -39,18 +39,18 @@ "access": "public" }, "files": [ - "/lib", + "/build", "package.json", "/scripts/prune-profiler-binaries.js" ], "scripts": { - "clean": "rm -rf build && rm -rf lib", + "clean": "rm -rf build", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", "fix": "eslint . --format stylish --fix", - "build": "yarn build:lib", - "build:lib": "yarn build:types && rollup -c rollup.npm.config.mjs", - "build:transpile": "yarn build:lib", - "build:types:downlevel": "yarn downlevel-dts lib/types lib/types-ts3.8 --to ts3.8", + "build": "yarn build:types && yarn build:transpile", + "build:transpile": "yarn rollup -c rollup.npm.config.mjs", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:types": "tsc -p tsconfig.types.json && yarn build:types:downlevel", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:dev": "yarn clean && yarn build", diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs index 05327bc1a29a..80b26c2ac232 100644 --- a/packages/profiling-node/rollup.npm.config.mjs +++ b/packages/profiling-node/rollup.npm.config.mjs @@ -4,7 +4,7 @@ export default makeNPMConfigVariants( makeBaseNPMConfig({ packageSpecificConfig: { output: { - dir: 'lib', + dir: 'build', // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', // set preserveModules to false because for profiling we actually want diff --git a/packages/profiling-node/tsconfig.json b/packages/profiling-node/tsconfig.json index 68bd9a52df2a..29acbf3f36e9 100644 --- a/packages/profiling-node/tsconfig.json +++ b/packages/profiling-node/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "esnext", "lib": ["es2018"], - "outDir": "lib", + "outDir": "build", "types": ["node"] }, "include": ["src/**/*"] diff --git a/packages/profiling-node/tsconfig.types.json b/packages/profiling-node/tsconfig.types.json index d613534a1674..7a01535e9a4c 100644 --- a/packages/profiling-node/tsconfig.types.json +++ b/packages/profiling-node/tsconfig.types.json @@ -4,7 +4,7 @@ "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "outDir": "lib/types", + "outDir": "build/types", "types": ["node"] }, "files": ["src/index.ts"] diff --git a/packages/react-router/package.json b/packages/react-router/package.json index bc0ab65a1bc4..d943fab5dd76 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -39,6 +39,9 @@ "@sentry/core": "9.10.1", "@sentry/node": "9.10.1", "@sentry/vite-plugin": "^3.2.4", + "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/api": "^1.9.0", "glob": "11.0.1" }, "devDependencies": { @@ -67,6 +70,7 @@ "clean": "rimraf build coverage sentry-react-router-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/react-router/src/server/index.ts b/packages/react-router/src/server/index.ts index 6ac8d97b4241..44acfec7d4f2 100644 --- a/packages/react-router/src/server/index.ts +++ b/packages/react-router/src/server/index.ts @@ -1,3 +1,4 @@ export * from '@sentry/node'; export { init } from './sdk'; +export { sentryHandleRequest } from './sentryHandleRequest'; diff --git a/packages/react-router/src/server/sdk.ts b/packages/react-router/src/server/sdk.ts index bae99dee4983..d1e6b32b1d96 100644 --- a/packages/react-router/src/server/sdk.ts +++ b/packages/react-router/src/server/sdk.ts @@ -1,6 +1,7 @@ -import { applySdkMetadata, setTag } from '@sentry/core'; +import { applySdkMetadata, logger, setTag } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; +import { DEBUG_BUILD } from '../common/debug-build'; /** * Initializes the server side of the React Router SDK @@ -10,11 +11,14 @@ export function init(options: NodeOptions): NodeClient | undefined { ...options, }; + DEBUG_BUILD && logger.log('Initializing SDK...'); + applySdkMetadata(opts, 'react-router', ['react-router', 'node']); const client = initNodeSdk(opts); setTag('runtime', 'node'); + DEBUG_BUILD && logger.log('SDK successfully initialized'); return client; } diff --git a/packages/react-router/src/server/sentryHandleRequest.ts b/packages/react-router/src/server/sentryHandleRequest.ts new file mode 100644 index 000000000000..9c5f4abf72e8 --- /dev/null +++ b/packages/react-router/src/server/sentryHandleRequest.ts @@ -0,0 +1,52 @@ +import { context } from '@opentelemetry/api'; +import { RPCType, getRPCMetadata } from '@opentelemetry/core'; +import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getRootSpan } from '@sentry/core'; +import type { AppLoadContext, EntryContext } from 'react-router'; + +type OriginalHandleRequest = ( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext, +) => Promise; + +/** + * Wraps the original handleRequest function to add Sentry instrumentation. + * + * @param originalHandle - The original handleRequest function to wrap + * @returns A wrapped version of the handle request function with Sentry instrumentation + */ +export function sentryHandleRequest(originalHandle: OriginalHandleRequest): OriginalHandleRequest { + return async function sentryInstrumentedHandleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext, + ) { + const parameterizedPath = + routerContext?.staticHandlerContext?.matches?.[routerContext.staticHandlerContext.matches.length - 1]?.route.path; + if (parameterizedPath) { + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const routeName = `/${parameterizedPath}`; + + // The express instrumentation writes on the rpcMetadata and that ends up stomping on the `http.route` attribute. + const rpcMetadata = getRPCMetadata(context.active()); + if (rpcMetadata?.type === RPCType.HTTP) { + rpcMetadata.route = routeName; + } + + // The span exporter picks up the `http.route` (ATTR_HTTP_ROUTE) attribute to set the transaction name + rootSpan.setAttributes({ + [ATTR_HTTP_ROUTE]: routeName, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }); + } + } + return originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext); + }; +} diff --git a/packages/react/package.json b/packages/react/package.json index f970a4acc968..6fdefba78da2 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -85,6 +85,7 @@ "clean": "rimraf build coverage sentry-react-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/remix/README.md b/packages/remix/README.md index fbf8480d5e25..9260def2b700 100644 --- a/packages/remix/README.md +++ b/packages/remix/README.md @@ -90,23 +90,6 @@ function App() { export default withSentry(App); ``` -You can disable or configure `ErrorBoundary` using a second parameter to `withSentry`. - -```ts - -withSentry(App, { - wrapWithErrorBoundary: false -}); - -// or - -withSentry(App, { - errorBoundaryOptions: { - fallback:

An error has occurred

- } -}); -``` - To set context information or send manual events, use the exported functions of `@sentry/remix`. ```ts diff --git a/packages/remix/package.json b/packages/remix/package.json index 7507eb8d462e..62b36bbcc643 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -96,6 +96,7 @@ "clean": "rimraf build coverage sentry-remix-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:integration": "run-s test:integration:clean test:integration:prepare test:integration:client test:integration:server", "test:integration:ci": "run-s test:integration:clean test:integration:prepare test:integration:client:ci test:integration:server", diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 9d47aae36d4a..8e55c9d683c3 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -96,12 +96,14 @@ function startNavigationSpan(matches: RouteMatch[]): void { } /** - * Wraps a remix `root` (see: https://remix.run/docs/en/v1/guides/migrating-react-router-app#creating-the-root-route) + * Wraps a remix `root` (see: https://remix.run/docs/en/main/start/quickstart#the-root-route) * To enable pageload/navigation tracing on every route. - * Also wraps the application with `ErrorBoundary`. * * @param OrigApp The Remix root to wrap - * @param options The options for ErrorBoundary wrapper. + * @param useEffect The `useEffect` hook from `react` + * @param useLocation The `useLocation` hook from `@remix-run/react` + * @param useMatches The `useMatches` hook from `@remix-run/react` + * @param instrumentNavigation Whether to instrument navigation spans. Defaults to `true`. */ export function withSentry

, R extends React.ComponentType

>( OrigApp: R, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 7809455ce3fa..6c5319349294 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -113,6 +113,7 @@ export { withScope, zodErrorsIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 408e430794ed..ff4f05fe67e9 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -47,6 +47,7 @@ "clean": "rimraf build sentry-replay-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/{bundles,npm/cjs}/*.js && es-check es2020 ./build/npm/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 545ed7d43f54..1a7007b55e15 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -54,6 +54,7 @@ "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --format stylish", "lint:prettier": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", + "lint:es-compatibility": "es-check es2020 ./build/{bundles,npm/cjs}/*.js && es-check es2020 ./build/npm/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index a872e89489ab..2818255532bb 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -32,6 +32,7 @@ "clean": "rimraf build", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch" }, diff --git a/packages/solid/package.json b/packages/solid/package.json index 3273b2300ac5..75ffb0372bf7 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -80,6 +80,7 @@ "clean": "rimraf build coverage sentry-solid-*.tgz ./*.d.ts ./*.d.ts.map", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 541a226bfe75..8b7ad3853b7b 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -96,6 +96,7 @@ "clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index a36b7fbfaad1..da00b43a4fde 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -116,6 +116,7 @@ export { withScope, zodErrorsIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 962a56a2f9fb..3b703069eaec 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -67,6 +67,7 @@ "clean": "rimraf build coverage sentry-svelte-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 60d96f93c116..9e2ef43a8716 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -75,6 +75,7 @@ "clean": "rimraf build coverage sentry-sveltekit-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 9df3ddd688c8..f50420fd2937 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -118,6 +118,7 @@ export { withScope, zodErrorsIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 852897606cd3..22cf7155ac0b 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -74,6 +74,7 @@ "clean": "rimraf build coverage sentry-tanstackstart-react-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json index 22beff0a921a..733ec6bcd4d5 100644 --- a/packages/tanstackstart/package.json +++ b/packages/tanstackstart/package.json @@ -55,6 +55,7 @@ "clean": "rimraf build coverage sentry-tanstackstart-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/types/package.json b/packages/types/package.json index fd150a189432..649f96564fe2 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -52,6 +52,7 @@ "build:tarball": "npm pack", "clean": "rimraf build sentry-types-*.tgz", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "fix": "eslint . --format stylish --fix", "yalc:publish": "yalc publish --push --sig" }, diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index c3c9f8b7a967..c78ad9f5e1b1 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -66,6 +66,7 @@ "clean": "rimraf build coverage sentry-vercel-edge-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/vue/package.json b/packages/vue/package.json index 241c6cde005d..4371d528b739 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -70,6 +70,7 @@ "clean": "rimraf build coverage sentry-vue-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", "test": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" diff --git a/packages/wasm/package.json b/packages/wasm/package.json index b1c30bca7dbe..243d4110d8f8 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -62,6 +62,7 @@ "clean": "rimraf build coverage sentry-wasm-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2020 ./build/{bundles,npm/cjs}/*.js && es-check es2020 ./build/npm/esm/*.js --module", "yalc:publish": "yalc publish --push --sig" }, "volta": { diff --git a/vite/vite.config.ts b/vite/vite.config.ts index a26454ad5712..53823d2b9451 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -8,6 +8,16 @@ export default defineConfig({ coverage: { enabled: true, reportsDirectory: './coverage', + exclude: [ + '**/node_modules/**', + '**/test/**', + '**/tests/**', + '**/__tests__/**', + '**/rollup*.config.*', + '**/build/**', + '.eslint*', + 'vite.config.*', + ], }, reporters: ['default', ...(process.env.CI ? [['junit', { classnameTemplate: '{filepath}' }]] : [])], outputFile: {