Source code
Revision control
Copy as Markdown
Other Tools
/* 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
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(() => {
const enrollments = lazy.NimbusFeatures.glean.getAllEnrollments();
for (const enrollment of enrollments) {
const cfg = enrollment.value.gleanMetricConfiguration;
if (typeof cfg === "object" && cfg !== null) {
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
)
);
// 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();
},
};