From 017755bcd5c5cfc7143e52096cbc69ce2f90289b Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 22 Mar 2025 13:50:02 +0530 Subject: [PATCH] fix: audit health data and update health notification This commit adds a fallback mechanism for health tracking initialization using `localStorage` to enable fast boot-time access to the health tracking flag (`Phoenix.healthTrackingDisabled`) before persistent preferences are fully loaded. Key changes: - `Phoenix.healthTrackingDisabled` is initialized from `localStorage` at boot. - Introduced `Phoenix._setHealthTrackingDisabled()` to update both the in-memory flag and localStorage. - Added a safety check in `HealthDataManager.js` to detect mismatches between boot-time localStorage and actual persisted preferences. If a mismatch is detected, a one-time `"disableErr"` metric is raised to track potential early metric leakage. - Updated Bugsnag and metrics initialization logic to respect `Phoenix.healthTrackingDisabled`. - Updated the health data opt-out UI and strings to reflect necessary/always-collected data. - Updated test runner to mimic `localStorage`-based health flag boot logic. This ensures **eventual consistency** between boot-time behavior and persisted user preferences, while maintaining **privacy guarantees** by deferring any personal or anonymous health reporting until health tracking is explicitly enabled. --- .../default/HealthData/HealthDataManager.js | 13 +++++++++ .../default/HealthData/SendToAnalytics.js | 27 ++++++++++++++++--- .../healthdata-preview-dialog.html | 4 +++ src/index.html | 15 +++++------ src/loggerSetup.js | 2 +- src/nls/root/strings.js | 3 ++- src/utils/Metrics.js | 1 + test/SpecRunner.html | 6 +++++ 8 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/extensions/default/HealthData/HealthDataManager.js b/src/extensions/default/HealthData/HealthDataManager.js index 45515e36a5..bd43e0ab6f 100644 --- a/src/extensions/default/HealthData/HealthDataManager.js +++ b/src/extensions/default/HealthData/HealthDataManager.js @@ -109,6 +109,19 @@ define(function (require, exports, module) { isPowerUserFn: isPowerUser }); healthDataDisabled = !prefs.get("healthDataTracking"); + if (healthDataDisabled && !Phoenix.healthTrackingDisabled) { + // Phoenix.healthTrackingDisabled is initialized at boot using localStorage. + // However, there's a theoretical edge case where the browser may have cleared + // localStorage, causing a mismatch between the boot-time flag and the actual + // persisted user preference. + // + // This means we might unintentionally log some metrics during the short window + // before the real preference is loaded and applied. + // + // To track this discrepancy, we emit a one-time metric just before disabling tracking, + // so we’re aware of this inconsistency and can address it if needed. + Metrics.countEvent(Metrics.PLATFORM, "metricBoot", "disableErr"); + } Metrics.setDisabled(healthDataDisabled); SendToAnalytics.sendPlatformMetrics(); SendToAnalytics.sendThemesMetrics(); diff --git a/src/extensions/default/HealthData/SendToAnalytics.js b/src/extensions/default/HealthData/SendToAnalytics.js index a2d1d46900..459f41c331 100644 --- a/src/extensions/default/HealthData/SendToAnalytics.js +++ b/src/extensions/default/HealthData/SendToAnalytics.js @@ -21,7 +21,7 @@ * */ -/*global Phoenix*/ +/*global AppConfig*/ define(function (require, exports, module) { const Metrics = brackets.getModule("utils/Metrics"), PreferencesManager = brackets.getModule("preferences/PreferencesManager"), @@ -29,6 +29,8 @@ define(function (require, exports, module) { NodeUtils = brackets.getModule("utils/NodeUtils"), themesPref = PreferencesManager.getExtensionPrefs("themes"); + const BugsnagPerformance = window.BugsnagPerformance; + const PLATFORM = Metrics.EVENT_TYPE.PLATFORM, PERFORMANCE = Metrics.EVENT_TYPE.PERFORMANCE, STORAGE = Metrics.EVENT_TYPE.STORAGE; @@ -133,17 +135,34 @@ define(function (require, exports, module) { _sendStorageMetrics(); } + let bugsnagPerformanceInited = false; + function _initBugsnagPerformance() { + bugsnagPerformanceInited = true; + BugsnagPerformance.start({ + apiKey: '94ef94f4daf871ca0f2fc912c6d4764d', + appVersion: AppConfig.version, + releaseStage: window.__TAURI__ ? + `tauri-${AppConfig.config.bugsnagEnv}-${Phoenix.platform}` : AppConfig.config.bugsnagEnv, + autoInstrumentRouteChanges: false, + autoInstrumentNetworkRequests: false, + autoInstrumentFullPageLoads: false + }); + } + function _bugsnagPerformance(key, valueMs) { - if(Metrics.isDisabled() || !window.BugsnagPerformance || Phoenix.isTestWindow){ + if(Metrics.isDisabled() || !BugsnagPerformance || Phoenix.isTestWindow){ return; } + if(!bugsnagPerformanceInited) { + _initBugsnagPerformance(); + } let activityStartTime = new Date(); let activityEndTime = new Date(activityStartTime.getTime() + valueMs); - window.BugsnagPerformance + BugsnagPerformance .startSpan(key, { startTime: activityStartTime }) .end(activityEndTime); } - + // Performance function sendStartupPerformanceMetrics() { const healthReport = PerfUtils.getHealthReport(); diff --git a/src/extensions/default/HealthData/htmlContent/healthdata-preview-dialog.html b/src/extensions/default/HealthData/htmlContent/healthdata-preview-dialog.html index e6d4e8185c..4ed36a6ae2 100644 --- a/src/extensions/default/HealthData/htmlContent/healthdata-preview-dialog.html +++ b/src/extensions/default/HealthData/htmlContent/healthdata-preview-dialog.html @@ -9,6 +9,10 @@

{{Strings.HEALTH_DATA_PREVIEW}}

{{Strings.HEALTH_DATA_DO_TRACK}} +
+ ℹ️ + {{Strings.HEALTH_DATA_PREVIEW_NECESSARY}} +

{{{content}}}

diff --git a/src/index.html b/src/index.html index 220bb1237e..c13f595ff0 100644 --- a/src/index.html +++ b/src/index.html @@ -341,6 +341,7 @@ } else if (navigator.platform && navigator.platform.indexOf("Linux") >= 0) { platform = "linux"; } + // window.Phoenix should never be reassigned as code may cache references to this object. window.Phoenix = { PHOENIX_INSTANCE_ID: "PH-" + Math.round( Math.random()*1000000000000), browser: getBrowserDetails(), @@ -372,6 +373,12 @@ Phoenix.isSupportedBrowser = Phoenix.isNativeApp || (Phoenix.browser.isDeskTop && ("serviceWorker" in navigator)); window.testEnvironment = window.Phoenix.isTestWindow; + const healthDisabled = localStorage.getItem("PH_HEALTH_DISABLED"); + window.Phoenix.healthTrackingDisabled = (healthDisabled === "true"); + window.Phoenix._setHealthTrackingDisabled = function (isDisabled) { + window.Phoenix.healthTrackingDisabled = isDisabled; + localStorage.setItem("PH_HEALTH_DISABLED", String(isDisabled)); + }; // now setup PhoenixBaseURL, which if of the form https://phcode.dev/ or tauri://localhost/ const url = new URL(window.location.href); @@ -394,14 +401,6 @@ diff --git a/src/loggerSetup.js b/src/loggerSetup.js index f58b06c3ee..9e2469c52a 100644 --- a/src/loggerSetup.js +++ b/src/loggerSetup.js @@ -40,7 +40,7 @@ } } const urlParams = new URLSearchParams(window.location.search || ""); - const isBugsnagEnabled = (!window.testEnvironment && isBugsnagLoggableURL); + const isBugsnagEnabled = (!window.testEnvironment && isBugsnagLoggableURL && !Phoenix.healthTrackingDisabled); const MAX_ERR_SENT_RESET_INTERVAL = 60000, MAX_ERR_SENT_FIRST_MINUTE = 10, MAX_ERR_ALLOWED_IN_MINUTE = 2; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index b019c5745f..cd3f9855ce 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -891,6 +891,7 @@ define({ "HEALTH_DATA_NOTIFICATION_MESSAGE": "{APP_NAME} does not collect or process any personally identifiable information, but collects anonymous usage statistics to guard your privacy. Anonymous data is exempt from GDPR/CCPA notification requirements, but we believe you need to have a choice to opt out of anonymous data collection as well.

You can see your data or choose not to share any anonymous data by selecting Help > Health Report. These anonymous app usage statistics and error reports helps prioritize features, find bugs, and spot usability issues for improving your experience with {APP_NAME}. Without this data, we would not know what features it is worth building for you!
", "HEALTH_DATA_PREVIEW": "{APP_NAME} Health Report", "HEALTH_DATA_PREVIEW_INTRO": "

{APP_NAME} does not collect or process any personally identifiable information, but collects anonymous usage statistics to guard your privacy. These anonymous app usage statistics and error reports helps prioritize features, find bugs, and spot usability issues for improving your experience with {APP_NAME}.

Below is a preview of the data that will be sent in your next Health Report if it is enabled. (Also see developer console for error logs marked 'Caught Critical error'.)

", + "HEALTH_DATA_PREVIEW_NECESSARY": "Security/app updates, analytics library initialization, user counts, and usage time are always anonymously collected as necessary app health indicators. These are aggregate statistics and no personal data is sent/logged.", // extensions/default/InlineTimingFunctionEditor "INLINE_TIMING_EDITOR_TIME": "Time", @@ -1556,4 +1557,4 @@ define({ // surveys "SURVEY_TITLE_VOTE_FOR_FEATURES_YOU_WANT": "Vote for the features you want to see next!" -}); \ No newline at end of file +}); diff --git a/src/utils/Metrics.js b/src/utils/Metrics.js index c1b15bdeef..d502892d2a 100644 --- a/src/utils/Metrics.js +++ b/src/utils/Metrics.js @@ -403,6 +403,7 @@ define(function (require, exports, module) { } function setDisabled(shouldDisable) { + Phoenix._setHealthTrackingDisabled(shouldDisable); disabled = shouldDisable; } diff --git a/test/SpecRunner.html b/test/SpecRunner.html index 351d64df47..02a4d7bb3a 100644 --- a/test/SpecRunner.html +++ b/test/SpecRunner.html @@ -293,6 +293,12 @@ Phoenix.isSupportedBrowser = Phoenix.isNativeApp || (Phoenix.browser.isDeskTop && ("serviceWorker" in navigator)); window.testEnvironment = window.Phoenix.isTestWindow; + const healthDisabled = localStorage.getItem("PH_HEALTH_DISABLED"); + window.Phoenix.healthTrackingDisabled = (healthDisabled === "true"); + window.Phoenix._setHealthTrackingDisabled = function (isDisabled) { + window.Phoenix.healthTrackingDisabled = isDisabled; + localStorage.setItem("PH_HEALTH_DISABLED", String(isDisabled)); + }; // now setup PhoenixBaseURL, which if of the form https://phcode.dev/ or tauri://localhost/ const url = new URL(window.location.href);