Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions messages/en.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,12 @@
"frontend-onboarding-median-time": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-median-time-subtitle": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-no-dropoff": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-people": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart unit and tooltip label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-setup-cli-and-ai": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-setup-cli-only": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-setup-cli-outcomes-description": "Used in Capgo web console areas: pages/admin/dashboard. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-setup-cli-outcomes-v2": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-setup-no-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-transition": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-version-1": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-version-2": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down
18 changes: 12 additions & 6 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2897,12 +2897,12 @@
"admin-notifications-delivery-chart": "Daily delivery",
"admin-notifications-last-month-chart": "Trailing 30 days",
"admin-notifications-campaigns-chart": "Campaigns created per day",
"frontend-onboarding": "Frontend onboarding",
"frontend-onboarding": "Onboarding",
"frontend-onboarding-version-1": "Onboarding v1",
"frontend-onboarding-version-2": "Onboarding v2",
"frontend-onboarding-attempts": "Onboarding attempts",
"frontend-onboarding-attempts-subtitle": "Unique frontend attempts",
"frontend-onboarding-completed": "Frontend onboarding completed",
"frontend-onboarding-attempts-subtitle": "Unique onboarding attempts",
"frontend-onboarding-completed": "Onboarding completed",
"frontend-onboarding-completed-subtitle": "{count} attempts reached setup",
"frontend-onboarding-median-time": "Median completion time",
"frontend-onboarding-median-time-subtitle": "Completed attempts only",
Expand All @@ -2914,10 +2914,10 @@
"frontend-onboarding-daily-details-to-organization": "Daily App details → Organization conversion (v2)",
"frontend-onboarding-daily-intent-to-details": "Daily Intent → App details conversion",
"frontend-onboarding-daily-organization-to-setup": "Daily Organization → Setup reached conversion (v2)",
"frontend-onboarding-funnel-v2": "Frontend onboarding funnel (v2)",
"frontend-onboarding-funnel-v1-legacy": "Frontend onboarding funnel (v1, legacy)",
"frontend-onboarding-funnel-v2": "Onboarding funnel (v2)",
"frontend-onboarding-funnel-v1-legacy": "Onboarding funnel (v1, legacy)",
"frontend-onboarding-funnel-description": "Progress through the new-user app-creation wizard",
"frontend-onboarding-graph-v2": "Frontend onboarding graph (v2)",
"frontend-onboarding-graph-v2": "Onboarding graph (v2)",
"frontend-onboarding-graph-v2-description": "Explore behavior inside each onboarding stage",
"frontend-onboarding-graph-stage-intent": "Intent",
"frontend-onboarding-graph-stage-app-details": "App details",
Expand All @@ -2942,6 +2942,12 @@
"frontend-onboarding-graph-percent-of-total": "{percent}% of total",
"frontend-onboarding-graph-percent-of-previous": "{percent}% of previous",
"frontend-onboarding-graph-percent-of-parent-stage": "{percent}% of parent stage",
"frontend-onboarding-setup-cli-outcomes-v2": "Setup → CLI outcomes (v2)",
"frontend-onboarding-setup-cli-outcomes-description": "Unique people who reached setup, grouped by their CLI and copied AI instruction activity within 24 hours",
"frontend-onboarding-setup-cli-only": "Started CLI",
"frontend-onboarding-setup-cli-and-ai": "Started CLI + AI instructions",
"frontend-onboarding-setup-no-cli": "Didn't start CLI",
"frontend-onboarding-people": "people",
"frontend-onboarding-no-dropoff": "No drop-off",
"frontend-onboarding-transition": "{from} → {to}"
}
68 changes: 42 additions & 26 deletions src/components/admin/AdminBarChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
type: String,
default: '',
},
orientation: {
type: String as () => 'horizontal' | 'vertical',
default: 'horizontal',
},
colors: {
type: Array as () => string[],
default: () => [],
},
})

const isDark = useDark()
Expand Down Expand Up @@ -80,17 +88,17 @@
{
label: props.label,
data: props.values,
backgroundColor: props.labels.map((_, index) => palette[index % palette.length]),
backgroundColor: props.labels.map((_, index) => props.colors[index] ?? palette[index % palette.length]),
borderRadius: 6,
borderSkipped: false,
},
],
}))

