--- 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.