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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivacyMetricsService:
"moz-src:///browser/components/protections/PrivacyMetricsService.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
import {
actionTypes as at,
actionCreators as ac,
import {
WIDGET_REGISTRY,
isWidgetEnabled,
const PREF_WIDGETS_ENABLED = "widgets.enabled";
const PRIVACY_ENTRY = WIDGET_REGISTRY.find(w => w.id === "privacy");
// Prefs that can flip the widget's registry enablement (incl. trainhop config).
// A change to any of these should re-evaluate whether to start fetching.
const ENABLEMENT_PREFS = new Set([
PREF_WIDGETS_ENABLED,
PRIVACY_ENTRY.enabledPref,
PRIVACY_ENTRY.systemEnabledPref,
"trainhopConfig",
]);
/**
* Feed for the Privacy widget. Runs in the parent process and reads the daily
* tracker-blocked count straight from PrivacyMetricsService (same process, so
* no IPC), broadcasting it to the Redux store on startup and each SYSTEM_TICK.
*/
export class PrivacyFeed {
get enabled() {
const prefs = this.store.getState()?.Prefs.values;
// Share the registry enablement logic the UI uses so trainhop rollouts
// (trainhopConfig.widgets.privacyEnabled) start the feed even when the
// system pref defaults false. Otherwise the widget renders but the counter
// stays stuck at the empty state.
return isWidgetEnabled(PRIVACY_ENTRY, prefs, prefs?.[PREF_WIDGETS_ENABLED]);
}
/**
* Count of distinct sites visited today, read from the Places history DB.
*
* NOTE: this is "sites visited today", not "sites where a tracker was
* blocked" — the tracking DB records no per-site data, so this is a proxy
* Places, so they're inherently excluded.
*
* @returns {Promise<number>}
*/
async getSitesVisitedToday() {
const db = await lazy.PlacesUtils.promiseDBConnection();
// Count non-hidden pages only — hidden = 1 covers redirect sources, embeds,
// and framed visits, so e.g. amazon.com -> www.amazon.com counts as one
// site, not two.
//
// Day boundary is UTC midnight to match the tracker count, which keys on
// TrackingDBService's UTC date and so can't be local. RISK: both "today"
// readouts therefore reset at UTC midnight — mid-day for users far from GMT.
// Inherent to the tracking DB (about:protections behaves the same); a true
// local-day "today" would need event-level timestamps in TrackingDBService.
const rows = await db.execute(
`SELECT COUNT(DISTINCT p.origin_id) AS count
FROM moz_historyvisits v
JOIN moz_places p ON v.place_id = p.id
WHERE p.hidden = 0
AND v.visit_date >= (strftime('%s','now','start of day') * 1000000)`
);
return rows[0]?.getResultByName("count") ?? 0;
}
async updateStats() {
// @backward-compat { version 145 }
// getTodayStats() ships alongside this widget; older platforms have
// version reaches release, then remove this check.
if (typeof lazy.PrivacyMetricsService?.getTodayStats !== "function") {
return;
}
const [stats, sitesToday] = await Promise.all([
lazy.PrivacyMetricsService.getTodayStats(),
this.getSitesVisitedToday(),
]);
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_PRIVACY_UPDATE,
data: {
// The headline count is the total blocked across all categories
// (cookies, trackers, fingerprinters, cryptominers, social) — the
// same total about:protections shows — not just the "trackers" slice.
trackersToday: stats.total,
sitesToday,
lastUpdated: stats.lastUpdated,
},
})
);
}
async onAction(action) {
switch (action.type) {
// INIT/SYSTEM_TICK keep the count fresh across the session; NEW_TAB_INIT
// refreshes it each time a tab is opened so the count is current whenever
// the user looks at it (the daily total changes as they browse).
case at.INIT:
case at.SYSTEM_TICK:
case at.NEW_TAB_INIT:
if (this.enabled) {
await this.updateStats();
}
break;
case at.PREF_CHANGED:
// Enablement can flip on after startup (e.g. a trainhop rollout lands
// its config); fetch as soon as it does.
if (ENABLEMENT_PREFS.has(action.data?.name) && this.enabled) {
await this.updateStats();
}
break;
}
}
}