Skip to content

Commit 2d5d161

Browse files
author
Beatriz Rizental
authored
Bug 1686685 - Implement the deletion request ping (#42)
1 parent 652afe5 commit 2d5d161

File tree

6 files changed

+135
-58
lines changed

6 files changed

+135
-58
lines changed

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ export const KNOWN_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
3535

3636
// The default server pings are sent to.
3737
export const DEFAULT_TELEMETRY_ENDPOINT = "https://incoming.telemetry.mozilla.org";
38+
39+
// The name of the deletion-request ping.
40+
export const DELETION_REQUEST_PING_NAME = "deletion-request";

src/glean.ts

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import EventsDatabase from "metrics/events_database";
1414
import UUIDMetricType from "metrics/types/uuid";
1515
import DatetimeMetricType, { DatetimeMetric } from "metrics/types/datetime";
1616
import Dispatcher from "dispatcher";
17+
import CorePings from "internal_pings";
1718

1819
class Glean {
1920
// The Glean singleton.
@@ -29,6 +30,8 @@ class Glean {
2930
private _initialized: boolean;
3031
// Instances of Glean's core metrics.
3132
private _coreMetrics: CoreMetrics;
33+
// Instances of Glean's core pings.
34+
private _corePings: CorePings;
3235
// The ping uploader.
3336
private _pingUploader: PingUploader
3437
// A task dispatcher to help execute in order asynchronous external API calls.
@@ -53,6 +56,7 @@ class Glean {
5356
this._dispatcher = new Dispatcher();
5457
this._pingUploader = new PingUploader();
5558
this._coreMetrics = new CoreMetrics();
59+
this._corePings = new CorePings();
5660
this._initialized = false;
5761
this._db = {
5862
metrics: new MetricsDatabase(),
@@ -77,6 +81,10 @@ class Glean {
7781
return Glean.instance._coreMetrics;
7882
}
7983

84+
private static get corePings(): CorePings {
85+
return Glean.instance._corePings;
86+
}
87+
8088
private static get uploadEnabled(): boolean {
8189
return Glean.instance._uploadEnabled || false;
8290
}
@@ -109,6 +117,9 @@ class Glean {
109117
private static async onUploadDisabled(): Promise<void> {
110118
Glean.uploadEnabled = false;
111119
await Glean.clearMetrics();
120+
// Note that `submit` is a dispatched function.
121+
// The actual submission will only happen after we leave `onUploadDisabled`.
122+
Glean.corePings.deletionRequest.submit();
112123
}
113124

114125
/**
@@ -126,7 +137,7 @@ class Glean {
126137
//
127138
// Note: This will throw in case the stored metric is incorrect or inexistent.
128139
// The most likely is that it throws if the metrics hasn't been set,
129-
// e.g. we start Glean for the first with tupload disabled.
140+
// e.g. we start Glean for the first with upload disabled.
130141
let firstRunDate: Date;
131142
try {
132143
firstRunDate = new DatetimeMetric(
@@ -220,6 +231,13 @@ class Glean {
220231
// IMPORTANT!
221232
// Any pings we want to send upon initialization should happen before this.
222233
await Glean.metricsDatabase.clear(Lifetime.Application);
234+
235+
// We need to mark Glean as initialized before dealing with the upload status,
236+
// otherwise we will not be able to submit deletion-request pings if necessary.
237+
//
238+
// This is fine, we are inside a dispatched task that is guaranteed to run before any
239+
// other task. No external API call will be executed before we leave this task.
240+
Glean.instance._initialized = true;
223241

224242
// The upload enabled flag may have changed since the last run, for
225243
// example by the changing of a config file.
@@ -242,96 +260,52 @@ class Glean {
242260

243261
if (clientId) {
244262
if (clientId !== KNOWN_CLIENT_ID) {
245-
// Temporarily enable uploading so we can submit a
246-
// deletion request ping.
247-
Glean.uploadEnabled = true;
248263
await Glean.onUploadDisabled();
249264
}
250265
} else {
266+
// Call `clearMetrics` directly here instead of `onUploadDisabled` to avoid sending
267+
// a deletion-request ping for a user that has already done that.
251268
await Glean.clearMetrics();
252269
}
253270
}
254271

255-
Glean.instance._initialized = true;
256-
257272
await Glean.pingUploader.scanPendingPings();
258273

259274
// Even though this returns a promise, there is no need to block on it returning.
260275
//
261-
// On the contrary we _want_ the dispatcher to execute tasks async.
276+
// On the contrary we _want_ the uploading tasks to be executed async.
262277
void Glean.pingUploader.triggerUpload();
263278
});
264279
}
265280

266-
/**
267-
* Gets this Glean's instance metrics database.
268-
*
269-
* @returns This Glean's instance metrics database.
270-
*/
271281
static get metricsDatabase(): MetricsDatabase {
272282
return Glean.instance._db.metrics;
273283
}
274284

275-
/**
276-
* Gets this Glean's instance events database.
277-
*
278-
* @returns This Glean's instance events database.
279-
*/
280285
static get eventsDatabase(): EventsDatabase {
281286
return Glean.instance._db.events;
282287
}
283288

284-
285-
/**
286-
* Gets this Glean's instance pings database.
287-
*
288-
* @returns This Glean's instance pings database.
289-
*/
290289
static get pingsDatabase(): PingsDatabase {
291290
return Glean.instance._db.pings;
292291
}
293292

294-
/**
295-
* Gets this Glean's instance initialization status.
296-
*
297-
* @returns Whether or not the Glean singleton has been initialized.
298-
*/
299293
static get initialized(): boolean {
300294
return Glean.instance._initialized;
301295
}
302296

303-
/**
304-
* Gets this Glean's instance application id.
305-
*
306-
* @returns The application id or `undefined` in case Glean has not been initialized yet.
307-
*/
308297
static get applicationId(): string | undefined {
309298
return Glean.instance._applicationId;
310299
}
311300

312-
/**
313-
* Gets this Glean's instance server endpoint.
314-
*
315-
* @returns The server endpoint or `undefined` in case Glean has not been initialized yet.
316-
*/
317301
static get serverEndpoint(): string | undefined {
318302
return Glean.instance._config?.serverEndpoint;
319303
}
320304

321-
/**
322-
* Whether or not to log pings upon collection.
323-
*
324-
* @returns Whether or not to log pings upon collection.
325-
*/
326305
static get logPings(): boolean {
327306
return Glean.instance._config?.debug?.logPings || false;
328307
}
329308

330-
/**
331-
* Gets this Gleans's instance dispatcher.
332-
*
333-
* @returns The dispatcher instance.
334-
*/
335309
static get dispatcher(): Dispatcher {
336310
return Glean.instance._dispatcher;
337311
}

src/internal_pings.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { DELETION_REQUEST_PING_NAME } from "./constants";
6+
import PingType from "pings";
7+
8+
/**
9+
* Glean-provided pings, all enabled by default.
10+
*
11+
* Pings initialized here should be defined in `pings.yaml`
12+
* and for now manually translated into JS code.
13+
*
14+
* TODO: This file should be auto-generated once Bug 1691365 is resolved.
15+
*/
16+
class CorePings {
17+
// This ping is intended to communicate to the Data Pipeline
18+
// that the user wishes to have their reported Telemetry data deleted.
19+
// As such it attempts to send itself at the moment the user opts out of data collection.
20+
readonly deletionRequest: PingType;
21+
22+
constructor() {
23+
this.deletionRequest = new PingType(
24+
DELETION_REQUEST_PING_NAME,
25+
/* include client id */ true,
26+
/* send if empty */ true
27+
);
28+
}
29+
}
30+
31+
export default CorePings;

src/pings/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
6-
import collectAndStorePing from "pings/maker";
5+
import { DELETION_REQUEST_PING_NAME } from "../constants";
76
import { generateUUIDv4 } from "utils";
7+
import collectAndStorePing from "pings/maker";
88
import Glean from "glean";
99

1010
/**
@@ -21,15 +21,19 @@ class PingType {
2121
* @param name The name of the ping.
2222
* @param includeClientId Whether to include the client ID in the assembled ping when submitting.
2323
* @param sendIfEmtpy Whether the ping should be sent empty or not.
24-
* @param reasonCodes The valid reason codes for this ping.
24+
* @param reasonCodes Optional. The valid reason codes for this ping.
2525
*/
2626
constructor (
2727
readonly name: string,
2828
readonly includeClientId: boolean,
2929
readonly sendIfEmtpy: boolean,
30-
readonly reasonCodes: string[]
30+
readonly reasonCodes: string[] = []
3131
) {}
3232

33+
private isDeletionRequest(): boolean {
34+
return this.name === DELETION_REQUEST_PING_NAME;
35+
}
36+
3337
/**
3438
* Collects and submits a ping for eventual uploading.
3539
*
@@ -48,9 +52,9 @@ class PingType {
4852
console.info("Glean must be initialized before submitting pings.");
4953
return;
5054
}
51-
52-
if (!Glean.isUploadEnabled()) {
53-
console.info("Glean disabled: not submitting any pings.");
55+
56+
if (!Glean.isUploadEnabled() && !this.isDeletionRequest()) {
57+
console.info("Glean disabled: not submitting pings. Glean may still submit the deletion-request ping.");
5458
return;
5559
}
5660

src/upload/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class PingUploader implements PingsDatabaseObserver {
148148
* @returns The status number of the response or `undefined` if unable to attempt upload.
149149
*/
150150
private async attemptPingUpload(ping: QueuedPing): Promise<UploadResult> {
151-
if (!Glean.initialize) {
151+
if (!Glean.initialized) {
152152
console.warn("Attempted to upload a ping, but Glean is not initialized yet. Ignoring.");
153153
return { result: UploadResultStatus.RecoverableFailure };
154154
}

tests/glean.spec.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import assert from "assert";
66
import sinon from "sinon";
77

8-
import { CLIENT_INFO_STORAGE, KNOWN_CLIENT_ID } from "../src/constants";
8+
import { CLIENT_INFO_STORAGE, DELETION_REQUEST_PING_NAME, KNOWN_CLIENT_ID } from "../src/constants";
99
import Glean from "glean";
1010
import { Lifetime } from "metrics";
1111
import StringMetricType from "metrics/types/string";
@@ -210,10 +210,75 @@ describe("Glean", function() {
210210
ping.submit();
211211

212212
await Glean.dispatcher.testBlockOnQueue();
213+
// TODO: Make this nicer once we resolve Bug 1691033 is resolved.
214+
await Glean["pingUploader"]["currentJob"];
213215

216+
// Check that one ping was sent,
217+
// but that ping is not our custom ping, but the deletion-request.
218+
assert.ok(postSpy.getCall(0).args[0].indexOf(DELETION_REQUEST_PING_NAME) !== -1);
219+
assert.strictEqual(postSpy.callCount, 1);
220+
});
221+
222+
it("deletion request is sent when toggling upload from on to off", async function() {
223+
const postSpy = sandbox.spy(Uploader, "post");
224+
225+
Glean.setUploadEnabled(false);
226+
await Glean.dispatcher.testBlockOnQueue();
227+
228+
assert.strictEqual(postSpy.callCount, 1);
229+
assert.ok(postSpy.getCall(0).args[0].indexOf(DELETION_REQUEST_PING_NAME) !== -1);
230+
231+
postSpy.resetHistory();
232+
Glean.setUploadEnabled(true);
233+
await Glean.dispatcher.testBlockOnQueue();
214234
assert.strictEqual(postSpy.callCount, 0);
235+
});
236+
237+
it("deletion request ping is sent when toggling upload status between runs", async function() {
238+
const postSpy = sandbox.spy(Uploader, "post");
215239

216-
// TODO: Check that a deletion request ping was sent after Bug 1686685 is resolved.
240+
Glean.setUploadEnabled(true);
241+
await Glean.dispatcher.testBlockOnQueue();
242+
243+
// Can't use testResetGlean here because it clears all stores
244+
// and when there is no client_id at all stored, a deletion ping is also not set.
245+
await Glean.testUninitialize();
246+
Glean.initialize(GLOBAL_APPLICATION_ID, false);
247+
await Glean.dispatcher.testBlockOnQueue();
248+
// TODO: Make this nicer once we resolve Bug 1691033 is resolved.
249+
await Glean["pingUploader"]["currentJob"];
250+
251+
// A deletion request is sent
252+
assert.strictEqual(postSpy.callCount, 1);
253+
assert.ok(postSpy.getCall(0).args[0].indexOf(DELETION_REQUEST_PING_NAME) !== -1);
254+
});
255+
256+
it("deletion request ping is not sent if upload status does not change between runs", async function () {
257+
const postSpy = sandbox.spy(Uploader, "post");
258+
259+
Glean.setUploadEnabled(false);
260+
await Glean.dispatcher.testBlockOnQueue();
261+
262+
// A deletion request is sent
263+
assert.strictEqual(postSpy.callCount, 1);
264+
assert.ok(postSpy.getCall(0).args[0].indexOf(DELETION_REQUEST_PING_NAME) !== -1);
265+
266+
// Can't use testResetGlean here because it clears all stores
267+
// and when there is no client_id at all stored, a deletion ping is also not set.
268+
await Glean.testUninitialize();
269+
Glean.initialize(GLOBAL_APPLICATION_ID, false);
270+
await Glean.dispatcher.testBlockOnQueue();
271+
// TODO: Make this nicer once we resolve Bug 1691033 is resolved.
272+
await Glean["pingUploader"]["currentJob"];
273+
274+
postSpy.resetHistory();
275+
assert.strictEqual(postSpy.callCount, 0);
276+
});
277+
278+
it("deletion request ping is not sent when user starts Glean for the first time with upload disabled", async function () {
279+
const postSpy = sandbox.spy(Uploader, "post");
280+
await Glean.testResetGlean(GLOBAL_APPLICATION_ID, false);
281+
assert.strictEqual(postSpy.callCount, 0);
217282
});
218283

219284
it("setting log pings works before and after glean and on initialize", async function () {

0 commit comments

Comments
 (0)