Skip to content
Closed
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Framework rules

Follow these when integrating PostHog into this framework.

- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message "<VAR> variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once <VAR> is configured" (substituting the actual variable name); production stays a no-op
- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead
- Include enableExceptionAutocapture: true in the PostHog constructor options
- Add posthog.capture() calls in route handlers for meaningful user actions – every route that creates, updates, or deletes data should track an event with contextual properties
- Add posthog.captureException(err, distinctId) in the application's error handler (e.g., Express error middleware, Fastify setErrorHandler, Koa app.on('error'))
- The SDK batches events and flushes asynchronously. await flush() or await shutdown() before letting that process exit. If unsure, set flushAt 1 and flushInterval 0.
- `posthog.capture()` enqueues synchronously and returns; the batched HTTP send happens afterwards. Treat every per-request handler as short-lived even when the framework feels like a server: Next.js / Nuxt / SvelteKit / Remix route handlers, serverless and edge functions, and Lambda are torn down per invocation before the send runs. Create the client with flushAt 1 and flushInterval 0, then await the send before returning. Always use `await posthog.flush()` for a shared/singleton client, `await posthog.shutdown()` for a per-request client. Never skip the awaited flush or risk the enqueued event being silently dropped.
- Reverse proxy is NOT needed for server-side Node.js – only client-side JavaScript needs a proxy to avoid ad blockers
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

# Conversation IDs

A PostHog `$session_id` normally follows the MCP protocol session. The handshake-free `2026-07-28` revision has no protocol session, so each request gets a new session unless you add another correlation signal.

`$mcp_conversation_id` groups those calls by conversation. The SDK also derives `$session_id` from an accepted conversation handle, so PostHog session queries group the same calls.

**Enabled by default**

Conversation IDs are enabled by default. Calls stay correlated when the agent echoes the handle returned in tool results.

## Enabling

No extra configuration is needed in either SDK:

### TypeScript

```typescript
instrument(server, posthog)
```

### Python

```python
instrument(server, posthog)
```

The SDK does three things:

1. **Injects an optional `conversation_id` argument** into compatible tool schemas, with a description telling the agent to reuse the value the server returns.
2. **Creates a handle** when needed and returns it on eligible tool responses as a `{"conversation_id":"…"}` text block.
3. **Captures the handle** as `$mcp_conversation_id`. When the agent echoes a valid handle, the SDK derives a stable `$session_id` from it. Until then, existing protocol sessions remain in use.

The SDK reuses an echoed UUIDv7 that matches the handles it creates. It replaces missing or arbitrary values with a new handle to avoid merging unrelated conversations.

## Event properties

```
{
event: "$mcp_tool_call",
properties: {
"$session_id": "ses_2a3f…", // derived from the conversation handle
"$mcp_conversation_id": "0198f2d6-…", // echoed UUIDv7 conversation handle
"$mcp_tool_name": "search_events",
...
}
}
```

A new request or connection made by the same agent reusing the same `conversation_id` shares both `$mcp_conversation_id` and `$session_id`. You can group by the conversation property in HogQL:

SQL

