Skip to content
Merged
4 changes: 4 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,10 @@
"native-observe-open-logs": "Open logs",
"native-observe-overview": "All versions summary",
"native-observe-overview-help": "Cards, charts, and action breakdown aggregate every reported native app version during {period}.",
"native-observe-plugin-adoption": "Updater plugin adoption",
"native-observe-plugin-adoption-help": "Current production devices grouped by their reported updater plugin version.",
"native-observe-plugin-version": "Plugin version",
"native-observe-no-plugin-data": "No updater plugin versions reported yet",
"native-observe-p50": "P50",
"native-observe-p90": "P90",
"native-observe-p99": "P99",
Expand Down
48 changes: 48 additions & 0 deletions src/pages/app/[app].observe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ interface NativeObserveStatsResponse {
launch_p90_ms: number | null
webview_load_p90_ms: number | null
}>
pluginVersions: Array<{
plugin_version: string
devices: number
total_devices: number
}>
releaseMarkers: Array<{
version_name: string
channel_name: string
Expand All @@ -95,6 +100,7 @@ const periodDayOptions: PeriodDayOption[] = [1, 3, 7, 30]
const stats = ref<NativeObserveStatsResponse | null>(null)
const statsLoading = ref(false)
let latestStatsRequest = 0
const topPluginVersions = computed(() => stats.value?.pluginVersions.slice(0, 8) ?? [])

const hasData = computed(() => (stats.value?.overview.total_events ?? 0) > 0)
const topActions = computed(() => stats.value?.actionBreakdown.slice(0, 10) ?? [])
Expand Down Expand Up @@ -447,6 +453,48 @@ watch([packageId, days], async () => {
<Spinner size="w-5 h-5" />
</div>

<div class="p-4 bg-white border rounded-lg shadow-sm dark:bg-slate-800 border-slate-200 dark:border-slate-700">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h2 class="text-base font-semibold text-slate-950 dark:text-white">
{{ t('native-observe-plugin-adoption') }}
</h2>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
{{ t('native-observe-plugin-adoption-help') }}
</p>
</div>
<IconRocket class="w-5 h-5 text-violet-500" />
</div>
<div v-if="topPluginVersions.length" class="overflow-x-auto">
<table class="d-table d-table-sm w-full min-w-[460px]">
<thead>
<tr>
<th class="whitespace-nowrap">
{{ t('native-observe-plugin-version') }}
</th>
<th class="whitespace-nowrap">
{{ t('devices') }}
</th>
<th class="whitespace-nowrap">
{{ t('adoption-rate') }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="version in topPluginVersions" :key="version.plugin_version">
<td class="font-medium text-slate-900 dark:text-slate-100">
{{ version.plugin_version }}
</td>
<td>{{ formatCount(version.devices) }}</td>
<td>{{ formatPercent(version.total_devices > 0 ? (version.devices / version.total_devices) * 100 : 0) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="text-sm text-slate-500 dark:text-slate-400">
{{ t('native-observe-no-plugin-data') }}
</div>
</div>
<div v-if="!hasData" class="flex flex-col items-center justify-center h-72 bg-white border rounded-lg shadow-sm dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-500 dark:text-slate-400">
<IconActivity class="w-12 h-12 mb-3" />
<h2 class="text-lg font-semibold text-slate-800 dark:text-slate-100">
Expand Down
42 changes: 34 additions & 8 deletions supabase/functions/_backend/private/native_observe_stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@
const supportedPeriodDays = [1, 3, 7, 30] as const
type NativeObservePeriodDays = typeof supportedPeriodDays[number]

type NativeObserveStatsRequest = {
interface NativeObserveStatsRequest {
app_id: string
days?: number
}

type NativeObserveMetricRow = {
interface NativeObserveMetricRow {
day?: string
action: string
events: number | string
devices: number | string
p50_ms: number | string | null

Check warning on line 26 in supabase/functions/_backend/private/native_observe_stats.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this union type with a type alias.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ9fuCXskHs2w76So7GD&open=AZ9fuCXskHs2w76So7GD&pullRequest=2681
p90_ms: number | string | null
p99_ms: number | string | null
}

type NativeObserveVersionRow = {
interface NativeObserveVersionRow {
version_name: string
events: number | string
devices: number | string
Expand All @@ -38,7 +38,13 @@
webview_load_p90_ms: number | string | null
}

type NativeObserveOverviewRow = {
interface NativeObservePluginVersionRow {
plugin_version: string
devices: number | string
total_devices: number | string
}

interface NativeObserveOverviewRow {
events: number | string
devices: number | string
issue_count: number | string
Expand All @@ -50,17 +56,18 @@
webview_load_p90_ms: number | string | null
}

type NativeObserveReleaseMarker = {
interface NativeObserveReleaseMarker {
version_name: string
channel_name: string
deployed_at: string
}

type NativeObserveNumericValue = number | string | null | undefined

type BuildNativeObserveResponseInput = {
interface BuildNativeObserveResponseInput {
labels: string[]
days: NativeObservePeriodDays
pluginVersionRows: NativeObservePluginVersionRow[]
start: string
end: string
dailyRows: NativeObserveMetricRow[]
Expand Down Expand Up @@ -188,6 +195,18 @@
ORDER BY events DESC, version_name ASC
LIMIT 12`

const pluginVersionStatsQuery = `SELECT
COALESCE(NULLIF(plugin_version, ''), 'unknown') AS plugin_version,
count(*)::integer AS devices,
sum(count(*)) OVER ()::integer AS total_devices
FROM public.devices
WHERE app_id = $1
AND is_prod IS TRUE
AND is_emulator IS NOT TRUE
GROUP BY plugin_version
ORDER BY devices DESC, plugin_version ASC
LIMIT 12`

const releaseMarkersQuery = `SELECT
COALESCE(NULLIF(app_versions.name, ''), 'unknown') AS version_name,
COALESCE(NULLIF(channels.name, ''), 'unknown') AS channel_name,
Expand Down Expand Up @@ -238,8 +257,8 @@
return Math.round(numeric * factor) / factor
}

function createSeries(length: number) {
return Array.from({ length }, () => 0)
function createSeries(length: number): number[] {
return Array.from<number>({ length }).fill(0)
}

function setMetric(series: Array<number | null>, index: number, value: NativeObserveNumericValue) {
Expand Down Expand Up @@ -354,6 +373,11 @@
launch_p90_ms: toMetric(row.launch_p90_ms),
webview_load_p90_ms: toMetric(row.webview_load_p90_ms),
})),
pluginVersions: input.pluginVersionRows.map(row => ({
plugin_version: row.plugin_version,
devices: toCount(row.devices),
total_devices: toCount(row.total_devices),
})),
releaseMarkers: input.releaseMarkers,
}
}
Expand All @@ -371,6 +395,7 @@
const dailyResult = await db.query<NativeObserveMetricRow>(dailyStatsQuery, params)
const actionResult = await db.query<NativeObserveMetricRow>(actionStatsQuery, params)
const overviewResult = await db.query<NativeObserveOverviewRow>(overviewStatsQuery, paramsWithIssues)
const pluginVersionResult = await db.query<NativeObservePluginVersionRow>(pluginVersionStatsQuery, [appId])
const versionResult = await db.query<NativeObserveVersionRow>(versionStatsQuery, paramsWithIssues)
const releaseMarkersResult = await db.query<NativeObserveReleaseMarker>(releaseMarkersQuery, params.slice(0, 3))

Expand All @@ -381,6 +406,7 @@
end: endInclusive.toISOString(),
dailyRows: dailyResult.rows,
actionRows: actionResult.rows,
pluginVersionRows: pluginVersionResult.rows,
versionRows: versionResult.rows,
overviewRow: overviewResult.rows[0],
releaseMarkers: releaseMarkersResult.rows,
Expand Down
8 changes: 8 additions & 0 deletions tests/native-observe-stats.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ describe('native observe stats helpers', () => {
versionRows: [
{ version_name: '1.2.3', events: 8, devices: 4, issue_count: 1, affected_devices: 1, launch_p90_ms: 912.8, webview_load_p90_ms: 1450 },
],
pluginVersionRows: [
{ plugin_version: '7.0.0', devices: 3, total_devices: 4 },
{ plugin_version: '6.9.0', devices: 1, total_devices: 4 },
],
overviewRow: {
events: 8,
devices: 4,
Expand All @@ -57,6 +61,10 @@ describe('native observe stats helpers', () => {
expect(response.overview.launch_p90_ms).toBe(913)
expect(response.daily.launches).toEqual([4, 0])
expect(response.daily.webview_loads).toEqual([3, 0])
expect(response.pluginVersions).toEqual([
{ plugin_version: '7.0.0', devices: 3, total_devices: 4 },
{ plugin_version: '6.9.0', devices: 1, total_devices: 4 },
])
expect(response.daily.issue_events).toEqual([0, 1])
expect(response.daily.launch_p50_ms).toEqual([410, null])
expect(response.actionBreakdown[1]).toMatchObject({ action: 'app_crash', is_issue: true })
Expand Down
Loading