Alerts & triage
A burst of errors is only useful if it arrives as one decision, not fifteen interruptions. Crossdeck groups every captured error into an issue by fingerprint, computes a priority that weighs paying-customer impact, and notifies you under a small set of rules that hold no matter how loud an incident gets: one email per new issue, spikes measured against an issue's own baseline, and muted means nothing fires. This page is that contract, written down.
TL;DR
- Events group into issues by fingerprint — a hash of the error's type, top stack frame, and event kind. Same bug across users, sessions, and re-deploys → one issue. The error message is never a fingerprint input, so user data in messages can't split one bug into hundreds of issues.
- Four statuses:
open,resolved,regressed,ignored. A resolved issue that fires again flips toregressedautomatically — the loudest signal in the system. - Priority is computed on every event from severity, paying customers affected, distinct users hit, rate anomalies, and category tags. You can pin it manually; Crossdeck never overwrites a manual priority.
- One email per new issue. Re-alerts fire only on regression, escalation to high priority, or six-plus quiet hours followed by 100+ events. Hard cap: 4 emails per issue per rolling 24 hours.
- A spike is a rate anomaly, not growth. An issue spikes when its rate breaks at least 10× from its own baseline, normalized by your project's overall volume — and it notifies once per incident, then stays silent until it recovers and breaks again.
- Muted or ignored means nothing fires. No email, no Slack, no exception for spikes. An ignored pattern that spikes surfaces only as a quiet review chip in the dashboard.
From error to issue
Every error.* event that reaches Crossdeck is stamped with a fingerprint before it touches storage — an opaque SHA-256 hash of a fixed, versioned tuple. All events sharing a fingerprint accumulate on a single issue: one row in your Issues list, one count, one triage state.
The fingerprint is computed from exactly four inputs:
| Input | Why it's in the hash |
|---|---|
| Event type | error.unhandled, error.http, and error.unhandledrejection group separately. An unhandled exception and an HTTP 500 in the same function are different bugs. |
| Exception class | The runtime symbol — TypeError, HTTPError. A code identifier, not user data. |
| Top frame function | Where the error originated. Anonymous functions collapse to a single placeholder. |
| Top frame file, normalized | Build hashes (main-abc123.js → main-def456.js), query strings, emails-in-URLs, and UUIDs-in-paths are stripped first, so a re-deploy doesn't mint a new issue for an old bug. |
Just as important is what is never hashed: the exception message, stack frame bodies, breadcrumbs, URL query values, user and session IDs, timestamps. Messages routinely carry per-occurrence data — "Cannot read 'X' of null" where X is user input — and hashing them would split one bug into an issue per value. Two different users hitting the same logical bug always land on the same issue.
Issue statuses
An issue is always in exactly one of four states:
| Status | How it gets there | Behavior |
|---|---|---|
open | Every new issue starts here. | Shows in the Open tab. Alerts fire per the contract below. |
resolved | You click Resolve. | Leaves the Open tab. The fingerprint keeps watching for new events. |
regressed | Automatic — a new event lands on a resolved issue. | Returns to the Open tab with a Regressed badge and a status-history entry. Triggers a regression alert. |
ignored | You click Ignore. | Hidden from the Open list, silenced everywhere, keeps counting under the Ignored tab. Unignore restores it. |
Regressed is the loudest signal Crossdeck has. A regression means you looked at the bug, decided it was fixed, and the code disagrees. The alert decider evaluates a regression before every other alert type, and the issue surfaces back in your Open tab automatically — you never re-open it by hand to find out a fix didn't take.
Priority
Every issue carries a computed priority — high, medium, or low — recomputed on every event, so an issue that escalates from 10 events an hour to 200 climbs on its own, and falls back when the surge dies down.
The base tier comes from how the error was captured:
- Unhandled error (
error.unhandled,error.unhandledrejection) → high - Handled error → medium
- Warning or info level → low
Then each of these modifiers bumps the tier up by one (ceiling at high):
- At least one paying customer affected. The single heaviest signal in the product — an error a paying customer hit is never just noise.
- Five or more distinct users hit. Five hundred events from one user is a nuisance; the same error across many humans is a different class of problem.
- The rate is anomalous — the same normalized spike verdict described below.
- First seen within the last hour. Fresh issues surface for triage.
- Category tags include
security, orpaymentswith a paying customer affected.
Two floors hold underneath the blend. Un-actionable noise — ad-blocker hits, browser-extension errors, aborted requests mid-navigation — is pinned to low and no per-event modifier can raise it; only a genuine anomaly (a spike, or sudden breadth across many users) lifts it, because a flood of "noise" hitting everyone at once is your outage alarm. And connection noise (HTTP status 0 — the request never reached a server) is a hard floor nothing pierces.
Manual override
The Priority select on the issue page (Auto / High / Medium / Low) pins a manual priority. Crossdeck never overwrites a manual priority — the recompute keeps running underneath, but display and sort use your pin until you set it back to Auto. When the pinned value differs from the computed one, the priority pill says so.
Default sort
The Issues list opens on the Open tab sorted by most paying customers affected. Priority, recency, frequency, and newest-issue sorts are one dropdown away, but the default ordering is the business question: which of these is costing me revenue right now?
The notification contract
These are commitments, not defaults. Each one is enforced by a state machine on the issue itself, so they hold under concurrency, retries, and incident-scale volume.
1. One email per new issue
The first occurrence of a new fingerprint sends one email. A burst of fifteen events in two minutes produces one email, not fifteen. If that first send fails, Crossdeck retries up to 3 attempts, spaced at least one minute apart — and sends no other alert type for the issue until the first email is settled.
2. Re-alerts only when something meaningful changed
After the first email, you hear about an issue again in exactly three cases:
- Regression — the issue was resolved and is firing again.
- Escalation — its priority climbed to
highsince the last alert. Climbing low → medium is not an email. - Still happening — at least 6 hours of silence since the last alert, followed by 100 or more new events.
On top of those triggers sits a hard cap: at most 4 emails per issue in any rolling 24-hour window, no matter what. (Spike notifications are the one exemption — see channels below.)
3. Muted or ignored means nothing fires
Muting an issue, or setting its status to ignored, short-circuits every notification decision before anything else is evaluated. No email, no Slack message, no exception for spikes. Ignoring is your severity verdict — "this is unactionable" — and volume doesn't change unactionable into actionable. Sixty events becoming six hundred is still just a number quietly incrementing under the Ignored tab.
One nuance: a genuine spike inside an ignored or muted pattern is still recorded. It surfaces as a quiet chip on the Issues page — "Ignored pattern spiking — review?" — that links to the Ignored tab. Crossdeck may escalate visibility; it never overrides your verdict with an interruption.
4. Spikes notify once per incident
When an issue's rate breaks from baseline (the exact definition is next), the incident opens and sends one notification. The issue then holds a "spiking" state and sends nothing further — no drumbeat — until the rate genuinely returns toward baseline and the incident closes. Only a fresh breach after recovery can notify again.
5. Noise never notifies
Issues classified as noise — ad-blocker blocks, browser-extension errors, requests cancelled by navigation, HTTP status 0 — never produce an email or Slack message. There is deliberately no "send me noise anyway" toggle: a team that wants alerts on a pattern Crossdeck classifies as noise should change the classification, not re-introduce the spam.
How spike detection works
A spike is a rate anomaly, never raw growth. The question is not "are there more events than yesterday?" — it's "did this issue break hard from its own baseline, in a way your project's overall traffic doesn't explain?"
Concretely, every issue maintains a rolling events-per-hour baseline (an exponential moving average of past hours). On each event, Crossdeck computes:
- The issue's rate ratio — this hour's event count divided by the issue's own baseline (floored at 0.5 events/hour, so a rare 2-a-day error spiking to 200 an hour is detectable without the ratio dividing toward infinity).
- Your project's growth factor — the project's total error volume this hour against the project's baseline, never less than 1, and only trusted once the project has a real baseline of its own.
- The normalized ratio — issue ratio ÷ project growth factor. This is the number the notification cites.
The normalization is the part that keeps the signal honest. On a viral day where everything 10×es together, every issue's raw ratio climbs — and the project growth factor divides it right back out. Every issue tracking smoothly upward alongside traffic is the product succeeding, not an incident. A quiet project, meanwhile, never excuses a spike: the growth factor can dampen, never amplify.
| Threshold | Value | What it does |
|---|---|---|
| Events-per-hour floor | 10 | Absolute minimum to open an incident. A "spike" of 9 events an hour isn't one. |
| Open ratio | 10× | Normalized ratio at or above this opens the incident — the one moment that may notify. |
| Recovery ratio | 3× | The incident closes only when the normalized ratio drops below this. The gap between 10× and 3× is hysteresis: a rate flapping around the open threshold can't close and re-open the incident repeatedly. |
| Minimum incident duration | 1 hour | An incident can't close before this dwell time, however fast the rate recovers — flap damping on the other edge. |
The incident lifecycle is a two-state machine: none → spiking on a breach (notifies, once), spiking → none on recovery (re-arms, silently). There is no third state and no path that notifies twice within one incident.
Channels
Error alert emails go to the project owner's account email. All five flavours — first occurrence, regression, escalation, still-happening, and spike — share one template family, and each cites the source-map-resolved top frame when a map is available (see Source maps).
The 4-per-24h hard cap applies to first, regression, escalation, and still-happening emails. Spike notifications are exempt from the cap, deliberately: a genuine rate incident must always land, and the once-per-incident state machine is its own rate limit — an issue physically cannot produce a second spike notification until the first incident recovers.
Email alerts can be turned off project-wide in the project's notification settings (notifications.email.errorAlerts); per-issue, use Mute alerts on the issue page.
Slack
Connect Slack from Developers → Integrations. The flow is Slack OAuth: you're redirected to Slack, pick a single channel, and approve. Crossdeck requests only the incoming-webhook scope — the most restrictive scope Slack offers. It grants posting to the one channel you chose, and nothing else: Crossdeck cannot read messages, list members, or post anywhere other than that channel. One channel per project.
Once connected, Send test message fires a synthetic alert through the same code path real alerts use — if it lands, the wiring is correct. The Alert preferences panel on the same page lists every alert stream grouped by kind (customer activity, errors, pipeline health); everything is on by default, and a flipped toggle silences that stream entirely. Two keys govern errors:
error.new_issue— the first occurrence of a new fingerprint. One message per new issue, severity-coded by priority (high red, medium amber, low green), with the resolved top frame and a link to the issue.error.rate_spike— the spike notification. One line per incident, citing this hour's count and the normalized ratio, and stating its own contract in the message: "One message per incident — silent until it recovers and spikes again."
Slack honors the same gates as email: muted issues, ignored issues, and noise-classified issues never post.
Custom alert rules
Everything above is about errors. Custom alert rules extend the same discipline to any event you track: "notify me when checkout_failed fires at least 10 times in 5 minutes." You define them on the Alerts page in the dashboard.
A rule has five parts:
| Part | What it does | Bounds |
|---|---|---|
| Event | The exact event name from cd.track(name, …). Exact match — no wildcards. | 1–200 chars |
| Threshold | The rule fires when the count of matching events in the window is at or above this number. The comparison is always ≥ — there is no operator to choose, and no fudge factor or rounding on either side. | 1–1,000,000 |
| Window | A sliding window, in seconds, ending now. Evaluated exactly as configured. | 1 min – 24 h, default 5 min |
| Cooldown | After a fire, the rule stays silent for this long, however high the count climbs — the page-storm guard. | 1 min – 24 h, default 15 min |
| Property filters | Optional equality filters on event properties (e.g. plan = pro), string or number. All filters must match for an event to count. Equality only — the semantics stay simple and auditable. | Up to 5 |
How rules evaluate
Every enabled rule is evaluated once per minute: the count query runs over both environments and excludes bot traffic, then the result is compared against the threshold. The cooldown check and the fire decision happen in a single atomic transaction on the rule itself, so two evaluator runs racing can never double-page you. Each fire is recorded in a per-rule history — the count observed, when it fired, and which channels were attempted and which actually delivered — shown on the Alerts page.
Where a fire goes
Each rule picks its own channels; at least one must be enabled.
- Slack — posts to the project's connected channel (the same single-channel connection described above), citing the event, the count against the threshold, and a link to the breakdown chart.
- Email — up to 10 recipients per rule, using the same alert template family.
- Webhook — a JSON
POSTto an HTTPS endpoint you provide (plainhttp://is rejected). Every delivery is signed: anx-crossdeck-signature: sha256=…header carries an HMAC-SHA256 over the raw body, computed with a per-rule secret minted when the rule is created — verify it the same way you would a Stripe signature. Deliveries time out after 5 seconds and count as delivered only on a 2xx response.
A failed channel never blocks the others — each is attempted independently, and the fire history records exactly which ones landed.
Rules are workspace-scoped: creating, editing, or deleting one requires project membership, and every change lands in the audit log under alert_rules with the actor's identity, IP, and the rule's before/after shape.
Plain-English summaries
Every issue page carries a "What this means" card: three short blocks — What broke, Who's affected, What to do — plus a severity and confidence label, written for whoever runs the business rather than whoever reads stack traces. It loads automatically when you open an issue.
The summary explains the evidence; it doesn't guess. The model receives a fixed, redacted slice of the issue and nothing more: exception type and message, the top stack frame, event type, current priority and level, environment, event count, distinct users and paying customers affected, the last URL and runtime, first-seen time, and up to eight breadcrumb summaries that have already had emails, IP addresses, bearer tokens, and query-string secrets redacted and been clipped to 120 characters. It never sees customer IDs, IP addresses, Stripe metadata, or auth tokens, and it consults no outside sources — the answer is grounded in your project's state or it isn't given.
Two refusals are built in. Issues classified as noise don't run the model at all — they get a deterministic explanation naming the noise category, severity low, confidence high, because a model guessing about an ad-blocker block is how "Google Tag Manager failed to load" ends up labeled critical. And cross-origin script errors that carry no detail are labeled low severity, low confidence, since the browser hid the evidence.
The result is cached on the issue, so subsequent views render instantly; it regenerates only when you ask for a fresh read or the underlying error message changes. Every generation is recorded in an audit log.
Mute vs ignore
Two controls silence an issue, and they answer different questions.
| Mute alerts | Ignore | |
|---|---|---|
| What it says | "I know about this — stop interrupting me." | "This is unactionable — I never want to hear about it." |
| Mechanism | Per-issue alertsMuted toggle on the issue page. | Status set to ignored. |
| Issues list | Stays in the Open tab, fully visible. | Leaves the Open tab; keeps counting under the Ignored tab. |
| Email + Slack | Nothing fires, spikes included. | Nothing fires, spikes included. |
| Spike behavior | A genuine spike sets the quiet review chip in the dashboard — never a notification. | |
| Undo | Click the toggle again. | Click Unignore — the issue returns to the Open list. |
Use mute for an issue you're actively working on whose alerts you've already absorbed. Use ignore for patterns that will never be actionable — the counting continues, the interruptions stop, forever.
Related
- Capture an error — how errors get from your app into Crossdeck in the first place: automatic capture,
captureError(), levels, and category tags. - Web SDK error codes — the event types (
error.unhandled,error.http, …) that feed the fingerprint and the priority blend. - Source maps — upload maps so alerts and the issue page show readable frames instead of minified ones.