Skip to content

feat(workflows): open the email domain wizard from channels, configure and broadcasts - #110088

Closed
Silthus wants to merge 13 commits into
PostHog:masterfrom
Silthus:feat/email-domain-wizard
Closed

Silthus wants to merge 13 commits into
PostHog:masterfrom
Silthus:feat/email-domain-wizard

Conversation

@Silthus

@Silthus Silthus commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Note

Merge risk: low (qa-swarm danger rubric)

  • Every entry point checks the flag and falls back to the legacy modal.
  • A test covers each entry point.

Problem

  • The wizard page from layer 4 is only reachable by URL. People start email setup from the channels page, from "Configure" on an unverified sender, and from the broadcast sender picker.

Note

Stack (5 PRs, merge in order): 1. status endpoint #110516 → 2. Domain Connect and host detection #110517 → 3. wizard logics #110518 → 4. wizard UI #110519 → 5. entry points #110088
Every PR targets master because the branches live on a fork. Each one carries the commits of the PRs below it until those merge. Review only this layer: this layer's diff.

Changes

  • With workflows-email-domain-wizard on:
    • The empty channels page and "New channel > Email" open the wizard page.
    • "Configure" on an unverified sender opens that sender's page.
    • The broadcast sender modal shows the wizard inline, reports the new sender to the broadcast right after it is created, and stays open for the DNS settings, with an "Open full page" link.
  • With the flag off, every entry point keeps the old email sender modal unchanged.
  • email domain setup started reports which entry point was used.

How did you test this code?

Test rationale:

  • emailDomainEntryPoints.test.tsx renders each entry point with the flag off and on. Off keeps the old modal; on goes to the page, or shows the wizard in the broadcast modal and hands the new sender to the broadcast.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code. Backend by Opus 5.5 (claude-opus-5-5), frontend by Fable 5.1 (claude-fable-5-1), stack split, integration and review fixes by Opus 5.5. Michael Reichenbach directed the work and picked the flow from three click-through prototypes.

  • Skills: /improving-drf-endpoints, /implementing-mcp-tools, /writing-kea-logics, /writing-ui-components, /writing-user-facing-copy, /adopting-generated-api-types, /writing-tests, /writing-code-comments, /writing-pr-descriptions, /resolving-merge-conflicts.
  • This stack replaces the single PR feat(workflows): open the email domain wizard from channels, configure and broadcasts #110088, which became the top layer. Its review history stays there.
  • Reviews: Codex (GPT-6.1) reviewed each half, the combined branch, the test suite and the final stack; Fable 5.1 ran an adversarial review. Every finding was checked against the code; fixed findings carry a test that fails without the fix. One finding (polling does not pause on a hidden tab) was a false positive: kea-disposables pauses keyed timers while the page is hidden, and a test covers it.
  • CodeRabbit CLI: skipped, the CLI is signed out.
  • All sample data (acme.com, example.com, tokens) is invented.

🤖 Generated with Claude Code

@trunk-io

trunk-io Bot commented Oct 1, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a feature-flagged email-domain setup wizard with domain selection, DNS configuration, and verification screens. Adds backend SES and Maildev status checks, DNS record publication and provider detection, and an integration status endpoint with caching and refresh support. Adds verification-event reporting, setup-method tracking, Domain Connect integration, and an MCP status retrieval tool.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 0d0d8

The wizard has bounded domain-selection and sender-editing concerns, and concurrent updates can lose verification or setup-method state. Address the config-write race and retry handling before rollout, or explicitly accept these risks with follow-up ownership.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 0d0d8

The new status check can change sender verification under read-only access. Project ownership and domain-verification checks constrain the impact, but the change expands who can complete sender setup.

Retained concerns

  • Medium · security · observed: The new read-authorized status endpoint performs a persistent verification transition and worker reloads. A project-scoped caller passing the read and membership checks can complete verification for all matching same-domain senders, although the existing verification POST requires admin-level access. Provider readiness and team association constrain this capability, but do not preserve the previous completion-authority boundary.
Security review details

Security Blast Radius

  • observed — The observed persistent mutation is bounded to email integrations matching the selected integration's team, domain and provider. It can affect multiple sender records and their worker copies, but the status path does not write DNS or SES configuration.

Security Findings and Attack Paths

  • observed — A caller with project access and integration:read can invoke status to reach local verification writes when the provider check succeeds. GET lowers the management gate from admin to member and enables token access through an explicit read scope. This path expands completion authority; it does not establish arbitrary verification, cross-project access, or control over DNS and SES.

Trust Boundaries and Controls

  • observed — External readiness is checked against the team's SES tenant before local completion. Domain Connect email resolution independently loads by integration ID and team ID and rejects non-email integrations. Setup-method persistence also locks the integration within the current team.
  • observed — Customer-controlled authoritative nameserver addresses are filtered to global, non-multicast IPs before direct DNS queries. Discovery considers at most two nameservers and uses three-second lookup lifetimes. Domain Connect discovery and URL generation check provider endpoints against a centralized allowlist.

Resilience and Maintainability Implications

  • observed — The new report separates current provider setup status from the persisted verified flag. Available base snapshots already retain that flag across MAIL FROM changes, and the head continues this behavior. Consequently, verified is historical local state rather than a fresh assertion that every current DNS and MAIL FROM condition is satisfied; this is not attributed as a newly introduced verification-reset defect.

Hardening Proposals

  • proposed — Separate observational status retrieval from sender activation, preserving the intended management authority for completion. If read-triggered synchronization is intentional, explicitly define that capability and its MCP authorization semantics rather than treating the operation as purely read-only.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the problem, user-visible changes, test rationale, feature-flag release status, and agent context. It is mostly complete, but it does not include frontend screenshots or a lin…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (4)
products/workflows/frontend/Channels/EmailDomain/domainInput.ts-41-46 (1)

41-46: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

rootDomainOf and nameFromDomain give wrong results for multi-part public suffixes.

Both functions keep only the last two labels. For acme.co.uk, rootDomainOf returns co.uk. As a result, inferDomains groups unrelated .co.uk hosts under one root and can recommend the wrong domain. For the same input, nameFromDomain returns Co as the sender name. UK, AU, and JP customers are affected. Use a public-suffix-aware helper, or at minimum handle common second-level suffixes such as co.uk, com.au, and co.jp.

products/workflows/frontend/Channels/EmailDomain/emailDomainWizardLogic.ts-424-431 (1)

424-431: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Record the analytics event even when config.domain is missing.

Line 430 falls back to '' when both createdIntegration.config?.domain and values.sendingDomain are empty. With an empty domain, domainConnectCheck('') sends a request that is certain to fail. The failure is swallowed, so no data is lost. The only effect is one wasted request. The bigger concern is ordering: props.onCreated runs first. In the modal, onCreated swaps DomainStep for ModalSettings, which unmounts this keyed logic while the listener is still awaiting. The capture still runs after the unmount, and reading values.sendingDomain after the unmount can throw. Read values.sendingDomain before calling onCreated.

Proposed fix
-            actions.loadIntegrations()
-            props.onCreated(createdIntegration)
-            const domain = createdIntegration.config?.domain ?? values.sendingDomain ?? ''
+            const domain = createdIntegration.config?.domain ?? values.sendingDomain ?? ''
+            actions.loadIntegrations()
+            props.onCreated(createdIntegration)
+            if (!domain) {
+                return
+            }
products/workflows/frontend/Channels/EmailDomain/components/FirstSenderCard.tsx-31-31 (1)

31-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Block Enter from saving an empty sender name.

The Save button checks !senderName.trim() and disables itself for a blank name. onPressEnter does not run this check. If the user clears the name and presses Enter, saveSender persists an empty config.name.

Proposed fix
-                            onPressEnter={nameChanged ? saveSender : undefined}
+                            onPressEnter={nameChanged && senderName.trim() && !savedSenderLoading ? saveSender : undefined}
frontend/src/lib/api.ts-6141-6147 (1)

