Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 1d1c913

Browse files
committed
Bug 1461690 Part 1: Write uninstall ping. r=chutten,Gijs
This covers the in-Firefox portions of the install ping, except for the otherInstalls count, which is added in part 2. Later parts will cover the uninstaller. Differential Revision: https://phabricator.services.mozilla.com/D92522
1 parent 44f7c14 commit 1d1c913

6 files changed

Lines changed: 323 additions & 0 deletions

File tree

toolkit/components/telemetry/app/TelemetryControllerParent.jsm

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const NEWPROFILE_PING_DEFAULT_DELAY = 30 * 60 * 1000;
4242
// Ping types.
4343
const PING_TYPE_MAIN = "main";
4444
const PING_TYPE_DELETION_REQUEST = "deletion-request";
45+
const PING_TYPE_UNINSTALL = "uninstall";
4546

4647
// Session ping reasons.
4748
const REASON_GATHER_PAYLOAD = "gather-payload";
@@ -253,6 +254,19 @@ var TelemetryController = Object.freeze({
253254
return Impl.removeAbortedSessionPing();
254255
},
255256

257+
/**
258+
* Create an uninstall ping and write it to disk, replacing any already present.
259+
* This is stored independently from other pings, and only read by
260+
* the Windows uninstaller.
261+
*
262+
* WINDOWS ONLY, does nothing and resolves immediately on other platforms.
263+
*
264+
* @return {Promise} Resolved when the ping has been saved.
265+
*/
266+
saveUninstallPing() {
267+
return Impl.saveUninstallPing();
268+
},
269+
256270
/**
257271
* Allows waiting for TelemetryControllers delayed initialization to complete.
258272
* The returned promise is guaranteed to resolve before TelemetryController is shutting down.
@@ -689,6 +703,30 @@ var Impl = {
689703
return TelemetryStorage.removeAbortedSessionPing();
690704
},
691705

706+
_countOtherInstalls() {
707+
// TODO
708+
throw new Error("_countOtherInstalls - not implemented");
709+
},
710+
711+
async saveUninstallPing() {
712+
if (AppConstants.platform != "win") {
713+
return undefined;
714+
}
715+
716+
this._log.trace("saveUninstallPing");
717+
718+
let payload = {};
719+
try {
720+
payload.otherInstalls = this._countOtherInstalls();
721+
} catch (e) {
722+
this._log.warn("saveUninstallPing - _countOtherInstalls failed", e);
723+
}
724+
const options = { addClientId: true, addEnvironment: true };
725+
const pingData = this.assemblePing(PING_TYPE_UNINSTALL, payload, options);
726+
727+
return TelemetryStorage.saveUninstallPing(pingData);
728+
},
729+
692730
/**
693731
* This triggers basic telemetry initialization and schedules a full initialized for later
694732
* for performance reasons.
@@ -836,6 +874,16 @@ var Impl = {
836874
EcosystemTelemetry.startup();
837875
TelemetryPrioPing.startup();
838876

877+
if (uploadEnabled) {
878+
await this.saveUninstallPing().catch(e =>
879+
this._log.warn("_delayedInitTask - saveUninstallPing failed", e)
880+
);
881+
} else {
882+
await TelemetryStorage.removeUninstallPings().catch(e =>
883+
this._log.warn("_delayedInitTask - saveUninstallPing", e)
884+
);
885+
}
886+
839887
this._delayedInitTaskDeferred.resolve();
840888
} catch (e) {
841889
this._delayedInitTaskDeferred.reject(e);
@@ -1004,6 +1052,10 @@ var Impl = {
10041052
let id = await ClientID.getClientID();
10051053
this._clientID = id;
10061054
Telemetry.scalarSet("telemetry.data_upload_optin", true);
1055+
1056+
await this.saveUninstallPing().catch(e =>
1057+
this._log.warn("_onUploadPrefChange - saveUninstallPing failed", e)
1058+
);
10071059
})();
10081060

10091061
this._shutdownBarrier.client.addBlocker(
@@ -1023,6 +1075,7 @@ var Impl = {
10231075
// 3. Remove all pending pings
10241076
await TelemetryStorage.removeAppDataPings();
10251077
await TelemetryStorage.runRemovePendingPingsTask();
1078+
await TelemetryStorage.removeUninstallPings();
10261079
} catch (e) {
10271080
this._log.error(
10281081
"_onUploadPrefChange - error clearing pending pings",

toolkit/components/telemetry/app/TelemetryStorage.jsm

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,26 @@ var Policy = {
110110
AppConstants.platform == "android"
111111
? PENDING_PINGS_QUOTA_BYTES_MOBILE
112112
: PENDING_PINGS_QUOTA_BYTES_DESKTOP,
113+
/**
114+
* @param {string} id The ID of the ping that will be written into the file. Can be "*" to
115+
* make a pattern to find all pings for this installation.
116+
* @return
117+
* {
118+
* directory: <nsIFile>, // Directory to save pings
119+
* file: <string>, // File name for this ping (or pattern for all pings)
120+
* }
121+
*/
122+
getUninstallPingPath: id => {
123+
// UpdRootD is e.g. C:\ProgramData\Mozilla\updates\<PATH HASH>
124+
const updateDirectory = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
125+
const installPathHash = updateDirectory.leafName;
126+
127+
return {
128+
// e.g. C:\ProgramData\Mozilla
129+
directory: updateDirectory.parent.parent.clone(),
130+
file: `uninstall_ping_${installPathHash}_${id}.json`,
131+
};
132+
},
113133
};
114134

