Marks and Compare

A mark is a named moment, and compare puts the window before a mark beside the window after it.

Time selectors

Every agent-facing endpoint takes since and until instead of from and to, because an agent thinks in "since I changed the code" rather than in epoch milliseconds. A selector is one of five forms.

Form Example Meaning

Duration

30s, 5m, 2h, 1d

That long before until for since, or before now for until.

Epoch milliseconds

1758000000000

The instant, 13 or more digits.

A mark name

before, after-fix

The newest mark with that name.

start

start

The newest automatic start mark, of service when one is given.

now

now

Now, and the default for until.

The default since is 15m, and the default until is now. since=start therefore means "since the application was last restarted", which is what an agent that just rebuilt the application wants.

A since that resolves to a moment after until is a 400, and a usage error in the CLI. A mark name that matches no mark is a 404 naming it, and exit code 4 in the CLI. The from and to parameters of the UI endpoints still work everywhere, and they win when both pairs are given.

Marks

A mark is a named moment: before, after-fix, v2. It is one row in the store, shared like everything else, and swept with the retention.

$ java -jar spider-sense.jar mark before
mark before at 2026-09-17T08:19:28.122+09:00

$ java -jar spider-sense.jar mark after --note="after the fix"
mark after at 2026-09-17T08:19:35.837+09:00 — after the fix

The name is required and matches [A-Za-z0-9._-]{1,64}. --note=<text> keeps a note with the moment, and --service=<name> ties the mark to one service. Over HTTP it is POST /api/marks with { "name": "before", "note": "…", "service": "…", "at": … }, where everything but the name is optional and at defaults to now.

marks lists the newest 50, newest first, and GET /api/marks answers the same.

$ java -jar spider-sense.jar marks
# marks

| at | name | service | note |
| --- | --- | --- | --- |
| 2026-09-17T08:19:35.837+09:00 | after | — | after the fix |
| 2026-09-17T08:19:28.122+09:00 | before | — | — |
| 2026-09-17T08:19:16.727+09:00 | start | spring-orders | pid 178056 |
| 2026-09-17T08:16:42.507+09:00 | file-mode | — | — |
| 2026-09-17T08:15:38.928+09:00 | after | — | — |
| 2026-09-17T08:14:20.087+09:00 | before | — | — |
| 2026-09-17T08:14:06.284+09:00 | start | spring-orders | pid 170140 |

Automatic start marks

Spider Sense records a mark by itself whenever a service restarts. When the writer sees a service with a process.pid it has not stored for that service, it inserts a mark named start with that service and the note pid <pid>. --since=start therefore needs no cooperation from anyone, and it covers the run that is going on now.

The UI’s Mark button on the top bar records a mark the same way, and marks are drawn as vertical lines on the charts. Layout and Controls covers the button and the lines.

Compare

compare answers the question after a change: did it help.

java -jar spider-sense.jar compare --before=<selector> --after=<selector> [--until=<selector>] [--service=<name>]

It takes no --since, because its windows are the two selectors. Before is the window [before, after) and after is the window [after, until), with until defaulting to now. The usual use is two marks: mark before, exercise, change the code, mark after, exercise the same way, then compare --before=before --after=after.

The answer has four parts.

Totals

Requests, errors, p95 and Apdex, for each window.

Endpoints

One row per endpoint, with calls, errors, p95, db/req and db ms/req for both windows. The JSON side of a row is { "calls", "errors", "p50Ms", "p95Ms", "maxMs", "dbCallsPerRequest", "dbMsPerRequest" }.

Queries

One row per query group, with calls, calls/req, p95 and total for both windows. callsPerRequest divides by the entry spans of the window, of the service when one is given.

Errors

One row per error group, with the occurrence count in each window.

Verdicts

Every row carries a verdict, decided in this order.

  • new when only the after window has data, and gone when only the before window has.

  • worse when errors appear or grow, or when p95 grows by more than 20% and by at least 10 ms. For a query the measure is calls per request, and the second bound is 0.5 calls.

  • better when the same measure shrinks by that much, or when errors disappear.

  • same otherwise.

Endpoints are sorted worst first: worse, then new, same, better, gone, each group by total time descending. Queries and errors are sorted the same way.

The text rendering

The verdict is the first column and the worst rows come first, so the top of each table is the answer. Every other cell holds both windows as before → after, with where a side has nothing.

$ java -jar spider-sense.jar compare --before=before --after=after
# compare  2026-09-17T08:19:28+09:00 → 08:19:35  vs  2026-09-17T08:19:35+09:00 → 08:19:45  (all services)

