Skip to content

fix(stats): unify success rate formula across admin and public metrics - #2988

Merged
riderx merged 1 commit into
mainfrom
cursor/unify-success-rate-metrics-04fe
Aug 11, 2026
Merged

fix(stats): unify success rate formula across admin and public metrics#2988
riderx merged 1 commit into
mainfrom
cursor/unify-success-rate-metrics-04fe

Conversation

@riderx

@riderx riderx commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Public /data live update metrics now compute headline success rate from version_usage using install / (install + fail), matching the admin dashboard formula.
  • Fixed getUpdateStatsCF, getUpdate_stats SQL, and getUpdateStatsSB, which previously used get / (install + get) — not a real install success rate.
  • Public dimensional breakdowns (platform/country/version) now use event counts from app_log instead of device-day dedup that produced a separate ~70% number.

Motivation (AI generated)

The marketing /data page and admin dashboard showed different success rates because they used different datasets and formulas. A prior fix aligned public stats with global_stats, but a follow-up PR reintroduced a separate Analytics Engine device-day calculation from app_log. Meanwhile, the cron snapshot stored in global_stats used get/(install+get), which is also semantically wrong for "successful update installations".

Business Impact (AI generated)

Capgo can now publish one consistent, correct install success rate on the marketing page and admin dashboard. This removes misleading lower public numbers and prevents overstating success via the old get-ratio formula.

Test Plan (AI generated)

  • npx vitest run tests/public-live-update-metrics.unit.test.ts tests/public-stats.unit.test.ts tests/cron-email-stats-backtest.unit.test.ts
  • Deploy and compare /private/website_stats/live_updates success_rate with admin success_rate metric for the same 30-day window
  • Verify next global_stats cron run stores updated success_rate values after migration

Generated with AI

Open in Web Open in Cursor 

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved update success-rate accuracy by counting completed installs and failures, excluding download events.
    • Updated live metrics to reflect outcomes across platforms, countries, and updater versions more reliably.
    • Preserved existing app health-status thresholds while correcting the underlying calculations.
  • Tests

    • Updated analytics coverage to verify install/failure-based metrics and version-usage data.

Public /data used app_log device-day dedup (~70%) while admin used
version_usage install/(install+fail). global_stats cron also used the
wrong get/(install+get) ratio. Align all paths on install/(install+fail).
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The update metrics now calculate success rates from install/set outcomes and failures. Public live metrics use VERSION_USAGE and app-log dimensions. A new PostgreSQL function aggregates recent version usage. Unit tests match the updated queries and fields.

Changes

Update success-rate metrics

Layer / File(s) Summary
Update statistics aggregation
supabase/migrations/20260811083707_fix_update_stats_success_rate_formula.sql, supabase/functions/_backend/utils/supabase.ts, supabase/functions/_backend/utils/cloudflare.ts, supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts
Per-app and aggregate success rates use installs or set events divided by installs or set events plus failures. The new public.get_update_stats() function aggregates recent version_usage records and preserves the existing health threshold.
Public live metrics queries
supabase/functions/_backend/utils/cloudflare.ts, supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts, tests/public-live-update-metrics.unit.test.ts
Public metrics require VERSION_USAGE, query install/fail counts directly, and derive platform, country, and updater-version outcomes from app-log fields. Tests update fixtures and verify the version_usage query.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: wcaleniewolny

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: aligning success-rate formulas across admin and public metrics.
Description check ✅ Passed The description includes a relevant summary and test plan, with backend screenshots reasonably omitted; the repository checklist is missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing cursor/unify-success-rate-metrics-04fe (b28e75e) with main (15d5c37)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@sonarqubecloud

Copy link
Copy Markdown

@riderx
riderx marked this pull request as ready for review August 11, 2026 09:21

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Cursor Bugbot passed with no findings, but this is above the low-risk approval threshold (public/admin success-rate formula change plus a stats SQL migration). Leaving a non-blocking comment and assigning human reviewers.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor
cursor Bot requested review from Dalanir and WcaleNieWolny August 11, 2026 09:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts (1)

2849-2897: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve the public success_rate contract.

These changes replace device-day outcomes with raw event outcomes while retaining the same public field. Existing clients cannot select the previous semantics.

