diff --git a/packages/core/src/utils/meta.ts b/packages/core/src/utils/meta.ts
index 7db802582eef..89bf7514822f 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,8 @@ import { getTraceData } from './traceData';
* ```
*
*/
-export function getTraceMetaTags(): string {
- return Object.entries(getTraceData())
+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 78a0385b7ed5..6fb0ae45465a 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.33.0",
"@sentry/core": "9.33.0",
+ "@sentry/cloudflare": "9.33.0",
"@sentry/node": "9.33.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..3b2e82ee6044
--- /dev/null
+++ b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts
@@ -0,0 +1,47 @@
+import { captureException, getClient, getCurrentScope } from '@sentry/core';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { H3Error } from 'h3';
+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.
+ */
+export async function sentryCaptureErrorHook(error: Error, errorContext: CapturedErrorContext): Promise {
+ const sentryClient = 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) {
+ getCurrentScope().setTransactionName(`${method} ${path}`);
+ }
+
+ const structuredContext = extractErrorContext(errorContext);
+
+ 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..dbe41b848a0c
--- /dev/null
+++ b/packages/nuxt/src/runtime/plugins/index.ts
@@ -0,0 +1,2 @@
+// fixme: Can this be exported like this?
+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
new file mode 100644
index 000000000000..9d10e9bd86d0
--- /dev/null
+++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
@@ -0,0 +1,155 @@
+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';
+import type { H3Event } from 'h3';
+import type { NitroApp, NitroAppPlugin } from 'nitropack';
+import type { NuxtRenderHTMLContext } from 'nuxt/app';
+import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
+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 (
+ // basic properties
+ 'protocol' in event &&
+ 'host' in event &&
+ typeof event.protocol === 'string' &&
+ typeof event.host === 'string' &&
+ // context property
+ 'context' in event &&
+ typeof event.context === 'object' &&
+ 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
+ );
+}
+
+/**
+ * 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 => {
+ const traceDataMap = new WeakMap