diff --git a/browser/experiments/Experiments.jsm b/browser/experiments/Experiments.jsm index 3b2d8aadbcfc..7a4f5701ef82 100644 --- a/browser/experiments/Experiments.jsm +++ b/browser/experiments/Experiments.jsm @@ -28,6 +28,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing", "resource://gre/modules/TelemetryPing.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog", + "resource://gre/modules/TelemetryLog.jsm"); // CertUtils.jsm doesn't expose a single "CertUtils" object like a normal .jsm // would. XPCOMUtils.defineLazyGetter(this, "CertUtils", @@ -62,6 +64,27 @@ const PREF_BRANCH_TELEMETRY = "toolkit.telemetry."; const PREF_TELEMETRY_ENABLED = "enabled"; const PREF_TELEMETRY_PRERELEASE = "enabledPreRelease"; +const TELEMETRY_LOG = { + // log(key, [kind, experimentId, details]) + ACTIVATION_KEY: "EXPERIMENT_ACTIVATION", + ACTIVATION: { + ACTIVATED: "ACTIVATED", // successfully activated + INSTALL_FAILURE: "INSTALL_FAILURE", // failed to install the extension + REJECTED: "REJECTED", // experiment was rejected because of it's conditions, + // provides details on which + }, + + // log(key, [kind, experimentId, optionalDetails...]) + TERMINATION_KEY: "EXPERIMENT_TERMINATION", + TERMINATION: { + USERDISABLED: "USERDISABLED", // the user disabled this experiment + FROM_API: "FROM_API", // the experiment disabled itself + EXPIRED: "EXPIRED", // experiment expired e.g. by exceeding the end-date + RECHECK: "RECHECK", // disabled after re-evaluating conditions, + // provides details on which + }, +}; + const gPrefs = new Preferences(PREF_BRANCH); const gPrefsTelemetry = new Preferences(PREF_BRANCH_TELEMETRY); let gExperimentsEnabled = false; @@ -737,11 +760,12 @@ Experiments.Experiments.prototype = { // Make sure we keep experiments that are or were running. // We remove them after KEEP_HISTORY_N_DAYS. for (let [id, entry] of this._experiments) { - if (experiments.has(id) || !entry.startDate || !entry.shouldDiscard()) { + if (experiments.has(id) || !entry.startDate || entry.shouldDiscard()) { + gLogger.trace("Experiments::updateExperiments() - discarding entry for " + id); continue; } - experiments.set(id, experiment); + experiments.set(id, entry); } this._experiments = experiments; @@ -805,16 +829,25 @@ Experiments.Experiments.prototype = { } if (!experiment.enabled) { - return Promise.reject(); + let message = "experiment is not enabled"; + gLogger.debug("Experiments::disableExperiment() - " + message); + return Promise.reject(new Error(message)); } return Task.spawn(function* Experiments_disableExperimentTaskOuter() { + gLogger.trace("Experiments_disableExperimentTaskOuter"); // We need to wait on possible previous disable tasks. yield this._pendingTasks.disableExperiment; let disableTask = Task.spawn(function* Experiments_disableExperimentTaskInner() { yield this._pendingTasksDone([this._pendingTasks.disableExperiment]); - yield experiment.stop(userDisabled); + + let terminationKind = TELEMETRY_LOG.TERMINATION.USERDISABLED; + if (!userDisabled) { + terminationKind = TELEMETRY_LOG.TERMINATION.FROM_API; + } + + yield experiment.stop(terminationKind); Services.obs.notifyObservers(null, OBSERVER_TOPIC, null); this._pendingTasks.disableExperiment = null; }.bind(this)); @@ -839,6 +872,7 @@ Experiments.Experiments.prototype = { this._checkForShutdown(); let activeExperiment = this._getActiveExperiment(); let activeChanged = false; + let now = this._policy.now(); if (activeExperiment) { let wasStopped = yield activeExperiment.maybeStop(); @@ -865,14 +899,23 @@ Experiments.Experiments.prototype = { if (!activeExperiment) { for (let [id, experiment] of this._experiments) { let applicable; + let reason = null; yield experiment.isApplicable().then( result => applicable = result, - reason => { + result => { applicable = false; - // TODO telemetry: experiment rejection reason + reason = result; } ); + if (!applicable && reason && reason[0] != "was-active") { + // Report this from here to avoid over-reporting. + let desc = TELEMETRY_LOG.ACTIVATION; + let data = [TELEMETRY_LOG.ACTIVATION.REJECTED, id]; + data = data.concat(reason); + TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY, data); + } + if (applicable) { gLogger.debug("Experiments::evaluateExperiments() - activating experiment " + id); try { @@ -1173,7 +1216,7 @@ Experiments.ExperimentEntry.prototype = { // Not applicable if it already ran. if (!this.enabled && this._endDate) { - return Promise.reject("was already active"); + return Promise.reject(["was-active"]); } // Define and run the condition checks. @@ -1222,7 +1265,7 @@ Experiments.ExperimentEntry.prototype = { if (!result) { gLogger.debug("ExperimentEntry::isApplicable() - id=" + data.id + " - test '" + check.name + "' failed"); - return Promise.reject(check.name); + return Promise.reject([check.name]); } } @@ -1256,7 +1299,7 @@ Experiments.ExperimentEntry.prototype = { Cu.evalInSandbox(jsfilter, sandbox); } catch (e) { gLogger.error("ExperimentEntry::runFilterFunction() - failed to eval jsfilter: " + e.message); - throw "jsfilter:evalFailure"; + throw ["jsfilter-evalfailed"]; } // You can't insert arbitrarily complex objects into a sandbox, so @@ -1272,13 +1315,17 @@ Experiments.ExperimentEntry.prototype = { catch (e) { gLogger.debug("ExperimentEntry::runFilterFunction() - filter function failed: " + e.message + ", " + e.stack); - throw "jsfilter:rejected " + e.message; + throw ["jsfilter-threw", e.message]; } finally { Cu.nukeSandbox(sandbox); } - throw new Task.Result(result); + if (!result) { + throw ["jsfilter-false"]; + } + + throw new Task.Result(true); }.bind(this)); }, @@ -1297,7 +1344,8 @@ Experiments.ExperimentEntry.prototype = { gLogger.error("ExperimentEntry::start() - " + message); this._failedStart = true; - // TODO telemetry: install failure + TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY, + [TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]); deferred.reject(new Error(message)); }; @@ -1328,7 +1376,8 @@ Experiments.ExperimentEntry.prototype = { this._startDate = this._policy.now(); this._enabled = true; - // TODO telemetry: install success + TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY, + [TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]); deferred.resolve(); }, @@ -1352,11 +1401,13 @@ Experiments.ExperimentEntry.prototype = { /* * Stop running the experiment if it is active. - * @param userDisabled (optional) Whether this is disabled by user action, defaults to false. + * @param terminationKind (optional) The termination kind, e.g. USERDISABLED or EXPIRED. + * @param terminationReason (optional) The termination reason details for + * termination kind RECHECK. * @return Promise<> Resolved when the operation is complete. */ - stop: function (userDisabled=false) { - gLogger.trace("ExperimentEntry::stop() - id=" + this.id + ", userDisabled=" + userDisabled); + stop: function (terminationKind, terminationReason) { + gLogger.trace("ExperimentEntry::stop() - id=" + this.id + ", terminationKind=" + terminationKind); if (!this._enabled) { gLogger.warning("ExperimentEntry::stop() - experiment not enabled: " + id); return Promise.reject(); @@ -1386,6 +1437,7 @@ Experiments.ExperimentEntry.prototype = { } updateDates(); + this._logTermination(terminationKind, terminationReason); AddonManager.removeAddonListener(listener); deferred.resolve(); @@ -1397,13 +1449,29 @@ Experiments.ExperimentEntry.prototype = { AddonManager.addAddonListener(listener); addon.uninstall(); - - // TODO telemetry: experiment disabling, differentiate by userDisabled }); return deferred.promise; }, + _logTermination: function (terminationKind, terminationReason) { + if (terminationKind === undefined) { + return; + } + + if (!(terminationKind in TELEMETRY_LOG.TERMINATION)) { + gLogger.warn("ExperimentEntry::stop() - unknown terminationKind " + terminationKind); + return; + } + + let data = [terminationKind, this.id]; + if (terminationReason) { + data = data.concat(terminationReason); + } + + TelemetryLog.log(TELEMETRY_LOG.TERMINATION_KEY, data); + }, + /* * Stop if experiment stop criteria are met. * @return Promise Resolved when done stopping or checking, @@ -1413,11 +1481,17 @@ Experiments.ExperimentEntry.prototype = { gLogger.trace("ExperimentEntry::maybeStop()"); return Task.spawn(function ExperimentEntry_maybeStop_task() { - let shouldStop = yield this._shouldStop(); - if (shouldStop) { - yield this.stop(); + let result = yield this._shouldStop(); + if (result.shouldStop) { + let expireReasons = ["endTime", "maxActiveSeconds"]; + if (expireReasons.indexOf(result.reason[0]) != -1) { + yield this.stop(TELEMETRY_LOG.TERMINATION.EXPIRED); + } else { + yield this.stop(TELEMETRY_LOG.TERMINATION.RECHECK, result.reason); + } } - throw new Task.Result(shouldStop); + + throw new Task.Result(result.shouldStop); }.bind(this)); }, @@ -1427,13 +1501,13 @@ Experiments.ExperimentEntry.prototype = { let maxActiveSec = data.maxActiveSeconds || 0; if (!this._enabled) { - return Promise.resolve(false); + return Promise.resolve({shouldStop: false}); } let deferred = Promise.defer(); this.isApplicable().then( - () => deferred.resolve(false), - () => deferred.resolve(true) + () => deferred.resolve({shouldStop: false}), + reason => deferred.resolve({shouldStop: true, reason: reason}) ); return deferred.promise; diff --git a/browser/experiments/test/xpcshell/head.js b/browser/experiments/test/xpcshell/head.js index 8e28fb00e8bf..c178fa2ddab7 100644 --- a/browser/experiments/test/xpcshell/head.js +++ b/browser/experiments/test/xpcshell/head.js @@ -25,6 +25,9 @@ const EXPERIMENT2_ID = "test-experiment-2@tests.mozilla.org" const EXPERIMENT2_XPI_SHA1 = "sha1:81877991ec70360fb48db84c34a9b2da7aa41d6a"; const EXPERIMENT2_XPI_NAME = "experiment-2.xpi"; +const EXPERIMENT3_ID = "test-experiment-3@tests.mozilla.org"; +const EXPERIMENT4_ID = "test-experiment-4@tests.mozilla.org"; + const FAKE_EXPERIMENTS_1 = [ { id: "id1", diff --git a/browser/experiments/test/xpcshell/test_activate.js b/browser/experiments/test/xpcshell/test_activate.js index 57755e46729f..0c2b77266981 100644 --- a/browser/experiments/test/xpcshell/test_activate.js +++ b/browser/experiments/test/xpcshell/test_activate.js @@ -118,15 +118,15 @@ add_task(function* test_startStop() { yield experiment.start(); Assert.equal(experiment.enabled, true, "Experiment should now be enabled."); - let shouldStop = yield experiment._shouldStop(); - Assert.equal(shouldStop, false, "shouldStop should be false."); + let result = yield experiment._shouldStop(); + Assert.equal(result.shouldStop, false, "shouldStop should be false."); let maybeStop = yield experiment.maybeStop(); Assert.equal(maybeStop, false, "Experiment should not have been stopped."); Assert.equal(experiment.enabled, true, "Experiment should be enabled."); defineNow(gPolicy, futureDate(endDate, MS_IN_ONE_DAY)); - shouldStop = yield experiment._shouldStop(); - Assert.equal(shouldStop, true, "shouldStop should now be true."); + result = yield experiment._shouldStop(); + Assert.equal(result.shouldStop, true, "shouldStop should now be true."); maybeStop = yield experiment.maybeStop(); Assert.equal(maybeStop, true, "Experiment should have been stopped."); Assert.equal(experiment.enabled, false, "Experiment should be disabled."); diff --git a/browser/experiments/test/xpcshell/test_conditions.js b/browser/experiments/test/xpcshell/test_conditions.js index ea9a68bd254a..4ca1d315ad79 100644 --- a/browser/experiments/test/xpcshell/test_conditions.js +++ b/browser/experiments/test/xpcshell/test_conditions.js @@ -74,6 +74,20 @@ add_task(function* test_setup() { let experiments = new Experiments.Experiments(); }); +function arraysEqual(a, b) { + if (a.length !== b.length) { + return false; + } + + for (let i=0; i 0, "timestamp should be greater than 0"); + + if (data === undefined) { + Assert.equal(event.length, 2, "event array should have 2 entries"); + } else { + Assert.equal(event.length, data.length + 2, "event entry count should match expected count"); + for (var i = 0; i < data.length; ++i) { + Assert.equal(typeof(event[i + 2]), "string", "event entry should be a string"); + Assert.equal(event[i + 2], data[i], "event entry should match expected entry"); + } + } +} + +function run_test() { + run_next_test(); +} + +add_task(function* test_setup() { + loadAddonManager(); + gProfileDir = do_get_profile(); + + gHttpServer = new HttpServer(); + gHttpServer.start(-1); + let port = gHttpServer.identity.primaryPort; + gHttpRoot = "http://localhost:" + port + "/"; + gDataRoot = gHttpRoot + "data/"; + gManifestHandlerURI = gHttpRoot + MANIFEST_HANDLER; + gHttpServer.registerDirectory("/data/", do_get_cwd()); + gHttpServer.registerPathHandler("/" + MANIFEST_HANDLER, (request, response) => { + response.setStatusLine(null, 200, "OK"); + response.write(JSON.stringify(gManifestObject)); + response.processAsync(); + response.finish(); + }); + do_register_cleanup(() => gHttpServer.stop(() => {})); + + disableCertificateChecks(); + + Services.prefs.setBoolPref(PREF_EXPERIMENTS_ENABLED, true); + Services.prefs.setIntPref(PREF_LOGGING_LEVEL, 0); + Services.prefs.setBoolPref(PREF_LOGGING_DUMP, true); + Services.prefs.setCharPref(PREF_MANIFEST_URI, gManifestHandlerURI); + Services.prefs.setIntPref(PREF_FETCHINTERVAL, 0); + + gReporter = yield getReporter("json_payload_simple"); + yield gReporter.collectMeasurements(); + let payload = yield gReporter.getJSONPayload(true); + + gPolicy = new Experiments.Policy(); + let dummyTimer = { cancel: () => {}, clear: () => {} }; + patchPolicy(gPolicy, { + updatechannel: () => "nightly", + healthReportPayload: () => Promise.resolve(payload), + oneshotTimer: (callback, timeout, thisObj, name) => dummyTimer, + }); + + yield removeCacheFile(); +}); + +// Test basic starting and stopping of experiments. + +add_task(function* test_telemetryBasics() { + const OBSERVER_TOPIC = "experiments-changed"; + let observerFireCount = 0; + let expectedLogLength = 0; + + // Dates the following tests are based on. + + let baseDate = new Date(2014, 5, 1, 12); + let startDate1 = futureDate(baseDate, 50 * MS_IN_ONE_DAY); + let endDate1 = futureDate(baseDate, 100 * MS_IN_ONE_DAY); + let startDate2 = futureDate(baseDate, 150 * MS_IN_ONE_DAY); + let endDate2 = futureDate(baseDate, 200 * MS_IN_ONE_DAY); + + // The manifest data we test with. + + gManifestObject = { + "version": 1, + experiments: [ + { + id: EXPERIMENT1_ID, + xpiURL: gDataRoot + EXPERIMENT1_XPI_NAME, + xpiHash: EXPERIMENT1_XPI_SHA1, + startTime: dateToSeconds(startDate1), + endTime: dateToSeconds(endDate1), + maxActiveSeconds: 10 * SEC_IN_ONE_DAY, + appName: ["XPCShell"], + channel: ["nightly"], + }, + { + id: EXPERIMENT2_ID, + xpiURL: gDataRoot + EXPERIMENT2_XPI_NAME, + xpiHash: EXPERIMENT2_XPI_SHA1, + startTime: dateToSeconds(startDate2), + endTime: dateToSeconds(endDate2), + maxActiveSeconds: 10 * SEC_IN_ONE_DAY, + appName: ["XPCShell"], + channel: ["nightly"], + }, + ], + }; + + // Data to compare the result of Experiments.getExperiments() against. + + let experimentListData = [ + { + id: EXPERIMENT2_ID, + name: "Test experiment 2", + description: "And yet another experiment that experiments experimentally.", + }, + { + id: EXPERIMENT1_ID, + name: EXPERIMENT1_NAME, + description: "Yet another experiment that experiments experimentally.", + }, + ]; + + let experiments = new Experiments.Experiments(gPolicy); + + // Trigger update, clock set to before any activation. + // Use updateManifest() to provide for coverage of that path. + + let now = baseDate; + defineNow(gPolicy, now); + + yield experiments.updateManifest(); + let list = yield experiments.getExperiments(); + Assert.equal(list.length, 0, "Experiment list should be empty."); + + expectedLogLength += 2; + let log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-2], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.REJECTED, EXPERIMENT1_ID, "startTime"]); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.REJECTED, EXPERIMENT2_ID, "startTime"]); + + // Trigger update, clock set for experiment 1 to start. + + now = futureDate(startDate1, 5 * MS_IN_ONE_DAY); + defineNow(gPolicy, now); + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 1, "Experiment list should have 1 entry now."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.ACTIVATED, EXPERIMENT1_ID]); + + // Trigger update, clock set for experiment 1 to stop. + + now = futureDate(endDate1, 1000); + defineNow(gPolicy, now); + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 1, "Experiment list should have 1 entry."); + + expectedLogLength += 2; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-2], TLOG.TERMINATION_KEY, + [TLOG.TERMINATION.EXPIRED, EXPERIMENT1_ID]); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.REJECTED, EXPERIMENT2_ID, "startTime"]); + + // Trigger update, clock set for experiment 2 to start with invalid hash. + + now = startDate2; + defineNow(gPolicy, now); + gManifestObject.experiments[1].xpiHash = "sha1:0000000000000000000000000000000000000000"; + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 1, "Experiment list should have 1 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.INSTALL_FAILURE, EXPERIMENT2_ID]); + + // Trigger update, clock set for experiment 2 to properly start now. + + now = futureDate(now, MS_IN_ONE_DAY); + defineNow(gPolicy, now); + gManifestObject.experiments[1].xpiHash = EXPERIMENT2_XPI_SHA1; + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 2, "Experiment list should have 2 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.ACTIVATED, EXPERIMENT2_ID]); + + // Fake user-disable of an experiment. + + now = futureDate(now, MS_IN_ONE_DAY); + defineNow(gPolicy, now); + + yield experiments.disableExperiment(EXPERIMENT2_ID); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 2, "Experiment list should have 2 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.TERMINATION_KEY, + [TLOG.TERMINATION.USERDISABLED, EXPERIMENT2_ID]); + + // Trigger update with experiment 1a ready to start. + + now = futureDate(now, MS_IN_ONE_DAY); + defineNow(gPolicy, now); + gManifestObject.experiments[0].id = EXPERIMENT3_ID; + gManifestObject.experiments[0].endTime = dateToSeconds(futureDate(now, 50 * MS_IN_ONE_DAY)); + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 3, "Experiment list should have 3 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.ACTIVATED, EXPERIMENT3_ID]); + + // Trigger non-user-disable of an experiment via the API + + now = futureDate(now, MS_IN_ONE_DAY); + defineNow(gPolicy, now); + + yield experiments.disableExperiment(EXPERIMENT3_ID, false); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 3, "Experiment list should have 3 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.TERMINATION_KEY, + [TLOG.TERMINATION.FROM_API, EXPERIMENT3_ID]); + + // Trigger update with experiment 1a ready to start. + + now = futureDate(now, MS_IN_ONE_DAY); + defineNow(gPolicy, now); + gManifestObject.experiments[0].id = EXPERIMENT4_ID; + gManifestObject.experiments[0].endTime = dateToSeconds(futureDate(now, 50 * MS_IN_ONE_DAY)); + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 4, "Experiment list should have 4 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY, + [TLOG.ACTIVATION.ACTIVATED, EXPERIMENT4_ID]); + + // Trigger experiment termination by something other than expiry via the manifest. + + now = futureDate(now, MS_IN_ONE_DAY); + defineNow(gPolicy, now); + gManifestObject.experiments[0].os = "Plan9"; + + yield experiments.updateManifest(); + list = yield experiments.getExperiments(); + Assert.equal(list.length, 4, "Experiment list should have 4 entries."); + + expectedLogLength += 1; + log = TelemetryPing.getPayload().log; + Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries."); + checkEvent(log[log.length-1], TLOG.TERMINATION_KEY, + [TLOG.TERMINATION.RECHECK, EXPERIMENT4_ID, "os"]); + + // Cleanup. + + yield experiments.uninit(); + yield removeCacheFile(); +}); + +add_task(function* shutdown() { + yield gReporter._shutdown(); + yield removeCacheFile(); +}); diff --git a/browser/experiments/test/xpcshell/xpcshell.ini b/browser/experiments/test/xpcshell/xpcshell.ini index 06a7088b4eea..957ef04b37e1 100644 --- a/browser/experiments/test/xpcshell/xpcshell.ini +++ b/browser/experiments/test/xpcshell/xpcshell.ini @@ -12,4 +12,5 @@ support-files = [test_api.js] [test_conditions.js] [test_fetch.js] +[test_telemetry.js] [test_healthreport.js]