Skip to content

fix(dashboards): cancel in-flight tile stream on refresh - #76429

Draft
posthog[bot] wants to merge 2 commits into
masterfrom
posthog-self-driving/fixdashboards-cancel-stale-tile-streams-bb485c
Draft

fix(dashboards): cancel in-flight tile stream on refresh#76429
posthog[bot] wants to merge 2 commits into
masterfrom
posthog-self-driving/fixdashboards-cancel-stale-tile-streams-bb485c

Conversation

@posthog

@posthog posthog Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Problem

  • Users refresh a dashboard, see correct numbers for a few seconds, then watch tiles silently revert to stale data — no error, just wrong numbers that look plausible.
  • Two independent signals point at the same cause: a support ticket describing tiles reverting after refresh across several dashboards, and a replay scan of a session showing the related empty-state variant (already fixed in #74717).
  • Root cause: nothing could cancel an in-flight SSE tile stream.
    • api.dashboards.streamTiles (frontend/src/lib/api.ts) returns a cancel function that loadDashboardStreaming (frontend/src/scenes/dashboard/dashboardLogic.tsx) discarded.
    • abortAnyRunningQuery only aborted the per-insight refresh AbortController, not the stream.
    • receiveTileFromStream merged whatever tile arrived with no ordering guard, appending duplicates rather than replacing.
  • So hitting Refresh while the initial stream was still delivering slow tiles would write fresh results, then the older stream would land a tile computed against the pre-refresh filters seconds later, overwriting it.

Changes

  • abortAnyRunningQuery now also cancels the in-flight tile stream (stored on cache.cancelTileStream) and bumps a generation counter.
  • The stream's onMessage/onComplete/onError callbacks check that generation before dispatching, so anything already in flight from a superseded stream is dropped even if cancellation races with delivery.
  • receiveTileFromStream now replaces a tile with a matching id instead of appending a duplicate entry.
  • A new tileStreamCancelled action flips dashboardStreaming back to false when a stream is cancelled mid-flight, so the loading state doesn't get stuck.

How did you test this code?

Added two cases to dashboardLogic.test.ts:

  • replaces an existing tile instead of appending a duplicate for the same tile id — catches the literal append-not-replace bug in the reducer.
  • cancels the in-flight tile stream on refresh so a tile it delivers afterwards is dropped — mocks api.dashboards.streamTiles, simulates a refresh mid-stream via abortAnyRunningQuery, and asserts a late tile delivery is dropped. This is the actual reported regression.

Both tests fail against the pre-fix code (verified) and pass with the fix. Ran the full dashboardLogic.test.ts suite (91 passed) and pnpm --filter=@posthog/frontend typescript:check — no new errors introduced (remaining errors are pre-existing, unrelated to a missing @posthog/quill build).

I could not manually reproduce the visual revert in a browser in this environment (no way to drive real SSE timing through the UI here), so this relies on the logic-level regression test above rather than a live repro.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

No docs affected — internal dashboard loading behavior, no user-facing API or workflow change.

🤖 Agent context

Autonomy: Fully autonomous

I (Claude Code) traced the bug from an inbox report combining a support ticket and a replay scan finding, both pointing at dashboardLogic.tsx's tile streaming path. Checked for existing work first: searched open PRs/issues touching dashboardLogic.tsx and streamTiles — found related-but-different dashboard-race fixes (#69543, #68476, #76386, #69660) but none add stream cancellation or an ordering guard, so this is additive.

Scoped the fix to the confirmed root cause (stream cancellation + ordering guard) rather than also touching the narrower non-streaming loadDashboard/loadDashboardSuccess race the report flagged as a related-but-unconfirmed variant.

No repo skills were required beyond /writing-tests (invoked before adding the regression tests).


Created with PostHog Desktop from this inbox report.

Nothing cancelled the SSE tile stream when a refresh started: `api.dashboards.streamTiles` returns a cancel function that `loadDashboardStreaming` discarded, and `abortAnyRunningQuery` only aborted the per-insight refresh controller. So a refresh could write fresh results, then the still-running initial stream would deliver a tile computed against the old filters a few seconds later, silently reverting it.

`abortAnyRunningQuery` (called on refresh and on cancel) now also cancels the tile stream and bumps a generation counter that the stream's callbacks check before dispatching, so anything already in flight from a superseded stream is dropped. `receiveTileFromStream` now replaces a tile with a matching id instead of appending a duplicate.

Generated-By: PostHog Code
Task-Id: e37dda42-f084-46c4-b49c-ef6e4e848d7b
@trunk-io

trunk-io Bot commented Aug 2, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Bundle size — 🔺 +467 B (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 65.51 MiB · 🔺 +467 B (+0.0%)

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.25 MiB · 22 files no change ███░░░░░░░ 27.7% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.14 MiB · 3,035 files 🔺 +467 B (+0.0%) ████████░░ 83.8% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
285.6 KiB ../node_modules/.pnpm/posthog-js@1.409.5/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
231.5 KiB ../node_modules/.pnpm/posthog-js@1.409.5/node_modules/posthog-js/dist/module.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.3 KiB src/lib/api.ts
94.7 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Toolbar bundle — eager 2.19 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.19 MiB · 17 files no change ████░░░░░░ 38.3% of 5.72 MiB
Deferred (lazy) 2.08 MiB · 33 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
718.3 KiB dist/toolbar/toolbar-app-FT4UJ3IP.css
551.4 KiB dist/toolbar/chunk-chunk-AGSGCCBT.js
484.6 KiB dist/toolbar/chunk-chunk-T44C2V5C.js
133.6 KiB dist/toolbar/chunk-chunk-KBLX73CM.js
131.8 KiB dist/toolbar/chunk-chunk-T5KY5WYR.js
71.0 KiB dist/toolbar/toolbar-app-FKUW2FOM.js
69.0 KiB dist/toolbar/chunk-chunk-27JL52RE.js
35.6 KiB dist/toolbar/chunk-chunk-3JVI3ZTF.js
20.9 KiB dist/toolbar/chunk-chunk-HJ3ZJMTU.js
12.2 KiB dist/toolbar/chunk-chunk-PIK3PADE.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

Dist folder size — 🔺 +9.3 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1381.31 MiB · 🔺 +9.3 KiB (+0.0%)

CI's typegen-drift check expects the generated action interface sorted alphabetically; the new tileStreamCancelled entry was placed after tileStreamingFailure instead of before tileStreamingComplete.

Generated-By: PostHog Code
Task-Id: e37dda42-f084-46c4-b49c-ef6e4e848d7b
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.

0 participants