Bug 1295058 - Make sync ping only submit every 12 hours or on browser shutdown r?markh,gfritzsche draft
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Wed, 31 Aug 2016 12:50:34 -0400
changeset 408833 3f3877eb2e493861c1e80ad7dc23f6e20a90b773
parent 408832 9beac1f92bd0b559e46039061a3ce9a09350ce96
child 530176 401ff5eb1d23d031622df30717b22ccb1b3c27f4
push id28284
push userbmo:tchiovoloni@mozilla.com
push dateThu, 01 Sep 2016 17:10:06 +0000
reviewersmarkh, gfritzsche
bugs1295058
milestone51.0a1
Bug 1295058 - Make sync ping only submit every 12 hours or on browser shutdown r?markh,gfritzsche MozReview-Commit-ID: 9BcMGAP0w7U
services/sync/modules/telemetry.js
services/sync/services-sync.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/sync_ping_schema.json
services/sync/tests/unit/test_telemetry.js
toolkit/components/telemetry/docs/data/sync-ping.rst
--- a/services/sync/modules/telemetry.js
+++ b/services/sync/modules/telemetry.js
@@ -205,23 +205,21 @@ class TelemetryRecord {
 
     // All engines that have finished (ie, does not include the "current" one)
     // We omit this from the ping if it's empty.
     this.engines = [];
     // The engine that has started but not yet stopped.
     this.currentEngine = null;
   }
 
