From 67851ba55df38ecdf60c8f7a3da57893e6acfc58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Tue, 13 Dec 2022 09:13:41 +0100 Subject: [PATCH] fix(tracing): autoSessionTracking not working on the server-side (#466) --- lib/core/hooks.js | 5 +++- lib/core/utils.js | 14 ++++++++++ lib/module.js | 69 ++++++++++++++++++++++++++--------------------- package.json | 2 +- types/sentry.d.ts | 7 +++++ yarn.lock | 8 +++--- 6 files changed, 69 insertions(+), 36 deletions(-) diff --git a/lib/core/hooks.js b/lib/core/hooks.js index 969975ab..6edb5a12 100644 --- a/lib/core/hooks.js +++ b/lib/core/hooks.js @@ -133,10 +133,11 @@ export async function webpackConfigHook (moduleContainer, webpackConfigs, option * * @param {ThisParameterType} moduleContainer * @param {import('../../types/sentry').ResolvedModuleConfiguration} moduleOptions + * @param {import('../../types/sentry').SentryHandlerProxy} sentryHandlerProxy * @param {import('consola').Consola} logger * @return {Promise} */ -export async function initializeServerSentry (moduleContainer, moduleOptions, logger) { +export async function initializeServerSentry (moduleContainer, moduleOptions, sentryHandlerProxy, logger) { if (process.sentry) { return } @@ -154,6 +155,8 @@ export async function initializeServerSentry (moduleContainer, moduleOptions, lo if (canInitialize(moduleOptions)) { Sentry.init(config) + sentryHandlerProxy.errorHandler = Sentry.Handlers.errorHandler() + sentryHandlerProxy.requestHandler = Sentry.Handlers.requestHandler(moduleOptions.requestHandlerConfig) } process.sentry = Sentry diff --git a/lib/core/utils.js b/lib/core/utils.js index ef414fd7..ae646b0f 100644 --- a/lib/core/utils.js +++ b/lib/core/utils.js @@ -37,3 +37,17 @@ export const clientSentryEnabled = options => !options.disabled && !options.disa * @return {boolean} True if node Sentry is enabled. */ export const serverSentryEnabled = options => !options.disabled && !options.disableServerSide + +/** + * @param {(...args: any[]) => any} fn + * @return {(...args: any[]) => any} + */ +export function callOnce (fn) { + let called = false + return function callOnceWrapper () { + if (!called) { + called = true + return fn(arguments) + } + } +} diff --git a/lib/module.js b/lib/module.js index 4b0228d0..01fa656d 100644 --- a/lib/module.js +++ b/lib/module.js @@ -1,8 +1,8 @@ import consola from 'consola' import merge from 'lodash.mergewith' -import { Handlers as SentryHandlers, captureException, withScope } from '@sentry/node' +import { captureException, withScope } from '@sentry/node' import { buildHook, initializeServerSentry, shutdownServerSentry, webpackConfigHook } from './core/hooks' -import { boolToText, canInitialize, clientSentryEnabled, envToBool, serverSentryEnabled } from './core/utils' +import { boolToText, callOnce, canInitialize, clientSentryEnabled, envToBool, serverSentryEnabled } from './core/utils' const logger = consola.withScope('nuxt:sentry') @@ -73,21 +73,6 @@ export default function SentryModule (moduleOptions) { options.publishRelease = merged } - if (serverSentryEnabled(options)) { - // @ts-ignore - this.nuxt.hook('render:setupMiddleware', app => app.use(SentryHandlers.requestHandler(options.requestHandlerConfig))) - // @ts-ignore - this.nuxt.hook('render:errorMiddleware', app => app.use(SentryHandlers.errorHandler())) - // @ts-ignore - this.nuxt.hook('generate:routeFailed', ({ route, errors }) => { - // @ts-ignore - errors.forEach(({ error }) => withScope((scope) => { - scope.setExtra('route', route) - captureException(error) - })) - }) - } - if (canInitialize(options) && (clientSentryEnabled(options) || serverSentryEnabled(options))) { const status = `(client side: ${boolToText(clientSentryEnabled(options))}, server side: ${boolToText(serverSentryEnabled(options))})` logger.success(`Sentry reporting is enabled ${status}`) @@ -105,21 +90,45 @@ export default function SentryModule (moduleOptions) { logger.info(`Sentry reporting is disabled (${why})`) } - this.nuxt.hook('build:before', () => buildHook(this, options, logger)) + this.nuxt.hook('build:before', callOnce(() => buildHook(this, options, logger))) - // This is messy but Nuxt provides many modes that it can be started with like: - // - nuxt dev - // - nuxt build - // - nuxt start - // - nuxt generate - // but it doesn't really provide great way to differentiate those or enough hooks to - // pick from. This should ensure that server Sentry will only be initialized **after** - // the release version has been determined and the options template created but before - // the build is started (if building). - const initHook = this.options._build ? 'build:compile' : 'ready' if (serverSentryEnabled(options)) { - this.nuxt.hook(initHook, () => initializeServerSentry(this, options, logger)) - this.nuxt.hook('generate:done', () => shutdownServerSentry()) + /** + * Proxy that provides a dummy request handler before Sentry is initialized and gets replaced with Sentry's own + * handler after initialization. Otherwise server-side request tracing would not work as it depends on Sentry being + * initialized already during handler creation. + * @type {import('../types/sentry').SentryHandlerProxy} + */ + const sentryHandlerProxy = { + errorHandler: (error, req, res, next) => { next(error) }, + requestHandler: (req, res, next) => { next() }, + } + // @ts-ignore + this.nuxt.hook('render:setupMiddleware', app => app.use((req, res, next) => { sentryHandlerProxy.requestHandler(req, res, next) })) + // @ts-ignore + this.nuxt.hook('render:errorMiddleware', app => app.use((error, req, res, next) => { sentryHandlerProxy.errorHandler(error, req, res, next) })) + // @ts-ignore + this.nuxt.hook('generate:routeFailed', ({ route, errors }) => { + // @ts-ignore + errors.forEach(({ error }) => withScope((scope) => { + scope.setExtra('route', route) + captureException(error) + })) + }) + // This is messy but Nuxt provides many modes that it can be started with like: + // - nuxt dev + // - nuxt build + // - nuxt start + // - nuxt generate + // but it doesn't really provide great way to differentiate those or enough hooks to + // pick from. This should ensure that server Sentry will only be initialized **after** + // the release version has been determined and the options template created but before + // the build is started (if building). + const initHook = this.options._build ? 'build:compile' : 'ready' + this.nuxt.hook(initHook, callOnce(() => initializeServerSentry(this, options, sentryHandlerProxy, logger))) + const shutdownServerSentryOnce = callOnce(() => shutdownServerSentry()) + this.nuxt.hook('generate:done', shutdownServerSentryOnce) + this.nuxt.hook('close', shutdownServerSentryOnce) } // Enable publishing of sourcemaps diff --git a/package.json b/package.json index 62cd1567..dcbd1a12 100755 --- a/package.json +++ b/package.json @@ -80,6 +80,6 @@ "playwright-chromium": "^1.28.1", "release-it": "^15.5.1", "sentry-testkit": "^4.1.0", - "typescript": "^4.9.4" + "typescript": "4.8.4" } } diff --git a/types/sentry.d.ts b/types/sentry.d.ts index b3fe585c..d59a4da3 100644 --- a/types/sentry.d.ts +++ b/types/sentry.d.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { IncomingMessage, ServerResponse } from 'http' import { Options as WebpackOptions } from 'webpack' import { BrowserTracing } from '@sentry/tracing' import { Options as SentryOptions } from '@sentry/types' @@ -5,6 +7,11 @@ import { BrowserOptions } from '@sentry/browser' import { SentryCliPluginOptions } from '@sentry/webpack-plugin' import { Handlers } from '@sentry/node' +export interface SentryHandlerProxy { + errorHandler: (error: any, req: IncomingMessage, res: ServerResponse, next: (error: any) => void) => void + requestHandler: (req: IncomingMessage, res: ServerResponse, next: (error?: any) => void) => void +} + export type IntegrationsConfiguration = Record export interface LazyConfiguration { diff --git a/yarn.lock b/yarn.lock index 7969bfd9..9488047e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15075,10 +15075,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@4.8.4: + version "4.8.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" + integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== ua-parser-js@^0.7.28: version "0.7.31"