Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/nuxt/src/runtime/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
// fixme: Can this be exported like this?
export { sentryCloudflareNitroPlugin } from './sentry-cloudflare.server';
67 changes: 18 additions & 49 deletions packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CloudflareOptions } from '@sentry/cloudflare';
import { setAsyncLocalStorageAsyncContextStrategy, wrapRequestHandler } from '@sentry/cloudflare';
import { debug, getDefaultIsolationScope, getIsolationScope, getTraceData } from '@sentry/core';
Expand All @@ -8,50 +8,7 @@ import type { NuxtRenderHTMLContext } from 'nuxt/app';
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
import { addSentryTracingMetaTags } from '../utils';

interface CfEventType {
protocol: string;
host: string;
method: string;
headers: Record<string, string>;
context: {
cf: {
httpProtocol?: string;
country?: string;
// ...other CF properties
};
cloudflare: {
context: ExecutionContext;
request?: Record<string, unknown>;
env?: Record<string, unknown>;
};
};
}

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
);
}
import { getCfProperties, getCloudflareProperties, hasCfProperty, isEventType } from '../utils/event-type-check';

/**
* Sentry Cloudflare Nitro plugin for when using the "cloudflare-pages" preset in Nuxt.
Expand Down Expand Up @@ -107,13 +64,13 @@ export const sentryCloudflareNitroPlugin =
const request = new Request(url, {
method: event.method,
headers: event.headers,
cf: event.context.cf,
cf: getCfProperties(event),
}) as Request<unknown, IncomingRequestCfProperties<unknown>>;

const requestHandlerOptions = {
options: cloudflareOptions,
request,
context: event.context.cloudflare.context,
context: getCloudflareProperties(event).context,
};

return wrapRequestHandler(requestHandlerOptions, () => {
Expand All @@ -124,7 +81,7 @@ export const sentryCloudflareNitroPlugin =
const traceData = getTraceData();
if (traceData && Object.keys(traceData).length > 0) {
// 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);
traceDataMap.set(getCfProperties(event), traceData);
debug.log('Stored trace data for later use in HTML meta-tags: ', traceData);
}

Expand All @@ -144,7 +101,19 @@ 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?.cf ? traceDataMap.get(event.context.cf) : undefined;
let storedTraceData: ReturnType<typeof getTraceData> | undefined = undefined;

if (
event?.context &&
'_platform' in event.context &&
event.context._platform &&
hasCfProperty(event.context._platform)
) {
storedTraceData = traceDataMap.get(event.context._platform.cf);
} else if (event?.context && hasCfProperty(event.context)) {
// legacy support (before Nitro v2.11.7 (PR: https://github.com/nitrojs/nitro/pull/3224))
storedTraceData = traceDataMap.get(event.context.cf);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Cloudflare Plugin Trace Data Inconsistency

The Cloudflare plugin's trace data storage and retrieval use different priority orders for identifying the cf object as a WeakMap key. This inconsistency can cause trace data lookups to fail, resulting in missing Sentry tracing meta-tags in the HTML. Also, accessing _platform on an H3Event type may lead to TypeScript compilation errors.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is no problem when the same nitro version is used (which is the case on the same server)


if (storedTraceData && Object.keys(storedTraceData).length > 0) {
debug.log('Using stored trace data for HTML meta-tags: ', storedTraceData);
Expand Down
105 changes: 105 additions & 0 deletions packages/nuxt/src/runtime/utils/event-type-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { CfProperties, ExecutionContext } from '@cloudflare/workers-types';

interface EventBase {
protocol: string;
host: string;
method: string;
headers: Record<string, string>;
}

interface MinimalCloudflareProps {
context: ExecutionContext;
request?: Record<string, unknown>;
env?: Record<string, unknown>;
}

interface CloudflareContext {
cf: CfProperties;
cloudflare: MinimalCloudflareProps;
}

// Direct shape: cf and cloudflare are directly on context
interface CfEventDirect extends EventBase {
context: CloudflareContext;
}

// Nested shape: cf and cloudflare are under _platform
// Since Nitro v2.11.7 (PR: https://github.com/nitrojs/nitro/pull/3224)
interface CfEventPlatform extends EventBase {
context: {
_platform: CloudflareContext;
};
}

export type CfEventType = CfEventDirect | CfEventPlatform;

function hasCloudflareProperty(context: unknown): boolean {
return (
context !== null &&
typeof context === 'object' &&
// context.cloudflare properties
'cloudflare' in context &&
typeof context.cloudflare === 'object' &&
context.cloudflare !== null &&
'context' in context.cloudflare
);
}

/**
* Type guard to check if an event context object has cf properties
*/
export function hasCfProperty(context: unknown): context is { cf: CfProperties } {
return (
context !== null &&
typeof context === 'object' &&
// context.cf properties
'cf' in context &&
typeof context.cf === 'object' &&
context.cf !== null
);
}

function hasCfAndCloudflare(context: unknown): context is CloudflareContext {
return hasCfProperty(context) && hasCloudflareProperty(context);
}

/**
* Type guard to check if an event is a Cloudflare event (nested in _platform or direct)
*/
export 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
(hasCfAndCloudflare(event.context) || ('_platform' in event.context && hasCfAndCloudflare(event.context._platform)))
);
}

/**
* Extracts cf properties from a Cloudflare event
*/
export function getCfProperties(event: CfEventType): CfProperties {
if ('cf' in event.context) {
return event.context.cf;
}
return event.context._platform.cf;
}

/**
* Extracts cloudflare properties from a Cloudflare event
*/
export function getCloudflareProperties(event: CfEventType): MinimalCloudflareProps {
if ('cloudflare' in event.context) {
return event.context.cloudflare;
}
return event.context._platform.cloudflare;
}
Loading