Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7cdf1d4
chore(node): start the posthog-node 6 capture v1 release branch
eli-r-ph Oct 23, 2026
c716deb
test(node): read capture events from either wire format (#5414)
eli-r-ph Oct 27, 2026
5db747f
feat(node)!: send every event through capture v1 (#5409)
eli-r-ph Oct 27, 2026
763aab5
feat(node): add a self-contained capture lane (#5415)
eli-r-ph Oct 27, 2026
86fd5a1
fix(nextjs): restore accurate integration fixtures and cooldown compl…
eli-r-ph Oct 27, 2026
c4bdc81
feat(node)!: send analytics and AI events on separate capture lanes (…
eli-r-ph Oct 28, 2026
ea36374
feat(node)!: retry and compress capture requests by the v1 rules (#5411)
eli-r-ph Oct 29, 2026
47eded7
feat(node)!: capture redirect, uuid and duplicate rules (#5416)
eli-r-ph Oct 29, 2026
445ce89
feat(node)!: add per-event capture options and send their values unch…
eli-r-ph Oct 29, 2026
0ac45c1
feat(node)!: build every event in one layer order, with global and co…
eli-r-ph Oct 29, 2026
5a99b41
feat(node)!: take an options object in captureException (#5417)
eli-r-ph Oct 29, 2026
971007c
feat(node)!: disable on personal keys, keep event content out of logs…
eli-r-ph Oct 29, 2026
3f74998
feat(node)!: drop events with invalid names or ids before sending, re…
eli-r-ph Oct 29, 2026
6050236
feat(node)!: generate event uuids from crypto.getRandomValues (#5459)
eli-r-ph Oct 29, 2026
abc9e91
feat(ai): send AI events through captureAi on posthog-node 6 (#5460)
eli-r-ph Oct 29, 2026
91c75a2
chore(node): run the YAML compliance harness against capture v1 (#5456)
eli-r-ph Oct 29, 2026
52868b6
docs(node): add the 5.x to 6.0 migration guide (#5461)
eli-r-ph Oct 29, 2026
6f350fe
fix(ai): set the personless option on AI events with no user id (#5497)
eli-r-ph Oct 31, 2026
2ad361c
docs(node): ask users to review before_send hooks for the 6.0 event s…
eli-r-ph Oct 31, 2026
4811d54
docs(node): keep PostHog keys and strip other Authorization values be…
eli-r-ph Oct 31, 2026
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
5 changes: 5 additions & 0 deletions .changeset/posthog-ai-capture-ai-node-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/ai': minor
---

With posthog-node 6, every AI event is sent through `captureAi` on the capture v1 AI lane, and `enableFullAiCapture` only controls truncation and media redaction. With posthog-node 5, nothing changes: AI events use `capture` unless `enableFullAiCapture` is set.
5 changes: 5 additions & 0 deletions .changeset/posthog-ai-node-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/ai': patch
---

Supports posthog-node 6. Exception autocapture passes an options object to `captureException` when the client is posthog-node 6, and positional arguments for 5.x.
5 changes: 5 additions & 0 deletions .changeset/posthog-ai-personless-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/ai': patch
---

An AI event with no user distinct id now also sets the `process_person_profile: false` event option, so with posthog-node 6 it stays personless even when a context or registered option turns person processing on. An empty or whitespace-only distinct id now counts as missing in every integration, so the event uses the trace or run id and stays personless.
5 changes: 5 additions & 0 deletions .changeset/posthog-core-debug-payload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/core': patch
---

Debug mode logs an emitted event's payload through an overridable `describeDebugPayload`, so an SDK can log a summary instead. The default is unchanged.
5 changes: 5 additions & 0 deletions .changeset/posthog-core-fetch-redirect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/core': patch
---

`PostHogFetchOptions` has an optional `redirect` field, so a custom `fetch` sees how capture asks it to handle redirects.
5 changes: 5 additions & 0 deletions .changeset/posthog-core-v7-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/core': patch
---

Export `V7Generator`, so an SDK can generate v7 uuids from its own random source.
5 changes: 5 additions & 0 deletions .changeset/posthog-mcp-node-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/mcp': patch
---

Accepts posthog-node 6 as a peer dependency.
6 changes: 6 additions & 0 deletions .changeset/posthog-node-capture-api-shapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'posthog-node': major
---

`captureException()` and `captureExceptionImmediate()` take an options object (`distinctId`, `properties`, `options`, `uuid`, `flags`, `level`); the old positional form still works in 6.x and logs a deprecation error once per method. `level` is read case-insensitively, with the same aliases as posthog-python (`critical` → `fatal`, `warn` → `warning`); an unknown level keeps `error`.
`registerOptions()` and `unregisterOptions()` return a promise, and `prepareEventMessage()` returns a flat message with top-level `options`, `uuid`, `timestamp` and `disableGeoip`.
5 changes: 5 additions & 0 deletions .changeset/posthog-node-capture-crypto-uuids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': major
---

Generated event uuids and personless distinct ids take their random bits from `crypto.getRandomValues` instead of `Math.random`.
7 changes: 7 additions & 0 deletions .changeset/posthog-node-capture-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'posthog-node': major
---

A personal API key (`phx_...`) passed as the project API key now disables the client, like a missing key, and either case logs one error that never includes the key.
Debug mode logs event names, uuids and counts instead of whole events, and a `before_send` function that throws is logged with the error's type instead of its message. In debug mode, a capture error can still include the server or proxy response text.
A new client option, `captureTracingHeaders` (default `true`), controls whether the Express and NestJS integrations read the `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` request headers into the request context.
5 changes: 5 additions & 0 deletions .changeset/posthog-node-capture-lanes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': major
---

Analytics and AI events are batched and sent on separate lanes, in batches of about 5 MiB at most, and events that fail with a network error are kept and retried on the next flush. `flush()` no longer rejects on delivery failures (they arrive through the `'error'` event), and `maxQueueSize` now applies to analytics events only.
8 changes: 8 additions & 0 deletions .changeset/posthog-node-capture-layers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'posthog-node': major
---

Values set on the event, in context or with `register()` / the new `registerOptions()` now win over the values the SDK adds (`$is_server`, `$geoip_disable`, feature flag properties, trace ids), and `before_send` sees and can change all of them; context and registered properties and options, including the new context `options`, also reach identify, alias and group identify events.
An empty or whitespace distinct id counts as missing, and `$groupidentify` without a distinct id uses the context id, then `$<type>_<key>`.
`$geoip_disable` and `$groups` are filled before `before_send`, so a hook that changes the message's `disableGeoip` or `groups` field has no effect: edit the properties instead.
Reserved exception properties such as `$exception_level` passed in `captureException()` properties are now ignored, so the SDK's value is sent.
7 changes: 7 additions & 0 deletions .changeset/posthog-node-capture-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'posthog-node': major
---

Every capture call takes a per-event `options` map that is sent to capture unchanged (the SDK no longer converts option values); legacy `$` option properties such as `$process_person_profile` still fill unset options and are removed from `properties`.
A `$session_id` or `$window_id` that is not a string is dropped with a warning.
Capture drops an event whose option value it cannot read, such as `cookieless_mode: [1, 2, 3]`, and the SDK reports it through `'error'` with `details: 'invalid_options'`.
5 changes: 5 additions & 0 deletions .changeset/posthog-node-capture-sender-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': major
---

Capture follows only same-origin 307 and 308 redirects; invalid event uuids are replaced with a warning, and dropped duplicate copies are reported.
6 changes: 6 additions & 0 deletions .changeset/posthog-node-capture-sender.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'posthog-node': major
---

Capture retries only 404, 408, 429, 500, 502, 503, 504 and network errors, waiting 100 ms doubled per retry with jitter (at most 30 s, at least `Retry-After`).
Adds `captureMaxRetries`, `captureCompression`, `captureAiCompression`, `captureAiMaxQueueSize` and `captureAiRequestTimeoutMs`.
5 changes: 5 additions & 0 deletions .changeset/posthog-node-capture-v1-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': major
---

Capture always uses capture v1: `POSTHOG_CAPTURE_MODE` is no longer read, `captureAi` sends to `/i/v1/ai/events`, and `CaptureV1Error` is now `CaptureError`.
5 changes: 5 additions & 0 deletions .changeset/posthog-node-capture-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': major
---

Send events only through capture v1, with AI events on their own lane. See the 6.0 migration guide.
6 changes: 6 additions & 0 deletions .changeset/posthog-node-capture-wire-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'posthog-node': major
---

An event whose name is missing, empty or not a string, or whose name, distinct id, session id or window id is not well-formed Unicode (a lone surrogate), is dropped before sending and reported through the `'error'` event, instead of being sent and failing at capture (an object name was sent as `"[object Object]"`). Lone surrogates in property strings are replaced with U+FFFD, so one bad character no longer fails the whole request.
An unparseable event timestamp is still replaced with the current time, and now logs a warning that never includes the value.
22 changes: 8 additions & 14 deletions .github/workflows/sdk-compliance-tests-node-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,13 @@ jobs:
continue-on-error: true
permissions:
contents: read
strategy:
fail-fast: false
matrix:
mode: [v0, v1]
defaults:
run:
shell: bash
env:
NODE_IMAGE: node:24.21.0-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6
# Harness release 1.16.0; pin the published multi-platform image index.
HARNESS_IMAGE: ghcr.io/posthog/sdk-test-harness-v2@sha256:14c539d87c758a8798d3681d26e081cd156b739052d4fdefc51065f92e7cd2f2
CAPTURE_MODE: ${{ matrix.mode }}
# Harness release 1.26.0; pin the published multi-platform image index.
HARNESS_IMAGE: ghcr.io/posthog/sdk-test-harness-v2@sha256:74dae248b19cc6030d803e2904b08d3bd1036b5c215414636a42d090ac27cba0
steps:
# Test the PR head or the triggering commit for push/manual runs.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -81,22 +76,22 @@ jobs:
timeout-minutes: 35
run: |
bash compliance/node/v2/run-compliance.sh node-compliance:local "$HARNESS_IMAGE" \
"$CAPTURE_MODE" "$RUNNER_TEMP/node-compliance/reports" \
"$RUNNER_TEMP/node-compliance/reports" \
2>&1 | tee "$RUNNER_TEMP/node-compliance/run.log"
- name: Run isolated acceptance profile
if: steps.prepare.outcome == 'success'
continue-on-error: true
timeout-minutes: 25
run: |
bash compliance/node/v2/run-compliance.sh node-compliance:local "$HARNESS_IMAGE" \
"$CAPTURE_MODE" "$RUNNER_TEMP/node-compliance/acceptance" acceptance \
"$RUNNER_TEMP/node-compliance/acceptance" acceptance \
2>&1 | tee "$RUNNER_TEMP/node-compliance/acceptance-run.log"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
continue-on-error: true
timeout-minutes: 3
with:
name: node-v2-${{ matrix.mode }}
name: node-v2-v1
path: ${{ runner.temp }}/node-compliance
if-no-files-found: error
retention-days: 14
Expand All @@ -121,8 +116,9 @@ jobs:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
continue-on-error: true
with:
pattern: node-v2-*
path: ${{ runner.temp }}/node-compliance-results
# A download by name extracts into `path` itself, so name the folder the report reads.
name: node-v2-v1
path: ${{ runner.temp }}/node-compliance-results/node-v2-v1
# No checkout or execution of code from the tested revision in this job.
- name: Summarize results and update PR comment
continue-on-error: true
Expand Down Expand Up @@ -167,9 +163,7 @@ jobs:
const lines = [marker, '# Node SDK compliance v2', '',
`Commit: ${sha}`, `[Workflow run and artifacts](${runUrl})`, ''];
for (const [mode, profile, suite, reports] of [
['v0', 'node-legacy', 'acceptance', 'acceptance'],
['v1', 'node-analytics-v1', 'acceptance', 'acceptance'],
['v0', 'node-legacy', 'migration', 'reports'],
['v1', 'node-analytics-v1', 'migration', 'reports'],
]) {
lines.push(`## Capture ${mode} — ${suite}`, '');
Expand Down
106 changes: 106 additions & 0 deletions .github/workflows/sdk-compliance-tests-node.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Node SDK compliance

on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'packages/node/**'
- 'packages/core/**'
- 'packages/types/**'
- 'tooling/**'
- 'compliance/node/adapter.mjs'
- 'compliance/node/Dockerfile'
- '.github/workflows/sdk-compliance-tests-node.yml'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'patches/**'
pull_request:
paths:
- 'packages/node/**'
- 'packages/core/**'
- 'packages/types/**'
- 'tooling/**'
- 'compliance/node/adapter.mjs'
- 'compliance/node/Dockerfile'
- '.github/workflows/sdk-compliance-tests-node.yml'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'patches/**'

permissions: {}

jobs:
compliance:
# The harness has one enable_compression flag but a test per codec, so each
# job runs the adapter with one codec. The SDK has no brotli support.
name: Node SDK compliance (${{ matrix.compression }})
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
compression: [gzip, deflate, zstd]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ matrix.compression }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HARNESS_IMAGE: ghcr.io/posthog/sdk-test-harness:1.26.0@sha256:9db022b5729dcf1557b9e622a36b6d6afa42e1b42653f031b0da6d73ca5d77fe
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Pull test harness
run: docker pull "$HARNESS_IMAGE"

- name: Build SDK adapter
run: docker build -f compliance/node/Dockerfile -t sdk-adapter:test .

- name: Start SDK adapter
env:
COMPRESSION: ${{ matrix.compression }}
run: |
docker network create test-network
docker run -d --name sdk-adapter --network test-network -e COMPRESSION="$COMPRESSION" sdk-adapter:test

- name: Run compliance suites
run: |
mkdir -p report
docker run --rm \
--name test-harness \
--network test-network \
-v "${GITHUB_WORKSPACE}/report:/report" \
"$HARNESS_IMAGE" \
run \
--adapter-url http://sdk-adapter:8080 \
--mock-url http://test-harness:8081 \
--output text \
--report /report/sdk-compliance-report.md \
--sdk-type server

- name: Publish report to the job summary
if: always()
run: |
if [ -f report/sdk-compliance-report.md ]; then
cat report/sdk-compliance-report.md >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload compliance report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: node-sdk-compliance-report-${{ matrix.compression }}
path: report/sdk-compliance-report.md

- name: Show adapter logs on failure
if: failure()
run: docker logs sdk-adapter || true

- name: Stop containers
if: always()
run: |
docker rm -f sdk-adapter || true
docker network rm test-network || true
3 changes: 2 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,13 @@
},
"overrides": [
{
"files": ["compliance/node/v2/**"],
"files": ["compliance/node/v2/**", "compliance/node/adapter.mjs"],
"env": { "node": true },
"rules": {
"compat/compat": "off",
"posthog-js/no-direct-null-check": "off",
"posthog-js/no-direct-array-check": "off",
"posthog-js/no-direct-function-check": "off",
"posthog-js/no-direct-number-check": "off",
"posthog-js/no-direct-undefined-check": "off"
}
Expand Down
4 changes: 2 additions & 2 deletions compliance/node/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ RUN pnpm --filter posthog-node build
WORKDIR /app/adapter

# Copy adapter code
COPY compliance/node/adapter.js ./
COPY compliance/node/adapter.mjs ./

# Install adapter dependencies (express) in clean directory
RUN npm init -y && npm install express

EXPOSE 8080

USER node
CMD ["node", "adapter.js"]
CMD ["node", "adapter.mjs"]
42 changes: 0 additions & 42 deletions compliance/node/Dockerfile.v1

This file was deleted.

14 changes: 7 additions & 7 deletions compliance/node/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# PostHog Node SDK Compliance Adapter

Compliance adapter for the posthog-node SDK with the [PostHog SDK Test Harness](https://github.com/PostHog/posthog-sdk-test-harness).
It runs the capture v1 suites (`capture_v1`, `capture_ai_v1`) and the feature flag suites.

## Running Tests

```bash
# From compliance/node — legacy /batch/ contract (capture_v0)
# From compliance/node
docker-compose up --build --abort-on-container-exit

# Capture V1 contract (POST /i/v1/analytics/events)
POSTHOG_CAPTURE_MODE=v1 docker-compose up --build --abort-on-container-exit
# Another codec for the compression tests
COMPRESSION=zstd docker-compose up --build --abort-on-container-exit
```

The adapter's capture mode is fixed per process: with `POSTHOG_CAPTURE_MODE=v1`
it advertises the `capture_v1` capability and the harness runs the V1 contract;
otherwise it advertises `capture_v0`. CI runs both as separate jobs
(`Dockerfile` for v0, `Dockerfile.v1` for v1).
Each adapter process runs one codec (`COMPRESSION`: `gzip`, `deflate` or `zstd`, default `gzip`) and advertises only that `encoding_<codec>` capability.
CI runs one job per codec (`.github/workflows/sdk-compliance-tests-node.yml`).
The Gherkin harness adapter lives in `v2/`.

## Documentation

Expand Down
Loading