Skip to content

feat(admin): add churn chart unit toggle#2101

Merged
riderx merged 5 commits into
mainfrom
codex/admin-churn-chart-unit-toggle
May 10, 2026
Merged

feat(admin): add churn chart unit toggle#2101
riderx merged 5 commits into
mainfrom
codex/admin-churn-chart-unit-toggle

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented May 10, 2026

Summary (AI generated)

  • Added a $ / % toggle to the admin revenue churn chart.
  • Kept the existing lost MRR view and added a churn-rate view using lost MRR divided by previous-day MRR.
  • Extended the admin global stats trend query to return previous-day MRR for the chart calculation.

Motivation (AI generated)

  • Admins need to inspect churn as absolute lost revenue or as a normalized percentage from the same chart.

Business Impact (AI generated)

  • Makes churn analysis faster by avoiding separate calculations outside the dashboard.
  • Helps revenue review distinguish high-dollar churn from high-rate churn movement.

Test Plan (AI generated)

  • bunx eslint --no-ignore src/pages/admin/dashboard/revenue.vue supabase/functions/_backend/utils/pg.ts
  • bun lint
  • bun typecheck
  • git diff --check origin/main...HEAD
  • bun run build

Summary by CodeRabbit

  • New Features
    • Admin revenue dashboard: toggle churn display between lost-revenue dollars ($) and churn percentage (%) with updated chart formatting.
    • Added a Plan Conversion Rate chart showing per-plan conversion trends alongside overall conversion.
    • Trend data now supports previous-period MRR to power accurate churn-rate calculations and per-plan breakdowns.

Review Change Stack

@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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 56 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5745b544-ad78-425f-ba57-01aa8e25de2f

📥 Commits

Reviewing files that changed from the base of the PR and between 79372a0 and 79264fc.

📒 Files selected for processing (2)
  • supabase/functions/_backend/utils/pg.ts
  • tests/admin-stripe-backfill-scripts.unit.test.ts
📝 Walkthrough

Walkthrough

Adds plan-level conversion-rate columns and previous-period MRR to global_stats (migration, types, trigger, backfill), exposes them via getAdminGlobalStatsTrend (correlated subqueries), and updates the admin revenue page with a churn $ / % toggle plus a Plan Conversion Rate chart.

Changes

Churn & Plan Conversion Rates

Layer / File(s) Summary
DB migration
supabase/migrations/20260510214806_add_plan_conversion_rates_to_global_stats.sql
Adds four plan_*_conversion_rate double precision NOT NULL DEFAULT 0 columns with comments.
DB types
supabase/functions/_backend/utils/supabase.types.ts
public.global_stats Row/Insert/Update types gain the four plan conversion-rate fields.
Trigger compute
supabase/functions/_backend/triggers/logsnag_insights.ts
Adds calculateConversionRate/getPlanConversionRates, computes org and per-plan conversion rates, upserts plan_*_conversion_rate fields.
Trend query & mapping
supabase/functions/_backend/utils/pg.ts
AdminGlobalStatsTrend gains plan conversion-rate and previous_mrr fields; getAdminGlobalStatsTrend selects plan_*_conversion_rate and computes previous-day MRR/per-plan previous MRR via correlated subqueries and maps results.
Backfill script
scripts/backfill_org_conversion_rate_trend.ts
Backfill computes per-plan rates, includes current_plan_rates/next_plan_rates, expands change detection with tolerance, and updates plan_*_conversion_rate along with org_conversion_rate; dry-run logging updated.
Frontend data & helper
src/pages/admin/dashboard/revenue.vue
Adds churnChartMode state and toChurnRate(lostRevenue, previousMrr) helper (guards + rounding); reads conversion-rate and previous_mrr fields.
Frontend series & UI
src/pages/admin/dashboard/revenue.vue
Adds planConversionSeries, churnRateSeries, selectors (churnChartSeries, title, prefix/suffix), a Plan Conversion Rate chart, and $/% toggle buttons that bind the churn chart to selected series/format.
Tests
tests/admin-stripe-backfill-scripts.unit.test.ts
Unit test updated to expect current_plan_rates and next_plan_rates in backfill output assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Cap-go/capgo#2002: Backfill logic and changes to scripts/backfill_org_conversion_rate_trend.ts touching the same script and conversion-rate computations.

Poem

