Skip to content

feat(product-analytics): prepopulate the home tab with a dashboard template - #106651

Draft
GeneralistDev wants to merge 6 commits into
masterfrom
posthog/product-analytics-home-tab-dashboard-template
Draft

GeneralistDev wants to merge 6 commits into
masterfrom
posthog/product-analytics-home-tab-dashboard-template

Conversation

@GeneralistDev

@GeneralistDev GeneralistDev commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Problem

The Insights "Home" tab shows six compact-list widgets (recently viewed, firing alerts, active users, trending, new events, activity) with no way to customize or act on them. A user landing there gets a fixed dashboard-adjacent summary instead of a real, editable dashboard.

Changes

  • The Home tab now embeds a real PostHog dashboard, built from a template the team picks on first visit (SaaS, e-commerce, mobile app, or a generic template) — the dashboard is fully editable afterward, same as any other dashboard.
  • A team without a Home tab dashboard sees a picker ("What does your product do?"), illustrated with a hedgehog; picking a template creates a pinned dashboard and saves it as the team's home_tab_dashboard.
  • Editing the Home tab dashboard requires an explicit "Customize" action, not dragging or resizing a tile — the shared Dashboard/DashboardItems components gained an opt-in requireExplicitEditMode prop for this (default off everywhere else, so no other dashboard consumer's behavior changes).
  • home_tab_dashboard is a new field on Team/Project, separate from Team.primary_dashboard (used by the unrelated project homepage). It lives on a new TeamHomeTabDashboard extension table, following posthog/models/team/README.md.
  • Three new GLOBAL dashboard templates ("SaaS product", "E-commerce", "Mobile app") ship via a data migration, built from automatically captured pageview/screen events so they work before any custom instrumentation.
  • The now-dead RecentlyViewed/FiringAlerts/ActiveUsers/Trending/NewEvents/Activity widgets and their kea logics are removed.

This is a separate approach from the still-open #105851 ("use a shared notebook for home"), scoped only to the product-analytics Insights Home tab. It does not touch frontend/src/scenes/project-homepage/, Team.primary_dashboard, or Django onboarding.

Picker state Dashboard state (editor view, "Customize" enters edit mode)
home_tab_picker_hedgehog home_tab_dashboard_editable

How did you test this code?

Backend, run via hogli test equivalents locally:

  • posthog/api/test/test_team.py and posthog/api/test/test_project.py (shared factory covers both /api/environments/ and /api/projects/): test_update_home_tab_dashboard and test_cant_set_home_tab_dashboard_to_another_teams_dashboard — guard the exact bug found while building this: the generic passthrough save(update_fields=[...]) loop would raise FieldDoesNotExist for a property-backed field unless it is special-cased, and it needs the same cross-team validation as primary_dashboard.
  • posthog/models/team/test/test_extensions.py::TestHomeTabDashboardProperty — the Team.home_tab_dashboard getter/setter round-trips through the extension table and never touches primary_dashboard.
  • products/dashboards/backend/api/test/test_dashboard_templates.py::TestHomeTabDashboardTemplates — a smoke test per new template (parameterized) that create_dashboard_from_template succeeds and produces tiles with well-formed queries, so a JSON typo in the migration fails here instead of as a 500 when a customer picks the template.
  • Full local runs (856 and 640 passed across two runs) of posthog/api/test/test_team.py, posthog/api/test/test_project.py, posthog/models/team/test/, products/dashboards/backend/api/test/test_dashboard_templates.py, posthog/api/test/dashboards/test_dashboard.py.
  • uv run mypy --cache-fine-grained . and pnpm --filter=@posthog/frontend typescript:check: clean.

Frontend: rendered Scenes-App/Saved Insights → Home and HomeWithDashboard stories in a headless Storybook (Playwright/Chromium) at 1300px and 520px; screenshots above. For the explicit-edit-mode change, also clicked through the flow in Storybook with an editor-permission dashboard: confirmed "Customize" is the only way into layout edit mode (dragging/resizing a tile no longer auto-enters it), and that Save/Cancel appear once in edit mode. Manual QA in the running app was not done in this sandbox.

Not run: hogli migrations:check's async-migrations step fails in this sandbox on a freshly-created dev DB (unrelated legacy async migrations 0001-0007 show as pending) — migrate --check, migrate_clickhouse --check, and makemigrations --check --dry-run all pass, and the two new migrations were applied and exercised directly.

Release status

  • No feature flag controls this change

Automatic notifications

  • Publish to changelog?

Docs update

None — no existing doc's scope covers the product-analytics Home tab specifically.

🤖 Agent context

Autonomy: Fully autonomous

Agent: Claude Code, Sonnet 5

Implemented from a detailed design brief covering the backend extension model, the data migration for the three templates, and the frontend picker/wiring. Skills invoked: /django-migrations, /improving-drf-endpoints, /adopting-generated-api-types, /writing-kea-logics, /writing-ui-components, /writing-user-facing-copy, /writing-tests, /writing-code-comments, /writing-pr-descriptions, /running-ci-preflight.

During implementation, two deviations from the literal brief were needed for the feature to work end to end:

  • The brief scoped the serializer change to posthog/api/team.py, but teamLogic.updateCurrentTeam() actually PATCHes /api/projects/{id}, served by ProjectBackwardCompatSerializer in posthog/api/project.py. That serializer needed the matching home_tab_dashboard field, team_passthrough_fields entry, and the same extension-popping fix in update(), or the picker's save would silently no-op.
  • The brief's model example imported Team directly; that produces a real circular import once the model is registered in products/dashboards/backend/models/__init__.py (pulled in transitively from inside Team's own module body via posthog.helpers.dashboard_templates). Switched to Django's string-based FK references ("posthog.Team", "dashboards.Dashboard"), the documented pattern in products/README.md for this exact situation.

A follow-up request added the hedgehog illustration and the explicit-edit-mode requirement described above.

POSTHOG_TASK_RUN_ID/CI were set for this session, so the local CodeRabbit review pass was skipped per repo instructions.

…mplate

Replace the Insights Home tab's compact-list widgets with a real, editable PostHog dashboard, prepopulated from a template chosen by what the team builds (SaaS, e-commerce, mobile app, or the existing generic template).

- Adds `TeamHomeTabDashboard`, a Team extension model separate from `Team.primary_dashboard`, exposed as `home_tab_dashboard` on the team/project serializers.
- Seeds three new GLOBAL dashboard templates ("SaaS product", "E-commerce", "Mobile app") via a data migration, built from automatically captured pageview/screen events.
- Adds a template picker (`HomeTabTemplatePicker`) shown until a team picks a template; afterwards the Home tab renders the created dashboard via the existing `Dashboard` component.
- Removes the now-dead RecentlyViewed/FiringAlerts/ActiveUsers/Trending/NewEvents/Activity widgets and their logics.

