From bcae1761da67c9a17ac50786e190b6d16591c78b Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Thu, 13 Aug 2026 13:39:20 +0300 Subject: [PATCH 01/13] feat(console): surface how many devices have a bundle Reach already lived in channel statistics, but people look at the bundle page and Observe after upload. Show the same check-in share there, on the app dashboard, and in the 24h deploy email. Co-authored-by: Cursor --- messages/en.context.json | 12 +- messages/en.json | 10 +- playwright/e2e/bundle-adoption.spec.ts | 18 + playwright/visual-diff.config.ts | 10 + src/components/bundle/BundleAdoptionCard.vue | 171 +++++++ src/components/dashboard/ReleaseBanner.vue | 26 +- src/pages/app/[app].bundle.[bundle].vue | 438 +++++++++--------- src/pages/app/[app].devices.vue | 2 +- src/pages/app/[app].observe.updater.vue | 28 ++ src/pages/app/[app].vue | 9 + src/services/bundleAdoption.ts | 115 +++++ .../functions/_backend/triggers/cron_email.ts | 48 +- .../_backend/utils/cron_email_stats.ts | 28 ++ tests/bundle-adoption.unit.test.ts | 59 +++ tests/cron-email-stats.unit.test.ts | 22 + 15 files changed, 764 insertions(+), 232 deletions(-) create mode 100644 playwright/e2e/bundle-adoption.spec.ts create mode 100644 src/components/bundle/BundleAdoptionCard.vue create mode 100644 src/services/bundleAdoption.ts create mode 100644 tests/bundle-adoption.unit.test.ts diff --git a/messages/en.context.json b/messages/en.context.json index cbabf70e51..9fd07294ba 100644 --- a/messages/en.context.json +++ b/messages/en.context.json @@ -632,8 +632,12 @@ "builds-last-month-trend": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "builds-period": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "builds-trend": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "bundle": "Used in Capgo web console areas: app, components/dashboard, components/tables, composables, layouts. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle": "Used in Capgo web console areas: app, components/bundle, components/dashboard, components/tables, composables. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-active": "Used in Capgo web console areas: pages/app. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle-adoption": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle-adoption-devices": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle-adoption-empty": "Used in Capgo web console areas: components/bundle. Role: empty state text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle-adoption-help": "Used in Capgo web console areas: components/bundle. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-assigned-to-this-channel": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-comment": "Used in Capgo web console areas: pages/app. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-comment-empty": "Used in Capgo web console areas: pages/app. Role: empty state text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -1501,7 +1505,7 @@ "link-this-bundle-to-another-channel": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "linked-bundle": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "loading": "Used in Capgo web console areas: components, components/bundle, components/dashboard, components/permissions, components/tables. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", - "loading-statistics": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "loading-statistics": "Used in Capgo web console areas: components/bundle, pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "loading-version": "Used in Capgo web console areas: components/tables. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "log-as": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "log-in": "Used in Capgo web console areas: pages. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -2169,6 +2173,7 @@ "registration-to-subscription-conversion": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "registrations-by-source": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "registrations-by-source-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.", + "release-banner-adoption": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "release-or-30-days": "Used in Capgo web console. Role: filter or date-range option label about \"release or 30 days\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "release-status": "Used in Capgo web console. Role: UI label about \"release status\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "released": "Used in Capgo web console areas: components/dashboard, pages/app. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -2652,6 +2657,9 @@ "view": "Used in Capgo web console areas: components/tables, composables, pages/admin/dashboard, pages/settings/organization. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "view-action-logs": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "view-adoption": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "view-bundle-devices": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "view-channel-adoption": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "view-channel-adoption-named": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "view-deliveries": "Used in Capgo web console areas: pages/settings/organization. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "view-dependencies": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "view-logs": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", diff --git a/messages/en.json b/messages/en.json index 20c4d5400e..4cd2452bed 100644 --- a/messages/en.json +++ b/messages/en.json @@ -609,6 +609,10 @@ "builds-trend": "Native Builds Trend (Total, iOS, Android)", "bundle": "Bundle", "bundle-active": "Active", + "bundle-adoption": "Bundle reach", + "bundle-adoption-devices": "{count} / {total} devices", + "bundle-adoption-empty": "No devices have reported {version} yet. They pick it up the next time they check for updates.", + "bundle-adoption-help": "{count} of {total} devices ({percent}) currently report {version}.", "bundle-comment": "Comment", "bundle-comment-empty": "No comment", "bundle-comment-placeholder": "Release notes, commit hash, or context", @@ -1543,6 +1547,7 @@ "new-name-to-short": "The apikey name is too short. It must be at least 4 characters long", "new-record": "New Record", "new-release-available": "New release available", + "release-banner-adoption": "{percent} of devices", "next": "Next", "next-run": "Next update", "no": "no", @@ -1791,7 +1796,7 @@ "org-notifications-bundle-deployed-desc": "Send bundle deployment notifications to the organization's management email", "org-notifications-channel-self-rejected-desc": "Send channel self-assignment rejection notifications to the organization's management email", "org-notifications-credit-usage-desc": "Send credit usage alerts to the organization's management email", - "org-notifications-deploy-stats-desc": "Send 24-hour deployment statistics to the organization's management email", + "org-notifications-deploy-stats-desc": "Send 24-hour install counts and how many devices reached the deployed bundle to the organization's management email", "org-notifications-description": "Control which email notifications are sent to your organization's management email address. These settings only apply when the management email is different from individual admin email addresses.", "org-notifications-device-error-desc": "Send device update error notifications to the organization's management email", "org-notifications-monthly-stats-desc": "Send monthly statistics to the organization's management email", @@ -2490,6 +2495,9 @@ "version-rules-tester-description": "Learn how versions work and simulate update scenarios", "view": "View", "view-adoption": "View adoption", + "view-bundle-devices": "View devices on this bundle", + "view-channel-adoption": "View channel adoption", + "view-channel-adoption-named": "View {channel} adoption", "view-deliveries": "View Deliveries", "view-dependencies": "View dependencies", "view-on-npm": "View on npm", diff --git a/playwright/e2e/bundle-adoption.spec.ts b/playwright/e2e/bundle-adoption.spec.ts new file mode 100644 index 0000000000..e99724c5c6 --- /dev/null +++ b/playwright/e2e/bundle-adoption.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '../support/commands' + +test.describe('Bundle reach', () => { + test.beforeEach(async ({ page }) => { + await page.login('test@capgo.app', 'testtest') + }) + + test('shows reach on the bundle page and observe updater', async ({ page }) => { + await page.goto('/app/com.demo.app/bundle/3') + const bundleCard = page.locator('[data-test="bundle-adoption-card"]') + await expect(bundleCard).toBeVisible() + await expect(bundleCard.getByRole('heading', { name: 'Bundle reach' })).toBeVisible() + await expect(page.locator('[data-test="bundle-adoption-devices"]')).toBeVisible() + + await page.goto('/app/com.demo.app/observe/updater') + await expect(page.locator('[data-test="bundle-adoption-card"]')).toBeVisible() + }) +}) diff --git a/playwright/visual-diff.config.ts b/playwright/visual-diff.config.ts index 32317805c2..1fc9e1d614 100644 --- a/playwright/visual-diff.config.ts +++ b/playwright/visual-diff.config.ts @@ -61,6 +61,16 @@ export const visualDiffRoutes: VisualDiffRoute[] = [ { slug: 'observe-native', path: '/app/com.demo.app/observe/native', auth: true }, { slug: 'observe-compatibility', path: '/app/com.demo.app/observe/compatibility', auth: true }, { slug: 'observe-plugins', path: '/app/com.demo.app/observe/plugins', auth: true }, + { + slug: 'bundle-detail', + path: '/app/com.demo.app/bundle/3', + auth: true, + }, + { + slug: 'channel-statistics', + path: '/app/com.demo.app/channel/1/statistics', + auth: true, + }, { slug: 'api-keys-app-preview', path: '/apikeys', diff --git a/src/components/bundle/BundleAdoptionCard.vue b/src/components/bundle/BundleAdoptionCard.vue new file mode 100644 index 0000000000..790cedadb0 --- /dev/null +++ b/src/components/bundle/BundleAdoptionCard.vue @@ -0,0 +1,171 @@ + + + diff --git a/src/components/dashboard/ReleaseBanner.vue b/src/components/dashboard/ReleaseBanner.vue index 904cbe79ca..39ecdd085a 100644 --- a/src/components/dashboard/ReleaseBanner.vue +++ b/src/components/dashboard/ReleaseBanner.vue @@ -4,7 +4,10 @@ import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import IconCheckCircle from '~icons/lucide/check-circle' import IconTrendingUp from '~icons/lucide/trending-up' -import { formatDistanceToNow } from '~/services/date' +import { getLatestDayVersionAdoption } from '~/services/bundleAdoption' +import { useChartData } from '~/services/chartDataService' +import { formatDistanceToNow, getChartDateRange } from '~/services/date' +import { formatNumberValue } from '~/services/formatLocale' import { useSupabase } from '~/services/supabase' import { useOrganizationStore } from '~/stores/organization' @@ -21,6 +24,7 @@ const isLoading = ref(false) const lastVersion = ref('') const lastReleaseDate = ref(null) const defaultChannelId = ref(null) +const adoptionPercent = ref(null) const HOURS_48_IN_DAYS = 2 @@ -30,6 +34,12 @@ const lastReleaseDisplay = computed(() => { return formatDistanceToNow(new Date(lastReleaseDate.value)) }) +const adoptionPercentLabel = computed(() => { + if (adoptionPercent.value === null) + return '' + return `${formatNumberValue(adoptionPercent.value, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%` +}) + const hasRecentRelease = computed(() => { if (!lastReleaseDate.value || isLoading.value) return false @@ -53,6 +63,7 @@ async function fetchReleaseInfo() { lastVersion.value = '' lastReleaseDate.value = null defaultChannelId.value = null + adoptionPercent.value = null return } @@ -77,10 +88,20 @@ async function fetchReleaseInfo() { if (latestVersion) { lastVersion.value = latestVersion.name lastReleaseDate.value = latestVersion.created_at + try { + const { startDate, endDate } = getChartDateRange(false) + const chartData = await useChartData(supabase, props.appId, startDate, endDate, 'bundle') + adoptionPercent.value = getLatestDayVersionAdoption(chartData?.datasets ?? [], latestVersion.name)?.percent ?? 0 + } + catch (error) { + console.error('Error fetching bundle adoption:', error) + adoptionPercent.value = null + } } else { lastVersion.value = '' lastReleaseDate.value = null + adoptionPercent.value = null } defaultChannelId.value = defaultChannel?.id || null @@ -128,6 +149,9 @@ watch(() => [props.appId, organizationStore.currentOrganization?.gid], () => {