🐰 A toggle hops from dollar to rate,
Previous MRR helps numbers relate.
Plan rates bloom, charts take flight,
Admins flip views day and night.
The dashboard smiles — percent or $ delight.

🚥 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 'feat(admin): add churn chart unit toggle' directly describes the primary change: adding a toggle to switch between currency and percentage units on the admin churn chart.
Description check ✅ Passed The description includes a Summary section explaining the change and Business Impact/Motivation sections explaining why. However, it lacks the 'Test plan' section as a structured subsection, instead embedding test commands inline without clear step-by-step reproduction instructions.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/admin-churn-chart-unit-toggle

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

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/utils/pg.ts`:
- Line 1197: The current LAG(...) OVER (ORDER BY date_id ASC) in the calculation
of previous_mrr pulls the previous available row (not necessarily the previous
calendar day); to fix, join or generate a contiguous calendar of dates (e.g.,
via generate_series or an existing calendar table) and left-join global_stats
onto that calendar, compute mrr (COALESCE(mrr,0)) on the calendar rows, then use
LAG(COALESCE(mrr,0)) OVER (ORDER BY calendar_date) to produce previous_day MRR;
update the expression that defines previous_mrr (and any references to date_id)
to use the calendar_date-backed ordering so missing days become zeros rather
than skipping and distorting churn calculations.
🪄 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: a2225621-6728-4157-8bfc-b86295917ce0

📥 Commits

Reviewing files that changed from the base of the PR and between b11b7be and 7f6342f.

📒 Files selected for processing (2)
  • src/pages/admin/dashboard/revenue.vue
  • supabase/functions/_backend/utils/pg.ts

Comment thread supabase/functions/_backend/utils/pg.ts Outdated
@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 10, 2026

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing codex/admin-churn-chart-unit-toggle (79264fc) with main (1ac55c4)

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.

Copy link
Copy Markdown

@SpeedyArt SpeedyArt left a comment

Choose a reason for hiding this comment

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

I think the new plan-rate series are using the wrong denominator.

The backend now returns only previous_mrr, and the frontend computes each plan line as churn_revenue_<plan> / previous_mrr. But the numerator is plan-specific lost MRR while previous_mrr is total prior MRR, so the labeled Solo Churn (%), Maker Churn (%), etc. are not actually plan churn rates. For example, if Solo lost $10 against $100 of prior Solo MRR while total prior MRR was $1,000, the chart would show 1% instead of the plan’s 10% churn rate.

global_stats already has separate plan revenue columns (revenue_solo, revenue_maker, revenue_team, revenue_enterprise; those are ARR, so the per-plan MRR denominator would be prior-day revenue / 12). Could we return previous per-plan MRR denominators and use those for the plan series, or else relabel these lines as each plan’s lost MRR share of total prior MRR if that is the intended metric? A small regression with two plans that have very different prior MRR would catch the distinction.

Copy link
Copy Markdown

Thanks, the latest commit looks like it addresses the denominator issue I raised: the backend now returns per-plan previous_mrr_* values and the UI uses those for the plan-rate series instead of dividing each plan by total previous MRR.

@riderx riderx force-pushed the codex/admin-churn-chart-unit-toggle branch from 104b63f to 79372a0 Compare May 10, 2026 22:14
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.

🧹 Nitpick comments (2)
tests/admin-stripe-backfill-scripts.unit.test.ts (1)

14-89: ⚡ Quick win

Add one plan-only delta case to lock in new change-detection behavior.

Line 161 in the script now flags rows when plan rates change even if org_conversion_rate does not. This test currently covers “both changed” and “none changed,” but not “only plan changed.”

Suggested additional assertion case
+  it.concurrent('marks row as changed when only plan conversion rate differs', () => {
+    const rows = buildOrgConversionRateBackfillRows([
+      {
+        date_id: '2026-04-10',
+        paying: 40,
+        org_conversion_rate: 20,
+        plan_enterprise: 0,
+        plan_enterprise_conversion_rate: 0,
+        plan_maker: 10,
+        plan_maker_conversion_rate: 4.5, // intentionally stale
+        plan_solo: 30,
+        plan_solo_conversion_rate: 15,
+        plan_team: 0,
+        plan_team_conversion_rate: 0,
+      },
+    ] as any, Array.from({ length: 200 }, () => ({ created_at: '2026-04-01T12:00:00.000Z' })))
+
+    expect(rows[0]?.current_rate).toBe(20)
+    expect(rows[0]?.next_rate).toBe(20)
+    expect(rows[0]?.changed).toBe(true)
+  })
🤖 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 `@tests/admin-stripe-backfill-scripts.unit.test.ts` around lines 14 - 89, The
test for buildOrgConversionRateBackfillRows needs an extra case that covers
"only plan changed" (plan rate deltas without org_conversion_rate change) so the
new change-detection logic is exercised; add another input row in the
it.concurrent('marks only changed org conversion rows') scenario (or a new it
case) where date_id differs (e.g., '2026-04-03') with org_conversion_rate equal
to the prior value but one or more plan_*_conversion_rate values changed, then
assert the resulting row has changed: true while current_rate and next_rate
(org_conversion_rate) remain equal — locate buildOrgConversionRateBackfillRows
and the test's expected array to insert the new input and expected output.
supabase/functions/_backend/utils/pg.ts (1)

1209-1233: ⚡ Quick win

Use one previous-day join instead of five correlated subqueries.

Line 1209-1233 repeats the same previous-day lookup for each metric. A single LEFT JOIN to prev (once per row) will be easier to maintain and reduce redundant lookups.

🤖 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/_backend/utils/pg.ts` around lines 1209 - 1233, The query
repeats five correlated subqueries on global_stats to fetch previous-day
metrics; replace them with a single LEFT JOIN to a prev alias (join condition:
prev.date_id = (gs.date_id::date - 1)::text) and then reference prev.mrr,
prev.revenue_solo, prev.revenue_maker, prev.revenue_team,
prev.revenue_enterprise in the SELECT, applying the same COALESCE(...,0)::float
and /12 casts where needed (e.g., previous_mrr := COALESCE(prev.mrr,0)::float;
previous_mrr_solo := (COALESCE(prev.revenue_solo,0)::float/12) etc.), and remove
the five correlated subqueries that reference prev in the SELECT.
🤖 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.