Skills invoked: /django-migrations, /improving-drf-endpoints, /adopting-generated-api-types, /writing-kea-logics, /writing-ui-components, /writing-user-facing-copy, /writing-tests, /writing-code-comments, /writing-pr-descriptions.

Generated-By: PostHog Desktop
Task-Id: 0aa0698f-585a-4ae9-9e72-04665643a7dd
Generated-By: PostHog Desktop
Task-Id: 0aa0698f-585a-4ae9-9e72-04665643a7dd
…rd-template

Generated-By: PostHog Desktop
Task-Id: 0aa0698f-585a-4ae9-9e72-04665643a7dd
@trunk-io

trunk-io Bot commented Sep 25, 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 Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

⚠️ Complexity (TypeScript) — 11 functions above the limit (max 29)

Cyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions.

Function Location Complexity Limit
DashboardScene frontend/src/scenes/dashboard/Dashboard.tsx:101 29 10
DashboardItems frontend/src/scenes/dashboard/DashboardItems.tsx:82 27 10
refreshDashboardItems frontend/src/scenes/dashboard/dashboardLogic.tsx:4066 27 10
<anonymous> frontend/src/scenes/dashboard/DashboardItems.tsx:514 22 10
saveEditModeChanges frontend/src/scenes/dashboard/dashboardLogic.tsx:1630 19 10
recording_domains frontend/src/scenes/team-activity/teamActivityDescriber.tsx:211 19 10
updateWidgetTile frontend/src/scenes/dashboard/dashboardLogic.tsx:1879 16 10
copyToDashboard frontend/src/scenes/dashboard/dashboardLogic.tsx:1806 15 10
session_recording_masking_config frontend/src/scenes/team-activity/teamActivityDescriber.tsx:266 14 10
moveToDashboardSuccess frontend/src/scenes/dashboard/dashboardLogic.tsx:3804 12 10
<anonymous> frontend/src/scenes/dashboard/dashboardLogic.tsx:5096 11 10
⚠️ Duplication (Python) — 1 new duplicated block (worst 100 tokens)

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

First copy Second copy Lines Tokens
posthog/api/project.py:688 posthog/api/project.py:799 50 100
✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

🚨 Comment density — 7% of added code lines are comments (26 of 373)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
frontend/src/scenes/saved-insights/homeTabLogic.ts 5 79
posthog/api/project.py 4 15
products/dashboards/backend/api/test/test_dashboard_templates.py 4 28
frontend/src/scenes/dashboard/dashboardLogic.tsx 3 9
products/dashboards/backend/models/team_home_tab_dashboard.py 3 10
frontend/src/scenes/team-activity/teamActivityDescriber.tsx 2 3
posthog/api/team.py 2 20
frontend/src/scenes/dashboard/Dashboard.tsx 1 17

This check does not block merging. It updates on every push and clears when the share drops.

⚠️ Bundle size — 🔺 +757.4 KiB (+1.1%)

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

Total: 69.55 MiB · 🔺 +757.4 KiB (+1.1%)

