The score exists to be compared against itself. It is a number you can watch move as you fix things, and gate a pull request on — not a benchmark against other projects.
One entry point
Every entry point starts at 100 and loses the weight of each requirement it fails, floored at 0.
| Requirement | Weight |
|---|---|
wide-event | 40 |
audit | 25 |
structured-errors | 20 |
page-error-handling | 20 |
context | 15 |
error-handling | 15 |
A rule that passes costs nothing. A rule that reports n/a costs nothing either — it never applied, so it is not held against the entry point. Opportunities carry no weight at all and cannot appear here.
A check you disabled with a comment is also n/a, so it costs nothing — and the report counts how many, because a score that is partly the result of disabled checks has to say so.
Take the login handler from the playground:
CHECKS
✓ useLogger wide event emitted per request
✓ log.set context attached with log.set()
✗ log.audit sensitive action with no audit trail
structured-errors and error-handling were n/a — the handler throws nothing and catches nothing. One requirement failed, so the score is 100 − 25 = 75.
wide-event at 40 outweighs any pair of the smaller rules.Which makes the arithmetic legible in both directions. Here is the worst entry point in the playground going from 20 to 100, one line at a time, each fix worth exactly the weight of the rule it satisfies:
The project score
The global score is the average of every entry point, weighted by how much a blind spot there would cost you:
| Entry point | Weight |
|---|---|
| Flagged money or auth | ×2 |
| Page | ×0.5 |
| Everything else | ×1 |
Pages weigh less because a page that swallows a fetch error is a worse user experience than an observability hole, and they are usually the most numerous files in an app. Sensitive handlers weigh double because that is where you will need the event.
Three entry points — a sensitive handler at 75, a plain handler at 100, and a page at 50:
(75 × 2) + (100 × 1) + (50 × 0.5) 275
───────────────────────────────── = ───── = 79
2 + 1 + 0.5 3.5
A project with no entry points at all scores 100.
Grades
| Score | Grade |
|---|---|
| 90–100 | excellent |
| 70–89 | good |
| 50–69 | needs work |
| 0–49 | at risk |
Coverage classification
Alongside the score, each entry point is classified — this is what the summary block in the JSON counts, and what the coverage rows in the report are built from.
| Class | When |
|---|---|
instrumented | A handler that passes both wide-event and context, or a page that handles its fetch errors |
partial | A handler that passes one of the two |
dark | A handler that passes neither, or a page that swallows fetch errors |
exempt | evlog infrastructure — every rule is n/a, so it sits at 100 and is counted apart from your own entry points |
dark is the number to watch. A dark entry point produces nothing you can query: when it misbehaves, the only evidence you will have is the user's description of it.
Sensitivity
Sensitivity is what promotes an entry point to double weight and adds the audit requirement, so it is worth knowing exactly how it is decided.
Money — $
- Imports
stripe,@stripe/stripe-js,paddle-sdk, or@lemonsqueezy/lemonsqueezy.js - Path contains
checkout,payment,billing,invoice,refund,subscription,charge, orpayout
Auth — A
- Imports
better-auth,next-auth,lucia,@auth/core, or@auth/nextjs - Path contains
auth,oauth,login,logout,signin,signup,register,password,token,session,mfa, orotp
PII — @
Fields matching email, phone, address, ssn, or iban and a write call (create, update, insert, upsert) in the same handler.
Money and auth are high sensitivity: double weight, and an audit trail is required. PII is medium: no extra requirement, but it is marked in the report.
How the matching works
Two decisions keep this from producing noise.
Imports are read from the AST. A package counts only when it is genuinely imported, so a // TODO: drop stripe comment does not make a route handle money.
Path terms match whole words, allowing a plural. /api/authors is not an auth route, and /api/refunds is a money route. This matters more than it looks: a wrongly flagged route is handed a 25-point requirement it has no reason to satisfy, and counts double in the average — the fastest way to make the whole number untrustworthy.
Every reason is shown in full when you inspect an entry point, so a wrong call is one line to spot:
FLAGGED SENSITIVE BECAUSE
▍ auth: path says "auth"
Reading the score honestly
- A high score is not proof of good observability. It says the shape is there: an event per entry point, context attached, errors explainable, sensitive actions audited. Whether the context you attach is the context you will need is a judgement no static analyser can make. Best practices →
- Compare runs, not projects. A 200-route app that scores 70 is in better shape than a 6-route app that scores 70.
- Watch
darkandMoney & authrather than the headline number. Those are the two places where the next incident is hiding.
Next
Rules
Every check evlog map runs — the six requirements that move the score, the four suggestions that never do, what satisfies each one, and how evlog helpers are recognised in your code.
CI
Gate a pull request on your observability score with --min-score, read the JSON contract, and keep the map file out of the way. Exit codes, GitHub Actions, and jq recipes.