Skip to content
Docs · Instrumentation

A failure your code swallows never reaches Déjà.

Déjà scores the failures that reach Sentry as issues, and can attribute one only when its title names the field that broke. Three common patterns keep a failure out of Sentry: a consumer that dead-letters, validation that returns a result, and a downstream call that is caught and logged. For each, the change that makes the failure visible, and what it costs.

Written for the engineer whose incidents end in a no-attribution receipt — or in no receipt at all.

01 · What Déjà reads

A Sentry issue, and one line of it decides.

What the pipeline does with a Sentry delivery, one step per row, with the function that does it. A log line is not a Sentry issue, and nothing here reads one.

Deliveries scored

Sentry's issue and error webhooks, the two the Sentry connection asks you to enable. An alert-rule notification is logged and not scored, so a Sentry alert rule is not a way to forward a failure.

sentry/event-router.ts · routeSentryEvent · sentry/alert-handler.ts · handleAlertEvent

What scoring is given

The issue's title, id, project slug, event type and time. Tags, extra data, the user and the stack trace are not scoring inputs.

sentry/issue-handler.ts · handleIssueEvent · sde-phase1-queue.ts · runSentryPhase1Job

The title

The issue title as Sentry sends it, which Sentry builds from the exception's type, a colon, and the first line of its message. A delivery with no title falls back to the first exception's type and message.

sentry/event-router.ts · exceptionTitle

Error type

The title up to its first colon or space. It scores the error-type factor, W4: a missing-field name (KeyError, AttributeError, MissingFieldError) matches a removed or renamed field; a type name (TypeError, ValueError, ValidationError) matches a changed type, except a JavaScript TypeError from reading a property, which counts as a missing field.

sde-phase1-queue.ts · runSentryPhase1Job · sde/ccs-math-engine.ts · classifyError

The field

The property a JavaScript engine names in (reading 'x'), Cannot read property 'x' of, or Safari's evaluating 'obj.x'. Otherwise the first quoted word in the title: two or more letters, digits, _, . or -. If that word is capitalised like a class name ('NoneType'), no quoted word is used. Otherwise a bare Python KeyError: x.

sde-phase2-deduction.ts · extractMissingField

No field found

No change is scored. The receipt is a no-attribution receipt (R1-N) whose reason is no_field_extracted.

sde-phase2-deduction.ts · runSdePhase2DeductionInner

Candidates

Recorded changes to that field, under the name as written or its snake_case or camelCase form, from the last 30 days.

sde-phase2-deduction.ts · runSdePhase2DeductionInner

Service zone

The zone named like the Sentry project's slug. If no zone has that name, one of the vault's other active zones.

sde-phase1-queue.ts · resolveSentryServiceZone

Repeats

Each Sentry issue is scored at most once per release. Repeats are suppressed for 90 days when the event carries a release, and for 7 days when it does not.

sentry/event-dedup.ts · buildIssueDeliveryId · sentry/event-dedup.ts · isDuplicate

02 · The message

Put the field in the title, quoted, and first.

A runtime's own error rarely names the field that went missing, so name it yourself. One helper does it for all three patterns below.

report-field-failure.ts
import * as Sentry from "@sentry/node"

// Sentry titles this "MissingFieldError: 'total_cents' missing
// in orders.created"; Déjà reads the field from that title.
export function reportFieldFailure(
   field: string,
   kind: "missing" | "wrong-type",
   where: string,
   extra: Record<string, unknown> = {},
): void {
   const what = kind === "missing" ? "missing" : "has the wrong type"
   const err = new Error(`'${field}' ${what} in ${where}`)
   err.name = kind === "missing" ? "MissingFieldError" : "FieldTypeError"
   Sentry.captureException(err, {
      // One Sentry issue per field, not one per line of code.
      fingerprint: ["field-failure", where, field, kind],
      extra,
   })
}

The error's name sets the error-type factor: a MissingFieldError is scored against a removed or renamed field, a FieldTypeError against a changed type.

The fingerprint gives each field its own Sentry issue. Without it Sentry groups by stack trace, every field reported from one line lands in one issue, and Déjà reads that issue's title once.

Report the field's own name, the last segment of its path: customer.email is looked for as one name. And report the name, never the value — the title is stored.

