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
/* The purpose of this class is to limit (cap) sending of DAP reports.
* The current DAP draft standard is available here:
import { DAPTelemetrySender } from "./DAPTelemetrySender.sys.mjs";
const DAY_IN_MILLI = 1000 * 60 * 60 * 24;
const MINUTE_IN_MILLI = 60 * 1000;
let lazy = {};
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
return console.createInstance({
prefix: "DAPReportController",
maxLogLevelPref: "toolkit.telemetry.dap.logLevel",
});
});
ChromeUtils.defineESModuleGetters(lazy, {
setTimeout: "resource://gre/modules/Timer.sys.mjs",
IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs",
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
});
const DB_NAME = "SubmissionCap";
const DB_VERSION = 1;
const FREQ_CAP_STORE = "freq_caps";
const REPORT_STORE = "reports";
export class Task {
constructor({ taskId, vdaf, bits, length, defaultMeasurement }) {
this._taskId = taskId;
this._vdaf = vdaf;
this._bits = bits;
this._length = length;
this._defaultMeasurement = defaultMeasurement;
}
}
export class DAPReportController {
constructor({ tasks, options, DateNowFn = Date.now } = {}) {
this._tasks = tasks;
this._windowDays = options.windowDays;
this._submissionTimerMins = options.submissionIntervalMins;
this._timerId = null;
this._now = DateNowFn;
}
get db() {
return this._db || (this._db = this.#createOrOpenDb());
}
async #createOrOpenDb() {
try {
return await this.#openDatabase();
} catch {
throw new Error("DAPVisitCounter unable to load database.");
}
}
async #openDatabase() {
return await lazy.IndexedDB.open(DB_NAME, DB_VERSION, db => {
if (!db.objectStoreNames.contains(FREQ_CAP_STORE)) {
db.createObjectStore(FREQ_CAP_STORE, { keyPath: "taskId" });
}
if (!db.objectStoreNames.contains(REPORT_STORE)) {
db.createObjectStore(REPORT_STORE, { keyPath: "taskId" });
}
});
}
/* Clears a pending report and updates the freq cap data
*/
async #releasePendingReport(report) {
const tx = (await this.db).transaction(
[REPORT_STORE, FREQ_CAP_STORE],
"readwrite"
);
const reportStore = tx.objectStore(REPORT_STORE);
const capStore = tx.objectStore(FREQ_CAP_STORE);
await reportStore.delete(report.taskId);
let cap = {
taskId: report.taskId,
nextReset: this._now() + this._windowDays * DAY_IN_MILLI,
};
await capStore.put(cap);
await tx.done;
}
async getFreqCap(taskId) {
const tx = (await this.db).transaction(FREQ_CAP_STORE, "readonly");
const cap = await tx.objectStore(FREQ_CAP_STORE).get(taskId);
await tx.done;
return cap;
}
async recordMeasurement(taskId, measurement) {
try {
const cap = await this.getFreqCap(taskId);
if (cap === undefined || cap.nextReset < this._now()) {
const taskMetaData = this._tasks[taskId];
const report = {
taskId,
vdaf: taskMetaData._vdaf,
bits: taskMetaData._bits,
length: taskMetaData._length,
measurement,
};
const tx = (await this.db).transaction(REPORT_STORE, "readwrite");
await tx.objectStore(REPORT_STORE).put(report);
await tx.done;
} else {
lazy.logConsole.debug(`reached cap, nextReset: ${cap.nextReset}`);
}
} catch (err) {
if (err.name === "NotFoundError") {
console.error(
`Object store ${FREQ_CAP_STORE} or ${REPORT_STORE} not found`
);
} else {
console.error("IndexedDB access error:", err);
}
}
}
/* Deletes any pending report or freq cap data from DB
*/
async deleteState() {
const taskIds = Object.keys(this._tasks);
const tx = (await this.db).transaction(
[REPORT_STORE, FREQ_CAP_STORE],
"readwrite"
);
const reportStore = tx.objectStore(REPORT_STORE);
const capStore = tx.objectStore(FREQ_CAP_STORE);
for (const taskId of taskIds) {
reportStore.delete(taskId);
}
for (const taskId of taskIds) {
capStore.delete(taskId);
}
await tx.done;
}
/* Shutdowns DAPReportCollector, sends any pending reports
* and deletes any pending report or freq cap data from DB
*/
async cleanup(timeout, reason) {
lazy.clearTimeout(this._timerId);
await this.submit(timeout, reason);
await this.deleteState();
}
async startTimedSubmission() {
await this.submit(30 * 1000, "periodic");
this._timerId = lazy.setTimeout(
() => this.startTimedSubmission(),
this.#timeoutValue()
);
}
#timeoutValue() {
return this._submissionTimerMins * MINUTE_IN_MILLI;
}
async getReportToSubmit(taskId) {
const tx = (await this.db).transaction(REPORT_STORE, "readonly");
const report = await tx.objectStore(REPORT_STORE).get(taskId);
await tx.done;
return report;
}
async submit(timeout, reason) {
let sendPromises = [];
for (const [taskId, metadata] of Object.entries(this._tasks)) {
let task = {
id: taskId,
vdaf: metadata._vdaf,
bits: metadata._bits,
length: metadata._length,
time_precision: 60,
};
let measurement = metadata._defaultMeasurement;
let report = await this.getReportToSubmit(taskId);
if (report) {
measurement = report.measurement;
}
sendPromises.push(
DAPTelemetrySender.sendDAPMeasurement(task, measurement, {
timeout,
reason,
})
);
if (report) {
this.#releasePendingReport(report);
}
}
try {
await Promise.all(sendPromises);
} catch (e) {
lazy.logConsole.error("Failed to send report: ", e);
}
}
}