Repository navigation
Conversation
…lusions Unfinished: the red/green proof runs and the OpenAPI regeneration were interrupted by a devbox rebalance. Tests are written but not yet run green; generated OpenAPI output is not regenerated. Continue test-first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ender endpoint Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…equire write scope Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…quest_access comment Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…haracters Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A long blocked CC/BCC list wrote one log row per address. Rows now hold as many comma-joined addresses as fit under the untruncated log length, so the row count follows total characters. Every address stays visible. The case-insensitive membership matrix also stores member emails with mixed case, which proves the stored-side case fold. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dbox send Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r day Every sandbox send, test sends included, claims one token per recipient copy from a daily project bucket and one per distinct address from a daily per-address bucket, all or nothing in one Valkey call. A denied claim skips the send and spends nothing. A limiter error, a missing limiter, or an unset or invalid cap setting skips every sandbox send. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…final recipient check A cap setting with trailing text no longer reads as its numeric prefix, and a send the final membership recheck blocks no longer spends daily tokens. The project cap message also reads correctly for a send with more copies than the whole daily cap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Test sends run inline in cdp-api, whose deps had no SES Valkey pool, so every sandbox test send failed closed. cdp-api now gets the same pool as the email workers and claims the same daily caps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he clock in the tier test Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…subtraction Cover the signed-code requirement on the sandbox delivered event, the per-day floor at zero for sandbox sends, and that bounces and complaints of sandbox sends still count. Move the capture property mapping into a switch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e empty A sandbox send with an empty sandbox tenant name, configuration set or From address setting went to SES and failed with a provider error. It now skips before SES with the same message as the switched-off sender, and the worker logs which settings are empty once at startup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…send A sandbox send claims its daily cap tokens right before the SES call. When SES throttled that call, the send was rescheduled and the retry claimed the tokens again, so a project under SES throttling reached its daily sandbox cap with fewer emails than the cap allows. The send now gives its claim back before it is rescheduled, so it spends its tokens once. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merging to
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 |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughThe change adds sandbox email sender provisioning and management, mirrors sandbox SES tenant state to workers, and adds recipient eligibility checks, daily caps, pause checks, and sandbox SES routing to CDP email delivery. It also captures trusted sandbox delivery events and excludes sandbox sends from tier-qualifying send counts. Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to A delayed claim-return replay could allow sandbox sends beyond their daily cap. Extend replay protection to the bucket’s active lifetime before merging, unless that bounded risk is explicitly accepted. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to The sandbox path preserves project ownership and restricts recipients, while ordinary throttling retries reacquire sending limits. However, delayed quota refunds can outlive their duplicate protection, potentially weakening those limits during recovery. Production permission scope and long-delay recovery behavior remain incompletely verified. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (1)
nodejs/src/cdp/services/messaging/email.service.ts-812-814 (1)
812-814: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRestore email queue parameters for every SES throttle retry.
When SES throttles a non-sandbox send, the retry is scheduled without
queueParameters. On dequeue, the executor can resume the Hog VM instead of retrying the email, dropping the send. Restoreparamsfor every throttled send:Suggested fix
if (throttled) { - if (isSandbox) { - result.invocation.queueParameters = params - } + result.invocation.queueParameters = params
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
b627d3f0-6b80-4af7-a200-69c0c91c4f95
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlproducts/integrations/frontend/generated/api.tsis excluded by!**/generated/**
📒 Files selected for processing (43)
.github/scripts/check-idor-model-coverage.pydocs/internal/workflows-email-sending-tiers.mdnodejs/package.jsonnodejs/src/cdp/cdp-api.tsnodejs/src/cdp/cdp-services.tsnodejs/src/cdp/config.tsnodejs/src/cdp/services/managers/organization-members.service.tsnodejs/src/cdp/services/managers/sandbox-sender-state.service.tsnodejs/src/cdp/services/messaging/email-tracking.service.test.tsnodejs/src/cdp/services/messaging/email-tracking.service.tsnodejs/src/cdp/services/messaging/email.service.test.tsnodejs/src/cdp/services/messaging/email.service.tsnodejs/src/cdp/services/messaging/helpers/ses.tsnodejs/src/cdp/services/messaging/sandbox-email-sender.tsnodejs/src/cdp/services/rate-limiter/rate-limiter.service.tsnodejs/src/cdp/types.tsnodejs/src/cdp/utils.tsnodejs/src/server.tsposthog/api/integration.pyposthog/models/integration/__init__.pyposthog/models/integration/email.pyposthog/plugins/plugin_server_api.pyposthog/settings/ses.pyproducts/integrations/mcp/tools.yamlproducts/workflows/backend/facade/api.pyproducts/workflows/backend/facade/contracts.pyproducts/workflows/backend/migrations/0030_sandboxsendertenantstate.pyproducts/workflows/backend/migrations/max_migration.txtproducts/workflows/backend/models/__init__.pyproducts/workflows/backend/models/sandbox_sender_tenant_state.pyproducts/workflows/backend/presentation/views/hog_flow.pyproducts/workflows/backend/providers/ses.pyproducts/workflows/backend/services/email_sending_tier.pyproducts/workflows/backend/services/sandbox_sender.pyproducts/workflows/backend/services/ses_tenant_events.pyproducts/workflows/backend/services/ses_tenant_state.pyproducts/workflows/backend/tasks/ses_tenant_state.pyproducts/workflows/backend/test/test_email_sending_tier.pyproducts/workflows/backend/test/test_sandbox_sender_lifecycle.pyproducts/workflows/backend/test/test_ses_tenant_events.pyproducts/workflows/backend/test/test_ses_tenant_state.pyproducts/workflows/backend/tests/api/test_email_reputation.pyproducts/workflows/backend/tests/api/test_sandbox_sender.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.
ioredis resends a command whose reply it lost when it reconnects. A replayed claim return would credit the daily cap buckets twice and let sends past the cap. Each grant now carries a claim ID, and the return script sets a short-lived marker for it first, so a second return of the same claim changes nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
cd387d67-c3a5-4e32-a1c7-9eebcabeb0c6
📒 Files selected for processing (3)
nodejs/src/cdp/services/messaging/sandbox-email-sender.tsnodejs/src/cdp/services/rate-limiter/rate-limiter.service.test.tsnodejs/src/cdp/services/rate-limiter/rate-limiter.service.ts
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 0 remain after this review.
ioredis keeps an unanswered command and resends it after it reconnects, which can be long after the return marker expired. Each return now carries a deadline a few minutes out, and the return script ignores it after that, so a late replay cannot credit the buckets again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Note 🤖 Claude Opus 5.5 responding on behalf of Michael @coderabbitai on restoring |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
ast-grep outline nodejs/src/cdp/services/messaging/email.service.ts --match 'executeSendEmail|sendEmail' --view expanded
rg -n -C 8 'queueParameters|Throttling|returnDailyCaps' nodejs/src/cdp/services/messaging/email.service.tsLength of output: 8321
✏️ Learnings added
You are interacting with an AI system. |
Keep master UTM tagging and throttle retries. Append the organization footer to delivered and stored content, disable tracking for sandbox sends, and fold in the empty-route guard from PostHog#112071. Co-Authored-By: Codex GPT 6.1 Sol <noreply@openai.com>
Keep master UTM tagging and throttle retries. Append the organization footer to delivered and stored content, disable tracking for sandbox sends, and fold in the empty-route guard from PostHog#112071. Co-Authored-By: Codex GPT 6.1 Sol <noreply@openai.com>
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the |
Problem
Two sandbox sender sends misbehave when the deployment or SES is not in the expected state:
Stacked on #111904. This cumulative fork PR targets
master. The review delta isd9ef8d19c62...fix/workflows-sandbox-send-guards.Note
🔵 Merge risk: low (merge-risk rubric)
WORKFLOWS_SANDBOX_SENDER_ENABLED, the existing claim scripts are unchanged, and the new paths have focused tests against real Valkey.Changes
switch_off, because Django already treats empty sandbox settings as not provisioned.RateLimiterService.returnClaimadds the tokens back to each bucket that still exists. Reads cap the pool at capacity, so a return can never raise the limit above it.TIME. A replay that arrives after the marker expires is dropped.Why return the claim instead of carrying it with the rescheduled send: carrying it needs a marker on the queued email parameters, which the retry must trust. Returning keeps the cap state in Valkey only, and the retry re-runs every sandbox check (pause gate, membership, caps) as before. A return that fails leaves the tokens spent, which only makes the cap stricter.
How did you test this code?
The ticket seams are
EmailService.executeSendEmailinemail.service.test.ts, with real Postgres fixtures, the real Valkey rate limiter, and SES mocked atsesV2Client.send. The replay guard has its own seam onRateLimiterServiceagainst real Redis.switch_offevent, and that an own-domain send on the same service still goes out. The three new rows failed before the fix.returnClaimcases inrate-limiter.service.test.ts. A return sent twice credits once, and a return that reaches Valkey after its deadline credits nothing. Each failed before its fix.Verify, in
nodejs:Expected at
b6e1ba75e0f:Tests: 268 skipped, 11 passed, 279 total(7 empty-settings rows, 2 throttle cases, 2 return cases).Local gate at
b6e1ba75e0f:email.service.test.ts,email-tracking.service.test.ts, the rate limiter suites including the Valkey cluster test, eslint, oxlint, prettier, security semgrep on the touched files, and strict preflight. Node-wide TypeScript reports only the unbuilt@posthog/replay-anonymizerworkspace package, which this PR does not touch.Not checked: a real SES throttle response against the sandbox tenant, or a real ioredis reconnect replay. Fork CI waits for maintainer approval, so no CI run exists yet.
Release status
Both changes only affect sandbox sends, which exist only behind
workflows-sandbox-senderand send only whileWORKFLOWS_SANDBOX_SENDER_ENABLEDis on.Automatic notifications
Docs update
None. No new setting. The empty-settings behavior matches the existing rule that empty sandbox settings mean "not provisioned".
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (wrote all code in this PR). Reviews by GPT-6.1 Sol (Codex) and Claude Opus 5.5.
tdd,/writing-tests,/writing-pr-descriptions,/reviewing-with-coderabbit,qa-swarm,merge-risk.switch_offwas kept, as above.masterand outside this PR, so it is a follow-up.example.com.🤖 Generated with Claude Code