Skip to content

Commit e168dbd

Browse files
committed
fixup! fix(cloudflare): Fix instrumentDurableObjectWithSentry breaking Cloudflare Agents
1 parent a9c7963 commit e168dbd

3 files changed

Lines changed: 49 additions & 5 deletions

File tree

packages/cloudflare/src/request.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import { addCloudResourceContext, addCultureContext, addRequest } from './scope-
1919
import { init } from './sdk';
2020
import { classifyResponseStreaming } from './utils/streaming';
2121

22+
function getRequestErrorMechanismType(context: ExecutionContext | undefined): string {
23+
// Durable Object fetch handlers use DO state as context (see instrumentDurableObjectWithSentry)
24+
return context && 'storage' in context ? 'auto.faas.cloudflare.durable_object' : 'auto.http.cloudflare';
25+
}
26+
2227
interface RequestHandlerWrapperOptions {
2328
options: CloudflareOptions;
2429
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
@@ -53,6 +58,7 @@ export function wrapRequestHandler(
5358
// it acquires the lock, then flushAndDispose tries to wait for the same lock,
5459
// creating a deadlock.
5560
const waitUntil = context ? getOriginalWaitUntil(context)?.bind(context) : undefined;
61+
const errorMechanismType = getRequestErrorMechanismType(context);
5662

5763
const client = init({ ...options, ctx: context });
5864
isolationScope.setClient(client);
@@ -96,7 +102,7 @@ export function wrapRequestHandler(
96102
return await handler();
97103
} catch (e) {
98104
if (captureErrors) {
99-
captureException(e, { mechanism: { handled: false, type: 'auto.http.cloudflare' } });
105+
captureException(e, { mechanism: { handled: false, type: errorMechanismType } });
100106
}
101107
throw e;
102108
} finally {
@@ -129,7 +135,7 @@ export function wrapRequestHandler(
129135
} catch (e) {
130136
span.end();
131137
if (captureErrors) {
132-
captureException(e, { mechanism: { handled: false, type: 'auto.http.cloudflare' } });
138+
captureException(e, { mechanism: { handled: false, type: errorMechanismType } });
133139
}
134140
waitUntil?.(flushAndDispose(client));
135141
throw e;

packages/cloudflare/src/wrapMethodWithSentry.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DurableObjectStorage } from '@cloudflare/workers-types';
2-
import type { SerializedTraceData } from '@sentry/core';
2+
import type { SerializedTraceData, Span } from '@sentry/core';
33
import {
44
captureException,
55
continueTrace,
@@ -18,13 +18,40 @@ import { flushAndDispose } from './flush';
1818
import { ensureInstrumented } from './instrument';
1919
import { init } from './sdk';
2020
import { extractRpcMeta } from './utils/rpcMeta';
21-
import { buildSpanLinks, getStoredSpanContext, storeSpanContext } from './utils/traceLinks';
21+
import {
22+
buildSpanLinks,
23+
getStoredSpanContext,
24+
storeSpanContext,
25+
} from './utils/traceLinks';
2226

2327
/** Extended DurableObjectState with originalStorage exposed by instrumentContext */
2428
interface InstrumentedDurableObjectState extends DurableObjectState {
2529
originalStorage?: DurableObjectStorage;
2630
}
2731

32+
/**
33+
* Resolves uninstrumented DO storage for the current invocation.
34+
* Prefer `thisArg.ctx` (the live Durable Object instance) over the context captured at
35+
* construction time to avoid cross-DO I/O errors in the same isolate.
36+
*/
37+
function resolveOriginalStorage(
38+
context: ExecutionContext | InstrumentedDurableObjectState | undefined,
39+
thisArg: unknown,
40+
): DurableObjectStorage | undefined {
41+
if (thisArg && typeof thisArg === 'object' && 'ctx' in thisArg) {
42+
const doCtx = (thisArg as { ctx: InstrumentedDurableObjectState }).ctx;
43+
if (doCtx?.originalStorage) {
44+
return doCtx.originalStorage;
45+
}
46+
}
47+
48+
if (context && 'originalStorage' in context && context.originalStorage) {
49+
return context.originalStorage;
50+
}
51+
52+
return undefined;
53+
}
54+
2855
type MethodWrapperOptions = {
2956
spanName?: string;
3057
spanOp?: string;
@@ -94,7 +121,7 @@ export function wrapMethodWithSentry<T extends OriginalMethod>(
94121
const context: typeof wrapperOptions.context | undefined = wrapperOptions.context;
95122

96123
const waitUntil = context?.waitUntil?.bind?.(context);
97-
const storage = context && 'originalStorage' in context ? context.originalStorage : undefined;
124+
const storage = resolveOriginalStorage(context, thisArg);
98125

99126
let scopeClient = scope.getClient();
100127
// Check if client exists AND is still usable (transport not disposed)

packages/cloudflare/test/traceLinks.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ describe('traceLinks', () => {
8282
expect(mockStorage.kv.put).not.toHaveBeenCalled();
8383
});
8484

85+
it('silently ignores sync read errors', () => {
86+
const mockStorage = createMockStorage();
87+
mockStorage.kv.get = vi.fn().mockImplementation(() => {
88+
throw new Error('Cannot perform I/O on behalf of a different Durable Object');
89+
});
90+
91+
const result = getStoredSpanContext(mockStorage, 'alarm');
92+
93+
expect(result).toBeUndefined();
94+
});
95+
8596
it('silently ignores storage errors', () => {
8697
const mockSpanContext = {
8798
traceId: 'abc123def456789012345678901234ab',

0 commit comments

Comments
 (0)