const chartOptions = computed<ChartOptions<'bar'>>(() => ({

Check failure on line 98 in src/components/admin/AdminBarChart.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AaAAtym3iF6uvZSGbuYf&open=AaAAtym3iF6uvZSGbuYf&pullRequest=3057
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
indexAxis: props.orientation === 'vertical' ? 'x' : 'y',
layout: {
padding: {
left: 0,
Expand All @@ -112,7 +120,7 @@
padding: 12,
callbacks: {
label: (context) => {
const value = Number(context.parsed.x ?? 0)
const value = Number((props.orientation === 'vertical' ? context.parsed.y : context.parsed.x) ?? 0)
if (props.valueMode === 'count')
return `${context.dataset.label || props.label}: ${formatCountValue(value)}`

Expand All @@ -126,31 +134,39 @@
},
},
},
scales: {
x: {
beginAtZero: true,
suggestedMax: props.valueMode === 'percent' ? 100 : undefined,
grid: {
color: isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.8)',
},
ticks: {
color: isDark.value ? '#9ca3af' : '#6b7280',
callback: (value) => {
if (props.valueMode === 'count')
return formatCountValue(Number(value))
return `${value}%`
scales: props.orientation === 'vertical'
? {
x: {
grid: { display: false },
ticks: { color: isDark.value ? '#9ca3af' : '#6b7280' },
},
y: {
beginAtZero: true,
suggestedMax: props.valueMode === 'percent' ? 100 : undefined,
grid: { color: isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.8)' },
ticks: {
color: isDark.value ? '#9ca3af' : '#6b7280',
precision: props.valueMode === 'count' ? 0 : undefined,
callback: value => props.valueMode === 'count' ? formatCountValue(Number(value)) : `${value}%`,
},
},
}
: {
x: {
beginAtZero: true,
suggestedMax: props.valueMode === 'percent' ? 100 : undefined,
grid: { color: isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.8)' },
ticks: {
color: isDark.value ? '#9ca3af' : '#6b7280',
precision: props.valueMode === 'count' ? 0 : undefined,
callback: value => props.valueMode === 'count' ? formatCountValue(Number(value)) : `${value}%`,
},
},
y: {
grid: { display: false },
ticks: { color: isDark.value ? '#9ca3af' : '#6b7280' },
},
},
},
y: {
grid: {
display: false,
},
ticks: {
color: isDark.value ? '#9ca3af' : '#6b7280',
},
},
},
}))
</script>

Expand Down
48 changes: 48 additions & 0 deletions src/pages/admin/dashboard/frontend-onboarding.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { FrontendOnboardingAnalytics } from '~/services/adminFrontendOnboar
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import AdminBarChart from '~/components/admin/AdminBarChart.vue'
import AdminDailyConversionChart from '~/components/admin/AdminDailyConversionChart.vue'
import AdminFilterBar from '~/components/admin/AdminFilterBar.vue'
import AdminFunnelChart from '~/components/admin/AdminFunnelChart.vue'
Expand Down Expand Up @@ -115,6 +116,24 @@ const v1FunnelSummaries = computed(() => buildFrontendOnboardingFunnelSummaries(
const v2FunnelSummaries = computed(() => buildFrontendOnboardingFunnelSummaries(visibleAnalytics.value?.funnels.v2 ?? []))
const hasDailyAttempts = computed(() => (visibleAnalytics.value?.daily_attempts ?? [])
.some(day => day.v1_attempts > 0 || day.v2_attempts > 0))
const setupCliOutcomes = computed(() => visibleAnalytics.value?.v2_setup_cli_outcomes ?? {
total_users: 0,
cli_only: 0,
cli_and_ai_instructions: 0,
no_cli: 0,
})
const setupCliOutcomeLabels = computed(() => [
t('frontend-onboarding-setup-cli-only'),
t('frontend-onboarding-setup-cli-and-ai'),
t('frontend-onboarding-setup-no-cli'),
])
const setupCliOutcomeValues = computed(() => [
setupCliOutcomes.value.cli_only,
setupCliOutcomes.value.cli_and_ai_instructions,
setupCliOutcomes.value.no_cli,
])
const setupCliOutcomeColors = ['#119eff', '#8b5cf6', '#94a3b8']
const hasSetupCliOutcomeData = computed(() => setupCliOutcomes.value.total_users > 0)

const onboardingGraphV2 = computed<AdminOnboardingJourneyGraphConfig>(() => {
const funnel = visibleAnalytics.value?.funnels.v2 ?? []
Expand Down Expand Up @@ -431,6 +450,35 @@ displayStore.defaultBack = '/dashboard'
</div>
</section>

<ChartCard
:title="t('frontend-onboarding-setup-cli-outcomes-v2')"
:total="setupCliOutcomes.total_users"
:unit="t('frontend-onboarding-people')"
:is-loading="isLoadingStats"
:has-data="hasSetupCliOutcomeData"
>
<template #header>
<div class="min-w-0">
<h2 class="text-xl font-semibold leading-tight text-slate-900 dark:text-white sm:text-2xl">
{{ t('frontend-onboarding-setup-cli-outcomes-v2') }}
</h2>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
{{ t('frontend-onboarding-setup-cli-outcomes-description') }}
</p>
</div>
</template>
<AdminBarChart
:labels="setupCliOutcomeLabels"
:values="setupCliOutcomeValues"
:colors="setupCliOutcomeColors"
:label="t('frontend-onboarding-people')"
:total="setupCliOutcomes.total_users"
value-mode="count"
orientation="vertical"
:is-loading="isLoadingStats"
/>
</ChartCard>

<section class="p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">
{{ t('frontend-onboarding-funnel-v1-legacy') }}
Expand Down
6 changes: 6 additions & 0 deletions src/services/adminFrontendOnboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export interface FrontendOnboardingAnalytics {
count: number
}>
}
v2_setup_cli_outcomes: {
total_users: number
cli_only: number
cli_and_ai_instructions: number
no_cli: number
}
posthog_configured: boolean
posthog_connected: boolean
}
Expand Down
85 changes: 69 additions & 16 deletions supabase/functions/_backend/utils/frontend_onboarding_analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const ONBOARDING_INTERACTION_EVENTS = [
'onboarding_store_url_entered',
] as const

const AI_INSTRUCTIONS_COPIED_EVENT = 'onboarding_ai_instructions_copied'

function sqlStr(value: string): string {
return `'${value.replace(/'/g, '\'\'')}'`
}
Expand Down Expand Up @@ -83,6 +85,10 @@ function attemptId(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}

function personId(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}

function onboardingVersion(value: unknown): FrontendOnboardingVersion | null {
return FRONTEND_ONBOARDING_VERSIONS.includes(value as FrontendOnboardingVersion)
? value as FrontendOnboardingVersion
Expand All @@ -102,6 +108,16 @@ function interactionEvents(value: unknown): FrontendOnboardingInteractionEvent[]
})
}

function timestamps(value: unknown): number[] {
if (!Array.isArray(value))
return []

return value.flatMap((timestamp) => {
const timestampMs = nullableMs(timestamp)
return timestampMs === null ? [] : [timestampMs]
})
}

export function assertFrontendOnboardingAttemptTotal(totalAttempts: unknown, limit = FRONTEND_ONBOARDING_ATTEMPT_LIMIT): number {
if (typeof totalAttempts !== 'number'
|| !Number.isFinite(totalAttempts)
Expand All @@ -126,33 +142,29 @@ function mapAttempts(rows: Record<string, unknown>[]): FrontendOnboardingAttempt
return [{
attemptId: id,
onboardingVersion: version,
personId: personId(row.person_id),
intentMs,
detailsMs: nullableMs(row.details_ms),
organizationMs: nullableMs(row.organization_ms),
setupMs: nullableMs(row.setup_ms),
aiInstructionsCopiedMs: timestamps(row.ai_instructions_copied_ms),
cliStartedMs: timestamps(row.cli_started_ms),
interactionEvents: interactionEvents(row.interaction_events),
}]
})
}

export function buildFrontendOnboardingHogql(startDate: string, cohortEndDate: string, followupEndDate: string): string {
const eventAllowlist = ['onboarding_step_viewed', ...ONBOARDING_INTERACTION_EVENTS].map(sqlStr).join(', ')
const eventAllowlist = ['onboarding_step_viewed', AI_INSTRUCTIONS_COPIED_EVENT, ...ONBOARDING_INTERACTION_EVENTS].map(sqlStr).join(', ')
const interactionEventAllowlist = ONBOARDING_INTERACTION_EVENTS.map(sqlStr).join(', ')
const versionAllowlist = FRONTEND_ONBOARDING_VERSIONS.join(', ')

return `
SELECT
onboarding_version,
attempt_id,
count() OVER () AS total_attempts,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'intent')) AS intent_ms,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'details')) AS details_ms,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'organization')) AS organization_ms,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'setup')) AS setup_ms,
groupUniqArrayIf(tuple(event, toUnixTimestamp64Milli(timestamp)), event != 'onboarding_step_viewed') AS interaction_events
FROM (
WITH frontend_events AS (
SELECT
event,
timestamp,
person_id,
toIntOrZero(toString(properties.onboarding_version)) AS onboarding_version,
JSONExtractString(toString(properties), 'onboarding_attempt_id') AS attempt_id,
JSONExtractString(toString(properties), 'step') AS step
Expand All @@ -162,11 +174,52 @@ export function buildFrontendOnboardingHogql(startDate: string, cohortEndDate: s
AND toIntOrZero(toString(properties.onboarding_version)) IN (${versionAllowlist})
AND timestamp >= parseDateTimeBestEffort(${sqlStr(startDate)})
AND timestamp < parseDateTimeBestEffort(${sqlStr(followupEndDate)})
), onboarding_attempts AS (
SELECT
onboarding_version,
attempt_id,
toString(argMin(person_id, timestamp)) AS person_id,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'intent')) AS intent_ms,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'details')) AS details_ms,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'organization')) AS organization_ms,
toUnixTimestamp64Milli(minIf(timestamp, event = 'onboarding_step_viewed' AND step = 'setup')) AS setup_ms,
groupUniqArrayIf(tuple(event, toUnixTimestamp64Milli(timestamp)), event IN (${interactionEventAllowlist})) AS interaction_events,
groupUniqArrayIf(toUnixTimestamp64Milli(timestamp), event = ${sqlStr(AI_INSTRUCTIONS_COPIED_EVENT)}) AS ai_instructions_copied_ms
FROM frontend_events
WHERE trim(attempt_id) != ''
GROUP BY onboarding_version, attempt_id
HAVING intent_ms >= toUnixTimestamp64Milli(parseDateTimeBestEffort(${sqlStr(startDate)}))
AND intent_ms < toUnixTimestamp64Milli(parseDateTimeBestEffort(${sqlStr(cohortEndDate)}))
), cli_starts AS (
SELECT
toString(person_id) AS person_id,
groupUniqArray(toUnixTimestamp64Milli(timestamp)) AS cli_started_ms
FROM events
WHERE timestamp >= parseDateTimeBestEffort(${sqlStr(startDate)})
AND timestamp < parseDateTimeBestEffort(${sqlStr(followupEndDate)})
AND (
JSONExtractString(toString(properties), 'channel') = 'onboarding-v2'
OR (event = 'CLI Command Invoked'
AND JSONExtractString(toString(properties), 'command_path') = 'init')
OR (event = 'Builder Onboarding Step'
AND JSONExtractString(toString(properties), 'step') IN ('welcome', 'resume-prompt'))
)
GROUP BY person_id
)
WHERE trim(attempt_id) != ''
GROUP BY onboarding_version, attempt_id
HAVING intent_ms >= toUnixTimestamp64Milli(parseDateTimeBestEffort(${sqlStr(startDate)}))
AND intent_ms < toUnixTimestamp64Milli(parseDateTimeBestEffort(${sqlStr(cohortEndDate)}))
SELECT
onboarding_version,
attempt_id,
onboarding_attempts.person_id AS person_id,
count() OVER () AS total_attempts,
intent_ms,
details_ms,
organization_ms,
setup_ms,
interaction_events,
ai_instructions_copied_ms,
cli_started_ms
FROM onboarding_attempts
LEFT JOIN cli_starts USING person_id
ORDER BY intent_ms ASC, onboarding_version ASC, attempt_id ASC
LIMIT ${FRONTEND_ONBOARDING_ATTEMPT_LIMIT}`
}
Expand All @@ -183,7 +236,7 @@ export async function getAdminFrontendOnboardingAnalytics(c: Context, startDate:

const previousStartMs = startMs - durationMs
const queryStartMs = Math.min(previousStartMs, startMs - FRONTEND_ONBOARDING_FOLLOWUP_MS)
const followupEndMs = endMs + FRONTEND_ONBOARDING_FOLLOWUP_MS
const followupEndMs = endMs + 2 * FRONTEND_ONBOARDING_FOLLOWUP_MS
if (queryStartMs < POSTHOG_MIN_DATE_MS || queryStartMs >= POSTHOG_MAX_DATE_MS
|| followupEndMs < POSTHOG_MIN_DATE_MS || followupEndMs >= POSTHOG_MAX_DATE_MS) {
throw new RangeError('derived analytics date boundaries must be within the supported PostHog range')
Expand Down
Loading
Loading