Skip to content

Fix rc-entitlement-drift-check aborting on transient RC 5xx#85

Merged
Patchalv merged 2 commits into
mainfrom
fix/mapvault-j-rc-drift-check-503
Jul 16, 2026
Merged

Fix rc-entitlement-drift-check aborting on transient RC 5xx#85
Patchalv merged 2 commits into
mainfrom
fix/mapvault-j-rc-drift-check-503

Conversation

@Patchalv

@Patchalv Patchalv commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes MAPVAULT-J: a single transient 503 from RevenueCat's GetCustomer API during the drift-check cron's per-profile loop threw all the way to the top-level handler, aborting drift detection for every other profile in that run — for what was just one flaky API call.
  • Extracts the RevenueCat HTTP client into supabase/functions/rc-entitlement-drift-check/rc-client.ts (dependency-free, no Sentry/Supabase imports) with checkCustomerActivePremiumWithRetry() (one retry on 5xx/timeout/network error, never on 4xx) and classifyProfilesDrift(), which isolates a still-failing customer instead of throwing.
  • Adds a separately-fingerprinted Sentry signal (rc_drift_check_partial_failure) for runs with incomplete coverage, so it never collapses with or suppresses the existing rc_entitlement_drift alert. Heartbeat log and response body gain a failed_count field.
  • Updates docs/payments.md with the new signal and operator guidance.
  • Adds the first CI for this repo: two separate GitHub Actions workflows — deno-tests.yml (runs the new rc-client.test.ts suite via Deno) and lint-typecheck.yml (runs the existing npm run lint / npm run typecheck, previously only enforced locally via husky).

Test plan

  • deno test --allow-net supabase/functions — 5/5 pass (retry-then-succeed, retry-exhausted-isolated, 404 healthy path, 4xx fail-fast, batch isolation via the real classifyProfilesDrift)
  • deno check rc-client.ts — clean
  • npm run typecheck / npm run lint — clean (verified passing with no .env present, matching a CI checkout)
  • Reviewed via two rounds of subagent code review; both new CI workflow files independently verified to run and pass locally
  • Manual staging invocation of the deployed function to confirm failed_count appears in real logs (not done in this session — recommend before/after deploy)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved entitlement drift checks by retrying temporary RevenueCat failures.
    • Prevented individual customer lookup failures from stopping the full drift check.
    • Added separate reporting for incomplete checks and drift findings.
    • Included failed-check counts in drift check results.
  • Documentation

    • Clarified partial-coverage behavior, retries, and failure reporting.
  • Tests

    • Added coverage for retries, permanent failures, and isolated customer errors.
  • Chores

    • Added automated Deno tests, linting, and type-checking for pull requests.

A single 503 from RevenueCat's GetCustomer API during the per-profile
loop threw all the way to the top-level handler, aborting drift
detection for every other profile in that run. Retry once on
transient failures (5xx/timeout/network error) and isolate a
still-failing customer instead of throwing, reporting incomplete
coverage via a separately-fingerprinted Sentry event so it never
collapses with the existing drift alert.

Also adds the first CI for this repo: separate GitHub Actions
workflows for the new Deno edge-function tests and for the existing
npm lint/typecheck scripts (previously only enforced locally via
husky).

Fixes MAPVAULT-J

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Patchalv, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b0f1095-5496-41ec-b363-6fda1ae57923

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd5d16 and 433409f.

📒 Files selected for processing (5)
  • .github/workflows/deno-tests.yml
  • .github/workflows/lint-typecheck.yml
  • docs/payments.md
  • supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.ts
📝 Walkthrough

Walkthrough

Changes

The pull request centralizes RevenueCat entitlement checks, adds retry-aware drift classification and partial-failure reporting, updates payment documentation, and introduces GitHub Actions workflows for Deno tests, linting, and typechecking.

Entitlement drift reliability

Layer / File(s) Summary
RevenueCat client and retry contracts
supabase/functions/rc-entitlement-drift-check/rc-client.ts
The shared client resolves the premium entitlement, handles RevenueCat response classes, retries transient failures, and returns structured customer-check results.
Profile classification and isolation
supabase/functions/rc-entitlement-drift-check/rc-client.ts, supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
Profile checks are classified into missing, stale, and failed buckets, with tests covering retries, non-retryable responses, and batch continuation.
Handler integration and partial-failure reporting
supabase/functions/rc-entitlement-drift-check/index.ts, docs/payments.md
The handler uses shared classification, emits separate drift and partial-failure Sentry events, returns failed_count, and documents partial coverage behavior.

Continuous integration validation

