Repository navigation
Conversation
|
Coherence0 spec problems · 0 failing chokepoints · 2 spec gaps · 0 guard failures 44 components, 7 bullets (5 invariants, 2 requirements), 0 problems Chokepoints: none declared. Spec gaps: 2 entrances with outside or unknown trust and no traced control, and 10 more held by the adoption baseline
Close one with |
* test(node): read capture events from either wire format Add shared helpers that find analytics capture requests on the legacy /batch/ endpoint or the capture v1 analytics endpoint, and return their events in the legacy shape. A v1 event's options, session and window ids and SDK info header map back to the properties they replace, so one assertion holds for both formats. Port the tests that read events off the wire to these helpers, and answer capture requests with a body that both senders accept. The same tests pass on the legacy default and with POSTHOG_CAPTURE_MODE=v1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ai): accept capture v1 requests in the cassette collector The collector accepted only the legacy /batch/ request. It now also accepts the capture v1 analytics and AI endpoints with a bearer token, and returns their events in the legacy shape, so the replay tests hold for both wire formats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(convex): read example events from either capture endpoint The example tests read events only from /batch/ calls and checked the api_key in the body. Read them from the legacy or the capture v1 analytics endpoint, and accept the project key from the body or the bearer token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(convex): give the sequenced fetch mock the headers field its type requires Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Node SDK compliance v2Commit: 86fd5a1 Capture v1 — acceptance✅ 28 passed / ❌ 3 non-passing / Drill-down: run artifacts → ❌ server_snapshot_payload_false — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_zero — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_null — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: Harness exit: 1; report validation exit: 1. Capture v1 — migration✅ 115 passed / ❌ 1 non-passing / Drill-down: run artifacts → ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. |
* refactor(node): name the capture lane on CaptureError and export it Rename CaptureV1Error to CaptureError (and its params and drop types), add the endpoint the batch was sent to, and export the class so callers can instanceof it. The sender now takes its lane path from config so the analytics and AI lanes can share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node)!: send every event through capture v1 Remove the POSTHOG_CAPTURE_MODE switch and the legacy /batch/ path. Every capture() event, including $ai_* events, goes to /i/v1/analytics/events; the SDK no longer reroutes by event name. captureAi and captureAiImmediate send to /i/v1/ai/events through the same sender, with one lazily created sender per lane. A 413 on the AI lane is now a terminal CaptureError instead of a split-and-retry. A client built while POSTHOG_CAPTURE_MODE is set logs one warning that the variable is no longer read. Tests that read events through the shared helpers pass unchanged; the wire snapshot, the capture lane wiring tests and the delivery error tests now assert the v1 wire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(compliance): run the node v2 harness on the capture v1 profile only posthog-node no longer has a v0 capture mode, so drop the v0 matrix leg and the node-legacy profile. The adapter stops setting POSTHOG_CAPTURE_MODE and stops advertising capture_v0, capture_v0_batch and capture_ai_v0. run-compliance.sh no longer takes a mode argument, and the PR comment reports both suites for the one profile. The job stays advisory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): add changeset for capture v1 only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): keep the process alive while a capture send retries The v1 sender waited between retries and timed each attempt with unref'd timers. A script ending in `await posthog.shutdown()` could exit during a retry wait and lose the batch. Use ref'd timers inside an in-flight send, as the v0 path did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ai): accept only capture v1 requests in the cassette collector posthog-node no longer sends to /batch/, so the collector now rejects the legacy endpoint and body, and requires the bearer token and the v1 event fields on every request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(convex): assert the capture v1 wire in the example tests posthog-node sends events only to the capture v1 analytics endpoint. Read events from that endpoint alone, and check the bearer token and the absence of an api_key in the body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): download the node compliance artifact into the folder the report reads With one artifact left, a pattern download extracts it straight into the target path, so the report found no node-v2-v1 folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): serialize capture v1 events once for reuse Add serializeV1Event, which builds an event's wire JSON once and records its byte size and the size of its properties, and buildV1Body, which joins already-serialized events into a request body. Nothing calls them yet; the capture lane uses them to batch by bytes without serializing twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node): add a self-contained capture lane A lane owns one endpoint's in-memory queue, flush timer, flushAt trigger, byte- and count-aware batching and in-flight send, and keeps events that failed without an HTTP response for the next flush. It takes its sender, config, callbacks, logger and clock from the caller. The sender contract it needs is declared here as V1SendOptions (an abort signal and keepUnsent); the capture sender implements it when the client switches to lanes. Nothing uses the lane yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Error tracking smokeCommit: 7/8 passed (0 skipped, 0 uncovered)
|
…5410) * refactor(core): let subclasses call normalizeMessage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(node): send pre-serialized events and let callers keep unsent ones The capture sender now takes events serialized once, builds the body from those JSON strings, accepts an abort signal that stops attempts and backoff waits, and can hand back events whose last attempt got no HTTP response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): cancel capture backoff waits cleanly on abort The default backoff wait now clears its timer when the send is aborted and removes its abort listener when it finishes, so a signal shared by every send of a lane does not collect listeners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node)!: send analytics and AI events on separate capture lanes Each lane has its own queue, timer, flushAt trigger, batching and in-flight send, so a slow or failing lane no longer delays or drops the other's events. flush() waits for both lanes and no longer rejects on delivery failures, maxQueueSize applies to analytics events only, batches stop at about 5 MiB, and events that fail with a network error are kept for the next flush. Shutdown drains both lanes against one deadline and aborts sends still running when it passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): add changeset for separate capture lanes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): keep the shutdown deadline while a capture body is compressing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node)!: retry capture requests on the fleet schedule
Capture waits 100 ms before the first retry and doubles each wait, with
+/-25% jitter, up to 30 seconds; Retry-After raises a wait and jitter never
shortens it. The new captureMaxRetries option (integer 0-9) sets the retry
count and falls back to fetchRetryCount; fetchRetryDelay no longer affects
capture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): choose the capture compression codec per lane
captureCompression ('none' | 'gzip' | 'deflate' | 'zstd', default 'gzip') sets
the analytics lane codec and maps disableCompression: true to 'none' when unset.
captureAiCompression sets the AI lane codec on its own (default 'gzip'). The
Node build compresses with node:zlib, with zstd from Node 22.15; the edge build
uses CompressionStream for gzip and deflate. An unavailable zstd falls back to
gzip with one debug log. Content-Encoding names the codec used, and each request
sends a fresh copy of the compressed body.
The convex example tests now decode capture bodies of any type and codec.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): size and time the AI capture lane on its own
captureAiMaxQueueSize (default 1000) replaces the fixed AI queue cap, and
captureAiRequestTimeoutMs (default 30000) sets the AI lane request timeout.
The analytics lane keeps requestTimeout. Invalid values log one error and
use the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(node): add changeset for capture retries and compression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(node): regenerate the public API references
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node)!: retry capture requests that get 404 or 429
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): keep captureAiMaxQueueSize as set when it is below flushAt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): cap capture settings at the timer limit and hide the compressor hook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(core): add an optional redirect mode to PostHogFetchOptions Type-only and additive: no core runtime change. It lets posthog-node send capture requests with `redirect: 'manual'`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node)!: follow only same-origin 307 and 308 capture redirects Capture requests use redirect: 'manual' and follow a 307 or 308 whose Location has the configured host's origin, at most 5 times, with the same method, headers and a fresh copy of the body. Any other redirect is a final failure reported as a CaptureError. A custom fetch that follows redirects itself still works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node): send event uuids in canonical form Before an event is queued or sent, and again after before_send, its uuid must be 32 hex digits, the hyphenated form, the braced hyphenated form or the urn:uuid: form, in any case; it is sent lowercase and hyphenated, and results match by that form. Any other uuid is replaced with a generated v7 uuid and one warning that does not repeat the value. An empty or missing uuid is generated silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node): report copies the server dropped as duplicates When a 2xx result carries duplicates_dropped: n, the first event with that uuid in the request takes the result, the next n are reported as drops with details duplicate_event_uuid and are never retried, and later copies take the kept result. Values that are not positive integers are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node): log one line per batch that loses capture events With no 'error' listener, a batch that loses events (server drops, exhausted retries, a final request failure) and a lane flush that drops events locally each log one console line. It names the lane, the count of events not delivered and each cause with its HTTP status, and never includes server response text, event content or uuids. With a listener there is no line: the listener gets the CaptureError as before. Local drops at shutdown now arrive in one report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): add changeset for capture redirects, uuids and duplicates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): regenerate the public API references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(react-native): regenerate the public API references The optional `redirect` field on `PostHogFetchOptions` is part of the React Native public API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): warn once per call for an invalid uuid, add a core changeset for the redirect option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…anged (#5412) * feat(node)!: send event option values unchanged The wire builder no longer converts option values. It sends the event's `options` object as given, removes each legacy option property (`$process_person_profile`, `$cookieless_mode`, `$ignore_sent_at`, `$product_tour_id`) from `properties`, and uses its value only when the matching option is missing or null. Unknown option keys pass through, and an `options` value that is not an object is logged once and ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node): warn when a non-string session or window id is dropped `$session_id` and `$window_id` still move to the top-level `session_id` and `window_id` only when they are strings. A null value is dropped silently; any other value is dropped with a warning that names its JSON type and never the value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node): add per-event options to every capture call `capture`, `captureImmediate`, `captureAi`, `captureAiImmediate`, `identify`, `alias`, `groupIdentify`, `setPersonProperties` and `unsetPersonProperties` (and their immediate forms) take an optional `options` map that is sent to capture unchanged. An `options` value that is not an object is logged once and the event is sent without it. `before_send` receives the options and its returned options are sent. The hook now gets copies of `properties` and its `$set`, `$set_once`, `$groups` and `$group_set` maps, so it cannot change the caller's objects. An event with no distinct id now sets the `process_person_profile` option to false, unless the caller set it, instead of the legacy `$process_person_profile` property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): add changeset for per-event capture options Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): regenerate the public API references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): accept options on alias in the IPostHog interface Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): give before_send a full copy of plain values and always an options object Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(node): before_send always gets an options object Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): note that capture drops an event with an unreadable option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ntext options (#5413) * feat(node): add registerOptions() and context options `registerOptions()` and `unregisterOptions()` set options for every event, next to `register()`. A context takes `options` next to `properties`; nested contexts inherit them unless `fresh: true`. Each event fills the options it left unset (missing or null) from the context, then from `registerOptions()`, then from the SDK's personless option, before `before_send` runs. A value that is not an object logs one error and is ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node)!: fill context, global and SDK values before before_send Each event now fills the properties it left unset (key missing or undefined) from the context, then from `register()`, then from the values the SDK adds: `$is_server`, `$geoip_disable` from the `disableGeoip` setting, feature flag properties from `flags` or `sendFeatureFlags`, and, on `$exception`, the OpenTelemetry trace ids (as a pair) and the injected `$release_id`. So a value set on the event, in context or with `register()` now beats every SDK value, and `before_send` sees and can change all of them; nothing is added after the hook. `$set`, `$set_once`, `$groups` and `$group_set` fill one level deep. Typed `groups` merge over a `$groups` property key by key. A per-call `disableGeoip` sets `$geoip_disable` under the event's own property, so `disableGeoip: false` now sends `$geoip_disable: false`. The minimal `$feature_flag_called` allowlist runs after every fill and keeps the properties that set options and session fields. `$bot_pageview` renaming happens before the hook. `$lib` and `$lib_version` are no longer added to properties; the request header carries them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(node)!: apply every layer to identify, alias and group identify Context and `register()` properties and options now reach `$identify`, `$create_alias` and `$groupidentify` like every other event. Identify's context properties go into the event properties, not into `$set`, and identify no longer sends an empty `$set_once`. An empty or whitespace-only distinct id counts as missing, so the context id or a personless id is used. `groupIdentify` without a distinct id uses the context id before `$<groupType>_<groupKey>`, and never takes the context session. `alias` never takes its target from a context or a registered property, and its `distinct_id` property is the resolved id. The `IPostHog` `alias` and `aliasImmediate` signatures accept `disableGeoip` and `options`, like the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): ignore reserved exception properties passed to captureException Following sdk-specs `exception-event-metadata`, properties passed to `captureException` can no longer set the exception properties the SDK or PostHog owns: `$exception_list`, `$exception_level`, `$exception_source`, `$debug_images` and the processor-owned `$exception_*` and `$cymbal_errors` keys. Other properties are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): add changeset for layered event values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): regenerate the public API references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): note that before_send edits properties, not the disableGeoip or groups fields Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): note ignored exception properties and the cost of a bad registered option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): regenerate the public API references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node)!: make registerOptions and unregisterOptions async
They return a promise, like register() and unregister().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node)!: return a flat message from prepareEventMessage
The result has the same shape as the message passed in: `options` holds the event options, and
`uuid`, `timestamp` and `disableGeoip` are top-level fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node)!: take an options object in captureException
captureException(error, { distinctId, properties, options, uuid, flags, level }) and the same for
captureExceptionImmediate. `level` sets `$exception_level` (default `error`). The 5.x positional
form still works and logs one deprecation error per process.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(mcp): correct the comment on why the sink passes groups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(examples): pass an options object to captureException
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(node): add changeset for the captureException options object
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(convex,next,nuxt): stay on posthog-node 5 until their own major
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ai): pass captureException arguments in the form the client takes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(node): regenerate the public API references
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(convex): declare the flag action return types
The build emits declarations, and with posthog-node 5 from the registry the
inferred types point into that package's own copy of @posthog/core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: exempt posthog-node and @posthog/core from the release-age wait
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): normalize exception levels like Python, warn per method and on extra arguments, keep the release cooldown
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…, add captureTracingHeaders (#5457) * feat(node)!: disable on personal keys, keep event content out of logs, add captureTracingHeaders Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): regenerate the public API references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): qualify the debug log note for server response text Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…pair lone surrogates in properties (#5458) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node)!: generate event uuids from crypto.getRandomValues Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): generate a missing event uuid from the crypto-backed generator too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(node): run the YAML compliance harness against capture v1 Rewrite the YAML adapter for capture v1 only: pass options and caller uuids unchanged, pick one codec per process, and declare the capture_ai_v1, event_options and harness 1.23.0 capabilities. Add a CI job per codec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): support the harness reported_drops, log_capture, generated_payloads, property_layers and fail_open features Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(node): name each reported drop once and size padding markers in bytes in the compliance adapter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): run the compliance harness from release 1.25.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(node): expect $geoip_disable: false from a per-call disableGeoip: false in the v2 adapter test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): run the compliance harness from release 1.26.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): run the Gherkin harness from release 1.26.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(node): pin the YAML harness image by digest and reuse the parsed attempt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs(node): add the 5.x to 6.0 migration guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): qualify the log guarantee and positional captureException note Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): describe the before_send copy, exception levels and captureException warnings in the 6.0 guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(node): note that before_send edits no longer reach the sendFeatureFlags request Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Problem
posthog-node still sends events to the legacy capture endpoints, and capture v1 is only an opt-in through an environment variable. posthog-node 6 makes capture v1 the only capture path, with AI events on their own lane at
/i/v1/ai/events, as posthog-python 8, posthog-go v2 and posthog-rs 1.0 do on theirv1branches.Changes
majorchangeset forposthog-node, so the next release from this branch is 6.0.0.Release info Sub-libraries affected
Libraries affected
The wrapper packages that depend on
posthog-nodeget their release plan in a later PR of this series.Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Authored with Claude Code (Claude Opus 5.5), directed by @eli-r-ph. Changeset only; each feature PR carries its own tests. Behavior follows the
v1branches of posthog-python, posthog-go and posthog-rs, adapted to posthog-node's own API and style.🤖 Generated with Claude Code