Skip to content

fix(insights): a session id is only unique inside its app (#248) - #532

Merged
SirAllap merged 3 commits into
SirAllap:mainfrom
snowyukitty:fix/dashboard-metric-consistency
Aug 24, 2026
Merged

SirAllap merged 3 commits into
SirAllap:mainfrom
snowyukitty:fix/dashboard-metric-consistency

Conversation

@snowyukitty

@snowyukitty snowyukitty commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the F18 part of #248. A session id is only unique inside the app that
issued it, and the insight queries grouped by session id alone β€” so two apps
working the same project, each running pytest test_a.py seven times, collapsed
into one card, Possible loop Β· 14Γ— identical command, attributed to whichever
source_app SQLite happened to return.

GROUP BY source_app, session_id[, cmd] splits the rows. The id each card is
keyed by carries the pair too: without that, the two rows the grouping fix
correctly separates are handed to key={i.id} in Alerts.tsx as one, and the
UI merges them straight back. rowId encodes the pair rather than joining it,
because source_app is not constrained to exclude a colon.

The isWithin/normalize() change from the first revision is dropped. It is
platform-bound, and the one path shape that does fail today belongs in its own
PR with its failing case.

How was it tested?

server/test/insights-group-app.test.ts is built around the collision β€” one
session id, one command, two apps β€” and asserts both halves, because each fails
alone:

tree result
base 26ef8bc 2 fail; the loop list holds only app-alpha:s-shared
GROUP BY fixed, ids untouched 1 fail β€” 9 insights, 6 distinct ids
ids joined rather than encoded 2 fail β€” 9 insights, 8 distinct ids
this branch 3 pass

The fixture carries all three session-scoped insights on the shared id, so the
uniqueness assertion covers spend: and errors: as well as loop:.

server/test/insights-scope.test.ts matched insight ids by prefix, which the new
id shape breaks. Its source_app came from splitting an OS path on /, which
does not split a Windows path, so it uses basename now and its four assertions
are exact.

Also run: bun run typecheck in server/ and web/, bunx vite build, and the
full server suite on both 26ef8bc and this branch β€” the five files that touch
insights fail identically on both, and this branch adds three passing tests.

Copy link
Copy Markdown
Owner

Thank you for digging into #248, @snowyukitty β€” the GROUP BY source_app change is correct and it fixes a real bug. I reproduced it against main before reviewing, so that part is not in question:

With two apps sharing a session_id and running the same command (7 events each), main collapses them into a single insight β€” Possible loop Β· 14Γ— identical command, attributed to whichever source_app SQLite happened to pick. With your insights.ts change applied, they split correctly into app-alpha:s-shared and app-beta:s-shared, 7Γ— each. That's the bug, and you found it.

Three things before I can merge it.

1. The added test passes on main, unpatched. I ran server/test/insights-group-app.test.ts against an unmodified tree: 1 pass. It uses two different session ids (s-1, s-2), and the old GROUP BY session_id, cmd already separated those β€” so the test never exercises the grouping it's named for. To fail before the fix, the two apps have to collide on both session_id and cmd:

for (let i = 0; i < 7; i++) db.insertEvent(event("app-alpha", "s-shared", "pytest test_a.py", i) as any);
for (let i = 0; i < 7; i++) db.insertEvent(event("app-beta",  "s-shared", "pytest test_a.py", i) as any);

Then assert both app-alpha:s-shared and app-beta:s-shared are present. That version fails on main and passes with your change β€” I verified both directions.

2. The insight ids carry the same bug. id: \loop:${l.session_id}:${l.cmd}`(andspend:${s.session_id}, and the error-rate one) don't include source_app. In exactly the case this PR fixes, the two now-correctly-separated insights get **identical ids** β€” and Alerts.tsx:225renders them withkey={i.id}`. The grouping fix needs to carry into the ids, otherwise it splits the rows and then collides them again one layer up.

3. Please drop the normalize() change from this PR. isWithin takes an explicit separator parameter precisely so Windows paths can be checked on any host β€” that's what server/test/windows-paths.test.ts exercises. normalize is platform-bound, so on a mismatched platform it disagrees with the s argument that was passed in. It also doesn't fix what it sounds like it fixes: posix normalize("/a/b/") keeps the trailing slash, so the mismatch case is unchanged, and the callers already pass resolve()-derived paths (see the note at db.ts:927). If there's a concrete path-shape that fails today, that's worth its own PR with the failing case.

Also two stray blank lines snuck in β€” one after isWithin in config.ts, one inside the SQL template before HAVING.

Fix 1 and 2, drop 3, and I'll merge this. It's a good catch and I'd rather land it with a test that actually guards the behaviour.


Generated by Claude Code

Review on #532 found the added test green on an unpatched main: its two
apps used different session ids, and `GROUP BY session_id, cmd` already
told those apart, so it never exercised the grouping it was named for.
The fixture now collides on both β€” one session id, one command, two apps β€”
and on 26ef8bc the loop list comes back holding only `app-alpha:s-shared`.

The same review found the grouping fix stopping short of the ids. `loop:`,
`spend:` and `errors:` were built from the session id alone, so the two
rows the fix correctly separates were handed to `key={i.id}` in Alerts.tsx
as one. `rowId` carries the pair the queries group by, encoded rather than
joined: an app `a:b` in session `c` and an app `a` in session `b:c` are two
rows that a plain join writes as one string, and source_app is not
constrained to exclude a colon. A third fixture pair pins that, and the
uniqueness assertion fails on a plain join with 9 insights sharing 8 ids.

insights-scope.test.ts matched ids by prefix, which the new shape breaks.
Its source_app came from splitting an OS path on "/", which does not split
a Windows path, so the assertions are now exact against `basename`.

The isWithin normalize() change is dropped. It is platform-bound, and the
one path shape that does fail today deserves its own PR.

Signed-off-by: snowyukitty <[email protected]>
@snowyukitty snowyukitty changed the title fix(insights): group by source_app and normalize isWithin paths (#248) fix(insights): a session id is only unique inside its app (#248) Aug 20, 2026
@snowyukitty

Copy link
Copy Markdown
Contributor Author

Before changing anything I ran the existing test against unpatched 26ef8bc; it reported 1 pass, because its fixture never made the two apps collide.

1. The fixture collides now. One session id, one command, two apps. On 26ef8bc:

Expected to contain: "app-beta:s-shared"
Received: [ "app-alpha:s-shared" ]

It carries all three session-scoped insights on that shared id, so the id assertion below covers spend: and errors: too, not just loop:.

2. The ids carry the pair. rowId sits next to key, and the comment says why they differ: key is the label, it truncates the session id, and two rows are allowed to render the same one; an id is a React key and may not.

The fixture produces nine insight rows β€” three kinds for each of the two colliding apps, two more loops from the pair in the next paragraph, and the hourly burn card. With GROUP BY fixed and the ids left alone, expect(new Set(ids).size).toBe(ids.length) gives:

Expected: 9
Received: 6

Three duplicate ids, one for each of loop:, spend: and errors:.

One thing changed the shape of the fix: `${app}:${sid}` is not injective. App a:b in session c and app a in session b:c are two rows and one string, and nothing constrains source_app to exclude a colon β€” the pre-change fixture in insights-scope.test.ts left a whole C:\Users\… path in it on Windows, drive colon included. rowId encodes each half instead of joining them, and a third fixture pair pins it. With a plain join the same assertion gives Expected: 9 / Received: 8.

3. Dropped, with both stray blank lines. server/src/config.ts at this head hashes identical to 26ef8bc.

insights-scope.test.ts matched ids by prefix (loop:s-in:, spend:b-in), which the new shape breaks, so those four assertions had to move either way.

One change you didn't ask for, and can refuse. That file's fixture built source_app with project.split("/").pop(), which does not split a Windows path; I switched it to basename so an app-anchored assertion is not green on CI and red on a Windows checkout. That swap is independent of the requested fix β€” say the word and I will revert it or split it out.

A separate Windows finding. insights-scope.test.ts > "a loop whose turn ran inside the scope (via cwd) is included" fails on unmodified 26ef8bc on Windows. Line 53 builds the cwd as SCOPED + "/wt/x" while isWithin compares with the platform sep. A reduced probe, paths abbreviated:

sep      "\"
mixed    "…\scoped/wt/x"  ->  false
native   "…\scoped\wt\x"  ->  true     (control)
outside  "…\other\wt"     ->  false    (control)

ci.yml currently runs on ubuntu-latest only, so this case is not exercised there. I will open it separately with the failing case rather than fold it in here.

Validation. bun run typecheck in server/ and web/, bunx vite build, and the full server suite on both 26ef8bc and this branch on the same machine. The five files that touch insights fail on the same five test titles in both runs, and this branch adds three passing tests. The full suite is flaky on Windows in the git and network tests, so I compared failure titles rather than totals.

Copy link
Copy Markdown
Owner

All three points check out, and I re-ran each one rather than reading the diff β€” same standard the first review held itself to.

1. The fixture collides now, and the test earns its name. Reverting only server/src/insights.ts to its state on the base commit, leaving your new test in place:

Expected to contain: "app-beta:s-shared"
Received: [ "app-alpha:s-shared" ]
2 fail, 1 pass

With insights.ts restored: 3 pass. That is the direction the earlier version never had.

2. encodeURIComponent is load-bearing, and I checked that specifically β€” it is the kind of thing that looks like defensive noise, so it is worth knowing which. Keeping your fix but degrading rowId to a plain `${app}:${sid}`:

Expected: 9
Received: 8

Exactly the number you reported. The third test fails alongside it at Expected: 2 / Received: 1. So both the encoding and the fixture pair that pins it are doing real work.

3. normalize() is gone. server/src/config.ts at this head is byte-identical to 26ef8bc, and the PR now touches three files. Both stray blank lines are gone with it.

On the id format change, which nobody asked about but is the thing that would bite later: I checked whether anything consumes an insight id as structure rather than as an opaque value. Nothing does β€” the only use is key={i.id} in Alerts.tsx, and no caller splits, slices or parses it. So re-encoding the halves is free.

The basename swap: keep it. You flagged it as unrequested and offered to revert. It belongs β€” an app-anchored assertion that is green on CI and red on a Windows checkout is a worse trade than a one-line fixture change, and it is in the same file as the assertions you had to move anyway.

And please do open the Windows isWithin finding separately. That is the right call and the probe you posted is the useful part of it. ci.yml runs ubuntu-latest only, so nothing here would have caught it.

Also ran: bun run typecheck clean, and all three insights suites at 12 pass / 0 fail.

CI on this PR is currently red for an unrelated reason β€” main was broken by an unpinned bun-version: latest picking up 1.4.0, fixed in #535 and now merged. I am updating this branch onto it so the run reflects your diff. Nothing here is waiting on you.

Thanks for the thorough second pass β€” the reproduction notes in your reply made this quick to verify.


Generated by Claude Code

@SirAllap SirAllap closed this Aug 24, 2026
@SirAllap SirAllap reopened this Aug 24, 2026
@SirAllap
SirAllap merged commit bf0f172 into SirAllap:main Aug 24, 2026
7 of 8 checks passed
Variable85 pushed a commit to Variable85/agentglass that referenced this pull request Sep 16, 2026
Reported by @snowyukitty in SirAllap#569, and it is the separate finding they
asked for when the platform-bound half was dropped from SirAllap#532.

`insights-scope.test.ts` built the in-scope cwd as `SCOPED + "/wt/x"`
while `isWithin` (config.ts) compares against the platform's own
separator. On Windows that fixture is `C:\...\scoped/wt/x`, which does
not start with `C:\...\scoped\` β€” so the case that exists to prove a turn
is in scope *through the directory it ran in* was, on that platform,
proving nothing.

Reduced probe with explicit win32 semantics, which runs anywhere:

  isWithin(scoped + "/wt/x",              scoped) -> false   (before)
  isWithin(join(scoped, "wt", "x"),       scoped) -> true    (after)
  isWithin(join("C:\\fixture", "other"),  scoped) -> false   (control)

Fixture only. The scoping behaviour it exercises is unchanged, and `join`
was already imported.

Co-Authored-By: Claude Opus 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants