-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FXVPN-12 Add a telemetry controller to the background script (#136)
- Loading branch information
Showing
4 changed files
with
331 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |