Skip to content

[codex] Add Stripe LTV metrics#2201

Merged
riderx merged 3 commits into
mainfrom
codex/stripe-ltv-backfill
May 11, 2026
Merged

[codex] Add Stripe LTV metrics#2201
riderx merged 3 commits into
mainfrom
codex/stripe-ltv-backfill

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented May 11, 2026

Summary (AI generated)

  • Added subscription_ended_at to Stripe subscription metadata stored in Supabase.
  • Added average, shortest, and longest LTV metrics to global stats.
  • Added Stripe subscription end-date and LTV metric backfill scripts.
  • Added an admin revenue dashboard chart for average, shortest, and longest LTV.

Motivation (AI generated)

Capgo needs historical and ongoing LTV visibility in admin analytics. Persisting subscription end dates from Stripe makes churned subscription lifetime measurable without repeatedly querying Stripe, while global stats keeps dashboard reads cheap.

Business Impact (AI generated)

This gives the team clearer visibility into customer lifetime value distribution, helping revenue analysis, retention decisions, and plan performance tracking from the admin panel.

Test Plan (AI generated)

  • bun run lint
  • bun run lint:backend
  • bunx vitest run tests/backfill-ltv-metrics.unit.test.ts tests/backfill-stripe-subscription-end-dates.unit.test.ts tests/stripe-subscription-events.unit.test.ts tests/logsnag-insights-revenue.unit.test.ts
  • Commit hook: bun run cli:build && vue-tsc --noEmit
  • bun run build

Summary by CodeRabbit

  • New Features

    • Added LTV metrics (average, shortest, longest) and a new "LTV by Customer" chart in the admin dashboard.
  • New Scripts

    • Two CLI backfill scripts to populate subscription end dates and LTV/LTV metrics (dry-run and apply modes with concurrency controls).
  • Backend

    • Webhook and trigger updates to compute/persist cancellation timestamps and LTV stats into daily snapshots.
  • Database Migration

    • Added three LTV columns to global stats.
  • Tests

    • Unit tests for LTV and subscription-end backfill logic.
  • Bug Fixes

    • Prefer actual Stripe timestamps when available for cancellation tracking.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ac89721-92aa-413d-a98d-9367fd6a8b07

📥 Commits

Reviewing files that changed from the base of the PR and between 5b94c25 and 8ff2ff1.

📒 Files selected for processing (1)
  • tests/chart-refresh-rpc.test.ts

📝 Walkthrough

Walkthrough

Adds LTV metrics end-to-end: database columns and types, Stripe subscription end-date derivation and webhook/sync wiring, daily LTV computation in the insights trigger, admin trend/UI changes, two backfill CLI scripts, and unit tests.

Changes

LTV Metrics Infrastructure

