Skip to content

feat(messaging): capture email first code send once per project - #583

Draft
Silthus wants to merge 3 commits into
email-v2/10-mcp-strict-email-bodiesfrom
email-v2/11-first-code-send-event
Draft

Silthus wants to merge 3 commits into
email-v2/10-mcp-strict-email-bodiesfrom
email-v2/11-first-code-send-event

Conversation

@Silthus

@Silthus Silthus commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

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

  • The launch drip campaign for sending email from code has no signal that a project has started sending. A developer who already sends keeps getting the emails that tell them how to start.
  • The drip needs one event per project, on the person who set up the sending, and never twice.

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 captured

  • New table messaging_firstcodesend: one row per project, with a unique constraint on the team. The migration is a plain CREATE TABLE. Its team key has no database constraint, so it takes no lock on posthog_team.
  • On the send path, behind workflows-email-from-code: after a project's first send, each send with a key that has a creator adds one root-team lookup and one exists() query. The insert runs once per project.
  • A failed capture is logged, and the send still answers 202.

Changes

Kind Files Lines
Production 8 +134 −4
Tests 1 +147 −4
  • The first new send on send/email/ with a project secret API key captures email first code send for the person who created that key, with the organization and project as groups. Its properties are team_id and addressed_by (template_key, key or id).
  • It fires once per project. The messaging_firstcodesend row 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.
  • A replay, a refused send, a personal API key or the flag off captures nothing.
  • A key with no creator captures nothing and claims nothing. That includes a creator deleted between the send and its commit callback. The next send with a key that has a creator still captures, so the event always lands on a person.
  • After the first send, a lookup skips the insert. The insert and its constraint still settle a race.

How did you test this code?

Test seams: send/email/ through the DRF client (test_transactional_email.py), and record_first_code_send for the once-only rule.

Test rationale, each run red first:

  • The first send of a project that captures nothing, or captures for someone other than the key's creator.
  • A replay, a key with no creator, a personal API key, a refused send or the flag off that captures.
  • A creator deleted before the commit callback whose send claims the project's row, so a later send never captures.
  • A failed capture that fails the send.
  • A second insert for the same project that succeeds, or a lost race that raises instead of answering "already recorded". This pins the unique constraint as the arbiter. It doesn't run two connections at once: it relies on PostgreSQL to settle two racing inserts on a unique index.
  • A second environment of the same project that records again.
  • A later send that still runs the insert.

Proofs: a live run on a local stack, Django at this head, with cdp-api and the workers at #580. Capture is replaced by a recorder, so no event leaves the machine (saved with fleet proof).

Case Answer email first code send captured Rows
Send with a key that has no creator 202, delivered 0 0
First send with the creator's key 202, delivered 1, on the creator, with the organization and project groups 1
Second send, new idempotency key 202, delivered none new 1
Retry of that send, same idempotency key 202 replayed: true, no second email none new 1

Prod shape: a gap until read-only production access exists on 2026-11-02.

CI: the fork has no Depot runners, so those jobs never start, and no secret for the project-board app, so flags-project-board fails; the other GitHub-hosted checks pass. The lane check passes: the PR changes only the Python side.

Release status

  • This change is behind a feature flag and is not available to users

Docs update

None.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

Question Answer Who Where
Which event ends the launch drip? email first code send, once per project, on the key's creator, with the org as a group 👤 Human map #461, 2026-10-25
How is "once" enforced? A unique constraint, not a read and then a write 👤 Human map #461, 2026-10-25
Does a send with a key that has no creator use up the project's event? No: it captures nothing and claims nothing, so a later send with a creator still captures 🤖 Agent pending approval
Where does the once-per-project row live? A messaging table with one row per project, not Team (a hot table) or a workflows config row 🤖 Agent pending approval

Involvement: one brief from Michael; review round 1 (GPT-6.1 Sol, Opus fresh eyes) fixed; live run captured.

🤖 Generated with Claude Code

@Silthus
Silthus force-pushed the email-v2/11-first-code-send-event branch from 6716e7a to dc584c1 Compare October 26, 2026 04:05
Silthus and others added 3 commits October 26, 2026 04:38
The first new send on send/email/ behind a project secret API key
captures `email first code send` for the key's creator, with the org
group, so the launch drip can exit on it. A unique row per team in
messaging_firstcodesend keeps it to one event, also across retries and
parallel sends. A key without a creator captures nothing, and a failed
capture is logged without touching the send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A send now checks for the project's row before it inserts, so only the
first send of a project writes. The unique row still settles two sends
that both miss the check. The key's creator loads only when the send
claims the row, and the addressing mode binds before the commit hook.

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

The commit callback now checks for an existing row first, then resolves
the key's creator from the database, and claims the project's row only
when that creator still exists. A send whose creator was deleted before
the callback no longer uses up the event for a later send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus force-pushed the email-v2/10-mcp-strict-email-bodies branch from f52712d to fe51fc2 Compare October 26, 2026 05:30
@Silthus
Silthus force-pushed the email-v2/11-first-code-send-event branch from dc584c1 to 42bfa91 Compare October 26, 2026 05:30

This branch has not been deployed

No deployments
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