Skip to content

feat(apps): app-level stats_mode for device telemetry privacy - #3305

Draft
riderx wants to merge 3 commits into
mainfrom
cursor/app-stats-mode-setting-a240
Draft

feat(apps): app-level stats_mode for device telemetry privacy#3305
riderx wants to merge 3 commits into
mainfrom
cursor/app-stats-mode-setting-a240

Conversation

@riderx

@riderx riderx commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Added apps.stats_mode column (all | updatesOnly | billingOnly) defaulting to all for new and existing apps.
  • Exposed the setting via PUT /public/app/:id (same pattern as expose_metadata / allow_device_custom_id).
  • Enforced the mode server-side on /stats: drop disallowed actions and strip fields that should not be stored in restricted modes (defense in depth vs older plugins).
  • Added dashboard app settings UI with per-tier copy (collected fields, benefits, Capgo usage, HIPAA/NSPrivacy context).
  • Added unit tests for mode normalization/stripping and backend integration tests for API + ingest behavior.

Pairs with Cap-go/capacitor-updater#860 (statsMode client gating). Wire values match the plugin contract.

Motivation (AI generated)

Customers in HIPAA-sensitive or strict App Store privacy (NSPrivacy) programs need an explicit, documented way to collect less device telemetry without disabling Capgo updates entirely. The native plugin PR adds client-side statsMode; this PR stores the org policy on the app and enforces it on ingest.

Business Impact (AI generated)

  • Enables privacy-conscious customers to adopt Capgo while limiting stored telemetry.
  • Default all preserves current analytics, rollout debugging, and billing accuracy for existing apps.
  • Clear dashboard copy reduces support back-and-forth when customers evaluate privacy tiers.

Privacy tiers (field map)

Mode Actions kept Payload / stored fields
all (default, recommended) All /stats actions Full payload + edge-inferred country_code, custom_id, metadata, install_source, defaultChannel, key_id, old_version_name, health/WebView events
updatesOnly OTA pipeline only (download/install/set/reset/delete + related failures; mirrors updater UPDATES_ONLY_STATS_ACTIONS) Full payload for allowed actions; drops health/WebView/foreground/background/crash-class events
billingOnly set, download_complete, set_fail, update_fail, download_fail Billing minimum only: device_id, app_id, platform, version_name, version_build, version_os, plugin_version, is_emulator, is_prod; strips custom_id, metadata, install_source, channel fields, key_id, old_version_name, and does not persist country

Test Plan (AI generated)

  • bunx vitest run tests/stats-mode.unit.test.ts
  • bun test:backendtests/stats-mode-ingest.test.ts (API default/update, billingOnly action drop, billingOnly custom_id strip, updatesOnly action drop)
  • Manual: App Settings → Device telemetry collection → save each mode and verify apps.stats_mode in DB
  • Manual: with billingOnly, POST app_crash to /stats200 but no stat row

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

Add app-level stats_mode (all | updatesOnly | billingOnly) with dashboard
UI, PUT /app API support, and server-side /stats enforcement aligned with
Cap-go/capacitor-updater#860. Default remains full collection for all apps.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@riderx
riderx deployed to deepsec-pr September 10, 2026 22:56 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codspeed-hq

codspeed-hq Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing cursor/app-stats-mode-setting-a240 (0616acb) with main (326249b)

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.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot deployed to deepsec-pr September 10, 2026 23:07 Active
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai review

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot deployed to deepsec-pr September 10, 2026 23:14 Active
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai review

@sonarqubecloud

Copy link
Copy Markdown

@rihoarvutikonto

Copy link
Copy Markdown

Community review — unique notes, not repeating the bot summaries.

  1. updatesOnly is action-filter only. applyStatsModeToBody / applyStatsModeToDevice strip custom_id, metadata, install_source, defaultChannel, and key_id only when usesBillingStatsPayload (billingOnly). For updatesOnly the body is returned unchanged, so device identifiers still persist on the allowed update actions. If dashboard copy talks about HIPAA / NSPrivacy for anything other than billingOnly, that copy overclaims. Either strip the same fields in updatesOnly, or say clearly that updatesOnly only drops events, not identity fields.

  2. Dropped events return { success: true }. Older plugins will think /stats recorded get / session actions. Fine for not breaking the plugin, but progressive-rollout auto-pause and Observe health depend on failure actions. billingOnly keeps download_fail / set_fail / update_fail and drops unzip_fail, checksum_fail, decrypt_fail, insufficient_disk_space, … so auto-pause on a privacy-restricted app is mostly blind. Worth a dashboard warning when stats_mode !== 'all' on a channel with auto-pause enabled.

  3. Cache vs owner mode. post() first gates on cachedAppStatus.stats_mode and can return { success: true } before getAppOwnerPostgres refreshes stats_mode. A just-tightened app can keep ingesting full stats until the cache expires; a just-loosened app can drop them. Apply the owner row (or document the cache TTL) before the first drop.

  4. Two writers. Dashboard updateStatsMode patches apps from the browser; PUT /public/app/:id also sets it. Confirm RLS matches the public API permission check so a member without app.update cannot flip telemetry policy from the settings radio.

  5. submit() persists stats_mode in a separate try/catch after the rest of the form. A failed earlier update still writes the radio selection, and a failed stats_mode write still toasts success from the previous step. One error path is safer for a privacy control.

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.

3 participants