{{ t('version') }} {{ lastVersion }} — {{ t('released') }} {{ lastReleaseDisplay }} +

diff --git a/src/pages/app/[app].bundle.[bundle].vue b/src/pages/app/[app].bundle.[bundle].vue index 493f140377..f9f0b2e118 100644 --- a/src/pages/app/[app].bundle.[bundle].vue +++ b/src/pages/app/[app].bundle.[bundle].vue @@ -97,6 +97,14 @@ watch([() => channels.value, () => promotableChannelIds.value], () => { const canPromoteBundle = computed(() => promotableChannelIds.value.size > 0) +const linkedChannelsForVersion = computed(() => { + if (!version.value) + return [] + return channels.value.filter(channel => channel.version === version.value!.id) +}) + +const primaryLinkedChannel = computed(() => linkedChannelsForVersion.value[0] ?? null) + const canEditBundleMetadata = computedAsync(async () => { if (!version.value?.app_id) return false @@ -815,236 +823,244 @@ async function deleteBundle() {
-
-
- - {{ version.name }} - - - {{ version.id.toString() }} - - - {{ formatDate(version.created_at) }} - - - {{ formatDate(version.updated_at) }} - - - - - {{ hideString(version.checksum) }} - -
+
+ +
+
+ + {{ version.name }} + + + {{ version.id.toString() }} + + + {{ formatDate(version.created_at) }} + + + {{ formatDate(version.updated_at) }} + + + + + {{ hideString(version.checksum) }} + +
+ + +
+
{{ t('checksum-type-info') }}
+
{{ t('min-plugin-version') }}: {{ checksumInfo.minPluginVersion }}
+
{{ t('checksum-sha256-desc') }}
+
{{ t('checksum-crc32-desc') }}
+ +
+
+
- -
-
{{ t('checksum-type-info') }}
-
{{ t('min-plugin-version') }}: {{ checksumInfo.minPluginVersion }}
-
{{ t('checksum-sha256-desc') }}
-
{{ t('checksum-crc32-desc') }}
- -
+ + + + + {{ version.min_update_version }} + + + +
+
+ + {{ chn!.name }} + +
- - -
- - - {{ version.min_update_version }} - - - -
-
+ + + {{ t('set-bundle') }} + + + {{ t('encrypted') }} + + + {{ t('not-encrypted-bundle') }} + + + + {{ hideString(version.session_key) }} + + + + {{ version.key_id }} + + + + {{ version.cli_version }} + + + + {{ version.external_url }} + + + +
- {{ chn!.name }} + {{ version.link || t('bundle-link-empty') }}
-
- - - {{ t('set-bundle') }} - - - {{ t('encrypted') }} - - - {{ t('not-encrypted-bundle') }} - - - - {{ hideString(version.session_key) }} - - - - {{ version.key_id }} - - - - {{ version.cli_version }} - - - - {{ version.external_url }} - - - -
- - {{ version.link || t('bundle-link-empty') }} + + + +
+ + {{ version.comment || t('bundle-comment-empty') }} + + +
+
+ + + + + - -
-
- - -
- - {{ version.comment || t('bundle-comment-empty') }} + + + + + + - -
-
- - - - - - - - - - - - - - - - - - - {{ t('bundle-active') }} - - - - - - - {{ t('bundle-deleted') }} - -
+ + + + + + {{ t('bundle-deleted') }} + +
+
diff --git a/src/pages/app/[app].devices.vue b/src/pages/app/[app].devices.vue index ead3c33868..6d64bfc395 100644 --- a/src/pages/app/[app].devices.vue +++ b/src/pages/app/[app].devices.vue @@ -58,7 +58,7 @@ watchEffect(async () => {
- +
diff --git a/src/pages/app/[app].observe.updater.vue b/src/pages/app/[app].observe.updater.vue index e23fbe7566..8e13950276 100644 --- a/src/pages/app/[app].observe.updater.vue +++ b/src/pages/app/[app].observe.updater.vue @@ -85,6 +85,7 @@ const insightsLoading = ref(false) const selectedDays = ref(7) const app = ref() const insights = ref(null) +const publicChannel = ref<{ id: number, name: string, versionName: string } | null>(null) let latestInsightsRequest = 0 const appRouteSegment = computed(() => { @@ -160,6 +161,25 @@ async function loadAppInfo() { .eq('app_id', id.value) .single() app.value = dataApp || app.value + + const { data: channelData } = await supabase + .from('channels') + .select(` + id, + name, + version:app_versions!channels_version_fkey(id, name) + `) + .eq('app_id', id.value) + .eq('public', true) + .order('id', { ascending: true }) + .limit(1) + .maybeSingle() + + const version = channelData?.version as { id?: number, name?: string } | { id?: number, name?: string }[] | null | undefined + const versionRow = Array.isArray(version) ? version[0] : version + publicChannel.value = channelData && versionRow?.name + ? { id: channelData.id, name: channelData.name, versionName: versionRow.name } + : null } catch (error) { console.error(error) @@ -267,6 +287,14 @@ watchEffect(async () => {
+

diff --git a/src/pages/app/[app].vue b/src/pages/app/[app].vue index 91a649f218..bafd4eb068 100644 --- a/src/pages/app/[app].vue +++ b/src/pages/app/[app].vue @@ -202,6 +202,15 @@ watchEffect(async () => { :force-demo="appNotFound" class="col-span-full sm:col-span-6 xl:col-span-4" /> + + metaCounts?: Array + metaCountValues?: Array +} + +function toNonNegativeInt(value: number | null | undefined): number { + if (typeof value !== 'number' || !Number.isFinite(value)) + return 0 + return Math.max(0, Math.round(value)) +} + +function getCountSeries(dataset: ChartDatasetLike): Array { + if (Array.isArray(dataset.metaCountValues) && dataset.metaCountValues.length > 0) + return dataset.metaCountValues + if (Array.isArray(dataset.metaCounts) && dataset.metaCounts.length > 0) + return dataset.metaCounts + return [] +} + +function findLatestIndex(datasets: ChartDatasetLike[]): number { + let lastIndex = -1 + + for (const dataset of datasets) { + const counts = getCountSeries(dataset) + const percents = dataset.data ?? [] + const length = Math.max(counts.length, percents.length) + + for (let index = length - 1; index >= 0; index--) { + const percent = percents[index] + if (toNonNegativeInt(counts[index]) > 0 || (typeof percent === 'number' && Number.isFinite(percent) && percent > 0)) { + lastIndex = Math.max(lastIndex, index) + break + } + } + } + + return lastIndex +} + +function percentFromShare(count: number, total: number, fallbackPercent: number | null | undefined): number { + if (total > 0) + return Math.round((count / total) * 1000) / 10 + if (typeof fallbackPercent === 'number' && Number.isFinite(fallbackPercent)) + return Math.max(0, Math.round(fallbackPercent * 10) / 10) + return 0 +} + +/** + * Latest-day unique-device share for a specific bundle, or the leading bundle + * when no version name is given. Counts come from daily check-in stats. + */ +export function getLatestDayVersionAdoption( + datasets: ChartDatasetLike[], + versionName?: string, +): VersionAdoption | null { + if (!datasets.length) + return null + + const lastIndex = findLatestIndex(datasets) + if (lastIndex < 0) { + if (!versionName) + return null + return { + versionName, + count: 0, + total: 0, + percent: 0, + } + } + + const total = datasets.reduce((sum, dataset) => { + return sum + toNonNegativeInt(getCountSeries(dataset)[lastIndex]) + }, 0) + + const namedDataset = versionName + ? datasets.find(dataset => dataset.label === versionName) + : undefined + + if (versionName && !namedDataset) { + return { + versionName, + count: 0, + total, + percent: 0, + } + } + + const target = namedDataset ?? datasets.reduce((current, dataset) => { + const count = toNonNegativeInt(getCountSeries(dataset)[lastIndex]) + if (!current) + return dataset + const currentCount = toNonNegativeInt(getCountSeries(current)[lastIndex]) + return count > currentCount ? dataset : current + }, null) + + if (!target) + return null + + const count = toNonNegativeInt(getCountSeries(target)[lastIndex]) + return { + versionName: target.label, + count, + total, + percent: percentFromShare(count, total, target.data?.[lastIndex]), + } +} diff --git a/supabase/functions/_backend/triggers/cron_email.ts b/supabase/functions/_backend/triggers/cron_email.ts index 3414542da8..0dfd02950b 100644 --- a/supabase/functions/_backend/triggers/cron_email.ts +++ b/supabase/functions/_backend/triggers/cron_email.ts @@ -7,13 +7,14 @@ import { getPreviousMonthUtcRange, shouldRetryDeployInstallStats, shouldSendDeployInstallStatsEmail, + summarizeDeviceVersionAdoption, sumVersionInstalls, } from '../utils/cron_email_stats.ts' import { BRES, middlewareAPISecret, parseBody, simpleError } from '../utils/hono.ts' import { cloudlog, cloudlogErr } from '../utils/logging.ts' import { sendEmailToOrgMembers } from '../utils/org_email_notifications.ts' import { findBestPlan } from '../utils/plans.ts' -import { readStatsVersion } from '../utils/stats.ts' +import { readDeviceVersionCounts, readStatsVersion } from '../utils/stats.ts' import { getCurrentPlanNameOrg, supabaseAdmin } from '../utils/supabase.ts' export const app = new Hono() @@ -266,22 +267,37 @@ async function handleDeployInstallStats( // Coerce installs with Number() — Analytics Engine sum() can arrive as stringy Float64. const installs = sumVersionInstalls(versionStats, versionName, versionId) - const metadata = { - app_id: appId, - app_name: appName ?? '', - deploy_id: deployId?.toString(), - version_id: versionId?.toString(), - version_name: versionName ?? '', - channel_id: channelId?.toString(), - channel_name: channelName ?? '', - platform: platform ?? '', - deployed_at: windowStart, - install_count_24h: installs.toString(), - window_hours: '24', - } - if (shouldSendDeployInstallStatsEmail(installs)) { - await sendEmailToOrgMembers(c, 'bundle:install_stats_24h', 'deploy_stats_24h', metadata, orgId ?? await getOrgIdForApp(c, appId)) + let adoption = summarizeDeviceVersionAdoption({}, versionName) + try { + const deviceCounts = await readDeviceVersionCounts(c, appId, channelName) + adoption = summarizeDeviceVersionAdoption(deviceCounts, versionName) + } + catch (error) { + cloudlogErr({ + requestId: c.get('requestId'), + message: 'Failed to read device version counts for deploy stats email', + error, + metadata: { appId, versionName, channelName }, + }) + } + + await sendEmailToOrgMembers(c, 'bundle:install_stats_24h', 'deploy_stats_24h', { + app_id: appId, + app_name: appName ?? '', + deploy_id: deployId?.toString(), + version_id: versionId?.toString(), + version_name: versionName ?? '', + channel_id: channelId?.toString(), + channel_name: channelName ?? '', + platform: platform ?? '', + deployed_at: windowStart, + install_count_24h: installs.toString(), + window_hours: '24', + device_count: adoption.device_count.toString(), + total_devices: adoption.total_devices.toString(), + adoption_percent: adoption.adoption_percent, + }, orgId ?? await getOrgIdForApp(c, appId)) return c.json(BRES) } diff --git a/supabase/functions/_backend/utils/cron_email_stats.ts b/supabase/functions/_backend/utils/cron_email_stats.ts index 8b556ad309..b05ef16e26 100644 --- a/supabase/functions/_backend/utils/cron_email_stats.ts +++ b/supabase/functions/_backend/utils/cron_email_stats.ts @@ -215,3 +215,31 @@ export function shouldRetryDeployInstallStats(deployedAt: Date, now: Date = new export function shouldSendDeployInstallStatsEmail(installs: number): boolean { return installs > 1 } + +export interface DeviceVersionAdoptionSummary { + device_count: number + total_devices: number + adoption_percent: string +} + +/** Live unique-device reach for a deployed bundle from version_name counts. */ +export function summarizeDeviceVersionAdoption( + counts: Record, + versionName?: string | null, +): DeviceVersionAdoptionSummary { + const totalDevices = Object.values(counts).reduce((sum, value) => { + return sum + Math.max(0, Math.round(toStatNumber(value))) + }, 0) + const deviceCount = versionName + ? Math.max(0, Math.round(toStatNumber(counts[versionName] ?? 0))) + : 0 + const adoptionPercent = totalDevices > 0 + ? (Math.round((deviceCount / totalDevices) * 1000) / 10).toFixed(1) + : '0.0' + + return { + device_count: deviceCount, + total_devices: totalDevices, + adoption_percent: adoptionPercent, + } +} diff --git a/tests/bundle-adoption.unit.test.ts b/tests/bundle-adoption.unit.test.ts new file mode 100644 index 0000000000..ecaaa01e60 --- /dev/null +++ b/tests/bundle-adoption.unit.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import { getLatestDayVersionAdoption } from '~/services/bundleAdoption' + +describe('getLatestDayVersionAdoption', () => { + it.concurrent('returns the named bundle share on the latest day with counts', () => { + const adoption = getLatestDayVersionAdoption([ + { label: '1.0.0', metaCountValues: [80, 60, 20], data: [80, 60, 20] }, + { label: '1.1.0', metaCountValues: [20, 40, 80], data: [20, 40, 80] }, + ], '1.1.0') + + expect(adoption).toEqual({ + versionName: '1.1.0', + count: 80, + total: 100, + percent: 80, + }) + }) + + it.concurrent('returns zero for a bundle that has not been reported yet', () => { + const adoption = getLatestDayVersionAdoption([ + { label: '1.0.0', metaCounts: [100], data: [100] }, + ], '1.2.0') + + expect(adoption).toEqual({ + versionName: '1.2.0', + count: 0, + total: 100, + percent: 0, + }) + }) + + it.concurrent('picks the leading bundle when no version is given', () => { + const adoption = getLatestDayVersionAdoption([ + { label: '1.0.0', metaCountValues: [10, 30] }, + { label: '1.1.0', metaCountValues: [90, 70] }, + ]) + + expect(adoption).toEqual({ + versionName: '1.1.0', + count: 70, + total: 100, + percent: 70, + }) + }) + + it.concurrent('skips trailing empty days', () => { + const adoption = getLatestDayVersionAdoption([ + { label: '1.0.0', metaCountValues: [40, 0], data: [40, 0] }, + { label: '1.1.0', metaCountValues: [60, 0], data: [60, 0] }, + ], '1.1.0') + + expect(adoption).toEqual({ + versionName: '1.1.0', + count: 60, + total: 100, + percent: 60, + }) + }) +}) diff --git a/tests/cron-email-stats.unit.test.ts b/tests/cron-email-stats.unit.test.ts index 42b2fb796b..b1b9b3e5cf 100644 --- a/tests/cron-email-stats.unit.test.ts +++ b/tests/cron-email-stats.unit.test.ts @@ -7,6 +7,7 @@ import { getPreviousMonthUtcRange, shouldRetryDeployInstallStats, shouldSendDeployInstallStatsEmail, + summarizeDeviceVersionAdoption, sumVersionInstalls, toStatNumber, } from '../supabase/functions/_backend/utils/cron_email_stats.ts' @@ -198,3 +199,24 @@ describe('shouldRetryDeployInstallStats', () => { expect(shouldRetryDeployInstallStats(deployedAt, now)).toBe(false) }) }) + +describe('summarizeDeviceVersionAdoption', () => { + it.concurrent('computes live device reach for a deployed bundle', () => { + expect(summarizeDeviceVersionAdoption({ + '1.0.0': 20, + '1.1.0': 80, + }, '1.1.0')).toEqual({ + device_count: 80, + total_devices: 100, + adoption_percent: '80.0', + }) + }) + + it.concurrent('returns zeros when no devices have reported', () => { + expect(summarizeDeviceVersionAdoption({}, '1.1.0')).toEqual({ + device_count: 0, + total_devices: 0, + adoption_percent: '0.0', + }) + }) +}) From d089e15ada531d00eeccf51bce556e63455ccd36 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Thu, 13 Aug 2026 13:43:03 +0300 Subject: [PATCH 02/13] fix(backend): type device adoption reduce as number Object.values on Record infers a union accumulator, which failed CI typecheck. Co-authored-by: Cursor --- supabase/functions/_backend/utils/cron_email_stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supabase/functions/_backend/utils/cron_email_stats.ts b/supabase/functions/_backend/utils/cron_email_stats.ts index b05ef16e26..1ce1c0d193 100644 --- a/supabase/functions/_backend/utils/cron_email_stats.ts +++ b/supabase/functions/_backend/utils/cron_email_stats.ts @@ -227,7 +227,7 @@ export function summarizeDeviceVersionAdoption( counts: Record, versionName?: string | null, ): DeviceVersionAdoptionSummary { - const totalDevices = Object.values(counts).reduce((sum, value) => { + const totalDevices = Object.values(counts).reduce((sum, value) => { return sum + Math.max(0, Math.round(toStatNumber(value))) }, 0) const deviceCount = versionName From b37efc12839b7e0b51a9e7544dd4cc012681d607 Mon Sep 17 00:00:00 2001 From: Martin DONADIEU Date: Thu, 13 Aug 2026 13:54:39 +0300 Subject: [PATCH 03/13] docs: add bundle reach preview for PR 3032 --- .github/pr-previews/3032-bundle-reach.webp | Bin 0 -> 29328 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/pr-previews/3032-bundle-reach.webp diff --git a/.github/pr-previews/3032-bundle-reach.webp b/.github/pr-previews/3032-bundle-reach.webp new file mode 100644 index 0000000000000000000000000000000000000000..759aec9358f71dfd001bae3b328adcee210321b5 GIT binary patch literal 29328 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 z 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 Date: Thu, 13 Aug 2026 13:55:32 +0300 Subject: [PATCH 04/13] fix(console): use native progress for bundle reach Sonar flagged the fake progressbar and the extra try/catch in the 24h deploy email. Co-authored-by: Cursor --- .../bundle-reach.webp | Bin artifacts/observe-reach.webp | Bin 0 -> 37952 bytes src/components/bundle/BundleAdoptionCard.vue | 15 +++----- .../functions/_backend/triggers/cron_email.ts | 35 +++++++++++------- 4 files changed, 27 insertions(+), 23 deletions(-) rename .github/pr-previews/3032-bundle-reach.webp => artifacts/bundle-reach.webp (100%) create mode 100644 artifacts/observe-reach.webp diff --git a/.github/pr-previews/3032-bundle-reach.webp b/artifacts/bundle-reach.webp similarity index 100% rename from .github/pr-previews/3032-bundle-reach.webp rename to artifacts/bundle-reach.webp diff --git a/artifacts/observe-reach.webp b/artifacts/observe-reach.webp new file mode 100644 index 0000000000000000000000000000000000000000..ff0023d7b0e23261c5a30e86602d7373b217df34 GIT binary patch literal 37952 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 literal 0 HcmV?d00001 diff --git a/src/components/bundle/BundleAdoptionCard.vue b/src/components/bundle/BundleAdoptionCard.vue index 790cedadb0..05f88241cd 100644 --- a/src/components/bundle/BundleAdoptionCard.vue +++ b/src/components/bundle/BundleAdoptionCard.vue @@ -37,7 +37,6 @@ const percentLabel = computed(() => { const countLabel = computed(() => formatNumberValue(adoption.value?.count ?? 0)) const totalLabel = computed(() => formatNumberValue(adoption.value?.total ?? 0)) -const progressWidth = computed(() => `${Math.max(0, Math.min(100, adoption.value?.percent ?? 0))}%`) const hasDevices = computed(() => (adoption.value?.total ?? 0) > 0) async function loadAdoption() { @@ -134,16 +133,12 @@ watch(() => [props.appId, props.versionName] as const, () => { {{ t('bundle-adoption-devices', { count: countLabel, total: totalLabel }) }}

-
-
-
+ />