test_urlTelemetry_generic.js (16575B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 ChromeUtils.defineESModuleGetters(this, { 5 BrowserSearchTelemetry: 6 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 7 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 8 SearchSERPTelemetry: 9 "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs", 10 SearchSERPTelemetryUtils: 11 "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs", 12 TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", 13 sinon: "resource://testing-common/Sinon.sys.mjs", 14 }); 15 16 const TEST_PROVIDER_INFO = [ 17 { 18 telemetryId: "example", 19 searchPageRegexp: /^https:\/\/www\.example\.com\/search/, 20 queryParamNames: ["q"], 21 codeParamName: "abc", 22 taggedCodes: ["ff", "tb"], 23 expectedOrganicCodes: ["baz"], 24 organicCodes: ["foo"], 25 followOnParamNames: ["a"], 26 extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/], 27 shoppingTab: { 28 regexp: "&site=shop", 29 }, 30 searchMode: { 31 mode: "image_search", 32 }, 33 components: [ 34 { 35 type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, 36 default: true, 37 }, 38 ], 39 }, 40 { 41 telemetryId: "example2", 42 searchPageRegexp: /^https:\/\/www\.example2\.com\/search/, 43 queryParamNames: ["a", "q"], 44 codeParamName: "abc", 45 taggedCodes: ["ff", "tb"], 46 expectedOrganicCodes: ["baz"], 47 organicCodes: ["foo"], 48 followOnParamNames: ["a"], 49 extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/], 50 components: [ 51 { 52 type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, 53 default: true, 54 }, 55 ], 56 }, 57 { 58 telemetryId: "example3", 59 searchPageRegexp: /^https:\/\/www\.example3\.com\/search/, 60 queryParamNames: ["a", "q"], 61 codeParamName: "abc", 62 taggedCodes: ["ff", "tb"], 63 expectedOrganicCodes: ["baz"], 64 organicCodes: ["foo"], 65 followOnParamNames: ["a"], 66 followOnCookies: [ 67 { 68 host: "www.example3.com", 69 name: "_dummyCookieName", 70 codeParamName: "abc", 71 extraCodePrefixes: ["xyz"], 72 extraCodeParamName: "dummyExtraCodeParamName", 73 }, 74 ], 75 extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/], 76 components: [ 77 { 78 type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, 79 default: true, 80 }, 81 ], 82 }, 83 { 84 telemetryId: "example4", 85 searchPageRegexp: /^https:\/\/www\.example4\.com\/search/, 86 queryParamNames: ["a", "q"], 87 codeParamName: "abc", 88 taggedCodes: ["ff", "tb"], 89 expectedOrganicCodes: ["baz"], 90 organicCodes: ["foo"], 91 followOnParamNames: ["a"], 92 followOnCookies: [ 93 { 94 host: "www.example4.com", 95 name: "_dummyCookieName", 96 codeParamName: "abc", 97 extraCodePrefixes: ["xyz"], 98 extraCodeParamName: "dummyExtraCodeParamName", 99 }, 100 ], 101 extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/], 102 components: [ 103 { 104 type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, 105 default: true, 106 }, 107 ], 108 }, 109 { 110 telemetryId: "example5", 111 searchPageRegexp: /^https:\/\/www\.example5\.com\/search/, 112 queryParamNames: ["a", "q"], 113 codeParamName: "abc", 114 taggedCodes: ["ff", "tb"], 115 expectedOrganicCodes: ["baz"], 116 organicCodes: ["foo"], 117 followOnParamNames: ["a"], 118 followOnCookies: [ 119 { 120 host: "www.example5.com", 121 name: "_dummyCookieName", 122 codeParamName: "abc", 123 // No required extra code param/prefixes. 124 extraCodePrefixes: [], 125 extraCodeParamName: "", 126 }, 127 ], 128 extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/], 129 components: [ 130 { 131 type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, 132 default: true, 133 }, 134 ], 135 }, 136 ]; 137 138 const TESTS = [ 139 { 140 title: "Tagged search", 141 trackingUrl: "https://www.example.com/search?q=test&abc=ff", 142 expectedSearchCountEntry: "example:tagged:ff", 143 expectedAdKey: "example:tagged", 144 adUrls: ["https://www.example.com/ad2"], 145 nonAdUrls: ["https://www.example.com/ad3"], 146 impression: { 147 provider: "example", 148 tagged: "true", 149 partner_code: "ff", 150 source: "unknown", 151 is_shopping_page: "false", 152 is_private: "false", 153 shopping_tab_displayed: "false", 154 is_signed_in: "false", 155 }, 156 }, 157 { 158 title: "Tagged search with shopping", 159 trackingUrl: "https://www.example.com/search?q=test&abc=ff&site=shop", 160 expectedSearchCountEntry: "example:tagged:ff", 161 expectedAdKey: "example:tagged", 162 adUrls: ["https://www.example.com/ad2"], 163 nonAdUrls: ["https://www.example.com/ad3"], 164 impression: { 165 provider: "example", 166 tagged: "true", 167 partner_code: "ff", 168 source: "unknown", 169 is_shopping_page: "true", 170 is_private: "false", 171 shopping_tab_displayed: "false", 172 is_signed_in: "false", 173 }, 174 }, 175 { 176 title: "Tagged image search", 177 trackingUrl: "https://www.example.com/search?q=test&abc=ff&mode=image", 178 expectedSearchCountEntry: "example:tagged:ff", 179 expectedAdKey: "example:tagged", 180 adUrls: ["https://www.example.com/ad2"], 181 nonAdUrls: ["https://www.example.com/ad3"], 182 impression: { 183 provider: "example", 184 tagged: "true", 185 partner_code: "ff", 186 search_mode: "image_search", 187 source: "unknown", 188 is_shopping_page: "false", 189 is_private: "false", 190 shopping_tab_displayed: "false", 191 is_signed_in: "false", 192 }, 193 }, 194 { 195 title: "Tagged follow-on", 196 trackingUrl: "https://www.example.com/search?q=test&abc=tb&a=next", 197 expectedSearchCountEntry: "example:tagged-follow-on:tb", 198 expectedAdKey: "example:tagged-follow-on", 199 adUrls: ["https://www.example.com/ad2"], 200 nonAdUrls: ["https://www.example.com/ad3"], 201 impression: { 202 provider: "example", 203 tagged: "true", 204 partner_code: "tb", 205 source: "unknown", 206 is_shopping_page: "false", 207 is_private: "false", 208 shopping_tab_displayed: "false", 209 is_signed_in: "false", 210 }, 211 }, 212 { 213 setUp() { 214 Services.cookies.removeAll(); 215 const cv = Services.cookies.add( 216 "www.example3.com", 217 "/", 218 "_dummyCookieName", 219 "abc=tb&def=ghi", 220 false, 221 false, 222 false, 223 Date.now() + 10 * 60 * 60 * 1000, 224 {}, 225 Ci.nsICookie.SAMESITE_UNSET, 226 Ci.nsICookie.SCHEME_HTTPS 227 ); 228 Assert.equal(cv.result, Ci.nsICookieValidation.eOK, "Valid cookie"); 229 }, 230 tearDown() { 231 Services.cookies.removeAll(); 232 }, 233 title: "Tagged follow-on with cookie", 234 trackingUrl: 235 "https://www.example3.com/search?q=test&a=next&dummyExtraCodeParamName=xyz", 236 expectedSearchCountEntry: "example3:tagged-follow-on:tb", 237 expectedAdKey: "example3:tagged-follow-on", 238 adUrls: ["https://www.example.com/ad2"], 239 nonAdUrls: ["https://www.example.com/ad3"], 240 impression: { 241 provider: "example3", 242 tagged: "true", 243 partner_code: "tb", 244 source: "unknown", 245 is_shopping_page: "false", 246 is_private: "false", 247 shopping_tab_displayed: "false", 248 is_signed_in: "false", 249 }, 250 }, 251 { 252 setUp() { 253 Services.cookies.removeAll(); 254 const cv = Services.cookies.add( 255 "www.example4.com", 256 "/", 257 "_dummyCookieName", 258 "abc=tb&def=ghi", 259 false, 260 false, 261 false, 262 Date.now() + 10 * 60 * 60 * 1000, 263 {}, 264 Ci.nsICookie.SAMESITE_UNSET, 265 Ci.nsICookie.SCHEME_HTTPS 266 ); 267 Assert.equal(cv.result, Ci.nsICookieValidation.eOK, "Valid cookie"); 268 }, 269 tearDown() { 270 Services.cookies.removeAll(); 271 }, 272 title: 273 "Tagged follow-on with cookie and unexpected extraCodeParam casing in URL", 274 trackingUrl: 275 "https://www.example4.com/search?q=test&a=next&DUMMYEXTRACODEPARAMNAME=xyz", 276 expectedSearchCountEntry: "example4:tagged-follow-on:tb", 277 expectedAdKey: "example4:tagged-follow-on", 278 adUrls: ["https://www.example.com/ad2"], 279 nonAdUrls: ["https://www.example.com/ad3"], 280 impression: { 281 provider: "example4", 282 tagged: "true", 283 partner_code: "tb", 284 source: "unknown", 285 is_shopping_page: "false", 286 is_private: "false", 287 shopping_tab_displayed: "false", 288 is_signed_in: "false", 289 }, 290 }, 291 { 292 setUp() { 293 Services.cookies.removeAll(); 294 Services.cookies.add( 295 "www.example5.com", 296 "/", 297 "_dummyCookieName", 298 "abc=tb&def=ghi", 299 false, 300 false, 301 false, 302 Date.now() + 10 * 60 * 60 * 1000, 303 {}, 304 Ci.nsICookie.SAMESITE_UNSET, 305 Ci.nsICookie.SCHEME_HTTPS 306 ); 307 }, 308 tearDown() { 309 Services.cookies.removeAll(); 310 }, 311 title: "Tagged follow-on with cookie and no required url param", 312 trackingUrl: "https://www.example5.com/search?q=test&a=next", 313 expectedSearchCountEntry: "example5:tagged-follow-on:tb", 314 expectedAdKey: "example5:tagged-follow-on", 315 adUrls: ["https://www.example.com/ad2"], 316 nonAdUrls: ["https://www.example.com/ad3"], 317 impression: { 318 provider: "example5", 319 tagged: "true", 320 partner_code: "tb", 321 source: "unknown", 322 is_shopping_page: "false", 323 is_private: "false", 324 shopping_tab_displayed: "false", 325 is_signed_in: "false", 326 }, 327 }, 328 { 329 title: "Organic search matched code", 330 trackingUrl: "https://www.example.com/search?q=test&abc=foo", 331 expectedSearchCountEntry: "example:organic:foo", 332 expectedAdKey: "example:organic", 333 adUrls: ["https://www.example.com/ad2"], 334 nonAdUrls: ["https://www.example.com/ad3"], 335 impression: { 336 provider: "example", 337 tagged: "false", 338 partner_code: "foo", 339 source: "unknown", 340 is_shopping_page: "false", 341 is_private: "false", 342 shopping_tab_displayed: "false", 343 is_signed_in: "false", 344 }, 345 }, 346 { 347 title: "Organic search non-matched code", 348 trackingUrl: "https://www.example.com/search?q=test&abc=ff123", 349 expectedSearchCountEntry: "example:organic:other", 350 expectedAdKey: "example:organic", 351 adUrls: ["https://www.example.com/ad2"], 352 nonAdUrls: ["https://www.example.com/ad3"], 353 impression: { 354 provider: "example", 355 tagged: "false", 356 partner_code: "other", 357 source: "unknown", 358 is_shopping_page: "false", 359 is_private: "false", 360 shopping_tab_displayed: "false", 361 is_signed_in: "false", 362 }, 363 }, 364 { 365 title: "Organic search non-matched code 2", 366 trackingUrl: "https://www.example.com/search?q=test&abc=foo123", 367 expectedSearchCountEntry: "example:organic:other", 368 expectedAdKey: "example:organic", 369 adUrls: ["https://www.example.com/ad2"], 370 nonAdUrls: ["https://www.example.com/ad3"], 371 impression: { 372 provider: "example", 373 tagged: "false", 374 partner_code: "other", 375 source: "unknown", 376 is_shopping_page: "false", 377 is_private: "false", 378 shopping_tab_displayed: "false", 379 is_signed_in: "false", 380 }, 381 }, 382 { 383 title: "Organic search expected organic matched code", 384 trackingUrl: "https://www.example.com/search?q=test&abc=baz", 385 expectedSearchCountEntry: "example:organic:none", 386 expectedAdKey: "example:organic", 387 adUrls: ["https://www.example.com/ad2"], 388 nonAdUrls: ["https://www.example.com/ad3"], 389 impression: { 390 provider: "example", 391 tagged: "false", 392 partner_code: "", 393 source: "unknown", 394 is_shopping_page: "false", 395 is_private: "false", 396 shopping_tab_displayed: "false", 397 is_signed_in: "false", 398 }, 399 }, 400 { 401 title: "Organic search no codes", 402 trackingUrl: "https://www.example.com/search?q=test", 403 expectedSearchCountEntry: "example:organic:none", 404 expectedAdKey: "example:organic", 405 adUrls: ["https://www.example.com/ad2"], 406 nonAdUrls: ["https://www.example.com/ad3"], 407 impression: { 408 provider: "example", 409 tagged: "false", 410 partner_code: "", 411 source: "unknown", 412 is_shopping_page: "false", 413 is_private: "false", 414 shopping_tab_displayed: "false", 415 is_signed_in: "false", 416 }, 417 }, 418 { 419 title: "Different engines using the same adUrl", 420 trackingUrl: "https://www.example2.com/search?q=test", 421 expectedSearchCountEntry: "example2:organic:none", 422 expectedAdKey: "example2:organic", 423 adUrls: ["https://www.example.com/ad2"], 424 nonAdUrls: ["https://www.example.com/ad3"], 425 impression: { 426 provider: "example2", 427 tagged: "false", 428 partner_code: "", 429 source: "unknown", 430 is_shopping_page: "false", 431 is_private: "false", 432 shopping_tab_displayed: "false", 433 is_signed_in: "false", 434 }, 435 }, 436 ]; 437 438 /** 439 * This function is primarily for testing the Ad URL regexps that are triggered 440 * when a URL is clicked on. These regexps are also used for the `withads` 441 * probe. However, we test the adclicks route as that is easier to hit. 442 * 443 * @param {string} serpUrl 444 * The url to simulate where the page the click came from. 445 * @param {string} adUrl 446 * The ad url to simulate being clicked. 447 * @param {string} [expectedAdKey] 448 * The expected key to be logged for the scalar. Omit if no scalar should be 449 * logged. 450 */ 451 async function testAdUrlClicked(serpUrl, adUrl, expectedAdKey) { 452 info(`Testing Ad URL: ${adUrl}`); 453 let channel = NetUtil.newChannel({ 454 uri: NetUtil.newURI(adUrl), 455 triggeringPrincipal: Services.scriptSecurityManager.createContentPrincipal( 456 NetUtil.newURI(serpUrl), 457 {} 458 ), 459 loadUsingSystemPrincipal: true, 460 }); 461 SearchSERPTelemetry._contentHandler.observeActivity( 462 channel, 463 Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION, 464 Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE 465 ); 466 // Since the content handler takes a moment to allow the channel information 467 // to settle down, wait the same amount of time here. 468 await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); 469 470 const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); 471 if (!expectedAdKey) { 472 Assert.ok( 473 !("browser.search.adclicks.unknown" in scalars), 474 "Should not have recorded an ad click" 475 ); 476 } else { 477 TelemetryTestUtils.assertKeyedScalar( 478 scalars, 479 "browser.search.adclicks.unknown", 480 expectedAdKey, 481 1 482 ); 483 } 484 } 485 486 do_get_profile(); 487 488 add_setup(async function () { 489 Services.fog.initializeFOG(); 490 await SearchSERPTelemetry.init(); 491 SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); 492 sinon.stub(BrowserSearchTelemetry, "shouldRecordSearchCount").returns(true); 493 494 registerCleanupFunction(async () => { 495 sinon.restore(); 496 }); 497 }); 498 499 add_task(async function test_parsing_search_urls() { 500 for (const test of TESTS) { 501 info(`Running ${test.title}`); 502 if (test.setUp) { 503 test.setUp(); 504 } 505 let browser = { 506 getTabBrowser: () => {}, 507 // There is no concept of browsing in unit tests, so assume in tests that we 508 // are not in private browsing mode. We have browser tests that check when 509 // private browsing is used. 510 contentPrincipal: { 511 originAttributes: { 512 privateBrowsingId: 0, 513 }, 514 }, 515 }; 516 SearchSERPTelemetry.updateTrackingStatus(browser, test.trackingUrl); 517 SearchSERPTelemetry.reportPageImpression( 518 { 519 url: test.trackingUrl, 520 shoppingTabDisplayed: false, 521 }, 522 browser 523 ); 524 let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); 525 TelemetryTestUtils.assertKeyedScalar( 526 scalars, 527 "browser.search.content.unknown", 528 test.expectedSearchCountEntry, 529 1 530 ); 531 532 if ("adUrls" in test) { 533 for (const adUrl of test.adUrls) { 534 await testAdUrlClicked(test.trackingUrl, adUrl, test.expectedAdKey); 535 } 536 for (const nonAdUrls of test.nonAdUrls) { 537 await testAdUrlClicked(test.trackingUrl, nonAdUrls); 538 } 539 } 540 541 let recordedEvents = Glean.serp.impression.testGetValue(); 542 543 Assert.equal( 544 recordedEvents.length, 545 1, 546 "should only see one impression event" 547 ); 548 549 // To allow deep equality. 550 test.impression.impression_id = recordedEvents[0].extra.impression_id; 551 Assert.deepEqual(recordedEvents[0].extra, test.impression); 552 553 if (test.tearDown) { 554 test.tearDown(); 555 } 556 557 // We need to clear Glean events so they don't accumulate for each iteration. 558 Services.fog.testResetFOG(); 559 } 560 });