diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js')
-rw-r--r-- | toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js | 1109 |
1 files changed, 1109 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js new file mode 100644 index 0000000000..4369c5a608 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js @@ -0,0 +1,1109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +ChromeUtils.defineESModuleGetters(this, { + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const PRERELEASE_CHANNELS = Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +function checkEventFormat(events) { + Assert.ok(Array.isArray(events), "Events should be serialized to an array."); + for (let e of events) { + Assert.ok(Array.isArray(e), "Event should be an array."); + Assert.greaterOrEqual( + e.length, + 4, + "Event should have at least 4 elements." + ); + Assert.lessOrEqual(e.length, 6, "Event should have at most 6 elements."); + + Assert.equal(typeof e[0], "number", "Element 0 should be a number."); + Assert.equal(typeof e[1], "string", "Element 1 should be a string."); + Assert.equal(typeof e[2], "string", "Element 2 should be a string."); + Assert.equal(typeof e[3], "string", "Element 3 should be a string."); + + if (e.length > 4) { + Assert.ok( + e[4] === null || typeof e[4] == "string", + "Event element 4 should be null or a string." + ); + } + if (e.length > 5) { + Assert.ok( + e[5] === null || typeof e[5] == "object", + "Event element 5 should be null or an object." + ); + } + + let extra = e[5]; + if (extra) { + Assert.ok( + Object.keys(extra).every(k => typeof k == "string"), + "All extra keys should be strings." + ); + Assert.ok( + Object.values(extra).every(v => typeof v == "string"), + "All extra values should be strings." + ); + } + } +} + +/** + * @param summaries is of the form + * [{process, [event category, event object, event method], count}] + * @param clearScalars - true if you want to clear the scalars + */ +function checkEventSummary(summaries, clearScalars) { + let scalars = Telemetry.getSnapshotForKeyedScalars("main", clearScalars); + + for (let [process, [category, eObject, method], count] of summaries) { + let uniqueEventName = `${category}#${eObject}#${method}`; + let summaryCount; + if (process === "dynamic") { + summaryCount = + scalars.dynamic["telemetry.dynamic_event_counts"][uniqueEventName]; + } else { + summaryCount = + scalars[process]["telemetry.event_counts"][uniqueEventName]; + } + Assert.equal( + summaryCount, + count, + `${uniqueEventName} had wrong summary count` + ); + } +} + +function checkRegistrationFailure(failureType) { + let snapshot = Telemetry.getSnapshotForHistograms("main", true); + Assert.ok( + "parent" in snapshot, + "There should be at least one parent histogram when checking for registration failures." + ); + Assert.ok( + "TELEMETRY_EVENT_REGISTRATION_ERROR" in snapshot.parent, + "TELEMETRY_EVENT_REGISTRATION_ERROR should exist when checking for registration failures." + ); + let values = snapshot.parent.TELEMETRY_EVENT_REGISTRATION_ERROR.values; + Assert.ok( + !!values, + "TELEMETRY_EVENT_REGISTRATION_ERROR's values should exist when checking for registration failures." + ); + Assert.equal( + values[failureType], + 1, + `Event registration ought to have failed due to type ${failureType}` + ); +} + +function checkRecordingFailure(failureType) { + let snapshot = Telemetry.getSnapshotForHistograms("main", true); + Assert.ok( + "parent" in snapshot, + "There should be at least one parent histogram when checking for recording failures." + ); + Assert.ok( + "TELEMETRY_EVENT_RECORDING_ERROR" in snapshot.parent, + "TELEMETRY_EVENT_RECORDING_ERROR should exist when checking for recording failures." + ); + let values = snapshot.parent.TELEMETRY_EVENT_RECORDING_ERROR.values; + Assert.ok( + !!values, + "TELEMETRY_EVENT_RECORDING_ERROR's values should exist when checking for recording failures." + ); + Assert.equal( + values[failureType], + 1, + `Event recording ought to have failed due to type ${failureType}` + ); +} + +add_task(async function test_event_summary_limit() { + Telemetry.clearEvents(); + Telemetry.clearScalars(); + + const limit = 500; // matches kMaxEventSummaryKeys in TelemetryScalar.cpp. + let objects = []; + for (let i = 0; i < limit + 1; i++) { + objects.push("object" + i); + } + // Using "telemetry.test.dynamic" as using "telemetry.test" will enable + // the "telemetry.test" category. + Telemetry.registerEvents("telemetry.test.dynamic", { + test_method: { + methods: ["testMethod"], + objects, + record_on_release: true, + }, + }); + for (let object of objects) { + Telemetry.recordEvent("telemetry.test.dynamic", "testMethod", object); + } + + TelemetryTestUtils.assertNumberOfEvents( + limit + 1, + {}, + { process: "dynamic" } + ); + let scalarSnapshot = Telemetry.getSnapshotForKeyedScalars("main", true); + Assert.equal( + Object.keys(scalarSnapshot.dynamic["telemetry.dynamic_event_counts"]) + .length, + limit, + "Should not have recorded more than `limit` events" + ); +}); + +add_task(async function test_recording_state() { + Telemetry.clearEvents(); + Telemetry.clearScalars(); + + const events = [ + ["telemetry.test", "test1", "object1"], + ["telemetry.test.second", "test", "object1"], + ]; + + // Both test categories should be off by default. + events.forEach(e => Telemetry.recordEvent(...e)); + TelemetryTestUtils.assertEvents([]); + checkEventSummary( + events.map(e => ["parent", e, 1]), + true + ); + + // Enable one test category and see that we record correctly. + Telemetry.setEventRecordingEnabled("telemetry.test", true); + events.forEach(e => Telemetry.recordEvent(...e)); + TelemetryTestUtils.assertEvents([events[0]]); + checkEventSummary( + events.map(e => ["parent", e, 1]), + true + ); + + // Also enable the other test category and see that we record correctly. + Telemetry.setEventRecordingEnabled("telemetry.test.second", true); + events.forEach(e => Telemetry.recordEvent(...e)); + TelemetryTestUtils.assertEvents(events); + checkEventSummary( + events.map(e => ["parent", e, 1]), + true + ); + + // Now turn of one category again and check that this works as expected. + Telemetry.setEventRecordingEnabled("telemetry.test", false); + events.forEach(e => Telemetry.recordEvent(...e)); + TelemetryTestUtils.assertEvents([events[1]]); + checkEventSummary( + events.map(e => ["parent", e, 1]), + true + ); +}); + +add_task(async function recording_setup() { + // Make sure both test categories are enabled for the remaining tests. + // Otherwise their event recording won't work. + Telemetry.setEventRecordingEnabled("telemetry.test", true); + Telemetry.setEventRecordingEnabled("telemetry.test.second", true); +}); + +add_task(async function test_recording() { + Telemetry.clearScalars(); + Telemetry.clearEvents(); + + // Record some events. + let expected = [ + { optout: false, event: ["telemetry.test", "test1", "object1"] }, + { optout: false, event: ["telemetry.test", "test2", "object2"] }, + + { optout: false, event: ["telemetry.test", "test1", "object1", "value"] }, + { + optout: false, + event: ["telemetry.test", "test1", "object1", "value", null], + }, + { + optout: false, + event: ["telemetry.test", "test1", "object1", null, { key1: "value1" }], + }, + { + optout: false, + event: [ + "telemetry.test", + "test1", + "object1", + "value", + { key1: "value1", key2: "value2" }, + ], + }, + + { optout: true, event: ["telemetry.test", "optout", "object1"] }, + { optout: false, event: ["telemetry.test.second", "test", "object1"] }, + { + optout: false, + event: [ + "telemetry.test.second", + "test", + "object1", + null, + { key1: "value1" }, + ], + }, + ]; + + for (let entry of expected) { + entry.tsBefore = Math.floor(Telemetry.msSinceProcessStart()); + try { + Telemetry.recordEvent(...entry.event); + } catch (ex) { + Assert.ok( + false, + `Failed to record event ${JSON.stringify(entry.event)}: ${ex}` + ); + } + entry.tsAfter = Math.floor(Telemetry.msSinceProcessStart()); + } + + // Strip off trailing null values to match the serialized events. + for (let entry of expected) { + let e = entry.event; + while (e.length >= 3 && e[e.length - 1] === null) { + e.pop(); + } + } + + // Check that the events were summarized properly. + let summaries = {}; + expected.forEach(({ optout, event }) => { + let [category, eObject, method] = event; + let uniqueEventName = `${category}#${eObject}#${method}`; + if (!(uniqueEventName in summaries)) { + summaries[uniqueEventName] = ["parent", event, 1]; + } else { + summaries[uniqueEventName][2]++; + } + }); + checkEventSummary(Object.values(summaries), true); + + // The following should not result in any recorded events. + Telemetry.recordEvent("unknown.category", "test1", "object1"); + checkRecordingFailure(0 /* UnknownEvent */); + Telemetry.recordEvent("telemetry.test", "unknown", "object1"); + checkRecordingFailure(0 /* UnknownEvent */); + Telemetry.recordEvent("telemetry.test", "test1", "unknown"); + checkRecordingFailure(0 /* UnknownEvent */); + + let checkEvents = (events, expectedEvents) => { + checkEventFormat(events); + Assert.equal( + events.length, + expectedEvents.length, + "Snapshot should have the right number of events." + ); + + for (let i = 0; i < events.length; ++i) { + let { tsBefore, tsAfter } = expectedEvents[i]; + let ts = events[i][0]; + Assert.greaterOrEqual( + ts, + tsBefore, + "The recorded timestamp should be greater than the one before recording." + ); + Assert.lessOrEqual( + ts, + tsAfter, + "The recorded timestamp should be less than the one after recording." + ); + + let recordedData = events[i].slice(1); + let expectedData = expectedEvents[i].event.slice(); + Assert.deepEqual( + recordedData, + expectedData, + "The recorded event data should match." + ); + } + }; + + // Check that the expected events were recorded. + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + checkEvents(snapshot.parent, expected); + + // Check serializing only opt-out events. + snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + let filtered = expected.filter(e => !!e.optout); + checkEvents(snapshot.parent, filtered); +}); + +add_task(async function test_clear() { + Telemetry.clearEvents(); + + const COUNT = 10; + for (let i = 0; i < COUNT; ++i) { + Telemetry.recordEvent("telemetry.test", "test1", "object1"); + Telemetry.recordEvent("telemetry.test.second", "test", "object1"); + } + + // Check that events were recorded. + // The events are cleared by passing the respective flag. + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + Assert.equal( + snapshot.parent.length, + 2 * COUNT, + `Should have recorded ${2 * COUNT} events.` + ); + + // Now the events should be cleared. + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false); + Assert.equal( + Object.keys(snapshot).length, + 0, + `Should have cleared the events.` + ); + + for (let i = 0; i < COUNT; ++i) { + Telemetry.recordEvent("telemetry.test", "test1", "object1"); + Telemetry.recordEvent("telemetry.test.second", "test", "object1"); + } + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true, 5); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events"); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + Assert.equal( + snapshot.parent.length, + 2 * COUNT - 5, + `Should have returned ${2 * COUNT - 5} events` + ); + + Telemetry.recordEvent("telemetry.test", "test1", "object1"); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false, 5); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events"); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + Assert.equal( + snapshot.parent.length, + 2 * COUNT - 5 + 1, + `Should have returned ${2 * COUNT - 5 + 1} events` + ); +}); + +add_task(async function test_expiry() { + Telemetry.clearEvents(); + + // Recording call with event that is expired by version. + Telemetry.recordEvent("telemetry.test", "expired_version", "object1"); + checkRecordingFailure(1 /* Expired */); + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + Object.keys(snapshot).length, + 0, + "Should not record event with expired version." + ); + + // Recording call with event that has expiry_version set into the future. + Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1"); + TelemetryTestUtils.assertNumberOfEvents(1); +}); + +add_task(async function test_invalidParams() { + Telemetry.clearEvents(); + + // Recording call with wrong type for value argument. + Telemetry.recordEvent("telemetry.test", "test1", "object1", 1); + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + Object.keys(snapshot).length, + 0, + "Should not record event when value argument with invalid type is passed." + ); + checkRecordingFailure(3 /* Value */); + + // Recording call with wrong type for extra argument. + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid"); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + Object.keys(snapshot).length, + 0, + "Should not record event when extra argument with invalid type is passed." + ); + checkRecordingFailure(4 /* Extra */); + + // Recording call with unknown extra key. + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, { + key3: "x", + }); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + Object.keys(snapshot).length, + 0, + "Should not record event when extra argument with invalid key is passed." + ); + checkRecordingFailure(2 /* ExtraKey */); + + // Recording call with invalid value type. + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, { + key3: 1, + }); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + Object.keys(snapshot).length, + 0, + "Should not record event when extra argument with invalid value type is passed." + ); + checkRecordingFailure(4 /* Extra */); +}); + +add_task(async function test_storageLimit() { + Telemetry.clearEvents(); + + let limitReached = TestUtils.topicObserved( + "event-telemetry-storage-limit-reached" + ); + // Record more events than the storage limit allows. + let LIMIT = 1000; + let COUNT = LIMIT + 10; + for (let i = 0; i < COUNT; ++i) { + Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i)); + } + + await limitReached; + Assert.ok(true, "Topic was notified when event limit was reached"); + + // Check that the right events were recorded. + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.ok("parent" in snapshot, "Should have entry for main process."); + let events = snapshot.parent; + Assert.equal( + events.length, + COUNT, + `Should have only recorded all ${COUNT} events` + ); + Assert.ok( + events.every((e, idx) => e[4] === String(idx)), + "Should have recorded all events." + ); +}); + +add_task(async function test_valueLimits() { + Telemetry.clearEvents(); + + // Record values that are at or over the limits for string lengths. + let LIMIT = 80; + let expected = [ + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT - 10), null], + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT), null], + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 1), null], + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 10), null], + + [ + "telemetry.test", + "test1", + "object1", + null, + { key1: "a".repeat(LIMIT - 10) }, + ], + ["telemetry.test", "test1", "object1", null, { key1: "a".repeat(LIMIT) }], + [ + "telemetry.test", + "test1", + "object1", + null, + { key1: "a".repeat(LIMIT + 1) }, + ], + [ + "telemetry.test", + "test1", + "object1", + null, + { key1: "a".repeat(LIMIT + 10) }, + ], + ]; + + for (let event of expected) { + Telemetry.recordEvent(...event); + if (event[3]) { + event[3] = event[3].substr(0, LIMIT); + } else { + event[3] = undefined; + } + if (event[4]) { + event[4].key1 = event[4].key1.substr(0, LIMIT); + } + } + + // Strip off trailing null values to match the serialized events. + for (let e of expected) { + while (e.length >= 3 && e[e.length - 1] === null) { + e.pop(); + } + } + + // Check that the right events were recorded. + TelemetryTestUtils.assertEvents(expected); +}); + +add_task(async function test_unicodeValues() { + Telemetry.clearEvents(); + + // Record string values containing unicode characters. + let value = "漢語"; + Telemetry.recordEvent("telemetry.test", "test1", "object1", value); + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, { + key1: value, + }); + + // Check that the values were correctly recorded. + TelemetryTestUtils.assertEvents([{ value }, { extra: { key1: value } }]); +}); + +add_task(async function test_dynamicEvents() { + Telemetry.clearEvents(); + Telemetry.clearScalars(); + Telemetry.canRecordExtended = true; + + // Register some test events. + Telemetry.registerEvents("telemetry.test.dynamic", { + // Event with only required fields. + test1: { + methods: ["test1"], + objects: ["object1"], + }, + // Event with extra_keys. + test2: { + methods: ["test2", "test2b"], + objects: ["object1"], + extra_keys: ["key1", "key2"], + }, + // Expired event. + test3: { + methods: ["test3"], + objects: ["object1"], + expired: true, + }, + // A release-channel recording event. + test4: { + methods: ["test4"], + objects: ["object1"], + record_on_release: true, + }, + }); + + // Record some valid events. + Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1"); + Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, { + key1: "foo", + key2: "bar", + }); + Telemetry.recordEvent("telemetry.test.dynamic", "test2b", "object1", null, { + key1: "foo", + key2: "bar", + }); + Telemetry.recordEvent( + "telemetry.test.dynamic", + "test3", + "object1", + "some value" + ); + Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1", null); + + // Test recording an unknown event. + Telemetry.recordEvent("telemetry.test.dynamic", "unknown", "unknown"); + checkRecordingFailure(0 /* UnknownEvent */); + + // Now check that the snapshot contains the expected data. + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false); + Assert.ok( + "dynamic" in snapshot, + "Should have dynamic events in the snapshot." + ); + + let expected = [ + ["telemetry.test.dynamic", "test1", "object1"], + [ + "telemetry.test.dynamic", + "test2", + "object1", + null, + { key1: "foo", key2: "bar" }, + ], + [ + "telemetry.test.dynamic", + "test2b", + "object1", + null, + { key1: "foo", key2: "bar" }, + ], + // "test3" is epxired, so it should not be recorded. + ["telemetry.test.dynamic", "test4", "object1"], + ]; + let events = snapshot.dynamic; + Assert.equal( + events.length, + expected.length, + "Should have recorded the right amount of events." + ); + for (let i = 0; i < expected.length; ++i) { + Assert.deepEqual( + events[i].slice(1), + expected[i], + "Should have recorded the expected event data." + ); + } + + // Check that we've summarized the recorded events + checkEventSummary( + expected.map(ev => ["dynamic", ev, 1]), + true + ); + + // Check that the opt-out snapshot contains only the one expected event. + snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false); + Assert.ok( + "dynamic" in snapshot, + "Should have dynamic events in the snapshot." + ); + Assert.equal( + snapshot.dynamic.length, + 1, + "Should have one opt-out event in the snapshot." + ); + expected = ["telemetry.test.dynamic", "test4", "object1"]; + Assert.deepEqual(snapshot.dynamic[0].slice(1), expected); + + // Recording with unknown extra keys should be ignored and print an error. + Telemetry.clearEvents(); + Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1", null, { + key1: "foo", + }); + Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, { + key1: "foo", + unknown: "bar", + }); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.ok( + !("dynamic" in snapshot), + "Should have not recorded dynamic events with unknown extra keys." + ); + + // Other built-in events should not show up in the "dynamic" bucket of the snapshot. + Telemetry.recordEvent("telemetry.test", "test1", "object1"); + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.ok( + !("dynamic" in snapshot), + "Should have not recorded built-in event into dynamic bucket." + ); + + // Test that recording opt-in and opt-out events works as expected. + Telemetry.clearEvents(); + Telemetry.canRecordExtended = false; + + Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1"); + Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1"); + + expected = [ + // Only "test4" should have been recorded. + ["telemetry.test.dynamic", "test4", "object1"], + ]; + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + snapshot.dynamic.length, + 1, + "Should have one opt-out event in the snapshot." + ); + Assert.deepEqual( + snapshot.dynamic.map(e => e.slice(1)), + expected + ); +}); + +add_task(async function test_dynamicEventRegistrationValidation() { + Telemetry.canRecordExtended = true; + Telemetry.clearEvents(); + + // Test registration of invalid categories. + Telemetry.getSnapshotForHistograms("main", true); // Clear histograms before we begin. + Assert.throws( + () => + Telemetry.registerEvents("telemetry+test+dynamic", { + test1: { + methods: ["test1"], + objects: ["object1"], + }, + }), + /Category parameter should match the identifier pattern\./, + "Should throw when registering category names with invalid characters." + ); + checkRegistrationFailure(2 /* Category */); + Assert.throws( + () => + Telemetry.registerEvents( + "telemetry.test.test.test.test.test.test.test.test", + { + test1: { + methods: ["test1"], + objects: ["object1"], + }, + } + ), + /Category parameter should match the identifier pattern\./, + "Should throw when registering overly long category names." + ); + checkRegistrationFailure(2 /* Category */); + + // Test registration of invalid event names. + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic1", { + "test?1": { + methods: ["test1"], + objects: ["object1"], + }, + }), + /Event names should match the identifier pattern\./, + "Should throw when registering event names with invalid characters." + ); + checkRegistrationFailure(1 /* Name */); + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic2", { + test1test1test1test1test1test1test1: { + methods: ["test1"], + objects: ["object1"], + }, + }), + /Event names should match the identifier pattern\./, + "Should throw when registering overly long event names." + ); + checkRegistrationFailure(1 /* Name */); + + // Test registration of invalid method names. + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic3", { + test1: { + methods: ["test?1"], + objects: ["object1"], + }, + }), + /Method names should match the identifier pattern\./, + "Should throw when registering method names with invalid characters." + ); + checkRegistrationFailure(3 /* Method */); + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic", { + test1: { + methods: ["test1test1test1test1test1test1test1"], + objects: ["object1"], + }, + }), + /Method names should match the identifier pattern\./, + "Should throw when registering overly long method names." + ); + checkRegistrationFailure(3 /* Method */); + + // Test registration of invalid object names. + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic4", { + test1: { + methods: ["test1"], + objects: ["object?1"], + }, + }), + /Object names should match the identifier pattern\./, + "Should throw when registering object names with invalid characters." + ); + checkRegistrationFailure(4 /* Object */); + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic5", { + test1: { + methods: ["test1"], + objects: ["object1object1object1object1object1object1"], + }, + }), + /Object names should match the identifier pattern\./, + "Should throw when registering overly long object names." + ); + checkRegistrationFailure(4 /* Object */); + + // Test validation of invalid key names. + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic6", { + test1: { + methods: ["test1"], + objects: ["object1"], + extra_keys: ["a?1"], + }, + }), + /Extra key names should match the identifier pattern\./, + "Should throw when registering extra key names with invalid characters." + ); + checkRegistrationFailure(5 /* ExtraKeys */); + + // Test validation of key names that are too long - we allow a maximum of 15 characters. + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic7", { + test1: { + methods: ["test1"], + objects: ["object1"], + extra_keys: ["a012345678901234"], + }, + }), + /Extra key names should match the identifier pattern\./, + "Should throw when registering extra key names which are too long." + ); + checkRegistrationFailure(5 /* ExtraKeys */); + Telemetry.registerEvents("telemetry.test.dynamic8", { + test1: { + methods: ["test1"], + objects: ["object1"], + extra_keys: ["a01234567890123"], + }, + }); + + // Test validation of extra key count - we only allow 10. + Assert.throws( + () => + Telemetry.registerEvents("telemetry.test.dynamic9", { + test1: { + methods: ["test1"], + objects: ["object1"], + extra_keys: [ + "a1", + "a2", + "a3", + "a4", + "a5", + "a6", + "a7", + "a8", + "a9", + "a10", + "a11", + ], + }, + }), + /No more than 10 extra keys can be registered\./, + "Should throw when registering too many extra keys." + ); + checkRegistrationFailure(5 /* ExtraKeys */); + Telemetry.registerEvents("telemetry.test.dynamic10", { + test1: { + methods: ["test1"], + objects: ["object1"], + extra_keys: ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"], + }, + }); +}); + +// When add-ons update, they may re-register some of the dynamic events. +// Test through some possible scenarios. +add_task(async function test_dynamicEventRegisterAgain() { + Telemetry.canRecordExtended = true; + Telemetry.clearEvents(); + + const category = "telemetry.test.register.again"; + let events = { + test1: { + methods: ["test1"], + objects: ["object1"], + }, + }; + + // First register the initial event and make sure it can be recorded. + Telemetry.registerEvents(category, events); + let expected = [[category, "test1", "object1"]]; + expected.forEach(e => Telemetry.recordEvent(...e)); + + let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + snapshot.dynamic.length, + expected.length, + "Should have right number of events in the snapshot." + ); + Assert.deepEqual( + snapshot.dynamic.map(e => e.slice(1)), + expected + ); + + // Register the same event again and make sure it can still be recorded. + Telemetry.registerEvents(category, events); + Telemetry.recordEvent(category, "test1", "object1"); + + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + snapshot.dynamic.length, + expected.length, + "Should have right number of events in the snapshot." + ); + Assert.deepEqual( + snapshot.dynamic.map(e => e.slice(1)), + expected + ); + + // Now register another event in the same category and make sure both events can be recorded. + events.test2 = { + methods: ["test2"], + objects: ["object2"], + }; + Telemetry.registerEvents(category, events); + + expected = [ + [category, "test1", "object1"], + [category, "test2", "object2"], + ]; + expected.forEach(e => Telemetry.recordEvent(...e)); + + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + snapshot.dynamic.length, + expected.length, + "Should have right number of events in the snapshot." + ); + Assert.deepEqual( + snapshot.dynamic.map(e => e.slice(1)), + expected + ); + + // Check that adding a new object to an event entry works. + events.test1.methods = ["test1a"]; + events.test2.objects = ["object2", "object2a"]; + Telemetry.registerEvents(category, events); + + expected = [ + [category, "test1", "object1"], + [category, "test2", "object2"], + [category, "test1a", "object1"], + [category, "test2", "object2a"], + ]; + expected.forEach(e => Telemetry.recordEvent(...e)); + + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + snapshot.dynamic.length, + expected.length, + "Should have right number of events in the snapshot." + ); + Assert.deepEqual( + snapshot.dynamic.map(e => e.slice(1)), + expected + ); + + // Make sure that we can expire events that are already registered. + events.test2.expired = true; + Telemetry.registerEvents(category, events); + + expected = [[category, "test1", "object1"]]; + expected.forEach(e => Telemetry.recordEvent(...e)); + + snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true); + Assert.equal( + snapshot.dynamic.length, + expected.length, + "Should have right number of events in the snapshot." + ); + Assert.deepEqual( + snapshot.dynamic.map(e => e.slice(1)), + expected + ); +}); + +add_task( + { + skip_if: () => gIsAndroid, + }, + async function test_productSpecificEvents() { + const EVENT_CATEGORY = "telemetry.test"; + const DEFAULT_PRODUCTS_EVENT = "default_products"; + const DESKTOP_ONLY_EVENT = "desktop_only"; + const MULTIPRODUCT_EVENT = "multiproduct"; + const MOBILE_ONLY_EVENT = "mobile_only"; + + Telemetry.clearEvents(); + + // Try to record the desktop and multiproduct event + Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"); + Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1"); + Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"); + + // Try to record the mobile-only event + Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1"); + + let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent; + + let expected = [ + [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"], + [EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1"], + [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"], + ]; + Assert.equal( + events.length, + expected.length, + "Should have recorded the right amount of events." + ); + for (let i = 0; i < expected.length; ++i) { + Assert.deepEqual( + events[i].slice(1), + expected[i], + "Should have recorded the expected event data." + ); + } + } +); + +add_task( + { + skip_if: () => !gIsAndroid, + }, + async function test_mobileSpecificEvents() { + const EVENT_CATEGORY = "telemetry.test"; + const DEFAULT_PRODUCTS_EVENT = "default_products"; + const DESKTOP_ONLY_EVENT = "desktop_only"; + const MULTIPRODUCT_EVENT = "multiproduct"; + const MOBILE_ONLY_EVENT = "mobile_only"; + + Telemetry.clearEvents(); + + // Try to record the mobile-only and multiproduct event + Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"); + Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1"); + Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"); + + // Try to record the mobile-only event + Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1"); + + let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent; + + let expected = [ + [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"], + [EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1"], + [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"], + ]; + Assert.equal( + events.length, + expected.length, + "Should have recorded the right amount of events." + ); + for (let i = 0; i < expected.length; ++i) { + Assert.deepEqual( + events[i].slice(1), + expected[i], + "Should have recorded the expected event data." + ); + } + } +); |