6141-6147: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the generated client

The generated integrationsEmailStatusRetrieve function already supports this endpoint, including the refresh query parameter. Use it with the generated EmailDomainStatusApi response type instead of adding an ApiRequest wrapper.

Source: Coding guidelines

🧹 Nitpick comments (3)
products/workflows/frontend/Channels/EmailDomain/EmailDomainSetupModal.tsx (1)

66-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a kea logic for the modal's local state instead of useState/useEffect.

The modal stores integrationId with useState and fires the analytics event from useEffect. The coding guidelines forbid both patterns for local state. Move integrationId and the mount-time capture call into a small keyed logic, using a reducer and afterMount.

As per coding guidelines: "Don't use useState or useEffect to store local state."

Source: Coding guidelines

frontend/src/lib/api.ts (1)

116-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a type-only import for DnsHostType and EmailDomainStatusType.

Both symbols are interfaces used only in type annotations. Remove them from the regular ~/types import.

Suggested fix
 import {
-    DnsHostType,
-    EmailDomainStatusType,
     EmailSenderDomainStatus,
     EndpointType,
     EndpointVersionType,
...
 } from '~/types'
+import type { DnsHostType, EmailDomainStatusType } from '~/types'

Source: Coding guidelines

posthog/models/integration/email.py (1)

192-193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document or resolve the local-import exception.

These imports follow the existing model-to-workflows boundary, but the applicable rule still requires module-level imports. The inline PLC0415 comments do not document an approved exception. Add this boundary to the approved guidance, or route these calls through a lightweight module-level adapter that does not import the workflows facade during model initialization.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 49ea9d01-8698-4dbb-b195-00ea0922c34d

📥 Commits

Reviewing files that changed from the base of the PR and between c8ef65b and 489f8d0.

