Build a Sentry alternative
Build a Sentry alternative as an ingest endpoint and fingerprint grouping. Source maps, tracing, and session replay are the other product. Here is the split.
You can build a Sentry alternative as an ingest URL and a grouped issue list. The catalog calls Sentry that loop, not an APM suite.
You pay $26–$89 a month. The verdict is “Own your error data”.
Yesterday I wrote about should I vibe code Figma. That was a regular backlog post. This one is a teardown.
Build a Sentry alternative as an ingest endpoint
The Sentry page splits the work into three rows.
The first row is 3 days. Ingest endpoint, fingerprint grouping, issue list, alerts. That is a POST URL, a hash of the crash, a table of issues, and one email when a new issue appears. You can vibe-code this.
The second row is 2 weeks and up. Releases, regression reopening, thresholds, secret scrubbing. Still your own apps. More screens, a release string on each event, a cap that drops flood traffic.
The third row is 3 months and up. Source maps, an SDK for every language, tracing, session replay. Skip that row.
If you tell an agent to “clone Sentry”, it aims at the third row. The useful v1 is the first one.
Here is the shape of the data. Nothing else has to exist at 3 days.
const event = {
id: 'evt-slot-login',
projectId: 'crashpad-slot',
release: '1.4.2',
exception: {
type: 'TypeError',
value: 'Cannot read properties of undefined (reading "plan")',
},
frames: [
{ module: 'slot/login', function: 'submitReset', inApp: true },
{ module: 'astro/actions', function: 'callAction', inApp: false },
],
}
const issue = {
id: 'iss-slot-plan',
fingerprint: 'a1c3e9',
state: 'unresolved',
firstRelease: '1.4.2',
lastRelease: '1.4.2',
eventCount: 1,
}
An event, a fingerprint, and an issue with unresolved, resolved, and ignored states. Releases can wait a week if you only have one app.
The audience line on the page is a developer running a few production apps who wants error reports on infrastructure they control. If that is you, an ingest URL and a list are enough.
The same v1 also replaces Rollbar, Bugsnag, and Honeybadger. UptimeRobot is the ping cousin at $24–$69 a month. Cronitor watches heartbeats at $10–$49. When the crash is already an incident, that is Statuspage at $29–$399, and I wrote about building a Statuspage alternative earlier this month.
How does Sentry work
Sentry publishes the path of an event. I am summarizing those docs, not reconstructing their database.
An SDK posts an envelope to /api/{PROJECT_ID}/envelope/. The older /store/ endpoint is deprecated. An envelope is newline-delimited JSON: a header, an item header, then the event.
The first hop is Relay. The architecture overview shows your app hitting a load balancer, then Relay, then Kafka. Relay validates the DSN and project, reads project config from Redis (and caches it in memory), applies quotas, and publishes a normalized payload.
A Sentry ingest consumer reads that topic and starts preprocess-event. That task may call Symbolicator, then process-event, then save-event. The saved event goes to another Kafka topic. Snuba consumes it and writes ClickHouse. Postgres holds the issue row, the group. Alerts fire from a post-process task, not from the ingest request.
The self-hosted data flow is the same sequence with nodestore in the middle for the raw payload. Treat exact 2026 cluster sizes as an educated guess. Persist the event, attach a group, then alert.
A 3-day v1 does not need Kafka. One Worker, one D1 database. The catalog’s stack is Astro, Workers, and D1. Use that, or Rails if that is your weekday language.
How Sentry fingerprint grouping works
A thousand identical crashes should become one issue. The grouping docs are the public map.
Relay stamps the event with the current grouping config the moment it arrives. If the client did not set a fingerprint, stack traces go through normalize_stacktraces_for_grouping first.
That pass marks frames in-app or not. Server fingerprint rules can override the default, or fold {{ default }} back into it. The grouping algorithm itself runs only when the fingerprint is still empty or still uses that placeholder.
The algorithm can produce more than one hash. Sentry stores those on a GroupHash model. An event may get an “app” hash from in-app frames and a “system” hash from the full stack. Either hash matching an existing group is enough. If none match, Sentry creates a group immediately. The docs say 100% of events get a group, because the UI expects one.
Creating a group is expensive on their side because alerts and regression checks run from that path.
SaaS Sentry now adds an AI fallback after the hash lookup misses. Their Issue Grouping post says an embedding of the message and in-app frames can merge a near-miss. Skip that model in week one and hash the crash yourself.
The catalog’s first hard rule matches their public one. Compute the fingerprint on the server. A client that sends fingerprint: ["unique-every-time"] should not get a vote.
function fingerprint(event) {
const frames = event.frames
.filter((frame) => frame.inApp)
.map((frame) => `${frame.module}:${frame.function}`)
const message = event.exception.value
.replace(/\d+/g, 'N')
.replace(/[0-9a-f-]{36}/gi, 'UUID')
.replace(/"[^"]+"/g, '"STR"')
return hash([event.exception.type, ...frames, message].join('|'))
}
Keep module and function names. Drop line and column numbers. They shift every deploy. Strip hex ids and quoted strings so user 4821 not found and user 977 not found land together. Attach the event under a uniqueness constraint so two concurrent first events do not create twins.
Sentry’s fallback, when there is no stack, is message grouping. They take the first line, replace numbers and UUIDs, and hash that, which creates a lot of groups. Use a stack when you have one.
How Sentry ingest authentication works
The catalog’s first rule is the same as Sentry’s public one. Authenticate every ingest request with a per-project key.
A DSN looks like {PROTOCOL}://{PUBLIC_KEY}@{HOST}/{PROJECT_ID}. Sentry’s DSN explainer says the secret portion is optional and effectively deprecated. The DSN only allows writes. It does not read issues. They still tell you to rotate it, and to configure it dynamically if you can, because anyone who has it can send junk events.
The authentication spec wants this header:
X-Sentry-Auth: Sentry sentry_version=7, sentry_key=PUBLIC_KEY
You can send the same fields as query string parameters when a header is awkward. Protocol version 7 is required. sentry_client is recommended, like crashpad.js/0.1. If both the header and the envelope dsn field are present they must match. If both are missing, Relay rejects the envelope with 403.
function projectFromRequest(req, projects) {
const key = sentryKey(req)
const project = projects.find((item) => item.publicKey === key)
if (!project) {
throw new Error('rejected')
}
return project
}
function sentryKey(req) {
const header = req.headers['x-sentry-auth'] || ''
const match = /sentry_key=([^,\s]+)/.exec(header)
if (match) return match[1]
return req.query.sentry_key
}
Unknown keys and over-limit requests should look the same to the sender. Do not confirm that a project exists.
The mini-prompt on the page is “An ingest endpoint that drops traffic on purpose”. Cap payload size before you parse the whole body. Cap events per project per window. Traffic over the cap is dropped, not queued, because a backlog of the same Slot TypeError helps nobody.
async function acceptEvent(db, project, body) {
if (body.byteLength > project.maxBytes) {
await db.bumpDropped(project.id)
return { status: 413 }
}
const used = await db.incrementWindow(project.id)
if (used > project.rateCap) {
await db.bumpDropped(project.id)
return { status: 429 }
}
return { status: 200 }
}
Respond fast, because slow error reporting degrades the app you are watching, and show the dropped count on the project page so you know the cap fired.
Sentry’s own edge does something similar. Relay’s dynamic sampling notes run inbound filters, then quota, then sampling. Quotas count before sampling. Dynamic sampling itself applies to spans and transactions, not errors. Your v1 can skip sampling and still drop a flood.
Scrub before you store. Sentry scrubs in the SDK, on arrival, and optionally in a self-hosted Relay before data leaves your network. The catalog rule is the same: walk keys named password, secret, token, authorization, and mask card-shaped numbers. Never persist the raw payload, including in logs.
Source maps, tracing, and session replay
The skip row is Sentry the platform.
Symbolicator is their service for turning minified and native stacks into something you can read. The Symbolicator docs list separate endpoints for native frames, minidumps, JavaScript source maps, and JVM ProGuard files. JavaScript needs the minified file plus a source map, usually uploaded as an artifact bundle with a debug id. Native needs DWARF or PDB files, then a SymCache. They run separate Symbolicator pools per platform so JavaScript volume does not starve native work. Sentry’s public source maps guide is that upload step. Without it, a browser event points at app.min.js:1:84921.
Session Replay is a different product. The browser replay docs record DOM state, clicks, and network calls, then play them back. Default privacy masks text, images, and inputs.
Distributed tracing is the other skip. Cross-service spans, sampling, and a waterfall UI. The catalog’s non-goal already names it: do not build APM, distributed tracing, session replay, or SDKs for languages you do not use.
A tiny SDK for the one language you write is enough. fetch the envelope and include the stack you already have.
A Sentry alternative for a few production apps
I would rebuild Sentry for a couple of apps I deploy myself, where the current monitoring is a log file I open after a customer emails. One project per app, a key per project, fingerprints computed on the server, and emails for new issues plus regressions after I mark something resolved.
Regression is a release comparison, not a forever reopen. The catalog rule: reopen only when the new event’s release is newer than the resolving one. An old server still running 1.4.1 should stay quiet. Compare semver when the strings parse. Fall back to the order you first saw each release when they are git SHAs. Ignored issues keep counting and never alert.
I would host it next to the product, on Astro, Workers, and D1, because that is the stack on the card. Use Rails if that is your weekday language. Add release tags once you ship more than one version a week. I would not build a replay player.
If the rent is “is the site up”, that is UptimeRobot, 6 hours for cron HTTP checks and an email. A dead cron is Cronitor. A public status note is Statuspage.
Start with the first row
Go to Build your own Sentry. Copy the AHA prompt, or the stack you already write.
Tell the agent the first estimate only: ingest endpoint, fingerprint grouping, issue list, alerts. Three days.
If the company already lives in traces and session replay, keep paying.