Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Hamster Posthog User Audit

ASecurity

Per-user PostHog event audit for Hamster Studio. Use when the user asks to \\\"review my events in posthog\\\", \\\"audit attribution for <user>\\\", \\\"tell me the story of what user <X> did\\\", \\\"verify $ai_generation events for <user>\\\", or \\\"did event Y fire when I clicked Z\\\". Two modes — guided (ask the user to perform a sequence of actions, then audit that exact window) and free (audit whatever happened in a recent window and reconstruct the story). Sister to `hamster-posthog...

2 stars
0 votes
0 copies
0 views
Added 9/27/2026
toolspythongoshellbashsqlapi

Works with

cliapi

Security Analysis

A100/100

Scanned 9/27/2026

$npx -y skills add gethamster/skills --skill hamster-posthog-user-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hamster Posthog User Audit?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Hamster Posthog User Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gethamster-hamster-posthog-user-audit/badge)](https://www.skillsdirectory.com/skills/gethamster-hamster-posthog-user-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: hamster-posthog-user-audit
description: "Per-user PostHog event audit for Hamster Studio. Use when the user asks to \\\"review my events in posthog\\\", \\\"audit attribution for <user>\\\", \\\"tell me the story of what user <X> did\\\", \\\"verify $ai_generation events for <user>\\\", or \\\"did event Y fire when I clicked Z\\\". Two modes — guided (ask the user to perform a sequence of actions, then audit that exact window) and free (audit whatever happened in a recent window and reconstruct the story). Sister to `hamster-posthog-dashboards`; this one is single-user-scoped."
version: "1.0.0"
---

# hamster-posthog-user-audit

When the question is **"what did this one user do, and were the events populated correctly?"**, use this skill. For dashboard-level analysis (multiple users, aggregates), use `hamster-posthog-dashboards` instead.

## When to invoke

- "Review LLM events for `<email>` in `<env>`"
- "Tell me the story of what I did" / "what events did I fire?"
- "Audit `<user>`'s events"
- "Verify `$ai_generation` attribution for `<user>` over `<window>`"
- "I'm about to do X — watch my events and tell me if they're right"
- "Did `<EventName>` fire when I clicked `<Y>`?"

## When NOT to invoke

- Question is about a dashboard or aggregate metric → `hamster-posthog-dashboards`.
- User wants to *add* event tracking → `hamster-analytics`.
- User wants raw event data dumped to a file with no analysis → call `posthog_client.fetch_events()` directly.

## Two modes

The skill operates in one of two modes. **Confirm the mode with the user if it's ambiguous.**

### Guided mode

Use when the user wants a controlled experiment — they will perform a specific sequence of actions and want you to verify that the right events fire with the right properties.

1. **Define the action plan.** Either accept the user's plan or propose one (e.g. "1. sign in, 2. create a brief, 3. click Generate Plan, 4. send a chat message in that brief, 5. wait 2 minutes for background jobs, 6. tell me when done").
2. **Note the start timestamp** (UTC, second precision) before the user begins.
3. **Tell the user to proceed and to confirm when complete.** Don't try to drive the browser; this is a manual flow.
4. **Wait for confirmation.** Don't poll or pre-fetch — there's a 5–30s ingestion lag plus background-job lag (BullMQ jobs can run 1–10+ minutes after the user action).
5. **Run the audit** against `[start_time, now]`. Use Q1–Q8 below.
6. **Compare what fired against what should have fired** given the action plan. Flag every gap.
7. **Verdict**: pass / fail / partial. List the specific events that fired correctly and the ones that didn't, with property-level details.

### Free mode

Use when the user already did stuff and wants a retrospective.

1. **Default window: last 7 days.** Adjust to match the user's stated activity if they say "last hour" / "yesterday" / etc.
2. Run Q1–Q8.
3. **Reconstruct the story** chronologically, broken into sessions.
4. **Verify properties** for each event class.
5. **Flag findings** as F1, F2, … with action items.

### Mode-decision shortcuts

| User says | Mode |
| --- | --- |
| "review my events", "what did I do", "tell me the story" | free |
| "I just did X", "I'm about to test", "watch what fires" | guided |
| "audit `<user>`" with no other context | free |
| "verify event X fires correctly when I do Y" | guided |

## Inputs the user MUST provide

| Input | Required | Default |
| --- | --- | --- |
| Email (or distinct_id) | yes | — |
| Env (`prod`, `tux`, `dev`, `local`) | yes | — |
| Window | no (free) / no (guided — defined by start_time→now) | 7d |
| Focus (`LLM only`, `product only`, `billing`, `all`) | no | `all` |

If any required input is missing, ask via `AskUserQuestion`.

## Credentials & environment

PostHog query API hits `POST /api/projects/<id>/query` with `{"query": {"kind": "HogQLQuery", "query": "..."}}`.

| Var | Value | Where |
| --- | --- | --- |
| `POSTHOG_API_KEY` | `phx_…` personal API key | `packages/glass-box/.env.<env>` |
| `POSTHOG_PROJECT_ID` | env-specific | see below |
| Host | `https://us.posthog.com` | hard-coded |

Project IDs (named `POSTHOG_PROD_PROJECT_ID` in the env files but means "the prod-side project for that env"):

| Env | Project ID |
| --- | --- |
| prod | `371628` |
| tux | `371881` |

Use the `POSTHOG_API_KEY`, **not** `POSTHOG_DASHBOARD_API_KEY` — the dashboard key lacks `events:read`.

## How to run queries

The simplest path — raw `requests`, no glass-box init (which pulls Supabase and slows things down):

```bash
uv run --no-project --with requests python /tmp/posthog_query.py
```

Inside the script:

```python
import json
from urllib import request
HOST = 'https://us.posthog.com'
PROJ = '<env-specific>'  # 371628 prod, 371881 tux
KEY = '<phx_…>'

def hq(q):
    body = json.dumps({'query': {'kind': 'HogQLQuery', 'query': q}}).encode()
    req = request.Request(f'{HOST}/api/projects/{PROJ}/query', data=body,
        headers={'Authorization': f'Bearer {KEY}', 'Content-Type': 'application/json'},
        method='POST')
    return json.loads(request.urlopen(req, timeout=60).read())
```

For a one-off use of `PostHogClient.query(hogql)` from `packages/glass-box/replay/utils/posthog_client.py`, the import side-effects pull in Supabase. If that's already configured locally, fine; otherwise prefer the raw approach.

## HogQL pitfalls (memorise these)

1. **`$`-prefixed keys need bracket syntax.** `properties.$ai_trace_id` returns NULL silently. Use `properties['$ai_trace_id']`.
2. **`OPERATION_PROPERTY_NONE` is the literal string `'none'`.** Filter as missing in property-presence audits — `prop NOT IN ('', 'none')`.
3. **Shell escaping breaks complex HogQL.** Write the query to a temp `.py` file and run that — don't try to inline through `python -c "…"` with nested quotes.
4. **A person can have multiple distinct_ids** (anonymous browser ID + Supabase user.id). Use `person.properties.email = '<email>'` instead of `distinct_id = '<id>'` so you catch both.

## The query playbook (Q1–Q8)

### Q1 — Person + distinct_ids

```sql
SELECT person.id, person.properties.email, person.properties.name,
       max(timestamp) as last_seen, count() as evt_count
FROM events
WHERE person.properties.email = '<email>'
  AND timestamp > <start>
GROUP BY person.id, person.properties.email, person.properties.name
LIMIT 5
```

```sql
SELECT distinct_id, count() as n,
       min(timestamp) as first_seen, max(timestamp) as last_seen
FROM events
WHERE person.properties.email = '<email>'
  AND timestamp > <start>
GROUP BY distinct_id ORDER BY n DESC
```

Expect 1–2: the Supabase user.id (highest count) and a UUIDv7 anonymous browser ID from before identify.

### Q2 — Event-name overview

```sql
SELECT event, count() as n, min(timestamp) as first, max(timestamp) as last
FROM events
WHERE person.properties.email = '<email>'
  AND timestamp > <start>
GROUP BY event ORDER BY n DESC
```

Skim for:

- Typed product events (`Brief Created`, `Plan Generation Started`, etc.)
- LLM events (`$ai_generation`, `$ai_trace`, `$ai_span`)
- Drift events (`User Signed In` vs `User Logged In`, `checkout.started`, `user.signedUp`)
- Infra events (`$identify`, `$pageview`, `$autocapture` — usually skip from the story but useful for session boundaries)

### Q3 — `$ai_generation` attribution audit

```sql
SELECT
  count() as total,
  countIf(notEmpty(toString(properties['$ai_span_name']))) as has_span_name,
  countIf(notEmpty(toString(properties['$ai_trace_id']))) as has_trace,
  countIf(properties['service.name'] IS NOT NULL) as has_service_name,
  countIf(properties.account_id IS NOT NULL AND properties.account_id NOT IN ('', 'none')) as has_account,
  countIf(properties.prompt_key IS NOT NULL AND properties.prompt_key NOT IN ('', 'none')) as has_prompt_key,
  countIf(properties.step_id IS NOT NULL AND properties.step_id NOT IN ('', 'none')) as has_step_id,
  countIf(properties.agent_name IS NOT NULL AND properties.agent_name NOT IN ('', 'none')) as has_agent_name,
  countIf(distinct_id = toString(properties['$ai_trace_id'])) as anonymous_n
FROM events
WHERE person.properties.email = '<email>'
  AND event = '$ai_generation'
  AND timestamp > <start>
```

Acceptance bar (post-PR-#2186):

| Metric | Target |
| --- | --- |
| `anonymous_n / total` | ≤ 5% (was 96% pre-fix prod-wide) |
| `has_account / total` | ≥ 95% |
| `has_trace / total` | 100% |
| `has_span_name`, `has_service_name` | open instrumentation gaps — report whatever you see |
| `has_prompt_key` | gradually improving; B2 brief in flight |
| `has_step_id`, `has_agent_name` | gradually improving; B3 brief in flight |

### Q4 — workflow_type roots + cost

```sql
SELECT
  toString(properties.workflow_type) as wt,
  count() as n,
  round(sum(toFloat(coalesce(toString(properties['$ai_total_cost_usd']), '0'))), 4) as cost
FROM events
WHERE person.properties.email = '<email>'
  AND event = '$ai_generation'
  AND timestamp > <start>
GROUP BY wt ORDER BY n DESC
```

Map roots through the glossary at `packages/glass-box/data/posthog-reports/llm-cost/003-workflow-type-glossary.md`:

| Tier | Examples | User-driven? |
| --- | --- | --- |
| A — HTTP-rooted | `chat-*`, `plan_generation`, `subtask_generation` | yes (request) |
| B — consumer-rooted | `chat-processing-*`, `flow:*`, `uploaded-document-processing` | yes (job) |
| C — indirectly user-driven | `*_generation` (title, version_metadata), `meeting-summary` | yes (deferred) |
| D — system | `context-graph-inference`, `routine-execution-*` | no |

### Q5 — sessions

```sql
SELECT
  toString(properties['$session_id']) as sid,
  count() as n,
  countIf(event = '$ai_generation') as ai_gen,
  countIf(event = 'Chat Message Sent') as chat_msgs,
  countIf(event = 'Brief Created') as briefs,
  min(timestamp) as start_ts, max(timestamp) as end_ts,
  round(sum(toFloat(coalesce(toString(properties['$ai_total_cost_usd']), '0'))), 4) as total_cost
FROM events
WHERE person.properties.email = '<email>'
  AND timestamp > <start>
  AND properties['$session_id'] IS NOT NULL
GROUP BY sid ORDER BY start_ts ASC
```

BullMQ-fired LLM events have NULL `$session_id` until `_sessionId` is propagated end-to-end. Sum-of-sessions ai_gen count will be smaller than the global total — that's expected.

### Q6 — chronological story

```sql
SELECT formatDateTime(timestamp, '%Y-%m-%d %H:%i:%S') as ts,
       event,
       toString(properties.workflow_type) as wt,
       toString(properties['$ai_total_cost_usd']) as cost,
       toString(properties['$ai_model']) as model
FROM events
WHERE person.properties.email = '<email>'
  AND timestamp > <start>
  AND event NOT IN ('$autocapture', '$pageview', '$pageleave', '$set', '$identify',
                    '$groupidentify', '$web_vitals', '$conversations_loaded')
ORDER BY timestamp ASC
LIMIT 200
```

Use `%i` for minute (ClickHouse) — `%M` renders as month name in some PostHog versions.

### Q7 — Trace continuity for one chat thread

This is the cheapest test for F7-class regressions. Pick a thread_id from Q6's output:

```sql
SELECT toString(properties['$ai_trace_id']) as tid, event,
       toString(properties.workflow_type) as wt, timestamp
FROM events
WHERE person.properties.email = '<email>'
  AND timestamp > <start>
  AND properties.thread_id = '<thread_uuid>'
  AND event IN ('Chat Message Sent', '$ai_generation', 'AI Response Received',
                'AI Tool Called', 'Context Retrieved')
ORDER BY timestamp ASC
```

**Pass** = `Chat Message Sent`, all `chat-processing > core_chat > …` events, AND `chat-* > title_generation` share **one** `$ai_trace_id`.

**Fail** = ≥2 distinct trace_ids for one thread → F7 (BullMQ trace-context propagation broken; PR #2186 territory). Pre-fix expectation in prod was 3 disjoint trace_ids per chat send.

### Q8 — Property dump per event class

For each typed product event the user fired (from Q2), pull one example:

```sql
SELECT properties FROM events
WHERE person.properties.email = '<email>'
  AND event = '<EventName>'
  AND timestamp > <start>
ORDER BY timestamp DESC LIMIT 1
```

Decode the JSON, filter to non-`$`-prefixed keys, and check the row matches the property-quality matrix below.

## Property-quality matrix

For every typed product event, these MUST be present (non-empty, non-`'none'`):

| Event class | Required app keys | Optional |
| --- | --- | --- |
| `*Created`/`*Updated`/`*Deleted` | `account_id`, `user_id`, `<entity>_id` | feature-specific |
| `Chat Message Sent` | `account_id`, `user_id`, `thread_id`, `message_length` | `has_document_context` |
| `AI Tool Called` | `account_id`, `user_id`, `tool_name`, `workflow_type`, `success` | `thread_id`, model fields |
| `AI Response Received` | `account_id`, `user_id`, `thread_id`, `response_time_ms`, `workflow_type` | `brief_id`, `tool_call_count` |
| `Context Retrieved` | `account_id`, `user_id`, `query_length`, `results_count`, `retrieval_time_ms` | `source_types` |
| `Plan Generation Started` | `account_id`, `user_id`, `brief_id`, `generation_type` | — |
| `User Signed In` | `user_id`, `email` | (no `account_id` — pre-team-selection) |
| `Account Switched` | `account_id`, `account_slug`, `account_type`, `new_account_id`, `team_role` | `from_account_id` |
| `Team Created` | `account_id`, `account_name`, `account_type`, `created_via`, `user_id` | — |
| `Feedback Submitted` | `account_id`, `user_id`, `source_url` | — |
| `Integration Connected` | `account_id`, `user_id`, `connection_id`, `integration_type` | `workspace_id`, `workspace_name` |
| `Checkout Started` (typed) | `account_id`, `user_id`, `plan_id`, `product_id` | — |
| `Brief Created` | `account_id`, `user_id`, `brief_id`, `has_initial_content` | `content_length` |
| `Blueprint Created` | `account_id`, `user_id`, `blueprint_id`, `creation_method` | `blueprint_type` |

Source of truth for the property shapes: `packages/analytics/src/event-types.ts` (find the `*Properties` interface for each event name).

For every `$ai_generation`:

- `$ai_trace_id` present (always — set by exporter)
- `distinct_id` resolves to the Supabase user.id (NOT `$ai_trace_id`) for user-driven flows
- `account_id` populated for B-tier and most C-tier flows
- `prompt_key` populated for primary agent calls (not Input Validator, not system jobs)
- `workflow_type` populated and non-`'none'`
- `$ai_model`, `$ai_total_cost_usd`, `$ai_input_tokens`, `$ai_output_tokens`, `$ai_latency` populated

## Output: the report

Write to `packages/glass-box/data/posthog-reports/tracking-quality/<NNN>-<slug>.md` (next free number — 001 is the per-user audit baseline; subsequent reports are follow-ups or guided-mode runs).

Frontmatter:

```yaml
---
dashboard_id: n/a (single-user audit) | <id> if a dashboard backs the report
env: prod | tux | dev | local
spec_section: posthog-insights-scope.md §2 + posthog-events-inventory.md
parent_report: <prev report path or 'none'>
supersedes: none
created: YYYY-MM-DD
status: action-items-issued
mode: guided | free
subject: <email or user_id>
window: <ISO start> → <ISO end>
---
```

Sections in order:

1. **TL;DR** — 2–3 sentences. The one conclusion.
2. **Headline numbers** — table from Q3 + cost from Q4 + session count from Q5.
3. **The story** — chronological narrative from Q6, broken into sessions. For guided mode: the action plan + observed events side-by-side.
4. **Findings** — numbered F1, F2, … each with: what the data shows / why it matters / confidence / source query.
5. **Hypotheses** — for medium/low-confidence findings.
6. **Actions** — prioritised P0/P1/P2: code changes, inventory/doc updates, follow-up investigations.
7. **Decisions taken** — what this audit settled.
8. **Open questions** — what needs more data.
9. **Children** — list of reports spawned from this one.

After writing the report:

- Add a row to `packages/glass-box/data/posthog-reports/tracking-quality/README.md` "Reports" table.
- Update P0/P1 counts in `docs/architecture/posthog-insights-progress.md` §1 (tracking-quality row).

## Common findings (memorise)

| Pattern | What it means | Action |
| --- | --- | --- |
| ≥30% `anonymous_n` on `$ai_generation` for user-driven flows | F7 — BullMQ producers don't `stampJobObservability`; consumer opens fresh trace + has no userId | Wait for / verify PR #2186 deploy |
| `account_id` missing only on `*-title_generation` rows | F9 — `thread-title-generation.consumer.ts` partial `setRequestCorrelation` | PR #2186 |
| `$ai_span_name` 0% present | OTel exporter not propagating the metadata key | Investigate `@posthog/ai/otel` version; possibly rename key in `buildExperimentalTelemetry` |
| `service.name` / `$service` 0% present | OTel resource attribute set, event property not | Add `'service.name'` to `metadata` in `buildExperimentalTelemetry` |
| `prompt_key` missing on Title Generator / Version Metadata Generator | F12 — `promptKey` not plumbed at those call sites | B2 brief |
| `Chat Message Sent` and `chat-processing > core_chat` have different `$ai_trace_id` | F7 (browser→server boundary, not just BullMQ→BullMQ) | PR #2186 |
| Event name typed in `event-types.ts` but listed TYPED-NOT-FIRED in inventory | Inventory drift | Flip to TYPED-&-FIRED |
| `<event>` fires in prod but isn't in the inventory at all | Catalog drift | Add to appropriate §3.X subsection |
| `checkout.started` (untyped) double-fires alongside typed `Checkout Started` | Bus-forwarder drift | Retire `apps/web/components/analytics-provider.tsx:138` |

## Pitfalls

- **`properties.$foo` returns NULL silently.** Use bracket syntax for `$`-prefixed keys.
- **`'none'` is the missing-value sentinel.** Treat as absent in coverage calculations.
- **PR-deploy lag matters.** When comparing pre/post-fix, verify the *deploy* time, not the *merge* time. Cloud Run rollouts can be ≥30 minutes after merge.
- **`$session_id` is NULL on background BullMQ events.** Don't expect every `$ai_generation` to be in a session.
- **A person can have multiple distinct_ids** (anonymous → identified). Use `person.properties.email = …`.
- **A person can have multiple accounts** (personal + team(s)). Watch for `Account Switched`.
- **Don't run `inspect_posthog_dashboard.py` for this** — it's dashboard-scoped. Go direct via Q1–Q8.
- **In guided mode, give background jobs time to fire** — title-generation, version-metadata, plan-generation chains can run 30s–10min after the user's last click. Tell the user "wait 2 minutes after your last action before saying you're done."

## Anti-patterns

- **Writing the report before running Q1.** You'll guess wrong about distinct_ids.
- **Reporting "100% have property X" without checking the `'none'` sentinel.** You'll over-report coverage.
- **Forgetting the pre-identify anonymous distinct_id.** You'll miss the first events of a session.
- **Conflating typed-catalog status with live-fire status.** Both can be wrong (catalog drift OR fire drift).
- **Skipping Q7 (trace continuity) for chat flows.** It's the cheapest F7 regression test.
- **Acting on guided-mode findings before background jobs fire.** Re-run the audit after a 2-minute wait if any C-tier event might still be in flight.

## Cross-references

- `docs/architecture/observability.md` — `$ai_*` taxonomy + correlation envelope
- `docs/architecture/posthog-events-inventory.md` — typed catalog + drift table
- `packages/glass-box/data/posthog-reports/llm-cost/003-workflow-type-glossary.md` — `workflow_type` root taxonomy
- `packages/glass-box/data/posthog-reports/tracking-quality/001-nicolas-prod-event-audit.md` — worked example, free mode
- `packages/analytics/src/event-types.ts` — typed catalog source of truth
- `apps/ai/src/common/aisdk/sdk.ts` — `buildExperimentalTelemetry()`
- `packages/api/core/src/context/bullmq.ts` — `stampJobObservability` / `withBullMQContext`
- Sister skill: `hamster-posthog-dashboards` (dashboard-level analysis)
- Sister skill: `hamster-analytics` (event instrumentation patterns when the audit surfaces a code fix)

Attribution

gethamstergethamster
View sourceSee grades on GitHubMore from gethamster →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

ucoz-landing-skill

Create and edit uCoz homepage landing pages via MCP: custom templates, hero sections, lead forms, navigation menus, SEO, and responsive layout. Includes a visual design system (style selection, layout/grid, section recipes, typography/spacing, color tokens, component states, icons, modern CSS/JS, motion, imagery, social proof, copy/voice, accessibility). Uses ucoz-mcp tools for templates, site file uploads, and site modules.

107 votes

Paperclip

Interact with the Paperclip control plane API for task coordination and governance. Use when checking assignments, updating issue status, posting comments, delegating work, managing routines, or calling Paperclip API endpoints.

953191 votes

Pptx

Presentation toolkit (.pptx). Create/edit slides, layouts, content, speaker notes, comments, for programmatic presentation creation and modification.

471861 votes

Daw Music

Digital Audio Workstation usage, music composition, interactive music systems, and game audio implementation for immersive soundscapes.

761 votes

Instantly Rdsthomas Mission Control

Instantly.ai cold email outreach API - manage campaigns, leads, accounts, and analytics. Use for cold email automation, lead management, campaign creation/monitoring, and email account warmup.

761 votes
View all in tools →