Repository navigation
feat(messaging): capture email first code send once per project - #127187
trunk-io[bot] merged 29 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The analytics client returns None instead of raising when it drops an event, so the claim now also goes back when capture queues nothing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event
…peration The service claims the project, runs the capture, and releases the claim when the capture does not queue, so the view no longer drives the claim protocol. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event
…irst-code-send-event
…irst-code-send-event Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event
🤖 CI report
|
| First copy | Second copy | Lines | Tokens |
|---|---|---|---|
products/warehouse_sources/sources/folk/folk.py:144 |
products/warehouse_sources/sources/select_star/select_star.py:156 |
18 | 90 |
products/warehouse_sources/sources/ruddr/ruddr.py:127 |
products/warehouse_sources/sources/tempo/tempo.py:136 |
20 | 72 |
⚠️ Duplication (TypeScript) — 11 new duplicated blocks (worst 195 tokens)
New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.
| First copy | Second copy | Lines | Tokens |
|---|---|---|---|
packages/agent/packages/agent/src/adapters/claude/claude-agent.slash-command.test.ts:49 |
packages/agent/packages/agent/src/adapters/claude/claude-agent.task-notification.test.ts:53 |
38 | 195 |
products/replay_vision/frontend/components/QuillScanList.tsx:31 |
products/replay_vision/frontend/components/ScanList.tsx:30 |
34 | 191 |
products/replay_vision/frontend/components/ChapterFilmstrip.tsx:26 |
products/replay_vision/frontend/components/QuillChapterFilmstrip.tsx:27 |
31 | 174 |
products/replay_vision/frontend/components/QuillScanList.tsx:203 |
products/replay_vision/frontend/components/ScanList.tsx:179 |
15 | 172 |
frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewDeviceTab.tsx:11 |
frontend/src/scenes/session-recordings/player/watch/QuillOverviewDeviceCard.tsx:11 |
14 | 144 |
products/replay_vision/frontend/components/QuillScanList.tsx:87 |
products/replay_vision/frontend/components/ScanList.tsx:81 |
27 | 138 |
frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewSessionTab.tsx:14 |
frontend/src/scenes/session-recordings/player/watch/QuillOverviewSessionCard.tsx:14 |
12 | 102 |
products/replay_vision/frontend/components/QuillScanList.tsx:257 |
products/replay_vision/frontend/components/ScanList.tsx:231 |
24 | 101 |
products/replay_vision/frontend/components/ObservationProgressBar.tsx:12 |
products/replay_vision/frontend/components/QuillObservationProgressBar.tsx:10 |
19 | 87 |
frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewPersonTab.tsx:15 |
frontend/src/scenes/session-recordings/player/watch/QuillOverviewPersonCard.tsx:16 |
14 | 84 |
products/replay_vision/frontend/components/ObservationProgressBar.tsx:45 |
products/replay_vision/frontend/components/QuillObservationProgressBar.tsx:43 |
13 | 70 |
ℹ️ MCP UI apps size — 35 app(s), 29126.5 KB JS
Built size of each MCP UI app (main.js + styles.css).
| App | JS | CSS |
|---|---|---|
| debug | 844.4 KB | 211.6 KB |
| explain-chart | 1119.1 KB | 211.6 KB |
| action | 767.8 KB | 211.6 KB |
| action-list | 835.4 KB | 211.6 KB |
| cohort | 766.8 KB | 211.6 KB |
| cohort-list | 834.4 KB | 211.6 KB |
| email-template | 766.6 KB | 211.6 KB |
| error-details | 771.7 KB | 211.6 KB |
| error-issue | 769.0 KB | 211.6 KB |
| error-issue-list | 836.8 KB | 211.6 KB |
| experiment | 832.5 KB | 211.6 KB |
| experiment-list | 836.1 KB | 211.6 KB |
| experiment-results | 834.3 KB | 211.6 KB |
| feature-flag | 834.8 KB | 211.6 KB |
| feature-flag-list | 838.5 KB | 211.6 KB |
| feature-flag-testing | 770.0 KB | 211.6 KB |
| inline-scan | 767.2 KB | 211.6 KB |
| insight-actors | 833.8 KB | 211.6 KB |
| llm-costs | 831.1 KB | 211.6 KB |
| session-recording | 769.0 KB | 211.6 KB |
| survey | 768.4 KB | 211.6 KB |
| survey-global-stats | 831.2 KB | 211.6 KB |
| survey-list | 836.1 KB | 211.6 KB |
| survey-stats | 831.2 KB | 211.6 KB |
| trace-span | 767.1 KB | 211.6 KB |
| trace-span-list | 835.3 KB | 211.6 KB |
| vision-observation-list | 834.5 KB | 211.6 KB |
| workflow | 767.1 KB | 211.6 KB |
| workflow-list | 834.7 KB | 211.6 KB |
| workflow-setup-status | 766.8 KB | 211.6 KB |
| query-results | 1090.8 KB | 211.6 KB |
| render-ui | 1195.0 KB | 211.6 KB |
| visual-review-snapshots | 771.6 KB | 211.6 KB |
| workflow-data-suggestions | 769.4 KB | 211.6 KB |
| workflow-sending-rules | 768.1 KB | 211.6 KB |
⚠️ Backend coverage — 97.0% of changed backend lines covered — 4 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ███████████████████░ 97.0% (168 / 172)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/messaging/backend/tests/api/test_transactional_email.py |
96.5% | 63–66 |
🤖 Agents: add a test only if an uncovered line exposes a realistic regression that existing tests miss. Otherwise explain why no new test is needed under "How did you test this code?". Gap list: the patch-coverage artifact on this run (gh run download 382526494170323 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
demo |
██████████████░░░░░░ 71.2% |
1,979 / 2,778 |
batch_exports |
████████████████░░░░ 82.2% |
23,523 / 28,601 |
engineering_analytics |
█████████████████░░░ 84.9% |
11,753 / 13,841 |
commands |
█████████████████░░░ 85.4% |
1,621 / 1,898 |
mcp_analytics |
█████████████████░░░ 86.3% |
5,812 / 6,733 |
posthog_ai |
██████████████████░░ 87.9% |
4,304 / 4,896 |
notebooks |
██████████████████░░ 88.8% |
15,454 / 17,407 |
mcp_registry |
██████████████████░░ 89.0% |
1,596 / 1,794 |
dashboards |
██████████████████░░ 89.4% |
8,048 / 9,003 |
product_tours |
██████████████████░░ 89.7% |
1,355 / 1,510 |
warehouse_suggestions |
██████████████████░░ 90.1% |
3,836 / 4,257 |
cohorts |
██████████████████░░ 90.3% |
9,203 / 10,195 |
signals |
██████████████████░░ 90.7% |
102,114 / 112,573 |
today |
██████████████████░░ 91.2% |
3,949 / 4,330 |
canvas |
██████████████████░░ 91.2% |
8,199 / 8,988 |
tasks |
██████████████████░░ 91.3% |
83,321 / 91,241 |
managed_warehouse |
██████████████████░░ 91.4% |
12,928 / 14,151 |
data_warehouse |
██████████████████░░ 91.4% |
15,913 / 17,416 |
data_modeling |
██████████████████░░ 91.5% |
11,714 / 12,806 |
autoresearch |
██████████████████░░ 91.5% |
11,717 / 12,805 |
cdp |
██████████████████░░ 91.5% |
10,433 / 11,399 |
metrics |
██████████████████░░ 91.8% |
11,380 / 12,399 |
streamlit_apps |
██████████████████░░ 91.9% |
3,139 / 3,415 |
business_knowledge |
██████████████████░░ 92.0% |
10,556 / 11,476 |
wizard |
██████████████████░░ 92.2% |
7,002 / 7,594 |
review_hog |
██████████████████░░ 92.3% |
17,768 / 19,256 |
conversations |
██████████████████░░ 92.3% |
31,368 / 33,969 |
error_tracking |
███████████████████░ 92.5% |
20,375 / 22,018 |
early_access_features |
███████████████████░ 92.6% |
1,339 / 1,446 |
alerts_platform |
███████████████████░ 92.6% |
8,643 / 9,329 |
slack_app |
███████████████████░ 92.7% |
22,444 / 24,215 |
notifications |
███████████████████░ 92.8% |
1,218 / 1,312 |
stamphog |
███████████████████░ 93.0% |
10,414 / 11,193 |
alerts |
███████████████████░ 93.0% |
11,701 / 12,575 |
surveys |
███████████████████░ 93.3% |
7,256 / 7,780 |
web_analytics |
███████████████████░ 93.4% |
26,472 / 28,347 |
approvals |
███████████████████░ 93.7% |
5,004 / 5,341 |
marketing_analytics |
███████████████████░ 93.8% |
22,066 / 23,535 |
workflows |
███████████████████░ 93.8% |
27,870 / 29,712 |
context_layer |
███████████████████░ 93.8% |
3,984 / 4,247 |
exports |
███████████████████░ 93.9% |
4,324 / 4,607 |
cross_project_dashboards |
███████████████████░ 93.9% |
858 / 914 |
mcp_store |
███████████████████░ 93.9% |
9,193 / 9,789 |
ai_observability |
███████████████████░ 94.0% |
33,124 / 35,252 |
authorization |
███████████████████░ 94.0% |
671 / 714 |
billing_alerts |
███████████████████░ 94.0% |
2,322 / 2,470 |
customer_analytics |
███████████████████░ 94.1% |
30,760 / 32,679 |
messaging |
███████████████████░ 94.2% |
7,825 / 8,308 |
replay_vision |
███████████████████░ 94.4% |
33,353 / 35,345 |
logs |
███████████████████░ 94.5% |
19,437 / 20,572 |
reminders |
███████████████████░ 94.8% |
782 / 825 |
experiments |
███████████████████░ 94.8% |
45,081 / 47,546 |
warehouse_sources |
███████████████████░ 94.8% |
135,154 / 142,521 |
actions |
███████████████████░ 94.9% |
997 / 1,051 |
growth |
███████████████████░ 95.0% |
11,563 / 12,176 |
endpoints |
███████████████████░ 95.0% |
9,665 / 10,177 |
annotations |
███████████████████░ 95.1% |
1,097 / 1,153 |
skills |
███████████████████░ 95.2% |
7,633 / 8,018 |
access_control |
███████████████████░ 95.4% |
8,461 / 8,871 |
data_catalog |
███████████████████░ 95.7% |
4,714 / 4,928 |
subscriptions |
███████████████████░ 95.7% |
14,017 / 14,651 |
product_analytics |
███████████████████░ 95.8% |
30,525 / 31,863 |
feature_flags |
███████████████████░ 95.8% |
29,678 / 30,973 |
revenue_analytics |
███████████████████░ 95.9% |
1,877 / 1,958 |
data_quality |
███████████████████░ 96.0% |
8,561 / 8,915 |
tracing |
███████████████████░ 96.5% |
4,572 / 4,737 |
account_audits |
███████████████████░ 96.7% |
770 / 796 |
pulse |
███████████████████░ 97.3% |
2,036 / 2,093 |
analytics_platform |
████████████████████ 98.0% |
2,883 / 2,943 |
groups |
████████████████████ 98.2% |
1,604 / 1,634 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
⚠️ Django migration SQL — 1 new migration to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/messaging/backend/migrations/0016_first_code_send.py
/home/runner/work/tmp/tool_cache/Python/3.14.7/x64/lib/python3.14/site-packages/anyio/from_thread.py:119: SyntaxWarning: 'return' in a 'finally' block
return result
2026-11-04T19:03:08.737060Z [error ] Path must be a valid database or directory containing databases. [posthog.exceptions_capture] pid=5778 tid=139890638388096
Traceback (most recent call last):
File "/home/runner/work/posthog/posthog/posthog/geoip.py", line 15, in <module>
geoip: Optional[GeoIP2] = GeoIP2(cache=8)
~~~~~~^^^^^^^^^
File "/home/runner/work/tmp/tool_cache/Python/3.14.7/x64/lib/python3.14/site-packages/django/contrib/gis/geoip2.py", line 114, in __init__
raise GeoIP2Exception(
"Path must be a valid database or directory containing databases."
)
django.contrib.gis.geoip2.GeoIP2Exception: Path must be a valid database or directory containing databases.
/home/runner/work/tmp/tool_cache/Python/3.14.7/x64/lib/python3.14/site-packages/sshtunnel.py:1040: SyntaxWarning: 'return' in a 'finally' block
return (ssh_host,
System check identified some issues:
WARNINGS:
?: (axes.W001) You are using the django-axes cache handler for login attempt tracking. Your cache configuration is however invalid and will not work correctly with django-axes. This can leave security holes in your login systems as attempts are not tracked correctly. Reconfigure settings.AXES_CACHE and settings.CACHES per django-axes configuration documentation.
?: (staticfiles.W004) The directory '/home/runner/work/posthog/posthog/frontend/dist' in the STATICFILES_DIRS setting does not exist.
BEGIN;
--
-- Create model FirstCodeSend
--
CREATE TABLE "messaging_firstcodesend" ("id" uuid NOT NULL PRIMARY KEY, "created_at" timestamp with time zone NOT NULL, "team_id" integer NOT NULL, CONSTRAINT "unique_first_code_send_per_team" UNIQUE ("team_id"));
CREATE INDEX "messaging_firstcodesend_team_id_4e029583" ON "messaging_firstcodesend" ("team_id");
COMMIT;Last updated: 2026-11-04 19:03 UTC (e876d99)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 1 Safe | 0 Needs Review | 0 Blocked
✅ Safe
Brief or no lock, backwards compatible
messaging.0016_first_code_send
└─ #1 ✅ CreateModel
Creating new table is safe
model: FirstCodeSend
│
└──> ℹ️ INFO:
ℹ️ Skipped operations on newly created tables (empty tables
don't cause lock contention).
Last updated: 2026-11-04 19:04 UTC (e876d99)
…irst-code-send-event
The first send on send/email/ through a project secret API key that a person made captures `email first code send` for that person. A project claims the event once. The claim goes back when capture raises or drops the event, so a later send reports it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…first-code-send-event The scope cut parks local templates. This merge takes the tree of this layer's own commits replayed onto #127167, so the branch drops the parked layers that sat below it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event
A key made in an environment captured the first code send under the environment, while the claim it takes belongs to the project. The event now carries the project's id and group. A code send already checked the code-send flag before it went out, so only a send by template id checks it again. The key's creator check now runs before the flag check. The claim uses get_or_create, and the creator comes from one query. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d helpers The environment test passes its team by keyword to the key helper. The first code send tests send with the current send scope, so the legacy scope fallback captures nothing and the capture mock fails only the first code send event. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event
…irst-code-send-event
…irst-code-send-event
|
@PostHog review |
|
@Silthus PostHog Review is reviewing this pull request. |
…irst-code-send-event Brings in #127123's master merge, which adds messaging migration 0012_email_hard_bounce and renumbers the code-send key migration to 0015_transactional_email_key. The first code send migration becomes 0016_first_code_send on top of it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…irst-code-send-event Brings 03's fcf42b0 through layers 08 and 10. The first code send migration stays 0016_first_code_send on top of 0015_transactional_email_key. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new stamphog review started for this PR — the fresh verdict replaces this approval.
There was a problem hiding this comment.
Not approved yet — waiting on the conditions below.
The Migration risk check has not finished for this commit, so stamphog cannot tell a safe migration from a risky one yet. The review runs again on the next push, or you can re-request it once the check reports.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✗ | matches: migrations |
| size | ✓ | 118L, 7F substantive, 344L/9F incl. docs/generated/snapshots — within ceiling |
| tier | ✗ | classified as T2-never: T2-never (344L, 9F, two-areas, feat) |
| stamphog 2.7.0 | .stamphog/policy.yml @ unknown · reviewed head 00f9015 |
|
@PostHog review |
|
@Silthus PostHog Review is reviewing this pull request. |
|
/trunk merge |
|
😎 Stack merged successfully - details. |
|
/trunk cancel |
|
/trunk merge |
1 similar comment
|
/trunk merge |
…t layer Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
Not approved yet — waiting on the conditions below.
The Migration risk check has not finished for this commit, so stamphog cannot tell a safe migration from a risky one yet. The review runs again on the next push, or you can re-request it once the check reports.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✗ | matches: migrations |
| size | ✓ | 118L, 7F substantive, 344L/9F incl. docs/generated/snapshots — within ceiling |
| tier | ✗ | classified as T2-never: T2-never (344L, 9F, two-areas, feat) |
| stamphog 2.7.0 | .stamphog/policy.yml @ unknown · reviewed head e876d99 |
|
/trunk merge |
Moved from Silthus#583.
Re-cut on 2026-11-02 onto #127167, after the scope cut parked local templates and sends by key. This layer keeps only what still applies to a code send.
Part of the code-first rebuild of the email send stack; new in the rebuild, with no closed PR to replace: the event the launch drip campaign exits on.
Problem
Impact
Pays into Grow Workflows: the drip stops for projects that already send from code, so it talks only to those still getting started. In scope: Messaging,
send/email/sends with a project secret API key.Merge Danger
Warning
🟡 Merge risk: medium · one-way door: the migration creates
messaging_firstcodesend, and a captured event stays capturedmessaging_firstcodesend: one row per project, with a unique constraint on the team. The migration is a plainCREATE TABLE. Its team key has no database constraint, so it takes no lock onposthog_team.workflows-email-from-code: the work runs in a commit callback after the send is queued, so it never changes the answer or the mail. After a project's first send, each send with a key that has a creator adds one root-team lookup and oneexists()query. The insert runs once per project.202.Changes
send/email/with a project secret API key capturesemail first code sendfor the person who created that key, with the organization and project as groups. Its properties areteam_idandaddressed_by:template_keyfor a code send,template_idfor a send by id.messaging_firstcodesendrow decides it, through its unique constraint, not through a read and then a write. Retries, replays and parallel sends never capture a second time. A send from a project's other environments counts for the project.workflows-email-from-codeoff, or master'smessaging_templates/{id}/send/route captures nothing and claims nothing.None. The send still answers202.report_first_code_send(team_id, capture), claims the row, captures, and releases the claim when the capture fails. The view only calls it after commit.capture()is its only acknowledgement. An event the client loses after that keeps the claim, and so does a process that dies between the claim and the capture. Then the project never gets the event. A durable outbox is outside this change.How did you test this code?
Test seams:
send/email/through the DRF client (test_transactional_email.py), andreport_first_code_sendfor the once-only rule.Test rationale, each run red first:
captureraises, andcapturereturnsNone.workflows-transactional-email-apion, that claims the event.Proofs at
00f9015(saved withfleet proof), after the merge of #127183 atb9cb59d, which carries #127181, #127167'sfcf42b0, #127123'sc81b9e0and master:e88ee89, analytics captured locally. Every answer, mail and event matches the run at the previous head71e6e79.0016_first_code_sendapplies on top of0015_transactional_email_keyand master's0012_email_hard_bounce, with nothing left unapplied.202and captures nothing.email first code sendon that creator, grouped under the project. A second send and a replay still leave one event. A send from an environment's key groups the event under the parent project.71e6e79: in both regions, every project secret API key that can send has a creator, so the event always has a person to land on. Counts stay in the private ledger.258aef7. A fresh review of the changes since71e6e79(the base merges and the migration rename) found nothing.CREATE TABLEwith a unique constraint onteam_id. Django also adds a plainteam_idindex, which repeats the unique index; on a table with one row per project it costs nothing, so the migration stays as proven. The Python and TypeScript duplication warnings name files outside this stack (warehouse sources and others) and come frommaster. Of the backend coverage gaps, onlytest_transactional_email.pylines 63 to 66 are this layer's: the field annotations of the test-onlyCapturedFirstCodeSenddataclass. Every other uncovered line sits outside this layer's hunks.Earlier live run, 2026-10-31
Live run, 2026-10-31. Django at the top of the stack, which contains this head, with a project secret API key that has a creator. Analytics captured to a local file.
email first code sendeventsaddressed_by: template_keyand the project, organization and instance groupsreplayed: trueProd shape: a gap until read-only production access exists on 2026-11-02.
Release status
Docs update
None.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)
srv2of map Map: send code-managed email templates with one PostHog call Silthus/posthog#461; restacked as lanedj3onto fix(mcp): refuse unknown fields in the email template tools #127183 at3d54a9f, with migration0016_first_code_sendabove master's messaging migrations.qa-swarm,modernize-pr,tdd,writing-tests,django-migrations,writing-dataclasses,pr-description,writing-pr-descriptions.email first code send, once per project, on the key's creator, with the org as a groupTeam(a hot table) or a workflows config rowInvolvement: one brief from Michael; review round 1 (GPT-6.1 Sol, Opus fresh eyes) fixed; dj3 round 1 (GPT-6.1 Sol) fixed, its final read found a dropped event that kept the claim, fixed; QA swarm on 2026-11-01 with two panel rounds (GPT-6.1 Sol review, Opus 5.5 fresh eyes): 3 findings fixed, 2 rejected; live run and shadow diff captured.
🤖 Generated with Claude Code