Layer / File(s) Summary
Database Schema and Type Definitions
supabase/migrations/20260511101826_add_ltv_global_stats.sql, supabase/functions/_backend/utils/supabase.types.ts
Adds average_ltv, shortest_ltv, longest_ltv columns to global_stats (double precision DEFAULT 0 NOT NULL) and updates generated TS types.
Stripe subscription end-date helpers
supabase/functions/_backend/utils/stripe.ts, supabase/functions/_backend/utils/stripe_event.ts
Adds helpers to select licensed subscription items, derive ISO canceledAt (ended_at / cancel_at / current_period_end), updates getSubscriptionData() return shape, and writes stripe_info.canceled_at in sync/event flows.
Daily LTV Computation Trigger
supabase/functions/_backend/triggers/logsnag_insights.ts
Adds getLtvStats() using Drizzle/SQL CTE to compute average/min/max LTV over the snapshot window and wires results into the daily global_stats upsert.
Admin Data Access
supabase/functions/_backend/utils/pg.ts
Extends AdminGlobalStatsTrend and getAdminGlobalStatsTrend() SQL/mapping to include average_ltv, shortest_ltv, and longest_ltv (defaulting to 0).
Admin Dashboard Visualization
src/pages/admin/dashboard/revenue.vue
Extends trend data shape with LTV fields, adds ltvSeries computed property, and renders a new "LTV by Customer" ChartCard using an AdminMultiLineChart.
Subscription End Date Backfill
scripts/backfill_stripe_subscription_end_dates.ts
Adds CLI Bun script to derive and backfill subscription anchors and canceled_at from Stripe with dry-run/apply/refresh modes, concurrency, customer scoping, and failure reporting to ./tmp/stripe_subscription_end_date_backfill_failures.json.
LTV Metrics Backfill
scripts/backfill_ltv_metrics.ts
Adds CLI Bun script implementing estimateCustomerLtv, calculateLtvMetrics, and buildLtvBackfillRows; fetches global_stats and stripe_info via paginated queries, computes per-date LTV metrics, supports dry-run and --apply with concurrency.
Package Configuration
package.json, scripts/admin_stripe_backfill_utils.ts
Adds npm scripts stripe:backfill-subscription-end-dates and stripe:backfill-ltv-metrics; bumps Stripe API version to 2026-04-22.dahlia.
Tests
tests/backfill-ltv-metrics.unit.test.ts, tests/backfill-stripe-subscription-end-dates.unit.test.ts, tests/stripe-subscription-events.unit.test.ts, tests/chart-refresh-rpc.test.ts
Adds Vitest suites validating LTV estimation, aggregation, subscription end snapshot derivation, event handling, and tightens chart-refresh auth test setup.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Backfill CLI
  participant Supabase as Supabase DB/API
  participant StripeAPI as Stripe API
  participant FS as Filesystem

  CLI->>Supabase: fetch `stripe_info` / `global_stats` (paginated)
  Supabase-->>CLI: rows
  CLI->>StripeAPI: fetch Subscription objects (concurrent)
  StripeAPI-->>CLI: Subscription responses
  CLI->>CLI: compute snapshots / estimate LTV
  CLI->>Supabase: update `global_stats` / `stripe_info` for candidates
  Supabase-->>CLI: update acknowledgements
  CLI->>FS: write failure report (if any)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Cap-go/capgo#2094: Modifies the same admin/global_stats pipeline and backfill patterns.
  • Cap-go/capgo#2002: Related backfill scripts and changes to admin_stripe_backfill_utils (Stripe client setup).
  • Cap-go/capgo#2112: Similar global_stats pipeline additions for a different metric; touches migrations, types, triggers, and admin UI.

Suggested reviewers

  • jihadMo

Poem

