Skip to content

Dashboard apply creates empty dashboards: insight tiles silently dropped by PostHog API #13

Description

@timkendall

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

  1. 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 },
    }),
  ],
});
  1. Run posthog-definitions apply.
  2. 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

  1. Link each insight to the dashboard: PATCH /insights/:id/ with { dashboards: [dashboardId] }.
  2. 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):

  1. After insights are created/updated, link each dashboard insight tile via PATCH /insights/:id/ with dashboards: [dashboardId].
  2. Re-fetch the dashboard to resolve tile IDs.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions