Repository navigation
feat(recorder): support data-before-send for heatmap events - #4646
RayanAlyasi wants to merge 2 commits into
Conversation
|
@RayanAlyasi is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
[Medium impact] The change affects heatmap collection for sites using the recorder. Fix the final send for pending hook results before merging. FindingsSummaryAdds
Reviews (1) · Last reviewed commit: "feat(recorder): support data-before-send..." · Reviewed by Greptile |
| Promise.resolve(callback('heatmap', event)).then(result => { | ||
| if (result) bufferHeatmapEvent(result); | ||
| }); |
There was a problem hiding this comment.
If a click's beforeSend hook is still pending when the page closes, flushHeatmap(true) runs before that click enters heatmapBuffer. When the hook resolves, this code only schedules the normal five-second send, which may never run before the page closes.
Buffer synchronous hook results immediately, and make results that finish after the final flush use an immediate keepalive send. Add a test that closes the page before a delayed hook resolves.
There was a problem hiding this comment.
This only happens with an async hook that is still pending when the page unloads. Synchronous hooks, and async functions that don't wait, are buffered before visibilitychange/beforeunload fire, so the final flush includes them. A pending promise doesn't resolve once the page is gone, so a later keepalive send wouldn't run either.
Checked in Chromium by clicking a link that navigates away: the click is sent with no hook, a synchronous hook and an async hook that doesn't wait; it's lost only when the hook waits (20 ms or more). If you'd prefer to avoid this case entirely, I can make the recorder's hook synchronous-only.
There was a problem hiding this comment.
You're right. A hook promise that is still pending when the document is unloaded cannot be relied on to resolve afterward, so scheduling a keepalive send from its callback would not make this case reliable.
Synchronous results—including async functions that resolve immediately—are buffered before the final flush, while a genuinely delayed promise is inherently best-effort during unload. I’m withdrawing this finding; I don’t think the recorder should make the hook synchronous-only when the tracker API already supports awaiting promises.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
| } | ||
|
|
||
| Promise.resolve(callback('heatmap', event)).then(result => { | ||
| if (result) bufferHeatmapEvent(result); |
There was a problem hiding this comment.
bufferHeatmapEvent assigns timestamp only after the async hook resolves, and the server stores that value as createdAt. A delayed hook therefore moves a click into a later time, potentially outside the report's selected date range.
Capture the event time before calling the hook and preserve it when buffering the result.
There was a problem hiding this comment.
Fixed in 9eb0891. The time is now taken when the event is queued, before the hook runs, and kept whatever the hook returns. Added a test where the hook resolves later.
Related to #4546
Problem
The tracker lets a site inspect, change or drop what it sends with
data-before-send(orumamiConfig.beforeSend). The recorder has no equivalent, so heatmap events can't be adjusted:/orders/HM547WwgG) each become their own heatmap page, and the raw path is stored even when the site normalizes it for page views.location.hrefwith window-based coordinates, and the site can't correct them.Change
before-sendthe same way the tracker does: adata-before-sendattribute naming a function onwindow, otherwiseumamiConfig.beforeSend.('heatmap', event)before it is queued.heatmapmatches the payload type sent to/api/record.A site that already sets
umamiConfig.beforeSendfor the tracker will also receive heatmap events in it, withtype: 'heatmap'.#4617 covers part of this with
data-transform-url, which rewrites URLs in heatmap events and replay meta events. This PR follows the tracker's existingbefore-sendAPI instead, as suggested on #4546, and also allows adjusting or dropping events.Tests
Added
src/recorder/index.test.ts, using the same setup as #4617's recorder tests:data-before-sendfunction receives('heatmap', event)and its result is sentumamiConfig.beforeSendis used, and a returned promise is awaiteddata-before-sendtakes precedence overumamiConfig.beforeSendThe four
before-sendtests fail ondevand pass with this change.pnpm testpasses 1058 tests.src/app/mcp/route.test.tsfails, andpnpm lintreports 6 errors in files this PR doesn't touch; both are identical ondevwithout this change. Biome is clean on the changed files.pnpm build:recordergrowsrecorder.jsby 171 bytes.recorder.jsin Chromium with real clicks against a stub/api/record: unchanged without a hook, URL rewritten withdata-before-send, click dropped when the hook returnsnull, and an asyncumamiConfig.beforeSendresult sent.Need help on this PR? Tag
@codesmith-botwith what you need.