From 6006c3a6bbf7e93ee44064f5255f68f967bd860c Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Wed, 5 Mar 2025 16:29:21 +0100 Subject: [PATCH 1/5] 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 932f82a86037..6d94d1a6e0ee 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.26.0", "@sentry/core": "9.26.0", + "@sentry/cloudflare": "9.26.0", "@sentry/node": "9.26.0", "@sentry/opentelemetry": "9.26.0", "@sentry/rollup-plugin": "3.4.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 fc62672f51207f08f80f15d52eb9d61d4bdc9534 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 5 Jun 2025 11:01:12 +0200 Subject: [PATCH 2/5] 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 8aa94a63d5cb253c7a31f37c6628dd1085510f30 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 5 Jun 2025 14:47:17 +0200 Subject: [PATCH 3/5] 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 617fd213ef5602b825bce597b0d3be72a52bc7df Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 5 Jun 2025 17:27:57 +0200 Subject: [PATCH 4/5] 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 243b681c489943420cb3176708ec5535fb7f7eaf Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 6 Jun 2025 18:11:08 +0200 Subject: [PATCH 5/5] 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); });