feat(admin): add Plans analytics dashboard - #2976
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughAdds a read-only admin Plans analytics dashboard. It reconstructs visits and checkout intent from PostHog, classifies historical billing states, exposes quality metadata, and renders localized accessible charts with loading and failure states. ChangesPlans analytics dashboard
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Comment |
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment; not approving because this exceeds the low-risk auto-approval threshold (large admin Plans analytics change with billing-history reconstruction and private admin API surface). Cursor Bugbot was not present after the initial check poll, so that signal was skipped; human review is needed and a reviewer will be assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment; not approving because this exceeds the low-risk auto-approval threshold (large admin Plans analytics change with billing-history reconstruction and private admin API surface). Cursor Bugbot was not present after the initial check poll, so that signal was skipped; human review is needed and riderx is already assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
Actionable comments posted: 21
🤖 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 `@docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.md`:
- Around line 1393-1398: Update the validation commands in the “Step 1: Run
formatting and lint first” section to use the repository-required commands `bun
lint:fix` and `bun lint:backend` instead of the `bun run` variants.
- Around line 1234-1235: Update the planned “plans-analytics-posthog-timeout”
translation to use the design-approved message: “This range was too large to
process. Select a shorter period and try again.” Keep the separate
“plans-analytics-range-too-large” translation unchanged.
- Around line 1356-1362: Update the fifth card implementation to use the defined
translation keys for its title, deferred description, and external-link label
instead of literal user-facing text. Keep the checkout-completion URL literal,
render the card full-width, and preserve the external link’s new-tab behavior
with rel="noopener noreferrer".
- Around line 271-272: Update the PostHog configuration around POSTHOG_READ_HOST
and POSTHOG_READ_PROJECT_ID to require both environment variables when
POSTHOG_READ_KEY is present; remove the hard-coded host and project fallbacks,
and preserve the existing behavior only when all required configuration is
supplied.
- Around line 402-423: Extend the test named “keeps range-wide uniques distinct
from daily uniques and reconciles graph totals” with multiple checkout-start
events for the same organization on one day, including different billing states
at their attributed openings. Assert Graph 3 deduplicates the organization and
Graph 4 selects the earliest attributed checkout, while preserving the existing
traffic and reconciliation assertions.
- Around line 737-756: Update loadPlansBillingHistories orchestration to
validate each orgIds entry as a UUID before executing the ANY($1::uuid[]) query;
exclude invalid IDs from the database request and add them to
excludedMissingOrganization, while preserving valid IDs for billing-history
loading.
- Around line 947-965: Update the SQL WHERE clause to apply event-specific
windows: retain User visit rows only when timestamp is in [queryStart, end), and
retain Checkout Started rows only when timestamp is in [start, queryEnd).
Replace the shared timestamp bounds while preserving the event filter and
half-open interval semantics.
- Around line 1102-1147: Expand the admin Plans analytics test coverage beyond
adapter output and source-text assertions by exercising the page state machine
and rendered behavior. Add tests for response validation, loading, valid empty
data, partial billing warnings, timeout, unavailable, and too-large states, plus
UTC display and link rendering; use the page’s existing fetch/rendering symbols
and preserve the current adapter and wiring checks.
- Around line 134-139: Update the histogram query’s window partitioning around
lagInFrame to use the same normalized runtime identity key as the legacy repair
logic—orgId combined with sessionId when present, otherwise actorId—instead of
partitioning by org_id and distinct_id separately. Ensure the 30-second
threshold is validated per normalized session-or-actor identity before enabling
legacy repair.
- Around line 914-917: The contract constants must keep legacy reconstruction
fail-closed: change LEGACY_PATH_SOURCE from 'event' to 'unavailable' and
represent the threshold as null wherever this contract is declared or used. Only
permit switching to an event-based source after successful evidence validation
and accompanying tests, while preserving the existing safeguards against
fabricated or duplicate visits.
- Around line 517-523: Update the legacy candidate predicate in the analytics
reconstruction plan to require page !== 'plans' alongside the existing legacy
event and normalized-path checks. Keep the exact candidate predicate unchanged
so events matching page === 'plans' are classified exclusively as exact.
- Around line 1295-1300: Update loadPlansAnalytics to pass the result of
adminStore.fetchStats('plans_analytics') through parsePlansAnalyticsResponse
before assigning it to data.value, and remove the unchecked cast. Preserve the
existing loading and error-handling flow while ensuring only validated
PlansAnalyticsResponse data reaches the chart adapters.
In `@docs/superpowers/specs/2026-08-10-plans-analytics-dashboard-design.md`:
- Around line 181-205: Align the PlansAnalyticsResponse contract with the
consumer’s established type names by replacing DailyBillingSeries and
DailyCheckoutIntentSeries with DailyBillingPoint and DailyCheckoutIntentPoint,
or explicitly defining aliases for the existing names. Apply this consistently
to visitorBreakdown, checkoutIntent, and checkoutVisitorBreakdown so the wire
contract is unambiguous.
In `@supabase/functions/_backend/utils/plans_analytics_model.ts`:
- Around line 220-248: Rename the traffic series label associated with
uniqueVisitorOrganizations to “New visitor orgs” while leaving the underlying
counting logic unchanged. Update the corresponding display or returned label
wherever this series is defined or consumed near the analytics model.
In `@supabase/functions/_backend/utils/plans_analytics.ts`:
- Around line 373-395: Bound the organization set derived in the plans analytics
flow before issuing PostHog requests or calling loadPlansBillingHistories; when
it exceeds the explicit maximum, return emptyPlansAnalyticsResponse with
posthogFailureReason set to 'too_large'. Replace the serial await loop around
queryPosthogHogql and buildBillingTransitionsQuery with bounded-concurrency
batch execution, while preserving existing failure handling, row limits, and
transitionRows aggregation.
In `@supabase/functions/_backend/utils/plans_billing_history.ts`:
- Around line 407-446: Update the carry_in lateral lookup in the RevenueRow
query to select the latest daily_revenue_metrics row directly for each relevant
customer before the start date, ordering by date_id descending and limiting to
one row. Remove its dependency on processed_stripe_events.date_id while
preserving the existing carry-in columns and in_range behavior.
In `@supabase/functions/_backend/utils/posthog_read.ts`:
- Around line 25-26: Update the host and project resolution in the PostHog read
configuration to use the environment-specific configuration helper via
getRightKey() instead of the hard-coded EU host and project defaults. Preserve
the existing trimming and trailing-slash normalization while ensuring unset
values resolve from the current environment configuration.
- Around line 40-48: Update the PostHog response handling around response.json()
and row materialization to enforce a safe memory bound before allocating the
complete results array and mapped row objects. Prefer bounded response parsing,
or reduce the query limit to a Worker-safe maximum, while preserving
column-to-row mapping for accepted results.
In `@tests/admin-plans-analytics-dashboard.unit.test.ts`:
- Around line 202-298: Replace raw .vue source-text assertions in
tests/admin-plans-analytics-dashboard.unit.test.ts:202-298 with
mounted-component or pure-helper behavior assertions, preserving contracts for
the admin guard, fetchStats('plans_analytics'), and parsePlansAnalyticsResponse;
replace exact message-copy checks with required-key presence checks and remove
styling assertions for class="space-y-6" and lg:grid-cols-2. In
tests/admin-stacked-bar-chart.unit.test.ts:51-58, assert rendered Chart.js
dataset options for accessibleBorders enabled and disabled rather than matching
implementation source strings.
- Around line 63-67: Remove the two redundant toMatchTypeOf assertions from the
keeps the frontend response DTO identical to the backend wire contract test,
retaining only the toEqualTypeOf assertion to enforce exact type equality.
In `@tests/admin-stacked-bar-chart.unit.test.ts`:
- Around line 51-58: Update the test in the accessibleBorders assertion to match
the accessibleBorders prop block, including its default value, rather than
checking the bare “default: false” substring; keep the existing source-content
assertions for the contrasting boundary configuration.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ddddffd5-82f7-4081-b120-2baff2b23383
📒 Files selected for processing (22)
docs/admin/plans-checkout-completion.mddocs/superpowers/plans/2026-08-10-plans-analytics-dashboard.mddocs/superpowers/specs/2026-08-10-plans-analytics-dashboard-design.mdmessages/en.jsonsrc/components/admin/AdminStackedBarChart.vuesrc/constants/adminTabs.tssrc/pages/admin/dashboard/plans.vuesrc/services/adminPlansAnalytics.tssrc/stores/adminDashboard.tssupabase/functions/_backend/private/admin_stats.tssupabase/functions/_backend/utils/builder_analytics.tssupabase/functions/_backend/utils/plans_analytics.tssupabase/functions/_backend/utils/plans_analytics_model.tssupabase/functions/_backend/utils/plans_billing_history.tssupabase/functions/_backend/utils/posthog_read.tstests/admin-plans-analytics-dashboard.unit.test.tstests/admin-stacked-bar-chart.unit.test.tstests/admin-stats.unit.test.tstests/plans-analytics-model.unit.test.tstests/plans-analytics-orchestration.unit.test.tstests/plans-billing-history.unit.test.tstests/posthog-read.unit.test.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
There was a problem hiding this comment.
All reported issues were addressed
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment; not approving because this exceeds the low-risk auto-approval threshold (large admin Plans analytics change with billing-history reconstruction and private admin API surface). Cursor Bugbot was not present after the initial check poll, so that signal was skipped; human review is needed and riderx is already assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
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 `@messages/en.json`:
- Line 2012: Update messages/en.json:2012-2012 to use timeout-specific retry
guidance instead of claiming the selected range is too large, and update the
expected localized timeout message in
tests/admin-plans-analytics-dashboard.unit.test.ts:56-56 to match.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 84b6d1cf-f7d1-4ee9-988c-993c9cbd17b3
📒 Files selected for processing (17)
docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.mddocs/superpowers/specs/2026-08-10-plans-analytics-dashboard-design.mdmessages/en.jsonsrc/components/admin/AdminStackedBarChart.vuesrc/components/admin/adminStackedBarChart.tssrc/pages/admin/dashboard/plans.vuesrc/services/adminPlansAnalytics.tssupabase/functions/_backend/utils/builder_analytics.tssupabase/functions/_backend/utils/plans_analytics.tssupabase/functions/_backend/utils/plans_billing_history.tssupabase/functions/_backend/utils/posthog_read.tstests/admin-plans-analytics-dashboard.unit.test.tstests/admin-stacked-bar-chart.unit.test.tstests/plans-analytics-model.unit.test.tstests/plans-analytics-orchestration.unit.test.tstests/plans-billing-history.unit.test.tstests/posthog-read.unit.test.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
There was a problem hiding this comment.
Risk: medium. Left a non-blocking comment; not approving because this exceeds the low-risk auto-approval threshold (large admin Plans analytics change with billing-history reconstruction and private admin API surface). Cursor Bugbot was not present after the initial check poll, so that signal was skipped; human review is needed and riderx is already assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
|
There was a problem hiding this comment.
3 issues found across 17 files (changes from recent commits).
Confidence score: 4/5
- In
tests/admin-plans-analytics-dashboard.unit.test.ts, looseningtoMatchObject(requiredMessages)to a presence/type check means most message literals are no longer value-verified, which can let wording/key regressions slip through unnoticed — restore exact-value assertions (or add explicit per-key checks) for the full required message set. - In
docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.md, the added 08-02 12:00 fixture implies three opens on day 2 whiletotalOpensstill expects[1, 2], so readers reusing this example can inherit a failing or misleading test — update the expectation to match the fixture (e.g.,[1, 3]). - In
tests/posthog-read.unit.test.ts, swappedit.eachcase labels don’t change behavior but can mislead future debugging and maintenance by describing the wrong override scenario — align the case names with their actual host/project overrides.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/admin-plans-analytics-dashboard.unit.test.ts">
<violation number="1" location="tests/admin-plans-analytics-dashboard.unit.test.ts:262">
P2: Replacing the toMatchObject(requiredMessages) assertion with a presence + string-type loop drops exact-value verification for every message except the timeout one. The ~30 other string literals left in requiredMessages are no longer compared against en.json, so a typo or wording change in any other message drifts through CI undetected while the constants still look like they're guarding the copy.</violation>
</file>
<file name="tests/posthog-read.unit.test.ts">
<violation number="1" location="tests/posthog-read.unit.test.ts:52">
P3: The it.each case labels are swapped relative to their overrides: "host override without project" blanks POSTHOG_READ_PROJECT_ID (keeps the host), and "project override without host" blanks POSTHOG_READ_HOST (keeps the project). Both still exercise the mismatch path so tests pass, but the generated test names read backwards and will mislead anyone debugging a failure.</violation>
</file>
<file name="docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.md">
<violation number="1" location="docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.md:416">
P3: The new 08-02 12:00 fixture event adds a third logical opening on day 2, but the `totalOpens` assertion still expects [1, 2]; it should be [1, 3]. Followers copying this fixture will get a failing/incorrect test, so update the expected array to match the added event.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| expect(completionDoc).toContain('pending until the agreed observation window') | ||
| expect(completionDoc).toContain('separate approved design') | ||
| const messages = JSON.parse(messagesText) as Record<string, unknown> | ||
| for (const key of Object.keys(requiredMessages)) { |
There was a problem hiding this comment.
P2: Replacing the toMatchObject(requiredMessages) assertion with a presence + string-type loop drops exact-value verification for every message except the timeout one. The ~30 other string literals left in requiredMessages are no longer compared against en.json, so a typo or wording change in any other message drifts through CI undetected while the constants still look like they're guarding the copy.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/admin-plans-analytics-dashboard.unit.test.ts, line 262:
<comment>Replacing the toMatchObject(requiredMessages) assertion with a presence + string-type loop drops exact-value verification for every message except the timeout one. The ~30 other string literals left in requiredMessages are no longer compared against en.json, so a typo or wording change in any other message drifts through CI undetected while the constants still look like they're guarding the copy.</comment>
<file context>
@@ -215,7 +258,13 @@ describe('admin Plans analytics dashboard', () => {
expect(completionDoc).toContain('separate approved design')
- expect(JSON.parse(messagesText)).toMatchObject(requiredMessages)
+ const messages = JSON.parse(messagesText) as Record<string, unknown>
+ for (const key of Object.keys(requiredMessages)) {
+ expect(messages).toHaveProperty(key)
+ expect(messages[key]).toEqual(expect.any(String))
</file context>
| ['host override without project', posthogEnv({ POSTHOG_READ_PROJECT_ID: '' })], | ||
| ['project override without host', posthogEnv({ POSTHOG_READ_HOST: '' })], |
There was a problem hiding this comment.
P3: The it.each case labels are swapped relative to their overrides: "host override without project" blanks POSTHOG_READ_PROJECT_ID (keeps the host), and "project override without host" blanks POSTHOG_READ_HOST (keeps the project). Both still exercise the mismatch path so tests pass, but the generated test names read backwards and will mislead anyone debugging a failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/posthog-read.unit.test.ts, line 52:
<comment>The it.each case labels are swapped relative to their overrides: "host override without project" blanks POSTHOG_READ_PROJECT_ID (keeps the host), and "project override without host" blanks POSTHOG_READ_HOST (keeps the project). Both still exercise the mismatch path so tests pass, but the generated test names read backwards and will mislead anyone debugging a failure.</comment>
<file context>
@@ -36,14 +47,51 @@ describe('postHog read transport', () => {
+ it.each([
+ ['key', posthogEnv({ POSTHOG_READ_KEY: '' })],
+ ['host override without project', posthogEnv({ POSTHOG_READ_PROJECT_ID: '' })],
+ ['project override without host', posthogEnv({ POSTHOG_READ_HOST: '' })],
+ ])('does not fetch when the %s part of the read configuration is missing', async (_missing, environment) => {
</file context>
| ['host override without project', posthogEnv({ POSTHOG_READ_PROJECT_ID: '' })], | |
| ['project override without host', posthogEnv({ POSTHOG_READ_HOST: '' })], | |
| ['host override without project', posthogEnv({ POSTHOG_READ_HOST: '' })], | |
| ['project override without host', posthogEnv({ POSTHOG_READ_PROJECT_ID: '' })], |
| const openings = buildLogicalPlansOpenings([ | ||
| event({ timestampMs: ms('2026-08-01T08:00:00Z'), orgId: 'org-a', page: 'plans', path: '' }), | ||
| event({ timestampMs: ms('2026-08-02T08:00:00Z'), orgId: 'org-a', page: 'plans', path: '' }), | ||
| event({ timestampMs: ms('2026-08-02T12:00:00Z'), orgId: 'org-a', page: 'plans', path: '' }), |
There was a problem hiding this comment.
P3: The new 08-02 12:00 fixture event adds a third logical opening on day 2, but the totalOpens assertion still expects [1, 2]; it should be [1, 3]. Followers copying this fixture will get a failing/incorrect test, so update the expected array to match the added event.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.md, line 416:
<comment>The new 08-02 12:00 fixture event adds a third logical opening on day 2, but the `totalOpens` assertion still expects [1, 2]; it should be [1, 3]. Followers copying this fixture will get a failing/incorrect test, so update the expected array to match the added event.</comment>
<file context>
@@ -403,23 +413,29 @@ describe('Plans analytics model', () => {
const openings = buildLogicalPlansOpenings([
event({ timestampMs: ms('2026-08-01T08:00:00Z'), orgId: 'org-a', page: 'plans', path: '' }),
event({ timestampMs: ms('2026-08-02T08:00:00Z'), orgId: 'org-a', page: 'plans', path: '' }),
+ event({ timestampMs: ms('2026-08-02T12:00:00Z'), orgId: 'org-a', page: 'plans', path: '' }),
event({ timestampMs: ms('2026-08-02T09:00:00Z'), orgId: 'org-b', page: 'plans', path: '' }),
], ms('2026-08-01T00:00:00Z'), ms('2026-08-03T00:00:00Z'), 30)
</file context>





Summary
Measurement semantics
Test plan
Screenshots
Authenticated local visual verification is unavailable because the local Supabase authentication service is not running. The page layout, states, accessible chart tables, UTC labeling, and safe external link are covered by focused behavior and wiring tests.
Checklist
Out of scope