Skip to content

feat(posthog-tools): dashboards-as-code pipeline + developer-funnel dashboard#311

Merged
blove merged 25 commits into
mainfrom
worktree-gtm+dashboards-as-code
May 14, 2026
Merged

feat(posthog-tools): dashboards-as-code pipeline + developer-funnel dashboard#311
blove merged 25 commits into
mainfrom
worktree-gtm+dashboards-as-code

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 14, 2026

Summary

Implements Spec 1A (analytics-foundation sub-spec A): the PostHog dashboards-as-code pipeline. Round-trip verified end-to-end against PostHog project 406826.

  • New Nx project posthog-tools at tools/posthog/
  • Typed PostHog client via openapi-fetch + 107k LOC of generated types from PostHog's OpenAPI spec
  • sync CLI with --plan (read-only diff) and --apply (idempotent upsert + writeback + insight→dashboard wiring pass) and --apply --delete-orphans (explicit deletion only)
  • report CLI: pulls dashboards tagged gtm, renders weekly markdown snapshot with sparklines, fails loud on transport errors
  • Maps our local InsightLocal zod schema to PostHog's modern query/HogQL body shape (InsightVizNode + TrendsQuery/FunnelsQuery); legacy filters shape is rejected by modern PostHog accounts
  • Sample dashboard developer-funnel + 4 insights — round-trip verified to render with correct query shapes on production PostHog
  • 38 tests across env.spec.ts, schema.spec.ts, sync.spec.ts, report.spec.ts, taxonomy.spec.ts
  • Permanent taxonomy guard test: every event referenced in any insight JSON must appear in docs/gtm/taxonomy.md. Stays green on every CI run; catches event renames or insight drift.
  • New CI job posthog-sync-plan with Nx-affected gating + soft-skip on missing secret (works for fork PRs)
  • Decomposition update: gtm.md §6/§7 + meta-spec §6 now reflect 1A–1D sub-specs

Spec: docs/superpowers/specs/gtm/2026-05-14-analytics-foundation-1a-dashboards-as-code-design.md
Plan: docs/superpowers/plans/gtm/2026-05-14-analytics-foundation-1a-dashboards-as-code.md

End-to-end verification

Manually ran the full pipeline against PostHog project 406826 ("Angular Agent Framework"):

```
$ npm run posthog:sync # --plan
[create] dashboard developer-funnel
[create] insight cockpit-recipe-completion
[create] insight install-command-clicks
[create] insight pageviews-by-landing
[create] insight six-signal-activation-funnel
[orphan] ... (PostHog default-onboarding artifacts, correctly NOT auto-deleted)

$ npm run posthog:apply
applied: 5, failed: 0
Writeback complete.
```

