Standard OTLP in
Traces, logs and metrics arrive as OpenTelemetry. Nothing is proprietary, so an application already running an OTel SDK can point it here and skip our SDKs entirely — and can point it somewhere else later.
Developer documentation
SnagSpy takes OpenTelemetry from your application, groups errors into issues, correlates them with deploys, database work, service topology and logs, and tells you why something broke. Start with a guide below; the API reference has a page of its own, and every setting, every limit and an honest list of what it does not do are further down.
Everything below was checked against the running code rather than written from memory. If a capability is missing here, assume it is missing from the platform and say so — the section at the end lists the gaps deliberately.
The platform is a modular monolith in Go with a Nuxt dashboard. Two processes matter to you: ingest, which accepts telemetry, and the API, which serves everything you read. They are separate so a flood of telemetry cannot take the dashboard down with it.
Traces, logs and metrics arrive as OpenTelemetry. Nothing is proprietary, so an application already running an OTel SDK can point it here and skip our SDKs entirely — and can point it somewhere else later.
An issue carries the deploy that preceded it, the slow queries around it, the services upstream of it and the logs beside it.
AI investigations select and phrase; the platform computes. Every number in an answer is one you can check, and an answer that cannot be grounded is not given.
import { init, captureException } from "@snagspy/browser"
init({ dsn: globalThis._importMeta_.env.VITE_SNAGSPY_DSN, environment: "production" })
try { risky() } catch (error) { captureException(error) }Both SDKs redact secret-shaped values before anything leaves the process. That is not a server-side courtesy — a password sent to us and scrubbed on arrival has still crossed the network.
Both are thin ergonomics layers over OpenTelemetry rather than instrumentation libraries of their own. What they add is the part that is easy to get wrong: DSN handling, exception recording in the shape the platform groups on, and redaction before send.
| SDK | Package | Adds |
|---|---|---|
| Go | sdk/go/snagspy | DSN parsing, CaptureException, scrubbing, HTTP middleware. |
| Browser & Node | sdk/js | DSN parsing, error and unhandled-rejection capture, stack normalisation to V8’s shape, session replay, user feedback, scrubbing, a Vite plugin for source maps. |
The browser SDK normalises every engine’s stack to V8’s shape before sending, and carries debug ids in a span attribute. A debug id is 32 hexadecimal characters with no separators — the SDK strips the hyphens of a canonical UUID for you.
The ingest service is separate from the API and listens on its own port (:8081 by default; publish it as INGEST_PUBLIC_URL). It takes OTLP over HTTP, and every route authenticates with a DSN rather than a session.
Two buckets apply to every upload: a per-project request limit and a per-organisation event limit. Both are described on the API reference page.
The endpoints, the headers and a collector configuration are in the OpenTelemetry guide. →Replay is recorded with rrweb, and masking is inverted from rrweb’s defaults: text is masked unless explicitly allowed, rather than exposed unless explicitly masked. The masking happens in the browser before capture, because nothing server-side can undo an unmasked recording that has already arrived.
Content-Type: application/octet-stream
X-Replay-Session: <session id>
X-Replay-Seq: <0, 1, 2, …>
X-Replay-Encoding: json-zstdThe browser SDK sends these for you. The body is opaque compressed bytes that this platform contractually does not read. An absent or unparseable client clock is not an error — the server substitutes its own, because a recording with a wrong timestamp is still worth keeping.
Watching a recording takes owner scope, re-checks project consent at read time, and is written to the audit trail before the bytes are read.
Artifacts are uploaded per project from your build — the JavaScript SDK ships a Vite plugin that does it for you. The row lives in PostgreSQL and the bytes in an object store, addressed by content, so uploading the same map twice costs one copy.
Frames are resolved at read time, not at ingest. A frame that cannot be resolved says why rather than failing the page, so a missing upload degrades to a minified frame with an explanation instead of an error.
Usage is metered and reported. It is never acted on at the write path. Going past a plan figure does not throttle you, reject your telemetry or trigger a charge — there is no pay-as-you-go billing here at all. You are told once per signal per billing period, and the message says plainly that nothing has been throttled. The one thing a plan does decide is whether a signal is sold at all: a plan that does not include session replay refuses replay uploads, which is a different act from discarding telemetry somebody paid for.
A plan narrows the platform ceiling and never widens it. The ceilings are 90 days for errors and events and 30 days for trace spans; log lines follow the trace setting. An organisation may narrow its own retention in its own settings. The audit log is not customer-configurable.
A lapsed plan keeps its data for a further 14 days. Limits, seats and features revert the moment a paid period ends, because paying restores them; the retention window does not, because deleted error history does not come back.
Destinations are configured per organisation. A destination’s kind chooses the notifier, and a kind with no notifier behind it fails loudly rather than being skipped.
| Kind | Needs | Notes |
|---|---|---|
| Slack | An incoming-webhook URL | Carries a text field alongside the structured alert. |
| Discord | A webhook URL | Carries a content field. |
| Generic webhook | A URL | Outbound requests are checked against a dialler that refuses private and link-local addresses. |
| SMTP_* configured | A destination is not confirmed until the address proves itself. |
Record deploys from your pipeline, or connect GitHub or GitLab per project. Each provider is its own webhook adapter, and a deployment records which provider vouched for it rather than only that a webhook did.
A scheduled summary, set per organisation. You can preview the next one without sending it to anybody.
Create a check, then have your job call its check-in URL — the dashboard issues one per check and hands you a ready-made crontab line. The token in the URL is the credential, so the call is unauthenticated by design: a cron job on a box with no secrets can still report in.
# at the end of your job
curl -fsS "$SNAGSPY_CHECKIN_URL"A check that does not arrive within its grace raises an alert. Uptime monitors are the other direction: the platform probes a URL you name and records each result against the monitor.
An investigation reads one issue, alert or project and explains what happened. The division of labour is deliberate and worth knowing before you trust an answer: the model selects and phrases; the platform computes. Every figure in an answer comes from a query, not from the model, and an answer that cannot be grounded in one is not given.
The stack is Docker Compose: PostgreSQL, Redis, ClickHouse, the API, ingest, a worker and a one-shot migrate service. Everything is configured by environment.
DATABASE_URLREDIS_URLENVLOG_LEVELDASHBOARD_ORIGINAPI_PUBLIC_URLINGEST_PUBLIC_URLSESSION_COOKIE_NAMESESSION_TTLSECURE_COOKIESSHUTDOWN_TIMEOUTINGEST_REQUESTS_PER_MINUTEINGEST_EVENTS_PER_MINUTEAUTH_ATTEMPTS_PER_MINUTEINVESTIGATIONS_PER_HOURCLICKHOUSE_URLREAD_EVENTS_FROM_CLICKHOUSESECRET_ENCRYPTION_KEYARTIFACT_DIR / ARTIFACT_S3_*BACKUP_ENCRYPTION_KEYBACKUP_DIR / BACKUP_S3_* / BACKUP_WAL_SPOOLBACKUP_RETENTION_DAYS / BACKUP_MIN_ARTIFACTSANTHROPIC_API_KEY / ANTHROPIC_MODELGENIUSPAY_API_KEY / _API_SECRET / _WEBHOOK_SECRETGENIUSPAY_BASE_URLSMTP_HOST / _PORT / _USERNAME / _PASSWORD / _FROMOPERATOR_WEBHOOK_URLACCOUNT_ALERT_WEBHOOK_URLOTLP_ENDPOINTcmd/apicmd/ingestcmd/workercmd/alertscmd/migratecmd/seedcmd/plancmd/retentioncmd/purgecmd/erasecmd/refingerprintcmd/backfillcmd/backupMigrations are embedded in the binary. A new .sql file does nothing until the migrate image is rebuilt, and the container still logs “migrations applied” either way. Check the schema version rather than the log line.
The sub-processor register and the retention ceilings are published separately and are part of the same commitment: see the security and privacy pages.
Listed rather than omitted. A developer who cannot find a feature assumes they missed it, and finds out the expensive way.