> ## Documentation Index
> Fetch the complete documentation index at: https://developers.autoplay.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# PostHog skill

> Connects an existing PostHog project to Autoplay live activity. Covers verifying posthog-js initialization, identifying logged-in users with the app's stable user ID, and using onboard_product/PostHogProvider to create and verify the PostHog to Autoplay destination. Use when the customer already uses PostHog, posthog-js, posthog.identify, product_id, or asks how to send PostHog activity to Autoplay.

# Session Replay Provider — PostHog

> Read `autoplay-core` first. The PostHog `session_id` is the `session_id` used
> for all session scoping throughout your Autoplay integration.
>
> **Who does this: Agent, entirely — you run every step below yourself.**
> There is no separate tool that registers the product or creates the PostHog
> destination for you — unless the customer tells you they already ran the
> standalone `autoplay-setup` CLI or the registration script by hand, assume
> none of that has happened yet and do it as part of this skill (Step 4).
>
> **Before Step 4, get these four things from the customer** — you cannot fetch
> them yourself, so ask for the exact values and **wait for their real answer
> before running the step that needs it**. When you ask, **tell them exactly
> where to find each one** — don't just ask "what's your Project ID?" with no
> guidance:
>
> * PostHog **Project ID** — Settings → Project → General.
>   [Walkthrough](https://app.arcade.software/share/uQizURhG2MTNIxQjiIA1)
> * PostHog **Project API Key** (`phc_…`) — same screen as Project ID.
>   [Walkthrough](https://app.arcade.software/share/uQizURhG2MTNIxQjiIA1)
> * PostHog **Personal API Key** (`phx_…`, with `project:read` +
>   `hog_function:write` scopes) — Settings → \[name] → Personal API keys →
>   Create personal API key.
>   [Walkthrough](https://app.arcade.software/share/lOvmHy4FeRDtjD9xBTBb)
> * A **contact email** for the registration — just ask, no dashboard involved.
>
> Never invent a placeholder project id, API key, or webhook secret and
> continue as if it were real — if `onboard_product(...)` or
> `create_destination(...)` would need a value you don't have yet, stop and
> ask rather than guessing or skipping ahead.
>
> **Your job end-to-end: wire the frontend code, then register the product and
> stand up the destination yourself.** Steps 1–3 are the frontend edits
> (`posthog-js` install/init/identify). Step 4 runs `onboard_product(...)` and
> `PostHogProvider.create_destination(...)` — do not skip Step 4 assuming
> something else handles it. Step 5 confirms events are actually flowing.
>
> **Stay scoped** — don't start the dev server, run `npm install` for unrelated
> app dependencies, or scaffold unrelated app config (`package.json`,
> `tsconfig.json`, pages) while doing this. And **stay inside this project's
> directory tree** — `posthog.init()`, the `register.py`-style script, and
> every file this skill touches live inside the current project root. Never
> search, list, or read outside it (no scanning the home directory, sibling
> folders, or other projects looking for "an existing onboarding script" —
> there isn't one to find; you write it, right here, in this project, in
> Step 4).

## Step 1 — Ensure `posthog-js` is installed

Check the app's `package.json` for `posthog-js`.

* **Present** → an init likely already exists; you'll *patch* it (Step 2).
* **Missing** → install it (`npm install posthog-js`, or the project's package
  manager) and treat this as **greenfield** — you'll *scaffold* a new init.

In a **monorepo**, find the actual frontend app first (the package that renders
the browser UI and owns `posthog.init`); install and edit there, not at the root.

## Step 2 — Get the browser init + identify in place

Two paths:

* **An init already exists** → patch it: ensure `posthog.identify(...)` includes
  the issued Autoplay `product_id` from registration. Init and identify often
  live in different files (init at app bootstrap, identify where auth state is
  known). See
  `references/identify-patterns.md`.
* **Greenfield (you just installed posthog-js)** → scaffold a minimal init with
  the right `api_host` + autocapture. See
  `references/scaffold-patterns.md` and the
  framework walkthroughs in `examples/` (React/Vite, Vue/Nuxt,
  Next.js).

Minimal init shape:

```javascript theme={null}
import posthog from 'posthog-js'

posthog.init('YOUR_POSTHOG_PROJECT_API_KEY', {   // the public phc_… key
    api_host: 'YOUR_POSTHOG_HOST',                // MUST match where the destination lives
    person_profiles: 'identified_only',
    session_idle_timeout_seconds: 120,
})
```

**Identity is set on login (Step 3), NOT in the init.** Do **not** call
`posthog.identify(posthog.get_distinct_id(), …)` in `loaded` or anywhere — that
"identifies" the *anonymous* id and is the #1 mistake here. Until the user logs
in they are anonymous (that's correct); `session_id` still scopes everything.

**Always show edits as a diff and confirm before writing.** If you can't safely
locate or edit the init (ambiguous/unfamiliar setup), **don't guess** — fall
back to Step 5.

## Step 3 — Identify with the app's stable user id on login (REQUIRED)

This is the most important call in the integration. The moment auth knows who
the user is, pass **the application's own stable user id** as the distinct id —
never the anonymous `posthog.get_distinct_id()`:

```javascript theme={null}
// On login (and on app load when restoring an already-logged-in session):
posthog.identify(user.id, {        // user.id = YOUR app's stable user id
    product_id: 'YOUR_AUTOPLAY_PRODUCT_ID',  // issued by onboard_product in Step 4
    email: user.email,             // recommended; enables email-based scoping

    // Optional: only if the customer is segmenting who should receive Autoplay.
    // Replace this condition with their own rule: plan, signup date, feature
    // flag, experiment assignment, workspace type, region, or any other property.
    autoplay_experiment_group: isInAutoplayExperiment(user) ? 'autoplay' : 'comparison',
    autoplay_experiment_id: 'your-experiment-id',
})

// On logout:
posthog.reset()                    // clears identity so the next user starts clean
```

Why this matters:

* It makes PostHog's `distinct_id` **equal to your app's user id**, so the exact
  id you use internally is the one Autoplay receives (`ActionsPayload.user_id`).
* PostHog **links the user's earlier anonymous activity** to this identified
  person — no orphaned anonymous ids after login.
* `posthog.reset()` on logout stops the next user (e.g. shared device) from
  inheriting the previous identity.

Find where auth state becomes known — after a successful login, and wherever the
app rehydrates a session for an already-logged-in user (e.g. an auth context /
`onAuthStateChanged` / session loader) — and call `identify` there with the real
user id. See `references/identify-patterns.md`.

If the customer is running an Autoplay experiment, do not use `plan` alone as the assignment. Add separate stable custom person properties such as `autoplay_experiment_group` and `autoplay_experiment_id` (shown above) so PostHog can filter the destination in Step 4 to the selected group while leaving comparison users out of the Autoplay stream. Tell the customer these custom properties appear in PostHog as person properties after PostHog receives the identify call and a later event for that person.

If the app has **no auth yet** (greenfield), there is no user to identify — wire
the init now, and leave a clear comment at the auth boundary showing exactly the
`posthog.identify(user.id, …)` call to add once login exists. Do not fake it
with the anonymous id.

## Step 4 — Register the product and create the destination

Run this once you have the Project ID, Personal API Key, and contact email from
the customer. First, register the product:

```python theme={null}
import asyncio
from autoplay_sdk.admin import onboard_product
from autoplay_sdk.providers import PostHogProvider

async def main() -> None:
    result = await onboard_product(
        "YOUR_POSTHOG_PROJECT_ID",
        contact_email="you@yourcompany.com",  # from the customer
        user_activity_provider=PostHogProvider(),
        print_operator_summary=True,
    )

asyncio.run(main())
```

This prints `product_id`, `provider_project_id`, `ingest_url`,
`ingest_secret`, `mcp_url`, `mcp_key`, and, on first registration only,
`owner_token`. Save `owner_token` securely; it is shown once and is required to
re-register or rotate credentials later. If registration returns **409**, the
product already exists for that PostHog project — ask the customer for the
saved `owner_token`, re-run with `owner_token="<saved token>"`, and then update
the destination below with the new `ingest_secret`.

Then create and verify the destination — no manual "add a webhook in PostHog"
step is needed, this does it for you:

```python theme={null}
import asyncio
from autoplay_sdk.providers import PostHogProvider

async def main() -> None:
    provider = PostHogProvider()
    dest = await provider.create_destination(
        host="https://us.posthog.com",             # or eu.posthog.com — must match posthog.init()'s api_host
        project_id="YOUR_POSTHOG_PROJECT_ID",
        personal_api_key="YOUR_PERSONAL_API_KEY",  # phx_… from the customer
        webhook_url="YOUR_INGEST_URL",             # ingest_url printed above
        webhook_secret="YOUR_INGEST_SECRET",       # ingest_secret printed above
    )
    status = await provider.verify(
        host="https://us.posthog.com",
        project_id="YOUR_POSTHOG_PROJECT_ID",
        personal_api_key="YOUR_PERSONAL_API_KEY",
        destination_id=dest.id,
    )
    print("destination", dest.id, "enabled:", status.ok)

asyncio.run(main())
```

It's idempotent — safe to re-run. Confirm `status.ok` is `True` before moving on.

**Optional — experiment cohort filtering.** `create_destination(...)` above has no `filters` argument — it always creates an unfiltered destination that forwards every event. If the customer is running an Autoplay experiment (and set `autoplay_experiment_group` in Step 3), filtering to that cohort is a one-time **dashboard** step you cannot do yourself: in PostHog, **Data pipeline → Destinations → Autoplay Event Stream**, add a filter **Person properties → `autoplay_experiment_group` → equals → `autoplay`** (optionally combined with another eligibility property like `plan → equals → trial`), then use the destination's **Testing** tab to confirm. Comparison-group events stay in PostHog but never reach Autoplay once this filter is set — the customer's app/PostHog/warehouse stays the source of truth for who was excluded, not Autoplay.

Note the Hog script only forwards a fixed set of fields to Autoplay (`event`, `email`, `timestamp`, `session_id`, `current_url`, etc.) — `autoplay_experiment_group`/`autoplay_experiment_id` gate whether the destination fires but are never included in the payload itself, so they won't show up in the activity Autoplay stores.

## Step 5 — Confirm events are flowing

Ask the customer to click around their app while logged in as an identified
user, then check activity landed yourself with the `mcp_key` from Step 4:

```bash theme={null}
curl "https://mcp.autoplay.ai/users/YOUR_AUTOPLAY_PRODUCT_ID/YOUR_USER_ID/live-activity?limit=10" \
  -H "Authorization: Bearer YOUR_MCP_KEY"
```

A `200` with a populated `actions` array means it's working — report that back
and hand off with a short summary of the files you changed. An empty array
means the user hasn't browsed yet or events haven't landed — wait a few seconds
and retry before assuming something's wrong.

**Fallback — if you couldn't safely wire the frontend code:** output the exact
snippet for the customer to paste, then let them confirm before you continue to
Step 4:

```javascript theme={null}
// Call this on login, with YOUR app's user id (not the anonymous id):
posthog.identify(user.id, { product_id: 'YOUR_AUTOPLAY_PRODUCT_ID', email: user.email })
// And on logout:
posthog.reset()
```

## Common mistakes

* **Identifying the anonymous id.** `posthog.identify(posthog.get_distinct_id(),
  …)` re-stamps the anonymous id and never sets your real user id. Always pass
  the app's stable user id (Step 3).
* **Forgetting `posthog.reset()` on logout.** The next user inherits the
  previous identity on shared sessions.
* **`api_host` must match the destination's host.** If the app sends events to a
  different host than the one the destination is created on, nothing flows.
* **Monorepos:** editing the wrong package. Confirm which app owns `posthog.init`.

## Reference

* `references/identify-patterns.md` — where
  init vs identify live, per framework
* `references/scaffold-patterns.md` — greenfield
  init scaffolds
* `examples/` — React/Vite, Vue/Nuxt, Next.js walkthroughs
* Quickstart: [https://developers.autoplay.ai/quickstart](https://developers.autoplay.ai/quickstart)
* PostHog identify docs: [https://posthog.com/docs/product-analytics/identify](https://posthog.com/docs/product-analytics/identify)
