Skip to content

Commit

Permalink
FXVPN-12 Add a telemetry controller to the background script (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
strseb authored Dec 16, 2024
1 parent 202e9d6 commit 308d83b
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/background/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { expose } from "../shared/ipc.js";
import { TabReloader } from "./tabReloader.js";
import { ConflictObserver } from "./conflictObserver.js";
import { ButterBarService } from "./butterBarService.js";
import { Telemetry } from "./telemetry.js";

const log = Logger.logger("Main");

Expand All @@ -33,6 +34,11 @@ class Main {
this.extController,
this.proxyHandler
);
telemetry = new Telemetry(
this.vpnController,
this.extController,
this.proxyHandler
);
tabHandler = new TabHandler(
this,
this.extController,
Expand Down Expand Up @@ -62,6 +68,7 @@ class Main {
expose(this.proxyHandler);
expose(this.conflictObserver);
expose(this.butterBarService);
expose(this.telemetry);

this.#handlingEvent = false;
this.#processPendingEvents();
Expand Down
106 changes: 106 additions & 0 deletions src/background/telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* 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 { PropertyType } from "../shared/ipc.js";
import { computed, IBindable, property } from "../shared/property.js";
import { ExtensionController } from "./extensionController/extensionController.js";
import { ProxyHandler } from "./proxyHandler/proxyHandler.js";
import { SiteContext } from "./proxyHandler/siteContext.js";
import { VPNController } from "./vpncontroller/vpncontroller.js";

//@ts-check

/**
* ConflictCheck checks for
*/
export class Telemetry {
// Things to expose to the UI
static properties = {
setTelemetryEnabled: PropertyType.Function,
record: PropertyType.Function,
};
/** @type { IBindable<Boolean>}*/
telemetryEnabled = property(false);

/**
* @param {VPNController} controller
* @param {ExtensionController} extensionController
* @param {ProxyHandler} proxyHandler
*/
constructor(controller, extensionController, proxyHandler) {
this.#controller = controller;
this.telemetryEnabled = computed(controller.settings, (vpnSettings) => {
return vpnSettings.extensionTelemetryEnabled;
});
extensionController.state.subscribe((state) => {
if (state.enabled == this.#enabled) {
return;
}
this.record("fx_protection_mode_changed", {
message_state: state.name,
});
this.#enabled = state.enabled;
this.#enabled ? this.startSession() : this.stopSession();
});
proxyHandler.siteContexts.subscribe((ctxMap) => {
const counts = Telemetry.evaluateSiteContexts(ctxMap);
this.record("count_excluded", counts.excluded);
this.record("count_geoprefed", counts.geoPrefed);
});
}
setTelemetryEnabled(enabled) {
this.#controller.postToApp("settings", {
settings: {
extensionTelemetryEnabled: enabled,
},
});
}
record(eventName = "", data) {
if (!this.telemetryEnabled.value) {
// Don't send telemetry, unless we're sure we're enabled.
return;
}
if (eventName == "") {
return;
}
this.#controller.postToApp("telemetry", {
name: eventName,
args: data,
});
}
startSession() {
this.#controller.postToApp("start_session");
}
stopSession() {
this.#controller.postToApp("stop_session");
}

#controller;
#enabled = false;

/**
* Consumes a size Context Map and returns how many pages
* are either geopreffed to a location and how many are excluded
* @param {Map<string,SiteContext>} contextMap
*/
static evaluateSiteContexts(contextMap) {
let v;
try {
v = contextMap.values();
} catch (error) {
v = Object.values(contextMap);
}

return v.reduce(
(acc, curr) => {
curr.excluded ? acc.excluded++ : acc.geoPrefed++;
return acc;
},
{
excluded: 0,
geoPrefed: 0,
}
);
}
}
1 change: 0 additions & 1 deletion src/components/vpncard.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { resetSizing, fontStyling, positioner, inCopyLink } from "./styles.js";

import { VPNState } from "../background/vpncontroller/states.js";
import "./mz-rings.js";

import "./mz-pill.js";

/**
Expand Down
218 changes: 218 additions & 0 deletions tests/jest/background/telemetry.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/* 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 wtesth this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { beforeEach, describe, expect, test, jest } from "@jest/globals";
import { property } from "../../../src/shared/property";
import { VPNSettings } from "../../../src/background/vpncontroller";
import { Telemetry } from "../../../src/background/telemetry";
import { SiteContext } from "../../../src/background/proxyHandler";
import {
FirefoxVPNState,
StateFirefoxVPNConnecting,
StateFirefoxVPNDisabled,
StateFirefoxVPNEnabled,
StateFirefoxVPNIdle,
} from "../../../src/background/extensionController";

// Mock the browser API
const mocksendMessage = jest.fn();

const controller = {
settings: property(new VPNSettings()),
postToApp: mocksendMessage,
};

const extensionController = {
state: property(new FirefoxVPNState()),
};
const proxyHandler = {
siteContexts: property(new Map()),
};

describe("Telemetry", () => {
beforeEach(() => {
mocksendMessage.mockReset();
controller.settings = property(new VPNSettings());
extensionController.state = property(new FirefoxVPNState());
proxyHandler.siteContexts = property(new Map());
});

describe("telemetryEnabled", () => {
it("Reacts to changes from the VPNController", async () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
expect(target.telemetryEnabled.value).toBe(
controller.settings.value.extensionTelemetryEnabled
);
// Now let's "fake" that the underlying settings changed.
controller.settings.value = {
...controller.settings.value,
extensionTelemetryEnabled: true,
};
expect(target.telemetryEnabled.value).toBe(true);
});
it("It will send changes to the VPNController", async () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
expect(target.telemetryEnabled.value).toBe(
controller.settings.value.extensionTelemetryEnabled
);
// Now let's "fake" that the underlying settings changed.
const newvalue = !target.telemetryEnabled.value;
target.setTelemetryEnabled(newvalue);
expect(mocksendMessage).toBeCalledWith("settings", {
settings: {
extensionTelemetryEnabled: newvalue,
},
});
});
});

describe("record", () => {
beforeEach(() => {
mocksendMessage.mockReset();
// Make sure to default telemetry = on
const setting = new VPNSettings();
setting.extensionTelemetryEnabled = true;
controller.settings = property(setting);
});

it("Does not send data when telemetry is disabled", () => {
controller.settings.set({ extensionTelemetryEnabled: false });
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
target.record("THIS SHOULD NOT BE SENT");
expect(mocksendMessage).toBeCalledTimes(0);
});
it("Does not send data when there is no event", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
target.record();
expect(mocksendMessage).toBeCalledTimes(0);
});
it("Does send event data to the controller", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
target.record("hello_event", 43);
expect(mocksendMessage).toBeCalledWith("telemetry", {
name: "hello_event",
args: 43,
});
});
});

describe("evaluateSiteContexts", () => {
it("returns the correct amount of values", () => {
const data = new Map();
data.set(
"a",
new SiteContext({
excluded: true,
})
);
data.set(
"b",
new SiteContext({
excluded: true,
})
);
data.set(
"c",
new SiteContext({
excluded: false,
})
);
const res = Telemetry.evaluateSiteContexts(data);
expect(res.excluded).toBe(2);
expect(res.geoPrefed).toBe(1);
});
});

describe("sessions", () => {
it("Will start a session when the ExtensionController is 'enabled'", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
extensionController.state.set(new StateFirefoxVPNEnabled(true, 0));
expect(mocksendMessage).toBeCalledWith("start_session");
});
it("Will *NOT* start a session when switching from partial to full", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
extensionController.state.set(new StateFirefoxVPNEnabled(true, 0));
expect(mocksendMessage).toBeCalledWith("start_session");
mocksendMessage.mockReset();
extensionController.state.set(new StateFirefoxVPNEnabled(false, 0));
expect(mocksendMessage).not.toBeCalled();
});
it("Will stop a session when switching from started -> stopped", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
extensionController.state.set(new StateFirefoxVPNEnabled(true, 0));
expect(mocksendMessage).toBeCalledWith("start_session");
mocksendMessage.mockReset();
extensionController.state.set(new StateFirefoxVPNDisabled(false));
expect(mocksendMessage).toBeCalledWith("stop_session");
});
it("Will ignore idle/connecting", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
// in those states, nothing may be sent!
[new StateFirefoxVPNConnecting(), new StateFirefoxVPNIdle()].forEach(
(s) => {
extensionController.state.set(s);
expect(mocksendMessage).not.toBeCalled();
}
);
});
});

describe("proxyHandler", () => {
beforeEach(() => {
// Make sure to default telemetry = on
const setting = new VPNSettings();
setting.extensionTelemetryEnabled = true;
controller.settings = property(setting);
});
it("Will record changes to the sitecontext list", () => {
const target = new Telemetry(
controller,
extensionController,
proxyHandler
);
const m = new Map();
m.set("a", new SiteContext({ excluded: true }));
m.set("b", new SiteContext({ excluded: false }));
m.set("c", new SiteContext({ excluded: true }));
proxyHandler.siteContexts.set(m);
expect(mocksendMessage).toBeCalledTimes(2);
});
});
});

0 comments on commit 308d83b

Please sign in to comment.