Skip to content

Commit

Permalink
Additional metrics for breakage testing (#2918)
Browse files Browse the repository at this point in the history
* Extract messaging into a component.

* Pipe popup interactions to a/b metrics

* Implement refresh metrics

* Move all metrics to the same file

* Replace PixelMetric for broken site reports with dashboard use version.

* Define a set of metrics for breakage experiments (https://app.asana.com/0/1163321984198618/1208737552969233/f)

* Revert config URL

* Delay TDS override check to guarantee that config has been processed.

* Lint fix

* Fix cyclic dependency. We need to keep the popup-messaging file to handle
static access to the messaging port.

* Fix merge

* Docs

* RefreshMetric tests and fixes
  • Loading branch information
sammacbeth authored Jan 31, 2025
1 parent b98103c commit 7c9aa12
Show file tree
Hide file tree
Showing 14 changed files with 449 additions and 195 deletions.
16 changes: 14 additions & 2 deletions shared/js/background/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import RemoteConfig from './components/remote-config';
import initDebugBuild from './devbuild';
import initReloader from './devbuild-reloader';
import tabManager from './tab-manager';
import AbnExperimentMetrics, { AppUseMetric, PixelMetric, SearchMetric } from './components/abn-experiments';
import AbnExperimentMetrics from './components/abn-experiments';
import MessageRouter from './components/message-router';
import { AppUseMetric, SearchMetric, DashboardUseMetric, RefreshMetric } from './metrics';
// NOTE: this needs to be the first thing that's require()d when the extension loads.
// otherwise FF might miss the onInstalled event
require('./events');
Expand Down Expand Up @@ -60,6 +62,7 @@ const devtools = new Devtools({ tds });
* trackers: TrackersGlobal;
* remoteConfig: RemoteConfig;
* abnMetrics: AbnExperimentMetrics?;
* messaging: MessageRouter;
* }}
*/
const components = {
Expand All @@ -74,11 +77,20 @@ const components = {
devtools,
remoteConfig,
abnMetrics,
messaging: new MessageRouter({ tabManager }),
};

// Chrome-only components
if (BUILD_TARGET === 'chrome' || BUILD_TARGET === 'chrome-mv2') {
components.metrics = [new AppUseMetric({ abnMetrics }), new SearchMetric({ abnMetrics }), new PixelMetric({ abnMetrics })];
components.metrics = [
new AppUseMetric({ abnMetrics }),
new SearchMetric({ abnMetrics }),
new DashboardUseMetric({ abnMetrics, messaging: components.messaging }),
new RefreshMetric({
abnMetrics,
tabTracking: components.tabTracking,
}),
];
components.fireButton = new FireButton({ settings, tabManager });
}
// MV3-only components
Expand Down
2 changes: 1 addition & 1 deletion shared/js/background/before-request.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import browser from 'webextension-polyfill';
import EventEmitter2 from 'eventemitter2';
import ATB from './atb';
import { postPopupMessage } from './popupMessaging';
import { postPopupMessage } from './popup-messaging';

const utils = require('./utils');
const trackers = require('./trackers');
Expand Down
2 changes: 1 addition & 1 deletion shared/js/background/companies.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const TopBlocked = require('./classes/top-blocked');
const Company = require('./classes/company');
const browserWrapper = require('./wrapper');
const { postPopupMessage } = require('./popupMessaging');
const { postPopupMessage } = require('./popup-messaging');

const Companies = (() => {
let companyContainer = {};
Expand Down
64 changes: 0 additions & 64 deletions shared/js/background/components/abn-experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* sent: boolean;
* } & ExperimentMetric} ExperimentMetricCounter
*/
import browser from 'webextension-polyfill';
import { sendPixelRequest } from '../pixels';

/**
Expand Down Expand Up @@ -47,69 +46,6 @@ export function generateRetentionMetrics() {
});
}

export class AppUseMetric {
/**
* Metric that fires once on creation.
* @param {{
* abnMetrics: AbnExperimentMetrics
* }} opts
*/
constructor({ abnMetrics }) {
// trigger on construction: happens whenever the service worker is spun up, which should correlate with browser activity.
abnMetrics.remoteConfig.ready.then(() => abnMetrics.onMetricTriggered('app_use'));
}
}