⛔ Files ignored due to path filters (4)
  • products/integrations/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/integrations/frontend/generated/api.ts is excluded by !**/generated/**
  • services/mcp/src/generated/integrations/api.ts is excluded by !**/generated/**
  • services/mcp/src/tools/generated/integrations.ts is excluded by !**/generated/**
📒 Files selected for processing (61)
  • frontend/src/lib/api.ts
  • frontend/src/lib/constants.tsx
  • frontend/src/lib/integrations/IntegrationEmailDomainView.tsx
  • frontend/src/productScenes.tsx
  • frontend/src/products.tsx
  • frontend/src/scenes/sceneTypes.ts
  • frontend/src/types.ts
  • posthog/api/integration.py
  • posthog/api/integration_email_status_serializers.py
  • posthog/api/test/test_integration.py
  • posthog/dns_hosts.py
  • posthog/existing_email_tools.py
  • posthog/models/integration/email.py
  • posthog/test/test_dns_hosts.py
  • products/integrations/mcp/tools.yaml
  • products/workflows/backend/facade/api.py
  • products/workflows/backend/facade/contracts.py
  • products/workflows/backend/facade/enums.py
  • products/workflows/backend/providers/__init__.py
  • products/workflows/backend/providers/maildev.py
  • products/workflows/backend/providers/ses.py
  • products/workflows/backend/services/email_domain_dns.py
  • products/workflows/backend/services/email_domain_status.py
  • products/workflows/backend/test/test_email_domain_status.py
  • products/workflows/frontend/Broadcasts/steps/BroadcastContentStep.tsx
  • products/workflows/frontend/Channels/EmailDomain/EmailDomain.stories.tsx
  • products/workflows/frontend/Channels/EmailDomain/EmailDomainScene.tsx
  • products/workflows/frontend/Channels/EmailDomain/EmailDomainSetup.tsx
  • products/workflows/frontend/Channels/EmailDomain/EmailDomainSetupModal.tsx
  • products/workflows/frontend/Channels/EmailDomain/buildSetupPrompt.ts
  • products/workflows/frontend/Channels/EmailDomain/components/AgentHandoffModal.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/AgentSetupButton.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/BigAction.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/DnsHostLink.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/DnsRecordRows.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/FirstSenderCard.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/NextWorkflowCard.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/StepHeading.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/StepIndicator.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/VerificationStepsStrip.tsx
  • products/workflows/frontend/Channels/EmailDomain/dnsHosts.ts
  • products/workflows/frontend/Channels/EmailDomain/domainInput.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainLogic.test.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainLogic.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainRoutes.test.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainWizardLogic.test.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainWizardLogic.ts
  • products/workflows/frontend/Channels/EmailDomain/hoggies.ts
  • products/workflows/frontend/Channels/EmailDomain/steps/DomainStep.tsx
  • products/workflows/frontend/Channels/EmailDomain/steps/ReadyStep.tsx
  • products/workflows/frontend/Channels/EmailDomain/steps/SettingsStep.tsx
  • products/workflows/frontend/Channels/EmailDomain/steps/UnavailableStep.tsx
  • products/workflows/frontend/Channels/EmailDomain/steps/VerifyingStep.tsx
  • products/workflows/frontend/Channels/EmailDomain/useStartEmailDomainSetup.ts
  • products/workflows/frontend/Channels/EmailSetup/EmailSetupModal.tsx
  • products/workflows/frontend/Channels/MessageChannels.tsx
  • products/workflows/frontend/MessagingTabActions.tsx
  • products/workflows/manifest.tsx
  • services/mcp/schema/generated-tool-definitions.json
  • services/mcp/schema/tool-definitions-all.json
  • services/mcp/src/api/generated.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

@coderabbitai coderabbitai 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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
products/workflows/frontend/Channels/EmailDomain/emailDomainLogic.ts-850-859 (1)

850-859: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reset manualSetupRecorded when the update fails.

recordManualSetup sets cache.manualSetupRecorded = true before the request. The request swallows its error. If the PATCH fails, the logic never retries for that mount. The verification event then reports manual only as the server default and loses an explicit manual after a previous auto. Reset the flag in the catch handler.

Proposed fix
             await api.integrations
                 .updateEmailConfig(integrationId, { config: { ...integration.config, setup_method: 'manual' } })
-                .catch(() => null)
+                .catch(() => {
+                    cache.manualSetupRecorded = false
+                })
posthog/api/integration.py-2581-2584 (1)

2581-2584: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Prevent stale email config writes.

email_update loads Integration before calling EmailIntegration.update_native_integration, which later saves the entire stale config. If _record_email_setup_method or domain verification commits first, this save can remove setup_method or verified.

email/status and email/verify reload matching senders under select_for_update(), so they do not have the same stale-save path. Use a database-side JSON-key merge for _record_email_setup_method, and apply the same merge pattern to EmailIntegration.update_native_integration; changing only this method does not prevent a later stale email_update save.

Suggested JSON-key update
+from django.contrib.postgres.fields import ArrayField
 from django.db import models, transaction
@@
     def _record_email_setup_method(self, integration_id: int, setup_method: str) -> None:
-        with transaction.atomic():
-            instance = Integration.objects.select_for_update().get(id=integration_id, team_id=self.team_id)
-            instance.config["setup_method"] = setup_method
-            instance.save(update_fields=["config"])
+        Integration.objects.filter(id=integration_id, team_id=self.team_id).update(
+            config=models.Func(
+                models.F("config"),
+                models.Value(["setup_method"], output_field=ArrayField(models.TextField())),
+                models.Value(setup_method, output_field=models.JSONField()),
+                function="jsonb_set",
+            )
+        )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 61b0d24a-1a8a-4a34-8a33-8d28a17ec3bf

📥 Commits

Reviewing files that changed from the base of the PR and between 489f8d0 and 0d0d8ff.

📒 Files selected for processing (19)
  • posthog/api/integration.py
  • posthog/api/test/test_integration.py
  • posthog/domain_connect.py
  • posthog/models/integration/email.py
  • posthog/test/test_dns_hosts.py
  • products/workflows/backend/facade/api.py
  • products/workflows/backend/providers/ses.py
  • products/workflows/backend/services/email_domain_dns.py
  • products/workflows/backend/services/existing_email_tools.py
  • products/workflows/backend/test/test_email_domain_status.py
  • products/workflows/backend/test/test_email_integration_ses.py
  • products/workflows/backend/test/test_existing_email_tools.py
  • products/workflows/frontend/Channels/EmailDomain/EmailDomain.stories.tsx
  • products/workflows/frontend/Channels/EmailDomain/components/FirstSenderCard.tsx
  • products/workflows/frontend/Channels/EmailDomain/domainInput.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainLogic.test.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainLogic.ts
  • products/workflows/frontend/Channels/EmailDomain/emailDomainWizardLogic.test.ts
  • products/workflows/frontend/Channels/EmailDomain/steps/SettingsStep.tsx
💤 Files with no reviewable changes (2)
  • posthog/test/test_dns_hosts.py
  • products/workflows/backend/services/existing_email_tools.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@Silthus
Silthus force-pushed the feat/email-domain-wizard branch 2 times, most recently from f14753a to 41cefba Compare October 2, 2026 06:13
@Silthus Silthus changed the title feat(workflows): add the email sending domain setup wizard feat(workflows): open the email domain wizard from channels, configure and broadcasts Oct 2, 2026
@Silthus
Silthus force-pushed the feat/email-domain-wizard branch from 41cefba to 707bfae Compare October 7, 2026 10:39
Silthus and others added 6 commits October 7, 2026 11:22
…email domain tests

FakeDnsZone answers dnspython at the network edge, so the real resolver,
zone_for_name and CNAME chaining run unchanged. FakeSes serves the SES,
SESv2 and STS clients through boto3.client and flips each identity part
to Success only when the fake zone holds its records.
FakeDomainConnectProvider serves settings over HTTP, checks the apply URL
signature, renders our real template and publishes its records.
Every fake reads one FaultInjector keyed by operation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add GET integrations/{id}/email/status. It reads SES identity attributes and
the zone's own nameservers without changing SES, and returns the overall
status, three setup steps and every DNS record with pending, found or
verified. Records the zone's nameservers do not confirm, such as a DMARC
CNAME into another zone, are looked up again through a public resolver.
Responses are cached for 10 seconds; refresh=true skips the cache.

When SES reports the domain verified, the senders on that domain are marked
verified once and an "email domain verified" event reports the setup method
(manual or agent). The verify endpoint shares the same write.

The endpoint stays honest through provider and DNS failures:

- An SES or STS read error is a typed 503 with the code
  email_provider_unavailable instead of an unhandled 500. Errors other
  than throttling go to error tracking.
- A check where a DNS lookup got no answer neither verifies the domain nor
  is cached, so the next poll decides once DNS answers again.
- A failed worker reload after the verified write no longer fails the
  request. Workers refresh integrations on their own schedule.
- The SES MAIL FROM status counts only when SES holds this sender's MAIL
  FROM domain, and the MAIL FROM SPF record shows verified only when it is
  published.
- DNS checks stay off internal addresses, and the AWS account id is looked
  up once instead of on every read.

Scenarios drive the endpoint end to end against the SES and DNS fakes.

The status tool is enabled for MCP behind workflows-email-domain-agent-setup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-click email setup

domain-connect/check now returns the DNS host detected from the root
domain's nameservers, with a link to its DNS settings, and the email tools
that already send for the domain, detected from SPF and DKIM records.
Its response gets a typed schema.

The Domain Connect apply-url call records setup_method auto on the email
integration and reports the "email domain verified" event when the domain
verifies during that call. The sender update records setup_method manual
and skips the SES MAIL FROM write when the bounce subdomain is unchanged.

Lookups that got no answer are retried instead of cached:

- A DNS timeout or SERVFAIL on the _domainconnect record, and a timeout,
  rate limit, 5xx or malformed answer from the provider settings, no
  longer hide one-click setup for an hour. Only a definitive answer is
  cached.
- Existing email tool detection no longer caches an empty list for an
  hour after a lookup got no answer.

Scenarios prove one-click email setup end to end: the check endpoint finds
the provider and DNS host, the apply URL is signed, the fake provider
renders our real template, publishes its records, and status polls reach
verified with the auto setup method. They cover a person returning without
approval, the sender update, and the provider failures. They replace the
tests that patched generate_apply_url, the verify call and the private
discovery helpers. The case for an existing DMARC record is skipped until

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silthus and others added 7 commits October 7, 2026 11:24
…in connect check

Adds the workflows-email-domain-wizard and workflows-email-domain-agent-setup
flags. The Domain Connect check client and logic read the generated
EmailDomainConnectCheckApi type instead of a hand-written copy, so they see
the new dns_host and existing_email_tools keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ainst a fake backend

The sending domain page state lives in six logics keyed by the integration
id, each with one job: the sender record (load, name, setup method, test
email), the status and its polling schedule, host detection and the Domain
Connect round trip, the manual records path, the agent hand-off prompt, and
a composer that derives the phase, the verification steps and the
breadcrumbs from the others. The domain step logic infers likely sending
domains from the project's event hosts. Domain parsing, domain inference,
the agent prompt, the test email workflow and the page URL are pure modules.

The status logic polls the read-only email status endpoint while the tab is
visible, keeps polling through a 500 or an email_provider_unavailable 503,
and stops on 400, 403 and 404. Both manual paths record setup_method manual
on the integration.

Tests talk to the HTTP boundary: a stateful fake backend typed with the
generated API types holds senders, SES state and published DNS records, and
a test advances it from pending to records_found to verified or injects a
failure per endpoint. The polling schedule and the phase decisions are
proven against it.

The scene key and the urls.workflowsEmailDomain helper are registered here
because the logics use them. The page itself, its route and the steps come
in the next layer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds a flag-gated page at /workflows/channels/email/:id that walks through
picking a domain, adding the DNS settings at the detected host, and
watching verification until the domain is ready to send. The page handles
the Domain Connect return, offers an agent hand-off prompt behind a second
flag, and shows an unavailable state with a Try again button when the
sender or the status cannot be loaded.

Every step and component reads from the logic that owns its state, bound
once per integration id by BindEmailDomainLogics. The record rows tell
people to merge include:amazonses.com into an SPF record that already
exists and to keep an existing DMARC record. The Domain Connect return
reads the kea router instead of window.location, so the same code path
runs in the app and under the memory history in tests.

The stories render the same fake backend the tests use, staged per state,
so a story and a test cannot drift apart. Rendered flow tests drive the
real scene outside in: the manual path from a typed domain to a verified
first sender with a renamed sender and a test email; the one-click path
through Cloudflare and the return with domain_connect=email; and recovery
from a failed create, a failed sender load, a status outage after the
records were found, a failed host detection, a failed restart and a return
without approval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e and broadcasts

With workflows-email-domain-wizard on, the empty channels page, the new
channel menu and configure on an unverified sender open the sending domain
page, and the broadcast sender modal shows the wizard inline. The modal
reports the new sender to the broadcast right after creating it and stays
open for the DNS settings. With the flag off, every entry point keeps the
old email sender modal.

Rendered tests open the setup from each entry point with the flag off and
on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Silthus
Silthus force-pushed the feat/email-domain-wizard branch from 707bfae to 2f97474 Compare October 7, 2026 11:45
@Silthus

Silthus commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Note

🤖 GPT-6.1 Sol responding on behalf of Michael

Replaced by the draft fork PR #341 in its native fork stack. @meikelmosby will start migrating the stack from the fork into PostHog/posthog for upstream review and CI. Closing this master-targeted copy to keep the handoff clear.

@Silthus Silthus closed this Oct 7, 2026
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