Layer / File(s) Summary
Pull request validation workflows
.github/workflows/deno-tests.yml, .github/workflows/lint-typecheck.yml
Pull requests run Deno tests for function changes plus Node-based lint and typecheck commands with read-only repository access.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preventing rc-entitlement-drift-check from aborting on transient RevenueCat 5xx failures.
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.

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.

@Patchalv Patchalv self-assigned this Jul 16, 2026

@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

🧹 Nitpick comments (1)
supabase/functions/rc-entitlement-drift-check/rc-client.test.ts (1)

29-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the network-error and timeout retry branches.

The retry contract includes TypeError network failures and timeout/abort DOMExceptions, but tests only exercise HTTP responses. Extend the fetch stub to return rejected promises and verify both branches retry exactly once.

Also applies to: 43-121

🤖 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 `@supabase/functions/rc-entitlement-drift-check/rc-client.test.ts` around lines
29 - 36, Extend withStubbedFetch and the related tests to support rejected fetch
promises, then add cases covering TypeError network failures and timeout/abort
DOMExceptions. Assert each failure is retried exactly once and that the
subsequent successful response completes the expected flow.
🤖 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 @.github/workflows/deno-tests.yml:
- Line 16: Add persist-credentials: false to the actions/checkout@v4 step in
both .github/workflows/deno-tests.yml (lines 16-16) and
.github/workflows/lint-typecheck.yml (lines 13-13), preserving the existing
checkout behavior while preventing the GitHub token from being stored in local
git configuration.
- Line 32: Update the Deno test command in the workflow to remove the broad
--allow-net permission, leaving the supabase/functions test suite without
outbound network access. Do not add replacement permissions; future
network-dependent tests should scope access to their specific host.

In `@docs/payments.md`:
- Line 101: Update the stale “What it does” procedure step around the RevenueCat
customer listing to describe loading Supabase profiles and performing individual
RevenueCat customer lookups, matching the later documented workflow instead of
pagination through all RevenueCat customers.

In `@supabase/functions/rc-entitlement-drift-check/rc-client.ts`:
- Around line 6-7: Bound the sequential profile-check retry path by an overall
deadline shorter than the 10-minute lock lifetime, rather than allowing each
profile to consume its full retry budget indefinitely. Before each attempt and
between profiles, check the remaining time; stop processing when the budget is
exhausted and classify all untouched profiles as failed. Apply this to the retry
logic associated with RC_CUSTOMER_CHECK_MAX_ATTEMPTS and
RC_CUSTOMER_CHECK_RETRY_DELAY_MS while preserving existing per-profile retry
behavior within the remaining budget.

---

Nitpick comments:
In `@supabase/functions/rc-entitlement-drift-check/rc-client.test.ts`:
- Around line 29-36: Extend withStubbedFetch and the related tests to support
rejected fetch promises, then add cases covering TypeError network failures and
timeout/abort DOMExceptions. Assert each failure is retried exactly once and
that the subsequent successful response completes the expected flow.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2c2d086-4663-49f0-91f1-a80a292241b4

📥 Commits

Reviewing files that changed from the base of the PR and between 138c2e2 and 0fd5d16.

📒 Files selected for processing (6)
  • .github/workflows/deno-tests.yml
  • .github/workflows/lint-typecheck.yml
  • docs/payments.md
  • supabase/functions/rc-entitlement-drift-check/index.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript strict mode with no any types
Use ES module imports (import/export), not require
Destructure imports: import { useState } from 'react'
Use @/ path alias for all imports; never use relative imports across directories
File naming: kebab-case for files, PascalCase for component exports
Use track(), identifyUser(), resetUser(), updateUserProperties() from lib/analytics.ts — never call PostHog directly
Check FEATURE_FLAGS.featureName to gate behavior; add or toggle flags in lib/feature-flags.ts
Use supabase-js SDK for all queries (auto-handles auth tokens)
Never hardcode API keys. Use environment variables via .env