🐰 I nibble data, hop through rows,
I fetch subscriptions where the river flows,
Backfills hum beneath the moonlit sky,
LTVs settle and charts fly high,
Hop, code, hop — the metrics sigh.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title directly relates to the main objective of adding Stripe LTV (lifetime value) metrics, which is the core feature across all file changes.
Description check ✅ Passed The PR description comprehensively covers Summary, Motivation, Business Impact, and Test Plan sections, fully aligned with the template requirements and change objectives.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 codex/stripe-ltv-backfill

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 11, 2026

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing codex/stripe-ltv-backfill (8ff2ff1) with main (30cedef)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@riderx riderx marked this pull request as ready for review May 11, 2026 11:28
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@scripts/backfill_ltv_metrics.ts`:
- Around line 139-161: estimateCustomerLtv is currently counting a minimum paid
period whenever getPaidStart falls back to created_at, which treats unpaid
trials as revenue; modify the logic so a paid-start is only used when there is
explicit evidence of payment. Specifically, update getPaidStart (or add a
pre-check in estimateCustomerLtv) to return null unless the row contains a
paid-indicator (e.g., first_paid_at / has_paid_invoice / successful payment
status field) and only then fall back to created_at; keep
getKnownSubscriptionEnd and effectiveEnd logic unchanged but ensure rows without
any paid-indicator are rejected early (return null) so unpaid trials/failed
payments are not counted as revenue.

In `@scripts/backfill_stripe_subscription_end_dates.ts`:
- Around line 138-141: The current conditional only assigns
update.subscription_anchor_start/end when candidate.next_anchor_start/end are
truthy, so refresh mode (--refresh-existing) cannot clear anchors back to null;
change the checks for candidate.next_anchor_start and candidate.next_anchor_end
to test for the presence of the property (e.g., using "next_anchor_start" in
candidate or hasOwnProperty) and assign update.subscription_anchor_start =
candidate.next_anchor_start and update.subscription_anchor_end =
candidate.next_anchor_end even when those values are null so stale anchors get
cleared; update the logic around candidate -> update assignment in the backfill
loop where candidate.next_anchor_start / candidate.next_anchor_end are handled.

In `@src/pages/admin/dashboard/replication.vue`:
- Around line 111-112: The thrown Error for the missing session (check around
the session?.access_token guard) has an outdated message referencing a
replication secret; update the Error message to accurately reflect that the user
session is missing or expired and instruct operators to sign in or refresh the
session (e.g., "No active session: please sign in or refresh your authentication
token"); locate the guard that throws new Error(...) in the replication.vue
logic and replace the message string accordingly so it no longer mentions
replication secret fallback.

In `@supabase/functions/_backend/triggers/logsnag_insights.ts`:
- Around line 588-638: The source CTE is including trials/failed checkouts
because paid_start falls back to si.created_at; restrict rows to actual paid
subscriptions by adding a filter such as AND si.paid_at IS NOT NULL (or another
column that reliably indicates a successful payment) in the WHERE of the source
CTE so that the COALESCE(si.paid_at, si.subscription_anchor_start,
si.created_at) / paid_start logic only runs for records with real payments;
update the source CTE (referenced as source, paid_start, and the COALESCE
expression) to include this gating condition before ltv_values is computed.

In `@supabase/functions/_backend/utils/stripe_event.ts`:
- Line 69: The code sets data.subscription_ended_at using
getSubscriptionEndDate(subscription, firstItem) but getSubscriptionEndDate reads
item.current_period_end when cancel_at_period_end is true, and firstItem can be
the wrong line item for multi-item subscriptions; change the call to use the
licensed line item by passing currentLicensedItem instead of firstItem (ensure
currentLicensedItem is the same variable chosen at line 52) so
subscription_ended_at is derived from the licensed item's current_period_end via
getSubscriptionEndDate.

In `@supabase/functions/_backend/utils/stripe.ts`:
- Line 287: The current unconditional assignment
updateData.subscription_ended_at = subscriptionData?.endedAt ?? null wipes
existing end timestamps when subscriptionData is missing; change this to only
assign subscription_ended_at when subscriptionData actually provides an endedAt
value (i.e., check subscriptionData and subscriptionData.endedAt !== undefined)
so you don't overwrite previously stored timestamps on transient fetch
failures—use a conditional (e.g., if subscriptionData && typeof
subscriptionData.endedAt !== 'undefined') to set
updateData.subscription_ended_at, otherwise leave updateData untouched.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58ad3341-1c44-44bc-8286-ef4f5e997b2d

📥 Commits

Reviewing files that changed from the base of the PR and between dcd0dd6 and 10972b0.

📒 Files selected for processing (17)
  • package.json
  • scripts/admin_stripe_backfill_utils.ts
  • scripts/backfill_ltv_metrics.ts
  • scripts/backfill_stripe_subscription_end_dates.ts
  • src/components/Sidebar.vue
  • src/pages/admin/dashboard/replication.vue
  • src/pages/admin/dashboard/revenue.vue
  • supabase/functions/_backend/triggers/logsnag_insights.ts
  • supabase/functions/_backend/triggers/stripe_event.ts
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/postgres_schema.ts
  • supabase/functions/_backend/utils/stripe.ts
  • supabase/functions/_backend/utils/stripe_event.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/migrations/20260511101826_add_ltv_global_stats.sql
  • tests/backfill-ltv-metrics.unit.test.ts
  • tests/backfill-stripe-subscription-end-dates.unit.test.ts

Comment thread scripts/backfill_ltv_metrics.ts
Comment thread scripts/backfill_stripe_subscription_end_dates.ts
Comment thread src/pages/admin/dashboard/replication.vue Outdated
Comment thread supabase/functions/_backend/triggers/logsnag_insights.ts
Comment thread supabase/functions/_backend/utils/stripe_event.ts Outdated
Comment thread supabase/functions/_backend/utils/stripe.ts Outdated
@riderx riderx force-pushed the codex/stripe-ltv-backfill branch from 10972b0 to 1e9c630 Compare May 11, 2026 11:46
@riderx riderx force-pushed the codex/stripe-ltv-backfill branch from 1e9c630 to b6a4255 Compare May 11, 2026 13:58
@riderx riderx marked this pull request as draft May 11, 2026 13:59
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 `@supabase/functions/_backend/triggers/stripe_event.ts`:
- Around line 990-994: In cancelingOrFinished, prevent stale webhook updates
from overwriting canceled_at by first reading the row's last_stripe_event_at and
comparing it to the incoming event timestamp (from stripeData, e.g.,
stripeData.event_timestamp or stripeData.timestamp); only call
supabaseAdmin(c).from('stripe_info').update({ canceled_at: canceledAt
}).eq('customer_id', stripeData.customer_id) if the incoming event timestamp is
strictly newer than last_stripe_event_at on the DB row. Use the existing
canceledAt variable and perform a select for last_stripe_event_at before the
update, and skip the update when the DB timestamp is >= incoming event
timestamp.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcf7e2d4-a95a-493d-aa29-155624e52c40

📥 Commits

Reviewing files that changed from the base of the PR and between 1e9c630 and b6a4255.

📒 Files selected for processing (15)
  • package.json
  • scripts/admin_stripe_backfill_utils.ts
  • scripts/backfill_ltv_metrics.ts
  • scripts/backfill_stripe_subscription_end_dates.ts
  • src/pages/admin/dashboard/revenue.vue
  • supabase/functions/_backend/triggers/logsnag_insights.ts
  • supabase/functions/_backend/triggers/stripe_event.ts
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/stripe.ts
  • supabase/functions/_backend/utils/stripe_event.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/migrations/20260511101826_add_ltv_global_stats.sql
  • tests/backfill-ltv-metrics.unit.test.ts
  • tests/backfill-stripe-subscription-end-dates.unit.test.ts
  • tests/stripe-subscription-events.unit.test.ts
✅ Files skipped from review due to trivial changes (3)
  • package.json
  • tests/backfill-ltv-metrics.unit.test.ts
  • tests/backfill-stripe-subscription-end-dates.unit.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • supabase/functions/_backend/utils/pg.ts
  • src/pages/admin/dashboard/revenue.vue
  • supabase/functions/_backend/triggers/logsnag_insights.ts
  • scripts/backfill_ltv_metrics.ts
  • scripts/backfill_stripe_subscription_end_dates.ts

Comment thread supabase/functions/_backend/triggers/stripe_event.ts
@riderx riderx marked this pull request as ready for review May 11, 2026 14:28
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@riderx riderx force-pushed the codex/stripe-ltv-backfill branch from b6a4255 to 11ac8f2 Compare May 11, 2026 14:31
@riderx riderx marked this pull request as draft May 11, 2026 14:31
@riderx riderx marked this pull request as ready for review May 11, 2026 14:36
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit ee54a9c into main May 11, 2026
52 checks passed
@riderx riderx deleted the codex/stripe-ltv-backfill branch May 11, 2026 15:56
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