From 911e29865ebe582ed3fde7b082ba9e7ca7180640 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Wed, 5 Mar 2025 16:29:21 +0100 Subject: [PATCH 01/20] feat(nuxt): Add Cloudflare Nitro plugin --- packages/nuxt/package.json | 5 ++ .../src/runtime/hooks/captureErrorHook.ts | 46 ++++++++++ packages/nuxt/src/runtime/plugins/index.ts | 2 + .../plugins/sentry-cloudflare.server.ts | 70 ++++++++++++++++ .../nuxt/src/runtime/plugins/sentry.server.ts | 83 ++----------------- packages/nuxt/src/runtime/utils.ts | 34 ++++++++ 6 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 packages/nuxt/src/runtime/hooks/captureErrorHook.ts create mode 100644 packages/nuxt/src/runtime/plugins/index.ts create mode 100644 packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index d9dbd9a531b6..b8040e1e7f0a 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -33,6 +33,10 @@ "types": "./build/module/types.d.ts", "import": "./build/module/module.mjs", "require": "./build/module/module.cjs" + }, + "./module/plugins": { + "types": "./build/module/runtime/plugins/index.d.ts", + "import": "./build/module/runtime/plugins/index.js" } }, "publishConfig": { @@ -45,6 +49,7 @@ "@nuxt/kit": "^3.13.2", "@sentry/browser": "9.30.0", "@sentry/core": "9.30.0", + "@sentry/cloudflare": "9.26.0", "@sentry/node": "9.30.0", "@sentry/rollup-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0", diff --git a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts new file mode 100644 index 000000000000..4a588ec58a45 --- /dev/null +++ b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts @@ -0,0 +1,46 @@ +import * as SentryNode from '@sentry/node'; +import { H3Error } from 'h3'; +import { extractErrorContext, flushIfServerless } from '../utils'; +import type { CapturedErrorContext } from 'nitropack'; + +/** + * Hook that can be added in a Nitro plugin. It captures an error and sends it to Sentry. + */ +export async function sentryCaptureErrorHook(error: Error, errorContext: CapturedErrorContext): Promise { + const sentryClient = SentryNode.getClient(); + const sentryClientOptions = sentryClient?.getOptions(); + + if ( + sentryClientOptions && + 'enableNitroErrorHandler' in sentryClientOptions && + sentryClientOptions.enableNitroErrorHandler === false + ) { + return; + } + + // Do not handle 404 and 422 + if (error instanceof H3Error) { + // Do not report if status code is 3xx or 4xx + if (error.statusCode >= 300 && error.statusCode < 500) { + return; + } + } + + const { method, path } = { + method: errorContext.event?._method ? errorContext.event._method : '', + path: errorContext.event?._path ? errorContext.event._path : null, + }; + + if (path) { + SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); + } + + const structuredContext = extractErrorContext(errorContext); + + SentryNode.captureException(error, { + captureContext: { contexts: { nuxt: structuredContext } }, + mechanism: { handled: false }, + }); + + await flushIfServerless(); +} diff --git a/packages/nuxt/src/runtime/plugins/index.ts b/packages/nuxt/src/runtime/plugins/index.ts new file mode 100644 index 000000000000..5c04178922b3 --- /dev/null +++ b/packages/nuxt/src/runtime/plugins/index.ts @@ -0,0 +1,2 @@ +// fixme: Can this be exported like this? +export { cloudflareNitroPlugin } from './sentry-cloudflare.server'; diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts new file mode 100644 index 000000000000..5f62c1473f1b --- /dev/null +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -0,0 +1,70 @@ +import { wrapRequestHandler, setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; +import type { NitroApp, NitroAppPlugin } from 'nitropack'; +import type { CloudflareOptions } from '@sentry/cloudflare'; +import type { ExecutionContext } from '@cloudflare/workers-types'; +import type { NuxtRenderHTMLContext } from 'nuxt/app'; +import { addSentryTracingMetaTags } from '../utils'; +import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; + +interface CfEventType { + protocol: string; + host: string; + context: { + cloudflare: { + context: ExecutionContext; + }; + }; +} + +function isEventType(event: unknown): event is CfEventType { + return ( + event !== null && + typeof event === 'object' && + 'protocol' in event && + 'host' in event && + 'context' in event && + typeof event.protocol === 'string' && + typeof event.host === 'string' && + typeof event.context === 'object' && + event?.context !== null && + 'cloudflare' in event.context && + typeof event.context.cloudflare === 'object' && + event?.context.cloudflare !== null && + 'context' in event?.context?.cloudflare + ); +} + +export const cloudflareNitroPlugin = + (sentryOptions: CloudflareOptions): NitroAppPlugin => + (nitroApp: NitroApp): void => { + nitroApp.localFetch = new Proxy(nitroApp.localFetch, { + async apply(handlerTarget, handlerThisArg, handlerArgs: [string, unknown]) { + // fixme: is this the correct spot? + setAsyncLocalStorageAsyncContextStrategy(); + + const pathname = handlerArgs[0]; + const event = handlerArgs[1]; + + if (isEventType(event)) { + const requestHandlerOptions = { + options: sentryOptions, + request: { ...event, url: `${event.protocol}//${event.host}${pathname}` }, + context: event.context.cloudflare.context, + }; + + // todo: wrap in isolation scope (like regular handler) + return wrapRequestHandler(requestHandlerOptions, () => handlerTarget.apply(handlerThisArg, handlerArgs)); + } + + return handlerTarget.apply(handlerThisArg, handlerArgs); + }, + }); + + // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context + nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => { + // fixme: it's attaching the html meta tag but it's not connecting the trace + addSentryTracingMetaTags(html.head); + }); + + nitroApp.hooks.hook('error', sentryCaptureErrorHook); + }; diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index a785e8452fac..3c55dc5c971c 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,61 +1,16 @@ -import { - flush, - getDefaultIsolationScope, - getIsolationScope, - GLOBAL_OBJ, - logger, - vercelWaitUntil, - withIsolationScope, -} from '@sentry/core'; -import * as SentryNode from '@sentry/node'; +import { getDefaultIsolationScope, getIsolationScope, logger, withIsolationScope } from '@sentry/core'; // eslint-disable-next-line import/no-extraneous-dependencies -import { type EventHandler, H3Error } from 'h3'; +import { type EventHandler } from 'h3'; // eslint-disable-next-line import/no-extraneous-dependencies import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; -import { addSentryTracingMetaTags, extractErrorContext } from '../utils'; +import { addSentryTracingMetaTags, flushIfServerless } from '../utils'; +import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); - nitroApp.hooks.hook('error', async (error, errorContext) => { - const sentryClient = SentryNode.getClient(); - const sentryClientOptions = sentryClient?.getOptions(); - - if ( - sentryClientOptions && - 'enableNitroErrorHandler' in sentryClientOptions && - sentryClientOptions.enableNitroErrorHandler === false - ) { - return; - } - - // Do not handle 404 and 422 - if (error instanceof H3Error) { - // Do not report if status code is 3xx or 4xx - if (error.statusCode >= 300 && error.statusCode < 500) { - return; - } - } - - const { method, path } = { - method: errorContext.event?._method ? errorContext.event._method : '', - path: errorContext.event?._path ? errorContext.event._path : null, - }; - - if (path) { - SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); - } - - const structuredContext = extractErrorContext(errorContext); - - SentryNode.captureException(error, { - captureContext: { contexts: { nuxt: structuredContext } }, - mechanism: { handled: false }, - }); - - await flushIfServerless(); - }); + nitroApp.hooks.hook('error', sentryCaptureErrorHook); // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => { @@ -63,34 +18,6 @@ export default defineNitroPlugin(nitroApp => { }); }); -async function flushIfServerless(): Promise { - const isServerless = - !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions - !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda - !!process.env.VERCEL || - !!process.env.NETLIFY; - - // @ts-expect-error This is not typed - if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { - vercelWaitUntil(flushWithTimeout()); - } else if (isServerless) { - await flushWithTimeout(); - } -} - -async function flushWithTimeout(): Promise { - const sentryClient = SentryNode.getClient(); - const isDebug = sentryClient ? sentryClient.getOptions().debug : false; - - try { - isDebug && logger.log('Flushing events...'); - await flush(2000); - isDebug && logger.log('Done flushing events'); - } catch (e) { - isDebug && logger.log('Error while flushing events:\n', e); - } -} - function patchEventHandler(handler: EventHandler): EventHandler { return new Proxy(handler, { async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index 07b4dccdffd9..636618cd7460 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -1,9 +1,11 @@ import type { ClientOptions, Context } from '@sentry/core'; +import { flush, GLOBAL_OBJ, logger, vercelWaitUntil } from '@sentry/core'; import { captureException, getClient, getTraceMetaTags, logger } from '@sentry/core'; import type { VueOptions } from '@sentry/vue/src/types'; import type { CapturedErrorContext } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import type { ComponentPublicInstance } from 'vue'; +import * as SentryNode from '@sentry/node'; /** * Extracts the relevant context information from the error context (H3Event in Nitro Error) @@ -78,3 +80,35 @@ export function reportNuxtError(options: { }); }); } + +async function flushWithTimeout(): Promise { + const sentryClient = SentryNode.getClient(); + const isDebug = sentryClient ? sentryClient.getOptions().debug : false; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +} + +/** + * Flushes if in a serverless environment + */ +export async function flushIfServerless(): Promise { + const isServerless = + !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions + !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda + !!process.env.CF_PAGES || // Cloudflare + !!process.env.VERCEL || + !!process.env.NETLIFY; + + // @ts-expect-error This is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + vercelWaitUntil(flushWithTimeout()); + } else if (isServerless) { + await flushWithTimeout(); + } +} From 06f7a7c4ab7dae9c6327fde23d12572a7ecfd96e Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 5 Jun 2025 11:01:12 +0200 Subject: [PATCH 02/20] sort imports --- packages/nuxt/src/runtime/hooks/captureErrorHook.ts | 3 ++- .../src/runtime/plugins/sentry-cloudflare.server.ts | 8 ++++---- packages/nuxt/src/runtime/plugins/sentry.server.ts | 2 +- packages/nuxt/src/runtime/utils.ts | 13 ++++++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts index 4a588ec58a45..f96ad225f810 100644 --- a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts +++ b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts @@ -1,7 +1,8 @@ import * as SentryNode from '@sentry/node'; +// eslint-disable-next-line import/no-extraneous-dependencies import { H3Error } from 'h3'; -import { extractErrorContext, flushIfServerless } from '../utils'; import type { CapturedErrorContext } from 'nitropack'; +import { extractErrorContext, flushIfServerless } from '../utils'; /** * Hook that can be added in a Nitro plugin. It captures an error and sends it to Sentry. diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 5f62c1473f1b..236b20244451 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,10 +1,10 @@ -import { wrapRequestHandler, setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; -import type { NitroApp, NitroAppPlugin } from 'nitropack'; -import type { CloudflareOptions } from '@sentry/cloudflare'; import type { ExecutionContext } from '@cloudflare/workers-types'; +import type { CloudflareOptions } from '@sentry/cloudflare'; +import { setAsyncLocalStorageAsyncContextStrategy, wrapRequestHandler } from '@sentry/cloudflare'; +import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; -import { addSentryTracingMetaTags } from '../utils'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; +import { addSentryTracingMetaTags } from '../utils'; interface CfEventType { protocol: string; diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 3c55dc5c971c..baf9f2029051 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -4,8 +4,8 @@ import { type EventHandler } from 'h3'; // eslint-disable-next-line import/no-extraneous-dependencies import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; -import { addSentryTracingMetaTags, flushIfServerless } from '../utils'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; +import { addSentryTracingMetaTags, flushIfServerless } from '../utils'; export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index 636618cd7460..f91ff8ea9342 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -1,11 +1,18 @@ import type { ClientOptions, Context } from '@sentry/core'; -import { flush, GLOBAL_OBJ, logger, vercelWaitUntil } from '@sentry/core'; -import { captureException, getClient, getTraceMetaTags, logger } from '@sentry/core'; +import { + captureException, + flush, + getClient, + getTraceMetaTags, + GLOBAL_OBJ, + logger, + vercelWaitUntil, +} from '@sentry/core'; +import * as SentryNode from '@sentry/node'; import type { VueOptions } from '@sentry/vue/src/types'; import type { CapturedErrorContext } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import type { ComponentPublicInstance } from 'vue'; -import * as SentryNode from '@sentry/node'; /** * Extracts the relevant context information from the error context (H3Event in Nitro Error) From d5b7d811ad866499327c35921f54e320c08d3df5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 5 Jun 2025 14:47:17 +0200 Subject: [PATCH 03/20] import from core, not node --- packages/nuxt/src/runtime/hooks/captureErrorHook.ts | 8 ++++---- packages/nuxt/src/runtime/utils.ts | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts index f96ad225f810..3b2e82ee6044 100644 --- a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts +++ b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts @@ -1,4 +1,4 @@ -import * as SentryNode from '@sentry/node'; +import { captureException, getClient, getCurrentScope } from '@sentry/core'; // eslint-disable-next-line import/no-extraneous-dependencies import { H3Error } from 'h3'; import type { CapturedErrorContext } from 'nitropack'; @@ -8,7 +8,7 @@ import { extractErrorContext, flushIfServerless } from '../utils'; * Hook that can be added in a Nitro plugin. It captures an error and sends it to Sentry. */ export async function sentryCaptureErrorHook(error: Error, errorContext: CapturedErrorContext): Promise { - const sentryClient = SentryNode.getClient(); + const sentryClient = getClient(); const sentryClientOptions = sentryClient?.getOptions(); if ( @@ -33,12 +33,12 @@ export async function sentryCaptureErrorHook(error: Error, errorContext: Capture }; if (path) { - SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); + getCurrentScope().setTransactionName(`${method} ${path}`); } const structuredContext = extractErrorContext(errorContext); - SentryNode.captureException(error, { + captureException(error, { captureContext: { contexts: { nuxt: structuredContext } }, mechanism: { handled: false }, }); diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index f91ff8ea9342..d2974def2165 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -8,7 +8,6 @@ import { logger, vercelWaitUntil, } from '@sentry/core'; -import * as SentryNode from '@sentry/node'; import type { VueOptions } from '@sentry/vue/src/types'; import type { CapturedErrorContext } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -89,7 +88,7 @@ export function reportNuxtError(options: { } async function flushWithTimeout(): Promise { - const sentryClient = SentryNode.getClient(); + const sentryClient = getClient(); const isDebug = sentryClient ? sentryClient.getOptions().debug : false; try { From f11be16ea59b7e95930efb5fb549f8beaeab6926 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 5 Jun 2025 17:27:57 +0200 Subject: [PATCH 04/20] better cloudflare plugin --- packages/nuxt/src/runtime/plugins/index.ts | 2 +- .../plugins/sentry-cloudflare.server.ts | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/index.ts b/packages/nuxt/src/runtime/plugins/index.ts index 5c04178922b3..dbe41b848a0c 100644 --- a/packages/nuxt/src/runtime/plugins/index.ts +++ b/packages/nuxt/src/runtime/plugins/index.ts @@ -1,2 +1,2 @@ // fixme: Can this be exported like this? -export { cloudflareNitroPlugin } from './sentry-cloudflare.server'; +export { sentryCloudflareNitroPlugin } from './sentry-cloudflare.server'; diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 236b20244451..409fe65422ec 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,6 +1,7 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import type { CloudflareOptions } from '@sentry/cloudflare'; import { setAsyncLocalStorageAsyncContextStrategy, wrapRequestHandler } from '@sentry/cloudflare'; +import { getDefaultIsolationScope, getIsolationScope, logger } from '@sentry/core'; import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; @@ -34,14 +35,47 @@ function isEventType(event: unknown): event is CfEventType { ); } -export const cloudflareNitroPlugin = - (sentryOptions: CloudflareOptions): NitroAppPlugin => +/** + * Sentry Cloudflare Nitro plugin for when using the "cloudflare-pages" preset in Nuxt. + * This plugin automatically sets up Sentry error monitoring and performance tracking for Cloudflare Pages projects. + * + * Instead of adding a `sentry.server.config.ts` file, export this plugin in the `server/plugins` directory + * with the necessary Sentry options to enable Sentry for your Cloudflare Pages project. + * + * + * @example Basic usage + * ```ts + * // nitro/plugins/sentry.ts + * import { defineNitroPlugin } from '#imports' + * import { sentryCloudflareNitroPlugin } from '@sentry/nuxt/module/plugins' + * + * export default defineNitroPlugin(sentryCloudflareNitroPlugin({ + * dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', + * tracesSampleRate: 1.0, + * })); + * ``` + * + * @example Dynamic configuration with nitroApp + * ```ts + * // nitro/plugins/sentry.ts + * import { defineNitroPlugin } from '#imports' + * import { sentryCloudflareNitroPlugin } from '@sentry/nuxt/module/plugins' + * + * export default defineNitroPlugin(sentryCloudflareNitroPlugin(nitroApp => ({ + * dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', + * debug: nitroApp.h3App.options.debug + * }))); + * ``` + */ +export const sentryCloudflareNitroPlugin = + (optionsOrFn: CloudflareOptions | ((nitroApp: NitroApp) => CloudflareOptions)): NitroAppPlugin => (nitroApp: NitroApp): void => { nitroApp.localFetch = new Proxy(nitroApp.localFetch, { async apply(handlerTarget, handlerThisArg, handlerArgs: [string, unknown]) { - // fixme: is this the correct spot? setAsyncLocalStorageAsyncContextStrategy(); + const sentryOptions = typeof optionsOrFn === 'function' ? optionsOrFn(nitroApp) : optionsOrFn; + const pathname = handlerArgs[0]; const event = handlerArgs[1]; @@ -52,7 +86,16 @@ export const cloudflareNitroPlugin = context: event.context.cloudflare.context, }; - // todo: wrap in isolation scope (like regular handler) + const isolationScope = getIsolationScope(); + const newIsolationScope = + isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + logger.log( + `Patched Cloudflare handler (\`nitroApp.localFetch\`). ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, + ); + return wrapRequestHandler(requestHandlerOptions, () => handlerTarget.apply(handlerThisArg, handlerArgs)); } @@ -63,6 +106,7 @@ export const cloudflareNitroPlugin = // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => { // fixme: it's attaching the html meta tag but it's not connecting the trace + // fixme: its' actually connecting the trace but the meta tags are cached addSentryTracingMetaTags(html.head); }); From 0800086a7a124b900a0d8870a74eb0c9dd4369d4 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 6 Jun 2025 18:11:08 +0200 Subject: [PATCH 05/20] add some todo comments for trace propagation --- packages/cloudflare/src/request.ts | 39 +++++++++++++ .../plugins/sentry-cloudflare.server.ts | 57 +++++++++++++++---- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index f1905609fb94..519121243cc8 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -3,7 +3,9 @@ import { captureException, continueTrace, flush, + getCurrentScope, getHttpSpanDetailsFromUrlObject, + getTraceData, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, setHttpStatus, @@ -69,6 +71,40 @@ export function wrapRequestHandler( } } + // fixme: at this point, there is no active span + + // Check if we already have active trace data - if so, don't wrap with continueTrace + // This allows us to continue an existing trace from the parent context (e.g., Nuxt) + // todo: create an option for opting out of continueTrace + const existingPropagationContext = getCurrentScope().getPropagationContext(); + if (existingPropagationContext?.traceId) { + return startSpan( + { + name, + attributes, + }, + async span => { + // fixme: same as 2 + console.log('::traceData 2', getTraceData()); + console.log('::propagationContext 2', JSON.stringify(getCurrentScope().getPropagationContext())); + + try { + const res = await handler(); + setHttpStatus(span, res.status); + return res; + } catch (e) { + captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); + throw e; + } finally { + context?.waitUntil(flush(2000)); + } + }, + ); + } + + console.log('request.headers', request.headers); + + // No active trace, create one from headers return continueTrace( { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') }, () => { @@ -81,6 +117,9 @@ export function wrapRequestHandler( attributes, }, async span => { + console.log('::traceData 3', getTraceData()); + console.log('::propagationContext 3', JSON.stringify(getCurrentScope().getPropagationContext())); + try { const res = await handler(); setHttpStatus(span, res.status); diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 409fe65422ec..d53c51b3f79b 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,7 +1,14 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import type { CloudflareOptions } from '@sentry/cloudflare'; -import { setAsyncLocalStorageAsyncContextStrategy, wrapRequestHandler } from '@sentry/cloudflare'; -import { getDefaultIsolationScope, getIsolationScope, logger } from '@sentry/core'; +import { + getActiveSpan, + getTraceData, + setAsyncLocalStorageAsyncContextStrategy, + spanToJSON, + wrapRequestHandler, +} from '@sentry/cloudflare'; +import { continueTrace, getCurrentScope, getDefaultIsolationScope, getIsolationScope, logger } from '@sentry/core'; +import type { H3Event } from 'h3'; import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; @@ -86,27 +93,53 @@ export const sentryCloudflareNitroPlugin = context: event.context.cloudflare.context, }; - const isolationScope = getIsolationScope(); - const newIsolationScope = - isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + // fixme same as 5 + console.log('::traceData 1', getTraceData()); + console.log('::propagationContext 1', JSON.stringify(getCurrentScope().getPropagationContext())); - logger.log( - `Patched Cloudflare handler (\`nitroApp.localFetch\`). ${ - isolationScope === newIsolationScope ? 'Using existing' : 'Created new' - } isolation scope.`, - ); + const traceData = getTraceData(); - return wrapRequestHandler(requestHandlerOptions, () => handlerTarget.apply(handlerThisArg, handlerArgs)); + // return continueTrace({ sentryTrace: traceData['sentry-trace'] || '', baggage: traceData.baggage }, () => { + return wrapRequestHandler(requestHandlerOptions, () => { + const isolationScope = getIsolationScope(); + const newIsolationScope = + isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + logger.log( + `Patched Cloudflare handler (\`nitroApp.localFetch\`). ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, + ); + + console.log('::traceData 4', getTraceData()); + console.log('::propagationContext 4', JSON.stringify(getCurrentScope().getPropagationContext())); + + return handlerTarget.apply(handlerThisArg, handlerArgs); + }); + // }); } return handlerTarget.apply(handlerThisArg, handlerArgs); }, }); + // todo: start span in a hook before the request handler + // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context - nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => { + nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => { // fixme: it's attaching the html meta tag but it's not connecting the trace // fixme: its' actually connecting the trace but the meta tags are cached + console.log('event.headers', event.headers); + console.log('event.node.req.headers.cache-control', event.node.req.headers['cache-control']); + console.log('event.context', event.context); + + const span = getActiveSpan(); + + console.log('::active span', span ? spanToJSON(span) : 'no active span'); + + console.log('::traceData 5', getTraceData()); + console.log('::propagationContext 5', JSON.stringify(getCurrentScope().getPropagationContext())); + addSentryTracingMetaTags(html.head); }); From efd33a4b96cfe26832559f262ab664e4817e9c4d Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 12:42:00 +0200 Subject: [PATCH 06/20] Add continous tracing FR --- packages/cloudflare/src/request.ts | 2 ++ packages/core/src/utils/meta.ts | 6 ++-- packages/core/test/lib/utils/meta.test.ts | 16 ++++++++++ packages/nuxt/package.json | 2 +- .../plugins/sentry-cloudflare.server.ts | 29 +++++++++++++++++-- packages/nuxt/src/runtime/utils.ts | 6 ++-- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 519121243cc8..237a337d3cb9 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -3,12 +3,14 @@ import { captureException, continueTrace, flush, + getActiveSpan, getCurrentScope, getHttpSpanDetailsFromUrlObject, getTraceData, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, setHttpStatus, + spanToJSON, startSpan, withIsolationScope, } from '@sentry/core'; diff --git a/packages/core/src/utils/meta.ts b/packages/core/src/utils/meta.ts index 7db802582eef..3db960bed687 100644 --- a/packages/core/src/utils/meta.ts +++ b/packages/core/src/utils/meta.ts @@ -1,3 +1,4 @@ +import type { SerializedTraceData } from '../types-hoist/tracing'; import { getTraceData } from './traceData'; /** @@ -21,8 +22,9 @@ import { getTraceData } from './traceData'; * ``` * */ -export function getTraceMetaTags(): string { - return Object.entries(getTraceData()) +// todo add test for trace data argument +export function getTraceMetaTags(traceData?: SerializedTraceData): string { + return Object.entries(traceData || getTraceData()) .map(([key, value]) => ``) .join('\n'); } diff --git a/packages/core/test/lib/utils/meta.test.ts b/packages/core/test/lib/utils/meta.test.ts index 19fb68ef0e7d..71cdce3e6eee 100644 --- a/packages/core/test/lib/utils/meta.test.ts +++ b/packages/core/test/lib/utils/meta.test.ts @@ -29,4 +29,20 @@ describe('getTraceMetaTags', () => { expect(getTraceMetaTags()).toBe(''); }); + + it('uses provided traceData instead of calling getTraceData()', () => { + const getTraceDataSpy = vi.spyOn(TraceDataModule, 'getTraceData'); + + const customTraceData = { + 'sentry-trace': 'ab12345678901234567890123456789012-1234567890abcdef-1', + baggage: + 'sentry-environment=test,sentry-public_key=public12345,sentry-trace_id=ab12345678901234567890123456789012,sentry-sample_rate=0.5', + }; + + expect(getTraceMetaTags(customTraceData)) + .toBe(` +`); + + expect(getTraceDataSpy).not.toHaveBeenCalled(); + }); }); diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index b8040e1e7f0a..e108c3a3e64a 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -49,7 +49,7 @@ "@nuxt/kit": "^3.13.2", "@sentry/browser": "9.30.0", "@sentry/core": "9.30.0", - "@sentry/cloudflare": "9.26.0", + "@sentry/cloudflare": "9.30.0", "@sentry/node": "9.30.0", "@sentry/rollup-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0", diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index d53c51b3f79b..947d7028534c 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -2,12 +2,22 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import type { CloudflareOptions } from '@sentry/cloudflare'; import { getActiveSpan, - getTraceData, setAsyncLocalStorageAsyncContextStrategy, spanToJSON, wrapRequestHandler, } from '@sentry/cloudflare'; -import { continueTrace, getCurrentScope, getDefaultIsolationScope, getIsolationScope, logger } from '@sentry/core'; +import { + continueTrace, + getClient, + getCurrentScope, + getDefaultIsolationScope, + getIsolationScope, + getMainCarrier, + getTraceData, + logger, +} from '@sentry/core'; +import { getTraceMetaTags } from '@sentry/core/src'; +import { getAsyncContextStrategy } from '@sentry/core/src/asyncContext'; import type { H3Event } from 'h3'; import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -42,6 +52,8 @@ function isEventType(event: unknown): event is CfEventType { ); } +const TRACE_DATA_KEY = '__sentryTraceData'; + /** * Sentry Cloudflare Nitro plugin for when using the "cloudflare-pages" preset in Nuxt. * This plugin automatically sets up Sentry error monitoring and performance tracking for Cloudflare Pages projects. @@ -98,6 +110,11 @@ export const sentryCloudflareNitroPlugin = console.log('::propagationContext 1', JSON.stringify(getCurrentScope().getPropagationContext())); const traceData = getTraceData(); + if (traceData && Object.keys(traceData).length > 0) { + // Storing trace data in the event context for later use (enables correct connection of parent/child span relationships) + // @ts-expect-error Storing a new key in the event context + event.context[TRACE_DATA_KEY] = traceData; + } // return continueTrace({ sentryTrace: traceData['sentry-trace'] || '', baggage: traceData.baggage }, () => { return wrapRequestHandler(requestHandlerOptions, () => { @@ -140,7 +157,13 @@ export const sentryCloudflareNitroPlugin = console.log('::traceData 5', getTraceData()); console.log('::propagationContext 5', JSON.stringify(getCurrentScope().getPropagationContext())); - addSentryTracingMetaTags(html.head); + const storedTraceData = event.context[TRACE_DATA_KEY] as ReturnType | undefined; + if (storedTraceData && Object.keys(storedTraceData).length > 0) { + logger.log('Using stored trace data from event context for meta tags.'); + addSentryTracingMetaTags(html.head, storedTraceData); + } else { + addSentryTracingMetaTags(html.head); + } }); nitroApp.hooks.hook('error', sentryCaptureErrorHook); diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index d2974def2165..c9c0c1da9a49 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -1,4 +1,4 @@ -import type { ClientOptions, Context } from '@sentry/core'; +import type { ClientOptions, Context, SerializedTraceData } from '@sentry/core'; import { captureException, flush, @@ -41,8 +41,8 @@ export function extractErrorContext(errorContext: CapturedErrorContext | undefin * * Exported only for testing */ -export function addSentryTracingMetaTags(head: NuxtRenderHTMLContext['head']): void { - const metaTags = getTraceMetaTags(); +export function addSentryTracingMetaTags(head: NuxtRenderHTMLContext['head'], traceData?: SerializedTraceData): void { + const metaTags = getTraceMetaTags(traceData); if (metaTags) { logger.log('Adding Sentry tracing meta tags to HTML page:', metaTags); From f99497a42f135aa972737c49ceadf151d9fa6c07 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 13:29:23 +0200 Subject: [PATCH 07/20] enable continuing from propagation context --- packages/cloudflare/src/client.ts | 13 +++- packages/cloudflare/src/request.ts | 77 ++++++------------- .../plugins/sentry-cloudflare.server.ts | 43 +++-------- .../plugins/sentry-cloudflare.server.test.ts | 0 4 files changed, 46 insertions(+), 87 deletions(-) create mode 100644 packages/nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 224865e3731e..6d79924ddb93 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -37,7 +37,18 @@ interface BaseCloudflareOptions {} * * @see @sentry/core Options for more information. */ -export interface CloudflareOptions extends Options, BaseCloudflareOptions {} +export interface CloudflareOptions extends Options, BaseCloudflareOptions { + /** + * Enable or disable the automatic continuation of traces from the propagation context. + * + * When enabled, the SDK will continue a trace from the propagation context if it is present. + * + * When disabled, the SDK will fall back to the default case of continuing a trace from the request headers if they are present. + * + * @default false + */ + continueTraceFromPropagationContext?: boolean; +} /** * Configuration options for the Sentry Cloudflare SDK Client class diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 237a337d3cb9..f9852318df18 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -1,16 +1,14 @@ import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; +import type { Span } from '@sentry/core'; import { captureException, continueTrace, flush, - getActiveSpan, getCurrentScope, getHttpSpanDetailsFromUrlObject, - getTraceData, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, setHttpStatus, - spanToJSON, startSpan, withIsolationScope, } from '@sentry/core'; @@ -73,39 +71,14 @@ export function wrapRequestHandler( } } - // fixme: at this point, there is no active span - // Check if we already have active trace data - if so, don't wrap with continueTrace - // This allows us to continue an existing trace from the parent context (e.g., Nuxt) - // todo: create an option for opting out of continueTrace + // This allows us to continue an existing trace from the parent context (e.g. in Nuxt) + // todo: create a test to check if continueTraceFromPropagationContext works const existingPropagationContext = getCurrentScope().getPropagationContext(); - if (existingPropagationContext?.traceId) { - return startSpan( - { - name, - attributes, - }, - async span => { - // fixme: same as 2 - console.log('::traceData 2', getTraceData()); - console.log('::propagationContext 2', JSON.stringify(getCurrentScope().getPropagationContext())); - - try { - const res = await handler(); - setHttpStatus(span, res.status); - return res; - } catch (e) { - captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); - throw e; - } finally { - context?.waitUntil(flush(2000)); - } - }, - ); + if (options.continueTraceFromPropagationContext && existingPropagationContext?.traceId) { + return startSpan({ name, attributes }, cloudflareStartSpanCallback(handler, context)); } - console.log('request.headers', request.headers); - // No active trace, create one from headers return continueTrace( { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') }, @@ -113,28 +86,26 @@ export function wrapRequestHandler( // Note: This span will not have a duration unless I/O happens in the handler. This is // because of how the cloudflare workers runtime works. // See: https://developers.cloudflare.com/workers/runtime-apis/performance/ - return startSpan( - { - name, - attributes, - }, - async span => { - console.log('::traceData 3', getTraceData()); - console.log('::propagationContext 3', JSON.stringify(getCurrentScope().getPropagationContext())); - - try { - const res = await handler(); - setHttpStatus(span, res.status); - return res; - } catch (e) { - captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); - throw e; - } finally { - context?.waitUntil(flush(2000)); - } - }, - ); + return startSpan({ name, attributes }, cloudflareStartSpanCallback(handler, context)); }, ); }); } + +function cloudflareStartSpanCallback( + handler: (...args: unknown[]) => Response | Promise, + context?: ExecutionContext, +) { + return async (span: Span) => { + try { + const res = await handler(); + setHttpStatus(span, res.status); + return res; + } catch (e) { + captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); + throw e; + } finally { + context?.waitUntil(flush(2000)); + } + }; +} diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 947d7028534c..6559ef24599e 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -94,70 +94,47 @@ export const sentryCloudflareNitroPlugin = setAsyncLocalStorageAsyncContextStrategy(); const sentryOptions = typeof optionsOrFn === 'function' ? optionsOrFn(nitroApp) : optionsOrFn; - const pathname = handlerArgs[0]; const event = handlerArgs[1]; if (isEventType(event)) { const requestHandlerOptions = { - options: sentryOptions, + options: { ...sentryOptions, continueTraceFromPropagationContext: true }, request: { ...event, url: `${event.protocol}//${event.host}${pathname}` }, context: event.context.cloudflare.context, }; - // fixme same as 5 - console.log('::traceData 1', getTraceData()); - console.log('::propagationContext 1', JSON.stringify(getCurrentScope().getPropagationContext())); - - const traceData = getTraceData(); - if (traceData && Object.keys(traceData).length > 0) { - // Storing trace data in the event context for later use (enables correct connection of parent/child span relationships) - // @ts-expect-error Storing a new key in the event context - event.context[TRACE_DATA_KEY] = traceData; - } - - // return continueTrace({ sentryTrace: traceData['sentry-trace'] || '', baggage: traceData.baggage }, () => { return wrapRequestHandler(requestHandlerOptions, () => { const isolationScope = getIsolationScope(); const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + const traceData = getTraceData(); + if (traceData && Object.keys(traceData).length > 0) { + // Storing trace data in the event context for later use (enables correct connection of parent/child span relationships) + // @ts-expect-error Storing a new key in the event context + event.context[TRACE_DATA_KEY] = traceData; + logger.log('Stored trace data in the event context.'); + } + logger.log( `Patched Cloudflare handler (\`nitroApp.localFetch\`). ${ isolationScope === newIsolationScope ? 'Using existing' : 'Created new' } isolation scope.`, ); - console.log('::traceData 4', getTraceData()); - console.log('::propagationContext 4', JSON.stringify(getCurrentScope().getPropagationContext())); - return handlerTarget.apply(handlerThisArg, handlerArgs); }); - // }); } return handlerTarget.apply(handlerThisArg, handlerArgs); }, }); - // todo: start span in a hook before the request handler - // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => { - // fixme: it's attaching the html meta tag but it's not connecting the trace - // fixme: its' actually connecting the trace but the meta tags are cached - console.log('event.headers', event.headers); - console.log('event.node.req.headers.cache-control', event.node.req.headers['cache-control']); - console.log('event.context', event.context); - - const span = getActiveSpan(); - - console.log('::active span', span ? spanToJSON(span) : 'no active span'); - - console.log('::traceData 5', getTraceData()); - console.log('::propagationContext 5', JSON.stringify(getCurrentScope().getPropagationContext())); - const storedTraceData = event.context[TRACE_DATA_KEY] as ReturnType | undefined; + if (storedTraceData && Object.keys(storedTraceData).length > 0) { logger.log('Using stored trace data from event context for meta tags.'); addSentryTracingMetaTags(html.head, storedTraceData); diff --git a/packages/nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts b/packages/nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts new file mode 100644 index 000000000000..e69de29bb2d1 From 983752a9b05943068359a8baf49bf72dff1309e5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 13:37:55 +0200 Subject: [PATCH 08/20] add tests --- packages/cloudflare/src/request.ts | 1 - packages/cloudflare/test/request.test.ts | 72 +++++++++++++++++++ .../plugins/sentry-cloudflare.server.ts | 2 +- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index f9852318df18..2a08b89dfa55 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -73,7 +73,6 @@ export function wrapRequestHandler( // Check if we already have active trace data - if so, don't wrap with continueTrace // This allows us to continue an existing trace from the parent context (e.g. in Nuxt) - // todo: create a test to check if continueTraceFromPropagationContext works const existingPropagationContext = getCurrentScope().getPropagationContext(); if (options.continueTraceFromPropagationContext && existingPropagationContext?.traceId) { return startSpan({ name, attributes }, cloudflareStartSpanCallback(handler, context)); diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 4fc9b308ec54..060828c67b05 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -221,6 +221,78 @@ describe('withSentry', () => { }); }); + test('uses existing propagation context when continueTraceFromPropagationContext is enabled', async () => { + const mockGetCurrentScope = vi.spyOn(SentryCore, 'getCurrentScope').mockReturnValue({ + // return an existing propagation context + getPropagationContext: () => ({ + traceId: '12312012123120121231201212312012', + spanId: '1121201211212012', + }), + } as any); + + const mockStartSpan = vi.spyOn(SentryCore, 'startSpan'); + const mockContinueTrace = vi.spyOn(SentryCore, 'continueTrace'); + + const mockRequest = new Request('https://example.com') as any; + // Headers should be ignored when continuing from propagation context + mockRequest.headers.set('sentry-trace', '99999999999999999999999999999999-9999999999999999-1'); + + await wrapRequestHandler( + { + options: { + ...MOCK_OPTIONS, + continueTraceFromPropagationContext: true, + }, + request: mockRequest, + context: createMockExecutionContext(), + }, + () => new Response('test'), + ); + + // Should use startSpan directly instead of continueTrace + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockContinueTrace).not.toHaveBeenCalled(); + + mockGetCurrentScope.mockRestore(); + mockStartSpan.mockRestore(); + mockContinueTrace.mockRestore(); + }); + + test('falls back to header-based trace when continueTraceFromPropagationContext is disabled or no context exists', async () => { + const mockGetCurrentScope = vi.spyOn(SentryCore, 'getCurrentScope').mockReturnValue({ + // return an existing propagation context + getPropagationContext: () => ({ + traceId: '12312012123120121231201212312012', + spanId: '1121201211212012', + }), + } as any); + + const mockContinueTrace = vi.spyOn(SentryCore, 'continueTrace').mockImplementation((_, callback) => { + return callback(); + }); + + const mockRequest = new Request('https://example.com') as any; + mockRequest.headers.set('sentry-trace', '99999999999999999999999999999999-9999999999999999-1'); + + await wrapRequestHandler( + { + options: { + ...MOCK_OPTIONS, + continueTraceFromPropagationContext: false, // Explicitly disabled + }, + request: mockRequest, + context: createMockExecutionContext(), + }, + () => new Response('test'), + ); + + // Should use continueTrace even with existing propagation context when option is disabled + expect(mockContinueTrace).toHaveBeenCalledTimes(1); + + mockGetCurrentScope.mockRestore(); + mockContinueTrace.mockRestore(); + }); + test('creates a span that wraps request handler', async () => { const mockRequest = new Request('https://example.com') as any; mockRequest.cf = { diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 6559ef24599e..fc34acdfa5d1 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -111,7 +111,7 @@ export const sentryCloudflareNitroPlugin = const traceData = getTraceData(); if (traceData && Object.keys(traceData).length > 0) { - // Storing trace data in the event context for later use (enables correct connection of parent/child span relationships) + // Storing trace data in the event context for later use in HTML meta tags (enables correct connection of parent/child span relationships) // @ts-expect-error Storing a new key in the event context event.context[TRACE_DATA_KEY] = traceData; logger.log('Stored trace data in the event context.'); From 761af2c26ba4965679b198a856cc66d93a0515a8 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 13:45:25 +0200 Subject: [PATCH 09/20] add todo --- .../plugins/sentry-cloudflare.server.ts | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index fc34acdfa5d1..48c103ddae0c 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,23 +1,7 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import type { CloudflareOptions } from '@sentry/cloudflare'; -import { - getActiveSpan, - setAsyncLocalStorageAsyncContextStrategy, - spanToJSON, - wrapRequestHandler, -} from '@sentry/cloudflare'; -import { - continueTrace, - getClient, - getCurrentScope, - getDefaultIsolationScope, - getIsolationScope, - getMainCarrier, - getTraceData, - logger, -} from '@sentry/core'; -import { getTraceMetaTags } from '@sentry/core/src'; -import { getAsyncContextStrategy } from '@sentry/core/src/asyncContext'; +import { setAsyncLocalStorageAsyncContextStrategy, wrapRequestHandler } from '@sentry/cloudflare'; +import { getDefaultIsolationScope, getIsolationScope, getTraceData, logger } from '@sentry/core'; import type { H3Event } from 'h3'; import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -131,6 +115,8 @@ export const sentryCloudflareNitroPlugin = }, }); + // fixme: multiple pageload spans in one trace + // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => { const storedTraceData = event.context[TRACE_DATA_KEY] as ReturnType | undefined; From 55f1bd2940bf198077df6e504953e61193e6326b Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 13:47:57 +0200 Subject: [PATCH 10/20] put option to requesthandler option --- packages/cloudflare/src/client.ts | 13 +------------ packages/cloudflare/src/request.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 6d79924ddb93..224865e3731e 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -37,18 +37,7 @@ interface BaseCloudflareOptions {} * * @see @sentry/core Options for more information. */ -export interface CloudflareOptions extends Options, BaseCloudflareOptions { - /** - * Enable or disable the automatic continuation of traces from the propagation context. - * - * When enabled, the SDK will continue a trace from the propagation context if it is present. - * - * When disabled, the SDK will fall back to the default case of continuing a trace from the request headers if they are present. - * - * @default false - */ - continueTraceFromPropagationContext?: boolean; -} +export interface CloudflareOptions extends Options, BaseCloudflareOptions {} /** * Configuration options for the Sentry Cloudflare SDK Client class diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 2a08b89dfa55..6495e78d7744 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -17,7 +17,18 @@ import { addCloudResourceContext, addCultureContext, addRequest } from './scope- import { init } from './sdk'; interface RequestHandlerWrapperOptions { - options: CloudflareOptions; + options: CloudflareOptions & { + /** + * Enable or disable the automatic continuation of traces from the propagation context. + * + * When enabled, the SDK will continue a trace from the propagation context if it is present. + * + * When disabled, the SDK will fall back to the default case of continuing a trace from the request headers if they are present. + * + * @default false + */ + continueTraceFromPropagationContext?: boolean; + }; request: Request>; context: ExecutionContext; } From 95d162e061781e90e65b5677b607ad1899eeafe5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 13:54:24 +0200 Subject: [PATCH 11/20] switch if statement --- .../nuxt/src/runtime/plugins/sentry-cloudflare.server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 48c103ddae0c..5816d5b92a52 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -81,7 +81,10 @@ export const sentryCloudflareNitroPlugin = const pathname = handlerArgs[0]; const event = handlerArgs[1]; - if (isEventType(event)) { + if (!isEventType(event)) { + logger.log("Nitro Cloudflare plugin did not detect a Cloudflare event type. Won't patch Cloudflare handler."); + return handlerTarget.apply(handlerThisArg, handlerArgs); + } else { const requestHandlerOptions = { options: { ...sentryOptions, continueTraceFromPropagationContext: true }, request: { ...event, url: `${event.protocol}//${event.host}${pathname}` }, @@ -110,8 +113,6 @@ export const sentryCloudflareNitroPlugin = return handlerTarget.apply(handlerThisArg, handlerArgs); }); } - - return handlerTarget.apply(handlerThisArg, handlerArgs); }, }); From 5085e58c3f75d0efb455d43983cd9b363325b768 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 13:58:14 +0200 Subject: [PATCH 12/20] delete stuff --- packages/core/src/utils/meta.ts | 1 - .../nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts | 0 2 files changed, 1 deletion(-) delete mode 100644 packages/nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts diff --git a/packages/core/src/utils/meta.ts b/packages/core/src/utils/meta.ts index 3db960bed687..89bf7514822f 100644 --- a/packages/core/src/utils/meta.ts +++ b/packages/core/src/utils/meta.ts @@ -22,7 +22,6 @@ import { getTraceData } from './traceData'; * ``` * */ -// todo add test for trace data argument export function getTraceMetaTags(traceData?: SerializedTraceData): string { return Object.entries(traceData || getTraceData()) .map(([key, value]) => ``) diff --git a/packages/nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts b/packages/nuxt/test/runtime/plugins/sentry-cloudflare.server.test.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From 3ccab0cf2e009e0a7208c8413c11979df8b2cf78 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 14:37:53 +0200 Subject: [PATCH 13/20] delete continueTraceFromPropagationContext --- packages/cloudflare/src/request.ts | 53 ++++---------- packages/cloudflare/test/request.test.ts | 73 ------------------- .../plugins/sentry-cloudflare.server.ts | 8 +- 3 files changed, 16 insertions(+), 118 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 6495e78d7744..48f69b8d79de 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -4,7 +4,6 @@ import { captureException, continueTrace, flush, - getCurrentScope, getHttpSpanDetailsFromUrlObject, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -17,18 +16,7 @@ import { addCloudResourceContext, addCultureContext, addRequest } from './scope- import { init } from './sdk'; interface RequestHandlerWrapperOptions { - options: CloudflareOptions & { - /** - * Enable or disable the automatic continuation of traces from the propagation context. - * - * When enabled, the SDK will continue a trace from the propagation context if it is present. - * - * When disabled, the SDK will fall back to the default case of continuing a trace from the request headers if they are present. - * - * @default false - */ - continueTraceFromPropagationContext?: boolean; - }; + options: CloudflareOptions; request: Request>; context: ExecutionContext; } @@ -82,40 +70,25 @@ export function wrapRequestHandler( } } - // Check if we already have active trace data - if so, don't wrap with continueTrace - // This allows us to continue an existing trace from the parent context (e.g. in Nuxt) - const existingPropagationContext = getCurrentScope().getPropagationContext(); - if (options.continueTraceFromPropagationContext && existingPropagationContext?.traceId) { - return startSpan({ name, attributes }, cloudflareStartSpanCallback(handler, context)); - } - - // No active trace, create one from headers return continueTrace( { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') }, () => { // Note: This span will not have a duration unless I/O happens in the handler. This is // because of how the cloudflare workers runtime works. // See: https://developers.cloudflare.com/workers/runtime-apis/performance/ - return startSpan({ name, attributes }, cloudflareStartSpanCallback(handler, context)); + return startSpan({ name, attributes }, async (span: Span) => { + try { + const res = await handler(); + setHttpStatus(span, res.status); + return res; + } catch (e) { + captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); + throw e; + } finally { + context?.waitUntil(flush(2000)); + } + }); }, ); }); } - -function cloudflareStartSpanCallback( - handler: (...args: unknown[]) => Response | Promise, - context?: ExecutionContext, -) { - return async (span: Span) => { - try { - const res = await handler(); - setHttpStatus(span, res.status); - return res; - } catch (e) { - captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); - throw e; - } finally { - context?.waitUntil(flush(2000)); - } - }; -} diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 060828c67b05..b1641c2168b3 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -220,79 +220,6 @@ describe('withSentry', () => { trace_id: '12312012123120121231201212312012', }); }); - - test('uses existing propagation context when continueTraceFromPropagationContext is enabled', async () => { - const mockGetCurrentScope = vi.spyOn(SentryCore, 'getCurrentScope').mockReturnValue({ - // return an existing propagation context - getPropagationContext: () => ({ - traceId: '12312012123120121231201212312012', - spanId: '1121201211212012', - }), - } as any); - - const mockStartSpan = vi.spyOn(SentryCore, 'startSpan'); - const mockContinueTrace = vi.spyOn(SentryCore, 'continueTrace'); - - const mockRequest = new Request('https://example.com') as any; - // Headers should be ignored when continuing from propagation context - mockRequest.headers.set('sentry-trace', '99999999999999999999999999999999-9999999999999999-1'); - - await wrapRequestHandler( - { - options: { - ...MOCK_OPTIONS, - continueTraceFromPropagationContext: true, - }, - request: mockRequest, - context: createMockExecutionContext(), - }, - () => new Response('test'), - ); - - // Should use startSpan directly instead of continueTrace - expect(mockStartSpan).toHaveBeenCalledTimes(1); - expect(mockContinueTrace).not.toHaveBeenCalled(); - - mockGetCurrentScope.mockRestore(); - mockStartSpan.mockRestore(); - mockContinueTrace.mockRestore(); - }); - - test('falls back to header-based trace when continueTraceFromPropagationContext is disabled or no context exists', async () => { - const mockGetCurrentScope = vi.spyOn(SentryCore, 'getCurrentScope').mockReturnValue({ - // return an existing propagation context - getPropagationContext: () => ({ - traceId: '12312012123120121231201212312012', - spanId: '1121201211212012', - }), - } as any); - - const mockContinueTrace = vi.spyOn(SentryCore, 'continueTrace').mockImplementation((_, callback) => { - return callback(); - }); - - const mockRequest = new Request('https://example.com') as any; - mockRequest.headers.set('sentry-trace', '99999999999999999999999999999999-9999999999999999-1'); - - await wrapRequestHandler( - { - options: { - ...MOCK_OPTIONS, - continueTraceFromPropagationContext: false, // Explicitly disabled - }, - request: mockRequest, - context: createMockExecutionContext(), - }, - () => new Response('test'), - ); - - // Should use continueTrace even with existing propagation context when option is disabled - expect(mockContinueTrace).toHaveBeenCalledTimes(1); - - mockGetCurrentScope.mockRestore(); - mockContinueTrace.mockRestore(); - }); - test('creates a span that wraps request handler', async () => { const mockRequest = new Request('https://example.com') as any; mockRequest.cf = { diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 5816d5b92a52..8129a160c9e2 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -77,7 +77,7 @@ export const sentryCloudflareNitroPlugin = async apply(handlerTarget, handlerThisArg, handlerArgs: [string, unknown]) { setAsyncLocalStorageAsyncContextStrategy(); - const sentryOptions = typeof optionsOrFn === 'function' ? optionsOrFn(nitroApp) : optionsOrFn; + const cloudflareOptions = typeof optionsOrFn === 'function' ? optionsOrFn(nitroApp) : optionsOrFn; const pathname = handlerArgs[0]; const event = handlerArgs[1]; @@ -86,7 +86,7 @@ export const sentryCloudflareNitroPlugin = return handlerTarget.apply(handlerThisArg, handlerArgs); } else { const requestHandlerOptions = { - options: { ...sentryOptions, continueTraceFromPropagationContext: true }, + options: cloudflareOptions, request: { ...event, url: `${event.protocol}//${event.host}${pathname}` }, context: event.context.cloudflare.context, }; @@ -98,7 +98,7 @@ export const sentryCloudflareNitroPlugin = const traceData = getTraceData(); if (traceData && Object.keys(traceData).length > 0) { - // Storing trace data in the event context for later use in HTML meta tags (enables correct connection of parent/child span relationships) + // Storing trace data in the event context for later use in HTML meta-tags (enables correct connection of parent/child span relationships) // @ts-expect-error Storing a new key in the event context event.context[TRACE_DATA_KEY] = traceData; logger.log('Stored trace data in the event context.'); @@ -116,8 +116,6 @@ export const sentryCloudflareNitroPlugin = }, }); - // fixme: multiple pageload spans in one trace - // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => { const storedTraceData = event.context[TRACE_DATA_KEY] as ReturnType | undefined; From a86f1f095c14168edca5ef54c2345cf95534f8ee Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 26 Jun 2025 14:40:32 +0200 Subject: [PATCH 14/20] reset code in cloudflare SDK --- packages/cloudflare/src/request.ts | 30 ++++++++++++++---------- packages/cloudflare/test/request.test.ts | 1 + 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 48f69b8d79de..9d6f3177ae04 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -76,18 +76,24 @@ export function wrapRequestHandler( // Note: This span will not have a duration unless I/O happens in the handler. This is // because of how the cloudflare workers runtime works. // See: https://developers.cloudflare.com/workers/runtime-apis/performance/ - return startSpan({ name, attributes }, async (span: Span) => { - try { - const res = await handler(); - setHttpStatus(span, res.status); - return res; - } catch (e) { - captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); - throw e; - } finally { - context?.waitUntil(flush(2000)); - } - }); + return startSpan( + { + name, + attributes, + }, + async span => { + try { + const res = await handler(); + setHttpStatus(span, res.status); + return res; + } catch (e) { + captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); + throw e; + } finally { + context?.waitUntil(flush(2000)); + } + }, + ); }, ); }); diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index b1641c2168b3..4fc9b308ec54 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -220,6 +220,7 @@ describe('withSentry', () => { trace_id: '12312012123120121231201212312012', }); }); + test('creates a span that wraps request handler', async () => { const mockRequest = new Request('https://example.com') as any; mockRequest.cf = { From e93abe25469b7a1b11a24f3a785e2177099e2bbd Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 27 Jun 2025 10:20:30 +0200 Subject: [PATCH 15/20] fix type error --- packages/cloudflare/src/request.ts | 1 - .../plugins/sentry-cloudflare.server.ts | 39 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 9d6f3177ae04..f1905609fb94 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -1,5 +1,4 @@ import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; -import type { Span } from '@sentry/core'; import { captureException, continueTrace, diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 8129a160c9e2..e5907486cfcc 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,4 +1,4 @@ -import type { ExecutionContext } from '@cloudflare/workers-types'; +import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; import type { CloudflareOptions } from '@sentry/cloudflare'; import { setAsyncLocalStorageAsyncContextStrategy, wrapRequestHandler } from '@sentry/cloudflare'; import { getDefaultIsolationScope, getIsolationScope, getTraceData, logger } from '@sentry/core'; @@ -11,28 +11,44 @@ import { addSentryTracingMetaTags } from '../utils'; interface CfEventType { protocol: string; host: string; + method: string; + headers: Record; context: { + cf: { + httpProtocol?: string; + country?: string; + // ...other CF properties + }; cloudflare: { context: ExecutionContext; + request?: Record; + env?: Record; }; }; } function isEventType(event: unknown): event is CfEventType { + if (event === null || typeof event !== 'object') return false; + return ( - event !== null && - typeof event === 'object' && + // basic properties 'protocol' in event && 'host' in event && - 'context' in event && typeof event.protocol === 'string' && typeof event.host === 'string' && + // context property + 'context' in event && typeof event.context === 'object' && - event?.context !== null && + event.context !== null && + // context.cf properties + 'cf' in event.context && + typeof event.context.cf === 'object' && + event.context.cf !== null && + // context.cloudflare properties 'cloudflare' in event.context && typeof event.context.cloudflare === 'object' && - event?.context.cloudflare !== null && - 'context' in event?.context?.cloudflare + event.context.cloudflare !== null && + 'context' in event.context.cloudflare ); } @@ -85,9 +101,16 @@ export const sentryCloudflareNitroPlugin = logger.log("Nitro Cloudflare plugin did not detect a Cloudflare event type. Won't patch Cloudflare handler."); return handlerTarget.apply(handlerThisArg, handlerArgs); } else { + const url = `${event.protocol}//${event.host}${pathname}`; + const request = new Request(url, { + method: event.method, + headers: event.headers, + cf: event.context.cf, + }) as Request>; + const requestHandlerOptions = { options: cloudflareOptions, - request: { ...event, url: `${event.protocol}//${event.host}${pathname}` }, + request, context: event.context.cloudflare.context, }; From 5913937e97af516a956e9a91deca6d6aaff1a887 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 27 Jun 2025 11:03:56 +0200 Subject: [PATCH 16/20] delete `isDebug` --- packages/nuxt/src/runtime/utils.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index c9c0c1da9a49..4b6be839279e 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -88,15 +88,12 @@ export function reportNuxtError(options: { } async function flushWithTimeout(): Promise { - const sentryClient = getClient(); - const isDebug = sentryClient ? sentryClient.getOptions().debug : false; - try { - isDebug && logger.log('Flushing events...'); + logger.log('Flushing events...'); await flush(2000); - isDebug && logger.log('Done flushing events'); + logger.log('Done flushing events'); } catch (e) { - isDebug && logger.log('Error while flushing events:\n', e); + logger.log('Error while flushing events:\n', e); } } From db78cd86f6706049429ac2f230e6ed71e4f60ad8 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 27 Jun 2025 11:05:41 +0200 Subject: [PATCH 17/20] check for : in protocol --- packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index e5907486cfcc..0bea5af72ac9 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -101,7 +101,8 @@ export const sentryCloudflareNitroPlugin = logger.log("Nitro Cloudflare plugin did not detect a Cloudflare event type. Won't patch Cloudflare handler."); return handlerTarget.apply(handlerThisArg, handlerArgs); } else { - const url = `${event.protocol}//${event.host}${pathname}`; + // Usually, the protocol already includes ":" + const url = `${event.protocol}${event.protocol.endsWith(':') ? '' : ':'}//${event.host}${pathname}`; const request = new Request(url, { method: event.method, headers: event.headers, From eac96b63431d9595232f2880960137f77d0b06c8 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 27 Jun 2025 14:25:34 +0200 Subject: [PATCH 18/20] update yarn.lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index ac3970cfb153..a44f353e348e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29253,7 +29253,7 @@ vite@^5.0.0, vite@^5.4.11, vite@^5.4.5: optionalDependencies: fsevents "~2.3.3" -vitefu@^0.2.2, vitefu@^0.2.4, vitefu@^0.2.5: +vitefu@^0.2.2, vitefu@^0.2.4: version "0.2.5" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969" integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q== From b9c9cc0ec2001b1ced30c390ea549020e4a88202 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 27 Jun 2025 15:10:45 +0200 Subject: [PATCH 19/20] warn on double-init --- packages/nuxt/src/runtime/utils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index 4b6be839279e..84520ce3f639 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -44,6 +44,13 @@ export function extractErrorContext(errorContext: CapturedErrorContext | undefin export function addSentryTracingMetaTags(head: NuxtRenderHTMLContext['head'], traceData?: SerializedTraceData): void { const metaTags = getTraceMetaTags(traceData); + if (head.some(tag => tag.includes('meta') && tag.includes('sentry-trace'))) { + logger.warn( + 'Skipping addition of meta tags. Sentry tracing meta tags are already present in HTML page. Make sure to only set up Sentry once on the server-side. ', + ); + return; + } + if (metaTags) { logger.log('Adding Sentry tracing meta tags to HTML page:', metaTags); head.push(metaTags); From a20ebef017a36ec307cfd861cb8c3b8a5cfae2ab Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 27 Jun 2025 15:11:33 +0200 Subject: [PATCH 20/20] Add WeakMap --- .../runtime/plugins/sentry-cloudflare.server.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 0bea5af72ac9..9d10e9bd86d0 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -52,8 +52,6 @@ function isEventType(event: unknown): event is CfEventType { ); } -const TRACE_DATA_KEY = '__sentryTraceData'; - /** * Sentry Cloudflare Nitro plugin for when using the "cloudflare-pages" preset in Nuxt. * This plugin automatically sets up Sentry error monitoring and performance tracking for Cloudflare Pages projects. @@ -89,6 +87,8 @@ const TRACE_DATA_KEY = '__sentryTraceData'; export const sentryCloudflareNitroPlugin = (optionsOrFn: CloudflareOptions | ((nitroApp: NitroApp) => CloudflareOptions)): NitroAppPlugin => (nitroApp: NitroApp): void => { + const traceDataMap = new WeakMap>(); + nitroApp.localFetch = new Proxy(nitroApp.localFetch, { async apply(handlerTarget, handlerThisArg, handlerArgs: [string, unknown]) { setAsyncLocalStorageAsyncContextStrategy(); @@ -122,10 +122,9 @@ export const sentryCloudflareNitroPlugin = const traceData = getTraceData(); if (traceData && Object.keys(traceData).length > 0) { - // Storing trace data in the event context for later use in HTML meta-tags (enables correct connection of parent/child span relationships) - // @ts-expect-error Storing a new key in the event context - event.context[TRACE_DATA_KEY] = traceData; - logger.log('Stored trace data in the event context.'); + // Storing trace data in the WeakMap using event.context.cf as key for later use in HTML meta-tags + traceDataMap.set(event.context.cf, traceData); + logger.log('Stored trace data for later use in HTML meta-tags: ', traceData); } logger.log( @@ -142,10 +141,10 @@ export const sentryCloudflareNitroPlugin = // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => { - const storedTraceData = event.context[TRACE_DATA_KEY] as ReturnType | undefined; + const storedTraceData = event?.context?.cf ? traceDataMap.get(event.context.cf) : undefined; if (storedTraceData && Object.keys(storedTraceData).length > 0) { - logger.log('Using stored trace data from event context for meta tags.'); + logger.log('Using stored trace data for HTML meta-tags: ', storedTraceData); addSentryTracingMetaTags(html.head, storedTraceData); } else { addSentryTracingMetaTags(html.head);