Skip to content

feat(skills): move Mailgun sends to PostHog - #461

Draft
Silthus wants to merge 357 commits into
mainfrom
feat/migrate-email-mailgun
Draft

Silthus wants to merge 357 commits into
mainfrom
feat/migrate-email-mailgun

Conversation

@Silthus

@Silthus Silthus commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Parked until after 2026-11-25. Michael chose SendGrid as the first migrate-email provider on 2026-11-14; this PR stays a draft and resumes after that date, with the slim pass from research/2026-11-14-review-bloat-retro before its next review round.

Problem

A person who sends with Mailgun and asks to move those sends to PostHog gets no help: migrate-email covers Resend only.

With this PR, migrate-email-mailgun moves Mailgun sends to PostHog's email send in each language where both Mailgun and PostHog have an SDK, or where the app posts to Mailgun's HTTP API, and wizard migrate mailgun runs it. Each call site either moves with the same failure behavior, rendering, and tracking, or stays on Mailgun with the reason the report names.

Important

Stacked on #472, the core migrate-email skill. Its base stays feat/migrate-email-all-sdks until #472 merges; merge that first. Each provider PR adds its own provider and lands in any order. The lists they all touch, config.yaml, AGENTS.md and the skill test, conflict in one line each as they land.
It adds one provider folder, one config variant, one AGENTS.md row and the provider in the bundle test. The shared steps it uses come from #472.
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: 'mailgun' show whether people use this skill to move off Mailgun. Read it 14 days after the Wizard runs migrate as a family.

Merge risk

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 (senders, imported suppressions, keys, moved sends)
Tier: critical

  • New entry point, reach few: a person who runs wizard migrate mailgun. On the base, nothing runs. At HEAD the skill rewrites their Mailgun send code, and a missed rule harms silently once they deploy: a failure that used to pass now thrown (or the reverse), a refusal swallowed, a template rendered differently, open capture changed, or a suppressed address mailed. Silent harm at reach few is medium; the 559 changed lines with that row uncovered raise it one step.
  • Uncovered: no run applies a rewrite, typechecks, or sends: that waits for the PostHog SDK releases with the code-form send (live-run gap). A site stays whenever its client's error behavior, its template's rendering, or its domain's open tracking can't be read, and the report's "Before cutover" list names what the person checks first.
  • Unchanged: every other migrate-email variant and every shared step: this diff touches only references/mailgun/ and one line each in config.yaml, AGENTS.md, and the test's provider list. The shadow diff against feat(migrate-email): move Resend sends in every PostHog SDK's language #472's head shows the new Mailgun bundle and its manifest and skill-menu entries only; every other bundle is byte-identical.
  • Switch-off: none: a revert only stops new runs. The variant is live once a Wizard release ships it; fix forward in the skill text.

Rollout

Summary

