Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
59e73c0
docs: design plans analytics dashboard
WcaleNieWolny Aug 10, 2026
e7714cc
docs: clarify plans analytics failure states
WcaleNieWolny Aug 10, 2026
a1656e8
docs: add plans analytics implementation plan
WcaleNieWolny Aug 10, 2026
060a4ab
Merge remote-tracking branch 'origin/main' into wolny/plans-analytics…
WcaleNieWolny Aug 10, 2026
c58548d
refactor(analytics): share PostHog read transport
WcaleNieWolny Aug 10, 2026
af79aaf
feat(admin): model plans visit analytics
WcaleNieWolny Aug 10, 2026
da615f6
feat(admin): classify historical plans billing state
WcaleNieWolny Aug 10, 2026
375c31f
feat(admin): aggregate plans analytics
WcaleNieWolny Aug 10, 2026
b394fc8
feat(admin): expose plans analytics metric
WcaleNieWolny Aug 10, 2026
d84d29b
feat(admin): prepare plans analytics presentation
WcaleNieWolny Aug 10, 2026
cd607ec
feat(admin): add plans analytics dashboard
WcaleNieWolny Aug 10, 2026
877312a
fix(admin): mark legacy deduplication unverified
WcaleNieWolny Aug 10, 2026
3d67ab1
fix(admin): avoid console grep false positive
WcaleNieWolny Aug 10, 2026
77862fe
fix(admin): address plans analytics review
WcaleNieWolny Aug 10, 2026
98fc5e3
fix(admin): bound plans analytics queries
WcaleNieWolny Aug 10, 2026
458e588
fix(admin): scope plans visit query
WcaleNieWolny Aug 10, 2026
86bb39d
fix(admin): clarify plans analytics timeout
WcaleNieWolny Aug 10, 2026
8377aec
Merge remote-tracking branch 'origin/main' into wolny/plans-analytics…
WcaleNieWolny Aug 11, 2026
c43fc17
fix(i18n): add plans analytics translation contexts
WcaleNieWolny Aug 11, 2026
90b82a5
test(admin): strengthen plans analytics regressions
WcaleNieWolny Aug 11, 2026
052ead9
test(observe): accept native empty state
WcaleNieWolny Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/admin/plans-checkout-completion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Plans Checkout Completion Analytics

The current Plans analytics page measures checkout intent only. Completion must remain deferred until Capgo emits a reliable server-side `Checkout Completed` event.

The future event must contain `org_id`, a stable `checkout_attempt_id`, Stripe checkout session ID, product ID, recurrence, and completion timestamp. `Checkout Started` must carry the same `checkout_attempt_id` into Stripe metadata so completion is joined directly rather than inferred from a redirect.

The future full-width daily stacked chart uses the attributed Plans-opening UTC day. Each organization that started checkout that day appears once as Completed or Not completed. Recent attempts remain pending until the agreed observation window has elapsed; they must not be labeled abandoned prematurely.

Implementation requires a separate approved design for the observation window, late completions, retries, plan changes, and existing subscribers.
1,518 changes: 1,518 additions & 0 deletions docs/superpowers/plans/2026-08-10-plans-analytics-dashboard.md

Large diffs are not rendered by default.