export class SearchMetric {
/**
* Metric that fires whenever a new search is made
* @param {{
* abnMetrics: AbnExperimentMetrics
* }} opts
*/
constructor({ abnMetrics }) {
browser.webRequest.onCompleted.addListener(
async (details) => {
const params = new URL(details.url).searchParams;
if (params.get('q')?.length) {
await abnMetrics.remoteConfig.ready;
abnMetrics.onMetricTriggered('search');
}
},
{
urls: ['https://*.duckduckgo.com/*'],
types: ['main_frame'],
},
);
}
}

export class PixelMetric {
/**
* Metric that observes outgoing pixel calls and routes a subset of them as experiment metrics.
* Currently intercepts `epbf` pixels and triggers a `brokenSiteReport` metric.
* @param {{
* abnMetrics: AbnExperimentMetrics
* }} opts
*/
constructor({ abnMetrics }) {
browser.webRequest.onCompleted.addListener(
async (details) => {
await abnMetrics.remoteConfig.ready;
const url = new URL(details.url);
if (url.pathname.startsWith('/t/epbf_')) {
// broken site report
abnMetrics.onMetricTriggered('brokenSiteReport');
}
},
{
urls: ['https://improving.duckduckgo.com/t/*'],
tabId: -1,
},
);
}
}

/**
* A/B/N testing framework: metrics.
*
Expand Down
119 changes: 119 additions & 0 deletions shared/js/background/components/message-router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import browser from 'webextension-polyfill';

import messageHandlers from '../message-handlers';
import { getExtensionId } from '../wrapper';
import { getBrowserName } from '../utils';
import { getActivePort, setActivePort } from '../popup-messaging';

/**
* @typedef {import('webextension-polyfill').Runtime.Port} Port
* @typedef {import('@duckduckgo/privacy-dashboard/schema/__generated__/schema.types').IncomingExtensionMessage} OutgoingPopupMessage
*/

export class MessageReceivedEvent extends CustomEvent {
constructor(msg) {
super('messageReceived', { detail: msg });
}

get messageType() {
return this.detail.messageType;
}
}

export default class MessageRouter extends EventTarget {
constructor({ tabManager }) {
super();
const browserName = getBrowserName();
// Handle popup UI (aka privacy dashboard) messaging.
browser.runtime.onConnect.addListener((port) => {
if (port.name === 'privacy-dashboard') {
this.popupConnectionOpened(port);
}
});

// Handle any messages that come from content/UI scripts
browser.runtime.onMessage.addListener((req, sender) => {
if (sender.id !== getExtensionId()) return;

// TODO clean up message passing
const legacyMessageTypes = [
'addUserData',
'getUserData',
'removeUserData',
'getEmailProtectionCapabilities',
'getAddresses',
'refreshAlias',
'debuggerMessage',
];
for (const legacyMessageType of legacyMessageTypes) {
if (legacyMessageType in req) {
req.messageType = legacyMessageType;
req.options = req[legacyMessageType];
}
}

if (req.registeredTempAutofillContentScript) {
req.messageType = 'registeredContentScript';
}

if (req.messageType && req.messageType in messageHandlers) {
this.dispatchEvent(new MessageReceivedEvent(req));
return Promise.resolve(messageHandlers[req.messageType](req.options, sender, req));
}

// Count refreshes per page
if (req.pageReloaded && sender.tab !== undefined) {
const tab = tabManager.get({ tabId: sender.tab.id });
if (tab) {
tab.userRefreshCount += 1;
}
return;
}

// TODO clean up legacy onboarding messaging
if (browserName === 'chrome') {
if (req === 'healthCheckRequest' || req === 'rescheduleCounterMessagingRequest') {
return;
}
}

console.error('Unrecognized message to background:', req, sender);
});
}

/**
* Set up the messaging connection with the popup UI.
*
* Note: If the ServiceWorker dies while there is an open connection, the popup
* will take care of re-opening the connection.
*
* @param {Port} port
*/
popupConnectionOpened(port) {
setActivePort(port);
port.onDisconnect.addListener(() => {
if (getActivePort() === port) {
setActivePort(null);
}
});

port.onMessage.addListener(async (message) => {
const messageType = message?.messageType;

if (!messageType || !(messageType in messageHandlers)) {
console.error('Unrecognized message (privacy-dashboard -> background):', message);
return;
}
this.dispatchEvent(new MessageReceivedEvent(message));

const response = await messageHandlers[messageType](message?.options, port, message);
if (typeof message?.id === 'number') {
port.postMessage({
id: message.id,
messageType: 'response',
options: response,
});
}
});
}
}
6 changes: 5 additions & 1 deletion shared/js/background/components/tab-tracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { isRedirect } from '../utils';
* @typedef {import('../tab-manager.js')} TabManager
*/

export default class TabTracker {
export default class TabTracker extends EventTarget {
/**
* @param {{
* tabManager: TabManager;
* devtools: Devtools;
* }} options
*/
constructor({ tabManager, devtools }) {
super();
this.tabManager = tabManager;
this.createdTargets = new Map();

Expand Down Expand Up @@ -68,6 +69,9 @@ export default class TabTracker {

const currentTab = tabManager.get({ tabId: details.tabId });
const newTab = tabManager.create({ tabId: details.tabId, url: details.url });
if (currentTab && currentTab.site.url === details.url) {
this.dispatchEvent(new CustomEvent('tabRefresh', { detail: details }));
}

if (BUILD_TARGET === 'chrome') {
// Ensure that the correct declarativeNetRequest allowing rules are
Expand Down
12 changes: 5 additions & 7 deletions shared/js/background/components/tds.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ResourceLoader from './resource-loader.js';
import constants from '../../../data/constants';
import { generateRetentionMetrics } from './abn-experiments.js';
import { generateBreakageMetrics } from '../metrics.js';

/**
* @typedef {import('../settings.js')} Settings
Expand Down Expand Up @@ -41,7 +42,9 @@ export default class TDSStorage {
},
{ settings },
);
this.remoteConfig.onUpdate(this.checkShouldOverrideTDS.bind(this));
this.remoteConfig.onUpdate(() => {
setTimeout(this.checkShouldOverrideTDS.bind(this), 1);
});
}

ready() {
Expand Down Expand Up @@ -79,12 +82,7 @@ export default class TDSStorage {
}
this.abnMetrics.markExperimentEnrolled(CONTENT_BLOCKING, subFeatureName, [
...generateRetentionMetrics(),
{
metric: 'brokenSiteReport',
conversionWindowStart: 0,
conversionWindowEnd: 14,
value: 1,
},
...generateBreakageMetrics(),
]);
} else if (this.settings.getSetting(TDS_OVERRIDE_SETTINGS_KEY)) {
// User removed from experiment/rollout, reset TDS override and fetch default list
Expand Down
2 changes: 1 addition & 1 deletion shared/js/background/components/toggle-reports.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import browser from 'webextension-polyfill';
import { registerMessageHandler } from '../message-handlers';
import { postPopupMessage } from '../popupMessaging';
import { postPopupMessage } from '../popup-messaging';
import settings from '../settings';
import { getFeatureSettings, reloadCurrentTab, resolveAfterDelay } from '../utils';
import { getDisclosureDetails, sendBreakageReportForCurrentTab } from '../broken-site-report';
Expand Down
Loading

0 comments on commit 7c9aa12

Please sign in to comment.