Skip to content

fix(outreach): do not rest a campaign for a quota failure - #195

Merged
ralyodio merged 1 commit into
masterfrom
fix-quota-failure-backoff
Aug 13, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix-quota-failure-backoff

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why this exists separately

This commit was pushed to #194 before it merged, but GitHub's PR index was lagging and the squash only picked up the first commit. serpCreditsExhausted is not on master. Re-opening it on its own so it actually lands.

Verified against master:

$ git show origin/master:lib/alerts/valueserp.ts | grep -c serpCreditsExhausted
0

What it fixes

#194 shipped a back-off that rests a campaign whose discovery pass produced nothing. But an empty ValueSERP plan answers HTTP 402 without running the queries at all, which is indistinguishable from a query list with nothing left to give.

So as master stands right now, switching the paused campaigns back on before the 16:40 UTC quota reset would:

  1. run discovery, get 402 on every search,
  2. produce nothing → increment discovery_dry_streak,
  3. climb to the 24h back-off ceiling within a few hours,
  4. leave all three campaigns resting exactly when the credits refill.

Because the plan is one shared monthly bucket, this hits every campaign at once, every month. It converts a self-healing 12-hour outage into a ~36-hour one.

serpCreditsExhausted() exposes the cooldown #194 already tracks, so a pass with no credits behind it declines to vote and leaves the streak alone. The first pass that actually reaches Google is the one that decides whether the queries still work.

Tests

Two directions, because the distinction is the whole point:

  • a 402 sets the exhausted state and resetSerpCreditCooldown() clears it
  • an ordinary 400 does not report exhaustion — a genuinely bad query still counts against the back-off

npx tsc --noEmit clean, npx vitest run 1436 passed / 0 failed.

Note

The three campaigns are still paused pending this. Re-enabling them is safe once this deploys, or after 16:40 UTC either way.

🤖 Generated with Claude Code

The back-off asks whether a discovery pass produced anything, but an empty
ValueSERP plan answers HTTP 402 without running the queries at all — which
looks identical to a query list with nothing left to give.

Left as it was, the month-end 402 that prompted this work would have rested
every campaign for up to a day, and since the plan is one shared monthly
bucket they would all have been resting by the time it refilled, pushing
recovery well past the reset it was supposed to ride out.

serpCreditsExhausted() exposes the cooldown the previous commit already
tracked, so a pass with no credits behind it declines to vote and leaves the
streak alone. The first pass that actually reaches Google is the one that
decides whether the queries still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

35 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 27 | LOW: 3

Severity Rule Location
HIGH secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
HIGH tls-verification-disabled lib/onion.ts:47
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
HIGH secret-generic-credential lib/sp/platforms/linkedin.ts:25
HIGH sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM js-unescaped-html-sink app/(app)/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM redos-nested-quantifier lib/careers/jobs.ts:139
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:130
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:93
MEDIUM js-dynamic-code-execution tests/careers-page-templates.test.ts:21
MEDIUM js-dynamic-code-execution tests/careers-widget-script.test.ts:69
MEDIUM js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
MEDIUM js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/coinpay.test.ts:4
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit b66d1e4 into master Aug 13, 2026
8 checks passed
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