Skip to content

feat(apify): digest rate cap (1 per artist per 24h) + email_send_log audit trail#762

Open
sweetmantech wants to merge 5 commits into
mainfrom
feat/scrape-digest-rate-cap-audit
Open

feat(apify): digest rate cap (1 per artist per 24h) + email_send_log audit trail#762
sweetmantech wants to merge 5 commits into
mainfrom
feat/scrape-digest-rate-cap-audit

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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_body prefix scrape-digest:).
  • getScrapeDigestRecipients now returns { emails, artistIds } — the artist entity ids are the cap key.
  • maybeSendScrapeDigest: skips when any watched artist digested within 24h; on send, records one email_send_log row per artist entity (scrape-digest:<batchId>, resend id) via the existing best-effort logEmailAttempt.

Tests (RED→GREEN)

  • Cap: recent digest row → no send.
  • Audit: successful send → logEmailAttempt with artist id + sent.
  • Digest suite 9/9; full sweep lib/apify + lib/socials + lib/artist284 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, log every send to email_send_log, and enforce exact non-duplication per batch. This prevents multi-send bursts (chat#1855) and makes sends traceable.

  • New Features
    • Batch idempotency in maybeSendScrapeDigest: skips if a scrape-digest:<batchId> row exists via selectScrapeDigestLogs({ batchId }).
    • 24h per-artist rate cap using selectScrapeDigestLogs({ artistIds, since }), keyed by artistIds from getScrapeDigestRecipients.
    • Audit trail: on send, writes one email_send_log row per artist with raw_body scrape-digest:<batchId> via logEmailAttempt.
    • Idempotent run completion: updateApifyScraperRun updates only where completed_at IS NULL so webhook retries can’t re-trigger a digest.

Written for commit c10c383. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added safeguards to prevent duplicate scrape digest emails.
    • Added a 24-hour sending limit per artist.
    • Added tracking for digest delivery attempts.
  • Bug Fixes

    • Prevented completed scraper runs from being processed again after webhook retries.
    • Improved recipient handling by tracking both email addresses and associated artists.

sweetmantech and others added 4 commits July 6, 2026 16:55
… posts first

The Instagram scrape alert fired whenever a scrape returned posts, with no
comparison against posts already stored — every scrape re-announced the
profile's recent feed as new (observed: 6 of 7 alerts in one day announced
posts up to 10 days old). New reusable filterNewPostUrls diffs candidate
URLs against posts BEFORE upsert; the alert is gated on a non-empty result.
Persistence unchanged (recoupable/chat#1855, PR #3 of 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A roster scrape starts one Apify run per platform, each completing
independently — extending per-platform alerts would mean 4+ emails per
scrape. This registers every run under a batch_id at scrape start
(apify_scraper_runs, columns from recoupable/database#41), records each
webhook completion with its genuinely-new post URLs, and when the batch's
last run completes sends ONE digest (per-platform sections, BCC-only)
via the new digest module. Platforms with nothing new are omitted; a
batch with nothing new sends nothing. Instagram's solo alert is
suppressed for batch runs; legacy/non-batch runs keep today's behavior
(recoupable/chat#1855, PR #4 of 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…new post

Replaces the per-send LLM email body (nondeterministic branding, vendor
jargon reaching customers, no reliable post links) with a shared
deterministic renderer: stable subject/branding, one section per
platform, a direct link to every genuinely-new post, chat CTA secondary.
Used by both the batch digest and the legacy solo Instagram alert, which
now also requires new-post URLs and is BCC-only (recoupable/chat#1855,
PR #5 of 6; supersedes the interim body from PR #4 and, for this file,
the standalone BCC fix in api#758 — same invariant, tests included).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 9, 2026 11:26pm

Request Review

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The scrape digest pipeline now returns recipient emails with artist IDs, prevents duplicate and overly frequent sends through email-log checks, records successful attempts, and makes scraper completion updates idempotent.

Changes

Scrape digest delivery

Layer / File(s) Summary
Idempotent scraper completion
lib/supabase/apify_scraper_runs/updateApifyScraperRun.ts
Completion updates now target only runs whose completed_at is null.
Recipient identity contract
lib/apify/digest/getScrapeDigestRecipients.ts
Recipient lookup returns deduplicated emails and artistIds, including empty arrays when no valid socials exist.
Guarded digest sending
lib/supabase/email_send_log/selectScrapeDigestLogs.ts, lib/apify/digest/maybeSendScrapeDigest.ts
Digest sending checks batch and 24-hour artist logs, sends eligible digests, and records per-artist email attempts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant maybeSendScrapeDigest
  participant selectScrapeDigestLogs
  participant sendScrapeDigestEmail
  participant logEmailAttempt
  maybeSendScrapeDigest->>selectScrapeDigestLogs: Check batch and recent artist logs
  selectScrapeDigestLogs-->>maybeSendScrapeDigest: Return matching records
  maybeSendScrapeDigest->>sendScrapeDigestEmail: Send eligible digest
  sendScrapeDigestEmail-->>maybeSendScrapeDigest: Return send result
  maybeSendScrapeDigest->>logEmailAttempt: Record each artist send
Loading

Possibly related issues

Possibly related PRs

  • recoupable/api#760 — Modifies the same digest pipeline and scraper completion behavior.

Suggested reviewers: cubic-dev-ai

Poem

Digests now pause when logs say “sent,”
Artists keep their day’s consent.
Emails and IDs travel as a pair,
Retries find completion already there,
And every delivery leaves a trace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning maybeSendScrapeDigest (61 lines) and getScrapeDigestRecipients/selectScrapeDigestLogs exceed the 20-line SRP guideline and pack multiple responsibilities. Split gating, recipient lookup, send, and audit logging into smaller helpers/files; keep each exported function to one responsibility and under ~20 lines.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/scrape-digest-rate-cap-audit

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.

❤️ Share

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Confidence score: 2/5

  • In lib/apify/digest/maybeSendScrapeDigest.ts, sendScrapeDigestEmail can record failed Resend attempts as successfully sent because sendEmailWithResend returns a truthy error NextResponse without an id; merging as-is risks inaccurate audit logs and missed retries/triage for real delivery failures — update the success check to require a valid message id (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
Loading

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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
if (sent) {
if (resendId) {

@sweetmantech sweetmantech changed the base branch from feat/scrape-digest-template to main July 9, 2026 23:19
…dit onto the shipped template, add exact non-duplication

Review question (sweetman): why a rate cap instead of properly enforcing
non-duplication? Both, now, as separate concerns:
- Completion claim: updateApifyScraperRun updates WHERE completed_at IS
  NULL — a webhook retry can't re-claim a completed run, re-trigger digest
  assembly, or overwrite the recorded diff (the 3->0 corruption observed
  live during #760 testing).
- Batch idempotency: maybeSendScrapeDigest checks email_send_log for an
  existing scrape-digest:<batchId> row before sending — one digest per
  batch, ever, no time-window heuristics.
- The 24h per-artist cap remains as the product frequency rule from the
  issue spec, reading the same audit rows.
Every send is logged per watched artist (rawBody scrape-digest:<batchId>).
Also deletes 4 orphaned pre-rename supabase files that rode in again via
the merge (same both-added mechanism as #761) and supersedes the branch's
selectRecentScrapeDigestLogs with filter-object selectScrapeDigestLogs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Ported onto main (c10c383) after #761's merge, and — per @sweetmantech's review question — non-duplication is now enforced properly, not just rate-capped. Three separate mechanisms:

  1. Completion claimupdateApifyScraperRun updates WHERE completed_at IS NULL: a webhook retry can't re-claim a completed run, so it can never re-trigger digest assembly or overwrite the recorded diff (the 3→0 new_post_urls corruption observed live during feat(apify): one consolidated new-posts digest per scrape batch #760's testing).
  2. Batch idempotency — before sending, maybeSendScrapeDigest checks email_send_log for an existing scrape-digest:<batchId> row: one digest per batch, ever — exact identity, no time window.
  3. 24h per-artist cap — the product frequency rule from the issue spec, unchanged, reading the same audit rows.

Every send is logged per watched artist entity (this also closes the 'alert sends bypass the audit log' evidence item). Merge also removed 4 orphaned pre-rename supabase files that rode in again (same both-added mechanism as #761). TDD: 6 assembler cases (incl. batch-dedup and cap suppression) + 3 claim-guard tests; 470 tests green across the affected domains; tsc + lint clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/apify/digest/maybeSendScrapeDigest.ts`:
- Around line 34-45: Make the idempotency check and audit-log reservation atomic
in maybeSendScrapeDigest: reserve the batch before sendScrapeDigestEmail using a
pending audit status, or acquire a database-level batch lock/constraint so
concurrent runs cannot both proceed. Update the reservation to sent after
successful delivery and handle failed sends appropriately, using
selectScrapeDigestLogs and logEmailAttempt as the integration points.
- Around line 47-48: Update the audit-trail comment near the batch deduplication
logic to replace “watched artist entity” with the appropriate account-specific
terminology, such as “watched artist account,” while preserving the comment’s
meaning.

In `@lib/supabase/email_send_log/selectScrapeDigestLogs.ts`:
- Line 7: Update the JSDoc for the artistIds parameter to replace “watched
artist entities” with “watched artists” or “watched artist accounts,” following
the project’s account terminology guidelines.
- Around line 10-18: Add an explicit return type to selectScrapeDigestLogs using
the email send log table’s Tables type, with Pick limited to account_id and
created_at, and ensure the implementation returns that typed result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c991acdc-36c1-4039-bfc7-443148ad9cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 8241b61 and c10c383.

⛔ Files ignored due to path filters (2)
  • lib/apify/digest/__tests__/maybeSendScrapeDigest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/apify_scraper_runs/__tests__/updateApifyScraperRun.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (4)
  • lib/apify/digest/getScrapeDigestRecipients.ts
  • lib/apify/digest/maybeSendScrapeDigest.ts
  • lib/supabase/apify_scraper_runs/updateApifyScraperRun.ts
  • lib/supabase/email_send_log/selectScrapeDigestLogs.ts

Comment on lines +34 to +45
// Exact non-duplication: one digest per batch, ever.
const already = await selectScrapeDigestLogs({ batchId });
if (already.length > 0) return null;

// Product frequency cap: at most one digest per watched artist per 24h.
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const recent = await selectScrapeDigestLogs({ artistIds, since });
if (recent.length > 0) return null;

const artistName = sections.map(s => s.artistName).find(Boolean) ?? null;
return await sendScrapeDigestEmail({ emails, sections, artistName });
const sent = await sendScrapeDigestEmail({ emails, sections, artistName });
if (!sent) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

TOCTOU race between idempotency check and audit log write can cause duplicate sends.

The batch idempotency check (selectScrapeDigestLogs({ batchId }) on line 35) and the audit log write (logEmailAttempt on lines 50-58) are not atomic. While updateApifyScraperRun prevents the same run from being claimed twice, two different runs in the same batch completing concurrently can both pass the idempotency check before either writes audit entries. The race window includes the email send (a network call to Resend), widening the vulnerability window.

Consider writing audit entries before sending (e.g., with a pending status, updated to sent after successful delivery), or using a database-level constraint/advisory lock on the batch to ensure atomicity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/apify/digest/maybeSendScrapeDigest.ts` around lines 34 - 45, Make the
idempotency check and audit-log reservation atomic in maybeSendScrapeDigest:
reserve the batch before sendScrapeDigestEmail using a pending audit status, or
acquire a database-level batch lock/constraint so concurrent runs cannot both
proceed. Update the reservation to sent after successful delivery and handle
failed sends appropriately, using selectScrapeDigestLogs and logEmailAttempt as
the integration points.

Comment on lines +47 to +48
// Audit trail: record the send per watched artist entity — what the batch
// dedup and the 24h cap read (the legacy solo path never logged at all).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace "entity" terminology per coding guidelines.

The comment uses "watched artist entity" which violates the guideline to never use entity when referring to system actors.

As per coding guidelines: "Use account terminology, never entity or user, when naming IDs or referring to system actors; use specific terms like artist, workspace, or organization when appropriate."

✏️ Proposed fix
-  // Audit trail: record the send per watched artist entity — what the batch
+  // Audit trail: record the send per watched artist account — what the batch
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Audit trail: record the send per watched artist entity — what the batch
// dedup and the 24h cap read (the legacy solo path never logged at all).
// Audit trail: record the send per watched artist account — what the batch
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/apify/digest/maybeSendScrapeDigest.ts` around lines 47 - 48, Update the
audit-trail comment near the batch deduplication logic to replace “watched
artist entity” with the appropriate account-specific terminology, such as
“watched artist account,” while preserving the comment’s meaning.

Source: Coding guidelines

* Select scrape-digest send-log rows with optional filters.
*
* @param batchId - Rows recording exactly this batch's send (idempotency check).
* @param artistIds - Rows for any of these watched artist entities.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace "entity" terminology per coding guidelines.

The JSDoc uses "watched artist entities" which violates the guideline to never use entity when referring to system actors. Use "artists" or "artist accounts" instead.

As per coding guidelines: "Use account terminology, never entity or user, when naming IDs or referring to system actors; use specific terms like artist, workspace, or organization when appropriate."

✏️ Proposed fix
- * `@param` artistIds - Rows for any of these watched artist entities.
+ * `@param` artistIds - Rows for any of these watched artist accounts.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param artistIds - Rows for any of these watched artist entities.
* `@param` artistIds - Rows for any of these watched artist accounts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/supabase/email_send_log/selectScrapeDigestLogs.ts` at line 7, Update the
JSDoc for the artistIds parameter to replace “watched artist entities” with
“watched artists” or “watched artist accounts,” following the project’s account
terminology guidelines.

Source: Coding guidelines

Comment on lines +10 to +18
export async function selectScrapeDigestLogs({
batchId,
artistIds,
since,
}: {
batchId?: string;
artistIds?: string[];
since?: string;
} = {}) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add an explicit return type annotation.

The function lacks a return type annotation. Path instructions require "Return typed results using Tables<"table_name">". Since only account_id and created_at are selected, use Pick for an accurate typed result.

As per path instructions: "Return typed results using Tables<"table_name">".

✏️ Proposed fix
-export async function selectScrapeDigestLogs({
+export async function selectScrapeDigestLogs({
   batchId,
   artistIds,
   since,
 }: {
   batchId?: string;
   artistIds?: string[];
   since?: string;
 } = {}) {
+): Promise<Pick<Tables<"email_send_log">, "account_id" | "created_at">[]> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/supabase/email_send_log/selectScrapeDigestLogs.ts` around lines 10 - 18,
Add an explicit return type to selectScrapeDigestLogs using the email send log
table’s Tables type, with Pick limited to account_id and created_at, and ensure
the implementation returns that typed result.

Source: Path instructions

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — 2026-07-09

Preview: https://api-j5q1oe7k4-recoup.vercel.app (confirmed built from post-port head c10c383). Isolated natgeo fixture (fan-out audited: exactly ['sweetmantech@gmail.com']), real Apify datasets, controlled webhook deliveries. Each non-duplication mechanism probed independently:

# Check (issue Done-when) Expected Actual on preview Result
1 Batch completes → ONE digest + audit row send recorded in email_send_log digest sent (15 posts / 2 platforms); exactly 1 log row: raw_body: scrape-digest:<batchId>, status: sent, resend_id: 37d5f8f1-…, account_id = watched artist
2 Webhook retry → no re-send (completion claim) replayed delivery can't re-trigger assembly or corrupt the diff identical TikTok re-delivery: no second email, log rows still 1, and new_post_urls stayed 3 (the unguarded code overwrote it to 0 — verified during #760 testing)
3 24h cap suppresses a NEW digest-worthy batch second batch, 12 genuinely new posts, batch complete → no send batch 2 completed with new_post_urls: 12, digest suppressed by the cap; log rows for the artist still exactly 1
4 Batch idempotency is exact, not time-based scrape-digest:<batchId> equality check before every send unit-tested (never sends twice for the same batch) + enforced ahead of the cap in maybeSendScrapeDigest
5 CI all green on c10c383 format/lint/test/CodeRabbit/cubic/Vercel SUCCESS

This closes the last two evidence items from chat#1855: the 7-emails/day class of failure (cap + exact dedup) and "alert sends bypass the email audit log" (every digest send now writes email_send_log).

Fixture fully cleaned, including the test's own audit-log rows (0 posts/socials/runs/logs/artist rows remaining).

Verdict: non-duplication is enforced by identity (completion claim + batch idempotency) with the 24h cap as the separate product frequency rule — ready to merge, closing the chat#1855 chain.

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