/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { HttpServer } = ChromeUtils.importESModule( "resource://testing-common/httpd.sys.mjs" ); const { PlacesTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PlacesTestUtils.sys.mjs" ); const { NetUtil } = ChromeUtils.importESModule( "resource://gre/modules/NetUtil.sys.mjs" ); const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { NimbusTestUtils } = ChromeUtils.importESModule( "resource://testing-common/NimbusTestUtils.sys.mjs" ); const { DAPIncrementality } = ChromeUtils.importESModule( "resource://gre/modules/DAPIncrementality.sys.mjs" ); NimbusTestUtils.init(this); const BinaryInputStream = Components.Constructor( "@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream" ); const PREF_LEADER = "toolkit.telemetry.dap.leader.url"; const PREF_HELPER = "toolkit.telemetry.dap.helper.url"; const TRANSITION_TYPED = PlacesUtils.history.TRANSITION_TYPED; const TASK_ID = "o-91EcR2kfxfAmkKPPHifXKqiH7Upm0Ilw5joB3L_pE"; let server; let server_addr; // The dummy test server will record report sizes in this list. let server_requests = []; function uploadHandler(request, response) { Assert.equal( request.getHeader("Content-Type"), "application/dap-report", "Wrong Content-Type header." ); let body = new BinaryInputStream(request.bodyInputStream); server_requests.push(body.available()); response.setStatusLine(request.httpVersion, 200); } function resetServerRequests() { server_requests.length = 0; } add_setup(async function () { do_get_profile(); // Set up a mock server to represent the DAP endpoints. server = new HttpServer(); server.registerPrefixHandler("/leader_endpoint/tasks/", uploadHandler); server.start(-1); const i = server.identity; server_addr = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort; Services.prefs.setStringPref(PREF_LEADER, server_addr + "/leader_endpoint"); Services.prefs.setStringPref(PREF_HELPER, server_addr + "/helper_endpoint"); registerCleanupFunction(() => { Services.prefs.clearUserPref(PREF_LEADER); Services.prefs.clearUserPref(PREF_HELPER); return new Promise(resolve => { server.stop(resolve); }); }); }); function openSubCapDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open("SubmissionCap", 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function openIncrDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open("DAPIncrementality", 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function countRecords(store) { return new Promise((resolve, reject) => { const request = store.count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getFreqCapCount(db) { const tx = db.transaction("freq_caps", "readonly"); const store = tx.objectStore("freq_caps"); const count = await countRecords(store); await tx.done; return count; } async function getReportCount(db) { const tx = db.transaction("reports", "readonly"); const store = tx.objectStore("reports"); const count = await countRecords(store); await tx.done; return count; } async function getReport(db, taskId) { const tx = db.transaction("reports", "readonly"); const store = await tx.objectStore("reports"); return new Promise((resolve, reject) => { const request = store.get(taskId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getReferrerState(db, taskId) { const tx = db.transaction("referrer", "readonly"); const store = await tx.objectStore("referrer"); return new Promise((resolve, reject) => { const request = store.get(taskId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testVisitMeasurementNimbus() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); // Enroll in experiment to count 1 url const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "visitMeasurement", taskId: TASK_ID, length: 2, timePrecision: 3600, visitCountUrls: [ { url: "*://*.example.com/", bucket: 0, }, { url: "*://*.mozilla.org/", bucket: 1, }, ], }, }); Assert.notStrictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should be active" ); // Verify there are no pending reports to submit const subCapDb = await openSubCapDatabase(); let numRecords = await getReportCount(subCapDb); Assert.equal(numRecords, 0, "Should be no pending reports"); // Visit a url that does not match a pattern. let timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/path"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify there are 0 pending reports to send numRecords = await getReportCount(subCapDb); Assert.equal(numRecords, 0, "Should be no pending reports"); // Visit a url by typing in the url bar timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify there is 1 pending report to send and measurement matches bucket value. numRecords = await getReportCount(subCapDb); Assert.equal(numRecords, 1, "Should be 1 pending report"); // Trigger submission of the report await DAPIncrementality.dapReportContoller.submit(1000, "unit-test"); // Verify there are 0 pending reports numRecords = await getReportCount(subCapDb); Assert.equal(numRecords, 0, "Should be no pending reports"); // Verify submission capping is active numRecords = await getFreqCapCount(subCapDb); Assert.equal(numRecords, 1, "Should be 1 cap entry"); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify freq cap cleanup after unenrollment numRecords = await getFreqCapCount(subCapDb); Assert.equal(numRecords, 0, "Should be 0 cap entries"); // Verify pending report cleanup after unenrollment numRecords = await getReportCount(subCapDb); Assert.equal(numRecords, 0, "Should be no pending reports"); // Verify server requests Assert.deepEqual( server_requests, [390, 390, 390], "Should have one report on enrollment, second for triggered submission, third on unenrollment" ); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist after unenrollment" ); await cleanup(); } ); add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testParsingMultiUrlVisitMeasurementNimbus() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); const expectedVisitCountUrls = [ { url: "*://*.mozilla.org/", bucket: 0, }, { url: "*://*.example.com/", bucket: 1, }, ]; // Enroll in experiment to count 2 urls const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "visitMeasurement", taskId: TASK_ID, length: 2, timePrecision: 3600, visitCountUrls: expectedVisitCountUrls, }, }); const toKey = ({ url, bucket }) => `${url}#${bucket}`; const expectedKeySorted = expectedVisitCountUrls.map(toKey).sort(); const parsedKeySorted = DAPIncrementality.config.visitUrlPatterns .map(({ pattern, bucket }) => `${pattern.pattern}#${bucket}`) .sort(); Assert.deepEqual( parsedKeySorted, expectedKeySorted, "patterns and buckets match" ); // Trigger submission of the report await DAPIncrementality.dapReportContoller.submit(1000, "unit-test"); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify server requests Assert.deepEqual( server_requests, [390, 390, 390], "Should have one report on enrollment, second for triggered submission, third on unenrollment" ); await cleanup(); } ); add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testReferrerMeasurementNimbus() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); // Enroll experiment with 1 referrer url and 1 target url const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "referrerMeasurement", taskId: TASK_ID, length: 3, timePrecision: 3600, referrerUrls: [ { url: "*://*.mozilla.org/ref", bucket: 1, }, ], targetUrls: "*://*.mozilla.org/target", unknownReferrerBucket: 2, }, }); // Visit neither referrer nor target Urls let timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify no referrer state stored const incrDb = await openIncrDatabase(); let state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual( state, undefined, "No referrer state should be recorded" ); // Visit the referrer url timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/ref"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify referrer state matches the bucket for the referred url state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual(state.bucket, 1, "Referrer state should be 1"); // Visit target url timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/target"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify the pending report matches the bucket for the referred url const subCapDb = await openSubCapDatabase(); let pendingReport = await getReport(subCapDb, TASK_ID); Assert.equal( pendingReport.measurement, 1, "Pending measurement value should be 1" ); // Verify that the referrer state has been cleared state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual(state, undefined, "Referrer state should be cleared"); // Trigger submission of the report await DAPIncrementality.dapReportContoller.submit(1000, "unit-test"); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify server requests Assert.deepEqual( server_requests, [438, 438, 438], "Should have one report on enrollment, second for triggered submission, third on unenrollment" ); await cleanup(); } ); add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testReferrerMeasurementParsingMultiTargetNimbus() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); // Enroll experiment with 1 referrer url and 2 target urls const targetUrls = "*://*.mozilla.org/target1,*://*.mozilla.org/target2"; const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "referrerMeasurement", taskId: TASK_ID, length: 3, timePrecision: 3600, referrerUrls: [ { url: "*://*.mozilla.org/ref", bucket: 1, }, ], targetUrls, unknownReferrerBucket: 2, }, }); Assert.strictEqual( DAPIncrementality.config.targetUrlPatterns.length, 2, "List should contain 2 urls patterns" ); const missingUrls = DAPIncrementality.config.targetUrlPatterns.filter( s => !targetUrls.includes(s.pattern) ); Assert.deepEqual( missingUrls, [], "All urls in the list should be found in the targetUrlPatterns" ); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify server requests Assert.deepEqual( server_requests, [438, 438], "Should have one report on enrollment, second on unenrollment" ); await cleanup(); } ); add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testReferrerMeasurementUnknownBucketNotSetNimbus() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); // Enroll experiment with 1 referrer url and 1 target url without unknown bucket defined. const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "referrerMeasurement", taskId: TASK_ID, length: 3, timePrecision: 3600, referrerUrls: [ { url: "*://*.mozilla.org/ref", bucket: 1, }, ], targetUrls: "*://*.mozilla.org/target", }, }); // Visit target url without prior referrer visit let timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/target"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify no referrer state is recorded const incrDb = await openIncrDatabase(); let state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual( state, undefined, "No referrer state should be recorded" ); // Verify there is no pending report since there is no unknown bucket defined. const subCapDb = await openSubCapDatabase(); let pendingReport = await getReport(subCapDb, TASK_ID); Assert.equal( pendingReport, undefined, "No pending report should be recorded" ); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify server requests Assert.deepEqual( server_requests, [438, 438], "Should have one report on enrollment, second on unenrollment" ); await cleanup(); } ); add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testReferrerMeasurementUnknownBucketSet() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); // Enroll experiment with 1 referrer url and 1 target url const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "referrerMeasurement", taskId: TASK_ID, length: 3, timePrecision: 3600, referrerUrls: [ { url: "*://*.mozilla.org/ref", bucket: 1, }, ], targetUrls: "*://*.mozilla.org/target", unknownReferrerBucket: 2, }, }); // Visit neither referrer nor target Urls let timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify no referrer state stored const incrDb = await openIncrDatabase(); let state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual( state, undefined, "No referrer state should be recorded" ); // Visit target url without prior referrer visit timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/target"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify no referrer state stored state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual( state, undefined, "No referrer state should be recorded" ); // Verify there is a pending report to submit with the value for unknownReferrerBucket const subCapDb = await openSubCapDatabase(); let pendingReport = await getReport(subCapDb, TASK_ID); Assert.equal( pendingReport.measurement, 2, "Pending measurement value should be 2" ); // Trigger submission of the report await DAPIncrementality.dapReportContoller.submit(1000, "unit-test"); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify server requests Assert.deepEqual( server_requests, [438, 438, 438], "Should have one report on enrollment, second for triggered submission, third on unenrollment" ); await cleanup(); } ); add_task( { // Requires Normandy. skip_if: () => !AppConstants.MOZ_NORMANDY, }, async function testMultiReferrerMeasurementUnknownBucketNotSetNimbus() { resetServerRequests(); const { cleanup } = await NimbusTestUtils.setupTest(); await DAPIncrementality.startup(); Assert.strictEqual( DAPIncrementality.dapReportContoller, null, "dapReportContoller should not exist before enrollment" ); // Enroll experiment with 2 referrer urls and 1 target url without unknown bucket defined. const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig({ featureId: "dapIncrementality", value: { measurementType: "referrerMeasurement", taskId: TASK_ID, length: 3, timePrecision: 3600, referrerUrls: [ { url: "*://*.mozilla.org/ref", bucket: 1, }, { url: "*://*.mozilla.org/ref2", bucket: 2, }, ], targetUrls: "*://*.mozilla.org/target", }, }); // Visit first referrer url let timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/ref"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify that referrer state matches the specifed bucket for the first url const incrDb = await openIncrDatabase(); let state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual(state.bucket, 1, "Referrer state should be 1"); // Visit second referrer url timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/ref2"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify that referrer state matches the specifed bucket for the second url state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual(state.bucket, 2, "Referrer state should be 2"); // Visit target url timestamp = Date.now() * 1000; await PlacesTestUtils.addVisits({ url: NetUtil.newURI("http://www.mozilla.org/target"), transition: TRANSITION_TYPED, visitDate: timestamp, }); // Verify the pending report measurement is 2 const subCapDb = await openSubCapDatabase(); let pendingReport = await getReport(subCapDb, TASK_ID); Assert.equal( pendingReport.measurement, 2, "Pending measurement value should be 2" ); // Verify referrer state is cleard state = await getReferrerState(incrDb, TASK_ID); Assert.strictEqual(state, undefined, "Referrer state should be cleared"); // Trigger submission of the report await DAPIncrementality.dapReportContoller.submit(1000, "unit-test"); // Unenroll experiment await doExperimentCleanup(); Services.tm.spinEventLoopUntil( "Wait for DAPIncrementality to flush", () => DAPIncrementality.config === null ); // Verify server requests Assert.deepEqual( server_requests, [438, 438, 438], "Should have one report on enrollment, second for triggered submission, third on unenrollment" ); await cleanup(); } );