зеркало из https://github.com/mozilla/glean.js.git
Bug 1689547 - Provide a ping encryption plugin (#96)
This commit is contained in:
Родитель
d5e953e792
Коммит
2c1481d612
|
@ -2,6 +2,12 @@
|
|||
|
||||
[Full changelog](https://github.com/mozilla/glean.js/compare/v0.4.0...main)
|
||||
|
||||
* [#96](https://github.com/mozilla/glean.js/pull/96): Provide a ping encryption plugin.
|
||||
* This plugin listens to the `afterPingCollection` event. It receives the collected payload of a ping and returns an encrypted version of it using a JWK provided upon instantiation.
|
||||
* [#95](https://github.com/mozilla/glean.js/pull/95): Add a `plugins` property to the configuration options and create an event abstraction for triggering internal Glean events.
|
||||
* The only internal event triggered at this point is the `afterPingCollection` event, which is triggered after ping collection and logging, and before ping storing.
|
||||
* Plugins are built to listen to a specific Glean event. Each plugin must define an `action`, which is executed everytime the event they are listening to is triggered.
|
||||
* [#101](https://github.com/mozilla/glean.js/pull/101): BUGFIX: Only validate Debug View Tag and Source Tags when they are present.
|
||||
* [#101](https://github.com/mozilla/glean.js/pull/101): BUGFIX: Only validate Debug View Tag and Source Tags when they are present.
|
||||
* [#102](https://github.com/mozilla/glean.js/pull/102): BUGFIX: Include a Glean User-Agent header in all pings.
|
||||
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
],
|
||||
"rules": {
|
||||
"mocha/no-skipped-tests": "off",
|
||||
"mocha/no-pending-tests": "off"
|
||||
"mocha/no-pending-tests": "off",
|
||||
"mocha/no-setup-in-describe": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.4.0",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"jose": "^3.7.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2863,6 +2864,14 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-3.7.0.tgz",
|
||||
"integrity": "sha512-9vAWSk4zmWH60zbubOZ5Rju3bO6rr5eOUjxuU8snB17Fl2fc7ytFQ6AjtDk8vLzHvecNMj2F4yFkAyurJTpuww==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -7967,6 +7976,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"jose": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-3.7.0.tgz",
|
||||
"integrity": "sha512-9vAWSk4zmWH60zbubOZ5Rju3bO6rr5eOUjxuU8snB17Fl2fc7ytFQ6AjtDk8vLzHvecNMj2F4yFkAyurJTpuww=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
"browser": "./dist/webext/browser/core/pings/index.js",
|
||||
"import": "./dist/webext/esm/core/pings/index.js",
|
||||
"require": "./dist/webext/cjs/core/pings/index.js"
|
||||
},
|
||||
"./webext/plugins/*": {
|
||||
"browser": "./dist/webext/browser/plugins/*.js",
|
||||
"import": "./dist/webext/esm/plugins/*.js",
|
||||
"require": "./dist/webext/cjs/plugins/*.js"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
|
@ -30,6 +35,9 @@
|
|||
],
|
||||
"webext/private/metrics/*": [
|
||||
"./dist/webext/types/core/metrics/types/*"
|
||||
],
|
||||
"webext/plugins/*": [
|
||||
"./dist/webext/types/plugins/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -39,8 +47,9 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:core && npm run test:platform",
|
||||
"test": "npm run test:core && npm run test:platform && npm run test:plugins",
|
||||
"test:core": "ts-mocha \"tests/core/**/*.spec.ts\" --recursive",
|
||||
"test:plugins": "ts-mocha \"tests/plugins/**/*.spec.ts\" --recursive",
|
||||
"test:platform": "npm run build:test-webext && ts-mocha \"tests/platform/**/*.spec.ts\" --recursive --timeout 0",
|
||||
"build:test-webext": "cd tests/platform/utils/webext/sample/ && npm install && npm run build:xpi",
|
||||
"lint": "eslint . --ext .ts,.js,.json --max-warnings=0",
|
||||
|
@ -96,6 +105,7 @@
|
|||
"webpack-cli": "^4.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jose": "^3.7.0",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,53 +12,57 @@ export class CoreEvent<
|
|||
Context extends unknown[] = unknown[],
|
||||
// The expected type of the action result. To be returned by the plugin.
|
||||
Result extends unknown = unknown
|
||||
> {
|
||||
// The plugin to be triggered eveytime this even occurs.
|
||||
private plugin?: Plugin<CoreEvent<Context, Result>>;
|
||||
> {
|
||||
// The plugin to be triggered eveytime this even occurs.
|
||||
private plugin?: Plugin<CoreEvent<Context, Result>>;
|
||||
|
||||
constructor(readonly name: string) {}
|
||||
constructor(readonly name: string) {}
|
||||
|
||||
/**
|
||||
* Registers a plugin that listens to this event.
|
||||
*
|
||||
* @param plugin The plugin to register.
|
||||
*/
|
||||
registerPlugin(plugin: Plugin<CoreEvent<Context, Result>>): void {
|
||||
if (this.plugin) {
|
||||
console.error(
|
||||
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
|
||||
`That event is already watched by plugin '${this.plugin.name}'`,
|
||||
`Plugin '${plugin.name}' will be ignored.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters the currently registered plugin.
|
||||
*
|
||||
* If no plugin is currently registered this is a no-op.
|
||||
*/
|
||||
deregisterPlugin(): void {
|
||||
this.plugin = undefined;
|
||||
}
|
||||
get registeredPluginIdentifier(): string | undefined {
|
||||
return this.plugin?.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers this event.
|
||||
*
|
||||
* Will execute the action of the registered plugin, if there is any.
|
||||
*
|
||||
* @param args The arguments to be passed as context to the registered plugin.
|
||||
*
|
||||
* @returns The result from the plugin execution.
|
||||
*/
|
||||
trigger(...args: Context): Result | void {
|
||||
if (this.plugin) {
|
||||
return this.plugin.action(...args);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Registers a plugin that listens to this event.
|
||||
*
|
||||
* @param plugin The plugin to register.
|
||||
*/
|
||||
registerPlugin(plugin: Plugin<CoreEvent<Context, Result>>): void {
|
||||
if (this.plugin) {
|
||||
console.error(
|
||||
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
|
||||
`That event is already watched by plugin '${this.plugin.name}'`,
|
||||
`Plugin '${plugin.name}' will be ignored.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters the currently registered plugin.
|
||||
*
|
||||
* If no plugin is currently registered this is a no-op.
|
||||
*/
|
||||
deregisterPlugin(): void {
|
||||
this.plugin = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers this event.
|
||||
*
|
||||
* Will execute the action of the registered plugin, if there is any.
|
||||
*
|
||||
* @param args The arguments to be passed as context to the registered plugin.
|
||||
*
|
||||
* @returns The result from the plugin execution.
|
||||
*/
|
||||
trigger(...args: Context): Result | void {
|
||||
if (this.plugin) {
|
||||
return this.plugin.action(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -209,7 +209,7 @@ export async function collectPing(ping: PingType, reason?: string): Promise<Ping
|
|||
*
|
||||
* @returns The final submission path.
|
||||
*/
|
||||
function makePath(identifier: string, ping: PingType): string {
|
||||
export function makePath(identifier: string, ping: PingType): string {
|
||||
// We are sure that the applicationId is not `undefined` at this point,
|
||||
// this function is only called when submitting a ping
|
||||
// and that function return early when Glean is not initialized.
|
||||
|
@ -237,15 +237,25 @@ export async function collectAndStorePing(identifier: string, ping: PingType, re
|
|||
return;
|
||||
}
|
||||
|
||||
let modifiedPayload;
|
||||
try {
|
||||
modifiedPayload = await CoreEvents.afterPingCollection.trigger(collectedPayload);
|
||||
} catch(e) {
|
||||
console.error(
|
||||
`Error while attempting to modify ping payload for the "${ping.name}" ping using`,
|
||||
`the ${JSON.stringify(CoreEvents.afterPingCollection.registeredPluginIdentifier)} plugin.`,
|
||||
"Ping will not be submitted. See more logs below.\n\n",
|
||||
e
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (Glean.logPings) {
|
||||
console.info(JSON.stringify(collectedPayload, null, 2));
|
||||
}
|
||||
|
||||
const headers = getPingHeaders();
|
||||
|
||||
const modifiedPayload = await CoreEvents.afterPingCollection.trigger(collectedPayload);
|
||||
const finalPayload = modifiedPayload ? modifiedPayload : collectedPayload;
|
||||
|
||||
const headers = getPingHeaders();
|
||||
return Glean.pingsDatabase.recordPing(
|
||||
makePath(identifier, ping),
|
||||
identifier,
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/* 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 CompactEncrypt from "jose/jwe/compact/encrypt";
|
||||
import parseJwk from "jose/jwk/parse";
|
||||
import calculateThumbprint from "jose/jwk/thumbprint";
|
||||
import { JWK } from "jose/types";
|
||||
|
||||
import Plugin from "./index";
|
||||
import { PingPayload } from "../core/pings/database";
|
||||
import { JSONObject } from "../core/utils";
|
||||
import CoreEvents from "../core/events";
|
||||
|
||||
// These are the chosen defaults, because they are the ones expected by Glean's data pipeline.
|
||||
//
|
||||
// That is the case because they are the only algorithm and content encoding pair supported
|
||||
// by Firefox's hand-rolled JWE implementation.
|
||||
// See: https://searchfox.org/mozilla-central/rev/eeb8cf278192d68b3977d0adb4d43f1463439269/services/crypto/modules/jwcrypto.jsm#58-74
|
||||
const JWE_ALGORITHM = "ECDH-ES";
|
||||
const JWE_CONTENT_ENCODING = "A256GCM";
|
||||
|
||||
/**
|
||||
* A plugin that listens for the `afterPingCollection` event and encrypts **all** outgoing pings
|
||||
* with the JWK provided upon initialization.
|
||||
*
|
||||
* This plugin will modify the schema of outgoing pings to:
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* payload: "<encrypted-payload>"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class PingEncryptionPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
|
||||
/**
|
||||
* Creates a new PingEncryptionPlugin instance.
|
||||
*
|
||||
* @param jwk The JWK that will be used to encode outgoing ping payloads.
|
||||
*/
|
||||
constructor(private jwk: JWK) {
|
||||
super(CoreEvents["afterPingCollection"].name, "pingEncryptionPlugin");
|
||||
}
|
||||
|
||||
async action(payload: PingPayload): Promise<JSONObject> {
|
||||
const key = await parseJwk(this.jwk, JWE_ALGORITHM);
|
||||
const encoder = new TextEncoder();
|
||||
const encodedPayload = await new CompactEncrypt(encoder.encode(JSON.stringify(payload)))
|
||||
.setProtectedHeader({
|
||||
kid: await calculateThumbprint(this.jwk),
|
||||
alg: JWE_ALGORITHM,
|
||||
enc: JWE_CONTENT_ENCODING,
|
||||
typ: "JWE",
|
||||
})
|
||||
.encrypt(key);
|
||||
return { payload: encodedPayload };
|
||||
}
|
||||
}
|
||||
|
||||
export default PingEncryptionPlugin;
|
|
@ -15,7 +15,6 @@ import { JSONObject } from "../../src/core/utils";
|
|||
import TestPlatform from "../../src/platform/qt";
|
||||
import Plugin from "../../src/plugins";
|
||||
|
||||
const GLOBAL_APPLICATION_ID = "org.mozilla.glean.test.app";
|
||||
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
|
||||
constructor() {
|
||||
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
|
||||
|
@ -29,8 +28,10 @@ class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
|
|||
const sandbox = sinon.createSandbox();
|
||||
|
||||
describe("Glean", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean(GLOBAL_APPLICATION_ID);
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -45,7 +46,7 @@ describe("Glean", function() {
|
|||
await Glean["coreMetrics"]["firstRunDate"].testGetValue(CLIENT_INFO_STORAGE), undefined);
|
||||
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
assert.ok(await Glean["coreMetrics"]["clientId"].testGetValue(CLIENT_INFO_STORAGE));
|
||||
assert.ok(await Glean["coreMetrics"]["firstRunDate"].testGetValue(CLIENT_INFO_STORAGE));
|
||||
});
|
||||
|
@ -123,7 +124,7 @@ describe("Glean", function() {
|
|||
|
||||
it("client_id is set to known value when uploading disabled at start", async function() {
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
|
||||
await Glean.testInitialize(testAppId, false);
|
||||
assert.strictEqual(
|
||||
await Glean["coreMetrics"]["clientId"].testGetValue(CLIENT_INFO_STORAGE),
|
||||
KNOWN_CLIENT_ID
|
||||
|
@ -133,7 +134,7 @@ describe("Glean", function() {
|
|||
it("client_id is set to random value when uploading enabled at start", async function() {
|
||||
Glean.setUploadEnabled(false);
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
const clientId = await Glean["coreMetrics"]["clientId"]
|
||||
.testGetValue(CLIENT_INFO_STORAGE);
|
||||
assert.ok(clientId);
|
||||
|
@ -170,7 +171,7 @@ describe("Glean", function() {
|
|||
it("initialization throws if serverEndpoint is an invalida URL", async function() {
|
||||
await Glean.testUninitialize();
|
||||
try {
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { serverEndpoint: "" });
|
||||
await Glean.testInitialize(testAppId, true, { serverEndpoint: "" });
|
||||
assert.ok(false);
|
||||
} catch {
|
||||
assert.ok(true);
|
||||
|
@ -181,7 +182,7 @@ describe("Glean", function() {
|
|||
await Glean.testUninitialize();
|
||||
|
||||
const mockPlugin = new MockPlugin();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, {
|
||||
await Glean.testInitialize(testAppId, true, {
|
||||
// We need to ignore TypeScript here,
|
||||
// otherwise it will error since mockEvent is not listed as a Glean event in core/events.ts
|
||||
//
|
||||
|
@ -193,7 +194,7 @@ describe("Glean", function() {
|
|||
assert.deepStrictEqual(CoreEvents["afterPingCollection"]["plugin"], mockPlugin);
|
||||
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
assert.strictEqual(CoreEvents["afterPingCollection"]["plugin"], undefined);
|
||||
});
|
||||
|
||||
|
@ -221,12 +222,12 @@ describe("Glean", function() {
|
|||
|
||||
it("initializing twice is a no-op", async function() {
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
// initialize is dispatched, we must await on the queue being completed to assert things.
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
|
||||
// This time it should not be called, which means upload should not be switched to `false`.
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
|
||||
await Glean.testInitialize(testAppId, false);
|
||||
assert.ok(Glean.isUploadEnabled());
|
||||
});
|
||||
|
||||
|
@ -241,7 +242,7 @@ describe("Glean", function() {
|
|||
const postSpy = sandbox.spy(Glean.platform.uploader, "post");
|
||||
|
||||
// Start Glean with upload enabled.
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
// Immediatelly disable upload.
|
||||
Glean.setUploadEnabled(false);
|
||||
ping.submit();
|
||||
|
@ -280,7 +281,7 @@ describe("Glean", function() {
|
|||
// Can't use testResetGlean here because it clears all stores
|
||||
// and when there is no client_id at all stored, a deletion ping is also not sent.
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
|
||||
await Glean.testInitialize(testAppId, false);
|
||||
|
||||
// TODO: Make this nicer once Bug 1691033 is resolved.
|
||||
await Glean["pingUploader"]["currentJob"];
|
||||
|
@ -303,7 +304,7 @@ describe("Glean", function() {
|
|||
// Can't use testResetGlean here because it clears all stores
|
||||
// and when there is no client_id at all stored, a deletion ping is also not set.
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
|
||||
await Glean.testInitialize(testAppId, false);
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
// TODO: Make this nicer once we resolve Bug 1691033 is resolved.
|
||||
await Glean["pingUploader"]["currentJob"];
|
||||
|
@ -314,7 +315,7 @@ describe("Glean", function() {
|
|||
|
||||
it("deletion request ping is not sent when user starts Glean for the first time with upload disabled", async function () {
|
||||
const postSpy = sandbox.spy(Glean.platform.uploader, "post");
|
||||
await Glean.testResetGlean(GLOBAL_APPLICATION_ID, false);
|
||||
await Glean.testResetGlean(testAppId, false);
|
||||
assert.strictEqual(postSpy.callCount, 0);
|
||||
});
|
||||
|
||||
|
@ -322,7 +323,7 @@ describe("Glean", function() {
|
|||
await Glean.testUninitialize();
|
||||
|
||||
// Setting on initialize.
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { debug: { logPings: true } });
|
||||
await Glean.testInitialize(testAppId, true, { debug: { logPings: true } });
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
assert.ok(Glean.logPings);
|
||||
|
||||
|
@ -330,7 +331,7 @@ describe("Glean", function() {
|
|||
|
||||
// Setting before initialize.
|
||||
Glean.setLogPings(true);
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
assert.ok(Glean.logPings);
|
||||
|
||||
|
@ -345,7 +346,7 @@ describe("Glean", function() {
|
|||
const testTag = "test";
|
||||
|
||||
// Setting on initialize.
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { debug: { debugViewTag: testTag } });
|
||||
await Glean.testInitialize(testAppId, true, { debug: { debugViewTag: testTag } });
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
assert.strictEqual(Glean.debugViewTag, testTag);
|
||||
|
||||
|
@ -353,7 +354,7 @@ describe("Glean", function() {
|
|||
|
||||
// Setting before initialize.
|
||||
Glean.setDebugViewTag(testTag);
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
|
||||
await Glean.testInitialize(testAppId, true);
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
assert.strictEqual(Glean.debugViewTag, testTag);
|
||||
|
||||
|
@ -386,7 +387,7 @@ describe("Glean", function() {
|
|||
|
||||
it("setting source tags on initialize works", async function () {
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { debug: { sourceTags: ["1", "2", "3", "4", "5"] } });
|
||||
await Glean.testInitialize(testAppId, true, { debug: { sourceTags: ["1", "2", "3", "4", "5"] } });
|
||||
await Glean.dispatcher.testBlockOnQueue();
|
||||
assert.strictEqual(Glean.sourceTags, "1,2,3,4,5");
|
||||
});
|
||||
|
|
|
@ -9,8 +9,10 @@ import BooleanMetricType from "../../../src/core/metrics/types/boolean";
|
|||
import { Lifetime } from "../../../src/core/metrics";
|
||||
|
||||
describe("BooleanMetric", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {
|
||||
|
|
|
@ -7,10 +7,12 @@ import assert from "assert";
|
|||
import Glean from "../../../src/core/glean";
|
||||
import CounterMetricType from "../../../src/core/metrics/types/counter";
|
||||
import { Lifetime } from "../../../src/core/metrics";
|
||||
|
||||
|
||||
describe("CounterMetric", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {
|
||||
|
|
|
@ -11,8 +11,10 @@ import { JSONValue } from "../../../src/core/utils";
|
|||
import Glean from "../../../src/core/glean";
|
||||
|
||||
describe("MetricsDatabase", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
describe("record", function() {
|
||||
|
@ -483,9 +485,6 @@ describe("MetricsDatabase", function() {
|
|||
});
|
||||
|
||||
it("clears data from specific ping when specified", async function () {
|
||||
// Must initialize Glean, otherwise `testGetValue` will hang forever.
|
||||
await Glean.testResetGlean("something something", true);
|
||||
|
||||
const metric = new StringMetricType({
|
||||
category: "some",
|
||||
name: "metric",
|
||||
|
|
|
@ -13,12 +13,14 @@ import TimeUnit from "../../../src/core/metrics/time_unit";
|
|||
const sandbox = sinon.createSandbox();
|
||||
|
||||
describe("DatetimeMetric", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("datetime internal representation validation works as expected", function () {
|
||||
|
|
|
@ -18,8 +18,10 @@ class TestEventMetricType extends EventMetricType {
|
|||
}
|
||||
|
||||
describe("EventMetric", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("the API records to its storage engine", async function () {
|
||||
|
|
|
@ -11,8 +11,10 @@ import EventMetricType from "../../../src/core/metrics/types/event";
|
|||
import { JSONObject } from "../../../src/core/utils";
|
||||
|
||||
describe("EventsDatabase", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("stable serialization", function () {
|
||||
|
|
|
@ -9,8 +9,10 @@ import StringMetricType, { MAX_LENGTH_VALUE } from "../../../src/core/metrics/ty
|
|||
import { Lifetime } from "../../../src/core/metrics";
|
||||
|
||||
describe("StringMetric", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {
|
||||
|
|
|
@ -10,8 +10,10 @@ import UUIDMetricType from "../../../src/core/metrics/types/uuid";
|
|||
import { Lifetime } from "../../../src/core/metrics";
|
||||
|
||||
describe("UUIDMetric", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {
|
||||
|
|
|
@ -8,8 +8,10 @@ import Database, { Observer, isValidPingInternalRepresentation } from "../../../
|
|||
import Glean from "../../../src/core/glean";
|
||||
|
||||
describe("PingsDatabase", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
describe("record", function () {
|
||||
|
|
|
@ -24,12 +24,14 @@ async function submitSync(ping: PingType): Promise<void> {
|
|||
}
|
||||
|
||||
describe("PingType", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("collects and stores ping on submit", async function () {
|
||||
|
|
|
@ -14,9 +14,21 @@ import { JSONObject } from "../../../src/core/utils";
|
|||
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
|
||||
constructor() {
|
||||
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
|
||||
}
|
||||
|
||||
action(): Promise<JSONObject> {
|
||||
return Promise.resolve({ "you": "got mocked!" });
|
||||
}
|
||||
}
|
||||
|
||||
describe("PingMaker", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -61,7 +73,7 @@ describe("PingMaker", function() {
|
|||
assert.ok("telemetry_sdk_build" in clientInfo1);
|
||||
|
||||
// Initialize will also initialize core metrics that are part of the client info.
|
||||
await Glean.testInitialize("something something", true, {
|
||||
await Glean.testInitialize(testAppId, true, {
|
||||
appBuild:"build",
|
||||
appDisplayVersion: "display version",
|
||||
serverEndpoint: "http://localhost:8080"
|
||||
|
@ -131,18 +143,7 @@ describe("PingMaker", function() {
|
|||
// Disable ping uploading for it not to interfere with this tests.
|
||||
sandbox.stub(Glean["pingUploader"], "triggerUpload").callsFake(() => Promise.resolve());
|
||||
|
||||
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
|
||||
constructor() {
|
||||
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
|
||||
}
|
||||
|
||||
action(): Promise<JSONObject> {
|
||||
return Promise.resolve({ "you": "got mocked!" });
|
||||
}
|
||||
}
|
||||
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize("something something", true, { plugins: [ new MockPlugin() ]});
|
||||
await Glean.testResetGlean(testAppId, true, { plugins: [ new MockPlugin() ]});
|
||||
const ping = new PingType({
|
||||
name: "ping",
|
||||
includeClientId: true,
|
||||
|
@ -153,10 +154,68 @@ describe("PingMaker", function() {
|
|||
const recordedPing = (await Glean.pingsDatabase.getAllPings())["ident"];
|
||||
assert.deepStrictEqual(recordedPing.payload, { "you": "got mocked!" });
|
||||
|
||||
await Glean.testUninitialize();
|
||||
await Glean.testInitialize("something something", true);
|
||||
await Glean.testResetGlean(testAppId, true);
|
||||
await PingMaker.collectAndStorePing("ident", ping);
|
||||
const recordedPingNoPlugin = (await Glean.pingsDatabase.getAllPings())["ident"];
|
||||
assert.notDeepStrictEqual(recordedPingNoPlugin.payload, { "you": "got mocked!" });
|
||||
});
|
||||
|
||||
it("ping payload is logged before it is modified by a plugin", async function () {
|
||||
// Disable ping uploading for it not to interfere with this tests.
|
||||
sandbox.stub(Glean["pingUploader"], "triggerUpload").callsFake(() => Promise.resolve());
|
||||
|
||||
await Glean.testResetGlean(testAppId, true, {
|
||||
debug: {
|
||||
logPings: true
|
||||
},
|
||||
plugins: [ new MockPlugin() ]
|
||||
});
|
||||
|
||||
const ping = new PingType({
|
||||
name: "ping",
|
||||
includeClientId: true,
|
||||
sendIfEmpty: true,
|
||||
});
|
||||
|
||||
const consoleSpy = sandbox.spy(console, "info");
|
||||
await PingMaker.collectAndStorePing("ident", ping);
|
||||
|
||||
const loggedPayload = JSON.parse(consoleSpy.lastCall.args[0]) as JSONObject;
|
||||
|
||||
const recordedPing = (await Glean.pingsDatabase.getAllPings())["ident"];
|
||||
assert.deepStrictEqual(recordedPing.payload, { "you": "got mocked!" });
|
||||
assert.notDeepStrictEqual(loggedPayload, { "you": "got mocked!" });
|
||||
assert.ok("client_info" in loggedPayload);
|
||||
assert.ok("ping_info" in loggedPayload);
|
||||
});
|
||||
|
||||
it("pings are not recorded in case a plugin throws", async function () {
|
||||
class ThrowingPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
|
||||
constructor() {
|
||||
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
|
||||
}
|
||||
|
||||
action(): Promise<JSONObject> {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
await Glean.testResetGlean(testAppId, true, {
|
||||
debug: {
|
||||
logPings: true
|
||||
},
|
||||
plugins: [ new ThrowingPlugin() ]
|
||||
});
|
||||
|
||||
const ping = new PingType({
|
||||
name: "ping",
|
||||
includeClientId: true,
|
||||
sendIfEmpty: true,
|
||||
});
|
||||
|
||||
await PingMaker.collectAndStorePing("ident", ping);
|
||||
|
||||
const recordedPings = await Glean.pingsDatabase.getAllPings();
|
||||
assert.ok(!("ident" in recordedPings));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -55,12 +55,14 @@ function disableGleanUploader(): void {
|
|||
}
|
||||
|
||||
describe("PingUploader", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean("something something");
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
it("scanning the pending pings directory fills up the queue", async function() {
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/* 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 assert from "assert";
|
||||
import sinon from "sinon";
|
||||
import generateKeyPair from "jose/util/generate_key_pair";
|
||||
import fromKeyLike from "jose/jwk/from_key_like";
|
||||
import compactDecrypt from "jose/jwe/compact/decrypt";
|
||||
|
||||
import Glean from "../../src/core/glean";
|
||||
import PingType from "../../src/core/pings";
|
||||
import { JSONObject } from "../../src/core/utils";
|
||||
import TestPlatform from "../../src/platform/qt";
|
||||
import PingEncryptionPlugin from "../../src/plugins/encryption";
|
||||
import collectAndStorePing, { makePath } from "../../src/core/pings/maker";
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
describe("PingEncryptionPlugin", function() {
|
||||
const testAppId = `gleanjs.test.${this.title}`;
|
||||
|
||||
beforeEach(async function() {
|
||||
await Glean.testResetGlean(testAppId);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it("collect and store triggers the AfterPingCollection and deals with possible result correctly", async function () {
|
||||
|
||||
await Glean.testResetGlean(
|
||||
testAppId,
|
||||
true,
|
||||
{
|
||||
plugins: [
|
||||
new PingEncryptionPlugin({
|
||||
"crv": "P-256",
|
||||
"kid": "test",
|
||||
"kty": "EC",
|
||||
"x": "Q20tsJdrryWJeuPXTM27wIPb_YbsdYPpkK2N9O6aXwM",
|
||||
"y": "1onW1swaCcN1jkmkIwhXpCm55aMP8GRJln5E8WQKLJk"
|
||||
})
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
const ping = new PingType({
|
||||
name: "test",
|
||||
includeClientId: true,
|
||||
sendIfEmpty: true,
|
||||
});
|
||||
const pingId = "ident";
|
||||
|
||||
const postSpy = sandbox.spy(TestPlatform.uploader, "post").withArgs(
|
||||
sinon.match(makePath(pingId, ping)),
|
||||
sinon.match.string
|
||||
);
|
||||
|
||||
await collectAndStorePing(pingId, ping);
|
||||
assert.ok(postSpy.calledOnce);
|
||||
|
||||
const payload = JSON.parse(postSpy.args[0][1]) as JSONObject;
|
||||
assert.ok("payload" in payload);
|
||||
assert.ok(typeof payload.payload === "string");
|
||||
});
|
||||
|
||||
it("decrypting encrypted ping returns expected payload and protected headers", async function () {
|
||||
// Disable ping uploading for it not to interfere with this tests.
|
||||
sandbox.stub(Glean["pingUploader"], "triggerUpload").callsFake(() => Promise.resolve());
|
||||
|
||||
const { publicKey, privateKey } = await generateKeyPair("ECDH-ES");
|
||||
|
||||
await Glean.testResetGlean(
|
||||
testAppId,
|
||||
true,
|
||||
{
|
||||
plugins: [
|
||||
new PingEncryptionPlugin(await fromKeyLike(publicKey))
|
||||
],
|
||||
debug: {
|
||||
logPings: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const ping = new PingType({
|
||||
name: "test",
|
||||
includeClientId: true,
|
||||
sendIfEmpty: true,
|
||||
});
|
||||
const pingId = "ident";
|
||||
|
||||
const consoleSpy = sandbox.spy(console, "info");
|
||||
await collectAndStorePing(pingId, ping);
|
||||
|
||||
const preEncryptionPayload = JSON.parse(consoleSpy.lastCall.args[0]) as JSONObject;
|
||||
const encryptedPayload = (await Glean.pingsDatabase.getAllPings())[pingId]["payload"]["payload"];
|
||||
|
||||
const { plaintext, protectedHeader } = await compactDecrypt(
|
||||
encryptedPayload as string,
|
||||
privateKey
|
||||
);
|
||||
const decoder = new TextDecoder();
|
||||
const decodedPayload = JSON.parse(decoder.decode(plaintext)) as JSONObject;
|
||||
|
||||
assert.deepStrictEqual(decodedPayload, preEncryptionPayload);
|
||||
assert.ok("kid" in protectedHeader);
|
||||
assert.ok("alg" in protectedHeader);
|
||||
assert.ok("enc" in protectedHeader);
|
||||
assert.ok("typ" in protectedHeader);
|
||||
});
|
||||
});
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"extends": "../base.json",
|
||||
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
|
||||
"include": [
|
||||
"../../src/index/webext.ts",
|
||||
"../../src/core/metrics/types/*.ts",
|
||||
"../../src/plugins/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "ES6",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"extends": "../base.json",
|
||||
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
|
||||
"include": [
|
||||
"../../src/index/webext.ts",
|
||||
"../../src/core/metrics/types/*.ts",
|
||||
"../../src/plugins/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "CommonJS",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"extends": "../base.json",
|
||||
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
|
||||
"include": [
|
||||
"../../src/index/webext.ts",
|
||||
"../../src/core/metrics/types/*.ts",
|
||||
"../../src/plugins/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "ES2020",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"extends": "../base.json",
|
||||
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
|
||||
"include": [
|
||||
"../../src/index/webext.ts",
|
||||
"../../src/core/metrics/types/*.ts",
|
||||
"../../src/plugins/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/webext/types",
|
||||
"removeComments": false,
|
||||
|
|
Загрузка…
Ссылка в новой задаче