Repository navigation
feat(migrate-email): move Resend sends in every PostHog SDK's language - #472
Merged
Merged
Conversation
The migrate group keeps Statsig's mapping and SDK reference under references/statsig/, and its step files point there, but the generator copied only the top-level reference files. Copy references/<variant-id>/ into each variant's bundle, and nothing of the other variants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the sender setup of send-transactional-email's STEP 3 into
context/shared/email-sender-setup.md, so the email migration skill
includes the same text. A partial now takes the indentation of its
{{> name}} line on every line, so it nests under a step. The built
send skill is unchanged byte for byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0 prepares a move off Resend and changes no send call site. It finds every Resend send, classifies each one as ready to move or staying on Resend with a reason, sets up the PostHog sender next to Resend's DNS records, imports Resend's suppression list when the import tool exists, hands over the project secret key, checks the sending tier, and writes posthog-migrate-email-resend-report.md with a summary block. Steps are provider-neutral; references/resend/ holds what is specific to Resend. Statsig and Resend now publish as wizard migrate subcommands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixture runs wrote reasons into the summary block, which the Wizard can't parse, and counted a forwarding helper as one site or as each caller. The report step now shows bare values, and a site is the place that decides a send's content and fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the sending skill The send-transactional-email skill is gone from this stack, so migrate-email bundles the transactional email pages and names the sending-transactional-emails skill for the general move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… PostHog's reply-to rules The preflight looked for two MCP tools that don't exist. It now looks for the make-transactional pair, and a missing pair records the API as not confirmed instead of stopping, because it can also mean a missing scope. The mapping now keeps a site on Resend for reply-to addresses with a name or more than 10, and lists every reserved header and the header and tag length limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migrate-email skill now rewrites every call site PostHog can send: hosted Resend templates become local templates, React and html/text sites send rendered content, and the client carries migratedFrom. It checks the build and one email on one person before it writes the report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d provider in tests Fixture runs looped over or truncated a one-recipient list, and one run mocked posthog-node to rebuild a Resend request. The rewrite now narrows the helper's parameter to one address, and the verify step asserts on the fields posthog.emails.send gets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rate-email A moved site now treats recipient_suppressed like a send that went out, a site-built subject becomes a module placeholder so the template hash stays stable, fallbacks use single quotes, and the report warns that idempotency keys don't cross providers. The rules and Step 7 no longer contradict the mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… uses sending cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-email posthog-node has no migratedFrom option. The Wizard records the migration on its setup wizard finished event instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-email rewrites a site A posthog-node release with posthog.emails can still lack templateKey, local template content, or the extra send fields. The rewrite now checks the installed types for what each moving site needs, and keeps a site on Resend when the latest release lacks it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…partial imports in migrate-email Step 1 now admits the SMTP host and a framework mailer and needs at least one send; step 3 stops when it finds no site. A suppression import that stops partway stays unticked and lands in the report's "Before cutover" list. A template site needs its content from the person or stays on Resend. The sender setup is inlined into step 4, which keeps the existing `from` domain and records an unverified domain instead of waiting. AGENTS.md says the migrate subcommands run once the wizard runs migrate as a family. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e loop The selected variant's `references/<variant-id>/` files now go through the same loop as the root reference files, so the separate copy helper is gone. The partial expander drops the indentation it gained for the sender partial, which nothing includes any more, and the bundle test drops the migrate-statsig case that guarded nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt every behaviour change Step 8 checks that the app sends to the MCP's project, notes the start of the test, and counts only events since then whose workflow matches the send. It compares typecheck errors with a baseline step 7 takes before it changes code. A template's content waits in a temporary file for step 7 instead of in memory, an HTML fallback keeps a template on Resend, and a derived templateKey is normalized and unique. The report lists each suppressed-recipient branch, copy-address refusal, and cross-provider idempotency risk, and reminds the person to delete the full-access Resend key. The posthog-node client setup is inlined into step 7, and description.md says this skill wins over the general moving guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tes from files Step 4 records a domain as not verified, with its records or Domain Connect link, when the person can't publish them during the run. Step 3 asks the person to save each template from the dashboard to a file outside the repository and reads it, since the Wizard can't take pasted HTML. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keep template files traceable Step 8 takes the start of the test from `execute-sql` and compares Unix timestamps, so the project's timezone can't shift the window. The plan lists each saved template file, step 7 reads the module from it, and step 9 deletes every listed file. A derived templateKey falls back to the template id when the name normalizes to nothing and stays unique across the run. Step 8 may fix other posthog-node code the upgrade breaks, and the report lists those files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndency upgrades The clock query carries a fresh random literal so a cached answer can't widen the test window. Step 3 may write the template files it saves, the dashboard copy includes the template's alias or name for its key, and a major dependency upgrade lands in the report's "Before cutover" list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…end, and move templates only to PostHog The first PostHog release sends a template made in PostHog by template id, or rendered html and text. A Resend template site moves only once the person makes the template in PostHog, or stays on Resend for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the inventory, mapping and report conflicts toward the scope cut: a Resend template site moves only to a template made in PostHog, sent by templateId, and headers, tags and attachments stay on Resend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the scope cut, a moved template site sends a template made in PostHog by templateId with templateData. Rendered sends keep templateKey, subject, html and text. No local-template module, no upload on first send, and no cc, bcc, headers, tags or attachments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-node's migratedFrom client option sends the provider a send moved from with every email request. Step 7 now sets it, and the Resend mapping names its value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… site moves Review round 1 on #451: - Step 3 asks for the template ID once the person made the template in PostHog. A template without one stays: PostHog template not made yet. - Step 7 checks that the client takes migratedFrom, and undoes the posthog-node install when no site is left to move. - Template files from an API read are deleted once their site is classified, and the full-access Resend key right after Step 5. - Step 8 reads only the token's line of the env file, and picks a signed-in flow for the test email. - replyTo moves as one address or a list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cutover Review round 2 on #451: - Step 3 asks for the address a PostHog template sends from and writes it in the site's from column, so Step 4 sets up a sender for its domain. - The report asks the person to send a test of each PostHog template and check that it is transactional, has a verified sender and fills its variables. - Step 5 asks to delete the full-access key on every way out of the step, and after a partial import only once the person won't repeat it. - Step 8 allows a signed-out flow: the person signs in as the recipient afterwards, so product events show on the same distinct ID. - migratedFrom is set only when the client options take it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mes a person Review round 3 on #451: - Step 5 asks once whether to repeat an import that stopped partway, and asks about the full-access key after that answer or the repeat. - Step 8's test email comes from a moved site that passes distinctId; without one, the chain is recorded as not checked. - The client example marks migratedFrom as conditional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
changed the base branch from
feat/migrate-email-resend-cutover
to
main
November 13, 2026 16:19
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Map each SendGrid list to PostHog's reason, name Global Unsubscribes for Step 3's question about sites that rely on the suppression list, and replace "Suppression writes" with the calls section Step 3 and Step 5 read, without the import's entry rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…ules - account.md maps each SuppressionReason to PostHog's reason: bounce, complaint, or manual. It says PostHog blocks a moved send only to a bounce entry, and names how an address gets onto the outbound list for Step 3's question on sites that rely on Postmark's list. - mapping.md drops which suppression-list calls count, which Step 5 no longer reads, points those calls at Step 3 too, and says PostHog refuses only a hard-bounced address with recipient_suppressed. - A send without a text body notes that PostHog makes the text part, and code that reads a header of Postmark's answer stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
- account.md maps each SparkPost source to PostHog's reason (bounce, complaint, manual), and says only a bounce blocks a moved send. - mapping.md feeds Step 3's new suppression question: the Manually Added and Compliance entries and the app's suppression writes, per account, with the reason `relies on SparkPost's suppression list`. - mapping.md drops the "which entries count" text Step 5 no longer reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…ter #472 BOUNCE imports as bounce and COMPLAINT as complaint. PostHog refuses a moved send only for a bounce entry, so the widening checks count only BOUNCE entries. Name where Amazon SES entries are added by hand for Step 3's question, and drop the entries-that-count text Step 5 no longer reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…t rules Map each Rejection Denylist entry to PostHog's reason, drop the "which adds count" text Step 5 no longer reads, and name the Rejection Denylist and its custom and unsubscribe entries for Step 3's new question. PostHog now makes a text part whenever a send has none, so an HTML send that Mailchimp Transactional sent with no text part stays, and the Sending Defaults' plain-text box joins the account questions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…472 Map Bounces, Complaints and Unsubscribes to PostHog's bounce, complaint and manual reasons, and leave out allowlists. Feed Step 3's suppression-list question with Mailgun's unsubscribes, which also stopped transactional sends. Drop the unsubscribes report count and the "which entries count" text that Step 5 no longer reads, and say that PostHog blocks a moved send only to an address that hard-bounced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…-email-track-opens-and-clicks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…y send_message fields from the installed gem A site that reads a header of SparkPost's answer, other than the retry delay, now stays, as Resend's does under #472's Step 7. A Retry-After read moves to the PostHog error's retry delay. A Ruby send_message call maps the fields the installed gem sent, where a keyword can override a positional argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…Step 3's list question Follow #472's shared steps: each entry on the provider's list maps to a PostHog reason (bounce, complaint, manual), and Step 3's question about sites that rely on the list covers only sites whose host's provider keeps one, naming that provider in the reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
#472's template check keeps a template the provider sent with no text part. A Brevo template has no text part of its own to turn off, and Brevo makes a plain text version of HTML emails itself, so the check doesn't keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
that referenced
this pull request
Nov 13, 2026
…og's retry delay A Loops retry loop that waited for Retry-After now reads the retry delay PostHog's error carries since #472, converted to the loop's unit, and keeps its own path only when PostHog named no wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
migrate-email-resendmoves a Resend send to PostHog only when the code is JavaScript or TypeScript. A Python, Go, Ruby, PHP, .NET, Rust, or Elixir backend gets a plan where every site stays on Resend, with the reason "until the skill moves that language". PostHog's SDKs in all eight languages send transactional email now, so those sites can move.Impact
No metric: PostHog's send API does not record the
X-PostHog-Migrated-Fromheader thatmigrated_fromsets, so no insight can count the sends this skill moves, per language or at all. Recording it on$workflows_email_sentwould let one insight count moved sends per provider and per organization.Caution
🔴 Merge risk: high · two-way door until the next context-mill release ships the skill; one-way once agents run it, because a migration's changes persist in each app's code and PostHog account (rewritten send code and email files, senders, imported suppressions, keys).
rewrite-node.md, plus the rules this PR adds for every language. A send of a Resend template moves by bringing the email into the code, where feat(skills): move Resend sends to posthog-node in migrate-email #451 moved it only to a template made in PostHog. A site whose emails had no open pixel moves only on the person's yes, and a template made in PostHog asks about Resend's click tracking. Webhook handlers are listed: a site whose handler matches events byemail_idor a tag stays, and the report names the PostHog events that replace the rest. A site with its own request deadline keeps it, with no retries, and one that told network failures apart stays. These rows are uncovered until the owed live run: a wrong rewrite changes what an email holds, adds an open pixel the person didn't agree to, sends an email twice past its deadline, or leaves a handler waiting for events that no longer come, without an error.migrate-email-resendbundle changes, and every other bundle is byte-identical.Rollout
Changes
rewrite-node.md,rewrite-python.md,rewrite-go.md,rewrite-ruby.md,rewrite-php.md,rewrite-dotnet.md,rewrite-rust.md, andrewrite-elixir.md. Each one gives the install, the client with the secret key andmigrated_from, the flag-loading setting for a send-only client, both send forms, the result and the error with its code and name, the branch for a suppressed recipient, and the check to run. The names come from the SDK release PRs and matchreferences/sdks.mdin the PostHog skill.rewrite-node.md.codechanges from an HTTP status to a string code, .NET'sGuidmessage ID becomes astring, Elixir's error name becomes an atom, and Resend's Elixir SDK takes no idempotency key.template-to-code.md, from the provider stack): its HTML and text become a file next to the send, rendered with React Email, the app's template engine, or the language's own way, and the send uses the code form. A template made in PostHog is used only when the person asks for one.reasonvalue. An app call that removes an address becomes an "After cutover" item, and the report says which entries don't block moved sends.Cargo.toml,mix.exs, and.csprojmanifests.posthog-node.How it was tested
f98eb3b:vitest run scripts, 342 tests in 30 files, andnode scripts/build.jspass.migrate-email-skill.test.jskeeps the tests that run the built skill: every reference path in the bundle resolves, the transactional email docs and every rewrite file are in the bundle, the step chain walks from1-detect.mdto9-report.mdthrough the built links, the env key check runs in bash on fourteen env files, each provider bundle holds no provider folder but its own, the Wizard menu lists each migrate command, and in each provider bundle every "Step N says under" reference, the entry's and the report's pointers to Step 5's "Re-import only" included, names a section that step has. The tests that matched the skill's own sentences are gone (QA swarm 2026-11-12).a090b5btof98eb3b: only themigrate-email-resendbundle,manifest.jsonandskill-menu.jsonchange; every other bundle is byte-identical. Saved in proof storage.resendpackage for Elixir). The QA swarm checked each SDK claim it raised against the SDK sources too.96e10a2, carried tof98eb3b:build.ymlruns only on PRs intomain, and this PR's base is feat(skills): move Resend sends to posthog-node in migrate-email #451's branch. The lane gate ran it locally.Review
Round 1 (Opus 5.5) found 2 major and 5 minor issues, and all of them are fixed in
c4ed38c:go mod tidyruns after the rewrite.Round 2 (GPT-6.1 Sol) found 1 issue, fixed in
a658c75: posthog-rs builds either the async client or the blocking one, so the Rust file uses the one the app's features build.The final read on
a658c75found nothing.6ab1cc2merges the migration lane's per-SDK rules, from feat(skills): move SendGrid sends to PostHog in every SDK language #459's shared SDK reference, so the rewrite files are the only place for them. Each language now covers its network error after retries and its bad-argument error, plus .NET's lack of a synchronous send. Checked against the current SDK PR heads inf6b6af8:cc,bcc, or attachments yet, so they stay on the provider.PostHog::initalone.Final reads on the merge found three Elixir problems, all fixed. Email gets its own named instance when the app already uses PostHog (
ef2c2e9). A missing host or token can no longer stop the app from starting (fbd1e83), and neither can one the app configures asnil(78d56ce). The final read on78d56cefound nothing.bb711c5: Step 7 lists the ten error names once, and each rewrite file points there instead of at a provider mapping. Each rewrite file also says whether reply-to takes one address or a list, checked on each SDK head. The final read onbb711c5found nothing.5398fd9: behind a reverse proxy, email gets its own client on PostHog's host, for every SDK, in place of the Go and Ruby special cases. A client with the secret key loads flag definitions from its host with it: Go and Ruby when they start, and Python and .NET when the app evaluates a flag (checked on the SDK heads). A final read on the stack found this.5398fd9has no final read of its own yet, because feat(migrate-email): move Resend sends in every PostHog SDK's language #472 is past its sixth.6924d48: a GPT-6.1 Sol final read on5398fd9(the 7th, on Michael's word) found the proxy test matched only the separate-client wording. The test now also asserts "leave the secret key off the app's existing client": red with that clause removed, green with it (250 tests).2ebcce0: a send of a template stored at Resend now moves by bringing the email into the code, on Michael's direction for every provider: its HTML and text become a file next to the send, the code renders the variables, and the send uses the code form. A template made in PostHog, sent by its ID, is used only when the person asks for one.template-to-code.mdcomes from the provider stack as is, and Steps 3, 7 and 9 carry the same change. Resend never escapes a value, so the shared escaping rule applies. A fallback covers a missing value only. Afrom, reply-to or subject that the site passes wins over the template's, as Resend does. Five new tests fail on6924d48and pass here (254 tests); the build passes.6b41f33: the shared step changes from #459 move here, so each provider PR adds only its own provider. The steps no longer assume Node.js, keep each moved site failing the way it did, and list the app's webhook handlers and suppression writes for the report. feat(skills): move SendGrid sends to PostHog in every SDK language #459's two Resend shadow runs with these steps classified every site the same.QA swarm 2026-11-12, on
82d6ebfwith feat(skills): move Resend sends to posthog-node in migrate-email #451's fixes merged in (22df812):e36fd64and08c9d15. .NET gets aPostHogClientof its own for email, sinceAddPostHog()registersIPostHogEmailsfrom the app's client. In Go and .NET, a deadline longer than the SDK's request timeout needs a client with that timeout. The Rust blocking client isn't awaited. Elixir always uses a named instance. A send in the code form is sensitive in PostHog and counts no clicks, so a site whose clicks feed a handler moves only to a non-sensitive PostHog template. Temporary template copies are deleted on every way out. A template the app changes at Resend stays. A Go link value with another scheme, such astel:, stays. The tests that matched the skill's wording, and anit.eachthat ran no case, are deleted. Rejected: a missing token crashing Node.js and Ruby (both SDKs disable the client instead), the React Email rebuild (an app on React Email edits its emails as components), and prod shape (PostHog stores no person's code), now recorded as a gap.bb14da9. A blank .NET host falls back instead of throwing, a .NET deadline over 100 seconds stays (itsHttpClientdefault), a shared template's copy waits for its last site, a stored message ID points to the suppressed-send rule, and an error handler that also covers a call staying on Resend keeps Resend's handler. ReviewHog round 2 onbb14da9: a Go template that fills CSS from a value, such asrgb(255, 0, 0), getsZgotmplZfromhtml/template(checked in Go 1.23), so such a site stays; fixed ineeeb620. ReviewHog round 3 oneeeb620: the report's "Before cutover" check now names a template brought into the code, and the Python email client setsenable_local_evaluation=False, so a worker forked under a preloading server doesn't poll flags with the email key; fixed in64bb2a2. A final read on64bb2a2found thatAsyncPosthoghas no such option and raisesTypeErroron it; fixed in8ead8e1. Final reads onbb14da9,eeeb620and8ead8e1found nothing.ECONNRESETorrequests.Timeout, keeps its site on the provider, since every SDK reports onenetwork_error; a kept deadline's own timeout still matches in Go and .NET. A limit on connecting alone, or apart from reading, keeps its site too, except in Go, whose email client takes the request's transport. Fixed in0158aef,f6978e7andad23a92, with Resend's Node.jsapplication_errorfor a network failure now also takingnetwork_error.0158aef: posthog-php sends email through curl, which Composer doesn't ask for, so the app requiresext-curland the report lists enabling it where that isn't shown; fixed inf6978e7. A final read onf6978e7found that Go keeps transport limits; fixed inad23a92. ReviewHog round 5 onad23a92: posthog-python hands its timeout torequestsorhttpxas one number, which limits each network wait, so an aiohttp total timeout keeps its site on the provider; fixed in7ccc0ee. The reads onad23a92and7ccc0ee, run under the standing review rule past Fleet's cap, found nothing.eef1481: a site whose provider didn't track its clicks, or where nobody knows, moves to a PostHog template only when the person marks it sensitive, since PostHog sends a non-sensitive template's links through its click redirect; Resend's account file says where its per-domain click tracking shows. A library's default timeout, such ashttpx's orky's, is not the site's deadline. Every SDK's timeout limits the whole request except Python's and Ruby's, which limit each network wait, and a limit of another kind keeps its site on the provider, such as axios's idletimeoutandky's, which ends once the headers arrive. Resend's sensitive template sends its links without the click redirect. The message ID is a UUID, so .NET code that keeps aGuidtakesGuid.Parse. A final read oneef1481found that Faraday'stimeoutlimits each wait only on itsnet_httpadapter; fixed inefb8451. ReviewHog round 6 onefb8451: a handler that acts on a Resend event with no PostHog event, such asemail.delivery_delayed, keeps its sites on Resend; fixed inf658354. Final reads onefb8451andf658354, under the standing review rule, found nothing.ced460c: a test that mocked the provider's client runs the real PostHog SDK and intercepts PostHog's send request, asserting on that request and on what the app does. Node.js, Python and Ruby intercept in process, withmsw,nock14 or later, undici'sMockAgentor a fake globalfetch,responses,requests-mock,respxorpytest-httpx, andwebmock. Go, .NET, PHP, Rust and Elixir can't swap the SDK's HTTP client from a test, so the test runs a local server and points the email client's host and app host at it, through an optional app host variable; Go and .NET allow anhttpapp host only for a loopback address (d51979c). No test tool is added as a dev dependency without asking the person once, in any language. Node.js, Go, .NET and PHP need none; in Python, Ruby, Rust and Elixir, on a no, the test runs a local server from the standard library, such as Python'shttp.serveror Ruby'sTCPServer, and no test keeps a mock of the send (6f7abcb,8776c81,63c3dfe). An earlier version kept the mock on a no; final read 12 found it, and the judge on96e10a2failed its rejection. Also relayed from the provider lanes: a Ruby request that sets only some ofNet::HTTP's timeouts, or sets them apart, stays, since posthog-ruby sets all three to one number (e14c1fc); Step 3 writesmust be marked sensitiveinto a template's plan note, so the report checks it for every provider (a943460); and a branch on the nameservice_unavailableskipsnetwork_error, which every PostHog SDK also names so, so a network failure takes the path the old code gave it (45c5716). A final read ona943460and ReviewHog round 7 found thehttpapp host point; a final read ond51979c, under the standing review rule, found nothing.21e1d27). Step 5 gains "Re-import only", which runs the import alone with no plan file, and the skill entry points to it, so the person re-runs only the import. It first asks the person whether production sends from the MCP's project, with a differing local token as a hint, since the local env file may name a development project; on a no it offers to switch the MCP to the production project and switches back when the step ends, and it stops when they can't tell or the project isn't in the organization's list. With no import tool it tells the person where to add the new addresses by hand, naming the production project, and says so instead of a count, as it does when it couldn't get the list (a65bb90,b28e13b,b2dfe53,731b092,8929f93,e5735c8,8b9757e,349a407). Steps 5, 7 and 8 now read the project token withproject-get, the one MCP tool that returns it;projects-getlists projects without their tokens (731b092). When any site stays, the report asks for a weekly re-import while a site sends through the provider, gives the prompt, says a repeat is safe, and asks for a final import after the last site leaves. Weekly, not the release cadence, since the sites that stay add entries between releases. A provider'saccount.mdonly says where its list comes from.empty list: <provider name> keeps no suppression listbefore it checks the import tool, and the report leaves the ask out (d8dd2d4,94d44d3), relayed from the SMTP lane.2b8d0d6,5f5debd), relayed from the SMTP lane's judge.17c6ca1): PostHog adds its open pixel to every email it sends, so Step 3 asks the person once, for all sites whose emails had none, whether PostHog may add it. A yes moves them, with a plan note the report names; a no or no answer keeps them asopen tracking off, and a site nobody can tell about stays asopen tracking not checked. Step 7 points there, and Resend's account file says where its per-domain open tracking shows. A new bundle test fails when a "Step N says under" reference names a section the step lacks (red with the new heading renamed). A final read and ReviewHog round 8 on17c6ca1, under the standing review rule, found nothing.96e10a2merges feat(skills): move Resend sends to posthog-node in migrate-email #451'scff58f5, which brings this PR's network-failure rule for Resend down to feat(skills): move Resend sends to posthog-node in migrate-email #451; this tree already had it, so the merge changes no file.090ac92merges feat(skills): move Resend sends to posthog-node in migrate-email #451's2df1f26, which namesproject-getfor the project token in Steps 7 and 8; this tree already had it, so the merge changes no file.cb26a5fmerges feat(skills): move Resend sends to posthog-node in migrate-email #451'seeed7cb, which lists Resend webhook handlers and keeps the sites they act on, and states the SDK's limit for sites without a deadline. This tree already had both in its own words, so the merge keeps feat(migrate-email): move Resend sends in every PostHog SDK's language #472's text there, and carries feat(skills): move Resend sends to posthog-node in migrate-email #451's Step 8 check of each error in a rewritten line against its old version.e7a7f3fmerges feat(skills): move Resend sends to posthog-node in migrate-email #451's77b680b, which gives Step 8 one rule for which errors are new, as final read 30 asked; feat(migrate-email): move Resend sends in every PostHog SDK's language #472's own text takes the same rule.6c45e23merges feat(skills): move Resend sends to posthog-node in migrate-email #451's2fe8430, keeping feat(migrate-email): move Resend sends in every PostHog SDK's language #472's text, and4087e6ccarries feat(skills): move Resend sends to posthog-node in migrate-email #451's newer fixes in this tree's form, one rule for every SDK: Step 7 asks the person once whether a template saved in PostHog already has any of the moved sends' keys and gives such sends another key, since such a send takes that template's settings and fails when it isn't transactional (the ReviewHog P1 on7-rewrite.md, where a code send was called sensitive whatever its key); a send with rendered content that carries a login code, a reset link or another secret sets the SDK's sensitive flag; and before an SDK install or upgrade, Step 7 checks the language version the app runs against what the release supports, and keeps that language's sites on the provider when it doesn't. Step 8 counts a listed error against the list's number less the copies the changed lines no longer have, and its rule covers warnings (final read 31). Final read 32 on4087e6cfound that no rewrite file named the sensitive flag; each SDK's latest release takes it, checked in its package, and each rewrite file's "The send" now names it (808f61d), with the moved template keys in the report and an edge runtime's check. Final read 33 moved the keys to "After cutover" and gave an unnamed edge runtime its own reason (fd58f93). ReviewHog on808f61dandfd58f93found nothing. Final reads 34 to 36 scoped the runtime check to the dependency manifest the SDK goes into (f1ba031,a0b01bc,8f51a57,6b0083e): when the release doesn't support a server whose build installs that manifest, directly or through a workspace or a root lockfile install, every moving site on it stays, an edge site too, since the install would reach that server; only otherwise does an edge site get its own check. The report's key note names the click redirect and the subject on engagement events too.6b0083e: a request with its own deadline whose HTTP client also retried it would lose those retries on the move. Such a site now stays (ef71e1f), counted only when the retry setup covers a POST, as final read 39 asked (6fdfdf7), with the setups named as examples (6d4801f). The agent reads the setup's options, sinceaxios-retryand urllib3'sRetryretry a POST only at their defaults (caff403), and arequestsadapter with a number as itsmax_retriescounts too (7aad014), from the gate judge and final read 42. Final reads 40 to 43 and ReviewHog rounds 14 to 16 found nothing.66dc2b2merges feat(skills): move Resend sends to posthog-node in migrate-email #451'sa090b5b, which is2fe8430plusmain's omnibus and commandments files, so the merge brings only those. ReviewHog ona090b5bfound that a site reading Resend's response headers, such asretry-after, moved although PostHog's answer carries no headers. Such a site now stays, unless it reads only the retry delay of a refused send, which moves to the PostHog error's (6d3d8b0). Each rewrite file names its SDK's retry-delay field and unit under "The answer", checked against each SDK's latest release, and a site that keeps its own deadline keeps its own wait and retry (610e703, from final read 44). ReviewHog on610e703: a Resend template whosetextis an empty string sends no text part, which PostHog can't do, since it makes one from the HTML; such a template, and a code send withtext: ""besidehtml, now stay (d186900). Final read 46 widened it toreactand to atextthe code builds that can be empty, and the shared template rule now keeps any template whose provider sent its emails with no text part (088f35b). Final read 47: that rule now sits in the part Step 3 reads, and an emptyTextin Go moves, since Resend's Go SDK leaves it out of the request (3b204b7). Final read 48: a dashboard copy can't tell an empty plain text from a missing one, so the run asks, or offers the full-access key to read that template from the API, keeps the site when neither settles it, and every template's note records its plain text, so Step 7 never asks again (f3bb668,355df95,74040e5).74040e5: Step 5's rule for an app's calls that change Resend's suppression list keyed on reason text PostHog never stores. PostHog writes its own reason, such asImported (bounced), takes onlybounce,complaintormanual, keeps no source an app could check, and blocks a transactional send only to an address that hard-bounced. So the app no longer syncs these calls (b99cea5to4db6763, over final reads 52 to 59 and ReviewHog on9eb4180). The import passes thereasonvalue. Step 3 asks once which sites rely on Resend's list for addresses added by hand or by the app, before any template work, and keeps them, also on a non-answer. A call that removes an address becomes an "After cutover" item for when a moved send fails withrecipient_suppressed. The report says that an address that complained or was added by hand gets the moved sends, so the weekly re-import limits mail only to bounced addresses.4db6763: posthog-rs's async email send waits between retries withtokio::time::sleep, so a Rust site whose async code runs on another runtime, such as async-std or smol, or on a Tokio runtime built without timers, stays (0a6ae00,f98eb3b).Decisions
2ebcce0PostHogClient, Elixir a named instancea090b5band final read 44, 2026-11-1374040e5and final reads 52 to 58, 2026-11-13textis an empty stringreactor when the code builds atextthat can be empty: it sends no text part, and PostHog makes one from the HTML; a send withouttextmoves610e703, 2026-11-13project-get, called without an ID, the one tool that returnsapi_token; replacesprojects-get, which lists projects without it96e10a2, 2026-11-12open tracking off, unknown tracking keeps them asopen tracking not checkedtimeout,ky's, and a whole-request limit in Python or RubyNet::HTTPtimeouts4db6763, 2026-11-13enginesInvolvement: no grilling rounds, no prototypes. QA swarm 2026-11-12: one two-family panel with five team lenses, one round on the fixes, and sixty-one final reads, the two-family ones with their points fixed or declined with a reason; forty-six ReviewHog reviews, every finding fixed and the last two clean; relayed findings from six provider lanes settled. Proof captured: shadow diff; live run, CI and prod shape owed as gaps.
Merge
Stacked on #451. It ships with the eight SDK releases, like #450 and #451, and merges after them. The provider PRs, #459 to #468, each stand on this one alone and land in any order after it. Retarget them to
mainbefore this merges: deleting this branch on merge would close them.🤖 Generated with Claude Code