Files
tubestation/browser/components/StartupTelemetry.sys.mjs
Gijs Kruitbosch e66aa47674 Bug 1958070 - unify scheduling of telemetry-related startup tasks,r=firefox-desktop-core-reviewers ,mossop
This moves a handful of things to run slightly later, but primarily it makes
sure that the scheduling and conditions under which each bit of telemetry is
collected is clear and in one place, instead of spread around.

Differential Revision: https://phabricator.services.mozilla.com/D244421
2025-04-14 14:04:33 +00:00

496 lines
17 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserInitState: "resource:///modules/BrowserGlue.sys.mjs",
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
OsEnvironment: "resource://gre/modules/OsEnvironment.sys.mjs",
PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs",
ShellService: "resource:///modules/ShellService.sys.mjs",
TelemetryReportingPolicy:
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs",
UsageReporting: "resource://gre/modules/UsageReporting.sys.mjs",
});
/**
* Used to collect various bits of telemetry during browser startup.
*
*/
export let StartupTelemetry = {
// Some tasks are expensive because they involve significant disk IO, and
// may also write information to disk. If we submit the telemetry that may
// happen anyway, but if we don't then this is undesirable, so those tasks are
// only run if we will submit the results.
// Why run any telemetry code at all if we don't submit the data? Because
// local and autoland builds usually do not submit telemetry, but we still
// want to be able to run automated tests to check the code _worked_.
get _willUseExpensiveTelemetry() {
return (
AppConstants.MOZ_TELEMETRY_REPORTING &&
Services.prefs.getBoolPref(
"datareporting.healthreport.uploadEnabled",
false
)
);
},
_runIdleTasks(tasks, profilerMarker) {
for (let task of tasks) {
ChromeUtils.idleDispatch(async () => {
if (!Services.startup.shuttingDown) {
let startTime = Cu.now();
try {
await task();
} catch (ex) {
console.error(ex);
} finally {
ChromeUtils.addProfilerMarker(
profilerMarker,
startTime,
task.toSource()
);
}
}
});
}
},
browserIdleStartup() {
let tasks = [
// FOG doesn't need to be initialized _too_ early because it has a pre-init buffer.
() => this.initFOG(),
() => this.contentBlocking(),
() => this.dataSanitization(),
() => this.pipEnabled(),
() => this.sslKeylogFile(),
() => this.osAuthEnabled(),
() => this.startupConditions(),
() => this.httpsOnlyState(),
() => this.globalPrivacyControl(),
];
if (this._willUseExpensiveTelemetry) {
tasks.push(() => lazy.PlacesDBUtils.telemetry());
}
if (AppConstants.platform == "win") {
tasks.push(
() => this.pinningStatus(),
() => this.isDefaultHandler()
);
} else if (AppConstants.platform == "macosx") {
tasks.push(() => this.macDockStatus());
}
this._runIdleTasks(tasks, "startupTelemetryIdleTask");
},
/**
* Use this function as an entry point to collect telemetry that we hope
* to collect once per session, at any arbitrary point in time, and
*
* **which we are okay with sometimes not running at all.**
*
* See BrowserGlue.sys.mjs's _scheduleBestEffortUserIdleTasks for more
* details.
*/
bestEffortIdleStartup() {
let tasks = [
() => this.primaryPasswordEnabled(),
() => this.trustObjectCount(),
() => lazy.OsEnvironment.reportAllowedAppSources(),
];
if (AppConstants.platform == "win" && this._willUseExpensiveTelemetry) {
tasks.push(
() => lazy.BrowserUsageTelemetry.reportProfileCount(),
() => lazy.BrowserUsageTelemetry.reportInstallationTelemetry()
);
}
this._runIdleTasks(tasks, "startupTelemetryLateIdleTask");
},
/**
* Initialize Firefox-on-Glean.
*
* This is at the top because it's a bit different from the other code here
* which is strictly collecting specific metrics.
*/
async initFOG() {
// Handle Usage Profile ID. Similar logic to what's happening in
// `TelemetryControllerParent` for the client ID. Must be done before
// initializing FOG so that ping enabled/disabled states are correct
// before Glean takes actions.
await lazy.UsageReporting.ensureInitialized();
// If needed, delay initializing FOG until policy interaction is
// completed. See comments in `TelemetryReportingPolicy`.
await lazy.TelemetryReportingPolicy.ensureUserIsNotified();
Services.fog.initializeFOG();
// Register Glean to listen for experiment updates releated to the
// "gleanInternalSdk" feature defined in the t/c/nimbus/FeatureManifest.yaml
// This feature is intended for internal Glean use only. For features wishing
// to set a remote metric configuration, please use the "glean" feature for
// the purpose of setting the data-control-plane features via Server Knobs.
lazy.NimbusFeatures.gleanInternalSdk.onUpdate(() => {
let cfg = lazy.NimbusFeatures.gleanInternalSdk.getVariable(
"gleanMetricConfiguration"
);
Services.fog.applyServerKnobsConfig(JSON.stringify(cfg));
});
// Register Glean to listen for experiment updates releated to the
// "glean" feature defined in the t/c/nimbus/FeatureManifest.yaml
lazy.NimbusFeatures.glean.onUpdate(() => {
let cfg = lazy.NimbusFeatures.glean.getVariable(
"gleanMetricConfiguration"
);
Services.fog.applyServerKnobsConfig(JSON.stringify(cfg));
});
},
startupConditions() {
let nowSeconds = Math.round(Date.now() / 1000);
// Don't include cases where we don't have the pref. This rules out the first install
// as well as the first run of a build since this was introduced. These could by some
// definitions be referred to as "cold" startups, but probably not since we likely
// just wrote many of the files we use to disk. This way we should approximate a lower
// bound to the number of cold startups rather than an upper bound.
let lastCheckSeconds = Services.prefs.getIntPref(
"browser.startup.lastColdStartupCheck",
nowSeconds
);
Services.prefs.setIntPref(
"browser.startup.lastColdStartupCheck",
nowSeconds
);
try {
let secondsSinceLastOSRestart =
Services.startup.secondsSinceLastOSRestart;
let isColdStartup =
nowSeconds - secondsSinceLastOSRestart > lastCheckSeconds;
Glean.startup.isCold.set(isColdStartup);
Glean.startup.secondsSinceLastOsRestart.set(secondsSinceLastOSRestart);
} catch (ex) {
if (ex.name !== "NS_ERROR_NOT_IMPLEMENTED") {
console.error(ex);
}
}
},
contentBlocking() {
let tpEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.enabled"
);
Glean.contentblocking.trackingProtectionEnabled[
tpEnabled ? "true" : "false"
].add();
let tpPBEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.pbmode.enabled"
);
Glean.contentblocking.trackingProtectionPbmDisabled[
!tpPBEnabled ? "true" : "false"
].add();
let cookieBehavior = Services.prefs.getIntPref(
"network.cookie.cookieBehavior"
);
Glean.contentblocking.cookieBehavior.accumulateSingleSample(cookieBehavior);
let fpEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.fingerprinting.enabled"
);
let cmEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.cryptomining.enabled"
);
let categoryPref;
switch (
Services.prefs.getStringPref("browser.contentblocking.category", null)
) {
case "standard":
categoryPref = 0;
break;
case "strict":
categoryPref = 1;
break;
case "custom":
categoryPref = 2;
break;
default:
// Any other value is unsupported.
categoryPref = 3;
break;
}
Glean.contentblocking.fingerprintingBlockingEnabled.set(fpEnabled);
Glean.contentblocking.cryptominingBlockingEnabled.set(cmEnabled);
Glean.contentblocking.category.set(categoryPref);
},
dataSanitization() {
Glean.datasanitization.privacySanitizeSanitizeOnShutdown.set(
Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown")
);
Glean.datasanitization.privacyClearOnShutdownCookies.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies")
);
Glean.datasanitization.privacyClearOnShutdownHistory.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.history")
);
Glean.datasanitization.privacyClearOnShutdownFormdata.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.formdata")
);
Glean.datasanitization.privacyClearOnShutdownDownloads.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads")
);
Glean.datasanitization.privacyClearOnShutdownCache.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.cache")
);
Glean.datasanitization.privacyClearOnShutdownSessions.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions")
);
Glean.datasanitization.privacyClearOnShutdownOfflineApps.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps")
);
Glean.datasanitization.privacyClearOnShutdownSiteSettings.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.siteSettings")
);
Glean.datasanitization.privacyClearOnShutdownOpenWindows.set(
Services.prefs.getBoolPref("privacy.clearOnShutdown.openWindows")
);
let exceptions = 0;
for (let permission of Services.perms.all) {
// We consider just permissions set for http, https and file URLs.
if (
permission.type == "cookie" &&
permission.capability == Ci.nsICookiePermission.ACCESS_SESSION &&
["http", "https", "file"].some(scheme =>
permission.principal.schemeIs(scheme)
)
) {
exceptions++;
}
}
Glean.datasanitization.sessionPermissionExceptions.set(exceptions);
},
httpsOnlyState() {
const PREF_ENABLED = "dom.security.https_only_mode";
const PREF_WAS_ENABLED = "dom.security.https_only_mode_ever_enabled";
const _checkHTTPSOnlyPref = async () => {
const enabled = Services.prefs.getBoolPref(PREF_ENABLED, false);
const was_enabled = Services.prefs.getBoolPref(PREF_WAS_ENABLED, false);
let value = 0;
if (enabled) {
value = 1;
Services.prefs.setBoolPref(PREF_WAS_ENABLED, true);
} else if (was_enabled) {
value = 2;
}
Glean.security.httpsOnlyModeEnabled.set(value);
};
Services.prefs.addObserver(PREF_ENABLED, _checkHTTPSOnlyPref);
_checkHTTPSOnlyPref();
const PREF_PBM_WAS_ENABLED =
"dom.security.https_only_mode_ever_enabled_pbm";
const PREF_PBM_ENABLED = "dom.security.https_only_mode_pbm";
const _checkHTTPSOnlyPBMPref = async () => {
const enabledPBM = Services.prefs.getBoolPref(PREF_PBM_ENABLED, false);
const was_enabledPBM = Services.prefs.getBoolPref(
PREF_PBM_WAS_ENABLED,
false
);
let valuePBM = 0;
if (enabledPBM) {
valuePBM = 1;
Services.prefs.setBoolPref(PREF_PBM_WAS_ENABLED, true);
} else if (was_enabledPBM) {
valuePBM = 2;
}
Glean.security.httpsOnlyModeEnabledPbm.set(valuePBM);
};
Services.prefs.addObserver(PREF_PBM_ENABLED, _checkHTTPSOnlyPBMPref);
_checkHTTPSOnlyPBMPref();
},
globalPrivacyControl() {
const FEATURE_PREF_ENABLED = "privacy.globalprivacycontrol.enabled";
const FUNCTIONALITY_PREF_ENABLED =
"privacy.globalprivacycontrol.functionality.enabled";
const PREF_WAS_ENABLED = "privacy.globalprivacycontrol.was_ever_enabled";
const _checkGPCPref = async () => {
const feature_enabled = Services.prefs.getBoolPref(
FEATURE_PREF_ENABLED,
false
);
const functionality_enabled = Services.prefs.getBoolPref(
FUNCTIONALITY_PREF_ENABLED,
false
);
const was_enabled = Services.prefs.getBoolPref(PREF_WAS_ENABLED, false);
let value = 0;
if (feature_enabled && functionality_enabled) {
value = 1;
Services.prefs.setBoolPref(PREF_WAS_ENABLED, true);
} else if (was_enabled) {
value = 2;
}
Glean.security.globalPrivacyControlEnabled.set(value);
};
Services.prefs.addObserver(FEATURE_PREF_ENABLED, _checkGPCPref);
Services.prefs.addObserver(FUNCTIONALITY_PREF_ENABLED, _checkGPCPref);
_checkGPCPref();
},
async pinningStatus() {
let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService(
Ci.nsIWindowsShellService
);
let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
Ci.nsIWinTaskbar
);
try {
Glean.osEnvironment.isTaskbarPinned.set(
await shellService.isCurrentAppPinnedToTaskbarAsync(
winTaskbar.defaultGroupId
)
);
// Bug 1911343: Pinning regular browsing on MSIX
// causes false positives when checking for private
// browsing.
if (
AppConstants.platform === "win" &&
!Services.sysinfo.getProperty("hasWinPackageId")
) {
Glean.osEnvironment.isTaskbarPinnedPrivate.set(
await shellService.isCurrentAppPinnedToTaskbarAsync(
winTaskbar.defaultPrivateGroupId
)
);
}
} catch (ex) {
console.error(ex);
}
let classification;
let shortcut;
try {
shortcut = Services.appinfo.processStartupShortcut;
classification = shellService.classifyShortcut(shortcut);
} catch (ex) {
console.error(ex);
}
if (!classification) {
if (lazy.BrowserInitState.isLaunchOnLogin) {
classification = "Autostart";
} else if (shortcut) {
classification = "OtherShortcut";
} else {
classification = "Other";
}
}
// Because of how taskbar tabs work, it may be classifed as a taskbar
// shortcut, in which case we want to overwrite it.
if (lazy.BrowserInitState.isTaskbarTab) {
classification = "TaskbarTab";
}
Glean.osEnvironment.launchMethod.set(classification);
},
isDefaultHandler() {
// Report whether Firefox is the default handler for various files types
// and protocols, in particular, ".pdf" and "mailto"
[".pdf", "mailto"].every(x => {
Glean.osEnvironment.isDefaultHandler[x].set(
lazy.ShellService.isDefaultHandlerFor(x)
);
return true;
});
},
macDockStatus() {
// Report macOS Dock status
Glean.osEnvironment.isKeptInDock.set(
Cc["@mozilla.org/widget/macdocksupport;1"].getService(
Ci.nsIMacDockSupport
).isAppInDock
);
},
sslKeylogFile() {
Glean.sslkeylogging.enabled.set(Services.env.exists("SSLKEYLOGFILE"));
},
osAuthEnabled() {
// Manually read these prefs. This treats any non-empty-string
// value as "turned off", irrespective of whether it correctly
// decrypts to the correct value, because we cannot do the
// decryption if the primary password has not yet been provided,
// and for telemetry treating that situation as "turned off"
// seems reasonable.
const osAuthForCc = !Services.prefs.getStringPref(
lazy.FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
""
);
const osAuthForPw = !Services.prefs.getStringPref(
lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF,
""
);
Glean.formautofill.osAuthEnabled.set(osAuthForCc);
Glean.pwmgr.osAuthEnabled.set(osAuthForPw);
},
primaryPasswordEnabled() {
let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
Ci.nsIPK11TokenDB
);
let token = tokenDB.getInternalKeyToken();
Glean.primaryPassword.enabled.set(token.hasPassword);
},
trustObjectCount() {
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
// countTrustObjects also logs the number of trust objects for telemetry purposes
certdb.countTrustObjects();
},
pipEnabled() {
const TOGGLE_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.enabled";
const observe = (subject, topic) => {
const enabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF, false);
Glean.pictureinpicture.toggleEnabled.set(enabled);
// Record events when preferences change
if (topic === "nsPref:changed") {
if (enabled) {
Glean.pictureinpictureSettings.enableSettings.record();
}
}
};
Services.prefs.addObserver(TOGGLE_ENABLED_PREF, observe);
observe();
},
};