| totals | before | after |
| --- | --- | --- |
| requests | 21 | 12 |
| errors | 1 | 1 |
| p95 | 411.5 ms | 1,201.7 ms |
| apdex | 0.929 | 0.792 |

## endpoints

| verdict | endpoint | id | calls | errors | p95 | db/req | db ms/req |
| --- | --- | --- | --- | --- | --- | --- | --- |
| new | GET /api/slow | 7c7ff534a0ec | — → 3 | — → 0 | — → 1,202.5 ms | — → 0.0 | — → 0.0 ms |
| better | GET /api/reports/revenue | 7342d163d1ee | 3 → 3 | 0 → 0 | 549.8 ms → 417.1 ms | 2.0 → 2.0 | 419.8 ms → 383.6 ms |
| better | GET /api/orders/{id} | 264871b2ef8c | 3 → 3 | 0 → 0 | 66.5 ms → 7.6 ms | 6.7 → 6.7 | 1.0 ms → 0.5 ms |
| better | GET /api/flaky | 40e6cdb7cd69 | 3 → 3 | 1 → 1 | 22.0 ms → 2.1 ms | 0.0 → 0.0 | 0.0 ms → 0.0 ms |
| gone | GET /api/orders/{id}/enriched | 8c62f8d5090d | 3 → — | 0 → — | 174.5 ms → — | 6.7 → — | 0.5 ms → — |
| gone | GET /api/customers/search | cf03d98e70da | 3 → — | 0 → — | 29.6 ms → — | 1.0 → — | 0.9 ms → — |
| gone | POST /api/orders/{id}/pay | 48e41c5bc06c | 3 → — | 0 → — | 8.6 ms → — | 1.0 → — | 0.2 ms → — |
| gone | GET / | a239b7c70ac8 | 3 → — | 0 → — | 5.2 ms → — | 0.0 → — | 0.0 ms → — |

## queries

| verdict | id | calls | calls/req | p95 | total | statement |
| --- | --- | --- | --- | --- | --- | --- |
| same | e0e233d8ad75 | 3 → 3 | 0.1 → 0.3 | 269.7 ms → 247.1 ms | 782.8 ms → 698.9 ms | SELECT p.id AS product_id, p.sku AS sku, p.name AS name, SUM(l.quantity) AS quantity, SUM(l.quantity * l.unit_price) AS revenue FROM order_line l JOIN product p ON p.id = l.product_id JOIN orders o ON… |
| same | 4080653c9caa | 3 → 3 | 0.1 → 0.3 | 200.2 ms → 164.5 ms | 476.6 ms → 451.9 ms | SELECT o.status AS status, CAST(o.created_at AS DATE) AS order_day, SUM(o.total) AS revenue, COUNT(DISTINCT o.id) AS order_count, SUM(l.quantity) AS item_count FROM orders o JOIN order_line l ON l.ord… |
| same | 14eb3eff12dc | 22 → 11 | 1.0 → 0.9 | 0.2 ms → 0.1 ms | 2.0 ms → 0.5 ms | select p1_0.id,p1_0.name,p1_0.price,p1_0.sku from product p1_0 where p1_0.id=? |
| same | 62fb3169834e | 9 → 3 | 0.4 → 0.3 | 0.3 ms → 0.2 ms | 1.7 ms → 0.3 ms | select o1_0.id,o1_0.created_at,o1_0.customer_id,o1_0.status,o1_0.total from orders o1_0 where o1_0.id=? |
| same | 6ece3eeacddc | 6 → 3 | 0.3 → 0.3 | 0.4 ms → 0.2 ms | 1.0 ms → 0.4 ms | select l1_0.order_id,l1_0.id,l1_0.product_id,l1_0.quantity,l1_0.unit_price from order_line l1_0 where l1_0.order_id=? |
| same | a3b4c9a4fdef | 6 → 3 | 0.3 → 0.3 | 0.2 ms → 0.1 ms | 0.6 ms → 0.2 ms | select c1_0.id,c1_0.email,c1_0.name from customer c1_0 where c1_0.id=? |
| gone | a2ef3cae1e22 | 3 → — | 0.1 → — | 1.4 ms → — | 2.8 ms → — | select c1_0.id,c1_0.email,c1_0.name from customer c1_0 where lower(c1_0.name) like (?\|\|?\|\|?) escape ? order by c1_0.id fetch first ? rows only |

## errors

| verdict | id | type | message | before | after |
| --- | --- | --- | --- | --- | --- |
| same | 458522ccda9f | java.lang.IllegalStateException | Payment gateway timeout | 1 | 1 |

Exercise the after window exactly as the before window was exercised: the same endpoints, the same number of times. A different exercise moves every number, and the verdicts then say what the traffic did rather than what the change did. Check turns the same window into a pass or a fail.