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. This page is the complete reference: every endpoint, every setting, every limit — and an honest list of what it does not do.
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 "github.com/rellinxe/snagspy/sdk/go/snagspy"
client, err := snagspy.New(snagspy.Options{
DSN: os.Getenv("SNAGSPY_DSN"),
Environment: "production",
Release: buildSHA,
})
if err != nil {
log.Fatal(err)
}
defer client.Close(context.Background())
if err := doWork(); err != nil {
client.CaptureException(context.Background(), err)
}import { init, captureException } from "@snagspy/browser"
init({
dsn: globalThis._importMeta_.env.VITE_SNAGSPY_DSN,
environment: "production",
release: __BUILD_SHA__,
})
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). Every route here authenticates with a DSN, not a session.
| Method | Path | Body |
|---|---|---|
| POST | /v1/traces | OTLP trace payload. Spans carrying an exception become issues. |
| POST | /v1/logs | OTLP logs payload. |
| POST | /v1/metrics | OTLP metrics payload. |
| POST | /v1/replay | One replay chunk. See below. |
| GET | /healthz | Liveness. |
Two buckets apply to every upload: a per-project request limit and a per-organisation event limit. Both are described under Rate limits.
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.
POST /v1/replay
Content-Type: application/octet-stream
X-Replay-Session: <session id>
X-Replay-Seq: <0, 1, 2, …>
X-Replay-Encoding: json-zstd
X-Replay-Recorded-At: <RFC 3339, optional>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.
Upload artifacts to POST /v1/projects/{projectID}/artifacts. 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.
Served by the API process on :8080 by default. Everything is under /v1. Responses are JSON; errors carry a stable code, a human-readable message and a request_id.
| POST | /v1/auth/register | Create an account. Returns no session — log in afterwards. |
| POST | /v1/auth/login | Exchange credentials for a session cookie. |
| POST | /v1/auth/logout | End the current session. |
| GET | /v1/me | The signed-in user and their organisation. |
| GET | /v1/members | Members of the organisation. |
| GET | /v1/projects | List projects. |
| POST | /v1/projects | Create a project. |
| GET | /v1/projects/{projectID} | One project. |
| GET | /v1/projects/{projectID}/dsns | The project’s ingest keys. |
| POST | /v1/projects/{projectID}/dsns | Issue a new DSN. |
| DELETE | /v1/projects/{projectID}/dsns/{keyID} | Revoke a DSN. |
| PUT | /v1/projects/{projectID}/session-replay | Turn replay capture on or off for the project. |
| GET | /v1/api-keys | List API keys. |
| POST | /v1/api-keys | Create an API key. The secret is shown once. |
| DELETE | /v1/api-keys/{keyID} | Revoke an API key. |
| GET | /v1/settings | Organisation settings, including retention. |
| PATCH | /v1/settings | Change organisation settings. |
| GET | /v1/audit-log | The audit trail. Not customer-configurable. |
Available only when API_PUBLIC_URL is set. Without it every route here answers sso_unavailable, and the only other sign is a missing “single sign-on enabled” line at boot.
| POST | /v1/auth/sso/start | Begin an SSO login. |
| GET | /v1/auth/sso/callback | Identity-provider callback. |
| GET | /v1/settings/sso | The current connection. |
| PUT | /v1/settings/sso | Configure the connection. |
| DELETE | /v1/settings/sso | Remove the connection. |
| GET | /v1/settings/sso/domains | Claimed email domains. |
| POST | /v1/settings/sso/domains | Claim a domain. |
| POST | /v1/settings/sso/domains/verify | Prove a claimed domain. |
| GET | /v1/issues | List issues, filtered and paged. |
| GET | /v1/issues/{issueID} | One issue. |
| GET | /v1/issues/{issueID}/events | Occurrences of an issue. |
| PATCH | /v1/issues/{issueID} | Change status (resolve, ignore, reopen). |
| GET | /v1/users | End users seen in telemetry. |
| GET | /v1/feedback | User feedback submissions. |
| POST | /v1/feedback | Submit user feedback. |
| GET | /v1/traces | Trace list. Filters on spans, aggregates whole traces. |
| GET | /v1/traces/{traceID} | One trace with its spans. |
| GET | /v1/services | Service list with health. |
| GET | /v1/services/map | Observed service topology. |
| GET | /v1/services/structure | Structural facts about the topology. |
| GET | /v1/services/dependencies | Edges between services. |
| GET | /v1/services/approaching | Metrics climbing toward the threshold, with the slope. |
| GET | /v1/traffic | Traffic by project rather than by slowest service. |
| GET | /v1/queues | Queue and worker behaviour. |
| GET | /v1/anomalies | Detected anomalies. |
| GET | /v1/endpoints/surges | Per-endpoint abuse signal, no caller identity. |
| GET | /v1/database/slow-queries | Slow database queries. |
| GET | /v1/database/query-patterns | Query patterns. |
| GET | /v1/logs | Search log lines. |
| GET | /v1/logs/patterns | Recurring log shapes. |
| GET | /v1/logs/surges | Log volume surges. |
| GET | /v1/metrics/names | Metric names seen. |
| GET | /v1/metrics/series | Points for a named series. |
| GET | /v1/projects/{projectID}/replay | Recorded sessions. |
| GET | /v1/projects/{projectID}/replay/{sessionID} | One recording’s chunks. Owner scope, re-checks consent, and writes the audit trail before the bytes are read. |
| POST | /v1/deployments | Record a deployment. |
| GET | /v1/deployments | List deployments. |
| GET | /v1/deployments/{deploymentID} | One deployment. |
| GET | /v1/deployments/{deploymentID}/health | Did this deploy make things worse, with the arithmetic. |
| GET | /v1/releases | Releases, derived from deployments rather than stored. |
| GET | /v1/projects/{projectID}/integrations | Configured VCS integrations. |
| PUT | /v1/projects/{projectID}/integrations/{provider} | Configure github or gitlab. |
| DELETE | /v1/projects/{projectID}/integrations/{provider} | Remove one. |
| POST | /v1/projects/{projectID}/artifacts | Upload a source map or debug artifact. |
| GET | /v1/projects/{projectID}/artifacts | List artifacts. |
| DELETE | /v1/projects/{projectID}/artifacts/{artifactID} | Delete one. |
| GET | /v1/alerts | Alert history. |
| GET | /v1/alert-destinations | Where alerts are sent. |
| POST | /v1/alert-destinations | Add a destination. |
| DELETE | /v1/alert-destinations/{destinationID} | Remove one. |
| GET | /v1/reports/schedule | The digest schedule. |
| PUT | /v1/reports/schedule | Set it. |
| DELETE | /v1/reports/schedule | Stop it. |
| GET | /v1/reports/preview | Render the next digest without sending it. |
| GET | /v1/cron/checks | List cron checks. |
| POST | /v1/cron/checks | Create one. |
| PATCH | /v1/cron/checks/{checkID} | Change schedule or grace. |
| DELETE | /v1/cron/checks/{checkID} | Delete one. |
| GET | /v1/cron/checks/{checkID}/pings | Check-in history. |
| POST | /v1/cron/checks/{checkID}/rotate | Rotate the check-in token. |
| GET | /v1/checkins/{token} | Check in. Unauthenticated — the token is the credential. |
| POST | /v1/checkins/{token} | Check in, with a body. |
| GET | /v1/uptime/monitors | List uptime monitors. |
| POST | /v1/uptime/monitors | Create one. |
| PATCH | /v1/uptime/monitors/{monitorID} | Change one. |
| DELETE | /v1/uptime/monitors/{monitorID} | Delete one. |
| GET | /v1/uptime/monitors/{monitorID}/checks | Probe results. |
| GET | /v1/dashboards | List dashboards. |
| POST | /v1/dashboards | Create one. |
| GET | /v1/dashboards/{dashboardID} | One dashboard. |
| PATCH | /v1/dashboards/{dashboardID} | Rename or re-describe. |
| PUT | /v1/dashboards/{dashboardID}/widgets | Replace the widget set. |
| DELETE | /v1/dashboards/{dashboardID} | Delete one. |
Every route here needs ANTHROPIC_API_KEY. Without it the feature is off rather than failing per request.
| POST | /v1/issues/{issueID}/investigate | Investigate an issue. |
| GET | /v1/issues/{issueID}/investigation | The result. |
| POST | /v1/alerts/{alertID}/investigate | Investigate an alert. |
| GET | /v1/alerts/{alertID}/investigation | The result. |
| POST | /v1/projects/{projectID}/investigate | Investigate a project. |
| POST | /v1/projects/{projectID}/explain | Ask a question about the topology. |
| GET | /v1/investigations | List investigations. |
| GET | /v1/investigations/{investigationID} | One investigation. |
GET /v1/usage is always available. POST /v1/checkout exists only when GENIUSPAY_API_KEY is set — unset, the route is not mounted at all and answers 404 rather than 401, because “there is nothing here” is the honest answer.
| GET | /v1/usage | Metered usage against the plan, with a caveat in every response. |
| POST | /v1/checkout | Start a purchase. Org-admin scope. Returns a hosted checkout URL. |
| GET | /healthz | Liveness. On both the API and the ingest service. |
| GET | /readyz | Readiness, including dependencies. |
| Credential | Used for | How |
|---|---|---|
| DSN | Writing telemetry | In the SDK config. Public by design — it can write and cannot read. |
| Session cookie | The dashboard | POST /v1/auth/login. The cookie is slk_session and lasts 720h by default. |
| API key | Scripting the dashboard API | Authorization header. Created at POST /v1/api-keys, shown once. |
| Check-in token | Cron check-ins | In the URL. The token is the credential, so rotate it with POST /v1/cron/checks/{checkID}/rotate. |
| SSO | The dashboard, for a claimed domain | Needs API_PUBLIC_URL. Trusts a provider only for the email domain its connection claims, and only for accounts already invited. |
Cookie-authenticated requests are Origin-checked. A request without an Origin matching DASHBOARD_ORIGIN is refused with origin_rejected (403). Scripting with curl against a local stack therefore needs -H "Origin: http://localhost:3000".
Rate limits are abuse control, and they are the one thing here that really does refuse a request. They have nothing to do with what your plan includes: going past a plan figure never throttles you and never rejects telemetry. Redis is authoritative so every instance shares a bucket, with a per-process fallback that keeps limiting alive if Redis is unavailable.
| Limit | Default | Scope |
|---|---|---|
INGEST_REQUESTS_PER_MINUTE | 600 | Per project |
INGEST_EVENTS_PER_MINUTE | 30,000 | Per organisation |
AUTH_ATTEMPTS_PER_MINUTE | 10 | Per caller |
INVESTIGATIONS_PER_HOUR | 100 | Per organisation |
A refused request answers 429 with Retry-After.
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.
| Plan | Price | Errors | Logs | Metric points | Replay units | Investigations | Retention |
|---|---|---|---|---|---|---|---|
| free | Free | 5k | 25k | Not offered | Not offered | 10 | 7 days |
| team | $25/mo | 50k | 250k | 250k | 25k | 250 | 30 days |
| business | $75/mo | 65k | 1M | 1M | 250k | 2.5k | 90 days |
| enterprise | Custom | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited | 90 days |
This table is generated from the same constant the platform bills against, so it cannot advertise a figure the product does not use.
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 PATCH /v1/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 at /v1/alert-destinations. 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 with POST /v1/deployments, or connect GitHub or GitLab at /v1/projects/{projectID}/integrations. 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 at /v1/reports/schedule. Preview the next one without sending it at /v1/reports/preview.
Create a check, then have your job call its check-in URL. The token in the URL is the credential, so the endpoint 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 https://ingest.example.com/v1/checkins/<token>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 the result at /v1/uptime/monitors/{monitorID}/checks.
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_URL | PostgreSQL connection string. No default. |
REDIS_URL | Redis, used for rate limiting. No default. |
ENV | development (default) or production. Production refuses sandbox payment deliveries. |
LOG_LEVEL | info by default. |
DASHBOARD_ORIGIN | Where the dashboard is served. Cookie-authenticated requests must send a matching Origin or get origin_rejected. |
API_PUBLIC_URL | Switches single sign-on on. Unset, SSO answers sso_unavailable. |
INGEST_PUBLIC_URL | The endpoint published in DSNs. |
SESSION_COOKIE_NAME | slk_session by default. |
SESSION_TTL | 720h by default. |
SECURE_COOKIES | Set for HTTPS deployments. |
SHUTDOWN_TIMEOUT | 15s by default. |
INGEST_REQUESTS_PER_MINUTE | 600 by default, per project. |
INGEST_EVENTS_PER_MINUTE | 30000 by default, per organisation. |
AUTH_ATTEMPTS_PER_MINUTE | 10 by default. |
INVESTIGATIONS_PER_HOUR | 100 by default. |
CLICKHOUSE_URL | Enables the ClickHouse event store. Its presence also switches reads to it. |
READ_EVENTS_FROM_CLICKHOUSE | Derived from CLICKHOUSE_URL. Run the backfill before flipping it on a database with history. |
SECRET_ENCRYPTION_KEY | Required by anything storing an encrypted secret, or it answers secrets_not_durable. |
ARTIFACT_DIR / ARTIFACT_S3_* | Where source-map artifacts live. A service pointed at the wrong store fails quietly. |
BACKUP_ENCRYPTION_KEY | Required by every backup command. A different value makes existing artifacts unreadable. |
BACKUP_DIR / BACKUP_S3_* / BACKUP_WAL_SPOOL | Backup destination and write-ahead spool. |
BACKUP_RETENTION_DAYS / BACKUP_MIN_ARTIFACTS | How much history to keep. |
ANTHROPIC_API_KEY / ANTHROPIC_MODEL | AI investigations. Off entirely when unset. |
GENIUSPAY_API_KEY / _API_SECRET / _WEBHOOK_SECRET | Buying a plan. The key is the switch: unset, neither checkout nor the webhook route is mounted. |
GENIUSPAY_BASE_URL | Defaults to the merchant API base. |
SMTP_HOST / _PORT / _USERNAME / _PASSWORD / _FROM | Email alert delivery. |
OPERATOR_WEBHOOK_URL | Operator alerts about this deployment. Deliberately unset by default, so it sends nowhere. |
ACCOUNT_ALERT_WEBHOOK_URL | Commercial alerts about customers. Falls back to OPERATOR_WEBHOOK_URL. |
OTLP_ENDPOINT | Where this platform sends its own telemetry. |
cmd/api | The dashboard API. |
cmd/ingest | The telemetry ingest service. A separate process on purpose. |
cmd/worker | Scheduled jobs — retention, purge, digests, alert evaluation. |
cmd/alerts | Evaluate alerts once, by hand. |
cmd/migrate | Apply migrations. Embedded in the binary, so a new .sql file does nothing until the image is rebuilt. |
cmd/seed | Build the local fixture organisation and projects. |
cmd/plan | Assign a plan to an organisation. An operator action, and audited. |
cmd/retention | Apply retention now. |
cmd/purge | Delete telemetry belonging to organisations that no longer exist. |
cmd/erase | Erase one subject’s data on request. |
cmd/refingerprint | Re-key issue grouping across history. |
cmd/backfill | Copy existing events into ClickHouse before switching reads to it. |
cmd/backup | Take an encrypted backup. |
Migrations 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.