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
69 changes: 68 additions & 1 deletion packages/worker/src/sentry-options.node.test.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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()
})
39 changes: 38 additions & 1 deletion packages/worker/src/sentry-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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'
Expand All @@ -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,
}
}

Expand Down
Loading