Keep the legacy field behavior for existing clients. Add a versioned endpoint or a new install-based field for the new calculation.

  • supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts#L2849-L2897: preserve the legacy public response or gate the install-based calculation by API or plugin version.
  • supabase/functions/_backend/utils/cloudflare.ts#L3098-L3146: apply the same compatibility behavior in the matching deployment target.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts` around lines
2849 - 2897, The public success_rate calculation in the analytics response must
retain its legacy device-day outcome semantics for existing clients. In
supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts lines 2849-2897,
gate the raw install-based calculation by API or plugin version, or expose it
under a new install-based field/versioned endpoint while preserving the existing
success_rate field. Apply the same compatibility change in
supabase/functions/_backend/utils/cloudflare.ts lines 3098-3146.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts`:
- Around line 2849-2897: The public success_rate calculation in the analytics
response must retain its legacy device-day outcome semantics for existing
clients. In supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts lines
2849-2897, gate the raw install-based calculation by API or plugin version, or
expose it under a new install-based field/versioned endpoint while preserving
the existing success_rate field. Apply the same compatibility change in
supabase/functions/_backend/utils/cloudflare.ts lines 3098-3146.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40cb8e01-ec31-4145-bb72-6a2713076b2c

📥 Commits

Reviewing files that changed from the base of the PR and between 15d5c37 and b28e75e.

📒 Files selected for processing (5)
  • supabase/functions/_backend/plugin_runtime/utils/cloudflare.ts
  • supabase/functions/_backend/utils/cloudflare.ts
  • supabase/functions/_backend/utils/supabase.ts
  • supabase/migrations/20260811083707_fix_update_stats_success_rate_formula.sql
  • tests/public-live-update-metrics.unit.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files

Confidence score: 3/5

  • In supabase/functions/_backend/utils/cloudflare.ts, the global success_rate denominator is still filtered by app.get > 0, so install/fail-only slices are dropped and the top-line rate can be artificially high; this can misstate reliability trends and regressions — compute totals without that filter so all outcome types are counted.
  • In supabase/functions/_backend/utils/cloudflare.ts, headline success rate and platform/country/version rates are sourced from different Analytics Engine datasets with different failure taxonomies, which can produce conflicting numbers for the same period and erode trust in dashboards — align the datasets/taxonomy (or add explicit normalization) before relying on these metrics.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="supabase/functions/_backend/utils/cloudflare.ts">

<violation number="1" location="supabase/functions/_backend/utils/cloudflare.ts:2127">
P1: Global success_rate still excludes install/fail outcomes whenever their app has no `get` event in the same 60-second slice, because the total is reduced after the existing `app.get > 0` filter. Removing that filter (or filtering only on the outcome fields) would make the new install/(install+fail) calculation cover all version_usage outcomes and match the admin/global_stats view.</violation>

<violation number="2" location="supabase/functions/_backend/utils/cloudflare.ts:3099">
P2: The headline success rate and the per-dimension (platform/country/version) success rates are now computed from two different Analytics Engine datasets with different failure taxonomies. The daily/total headline reads VERSION_USAGE (install/fail — a single generic 'fail' action), while the breakdown queries read APP_LOG (set vs. the granular PUBLIC_FAILURE_ACTIONS list). Unless those two datasets are guaranteed to record byte-identical event streams, the published headline and the dimensional success rates will disagree, which is exactly the cross-source discrepancy this PR is meant to remove. Consider driving the headline and the breakdowns from the same source (e.g. compute the daily/total aggregates from the same app_log/set + failure-actions query as the breakdowns, or derive the breakdowns from version_usage), so a single consistent success rate is published.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

.map((app) => {
const totalEvents = app.set + app.get
const successRate = Number(Number(totalEvents > 0 ? (app.get / totalEvents) * 100 : 100).toFixed(2))
const totalOutcomes = app.set + app.failed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Global success_rate still excludes install/fail outcomes whenever their app has no get event in the same 60-second slice, because the total is reduced after the existing app.get > 0 filter. Removing that filter (or filtering only on the outcome fields) would make the new install/(install+fail) calculation cover all version_usage outcomes and match the admin/global_stats view.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/cloudflare.ts, line 2127:

<comment>Global success_rate still excludes install/fail outcomes whenever their app has no `get` event in the same 60-second slice, because the total is reduced after the existing `app.get > 0` filter. Removing that filter (or filtering only on the outcome fields) would make the new install/(install+fail) calculation cover all version_usage outcomes and match the admin/global_stats view.</comment>

<file context>
@@ -2124,8 +2124,8 @@ export async function getUpdateStatsCF(c: Context): Promise<UpdateStats> {
       .map((app) => {
-        const totalEvents = app.set + app.get
-        const successRate = Number(Number(totalEvents > 0 ? (app.get / totalEvents) * 100 : 100).toFixed(2))
+        const totalOutcomes = app.set + app.failed
+        const successRate = Number(Number(totalOutcomes > 0 ? (app.set / totalOutcomes) * 100 : 100).toFixed(2))
         return {
</file context>

const outcomeBase = `SELECT ${day} AS date, index1 AS app_id, blob1 AS device_id, max(if(blob2 = 'set', 1, 0)) AS succeeded, max(if(blob2 IN (${failureActions}), 1, 0)) AS failed, argMax(blob5, timestamp) AS platform, argMax(blob6, timestamp) AS country, argMax(blob7, timestamp) AS plugin_version FROM app_log WHERE ${window} AND (blob2 = 'set' OR blob2 IN (${failureActions})) GROUP BY date, app_id, device_id`
const outcomesQuery = `SELECT date, sum(succeeded) AS successes, sum(if(succeeded = 0, failed, 0)) AS failures FROM (${outcomeBase}) GROUP BY date`
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`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The headline success rate and the per-dimension (platform/country/version) success rates are now computed from two different Analytics Engine datasets with different failure taxonomies. The daily/total headline reads VERSION_USAGE (install/fail — a single generic 'fail' action), while the breakdown queries read APP_LOG (set vs. the granular PUBLIC_FAILURE_ACTIONS list). Unless those two datasets are guaranteed to record byte-identical event streams, the published headline and the dimensional success rates will disagree, which is exactly the cross-source discrepancy this PR is meant to remove. Consider driving the headline and the breakdowns from the same source (e.g. compute the daily/total aggregates from the same app_log/set + failure-actions query as the breakdowns, or derive the breakdowns from version_usage), so a single consistent success rate is published.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/cloudflare.ts, line 3099:

