Repository navigation
Conversation
Add the mandrill variant of migrate-email: how Mailchimp Transactional (formerly Mandrill) looks in code, how each send field, template, error style, and webhook maps to PostHog, and its DNS records, templates, and rejection denylist. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nsactional - headers, tags and metadata always stay: no PostHog SDK takes them yet - if/unless/IF convert only for booleans, IF:NAME=value only for strings, each only for lists - tracking switches next to a stored template keep the site - Go and Elixir decode base64 attachments; quote special from names - catches stay narrow per language, and network_error is raised again where the old catch never saw network errors - auto_text moves: PostHog makes the text part from html - drop claims about PostHog that the shared files don't make Co-Authored-By: Claude Opus 5.5 <[email protected]>
…drill # Conflicts: # context/skills/migrate-email/config.yaml # scripts/lib/tests/migrate-email-skill.test.js
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…actional
- A branch on ValidationError, by name or subclass, or on an invalid
recipient alone, becomes a branch on PostHog's validation_error name,
after recipient_suppressed. Other status checks such as 429 stay. The
Node.js throw carries name too.
- A bare {{name}} inside {{#each}} reads the item: it becomes
{{ item.name }}. {{this}} becomes {{ item }}.
- The client section says the package also serves templates, rejects,
webhooks, SMS and other APIs, and that Anymail, Swoosh and Bamboo
serve other providers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Test against a wizard branch:
Add Results will be posted here when complete. |
…ctional - A site that handles hard-bounce, spam and custom rejections apart stays: PostHog answers all three as recipient_suppressed. - Only the main account's denylist entries are imported. Subaccount entries apply only to subaccount sends, which stay. - A template site that also sets html or text stays: Mailchimp sends the site's html when the template has none. - A field the site doesn't set takes the account's Sending Defaults (tracking, inline CSS, HTML generation). account.md asks for them, and tracking off becomes captureEngagement: false. - PHP's network error follows the Guzzle version: answered in Guzzle 6, thrown in Guzzle 7. - Sites on the older PHP mandrill/mandrill client stay: its source is no longer published. - A site that went on for an invalid recipient but fails on any error is noted: a malformed address now fails. - Merge var names that differ only in case are one variable. - The mandrill npm package sits with the community clients. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ewrite throws The Node rule rethrew PostHog's error with only its status, and a network failure has none, so the old catch lost the code that says the request never reached PostHog. Carry error.code as well. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…de showed
- template_content entries with no name, such as [{}], fill no region:
the site moves like one with [].
- A catch of every error around the send stays as it is and raises no
network_error again: it caught network errors before too.
- A copy of the older PHP client kept in the app's own code, even under
other class names, stays like the package.
- The metadata row names metadata, not tags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
… rules to Mandrill, list rejects.add as a suppression writer Co-Authored-By: Claude Opus 5.5 <[email protected]>
…dress or an unchecked curl failure, and mirror reject removals Co-Authored-By: Claude Opus 5.5 <[email protected]>
…l check Co-Authored-By: Claude Opus 5.5 <[email protected]>
…n invalid-recipient branch becomes an address check Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t branch into the address check Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…l-mandrill Brings #472's rewrite-<language>.md files through the layer below. posthog-sdks.md is gone; this provider's mapping moves to the rewrite file in the next commit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mapping now names the rewrite file for the site's language in place of the deleted posthog-sdks.md. cc and bcc entries, bcc_address, attachments, and images now stay on Mailchimp Transactional, because no PostHog SDK takes them yet. The rewrite bullets that converted them, and the template rule that listed them, are gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ndrill Brings #472's Elixir fix (fall back past a nil :posthog host) and each lower provider's move to the rewrite file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's single list of error names in Step 7 and the reply-to rule in each rewrite file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's Step 7 rule: behind a reverse proxy, email gets its own client on PostHog's host. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's added assertion in the Step 7 proxy test. No skill text changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…at/migrate-email-mandrill
… code A send-template site now moves by bringing the published template into the code: the code renders its merge tags in either merge language, fills its mc:edit regions from template_content, and sends the rendered subject, html, text, and from, with the site's own subject and sender winning as Mailchimp Transactional does. A template made in PostHog is used only when the person asks for one. - A messages.send site with merge tags in its own content moves: the code renders the values before the send. - template_content, raw values, comparisons, each over objects, @index and ../, with, upper/lower/url/striptags/date, CURRENT_YEAR, DATE, EMAIL, and send-mc-template sites now move. - Unsubscribe tags, title case, partials, and tags the docs don't describe still keep a site on Mailchimp Transactional. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…email-mandrill # Conflicts: # scripts/lib/tests/migrate-email-skill.test.js
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
A person who sends with Mailchimp Transactional, formerly Mandrill, and asks to move those sends to PostHog gets no help:
migrate-emailhas no Mailchimp Transactional variant.With this PR,
migrate-email-mandrillmoves Mailchimp Transactional sends to PostHog's email send in each language where PostHog has one, whether the app uses the official SDKs, the oldermandrillclients, a community client or the HTTP API, andwizard migrate mandrillruns it.Important
Stacked on #466. Merge that first. This PR's own changes:
bba2c48...e1a4d44.It adds one provider folder, one config variant, one
AGENTS.mdrow and the provider in the bundle test. The shared steps it uses, and their review fixes, come from #459.Like #459, it waits for the PostHog SDK releases with the code-form email send. Nothing merges until code sends are live and usable for customers: Michael's rule for #451 and every PR stacked on it.
Impact
Sends that arrive with
migratedFrom: 'mandrill'show whether people use this skill to move off Mailchimp Transactional. Read it 14 days after the Wizard runsmigrateas a family.Merge risk
Warning
🟡 Merge risk: medium · two-way door
wizard migrate mandrill, so reach is few. Then the skill edits their send code, and a wrong rewrite harms silently once they deploy: a rejected or malformed recipient that used to pass quietly now fails the send, open or click capture changes, or a denylisted address gets mail. Each moved site keeps its error behavior, a malformed address is checked before the send where the site went on, a site stays whenever the account's Sending Defaults can't be checked, and the report's "Before cutover" list names what the person checks first.git revertthe merge commit. It removes the Mailchimp Transactional variant.Summary
references/mandrill/holds the three files each variant has:sdk-reference.md: the official packages in Node.js, Python, Ruby and PHP, the oldermandrillclients, community clients in Go, .NET and Elixir, the HTTP API, SMTP and framework mailers. It covers how each reports an error: the Node.js and PHP SDKs answer the error in place of the list, the others raise, and a PHP connection error follows the Guzzle version incomposer.lock(answered with Guzzle 6, thrown with Guzzle 7). Every SDK answers a refused recipient asrejectedorinvalidin a successful answer. It also covers the send fields, stored templates and merge languages, status reads, signed webhooks, and the APIs that are not sends.mapping.md:toentry or none, headers other thanReply-To,tags,metadata,subaccount,send_at,ip_pool, custom tracking and signing domains,inline_css,important, merge tags in inline content,send-mc-template,send-raw, SMTP and framework mailers. Sites withccorbccentries,bcc_address,attachmentsorimagesstay too, because no PostHog SDK takes them yet. A send through the older PHP clientmandrill/mandrill, or a copy of it kept in the app's code under other names, stays: its source is no longer published, so how it failed can't be read.account tracking not checked.captureEngagement: false; one of each stays, sincecaptureEngagement: falseturns off both. A template site with either off stays.IFandif, string compares,eachover lists, with a bare name inside the loop reading the item. Raw-HTML tags, atemplate_contententry (anmc:editregion),htmlortextnext to the template, other merge tags and helpers, and tests on values that are not always booleans, strings or lists keep it on Mailchimp Transactional. An emptytemplate_content,[]or[{}], moves. Merge var names that differ only in case are one variable.rejectedrecipient becomesrecipient_suppressed, handled the way the site handled it, and a branch on onereject_reasonmoves only when the site treatshard-bounce,spamandcustomthe same. A branch onValidationErrorreadsvalidation_error, afterrecipient_suppressed; a branch on aninvalidrecipient alone, or a site that went on for one, becomes a check of the address before the send, sincevalidation_erroralso covers other invalid sends. Python and Ruby raisenetwork_erroragain, which the old catch never saw.$workflows_email_*event. A handler that matches events by the send's_id, or a site that setsmetadata, keeps its sites on Mailchimp Transactional.rejects.addkeeps its Mailchimp Transactional write and also writes PostHog's list, andrejects.deleteremoves from PostHog's list where feat(skills): move SendGrid sends to PostHog in every SDK language #459's Step 5 allows it, until the last main-account send moves.account.md: Mailchimp Transactional's DKIM, verification and DMARC records next to PostHog's, a read-only key limited totemplates/info,templates/listandrejects/list, the Sending Defaults to ask for, and a template's published version. The suppression import takes the main account's unexpired hard bounces, spam complaints and custom entries from the Rejection Denylist. It leaves out subaccount entries (their sends stay), soft bounces, expired entries and unsubscribes, with the reason why.The mapping names PostHog's fields, client and errors through the rewrite file for the site's language. Those files come from #472, which the stack now sits on under #459: one
references/rewrite-<language>.mdper PostHog SDK (Node.js, Python, Go, Ruby, PHP, .NET, Rust and Elixir), and Step 7's single list of what every SDK's send takes. Steps 7 and 8 run each rewrite file's check on the whole project, in place of a typecheck per language. #459 deletesposthog-sdks.md, and the plan'slangcolumn is nowlanguage.Evidence
Lane gate on
e1a4d44:bunx pnpm@10 testpasses 261 tests in 29 files.bunx pnpm@10 run buildbuildsmigrate-email-mandrill.zip, andskill-menu.jsoncliEntrieslistsmandrillundermigrate, next to the other providers andstatsig.CI: green, 7 checks.
Proof gate:
fleet proof gateate1a4d44:gate: pass (kinds: live-run, prod-shape; review: carried from f8bc637; CI: green; judge: pass by gpt-6.1-sol).Final read at
f8bc637: GPT-6.1 Sol read this round's change,7813bb2and the forward merges below it: clean.Test seams (
migrate-email-skill.test.js): the Mailchimp Transactional bundle resolves every reference, holds its three files and the eight rewrite files, noposthog-sdks.md, and no other provider's folder; itsmapping.mdnamesmigratedFrom: 'mandrill'; it is amigratesubcommand.Reused at
e1a4d44. This head adds only clean merges of #472, at 5398fd9 and 6924d48 (one test assertion), which leave this PR's own diff unchanged. 5398fd9 changes one Step 7 rule: behind a reverse proxy, email gets its own client on PostHog's host. Every fixture's PostHog host is PostHog's own, so the rule can't change a site, and the dry run and final read atf8bc637stand. The lane gate above ran at this head.Dry run at
f8bc637(live-run proof).f8bc637adds7813bb2, which points the Mailchimp Transactional mapping at the rewrite file for the site's language in place ofposthog-sdks.md(8 references). It turns the rows fortoentries of typeccandbcc,bcc_address,attachmentsandimagesinto stay rows ("Always. PostHog's send doesn't take it yet"), so accno longer moves withpreserve_recipients: true. The template rule now names only tracking that was off. It drops theccandbccconversion and the attachment and image conversion bullet. The row for a language without a PostHog SDK that sends email now names Java or Kotlin and points at Step 7's list. The forward merge below it brings feat(migrate-email): move Resend sends in every PostHog SDK's language #472's rewrite files and Step 7's list of what every SDK's send takes. The same headless dry run, on the same fixture, now runs with answers that no longer say an SDK takescc,bccor attachments. The expected outcome was written before the run:receipt.tsstays on cc and attachments, and every other site lands as in the last run. Result: 11 found, 4 moved, 7 left, as expected.receipt.tsstays with the reason "ccrecipient andattachments". The plan uses thelanguagecolumn, the run never namesposthog-sdks.md, and the fixture was unchanged.Dry run on
5811e0e. Claude Code on Sonnet 5.5 ran the built skill as a dry run, headless, with only read tools, on a fixture app with eleven Mailchimp Transactional sends in TypeScript, Python and Ruby, plus amessages.infostatus read and a webhook route. The person's answers were in the prompt (Sending Defaults with open and click tracking on); the expected outcomes were written before each run. The fixture was unchanged after the run.welcome.ts: throws a non-list answer, went on for a refused recipientrecipient_suppressed, and throws an error withstatusCode,nameandcode. The To line loses the namereceipt.ts:ccwithpreserve_recipients: trueand a PDF attachmentccand the base64 attachment. Logsdata.idwhere it logged_id(stays fromf8bc637: cc and attachments)invoice.ts:sendTemplatewith Handlebars,paidanditemsbuilt by its callertemplateIdandtemplateData.if/elseandeachlisted as converteddigest.ts: severaltoentriestrial.ts:send_atsend_atpassword-changed.ts:tagstagspassword_reset.py:ApiClientError, then arejectedorinvalidcheckexcept EmailError: raisesnetwork_erroragain, returnsFalseonrecipient_suppressedand every other error, as beforeweekly_digest.py: oldermandrillclient withmetadatametadatausage_alert.rb:rescue MailchimpTransactional::ApiErrorPostHog::EmailError, raisesnetwork_erroragain, returns onrecipient_suppressed, and raises the rest as beforeteam_report.rb:bccbccaccount_update.rb:send_templatewith atemplate_contententrymc:editregionThe status read stays and the report says it can't find moved sends. The webhook route matches by address, so it keeps no site:
hard_bounceandspammap to$workflows_email_bouncedand$workflows_email_blocked, and the report names the webhook destination. Summary block:from: mandrill,sites_found: 11,sites_rewritten: 5,sites_left: 6, as expected.One earlier run missed a rule, by variance. Of the two runs at
0d08508, one left out the address check onwelcome.ts; the other, and every run since, applies it to both sites that went on for aninvalidrecipient.Prod-shape. A survey of real codebases' Mailchimp Transactional send shapes (saved at
733e0e3, reused: the commits after it are review fixes, forward merges and feat(migrate-email): move Resend sends in every PostHog SDK's language #472's rewrite files, which the dry run covers). It added[{}]as an emptytemplate_content, the rule that a catch of every error stays as it is, the copy of the older PHP client kept in an app's code, and fixture sites for the commonest reasons a real site stays:tags,metadata, atemplate_contententry and the older Python client.Gaps:
mandrill-apicallbacks or community clients.ValidationError, on onereject_reason, or a429retry; subaccount sends;rejects.addandrejects.deletecalls; the Guzzle 6 and 7 split;[{}]; merge var names that differ in case; the Mailchimp merge language (the fixture's template is Handlebars); the suppression import. A fixture for these is a follow-up.Decisions
rejectedrecipient_suppressed, handled the way the site handled the rejected recipientinvalidinvalidtovalidation_error, which also covers other invalid sendsreject_reasonhard-bounce,spamandcustomthe same, since PostHog answers all three asrecipient_suppressedValidationErrorbecomes a check of PostHog'svalidation_error, a status such as429a check of PostHog's status code, and other error names keep the site, never a wider catchnetwork_erroragain, which their old catch never saw; PHP follows the Guzzle version; a catch of every error stays as it isaccount tracking not checkedmandrill/mandrillccf8bc637, as the cc, bcc and attachments row says. Before, it moved only withpreserve_recipients: true, since otherwise the account's default decided what recipients sawtagsormetadataInvolvement: qa-swarm, 2 panel rounds (round 1: fresh-eyes, correctness, risk, design, anti-overengineering, sweep; round 2: correctness, fresh-eyes, risk, sweep, anti-overengineering) and 17 final reads, the last at
f8bc637, clean. Proof captured: 18 dry runs over the review, the last atf8bc637, and a prod-shape survey.🤖 Agent context
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5, high), as the migration-skills slice of Silthus/posthog#461. Reviews on GPT-6.1 Sol (high), the fresh-eyes lens on Opus 5.5; fixes on Opus 5.5 (high). Dry runs on Claude Sonnet 5.5 (claude-sonnet-5-5).🤖 Generated with Claude Code