115135
/**
@@ -346,6 +366,31 @@ var TelemetryStorage = {
346366
return TelemetryStorageImpl.removeAbortedSessionPing();
347367
},
348368

369+
/**
370+
* Save an uninstall ping to disk, removing any old ones from this
371+
* installation first.
372+
* This is stored independently from other pings, and only read by
373+
* the Windows uninstaller.
374+
*
375+
* WINDOWS ONLY, does nothing and resolves immediately on other platforms.
376+
*
377+
* @return {promise} Promise that is resolved when the ping has been saved.
378+
*/
379+
saveUninstallPing(ping) {
380+
return TelemetryStorageImpl.saveUninstallPing(ping);
381+
},
382+
383+
/**
384+
* Remove all uninstall pings from this installation.
385+
*
386+
* WINDOWS ONLY, does nothing and resolves immediately on other platforms.
387+
*
388+
* @return {promise} Promise that is resolved when the pings have been removed.
389+
*/
390+
removeUninstallPings() {
391+
return TelemetryStorageImpl.removeUninstallPings();
392+
},
393+
349394
/**
350395
* Save a single ping to a file.
351396
*
@@ -1975,6 +2020,49 @@ var TelemetryStorageImpl = {
19752020
});
19762021
},
19772022

2023+
async saveUninstallPing(ping) {
2024+
if (AppConstants.platform != "win") {
2025+
return;
2026+
}
2027+
2028+
// Remove any old pings from this install first.
2029+
await this.removeUninstallPings();
2030+
2031+
let { directory: pingFile, file } = Policy.getUninstallPingPath(ping.id);
2032+
pingFile.append(file);
2033+
2034+
await this.savePingToFile(ping, pingFile.path, /* overwrite */ true);
2035+
},
2036+
2037+
async removeUninstallPings() {
2038+
if (AppConstants.platform != "win") {
2039+
return;
2040+
}
2041+
2042+
const { directory, file } = Policy.getUninstallPingPath("*");
2043+
2044+
const iteratorOptions = { winPattern: file };
2045+
const iterator = new OS.File.DirectoryIterator(
2046+
directory.path,
2047+
iteratorOptions
2048+
);
2049+
2050+
await iterator.forEach(async entry => {
2051+
this._log.trace("removeUninstallPings - removing", entry.path);
2052+
try {
2053+
await OS.File.remove(entry.path);
2054+
this._log.trace("removeUninstallPings - success");
2055+
} catch (ex) {
2056+
if (ex.becauseNoSuchFile) {
2057+
this._log.trace("removeUninstallPings - no such file");
2058+
} else {
2059+
this._log.error("removeUninstallPings - error removing ping", ex);
2060+
}
2061+
}
2062+
});
2063+
iterator.close();
2064+
},
2065+
19782066
/**
19792067
* Remove FHR database files. This is temporary and will be dropped in
19802068
* the future.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
"uninstall" ping
3+
================
4+
5+
This opt-out ping is sent from the Windows uninstaller when the uninstall finishes. Notably it includes ``clientId`` and the :doc:`Telemetry Environment <environment>`. It follows the :doc:`common ping format <common-ping>`.
6+
7+
Structure:
8+
9+
.. code-block:: js
10+
11+
{
12+
type: "uninstall",
13+
... common ping data
14+
clientId: <UUID>,
15+
environment: { ... },
16+
payload: {
17+
otherInstalls: <integer>, // Optional, number of other installs on the system, max 11.
18+
}
19+
}
20+
21+
See also the `JSON schema <https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/templates/telemetry/uninstall/uninstall.4.schema.json>`_. These pings are recorded in the ``telemetry.uninstall`` table in Redash, using the default "Telemetry (BigQuery)" data source.
22+
23+
payload.otherInstalls
24+
---------------------
25+
This is a count of how many other installs of Firefox were present on the system at the time the ping was written. It is the number of values in the ``Software\Mozilla\Firefox\TaskBarIDs`` registry key, for both 32-bit and 64-bit architectures, for both HKCU and HKLM, excluding duplicates, and excluding a value for this install (if present). For example, if this is the only install on the system, the value will be 0. It may be missing in case of an error.
26+
27+
This count is capped at 11. This avoids introducing a high-resolution identifier in case of a system with a large, unique number of installs.
28+
29+
Uninstall Ping storage and lifetime
30+
-----------------------------------
31+
32+
On delayed Telemetry init (about 1 minute into each run of Firefox), if opt-out telemetry is enabled, this ping is written to disk. There is a single ping for each install, any uninstall pings from the same install are removed before the new ping is written.
33+
34+
The ping is removed if Firefox notices that opt-out telemetry has been disabled, either when the ``datareporting.healthreport.uploadEnabled`` pref goes false or when it is false on delayed init. Conversely, when opt-out telemetry is re-enabled, the ping is written as Telemetry is setting itself up again.
35+
36+
The ping is sent by the uninstaller some arbitrary time after it is written to disk by Firefox, so it could be significantly out of date when it is submitted. There should be little impact from stale data, since analysis is likely to focus on clients that uninstalled soon after running Firefox, and this ping mostly changes when Firefox itself is updated.

toolkit/components/telemetry/tests/unit/head.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,20 @@ function fakeIntlReady() {
439439
Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
440440
}
441441

442+
// Override the uninstall ping file names
443+
function fakeUninstallPingPath(aPathFcn) {
444+
const m = ChromeUtils.import(
445+
"resource://gre/modules/TelemetryStorage.jsm",
446+
null
447+
);
448+
m.Policy.getUninstallPingPath =
449+
aPathFcn ||
450+
(id => ({
451+
directory: new FileUtils.File(OS.Constants.Path.profileDir),
452+
file: `uninstall_ping_0123456789ABCDEF_${id}.json`,
453+
}));
454+
}
455+
442456
// Return a date that is |offset| ms in the future from |date|.
443457
function futureDate(date, offset) {
444458
return new Date(date.getTime() + offset);
@@ -570,3 +584,6 @@ fakeSchedulerTimer(
570584
);
571585
// Make pind sending predictable.
572586
fakeMidnightPingFuzzingDelay(0);
587+
588+
// Avoid using the directory service, which is not registered in some tests.
589+
fakeUninstallPingPath();
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/* Any copyright is dedicated to the Public Domain.
2+
* http://creativecommons.org/publicdomain/zero/1.0/
3+
*/
4+
"use strict";
5+
6+
const { TelemetryStorage } = ChromeUtils.import(
7+
"resource://gre/modules/TelemetryStorage.jsm"
8+
);
9+
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
10+
const { FileUtils } = ChromeUtils.import(
11+
"resource://gre/modules/FileUtils.jsm"
12+
);
13+
14+
const gFakeInstallPathHash = "0123456789ABCDEF";
15+
let gFakeVendorDirectory;
16+
let gFakeGetUninstallPingPath;
17+
18+
add_task(async function setup() {
19+
do_get_profile();
20+
21+
let fakeVendorDirectoryNSFile = new FileUtils.File(
22+
OS.Path.join(OS.Constants.Path.profileDir, "uninstall-ping-test")
23+
);
24+
fakeVendorDirectoryNSFile.createUnique(
25+
Ci.nsIFile.DIRECTORY_TYPE,
26+
FileUtils.PERMS_DIRECTORY
27+
);
28+
gFakeVendorDirectory = fakeVendorDirectoryNSFile.path;
29+
30+
gFakeGetUninstallPingPath = id => ({
31+
directory: fakeVendorDirectoryNSFile.clone(),
32+
file: `uninstall_ping_${gFakeInstallPathHash}_${id}.json`,
33+
});
34+
35+
fakeUninstallPingPath(gFakeGetUninstallPingPath);
36+
37+
registerCleanupFunction(() => {
38+
OS.File.removeDir(gFakeVendorDirectory);
39+
});
40+
});
41+
42+
function ping_path(ping) {
43+
let { directory: pingFile, file } = gFakeGetUninstallPingPath(ping.id);
44+
pingFile.append(file);
45+
return pingFile.path;
46+
}
47+
48+
add_task(async function test_store_ping() {
49+
// Remove shouldn't throw on an empty dir.
50+
await TelemetryStorage.removeUninstallPings();
51+
52+
// Write ping
53+
const ping1 = {
54+
id: "58b63aac-999e-4efb-9d5a-20f368670721",
55+
payload: { some: "thing" },
56+
};
57+
const ping1Path = ping_path(ping1);
58+
await TelemetryStorage.saveUninstallPing(ping1);
59+
60+
// Check the ping
61+
Assert.ok(await OS.File.exists(ping1Path));
62+
const readPing1 = JSON.parse(
63+
await OS.File.read(ping1Path, { encoding: "utf-8" })
64+
);
65+
Assert.deepEqual(ping1, readPing1);
66+
67+
// Write another file that shouldn't match the pattern
68+
const otherFilePath = OS.Path.join(gFakeVendorDirectory, "other_file.json");
69+
await OS.File.writeAtomic(otherFilePath, "");
70+
Assert.ok(await OS.File.exists(otherFilePath));
71+
72+
// Write another ping, should remove the earlier one
73+
const ping2 = {
74+
id: "7202c564-8f23-41b4-8a50-1744e9549260",
75+
payload: { another: "thing" },
76+
};
77+
const ping2Path = ping_path(ping2);
78+
await TelemetryStorage.saveUninstallPing(ping2);
79+
80+
Assert.ok(!(await OS.File.exists(ping1Path)));
81+
Assert.ok(await OS.File.exists(ping2Path));
82+
Assert.ok(await OS.File.exists(otherFilePath));
83+
84+
// Write an additional file manually so there are multiple matching pings to remove
85+
const ping3 = { id: "yada-yada" };
86+
const ping3Path = ping_path(ping3);
87+
88+
await OS.File.writeAtomic(ping3Path, "");
89+
Assert.ok(await OS.File.exists(ping3Path));
90+
91+
// Remove pings
92+
await TelemetryStorage.removeUninstallPings();
93+
94+
// Check our pings are removed but other file isn't
95+
Assert.ok(!(await OS.File.exists(ping1Path)));
96+
Assert.ok(!(await OS.File.exists(ping2Path)));
97+
Assert.ok(!(await OS.File.exists(ping3Path)));
98+
Assert.ok(await OS.File.exists(otherFilePath));
99+
100+
// Remove again, confirming that the remove doesn't cause an error if nothing to remove
101+
await TelemetryStorage.removeUninstallPings();
102+
103+
const ping4 = {
104+
id: "1f113673-753c-4fbe-9143-fe197f936036",
105+
payload: { any: "thing" },
106+
};
107+
const ping4Path = ping_path(ping4);
108+
await TelemetryStorage.saveUninstallPing(ping4);
109+
110+
// Open the ping without FILE_SHARE_DELETE, so a delete should fail.
111+
const ping4File = await OS.File.open(
112+
ping4Path,
113+
{ read: true, existing: true },
114+
{ winShare: OS.Constants.Win.FILE_SHARE_READ }
115+
);
116+
117+
// Check that there is no error if the file can't be removed.
118+
await TelemetryStorage.removeUninstallPings();
119+
120+
// And file should still exist.
121+
Assert.ok(await OS.File.exists(ping4Path));
122+
123+
// Close the file, it should be possible to remove now.
124+
ping4File.close();
125+
await TelemetryStorage.removeUninstallPings();
126+
Assert.ok(!(await OS.File.exists(ping4Path)));
127+
});

0 commit comments

Comments
 (0)