feat(apify): digest rate cap (1 per artist per 24h) + email_send_log audit trail#762
feat(apify): digest rate cap (1 per artist per 24h) + email_send_log audit trail#762sweetmantech wants to merge 1 commit into
Conversation
Caps the digest at one per watched artist entity per 24h and records every digest send in email_send_log (the solo alert path bypassed the log entirely — the 7-email burst on chat#1855 was only discoverable via the Resend admin endpoint). Recipients resolver now also returns the watched artist entity ids for the cap key (recoupable/chat#1855, PR #6 of 6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
1 issue found across 4 files
Confidence score: 2/5
- In
lib/apify/digest/maybeSendScrapeDigest.ts,sendScrapeDigestEmailcan record failed Resend attempts as successfully sent becausesendEmailWithResendreturns a truthy errorNextResponsewithout anid; merging as-is risks inaccurate audit logs and missed retries/triage for real delivery failures — update the success check to require a valid messageid(or explicit success flag) before writing a "sent" audit entry.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/apify/digest/maybeSendScrapeDigest.ts">
<violation number="1" location="lib/apify/digest/maybeSendScrapeDigest.ts:39">
P1: Failed email sends are logged as "sent" in the audit trail. `sendScrapeDigestEmail` calls `sendEmailWithResend`, which returns a `NextResponse` error object (truthy, but no `id`) when Resend returns an error. The `if (sent)` guard incorrectly treats this as a successful delivery, so `logEmailAttempt` records `status: "sent"` even though the email was never sent. The `resendId` is `undefined` in this case, so the audit trail has rows claiming a successful send with a null Resend ID — no way to distinguish real deliveries from API failures. Fix: gate on `resendId` instead of `sent`, since a successful Resend response always has an `id`.</violation>
</file>
Architecture diagram
sequenceDiagram
participant M as maybeSendScrapeDigest
participant R as getScrapeDigestRecipients
participant C as selectRecentScrapeDigestLogs
participant E as sendScrapeDigestEmail
participant L as logEmailAttempt
participant DB as Supabase (email_send_log)
Note over M,DB: Scrape digest orchestration with rate cap & audit trail
M->>M: Fetch batch runs & extract sections of new posts
alt No new posts in batch
M->>M: Return null immediately
else New posts found
M->>R: getScrapeDigestRecipients(socialIds)
R-->>M: { emails, artistIds }
Note over M,C: Rate cap: check if any artist was already alerted in last 24h
M->>C: selectRecentScrapeDigestLogs(artistIds, since)
C->>DB: SELECT account_id, created_at FROM email_send_log WHERE account_id IN artistIds AND raw_body LIKE 'scrape-digest:%' AND created_at >= since
DB-->>C: matching rows (or empty)
C-->>M: recent log rows
alt Recent log exists for any artist
M->>M: Return null (skip send)
else No recent log
M->>E: sendScrapeDigestEmail({ emails, sections })
E-->>M: Resend response (id)
Note over M,L: Audit trail: record send per watched artist entity
loop For each artistId
M->>L: logEmailAttempt({ rawBody: scrape-digest:{batchId}, status: sent, accountId: artistId, resendId})
L->>DB: INSERT into email_send_log
DB-->>L: ok
end
M->>M: Return sent response
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // Audit trail: record the send per artist entity so alerts are debuggable | ||
| // and the cap has something to read (the solo path never logged at all). | ||
| const resendId = (sent as { id?: string } | null)?.id; | ||
| if (sent) { |
There was a problem hiding this comment.
P1: Failed email sends are logged as "sent" in the audit trail. sendScrapeDigestEmail calls sendEmailWithResend, which returns a NextResponse error object (truthy, but no id) when Resend returns an error. The if (sent) guard incorrectly treats this as a successful delivery, so logEmailAttempt records status: "sent" even though the email was never sent. The resendId is undefined in this case, so the audit trail has rows claiming a successful send with a null Resend ID — no way to distinguish real deliveries from API failures. Fix: gate on resendId instead of sent, since a successful Resend response always has an id.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/apify/digest/maybeSendScrapeDigest.ts, line 39:
<comment>Failed email sends are logged as "sent" in the audit trail. `sendScrapeDigestEmail` calls `sendEmailWithResend`, which returns a `NextResponse` error object (truthy, but no `id`) when Resend returns an error. The `if (sent)` guard incorrectly treats this as a successful delivery, so `logEmailAttempt` records `status: "sent"` even though the email was never sent. The `resendId` is `undefined` in this case, so the audit trail has rows claiming a successful send with a null Resend ID — no way to distinguish real deliveries from API failures. Fix: gate on `resendId` instead of `sent`, since a successful Resend response always has an `id`.</comment>
<file context>
@@ -20,8 +22,31 @@ export async function maybeSendScrapeDigest(batchId: string | null | undefined)
+ // Audit trail: record the send per artist entity so alerts are debuggable
+ // and the cap has something to read (the solo path never logged at all).
+ const resendId = (sent as { id?: string } | null)?.id;
+ if (sent) {
+ await Promise.all(
+ artistIds.map(artistId =>
</file context>
| if (sent) { | |
| if (resendId) { |
Summary
The last slice of recoupable/chat#1855 (PR #6 of 6, stacked on api#761): even with the newness diff, repeated scrapes of an artist in one day (agent runs, verification, retries) could digest repeatedly — evidence on the issue: 7 alerts to one customer in a day. This caps digests at one per watched artist entity per 24h and gives them an audit trail: the old alert path called Resend directly and left zero rows in
email_send_log, so the platform had no record of who was alerted when.What changed
lib/supabase/email_send_log/selectRecentScrapeDigestLogs.ts(new): digest-log lookback by artist ids (raw_bodyprefixscrape-digest:).getScrapeDigestRecipientsnow returns{ emails, artistIds }— the artist entity ids are the cap key.maybeSendScrapeDigest: skips when any watched artist digested within 24h; on send, records oneemail_send_logrow per artist entity (scrape-digest:<batchId>, resend id) via the existing best-effortlogEmailAttempt.Tests (RED→GREEN)
logEmailAttemptwith artist id +sent.lib/apify+lib/socials+lib/artist→ 284 tests passed; tsc at exact baseline parity (200); eslint clean.Stack
database#41 → api#759 → api#760 → api#761 → this. Done-when for the chain (from the issue): a full artist scrape produces exactly one digest listing each platform's new posts; re-running back-to-back produces none the second time.
🤖 Generated with Claude Code
Summary by cubic
Cap scrape digests to one per watched artist per 24h and add an audit trail by logging each send to
email_send_log. This prevents multi-send bursts (chat#1855) and makes sends traceable.maybeSendScrapeDigest: sends at most once per artist in 24h usingselectRecentScrapeDigestLogs.email_send_logrow per artist withraw_bodyscrape-digest:<batchId>vialogEmailAttempt.getScrapeDigestRecipientsnow returns{ emails, artistIds }to key the cap; tests updated to cover cap and logging.Written for commit 68bd515. Summary will update on new commits.