From 1c0e150db2aa8ba890c1118c13f3a0d6e2ef45a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 26 Jul 2026 14:23:02 +0000 Subject: [PATCH 1/3] fix(sentry): drop bare Cloudflare DO isolate reset noise Exact Durable Object memory/CPU isolate-reset messages are transient platform limit hits (for example successive job_run_now on JobManager). Filter them in beforeSend while keeping wrapped recovery failures visible. Co-authored-by: Kent C. Dodds --- .../worker/src/sentry-options.node.test.ts | 62 +++++++++++++++++++ packages/worker/src/sentry-options.ts | 47 +++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/packages/worker/src/sentry-options.node.test.ts b/packages/worker/src/sentry-options.node.test.ts index 22a61af412..14c4a70f0b 100644 --- a/packages/worker/src/sentry-options.node.test.ts +++ b/packages/worker/src/sentry-options.node.test.ts @@ -1,6 +1,8 @@ import { expect, test } from 'vitest' import { isUserCodeError, UserCodeError } from './user-code-error.ts' import { + durableObjectIsolateCpuResetMessage, + durableObjectIsolateMemoryResetMessage, executorSandboxTimeoutMessage, filterSentryEvent, } from './sentry-options.ts' @@ -183,4 +185,64 @@ test('filterSentryEvent drops expected platform and caller noise and keeps real }), ).toBe(platformEvent) expect(filterSentryEvent(platformEvent)).toBe(platformEvent) + + // Bare Cloudflare DO isolate resets (memory / CPU) are transient platform + // limit hits — drop exact strings, including optional `Error:` prefix and + // missing trailing period. Wrapped recovery failures must stay visible. + expect( + filterSentryEvent({ + exception: { + values: [{ value: durableObjectIsolateMemoryResetMessage }], + }, + }), + ).toBeNull() + expect( + filterSentryEvent({ + exception: { + values: [ + { + value: + "Error: Durable Object's isolate exceeded its memory limit and was reset.", + }, + ], + }, + }), + ).toBeNull() + expect( + filterSentryEvent({ + exception: { + values: [ + { + value: + 'Durable Object exceeded its CPU time limit and was reset', + }, + ], + }, + }), + ).toBeNull() + expect( + filterSentryEvent({ + message: durableObjectIsolateCpuResetMessage, + }), + ).toBeNull() + + const exhaustedPublishRecovery = { + exception: { + values: [ + { + value: `package_publish_external_push could not recover after 3 transient Durable Object reset attempts: ${durableObjectIsolateMemoryResetMessage}`, + }, + ], + }, + } + expect(filterSentryEvent(exhaustedPublishRecovery)).toBe( + exhaustedPublishRecovery, + ) + + const unrelatedDoFailure = { + exception: { + values: [{ value: 'Durable Object was reset during migration' }], + }, + } + expect(filterSentryEvent(unrelatedDoFailure)).toBe(unrelatedDoFailure) }) diff --git a/packages/worker/src/sentry-options.ts b/packages/worker/src/sentry-options.ts index a48a5de6dc..adca4ef7ef 100644 --- a/packages/worker/src/sentry-options.ts +++ b/packages/worker/src/sentry-options.ts @@ -81,6 +81,48 @@ export function filterExecutorSandboxTimeoutSentryEvent(event: ErrorEvent) { return null } +/** + * Exact Cloudflare Durable Object isolate-reset messages. When a DO hits its + * memory or CPU limit, the platform resets the isolate and surfaces this error + * to the RPC caller (for example `job_run_now` → JobManager). The next call + * gets a fresh isolate; app-level retry is unsafe for non-idempotent jobs, and + * moving heavy orchestration off JobManager is an architectural change outside + * a triage fix. Match only the bare platform strings so wrapped failures such + * as exhausted `package_publish_external_push` recovery messages stay visible. + */ +export const durableObjectIsolateMemoryResetMessage = + "Durable Object's isolate exceeded its memory limit and was reset." + +export const durableObjectIsolateCpuResetMessage = + 'Durable Object exceeded its CPU time limit and was reset.' + +function normalizeDurableObjectIsolateResetMessage(message: string) { + const withoutErrorPrefix = message.trim().replace(/^Error:\s*/i, '') + return withoutErrorPrefix.endsWith('.') + ? withoutErrorPrefix + : `${withoutErrorPrefix}.` +} + +export function isDurableObjectIsolateResetMessage(message: string) { + const normalized = normalizeDurableObjectIsolateResetMessage(message) + return ( + normalized === durableObjectIsolateMemoryResetMessage || + normalized === durableObjectIsolateCpuResetMessage + ) +} + +export function isDurableObjectIsolateResetSentryEvent(event: ErrorEvent) { + return sentryEventMessages(event).some( + (message) => + typeof message === 'string' && isDurableObjectIsolateResetMessage(message), + ) +} + +export function filterDurableObjectIsolateResetSentryEvent(event: ErrorEvent) { + if (!isDurableObjectIsolateResetSentryEvent(event)) return event + return null +} + export function filterSentryEvent(event: ErrorEvent, hint?: EventHint) { // Marker first: primary mechanism for user-authored failures. if (filterUserCodeErrorSentryEvent(event, hint) === null) return null @@ -88,6 +130,7 @@ export function filterSentryEvent(event: ErrorEvent, hint?: EventHint) { // String-match backstops for paths that cannot yet be marked. if (filterUserModuleBundlerFailureSentryEvent(event) === null) return null if (filterExecutorSandboxTimeoutSentryEvent(event) === null) return null + if (filterDurableObjectIsolateResetSentryEvent(event) === null) return null return event } @@ -122,7 +165,9 @@ export function buildSentryOptions(env: Env): CloudflareOptions { // (`filterUserCodeErrorSentryEvent`). Bundler / sandbox-timeout string // matches remain as backstops for unmarked paths; see // filterUserModuleBundlerFailureSentryEvent and - // filterExecutorSandboxTimeoutSentryEvent. + // filterExecutorSandboxTimeoutSentryEvent. Bare Cloudflare Durable + // Object isolate memory/CPU reset strings are dropped the same way — + // see filterDurableObjectIsolateResetSentryEvent. beforeSend: filterSentryEvent, } } From f381add3573f7689bb0dbd592ae84ab2ec9f55fa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 26 Jul 2026 14:27:31 +0000 Subject: [PATCH 2/3] style: format sentry DO isolate reset filter Co-authored-by: Kent C. Dodds --- packages/worker/src/sentry-options.node.test.ts | 3 +-- packages/worker/src/sentry-options.ts | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/worker/src/sentry-options.node.test.ts b/packages/worker/src/sentry-options.node.test.ts index 14c4a70f0b..42d49963b2 100644 --- a/packages/worker/src/sentry-options.node.test.ts +++ b/packages/worker/src/sentry-options.node.test.ts @@ -213,8 +213,7 @@ test('filterSentryEvent drops expected platform and caller noise and keeps real exception: { values: [ { - value: - 'Durable Object exceeded its CPU time limit and was reset', + value: 'Durable Object exceeded its CPU time limit and was reset', }, ], }, diff --git a/packages/worker/src/sentry-options.ts b/packages/worker/src/sentry-options.ts index adca4ef7ef..1d92d1b29a 100644 --- a/packages/worker/src/sentry-options.ts +++ b/packages/worker/src/sentry-options.ts @@ -114,7 +114,8 @@ export function isDurableObjectIsolateResetMessage(message: string) { export function isDurableObjectIsolateResetSentryEvent(event: ErrorEvent) { return sentryEventMessages(event).some( (message) => - typeof message === 'string' && isDurableObjectIsolateResetMessage(message), + typeof message === 'string' && + isDurableObjectIsolateResetMessage(message), ) } From 01dc833f84944817e89af2f2385d1f6b966622ba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 26 Jul 2026 14:31:47 +0000 Subject: [PATCH 3/3] fix(sentry): keep chained DO reset recovery failures visible Only drop events where every reported message is a bare Cloudflare isolate reset, so outer recovery wrappers with an inner reset cause still reach Sentry. Co-authored-by: Kent C. Dodds --- .../worker/src/sentry-options.node.test.ts | 21 ++++++++++++++++--- packages/worker/src/sentry-options.ts | 13 ++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/worker/src/sentry-options.node.test.ts b/packages/worker/src/sentry-options.node.test.ts index 42d49963b2..ad39b5b808 100644 --- a/packages/worker/src/sentry-options.node.test.ts +++ b/packages/worker/src/sentry-options.node.test.ts @@ -225,7 +225,7 @@ test('filterSentryEvent drops expected platform and caller noise and keeps real }), ).toBeNull() - const exhaustedPublishRecovery = { + const exhaustedPublishRecoveryInline = { exception: { values: [ { @@ -234,8 +234,23 @@ test('filterSentryEvent drops expected platform and caller noise and keeps real ], }, } - expect(filterSentryEvent(exhaustedPublishRecovery)).toBe( - exhaustedPublishRecovery, + expect(filterSentryEvent(exhaustedPublishRecoveryInline)).toBe( + exhaustedPublishRecoveryInline, + ) + + const exhaustedPublishRecoveryChained = { + exception: { + values: [ + { + value: + 'package_publish_external_push could not recover after 3 transient Durable Object reset attempts', + }, + { value: durableObjectIsolateMemoryResetMessage }, + ], + }, + } + expect(filterSentryEvent(exhaustedPublishRecoveryChained)).toBe( + exhaustedPublishRecoveryChained, ) const unrelatedDoFailure = { diff --git a/packages/worker/src/sentry-options.ts b/packages/worker/src/sentry-options.ts index 1d92d1b29a..327a3b6d9b 100644 --- a/packages/worker/src/sentry-options.ts +++ b/packages/worker/src/sentry-options.ts @@ -112,10 +112,15 @@ export function isDurableObjectIsolateResetMessage(message: string) { } export function isDurableObjectIsolateResetSentryEvent(event: ErrorEvent) { - return sentryEventMessages(event).some( - (message) => - typeof message === 'string' && - isDurableObjectIsolateResetMessage(message), + const messages = sentryEventMessages(event).filter( + (message): message is string => + typeof message === 'string' && message.trim().length > 0, + ) + // Drop only when every reported message is a bare reset. A chained cause + // that pairs a recovery/wrapper value with an inner reset must stay visible. + return ( + messages.length > 0 && + messages.every((message) => isDurableObjectIsolateResetMessage(message)) ) }