# PostHog

> Send content and community events from PostHog; Patterna keeps the distinct id and drops the rest.

Source: https://staging.patterna.nl/docs/connector-posthog

PostHog already sees what people read and join. Point a PostHog destination at Patterna and those events become canonical events. Patterna keeps the distinct id only and drops person properties before anything is written.

**Status:** the PostHog destination is built and available since engine 0.2. It stays off for a tenant until an owner sets a PostHog token; without one the webhook accepts nothing.

> **Warning** Check your distinct id first. The webhook never stores a raw distinct id: an id already minted by an import (`u_…`) is kept, any other is replaced by a Patterna-minted hash under the tenant's `PATTERNA_ID_SECRET` (which Patterna holds, so Patterna could derive it again from the distinct id) or dropped. If your app calls `posthog.identify()` with your account id or your identity provider's subject (`auth0|…`), that id would reach Patterna. Use the webhook only when the distinct id is the random Patterna id (or a hash of your id that only you can map back); otherwise use the batch export with id mapping, or send the events from your backend. PostHog also does not know a user's Patterna mode: filter the destination so only users in `personalized` mode are sent (for example on a person property you set from consent). See [Identity across connectors](https://staging.patterna.nl/docs/identity-across-connectors).

## Set up the destination

1. In PostHog, open **Data pipelines** and choose **New destination**.
2. Pick the **HTTP Webhook** destination.
3. Set the URL to your tenant's webhook URL:

   ```text
   https://api.patterna.nl/v1/webhooks/posthog
   ```

4. Method `POST`. Add the header `X-Patterna-Token: <token>` with the token an owner set for this tenant (below). The destination never carries an API key. Leave the body as PostHog's default event payload (one event, or an array of events: `uuid`, `event`, `distinct_id`, `timestamp`, `properties`).
5. Add an event filter so only the mapped events go out:

   ```text
   content_viewed
   content_engaged
   content_scroll_depth
   community_joined
   community_left
   ```

6. Save and enable the destination.
7. In Patterna, open **Connectors → PostHog** and press **Test connection**. Within a few minutes the card shows rows in and rows mapped.

How it authenticates: the destination sends an `X-Patterna-Token` header. An owner sets the token (24 to 200 characters of random data) with `PATCH /settings` (`posthog_token`); Patterna stores only its SHA-256 hash. Set an empty token to switch the destination off again. Events from the webhook are stored as unsigned. The answer is `202` with `accepted`, `duplicates`, `skipped` (events with no mapping) and `dropped_fields`.

Sending other events does no harm: they are counted and dropped. The filter just keeps traffic and noise down.

## What maps to what

| PostHog event | Canonical event | Dropped |
|---|---|---|
| `content_viewed` | `item_viewed`, with `content_type` → `collection` | `content_title`, `content_slug` |
| `content_engaged` | `item_engaged` | |
| `content_scroll_depth` | folded into `item_engaged` | |
| `community_joined` / `community_left` | same name | |
| `cyclus_visited`, `cyclus_action`, `breathing_*`, `*_location_*`, `$screen` | not ingested | everything |
| category pages | not ingested | everything |
| person properties | not ingested | `email`, `name` |

`item_engaged` counts as a positive signal only when `engaged_seconds` is 20 or more. See [Event types](https://staging.patterna.nl/docs/reference-event-types) for weights.

`content_viewed` with `content_type` `community_post` becomes `community_post_viewed` with the post id only.

## What is kept and what is dropped

Kept:

- The distinct id, as the user id. Ids containing `@` are rejected. The webhook replaces it with a Patterna-minted hash unless it is already an import-minted `u_…` id (see the warning above); the batch export can do the same (below). See [Consent modes](https://staging.patterna.nl/docs/consent-modes) for what each mode stores.
- Event name, time, item or community id.
- From `$` properties: OS, app version and locale only.

Dropped before the row is written:

- Person properties and `$set`.
- IP address.
- Every other `$` property.
- Titles and slugs.

Every drop is counted per field. The **Connectors** card shows the counts under "Fields dropped", so you can see that the rule ran. See [Data we never store](https://staging.patterna.nl/docs/data-we-never-store).

## Alternative: batch export files

If you prefer not to stream, export PostHog events to an S3 bucket or a local directory as JSONL (optionally gzipped). The jobs read those files on the connectors schedule (every 15 minutes, at the interval set for your tenant) and apply the same mapping. Parquet exports are not supported yet.

The batch reader can map ids: with `id_mapping: "hmac"` on the connector, each distinct id becomes `u_` plus a keyed hash, so the raw distinct id is never written. The key sits in the jobs' environment (`PATTERNA_ID_SECRET`), which means Patterna, not you, could recompute the mapping from a known id. If that is not acceptable, hash on your side before the export, or send events from your backend with the Patterna id.

## Check it works

On **Connectors → PostHog** look at:

- **Last run** and **status**.
- **Rows in** and **rows mapped.** Rows in minus rows mapped is what was not ingested on purpose (for example `$screen`).
- **Fields dropped**, with counts per field.
- **Errors**, with the request id.

Then open **Audiences → trace** for a test user and confirm the events arrive.
