Repository navigation
Finish coderouter billing operations and telemetry - #9732
Conversation
📝 WalkthroughWalkthroughThe PR adds Stripe subscription reconciliation with protected cron and CLI entry points, updates checkout and webhook billing analytics, handles refunded charges, improves Stripe catalog provisioning, and adds a benchmark harness with an operations runbook. ChangesBilling reconciliation
Stripe billing lifecycle
Coderouter operations tooling
Stripe catalog provisioning
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Scheduler
participant BillingReconcileRoute
participant ReconcileService
participant Database
participant Stripe
Scheduler->>BillingReconcileRoute: send authenticated GET request
BillingReconcileRoute->>ReconcileService: start reconciliation
ReconcileService->>Database: load local subscription snapshots
ReconcileService->>Stripe: retrieve current subscriptions
Stripe-->>ReconcileService: return provider state
ReconcileService->>Stripe: repair detected drift
ReconcileService-->>BillingReconcileRoute: return reconciliation result
BillingReconcileRoute-->>Scheduler: return 200 or 503 response
Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 2 warnings)
✅ Passed checks (21 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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.
Actionable comments posted: 10
🤖 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 `@web/scripts/coderouter/benchmark.ts`:
- Around line 174-184: Update parseArgs to validate each inlineKey against the
supported keys origin, samples, timeout, and token before storing its value, and
throw for unsupported options such as --orign. Track keys already parsed and
reject duplicate occurrences before assigning either inline or next-argument
values.
- Around line 100-126: The sample aggregation in the benchmark result builder
repeatedly rescans samples for each status and timing name. Update the
surrounding aggregation logic to traverse samples once, accumulating status
counts in a Map and timing values in per-name arrays, then derive statusCounts
and serverTimingMs by sorting only the accumulated keys or percentile arrays;
preserve the existing output shape and percentile behavior.
- Around line 30-33: Validate and normalize origin before using it in requests
or JSON output: reject URL userinfo, query strings, and fragments, and permit
the environment-derived routeToken only when origin matches the approved-origin
policy. Ensure serialization and command output never expose routeToken or other
credentials, while preserving valid explicit-token behavior as appropriate.
- Around line 57-59: Move the CLI-visible error messages in the benchmark flow,
including the messages around the status check and the other referenced error
sites, into the repository’s locale source. Replace hard-coded text with the
established locale lookup while preserving stable machine-readable JSON schema
keys, and update every web/messages/*.json catalog with the corresponding
translations.
- Around line 130-141: The parseServerTiming function must split Server-Timing
entries on commas only when outside quoted strings. Replace the direct
value.split(",") iteration with quote-aware tokenization, then preserve the
existing parameter and duration parsing; add a fixture covering
cache;desc="edge, cache";dur=12.5 and verify it records the cache duration
correctly.
In `@web/scripts/coderouter/reconcile-billing.ts`:
- Around line 5-13: Update the CLI argument validation around unknown so it
throws without including unknown argument values, and wrap
reconcileStripeSubscriptions in a failure handler that records sanitized
internal telemetry, prints only a generic recovery message, and sets a nonzero
process.exitCode. Preserve the existing JSON success output and failure status
handling for successful reconciliation results.
In `@web/scripts/stripe/provision-catalog.sh`:
- Line 179: Update the price-check message at
web/scripts/stripe/provision-catalog.sh:179-179 and the catalog/product progress
messages at web/scripts/stripe/provision-catalog.sh:248-252 to use the locale
source and matching localized catalog entries. Make all output provider-neutral
by removing explicit Stripe/provider names unless the configured product
provider must be shown.
- Around line 77-86: Update stripe_post so retried POST requests are safe by
generating or accepting one stable Idempotency-Key per operation and passing it
on every retry through the curl headers; alternatively remove automatic retries
from this POST path. Keep the existing stripe_get retry behavior unchanged.
In `@web/services/billing/reconcile.ts`:
- Around line 120-121: Replace the fixed oldest-first ordering in the
reconciliation query with a persisted reconciliation cursor or work-queue
checkpoint, using an indexed keyset such as (lastReconciledAt, id) to rotate
through subscriptions across runs. Update the checkpoint after processing each
bounded batch, add the required schema index, and cover that a later
subscription is checked on a subsequent truncated run.
In `@web/tests/billing-reconcile.test.ts`:
- Around line 53-61: Update the retrieve mock in the billing reconciliation test
to replace the fixed setTimeout delay with a promise readiness barrier. Track
retrieve starts, resolve the barrier when both calls have begun, and await that
barrier so concurrency is deterministic while preserving the inFlight and peak
tracking behavior.
🪄 Autofix
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: ASSERTIVE
Plan: Pro Plus
Run ID: 202c707a-e49e-4e21-86ff-0d7cdbab399e
📒 Files selected for processing (17)
docs/coderouter-operations.mdweb/app/api/billing/checkout/route.tsweb/app/api/cron/billing-reconcile/route.tsweb/app/api/stripe/webhook/route.tsweb/package.jsonweb/scripts/coderouter/benchmark.tsweb/scripts/coderouter/reconcile-billing.tsweb/scripts/stripe/dev-stack.shweb/scripts/stripe/provision-catalog.shweb/services/analytics/stripeBilling.tsweb/services/billing/reconcile.tsweb/tests/billing-reconcile-route.test.tsweb/tests/billing-reconcile.test.tsweb/tests/stripe-billing-analytics.test.tsweb/tests/stripe-provision-catalog.test.tsweb/tests/stripe-webhook-route.test.tsweb/vercel.json
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/scripts/stripe/provision-catalog.sh (1)
110-146: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftKeep an authoritative fallback for product discovery.
The new
/products/searchlookup is not strongly consistent. Stripe documents that Product Search is unsuitable for read-after-write flows. A newly created product can remain absent from search results for under a minute normally and much longer during an outage. (docs.stripe.com)If a previous product creation succeeds but the response is lost, the next run can see no match and create a duplicate product. Keep an authoritative
/productsfallback before creating a product, or use an idempotency key that remains stable across reruns. Add a test for this recovery path.#!/bin/bash set -euo pipefail sed -n '100,160p' web/scripts/stripe/provision-catalog.sh rg -n -C 8 'products/search|stripe_post "/products"|next_page|ensure_product|resolve_product' \ web/scripts/stripe/provision-catalog.sh \ web/tests/stripe-provision-catalog.test.tsAlso applies to: 84-86
🤖 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 `@web/scripts/stripe/provision-catalog.sh` around lines 110 - 146, Update the product-resolution flow around the /products/search pagination and ensure_product logic to use an authoritative /products lookup before creating a new product, preserving matching filters for name, active status, and cmux/plan metadata. Ensure products temporarily missing from search are recovered without duplication, and add a test covering recovery after a prior creation response is lost.
🤖 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 `@web/scripts/coderouter/benchmark.ts`:
- Around line 225-226: Update the argument validation in main() to throw a
generic error without interpolating the raw value, and add a regression test
using a secret-like positional argument that verifies the thrown message does
not contain it.
In `@web/scripts/coderouter/reconcile-billing.ts`:
- Line 9: Replace the hard-coded CLI messages in reconcile-billing.ts, including
the output around the referenced console.error lines, with the project’s
localization API. Add matching translation keys and English-equivalent entries
to every supported locale catalog, preserving the existing message behavior and
dry-run guidance.
In `@web/services/billing/reconcile.ts`:
- Around line 125-128: Align the cursor ordering and index definitions: retain
the unreconciled-first ordering in web/services/billing/reconcile.ts lines
125-128, declare lastReconciledAt ASC NULLS FIRST followed by id ASC in
web/db/schema.ts lines 625-628, and create the physical index with the same
ordering in
web/db/migrations/20260807010000_stripe_reconcile_cursor/migration.sql lines
4-5.
- Around line 64-65: Update the batch flow around list and snapshots to acquire
a shared database-backed lease or advisory lock before selecting records, retain
it through Stripe retrieval and repair completion, and release it in finally.
Ensure every cron and CLI entry point uses this same claim mechanism, and add a
concurrency test proving a second invocation cannot process an already claimed
batch.
- Around line 94-96: Guard the markChecked call in the reconciliation flow so it
runs only when snapshots exist and options.dryRun is false; preserve normal
reconciliation updates. Add a dry-run test asserting markChecked is not called.
---
Outside diff comments:
In `@web/scripts/stripe/provision-catalog.sh`:
- Around line 110-146: Update the product-resolution flow around the
/products/search pagination and ensure_product logic to use an authoritative
/products lookup before creating a new product, preserving matching filters for
name, active status, and cmux/plan metadata. Ensure products temporarily missing
from search are recovered without duplication, and add a test covering recovery
after a prior creation response is lost.
🪄 Autofix
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: ASSERTIVE
Plan: Pro Plus
Run ID: 7c3b0eb7-392e-4b9e-bdf2-f2c5da8fdecc
📒 Files selected for processing (8)
web/db/migrations/20260807010000_stripe_reconcile_cursor/migration.sqlweb/db/schema.tsweb/scripts/coderouter/benchmark.tsweb/scripts/coderouter/reconcile-billing.tsweb/scripts/stripe/provision-catalog.shweb/services/billing/reconcile.tsweb/tests/billing-reconcile.test.tsweb/tests/coderouter-benchmark.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@web/db/migrations/20260807010000_stripe_reconcile_cursor/migration.sql`:
- Around line 4-6: Change the migration flow for
stripe_subscriptions_reconcile_cursor_idx so the index is built with CREATE
INDEX CONCURRENTLY in a separate non-transactional deployment step before this
migration; do not leave the normal CREATE INDEX statement in the
transaction-based migration path used by drizzle-orm/node-postgres/migrator or
drizzle-kit migrate.
🪄 Autofix
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: ASSERTIVE
Plan: Pro Plus
Run ID: 3139d7d3-97db-4507-b86a-8ae137a1de0f
📒 Files selected for processing (6)
web/db/migrations/20260807010000_stripe_reconcile_cursor/migration.sqlweb/db/schema.tsweb/scripts/coderouter/benchmark.tsweb/services/billing/reconcile.tsweb/tests/billing-reconcile.test.tsweb/tests/coderouter-benchmark.test.ts
| CREATE INDEX "stripe_subscriptions_reconcile_cursor_idx" | ||
| ON "stripe_subscriptions" | ||
| ("last_reconciled_at" ASC NULLS FIRST, "id" ASC); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the migration runner and determine whether it wraps migrations in a transaction.
fd -t f . | rg '(^|/)(drizzle[^/]*|.*migration.*|package\.json)$' | while IFS= read -r file; do
rg -n -C 3 'drizzle.*migrate|migrate\(|transaction|CREATE INDEX CONCURRENTLY|concurrent' "$file" || true
doneRepository: manaflow-ai/cmux
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Relevant files:\n'
fd -t f . | rg '(^|/)(package\.json|drizzle|migration\.ts|.*migration.*|turso|postgres|libsql).*' | head -200
printf '\nMigration SQL:\n'
cat -n web/db/migrations/20260807010000_stripe_reconcile_cursor/migration.sql
printf '\nMigration runner / drizzle config references:\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'drizzle.*migrate|migrate\(|BEGIN|COMMIT|transaction\(|turso|integrations|execute\(' web package.json 2>/dev/null || true
printf '\nFiles mentioning drizzle migrate in config scripts:\n'
python3 - <<'PY'
import json, os, subprocess
files = subprocess.check_output(['fd','-t','f','package.json'], text=True).splitlines()
for f in files[:50]:
try:
data=json.load(open(f))
except Exception:
continue
for k,v in data.get('scripts',{}).items():
if any(s in str(v) for s in ['drizzle','migrate','tursonorm','turso','migration']):
print(f'{f}:{k}={v}')
PYRepository: manaflow-ai/cmux
Length of output: 30158
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'web/drizzle.config.ts:\n'
cat -n web/drizzle.config.ts
printf '\nScripts using db-local migration entrypoint:\n'
cat -n web/scripts/db-local.sh | sed -n '1,180p'
printf '\nCloud VM migration script database/migrate usage:\n'
cat -n web/scripts/cloud-vm/migrate-vercel-aurora-iam.mjs | sed -n '1,140p'
printf '\nAWS RDS IAM migration script migrate call:\n'
cat -n web/scripts/migrate-aws-rds-iam.ts | sed -n '1,80p'Repository: manaflow-ai/cmux
Length of output: 10566
Run this index build non-transactionally before this migration.
stripe_subscriptions_reconcile_cursor_idx uses normal CREATE INDEX, which blocks writes to stripe_subscriptions while the index builds. Create it with CREATE INDEX CONCURRENTLY before this migration, or split this migration into a separate non-transactional deployment step. Existing production migration paths (drizzle-orm/node-postgres/migrator and local drizzle-kit migrate) run migrations in a transaction, so this statement cannot run directly in those flows.
🧰 Tools
🪛 SQLFluff (4.2.2)
[error] 4-6: CREATE INDEX should use CONCURRENTLY to avoid locking the table during the build.
(PG01)
🪛 Squawk (2.61.0)
[warning] 4-6: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
🤖 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 `@web/db/migrations/20260807010000_stripe_reconcile_cursor/migration.sql`
around lines 4 - 6, Change the migration flow for
stripe_subscriptions_reconcile_cursor_idx so the index is built with CREATE
INDEX CONCURRENTLY in a separate non-transactional deployment step before this
migration; do not leave the normal CREATE INDEX statement in the
transaction-based migration path used by drizzle-orm/node-postgres/migrator or
drizzle-kit migrate.
Source: Linters/SAST tools
Adds hourly Stripe/RDS reconciliation, delayed-event repair, refund and checkout-start analytics, internal runbooks and benchmark tooling, bounded retries/concurrency, privacy-safe Sentry breadcrumbs, and deterministic coverage.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Completes coderouter billing operations and telemetry for the private beta. Adds serialized hourly Stripe↔RDS reconciliation, hardened webhooks (including refunds), privacy‑safe checkout/refund analytics, and lightweight ops tooling.
New Features
/api/cron/billing-reconcile(BearerCRON_SECRET) runs under a global advisory lock to serialize cron/operator runs, uses per-subscription cursorlast_reconciled_at, bounded concurrency, and CLIbun coderouter:reconcile-billingwith--dry-run; exits nonzero on failures/truncation; returns 503 withRetry-After; scheduled at minute 23 invercel.json.charge.refundedby joining invoice→subscription, records analytics, and does not revoke active access.after()with a safe fallback; unified capture adds stable$insert_id, team grouping via$groups, and a single retry on 5xx only.bun coderouter:benchmarkreports status counts, p50/p95/p99, andServer-Timing, and refuses to send route tokens to unapproved origins; runbookdocs/coderouter-operations.mdcovers replay, reconciliation, latency evidence, and observability.Bug Fixes
/products/searchwithnext_pageandcurltimeouts/retries; dev stack listens forcharge.refunded.Written for commit 10351a5. Summary will update on new commits.
Summary by CodeRabbit
Billing
Observability
Operations