Reporting errors from your jobs
Send your job's stderr with a failure ping and PulseWatch parses the stack trace, groups it with identical failures, and puts the exception type, the failing line and a status (new, recurring, or a regression) directly in the alert. This is deterministic parsing: regex and hashing, no AI.
Sending stderr
# simplest: last 8 KB of combined output on failure ./job.sh 2>&1 | tail -c 8000 | curl -fsS --data-binary @- "https://pulsewatcher.up.railway.app/ping/<token>/fail"
Any non-JSON POST body works. A JSON body is treated as metrics instead, so send plain text for traces.
From application code
Catch the exception, post the stack trace to /fail, then re-raise so your own error handling still runs.
PING_URL="https://pulsewatcher.up.railway.app/ping/<token>" ./job.sh 2>&1 | tail -c 8000 | curl -fsS --data-binary @- "$PING_URL/fail"
Size caps per plan
We buffer at most the largest plan cap and read-and-discard the rest, so a huge upload is never held in memory. Anything past your cap is dropped, and the excerpt is flagged as truncated.
| Free | 4 KB |
| Pro | 16 KB |
| Business | 64 KB |
What we scrub, and what we store
Before anything is written down we strip ANSI colour codes and control characters, then redact anything that looks like a secret, replacing it with [REDACTED:kind].
| Passwords & tokens | password=, secret:, api_key, token, authorization and friends (including prefixed keys like db_password) |
| Authorization headers | Bearer … and Basic … (the scheme is kept, the credential is not) |
| JWTs | three-segment eyJ… values |
| Private keys | the whole -----BEGIN … PRIVATE KEY----- block |
| Connection strings | postgres://user:PASSWORD@host (the host is kept so you can still debug) |
| Cloud keys | AWS AKIA… ids and long high-entropy values after = or : |
| Card numbers | Luhn-valid 13–19 digit runs |
| Emails & IPs | on by default; Pro+ can turn this off when traces need them |
Scrubbing is best-effort, not a guarantee, so do not deliberately print secrets. Traces stay internal to your account. They appear in your dashboard and alerts, and are never shown on public status pages. See the privacy policy for retention.
Supported languages
| python | Traceback blocks, chained exceptions (the last one is reported) |
| node | at fn (file:line:col) frames, async stacks, Caused by / cause chains |
| java | at pkg.Class.m(File.java:N), Caused by chains (deepest cause wins), "... N more" |
| go | panic: / fatal error:, goroutine blocks, paired function + location lines |
| ruby | file.rb:N:in 'method': message (ExceptionClass) plus from frames |
| php | PHP Fatal error / Uncaught X, #N stack lines, "thrown in ... on line N" |
| dotnet | System.X: message, ---> inner exceptions, "in File.cs:line N" |
| shell | command not found, permission denied, and similar shell failures |
| unknown | anything else; a message heuristic is kept, exception type is null |
Output with no recognizable signature still produces a group: the language is unknown, the exception type is null, and the message falls back to the first error-ish line (or the last non-empty one).
How grouping works
A fingerprint is a hash of the language, exception type, the normalized message, and the file and function of the deepest frame that is not vendor or standard-library code. The line number is deliberately excluded, because it shifts whenever you edit anything above the failure. Including it would split one bug into many groups.
Normalizing replaces everything variable with a placeholder: numbers, hex, UUIDs, timestamps, quoted strings, paths, URLs, IPs, emails and durations. That means:
Timeout after 30s connecting to 10.0.0.5 Timeout after 45s connecting to 10.0.0.9 -> both become: timeout after <dur> connecting to <ip> SAME group KeyError: 'user_id' KeyError: 'order_id' -> both become: <str> SAME group KeyError: 'user_id' vs ValueError: 'user_id' -> exception type differs DIFFERENT groups
Grouping KeyError on different keys together is intended: it is usually one bug. When a group really is too broad, or just noisy, mute it. That is the escape hatch.
Alerts, escalation and muting
- New error. The first time a fingerprint appears it raises an alert, even if the monitor is already down. A new failure mode during an outage is news. Turn this off per monitor with
alert_on_new_error. - Escalation. Set
escalate_after_consecutive(minimum 2) and the same fingerprint failing N runs in a row sends onemonitor.error_persistingalert per threshold crossing, not one per run. - Muting. A muted group raises no error alerts and collapses in the UI. Your monitor's own up/down alerting is unaffected. Optional auto-unmute after N days.
- Dedup. At most one error-related alert per monitor per 60 seconds; several new fingerprints at once batch into a single message.
Webhook consumers get the full detail in details.error_group: see the webhooks page for the field table.