**/*.{ts,tsx}: Use TypeScript strict mode with no any types
Use functional components only, never class components
Use ES module imports (import/export), not require
Destructure imports: import { useState } from 'react'
Use @/ path alias for all imports instead of relative imports across directories
File naming: use kebab-case for files, PascalCase for component files
Use className prop for all styling with NativeWind; never use StyleSheet.create
Call Edge Functions via supabase.functions.invoke('fn-name', { body: {...} })
Use router.push() or router.replace() from expo-router for navigation
Use <Link href=""> from expo-router for navigation links
Use track(), identifyUser(), resetUser(), updateUserProperties() from lib/analytics.ts; never call PostHog directly
Check FEATURE_FLAGS.featureName to gate behavior; add or toggle flags in lib/feature-flags.ts
Use supabase-js SDK for all queries (auto-handles auth tokens)
Mutations enforcing business rules (freemium limits, invites) must go through Edge Functions, not direct client inserts
Never hardcode API keys; use environment variables via .env
When creating Supabase queries, always handle the error case

Files:

  • supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.ts
  • supabase/functions/rc-entitlement-drift-check/index.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{tsx,ts}: Use functional components only; no class components
Use className prop for all styling; never use StyleSheet.create
Import View, Text, Pressable from react-native (NativeWind patches them)
Conditional classes: use template literals with ternary operators
Bottom sheets (@gorhom/bottom-sheet) use their own styling API, not className
Use router.push() / router.replace() from expo-router for navigation
All user-visible strings must use t() from react-i18next — never hardcode strings in JSX or Alert calls

Files:

  • supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.ts
  • supabase/functions/rc-entitlement-drift-check/index.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

supabase/functions/**/*.ts: Mutations that enforce business rules (freemium limits, invites) go through Edge Functions, not direct client inserts
Always deploy Edge Functions with --no-verify-jwt — functions validate auth internally via auth.getUser()
Scheduled background work uses pg_cron + pg_net; bearers for cron → Edge Function calls live in supabase_vault, not function env vars

Files:

  • supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.ts
  • supabase/functions/rc-entitlement-drift-check/index.ts
🧠 Learnings (2)
📚 Learning: 2026-03-18T13:36:07.355Z
Learnt from: Patchalv
Repo: Patchalv/mapvault PR: 10
File: supabase/functions/grant-beta-premium/index.ts:78-89
Timestamp: 2026-03-18T13:36:07.355Z
Learning: In Patchalv/mapvault, Supabase Database Webhooks are handled by Edge Functions that are fire-and-forget. Do not return non-200 responses for errors; always return 200 for webhook endpoints. Surface errors to Sentry for manual recovery, not as 5xx retries. This guideline applies to all Edge Functions triggered by Supabase Database Webhooks in the repository.

Applied to files:

  • supabase/functions/rc-entitlement-drift-check/rc-client.test.ts
  • supabase/functions/rc-entitlement-drift-check/rc-client.ts
  • supabase/functions/rc-entitlement-drift-check/index.ts
📚 Learning: 2026-03-12T20:10:29.256Z
Learnt from: Patchalv
Repo: Patchalv/mapvault PR: 6
File: docs/mailerlite-integration-prd.md:98-129
Timestamp: 2026-03-12T20:10:29.256Z
Learning: Do not flag markdownlint warnings (e.g., MD040 fenced-code-language) for files under the docs/ directory in this repository. These are internal PRD/documentation files that are not shipped code, and lint churn on documentation should be avoided. Apply this guideline broadly to all markdown files within docs/ (docs/**/*.md); if stricter linting is ever required for docs, consider an explicit CI exception or documentation-only lint setting.

Applied to files:

  • docs/payments.md
🪛 LanguageTool
docs/payments.md

[style] ~101-~101: Consider using “who” when you are referring to a person instead of an object.
Context: ...t) before being given up on; a customer that still fails isn't retried further withi...

(THAT_WHO)

🪛 zizmor (1.26.1)
.github/workflows/deno-tests.yml

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

.github/workflows/lint-typecheck.yml