Nitpick comments:
In `@supabase/functions/_backend/utils/pg.ts`:
- Around line 1209-1233: The query repeats five correlated subqueries on
global_stats to fetch previous-day metrics; replace them with a single LEFT JOIN
to a prev alias (join condition: prev.date_id = (gs.date_id::date - 1)::text)
and then reference prev.mrr, prev.revenue_solo, prev.revenue_maker,
prev.revenue_team, prev.revenue_enterprise in the SELECT, applying the same
COALESCE(...,0)::float and /12 casts where needed (e.g., previous_mrr :=
COALESCE(prev.mrr,0)::float; previous_mrr_solo :=
(COALESCE(prev.revenue_solo,0)::float/12) etc.), and remove the five correlated
subqueries that reference prev in the SELECT.

In `@tests/admin-stripe-backfill-scripts.unit.test.ts`:
- Around line 14-89: The test for buildOrgConversionRateBackfillRows needs an
extra case that covers "only plan changed" (plan rate deltas without
org_conversion_rate change) so the new change-detection logic is exercised; add
another input row in the it.concurrent('marks only changed org conversion rows')
scenario (or a new it case) where date_id differs (e.g., '2026-04-03') with
org_conversion_rate equal to the prior value but one or more
plan_*_conversion_rate values changed, then assert the resulting row has
changed: true while current_rate and next_rate (org_conversion_rate) remain
equal — locate buildOrgConversionRateBackfillRows and the test's expected array
to insert the new input and expected output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0fd55489-8116-4897-8e52-35ce2aed6a98

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6342f and 79372a0.

📒 Files selected for processing (7)
  • scripts/backfill_org_conversion_rate_trend.ts
  • src/pages/admin/dashboard/revenue.vue
  • supabase/functions/_backend/triggers/logsnag_insights.ts
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/migrations/20260510214806_add_plan_conversion_rates_to_global_stats.sql
  • tests/admin-stripe-backfill-scripts.unit.test.ts
✅ Files skipped from review due to trivial changes (2)
  • supabase/migrations/20260510214806_add_plan_conversion_rates_to_global_stats.sql
  • supabase/functions/_backend/utils/supabase.types.ts

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit c47a103 into main May 10, 2026
40 checks passed
@riderx riderx deleted the codex/admin-churn-chart-unit-toggle branch May 10, 2026 22:36
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.

2 participants