-  // toJSON returns the actual payload we will submit.
   toJSON() {
     let result = {
       when: this.when,
       uid: this.uid,
       took: this.took,
-      version: PING_FORMAT_VERSION,
       failureReason: this.failureReason,
       status: this.status,
     };
     let engines = [];
     for (let engine of this.engines) {
       engines.push(engine.toJSON());
     }
     if (engines.length > 0) {
@@ -328,38 +326,69 @@ class TelemetryRecord {
 
 class SyncTelemetryImpl {
   constructor(allowedEngines) {
     log.level = Log.Level[Svc.Prefs.get("log.logger.telemetry", "Trace")];
     // This is accessible so we can enable custom engines during tests.
     this.allowedEngines = allowedEngines;
     this.current = null;
     this.setupObservers();
+
+    this.payloads = [];
+    this.discarded = 0;
+    this.maxPayloadCount = Svc.Prefs.get("telemetry.maxPayloadCount");
+    this.submissionInterval = Svc.Prefs.get("telemetry.submissionInterval") * 1000;
+    this.lastSubmissionTime = Telemetry.msSinceProcessStart();
+  }
+
+  getPingJSON(reason) {
+    return {
+      why: reason,
+      discarded: this.discarded || undefined,
+      version: PING_FORMAT_VERSION,
+      syncs: this.payloads.slice(),
+    };
+  }
+
+  finish(reason) {
+    // Note that we might be in the middle of a sync right now, and so we don't
+    // want to touch this.current.
+    let result = this.getPingJSON(reason);
+    this.payloads = [];
+    this.discarded = 0;
+    this.submit(result);
   }
 
   setupObservers() {
     for (let topic of TOPICS) {
       Observers.add(topic, this, this);
     }
   }
 
   shutdown() {
+    this.finish("shutdown");
     for (let topic of TOPICS) {
       Observers.remove(topic, this, this);
     }
   }
 
   submit(record) {
-    TelemetryController.submitExternalPing("sync", record);
+    // We still call submit() with possibly illegal payloads so that tests can
+    // know that the ping was built. We don't end up submitting them, however.
+    if (record.syncs.length) {
+      TelemetryController.submitExternalPing("sync", record);
+    }
   }
 
+
   onSyncStarted() {
     if (this.current) {
       log.warn("Observed weave:service:sync:start, but we're already recording a sync!");
       // Just discard the old record, consistent with our handling of engines, above.
+      this.current = null;
     }
     this.current = new TelemetryRecord(this.allowedEngines);
   }
 
   _checkCurrent(topic) {
     if (!this.current) {
       log.warn(`Observed notification ${topic} but no current sync is being recorded.`);
       return false;
@@ -368,19 +397,26 @@ class SyncTelemetryImpl {
   }
 
   onSyncFinished(error) {
     if (!this.current) {
       log.warn("onSyncFinished but we aren't recording");
       return;
     }
     this.current.finished(error);
-    let current = this.current;
+    if (this.payloads.length < this.maxPayloadCount) {
+      this.payloads.push(this.current.toJSON());
+    } else {
+      ++this.discarded;
+    }
     this.current = null;
-    this.submit(current.toJSON());
+    if ((Telemetry.msSinceProcessStart() - this.lastSubmissionTime) > this.submissionInterval) {
+      this.finish("schedule");
+      this.lastSubmissionTime = Telemetry.msSinceProcessStart();
+    }
   }
 
   observe(subject, topic, data) {
     log.trace(`observed ${topic} ${data}`);
 
     switch (topic) {
       case "xpcom-shutdown":
         this.shutdown();
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -70,8 +70,11 @@ pref("services.sync.log.logger.engine.ta
 pref("services.sync.log.logger.engine.addons", "Debug");
 pref("services.sync.log.logger.engine.apps", "Debug");
 pref("services.sync.log.logger.identity", "Debug");
 pref("services.sync.log.logger.userapi", "Debug");
 pref("services.sync.log.cryptoDebug", false);
 
 pref("services.sync.fxa.termsURL", "https://accounts.firefox.com/legal/terms");
 pref("services.sync.fxa.privacyURL", "https://accounts.firefox.com/legal/privacy");
+
+pref("services.sync.telemetry.submissionInterval", 43200); // 12 hours in seconds
+pref("services.sync.telemetry.maxPayloadCount", 500);
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -242,79 +242,89 @@ function do_check_array_eq(a1, a2) {
 // engine names to its list of allowed engines.
 function get_sync_test_telemetry() {
   let ns = {};
   Cu.import("resource://services-sync/telemetry.js", ns);
   let testEngines = ["rotary", "steam", "sterling", "catapult"];
   for (let engineName of testEngines) {
     ns.SyncTelemetry.allowedEngines.add(engineName);
   }
+  ns.SyncTelemetry.submissionInterval = -1;
   return ns.SyncTelemetry;
 }
 
 function assert_valid_ping(record) {
   if (record) {
     if (!SyncPingValidator(record)) {
       deepEqual([], SyncPingValidator.errors, "Sync telemetry ping validation failed");
     }
     equal(record.version, 1);
-    lessOrEqual(record.when, Date.now());
+    record.syncs.forEach(p => {
+      lessOrEqual(p.when, Date.now());
+    });
   }
 }
 
 // Asserts that `ping` is a ping that doesn't contain any failure information
 function assert_success_ping(ping) {
   ok(!!ping);
   assert_valid_ping(ping);
-  ok(!ping.failureReason);
-  equal(undefined, ping.status);
-  greater(ping.engines.length, 0);
-  for (let e of ping.engines) {
-    ok(!e.failureReason);
-    equal(undefined, e.status);
-    if (e.outgoing) {
-      for (let o of e.outgoing) {
-        equal(undefined, o.failed);
-        notEqual(undefined, o.sent);
+  ping.syncs.forEach(record => {
+    ok(!record.failureReason);
+    equal(undefined, record.status);
+    greater(record.engines.length, 0);
+    for (let e of record.engines) {
+      ok(!e.failureReason);
+      equal(undefined, e.status);
+      if (e.outgoing) {
+        for (let o of e.outgoing) {
+          equal(undefined, o.failed);
+          notEqual(undefined, o.sent);
+        }
+      }
+      if (e.incoming) {
+        equal(undefined, e.incoming.failed);
+        equal(undefined, e.incoming.newFailed);
+        notEqual(undefined, e.incoming.applied || e.incoming.reconciled);
       }
     }
-    if (e.incoming) {
-      equal(undefined, e.incoming.failed);
-      equal(undefined, e.incoming.newFailed);
-      notEqual(undefined, e.incoming.applied || e.incoming.reconciled);
-    }
-  }
+  });
 }
 
 // Hooks into telemetry to validate all pings after calling.
 function validate_all_future_pings() {
   let telem = get_sync_test_telemetry();
   telem.submit = assert_valid_ping;
 }
 
-function wait_for_ping(callback, allowErrorPings) {
+function wait_for_ping(callback, allowErrorPings, getFullPing = false) {
   return new Promise(resolve => {
     let telem = get_sync_test_telemetry();
     let oldSubmit = telem.submit;
     telem.submit = function(record) {
       telem.submit = oldSubmit;
       if (allowErrorPings) {
         assert_valid_ping(record);
       } else {
         assert_success_ping(record);
       }
-      resolve(record);
+      if (getFullPing) {
+        resolve(record);
+      } else {
+        equal(record.syncs.length, 1);
+        resolve(record.syncs[0]);
+      }
     };
     callback();
   });
 }
 
 // Short helper for wait_for_ping
-function sync_and_validate_telem(allowErrorPings) {
-  return wait_for_ping(() => Service.sync(), allowErrorPings);
+function sync_and_validate_telem(allowErrorPings, getFullPing = false) {
+  return wait_for_ping(() => Service.sync(), allowErrorPings, getFullPing);
 }
 
 // Used for the (many) cases where we do a 'partial' sync, where only a single
 // engine is actually synced, but we still want to ensure we're generating a
 // valid ping. Returns a promise that resolves to the ping, or rejects with the
 // thrown error after calling an optional callback.
 function sync_engine_and_validate_telem(engine, allowErrorPings, onError) {
   return new Promise((resolve, reject) => {
@@ -332,57 +342,60 @@ function sync_engine_and_validate_telem(
     // status properties are the same as they were initially, that it's just
     // a leftover.
     // This is only an issue since we're triggering the sync of just one engine,
     // without doing any other parts of the sync.
     let initialServiceStatus = ns.Status._service;
     let initialSyncStatus = ns.Status._sync;
 
     let oldSubmit = telem.submit;
-    telem.submit = function(record) {
+    telem.submit = function(ping) {
       telem.submit = oldSubmit;
-      if (record && record.status) {
-        // did we see anything to lead us to believe that something bad actually happened
-        let realProblem = record.failureReason || record.engines.some(e => {
-          if (e.failureReason || e.status) {
-            return true;
-          }
-          if (e.outgoing && e.outgoing.some(o => o.failed > 0)) {
-            return true;
-          }
-          return e.incoming && e.incoming.failed;
-        });
-        if (!realProblem) {
-          // no, so if the status is the same as it was initially, just assume
-          // that its leftover and that we can ignore it.
-          if (record.status.sync && record.status.sync == initialSyncStatus) {
-            delete record.status.sync;
-          }
-          if (record.status.service && record.status.service == initialServiceStatus) {
-            delete record.status.service;
-          }
-          if (!record.status.sync && !record.status.service) {
-            delete record.status;
+      ping.syncs.forEach(record => {
+        if (record && record.status) {
+          // did we see anything to lead us to believe that something bad actually happened
+          let realProblem = record.failureReason || record.engines.some(e => {
+            if (e.failureReason || e.status) {
+              return true;
+            }
+            if (e.outgoing && e.outgoing.some(o => o.failed > 0)) {
+              return true;
+            }
+            return e.incoming && e.incoming.failed;
+          });
+          if (!realProblem) {
+            // no, so if the status is the same as it was initially, just assume
+            // that its leftover and that we can ignore it.
+            if (record.status.sync && record.status.sync == initialSyncStatus) {
+              delete record.status.sync;
+            }
+            if (record.status.service && record.status.service == initialServiceStatus) {
+              delete record.status.service;
+            }
+            if (!record.status.sync && !record.status.service) {
+              delete record.status;
+            }
           }
         }
-      }
+      });
       if (allowErrorPings) {
-        assert_valid_ping(record);
+        assert_valid_ping(ping);
       } else {
-        assert_success_ping(record);
+        assert_success_ping(ping);
       }
+      equal(ping.syncs.length, 1);
       if (caughtError) {
         if (onError) {
-          onError(record);
+          onError(ping.syncs[0]);
         }
         reject(caughtError);
       } else {
-        resolve(record);
+        resolve(ping.syncs[0]);
       }
-    };
+    }
     Svc.Obs.notify("weave:service:sync:start");
     try {
       engine.sync();
     } catch (e) {
       caughtError = e;
     }
     if (caughtError) {
       Svc.Obs.notify("weave:service:sync:error", caughtError);
--- a/services/sync/tests/unit/sync_ping_schema.json
+++ b/services/sync/tests/unit/sync_ping_schema.json
@@ -1,47 +1,58 @@
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "description": "schema for Sync pings, documentation avaliable in toolkit/components/telemetry/docs/sync-ping.rst",
   "type": "object",
   "additionalProperties": false,
-  "required": ["when", "version", "took", "uid"],
+  "required": ["version", "syncs", "why"],
   "properties": {
     "version": { "type": "integer", "minimum": 0 },
-    "didLogin": { "type": "boolean" },
-    "when": { "type": "integer" },
-    "status": {
-      "type": "object",
-      "anyOf": [
-        {"required": ["sync"]},
-        {"required": ["service"]}
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "sync": { "type": "string" },
-        "service": { "type": "string" }
-      }
-    },
-    "why": { "enum": ["startup", "schedule", "score", "user", "tabs"] },
-    "took": { "type": "integer", "minimum": -1 },
-    "uid": {
-      "type": "string",
-      "oneOf": [
-        { "pattern": "^[0-9a-f]{32}$" },
-        { "maxLength": 0 }
-      ]
-    },
-    "failureReason": { "$ref": "#/definitions/error" },
-    "engines": {
+    "discarded": { "type": "integer", "minimum": 1 },
+    "why": { "enum": ["shutdown", "schedule"] },
+    "syncs": {
       "type": "array",
       "minItems": 1,
-      "items": { "$ref": "#/definitions/engine" }
+      "items": { "$ref": "#/definitions/payload" }
     }
   },
   "definitions": {
+    "payload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["when", "uid", "took"],
+      "properties": {
+        "didLogin": { "type": "boolean" },
+        "when": { "type": "integer" },
+        "uid": {
+          "type": "string",
+          "pattern": "^[0-9a-f]{32}$"
+        },
+        "status": {
+          "type": "object",
+          "anyOf": [
+            { "required": ["sync"] },
+            { "required": ["service"] }
+          ],
+          "additionalProperties": false,
+          "properties": {
+            "sync": { "type": "string" },
+            "service": { "type": "string" }
+          }
+        },
+        "why": { "enum": ["startup", "schedule", "score", "user", "tabs"] },
+        "took": { "type": "integer", "minimum": -1 },
+        "failureReason": { "$ref": "#/definitions/error" },
+        "engines": {
+          "type": "array",
+          "minItems": 1,
+          "items": { "$ref": "#/definitions/engine" }
+        }
+      }
+    },
     "engine": {
       "required": ["name"],
       "additionalProperties": false,
       "properties": {
         "failureReason": { "$ref": "#/definitions/error" },
         "name": { "enum": ["addons", "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs"] },
         "took": { "type": "integer", "minimum": 1 },
         "status": { "type": "string" },
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -168,17 +168,16 @@ add_task(function *test_uploading() {
     ok(!ping.engines[0].incoming);
 
     PlacesUtils.bookmarks.setItemTitle(bmk_id, "New Title");
 
     store.wipe();
     engine.resetClient();
 
     ping = yield sync_engine_and_validate_telem(engine, false);
-
     equal(ping.engines.length, 1);
     equal(ping.engines[0].name, "bookmarks");
     equal(ping.engines[0].outgoing.length, 1);
     ok(!!ping.engines[0].incoming);
 
   } finally {
     // Clean up.
     store.wipe();
@@ -439,8 +438,58 @@ add_task(function* test_nserror() {
       name: "nserror",
       code: Cr.NS_ERROR_UNKNOWN_HOST
     });
   } finally {
     Service.engineManager.unregister(engine);
     yield cleanAndGo(server);
   }
 });
+
+
+add_identity_test(this, function *test_discarding() {
+  let helper = track_collections_helper();
+  let upd = helper.with_updated_collection;
+  let telem = get_sync_test_telemetry();
+  telem.maxPayloadCount = 2;
+  telem.submissionInterval = Infinity;
+  let oldSubmit = telem.submit;
+
+  let server;
+  try {
+
+    yield configureIdentity({ username: "johndoe" });
+    let handlers = {
+      "/1.1/johndoe/info/collections": helper.handler,
+      "/1.1/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
+      "/1.1/johndoe/storage/meta/global": upd("meta",  new ServerWBO("global").handler())
+    };
+
+    let collections = ["clients", "bookmarks", "forms", "history", "passwords", "prefs", "tabs"];
+
+    for (let coll of collections) {
+      handlers["/1.1/johndoe/storage/" + coll] = upd(coll, new ServerCollection({}, true).handler());
+    }
+
+    server = httpd_setup(handlers);
+    Service.serverURL = server.baseURI;
+    telem.submit = () => ok(false, "Submitted telemetry ping when we should not have");
+
+    for (let i = 0; i < 5; ++i) {
+      Service.sync();
+    }
+    telem.submit = oldSubmit;
+    telem.submissionInterval = -1;
+    let ping = yield sync_and_validate_telem(true, true); // with this we've synced 6 times
+    equal(ping.syncs.length, 2);
+    equal(ping.discarded, 4);
+  } finally {
+    telem.maxPayloadCount = 500;
+    telem.submissionInterval = -1;
+    telem.submit = oldSubmit;
+    if (server) {
+      yield new Promise(resolve => server.stop(resolve));
+    }
+  }
+})
+
+
+
--- a/toolkit/components/telemetry/docs/data/sync-ping.rst
+++ b/toolkit/components/telemetry/docs/data/sync-ping.rst
@@ -1,118 +1,129 @@
 
 "sync" ping
 ===========
 
-This ping is generated after a sync is completed, for both successful and failed syncs. It's payload contains measurements
-pertaining to sync performance and error information. It does not contain the enviroment block, nor the clientId.
+This is an aggregated format that contains information about each sync that occurred during a timeframe. It is submitted every 12 hours, and on browser shutdown, but only if the syncs property would not be empty. The ping does not contain the enviroment block, nor the clientId.
+
+Each item in the syncs property is generated after a sync is completed, for both successful and failed syncs, and contains measurements pertaining to sync performance and error information.
 
 A JSON-schema document describing the exact format of the ping's payload property can be found at `services/sync/tests/unit/sync\_ping\_schema.json <https://dxr.mozilla.org/mozilla-central/source/services/sync/tests/unit/sync_ping_schema.json>`_.
 
 Structure:
 
 .. code-block:: js
 
     {
       version: 4,
       type: "sync",
       ... common ping data
       payload: {
         version: 1,
-        when: <integer milliseconds since epoch>,
-        took: <integer duration in milliseconds>,
-        uid: <string>, // Hashed FxA unique ID, or string of 32 zeros.
-        didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
-        why: <string>, // Optional, why the sync occured, excluded if we don't know.
+        discarded: <integer count> // Number of syncs discarded -- left out if zero.
+        why: <string>, // Why did we submit the ping? Either "shutdown" or "schedule".
+        // Array of recorded syncs. The ping is not submitted if this would be empty
+        syncs: [{
+          when: <integer milliseconds since epoch>,
+          took: <integer duration in milliseconds>,
+          uid: <string>, // Hashed FxA unique ID, or string of 32 zeros.
+          didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
+          why: <string>, // Optional, why the sync occured, excluded if we don't know.
 
-        // Optional, excluded if there was no error.
-        failureReason: {
-          name: <string>, // "httperror", "networkerror", "shutdownerror", etc.
-          code: <integer>, // Only present for "httperror" and "networkerror".
-          error: <string>, // Only present for "othererror" and "unexpectederror".
-          from: <string>, // Optional, and only present for "autherror".
-        },
-        // Internal sync status information. Omitted if it would be empty.
-        status: {
-          sync: <string>, // The value of the Status.sync property, unless it indicates success.
-          service: <string>, // The value of the Status.service property, unless it indicates success.
-        },
-        // Information about each engine's sync.
-        engines: [
-          {
-            name: <string>, // "bookmarks", "tabs", etc.
-            took: <integer duration in milliseconds>, // Optional, values of 0 are omitted.
+          // Optional, excluded if there was no error.
+          failureReason: {
+            name: <string>, // "httperror", "networkerror", "shutdownerror", etc.
+            code: <integer>, // Only present for "httperror" and "networkerror".
+            error: <string>, // Only present for "othererror" and "unexpectederror".
+            from: <string>, // Optional, and only present for "autherror".
+          },
+          // Internal sync status information. Omitted if it would be empty.
+          status: {
+            sync: <string>, // The value of the Status.sync property, unless it indicates success.
+            service: <string>, // The value of the Status.service property, unless it indicates success.
+          },
+          // Information about each engine's sync.
+          engines: [
+            {
+              name: <string>, // "bookmarks", "tabs", etc.
+              took: <integer duration in milliseconds>, // Optional, values of 0 are omitted.
 
-            status: <string>, // The value of Status.engines, if it holds a non-success value.
+              status: <string>, // The value of Status.engines, if it holds a non-success value.
 
-            // Optional, excluded if all items would be 0. A missing item indicates a value of 0.
-            incoming: {
-              applied: <integer>, // Number of records applied
-              succeeded: <integer>, // Number of records that applied without error
-              failed: <integer>, // Number of records that failed to apply
-              newFailed: <integer>, // Number of records that failed for the first time this sync
-              reconciled: <integer>, // Number of records that were reconciled
-            },
+              // Optional, excluded if all items would be 0. A missing item indicates a value of 0.
+              incoming: {
+                applied: <integer>, // Number of records applied
+                succeeded: <integer>, // Number of records that applied without error
+                failed: <integer>, // Number of records that failed to apply
+                newFailed: <integer>, // Number of records that failed for the first time this sync
+                reconciled: <integer>, // Number of records that were reconciled
+              },
 
-            // Optional, excluded if it would be empty. Records that would be
-            // empty (e.g. 0 sent and 0 failed) are omitted.
-            outgoing: [
-              {
-                sent: <integer>, // Number of outgoing records sent. Zero values are omitted.
-                failed: <integer>, // Number that failed to send. Zero values are omitted.
-              }
-            ],
-            // Optional, excluded if there were no errors
-            failureReason: { ... }, // Same as above.
+              // Optional, excluded if it would be empty. Records that would be
+              // empty (e.g. 0 sent and 0 failed) are omitted.
+              outgoing: [
+                {
+                  sent: <integer>, // Number of outgoing records sent. Zero values are omitted.
+                  failed: <integer>, // Number that failed to send. Zero values are omitted.
+                }
+              ],
+              // Optional, excluded if there were no errors
+              failureReason: { ... }, // Same as above.
 
-            // Optional, excluded if it would be empty or if the engine cannot
-            // or did not run validation on itself. Entries with a count of 0
-            // are excluded.
-            validation: [
-              {
-                name: <string>, // The problem identified.
-                count: <integer>, // Number of times it occurred.
-              }
-            ]
-          }
-        ]
+              // Optional, excluded if it would be empty or if the engine cannot
+              // or did not run validation on itself. Entries with a count of 0
+              // are excluded.
+              validation: [
+                {
+                  name: <string>, // The problem identified.
+                  count: <integer>, // Number of times it occurred.
+                }
+              ]
+            }
+          ]
+        }]
       }
     }
 
 info
 ----
 
-took
-~~~~
+discarded
+~~~~~~~~~
+
+The ping may only contain a certain number of entries in the ``"syncs"`` array, currently 500 (it is determined by the ``"services.sync.telemetry.maxPayloadCount"`` preference).  Entries beyond this are discarded, and recorded in the discarded count.
+
+syncs.took
+~~~~~~~~~~
 
 These values should be monotonic.  If we can't get a monotonic timestamp, -1 will be reported on the payload, and the values will be omitted from the engines. Additionally, the value will be omitted from an engine if it would be 0 (either due to timer inaccuracy or finishing instantaneously).
 
-uid
-~~~
+syncs.uid
+~~~~~~~~~
 
 This property containing a hash of the FxA account identifier, which is a 32 character hexidecimal string.  In the case that we are unable to authenticate with FxA and have never authenticated in the past, it will be a placeholder string consisting of 32 repeated ``0`` characters.
 
-why
-~~~
+syncs.why
+~~~~~~~~~
 
 One of the following values:
 
 - ``startup``: This is the first sync triggered after browser startup.
 - ``schedule``: This is a sync triggered because it has been too long since the last sync.
 - ``score``: This sync is triggered by a high score value one of sync's trackers, indicating that many changes have occurred since the last sync.
 - ``user``: The user manually triggered the sync.
 - ``tabs``: The user opened the synced tabs sidebar, which triggers a sync.
 
-status
-~~~~~~
+syncs.status
+~~~~~~~~~~~~
 
 The ``engine.status``, ``payload.status.sync``, and ``payload.status.service`` properties are sync error codes, which are listed in `services/sync/modules/constants.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/constants.js>`_, and success values are not reported.
 
-failureReason
-~~~~~~~~~~~~~
+syncs.failureReason
+~~~~~~~~~~~~~~~~~~~
 
 Stores error information, if any is present. Always contains the "name" property, which identifies the type of error it is. The types can be.
 
 - ``httperror``: Indicates that we recieved an HTTP error response code, but are unable to be more specific about the error. Contains the following properties:
 
     - ``code``: Integer HTTP status code.
 
 - ``nserror``: Indicates that an exception with the provided error code caused sync to fail.
@@ -128,17 +139,17 @@ Stores error information, if any is pres
 - ``othererror``: Indicates that it is a sync error code that we are unable to give more specific information on. As with the ``syncStatus`` property, it is a sync error code, which are listed in `services/sync/modules/constants.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/constants.js>`_.
 
     - ``error``: String identifying which error was present.
 
 - ``unexpectederror``: Indicates that some other error caused sync to fail, typically an uncaught exception.
 
    - ``error``: The message provided by the error.
 
-engine.name
-~~~~~~~~~~~
+syncs.engine.name
+~~~~~~~~~~~~~~~~~
 
 Third-party engines are not reported, so only the following values are allowed: ``addons``, ``bookmarks``, ``clients``, ``forms``, ``history``, ``passwords``, ``prefs``, and ``tabs``.
 
-engine.validation
-~~~~~~~~~~~~~~~~~
+syncs.engine.validation
+~~~~~~~~~~~~~~~~~~~~~~~
 
 For engines that can run validation on themselves, an array of objects describing validation errors that have occurred. Items that would have a count of 0 are excluded. Each engine will have its own set of items that it might put in the ``name`` field, but there are a finite number. See ``BookmarkProblemData.getSummary`` in `services/sync/modules/bookmark\_validator.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/bookmark_validator.js>`_ for an example.