Skip to content

Instantly share code, notes, and snippets.

@williamhzo
Last active March 20, 2026 12:37
Show Gist options
  • Select an option

  • Save williamhzo/1f8a08fdcb53f714a05ab26073bf0d5d to your computer and use it in GitHub Desktop.

Select an option

Save williamhzo/1f8a08fdcb53f714a05ab26073bf0d5d to your computer and use it in GitHub Desktop.
PostHog analytics report skill for Claude Code
name posthog-report
description Generate a PostHog analytics report for the current project. Queries events, funnels, errors, and user activity via PostHog MCP tools. Use when asked about analytics, metrics, user behavior, errors, funnels, or "how is the app doing".
disable-model-invocation true
allowed-tools mcp__posthog__query-run, mcp__posthog__query-generate-hogql-from-question, mcp__posthog__list-errors, mcp__posthog__error-details, mcp__posthog__error-tracking-issues-list, mcp__posthog__insights-get-all, mcp__posthog__insight-get, mcp__posthog__insight-query, mcp__posthog__entity-search, mcp__posthog__event-definitions-list, mcp__posthog__event-definition-update, mcp__posthog__properties-list, mcp__posthog__experiment-get-all, mcp__posthog__experiment-get, mcp__posthog__experiment-results-get, mcp__posthog__feature-flag-get-all, mcp__posthog__feature-flag-get-definition, mcp__posthog__surveys-get-all, mcp__posthog__survey-stats, mcp__posthog__surveys-global-stats, mcp__posthog__cohorts-list, mcp__posthog__persons-list, mcp__posthog__annotations-list, mcp__posthog__dashboards-get-all, mcp__posthog__dashboard-get, mcp__posthog__actions-get-all, mcp__posthog__logs-query, mcp__posthog__logs-list-attributes, mcp__posthog__docs-search
argument-hint [time range, e.g. '24h', '7d', '30d'] [focus, e.g. 'errors', 'funnel', 'traffic']

Generate a PostHog analytics report using PostHog MCP tools. Prefer structured queries (TrendsQuery, FunnelsQuery, PathsQuery) over raw HogQL. Only fall back to HogQL when the structured API cannot express the query.

Step 0: Parse arguments

Default time range: 7 days. If $ARGUMENTS specifies a range (e.g. "24h", "3d", "30d", "this week"), convert to an appropriate date_from / date_to:

  • "24h" / "today" -> date_from: "-1d"
  • "7d" / "week" -> date_from: "-7d"
  • "30d" / "month" -> date_from: "-30d"
  • "90d" / "quarter" -> date_from: "-90d"

Optional focus area from arguments: "errors", "funnel", "traffic", "users", or blank for full report.

Step 1: Discover project events

Run these in parallel:

  1. Event definitions: event-definitions-list to get all tracked events
  2. Custom event counts via structured TrendsQuery:
    {
      "query": {
        "kind": "InsightVizNode",
        "source": {
          "kind": "TrendsQuery",
          "series": [{ "kind": "EventsNode", "event": null, "custom_name": "All events", "math": "total" }],
          "breakdownFilter": { "breakdown": "event", "breakdown_type": "event", "breakdown_limit": 30 },
          "dateRange": { "date_from": "-7d" },
          "trendsFilter": { "display": "ActionsBarValue" },
          "filterTestAccounts": true
        }
      }
    }
  3. Existing saved insights: insights-get-all (limit 20) to check what the project already tracks
  4. Active experiments: experiment-get-all
  5. Feature flags: feature-flag-get-all
  6. Active surveys: surveys-get-all

Use discovered events to decide which queries to run next. If funnel-like events exist (initiated/confirmed/failed patterns), run funnel analysis. If error events exist, run error breakdown.

Step 2: Core queries (parallel where possible)

Use query-run with structured queries. Run as many as possible in parallel.

2a. Unique users trend (TrendsQuery)

{
  "query": {
    "kind": "InsightVizNode",
    "source": {
      "kind": "TrendsQuery",
      "series": [
        { "kind": "EventsNode", "event": null, "custom_name": "Total events", "math": "total" },
        { "kind": "EventsNode", "event": null, "custom_name": "Unique users", "math": "dau" }
      ],
      "dateRange": { "date_from": "{DATE_FROM}" },
      "interval": "day",
      "trendsFilter": { "display": "ActionsLineGraph" },
      "filterTestAccounts": true
    }
  }
}

2b. Top pages (TrendsQuery with breakdown)

Only if $pageview events exist:

{
  "query": {
    "kind": "InsightVizNode",
    "source": {
      "kind": "TrendsQuery",
      "series": [{ "kind": "EventsNode", "event": "$pageview", "custom_name": "Page views", "math": "total" }],
      "breakdownFilter": { "breakdown": "$pathname", "breakdown_type": "event", "breakdown_limit": 15 },
      "dateRange": { "date_from": "{DATE_FROM}" },
      "trendsFilter": { "display": "ActionsBarValue" },
      "filterTestAccounts": true
    }
  }
}

2c. Errors via MCP tools

Use list-errors to fetch error tracking data. This provides richer info than raw queries (affected users, first/last seen, status, sessions).

For the top individual errors, use error-details on the most frequent ones.

2d. Referrer / UTM breakdown (TrendsQuery)

{
  "query": {
    "kind": "InsightVizNode",
    "source": {
      "kind": "TrendsQuery",
      "series": [{ "kind": "EventsNode", "event": "$pageview", "custom_name": "Views by referrer", "math": "total" }],
      "breakdownFilter": { "breakdown": "$referring_domain", "breakdown_type": "event", "breakdown_limit": 10 },
      "dateRange": { "date_from": "{DATE_FROM}" },
      "trendsFilter": { "display": "ActionsBarValue" },
      "filterTestAccounts": true
    }
  }
}

Step 3: Adaptive queries based on discovered events

Funnel analysis (FunnelsQuery)

If events follow an initiated/confirmed/failed pattern, use a proper FunnelsQuery:

{
  "query": {
    "kind": "InsightVizNode",
    "source": {
      "kind": "FunnelsQuery",
      "series": [
        { "kind": "EventsNode", "event": "step_initiated", "custom_name": "Initiated" },
        { "kind": "EventsNode", "event": "step_confirmed", "custom_name": "Confirmed" }
      ],
      "dateRange": { "date_from": "{DATE_FROM}" },
      "funnelsFilter": {
        "funnelOrderType": "ordered",
        "funnelWindowInterval": 1,
        "funnelWindowIntervalUnit": "day",
        "funnelVizType": "steps"
      },
      "filterTestAccounts": true
    }
  }
}

Replace series events with the actual discovered event names.

User paths (PathsQuery)

If the project has pageview data, run a PathsQuery to see navigation flow:

{
  "query": {
    "kind": "InsightVizNode",
    "source": {
      "kind": "PathsQuery",
      "dateRange": { "date_from": "{DATE_FROM}" },
      "pathsFilter": {
        "includeEventTypes": ["$pageview"],
        "stepLimit": 5,
        "edgeLimit": 20
      },
      "filterTestAccounts": true
    }
  }
}

Feature usage (TrendsQuery)

For custom events (non-system), show usage ranked:

{
  "query": {
    "kind": "InsightVizNode",
    "source": {
      "kind": "TrendsQuery",
      "series": [
        { "kind": "EventsNode", "event": "event_name_1", "custom_name": "Event 1", "math": "total" },
        { "kind": "EventsNode", "event": "event_name_2", "custom_name": "Event 2", "math": "total" }
      ],
      "dateRange": { "date_from": "{DATE_FROM}" },
      "trendsFilter": { "display": "ActionsBarValue" },
      "filterTestAccounts": true
    }
  }
}

Build the series array from the custom events discovered in Step 1.

Error breakdown (HogQL fallback)

For *_failed events with error properties that need grouping by error message, HogQL is appropriate since structured queries can't group by arbitrary properties:

{
  "query": {
    "kind": "DataVisualizationNode",
    "source": {
      "kind": "HogQLQuery",
      "query": "SELECT event, properties.error as error, count() as count FROM events WHERE event LIKE '%_failed' AND timestamp > now() - INTERVAL {INTERVAL} GROUP BY event, error ORDER BY count DESC LIMIT 20"
    }
  }
}

Existing insights

Use entity-search to find saved insights/dashboards by name. If relevant ones exist, use insight-query to pull their latest data instead of re-querying.

Experiment results

For any active experiments from Step 1, use experiment-results-get with refresh: true.

Survey stats

If surveys exist from Step 1, use survey-stats for each active survey and surveys-global-stats for the aggregate.

Step 4: Format the report

Present findings as a structured report with markdown tables.

Overview

  • Total events, unique users, daily averages
  • Trend: compare first half vs second half of the period to show direction

Traffic

  • Top pages by views (table)
  • Referrer breakdown
  • Daily activity trend (highlight peaks/dips)
  • User paths summary (top flows)

Funnel (if applicable)

  • Step-by-step conversion with success/failure rates
  • Identify the biggest drop-off point
  • Calculate overall conversion rate

Errors

  • Top errors by frequency (table with count and type)
  • Include affected user count and session count from list-errors
  • Flag any error that represents >5% of its related flow
  • Note if exception messages are null/empty (suggests missing source maps)

Feature Usage

  • Custom events ranked by usage
  • Highlight underused features (bottom 20%)

Experiments (if any)

  • Active experiments and their current results
  • Statistical significance status

Surveys (if any)

  • Active surveys with response rates
  • Summary of feedback

Feature Flags

  • Active flags count and rollout status

Key Findings & Recommendations

  • 3-5 bullet points with the most actionable insights
  • Each finding should reference specific numbers
  • Suggest concrete next steps (e.g. "investigate X error", "add tracking for Y")

Rules

  • Prefer structured queries (TrendsQuery, FunnelsQuery, PathsQuery) over HogQL. Only use HogQL (via DataVisualizationNode) when the structured API cannot express the query (e.g. arbitrary property grouping, complex joins).
  • Use MCP tools first: list-errors over HogQL for errors, experiment-results-get over raw queries for experiments, entity-search to find saved insights.
  • If a query or tool call fails or returns empty, note it and move on. Don't retry the same call.
  • All percentages should be rounded to one decimal place.
  • If the focus argument is set, prioritize that section but still include the overview.
  • Present numbers with context: "168 lock failures (53% of 319 attempts)" not just "168 failures".
  • Replace {DATE_FROM} with the resolved date range string (e.g. -7d, -30d).
  • Replace {INTERVAL} in HogQL queries with the time range (e.g. 7 DAY, 30 DAY).
  • Set filterTestAccounts: true on all queries to exclude internal traffic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment