Bug 1120370 - Implement the new-profile ping. r?chutten, data-r=bsmedberg draft
authorAlessio Placitelli <alessio.placitelli@gmail.com>
Thu, 27 Apr 2017 09:36:01 +0200
changeset 576220 f54c4940cf0c73367a17a0b918e8739a73046b48
parent 567113 e17cbb839dd225a2da7e5d5bec43cf94e11749d8
child 628125 8842e66fc266f24586270b8bb2d04db255e05680
push id58282
push useralessio.placitelli@gmail.com
push dateThu, 11 May 2017 12:41:32 +0000
reviewerschutten
bugs1120370
milestone55.0a1
Bug 1120370 - Implement the new-profile ping. r?chutten, data-r=bsmedberg It schedules the ping to be sent on new profiles after 30 minutes from the Firefox startup. The ping is eventually sent at shutdown if the scheduled time wasn't reached. To reliably prevent sending the ping more than once, the "session-state.json" file is used to keep track of the "sent" state. The "new-profile" ping is protected behind a pref, disabled by default in this patch. MozReview-Commit-ID: 4g4lPRXe9q6
toolkit/components/telemetry/TelemetryController.jsm
toolkit/components/telemetry/TelemetrySend.jsm
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/docs/concepts/pings.rst
toolkit/components/telemetry/docs/data/new-profile-ping.rst
toolkit/components/telemetry/docs/internals/preferences.rst
toolkit/components/telemetry/tests/unit/test_TelemetryController.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -31,28 +31,34 @@ const PREF_BRANCH = "toolkit.telemetry."
 const PREF_BRANCH_LOG = PREF_BRANCH + "log.";
 const PREF_SERVER = PREF_BRANCH + "server";
 const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
 const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
 const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
 const PREF_UNIFIED = PREF_BRANCH + "unified";
+const PREF_NEWPROFILE_PING_ENABLED = PREF_BRANCH + "newProfilePing.enabled";
+const PREF_NEWPROFILE_PING_DELAY = PREF_BRANCH + "newProfilePing.delay";
 
 // Whether the FHR/Telemetry unification features are enabled.
 // Changing this pref requires a restart.
 const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
 
 const PING_FORMAT_VERSION = 4;
 
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = Preferences.get("toolkit.telemetry.initDelay", 60) * 1000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 1;
 
+// How long to wait (ms) before sending the new profile ping on the first
+// run of a new profile.
+const NEWPROFILE_PING_DEFAULT_DELAY = 30 * 60 * 1000;
+
 // Ping types.
 const PING_TYPE_MAIN = "main";
 const PING_TYPE_DELETION = "deletion";
 
 // Session ping reasons.
 const REASON_GATHER_PAYLOAD = "gather-payload";
 const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
 
@@ -317,16 +323,17 @@ this.TelemetryController = Object.freeze
     return Impl.promiseInitialized();
   },
 });
 
 var Impl = {
   _initialized: false,
   _initStarted: false, // Whether we started setting up TelemetryController.
   _shuttingDown: false, // Whether the browser is shutting down.
+  _shutDown: false, // Whether the browser has shut down.
   _logger: null,
   _prevValues: {},
   // The previous build ID, if this is the first run with a new build.
   // Undefined if this is not the first run, or the previous build ID is unknown.
   _previousBuildID: undefined,
   _clientID: null,
   // A task performing delayed initialization
   _delayedInitTask: null,
@@ -338,16 +345,18 @@ var Impl = {
   // This is a public barrier Telemetry clients can use to add blockers to the shutdown
   // of TelemetryController.
   // After this barrier, clients can not submit Telemetry pings anymore.
   _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for clients."),
   // This is a private barrier blocked by pending async ping activity (sending & saving).
   _connectionsBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for pending ping activity"),
   // This is true when running in the test infrastructure.
   _testMode: false,
+  // The task performing the delayed sending of the "new-profile" ping.
+  _delayedNewPingTask: null,
 
   get _log() {
     if (!this._logger) {
       this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
     }
 
     return this._logger;
   },
@@ -493,17 +502,17 @@ var Impl = {
    * @param {Object}  [aOptions.overrideEnvironment=null] set to override the environment data.
    * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
    * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
    */
   submitExternalPing: function send(aType, aPayload, aOptions) {
     this._log.trace("submitExternalPing - type: " + aType + ", aOptions: " + JSON.stringify(aOptions));
 
     // Reject pings sent after shutdown.
-    if (this._shuttingDown) {
+    if (this._shutDown) {
       const errorMessage = "submitExternalPing - Submission is not allowed after shutdown, discarding ping of type: " + aType;
       this._log.error(errorMessage);
       return Promise.reject(new Error(errorMessage));
     }
 
     // Enforce the type string to only contain sane characters.
     const typeUuid = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;
     if (!typeUuid.test(aType)) {
@@ -665,16 +674,17 @@ var Impl = {
    *   4) _delayedInitTask finished running and is nulled out.
    *
    * @return {Promise} Resolved when TelemetryController and TelemetrySession are fully
    *                   initialized. This is only used in tests.
    */
   setupTelemetry: function setupTelemetry(testing) {
     this._initStarted = true;
     this._shuttingDown = false;
+    this._shutDown = false;
     this._testMode = testing;
 
     this._log.trace("setupTelemetry");
 
     if (this._delayedInitTask) {
       this._log.error("setupTelemetry - init task already running");
       return this._delayedInitTaskDeferred.promise;
     }
@@ -725,16 +735,23 @@ var Impl = {
 
         // Load the ClientID.
         this._clientID = yield ClientID.getClientID();
 
         yield TelemetrySend.setup(this._testMode);
 
         // Perform TelemetrySession delayed init.
         yield TelemetrySession.delayedInit();
+
+        if (Preferences.get(PREF_NEWPROFILE_PING_ENABLED, false) &&
+            !TelemetrySession.newProfilePingSent) {
+          // Kick off the scheduling of the new-profile ping.
+          this.scheduleNewProfilePing();
+        }
+
         // Purge the pings archive by removing outdated pings. We don't wait for
         // this task to complete, but TelemetryStorage blocks on it during
         // shutdown.
         TelemetryStorage.runCleanPingArchiveTask();
 
         // Now that FHR/healthreporter is gone, make sure to remove FHR's DB from
         // the profile directory. This is a temporary measure that we should drop
         // in the future.
@@ -776,21 +793,27 @@ var Impl = {
   },
 
   // Do proper shutdown waiting and cleanup.
   async _cleanupOnShutdown() {
     if (!this._initialized) {
       return;
     }
 
+    this._shuttingDown = true;
+
     Preferences.ignore(PREF_BRANCH_LOG, configureLogging);
     this._detachObservers();
 
     // Now do an orderly shutdown.
     try {
+      if (this._delayedNewPingTask) {
+        await this._delayedNewPingTask.finalize();
+      }
+
       // Stop the datachoices infobar display.
       TelemetryReportingPolicy.shutdown();
       TelemetryEnvironment.shutdown();
 
       // Stop any ping sending.
       await TelemetrySend.shutdown();
 
       await TelemetrySession.shutdown();
@@ -802,33 +825,34 @@ var Impl = {
       await this._connectionsBarrier.wait();
 
       // Perform final shutdown operations.
       await TelemetryStorage.shutdown();
     } finally {
       // Reset state.
       this._initialized = false;
       this._initStarted = false;
-      this._shuttingDown = true;
+      this._shutDown = true;
     }
   },
 
   shutdown() {
     this._log.trace("shutdown");
 
     // We can be in one the following states here:
     // 1) setupTelemetry was never called
     // or it was called and
     //   2) _delayedInitTask was scheduled, but didn't run yet.
     //   3) _delayedInitTask is running now.
     //   4) _delayedInitTask finished running already.
 
     // This handles 1).
     if (!this._initStarted) {
       this._shuttingDown = true;
+      this._shutDown = true;
       return Promise.resolve();
     }
 
     // This handles 4).
     if (!this._delayedInitTask) {
       // We already ran the delayed initialization.
       return this._cleanupOnShutdown();
     }
@@ -867,16 +891,17 @@ var Impl = {
   _getState() {
     return {
       initialized: this._initialized,
       initStarted: this._initStarted,
       haveDelayedInitTask: !!this._delayedInitTask,
       shutdownBarrier: this._shutdownBarrier.state,
       connectionsBarrier: this._connectionsBarrier.state,
       sendModule: TelemetrySend.getShutdownState(),
+      haveDelayedNewProfileTask: !!this._delayedNewPingTask,
     };
   },
 
   /**
    * Called whenever the FHR Upload preference changes (e.g. when user disables FHR from
    * the preferences panel), this triggers sending the deletion ping.
    */
   _onUploadPrefChange() {
@@ -967,9 +992,55 @@ var Impl = {
 
     await sessionReset;
     await TelemetrySend.reset();
     await TelemetryStorage.reset();
     await TelemetryEnvironment.testReset();
 
     await controllerSetup;
   },
+
+  /**
+   * Schedule sending the "new-profile" ping.
+   */
+  scheduleNewProfilePing() {
+    this._log.trace("scheduleNewProfilePing");
+
+    const sendDelay =
+      Preferences.get(PREF_NEWPROFILE_PING_DELAY, NEWPROFILE_PING_DEFAULT_DELAY);
+
+    this._delayedNewPingTask = new DeferredTask(function* () {
+      try {
+        yield this.sendNewProfilePing();
+      } finally {
+        this._delayedNewPingTask = null;
+      }
+    }.bind(this), sendDelay);
+
+    this._delayedNewPingTask.arm();
+  },
+
+  /**
+   * Generate and send the new-profile ping
+   */
+  async sendNewProfilePing() {
+    this._log.trace("sendNewProfilePing - shutting down: " + this._shuttingDown);
+
+    // Generate the payload.
+    const payload = {
+      "reason": this._shuttingDown ? "shutdown" : "startup",
+    };
+
+    // Generate and send the "new-profile" ping. This uses the
+    // pingsender if we're shutting down.
+    let options = {
+      addClientId: true,
+      addEnvironment: true,
+      usePingSender: this._shuttingDown,
+    };
+    // TODO: we need to be smarter about when to send the ping (and save the
+    // state to file). |requestIdleCallback| is currently only accessible
+    // through DOM. See bug 1361996.
+    await TelemetryController.submitExternalPing("new-profile", payload, options)
+                             .then(() => TelemetrySession.markNewProfilePingSent(),
+                                   e => this._log.error("sendNewProfilePing - failed to submit new-profile ping", e));
+  },
 };
--- a/toolkit/components/telemetry/TelemetrySend.jsm
+++ b/toolkit/components/telemetry/TelemetrySend.jsm
@@ -781,17 +781,17 @@ var TelemetrySendImpl = {
    * @param {String} submissionURL The complete Telemetry-compliant URL for the ping.
    */
   _sendWithPingSender(pingId, submissionURL) {
     this._log.trace("_sendWithPingSender - sending " + pingId + " to " + submissionURL);
     try {
       const pingPath = OS.Path.join(TelemetryStorage.pingDirectoryPath, pingId);
       Telemetry.runPingSender(submissionURL, pingPath);
     } catch (e) {
-      this._log.error("_sendWithPingSender - failed to submit shutdown ping", e);
+      this._log.error("_sendWithPingSender - failed to submit ping", e);
     }
   },
 
   submitPing(ping, options) {
     this._log.trace("submitPing - ping id: " + ping.id + ", options: " + JSON.stringify(options));
 
     if (!this.sendingEnabled(ping)) {
       this._log.trace("submitPing - Telemetry is not allowed to send pings.");
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -593,16 +593,17 @@ this.TelemetrySession = Object.freeze({
    */
   getMetadata(reason) {
     return Impl.getMetadata(reason);
   },
   /**
    * Used only for testing purposes.
    */
   testReset() {
+    Impl._newProfilePingSent = false;
     Impl._sessionId = null;
     Impl._subsessionId = null;
     Impl._previousSessionId = null;
     Impl._previousSubsessionId = null;
     Impl._subsessionCounter = 0;
     Impl._profileSubsessionCounter = 0;
     Impl._subsessionStartActiveTicks = 0;
     Impl._subsessionStartTimeMonotonic = 0;
@@ -646,16 +647,33 @@ this.TelemetrySession = Object.freeze({
     return Impl.delayedInit();
   },
   /**
    * Send a notification.
    */
   observe(aSubject, aTopic, aData) {
     return Impl.observe(aSubject, aTopic, aData);
   },
+  /**
+   * Marks the "new-profile" ping as sent in the telemetry state file.
+   * @return {Promise} A promise resolved when the new telemetry state is saved to disk.
+   */
+  markNewProfilePingSent() {
+    return Impl.markNewProfilePingSent();
+  },
+  /**
+   * Returns if the "new-profile" ping has ever been sent for this profile.
+   * Please note that the returned value is trustworthy only after the delayed setup.
+   *
+   * @return {Boolean} True if the new profile ping was sent on this profile,
+   *         false otherwise.
+   */
+  get newProfilePingSent() {
+    return Impl._newProfilePingSent;
+  },
 });
 
 var Impl = {
   _histograms: {},
   _initialized: false,
   _logger: null,
   _prevValues: {},
   _slowSQLStartup: {},
@@ -710,17 +728,19 @@ var Impl = {
   // An accumulator of total memory across all processes. Only valid once the final child reports.
   _totalMemory: null,
   // A Set of outstanding USS report ids
   _childrenToHearFrom: null,
   // monotonically-increasing id for USS reports
   _nextTotalMemoryId: 1,
   _USSFromChildProcesses: null,
   _lastEnvironmentChangeDate: 0,
-
+  // We save whether the "new-profile" ping was sent yet, to
+  // survive profile refresh and migrations.
+  _newProfilePingSent: false,
 
   get _log() {
     if (!this._logger) {
       this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
     }
     return this._logger;
   },
 
@@ -2088,27 +2108,31 @@ var Impl = {
 
     this._previousSessionId = data.sessionId;
     this._previousSubsessionId = data.subsessionId;
     // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for
     // new subsession while loading still takes place. This will always be exactly
     // 1 - the current subsessions.
     this._profileSubsessionCounter = data.profileSubsessionCounter +
                                      this._subsessionCounter;
+    // If we don't have this flag in the state file, it means that this is an old profile.
+    // We don't want to send the "new-profile" ping on new profile, so se this to true.
+    this._newProfilePingSent = ("newProfilePingSent" in data) ? data.newProfilePingSent : true;
     return data;
   },
 
   /**
    * Get the session data object to serialise to disk.
    */
   _getSessionDataObject() {
     return {
       sessionId: this._sessionId,
       subsessionId: this._subsessionId,
       profileSubsessionCounter: this._profileSubsessionCounter,
+      newProfilePingSent: this._newProfilePingSent,
     };
   },
 
   _onEnvironmentChange(reason, oldEnvironment) {
     this._log.trace("_onEnvironmentChange", reason);
 
     let now = Policy.monotonicNow();
     let timeDelta = now - this._lastEnvironmentChangeDate;
@@ -2164,9 +2188,15 @@ var Impl = {
       // Overwrite the original reason.
       payload.info.reason = REASON_ABORTED_SESSION;
     } else {
       payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
     }
 
     return TelemetryController.saveAbortedSessionPing(payload);
   },
+
+  async markNewProfilePingSent() {
+    this._log.trace("markNewProfilePingSent");
+    this._newProfilePingSent = true;
+    return TelemetryStorage.saveSessionData(this._getSessionDataObject());
+  },
 };
--- a/toolkit/components/telemetry/docs/concepts/pings.rst
+++ b/toolkit/components/telemetry/docs/concepts/pings.rst
@@ -22,11 +22,11 @@ We send Telemetry with different ping ty
 
 Pings sent from code that ships with Firefox are listed in the :doc:`data documentation <../data/index>`.
 
 Important examples are:
 
 * :doc:`main <../data/main-ping>` - contains the information collected by Telemetry (Histograms, hang stacks, ...)
 * :doc:`saved-session <../data/main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side. This is temporary and will be removed soon.
 * :doc:`crash <../data/crash-ping>` - a ping that is captured and sent after Firefox crashes.
-* ``activation`` - *planned* - sent right after installation or profile creation
+* :doc:`new-profile <../data/new-profile-ping>` - sent on the first run of a new profile
 * ``upgrade`` - *planned* - sent right after an upgrade
 * :doc:`deletion <../data/deletion-ping>` - sent when FHR upload is disabled, requesting deletion of the data associated with this user
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/new-profile-ping.rst
@@ -0,0 +1,39 @@
+
+"new-profile" ping
+==================
+
+This opt-out ping is sent from Firefox Desktop 30 minutes after the browser is started, on the first session
+of a newly created profile. If the first session of a newly-created profile was shorter than 30 minutes, it
+gets sent using the :doc:`../internals/pingsender` at shutdown.
+
+.. note::
+
+  We don't sent the ping immediately after Telemetry completes initialization to give the user enough
+  time to tweak their data collection preferences.
+
+Structure:
+
+.. code-block:: js
+
+    {
+      type: "new-profile",
+      ... common ping data
+      clientId: <UUID>,
+      environment: { ... },
+      payload: {
+        reason: "startup" // or "shutdown"
+      }
+    }
+
+payload.reason
+--------------
+If this field contains ``startup``, then the ping was generated at the scheduled time after
+startup. If it contains ``shutdown``, then the browser was closed before the time the
+ping was scheduled. In the latter case, the ping is generated during shutdown and sent
+using the :doc:`../internals/pingsender`.
+
+Duplicate pings
+---------------
+We expect a low fraction of duplicates of this ping, mostly due to crashes happening
+right after sending the ping and before the telemetry state gets flushed to the disk. This should
+be fairly low in practice and manageable during the analysis phase.
--- a/toolkit/components/telemetry/docs/internals/preferences.rst
+++ b/toolkit/components/telemetry/docs/internals/preferences.rst
@@ -49,16 +49,24 @@ Preferences
 ``toolkit.telemetry.log.dump``
 
   Sets whether to dump Telemetry log messages to ``stdout`` too.
 
 ``toolkit.telemetry.shutdownPingSender.enabled``
 
   Allow the ``shutdown`` ping to be sent when the browser shuts down, from the second browsing session on, instead of the next restart, using the :doc:`ping sender <pingsender>`.
 
+``toolkit.telemetry.newProfilePing.enabled``
+
+  Enable the :doc:`../data/new-profile` ping on new profiles.
+
+``toolkit.telemetry.newProfilePing.delay``
+
+  Controls the delay after which the :doc:`../data/new-profile` is sent on new profiles.
+
 Data-choices notification
 -------------------------
 
 ``toolkit.telemetry.reportingpolicy.firstRun``
 
   This preference is not present until the first run. After, its value is set to false. This is used to show the infobar with a more aggressive timeout if it wasn't shown yet.
 
 ``datareporting.policy.firstRunURL``
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -3,16 +3,17 @@
 */
 /* This testcase triggers two telemetry pings.
  *
  * Telemetry code keeps histograms of past telemetry pings. The first
  * ping populates these histograms. One of those histograms is then
  * checked in the second request.
  */
 
+Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/ClientID.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
@@ -31,16 +32,20 @@ const APP_NAME = "XPCShell";
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_UNIFIED = PREF_BRANCH + "unified";
 
 var gClientID = null;
 
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
+});
+
 function sendPing(aSendClientId, aSendEnvironment) {
   if (PingServer.started) {
     TelemetrySend.setServer("http://localhost:" + PingServer.port);
   } else {
     TelemetrySend.setServer("http://doesnotexist");
   }
 
   let options = {
@@ -501,16 +506,104 @@ add_task(function* test_telemetryCleanFH
 
   // Trigger the cleanup and check that the files were removed.
   yield TelemetryStorage.removeFHRDatabase();
   for (let dbFilePath of DEFAULT_DB_PATHS) {
     Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath);
   }
 });
 
+add_task(function* test_sendNewProfile() {
+  if (gIsAndroid ||
+      (AppConstants.platform == "linux" && OS.Constants.Sys.bits == 32)) {
+    // We don't support the pingsender on Android, yet, see bug 1335917.
+    // We also don't suppor the pingsender testing on Treeherder for
+    // Linux 32 bit (due to missing libraries). So skip it there too.
+    // See bug 1310703 comment 78.
+    return;
+  }
+
+  const NEWPROFILE_PING_TYPE = "new-profile";
+  const PREF_NEWPROFILE_ENABLED = "toolkit.telemetry.newProfilePing.enabled";
+  const PREF_NEWPROFILE_DELAY = "toolkit.telemetry.newProfilePing.delay";
+
+  // Make sure Telemetry is shut down before beginning and that we have
+  // no pending pings.
+  let resetTest = async function() {
+    await TelemetryController.testShutdown();
+    await TelemetryStorage.testClearPendingPings();
+    PingServer.clearRequests();
+  };
+  yield resetTest();
+
+  // Make sure to reset all the new-profile ping prefs.
+  const stateFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
+  yield OS.File.remove(stateFilePath, { ignoreAbsent: true });
+  Preferences.set(PREF_NEWPROFILE_DELAY, 1);
+  Preferences.set(PREF_NEWPROFILE_ENABLED, true);
+
+  // Check that a new-profile ping is sent on the first session.
+  let nextReq = PingServer.promiseNextRequest();
+  yield TelemetryController.testReset();
+  let req = yield nextReq;
+  let ping = decodeRequestPayload(req);
+  checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+  Assert.equal(ping.payload.reason, "startup",
+               "The new-profile ping generated after startup must have the correct reason");
+
+  // Check that is not sent with the pingsender during startup.
+  Assert.throws(() => req.getHeader("X-PingSender-Version"),
+                "Should not have used the pingsender.");
+
+  // Make sure that the new-profile ping is sent at shutdown if it wasn't sent before.
+  yield resetTest();
+  yield OS.File.remove(stateFilePath, { ignoreAbsent: true });
+  Preferences.reset(PREF_NEWPROFILE_DELAY);
+
+  nextReq = PingServer.promiseNextRequest();
+  yield TelemetryController.testReset();
+  yield TelemetryController.testShutdown();
+  req = yield nextReq;
+  ping = decodeRequestPayload(req);
+  checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+  Assert.equal(ping.payload.reason, "shutdown",
+               "The new-profile ping generated at shutdown must have the correct reason");
+
+  // Check that the new-profile ping is sent at shutdown using the pingsender.
+  Assert.equal(req.getHeader("User-Agent"), "pingsender/1.0",
+               "Should have received the correct user agent string.");
+  Assert.equal(req.getHeader("X-PingSender-Version"), "1.0",
+               "Should have received the correct PingSender version string.");
+
+  // Check that no new-profile ping is sent on second sessions, not at startup
+  // nor at shutdown.
+  yield resetTest();
+  PingServer.registerPingHandler(
+    () => Assert.ok(false, "The new-profile ping must be sent only on new profiles."));
+  yield TelemetryController.testReset();
+  yield TelemetryController.testShutdown();
+
+  // Check that we don't send the new-profile ping if the profile already contains
+  // a state file (but no "newProfilePingSent" property).
+  yield resetTest();
+  yield OS.File.remove(stateFilePath, { ignoreAbsent: true });
+  const sessionState = {
+    sessionId: null,
+    subsessionId: null,
+    profileSubsessionCounter: 3785,
+  };
+  yield CommonUtils.writeJSON(sessionState, stateFilePath);
+  yield TelemetryController.testReset();
+  yield TelemetryController.testShutdown();
+
+  // Reset the pref and restart Telemetry.
+  Preferences.reset(PREF_NEWPROFILE_ENABLED);
+  PingServer.resetPingHandler();
+});
+
 // Testing shutdown and checking that pings sent afterwards are rejected.
 add_task(function* test_pingRejection() {
   yield TelemetryController.testReset();
   yield TelemetryController.testShutdown();
   yield sendPing(false, false)
     .then(() => Assert.ok(false, "Pings submitted after shutdown must be rejected."),
           () => Assert.ok(true, "Ping submitted after shutdown correctly rejected."));
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -137,17 +137,17 @@ function checkPingFormat(aPing, aType, a
     version: APP_VERSION,
     vendor: "Mozilla",
     platformVersion: PLATFORM_VERSION,
     xpcomAbi: "noarch-spidermonkey",
   };
 
   // Check that the ping contains all the mandatory fields.
   for (let f of MANDATORY_PING_FIELDS) {
-    Assert.ok(f in aPing, f + "must be available.");
+    Assert.ok(f in aPing, f + " must be available.");
   }
 
   Assert.equal(aPing.type, aType, "The ping must have the correct type.");
   Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version.");
 
   // Test the application section.
   for (let f in APPLICATION_TEST_DATA) {
     Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f],