Observability — Sentry (Errors, Traces, Logs, Metrics)
Tasuku sends four kinds of telemetry to Sentry: Issues (errors + warnings), Traces (sampled performance spans), Logs, and Metrics. Reporting is wired in exactly two files — one per runtime — behind a vendor-neutral facade, so the provider can be swapped without touching call sites.
No-op without a DSN. Local dev and tests have no
SENTRY_DSN/PUBLIC_SENTRY_DSN, so the facade falls back toconsoleand nothing leaves the machine.
Architecture: one facade, two adapters
Section titled “Architecture: one facade, two adapters”flowchart LR
APP["App code<br/>captureError / captureMessage<br/>countMetric / recordDistribution"]
FACADE["src/lib/obs/report.ts<br/>vendor-neutral facade<br/>(redacts context, console fallback)"]
SRV["sentry-server.ts<br/>@sentry/cloudflare<br/>(Worker)"]
CLI["sentry-client.ts<br/>@sentry/browser<br/>(island, lazy-loaded)"]
SENTRY["Sentry<br/>Issues · Traces · Logs · Metrics"]
APP --> FACADE
FACADE -->|Worker| SRV
FACADE -->|browser| CLI
SRV --> SENTRY
CLI --> SENTRY
src/lib/obs/report.ts— the facade. Everything in the app callscaptureError/captureMessage/countMetric/recordDistributionhere; nothing else imports a Sentry SDK. Context objects pass through the sameredact()guard as structured logs, so tokens/PII never reach the provider via our own call sites.src/lib/obs/sentry-server.ts— the Worker adapter.withErrorMonitoring(handler)wraps the Worker (src/worker.ts) so unhandledfetch/scheduledexceptions and request traces are captured. Importing it registers the Sentry reporter.src/lib/obs/sentry-client.ts— the browser adapter. Dynamically imported so the SDK lands in its own chunk, off the critical path.
Swapping providers = rewrite those two adapters. No other file changes.
What each signal carries
Section titled “What each signal carries”| Signal | Sentry product | What we send | Sampling |
|---|---|---|---|
| Exceptions | Issues | Unhandled fetch/scheduled errors; captureError | 100% (quota-capped) |
| Messages | Issues | captureMessage, sent at warning | 100% |
| Traces | Traces | Request spans; cron scan excluded | 10% |
| Logs | Logs | console.warn / console.error only | n/a |
| Metrics | Metrics | app.request count + app.request.duration ms, tagged {area, method, status} | unsampled |
Issues — warnings and above only
Section titled “Issues — warnings and above only”captureError always reports at error/fatal. captureMessage is sent at warning
(the SDK would otherwise default to info). A beforeSend floor drops any info/debug/
log event, so no informational events reach Issues — by policy (§9.3).
Traces — sampled, with the cron excluded
Section titled “Traces — sampled, with the cron excluded”Traces sample at 10% to respect the free-tier span budget. The every-minute reminder
Cron Trigger (scheduled()) would otherwise produce ~43k faas.cron transactions/month
and dominate the quota, so a tracesSampler returns 0 for it (and 0.1 for everything
else). Cron exception capture is unaffected — only the routine trace is dropped.
Logs — warn/error console output
Section titled “Logs — warn/error console output”enableLogs + consoleLoggingIntegration({ levels: ['warn', 'error'] }) forward existing
console.warn/console.error to Sentry Logs with no call-site changes. The app’s
structured JSON event logs go through console.log (info level) and are deliberately not
forwarded — they belong in Workers Logs, not Sentry (§9.3, same no-info policy as Issues).
Metrics — unsampled request health
Section titled “Metrics — unsampled request health”app.request (counter) and app.request.duration (distribution, ms) are emitted once per
request at the Worker, tagged with low-cardinality { area: api|web, method, status } —
never the path (which carries ids/content). Metrics are unsampled, so they count
accurately even though traces sample at 10%. Application Metrics are GA and on by default in
the SDK; the explicit enableMetrics: true is belt-and-suspenders.
Privacy scrubbing (§9.3)
Section titled “Privacy scrubbing (§9.3)”The app is privacy-strict, so both adapters strip personal data:
sendDefaultPii: false.beforeSendremoves the auto-attached request PII — cookies, theauthorizationheader, query string, andevent.user.- Facade context is run through
redact()(the same guard that blankstoken/secret/password/authorization/cookie/emailandnotes/title/query/displayName). tracePropagationTargetsis same-origin only (/^\//) — the trace header never leaks to Clerk or Microsoft Graph.
Source maps
Section titled “Source maps”When SENTRY_AUTH_TOKEN is present at build time, @sentry/vite-plugin emits hidden
source maps (no sourceMappingURL, never served), uploads them under the release the SDKs
report (__APP_VERSION__), then deletes them from dist so source never ships to users.
Absent the token, the build is byte-identical and uploads nothing. This is opt-in per build —
see the build-tool creds in Configuration & Secrets.
Configuration
Section titled “Configuration”| Value | Kind | Where | Read at |
|---|---|---|---|
PUBLIC_SENTRY_DSN | client DSN | .env / .dev.vars* | build (baked into the browser bundle) |
SENTRY_DSN | server DSN | wrangler.jsonc vars | run time (env.SENTRY_DSN) |
ENVIRONMENT | env tag | wrangler.jsonc vars | run time |
SENTRY_AUTH_TOKEN / SENTRY_ORG / SENTRY_PROJECT | build-tool creds | active .dev.vars | build (source-map upload) |
The browser SDK POSTs to the regional ingest host, so the CSP connect-src allows
https://*.ingest.{us,de}.sentry.io (src/lib/security/csp.ts). See
Configuration & Secrets — Compile vs. Deploy for how these values are
sourced and why a PUBLIC_* change requires a rebuild.
Free-tier discipline
Section titled “Free-tier discipline”Everything above is tuned to stay inside the Sentry free plan:
- Traces sampled at 10%; the cron transaction dropped entirely.
- Logs + (forwarded) Issues limited to warning/error.
- Metrics low-cardinality (no per-id tags) and unsampled but cheap.
Raise the sample rates if the quota allows.
Verifying
Section titled “Verifying”scripts/sentry-smoke.mjs sends one of each signal straight to the ingest endpoint (no SDK,
no deploy) to confirm a project is reachable:
node scripts/sentry-smoke.mjs "$PUBLIC_SENTRY_DSN"# → Issues (warning + exception), Traces, Logs, MetricsReal app telemetry only flows from the deployed Worker on real traffic — after a deploy, hit the URL a few times and check Explore → Metrics / Logs filtered to the right environment (~1 min to index).
See also
Section titled “See also”- Configuration & Secrets — Compile vs. Deploy — where the DSNs and build creds live.
src/lib/obs/report.ts,sentry-server.ts,sentry-client.ts— the facade and adapters.