fix(canvas): register system.canvases and retire the home-canvas concept - #77176
Conversation
The desktop channel home board lists a channel's canvases with `SELECT ... FROM system.canvases WHERE channel_id = ...`, but the canvases remodel never registered that table in the HogQL schema, so the query fails with "Unknown table" and every home board silently renders "No canvases yet." Register `system.canvases` over `posthog_canvas`, scope-gated like `system.tasks`, excluding soft-deleted rows to mirror the REST API's default filter. Generated-By: PostHog Code Task-Id: d75a71da-b662-4101-af83-0588664849c2
|
😎 Merged successfully - details. |
|
Hey @k11kirky! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
|
Reviews (1): Last reviewed commit: "fix(canvas): register system.canvases Ho..." | Re-trigger Greptile |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
🤖 CI report
|
| Root | Eager (shipped) | Δ vs base | Budget |
|---|---|---|---|
entry (logged-out pages, app bootstrap)src/index.tsx |
1.25 MiB · 22 files | 🔺 +574 B (+0.0%) | ███░░░░░░░ 27.7% of 4.51 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
8.14 MiB · 3,036 files | 🔺 +1.0 KiB (+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.5 KiB | ../node_modules/.pnpm/posthog-js@1.410.1/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.410.1/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 |
| 104.4 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 | 🔺 +96 B (+0.0%) | ████░░░░░░ 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.1 KiB | dist/toolbar/toolbar-app-GXPXCJTD.css |
| 551.6 KiB | dist/toolbar/chunk-chunk-2KIRIKZY.js |
| 484.6 KiB | dist/toolbar/chunk-chunk-PIOT3RXS.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-NAYZ2JJF.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-66NMDAV4.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-4NYOFLKW.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-OYRH6LH3.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 — 🔺 +507.0 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1390.04 MiB · 🔺 +507.0 KiB (+0.0%)
⚠️ Playwright — 1 flaky
🎭 Playwright report · View test results →
- Editing an insight updates the dashboard tile (chromium)
These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!
⚠️ Django migration SQL — 2 new migrations to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/canvas/backend/migrations/0007_soft_delete_home_canvases.py
BEGIN;
--
-- Raw Python operation
--
-- THIS OPERATION CANNOT BE WRITTEN AS SQL
COMMIT;products/canvas/backend/migrations/0008_remove_home_canvas.py
BEGIN;
--
-- Remove constraint unique_home_canvas_per_channel from model canvas
--
DROP INDEX IF EXISTS "unique_home_canvas_per_channel";
--
-- Alter field is_home on canvas
--
ALTER TABLE "posthog_canvas" ALTER COLUMN "is_home" SET DEFAULT false;
--
-- Custom state/database change combination
--
-- (no-op)
COMMIT;Last updated: 2026-08-04 10:21 UTC (e675e6e)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 0 Safe | 2 Needs Review | 0 Blocked
⚠️ Needs Review
May have performance impact
canvas.0007_soft_delete_home_canvases
└─ #1 ⚠️ RunPython: RunPython data migration needs review for performance
canvas.0008_remove_home_canvas
└─ #1 ⚠️ RemoveConstraint: Unscored Django operation: RemoveConstraint (needs manual review)
└─ #2 ⚠️ AlterField
Field alteration may cause table locks or data loss (check if changing type or constraints)
model: canvas, field: is_home, field_type: BooleanField
└─ #3 ✅ SeparateDatabaseAndState: Only state operations (no database changes)
📚 How to Deploy These Changes Safely
RunPython:
Use batching for large data migrations:
- Use
.iterator()to avoid loading all rows into memory - Use
.bulk_update()instead of saving individual objects - Batch size: 1,000-10,000 rows per batch
- Add pauses between batches
- Consider background jobs for very large updates (millions of rows)
See the migration safety guide
Last updated: 2026-08-04 10:22 UTC (e675e6e)
The channel landing page is a native view already, and the only navigation into a home canvas had no callers, so the auto-created seeded home board was vestigial: UI shipped as per-channel data that can't be updated fleet-wide, makes channel-open depend on the build pipeline, and hides query failures as "No canvases yet." (how the missing system.canvases table read as data loss). Remove the client machinery (ensure/reset/seed, tRPC routes, isHome plumbing), the backend is_home field, filter, and unique constraint (constraint dropped for real; column removed from Django state only with a db-side default first), and soft-delete the system-generated home boards. User-created canvases are untouched. Generated OpenAPI/MCP types and snapshots regenerated. Generated-By: PostHog Code Task-Id: d75a71da-b662-4101-af83-0588664849c2
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
A migrated canvas carries its pre-remodel source in legacy_code, and the
first publish nulled that column while creating the new version from the
incoming project only — the old source never became a version, so undo and
revert could not reach it and the only copy was discarded.
Materialize legacy_code as a real parent version ("Imported source") before
the first post-migration publish, with the same upload-then-commit posture
as the main project.
Generated-By: PostHog Code
Task-Id: d75a71da-b662-4101-af83-0588664849c2
Generated boards swallowed query failures into empty states, so real breakage (a missing table, an auth failure) rendered as "no data" — how the unregistered system.canvases table read as data loss. The authoring skills now require a rejected query to render a visible error with a retry, distinct from a successful-but-empty result, and the starter scaffold demonstrates the pattern. Generated-By: PostHog Code Task-Id: d75a71da-b662-4101-af83-0588664849c2
|
/trunk merge |
|
Could not verify team-posthog-code membership (GitHub API error). Auto-release is limited to members of the team-posthog-code GitHub team, so this will ship with the next scheduled release instead. |
Problem
After the canvas remodel landed (#73874 backend, #76649 desktop client), every channel in the desktop app appeared to have lost all of its canvases.
The data was intact — the migration converted the old desktop file-system rows into first-class
Canvasrows correctly, UUIDs preserved. The gap was in the read path: each channel's auto-created home canvas (the seeded board that lists the channel's canvases) queriessystem.canvasesvia HogQL, but that table was never registered in the HogQL schema. The query fails withUnknown table 'system.canvases'and the board's catch-all error handling silently renders "No canvases yet."Investigating the incident also surfaced that the home-canvas concept itself is vestigial and fragile: the channel landing page is already a native view (
WebsiteChannelHome), the only navigation into a home canvas (useOpenHomeCanvas) had zero callers, and the board is UI shipped as per-channel data — its code is frozen at publish time, can't be updated fleet-wide, makes channel-open depend on the build pipeline, and hides failures as empty states. So rather than only patching the query, this PR also retires the concept.Changes
canvasessystem table in the HogQL schema (posthog/hogql/database/schema/system.py), following thetaskstable's conventions: scope-gated (access_scope="canvas"), soft-deleted rows excluded via a table predicate. Useful for SQL access to canvases generally, and how the incident was diagnosed.ensureHomeCanvas/resetHomeCanvas/publishHomeSeed/buildHomeCanvasCode, the tRPC routes, the deaduseOpenHomeCanvashook, the "Reset to default" affordance, and allisHomeplumbing.useCanvasNavigationmoves to its own file.is_homefrom the serializers and views (create 409 race handling included), remove the field and theunique_home_canvas_per_channelconstraint. Per safe-migration policy the constraint is dropped for real, the column gets a database-side default and is removed from Django state only; a later migration can drop it physically.is_home=True→deleted=True, reversible). They were system-generated directory listings, not user content; user-created canvases are untouched.legacy_codewithout it ever becoming a version, so the pre-remodel source was unrecoverable. It is now materialized as a real parent version ("Imported source") on first publish.building-react-quill-canvases,querying-canvas-data) now require a rejected query to render a visible error with a retry — distinct from a successful-but-empty result — and the starter scaffold demonstrates the pattern. A swallowed error is how this incident presented as data loss.How did you test this code?
system.canvasestable: with the schema change stashed, the new tests fail exactly like production (Unknown table); with it applied, all pass. Added acanvasescase to the parameterized team-isolation suite plus SQL-shape and end-to-end tests that soft-deleted canvases are excluded (mirrors the existingtasks/sandbox_environmentspattern) — catches the table being missing, cross-team leaks, and the deleted-filter being dropped.TestLegacySourcePreservation): without the fix the publish leaves no parent version; with it, the legacy source round-trips from the materialized version. Canvas backend API tests pass against a fresh test DB;makemigrations --checkclean;sqlmigrateverified the migration emits onlyDROP INDEX+ALTER COLUMN ... SET DEFAULT false(state-only field removal).pnpm typecheck(24 packages), Biome lint,@posthog/core(3034) and@posthog/ui(2541) test suites pass.Automatic notifications
Docs update
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Started as an incident investigation ("all canvases are gone" after the remodel merged): verified via the canvases API that data survived, reproduced the home board's failing
system.canvasesquery against production, and confirmed the table was referenced only by the desktop client with no backend registration. After reviewing the home-canvas design (nobody customizes the boards; the native channel home already exists; the seed is the only flow that overwrites canvas content without user intent), the DRI decided to remove the concept entirely. Skills invoked:/writing-tests,/django-migrations,/improving-drf-endpoints,/posthog-desktop. Known environment gap:test_cloud_buildertests requirenpm ciincanvas_builder(unrelated to this change).Created with PostHog Code