[Run in PostHog](https://us.posthog.com/sql?open_query=SELECT%0A++properties.%24mcp_conversation_id+AS+conversation%2C%0A++arrayDistinct%28groupArray%28properties.%24mcp_tool_name%29%29+AS+tools_called%2C%0A++count%28%29+AS+tool_calls%0AFROM+events%0AWHERE+event+%3D+'%24mcp_tool_call'%0A++AND+properties.%24mcp_conversation_id+IS+NOT+NULL%0A++AND+timestamp+%3E+now%28%29+-+INTERVAL+7+DAY%0AGROUP+BY+conversation%0AORDER+BY+tool_calls+DESC%0ALIMIT+50)

```sql
SELECT
properties.$mcp_conversation_id AS conversation,
arrayDistinct(groupArray(properties.$mcp_tool_name)) AS tools_called,
count() AS tool_calls
FROM events
WHERE event = '$mcp_tool_call'
AND properties.$mcp_conversation_id IS NOT NULL
AND timestamp > now() - INTERVAL 7 DAY
GROUP BY conversation
ORDER BY tool_calls DESC
LIMIT 50
```

## Caveats

**Some tools can't take the injection**

The SDK cannot add `conversation_id` to schemas that use `oneOf`, `allOf`, `anyOf`, or `$ref`. It also skips tools without an input schema. It logs a warning and returns no handle for these tools. Use [`identify`](/docs/mcp-analytics/identifying-users.md) to group their calls by user.

A client with a stale cached tool listing does not know about the new parameter. The `ttlMs` setting on `tools/list` can extend this period.

**The handle is visible in tool output**

The SDK returns the handle as a `{"conversation_id":"…"}` text block. Clients that display raw tool results also display this JSON. The block contains data rather than an instruction because hardened clients can treat instructions in tool results as prompt injection.

The SDK preserves structured output and result metadata. Clients that only consume structured output may never see the text block, so they may not echo the handle.

**Agent-controlled values**

The SDK only reuses UUIDv7 values that match the shape of handles it can create. It replaces other values with a new handle. A client can still reuse a valid-looking handle across users, so don't use `$mcp_conversation_id` as a security boundary.

**It also anchors the PostHog session**

PostHog's session-level joins use `$session_id`. When conversation IDs are enabled, the SDK hashes the accepted `conversation_id` into a deterministic `$session_id`. Separate server instances derive the same value without shared storage.

## When to skip this

If your MCP server runs over a long-lived `2025-11-25` connection that already matches your conversation boundaries, you can disable conversation IDs. Set `enableConversationId: false` in TypeScript or `MCPAnalyticsOptions(enable_conversation_id=False)` in Python to omit the injected argument and response block.

Keep it enabled when:

- The same logical conversation crosses connections (HTTP/SSE clients that reconnect).
- Your server handles the `2026-07-28` revision and you want more than one request in each PostHog session.

### Still have questions?

Ask PostHog AI

### Was this page useful?

HelpfulCould be better
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

# Custom events and metadata

Use `eventProperties` to add metadata to captured events. Use `analytics.capture()` for events that are not MCP requests.

## `eventProperties` – metadata on every event

Pass an `eventProperties` callback to attach extra properties to automatically captured MCP events. The callback receives request context, such as headers, transport, and the request ID.

TypeScript

```typescript
import { instrument, getRequestHeaders } from "@posthog/mcp"

const analytics = instrument(server, posthog, {
eventProperties: async (request, extra) => ({
$app_version: process.env.GIT_SHA ?? "unknown",
$mcp_region: process.env.FLY_REGION ?? "unknown",
request_id: getRequestHeaders(extra)?.["x-request-id"],
}),
})
```

`getRequestHeaders` reads headers on both MCP SDK majors – see [MCP SDK v2](/docs/mcp-analytics/sdk-v2.md#if-your-callbacks-read-headers-change-them).

The returned object is spread flat onto the event's properties alongside the built-in `$mcp_*` keys:

JSON

```json
{
"event": "$mcp_tool_call",
"properties": {
"$mcp_tool_name": "search_events",
"$app_version": "a1b2c3d",
"$mcp_region": "iad",
"request_id": "req_…"
}
}
```

Return constants from `eventProperties` to add the same values to captured MCP events. This is similar to `posthog.register(...)` in other SDKs. Return values from the request context when metadata must vary between calls.

For group analytics, return `groups` from [`identify`](/docs/mcp-analytics/identifying-users.md). The SDK adds `$groups` to events for the session.

Returned values must be JSON-serializable. The SDK catches callback errors and sends them to your `logger`. These errors do not interrupt tool execution.

## `analytics.capture()` – emit an arbitrary event

Use `analytics.capture()` for events that aren't MCP requests, such as UI feedback or workflow milestones. It uses the SDK's sanitization, current server session and identity, and `beforeSend` hook. It returns a promise you can `await`.

Custom capture has no request context and doesn't run `eventProperties`. Pass custom metadata in `properties`.

You name the event. It's sent verbatim – it's your event, so it is **not** `$`\-prefixed.

TypeScript

```typescript
const analytics = instrument(server, posthog)

await analytics.capture({
event: "feedback_submitted",
properties: { rating: 5 },
})
```

PostHog receives:

- One event under the verbatim `event` name you passed, with your `properties` merged in.
- The current server session and cached identity apply. These may differ from the session of a concurrent tool request.

`capture()` is a method on the handle that `instrument()` returns, so you call it on the instrumented server's analytics handle directly.

## Which one to use

| You want to... | Use |
| --- | --- |
| Attach the same properties to every auto-captured event | `eventProperties` |
| Emit a one-off event that isn't an MCP request | `analytics.capture()` |
| Attach data only to matching requests | Check the request in `eventProperties` and return properties only for matches. |

### Still have questions?

Ask PostHog AI

### Was this page useful?

HelpfulCould be better
Loading
Loading