diff --git a/packages/worker/src/sentry-options.node.test.ts b/packages/worker/src/sentry-options.node.test.ts index ff3161c7d5..50ede8df4d 100644 --- a/packages/worker/src/sentry-options.node.test.ts +++ b/packages/worker/src/sentry-options.node.test.ts @@ -1,5 +1,9 @@ import { expect, test } from 'vitest' -import { filterRetryableD1LockSentryEvent } from './sentry-options.ts' +import { + filterRetryableD1LockSentryEvent, + filterSentryEvent, + filterUserModuleBundlerFailureSentryEvent, +} from './sentry-options.ts' test('filterRetryableD1LockSentryEvent drops transient D1 lock contention and keeps other errors', () => { expect( @@ -21,3 +25,66 @@ test('filterRetryableD1LockSentryEvent drops transient D1 lock contention and ke } expect(filterRetryableD1LockSentryEvent(unrelatedEvent)).toBe(unrelatedEvent) }) + +test('filterUserModuleBundlerFailureSentryEvent drops esbuild failures in caller modules', () => { + const userModuleBuildFailure = { + exception: { + values: [ + { + value: + 'Build failed with 1 error:\nvirtual:.__kody_root__/entry.ts:11:49: ERROR: Unexpected "^"', + }, + ], + }, + } + expect( + filterUserModuleBundlerFailureSentryEvent(userModuleBuildFailure), + ).toBeNull() + expect(filterSentryEvent(userModuleBuildFailure)).toBeNull() + + const userModuleMessageFailure = { + message: + 'Build failed with 1 error:\nvirtual:.__kody_root__/entry.ts:11:49: ERROR: Unexpected "^"', + } + expect( + filterUserModuleBundlerFailureSentryEvent(userModuleMessageFailure), + ).toBeNull() + expect(filterSentryEvent(userModuleMessageFailure)).toBeNull() + + const platformBuildFailure = { + exception: { + values: [ + { + value: + 'Build failed with 1 error:\npackages/worker/src/index.ts:1:0: ERROR: Unexpected "{"', + }, + ], + }, + } + expect(filterUserModuleBundlerFailureSentryEvent(platformBuildFailure)).toBe( + platformBuildFailure, + ) + expect(filterSentryEvent(platformBuildFailure)).toBe(platformBuildFailure) + + const unrelated = { + exception: { + values: [{ value: 'Execution timed out' }], + }, + } + expect(filterUserModuleBundlerFailureSentryEvent(unrelated)).toBe(unrelated) + expect(filterSentryEvent(unrelated)).toBe(unrelated) +}) + +test('filterSentryEvent still drops retryable D1 lock contention', () => { + expect( + filterSentryEvent({ + exception: { + values: [ + { + value: 'D1_ERROR: NOSENTRY database is locked: SQLITE_BUSY', + }, + ], + }, + }), + ).toBeNull() +}) diff --git a/packages/worker/src/sentry-options.ts b/packages/worker/src/sentry-options.ts index 3ed79cd68c..cad18018d3 100644 --- a/packages/worker/src/sentry-options.ts +++ b/packages/worker/src/sentry-options.ts @@ -2,6 +2,13 @@ import { type CloudflareOptions } from '@sentry/cloudflare' import { type ErrorEvent } from '@sentry/core' import { isRetryableD1LockSentryEvent } from './d1-retry.ts' +function sentryEventMessages(event: ErrorEvent) { + return [ + event.message, + ...(event.exception?.values?.map((value) => value.value) ?? []), + ] +} + /** * Shared Sentry options for the Cloudflare Worker and Durable Objects. * `dsn` may be undefined when Sentry is not configured (local dev / opt-out). @@ -11,6 +18,33 @@ export function filterRetryableD1LockSentryEvent(event: ErrorEvent) { return null } +/** + * Runtime bundling of caller-supplied modules (MCP execute, inline workflows) + * puts source under `.__kody_root__/`. When that source is invalid, esbuild + * throws `Build failed with … virtual:.__kody_root__/…`. Those are user-module + * mistakes, not platform defects — MCP execute already routes them as sandbox + * errors, but workflow instrumentation still auto-captures the rethrow. + */ +export function isUserModuleBundlerFailureSentryEvent(event: ErrorEvent) { + return sentryEventMessages(event).some( + (message) => + typeof message === 'string' && + message.includes('Build failed with') && + message.includes('.__kody_root__/'), + ) +} + +export function filterUserModuleBundlerFailureSentryEvent(event: ErrorEvent) { + if (!isUserModuleBundlerFailureSentryEvent(event)) return event + return null +} + +export function filterSentryEvent(event: ErrorEvent) { + if (filterRetryableD1LockSentryEvent(event) === null) return null + if (filterUserModuleBundlerFailureSentryEvent(event) === null) return null + return event +} + export function buildSentryOptions(env: Env): CloudflareOptions { const dsn = env.SENTRY_DSN?.trim() const environment = env.SENTRY_ENVIRONMENT?.trim() || 'development' @@ -32,7 +66,10 @@ export function buildSentryOptions(env: Env): CloudflareOptions { // application capture paths (for example scheduled_lane_failed) still // forwarded them. These are transient lock-contention errors retried in // app code and should not open or regress Sentry issues. - beforeSend: filterRetryableD1LockSentryEvent, + // + // User-module esbuild failures from inline workflows are similarly + // expected caller mistakes; see filterUserModuleBundlerFailureSentryEvent. + beforeSend: filterSentryEvent, } }