Bug description
When posthog-definitions apply creates or updates a dashboard with insight tiles, PostHog silently drops all insight tiles. The dashboard is tagged with the correct iac:hash:* tag, so subsequent applies report unchanged even though the live dashboard has no insight tiles (or only text tiles).
This makes dashboard IaC appear successful while leaving dashboards empty in the UI.
Environment
@posthog/definitions: 0.1.0-alpha.3
- PostHog host:
https://us.posthog.com (Cloud)
- Auth: personal API key with
dashboard:read, dashboard:write, insight:read, insight:write
Steps to reproduce
- Define a dashboard with at least one inline insight tile and one text tile:
import { dashboard, hogql, insight, text } from "@posthog/definitions";
export default dashboard({
key: "repro-empty-dashboard",
name: "Repro empty dashboard",
tiles: [
{
insight: insight({
key: "repro-insight",
name: "Repro insight",
query: hogql("SELECT count() FROM events LIMIT 1"),
}),
layout: { x: 0, y: 0, w: 6, h: 5 },
},
text({
body: "Footer text",
layout: { x: 6, y: 0, w: 6, h: 2 },
}),
],
});
- Run
posthog-definitions apply.
- Open the dashboard in PostHog, or fetch it via API:
GET /api/projects/{project_id}/dashboards/{id}/
Expected behavior
The dashboard should contain both tiles (insight + text) with the layouts defined in the spec.
Actual behavior
apply succeeds and reports the dashboard as created/unchanged.
- The dashboard hash tag (
iac:hash:*) matches the spec.
- Insight tiles are missing. Only text tiles may appear.
- Re-running
apply continues to report unchanged because diffing uses the hash tag, not the live tile count.
Root cause (as far as we can tell)
runDashboardOp serializes insight tiles and sends them in the dashboard create/PATCH payload:
// pipeline.ts — serializeInsightTile
return {
insight: { id },
layouts: layoutsFor(tile.layout),
};
On PostHog Cloud, this payload is accepted (200/201) but insight tiles are not persisted. We verified with direct API calls:
| Request |
Result |
POST /dashboards/ with tiles: [{ insight: { id: N }, layouts: ... }] |
Dashboard created, tiles: [] |
PATCH /dashboards/:id/ with tiles: [{ insight: N, layouts: ... }] |
Insight tile ignored; existing text tiles may remain |
PATCH /dashboards/:id/ with tiles: [{ id: tileId, insight: N, layouts: ... }] |
Layouts update only after the insight is already linked to the dashboard |
PATCH /insights/:id/ with { dashboards: [dashboardId] } |
Creates the insight↔dashboard link; tile appears with empty layouts |
Working two-step workaround
- Link each insight to the dashboard:
PATCH /insights/:id/ with { dashboards: [dashboardId] }.
- Fetch the dashboard to obtain tile IDs, then
PATCH /dashboards/:id/ with tiles that include { id, insight, layouts }.
Text tiles work in a single dashboard PATCH; insight tiles require the two-step flow above.
Suggested fix
In runDashboardOp (or a post-create reconciliation step):
- After insights are created/updated, link each dashboard insight tile via
PATCH /insights/:id/ with dashboards: [dashboardId].
- Re-fetch the dashboard to resolve tile IDs.
- PATCH the dashboard with the full tiles array (including
id for existing tiles) to apply layouts.
Optionally, include live tile count in the diff/hash comparison so a dashboard missing tiles is detected as drift rather than unchanged.
Additional context
- Hash-based diffing (
dashboardHashFromTags) compares the spec hash stored in tags, not the server-side tile list. A dashboard can therefore be permanently stuck as unchanged while empty.
- We worked around this in our repo with a post-apply
sync-dashboard-tiles script; happy to share or upstream a PR if useful.
Generated with Cursor.
Bug description
When
posthog-definitions applycreates or updates a dashboard with insight tiles, PostHog silently drops all insight tiles. The dashboard is tagged with the correctiac:hash:*tag, so subsequent applies report unchanged even though the live dashboard has no insight tiles (or only text tiles).This makes dashboard IaC appear successful while leaving dashboards empty in the UI.
Environment
@posthog/definitions:0.1.0-alpha.3https://us.posthog.com(Cloud)dashboard:read,dashboard:write,insight:read,insight:writeSteps to reproduce
posthog-definitions apply.Expected behavior
The dashboard should contain both tiles (insight + text) with the layouts defined in the spec.
Actual behavior
applysucceeds and reports the dashboard as created/unchanged.iac:hash:*) matches the spec.applycontinues to report unchanged because diffing uses the hash tag, not the live tile count.Root cause (as far as we can tell)
runDashboardOpserializes insight tiles and sends them in the dashboard create/PATCH payload:On PostHog Cloud, this payload is accepted (
200/201) but insight tiles are not persisted. We verified with direct API calls:POST /dashboards/withtiles: [{ insight: { id: N }, layouts: ... }]tiles: []PATCH /dashboards/:id/withtiles: [{ insight: N, layouts: ... }]PATCH /dashboards/:id/withtiles: [{ id: tileId, insight: N, layouts: ... }]PATCH /insights/:id/with{ dashboards: [dashboardId] }Working two-step workaround
PATCH /insights/:id/with{ dashboards: [dashboardId] }.PATCH /dashboards/:id/with tiles that include{ id, insight, layouts }.Text tiles work in a single dashboard PATCH; insight tiles require the two-step flow above.
Suggested fix
In
runDashboardOp(or a post-create reconciliation step):PATCH /insights/:id/withdashboards: [dashboardId].idfor existing tiles) to apply layouts.Optionally, include live tile count in the diff/hash comparison so a dashboard missing tiles is detected as drift rather than unchanged.
Additional context
dashboardHashFromTags) compares the spec hash stored in tags, not the server-side tile list. A dashboard can therefore be permanently stuck as unchanged while empty.sync-dashboard-tilesscript; happy to share or upstream a PR if useful.Generated with Cursor.