feat: PostHog destination (web device-mode + server cloud-mode) - #21
Merged
Merged
Conversation
Web (device-mode) + server (cloud-mode) destinations in one package with two tree-shakeable factories. Establishes the canonical identity-projection convention that GA4/Amplitude retrofit and the session-ID gap follow from. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 3 review surfaced three reliability defects in the originally specified flush/retry design; all three fixed: - flush() requeues its batch on exhausted-retry failure instead of dropping it; new maxBufferSize config (default 1000) bounds memory, dropping + logging oldest past the cap - timer-driven flush failures always log (were gated behind debug, and run outside the collector's send() error wrapper) - postWithRetry short-circuits non-retryable 4xx (retries 429 + 5xx only) +5 reliability tests; suite 208/208. Plan amended to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
Review of the web factory surfaced two plan-mandated defects: - loadSnippet injected array.js but never created window.posthog, so init() and every capture()/identify() before the async script loaded were dropped — the destination never actually initialized in-browser. Now installs PostHog's official queuing stub; array.js replays queued calls (the queue-before-load pattern our destination rules require). - onConsent only opted out on revoke; re-granting analytics consent was a no-op. Now calls opt_in_capturing() on re-grant (consent-first). +3 tests. Plan Task 5 amended to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
biome check . was linting Next.js and Astro build artifacts (900+ noise errors), making `npm run lint` unusable. Both dirs are gitignored generated output, same as dist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
Barrel exports both factories + PostHogBaseConfig/POSTHOG_DEFAULT_HOST. Full build verification (tsup ESM + dts, typecheck, lint, tests) surfaced a type error in the Task 5 queue stub: [key].concat(unknown[]) fails tsc; switched to [key, ...args] (identical runtime). Package builds clean, 223 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
Canonical event.user.{anonymousId,userId,traits} + user:identified, and
how each destination projects it: parallel-fields vendors (GA4/Amplitude)
no-op identify; merge/alias vendors (PostHog) must act on it. Notes the
tracked session-ID gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
- docs page + overview table row (Client + Server) - demo: env-gated createPostHogWeb registration (inert without a key) - root tsconfig paths entry for @junctionjs/destination-posthog - minor changeset for the new package Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
…ntity gap Final whole-branch review findings: - Critical: teardown() could return while a flush's fetch was still in flight (send() is awaited internally but the collector dispatches it fire-and-forget; the timer uses void flush()). On shutdown the runtime could exit mid-request and silently drop events. Now tracks in-flight flush promises and teardown awaits them (Promise.allSettled) after draining the buffer. Regression test fires an un-awaited send. - Important: server-mode capture now forwards device.userAgent as $raw_user_agent so PostHog derives browser/OS correctly (it otherwise reads our server's outbound UA). - Docs: qualify the web-mode identity claim — posthog-js manages its own anonymous distinct_id; Junction's anonymousId isn't bound at init. Recorded as a tracked known-gap alongside the sessionId gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LYiJTqGj9jPcivJbq3kMC
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Merged
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
@junctionjs/destination-posthog— a PostHog destination offered as two tree-shakeable factories in one package, mirroring the device-mode vs cloud-mode split. Import only the one you need; the other is eliminated by tree-shaking, andposthog-jsis loaded from PostHog's CDN at runtime, never bundled.What's included
createPostHogServer(cloud-mode) — transforms events and forwards them to PostHog's HTTP capture API. Batching (/capturefor single,/batchabove threshold), retry with exponential backoff on 429/5xx only (4xx short-circuits), requeue-on-failure with a boundedmaxBufferSize, and a shutdown path that awaits in-flight requests so events aren't silently dropped when a short-lived server/edge process exits. Forwards$raw_user_agentso PostHog derives browser/OS correctly.createPostHogWeb(device-mode) — installs PostHog's queue-before-load stub (soinit()and pre-load captures actually queue instead of being dropped), routes toposthog.capture()/posthog.identify(), keeps Junction the event source of truth (autocapture / pageview / session replay off by default), and honors consent both ways (opt-out on revoke, opt-in on re-grant)..claude/rules/destinations.md: how each destination projects Junction's canonicalevent.userinto the vendor's shape, and that merge/alias vendors (PostHog) must act onuser:identified..next/.astrooutput (it was emitting 900+ noise errors, makingnpm run lintunusable).Testing
npm run typecheck,npm run lint, and the fullvitestsuite (225 tests) all pass; the package builds ESM + dts cleanly. New tests cover the server reliability paths (retry classes, requeue, buffer cap, timer flush, in-flight-await on teardown) and the web queue-before-load stub + consent symmetry.Known gaps (tracked in the rules doc)
UserIdentityhas nosessionId; GA4/Amplitude/PostHog session mapping is a cross-destination follow-up.distinct_idto posthog-js's own cookie ID rather than binding Junction'sanonymousId(posthog-js needs it atinit()); reconcile viabootstrap.distinctIDlater.🤖 Generated with Claude Code