docs

Error reporting with Sentry (sentry)

Reports what breaks to Sentry without the SDK: every unhandled error that becomes a 5xx, and failed background work (event listeners, app.background() tasks). Events carry the stack trace, the route, the URL without query secrets, the user id, the request's earlier log lines as breadcrumbs, the release and the environment. Credentials, cookies, passwords and tokens are scrubbed before anything leaves the server. 4xx are ignored, identical errors are sent once per window, and a sample rate and a per minute cap protect your quota. No dependencies, no change to the core: the block listens to the logger.

Terminal
npx calm add sentry
Adds
Filesapp/blocks/sentry/index.js (what is captured, the log sink), event.js (DSN, stack frames, scrubbing, envelopes)
EnvironmentSENTRY_DSN (required in production), SENTRY_ENVIRONMENT, SENTRY_RELEASE

Setup in Sentry#

  1. Create a project (Projects, Create project, platform Node.js).
  2. Copy its DSN: Project settings, Client Keys (DSN). It looks like https://<key>@o123.ingest.sentry.io/456.
  3. In .env:
Terminal
SENTRY_DSN=https://<key>@o123.ingest.sentry.io/456
SENTRY_RELEASE=api@1.4.0      # optional, default: <package name>@<version>
SENTRY_ENVIRONMENT=staging    # optional, default: NODE_ENV

An invalid DSN is reported by calm doctor; a missing one is an error in production.

What is reported#

SourceReportedDetails
An error thrown in a route, service or hook that answers 5xxyesstack, transaction (POST /api/orders/:id), request, user id, breadcrumbs, status_code tag
4xx (validation, 401, 403, 404, 429)neverthe client's mistake, not a bug
log.error(msg, { err }) elsewhere (failed app.background() tasks, event listeners)yes (captureLogErrors)tag source: log, extra.log is the log message
Your own callsyescaptureException, captureMessage
JavaScript
// Report a handled problem
ctx.app.ext.sentry.captureException(err, { tags: { feature: 'checkout' }, extra: { orderId } });
ctx.app.ext.sentry.captureMessage('Payment provider slow', { level: 'warning' });

Both return the event id, or null when the event was ignored, deduplicated, sampled out or capped. err.cause chains are sent as chained exceptions.

Scrubbing#

  • Request headers: only an allowlist is sent (user-agent, content-type, accept, referer, origin, host...). Authorization and Cookie never are.
  • The URL is sent without its query string; the query string is sent separately with sensitive values replaced by [Filtered] (?token=..., ?code=..., ?signature=...).
  • Bodies (only with sendBody: true) and extra are scrubbed recursively: any key containing password, secret, token, authorization, cookie, session, apiKey, privateKey, credential, signature, totp, recoveryCode... or ending with code, otp, card, cvv, ssn. Add your own with sensitiveKeys.
  • Messages and breadcrumbs: Bearer ... / Basic ... values, CalmAPI tokens and secret query parameters are replaced.
  • The user is { id } only. Email (sendUserEmail) and IP (sendIp) are opt-in.

Options#

calm.config.jsJavaScript
export default defineConfig({
  blocks: {
    sentry: {
      sampleRate: 1, // 0 to 1
      dedupeSeconds: 60, // the same error (type, message, place in your code) once per window
      maxEventsPerMinute: 30, // per instance
      maxBreadcrumbs: 20, // log lines of the request (0: none)
      sendUserEmail: false,
      sendIp: false,
      sendBody: false,
      captureLogErrors: true,
      sensitiveKeys: ['iban', 'taxId'],
      ignoreErrors: ['ECONNRESET', /^Client closed/],
      beforeSend: (event) => (event.tags.route === 'GET /api/health' ? null : event),
    },
  },
});

After a 429 from Sentry, sending pauses for the Retry-After it asks for. app.ext.sentry.stats counts sent, deduplicated, sampled out, capped, ignored and failed events (also in devtools, Sentry panel).

Protocol#

Events go to https://<host>/api/<project>/envelope/ as an envelope (application/x-sentry-envelope: envelope header, item header, event), with X-Sentry-Auth: Sentry sentry_version=7, sentry_key=<public key>, sentry_client=calmapi-sentry/1.0.0. Only the public key of the DSN is used.

Security notes#

  • Error reports leave your infrastructure: review what you add to extra and tags, they are scrubbed by key name only.
  • Self-hosted Sentry works the same (the DSN points to it). Keep it on https.
  • The block reports its own sending problems with log.warn, never as errors, so a Sentry outage cannot loop.
<!-- block-guide:start (generated from block.json by scripts/sync-block-guides.mjs) -->

Official documentation#

Read these before changing the block; they are also in app/blocks/sentry/AGENTS.md and in the devtools Blocks panel.

For AI agents#

Coding agents read these notes in app/blocks/sentry/AGENTS.md:

  • Read app/blocks/sentry/index.js (capture rules, the log sink) and event.js (DSN, stack frames, scrubbing, envelope) before changing anything.
  • Errors reach Sentry through the logger: the core error handler logs 5xx with log.error(msg, { err }) after setting req.calmError. Do not add try/catch blocks that swallow errors to report them; let them throw.
  • Invariant: 4xx are never reported, and nothing leaves the server unscrubbed. Every new field added to an event must go through scrub() or scrubString().
  • Invariant: only the user id is sent by default. Email (sendUserEmail), IP (sendIp) and request bodies (sendBody) are opt-in options; do not turn them on without the user asking.
  • Add keys to scrub with the sensitiveKeys option in calm.config.js rather than editing the built-in list.
  • Report handled problems with ctx.app.ext.sentry.captureException(err, { tags, extra }) or captureMessage(text, { level }); both return the event id or null.
  • SENTRY_DSN lives in .env. The DSN's public key is not a secret, but keep it out of git anyway.
  • Inside this block, log failures with log.warn and never with an err field at error level: the sink would report its own failures in a loop.
  • Noise control lives in admit(): dedupe window, per minute cap, sample rate, and a pause after Sentry answers 429. Keep it in front of every send.
  • Tests point SENTRY_DSN at a local node:http server (http://key@127.0.0.1:port/42) and read the envelopes it receives.
<!-- block-guide:end -->

Last updated