references/mailgun/ holds the three files each variant has:

  • sdk-reference.md: Mailgun's libraries in Node.js (mailgun.js and the older mailgun-js), Python, Ruby, PHP, Go (v4 and v5) and Java, the HTTP API with the clients real sends use (requests, RestClient, Net::HTTP, curl, Guzzle, HttpClient, RestSharp), SMTP, and framework mailers. It covers how each reports errors (including that mailgun.js throws a network failure as a status-400 MailgunAPIError, and mailgun-php as HttpServerException::networkError()), the EU region setting, the clients that send nothing (Ruby's test mode, Python's dry_run), the send fields, stored templates, status reads, webhooks, and the APIs that are not sends.
  • mapping.md:
    • Fields. Each field maps to PostHog's or names what keeps a site on Mailgun: an html send without text (turns the plain-text part off, since PostHog would add a text part), batch sends, %unsubscribe_url% and its siblings, o:deliverytime and the other o: options, custom h: headers, v: values and o:tag, amp-html, MIME sends, mailing list recipients, SMTP and framework mailers, cc, bcc, attachment and inline, and any field the table doesn't name. recipient-variables on a send to one address moves: the code fills %recipient.<name>% before the send. A Reply-To with several addresses becomes a list. An Idempotency-Key moves when the code bounds its length and characters.
    • Tracking. Opens and clicks are worked out per site from the send's fields, else the domain's setting. Tracking never maps to captureEngagement: PostHog adds its open pixel to every email, and counts clicks only for a template made in PostHog that is not marked sensitive. Untracked opens follow feat(migrate-email): move Resend sends in every PostHog SDK's language #472's Step 3: this folder names the fields and domain setting that mean no open pixel or nobody knows. Conflicting fields keep the site, and so does a domain whose unsubscribe footer is on or not checked: Mailgun adds that footer and PostHog doesn't. A domain whose archive_to posts a copy of each delivered message to a URL, or whose setting isn't checked, keeps its sites too, since PostHog posts no copy.
    • Domains. Mailgun keeps suppression lists per sending domain. When moved sites send from several domains, the person agrees that they share PostHog's one list, or only one domain's sites move.
    • Templates. A template site moves by bringing the version it sends into the code (template-to-code.md): Handlebars values, if/unless/each/with/equal become code with Handlebars truth written out. A template made in PostHog, with Liquid, only when the person asks for one. A go engine version, other helpers, partials, a header that holds {{, text or html next to template, an inherited Reply-To with a name, and an equal whose value can be a string in one send and a number in another keep the site.
    • Errors. Never widen a catch, and keep network failures where they went: a PHP HttpServerException catch keeps network_error, a Guzzle 7 RequestException catch lets it through, Ruby's status 0 becomes network_error, and a catch of one kind of network failure (Python's Timeout alone, .NET's HttpRequestException, rest-client's ExceptionWithResponse) keeps the site. A plain HTTP send is read with its client's options, and an option or call that throws on some failed statuses only keeps the site, such as RestSharp's ThrowOnAnyError and Post, which pass a 404. A status branch lets the statuses it never checked pass as before. Telling one kind of network failure from another keeps the site, as Step 7 says. Under "HTTP", a timeout on the request counts as the site's own deadline, which Step 7 keeps; mailgun.js's client timeout is axios's, which the SDK can't keep, so its sites stay. A branch on 400 reads validation_error (plus network_error for mailgun.js), and a branch on 429 skips quota_limited. Node.js throws an error with type, status, details, name and code.
    • Code around the send. A client that sends nothing keeps its sites when the app turns it on outside its tests. A read of the answer's headers keeps the site, and so does a send a caller can cancel, unless the PostHog send takes that cancellation (Go, .NET). A suppressed send has no message id, so a site that hands the id on stays.
    • Webhooks. Each event maps to a $workflows_email_* event. A click maps only for a template made in PostHog that is not sensitive, so a site whose clicks feed a handler moves only there. A handler that matches events by message id, a v: value, or a tag keeps its sites on Mailgun, and so does one that acts on temporary_fail or unsubscribed, which have no PostHog event.
  • account.md: Mailgun's SPF, DKIM, tracking and MX records next to PostHog's, a read-only API key that the person deletes at the end of Step 5, and the US or EU host for every read. It reads the template version a site sends (domain and account templates, checked by id when the site names one) and the domain's tracking settings. It imports the bounces, complaints and unsubscribes of the domains whose sites move, with PostHog's bounce, complaint and manual reasons, and leaves out allowlists. Since PostHog blocks a moved send only to an address that hard-bounced, it names Mailgun's unsubscribes in Step 3's suppression-list question, so a site that must not mail them stays (relies on Mailgun's suppression list).

Evidence

  • Lane gate on 581b5d7: vitest run scripts passes 344 tests, the section-pointer check on this folder included, in 30 files; node scripts/build.js builds with a clean tree.
  • CI: gap. build.yml runs only on PRs into main; this PR's base is feat/migrate-email-all-sdks until feat(migrate-email): move Resend sends in every PostHog SDK's language #472 merges. The lane gate above ran on HEAD.
  • Live run: gap. An agent run of this migration on a real app needs the released email SDKs and the production send API; Michael's planned trial on generated codebases runs it after both land. Earlier headless dry runs (the last at 4260cda, before the core moved templates into the code) classified an 11-site fixture as expected; no run covers this head.
  • Prod shape: gap. The inputs are customer-written send code and config and each person's Mailgun account content; there is no store of real customer codebases to sample. Michael's planned trial on generated codebases covers it after the SDKs land.
  • Shadow diff against feat(migrate-email): move Resend sends in every PostHog SDK's language #472's head 090ac92: 287 bundles on the base, 288 at 581b5d7; only migrate-email-mailgun (new), manifest.json and skill-menu.json (its entries and the build timestamp) differ.
  • Proof gate: fleet proof gate at 581b5d7: review: HEAD reviewed in final read 13 by gpt-6.1-sol; shadow-diff: evidence at HEAD; the judge passes proof, owed, review, risk and quality; gate: pass with the live-run, prod-shape and CI gaps that Michael accepted.
  • Test seams (migrate-email-skill.test.js): mailgun in the provider list runs the shared checks on the Mailgun bundle: every reference resolves, its three files and the eight rewrite files are present, no other provider's folder ships, and the template-in-code contract holds.

Decisions

Question Answer Who Where
When this merges Not before code sends are live and usable for customers, for #451 and every PR on it 👤 Human this PR
Who lands it We land the provider PRs ourselves after the QA swarm and every ReviewHog finding 👤 Human this PR
Mailgun tracking Untracked opens follow #472's Step 3: the person is asked once, and yes moves the site with open pixel added: the person agreed; no or no answer stays (open tracking off), an unknown domain setting stays (open tracking not checked); opens tracked moves with a note when clicks were tracked; tracking never maps to captureEngagement 👤 Human #472
Mailgun events with no PostHog event A site whose emails a handler acts on for temporary_fail or unsubscribed stays (its <event> events feed a webhook handler), as Resend does for its events 🤖 Agent this PR
Which suppressions move After #472: bounces, complaints and unsubscribes of the domains whose sites move, as bounce, complaint and manual; allowlists left out. The unsubscribe count for the report is gone: Step 3's suppression-list question decides those sites, naming Mailgun's unsubscribes 🤖 Agent this PR
Several domains, one PostHog list Ask whether moved sites may share one list; if not, only one domain's sites move 🤖 Agent this PR
Templates Brought into the code by default; a template made in PostHog only on request. Replaced moving only to a PostHog template 🤖 Agent this PR
A catch of one kind of network failure Keeps the site on Mailgun, since PostHog's network_error doesn't say which kind 🤖 Agent this PR
Test for the recipient-variables rule Deleted: it pinned the mapping's wording; the shared provider checks and the live run stand in 🤖 Agent this PR
Shadow diff Run against #472's head: only the Mailgun bundle and its manifest and menu entries change 🤖 Agent this PR
Proof gaps Live run, prod shape and CI accepted as gaps until the SDK releases and #472's merge 👤 Human this PR
#472's recurring re-import, project-get and real-SDK tests No edit here: this folder has no re-import or sync text, no projects-get, and no PostHog mock 🤖 Agent this PR
Mailgun's unsubscribe footer A domain with it on, or not checked, keeps its sites, since PostHog adds no footer 🤖 Agent this PR
A request deadline Follows Step 7 in #472; this folder names only which Mailgun timeouts count as the site's own 🤖 Agent this PR
A branch on a transport error code Keeps the site on Mailgun (ReviewHog); now the core's Step 7 rule, so this folder names only Mailgun's own error classes and settings under it 🤖 Agent this PR
Final reads past the cap of six Ran reads 7 to 13 under the standing review rule, each on a narrow delta: ReviewHog's one row, three reconciles with #472's new rules (Step 7, then Step 3's untracked opens and Step 8's tests), and the judge's RestSharp 404 finding (read 9 widened it to ThrowOnAnyError, checked against RestSharp's source); reads 7, 8, 10, 11, 12 and 13 came back clean (13 on the fifth core merge). After #472 merged: read 14 on the main merge and the suppression reason mapping, then reads 15 to 18, each on one ReviewHog or read fix (httpx HTTPError, NoopHydrator, domain archive_to with a partial requests catch, an html send without text); 14, 16, 17, 18 and 19 came back clean 🤖 Agent this PR
RestSharp throwing setups ThrowOnAnyError and Post/PostAsync pass a 404 by default, so they keep the site as clients that throw on some failed statuses only (fleet judge, final read 9) 🤖 Agent this PR
Behavior fixtures for each rule Refused as unit tests: no seam runs an agent; the rules join the live run's fixture list 🤖 Agent this PR
Final-read escalation on the unsubscribe count Decided under the standing review rule: the repeat was a narrower residual, fixed where this folder reaches; Step 5's no-import-tool path is a core follow-up 🤖 Agent this PR

Involvement: ReviewHog, 4 rounds on this work after the 3 earlier threads (12 threads fixed, all resolved); qa-swarm, 2 panel rounds (round 1: fresh-eyes, correctness, risk, design, anti-overengineering, sweep, and the author's team perspective; round 2: the same without sweep) and 13 final reads. 13 swarm findings fixed, 3 rejected (the shadow diff, and two asks for agent-execution fixtures). Proof: live-run, prod-shape and CI recorded as gaps, which Michael accepted.

🤖 Agent context

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5, high). Reviews on GPT-6.1 Sol (high), the fresh-eyes lens on Opus 5.5.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

🧙 Wizard CI

Run 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:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci feature-flags
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci feature-flags/django
  • /wizard-ci feature-flags/next-js
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a wizard branch:

  • /wizard-ci all wizard:my-branch

Add wizard:<branch> to any command above to pin the wizard branch. It defaults to main.

Results will be posted here when complete.

Silthus and others added 28 commits November 4, 2026 12:07
PostmarkMessageBase's constructor sets TrackLinks to None, so a .NET send tracks links only when the code sets it, as postmarker's sends do. The rewrite adds captureEngagement: false only when Postmark tracked neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilgun's error rewrite

axios with a permissive validateStatus and got with throwHttpErrors: false answer a failed status without throwing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ull follows the server

Only an SDK message whose code never sets TrackLinks keeps the constructor's None; null and plain HTTP sends follow the server's setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Links default

The positional SendMessageAsync extension builds a PostmarkMessage, so its links were not tracked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…send leaves TrackOpens out

Only TrackLinks has an SDK default of None in postmarker and .NET; TrackOpens defaults to null in both and follows the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostHog answers a used-up quota with status 429 and code quota_limited, which no retry fixes, so a retry or wait on 429 checks the code too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d's Node rethrow

err.code keeps SendGrid's status for the old catch; err.posthogCode lets a 429 branch skip quota_limited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-js keeps PostHog's code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and keep suppression writes in step with PostHog's list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from PostHog's suppression list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ression list, and name Mailgun's suppression writes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… no imported list still holds the address, on every path of Step 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…domains whose sites move

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s that mirrored additions made

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 15 commits November 13, 2026 15:07
…ain text off

A dashboard copy shows no plain text both for a template without one and
for one whose plain text is an empty string. Ask which, or read the
template from the API; when nobody can tell, the site stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion and keep the answer

A dashboard copy has no key to read the template from the API, so the
question offers to make one. The answer goes into the template's plan
note, so a copy made again in Step 7 asks nothing new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nly in Step 3

The template's note records its plain text however the template was
read, so Step 7 never asks again, and the key is offered only in Step 3,
before Step 5 asks the person to delete it. A site stays whenever
neither the answer nor the API read settles it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostHog's bulk add takes only bounce, complaint or manual, and writes its own
reason text, such as "Imported (bounced)". The removal guard now keys on that
prefix, and the import maps each Resend origin to the reason value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…them

PostHog's list records no source an app could check before a removal, a bulk
add skips an address removed earlier, and an app can't call the import tool.
PostHog blocks a transactional send only to an address that hard-bounced, and
records that itself. So each such call becomes a "Before cutover" item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostHog blocks a transactional send only to an address that hard-bounced, so
the weekly re-import limits mail to bounced addresses, not complaints. A call
that removes an address becomes an "After cutover" item the person acts on
when a moved send fails with recipient_suppressed. A call that adds one names
the action before the deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-import only now names bounces alone, and an app call that adds an address
also records it where the moved sends can skip it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A moved send reaches every address an app's add call put on the provider's
list, and an app-side flag misses past adds and survives a reactivation. So
Step 3 asks once which sites rely on the call, and keeps them on the provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 3 asks which sites rely on the provider's list for addresses added by
hand or by the app, and a non-answer keeps every site when the app adds
addresses itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the list

The suppression-list question gets its own Step 3 section, asks only about
sites that would move otherwise, keeps them on a non-answer, and says what
would let them move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e work

The question now comes right after the field checks, so a site it keeps
needs no template copy or PostHog template, and the plan note carries what
would let the site move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-rs's async email send waits between retries with tokio::time::sleep
and sends through reqwest, so a site on async-std or smol stays on Resend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main equals #451's merged head a090b5b, which this branch already contains, so the tree is unchanged from f98eb3b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus changed the base branch from feat/migrate-email-all-sdks to main November 13, 2026 16:26
Silthus and others added 3 commits November 13, 2026 16:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed bd23423: 1 Should fix.

This is review round 6 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Silthus and others added 2 commits November 13, 2026 16:42
…errors

httpx.HTTPError is the base of both RequestError and HTTPStatusError, so a
catch of it after raise_for_status() took network failures too, unlike
requests.HTTPError. Map it to a catch of every PostHog error, and
httpx.RequestError alone to network failures only (ReviewHog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without raise_for_status(), a catch of httpx.HTTPError saw network
failures only, so it converts like httpx.RequestError, not to a catch of
every PostHog error (final read 15).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed f1b0c45: 1 Should fix.

This is review round 7 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

…work-only

A Mailgun PHP client built with NoopHydrator answers a 5xx as the
response and throws HttpServerException on a network failure only, so its
catch keeps only network_error and the getStatusCode() branch moves after
it, instead of sending 5xx answers into the catch (ReviewHog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed b4e2c85: 2 Should fix.

This is review round 8 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

…al requests catches

A domain with archive_to posts a copy of each delivered message to a URL
without o:archive-to on the send, and PostHog posts none, so its sites
stay; account.md reads it from GET /v4/domains/<domain>. A requests catch
of ConnectionError with Timeout leaves out other RequestException
failures, so it tells one network failure from another and stays, as
Step 7 says (ReviewHog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed e7b5794: 1 Should fix.

This is review round 9 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Mailgun's docs name no text part it makes for a send with html and no
text, only t:text for a template, and PostHog makes one from the HTML
whenever a send has no text. Such a site stays with `turns the plain-text
part off`, as template-to-code.md says for a template (ReviewHog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed f14a117: no findings.

@Silthus
Silthus marked this pull request as draft November 14, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant