StartupTelemetry.sys.mjs (16747B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 7 let lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 BrowserInitState: "resource:///modules/BrowserGlue.sys.mjs", 10 BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", 11 FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", 12 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", 13 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 14 OsEnvironment: "resource://gre/modules/OsEnvironment.sys.mjs", 15 PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs", 16 ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", 17 TelemetryReportingPolicy: 18 "resource://gre/modules/TelemetryReportingPolicy.sys.mjs", 19 UsageReporting: "resource://gre/modules/UsageReporting.sys.mjs", 20 }); 21 22 /** 23 * Used to collect various bits of telemetry during browser startup. 24 * 25 */ 26 export let StartupTelemetry = { 27 // Some tasks are expensive because they involve significant disk IO, and 28 // may also write information to disk. If we submit the telemetry that may 29 // happen anyway, but if we don't then this is undesirable, so those tasks are 30 // only run if we will submit the results. 31 // Why run any telemetry code at all if we don't submit the data? Because 32 // local and autoland builds usually do not submit telemetry, but we still 33 // want to be able to run automated tests to check the code _worked_. 34 get _willUseExpensiveTelemetry() { 35 return ( 36 AppConstants.MOZ_TELEMETRY_REPORTING && 37 Services.prefs.getBoolPref( 38 "datareporting.healthreport.uploadEnabled", 39 false 40 ) 41 ); 42 }, 43 44 _runIdleTasks(tasks, profilerMarker) { 45 for (let task of tasks) { 46 ChromeUtils.idleDispatch(async () => { 47 if (!Services.startup.shuttingDown) { 48 let startTime = ChromeUtils.now(); 49 try { 50 await task(); 51 } catch (ex) { 52 console.error(ex); 53 } finally { 54 ChromeUtils.addProfilerMarker( 55 profilerMarker, 56 startTime, 57 task.toSource() 58 ); 59 } 60 } 61 }); 62 } 63 }, 64 65 browserIdleStartup() { 66 let tasks = [ 67 // FOG doesn't need to be initialized _too_ early because it has a pre-init buffer. 68 () => this.initFOG(), 69 70 () => this.contentBlocking(), 71 () => this.dataSanitization(), 72 () => this.pipEnabled(), 73 () => this.sslKeylogFile(), 74 () => this.osAuthEnabled(), 75 () => this.startupConditions(), 76 () => this.httpsOnlyState(), 77 () => this.globalPrivacyControl(), 78 ]; 79 if (this._willUseExpensiveTelemetry) { 80 tasks.push(() => lazy.PlacesDBUtils.telemetry()); 81 } 82 if (AppConstants.platform == "win") { 83 tasks.push( 84 () => this.pinningStatus(), 85 () => this.isDefaultHandler() 86 ); 87 } else if (AppConstants.platform == "macosx") { 88 tasks.push(() => this.macDockStatus()); 89 } 90 91 this._runIdleTasks(tasks, "startupTelemetryIdleTask"); 92 }, 93 94 /** 95 * Use this function as an entry point to collect telemetry that we hope 96 * to collect once per session, at any arbitrary point in time, and 97 * 98 * **which we are okay with sometimes not running at all.** 99 * 100 * See BrowserGlue.sys.mjs's _scheduleBestEffortUserIdleTasks for more 101 * details. 102 */ 103 bestEffortIdleStartup() { 104 let tasks = [ 105 () => this.primaryPasswordEnabled(), 106 () => lazy.OsEnvironment.reportAllowedAppSources(), 107 ]; 108 if (AppConstants.platform == "win" && this._willUseExpensiveTelemetry) { 109 tasks.push( 110 () => lazy.BrowserUsageTelemetry.reportProfileCount(), 111 () => lazy.BrowserUsageTelemetry.reportInstallationTelemetry() 112 ); 113 } 114 this._runIdleTasks(tasks, "startupTelemetryLateIdleTask"); 115 }, 116 117 /** 118 * Initialize Firefox-on-Glean. 119 * 120 * This is at the top because it's a bit different from the other code here 121 * which is strictly collecting specific metrics. 122 */ 123 async initFOG() { 124 // Handle Usage Profile ID. Similar logic to what's happening in 125 // `TelemetryControllerParent` for the client ID. Must be done before 126 // initializing FOG so that ping enabled/disabled states are correct 127 // before Glean takes actions. 128 await lazy.UsageReporting.ensureInitialized(); 129 130 // If needed, delay initializing FOG until policy interaction is 131 // completed. See comments in `TelemetryReportingPolicy`. 132 await lazy.TelemetryReportingPolicy.ensureUserIsNotified(); 133 134 Services.fog.initializeFOG(); 135 136 // Register Glean to listen for experiment updates releated to the 137 // "gleanInternalSdk" feature defined in the t/c/nimbus/FeatureManifest.yaml 138 // This feature is intended for internal Glean use only. For features wishing 139 // to set a remote metric configuration, please use the "glean" feature for 140 // the purpose of setting the data-control-plane features via Server Knobs. 141 lazy.NimbusFeatures.gleanInternalSdk.onUpdate(() => { 142 let cfg = lazy.NimbusFeatures.gleanInternalSdk.getVariable( 143 "gleanMetricConfiguration" 144 ); 145 Services.fog.applyServerKnobsConfig(JSON.stringify(cfg)); 146 }); 147 148 // Register Glean to listen for experiment updates releated to the 149 // "glean" feature defined in the t/c/nimbus/FeatureManifest.yaml 150 lazy.NimbusFeatures.glean.onUpdate(() => { 151 const enrollments = lazy.NimbusFeatures.glean.getAllEnrollments(); 152 for (const enrollment of enrollments) { 153 const cfg = enrollment.value.gleanMetricConfiguration; 154 if (typeof cfg === "object" && cfg !== null) { 155 Services.fog.applyServerKnobsConfig(JSON.stringify(cfg)); 156 } 157 } 158 }); 159 }, 160 161 startupConditions() { 162 let nowSeconds = Math.round(Date.now() / 1000); 163 // Don't include cases where we don't have the pref. This rules out the first install 164 // as well as the first run of a build since this was introduced. These could by some 165 // definitions be referred to as "cold" startups, but probably not since we likely 166 // just wrote many of the files we use to disk. This way we should approximate a lower 167 // bound to the number of cold startups rather than an upper bound. 168 let lastCheckSeconds = Services.prefs.getIntPref( 169 "browser.startup.lastColdStartupCheck", 170 nowSeconds 171 ); 172 Services.prefs.setIntPref( 173 "browser.startup.lastColdStartupCheck", 174 nowSeconds 175 ); 176 try { 177 let secondsSinceLastOSRestart = 178 Services.startup.secondsSinceLastOSRestart; 179 let isColdStartup = 180 nowSeconds - secondsSinceLastOSRestart > lastCheckSeconds; 181 Glean.startup.isCold.set(isColdStartup); 182 Glean.startup.secondsSinceLastOsRestart.set(secondsSinceLastOSRestart); 183 } catch (ex) { 184 if (ex.name !== "NS_ERROR_NOT_IMPLEMENTED") { 185 console.error(ex); 186 } 187 } 188 }, 189 190 contentBlocking() { 191 let tpEnabled = Services.prefs.getBoolPref( 192 "privacy.trackingprotection.enabled" 193 ); 194 Glean.contentblocking.trackingProtectionEnabled[ 195 tpEnabled ? "true" : "false" 196 ].add(); 197 198 let tpPBEnabled = Services.prefs.getBoolPref( 199 "privacy.trackingprotection.pbmode.enabled" 200 ); 201 Glean.contentblocking.trackingProtectionPbmDisabled[ 202 !tpPBEnabled ? "true" : "false" 203 ].add(); 204 205 let cookieBehavior = Services.prefs.getIntPref( 206 "network.cookie.cookieBehavior" 207 ); 208 Glean.contentblocking.cookieBehavior.accumulateSingleSample(cookieBehavior); 209 210 let fpEnabled = Services.prefs.getBoolPref( 211 "privacy.trackingprotection.fingerprinting.enabled" 212 ); 213 let cmEnabled = Services.prefs.getBoolPref( 214 "privacy.trackingprotection.cryptomining.enabled" 215 ); 216 let categoryPref; 217 switch ( 218 Services.prefs.getStringPref("browser.contentblocking.category", null) 219 ) { 220 case "standard": 221 categoryPref = 0; 222 break; 223 case "strict": 224 categoryPref = 1; 225 break; 226 case "custom": 227 categoryPref = 2; 228 break; 229 default: 230 // Any other value is unsupported. 231 categoryPref = 3; 232 break; 233 } 234 235 Glean.contentblocking.fingerprintingBlockingEnabled.set(fpEnabled); 236 Glean.contentblocking.cryptominingBlockingEnabled.set(cmEnabled); 237 Glean.contentblocking.category.set(categoryPref); 238 }, 239 240 dataSanitization() { 241 Glean.datasanitization.privacySanitizeSanitizeOnShutdown.set( 242 Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") 243 ); 244 Glean.datasanitization.privacyClearOnShutdownCookies.set( 245 Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies") 246 ); 247 Glean.datasanitization.privacyClearOnShutdownHistory.set( 248 Services.prefs.getBoolPref("privacy.clearOnShutdown.history") 249 ); 250 Glean.datasanitization.privacyClearOnShutdownFormdata.set( 251 Services.prefs.getBoolPref("privacy.clearOnShutdown.formdata") 252 ); 253 Glean.datasanitization.privacyClearOnShutdownDownloads.set( 254 Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads") 255 ); 256 Glean.datasanitization.privacyClearOnShutdownCache.set( 257 Services.prefs.getBoolPref("privacy.clearOnShutdown.cache") 258 ); 259 Glean.datasanitization.privacyClearOnShutdownSessions.set( 260 Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions") 261 ); 262 Glean.datasanitization.privacyClearOnShutdownOfflineApps.set( 263 Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps") 264 ); 265 Glean.datasanitization.privacyClearOnShutdownSiteSettings.set( 266 Services.prefs.getBoolPref("privacy.clearOnShutdown.siteSettings") 267 ); 268 Glean.datasanitization.privacyClearOnShutdownOpenWindows.set( 269 Services.prefs.getBoolPref("privacy.clearOnShutdown.openWindows") 270 ); 271 272 let exceptions = 0; 273 for (let permission of Services.perms.all) { 274 // We consider just permissions set for http, https and file URLs. 275 if ( 276 permission.type == "cookie" && 277 permission.capability == Ci.nsICookiePermission.ACCESS_SESSION && 278 ["http", "https", "file"].some(scheme => 279 permission.principal.schemeIs(scheme) 280 ) 281 ) { 282 exceptions++; 283 } 284 } 285 Glean.datasanitization.sessionPermissionExceptions.set(exceptions); 286 }, 287 288 httpsOnlyState() { 289 const PREF_ENABLED = "dom.security.https_only_mode"; 290 const PREF_WAS_ENABLED = "dom.security.https_only_mode_ever_enabled"; 291 const _checkHTTPSOnlyPref = async () => { 292 const enabled = Services.prefs.getBoolPref(PREF_ENABLED, false); 293 const was_enabled = Services.prefs.getBoolPref(PREF_WAS_ENABLED, false); 294 let value = 0; 295 if (enabled) { 296 value = 1; 297 Services.prefs.setBoolPref(PREF_WAS_ENABLED, true); 298 } else if (was_enabled) { 299 value = 2; 300 } 301 Glean.security.httpsOnlyModeEnabled.set(value); 302 }; 303 304 Services.prefs.addObserver(PREF_ENABLED, _checkHTTPSOnlyPref); 305 _checkHTTPSOnlyPref(); 306 307 const PREF_PBM_WAS_ENABLED = 308 "dom.security.https_only_mode_ever_enabled_pbm"; 309 const PREF_PBM_ENABLED = "dom.security.https_only_mode_pbm"; 310 311 const _checkHTTPSOnlyPBMPref = async () => { 312 const enabledPBM = Services.prefs.getBoolPref(PREF_PBM_ENABLED, false); 313 const was_enabledPBM = Services.prefs.getBoolPref( 314 PREF_PBM_WAS_ENABLED, 315 false 316 ); 317 let valuePBM = 0; 318 if (enabledPBM) { 319 valuePBM = 1; 320 Services.prefs.setBoolPref(PREF_PBM_WAS_ENABLED, true); 321 } else if (was_enabledPBM) { 322 valuePBM = 2; 323 } 324 Glean.security.httpsOnlyModeEnabledPbm.set(valuePBM); 325 }; 326 327 Services.prefs.addObserver(PREF_PBM_ENABLED, _checkHTTPSOnlyPBMPref); 328 _checkHTTPSOnlyPBMPref(); 329 }, 330 331 globalPrivacyControl() { 332 const FEATURE_PREF_ENABLED = "privacy.globalprivacycontrol.enabled"; 333 const FUNCTIONALITY_PREF_ENABLED = 334 "privacy.globalprivacycontrol.functionality.enabled"; 335 const PREF_WAS_ENABLED = "privacy.globalprivacycontrol.was_ever_enabled"; 336 const _checkGPCPref = async () => { 337 const feature_enabled = Services.prefs.getBoolPref( 338 FEATURE_PREF_ENABLED, 339 false 340 ); 341 const functionality_enabled = Services.prefs.getBoolPref( 342 FUNCTIONALITY_PREF_ENABLED, 343 false 344 ); 345 const was_enabled = Services.prefs.getBoolPref(PREF_WAS_ENABLED, false); 346 let value = 0; 347 if (feature_enabled && functionality_enabled) { 348 value = 1; 349 Services.prefs.setBoolPref(PREF_WAS_ENABLED, true); 350 } else if (was_enabled) { 351 value = 2; 352 } 353 Glean.security.globalPrivacyControlEnabled.set(value); 354 }; 355 356 Services.prefs.addObserver(FEATURE_PREF_ENABLED, _checkGPCPref); 357 Services.prefs.addObserver(FUNCTIONALITY_PREF_ENABLED, _checkGPCPref); 358 _checkGPCPref(); 359 }, 360 361 // check if the launcher was used to open firefox 362 isUsingLauncher() { 363 if (Services.env.get("FIREFOX_LAUNCHED_BY_DESKTOP_LAUNCHER") == "TRUE") { 364 return true; 365 } 366 367 return false; 368 }, 369 370 async pinningStatus() { 371 let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService( 372 Ci.nsIWindowsShellService 373 ); 374 let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService( 375 Ci.nsIWinTaskbar 376 ); 377 378 try { 379 Glean.osEnvironment.isTaskbarPinned.set( 380 await shellService.isCurrentAppPinnedToTaskbarAsync( 381 winTaskbar.defaultGroupId 382 ) 383 ); 384 // Bug 1911343: Pinning regular browsing on MSIX 385 // causes false positives when checking for private 386 // browsing. 387 if ( 388 AppConstants.platform === "win" && 389 !Services.sysinfo.getProperty("hasWinPackageId") 390 ) { 391 Glean.osEnvironment.isTaskbarPinnedPrivate.set( 392 await shellService.isCurrentAppPinnedToTaskbarAsync( 393 winTaskbar.defaultPrivateGroupId 394 ) 395 ); 396 } 397 } catch (ex) { 398 console.error(ex); 399 } 400 401 let classification; 402 let shortcut; 403 try { 404 shortcut = Services.appinfo.processStartupShortcut; 405 classification = shellService.classifyShortcut(shortcut); 406 } catch (ex) { 407 console.error(ex); 408 } 409 410 if (!classification) { 411 if (lazy.BrowserInitState.isLaunchOnLogin) { 412 classification = "Autostart"; 413 } else if (shortcut) { 414 classification = "OtherShortcut"; 415 } else if (this.isUsingLauncher()) { 416 classification = "DesktopLauncher"; 417 } else { 418 classification = "Other"; 419 } 420 } 421 // Because of how taskbar tabs work, it may be classifed as a taskbar 422 // shortcut, in which case we want to overwrite it. 423 if (lazy.BrowserInitState.isTaskbarTab) { 424 classification = "TaskbarTab"; 425 } 426 Glean.osEnvironment.launchMethod.set(classification); 427 }, 428 429 isDefaultHandler() { 430 // Report whether Firefox is the default handler for various files types 431 // and protocols, in particular, ".pdf" and "mailto" 432 [".pdf", "mailto"].every(x => { 433 Glean.osEnvironment.isDefaultHandler[x].set( 434 lazy.ShellService.isDefaultHandlerFor(x) 435 ); 436 return true; 437 }); 438 }, 439 440 macDockStatus() { 441 // Report macOS Dock status 442 Glean.osEnvironment.isKeptInDock.set( 443 Cc["@mozilla.org/widget/macdocksupport;1"].getService( 444 Ci.nsIMacDockSupport 445 ).isAppInDock 446 ); 447 }, 448 449 sslKeylogFile() { 450 Glean.sslkeylogging.enabled.set(Services.env.exists("SSLKEYLOGFILE")); 451 }, 452 453 osAuthEnabled() { 454 const osAuthForCc = lazy.FormAutofillUtils.getOSAuthEnabled(); 455 const osAuthForPw = lazy.LoginHelper.getOSAuthEnabled(); 456 457 Glean.formautofill.osAuthEnabled.set(osAuthForCc); 458 Glean.pwmgr.osAuthEnabled.set(osAuthForPw); 459 }, 460 461 primaryPasswordEnabled() { 462 let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService( 463 Ci.nsIPK11TokenDB 464 ); 465 let token = tokenDB.getInternalKeyToken(); 466 Glean.primaryPassword.enabled.set(token.hasPassword); 467 }, 468 469 pipEnabled() { 470 const TOGGLE_ENABLED_PREF = 471 "media.videocontrols.picture-in-picture.video-toggle.enabled"; 472 473 const observe = (subject, topic) => { 474 const enabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF, false); 475 Glean.pictureinpicture.toggleEnabled.set(enabled); 476 477 // Record events when preferences change 478 if (topic === "nsPref:changed") { 479 if (enabled) { 480 Glean.pictureinpictureSettings.enableSettings.record(); 481 } 482 } 483 }; 484 485 Services.prefs.addObserver(TOGGLE_ENABLED_PREF, observe); 486 observe(); 487 }, 488 };