From 52be8613cc9a04f87452bd1e6789b48b73f1044b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 1 Aug 2025 09:23:49 +0200 Subject: [PATCH 1/5] fix(browser-utils): Ensure web vital client hooks unsubscribe correctly (#17272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So this was a fun one to track down 😅 In our standalone span web vitals code, we register multiple client hooks to listen e.g. for `afterStartPageloadSpan` events. This hook will only fire once (by design), so we want to unsbscribe from it afterwards. Howerver, we register two callbacks (one for LCP, once for CLS). Because we used to unsubscribe synchronously from within the client hook callback, we synchronously removed the callback from the client's hooks array. This synchronous array mutation (shrinking) caused the second callback to no longer be executed. This surfaced by the LCP span being sent but the CLS span not being sent, due to the CLS span's hook callback no longer being called. This PR fixes this incorrect unsubscription by deferring the unsubscription calls to the next tick. This way, the array mutation no longer happens synchronously and all remaining callback hooks are invoked correctly. If you're confused by this, rest assured, I was too 😅 Happy to explain better/in-person on request :D closes https://linear.app/getsentry/issue/JS-811/investigate-missing-standalone-cls-spans-in-latest-sdk-versions --- packages/browser-utils/src/metrics/utils.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index e56d0ee98d42..5caab5bc75cc 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -226,13 +226,21 @@ export function listenForWebVitalReportEvents( // we only want to collect LCP if we actually navigate. Redirects should be ignored. if (!options?.isRedirect) { _runCollectorCallbackOnce('navigation'); - unsubscribeStartNavigation?.(); - unsubscribeAfterStartPageLoadSpan?.(); + safeUnsubscribe(unsubscribeStartNavigation, unsubscribeAfterStartPageLoadSpan); } }); const unsubscribeAfterStartPageLoadSpan = client.on('afterStartPageLoadSpan', span => { pageloadSpanId = span.spanContext().spanId; - unsubscribeAfterStartPageLoadSpan?.(); + safeUnsubscribe(unsubscribeAfterStartPageLoadSpan); }); } + +/** + * Invoke a list of unsubscribers in a safe way, by deferring the invocation to the next tick. + * This is necessary because unsubscribing in sync can lead to other callbacks no longer being invoked + * due to in-place array mutation of the subscribers array on the client. + */ +function safeUnsubscribe(...unsubscribers: (() => void | undefined)[]): void { + unsubscribers.forEach(u => u && setTimeout(u, 0)); +} From aa4547cb2ce225b47a5d417100ce025d2ca78cc2 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:51:47 +0200 Subject: [PATCH 2/5] feat(nuxt): Align build-time options to follow bundler plugins structure (#17255) Aligns the build-time options of the Nuxt SDK and implements the missing options. Closes https://github.com/getsentry/sentry-javascript/issues/17064 --- .../buildTimeOptionsBase.ts | 21 +- packages/nuxt/.eslintrc.js | 2 +- packages/nuxt/src/common/types.ts | 34 ++- packages/nuxt/src/vite/sourceMaps.ts | 71 +++++-- .../nuxt/test/vite/buildOptions.test-d.ts | 129 ++++++++++++ packages/nuxt/test/vite/sourceMaps.test.ts | 194 ++++++++++++++---- packages/nuxt/test/vite/utils.test.ts | 14 +- packages/nuxt/tsconfig.test.json | 2 +- packages/nuxt/tsconfig.vite.json | 9 + packages/nuxt/vite.config.ts | 4 + 10 files changed, 409 insertions(+), 71 deletions(-) create mode 100644 packages/nuxt/test/vite/buildOptions.test-d.ts create mode 100644 packages/nuxt/tsconfig.vite.json diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index fbb8afd3b9fe..826ec0a4ae4c 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -84,6 +84,23 @@ export interface BuildTimeOptionsBase { */ silent?: boolean; + /** + * When an error occurs during release creation or sourcemaps upload, the plugin will call this function. + * + * By default, the plugin will simply throw an error, thereby stopping the bundling process. + * If an `errorHandler` callback is provided, compilation will continue unless an error is + * thrown in the provided callback. + * + * To allow compilation to continue but still emit a warning, set this option to the following: + * + * ```js + * (err) => { + * console.warn(err); + * } + * ``` + */ + errorHandler?: (err: Error) => void; + /** * Enable debug information logs about the SDK during build-time. * Enabling this will give you, for example, logs about source maps. @@ -184,7 +201,9 @@ export type UnstableRollupPluginOptions = { interface SourceMapsOptions { /** - * If this flag is `true`, any functionality related to source maps will be disabled. + * If this flag is `true`, any functionality related to source maps will be disabled. This includes the automatic upload of source maps. + * + * By default (`false`), the plugin automatically uploads source maps during a production build if a Sentry auth token is detected. * * @default false */ diff --git a/packages/nuxt/.eslintrc.js b/packages/nuxt/.eslintrc.js index a22f9710cf6b..e6ea40d78d05 100644 --- a/packages/nuxt/.eslintrc.js +++ b/packages/nuxt/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { { files: ['vite.config.ts'], parserOptions: { - project: ['tsconfig.test.json'], + project: ['tsconfig.vite.json'], }, }, ], diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 599b564f62a2..96d69354937c 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -1,3 +1,4 @@ +import type { BuildTimeOptionsBase } from '@sentry/core'; import type { init as initNode } from '@sentry/node'; import type { SentryRollupPluginOptions } from '@sentry/rollup-plugin'; import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; @@ -26,6 +27,7 @@ type SourceMapsOptions = { * Suppresses all logs. * * @default false + * @deprecated Use option `silent` instead of `sourceMapsUploadOptions.silent` */ silent?: boolean; @@ -43,6 +45,8 @@ type SourceMapsOptions = { * console.warn(err); * } * ``` + * + * @deprecated Use option `errorHandler` instead of `sourceMapsUploadOptions.errorHandler` */ errorHandler?: (err: Error) => void; @@ -50,6 +54,8 @@ type SourceMapsOptions = { * Options related to managing the Sentry releases for a build. * * More info: https://docs.sentry.io/product/releases/ + * + * @deprecated Use option `release` instead of `sourceMapsUploadOptions.release` */ release?: { /** @@ -62,6 +68,8 @@ type SourceMapsOptions = { * (the latter requires access to git CLI and for the root directory to be a valid repository) * * If you didn't provide a value and the plugin can't automatically detect one, no release will be created. + * + * @deprecated Use `release.name` instead of `sourceMapsUploadOptions.release.name` */ name?: string; }; @@ -71,6 +79,7 @@ type SourceMapsOptions = { * automatically generate and upload source maps to Sentry during a production build. * * @default true + * @deprecated Use option `sourcemaps.disable` instead of `sourceMapsUploadOptions.enabled` */ enabled?: boolean; @@ -81,12 +90,14 @@ type SourceMapsOptions = { * * To create an auth token, follow this guide: * @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens + * @deprecated Use option `authToken` instead of `sourceMapsUploadOptions.authToken` */ authToken?: string; /** * The organization slug of your Sentry organization. * Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable. + * @deprecated Use option `org` instead of `sourceMapsUploadOptions.org` */ org?: string; @@ -94,12 +105,15 @@ type SourceMapsOptions = { * The URL of your Sentry instance if you're using self-hosted Sentry. * * @default https://sentry.io by default the plugin will point towards the Sentry SaaS URL + * @deprecated Use `sentryUrl` instead of `sourceMapsUploadOptions.url` */ url?: string; /** * The project slug of your Sentry project. * Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable. + * + * @deprecated Use option `project` instead of `sourceMapsUploadOptions.project` */ project?: string; @@ -108,11 +122,14 @@ type SourceMapsOptions = { * It will not collect any sensitive or user-specific data. * * @default true + * @deprecated Use option `telemetry` instead of `sourceMapsUploadOptions.telemetry` */ telemetry?: boolean; /** * Options related to sourcemaps + * + * @deprecated Use option `sourcemaps` instead of `sourceMapsUploadOptions.sourcemaps` */ sourcemaps?: { /** @@ -124,6 +141,8 @@ type SourceMapsOptions = { * * The globbing patterns must follow the implementation of the `glob` package. * @see https://www.npmjs.com/package/glob#glob-primer + * + * @deprecated Use option `sourcemaps.assets` instead of `sourceMapsUploadOptions.sourcemaps.assets` */ assets?: string | Array; @@ -134,6 +153,8 @@ type SourceMapsOptions = { * or the default value for `assets` are uploaded. * * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) + * + * @deprecated Use option `sourcemaps.ignore` instead of `sourceMapsUploadOptions.sourcemaps.ignore` */ ignore?: string | Array; @@ -144,6 +165,8 @@ type SourceMapsOptions = { * @default [] - By default no files are deleted. * * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) + * + * @deprecated Use option `sourcemaps.filesToDeleteAfterUpload` instead of `sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload` */ filesToDeleteAfterUpload?: string | Array; }; @@ -152,7 +175,7 @@ type SourceMapsOptions = { /** * Build options for the Sentry module. These options are used during build-time by the Sentry SDK. */ -export type SentryNuxtModuleOptions = { +export type SentryNuxtModuleOptions = BuildTimeOptionsBase & { /** * Enable the Sentry Nuxt Module. * @@ -165,15 +188,12 @@ export type SentryNuxtModuleOptions = { * * These options are always read from the `sentry` module options in the `nuxt.config.(js|ts). * Do not define them in the `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files. + * + * @deprecated This option was deprecated as it adds unnecessary nesting. + * Put the options one level higher to the root-level of the `sentry` module options. */ sourceMapsUploadOptions?: SourceMapsOptions; - /** - * Enable debug functionality of the SDK during build-time. - * Enabling this will give you, for example, logs about source maps. - */ - debug?: boolean; - /** * * Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible. diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 176690734339..0e35ba1175ce 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -17,10 +17,20 @@ 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 { + // TODO(v11): remove deprecated options (also from SentryNuxtModuleOptions type) + const isDebug = moduleOptions.debug; + // eslint-disable-next-line deprecation/deprecation const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; - const sourceMapsEnabled = sourceMapsUploadOptions.enabled ?? true; + + const sourceMapsEnabled = + moduleOptions.sourcemaps?.disable === true + ? false + : moduleOptions.sourcemaps?.disable === false + ? true + : // eslint-disable-next-line deprecation/deprecation + sourceMapsUploadOptions.enabled ?? true; // In case we overwrite the source map settings, we default to deleting the files let shouldDeleteFilesFallback = { client: true, server: true }; @@ -42,6 +52,8 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu if ( isDebug && + !moduleOptions.sourcemaps?.filesToDeleteAfterUpload && + // eslint-disable-next-line deprecation/deprecation !sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload && (shouldDeleteFilesFallback.client || shouldDeleteFilesFallback.server) ) { @@ -134,10 +146,13 @@ function normalizePath(path: string): string { * * Only exported for Testing purposes. */ +// todo(v11): This "eslint-disable" can be removed again once we remove deprecated options. +// eslint-disable-next-line complexity export function getPluginOptions( moduleOptions: SentryNuxtModuleOptions, shouldDeleteFilesFallback?: { client: boolean; server: boolean }, ): SentryVitePluginOptions | SentryRollupPluginOptions { + // eslint-disable-next-line deprecation/deprecation const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; const shouldDeleteFilesAfterUpload = shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server; @@ -148,10 +163,17 @@ export function getPluginOptions( : []), ]; - if ( - typeof sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload === 'undefined' && - shouldDeleteFilesAfterUpload - ) { + // Check for filesToDeleteAfterUpload in new location first, then deprecated location + const sourcemapsOptions = moduleOptions.sourcemaps || {}; + // eslint-disable-next-line deprecation/deprecation + const deprecatedSourcemapsOptions = sourceMapsUploadOptions.sourcemaps || {}; + + const filesToDeleteAfterUpload = + sourcemapsOptions.filesToDeleteAfterUpload ?? + // eslint-disable-next-line deprecation/deprecation + deprecatedSourcemapsOptions.filesToDeleteAfterUpload; + + if (typeof filesToDeleteAfterUpload === 'undefined' && shouldDeleteFilesAfterUpload) { consoleSandbox(() => { // eslint-disable-next-line no-console console.log( @@ -164,16 +186,28 @@ export function getPluginOptions( } return { - org: sourceMapsUploadOptions.org ?? process.env.SENTRY_ORG, - project: sourceMapsUploadOptions.project ?? process.env.SENTRY_PROJECT, - authToken: sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, - telemetry: sourceMapsUploadOptions.telemetry ?? true, - url: sourceMapsUploadOptions.url ?? process.env.SENTRY_URL, + // eslint-disable-next-line deprecation/deprecation + org: moduleOptions.org ?? sourceMapsUploadOptions.org ?? process.env.SENTRY_ORG, + // eslint-disable-next-line deprecation/deprecation + project: moduleOptions.project ?? sourceMapsUploadOptions.project ?? process.env.SENTRY_PROJECT, + // eslint-disable-next-line deprecation/deprecation + authToken: moduleOptions.authToken ?? sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, + // eslint-disable-next-line deprecation/deprecation + telemetry: moduleOptions.telemetry ?? sourceMapsUploadOptions.telemetry ?? true, + // eslint-disable-next-line deprecation/deprecation + url: moduleOptions.sentryUrl ?? sourceMapsUploadOptions.url ?? process.env.SENTRY_URL, + headers: moduleOptions.headers, debug: moduleOptions.debug ?? false, - silent: sourceMapsUploadOptions.silent ?? false, - errorHandler: sourceMapsUploadOptions.errorHandler, + // eslint-disable-next-line deprecation/deprecation + silent: moduleOptions.silent ?? sourceMapsUploadOptions.silent ?? false, + // eslint-disable-next-line deprecation/deprecation + errorHandler: moduleOptions.errorHandler ?? sourceMapsUploadOptions.errorHandler, + bundleSizeOptimizations: moduleOptions.bundleSizeOptimizations, // todo: test if this can be overridden by the user release: { - name: sourceMapsUploadOptions.release?.name, + // eslint-disable-next-line deprecation/deprecation + name: moduleOptions.release?.name ?? sourceMapsUploadOptions.release?.name, + // Support all release options from BuildTimeOptionsBase + ...moduleOptions.release, ...moduleOptions?.unstable_sentryBundlerPluginOptions?.release, }, _metaOptions: { @@ -184,13 +218,16 @@ export function getPluginOptions( ...moduleOptions?.unstable_sentryBundlerPluginOptions, sourcemaps: { + disable: moduleOptions.sourcemaps?.disable, // The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server') // We cannot determine automatically how the build folder looks like (depends on the preset), so we have to accept that source maps are uploaded multiple times (with the vitePlugin for Nuxt and the rollupPlugin for Nitro). // If we could know where the server/client assets are located, we could do something like this (based on the Nitro preset): isNitro ? ['./.output/server/**/*'] : ['./.output/public/**/*'], - assets: sourceMapsUploadOptions.sourcemaps?.assets ?? undefined, - ignore: sourceMapsUploadOptions.sourcemaps?.ignore ?? undefined, - filesToDeleteAfterUpload: sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload - ? sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload + // eslint-disable-next-line deprecation/deprecation + assets: sourcemapsOptions.assets ?? deprecatedSourcemapsOptions.assets ?? undefined, + // eslint-disable-next-line deprecation/deprecation + ignore: sourcemapsOptions.ignore ?? deprecatedSourcemapsOptions.ignore ?? undefined, + filesToDeleteAfterUpload: filesToDeleteAfterUpload + ? filesToDeleteAfterUpload : shouldDeleteFilesFallback?.server || shouldDeleteFilesFallback?.client ? fallbackFilesToDelete : undefined, diff --git a/packages/nuxt/test/vite/buildOptions.test-d.ts b/packages/nuxt/test/vite/buildOptions.test-d.ts new file mode 100644 index 000000000000..ac2adde02ece --- /dev/null +++ b/packages/nuxt/test/vite/buildOptions.test-d.ts @@ -0,0 +1,129 @@ +import { describe, expectTypeOf, it } from 'vitest'; +import type { SentryNuxtModuleOptions } from '../../src/common/types'; + +describe('Sentry Nuxt build-time options type', () => { + it('includes all options based on type BuildTimeOptionsBase', () => { + const completeOptions: SentryNuxtModuleOptions = { + // --- BuildTimeOptionsBase options --- + org: 'test-org', + project: 'test-project', + authToken: 'test-auth-token', + sentryUrl: 'https://sentry.io', + headers: { Authorization: ' Bearer test-auth-token' }, + telemetry: true, + silent: false, + // eslint-disable-next-line no-console + errorHandler: (err: Error) => console.warn(err), + debug: false, + sourcemaps: { + disable: false, + assets: ['./dist/**/*'], + ignore: ['./dist/*.map'], + filesToDeleteAfterUpload: ['./dist/*.map'], + }, + release: { + name: 'test-release-1.0.0', + create: true, + finalize: true, + dist: 'test-dist', + vcsRemote: 'origin', + setCommits: { + auto: false, + repo: 'test/repo', + commit: 'abc123', + previousCommit: 'def456', + ignoreMissing: false, + ignoreEmpty: false, + }, + deploy: { + env: 'production', + started: 1234567890, + finished: 1234567900, + time: 10, + name: 'deployment-name', + url: 'https://example.com', + }, + }, + bundleSizeOptimizations: { + excludeDebugStatements: true, + excludeTracing: false, + excludeReplayShadowDom: true, + excludeReplayIframe: true, + excludeReplayWorker: true, + }, + + // --- SentryNuxtModuleOptions specific options --- + enabled: true, + autoInjectServerSentry: 'experimental_dynamic-import', + experimental_entrypointWrappedFunctions: ['default', 'handler', 'server', 'customExport'], + unstable_sentryBundlerPluginOptions: { + // Rollup plugin options + bundleSizeOptimizations: { + excludeDebugStatements: true, + }, + // Vite plugin options + sourcemaps: { + assets: './dist/**/*', + }, + }, + }; + + expectTypeOf(completeOptions).toEqualTypeOf(); + }); + + it('includes all deprecated options', () => { + const completeOptions: SentryNuxtModuleOptions = { + // SentryNuxtModuleOptions specific options + enabled: true, + debug: true, + autoInjectServerSentry: 'experimental_dynamic-import', // No need for 'as const' with type assertion + experimental_entrypointWrappedFunctions: ['default', 'handler', 'server', 'customExport'], + unstable_sentryBundlerPluginOptions: { + // Rollup plugin options + bundleSizeOptimizations: { + excludeDebugStatements: true, + }, + // Vite plugin options + sourcemaps: { + assets: './dist/**/*', + }, + }, + + // Deprecated sourceMapsUploadOptions + sourceMapsUploadOptions: { + silent: false, + // eslint-disable-next-line no-console + errorHandler: (err: Error) => console.warn(err), + release: { + name: 'deprecated-release', + }, + enabled: true, + authToken: 'deprecated-token', + org: 'deprecated-org', + url: 'https://deprecated.sentry.io', + project: 'deprecated-project', + telemetry: false, + sourcemaps: { + assets: './build/**/*', + ignore: ['./build/*.spec.js'], + filesToDeleteAfterUpload: ['./build/*.map'], + }, + }, + }; + + expectTypeOf(completeOptions).toEqualTypeOf(); + }); + + it('allows partial configuration', () => { + const minimalOptions: SentryNuxtModuleOptions = { enabled: true }; + + expectTypeOf(minimalOptions).toEqualTypeOf(); + + const partialOptions: SentryNuxtModuleOptions = { + enabled: true, + debug: false, + }; + + expectTypeOf(partialOptions).toEqualTypeOf(); + }); +}); diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts index aaa7d2035655..59eac291fcc7 100644 --- a/packages/nuxt/test/vite/sourceMaps.test.ts +++ b/packages/nuxt/test/vite/sourceMaps.test.ts @@ -18,16 +18,14 @@ describe('getPluginOptions', () => { process.env = {}; }); - it('uses environment variables when no moduleOptions are provided', () => { - const defaultEnv = { + it('uses environment variables as fallback when no moduleOptions are provided', () => { + process.env = { SENTRY_ORG: 'default-org', SENTRY_PROJECT: 'default-project', SENTRY_AUTH_TOKEN: 'default-token', SENTRY_URL: 'https://santry.io', }; - process.env = { ...defaultEnv }; - const options = getPluginOptions({} as SentryNuxtModuleOptions); expect(options).toEqual( @@ -110,46 +108,159 @@ describe('getPluginOptions', () => { ); }); - it('overrides options that were undefined with options from unstable_sentryRollupPluginOptions', () => { - const customOptions: SentryNuxtModuleOptions = { + it('prioritizes new BuildTimeOptionsBase options over deprecated ones', () => { + const options: SentryNuxtModuleOptions = { + // New options + org: 'new-org', + project: 'new-project', + authToken: 'new-token', + sentryUrl: 'https://new.sentry.io', + telemetry: false, + silent: true, + debug: true, + sourcemaps: { + assets: ['new-assets/**/*'], + ignore: ['new-ignore.js'], + filesToDeleteAfterUpload: ['new-delete.js'], + }, + release: { + name: 'test-release', + create: false, + finalize: true, + dist: 'build-123', + vcsRemote: 'upstream', + setCommits: { auto: true }, + deploy: { env: 'production' }, + }, + bundleSizeOptimizations: { excludeTracing: true }, + + // Deprecated options (should be ignored) sourceMapsUploadOptions: { - org: 'custom-org', - project: 'custom-project', + org: 'old-org', + project: 'old-project', + authToken: 'old-token', + url: 'https://old.sentry.io', + telemetry: true, + silent: false, sourcemaps: { - assets: ['custom-assets/**/*'], - filesToDeleteAfterUpload: ['delete-this.js'], + assets: ['old-assets/**/*'], + ignore: ['old-ignore.js'], + filesToDeleteAfterUpload: ['old-delete.js'], }, - url: 'https://santry.io', + release: { name: 'old-release' }, }, + }; + + const result = getPluginOptions(options); + + expect(result).toMatchObject({ + org: 'new-org', + project: 'new-project', + authToken: 'new-token', + url: 'https://new.sentry.io', + telemetry: false, + silent: true, debug: true, - unstable_sentryBundlerPluginOptions: { - org: 'unstable-org', + bundleSizeOptimizations: { excludeTracing: true }, + release: { + name: 'test-release', + create: false, + finalize: true, + dist: 'build-123', + vcsRemote: 'upstream', + setCommits: { auto: true }, + deploy: { env: 'production' }, + }, + sourcemaps: expect.objectContaining({ + assets: ['new-assets/**/*'], + ignore: ['new-ignore.js'], + filesToDeleteAfterUpload: ['new-delete.js'], + }), + }); + }); + + it('falls back to deprecated options when new ones are undefined', () => { + const options: SentryNuxtModuleOptions = { + debug: true, + sourceMapsUploadOptions: { + org: 'deprecated-org', + project: 'deprecated-project', + authToken: 'deprecated-token', + url: 'https://deprecated.sentry.io', + telemetry: false, sourcemaps: { - assets: ['unstable-assets/**/*'], + assets: ['deprecated/**/*'], }, - release: { - name: 'test-release', + release: { name: 'deprecated-release' }, + }, + }; + + const result = getPluginOptions(options); + + expect(result).toMatchObject({ + org: 'deprecated-org', + project: 'deprecated-project', + authToken: 'deprecated-token', + url: 'https://deprecated.sentry.io', + telemetry: false, + debug: true, + release: { name: 'deprecated-release' }, + sourcemaps: expect.objectContaining({ + assets: ['deprecated/**/*'], + }), + }); + }); + + it('supports bundleSizeOptimizations', () => { + const options: SentryNuxtModuleOptions = { + bundleSizeOptimizations: { + excludeDebugStatements: true, + excludeTracing: true, + excludeReplayShadowDom: true, + excludeReplayIframe: true, + excludeReplayWorker: true, + }, + }; + + const result = getPluginOptions(options); + + expect(result.bundleSizeOptimizations).toEqual({ + excludeDebugStatements: true, + excludeTracing: true, + excludeReplayShadowDom: true, + excludeReplayIframe: true, + excludeReplayWorker: true, + }); + }); + + it('merges with unstable_sentryBundlerPluginOptions correctly', () => { + const options: SentryNuxtModuleOptions = { + org: 'base-org', + bundleSizeOptimizations: { + excludeDebugStatements: false, + }, + unstable_sentryBundlerPluginOptions: { + org: 'override-org', + release: { name: 'override-release' }, + sourcemaps: { assets: ['override/**/*'] }, + bundleSizeOptimizations: { + excludeDebugStatements: true, }, - url: 'https://suntry.io', }, }; - const options = getPluginOptions(customOptions); - expect(options).toEqual( - expect.objectContaining({ - debug: true, - org: 'unstable-org', - project: 'custom-project', - sourcemaps: expect.objectContaining({ - assets: ['unstable-assets/**/*'], - filesToDeleteAfterUpload: ['delete-this.js'], - rewriteSources: expect.any(Function), - }), - release: expect.objectContaining({ - name: 'test-release', - }), - url: 'https://suntry.io', + + const result = getPluginOptions(options); + + expect(result).toMatchObject({ + org: 'override-org', + release: { name: 'override-release' }, + sourcemaps: expect.objectContaining({ + assets: ['override/**/*'], }), - ); + bundleSizeOptimizations: { + excludeDebugStatements: true, + }, + }); }); it.each([ @@ -180,17 +291,24 @@ describe('getPluginOptions', () => { expectedFilesToDelete: ['.*/**/server/**/*.map', '.*/**/output/**/*.map', '.*/**/function/**/*.map'], }, { - name: 'no fallback, but custom filesToDeleteAfterUpload is provided', + name: 'no fallback, but custom filesToDeleteAfterUpload is provided (deprecated)', clientFallback: false, serverFallback: false, customOptions: { sourceMapsUploadOptions: { - sourcemaps: { - filesToDeleteAfterUpload: ['custom/path/**/*.map'], - }, + sourcemaps: { filesToDeleteAfterUpload: ['deprecated/path/**/*.map'] }, }, }, - expectedFilesToDelete: ['custom/path/**/*.map'], + expectedFilesToDelete: ['deprecated/path/**/*.map'], + }, + { + name: 'no fallback, but custom filesToDeleteAfterUpload is provided (new)', + clientFallback: false, + serverFallback: false, + customOptions: { + sourcemaps: { filesToDeleteAfterUpload: ['new-custom/path/**/*.map'] }, + }, + expectedFilesToDelete: ['new-custom/path/**/*.map'], }, { name: 'no fallback, both source maps explicitly false and no custom filesToDeleteAfterUpload', diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 6380d2d6a0c7..1b256987828b 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -26,7 +26,7 @@ describe('findDefaultSdkInitFile', () => { 'should return the server file path with .%s extension if it exists', ext => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { - return !(filePath instanceof URL) && filePath.includes(`sentry.server.config.${ext}`); + return !(filePath instanceof URL) && filePath.toString().includes(`sentry.server.config.${ext}`); }); const result = findDefaultSdkInitFile('server'); @@ -38,7 +38,7 @@ describe('findDefaultSdkInitFile', () => { 'should return the client file path with .%s extension if it exists', ext => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { - return !(filePath instanceof URL) && filePath.includes(`sentry.client.config.${ext}`); + return !(filePath instanceof URL) && filePath.toString().includes(`sentry.client.config.${ext}`); }); const result = findDefaultSdkInitFile('client'); @@ -64,7 +64,8 @@ describe('findDefaultSdkInitFile', () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return ( !(filePath instanceof URL) && - (filePath.includes('sentry.server.config.js') || filePath.includes('instrument.server.js')) + (filePath.toString().includes('sentry.server.config.js') || + filePath.toString().includes('instrument.server.js')) ); }); @@ -74,7 +75,7 @@ describe('findDefaultSdkInitFile', () => { it('should return the latest layer config file path if client config exists', () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { - return !(filePath instanceof URL) && filePath.includes('sentry.client.config.ts'); + return !(filePath instanceof URL) && filePath.toString().includes('sentry.client.config.ts'); }); const nuxtMock = { @@ -98,7 +99,8 @@ describe('findDefaultSdkInitFile', () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return ( !(filePath instanceof URL) && - (filePath.includes('sentry.server.config.ts') || filePath.includes('instrument.server.ts')) + (filePath.toString().includes('sentry.server.config.ts') || + filePath.toString().includes('instrument.server.ts')) ); }); @@ -121,7 +123,7 @@ describe('findDefaultSdkInitFile', () => { it('should return the latest layer config file path if client config exists in former layer', () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { - return !(filePath instanceof URL) && filePath.includes('nuxt/sentry.client.config.ts'); + return !(filePath instanceof URL) && filePath.toString().includes('nuxt/sentry.client.config.ts'); }); const nuxtMock = { diff --git a/packages/nuxt/tsconfig.test.json b/packages/nuxt/tsconfig.test.json index c41efeacd92f..da5a816712e3 100644 --- a/packages/nuxt/tsconfig.test.json +++ b/packages/nuxt/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*", "vite.config.ts"], + "include": ["test/**/*"], "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used diff --git a/packages/nuxt/tsconfig.vite.json b/packages/nuxt/tsconfig.vite.json new file mode 100644 index 000000000000..3e2d75a55e61 --- /dev/null +++ b/packages/nuxt/tsconfig.vite.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + + "include": ["vite.config.ts"], + + "compilerOptions": { + "types": ["node"] + } +} diff --git a/packages/nuxt/vite.config.ts b/packages/nuxt/vite.config.ts index 0229ec105e04..75dc3957244a 100644 --- a/packages/nuxt/vite.config.ts +++ b/packages/nuxt/vite.config.ts @@ -5,5 +5,9 @@ export default { test: { environment: 'jsdom', setupFiles: ['./test/vitest.setup.ts'], + typecheck: { + enabled: true, + tsconfig: './tsconfig.test.json', + }, }, }; From 8f4d56fa1efc82a62d474ad88d96e4da36279d2a Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 1 Aug 2025 14:14:52 +0200 Subject: [PATCH 3/5] fix(browser): Ensure request from `diagnoseSdkConnectivity` doesn't create span (#17280) This patch ensures that we suppress tracing (and breadcrumbs) for `diagnoseSdkConnectivity`-made requests. The `suppressTracing` approach works well here. We could also get an native fetch implementation here but I'd say we only try this if for some reason `suppressTracing` doesn't work. --------- Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- .../diagnoseSdkConnectivity/init.js | 9 ++++ .../diagnoseSdkConnectivity/subject.js | 1 + .../diagnoseSdkConnectivity/test.ts | 49 +++++++++++++++++++ packages/browser/src/diagnose-sdk.ts | 32 ++++++------ packages/browser/test/diagnose-sdk.test.ts | 14 ++++++ 5 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js new file mode 100644 index 000000000000..8c0a0cd9fca4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ idleTimeout: 3000, childSpanTimeout: 3000 })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/subject.js b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/subject.js new file mode 100644 index 000000000000..c1239ac16b0e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/subject.js @@ -0,0 +1 @@ +Sentry.diagnoseSdkConnectivity().then(res => console.log('SDK connectivity:', res)); diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts new file mode 100644 index 000000000000..294e60b34bfd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts @@ -0,0 +1,49 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; + +sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE as string | undefined; + if (shouldSkipTracingTest() || !!bundle) { + // the CDN bundle doesn't export diagnoseSdkConnectivity. So skipping the test for bundles. + sentryTest.skip(); + } + + const pageloadRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload'); + + // mock sdk connectivity url to avoid making actual request to sentry.io + page.route('**/api/4509632503087104/envelope/**/*', route => { + return route.fulfill({ + status: 200, + body: '{}', + }); + }); + + const diagnoseMessagePromise = new Promise(resolve => { + page.on('console', msg => { + if (msg.text().includes('SDK connectivity:')) { + resolve(msg.text()); + } + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const pageLoadEvent = envelopeRequestParser(await pageloadRequestPromise); + + // undefined is expected and means the request was successful + expect(await diagnoseMessagePromise).toEqual('SDK connectivity: undefined'); + + // the request to sentry.io should not be traced, hence no http.client span should be sent. + const httpClientSpans = pageLoadEvent.spans?.filter(s => s.op === 'http.client'); + expect(httpClientSpans).toHaveLength(0); + + // no fetch breadcrumb should be sent (only breadcrumb for the console log) + expect(pageLoadEvent.breadcrumbs).toEqual([ + expect.objectContaining({ + category: 'console', + message: 'SDK connectivity: undefined', + }), + ]); +}); diff --git a/packages/browser/src/diagnose-sdk.ts b/packages/browser/src/diagnose-sdk.ts index a8b433856f01..0ad4bef69d6c 100644 --- a/packages/browser/src/diagnose-sdk.ts +++ b/packages/browser/src/diagnose-sdk.ts @@ -1,4 +1,4 @@ -import { getClient } from '@sentry/core'; +import { getClient, suppressTracing } from '@sentry/core'; /** * A function to diagnose why the SDK might not be successfully sending data. @@ -23,20 +23,22 @@ export async function diagnoseSdkConnectivity(): Promise< } try { - // If fetch throws, there is likely an ad blocker active or there are other connective issues. - await fetch( - // We are using the - // - "sentry-sdks" org with id 447951 not to pollute any actual organizations. - // - "diagnose-sdk-connectivity" project with id 4509632503087104 - // - the public key of said org/project, which is disabled in the project settings - // => this DSN: https://c1dfb07d783ad5325c245c1fd3725390@o447951.ingest.us.sentry.io/4509632503087104 (i.e. disabled) - 'https://o447951.ingest.sentry.io/api/4509632503087104/envelope/?sentry_version=7&sentry_key=c1dfb07d783ad5325c245c1fd3725390&sentry_client=sentry.javascript.browser%2F1.33.7', - { - body: '{}', - method: 'POST', - mode: 'cors', - credentials: 'omit', - }, + await suppressTracing(() => + // If fetch throws, there is likely an ad blocker active or there are other connective issues. + fetch( + // We are using the + // - "sentry-sdks" org with id 447951 not to pollute any actual organizations. + // - "diagnose-sdk-connectivity" project with id 4509632503087104 + // - the public key of said org/project, which is disabled in the project settings + // => this DSN: https://c1dfb07d783ad5325c245c1fd3725390@o447951.ingest.us.sentry.io/4509632503087104 (i.e. disabled) + 'https://o447951.ingest.sentry.io/api/4509632503087104/envelope/?sentry_version=7&sentry_key=c1dfb07d783ad5325c245c1fd3725390&sentry_client=sentry.javascript.browser%2F1.33.7', + { + body: '{}', + method: 'POST', + mode: 'cors', + credentials: 'omit', + }, + ), ); } catch { return 'sentry-unreachable'; diff --git a/packages/browser/test/diagnose-sdk.test.ts b/packages/browser/test/diagnose-sdk.test.ts index 36584a97f63b..5bc05dc6cf56 100644 --- a/packages/browser/test/diagnose-sdk.test.ts +++ b/packages/browser/test/diagnose-sdk.test.ts @@ -162,4 +162,18 @@ describe('diagnoseSdkConnectivity', () => { credentials: 'omit', }); }); + + it('calls suppressTracing to avoid tracing the fetch call to sentry', async () => { + const suppressTracingSpy = vi.spyOn(sentryCore, 'suppressTracing'); + + const mockClient: Partial = { + getDsn: vi.fn().mockReturnValue('https://test@example.com/123'), + }; + mockGetClient.mockReturnValue(mockClient); + mockFetch.mockResolvedValue(new Response('{}', { status: 200 })); + + await diagnoseSdkConnectivity(); + + expect(suppressTracingSpy).toHaveBeenCalledTimes(1); + }); }); From 1d59d8abed3ee2ecfbd109a5171626ba782a2d8d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 4 Aug 2025 09:49:41 +0200 Subject: [PATCH 4/5] ref(aws-serverless): Add `type` to captured event `mechanism` (#17248) see #17212 closes #17247 --- packages/aws-serverless/src/sdk.ts | 4 ++-- packages/aws-serverless/src/utils.ts | 4 ++-- packages/aws-serverless/test/sdk.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index f64b62b9a373..95a23ba514d8 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -306,11 +306,11 @@ export function wrapHandler( if (options.captureAllSettledReasons && Array.isArray(rv) && isPromiseAllSettledResult(rv)) { const reasons = getRejectedReasons(rv); reasons.forEach(exception => { - captureException(exception, scope => markEventUnhandled(scope)); + captureException(exception, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.promise')); }); } } catch (e) { - captureException(e, scope => markEventUnhandled(scope)); + captureException(e, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.handler')); throw e; } finally { clearTimeout(timeoutWarningTimer); diff --git a/packages/aws-serverless/src/utils.ts b/packages/aws-serverless/src/utils.ts index f1b0389743cb..f298a2bfec48 100644 --- a/packages/aws-serverless/src/utils.ts +++ b/packages/aws-serverless/src/utils.ts @@ -26,9 +26,9 @@ const headerGetter: TextMapGetter = { /** * Marks an event as unhandled by adding a span processor to the passed scope. */ -export function markEventUnhandled(scope: Scope): Scope { +export function markEventUnhandled(scope: Scope, type: string): Scope { scope.addEventProcessor(event => { - addExceptionMechanism(event, { handled: false }); + addExceptionMechanism(event, { handled: false, type }); return event; }); diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index 9c1e7f584b8d..ed25b69f49ef 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -501,7 +501,7 @@ describe('AWSLambda', () => { // @ts-expect-error just mocking around... expect(evtProcessor(event).exception.values[0]?.mechanism).toEqual({ handled: false, - type: 'generic', + type: 'auto.function.aws-serverless.handler', }); } }); From 1abe1e7b10e4e1bf9e490ed4e21896145cce2722 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 4 Aug 2025 12:19:40 +0200 Subject: [PATCH 5/5] meta(changelog): Update changelog for 10.1.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fcec79fe17d..41853c372711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 10.1.0 + +- feat(nuxt): Align build-time options to follow bundler plugins structure ([#17255](https://github.com/getsentry/sentry-javascript/pull/17255)) +- fix(browser-utils): Ensure web vital client hooks unsubscribe correctly ([#17272](https://github.com/getsentry/sentry-javascript/pull/17272)) +- fix(browser): Ensure request from `diagnoseSdkConnectivity` doesn't create span ([#17280](https://github.com/getsentry/sentry-javascript/pull/17280)) + ## 10.0.0 Version `10.0.0` marks a release of the Sentry JavaScript SDKs that contains breaking changes. The goal of this release is to primarily upgrade the underlying OpenTelemetry dependencies to v2 with minimal breaking changes.