Repository navigation
feat(node): send a flat body from emails.send - #5487
Conversation
emails.send now posts template_id and distinct_id at the top level of the request body. The server accepts both shapes, so this changes only the wire format. Co-Authored-By: Claude Opus 5.5 <[email protected]> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
Coherence📄 0 spec file errors · ⛔ 0 broken rules · 🚪 2 unchecked inputs · 🐛 0 bugs that came back What is Coherence?Coherence compares the code with its spec files ( This check read 44 parts of the code and 7 rules. Note These findings do not make this check fail. None of these findings are in files that this pull request changes. You do not need to fix them in this pull request. 🚪 Unchecked inputs (2)
What this means and how to fix itInput from outside this code comes in at these places. The spec does not say what checks that input. 10 more places were already like this when Coherence was added. This list does not show them. To close one of these, find the place by its name in the
To get the line to add, run |
Node SDK acceptance v2Commit: 2487e70 Capture v0✅ 83 passed / ❌ 4 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: ❌ 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. Capture v1✅ 148 passed / ❌ 4 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: ❌ 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. |
|
@PostHog review |
|
@mayteio PostHog Review is reviewing this pull request. |
ioannisj
left a comment
There was a problem hiding this comment.
🤖 An AI agent wrote this on behalf of @ioannisj.
No findings. The flat body matches what the serializer from PostHog/posthog#126287 reads, and the updated tests check the full body with and without distinctId.
Take main's flat send body from #5487: the template form sends template_id and distinct_id at the top level, like the code form. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(replay): inline stylesheets that load late or after a link changes (#5490) * fix(replay): inline stylesheets that load late or after a link changes Three recorder bugs left a `<link rel=stylesheet>` external in recordings, so replay had to load the CSS remotely or kept the wrong CSS: - A sheet that loaded more than 5 s after the recorder serialized its link was never inlined. When the timer fired, it also removed the load listener (regression from cafa9cc). The timer now checks for the sheet and leaves the load listener in place. An error event, a disconnected link or a full snapshot stops the tracking. - A `rel="preload"` link that changed to `rel="stylesheet"` after the snapshot was never inlined. - An `href` change on a stylesheet link recorded the old URL, because the old sheet stays attached until the new one loads and transformAttribute prefers `link.sheet.href`. No `_cssText` followed for the new sheet. After a `rel` or `href` mutation on a stylesheet link, the recorder now waits until `link.sheet.href` equals `link.href` and then sends `_cssText`, the same mutation that the budget path sends. Chrome 131 fires no load event after such an href change (Chrome 156 does), so the recorder also checks the sheet on a backoff. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): export link stylesheet helpers from the record entrypoint The `@posthog/rrweb-record` build resolves `@posthog/rrweb-snapshot` to `src/record.ts`, which did not export `onceLinkStylesheetUpToDate` and `isStylesheetLink`, so the browser SDK build failed. Review fixes for the changed-link inlining: - Before it sends `_cssText`, the recorder checks again that the link is not blocked and still has a mirror id, so a link blocked or removed while its sheet loaded is not captured. - `stop()` resets stylesheet load tracking, so no tracking outlives the recording. - The checks go on at the last delay instead of ending, so a link that leaves the DOM without a load or error event is released. - The `_cssText` mutation carries the link's current attributes (other than rel and href), because replay rebuilds the link from its snapshot-time attributes. A `media` change no longer reverts. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): skip link stylesheet tracking after the buffer is torn down A consumer can stop recording from inside the mutation callback. The mutation buffer then registered load tracking for changed stylesheet links after the stop had reset it, and that tracking held the stopped buffer until the link loaded. The buffer now records its teardown in reset() and registers no tracking after it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): keep changed-link stylesheet tracking across full snapshots Review fixes for the changed-link inlining: - A full snapshot taken while a changed link's new sheet was loading aborted its tracking, and the snapshot inlined the previous href's sheet, which is still attached at that time. Changed-link tracking now survives the reset at a full snapshot. Stop, and a new record() call, still end it. - A tracker that ends deletes its map entry only when the entry is its own. - The stop-inside-emit guard is now a mirror check at registration, which also covers shadow-root buffers. Their teardown does not call reset(), so the earlier tornDown flag missed them. - A throwing consumer emit or mask callback in the changed-link listener is caught, as in the deferred path, because it runs from a load event or timer. - The link check compares the lowercased nodeName, for XHTML documents. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(replay): slice changed-link inlining and reuse the serializer Remaining code-review findings for the changed-link stylesheet path: - A changed link's sheet is stringified in idle slices through the existing deferred-inlining task (CSSOM invalidation included), not in one task. A full snapshot, a stop, or a block drops the work in flight; the full snapshot inlines the current sheet itself. whenIdle and the slice sizes move to record/idle.ts so the stylesheet manager can share them. - The attributes sent with the CSS come from serializeNodeWithId with inlineStylesheet off, instead of a second transform-and-mask loop. The mutation buffer builds its serialization options in one method. - The preference for link.sheet.href moves from transformAttribute into the snapshot serializer, where base-URL drift happens, so the mutation path needs no special case for a changed href. - Repeated sheet checks wait at least 1 s, so a 0 ms stylesheetLoadTimeout does not poll on every timer tick, and tracking ends after 60 s, so a hung request does not keep the link and its listener until the next full snapshot. - onceStylesheetLoaded reads link.sheet through readLinkSheet. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): check blocking before every delayed stylesheet emit The stylesheet manager emits a link's CSS after the link was serialized in three paths: a late load, budget deferral, and a rel/href change. A link or one of its ancestors can be blocked in between, and the late-load path checked only the link itself (through re-serialization), which the slow-sheet fix kept open for longer. The manager now gets blockClass and blockSelector and checks the link and its ancestors, across shadow hosts, before each of these emits. The shadow-crossing check moves from the mutation buffer into utils so that both share it. The changed-link path also keeps one in-flight task per link: a newer change discards the older task, so repeated rel/href changes on a link whose sheet is current do not stringify and emit the same sheet again. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(replay): assert changed-link inlining never emits masked values The replay incident-risk check asks for a real-browser test that sensitive values are absent from the emitted bytes. The changed-link path now sends the link's current attributes with its CSS, so the test records a preload link that becomes a stylesheet under maskAllElementAttributes and asserts that neither an attribute value nor the CSS text is in any emitted event. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): end delayed stylesheet work when its recorder stops A changed link's idle inlining task outlived stop(). A host that stops and restarts recording (for example with maskAllElementAttributes turned on) refills the shared mirror, so the old task's mirror check passed again. The task then serialized the link with the old recorder's masking options and emitted into the new recording. The stylesheet manager now has a stop(), which the recorder's stop handler calls after the deferred flush. A stopped manager schedules nothing, drops its pending changed-link tasks, ends load tracking, and emits no stylesheet mutation. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): send removed link attributes and detect new sheets after pushState Replay rebuilds a changed link from the attributes recorded for it before, so the _cssText mutation now sends null for each attribute in the previous serialization that the link no longer has. Before, a removed media came back. onceLinkStylesheetUpToDate also counts a new sheet object as up to date. After a pushState, link.href resolves against the new base URL, so a relative href never matched sheet.href and the check gave up. Each new change to the link moves the baseline sheet. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): restart changed-link inlining after a CSSOM change, free the baseline sheet A CSSOM change to a changed link's new sheet during idle slicing cancelled the task, and replay kept the previous sheet for good. The changed-link path now starts again from the changed sheet, at most 3 times, then counts a failure. The restarted text includes the change, and its _cssText arrives after the recorded rule event. The sheet attached at a link's latest change now lives in the tracking entry, not in a module WeakMap, so it is released when tracking ends, at a reset or stop, and at once when the sheet is already current. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): flush pending changed-link CSS at pagehide and stop Changed-link inlining queued its idle work apart from the budget-deferred queue, so a page that went away before the idle callback ran lost the new sheet's CSS. The idle step and a new synchronous flush now share one slice routine, and pagehide and stop run that flush after the deferred queue's. Both share the existing 50-slice teardown cap, so the bound on synchronous work at teardown is unchanged. A sheet that does not fit is dropped and counted as abandoned. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): send removed attributes when a late-loading stylesheet is inlined Replay rebuilds a link from the attributes recorded for it before when _cssText arrives, so an attribute removed between the link's serialization and its sheet's load came back. onStylesheetLoad now also gets the link's serialization from before the load, and the late-load mutation sends null for each attribute the link no longer has, as the changed-link path does. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): guard the teardown flush against reentry, cancel idle work of dropped jobs An emit during the pagehide or stop flush can stop the recorder, and that teardown flushed again with a fresh 50-slice budget, so the cap did not hold. The flush now returns at once when it is already running. Changed-link jobs now live in one map from link to job. Releasing a job also cancels its queued idle callback, and stop, the full-snapshot reset and a newer change to the same link drop pending jobs at once, so a frozen page does not keep a job and its CSS cursor alive. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit 0bf0316) * chore: update versions and lockfile [version bump] (cherry picked from commit 1a214aa) * feat(ai): add Firebase Genkit integration (#5381) * feat(ai): add Firebase Genkit integration Genkit traces itself with OpenTelemetry, but its spans use the genkit: attribute namespace. The @posthog/ai/otel processor only forwards gen_ai.*, llm.*, ai.* and traceloop.* spans, so no Genkit flow, model call or tool call reached PostHog. The new @posthog/ai/genkit entry point exports PostHogGenkitSpanProcessor. It reads Genkit spans as they end and captures them with the existing helpers. The root span becomes an $ai_trace, each model action becomes an $ai_generation, and other Genkit actions become $ai_span events. Closes #3842 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ai): address genkit review feedback - Replace credential fields such as config.apiKey with [REDACTED] in span input and output state. - Capture embedder actions as $ai_embedding events without the vectors. - Send the googleai and vertexai plugins as the gemini and vertex providers. - Map media parts to the shared image and inline data blocks. - Rebuild span errors as Error objects with the original stack, and take the class name from the stack, because OpenTelemetry records error.code as exception.type. - Format span errors with formatAiError, so privacy mode applies to them. - Document the npm ERESOLVE workaround. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ai): bound Genkit media parsing and preserve span identity --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Manoel Aranda Neto <[email protected]> (cherry picked from commit bac61c2) * chore: update versions and lockfile [version bump] (cherry picked from commit 90150ac) * fix(replay): replay a plain link when a changed link's new sheet cannot be inlined (#5512) * fix(replay): replay a plain link when a changed link's new sheet cannot be inlined When a <link rel=stylesheet> changes href to a sheet that is unreadable (cross-origin without CORS) or stringifies to nothing, no _cssText was sent. If replay already showed the link as a <style>, it kept the old sheet's CSS, because replay cannot turn a <style> back into a <link>. The recorder now sends a remove and an add of the same node, as for a moved node, with a serialization that has no _cssText, so replay builds a <link> that loads the new href. It does this only when replay shows the link as a <style>: its last serialization has _cssText, or this recorder sent _cssText for it after the last full snapshot. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): record the plain-link re-add through the mutation buffer The re-add went out directly, so a move of the link that a frozen buffer still held reached replay after it. Replay then failed to remove the old <style>, and its CSS stayed. The buffer now gets one childList record that removes the link and one that adds it back, as for a node taken out and put back in place, so the re-add stays in order with buffered mutations. The fallback also runs inside a try/catch: it can run from a load event or timer, where a throwing consumer emit or mask callback must not surface on the page. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(replay): take the observer's pending records before re-adding a link The re-add of a link needs its next sibling in the mirror. If the page inserted that sibling in the same task, for example just before stop(), the observer had not delivered its record yet, so the buffer recorded the remove but dropped the add, and replay lost the link. The buffer now processes the observer's pending records (takeRecords) before the two re-add records. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit bc6b54c) * chore: update versions and lockfile [version bump] (cherry picked from commit 3987245) * ci: remove watcher workflows and runner egress auditing (#5510) * ci: remove watcher workflows and runner egress auditing * test: remove obsolete runner egress auditing assertions (cherry picked from commit 9564f12) * feat(node): send a flat body from emails.send (#5487) emails.send now posts template_id and distinct_id at the top level of the request body. The server accepts both shapes, so this changes only the wire format. Co-Authored-By: Claude Opus 5.5 <[email protected]> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be (cherry picked from commit c019e96) * chore: update versions and lockfile [version bump] (cherry picked from commit fb0c941) * fix(ai): capture vercel ai sdk file parts passed as bytes (#5496) * fix(ai): capture vercel ai sdk file parts passed as bytes Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ai): keep vercel files typed, sized and streamed Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ai): share one file budget per vercel generation Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit 3611fd2) * chore: update versions and lockfile [version bump] (cherry picked from commit a90eff1) * fix(browser): sanitize survey description HTML (#5491) * fix(browser): sanitize survey description HTML * fix(surveys): preserve safe new-tab description links * fix(browser): defer explicit survey renderer loading * fix(surveys): block stylesheet and form description markup * fix(surveys): isolate HTML sanitizer configuration * fix(surveys): align HTML allowlists with the backend (cherry picked from commit aedb488) * fix(browser): enforce script loading policies before reuse (#5493) (cherry picked from commit 5e872e1) * chore: update versions and lockfile [version bump] (cherry picked from commit 2dc13a1) * ci: disable error-tracking harness workflow (#5519) (cherry picked from commit dc06863) * fix(browser): sanitize web experiment transforms (#5492) * fix(browser): sanitize web experiment transforms * fix(browser): preserve experiment transform parsing contexts * test(browser): normalize near-origin Firefox navigation timings * fix(browser): preserve template and MathML experiment content * fix(browser): preserve safe experiment link and form attributes * chore(browser): update mangled experiment property names * fix(browser): isolate web experiment sanitizer configuration * fix(browser): preserve MathML annotation parsing contexts (cherry picked from commit 84a5277) * fix(browser): detect lightpanda headless browser traffic (#5505) * fix(browser): detect lightpanda headless browser traffic * fix: document lightpanda detection in shared SDK releases * chore: limit Lightpanda changeset to affected packages --------- Co-authored-by: Manoel Aranda Neto <[email protected]> (cherry picked from commit 3fbb667) * fix(ai): keep vercel file ids and one placeholder per file (#5520) Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit af74347) * fix(surveys): respect the selected position for tab widget surveys (#5503) * fix(surveys): respect the selected position for tab widget surveys * fix(surveys): release tab popup position fix in posthog-js --------- Co-authored-by: Manoel Aranda Neto <[email protected]> (cherry picked from commit 89c3a6b) * chore: update versions and lockfile [version bump] (cherry picked from commit 678b10f) * fix(react-native): upload source maps from the SOURCEMAP_FILE directory in posthog-xcode.sh (#5525) Co-authored-by: Claude Sonnet 5.5 <[email protected]> (cherry picked from commit f3fe629) * chore: update versions and lockfile [version bump] (cherry picked from commit cbfc706) * ci: move native plugin builds to macOS 15 (#5528) (cherry picked from commit 57dae19) * feat(browser): support customer-pinned script integrity (#5494) * feat(browser): support customer-pinned script integrity * test(browser): pin integrity fixtures to an exact loader version * docs(browser): keep script integrity guidance in PR context * test(browser): pin integrity to the selected compatibility core * fix(browser): enforce bootstrap integrity and pin manifests to releases * test(browser): distinguish config requests from script fallbacks (cherry picked from commit b033b8c) * ci: guard PR updates when PostHog upgrade has no changes (#5529) (cherry picked from commit 611a754) * chore: update versions and lockfile [version bump] (cherry picked from commit 76ec5c9) * fix(ai): load OpenTelemetry integration in Cloudflare Workers (#5530) * fix(ai): load OpenTelemetry integration in Cloudflare Workers * fix(ai): bundle OTel batching and test supported bundlers * test(ai): check declarations with browser exports * chore(ai): remove standalone OpenTelemetry license file * ci(ai): omit consumer log artifact uploads (cherry picked from commit 9909fee) * chore: update versions and lockfile [version bump] (cherry picked from commit 2908aea) * fix(browser): only capture $opt_in when the user wasn't already opted in (#5504) * fix(browser): only capture $opt_in when the user wasn't already opted in opt_in_capturing() captured $opt_in on every call. Consent tools often re-apply stored consent on every page load, which sent one billed event per page. It now skips the event when consent was already granted; opting out and back in, or reset() then opting in, still captures it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(browser): update public API references for opt_in_capturing docs Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(browser): heal a stale cookieless distinct_id when opt_in_capturing() skips $opt_in When consent was granted in another tab, this tab still holds the cookieless sentinel. capture() used to heal it as a side effect of sending $opt_in; with the event now skipped for already-granted consent, sync the identity directly. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(browser): adopt the shared identity before re-enabling persistence on a repeat opt-in Re-enabling persistence saves this tab's in-memory state, which overwrote the identity the other tab had persisted and left a freshly minted id. Sync the cookieless identity first, while the shared one can still be loaded. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit 7e023a2) * chore: update versions and lockfile [version bump] (cherry picked from commit 8006915) * feat(next): tracing-header identity, request properties and a proxy wrapper for onRequestError (#5473) - onRequestError takes identity from the X-POSTHOG-* tracing headers, then the PostHog cookie, then none. It drops a cookie session that posthog-js would have ended: 30 minutes idle or 24 hours old. - It adds $current_url, $request_method, $request_path and $user_agent, without the query string. They keep the app's basePath, and a Pages Router data request is reported as its page. - withPostHogProxy captures what proxy.ts or middleware throws, which Next.js never reports on 16 or for Node.js middleware. It rethrows at once and hands the send to event.waitUntil for at most 2 seconds, like Sentry. It marks the error it handles, so that onRequestError skips Next.js's own report of it, and it captures each failing request once, also when many throw one error object. - An error that escapes middleware or a proxy is sent as unhandled, with mechanism type middleware and $exception_source nextjs.middleware or nextjs.proxy, as the exception metadata spec asks of a request boundary. Route, render and action errors keep posthog-node's manual capture metadata. - @posthog/core: the cookie parser returns the session's last activity and start timestamps. Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit 5c8f314) * chore: update versions and lockfile [version bump] (cherry picked from commit 48f16d8) * fix(flags): send flag exposures read before consent when the user opts in (#5481) * fix(flags): send flag exposures read before consent when the user opts in * fix(flags): keep pending flag exposures tied to the identity that read them * fix(flags): recheck the identity before each pending flag exposure * fix(flags): adopt a sibling cookie identity before the opt-in exposure drain (cherry picked from commit 5764ddd) * test(ai): assert the Genkit processor sends through captureAi on node 6 The synced Genkit tests spied on capture, but a posthog-node 6 client sends AI events through captureAi, which captureAiEvent picks for it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(next): type the error properties builder from posthog-node On capture-v1, @posthog/next uses npm posthog-node 5.x, which ships its own @posthog/core copy, while this package imports the workspace core. ErrorPropertiesBuilder has a private field, so the two copies did not match and the build failed. Take the type from posthog-node instead. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Robbie <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: releaser-posthog-js[bot] <249978072+releaser-posthog-js[bot]@users.noreply.github.com> Co-authored-by: Shohruh Toyirov <[email protected]> Co-authored-by: Manoel Aranda Neto <[email protected]> Co-authored-by: Manoel Aranda Neto <[email protected]> Co-authored-by: Harley Alexander <[email protected]> Co-authored-by: Bernat Torres <[email protected]> Co-authored-by: Hugues Pouillot <[email protected]> Co-authored-by: Majicmaj <[email protected]> Co-authored-by: Muhammad Abdullah Shah <[email protected]> Co-authored-by: alexcastgonz <[email protected]> Co-authored-by: Sean Deehan <[email protected]> Co-authored-by: Aleksander Błaszkiewicz <[email protected]> Co-authored-by: Gustavo H. Strassburger <[email protected]>
Problem
The transactional email send API now takes
template_idanddistinct_idat the top level of the body (PostHog/posthog#126287), the same namesemails.sendtakes. The nestedtemplate.id/identifiers.distinct_idshape stays accepted for 5.61.0, but new versions should send the documented shape.Changes
emails.sendposts{ template_id, to, message_data, distinct_id }.distinct_idis still only sent when the caller passesdistinctId.posthog-node.Note
Merge after PostHog/posthog#126287 is deployed, so a release never sends the flat body to a server that doesn't read it yet.
The sdk-specs contract update is PostHog/sdk-specs, branch
posthog/flatten-email-send-body.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
emails.spec.tschanged; with the old source they fail, with the new source all email tests pass (npx vitest run src/__tests__/emails.spec.tsfrompackages/node). oxfmt is clean.message_datakeeps its name, because the server's per-template route and this SDK already use it.🤖 Generated with Claude Code
Created with PostHog Desktop