[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🔇 Additional comments (4)
supabase/functions/rc-entitlement-drift-check/rc-client.ts (1)

1-5: LGTM!

Also applies to: 8-70, 77-122, 144-180, 188-203

supabase/functions/rc-entitlement-drift-check/rc-client.test.ts (1)

9-28: LGTM!

Also applies to: 123-180

supabase/functions/rc-entitlement-drift-check/index.ts (1)

4-19: LGTM!

Also applies to: 117-136, 163-199

docs/payments.md (1)

96-99: LGTM!

Comment thread .github/workflows/deno-tests.yml
Comment thread .github/workflows/deno-tests.yml Outdated
Comment thread docs/payments.md
Comment on lines +6 to +7
const RC_CUSTOMER_CHECK_MAX_ATTEMPTS = 2;
const RC_CUSTOMER_CHECK_RETRY_DELAY_MS = 300;

@coderabbitai coderabbitai Bot Jul 16, 2026

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

Bound the sequential retry path to the lock lifetime.

Each timed-out profile can consume roughly 20.3 seconds across two attempts. At 50 profiles, this run can exceed 17 minutes, while index.ts considers the lock stale after 10 minutes, allowing overlapping drift checks.

Add an overall deadline or circuit breaker and classify untouched profiles as failed once the remaining time budget is exhausted.

Also applies to: 71-76, 123-143, 181-187

🤖 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 `@supabase/functions/rc-entitlement-drift-check/rc-client.ts` around lines 6 -
7, Bound the sequential profile-check retry path by an overall deadline shorter
than the 10-minute lock lifetime, rather than allowing each profile to consume
its full retry budget indefinitely. Before each attempt and between profiles,
check the remaining time; stop processing when the budget is exhausted and
classify all untouched profiles as failed. Apply this to the retry logic
associated with RC_CUSTOMER_CHECK_MAX_ATTEMPTS and
RC_CUSTOMER_CHECK_RETRY_DELAY_MS while preserving existing per-profile retry
behavior within the remaining budget.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid concern, verified the math (worst case ~20.3s/profile under a sustained outage, so ~50 profiles could exceed the 10-minute lock staleness window and risk an overlapping run) — but deliberately leaving it out of this PR. This same risk was flagged by an internal review before this PR was opened; the call was to keep this fix scoped to the retry/isolation change (which addresses the reported issue — a single flaky customer no longer aborts the whole run) rather than take on deadline/circuit-breaker design in the same change. Tracking as a follow-up rather than implementing here — happy to open a dedicated PR for it if useful.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Patchalv, agreed—keeping the retry/isolation fix focused in this PR is reasonable. The lock-lifetime bound needs its own design and validation, so I’m happy to track it as a follow-up rather than block this change.

If you’d like, I can open a GitHub issue with the deadline/circuit-breaker requirements and acceptance criteria.
🐇

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes please

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh issue create \
  --repo Patchalv/mapvault \
  --assignee Patchalv \
  --title "Bound RevenueCat entitlement drift checks to the job lock lifetime" \
  --body "$(cat <<'EOF'
## Summary

Bound the RevenueCat entitlement drift-check run so sustained transient failures cannot outlive the job's lock-staleness window and permit overlapping executions.

## Rationale

Customer checks run sequentially and each transient failure may use the full retry budget. Under a sustained RevenueCat outage, the cumulative duration can exceed the 10-minute stale-lock threshold in the drift-check handler. A second invocation could then acquire the lock and overlap with the still-running first invocation.

## Affected areas

- `supabase/functions/rc-entitlement-drift-check/rc-client.ts`
  - Customer retry and profile-classification flow.
- `supabase/functions/rc-entitlement-drift-check/index.ts`
  - Drift-check mutex lifetime and reporting of incomplete coverage.

## Required changes

- Introduce an overall execution deadline or circuit breaker shorter than the lock staleness lifetime.
- Check the remaining budget before each customer check, before retrying a transient failure, and between profiles.
- Preserve the current per-customer retry behavior when sufficient budget remains.
- When the deadline is reached, stop customer calls and classify every unprocessed profile as failed so the run remains explicitly partial.
- Ensure reporting continues to distinguish entitlement drift from partial-check failures.

## Acceptance criteria

- A prolonged sequence of timeouts/retryable RevenueCat failures cannot run beyond the configured deadline.
- The configured deadline is safely below the 10-minute lock-staleness threshold.
- No additional RevenueCat request is made once the budget is exhausted.
- Profiles not processed because the deadline elapsed appear in `driftCheckFailed` and contribute to `failed_count` / the partial-failure signal.
- Existing behavior remains unchanged for successful checks, 404 responses, and non-retryable 4xx responses.
- Tests cover deadline exhaustion during a retry and between profiles.

## Backlinks

- PR `#85`: https://github.com/Patchalv/mapvault/pull/85
- Review discussion: https://github.com/Patchalv/mapvault/pull/85#discussion_r3594314164

Requested by `@Patchalv`.
EOF
)"

Length of output: 2532


@Patchalv, created follow-up issue #86: Bound RevenueCat entitlement drift checks to the job lock lifetime.

It is assigned to you and includes the deadline/circuit-breaker requirements, failure classification behavior, tests, and backlinks to PR #85 and this discussion.

- persist-credentials: false on both workflows' checkout steps
- Drop --allow-net from the Deno test command entirely — the test
  suite fully stubs globalThis.fetch and never makes a real network
  call, so no permissions are needed
- Fix docs/payments.md's stale "lists every RevenueCat customer,
  paginated" step, which described an earlier bulk implementation;
  the code has done per-profile individual RC lookups for a while
- Add test coverage for the network-error (TypeError) and timeout
  (DOMException) retry branches, previously only HTTP-status paths
  were exercised

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Patchalv
Patchalv merged commit 7b354e9 into main Jul 16, 2026
3 checks passed
@Patchalv
Patchalv deleted the fix/mapvault-j-rc-drift-check-503 branch July 16, 2026 10:11
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