From 8bdd32bf5269bafa1ab5cd2b70ac038a7246dc76 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 25 Jul 2024 16:22:12 +0200 Subject: [PATCH 1/4] feat(nuxt): Add vue-router instrumentation --- .../nuxt-3/components/ErrorButton.vue | 11 +++++- .../nuxt-3/pages/client-error.vue | 2 +- .../nuxt-3/pages/test-param/[param].vue | 4 +++ .../nuxt-3/tests/errors.client.test.ts | 30 +++++++++++++++- packages/nuxt/src/client/sdk.ts | 9 ++--- .../nuxt/src/runtime/plugins/sentry.client.ts | 36 +++++++++++++++++-- 6 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/components/ErrorButton.vue b/dev-packages/e2e-tests/test-applications/nuxt-3/components/ErrorButton.vue index 84d8a7ac05ef..42d53ade03f7 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/components/ErrorButton.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/components/ErrorButton.vue @@ -1,6 +1,15 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/client-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/client-error.vue index d4054f7e8bee..25eaa672c87c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/client-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/client-error.vue @@ -3,7 +3,7 @@ import ErrorButton from '../components/ErrorButton.vue'; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue new file mode 100644 index 000000000000..a9bb6177cb15 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue @@ -0,0 +1,4 @@ + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts index cd0ae9051f71..fb03a08b4033 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts @@ -12,6 +12,7 @@ test.describe('client-side errors', async () => { const error = await errorPromise; + expect(error.transaction).toEqual('/client-error'); expect(error).toMatchObject({ exception: { values: [ @@ -25,6 +26,33 @@ test.describe('client-side errors', async () => { ], }, }); - expect(error.transaction).toEqual('/client-error'); + }); + + test('shows parametrized route on button error', async ({ page }) => { + const errorPromise = waitForError('nuxt-3', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button'; + }); + + await page.goto(`/test-param/1234`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.sdk.name).toEqual('sentry.javascript.nuxt'); + expect(error.transaction).toEqual('/test-param/:param()'); + expect(error.request.url).toMatch(/\/test-param\/1234/); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Param Route Button', + mechanism: { + handled: false, + }, + }, + ], + }, + }); }); }); diff --git a/packages/nuxt/src/client/sdk.ts b/packages/nuxt/src/client/sdk.ts index 254498acbcbc..3376e5fdbeb7 100644 --- a/packages/nuxt/src/client/sdk.ts +++ b/packages/nuxt/src/client/sdk.ts @@ -1,8 +1,4 @@ -import { - browserTracingIntegration, - getDefaultIntegrations as getBrowserDefaultIntegrations, - init as initBrowser, -} from '@sentry/browser'; +import { getDefaultIntegrations as getBrowserDefaultIntegrations, init as initBrowser } from '@sentry/browser'; import { applySdkMetadata } from '@sentry/core'; import type { Client } from '@sentry/types'; import type { SentryNuxtOptions } from '../common/types'; @@ -14,7 +10,8 @@ import type { SentryNuxtOptions } from '../common/types'; */ export function init(options: SentryNuxtOptions): Client | undefined { const sentryOptions = { - defaultIntegrations: [...getBrowserDefaultIntegrations(options), browserTracingIntegration()], + /* BrowserTracing is added later with the Nuxt client plugin */ + defaultIntegrations: [...getBrowserDefaultIntegrations(options)], ...options, }; diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index e15cdb31d7c6..a244ae1f4de8 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -1,11 +1,41 @@ import { getClient } from '@sentry/core'; -import { vueIntegration } from '@sentry/vue'; +import { browserTracingIntegration, vueIntegration } from '@sentry/vue'; import { defineNuxtPlugin } from 'nuxt/app'; +// --- Types are copied from @sentry/vue (so it does not need to be exported) --- +// The following type is an intersection of the Route type from VueRouter v2, v3, and v4. +// This is not great, but kinda necessary to make it work with all versions at the same time. +type Route = { + /** Unparameterized URL */ + path: string; + /** + * Query params (keys map to null when there is no value associated, e.g. "?foo" and to an array when there are + * multiple query params that have the same key, e.g. "?foo&foo=bar") + */ + query: Record; + /** Route name (VueRouter provides a way to give routes individual names) */ + name?: string | symbol | null | undefined; + /** Evaluated parameters */ + params: Record; + /** All the matched route objects as defined in VueRouter constructor */ + matched: { path: string }[]; +}; + +interface VueRouter { + onError: (fn: (err: Error) => void) => void; + beforeEach: (fn: (to: Route, from: Route, next?: () => void) => void) => void; +} + export default defineNuxtPlugin(nuxtApp => { - nuxtApp.hook('app:created', vueApp => { - const sentryClient = getClient(); + const sentryClient = getClient(); + if (sentryClient && '$router' in nuxtApp) { + sentryClient.addIntegration( + browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }), + ); + } + + nuxtApp.hook('app:created', vueApp => { if (sentryClient) { sentryClient.addIntegration(vueIntegration({ app: vueApp })); } From c59f3b394370e196a9231d3a36783ef00639d9d0 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 25 Jul 2024 16:46:52 +0200 Subject: [PATCH 2/4] change type of sourcemaps --- packages/nuxt/src/common/types.ts | 134 +++++++++++++++--------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 0187e83170a7..3cedf3c67894 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -5,93 +5,93 @@ export type SentryNuxtOptions = Omit[0] & object, 'app'> type SourceMapsOptions = { /** - * Options for the Sentry Vite plugin to customize the source maps upload process. + * If this flag is `true`, and an auth token is detected, the Sentry integration will + * automatically generate and upload source maps to Sentry during a production build. * - * 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. + * @default true */ - sourceMapsUploadOptions?: { - /** - * If this flag is `true`, and an auth token is detected, the Sentry integration will - * automatically generate and upload source maps to Sentry during a production build. - * - * @default true - */ - enabled?: boolean; + enabled?: boolean; - /** - * The auth token to use when uploading source maps to Sentry. - * - * Instead of specifying this option, you can also set the `SENTRY_AUTH_TOKEN` environment variable. - * - * To create an auth token, follow this guide: - * @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens - */ - authToken?: string; + /** + * The auth token to use when uploading source maps to Sentry. + * + * Instead of specifying this option, you can also set the `SENTRY_AUTH_TOKEN` environment variable. + * + * To create an auth token, follow this guide: + * @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens + */ + authToken?: string; - /** - * The organization slug of your Sentry organization. - * Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable. - */ - org?: string; + /** + * The organization slug of your Sentry organization. + * Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable. + */ + org?: string; + + /** + * The project slug of your Sentry project. + * Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable. + */ + project?: string; + /** + * If this flag is `true`, the Sentry plugin will collect some telemetry data and send it to Sentry. + * It will not collect any sensitive or user-specific data. + * + * @default true + */ + telemetry?: boolean; + + /** + * Options related to sourcemaps + */ + sourcemaps?: { /** - * The project slug of your Sentry project. - * Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable. + * A glob or an array of globs that specify the build artifacts and source maps that will be uploaded to Sentry. + * + * If this option is not specified, sensible defaults based on your adapter and nuxt.config.js + * setup will be used. Use this option to override these defaults, for instance if you have a + * customized build setup that diverges from Nuxt's defaults. + * + * The globbing patterns must follow the implementation of the `glob` package. + * @see https://www.npmjs.com/package/glob#glob-primer */ - project?: string; + assets?: string | Array; /** - * If this flag is `true`, the Sentry plugin will collect some telemetry data and send it to Sentry. - * It will not collect any sensitive or user-specific data. + * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. * - * @default true + * @default [] - By default no files are ignored. Thus, all files matching the `assets` glob + * or the default value for `assets` are uploaded. + * + * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) */ - telemetry?: boolean; + ignore?: string | Array; /** - * Options related to sourcemaps + * A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact + * upload to Sentry has been completed. + * + * @default [] - By default no files are deleted. + * + * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) */ - sourcemaps?: { - /** - * A glob or an array of globs that specify the build artifacts and source maps that will be uploaded to Sentry. - * - * If this option is not specified, sensible defaults based on your adapter and nuxt.config.js - * setup will be used. Use this option to override these defaults, for instance if you have a - * customized build setup that diverges from Nuxt's defaults. - * - * The globbing patterns must follow the implementation of the `glob` package. - * @see https://www.npmjs.com/package/glob#glob-primer - */ - assets?: string | Array; - - /** - * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. - * - * @default [] - By default no files are ignored. Thus, all files matching the `assets` glob - * or the default value for `assets` are uploaded. - * - * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) - */ - ignore?: string | Array; - - /** - * A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact - * upload to Sentry has been completed. - * - * @default [] - By default no files are deleted. - * - * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) - */ - filesToDeleteAfterUpload?: string | Array; - }; + filesToDeleteAfterUpload?: string | Array; }; }; /** * Build options for the Sentry module. These options are used during build-time by the Sentry SDK. */ -export type SentryNuxtModuleOptions = SourceMapsOptions & { +export type SentryNuxtModuleOptions = { + /** + * Options for the Sentry Vite plugin to customize the source maps upload process. + * + * 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. + */ + sourceMapsUploadOptions?: SourceMapsOptions; + /** * Enable debug functionality of the SDK during build-time. * Enabling this will give you, for example, logs about source maps. From 37d0159489250e57f56a90e97144fad542715ed5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 25 Jul 2024 17:25:40 +0200 Subject: [PATCH 3/4] delete test (not needed anymore) --- packages/nuxt/test/client/sdk.test.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/nuxt/test/client/sdk.test.ts b/packages/nuxt/test/client/sdk.test.ts index 83182bfc1c19..1a79cfe445a7 100644 --- a/packages/nuxt/test/client/sdk.test.ts +++ b/packages/nuxt/test/client/sdk.test.ts @@ -1,5 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { type BrowserClient, SDK_VERSION, getClient } from '@sentry/vue'; +import { SDK_VERSION } from '@sentry/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/client'; @@ -35,23 +35,6 @@ describe('Nuxt Client SDK', () => { expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); }); - describe('Automatically adds BrowserTracing integration', () => { - it.each([ - ['tracesSampleRate', { tracesSampleRate: 0 }], - ['tracesSampler', { tracesSampler: () => 1.0 }], - ['enableTracing', { enableTracing: true }], - ['no tracing option set', {}] /* enable "tracing without performance" by default */, - ])('adds a browserTracingIntegration if tracing is enabled via %s', (_, tracingOptions) => { - init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - ...tracingOptions, - }); - - const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); - expect(browserTracing).toBeDefined(); - }); - }); - it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); From 0641a017cbc27afc1b13f0f9fe6ee86148957a0f Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 26 Jul 2024 09:05:16 +0200 Subject: [PATCH 4/4] review comments --- packages/nuxt/src/common/types.ts | 2 +- .../nuxt/src/runtime/plugins/sentry.client.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 3cedf3c67894..2a42046b784a 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -5,7 +5,7 @@ export type SentryNuxtOptions = Omit[0] & object, 'app'> type SourceMapsOptions = { /** - * If this flag is `true`, and an auth token is detected, the Sentry integration will + * If this flag is `true`, and an auth token is detected, the Sentry SDK will * automatically generate and upload source maps to Sentry during a production build. * * @default true diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index a244ae1f4de8..a6bcd0115528 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -26,16 +26,25 @@ interface VueRouter { beforeEach: (fn: (to: Route, from: Route, next?: () => void) => void) => void; } +// Tree-shakable guard to remove all code related to tracing +declare const __SENTRY_TRACING__: boolean; + export default defineNuxtPlugin(nuxtApp => { - const sentryClient = getClient(); + // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside + // will get tree-shaken away + if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { + const sentryClient = getClient(); - if (sentryClient && '$router' in nuxtApp) { - sentryClient.addIntegration( - browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }), - ); + if (sentryClient && '$router' in nuxtApp) { + sentryClient.addIntegration( + browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }), + ); + } } nuxtApp.hook('app:created', vueApp => { + const sentryClient = getClient(); + if (sentryClient) { sentryClient.addIntegration(vueIntegration({ app: vueApp })); }