Example Sentry issue titles, the field Déjà takes from each, and the kind of change it is scored against.
Title Sentry sendsField Déjà takesScored againstWhy
MissingFieldError: 'total_cents' missing in orders.createdtotal_centsA removed or renamed fieldWhat the helper sends for a missing field.
FieldTypeError: 'total_cents' has the wrong type in orders.createdtotal_centsA changed typeWhat the helper sends for a wrong type.
KeyError: 'total_cents'total_centsA removed or renamed fieldPython's own error for a missing key already names it.
TypeError: Cannot read properties of undefined (reading 'trim')trimA removed or renamed fieldJavaScript names the property read from undefined: the one after the missing field.
ValidationError: 'total_cents' is requiredtotal_centsA changed typeThe right field, but a validation error is scored against a changed type, not a removed field.
ZodError: [noneNothing — an R1-NA captured ZodError: its message is JSON, and the title keeps only the first line.
Error: order 'A-1043' failed: 'total_cents' missingA-1043A removed or renamed fieldOnly the first quoted word is read.
03 · Pattern one

The consumer that dead-letters, and says nothing.

The handler throws, the catch moves the message to a dead-letter queue, and the only record is a log line. Sentry never hears of it, so Déjà never does — not even as a no-attribution receipt.

Before · the failure stops here
async function onMessage(msg: QueueMessage) {
   try {
      await handleOrder(JSON.parse(msg.body))
   } catch (err) {
      logger.warn({ err, messageId: msg.id }, "dead-lettered")
      await deadLetter.send(msg)
   }
}
After · the failure reaches Sentry
async function onMessage(msg: QueueMessage) {
   try {
      await handleOrder(JSON.parse(msg.body))
   } catch (err) {
      // Before the send, so a failing DLQ cannot take the report with it.
      Sentry.captureException(err, { extra: { messageId: msg.id } })
      logger.warn({ err, messageId: msg.id }, "dead-lettered")
      await deadLetter.send(msg)
   }
}

The message id goes in extra data, for whoever triages; Déjà does not score it. What Déjà can attribute depends on err. In JavaScript, reading a missing field returns undefined without an error; the TypeError comes when something is read from that undefined, and names what was read (the trim row above). Validate at the top of the handler, as in pattern two, and dead-letter what it rejects.

The same change in Python
try:
    # order["total_cents"] raises KeyError: 'total_cents'
    handle_order(json.loads(msg.body))
except Exception as err:
    sentry_sdk.capture_exception(err)
    dead_letter.send(msg)

Python's KeyError names the key itself, so capturing it is enough — if you index with [ ]. With .get() the missing value is None, and the error that follows usually quotes 'NoneType' first, which Déjà skips as a class name.

What it costs

One Sentry event per dead-lettered message. Usually the smallest of the three: the dead-letter rate is low until a producer breaks, and then the burst groups into Sentry issues, which Déjà scores once each per release. Every event still counts against your Sentry quota.

04 · Pattern two

Validation that returns a result, and nothing is thrown.

safeParse returns { success: false } instead of throwing, and the caller logs a warning and moves on. No exception, no Sentry issue.

Before · the failure stops here
function onOrder(payload: unknown) {
   const parsed = OrderCreated.safeParse(payload)
   if (!parsed.success) {
      logger.warn({ issues: parsed.error.issues }, "order skipped")
      return
   }
   return fulfil(parsed.data)
}
After · the failure reaches Sentry
function onOrder(payload: unknown) {
   const parsed = OrderCreated.safeParse(payload)
   if (!parsed.success) {
      const issue = parsed.error.issues[0]
      const field = String(issue?.path.at(-1) ?? "payload")
      const missing =
         issue?.code === "invalid_type" && issue.received === "undefined"
      reportFieldFailure(
         field,
         missing ? "missing" : "wrong-type",
         "orders.created",
      )
      return
   }
   return fulfil(parsed.data)
}

Report the field, not the ZodError. Its message is JSON, so the title Sentry builds from its first line is ZodError: [, and the first quoted word in the rest is code. The missing or wrong-type split chooses the error's name, so a dropped field is scored against a removed field and a retyped one against a changed type.

What it costs

One event per rejected payload — the most of the three, because when a producer drops a field every message fails. Déjà needs one: the fingerprint makes it one Sentry issue per field, scored once per release. To spare your quota, report the first rejection per field per minute and count the rest in a metric.

05 · Pattern three

The downstream call that is caught, and quietly logged.

A call to another service is wrapped in try/catch, the failure is logged as a warning, and the caller gets a fallback. The fallback is deliberate. The silence is not.

Before · the failure stops here
async function taxIdFor(invoiceId: string) {
   try {
      const invoice = await billing.getInvoice(invoiceId)
      return invoice.tax_id.trim()
   } catch (err) {
      logger.warn({ err, invoiceId }, "billing lookup failed")
      return null
   }
}
After · the failure reaches Sentry
async function taxIdFor(invoiceId: string) {
   try {
      const invoice = await billing.getInvoice(invoiceId)
      if (invoice.tax_id === undefined) {
         reportFieldFailure("tax_id", "missing", "billing.getInvoice", {
            invoiceId,
         })
         return null
      }
      return invoice.tax_id.trim()
   } catch (err) {
      Sentry.captureException(err, { tags: { downstream: "billing" } })
      logger.warn({ err, invoiceId }, "billing lookup failed")
      return null
   }
}

The behaviour does not change: the caller still gets null. Check the field before you use it — on a missing tax_id, invoice.tax_id.trim() throws (reading 'trim'), which names trim. In the catch, capture err itself: wrapping it in a new error with a summary message replaces the title Déjà reads. The tag is for you; Déjà does not score tags.

What it costs

Every caught failure becomes an event, routine ones included — timeouts, 404s, retries that later succeed. Most name no field, so each Sentry issue they form ends in an R1-N with no_field_extracted, not an attribution. Always report the field check; capture the rest if you want them in Sentry for their own sake.

06 · On Déjà's side

Scored once per issue, signed either way.

Scoring
At most once per Sentry issue and release, however many events the issue collects.
Receipts
An attribution (R1) counts toward the monthly allowance, which is advisory: signing never stops. A low-confidence (R1-L) or no-attribution (R1-N) receipt never counts. See pricing.
Storage
The title becomes the incident's title in Déjà, so it should carry a field's name and never its value. What else is kept from a Sentry event is on the engine page.

Other sources

Datadog and Splunk On-Call alerts are scored too. In a Splunk On-Call alert, Déjà looks for the field in the alert's display name, entity_display_name, by the same rule. A Datadog alert cannot name the field: Déjà's intake keeps no alert title or message, so what it scores is a service check's name or a fixed label such as Datadog monitor.alert. Report a failure you want attributed to Sentry.

Visible is not attributed. Déjà attributes a failure only when a recorded change to the field it names clears the threshold — the engine publishes the rule, and the scope says what never will be. A failure that reaches scoring and does not clear the threshold still ends in a signed receipt — low confidence (R1-L) or no attribution (R1-N) — that says why.