feat: PostHog telemetry, error tracking, docs analytics, and release annotations#141
Conversation
…g for SDK and CLI Replay of PRs #80 + #86 onto the new core (post batch recipient variables #125 and scheduled sends #127). - telemetry.ts / telemetry.test.ts ported byte-for-byte from #86, including the reviewed redaction pipeline, opt-outs (EMAIL_SDK_TELEMETRY=0/false/off, DO_NOT_TRACK, NODE_ENV=test, telemetry: false), first-run notice, exception dedupe/budget, and fire-and-forget delivery with a 3s timeout. - core.ts re-instruments the new send architecture: one "email sent" event per user-facing send() call (never per expanded recipient), with used_recipient_variables, used_send_at, and delivery_path (single | bulk_native | bulk_expanded) alongside the #86 fields; "email batch sent" summary for sendBatch; captureException for reportable send errors. - telemetrySource moved off the public EmailClientOptions type onto an internal module seam (setTelemetrySource) that only the bundled CLI flips; the public surface gains only `telemetry?: boolean`. - CLI reports "cli command run" and flushes captures before exit. - README telemetry disclosure sections and a combined minor changeset. Generated-By: PostHog Code Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
Port of the docs-site half of #86: posthog-js initialized once on hydration in __root.tsx (2026-01-30 defaults, capture_exceptions, web vitals, identified_only person profiles, session recording disabled), privacy page disclosure updates, and CSP pinned to the exact PostHog hosts (us-assets.i.posthog.com in script-src; us.i.posthog.com and us-assets.i.posthog.com in connect-src — no wildcards). Generated-By: PostHog Code Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
Posts a project-scoped annotation to PostHog project 468042 when changesets publishes, so release markers line up with telemetry charts. Skips gracefully when POSTHOG_PERSONAL_API_KEY is absent and never fails the release job. Generated-By: PostHog Code Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds end-to-end PostHog telemetry to the Email SDK — anonymous event capture in the SDK and CLI, redacted error reporting via a custom HTTP telemetry client, docs-site analytics via
Confidence Score: 5/5Safe to merge — the telemetry path is fully fire-and-forget and can never interrupt email delivery, all opt-outs work correctly, test isolation is enforced at the preload level, and the CSP is tightly scoped. The implementation is careful and well-defended: the redaction pipeline covers all the adversarially-tested patterns, the deliver() call wraps every network error in a silent catch, flush() is called before CLI process exit so no events are dropped, and the test preload guarantees network isolation unconditionally. The one new observation (PostHog initialisation deferred to a React effect on the docs site) is a coverage gap for error autocapture before hydration, not a correctness or security issue. apps/fumadocs/src/routes/__root.tsx — PostHog initialisation timing relative to hydration-phase errors. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant CLI
participant Client
participant Telemetry
participant PostHog API
User->>CLI: email-sdk send --adapter resend
CLI->>Telemetry: setTelemetrySource("cli")
CLI->>Client: "createEmailClient({ adapters })"
Client->>Telemetry: capture("client created")
Telemetry-->>PostHog API: POST /capture/ (fire-and-forget, 3s timeout)
CLI->>Client: client.send(message)
Client->>Client: sendWithAdapters()
alt "delivery_path = single"
Client->>Client: sendWithRetry()
else "delivery_path = bulk_native"
Client->>Client: provider.sendBulk()
else "delivery_path = bulk_expanded"
Client->>Client: sendBulkViaFallback() per recipient
end
alt Success
Client->>Telemetry: "capture("email sent", {success:true, delivery_path})"
else Failure
Client->>Telemetry: "capture("email sent", {success:false})"
Client->>Telemetry: captureException(error)
end
CLI->>Telemetry: capture("cli command run")
CLI->>Telemetry: flush()
Telemetry->>PostHog API: await all pending captures
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant CLI
participant Client
participant Telemetry
participant PostHog API
User->>CLI: email-sdk send --adapter resend
CLI->>Telemetry: setTelemetrySource("cli")
CLI->>Client: "createEmailClient({ adapters })"
Client->>Telemetry: capture("client created")
Telemetry-->>PostHog API: POST /capture/ (fire-and-forget, 3s timeout)
CLI->>Client: client.send(message)
Client->>Client: sendWithAdapters()
alt "delivery_path = single"
Client->>Client: sendWithRetry()
else "delivery_path = bulk_native"
Client->>Client: provider.sendBulk()
else "delivery_path = bulk_expanded"
Client->>Client: sendBulkViaFallback() per recipient
end
alt Success
Client->>Telemetry: "capture("email sent", {success:true, delivery_path})"
else Failure
Client->>Telemetry: "capture("email sent", {success:false})"
Client->>Telemetry: captureException(error)
end
CLI->>Telemetry: capture("cli command run")
CLI->>Telemetry: flush()
Telemetry->>PostHog API: await all pending captures
Reviews (3): Last reviewed commit: "fix(telemetry): address review nits — ex..." | Re-trigger Greptile |
…ction gaps Adversarial-review fixes: - error.name now runs through the redaction pipeline (allowlisted SDK/builtin names pass verbatim) before it reaches $exception_list type, error_name, the class dedupe key, or $exception_fingerprint - bun test preload forces EMAIL_SDK_TELEMETRY=0 so the in-process suite can never capture live or write telemetry state, regardless of NODE_ENV - home-dir redaction now consumes the remaining path segments on both separators (~<path-redacted>) instead of leaking filenames after the collapse - key=value secrets (password/passwd/pwd/pass/secret/token/api_key) redact before the token-length pattern so short values are caught too Generated-By: PostHog Code Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
… narrow batchAdapter type, add resetTelemetrySource seam Generated-By: PostHog Code Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Generated-By: PostHog Code Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
Replaces #86 (and the closed #80) as a clean replay of the entire PostHog telemetry feature onto the current core. Since those PRs were opened, main gained batch sends with per-recipient variable substitution (#125) and scheduled sends via
sendAt(#127), which reworkedcore.ts,types.ts,utils.ts, andpayloads.ts— so instead of a conflict-riddled rebase, this PR re-applies #86's head (which contained all of #80 plus the error-tracking delta) file by file onto the new architecture.What's included
SDK/CLI telemetry (
telemetry.ts, ported byte-for-byte from #86):client created,email sent,email batch sent,cli command runEMAIL_SDK_TELEMETRY=0/false/off,DO_NOT_TRACK=1/true,NODE_ENV=testauto-off,createEmailClient({ telemetry: false }), one-time stderr notice,$process_person_profile: false, fire-and-forget captures with a 3s timeout, CLI flush before exitRe-instrumentation for the new core (the part that differs from #86):
send/sendBatch. Main's send path now fans out three ways: plain single sends, nativeprovider.sendBulkforrecipientVariables, and client-side per-recipient expansion when the adapter has no native batch. Telemetry now fires exactly oneemail sentevent per user-facingsend()call — expanded per-recipient sends run throughsendWithRetryinternally and are never counted individually, so no double-countingemail sentgainedused_recipient_variables,used_send_at, anddelivery_path(single|bulk_native|bulk_expanded). On success the delivering adapter decides the path; on failure the primary resolved adapter does (the path that would have run)email batch sentsummary forsendBatchis unchanged from Add PostHog error tracking, richer telemetry, docs analytics, and release annotations #86 (message_count, succeeded, failed, recipients, uniform adapter ormixed, first failure code) and reflects the adapter that actually delivered after fallbackstelemetrySourcemoved off the public type (review item from #86):telemetrySourceon the publicEmailClientOptionsmarked@internal. It now lives as module state intelemetry.tsbehind an internalsetTelemetrySource()seam that only the bundled CLI flips at process start (neither getter nor setter is re-exported from the package entry point). The public options surface gains onlytelemetry?: booleanDocs site (email-sdk.dev):
posthog-jsinitialized once on hydration in__root.tsx(defaults: "2026-01-30",capture_exceptions, web vitals,person_profiles: "identified_only", session recording disabled)us-assets.i.posthog.cominscript-src,us.i.posthog.com+us-assets.i.posthog.cominconnect-src, no wildcards — rebased onto main's current CSP (which had since gained the Notra image host)Release pipeline:
release.ymlposts a project-scoped release annotation to PostHog project 468042 after changesets publishes; skips with a notice whenPOSTHOG_PERSONAL_API_KEYis absent and never fails the job (jq-built body, non-blocking curl)Disclosures:
@opencoredev/email-sdkcovering both original changesetsAdversarial review round (c271794)
Four verified findings fixed:
error.namebypassed redaction (major): a writableError.nameflowed verbatim into$exception_list[].type,error_name, the class dedupe key, and$exception_fingerprint. Names now pass through the same redaction pipeline as messages, with an allowlist so SDK error classes and JS built-ins still report verbatim (keeping fingerprints stable). Covered by hostile-name tests asserting the full payload carries no emails or paths.NODE_ENV=test(major):env EMAIL_SDK_TELEMETRY=1 NODE_ENV=production bun testcould capture live. A Bun[test] preload(rootbunfig.toml+ a package-level mirror, since Bun only reads the cwd's bunfig) now forcesEMAIL_SDK_TELEMETRY=0for every in-process run. Proven with that exact hostile env plus a global fetch spy: 182 pass, zero PostHog requests, no telemetry state file written.C:\Users\jsmith\Documents\payroll.xlsxpreviously becameC:~\Documents\payroll.xlsx. The redaction now consumes the remaining segments on both separators, yielding~<path-redacted>. Tests for POSIX and Windows tails.password=hunter2-style values (password/passwd/pwd/pass/secret/token/api_key, case-insensitive, value up to whitespace/quote) now redact tokey=<redacted>, applied before the token-length pattern so short secrets are caught too.Two review observations accepted as known behavior (not fixed here):
noticeShownmarker cannot persist; telemetry itself still works with a per-process identity.email sentattributesadapter/delivery_pathto the primary resolved adapter (the path that would have run), not whichever fallback failed last.Validation
bun run build+bun test: 182 pass / 0 fail (8 new redaction/isolation tests from the review round) (116 pre-existing tests untouched and green; 47 telemetry unit tests ported unchanged; 11 client-telemetry integration tests, including new coverage fordelivery_path,used_send_at, no double-counting on expansion, andtelemetry: false)NODE_ENV=testauto-disables the shared telemetry singleton underbun test; all enabled test instances use an injected fetch, and CLI subprocess tests additionally setEMAIL_SDK_TELEMETRY=0, so the suite makes no live network callsbunx oxlintclean on all touched filesapps/fumadocs:bun run types:checkandbun run buildgreenNote for #128 (README restyle): main's READMEs were not rewritten here — only the Telemetry sections were added, so #128 rebases over this cleanly.
Created with PostHog Code