Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"displayName": "PostHog",
"source": "./",
"description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from your AI coding tool. Optionally capture Claude Code sessions to PostHog LLM Analytics.",
"version": "1.1.68",
"version": "1.1.69",
"author": {
"name": "PostHog",
"email": "hey@posthog.com",
Expand All @@ -38,7 +38,7 @@
"displayName": "PostHog session capture",
"source": "./telemetry",
"description": "Send Claude Code sessions to PostHog LLM analytics. Hooks only: no skills, no commands, no MCP server. Install this instead of the PostHog plugin, not alongside it.",
"version": "1.0.4",
"version": "1.0.5",
"author": {
"name": "PostHog",
"email": "hey@posthog.com",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "posthog",
"description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from your AI coding tool. Optionally capture Claude Code sessions to PostHog LLM Analytics.",
"version": "1.1.68",
"version": "1.1.69",
"author": {
"name": "PostHog",
"email": "hey@posthog.com",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "posthog",
"version": "1.0.65",
"version": "1.0.66",
"description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Codex",
"author": {
"name": "PostHog",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "posthog",
"displayName": "PostHog",
"version": "1.1.61",
"version": "1.1.62",
"description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Cursor",
"author": {
"name": "PostHog",
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "posthog",
"version": "1.0.63",
"version": "1.0.64",
"description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Gemini CLI",
"mcpServers": {
"posthog": {
Expand Down
1 change: 1 addition & 0 deletions skills/.sync-manifest
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ finding-replay-for-issue
finding-sessions-to-watch
formatting-insight-axes
grouping-noisy-errors
implementing-api-surveys
inbox-exploration
instrument-error-tracking
instrument-feature-flags
Expand Down
1 change: 1 addition & 0 deletions skills/authoring-scouts/references/scheduled-precheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ scout-precheck-query: |
- The harness reads the default from the canonical skill file that PostHog ships, not from a project's copy of the skill.
Adding `scout-precheck-query` to a custom scout or to a team's edited copy changes nothing. For those scouts, save `precheck_query` on the config.
- A merged default does not apply to every project at once. A rollout setting decides which projects get it.
- The project profile's `surfaces` section runs every canonical default too, and reads its rows as "this surface is in use". Keep a default a test of activity on the surface. A backstop row or a "new since the last run" filter changes that answer for every project. `surfaces` binds `{since}` and `{interval_minutes}` to a 30-day span.
- When you write a canonical default, use the canonical scouts that already ship one (for example `signals-scout-mcp-tool-calls` above, or `signals-scout-surveys`) as models.

## Tell the body about the rows
Expand Down
7 changes: 4 additions & 3 deletions skills/authoring-scouts/references/scout-anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ The fleet's specialists all share this shape:
Examples: `count` vs `distinct_users` ratio (error tracking); reach over raw count (CSP); negative+mixed share vs baseline (MCP feedback).
Without this, the scout wastes every run re-deciding what "normal" means.

2. **Quick close-out.** A cheap early-exit so a quiet run costs almost nothing: if the watched event is absent from the profile's `top_events` or sitting at baseline (no fresh 24h activity), write one scratchpad entry and stop.
2. **Quick close-out.** A cheap early-exit so a quiet run costs almost nothing: if the surface is not in use, or the watched event sits at baseline (no fresh 24h activity), write one scratchpad entry and stop.
This keeps idle scouts cheap.
Decide "not in use" from the profile's `surfaces` entry for the scout, never from absence in `top_events`: `top_events` keeps only the top events by volume, so a quiet surface that is in use falls off it. A canonical scout gets a `surfaces` entry when it ships a `scout-precheck-query`. A scout with no entry, or an entry that reads `in_use: null`, checks with one bounded `execute-sql` count.
`top_events` counts are windowed (each row carries `window_days`), not lifetime — a project whose ingestion recently went dark reads identically to one that never had traffic. Before closing out a busy-looking project as empty on `top_events` thinness alone, rule out a capture gap with a direct `execute-sql` over a longer window (e.g. 30d); only close out when the low volume holds there.

```text
Expand Down Expand Up @@ -157,8 +158,8 @@ signal-vs-noise discriminator. Internalize that shape.

## Quick close-out: is <surface> even loud?

If <event> is absent from `top_events` or at baseline (no fresh 24h activity), <surface>
isn't where the signal is today. Cheap scratchpad entry + close out empty.
If `surfaces["<skill-name>"]` reads `in_use: false`, or <event> is at baseline (no fresh 24h activity), <surface>
isn't where the signal is today. Absence from `top_events` alone proves nothing. Cheap scratchpad entry + close out empty.

## How a run works

Expand Down
40 changes: 40 additions & 0 deletions skills/building-workflows/references/graph-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,45 @@ Retrieve `template-native-push` with `cdp-function-templates-retrieve` for the f
- Required: `distinctId`, `channels`, `title`. Optional: `body`, `image`, `data`, `ttlSeconds`, `android_*`, `ios_*` (retrieve the `inputs_schema` for the full set).
- Never hand-author `bytecode` — the server compiles it from `value`. Omit `order` too: the editor lays fields out in the template's `inputs_schema` order (fixed and consistent), not by the `order` on your inputs, so leaving it off doesn't change the form. Push has no delivered/opened/clicked signal (FCM/APNs respond synchronously), so a successful send means "accepted for delivery", nothing more.

### Replay Vision step

Replay Vision is PostHog's AI that watches session recordings. It replays a session as video, together with its events, and answers a question about it. Each scan uses credits from the project's Replay Vision quota.

A `function` node with `template_id: "template-posthog-replay-vision"` scans the trigger event's session and waits for the answer. Use it when the next step depends on what happened in the session, which event properties alone can't tell, for example to add session context to a Slack alert or to branch on whether the user struggled. The project must record sessions, and the trigger event must carry a `$session_id`.

```json
{
"id": "vision_1",
"name": "Did checkout fail?",
"type": "function",
"config": {
"template_id": "template-posthog-replay-vision",
"inputs": {
"analysis": { "value": { "type": "monitor", "prompt": "Did the user hit an error during checkout?" } }
}
},
"output_variable": { "key": "replay_vision" }
}
```

- **`analysis` is a literal object, not a template.** Never set `"templating"` on it, and never put `{...}` expressions in it. Its `value` takes one of these shapes:

| `type` | Required keys | What the step answers |
| ------------ | ----------------------------------------------- | --------------------------------------------------------- |
| `monitor` | `prompt` | A yes/no verdict on the question in `prompt`. |
| `classifier` | `prompt`, `tags` (1 to 100 category strings) | Which of the categories in `tags` fit the session. |
| `scorer` | `prompt`, `scale: {"min": <num>, "max": <num>}` | A score on the scale. `max` must be greater than `min`. |
| `summarizer` | `prompt` | A title, a summary, and chapters. |
| `scanner` | `scanner_id` (a saved scanner's UUID) | The saved scanner's answer, with its own type and prompt. |

- The inline types take the same optional settings as a saved scanner of that type. See the `creating-replay-vision-scanners` skill. Find saved scanners with the `vision-scanners` tools.
- `session_id` defaults to `{event.properties.$session_id}`. Leave it unset unless the session ID is in another property.
- `wait_for_session_end` defaults to `true`, which scans the recording after the session has been quiet for 35 minutes. Set it to `false` only when the trigger event comes after the session ended.
- The step result is `{"state", "output", "warnings"}`. `state` is `succeeded`, `failed`, `skipped`, `ineligible` or `lost`. `output` is the scanner's answer: `verdict`, `tags` or `score` with `reasoning` for a monitor, classifier or scorer, and `title`, `summary` and `chapters` for a summarizer. Every type also returns `confidence`. Use `output_variable` to keep the result, then branch on it.
- **Main environment only.** The server refuses the step in a child environment of a project.
- **Daily caps.** Each workflow can start 100 scans a day, and the project 500, unless the project settings change `replay_vision_scan_rate_limit_per_day` and `replay_vision_scan_team_rate_limit_per_day`. Past a cap the step returns `state: "skipped"` and the workflow continues.
- **Access.** The token needs `session_recording:read` (or `:write`) and `replay_scanner:write`, and the user needs access to recordings and to the saved scanner. Without them, the save fails with a message that names what is missing.

## Duration strings (`delay_duration`, `max_wait_duration`)

Must match `^\d*\.?\d+[dhms]$` — a number plus unit `s` | `m` | `h` | `d`. Examples: `30s`, `30m`, `2h`, `1d`, `1.5d` (=36h).
Expand Down Expand Up @@ -216,3 +255,4 @@ A `delay` waits either a fixed span or until a date carried by the person or the
- [ ] All durations match `^\d*\.?\d+[dhms]$` and dodge the silent per-unit clamp.
- [ ] Every `delay` sets exactly one of `delay_duration` and `delay_until`, and no `delay_until` carries hand-written `bytecode`.
- [ ] Function inputs are `{key: {value: ...}}`; no hand-written `bytecode` anywhere; no top-level `trigger` field set.
- [ ] A Replay Vision step's `analysis` value is a literal object of one of the five shapes, with no `templating`.
32 changes: 29 additions & 3 deletions skills/creating-surveys/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,38 @@ information that changes who receives the survey or how it is delivered.
| A shareable hosted form | `external_survey` | A hosted survey link; no in-app targeting |
| A custom form built in application code | `api` | The app renders questions and captures survey events |

Prefer one to three questions unless the user requests more. Use a rating plus
an optional open question for NPS/CSAT, or choice questions with at least two
choices. Inspect the current tool schema for question types, scales, branching,
Inspect the current tool schema for question types, scales, branching,
and translations instead of guessing their JSON shapes. Minimal starting points
are in [examples](references/examples.md).

### Design for responses

These choices go with higher response rates in in-app surveys. Apply the
question advice by default. Recommend the trigger and audience advice, and ask
before adding a trigger or audience the user did not request, since both change
who receives the survey. The evidence and its limits are in
[response rates](references/response-rates.md).

- **Trigger on the moment you are asking about.** Recommend showing the survey
after the event that marks it, such as a completed checkout or a used feature,
through `conditions.events`. Event-triggered surveys get about twice the
responses of page-based ones. A URL condition alone does about as well as
showing the survey on every page. Confirm the event exists in the project
before using it.
- **Make the first question one click.** Start with a single choice or a short
rating. Surveys that open with an open-text question get about a fifth of the
responses. Ask "why" in an optional open question after it.
- **Keep it short.** About 4 in 10 people who start a two- or three-question
survey stop before the end. Prefer one to three questions unless the user
requests more.
- **Turn partial responses on** with `enable_partial_responses: true`, so the
first answer is kept when someone stops. It defaults to off when a survey is
created through the API.
- **Recommend a targeted audience.** Surveys shown to a person or cohort
audience get more responses than surveys shown to everyone.
- **Keep any popup delay under 15 seconds.** Longer delays go with fewer
responses.

Survey names, questions, and appearance text are public content. Do not copy
private customer details into them without making that visibility clear first.

Expand Down
33 changes: 32 additions & 1 deletion skills/creating-surveys/references/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ against the project before adding them.
{
"name": "Product recommendation feedback",
"type": "popover",
"enable_partial_responses": true,
"questions": [
{
"type": "rating",
Expand All @@ -27,7 +28,37 @@ against the project before adding them.
```

This has no audience restriction. Add the user's intended display conditions
and targeting before launch. For CSAT, use a satisfaction question and scale 5.
and targeting before launch.

## In-app CSAT after an action

```json
{
"name": "Export satisfaction",
"type": "popover",
"enable_partial_responses": true,
"questions": [
{
"type": "rating",
"question": "How satisfied are you with this export?",
"display": "emoji",
"scale": 5
},
{
"type": "open",
"question": "What would make it better?",
"optional": true
}
],
"conditions": {
"events": { "values": [{ "name": "export_completed" }] }
}
}
```

Replace `export_completed` with an event the project already captures at that
moment. The survey shows only after the event fires, which is when the person
can answer.

## Hosted feedback form

Expand Down
41 changes: 41 additions & 0 deletions skills/creating-surveys/references/response-rates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Response rates by survey design

These comparisons come from PostHog popover surveys over one week in October 2026.
Response rate is the share of people who saw a survey and answered at least one question.
Each survey counts once, so a few high-traffic surveys do not dominate.

| Design choice | Typical response rate | Compared with |
| --------------------------------- | --------------------- | ------------------------------------------------- |
| Triggered by an event or action | 7.5% | 2.9% for a URL condition, 2.1% for none |
| First question is a single choice | 8.0% | 3.5% for a rating, 0.8% for open text |
| Shown on mobile | 13.6% | 3.2% on web |
| Middle-center modal | 12.8% | 2.8% for the default bottom-right (`right`) popup |

The share of responders who finish every question is about 57% for two questions
and about 45% for three or more. The number of questions does not change how
many people start.

A model that holds the other choices constant keeps these effects:

- An open-text first question cuts the odds of a response to about a fifth.
- An event trigger about doubles them.
- A centered modal (`middle_center` or `center`) raises them about 1.8 times.
- A choice question first and a targeted audience each raise them about 1.4 to
1.5 times.
- A popup delay over 15 seconds cuts them by about a quarter.

The mobile difference, the 0–10 rating scale, and the number of questions do not
hold up once the other choices are held constant.

## Limits

- These are correlations. Teams that set up event triggers may also write better
surveys in other ways.
- With partial responses off, a person counts only after finishing every question,
so surveys with partial responses on look better partly because of how they
count. Turn it on to keep first answers, not to raise the rate.
- A centered modal interrupts the user more than a corner popover. Its higher
response rate does not measure what the interruption costs. Suggest it only
when the user wants the most responses from a short, important question.
- API surveys depend on how each app captures `survey shown`, so their rates
are not comparable. Hosted forms count only people who opened the link.
Original file line number Diff line number Diff line change
Expand Up @@ -354,22 +354,21 @@ A user who adds `$feature/<flag-key>` to metric events changes nothing in the re

Where the property does matter:

- **Legacy experiments** (E13 in `mid-run-changes.md`) attribute metric events by `$feature/<flag-key>`, so events without it are not counted there.
- **Legacy experiments** (E13 in `mid-run-changes.md`) attributed metric events by `$feature/<flag-key>`. They no longer show results, so migrate them instead.
- **A custom exposure event** needs it (B4).
- **The user's own insight or SQL** that filters or breaks down by `$feature/<flag-key>`.
It undercounts events from a server SDK, which adds the property only when the capture call is given the evaluated flags or asked to evaluate them `[MEDIUM]`.
It also overcounts in the other direction: the property is on events of everyone whose SDK holds the flag's value, including people who were never exposed, so it is not a filter for participants.

**Verify directly.** `is_legacy` in `experiment-get`.
It is true when any primary, secondary or shared metric is legacy, and only a metric whose `kind` is `ExperimentTrendsQuery` or `ExperimentFunnelsQuery` attributes metric events by the property.
When it is false, or the metric in question has another `kind`, look elsewhere for a missing conversion: B2 (ids), B8 (order), D1 in `numbers-vs-sql.md` (scope).
It is true when any primary, secondary or shared metric is legacy.
When it is false, look elsewhere for a missing conversion: B2 (ids), B8 (order), D1 in `numbers-vs-sql.md` (scope).

Part of the public docs still asks for the property on metric events.
The exposures page states the current rule.

<!-- Source for maintainers (may rot): build_variant_property in
products/experiments/backend/hogql_queries/experiment_exposure_query_builder.py reads the property on the exposure side only;
the legacy runners experiment_trends_query_runner.py and experiment_funnels_query_runner.py break down metric events by it.
products/experiments/backend/hogql_queries/experiment_exposure_query_builder.py reads the property on the exposure side only.
Docs: https://posthog.com/docs/experiments/exposures#metric-events-dont-need-a-variant-property -->

## B12 — An exposure criteria filter that cannot match [MEDIUM]
Expand Down
Loading
Loading