Bug 1495792: Add a profile first-run time to the telemetry environment. r=janerik
authorDave Townsend <dtownsend@oxymoronical.com>
Mon, 15 Oct 2018 16:09:12 +0000
changeset 489878 ed09a03420aef61b84d91d1977ec969fb01cf94a
parent 489877 d71434f407554db9cf5320fbf8098c2a767f91e4
child 489879 dc57e840695b4534ce98f817f3bf2a42c35fd6d4
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjanerik
bugs1495792
milestone64.0a1
Bug 1495792: Add a profile first-run time to the telemetry environment. r=janerik Differential Revision: https://phabricator.services.mozilla.com/D8464
toolkit/components/telemetry/app/TelemetryEnvironment.jsm
toolkit/components/telemetry/docs/data/environment.rst
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
toolkit/modules/ProfileAge.jsm
toolkit/modules/tests/xpcshell/test_ProfileAge.js
toolkit/profile/nsToolkitProfileService.cpp
--- a/toolkit/components/telemetry/app/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/app/TelemetryEnvironment.jsm
@@ -1442,23 +1442,28 @@ EnvironmentCache.prototype = {
    * Update the cached profile data.
    * @returns Promise<> resolved when the I/O is complete.
    */
   async _updateProfile() {
     let profileAccessor = await ProfileAge();
 
     let creationDate = await profileAccessor.created;
     let resetDate = await profileAccessor.reset;
+    let firstUseDate = await profileAccessor.firstUse;
 
     this._currentEnvironment.profile.creationDate =
       Utils.millisecondsToDays(creationDate);
     if (resetDate) {
       this._currentEnvironment.profile.resetDate =
         Utils.millisecondsToDays(resetDate);
     }
+    if (firstUseDate) {
+      this._currentEnvironment.profile.firstUseDate =
+        Utils.millisecondsToDays(firstUseDate);
+    }
   },
 
   /**
    * Load the attribution data object and updates the environment.
    * @returns Promise<> resolved when the I/O is complete.
    */
   async _loadAttributionAsync() {
     try {
--- a/toolkit/components/telemetry/docs/data/environment.rst
+++ b/toolkit/components/telemetry/docs/data/environment.rst
@@ -68,16 +68,17 @@ Structure:
         },
         sandbox: {
           effectiveContentProcessLevel: <integer>,
         }
       },
       profile: {
         creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
         resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
+        firstUseDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
       },
       partner: { // This section may not be immediately available on startup
         distributionId: <string>, // pref "distribution.id", null on failure
         distributionVersion: <string>, // pref "distribution.version", null on failure
         partnerId: <string>, // pref mozilla.partner.id, null on failure
         distributor: <string>, // pref app.distributor, null on failure
         distributorChannel: <string>, // pref app.distributor.channel, null on failure
         partnerNames: [
@@ -371,16 +372,30 @@ The assumed creation date of this client
 It's read from a file-stored timestamp from the client's profile directory.
 
 .. note::
 
     If the timestamp file does not exist all files in the profile directory are scanned.
     The oldest creation or modification date of the scanned files is then taken to be the profile creation date.
     This has been shown to sometimes be inaccurate (`bug 1449739 <https://bugzilla.mozilla.org/show_bug.cgi?id=1449739>`_).
 
+resetDate
+~~~~~~~~~~~~
+
+The time of the last reset time for the profile. If the profile has never been
+reset this field will not be present.
+It's read from a file-stored timestamp from the client's profile directory.
+
+firstUseDate
+~~~~~~~~~~~~
+
+The time of the first use of profile. If this is an old profile where we can't
+determine this this field will not be present.
+It's read from a file-stored timestamp from the client's profile directory.
+
 partner
 -------
 
 If the user is using a partner repack, this contains information identifying the repack being used, otherwise "partnerNames" will be an empty array and other entries will be null. The information may be missing when the profile just becomes available. In Firefox for desktop, the information along with other customizations defined in distribution.ini are processed later in the startup phase, and will be fully applied when "distribution-customization-complete" notification is sent.
 
 Distributions are most reliably identified by the ``distributionId`` field. Partner information can be found in the `partner repacks <https://github.com/mozilla-partners>`_ (`the old one <https://hg.mozilla.org/build/partner-repacks/>`_ is deprecated): it contains one private repository per partner.
 Important values for ``distributionId`` include:
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -51,17 +51,18 @@ const PARTNER_ID = "NicePartner-ID-3785"
 const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete";
 
 const GFX_VENDOR_ID = "0xabcd";
 const GFX_DEVICE_ID = "0x1234";
 
 // The profile reset date, in milliseconds (Today)
 const PROFILE_RESET_DATE_MS = Date.now();
 // The profile creation date, in milliseconds (Yesterday).
-const PROFILE_CREATION_DATE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
+const PROFILE_FIRST_USE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
+const PROFILE_CREATION_DATE_MS = PROFILE_FIRST_USE_MS - MILLISECONDS_PER_DAY;
 
 const FLASH_PLUGIN_NAME = "Shockwave Flash";
 const FLASH_PLUGIN_DESC = "A mock flash plugin";
 const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d";
 const PLUGIN_MIME_TYPE1 = "application/x-shockwave-flash";
 const PLUGIN_MIME_TYPE2 = "text/plain";
 
 const PLUGIN2_NAME = "Quicktime";
@@ -293,16 +294,17 @@ function spoofGfxAdapter() {
     // If we can't test gfxInfo, that's fine, we'll note it later.
   }
 }
 
 function spoofProfileReset() {
   return CommonUtils.writeJSON({
     created: PROFILE_CREATION_DATE_MS,
     reset: PROFILE_RESET_DATE_MS,
+    firstUse: PROFILE_FIRST_USE_MS,
   }, OS.Path.join(OS.Constants.Path.profileDir, "times.json"));
 }
 
 function spoofPartnerInfo() {
   let prefsToSpoof = {};
   prefsToSpoof["distribution.id"] = DISTRIBUTION_ID;
   prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION;
   prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME;
@@ -448,16 +450,17 @@ function checkSettingsSection(data) {
     Assert.equal(data.settings.attribution.source, "google.com");
   }
 }
 
 function checkProfileSection(data) {
   Assert.ok("profile" in data, "There must be a profile section in Environment.");
   Assert.equal(data.profile.creationDate, truncateToDays(PROFILE_CREATION_DATE_MS));
   Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS));
+  Assert.equal(data.profile.firstUseDate, truncateToDays(PROFILE_FIRST_USE_MS));
 }
 
 function checkPartnerSection(data, isInitial) {
   const EXPECTED_FIELDS = {
     distributionId: DISTRIBUTION_ID,
     distributionVersion: DISTRIBUTION_VERSION,
     partnerId: PARTNER_ID,
     distributor: DISTRIBUTOR_NAME,
--- a/toolkit/modules/ProfileAge.jsm
+++ b/toolkit/modules/ProfileAge.jsm
@@ -89,16 +89,22 @@ async function getOldestProfileTimestamp
  * This is separate from the provider to simplify testing and enable extraction
  * to a shared location in the future.
  */
 class ProfileAgeImpl {
   constructor(profile, times) {
     this.profilePath = profile || OS.Constants.Path.profileDir;
     this._times = times;
     this._log = Log.repository.getLogger("Toolkit.ProfileAge");
+
+    if ("firstUse" in this._times && this._times.firstUse === null) {
+      // Indicates that this is a new profile that needs a first use timestamp.
+      this._times.firstUse = Date.now();
+      this.writeTimes();
+    }
   }
 
   /**
    * There are two ways we can get our creation time:
    *
    * 1. From the on-disk JSON file.
    * 2. By calculating it from the filesystem.
    *
@@ -118,16 +124,27 @@ class ProfileAgeImpl {
     } else {
       this._created = Promise.resolve(this._times.created);
     }
 
     return this._created;
   }
 
   /**
+   * Returns a promise to the time of first use of the profile. This may be
+   * undefined if the first use time is unknown.
+   */
+  get firstUse() {
+    if ("firstUse" in this._times) {
+      return Promise.resolve(this._times.firstUse);
+    }
+    return Promise.resolve(undefined);
+  }
+
+  /**
    * Return a promise representing the writing the current times to the profile.
    */
   writeTimes() {
     return CommonUtils.writeJSON(this._times, OS.Path.join(this.profilePath, FILE_TIMES));
   }
 
   /**
    * Calculates the created time by scanning the profile directory, sets it in
@@ -171,17 +188,20 @@ const PROFILES = new Map();
 
 async function initProfileAge(profile) {
   let timesPath = OS.Path.join(profile, FILE_TIMES);
 
   try {
     let times = await CommonUtils.readJSON(timesPath);
     return new ProfileAgeImpl(profile, times || {});
   } catch (e) {
-    return new ProfileAgeImpl(profile, {});
+    // Indicates that the file was missing or broken. In this case we want to
+    // record the first use time as now. The constructor will set this and write
+    // times.json
+    return new ProfileAgeImpl(profile, { firstUse: null });
   }
 }
 
 /**
  * Returns a promise that resolves to an instance of ProfileAgeImpl. Will always
  * return the same instance for every call for the same profile.
  *
  * @param {string} profile The path to the profile directory.
--- a/toolkit/modules/tests/xpcshell/test_ProfileAge.js
+++ b/toolkit/modules/tests/xpcshell/test_ProfileAge.js
@@ -1,40 +1,43 @@
 ChromeUtils.import("resource://gre/modules/ProfileAge.jsm");
 ChromeUtils.import("resource://gre/modules/osfile.jsm");
 ChromeUtils.import("resource://services-common/utils.js");
 
 const gProfD = do_get_profile();
+let ID = 0;
 
 // Creates a unique profile directory to use for a test.
 function withDummyProfile(task) {
   return async () => {
-    let profile = OS.Path.join(gProfD.path, "" + Math.floor(Math.random() * 100));
+    let profile = OS.Path.join(gProfD.path, "" + ID++);
     await OS.File.makeDir(profile);
     await task(profile);
     await OS.File.removeDir(profile);
   };
 }
 
 add_task(withDummyProfile(async (profile) => {
   let times = await ProfileAge(profile);
   Assert.ok((await times.created) > 0, "We can't really say what this will be, just assume if it is a number it's ok.");
   Assert.equal(await times.reset, undefined, "Reset time is undefined in a new profile");
+  Assert.ok((await times.firstUse) <= Date.now(), "Should have initialised a first use time.");
 }));
 
 add_task(withDummyProfile(async (profile) => {
   const CREATED_TIME = Date.now() - 2000;
   const RESET_TIME = Date.now() - 1000;
 
   CommonUtils.writeJSON({
     created: CREATED_TIME,
   }, OS.Path.join(profile, "times.json"));
 
   let times = await ProfileAge(profile);
   Assert.equal((await times.created), CREATED_TIME, "Should have seen the right profile time.");
+  Assert.equal((await times.firstUse), undefined, "Should be no first use time.");
 
   let times2 = await ProfileAge(profile);
   Assert.equal(times, times2, "Should have got the same instance.");
 
   let promise = times.recordProfileReset(RESET_TIME);
   Assert.equal((await times2.reset), RESET_TIME, "Should have seen the right reset time in the second instance immediately.");
   await promise;
 
@@ -52,12 +55,25 @@ add_task(withDummyProfile(async (profile
   // The last call to recordProfileReset should always win.
   let times = await ProfileAge(profile);
   await Promise.all([
     times.recordProfileReset(RESET_TIME),
     times.recordProfileReset(RESET_TIME2),
   ]);
 
   let results = await CommonUtils.readJSON(OS.Path.join(profile, "times.json"));
+  delete results.firstUse;
   Assert.deepEqual(results, {
     reset: RESET_TIME2,
   }, "Should have seen the right results.");
 }));
+
+add_task(withDummyProfile(async (profile) => {
+  const CREATED_TIME = Date.now() - 1000;
+
+  CommonUtils.writeJSON({
+    created: CREATED_TIME,
+    firstUse: null,
+  }, OS.Path.join(profile, "times.json"));
+
+  let times = await ProfileAge(profile);
+  Assert.ok((await times.firstUse) <= Date.now(), "Should have initialised a first use time.");
+}));
--- a/toolkit/profile/nsToolkitProfileService.cpp
+++ b/toolkit/profile/nsToolkitProfileService.cpp
@@ -813,17 +813,17 @@ nsToolkitProfileService::CreateTimesInte
     // We don't care about microsecond resolution.
     int64_t msec = PR_Now() / PR_USEC_PER_MSEC;
 
     // Write it out.
     PRFileDesc *writeFile;
     rv = creationLog->OpenNSPRFileDesc(PR_WRONLY, 0700, &writeFile);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    PR_fprintf(writeFile, "{\n\"created\": %lld\n}\n", msec);
+    PR_fprintf(writeFile, "{\n\"created\": %lld,\n\"firstUse\": null\n}\n", msec);
     PR_Close(writeFile);
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::GetProfileCount(uint32_t *aResult)
 {
     *aResult = 0;