<comment>The headline success rate and the per-dimension (platform/country/version) success rates are now computed from two different Analytics Engine datasets with different failure taxonomies. The daily/total headline reads VERSION_USAGE (install/fail — a single generic 'fail' action), while the breakdown queries read APP_LOG (set vs. the granular PUBLIC_FAILURE_ACTIONS list). Unless those two datasets are guaranteed to record byte-identical event streams, the published headline and the dimensional success rates will disagree, which is exactly the cross-source discrepancy this PR is meant to remove. Consider driving the headline and the breakdowns from the same source (e.g. compute the daily/total aggregates from the same app_log/set + failure-actions query as the breakdowns, or derive the breakdowns from version_usage), so a single consistent success rate is published.</comment>

<file context>
@@ -3095,17 +3095,17 @@ export async function getPublicLiveUpdateMetricsCF(c: Context, referenceDate = n
-  const outcomeBase = `SELECT ${day} AS date, index1 AS app_id, blob1 AS device_id, max(if(blob2 = 'set', 1, 0)) AS succeeded, max(if(blob2 IN (${failureActions}), 1, 0)) AS failed, argMax(blob5, timestamp) AS platform, argMax(blob6, timestamp) AS country, argMax(blob7, timestamp) AS plugin_version FROM app_log WHERE ${window} AND (blob2 = 'set' OR blob2 IN (${failureActions})) GROUP BY date, app_id, device_id`
-  const outcomesQuery = `SELECT date, sum(succeeded) AS successes, sum(if(succeeded = 0, failed, 0)) AS failures FROM (${outcomeBase}) GROUP BY date`
+  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 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`
</file context>

@riderx
riderx merged commit bc40030 into main Aug 11, 2026
82 of 85 checks passed
@riderx
riderx deleted the cursor/unify-success-rate-metrics-04fe branch August 11, 2026 10:11
riderx added a commit that referenced this pull request Aug 12, 2026
* fix(stats): restore device-day success rate for admin and public

Revert #2988 raw install/(install+fail) event counting that collapsed
both KPIs to ~24%. Public /data and admin global_stats now share the
app_log device-day formula again.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(stats): drop unused plugin_runtime device-day helper

logsnag_insights calls utils/cloudflare getDeviceDaySuccessRateCF only.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants