diff --git a/supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts b/supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts index b9beeef23f..0be2ee5cbe 100644 --- a/supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts +++ b/supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts @@ -1872,8 +1872,15 @@ export async function getUpdateStatsCF(c: Context): Promise { const result = await runQueryToCFA<{ app_id: string, failed: number, set: number, get: number }>(c, query) cloudlog({ requestId: c.get('requestId'), message: 'getUpdateStatsCF result', result }) + const total = result.reduce((acc, app) => { + acc.failed += app.failed || 0 + acc.set += app.set || 0 + acc.get += app.get || 0 + return acc + }, { failed: 0, set: 0, get: 0 }) + const apps = result - .filter(app => app.get > 0) + .filter(app => (app.set + app.failed) > 0) .map((app) => { const totalOutcomes = app.set + app.failed const successRate = Number(Number(totalOutcomes > 0 ? (app.set / totalOutcomes) * 100 : 100).toFixed(2)) @@ -1884,13 +1891,6 @@ export async function getUpdateStatsCF(c: Context): Promise { } }) - const total = apps.reduce((acc, app) => { - acc.failed += app.failed - acc.set += app.set - acc.get += app.get - return acc - }, { failed: 0, set: 0, get: 0 }) - const totalOutcomes = total.set + total.failed const totalSuccessRate = totalOutcomes > 0 ? (total.set / totalOutcomes) * 100 : 100 @@ -2837,7 +2837,7 @@ function buildBreakdownMetrics( } export async function getPublicLiveUpdateMetricsCF(c: Context, referenceDate = new Date()): Promise { - if (!c.env.APP_LOG || !c.env.VERSION_USAGE || !c.env.DEVICE_USAGE || !c.env.DEVICE_INFO || !getEnv(c, 'CF_ANALYTICS_TOKEN') || !getEnv(c, 'CF_ACCOUNT_ANALYTICS_ID')) + if (!c.env.APP_LOG || !c.env.DEVICE_USAGE || !c.env.DEVICE_INFO || !getEnv(c, 'CF_ANALYTICS_TOKEN') || !getEnv(c, 'CF_ACCOUNT_ANALYTICS_ID')) throw new Error('Public live update metric bindings are unavailable') const end = new Date(Date.UTC(referenceDate.getUTCFullYear(), referenceDate.getUTCMonth(), referenceDate.getUTCDate())) @@ -2847,7 +2847,7 @@ export async function getPublicLiveUpdateMetricsCF(c: Context, referenceDate = n const failureActions = PUBLIC_FAILURE_ACTIONS.map(action => `'${action}'`).join(', ') const day = `formatDateTime(toStartOfInterval(timestamp, INTERVAL '1' DAY), '%Y-%m-%d')` const appLogOutcomeFilter = `(blob2 = 'set' OR blob2 IN (${failureActions}))` - const dailySuccessQuery = `SELECT ${day} AS date, sum(if(blob3 = 'install', 1, 0)) AS installs, sum(if(blob3 = 'fail', 1, 0)) AS fails FROM version_usage WHERE ${window} GROUP BY date ORDER BY date ASC` + const dailySuccessQuery = `SELECT ${day} AS date, sum(if(blob2 = 'set', 1, 0)) AS installs, sum(if(blob2 IN (${failureActions}), 1, 0)) AS fails FROM app_log WHERE ${window} AND ${appLogOutcomeFilter} GROUP BY date ORDER BY date ASC` const failuresQuery = `SELECT action, count() AS devices FROM (SELECT ${day} AS date, blob2 AS action, index1 AS app_id, blob1 AS device_id FROM app_log WHERE ${window} AND blob2 IN (${failureActions}) GROUP BY date, action, app_id, device_id) GROUP BY action` const platformsShareQuery = `SELECT platform, count() AS devices FROM (SELECT double1 AS platform, index1 AS app_id, blob1 AS device_id FROM device_usage WHERE ${window} AND double1 IN (0.0, 1.0, 2.0) GROUP BY platform, app_id, device_id) GROUP BY platform` const platformsOutcomeQuery = `SELECT blob5 AS key, sum(if(blob2 = 'set', 1, 0)) AS successes, sum(if(blob2 IN (${failureActions}), 1, 0)) AS failures FROM app_log WHERE ${window} AND ${appLogOutcomeFilter} AND blob5 IN ('ios', 'android', 'electron') GROUP BY blob5` diff --git a/supabase/functions/_backend/utils/cloudflare.ts b/supabase/functions/_backend/utils/cloudflare.ts index 7bb6706441..9adcde9db6 100644 --- a/supabase/functions/_backend/utils/cloudflare.ts +++ b/supabase/functions/_backend/utils/cloudflare.ts @@ -2121,8 +2121,15 @@ export async function getUpdateStatsCF(c: Context): Promise { const result = await runQueryToCFA<{ app_id: string, failed: number, set: number, get: number }>(c, query) cloudlog({ requestId: c.get('requestId'), message: 'getUpdateStatsCF result', result }) + const total = result.reduce((acc, app) => { + acc.failed += app.failed || 0 + acc.set += app.set || 0 + acc.get += app.get || 0 + return acc + }, { failed: 0, set: 0, get: 0 }) + const apps = result - .filter(app => app.get > 0) + .filter(app => (app.set + app.failed) > 0) .map((app) => { const totalOutcomes = app.set + app.failed const successRate = Number(Number(totalOutcomes > 0 ? (app.set / totalOutcomes) * 100 : 100).toFixed(2)) @@ -2133,13 +2140,6 @@ export async function getUpdateStatsCF(c: Context): Promise { } }) - const total = apps.reduce((acc, app) => { - acc.failed += app.failed - acc.set += app.set - acc.get += app.get - return acc - }, { failed: 0, set: 0, get: 0 }) - const totalOutcomes = total.set + total.failed const totalSuccessRate = totalOutcomes > 0 ? (total.set / totalOutcomes) * 100 : 100 @@ -3086,7 +3086,7 @@ function buildBreakdownMetrics( } export async function getPublicLiveUpdateMetricsCF(c: Context, referenceDate = new Date()): Promise { - if (!c.env.APP_LOG || !c.env.VERSION_USAGE || !c.env.DEVICE_USAGE || !c.env.DEVICE_INFO || !getEnv(c, 'CF_ANALYTICS_TOKEN') || !getEnv(c, 'CF_ACCOUNT_ANALYTICS_ID')) + if (!c.env.APP_LOG || !c.env.DEVICE_USAGE || !c.env.DEVICE_INFO || !getEnv(c, 'CF_ANALYTICS_TOKEN') || !getEnv(c, 'CF_ACCOUNT_ANALYTICS_ID')) throw new Error('Public live update metric bindings are unavailable') const end = new Date(Date.UTC(referenceDate.getUTCFullYear(), referenceDate.getUTCMonth(), referenceDate.getUTCDate())) @@ -3096,7 +3096,7 @@ export async function getPublicLiveUpdateMetricsCF(c: Context, referenceDate = n const failureActions = PUBLIC_FAILURE_ACTIONS.map(action => `'${action}'`).join(', ') const day = `formatDateTime(toStartOfInterval(timestamp, INTERVAL '1' DAY), '%Y-%m-%d')` const appLogOutcomeFilter = `(blob2 = 'set' OR blob2 IN (${failureActions}))` - const dailySuccessQuery = `SELECT ${day} AS date, sum(if(blob3 = 'install', 1, 0)) AS installs, sum(if(blob3 = 'fail', 1, 0)) AS fails FROM version_usage WHERE ${window} GROUP BY date ORDER BY date ASC` + const dailySuccessQuery = `SELECT ${day} AS date, sum(if(blob2 = 'set', 1, 0)) AS installs, sum(if(blob2 IN (${failureActions}), 1, 0)) AS fails FROM app_log WHERE ${window} AND ${appLogOutcomeFilter} GROUP BY date ORDER BY date ASC` const failuresQuery = `SELECT action, count() AS devices FROM (SELECT ${day} AS date, blob2 AS action, index1 AS app_id, blob1 AS device_id FROM app_log WHERE ${window} AND blob2 IN (${failureActions}) GROUP BY date, action, app_id, device_id) GROUP BY action` const platformsShareQuery = `SELECT platform, count() AS devices FROM (SELECT double1 AS platform, index1 AS app_id, blob1 AS device_id FROM device_usage WHERE ${window} AND double1 IN (0.0, 1.0, 2.0) GROUP BY platform, app_id, device_id) GROUP BY platform` const platformsOutcomeQuery = `SELECT blob5 AS key, sum(if(blob2 = 'set', 1, 0)) AS successes, sum(if(blob2 IN (${failureActions}), 1, 0)) AS failures FROM app_log WHERE ${window} AND ${appLogOutcomeFilter} AND blob5 IN ('ios', 'android', 'electron') GROUP BY blob5` diff --git a/supabase/migrations/20260811101237_fix_update_stats_outcome_filter.sql b/supabase/migrations/20260811101237_fix_update_stats_outcome_filter.sql new file mode 100644 index 0000000000..d0fa5419fe --- /dev/null +++ b/supabase/migrations/20260811101237_fix_update_stats_outcome_filter.sql @@ -0,0 +1,43 @@ +CREATE OR REPLACE FUNCTION "public"."get_update_stats"() RETURNS TABLE("app_id" character varying, "failed" bigint, "install" bigint, "get" bigint, "success_rate" numeric, "healthy" boolean) + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO '' + AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + version_usage.app_id, + COALESCE(SUM(CASE WHEN action = 'fail' THEN 1 ELSE 0 END), 0) AS failed, + COALESCE(SUM(CASE WHEN action = 'install' THEN 1 ELSE 0 END), 0) AS install, + COALESCE(SUM(CASE WHEN action = 'get' THEN 1 ELSE 0 END), 0) AS get + FROM + public.version_usage + WHERE + timestamp >= (date_trunc('minute', now()) - INTERVAL '10 minutes') + AND timestamp < (date_trunc('minute', now()) - INTERVAL '9 minutes') + GROUP BY + version_usage.app_id + ) + SELECT + stats.app_id, + stats.failed, + stats.install, + stats.get, + CASE + WHEN (stats.install + stats.failed) > 0 THEN + ROUND((stats.install::numeric / (stats.install + stats.failed)) * 100, 2) + ELSE 100 + END AS success_rate, + CASE + WHEN (stats.install + stats.failed) > 0 THEN + ((stats.install::numeric / (stats.install + stats.failed)) * 100 >= 70) + ELSE true + END AS healthy + FROM + stats + WHERE + (stats.install + stats.failed) > 0; +END; +$$; + +ALTER FUNCTION "public"."get_update_stats"() OWNER TO "postgres"; diff --git a/tests/public-live-update-metrics.unit.test.ts b/tests/public-live-update-metrics.unit.test.ts index 2ac089f9ff..4f5f80f668 100644 --- a/tests/public-live-update-metrics.unit.test.ts +++ b/tests/public-live-update-metrics.unit.test.ts @@ -17,7 +17,6 @@ function createContext() { return { env: { APP_LOG: {}, - VERSION_USAGE: {}, DEVICE_USAGE: {}, DEVICE_INFO: {}, CF_ANALYTICS_TOKEN: 'analytics-token', @@ -45,7 +44,7 @@ describe('public live update metrics', () => { const query = String(init?.body ?? '') queries.push(query) - if (query.includes('FROM version_usage') && query.includes('AS installs')) { + if (query.includes('FROM app_log') && query.includes('AS installs') && query.includes('GROUP BY date')) { return analyticsResponse( [ { name: 'date', type: 'String' }, @@ -210,7 +209,7 @@ describe('public live update metrics', () => { expect(metrics.updater_versions[0]).toMatchObject({ key: '8.1.0', share: 60 }) expect(metrics.updater_versions.find(row => row.key === '8.1.0')?.success_rate).toBe(93.3) expect(queries.length).toBe(11) - expect(queries.join('\n')).toContain('FROM version_usage') + expect(queries.join('\n')).toContain('FROM app_log') expect(queries.join('\n')).toContain('blob6') expect(queries.join('\n')).toContain('blob7') expect(queries.join('\n')).toContain('blob10')