โ† All posts
ENGINEERING August 30, 2026 ยท 3 min read

Every signal knows whose it is

SR
Sentrinel Team
Product & engineering

The question that ends most incidents is not "what broke?" It is "who did it break for?" โ€” and its twin, "show me everything this person did." A monitoring tool that cannot answer the second one leaves you with a stack trace and no story.

Sentrinel answers it with a rule that sounds too simple to be a design decision: every signal stores the user it belonged to. Requests, application logs, errors, traces, product events, session replays โ€” each row carries the consumer identifier directly. None of them finds out later by joining back through the request.

Why not just join

The tempting design is to put the user on the request only. A log line has a request id; a trace has a request id; a replay has a session id that maps to requests. Follow the id back to the request row and read the user off it. One column, no duplication.

The problem is that those tables expire on different clocks. Request logs are the bulkiest thing in the store and have the shortest retention. Errors and issues live longer, because a bug from three weeks ago is still a bug. Replays are large and live for two weeks. Traces sit somewhere in between.

A trail assembled by join is only as long as the shortest-lived table in the chain. After the request rows age out, the error is still there, the log lines are still there โ€” and they no longer know who they belonged to, because the row that knew is gone. The answer to "show me everything this user did" quietly gets shorter as time passes, and nothing tells you it did.

A partial answer to that question is worse than no answer. It looks complete.

What it looks like

From a consumer's page you get the full record: their requests, the errors they hit, the log lines written while serving them, the traces behind their slow calls, the events they fired, and โ€” on the web โ€” the replay of what they saw when it broke. Each of those was stored with the identifier at write time, so each one is still attributable on its own retention clock, independently of the others.

The same identifier flows the other way. From an issue you see which users it hit and how many times each; from a replay you can jump to the request it captured and the trace behind that request.

Where the identifier comes from

The backend plugin resolves it per request from whatever your app uses โ€” a header, a decoded token, a session โ€” through a consumerIdentifier function you supply, and stamps every signal produced while that request is in flight. Logs written through the plugin's logger inside the request pick it up automatically; that is what the request-scoped log context is for.

On mobile, identify() sets it for the session and every subsequent request, event and log carries it. On the web, the browser SDK forwards it through the tunnel so the backend stamps it the same way.

The important property is that a single call at the boundary is enough. You do not annotate each log line, each span, each event. Anything produced downstream of that call already knows.

The bit we got wrong first

Application logs and traces originally did not carry the identifier โ€” they inherited it by join, exactly the design described above. It worked in every demo, because demos do not run for six weeks. It was the retention mismatch, worked through on paper, that made the case: a log line that says "card declined" is useful for as long as the log line exists, and only if it still knows whose card.

So we added the column to logs, replays and traces, and made the plugin write it. The migration was small. The change in what the product can promise was not: "everything this user did" now means everything, for as long as any of it is retained.

The limit

None of this works if the identifier is missing. Anonymous traffic is stored as anonymous, and a request with no resolvable user cannot be attributed later by any cleverness downstream. The place to invest is the consumerIdentifier function โ€” make it return something for every authenticated request, and the rest of the product fills in around it.