API verification of the resulting dashboard (https://us.posthog.com/project/406826/dashboard/1582272):

```
Dashboard: GTM · Developer funnel | tiles: 4

  • Six-signal activation (30-min window) | FunnelsQuery | events: cockpit:install_command_copied,
    cockpit:transport_connected, cockpit:chat_first_message, cockpit:thread_persisted,
    cockpit:interrupt_handled, cockpit:generative_component_rendered
  • Cockpit recipe completion | TrendsQuery | events: cockpit:recipe_start, cockpit:chat_first_message
  • Install command clicks | TrendsQuery | events: marketing:cta_click
  • Pageviews by landing path | TrendsQuery | events: $pageview
    ```

Bugs caught + fixed during end-to-end verification

The "test thoroughly" loop caught three real bugs that would have shipped broken otherwise:

  1. Empty dashboard tiles — PostHog's API doesn't accept tiles on dashboard create/update. Fixed by adding a wiring pass that PATCHes each insight with its desired dashboards: [<id>] list.
  2. Legacy filters rejected — PostHog migrated to a HogQL-based query schema; our flat-shape body was silently downgraded to empty placeholders, then rejected outright by the modern API. Fixed by adding toPostHogInsight() / toPostHogDashboard() mappers producing InsightVizNode / TrendsQuery / FunnelsQuery.
  3. Node 20 compatnode:fs/promises.glob requires Node 22; CI runs Node 20. Replaced with readdir + filter.

Test Plan

  • CI passes: lint, test, posthog-sync-plan (Nx-affected gate green)
  • npm run posthog:sync shows current PostHog state (5 already-synced, 8 orphan)
  • npm run posthog:apply is idempotent — re-running produces `applied: 5, failed: 0` with no side effects
  • PostHog UI confirms GTM · Developer funnel dashboard renders 4 tiles
  • Taxonomy guard test stays green (nx run posthog-tools:test)

Known limitations (deferred to follow-ups, not blocking)

  • Report CLI tile extraction yields empty rows when no events are flowing yet — structural template + sparklines work, table is empty because the four insights have zero events for now (events are produced by Spec 1B / 1C / 1D)
  • Single shared POSTHOG_PERSONAL_API_KEY in CI — production hardening TODO documented in tools/posthog/README.md to migrate to a read-only Personal API Key for CI
  • tools/posthog/cohorts/ ships empty with .gitkeep — cohorts come after data flows

Cleanup needed in PostHog after merge

The PostHog project has 8 orphan dashboards/insights/cohorts from default onboarding ("My App Dashboard", "Daily active users", "Internal / Test users", etc.). These are correctly not auto-deleted. Decide whether to:

  • Run npm run posthog:apply -- -- --delete-orphans (deletes them explicitly), or
  • Add them to the JSON tree as tombstone artifacts, or
  • Leave them as-is.

🤖 Generated with Claude Code

blove and others added 25 commits May 13, 2026 16:26
Decomposes the original analytics-foundation workstream into four
sub-specs (1A-1D). 1A: PostHog dashboards-as-code pipeline, one sample
dashboard, full test coverage, Nx-affected CI gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates gtm.md §6/§7 and meta-spec §6 to reflect the 4-spec decomposition
agreed during Spec 1A brainstorm.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ools

Devdeps for the Spec 1A dashboards-as-code pipeline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
namedInputs.taxonomy includes docs/gtm/taxonomy.md so taxonomy edits
mark this project as affected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Commits the generated posthog-api.gen.ts. Regenerate quarterly via
nx run posthog-tools:generate-types.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6 tests covering required key, host default, project id coercion,
and rejection of missing/short/non-numeric values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Schemas validate slug format, required fields, funnel-specific
constraints. Fixture validator parses every committed JSON file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three-attempt retry with exponential backoff (500ms, 1s, 2s, max 8s)
for transient PostHog errors. Lazy singleton via ph().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Loads local JSON, validates via zod, matches against remote by
posthog_id (or unique name fallback), classifies into
create/update/orphan. Ambiguous name matches force create.

6 tests covering match-by-id, name fallback, ambiguous case,
orphan detection, invalid JSON error handling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Apply order: cohorts → insights → dashboards. Creates before updates
within a tier. Atomic writeback via temp-file + rename. Partial success
tracked. Orphans only deleted with deleteOrphans:true.

6 new tests cover apply order, writeback persistence, slug resolution
failure, partial success, dry-run no-write, orphan protection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CLI accepts --plan, --apply, --apply --delete-orphans. Adapter wraps
openapi-fetch in the SyncClient interface used by tests. Direct-run
guard prevents tests from triggering main().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The prior filter (kind !== 'cohort' || path) accepted every create
because cohorts also have a path. Replaced with a failure-set check
so the "commit the writeback" hint only lists items that actually
succeeded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7 tests cover sparkline edge cases (empty, all-zero, normalization),
delta-cell formatting (new, percent, negative), and markdown structure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
generateReport() pulls dashboards tagged 'gtm', queries each tile's
insight, folds daily counts into 4 weekly buckets. Output goes to
docs/gtm/reports/<date>-weekly[-N].md (never overwrites).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The round-trip proof for Spec 1A. Four insights: pageviews-by-landing,
install-command-clicks, cockpit-recipe-completion (zero until Spec 1C),
six-signal-activation-funnel (zero until Spec 1C). Empty cohorts/.gitkeep
since cohorts come after data flows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every event referenced in any insight JSON must appear in
docs/gtm/taxonomy.md. This test stays green permanently and
catches event renames or insight drift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
npm run posthog:sync/apply/report/generate-types as thin Nx aliases.
.env.example documents POSTHOG_PERSONAL_API_KEY, POSTHOG_HOST,
POSTHOG_PROJECT_ID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New posthog-sync-plan job runs `nx run posthog-tools:sync:plan`
on PRs that affect posthog-tools (per nx show projects --affected).
Soft-skips when POSTHOG_PERSONAL_API_KEY secret is absent (fork PRs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops speculative JSON Schema references (zod-only), updates CLI
commands to nx run posthog-tools:*, documents POSTHOG_PERSONAL_API_KEY,
adds rename procedure, links to the spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- schema.spec.ts: replace node:fs/promises.glob (Node 22+) with readdir
  filter. CI runs Node 20; the original test would have failed.
- report.ts: add expectOk() helper that throws on PostHog HTTP errors
  instead of silently returning empty results. A 401 now fails the
  weekly snapshot rather than producing a misleading zero-row report.

Fixes flagged in final code review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First --apply against PostHog project 406826 succeeded. Records the
PostHog-assigned ids so future syncs match by posthog_id, not by name.

applied: 5, failed: 0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PostHog's API doesn't accept the tiles field on dashboard create/update —
it's read-only. Insights are associated with dashboards via the insight's
`dashboards: number[]` field, set via PATCH.

Changes:
- Drop tiles from dashboard create/update body (stripTiles helper).
- Keep tile-slug resolution as early validation (unknown slug still throws).
- Add a wiring pass after all tiers: for each local dashboard, PATCH each
  referenced insight with its full desired dashboards list. Idempotent.
- 2 new tests pin the new behavior (35 total).

Discovered when first --apply succeeded structurally but PostHog showed
tile count of 0 on the new dashboard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…hape

PostHog's API expects insight queries inside a nested `filters` object,
not flat at the top level. Our zod schemas describe local intent
(slug/kind/events/breakdown/date_from); PostHog needs
filters: { insight: 'TRENDS'|'FUNNELS', events: [...], breakdown,
breakdown_type, date_from, interval, funnel_window_interval, ... }.

Without this mapping, PostHog silently discards our query body and
stores blank-filter placeholder insights — the dashboard renders but
every tile is empty.

Adds toPostHogInsight() and toPostHogDashboard() with 3 new mapper
tests (38 total). Replaces the spread-of-item.local in createInsight /
updateInsight / createDashboard / updateDashboard.

Discovered when API verification showed tile filters all reduced to
{ insight: 'TRENDS', date_from: null, date_to: null } after first
--apply.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PostHog rejected our legacy filters body with:
  "Creating or updating insights with legacy filters is not available
   for this user."

Modern PostHog projects require the InsightVizNode / HogQL query
schema:
  query: {
    kind: 'InsightVizNode',
    source: {
      kind: 'TrendsQuery' | 'FunnelsQuery',
      series: [{ kind: 'EventsNode', event, math, ... }],
      dateRange: { date_from },
      interval, breakdownFilter, funnelsFilter, ...
    }
  }

Rewrote toPostHogInsight() accordingly. Updated the two mapper tests
to assert against the new schema shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TypeScript incremental cache, generated by tsc --noEmit during local
test runs. Not portable across machines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 14, 2026 2:06am

Request Review

@blove blove merged commit 8952ef1 into main May 14, 2026
16 checks passed
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