293 changes: 293 additions & 0 deletions docs/superpowers/specs/2026-08-10-plans-analytics-dashboard-design.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1982,6 +1982,37 @@
"plan-upgrade": "Subscribe",
"plan-upgrade-v2": "Upgrade",
"plans": "plans",
"plans-analytics-title": "Plans analytics",
"plans-analytics-timezone": "Reporting timezone: UTC",
"plans-analytics-traffic": "Plans page traffic",
"plans-analytics-traffic-description": "Unique organizations on their first Plans opening in the selected range, alongside total logical openings per UTC day",
"plans-analytics-unique-visitor-orgs": "Unique visitor orgs",
"plans-analytics-total-opens": "Total opens",
"plans-analytics-who-opened": "Who opened Plans?",
"plans-analytics-who-opened-description": "Daily unique organizations by billing state at their first Plans opening",
"plans-analytics-checkout-intent": "Checkout intent",
"plans-analytics-checkout-intent-description": "Daily Plans visitors who started checkout within the attribution window",
"plans-analytics-started-checkout": "Started checkout",
"plans-analytics-did-not-start": "Did not start",
"plans-analytics-who-opened-checkout": "Who opened checkout?",
"plans-analytics-who-opened-checkout-description": "Daily checkout starters by billing state at the attributed Plans opening",
"plans-analytics-checkout-completion": "Checkout completion",
"plans-analytics-checkout-completion-description": "TODO — this graph will be implemented after reliable checkout-completion tracking is available.",
"plans-analytics-checkout-completion-link": "Read the implementation requirements",
"plans-category-paying": "Paying",
"plans-category-active-trial": "Active trial",
"plans-category-expired-trial": "Expired trial — never subscribed",
"plans-category-canceled": "Canceled",
"plans-category-payment-problem": "Payment problem",
"plans-category-credits-only": "Credits only",
"plans-category-unknown": "Unknown",
"plans-analytics-partial-warning": "Some organizations could not be classified from historical billing records and appear as Unknown.",
"plans-analytics-legacy-unavailable": "Legacy Plans visits are unavailable because no event-time pathname could be verified.",
"plans-analytics-posthog-unconfigured": "PostHog analytics is not configured.",
"plans-analytics-posthog-timeout": "This range was too large to process. Select a shorter period and try again.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"plans-analytics-range-too-large": "This range returned too much data to process. Select a shorter period and try again.",
"plans-analytics-unavailable": "Plans analytics is temporarily unavailable.",
"plans-analytics-empty": "No Plans visits were recorded in this period.",
"plans-super-only": "Only super admins are allowed to view plans and billing",
"platform": "Platform",
"platform-android": "Android",
Expand Down
12 changes: 10 additions & 2 deletions src/components/admin/AdminStackedBarChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from 'chart.js'
import { computed } from 'vue'
import { Bar } from 'vue-chartjs'
import { buildAdminStackedBarChartData, buildAdminStackedBarChartOptions } from '~/components/admin/adminStackedBarChart'
import { applyAdminStackedBarAccessibleBorders, buildAdminStackedBarChartData, buildAdminStackedBarChartOptions } from '~/components/admin/adminStackedBarChart'
import { formatLocalDate } from '~/services/date'

interface DataSeries {
Expand All @@ -30,6 +30,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
accessibleBorders: {
type: Boolean,
default: false,
},
})

const isDark = useDark()
Expand All @@ -54,7 +58,11 @@ const chartData = computed(() => {
color: item.color,
}))

return buildAdminStackedBarChartData(labels, datasets)
return applyAdminStackedBarAccessibleBorders(
buildAdminStackedBarChartData(labels, datasets),
props.accessibleBorders,
isDark.value,
)
})

const chartOptions = computed(() => buildAdminStackedBarChartOptions(isDark.value))
Expand Down
18 changes: 18 additions & 0 deletions src/components/admin/adminStackedBarChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ export function buildAdminStackedBarChartData(
}
}

export function applyAdminStackedBarAccessibleBorders(
data: ChartData<'bar'>,
accessibleBorders: boolean,
isDark: boolean,
): ChartData<'bar'> {
if (!accessibleBorders)
return data

return {
...data,
datasets: data.datasets.map(dataset => ({
...dataset,
borderColor: isDark ? '#f8fafc' : '#0f172a',
borderWidth: 1,
})),
}
}

export function formatAdminStackedBarTooltip(label: string, value: number, total: number) {
const percentage = total > 0 ? (value / total) * 100 : 0
return `${label}: ${formatNumberValue(value)} (${formatNumberValue(percentage, { maximumFractionDigits: 1 })}%)`
Expand Down
2 changes: 2 additions & 0 deletions src/constants/adminTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import IconArrowPath from '~icons/heroicons/arrow-path'
import IconBanknotes from '~icons/heroicons/banknotes'
import IconBell from '~icons/heroicons/bell'
import IconBuildingOffice from '~icons/heroicons/building-office-2'
import IconChartBar from '~icons/heroicons/chart-bar-square'
import IconCircleStack from '~icons/heroicons/circle-stack'
import IconCommandLine from '~icons/heroicons/command-line'
import IconCurrencyDollar from '~icons/heroicons/currency-dollar'
Expand All @@ -19,6 +20,7 @@ export const adminTabs: Tab[] = [
{ label: 'users', icon: IconUsers, key: '/users' },
{ label: 'admin-organizations', icon: IconBuildingOffice, key: '/organizations' },
{ label: 'revenue', icon: IconBanknotes, key: '/revenue' },
{ label: 'plans-analytics-title', icon: IconChartBar, key: '/plans' },
{ label: 'credits', icon: IconCurrencyDollar, key: '/credits' },
{ label: 'notifications', icon: IconBell, key: '/notifications' },
]
Loading
Loading