File Size Δ vs base
render-query/src/render-query/render-query.js 20.88 MiB 🔺 +667.7 KiB (+3.2%)
posthog-app/_parent/products/alerts/frontend/views/InsightAlerts.js 13.1 KiB 🔺 +3.4 KiB (+34.5%)
posthog-app/_parent/products/ai_observability/frontend/evaluations/AIObservabilityEvaluationsScene.js 50.4 KiB 🔺 +1.7 KiB (+3.5%)
posthog-app/_parent/products/ai_observability/frontend/prompts/LLMPromptScene.js 55.5 KiB 🔺 +1.7 KiB (+3.1%)
posthog-app/_parent/products/ai_observability/frontend/tags/AIObservabilityTagScene.js 40.3 KiB 🔺 +1.7 KiB (+4.4%)
posthog-app/_parent/products/ai_observability/frontend/tags/AIObservabilityTagsScene.js 20.3 KiB 🔺 +1.7 KiB (+9.1%)
posthog-app/_parent/products/groups/frontend/pages/Groups.js 16.5 KiB 🔺 +1.7 KiB (+11.4%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ReplayScannersScene.js 91.9 KiB 🔺 +1.7 KiB (+1.9%)
posthog-app/src/queries/nodes/WebVitals/WebVitals.js 19.7 KiB 🔺 +1.7 KiB (+9.4%)
posthog-app/src/queries/Query/Query.js 13.5 KiB 🔺 +1.7 KiB (+14.4%)
posthog-app/src/scenes/activity/explore/EventsScene.js 15.6 KiB 🔺 +1.7 KiB (+12.2%)
posthog-app/src/scenes/activity/explore/SessionsScene.js 16.9 KiB 🔺 +1.7 KiB (+11.1%)
posthog-app/src/scenes/debug/DebugScene.js 32.0 KiB 🔺 +1.7 KiB (+5.6%)
posthog-app/src/scenes/insights/InsightQuickStart/InsightQuickStart.js 18.3 KiB 🔺 +1.7 KiB (+10.2%)
posthog-app/src/scenes/web-analytics/SessionAttributionExplorer/SessionAttributionExplorerScene.js 19.3 KiB 🔺 +1.7 KiB (+9.6%)
posthog-app/_parent/products/customer_analytics/frontend/scenes/CustomerJourneyTemplatesScene/CustomerJourneyTemplatesScene.js 20.1 KiB 🔺 +1.7 KiB (+9.2%)
posthog-app/_parent/products/customer_analytics/frontend/scenes/CustomerJourneyBuilderScene/CustomerJourneyBuilderScene.js 14.7 KiB 🔺 +1.7 KiB (+13.0%)
posthog-app/src/scenes/marketing-analytics/MarketingAnalyticsScene.js 139.4 KiB 🔺 +1.7 KiB (+1.2%)
posthog-app/src/scenes/saved-insights/SavedInsights.js 13.6 KiB 🔺 +1.7 KiB (+14.0%)
posthog-app/_parent/products/posthog_ai/frontend/components/tool/widgets/SearchSessionRecordingsWidget.js 14.0 KiB 🔺 +1.7 KiB (+13.4%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ReplayScanner.js 55.5 KiB 🔺 +1.7 KiB (+3.1%)
posthog-app/src/scenes/hog-functions/HogFunctionScene.js 58.4 KiB 🔺 +1.7 KiB (+2.9%)
posthog-app/src/lib/components/ActivityLog/describers.js 168.5 KiB 🔺 +1.7 KiB (+1.0%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/components/ScannerScoutsTab.js 30.8 KiB 🔺 +1.6 KiB (+5.6%)
posthog-app/src/scenes/data-management/definition/DefinitionView.js 38.9 KiB 🔺 +1.6 KiB (+4.4%)
posthog-app/_parent/products/endpoints/frontend/EndpointsScene.js 40.6 KiB 🔺 +1.6 KiB (+4.2%)
posthog-app/_parent/products/ai_observability/frontend/AIObservabilitySessionScene.js 14.2 KiB 🔺 +1.6 KiB (+12.7%)
posthog-app/_parent/products/posthog_ai/frontend/components/tool/widgets/CreateInsightWidget.js 14.0 KiB 🔺 +1.6 KiB (+12.8%)
posthog-app/_parent/products/posthog_ai/frontend/components/tool/widgets/QueryWidget.js 14.1 KiB 🔺 +1.6 KiB (+12.8%)
posthog-app/_parent/products/posthog_ai/frontend/scenes/TaskTracker/components/SidePanelRunnerImpl.js 19.2 KiB 🔺 +1.6 KiB (+9.0%)
posthog-app/_parent/products/signals/frontend/inbox/components/detail/ReportAiPanel.js 16.8 KiB 🔺 +1.6 KiB (+10.5%)
posthog-app/src/layout/panel-layout/navbar/tabs/NavTabChat.js 22.9 KiB 🔺 +1.6 KiB (+7.5%)
posthog-app/_parent/products/persons/frontend/pages/PersonsScene.js 18.6 KiB 🔺 +1.6 KiB (+9.1%)
posthog-app/_parent/products/posthog_ai/frontend/scenes/TaskTracker/TaskTracker.js 24.1 KiB 🔺 +1.6 KiB (+6.9%)
posthog-app/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js 178.6 KiB 🔺 +1.6 KiB (+0.9%)
posthog-app/src/scenes/cohorts/Cohort.js 46.7 KiB 🔺 +1.5 KiB (+3.3%)
posthog-app/_parent/products/early_access_features/frontend/EarlyAccessFeature.js 13.9 KiB 🔺 +1.4 KiB (+11.5%)
posthog-app/_parent/products/actions/frontend/pages/Action.js 14.0 KiB 🔺 +1.4 KiB (+11.0%)
posthog-app/src/scenes/product-tours/ProductTour.js 182.9 KiB 🔺 +1.4 KiB (+0.8%)
posthog-app/src/scenes/web-analytics/WebAnalyticsScene.js 229.4 KiB 🔺 +1.3 KiB (+0.6%)
posthog-app/_parent/products/endpoints/frontend/EndpointScene.js 64.1 KiB 🔺 +1.3 KiB (+2.1%)
posthog-app/_parent/products/signals/frontend/inbox/InboxScene.js 462.8 KiB 🔺 +1.2 KiB (+0.3%)
posthog-app/src/scenes/experiments/SharedMetrics/SharedMetric.js 21.8 KiB 🔺 +1.2 KiB (+6.0%)
posthog-app/_parent/products/business_knowledge/frontend/scenes/BusinessKnowledgeSettingsScene.js 18.0 KiB 🔺 +1.2 KiB (+7.1%)
posthog-app/_parent/products/conversations/frontend/scenes/settings/SupportSettingsScene.js 17.2 KiB 🔺 +1.2 KiB (+7.5%)
posthog-app/_parent/products/customer_analytics/frontend/scenes/CustomerAnalyticsConfigurationScene/CustomerAnalyticsConfigurationScene.js 18.8 KiB 🔺 +1.2 KiB (+6.8%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ScannerEditorScene.js 63.3 KiB 🔺 +1.2 KiB (+1.9%)
posthog-app/src/scenes/settings/SettingsMap.js 17.1 KiB 🔺 +1.2 KiB (+7.5%)
posthog-app/src/scenes/settings/SettingsScene.js 20.7 KiB 🔺 +1.2 KiB (+6.1%)
posthog-app/src/scenes/surveys/Survey.js 14.5 KiB 🔺 +1.2 KiB (+9.0%)
posthog-app/_parent/products/replay_vision/frontend/observations/ObservationRecording.js 18.3 KiB 🔺 +1.1 KiB (+6.6%)
posthog-app/src/scenes/session-recordings/detail/SessionRecordingDetail.js 18.7 KiB 🔺 +1.1 KiB (+6.4%)
posthog-app/src/scenes/session-recordings/file-playback/SessionRecordingFilePlaybackScene.js 21.3 KiB 🔺 +1.1 KiB (+5.6%)
posthog-app/src/scenes/session-recordings/kiosk/SessionRecordingsKiosk.js 26.6 KiB 🔺 +1.1 KiB (+4.4%)
posthog-app/src/scenes/session-recordings/player/modal/SessionPlayerModal.js 19.3 KiB 🔺 +1.1 KiB (+6.2%)
posthog-app/src/scenes/sessions/SessionProfileScene.js 31.6 KiB 🔺 +1.1 KiB (+3.7%)
posthog-app/_parent/products/logs/frontend/LogsScene.js 49.9 KiB 🔺 +1.1 KiB (+2.2%)
posthog-app/src/scenes/session-recordings/SessionRecordings.js 17.9 KiB 🔺 +1.1 KiB (+6.5%)
posthog-app/src/scenes/session-recordings/settings/SessionRecordingsSettingsScene.js 18.8 KiB 🔺 +1.1 KiB (+6.2%)
posthog-app/_parent/products/groups/frontend/pages/Group.js 33.3 KiB 🟢 -1.1 KiB (-3.1%)
posthog-app/_parent/products/persons/frontend/pages/PersonScene.js 42.7 KiB 🟢 -1.1 KiB (-2.4%)
posthog-app/src/scenes/notebooks/NotebookCanvasScene.js 22.9 KiB 🟢 -1.1 KiB (-4.4%)
posthog-app/src/scenes/notebooks/NotebookPanel/NotebookPanel.js 24.7 KiB 🟢 -1.1 KiB (-4.1%)
posthog-app/src/scenes/notebooks/NotebookScene.js 30.3 KiB 🟢 -1.1 KiB (-3.4%)
posthog-app/_parent/products/ai_observability/frontend/AIObservabilityScene.js 185.9 KiB 🟢 -1.1 KiB (-0.6%)
posthog-app/src/scenes/dashboard/Dashboard.js 13.5 KiB 🟢 -1.1 KiB (-7.3%)
posthog-app/_parent/products/conversations/frontend/scenes/ticket/SupportTicketScene.js 67.9 KiB 🔺 +1.0 KiB (+1.5%)
posthog-app/_parent/products/error_tracking/frontend/posthogAi/ErrorTrackingWidget.js 18.0 KiB 🔺 +1.0 KiB (+6.1%)

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.57 MiB · 22 files 🟢 -417 B (-0.0%) █████████░ 85.1% of 1.84 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
3.57 MiB · 628 files 🟢 -420 B (-0.0%) █████████░ 88.6% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.26 MiB · 2,297 files 🔺 +30 B (+0.0%) █████████░ 87.1% of 8.34 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/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 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
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts 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
🟢 [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/index.tsx
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
854 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.13_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
267.6 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
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
100.4 KiB src/lib/api.ts
84.9 KiB src/products.tsx
69.1 KiB src/lib/lemon-ui/icons/icons.tsx
63.9 KiB src/lib/utils/eventUsageLogic.ts
38.7 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
28.3 KiB src/scenes/scenes.ts
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.13_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
271.7 KiB src/taxonomy/core-filter-definitions-by-group.json
267.6 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
153.7 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
100.4 KiB src/lib/api.ts
98.5 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
84.9 KiB src/products.tsx

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.37 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.37 MiB · 19 files 🟢 -475 B (-0.0%) ████░░░░░░ 41.4% of 5.72 MiB
Deferred (lazy) 2.10 MiB · 44 files no change n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
789.8 KiB dist/toolbar/toolbar-app-BJ4B4DT7.css
650.5 KiB dist/toolbar/chunk-chunk-J2XTBSOD.js
483.6 KiB dist/toolbar/chunk-chunk-M677G2Z5.js
138.3 KiB dist/toolbar/chunk-chunk-AZCJQJSR.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-6ANSB5D6.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-47FXT3NK.js
21.0 KiB dist/toolbar/chunk-chunk-EOQOZYXZ.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

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

✅ Dist folder size — 🔺 +4.11 MiB (+0.4%)

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

Total: 947.18 MiB · 🔺 +4.11 MiB (+0.4%)

⚠️ MCP snapshots — 1 updated (1 modified, 0 added, 0 deleted)

Snapshots: MCP unit test snapshots updated

Changes: 1 snapshots (1 modified, 0 added, 0 deleted)

What this means:

  • Snapshots have been automatically updated to match current output

Next steps:

  • Review the changes to ensure they're intentional
  • If unexpected, investigate what caused the output to change

Review snapshot changes →

⚠️ Django migration SQL — 2 new migrations to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/dashboards/backend/migrations/0022_teamhometabdashboard.py

/opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/infi/clickhouse_orm/__init__.py:1: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  __import__("pkg_resources").declare_namespace(__name__)
System check identified some issues:

WARNINGS:
?: (axes.W001) You are using the django-axes cache handler for login attempt tracking. Your cache configuration is however invalid and will not work correctly with django-axes. This can leave security holes in your login systems as attempts are not tracked correctly. Reconfigure settings.AXES_CACHE and settings.CACHES per django-axes configuration documentation.
?: (staticfiles.W004) The directory '/home/runner/work/posthog/posthog/frontend/dist' in the STATICFILES_DIRS setting does not exist.
BEGIN;
--
-- Create model TeamHomeTabDashboard
--
CREATE TABLE "dashboards_teamhometabdashboard" ("team_id" integer NOT NULL PRIMARY KEY, "dashboard_id" bigint NULL);
ALTER TABLE "dashboards_teamhometabdashboard" ADD CONSTRAINT "dashboards_teamhomet_team_id_ac87bd9d_fk_posthog_t" FOREIGN KEY ("team_id") REFERENCES "posthog_team" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "dashboards_teamhometabdashboard" ADD CONSTRAINT "dashboards_teamhomet_dashboard_id_a02f96ea_fk_posthog_d" FOREIGN KEY ("dashboard_id") REFERENCES "posthog_dashboard" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "dashboards_teamhometabdashboard_dashboard_id_a02f96ea" ON "dashboards_teamhometabdashboard" ("dashboard_id");
COMMIT;

products/dashboards/backend/migrations/0023_home_tab_dashboard_templates.py

/opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/infi/clickhouse_orm/__init__.py:1: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  __import__("pkg_resources").declare_namespace(__name__)
System check identified some issues:

WARNINGS:
?: (axes.W001) You are using the django-axes cache handler for login attempt tracking. Your cache configuration is however invalid and will not work correctly with django-axes. This can leave security holes in your login systems as attempts are not tracked correctly. Reconfigure settings.AXES_CACHE and settings.CACHES per django-axes configuration documentation.
?: (staticfiles.W004) The directory '/home/runner/work/posthog/posthog/frontend/dist' in the STATICFILES_DIRS setting does not exist.
BEGIN;
--
-- Raw Python operation
--
-- THIS OPERATION CANNOT BE WRITTEN AS SQL
COMMIT;

Last updated: 2026-09-25 14:18 UTC (c7271b6)

❌ Django migration risk — blocked migration detected

We've analyzed your migrations for potential risks.

Summary: 0 Safe | 1 Needs Review | 1 Blocked

❌ Blocked

Causes locks or breaks compatibility

dashboards.0022_teamhometabdashboard
  │  └─ #1 ✅ CreateModel
  │     Creating new table is safe
  │     model: TeamHomeTabDashboard
  │
  └──> ℹ️  INFO:
       ℹ️  Skipped operations on newly created tables (empty tables
       don't cause lock contention).
  │
  └──> �[91m📋 POSTHOG POLICY VIOLATIONS:�[0m
       ❌ BLOCKED: CreateModel adds a ForeignKey to "posthog_team" - this
       table is read on virtually every request. Creating the FK
       constraint takes a SHARE ROW EXCLUSIVE lock on the referenced
       parent (posthog_team), which conflicts with the ROW EXCLUSIVE
       lock every INSERT/UPDATE/DELETE on it holds. Under write traffic
       the lock request queues, lock_timeout cancels it, and each
       bin/migrate retry repeats the stall. This has blocked deploys.
       Two options: (a) db_constraint=False on the ForeignKey - emits no
       FK constraint and takes NO lock on the parent at all (app-level
       enforcement only). This is the only truly lock-free path. (b) For
       a real database constraint, declare the FK with
       db_constraint=False, then add it back as a DB constraint via
       posthog.migration_helpers.AddForeignKeyNotValid in a later
       migration and ValidateForeignKey after that. NOT VALID still
       takes a *brief* SHARE ROW EXCLUSIVE lock on the parent for the
       metadata add (it skips the row scan), so it shrinks the lock
       window but does not eliminate it; VALIDATE then runs lock-free on
       the parent. If this FK genuinely must lock posthog_team on add,
       add "dashboards.0022_teamhometabdashboard" to posthog/management/
       migration_analysis/hot_table_acknowledged_migrations.txt to
       accept the risk, and coordinate the deploy with #team-
       infrastructure for a low-traffic window. See https://github.com/P
       ostHog/posthog/blob/master/docs/published/handbook/engineering/sa
       fe-django-migrations.md#foreign-keys-to-hot-tables

⚠️ Needs Review

May have performance impact

dashboards.0023_home_tab_dashboard_templates
  └─ #1 ⚠️ RunPython: RunPython data migration needs review for performance

📚 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-09-25 14:18 UTC (c7271b6)

@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[High risk] Adds database schema and API fields for home tab dashboard configuration.

The PR should not merge until Home-tab edits can be saved and dashboard creation reliably persists the team link.

Reviews (1) · Last reviewed commit: "Merge branch 'master' into posthog/produ..."

Comment thread frontend/src/scenes/saved-insights/HomeTab.tsx Outdated
use_template: templateKey,
pinned: true,
})
actions.updateCurrentTeam({ home_tab_dashboard: dashboard.id })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Team update finishes too late Dashboard creation completes without waiting for the request that links it to the team. If that request fails, the Home tab remains unconfigured while a pinned dashboard is left behind. If it is slow, the picker becomes clickable again and can create duplicates. Keep creation pending until the team update succeeds, and handle update failures.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/scenes/saved-insights/homeTabLogic.ts
Line: 77

Comment:
**Team update finishes too late** Dashboard creation completes without waiting for the request that links it to the team. If that request fails, the Home tab remains unconfigured while a pinned dashboard is left behind. If it is slow, the picker becomes clickable again and can create duplicates. Keep creation pending until the team update succeeds, and handle update failures.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +344 to +346
def remove_home_tab_dashboard_templates(apps, schema_editor):
DashboardTemplate = apps.get_model("dashboards", "DashboardTemplate")
DashboardTemplate.objects.filter(template_name__in=HOME_TAB_TEMPLATE_NAMES, team=None).delete()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Rollback deletes existing templates If a global template already has one of these names, the forward migration leaves it untouched, but the reverse migration deletes it. A rollback could therefore remove a template this migration did not create. Limit reversal to rows known to have been seeded here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/dashboards/backend/migrations/0023_home_tab_dashboard_templates.py
Line: 344-346

Comment:
**Rollback deletes existing templates** If a global template already has one of these names, the forward migration leaves it untouched, but the reverse migration deletes it. A rollback could therefore remove a template this migration did not create. Limit reversal to rows known to have been seeded here.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread posthog/api/project.py
Comment on lines +1231 to +1232
if "home_tab_dashboard" in validated_data:
team.home_tab_dashboard = validated_data.pop("home_tab_dashboard")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Home selection lacks audit trail This update saves home_tab_dashboard on an extension and removes it before the Project and Team change checks run. A request that only changes this setting produces no activity entry, so its history cannot be traced there. Record the extension change explicitly. The Team serializer has the same gap.

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/api/project.py
Line: 1231-1232

Comment:
**Home selection lacks audit trail** This update saves `home_tab_dashboard` on an extension and removes it before the Project and Team change checks run. A request that only changes this setting produces no activity entry, so its history cannot be traced there. Record the extension change explicitly. The Team serializer has the same gap.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

access_control: true,
week_start_day: true,
primary_dashboard: true,
home_tab_dashboard: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Create tool advertises rejected field The project-create tool offers home_tab_dashboard, but the backend rejects it whenever there is no existing project instance. A client that supplies the advertised field receives a validation error instead of creating the project. Remove it from the create schema while retaining it for updates.

Prompt To Fix With AI
This is a comment left during a code review.
Path: services/mcp/src/tools/generated/core.ts
Line: 162

Comment:
**Create tool advertises rejected field** The project-create tool offers `home_tab_dashboard`, but the backend rejects it whenever there is no existing project instance. A client that supplies the advertised field receives a validation error instead of creating the project. Remove it from the create schema while retaining it for updates.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@trunk-io

trunk-io Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 30732346-3fe6-46d0-9570-521fcf81b563

📥 Commits

Reviewing files that changed from the base of the PR and between c7271b6 and 429cfa0.

📒 Files selected for processing (1)
  • frontend/src/scenes/saved-insights/HomeTab.tsx

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The changes add a team-level Home tab dashboard setting and expose it through team and project APIs. A migration adds three global dashboard templates. The Home tab now renders the configured dashboard or offers template selection, which creates and assigns a dashboard. Dashboard consumers can require explicit edit mode. The previous Home tab widgets and their supporting logic are removed. Tests cover dashboard assignment, ownership validation, extension storage, and template application.

Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to 429cf

Dashboard selection can leave an unassigned dashboard if saving the team setting fails, and deleting a selected dashboard can leave Home pointing to it. Resolve these paths before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 429cf

The new Home tab keeps dashboard access within the selected team, but creating, linking, and deleting its dashboard can leave the team’s Home tab in a state that does not recover automatically. No cross-team data exposure was established.

Retained concerns

  • Medium · reliability · inferred: Dashboard creation and team association are separate operations. A failed association, retry, or concurrent selection can leave an extra pinned dashboard or overwrite the selected reference, without an evidenced reconciliation step.
  • Medium · reliability · inferred: Soft-deleting the selected dashboard leaves the Home-tab reference set. Normal users are then routed to a dashboard excluded from retrieval rather than back to the picker, making team-wide Home-tab recovery dependent on an explicit setting change or dashboard restoration.
Security review details

Security Blast Radius

  • inferred — A setting change can affect the shared Home tab of its target team. The inspected assignment paths reject attaching another team’s dashboard; no cross-team read or new infrastructure authority was established.

Trust Boundaries and Controls

  • observed — The picker supplies fixed template keys, but the API receives client-supplied values. Dashboard creation uses the request team, and the subsequent team-setting write applies server-side same-team validation; picker controls alone are not the authorization boundary.

Resilience and Maintainability Implications

  • inferred — Same-team validation contains a team-switch race from becoming a cross-team association, but it does not undo a dashboard already created for the former team or repair the Home selection.

Hardening Proposals

  • proposed — Make template creation and association idempotent or provide explicit compensation after a failed link. Clear or recover a Home reference when its dashboard is soft-deleted, with a non-debug recovery path.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and follows the required template. It explains the problem, user-visible changes, testing, release status, documentation, screenshots, and agent context. Minor omissions in…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (4)
posthog/api/team.py-2236-2240 (1)

2236-2240: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

home_tab_dashboard changes are not recorded in the activity log, in either serializer.

Both update() methods assign the new dashboard through the home_tab_dashboard property setter and pop the key from validated_data, but neither logs the change afterward. home_tab_dashboard is a @property backed by the TeamHomeTabDashboard extension, not a model field, so it never appears in instance.__dict__/team.__dict__. Each method's generic diff at the end of update() only compares values captured from __dict__, so it cannot detect this change. Every other extension-backed setting in both files (revenue_analytics_config, marketing_analytics_config, customer_analytics_config, workflows_config, feature_flag_policy_config) is routed through a dedicated helper that explicitly captures the diff. home_tab_dashboard has no equivalent call in either file, so changing a team's Home tab dashboard leaves no audit trail.

  • posthog/api/team.py#L2236-L2240: after instance.home_tab_dashboard = validated_data.pop("home_tab_dashboard"), capture the old/new dashboard IDs and call self._capture_diff(instance, "home_tab_dashboard", old_id, new_id), mirroring _update_revenue_analytics_config.
  • posthog/api/project.py#L1229-L1233: after team.home_tab_dashboard = validated_data.pop("home_tab_dashboard"), capture the old/new dashboard IDs and call capture_team_config_diff(team, "home_tab_dashboard", old_id, new_id, context=config_context), mirroring update_team_revenue_analytics_config.
frontend/src/scenes/saved-insights/HomeTab.tsx-29-31 (1)

29-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the editable dashboard placement for the Home dashboard.

DashboardPlacement.Builtin skips DashboardHeader, which mounts the customize and add-tile controls. Builtin mode also excludes the dashboard edit bar and edit menu. Although canEditDashboard is true for users with Editor access, they cannot enter dashboard edit mode from Home.

Suggested fix
-        return <Dashboard id={String(currentTeam.home_tab_dashboard)} placement={DashboardPlacement.Builtin} />
+        return <Dashboard id={String(currentTeam.home_tab_dashboard)} placement={DashboardPlacement.Dashboard} />
products/dashboards/backend/api/test/test_dashboard_templates.py-1807-1812 (1)

1807-1812: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Narrow the nullable JSON fields before use.

DashboardTemplate.tiles and Insight.query are nullable JSON fields. The repository-wide mypy check can reject len(template.tiles) and "kind" in tile.insight.query under strict_optional.

Proposed fix
-        self.assertEqual(dashboard.tiles.count(), len(template.tiles))
+        assert template.tiles is not None
+        self.assertEqual(dashboard.tiles.count(), len(template.tiles))
         insight_tiles = [tile for tile in dashboard.tiles.all() if tile.insight_id is not None]
         self.assertTrue(insight_tiles, "Expected at least one insight tile")
         for tile in insight_tiles:
             assert tile.insight is not None
+            assert tile.insight.query is not None
             self.assertIn("kind", tile.insight.query)
frontend/src/scenes/saved-insights/HomeTabTemplatePicker.tsx-12-17 (1)

12-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Show an error toast when Home tab dashboard creation fails.

createHomeTabDashboard can fail because of a network/server error or a missing migrated template. The Product analytics option has a hardcoded fallback, but the other three options can raise AttributeError when their global rows are deleted. createHomeTabDashboardFailure only clears the selection, so the picker remains retryable but gives no failure feedback. Handle that action with an error toast.

Suggested fix
-import { MakeLogicType, connect, kea, path, reducers } from 'kea'
+import { MakeLogicType, connect, kea, listeners, path, reducers } from 'kea'
 import { loaders } from 'kea-loaders'
 import posthog from 'posthog-js'
 
+import { lemonToast } from '`@posthog/lemon-ui`'
+
@@
     reducers({
@@
     }),
+    listeners(() => ({
+        createHomeTabDashboardFailure: () => {
+            lemonToast.error('Failed to set up your Home tab dashboard. Please try again.')
+        },
+    })),
     loaders(({ values, actions }) => ({
🧹 Nitpick comments (1)
posthog/api/test/test_team.py (1)

921-942: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parameterize the shared dashboard tests while preserving field-specific checks.

The four tests share most of their flow, so the applicable guidance favors combining them. They are not identical, however. Preserve the primary-dashboard assertions, home-dashboard preservation check, and the primary-dashboard setup in the invalid-dashboard case.

Suggested parameterized refactor
-        def test_update_primary_dashboard(self):
-            d = Dashboard.objects.create(name="Test", team=self.team)
-
-            # Can set it
-            response = self.client.patch("/api/environments/@current/", {"primary_dashboard": d.id})
-            response_data = response.json()
-
-            self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
-            self.assertEqual(response_data["name"], self.team.name)
-            self.assertEqual(response_data["primary_dashboard"], d.id)
-
-        def test_cant_set_primary_dashboard_to_another_teams_dashboard(self):
-            self.team.primary_dashboard_id = None  # Remove the default primary dashboard from the picture
-            self.team.save()
-
-            team_2 = Team.objects.create(organization=self.organization, name="Default project")
-            d = Dashboard.objects.create(name="Test", team=team_2)
-
-            response = self.client.patch("/api/environments/@current/", {"primary_dashboard": d.id})
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-            self.assertEqual(
-                response.json(),
-                self.validation_error_response("Dashboard does not belong to this team.", attr="primary_dashboard"),
-            )
-
-        def test_update_home_tab_dashboard(self):
-            d = Dashboard.objects.create(name="Home tab dashboard", team=self.team)
-
-            response = self.client.patch("/api/environments/@current/", {"home_tab_dashboard": d.id})
-            response_data = response.json()
-
-            self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
-            self.assertEqual(response_data["home_tab_dashboard"], d.id)
-            # Setting home_tab_dashboard must not disturb the unrelated primary_dashboard field
-            self.assertEqual(response_data["primary_dashboard"], self.team.primary_dashboard_id)
-
-        def test_cant_set_home_tab_dashboard_to_another_teams_dashboard(self):
-            team_2 = Team.objects.create(organization=self.organization, name="Default project")
-            d = Dashboard.objects.create(name="Test", team=team_2)
-
-            response = self.client.patch("/api/environments/@current/", {"home_tab_dashboard": d.id})
+        `@parameterized.expand`(
+            [
+                ("primary_dashboard", "Test", True),
+                ("home_tab_dashboard", "Home tab dashboard", False),
+            ]
+        )
+        def test_update_dashboard(self, dashboard_field, dashboard_name, is_primary_dashboard):
+            d = Dashboard.objects.create(name=dashboard_name, team=self.team)
+            response = self.client.patch("/api/environments/@current/", {dashboard_field: d.id})
+            response_data = response.json()
+
+            self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
+            self.assertEqual(response_data[dashboard_field], d.id)
+            if is_primary_dashboard:
+                self.assertEqual(response_data["name"], self.team.name)
+            else:
+                self.assertEqual(response_data["primary_dashboard"], self.team.primary_dashboard_id)
+
+        `@parameterized.expand`(
+            [
+                ("primary_dashboard", True),
+                ("home_tab_dashboard", False),
+            ]
+        )
+        def test_cant_set_dashboard_to_another_teams_dashboard(self, dashboard_field, clear_primary_dashboard):
+            if clear_primary_dashboard:
+                self.team.primary_dashboard_id = None
+                self.team.save()
+
+            team_2 = Team.objects.create(organization=self.organization, name="Default project")
+            d = Dashboard.objects.create(name="Test", team=team_2)
+
+            response = self.client.patch("/api/environments/@current/", {dashboard_field: d.id})
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
             self.assertEqual(
                 response.json(),
-                self.validation_error_response("Dashboard does not belong to this team.", attr="home_tab_dashboard"),
+                self.validation_error_response("Dashboard does not belong to this team.", attr=dashboard_field),
             )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: da544628-172d-450a-9576-7f15bb7b0a4d

📥 Commits

Reviewing files that changed from the base of the PR and between d5d656b and 7c8c97d.

⛔ Files ignored due to path filters (3)
  • frontend/src/generated/core/api.schemas.ts is excluded by !**/generated/**
  • services/mcp/src/generated/core/api.ts is excluded by !**/generated/**
  • services/mcp/src/tools/generated/core.ts is excluded by !**/generated/**
📒 Files selected for processing (34)
  • .github/scripts/check-idor-model-coverage.py
  • frontend/src/lib/api.mock.ts
  • frontend/src/scenes/saved-insights/ActiveUsers.tsx
  • frontend/src/scenes/saved-insights/Activity.tsx
  • frontend/src/scenes/saved-insights/AlertRow.tsx
  • frontend/src/scenes/saved-insights/EventRow.tsx
  • frontend/src/scenes/saved-insights/FiringAlerts.tsx
  • frontend/src/scenes/saved-insights/HomeTab.tsx
  • frontend/src/scenes/saved-insights/HomeTabTemplatePicker.tsx
  • frontend/src/scenes/saved-insights/InsightRow.tsx
  • frontend/src/scenes/saved-insights/NewEvents.tsx
  • frontend/src/scenes/saved-insights/PersonRow.tsx
  • frontend/src/scenes/saved-insights/RecentlyViewed.tsx
  • frontend/src/scenes/saved-insights/SavedInsights.stories.tsx
  • frontend/src/scenes/saved-insights/Trending.tsx
  • frontend/src/scenes/saved-insights/activeUsersLogic.ts
  • frontend/src/scenes/saved-insights/homeTabLogic.ts
  • frontend/src/scenes/saved-insights/newEventsLogic.ts
  • frontend/src/scenes/saved-insights/trendingInsightsLogic.ts
  • frontend/src/scenes/team-activity/teamActivityDescriber.tsx
  • frontend/src/types.ts
  • posthog/api/project.py
  • posthog/api/team.py
  • posthog/api/test/test_team.py
  • posthog/models/team/team.py
  • posthog/models/team/test/test_extensions.py
  • products/dashboards/backend/api/test/test_dashboard_templates.py
  • products/dashboards/backend/migrations/0022_teamhometabdashboard.py
  • products/dashboards/backend/migrations/0023_home_tab_dashboard_templates.py
  • products/dashboards/backend/migrations/max_migration.txt
  • products/dashboards/backend/models/__init__.py
  • products/dashboards/backend/models/team_home_tab_dashboard.py
  • services/mcp/src/api/generated.ts
  • services/mcp/tests/unit/__snapshots__/tool-schemas/project-settings-update.json
💤 Files with no reviewable changes (13)
  • frontend/src/scenes/saved-insights/PersonRow.tsx
  • frontend/src/scenes/saved-insights/RecentlyViewed.tsx
  • frontend/src/scenes/saved-insights/ActiveUsers.tsx
  • frontend/src/scenes/saved-insights/EventRow.tsx
  • frontend/src/scenes/saved-insights/Activity.tsx
  • frontend/src/scenes/saved-insights/Trending.tsx
  • frontend/src/scenes/saved-insights/AlertRow.tsx
  • frontend/src/scenes/saved-insights/FiringAlerts.tsx
  • frontend/src/scenes/saved-insights/newEventsLogic.ts
  • frontend/src/scenes/saved-insights/InsightRow.tsx
  • frontend/src/scenes/saved-insights/activeUsersLogic.ts
  • frontend/src/scenes/saved-insights/trendingInsightsLogic.ts
  • frontend/src/scenes/saved-insights/NewEvents.tsx

Included review availability: Your plan provides up to 12 included reviews per hour; 3 remain after this review.

Comment on lines +29 to +31
if (currentTeam?.home_tab_dashboard) {
return <Dashboard id={String(currentTeam.home_tab_dashboard)} placement={DashboardPlacement.Builtin} />
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -n -C5 'home_tab_dashboard' posthog/api/team.py posthog/api/project.py

Repository: PostHog/posthog

Length of output: 9197


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- changed file ---'
sed -n '1,180p' frontend/src/scenes/saved-insights/HomeTab.tsx
printf '%s\n' '--- dashboard and home-tab references ---'
rg -n -C6 'class Dashboard|def delete|deleted\s*=|home_tab_dashboard|TeamHomeTabDashboard|class Dashboard' posthog/models posthog/api frontend/src/scenes/saved-insights frontend/src | head -n 500
printf '%s\n' '--- relevant diff ---'
git diff --stat d5d656b206f2363bf52ed988950e160e22cfc479 7c8c97d4de0564de83f3f7e7bdd743780e64a189 -- frontend/src/scenes/saved-insights/HomeTab.tsx
git diff d5d656b206f2363bf52ed988950e160e22cfc479 7c8c97d4de0564de83f3f7e7bdd743780e64a189 -- frontend/src/scenes/saved-insights/HomeTab.tsx

Repository: PostHog/posthog

Length of output: 38566


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,180p' frontend/src/scenes/saved-insights/HomeTab.tsx
printf '%s\n' '--- model and extension bindings ---'
rg -n -C6 'home_tab_dashboard|TeamHomeTabDashboard|class Dashboard|def delete|deleted\s*=' posthog frontend/src/scenes/saved-insights frontend/src | head -n 500
printf '%s\n' '--- reviewed diff ---'
git diff --stat d5d656b206f2363bf52ed988950e160e22cfc479 7c8c97d4de0564de83f3f7e7bdd743780e64a189 -- frontend/src/scenes/saved-insights/HomeTab.tsx
git diff d5d656b206f2363bf52ed988950e160e22cfc479 7c8c97d4de0564de83f3f7e7bdd743780e64a189 -- frontend/src/scenes/saved-insights/HomeTab.tsx

Repository: PostHog/posthog

Length of output: 41138


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- extension model ---'
sed -n '1,180p' products/dashboards/backend/models/team_home_tab_dashboard.py
printf '%s\n' '--- dashboard declarations and deletion/read filters ---'
rg -l 'class Dashboard' posthog products | head -n 20
rg -n -C5 'class Dashboard|deleted\s*=\s*models|deleted=True|filter\(.*deleted|exclude\(deleted' products/dashboards/backend posthog/models | head -n 300
printf '%s\n' '--- Dashboard component ---'
fd -i '^Dashboard\\.(tsx|ts)$' frontend/src/scenes/dashboard frontend/src | head -n 20

Repository: PostHog/posthog

Length of output: 29196


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- extension model ---'
sed -n '1,180p' products/dashboards/backend/models/team_home_tab_dashboard.py
printf '%s\n' '--- dashboard declarations and deletion/read filters ---'
rg -l 'class Dashboard' posthog products | head -n 20
rg -n -C5 'class Dashboard|deleted\s*=\s*models|deleted=True|filter\(.*deleted|exclude\(deleted' products/dashboards/backend posthog/models | head -n 300
printf '%s\n' '--- Dashboard component ---'
fd -i '^Dashboard\.(tsx|ts)$' frontend/src/scenes/dashboard frontend/src | head -n 20

Repository: PostHog/posthog

Length of output: 28008


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- dashboard API bindings ---'
rg -n -C8 'class .*Dashboard.*ViewSet|queryset\s*=\s*Dashboard|Dashboard\.objects|deleted' products/dashboards/backend/api products/dashboards/backend/facade | head -n 320
printf '%s\n' '--- Dashboard component bindings ---'
rg -n -C8 'dashboard(Loading|Error|NotFound)|not.?found|error|useValues|dashboardLogic|dashboardsModel|dashboardData' frontend/src/scenes/dashboard/Dashboard.tsx frontend/src/scenes/dashboard | head -n 320

Repository: PostHog/posthog

Length of output: 41683


Clear a deleted Home tab dashboard.

deleteDashboard soft-deletes the dashboard by setting deleted: true. The SET_NULL rule on TeamHomeTabDashboard.dashboard does not run. The team can therefore keep exposing the deleted dashboard ID. HomeTab then stays on the <Dashboard> branch instead of rendering HomeTabTemplatePicker.

Clear home_tab_dashboard when the dashboard is soft-deleted, or serialize the deleted dashboard as null.

Comment on lines +72 to +77
const dashboard = await dashboardsCreate(String(values.currentTeamId), {
name: label,
use_template: templateKey,
pinned: true,
})
actions.updateCurrentTeam({ home_tab_dashboard: dashboard.id })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,105p' frontend/src/scenes/saved-insights/homeTabLogic.ts
rg -n 'updateCurrentTeam:|updateCurrentTeam\(' frontend/src/scenes/teamLogic.ts frontend/src/scenes/team* frontend/src 2>/dev/null | head -65

Repository: PostHog/posthog

Length of output: 12078


🏁 Script executed:

sed -n '270,325p' frontend/src/scenes/teamLogic.tsx
sed -n '375,435p' frontend/src/scenes/teamLogic.tsx
rg -n -C 8 'homeTabDashboard|createHomeTabDashboard|home_tab_dashboard' frontend/src/scenes/saved-insights frontend/src/scenes/teamLogic.test.ts frontend/src/scenes/teamLogic.tsx

Repository: PostHog/posthog

Length of output: 25917


🏁 Script executed:

sed -n '425,475p' frontend/src/scenes/teamLogic.tsx
sed -n '1,120p' frontend/src/scenes/saved-insights/HomeTab.tsx
sed -n '1,90p' frontend/src/scenes/saved-insights/HomeTabTemplatePicker.tsx
rg -n -C 5 'asyncActions|updateCurrentTeam' frontend/src/scenes/teamLogic.tsx frontend/src/scenes/saved-insights/homeTabLogic.ts

Repository: PostHog/posthog

Length of output: 13618


🏁 Script executed:

sed -n '330,375p' frontend/src/scenes/teamLogic.tsx
sed -n '620,690p' frontend/src/scenes/teamLogic.tsx

Repository: PostHog/posthog

Length of output: 5474


🏁 Script executed:

sed -n '470,625p' frontend/src/scenes/teamLogic.tsx

Repository: PostHog/posthog

Length of output: 9626


Await the team update before reporting success.

actions.updateCurrentTeam(...) dispatches the team loader but does not await its PATCH. If the PATCH fails, the home-tab loader can still return the pinned dashboard successfully. The team remains without home_tab_dashboard, so the picker appears again. A retry can create another unassigned dashboard.

Use the async action and add cleanup or an atomic operation if retries must not leave unassigned dashboards.

Suggested fix
-                    actions.updateCurrentTeam({ home_tab_dashboard: dashboard.id })
+                    await teamLogic.asyncActions.updateCurrentTeam({ home_tab_dashboard: dashboard.id })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dashboard = await dashboardsCreate(String(values.currentTeamId), {
name: label,
use_template: templateKey,
pinned: true,
})
actions.updateCurrentTeam({ home_tab_dashboard: dashboard.id })
const dashboard = await dashboardsCreate(String(values.currentTeamId), {
name: label,
use_template: templateKey,
pinned: true,
})
await teamLogic.asyncActions.updateCurrentTeam({ home_tab_dashboard: dashboard.id })

…to home tab dashboard

Adds a hedgehog illustration to the "What does your product do?" picker, and makes the Home tab's created dashboard require an explicit "Customize" action to enter layout edit mode rather than entering it implicitly when a tile is dragged or resized.

Generated-By: PostHog Desktop
Task-Id: 0aa0698f-585a-4ae9-9e72-04665643a7dd

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Make the rollback non-destructive. · 0023_home_tab_dashboard_templates.py:325-356

products/dashboards/backend/migrations/0023_home_tab_dashboard_templates.py:325-356
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Make the rollback non-destructive.

If a matching global template exists before migration 0023, get_or_create reuses it. The reverse callback then deletes that pre-existing row by name. Use a no-op reverse callback or track migration-owned rows explicitly.

Suggested fix
-def remove_home_tab_dashboard_templates(apps, schema_editor):
-    DashboardTemplate = apps.get_model("dashboards", "DashboardTemplate")
-    DashboardTemplate.objects.filter(template_name__in=HOME_TAB_TEMPLATE_NAMES, team=None).delete()
-
-
 class Migration(migrations.Migration):
@@
-        migrations.RunPython(create_home_tab_dashboard_templates, remove_home_tab_dashboard_templates),
+        migrations.RunPython(create_home_tab_dashboard_templates, migrations.RunPython.noop),

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 28238b94-642b-4781-9cf8-eea15cb9df3f

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8c97d and c7271b6.

📒 Files selected for processing (5)
  • frontend/src/scenes/dashboard/Dashboard.tsx
  • frontend/src/scenes/dashboard/DashboardItems.tsx
  • frontend/src/scenes/dashboard/dashboardLogic.tsx
  • frontend/src/scenes/saved-insights/HomeTab.tsx
  • frontend/src/scenes/saved-insights/HomeTabTemplatePicker.tsx

Included review availability: Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@posthog

posthog Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

…emplate

Clears `home_tab_dashboard` so the "what does your product do?" picker reappears, without a shell command. Gated on `preflight.is_debug` so it never shows outside local dev / E2E testing.

Generated-By: PostHog Desktop
Task-Id: 0aa0698f-585a-4ae9-9e72-04665643a7dd

This branch has not been deployed

No deployments
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.

1 participant