feat(runner): Sentry error monitoring for the authoring app + API worker (DEV-2134) - #88
Open
demtario wants to merge 6 commits into
Open
feat(runner): Sentry error monitoring for the authoring app + API worker (DEV-2134)#88demtario wants to merge 6 commits into
demtario wants to merge 6 commits into
Conversation
The runner had no error reporting: DEV-2129, DEV-2130 and the Tier-2 slot exhaustion were all found by hand or by the link-sweep script, days after they started failing. Workers Logs gives raw lines with no grouping or alerting. Wraps the fetch handler with withSentry() and both Durable Objects with instrumentDurableObjectWithSentry() — Tier-2 session orchestration and the share-build snapshotter run inside the DOs, so their failures never reach the handler's error path. Release comes from the Cloudflare version_metadata binding, so no deploy-workflow change is needed. The outer catch converts every unexpected throw into a 500 body, so withSentry() would never observe it; captureException is called there and at the two npm registry probes, which silently degrade version pinning. InvalidFilePathError stays unreported — it is a 400, not a fault. Errors only: tracesSampleRate 0, no replay, no profiling. Two things worth knowing for the follow-up frontend change: - The DSN var is named ERROR_REPORTING_DSN, not SENTRY_DSN. The SDK's getFinalOptions falls back to env.SENTRY_DSN whenever the options object omits a dsn, which initialises the client straight from env and defeats any gate. - Reporting is gated on PREVIEW_HOST matching the production host, the prod-vs-local switch Tier-2 previews already use. Verified locally: with the gate closed the SDK logs "No DSN provided, client will not send events"; with it open the client initialises and captures. A Tier-2 session was booted and its preview URL proxied through the wrapped Sandbox DO to confirm the Proxy does not break the Sandbox SDK or proxyToSandbox() name resolution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second half of DEV-2134, after the API worker. Adds @sentry/react behind a single init module (src/sentry.ts) plus an error boundary — an editor-shell render crash used to leave a blank page and no record of why. Reports at the callsites that previously swallowed a failure whole: saved-demo load, docs-example load and switch (the DEV-2130 class, tagged by which step failed), versions fetch, broker userinfo, and the demo save/fork/embed/share/ revoke calls. The writeFile/deleteFile "not mounted" catches are left alone — that is expected control flow. reportRuntimeError classifies preview failures instead of reporting all or nothing. DemoRuntime.onError is a mixed channel: on the Sandpack engine it carries compile errors from the code being edited, which are product output and would bury the signal under one issue per keystroke. On the container engine it carries SessionStartError (the Tier-2 pool refusing a session, the failure class behind PR #87) and ContainerBootFailure, which are exactly what we want. 410 is excluded as normal teardown, and both are fingerprinted by class so per-example boot logs don't shard one problem into hundreds of issues. Nothing is reported outside production. .env.production is committed and is therefore loaded by ci.yml's authoring build, whose output Playwright serves at localhost:4173 — so without a gate every PR run would file events. Init is conditional on window.location.hostname, mirroring the Worker's PREVIEW_HOST gate. Source maps upload via @sentry/vite-plugin on the deploy workflow's build step only; build.sourcemap follows the token's presence, so a build without one neither emits nor publishes .map files. Needs repo secret SENTRY_AUTH_TOKEN and repo variables SENTRY_ORG / SENTRY_PROJECT (slugs, not the numeric DSN ids). Verified in a real browser against the production bundle: on localhost no client is created and an unhandled error produces zero ingest requests; with the gate temporarily pointed at localhost the envelope POST returns 200, confirming the DSN and project. Typing broken syntax into the editor produced no event, but the hosted Sandpack bundler was unreachable in that environment, so the preview never raised a compile error — repeat that negative check on prod. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ap upload Follow-up to the authoring instrumentation, from review. A token with no SENTRY_ORG/SENTRY_PROJECT left the plugin enabled with no upload target. Verified with fake credentials: sentry-cli fails with 401, and vite still exits 0 — so a partial setup degrades silently to unreadable minified traces rather than failing the deploy. The enable condition now requires all three, so a partial setup is a clean no-op; documented the failure mode and the fact that post-upload cleanup still runs (a failed upload never publishes .map files). Also normalises the release: the `define` substitutes "" when GITHUB_SHA is absent, and a release of "" would not match the SHA-named artifact bundle the plugin uploads, silently breaking map resolution. Separately confirmed the cancelled-mount reporting decision. reportRuntimeError fires even when the mount effect was cancelled, on the grounds that a refused session still failed; the risk was that rapid example/version switching would make this the highest-volume issue source. Raced POST /api/session against an immediate DELETE three times against the local worker: all three returned 410, which is the status the classifier drops. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The token scope was documented as `project:releases` + `org:read`, which is the older user-auth-token wording. An Organization Auth Token — what the bundler plugin wants — has a fixed `org:ci` scope covering source-map upload and release creation, with nothing to select. Also names the org/project slugs (handsoncode / demos) instead of telling the reader to go find them, and calls out the Deploy Token on the release-tracking page as the wrong credential: it drives the release webhook and cannot upload source maps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
@qunabu In https://handsoncode.sentry.io/settings/projects/demos/ -> Project security, you need to set allowed domain to |
demtario
marked this pull request as ready for review
July 28, 2026 08:24
Reported by Bugbot on #88, and it reproduces. `ignoreErrors` is applied by the event-filters integration, which processes every event — including explicit captureException calls — so the `/Failed to fetch/i` and `/Load failed/i` patterns were discarding exactly the failures reportError was added to surface. An offline broker /broker/userinfo call or a dead /api/versions raises `TypeError: Failed to fetch`; verified in a browser that with those patterns in ignoreErrors the reported event never reaches the transport. The patterns are only meant to suppress global onerror/onunhandledrejection noise, so they now live in beforeSend behind a `mechanism.handled === false` check. captureException sets `handled: true`, and mechanism is populated before beforeSend runs, so intentional reports are never matched. Verified in a browser: reportError with `TypeError: Failed to fetch` and `TypeError: Load failed` both reach the transport, while an unhandled ResizeObserver error and an unhandled `Failed to fetch` rejection are both dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 94e54f8. Configure here.
Reported by Bugbot on #88. demo-revoke only reported from the fetch .catch, so a 403/404/500 DELETE left `res` non-null, skipped the success branch, and reported nothing — while the list stayed exactly as it was with no error shown. That is the silent-failure case the reporting was added for, and the one path where the comment claiming to cover it was wrong. save/fork/embed/share already report after !res.ok; revoke now matches. Verified in a browser with the DELETE stubbed to 403: the row stays unrevoked, no error surfaces in the UI, and `revoke failed (403)` is reported with context demo-revoke. Before the change that produced nothing at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Adds Sentry error reporting to the two runner surfaces: the authoring app and the API worker. Errors only — no tracing, no session replay, no profiling.
DEV-2134: https://app.clickup.com/t/86cax9zfa
Why
The runner had no error reporting at all. DEV-2129 (Babel 6.26 parse failure across 8 examples), DEV-2130 (blank React demo from a jsx/tsx entry mismatch) and the Tier-2 5-slot container exhaustion were all found by a human clicking around or by the link-sweep script, days after they started failing. Workers Logs gives raw lines with short retention, no grouping and no alerting.
Build settings — in place
SENTRY_AUTH_TOKENsntrys_…), fixedorg:ciscopeSENTRY_ORGhandsoncodeSENTRY_PROJECTdemosAll three are set, and
deploy-runner-authoring.ymlreads them from those exact places.vite.config.tsenables source-map upload only when all three are present, so a partial or broken setup is a clean no-op rather than a broken deploy.Worth knowing for later: a failed upload (rotated token, wrong slug) does not fail the build —
sentry-clierrors and vite still exits 0. Verified with fake credentials. The symptom is unreadable minified traces in Sentry, not a red deploy, so check the deploy log for[sentry-vite-plugin] Errorif prod traces ever stop resolving.apps/authoring@sentry/reactinitialised from a dedicatedsrc/sentry.ts, imported first inmain.tsxso a throw during module evaluation is still caught. Callsites report through areportErrorhelper instead of importing the SDK, which keeps the production gate (below) authoritative in one place.Sentry.ErrorBoundaryat the app root. An editor-shell render crash previously left a blank page and no record of why./broker/userinfo, and demo save / fork / embed / share / revoke. ThewriteFile/deleteFile"not mounted" catches are deliberately left alone; that is expected control flow, not a fault.@sentry/vite-pluginuploads source maps and tags the release withgithub.sha.build.sourcemapfollows whether upload is enabled, so a build without the credentials neither emits nor publishes.mapfiles (~12 MB otherwise).workers/apiwithSentry()on the fetch handler;instrumentDurableObjectWithSentryon bothSandboxandBuilderSandbox. Tier-2 session orchestration and the share-build snapshotter run inside the DOs, so their failures never reach the handler's error path.Sandboxis load-bearing —proxyToSandboxresolves the live-preview namespace by that literal name — so the wrapper is assigned to it rather than aliased. Theas unknown ascasts dodge TS2589 on the Sandbox SDK's recursive RPC generic, mirroring the existingproxyToSandboxcast a few lines below.try/catchconverts every unexpected throw into a 500 body, sowithSentry()alone would never observe one.captureExceptionis called there, plus at the two npm-registry probes that silently degrade version pinning.InvalidFilePathErrorstays unreported — it is input validation returning a 400, not a fault.observability.enabledstays on; this is additive.Decisions worth a reviewer's attention
The Worker's DSN var is
ERROR_REPORTING_DSN, notSENTRY_DSN. This looks like a naming quirk and is not.@sentry/cloudflare'sgetFinalOptionsdoesdsn: userOptions.dsn ?? getEnvVar(env, "SENTRY_DSN")— it reads that exact key straight offenvwhenever the options object omits a dsn. Under that name the SDK initialises from raw env and silently bypasses the production gate below. Commented at both the definition and the use site so it does not get "fixed" later.Nothing is reported outside production, and that needed a gate rather than an absent DSN.
.env.productionis committed, so it is loaded byci.yml's authoring build — andplaywright.config.tsserves that exact build atlocalhost:4173. Without a gate, every PR run and every localvite previewwould file events against the production project. The browser gates onwindow.location.hostname; the Worker gates onPREVIEW_HOST, the prod-vs-local switch the Tier-2 preview URLs already rely on and which.dev.varsalready overrides.Preview failures are classified by error class, not dropped wholesale. The preview iframe runs arbitrary authored and imported example code, so a compile error or a mid-keystroke typo is product output, not an application fault — reporting those would bury the signal under one issue per syntax error. But
DemoRuntime.onErroris a mixed channel:sandpack.tscontainer.ts"The session was closed."container.tsContainerBootFailurecontainer.tsmount()rejectionSessionStartError(has.status)SessionStartErroris the Tier-2 pool refusing a session — the exhaustion class behind #87, and one of the things this PR exists to surface.reportRuntimeErrorinApp.tsxdoes the classification, next to the existingdescribeRuntimeError. Both container cases are fingerprinted by class so per-example boot-log text doesn't shard one problem into hundreds of issues.Note that the
beforeSendforeign-frame filter is a weak backstop, not the guarantee. A genuinely cross-origin iframe error arrives as"Script error."with no stack frames, so it would pass the filter. What actually protects the quota is theengine !== "container"early return.The Worker release comes from the
version_metadatabinding. Cloudflare's own per-deploy version id, sodeploy-runner-api.ymlneeds no change at all — no SHA plumbing, and nothing that has to interact with the four--routesflags already in thedeployscript.The DSN is committed, in two places, on purpose. A DSN is a write-only ingest endpoint and ships inside the JS bundle by construction, so hiding it buys nothing — and keeping it out of
wrangler secretmeans changing it needs no Cloudflare access. Abuse is bounded Sentry-side (allowed domains, inbound filters, spike protection). It sits besideVITE_API_BASEin.env.productionand besideLOGIN_BROKER_URLin thewrangler.jsoncvarsblock, both already labelled as public non-secret config.packages/runtimeandpackages/editor-shellstay uninstrumented. They are libraries; they keep surfacing errors through their existing interfaces and the classifier lives in the app.Verification
Local, pre-deploy:
pnpm typecheckclean · 67/67 unit tests ·pnpm e2e10 passed / 85 skipped.localhost:4173— no Sentry client is created, and an unhandled error produces zero ingest requests (checked in a real browser, network panel).localhost, the envelopePOSTreturns 200 — DSN and project confirmed valid.Proxyhands the SDK anObject.create(ctx)stand-in forDurableObjectState. Booted a real Tier-2 session and proxied its preview URL through the wrappedSandboxDO — neither the Sandbox SDK norproxyToSandboxname resolution breaks.src/index.tsxof the React starter: (a) a syntax error produced no event and no UI error, becausepushUpdate()catches the transpile failure atsandpack.ts:248and keeps the last good sandbox — it never reaches the bundler; (b) an unresolvable import — valid syntax, fails at bundle time — did surfaceError: Setup failedin the UI throughshow-error→emitError→onError, and still produced no event. Envelope count stayed at 1 across both.reportRuntimeErrorfires even when the mount effect was cancelled, on the grounds that a refused session still failed — which makes rapid example/version switching the obvious volume risk. RacedPOST /api/sessionagainst an immediateDELETEthree times: 410 every time, which the classifier drops.SENTRY_*setup builds clean, and a failed upload still runs the post-upload cleanup, so.mapfiles are never published.Gaps — need prod to close
demos.handsontable.com, default browser inbound filters.Out of scope
/d/:idviewer and/embed/:id— instrumenting the embed means a Sentry script running onhandsontable.comdocs pages (consent/PII surface, third-party script in docs, client-environment noise we can't act on).containers/live+containers/builderNode-side instrumentation — needs the Node SDK baked into both images and container-level DSN plumbing. Follow-up; see DEV-2030.🤖 Generated with Claude Code
Note
Medium Risk
Touches production observability and worker/DO wrappers on session and sharing paths, but does not change auth or business logic; main risks are mis-gated events or DO instrumentation regressions, which the PR documents and tests locally.
Overview
Adds Sentry error reporting (errors only, no tracing/replay) to the demo runner’s authoring SPA and API Worker, with strict production-only gates so CI,
vite preview, andwrangler devdo not pollute the prod project.Authoring: New
sentry.tsinitializes@sentry/reactonly ondemos.handsontable.com;reportErrorcentralizes tagged captures at previously silent failures (demo load/save/fork/embed/share, docs loads, versions fetch, broker userinfo). A root ErrorBoundary replaces blank-page crashes. Container preview faults (SessionStartError,ContainerBootFailure) are reported with fingerprints; Sandpack user-code errors are not. Deploy builds passSENTRY_*+GITHUB_SHAso@sentry/vite-pluginuploads source maps and strips.mapfromdist/.API Worker:
withSentrywraps the fetch handler; Sandbox and BuilderSandbox DOs are instrumented. DSN isERROR_REPORTING_DSN(avoids SDK auto-init viaSENTRY_DSN); reporting is on whenPREVIEW_HOSTmatches prod. Unexpected handler errors and npm registry probe failures are captured. Release usesCF_VERSION_METADATA.Docs cover tokens, variables, and operational notes (failed map upload does not fail the build).
Reviewed by Cursor Bugbot for commit 797cb9b. Bugbot is set up for automated code reviews on this repo. Configure here.