private-aggregation.https.window.js (35030B)
1 // META: script=/resources/testdriver.js 2 // META: script=/resources/testdriver-vendor.js 3 // META: script=/common/utils.js 4 // META: script=resources/fledge-util.sub.js 5 // META: script=/common/subset-tests.js 6 // META: script=third_party/cbor-js/cbor.js 7 // META: timeout=long 8 // META: variant=?1-5 9 // META: variant=?6-10 10 // META: variant=?11-15 11 // META: variant=?16-20 12 13 'use strict'; 14 15 // To better isolate from private aggregation tests run in parallel, 16 // don't use the usual origin here. 17 const MAIN_ORIGIN = OTHER_ORIGIN1; 18 const ALT_ORIGIN = OTHER_ORIGIN4; 19 20 const MAIN_PATH = '/.well-known/private-aggregation/report-protected-audience'; 21 const DEBUG_PATH = 22 '/.well-known/private-aggregation/debug/report-protected-audience'; 23 24 const ADDITIONAL_BID_PUBLIC_KEY = 25 '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='; 26 27 const enableDebugMode = 'privateAggregation.enableDebugMode();'; 28 29 // The next 3 methods are for interfacing with the test handler for 30 // Private Aggregation reports; adopted wholesale from Chrome-specific 31 // wpt_internal/private-aggregation/resources/utils.js 32 const resetReports = url => { 33 url = `${url}?clear_stash=true`; 34 const options = { 35 method: 'POST', 36 mode: 'no-cors', 37 }; 38 return fetch(url, options); 39 }; 40 41 const delay = ms => new Promise(resolve => step_timeout(resolve, ms)); 42 43 async function pollReports(path, wait_for = 1, timeout = 5000 /*ms*/) { 44 const targetUrl = new URL(path, MAIN_ORIGIN); 45 const endTime = performance.now() + timeout; 46 const outReports = []; 47 48 do { 49 const response = await fetch(targetUrl); 50 assert_true(response.ok, 'pollReports() fetch response should be OK.'); 51 const reports = await response.json(); 52 outReports.push(...reports); 53 if (outReports.length >= wait_for) { 54 break; 55 } 56 await delay(/*ms=*/ 100); 57 } while (performance.now() < endTime); 58 59 return outReports.length ? outReports : null; 60 }; 61 62 function decodeBase64(inStr) { 63 let strBytes = atob(inStr); 64 let arrBytes = new Uint8Array(strBytes.length); 65 for (let i = 0; i < strBytes.length; ++i) { 66 arrBytes[i] = strBytes.codePointAt(i); 67 } 68 return arrBytes.buffer; 69 } 70 71 function byteArrayToBigInt(inArray) { 72 let out = 0n; 73 for (let byte of inArray) { 74 out = out * 256n + BigInt(byte); 75 } 76 return out; 77 } 78 79 async function getDebugSamples(path) { 80 const debugReports = await pollReports(path); 81 82 let samplesDict = new Map(); 83 84 // Extract samples for debug reports, and aggregate them, so we are not 85 // reliant on how aggregation happens. 86 for (let jsonReport of debugReports) { 87 let report = JSON.parse(jsonReport); 88 for (let payload of report.aggregation_service_payloads) { 89 let decoded = CBOR.decode(decodeBase64(payload.debug_cleartext_payload)); 90 assert_equals(decoded.operation, 'histogram'); 91 for (let sample of decoded.data) { 92 let convertedSample = { 93 bucket: byteArrayToBigInt(sample.bucket), 94 value: byteArrayToBigInt(sample.value) 95 }; 96 if (convertedSample.value !== 0n) { 97 let oldCount = 0n; 98 if (samplesDict.has(convertedSample.bucket)) { 99 oldCount = samplesDict.get(convertedSample.bucket); 100 } 101 102 samplesDict.set( 103 convertedSample.bucket, oldCount + convertedSample.value); 104 } 105 } 106 } 107 } 108 109 return samplesDict; 110 } 111 112 function stringifySamples(samplesDict) { 113 let samplesArray = []; 114 for (let [bucket, value] of samplesDict.entries()) { 115 // Stringify these so we can use assert_array_equals on them. 116 samplesArray.push(bucket + ' => ' + value); 117 } 118 samplesArray.sort(); 119 return samplesArray; 120 } 121 122 function maybeDelay(delayParam) { 123 if (delayParam) { 124 return `&pipe=trickle(d${delayParam / 1000})` 125 } else { 126 return ''; 127 } 128 } 129 130 function createIgOverrides(nameAndBid, fragments, originOverride = null) { 131 let originToUse = originOverride ? originOverride : MAIN_ORIGIN; 132 return { 133 name: nameAndBid, 134 biddingLogicURL: createBiddingScriptURL({ 135 origin: originToUse, 136 generateBid: 137 enableDebugMode + fragments.generateBidFragment, 138 reportWin: enableDebugMode + fragments.reportWinFragment, 139 bid: nameAndBid, 140 allowComponentAuction: true 141 }) + 142 maybeDelay(fragments.bidderDelayFactor ? 143 fragments.bidderDelayFactor * nameAndBid : 144 null) 145 }; 146 } 147 148 function expectAndConsume(samplesDict, bucket, val) { 149 assert_equals(samplesDict.get(bucket), val, 'sample in bucket ' + bucket); 150 samplesDict.delete(bucket); 151 } 152 153 function createAuctionConfigOverrides( 154 uuid, fragments, moreAuctionConfigOverrides = {}) { 155 return { 156 decisionLogicURL: 157 createDecisionScriptURL(uuid, { 158 origin: MAIN_ORIGIN, 159 scoreAd: enableDebugMode + fragments.scoreAdFragment, 160 reportResult: enableDebugMode + fragments.reportResultFragment 161 }) + 162 maybeDelay(fragments.sellerDelay), 163 seller: MAIN_ORIGIN, 164 interestGroupBuyers: [MAIN_ORIGIN], 165 privateAggregationConfig: 166 { aggregationCoordinatorOrigin: window.location.origin }, 167 ...moreAuctionConfigOverrides 168 }; 169 } 170 171 // Runs an auction with numGroups interest groups, "1" and "2", etc., with 172 // fragments.generateBidFragment/fragments.reportWinFragment/ 173 // fragments.scoreAdFragment/fragments.reportResultFragment 174 // expected to make some Private Aggregation contributions. 175 // Returns the collected samples. 176 async function runPrivateAggregationTest( 177 test, uuid, fragments, numGroups = 2, moreAuctionConfigOverrides = {}) { 178 await resetReports(MAIN_ORIGIN + MAIN_PATH); 179 await resetReports(MAIN_ORIGIN + DEBUG_PATH); 180 181 for (let i = 1; i <= numGroups; ++i) { 182 await joinCrossOriginInterestGroup( 183 test, uuid, MAIN_ORIGIN, createIgOverrides(i, fragments)); 184 } 185 186 const auctionConfigOverrides = 187 createAuctionConfigOverrides(uuid, fragments, moreAuctionConfigOverrides); 188 189 await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides); 190 return await getDebugSamples(DEBUG_PATH); 191 } 192 193 subsetTest(promise_test, async test => { 194 const uuid = generateUuid(test); 195 const fragments = { 196 generateBidFragment: ` 197 privateAggregation.contributeToHistogram({ bucket: 1n, value: 2 });`, 198 199 reportWinFragment: 200 `privateAggregation.contributeToHistogram({ bucket: 2n, value: 3 });`, 201 202 scoreAdFragment: 203 `privateAggregation.contributeToHistogram({ bucket: 3n, value: 4 });`, 204 205 reportResultFragment: 206 `privateAggregation.contributeToHistogram({ bucket: 4n, value: 5 });` 207 }; 208 209 const samples = await runPrivateAggregationTest(test, uuid, fragments); 210 assert_array_equals( 211 stringifySamples(samples), 212 [ 213 '1 => 4', // doubled since it's reported twice. 214 '2 => 3', 215 '3 => 8', // doubled since it's reported twice. 216 '4 => 5' 217 ]); 218 }, 'Basic contributions'); 219 220 subsetTest(promise_test, async test => { 221 const uuid = generateUuid(test); 222 const fragments = { 223 generateBidFragment: ` 224 privateAggregation.contributeToHistogramOnEvent( 225 'reserved.always', 226 { bucket: 1n, value: 2 });`, 227 228 reportWinFragment: ` 229 privateAggregation.contributeToHistogramOnEvent( 230 'reserved.always', 231 { bucket: 2n, value: 3 });`, 232 233 scoreAdFragment: ` 234 privateAggregation.contributeToHistogramOnEvent( 235 'reserved.always', 236 { bucket: 3n, value: 4 });`, 237 238 reportResultFragment: ` 239 privateAggregation.contributeToHistogramOnEvent( 240 'reserved.always', 241 { bucket: 4n, value: 5 });` 242 }; 243 244 const samples = await runPrivateAggregationTest(test, uuid, fragments); 245 assert_array_equals( 246 stringifySamples(samples), 247 [ 248 '1 => 4', // doubled since it's reported twice. 249 '2 => 3', 250 '3 => 8', // doubled since it's reported twice. 251 '4 => 5' 252 ]); 253 }, 'reserved.always'); 254 255 subsetTest(promise_test, async test => { 256 const uuid = generateUuid(test); 257 const fragments = { 258 generateBidFragment: ` 259 privateAggregation.contributeToHistogramOnEvent( 260 'reserved.win', 261 { bucket: 1n, value: interestGroup.name });`, 262 263 reportWinFragment: ` 264 privateAggregation.contributeToHistogramOnEvent( 265 'reserved.win', 266 { bucket: 2n, value: 3 });`, 267 268 scoreAdFragment: ` 269 privateAggregation.contributeToHistogramOnEvent( 270 'reserved.win', 271 { bucket: 3n, value: bid });`, 272 273 reportResultFragment: ` 274 privateAggregation.contributeToHistogramOnEvent( 275 'reserved.win', 276 { bucket: 4n, value: 5 });` 277 }; 278 279 const samples = await runPrivateAggregationTest(test, uuid, fragments); 280 assert_array_equals( 281 stringifySamples(samples), 282 [ 283 '1 => 2', // winning IG name 284 '2 => 3', 285 '3 => 2', // winning bid 286 '4 => 5' 287 ]); 288 }, 'reserved.win'); 289 290 subsetTest(promise_test, async test => { 291 const uuid = generateUuid(test); 292 const fragments = { 293 generateBidFragment: ` 294 privateAggregation.contributeToHistogramOnEvent( 295 'reserved.loss', 296 { bucket: 1n, value: interestGroup.name });`, 297 298 reportWinFragment: ` 299 privateAggregation.contributeToHistogramOnEvent( 300 'reserved.loss', 301 { bucket: 2n, value: 3 });`, 302 303 scoreAdFragment: ` 304 privateAggregation.contributeToHistogramOnEvent( 305 'reserved.loss', 306 { bucket: 3n, value: bid });`, 307 308 reportResultFragment: ` 309 privateAggregation.contributeToHistogramOnEvent( 310 'reserved.loss', 311 { bucket: 4n, value: 5 });` 312 }; 313 314 const samples = await runPrivateAggregationTest(test, uuid, fragments); 315 316 // No reserved.loss from reporting since they only run for winners. 317 assert_array_equals( 318 stringifySamples(samples), 319 [ 320 '1 => 1', // losing IG name 321 '3 => 1', // losing bid 322 ]); 323 }, 'reserved.loss'); 324 325 subsetTest(promise_test, async test => { 326 const uuid = generateUuid(test); 327 const fragments = { 328 generateBidFragment: ` 329 privateAggregation.contributeToHistogramOnEvent( 330 'reserved.once', 331 { bucket: 1n, value: interestGroup.name });`, 332 333 reportWinFragment: ` 334 privateAggregation.contributeToHistogramOnEvent( 335 'reserved.once', 336 { bucket: 2n, value: 3 });`, 337 338 scoreAdFragment: ` 339 privateAggregation.contributeToHistogramOnEvent( 340 'reserved.once', 341 { bucket: 3n, value: bid });`, 342 343 reportResultFragment: ` 344 privateAggregation.contributeToHistogramOnEvent( 345 'reserved.once', 346 { bucket: 4n, value: 5 });` 347 }; 348 349 const samples = 350 stringifySamples(await runPrivateAggregationTest(test, uuid, fragments)); 351 352 // No reserved.once from reporting since it throws an exception. 353 // bidder/scorer just pick one. 354 assert_equals(samples.length, 2, 'samples array length'); 355 assert_in_array(samples[0], ['1 => 1', '1 => 2'], 'samples[0]'); 356 assert_in_array(samples[1], ['3 => 1', '3 => 2'], 'samples[1]'); 357 }, 'reserved.once'); 358 359 subsetTest(promise_test, async test => { 360 const uuid = generateUuid(test); 361 const fragments = { 362 generateBidFragment: ` 363 privateAggregation.contributeToHistogramOnEvent( 364 'reserved.once', 365 { bucket: 1n, value: 1 });`, 366 367 reportWinFragment: ` 368 try { 369 privateAggregation.contributeToHistogramOnEvent( 370 'reserved.once', 371 { bucket: 2n, value: 2 }); 372 } catch (e) { 373 privateAggregation.contributeToHistogramOnEvent( 374 'reserved.always', 375 { bucket: 2n, value: (e instanceof TypeError ? 3 : 4) }); 376 }`, 377 378 scoreAdFragment: ` 379 privateAggregation.contributeToHistogramOnEvent( 380 'reserved.once', 381 { bucket: 3n, value: 4 });`, 382 383 reportResultFragment: ` 384 try { 385 privateAggregation.contributeToHistogramOnEvent( 386 'reserved.once', 387 { bucket: 4n, value: 5 }); 388 } catch (e) { 389 privateAggregation.contributeToHistogramOnEvent( 390 'reserved.always', 391 { bucket: 4n, value: (e instanceof TypeError ? 6 : 7) }); 392 }` 393 }; 394 395 const samples = 396 stringifySamples(await runPrivateAggregationTest(test, uuid, fragments)); 397 398 assert_array_equals(samples, [ 399 '1 => 1', 400 '2 => 3', 401 '3 => 4', 402 '4 => 6', 403 ]); 404 }, 'no reserved.once in reporting'); 405 406 subsetTest(promise_test, async test => { 407 const uuid = generateUuid(test); 408 await resetReports(ALT_ORIGIN + DEBUG_PATH); 409 await resetReports(ALT_ORIGIN + MAIN_PATH); 410 411 const fragments = { 412 generateBidFragment: ` 413 privateAggregation.contributeToHistogramOnEvent( 414 'reserved.once', { 415 bucket: {baseValue: 'average-code-fetch-time', offset: 0n}, 416 value: 1});`, 417 418 reportWinFragment: ` 419 privateAggregation.contributeToHistogramOnEvent( 420 'reserved.always', { 421 bucket: {baseValue: 'average-code-fetch-time', offset: 100000n}, 422 value: 1});`, 423 424 bidderDelayFactor: 200, 425 426 scoreAdFragment: ` 427 privateAggregation.contributeToHistogramOnEvent( 428 'reserved.once', { 429 bucket: {baseValue: 'average-code-fetch-time', offset: 200000n}, 430 value: 1});`, 431 432 reportResultFragment: ` 433 privateAggregation.contributeToHistogramOnEvent( 434 'reserved.always', { 435 bucket: {baseValue: 'average-code-fetch-time', offset: 300000n}, 436 value: 1});`, 437 438 sellerDelay: 500 439 }; 440 441 const altFragments = { 442 generateBidFragment: fragments.generateBidFragment, 443 bidderDelayFactor: 1000 444 }; 445 446 await joinCrossOriginInterestGroup( 447 test, uuid, ALT_ORIGIN, createIgOverrides('1', altFragments, ALT_ORIGIN)); 448 const auctionConfigOverrides = { 449 interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN] 450 }; 451 452 const samples = await runPrivateAggregationTest( 453 test, uuid, fragments, 3, auctionConfigOverrides); 454 455 let generateBidVal = -1; 456 let reportWinVal = -1; 457 let scoreAdVal = -1; 458 let reportResultVal = -1; 459 assert_equals(samples.size, 4, 'main domain samples'); 460 461 for (let [bucket, val] of samples.entries()) { 462 assert_equals(val, 1n, 'bucket val'); 463 if (0n <= bucket && bucket < 100000n) { 464 generateBidVal = Number(bucket - 0n); 465 } else if (100000n <= bucket && bucket < 200000n) { 466 reportWinVal = Number(bucket - 100000n); 467 } else if (200000n <= bucket && bucket < 300000n) { 468 scoreAdVal = Number(bucket - 200000n); 469 } else if (300000n <= bucket && bucket < 400000n) { 470 reportResultVal = Number(bucket - 300000n); 471 } else { 472 assert_unreached('Unexpected bucket number ' + bucket); 473 } 474 } 475 476 assert_greater_than_equal(generateBidVal, 400, 'generateBid code fetch time'); 477 assert_greater_than_equal(reportWinVal, 600, 'reportWin code fetch time'); 478 assert_greater_than_equal(scoreAdVal, 500, 'scoreAd code fetch time'); 479 assert_greater_than_equal( 480 reportResultVal, 500, 'reportResult code fetch time'); 481 482 let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH); 483 assert_equals(otherSamples.size, 1, 'alt domain samples'); 484 let otherGenerateBidVal = -1; 485 for (let [bucket, val] of otherSamples.entries()) { 486 assert_equals(val, 1n, 'other bucket val'); 487 if (0n <= bucket && bucket < 100000n) { 488 otherGenerateBidVal = Number(bucket - 0n); 489 } else { 490 assert_unreached('Unexpected other bucket number ' + bucket); 491 } 492 } 493 assert_greater_than_equal( 494 otherGenerateBidVal, 1000, 'other generateBid code fetch time'); 495 }, 'average-code-fetch-time'); 496 497 subsetTest(promise_test, async test => { 498 const uuid = generateUuid(test); 499 const fragments = { 500 generateBidFragment: ` 501 privateAggregation.contributeToHistogramOnEvent( 502 'reserved.once', { 503 bucket: {baseValue: 'percent-scripts-timeout', offset: 0n}, 504 value: 1}); 505 if (interestGroup.name === '1') { 506 while (true) {} 507 } 508 `, 509 510 reportWinFragment: ` 511 privateAggregation.contributeToHistogramOnEvent( 512 'reserved.always', { 513 bucket: {baseValue: 'percent-scripts-timeout', offset: 200n}, 514 value: 1}); 515 while(true) {}`, 516 517 scoreAdFragment: ` 518 privateAggregation.contributeToHistogramOnEvent( 519 'reserved.once', { 520 bucket: {baseValue: 'percent-scripts-timeout', offset: 400n}, 521 value: 1}); 522 if (bid == 2) { 523 while (true) {} 524 } 525 `, 526 527 reportResultFragment: ` 528 privateAggregation.contributeToHistogramOnEvent( 529 'reserved.always', { 530 bucket: {baseValue: 'percent-scripts-timeout', offset: 600n}, 531 value: 1});` 532 }; 533 534 const samples = await runPrivateAggregationTest(test, uuid, fragments, 3); 535 536 let expected = [ 537 '33 => 1', // 33% of generateBid (base bucket 0) 538 '300 => 1', // 100% of reportWin (base bucket 200) 539 '450 => 1', // 50% of scoreAd (base bucket 400) 540 '600 => 1', // 0% of reportResult (base bucket 600) 541 ].sort(); 542 543 assert_array_equals(stringifySamples(samples), expected); 544 }, 'percent-scripts-timeout'); 545 546 subsetTest(promise_test, async test => { 547 const uuid = generateUuid(test); 548 await resetReports(ALT_ORIGIN + DEBUG_PATH); 549 await resetReports(ALT_ORIGIN + MAIN_PATH); 550 551 const ADDITIONAL_BID_PUBLIC_KEY = 552 '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='; 553 554 // Join a negative group, one without ads. 555 // These shouldn't count towards participant number. 556 await joinNegativeInterestGroup( 557 test, MAIN_ORIGIN, 'some negative group', ADDITIONAL_BID_PUBLIC_KEY); 558 await joinCrossOriginInterestGroup(test, uuid, MAIN_ORIGIN, {ads: []}); 559 560 const fragments = { 561 generateBidFragment: ` 562 privateAggregation.contributeToHistogramOnEvent( 563 'reserved.once', { 564 bucket: {baseValue: 'participating-ig-count', offset: 0n}, 565 value: 1});`, 566 567 reportWinFragment: ` 568 privateAggregation.contributeToHistogramOnEvent( 569 'reserved.always', { 570 bucket: {baseValue: 'participating-ig-count', offset: 200n}, 571 value: 1});`, 572 573 scoreAdFragment: ` 574 privateAggregation.contributeToHistogramOnEvent( 575 'reserved.once', { 576 bucket: {baseValue: 'participating-ig-count', offset: 400n}, 577 value: 1});`, 578 579 reportResultFragment: ` 580 privateAggregation.contributeToHistogramOnEvent( 581 'reserved.always', { 582 bucket: {baseValue: 'participating-ig-count', offset: 600n}, 583 value: 1});` 584 }; 585 586 // ... and a different participant should get their own samples. 587 await joinCrossOriginInterestGroup( 588 test, uuid, ALT_ORIGIN, createIgOverrides('1', fragments, ALT_ORIGIN)); 589 await joinCrossOriginInterestGroup( 590 test, uuid, ALT_ORIGIN, createIgOverrides('2', fragments, ALT_ORIGIN)); 591 const auctionConfigOverrides = { 592 interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN] 593 }; 594 595 const samples = await runPrivateAggregationTest( 596 test, uuid, fragments, 5, auctionConfigOverrides); 597 598 let expected = [ 599 '5 => 1', // 5 in generateBid (base bucket 0) 600 '205 => 1', // 5 in reportWin (base bucket 200) 601 '400 => 1', // 0 in scoreAd (base bucket 400) 602 '600 => 1', // 0 in reportResult (base bucket 600) 603 ].sort(); 604 605 assert_array_equals(stringifySamples(samples), expected); 606 607 let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH); 608 assert_array_equals(stringifySamples(otherSamples), ['2 => 1']); 609 }, 'participating-ig-count'); 610 611 612 subsetTest(promise_test, async test => { 613 const uuid = generateUuid(test); 614 const fragments = { 615 generateBidFragment: ` 616 privateAggregation.contributeToHistogramOnEvent( 617 'reserved.once', { 618 bucket: { 619 baseValue: 'percent-igs-cumulative-timeout', 620 offset: 0n 621 }, 622 value: 1}); 623 privateAggregation.contributeToHistogramOnEvent( 624 'reserved.once', { 625 bucket: { 626 baseValue: 'cumulative-buyer-time', 627 offset: 10000n 628 }, 629 value: 1}); 630 setBid({bid: interestGroup.name, render: interestGroup.ads[0].renderURL}); 631 while (true) {} 632 `, 633 634 reportWinFragment: ` 635 privateAggregation.contributeToHistogramOnEvent( 636 'reserved.always', { 637 bucket: { 638 baseValue: 'percent-igs-cumulative-timeout', 639 offset: 200n 640 }, 641 value: 1}); 642 privateAggregation.contributeToHistogramOnEvent( 643 'reserved.always', { 644 bucket: { 645 baseValue: 'cumulative-buyer-time', 646 offset: 20000n 647 }, 648 value: 1}); 649 `, 650 651 scoreAdFragment: ` 652 privateAggregation.contributeToHistogramOnEvent( 653 'reserved.once', { 654 bucket: { 655 baseValue: 'percent-igs-cumulative-timeout', 656 offset: 400n 657 }, 658 value: 1}); 659 privateAggregation.contributeToHistogramOnEvent( 660 'reserved.once', { 661 bucket: { 662 baseValue: 'cumulative-buyer-time', 663 offset: 40000n 664 }, 665 value: 1}); 666 `, 667 668 reportResultFragment: ` 669 privateAggregation.contributeToHistogramOnEvent( 670 'reserved.always', { 671 bucket: { 672 baseValue: 'percent-igs-cumulative-timeout', 673 offset: 600n 674 }, 675 value: 1}); 676 privateAggregation.contributeToHistogramOnEvent( 677 'reserved.always', { 678 bucket: { 679 baseValue: 'cumulative-buyer-time', 680 offset: 60000n 681 }, 682 value: 1});` 683 }; 684 685 const auctionConfigOverrides = { 686 perBuyerTimeouts: { 687 '*': 500 // max. 688 }, 689 perBuyerCumulativeTimeouts: {'*': 2000} 690 }; 691 692 const samples = await runPrivateAggregationTest( 693 test, uuid, fragments, 15, auctionConfigOverrides); 694 695 // Timeout is reported as 3000 (limit + 1000) for generateBid 696 // and reportWin, as 0 for the seller methods. 697 expectAndConsume(samples, 13000n, 1n); // base is 10,000 698 expectAndConsume(samples, 23000n, 1n); 699 expectAndConsume(samples, 40000n, 1n); 700 expectAndConsume(samples, 60000n, 1n); 701 702 // percent time is 0 on the seller side. 703 expectAndConsume(samples, 400n, 1n); 704 expectAndConsume(samples, 600n, 1n); 705 706 assert_equals(samples.size, 2, 'buyer samples'); 707 708 let percentGenerateBid = -1; 709 let percentReportWin = -1; 710 711 for (let [bucket, val] of samples.entries()) { 712 assert_equals(val, 1n, 'bucket val'); 713 if (0n <= bucket && bucket <= 110n) { 714 percentGenerateBid = bucket; 715 } else if (200n <= bucket && bucket <= 310n) { 716 percentReportWin = bucket - 200n; 717 } else { 718 assert_unreached('Unexpected bucket number ' + bucket); 719 } 720 } 721 722 assert_equals( 723 percentGenerateBid, percentReportWin, 724 'same % in generateBid and reportWin'); 725 726 // This assumes that at least some time out; which may not be guaranteed with 727 // sufficient level of parallelism. At any rate, the denominator is 15, 728 // however, so only some percentages are possible. 729 assert_in_array( 730 percentGenerateBid, 731 [6n, 13n, 20n, 26n, 33n, 40n, 46n, 53n, 60n, 66n, 73n, 80n, 86n, 93n], 732 'percent timeout is as expected'); 733 }, 'percent-igs-cumulative-timeout, and cumulative-buyer-time when hit'); 734 735 subsetTest(promise_test, async test => { 736 const uuid = generateUuid(test); 737 const fragments = { 738 generateBidFragment: ` 739 privateAggregation.contributeToHistogramOnEvent( 740 'reserved.once', { 741 bucket: {baseValue: 'cumulative-buyer-time', offset: 0n}, 742 value: 1});`, 743 744 reportWinFragment: ` 745 privateAggregation.contributeToHistogramOnEvent( 746 'reserved.always', { 747 bucket: {baseValue: 'cumulative-buyer-time', offset: 200n}, 748 value: 1});`, 749 750 scoreAdFragment: ` 751 privateAggregation.contributeToHistogramOnEvent( 752 'reserved.once', { 753 bucket: {baseValue: 'cumulative-buyer-time', offset: 400n}, 754 value: 1});`, 755 756 reportResultFragment: ` 757 privateAggregation.contributeToHistogramOnEvent( 758 'reserved.always', { 759 bucket: {baseValue: 'cumulative-buyer-time', offset: 600n}, 760 value: 1});` 761 }; 762 763 const samples = await runPrivateAggregationTest(test, uuid, fragments, 5); 764 765 // 0s for all the bases. 766 let expected = ['0 => 1', '200 => 1', '400 => 1', '600 => 1'].sort(); 767 768 assert_array_equals(stringifySamples(samples), expected); 769 }, 'cumulative-buyer-time when not configured'); 770 771 subsetTest(promise_test, async test => { 772 const uuid = generateUuid(test); 773 const fragments = { 774 generateBidFragment: ` 775 privateAggregation.contributeToHistogramOnEvent( 776 'reserved.once', { 777 bucket: {baseValue: 'cumulative-buyer-time', offset: 0n}, 778 value: 1});`, 779 780 reportWinFragment: ` 781 privateAggregation.contributeToHistogramOnEvent( 782 'reserved.always', { 783 bucket: {baseValue: 'cumulative-buyer-time', offset: 10000n}, 784 value: 1});`, 785 786 scoreAdFragment: ` 787 privateAggregation.contributeToHistogramOnEvent( 788 'reserved.once', { 789 bucket: {baseValue: 'cumulative-buyer-time', offset: 20000n}, 790 value: 1});`, 791 792 reportResultFragment: ` 793 privateAggregation.contributeToHistogramOnEvent( 794 'reserved.always', { 795 bucket: {baseValue: 'cumulative-buyer-time', offset: 30000n}, 796 value: 1});` 797 }; 798 799 const auctionConfigOverrides = {perBuyerCumulativeTimeouts: {'*': 4000}}; 800 801 const samples = await runPrivateAggregationTest( 802 test, uuid, fragments, 5, auctionConfigOverrides); 803 804 // Sellers stuff is just 0s (so 1 to the base bucket offset). 805 expectAndConsume(samples, 20000n, 1n); 806 expectAndConsume(samples, 30000n, 1n); 807 808 assert_equals(samples.size, 2, 'buyer samples'); 809 810 let timeGenerateBid = -1; 811 let timeReportWin = -1; 812 813 for (let [bucket, val] of samples.entries()) { 814 assert_equals(val, 1n, 'bucket val'); 815 if (0n <= bucket && bucket <= 5000n) { 816 timeGenerateBid = bucket; 817 } else if (10000n <= bucket && bucket <= 15000n) { 818 timeReportWin = bucket - 10000n; 819 } else { 820 assert_unreached('Unexpected bucket number'); 821 } 822 } 823 824 assert_equals( 825 timeGenerateBid, timeReportWin, 'same time in generateBid and reportWin'); 826 827 // This assume this takes more than 0ms to run; it's not really required to 828 // be the case, but feels like a realistic assumption that makes the test 829 // more useful. 830 assert_true( 831 1n <= timeGenerateBid && timeGenerateBid <= 4000n, 832 'time ' + timeGenerateBid + ' is reasonable and non-zero'); 833 }, 'cumulative-buyer-time when configured'); 834 835 836 async function testStorageQuotaMetric(test, name) { 837 const uuid = generateUuid(test); 838 const fragments = { 839 generateBidFragment: ` 840 privateAggregation.contributeToHistogramOnEvent( 841 'reserved.once', { 842 bucket: {baseValue: '${name}', offset: 0n}, 843 value: 1});`, 844 845 reportWinFragment: ` 846 privateAggregation.contributeToHistogramOnEvent( 847 'reserved.always', { 848 bucket: {baseValue: '${name}', offset: 10000n}, 849 value: 1});`, 850 851 scoreAdFragment: ` 852 privateAggregation.contributeToHistogramOnEvent( 853 'reserved.once', { 854 bucket: {baseValue: '${name}', offset: 20000n}, 855 value: 1});`, 856 857 reportResultFragment: ` 858 privateAggregation.contributeToHistogramOnEvent( 859 'reserved.always', { 860 bucket: {baseValue: '${name}', offset: 30000n}, 861 value: 1});` 862 }; 863 864 const samples = await runPrivateAggregationTest(test, uuid, fragments, 5); 865 866 // Sellers stuff is just 0s (so 1 to the base bucket offset). 867 expectAndConsume(samples, 20000n, 1n); 868 expectAndConsume(samples, 30000n, 1n); 869 870 assert_equals(samples.size, 2, 'buyer samples'); 871 872 let generateBidVal = -1; 873 let reportWinVal = -1; 874 875 for (let [bucket, val] of samples.entries()) { 876 assert_equals(val, 1n, 'bucket val'); 877 if (0n <= bucket && bucket < 10000n) { 878 generateBidVal = Number(bucket); 879 } else if (10000n <= bucket && bucket <= 20000n) { 880 reportWinVal = Number(bucket - 10000n); 881 } else { 882 assert_unreached('Unexpected bucket number ' + bucket); 883 } 884 } 885 886 assert_equals( 887 generateBidVal, reportWinVal, 'same value in generateBid and reportWin'); 888 889 // We don't know what the impls quota is, or even how much we are using, 890 // but at least make sure it's in range. 891 assert_between_inclusive( 892 generateBidVal, 0, 110, 'reported percent value is in expected range'); 893 } 894 895 subsetTest(promise_test, async test => { 896 await testStorageQuotaMetric(test, 'percent-regular-ig-count-quota-used'); 897 }, 'percent-regular-ig-count-quota-used'); 898 899 subsetTest(promise_test, async test => { 900 await testStorageQuotaMetric(test, 'percent-negative-ig-count-quota-used'); 901 }, 'percent-negative-ig-count-quota-used'); 902 903 subsetTest(promise_test, async test => { 904 await testStorageQuotaMetric(test, 'percent-ig-storage-quota-used'); 905 }, 'percent-ig-storage-quota-used'); 906 907 908 async function testStorageUsageMetric(test, name, min) { 909 const uuid = generateUuid(test); 910 const spacing = 1000000000n; 911 const fragments = { 912 generateBidFragment: ` 913 privateAggregation.contributeToHistogramOnEvent( 914 'reserved.once', { 915 bucket: {baseValue: '${name}', offset: 0n}, 916 value: 1});`, 917 918 reportWinFragment: ` 919 privateAggregation.contributeToHistogramOnEvent( 920 'reserved.always', { 921 bucket: {baseValue: '${name}', offset: ${spacing}n}, 922 value: 1});`, 923 924 scoreAdFragment: ` 925 privateAggregation.contributeToHistogramOnEvent( 926 'reserved.once', { 927 bucket: {baseValue: '${name}', offset: 2n * ${spacing}n}, 928 value: 1});`, 929 930 reportResultFragment: ` 931 privateAggregation.contributeToHistogramOnEvent( 932 'reserved.always', { 933 bucket: {baseValue: '${name}', offset: 3n * ${spacing}n}, 934 value: 1});` 935 }; 936 937 await joinNegativeInterestGroup( 938 test, MAIN_ORIGIN, 'some negative group', ADDITIONAL_BID_PUBLIC_KEY); 939 await joinNegativeInterestGroup( 940 test, MAIN_ORIGIN, 'some negative group 2', ADDITIONAL_BID_PUBLIC_KEY); 941 await joinCrossOriginInterestGroup( 942 test, uuid, MAIN_ORIGIN, 943 {ads: [], name: 'Big group w/o ads'.padEnd(50000)}); 944 945 const samples = await runPrivateAggregationTest(test, uuid, fragments, 5); 946 947 // Sellers stuff is just 0s (so 1 to the base bucket offset). 948 expectAndConsume(samples, 2n * spacing, 1n); 949 expectAndConsume(samples, 3n * spacing, 1n); 950 951 assert_equals(samples.size, 2, 'buyer samples'); 952 953 let generateBidVal = -1; 954 let reportWinVal = -1; 955 956 for (let [bucket, val] of samples.entries()) { 957 assert_equals(val, 1n, 'bucket val'); 958 if (0n <= bucket && bucket < spacing) { 959 generateBidVal = bucket; 960 } else if (spacing <= bucket && bucket < 2n * spacing) { 961 reportWinVal = bucket - spacing; 962 } else { 963 assert_unreached('Unexpected bucket number ' + bucket); 964 } 965 } 966 967 assert_equals( 968 generateBidVal, reportWinVal, 'same value in generateBid and reportWin'); 969 970 assert_true( 971 generateBidVal >= BigInt(min), 972 'reported value should be at least ' + min + ' but is ' + generateBidVal); 973 } 974 975 subsetTest(promise_test, async test => { 976 // 5 regular Igs + one ad less. 977 await testStorageUsageMetric(test, 'regular-igs-count', 6); 978 }, 'regular-igs-count'); 979 980 subsetTest(promise_test, async test => { 981 // 2 negative IGs 982 await testStorageUsageMetric(test, 'negative-igs-count', 2); 983 }, 'negative-igs-count'); 984 985 subsetTest(promise_test, async test => { 986 // The big group has a 50,000 character name 987 await testStorageUsageMetric(test, 'ig-storage-used', 50000); 988 }, 'ig-storage-used'); 989 990 subsetTest(promise_test, async test => { 991 const uuid = generateUuid(test); 992 await resetReports(MAIN_ORIGIN + MAIN_PATH); 993 await resetReports(MAIN_ORIGIN + DEBUG_PATH); 994 await resetReports(ALT_ORIGIN + MAIN_PATH); 995 await resetReports(ALT_ORIGIN + DEBUG_PATH); 996 997 const fragments = { 998 generateBidFragment: ` 999 privateAggregation.contributeToHistogramOnEvent( 1000 'reserved.once', 1001 { bucket: 1n, value: 2 });`, 1002 1003 reportWinFragment: ` 1004 privateAggregation.contributeToHistogramOnEvent( 1005 'reserved.always', 1006 { bucket: 2n, value: 3 });`, 1007 1008 scoreAdFragment: ` 1009 privateAggregation.contributeToHistogramOnEvent( 1010 'reserved.once', 1011 { bucket: 3n, value: 4 });`, 1012 1013 reportResultFragment: ` 1014 privateAggregation.contributeToHistogramOnEvent( 1015 'reserved.always', 1016 { bucket: 4n, value: 5 });` 1017 }; 1018 1019 // 4 IGs in main origin, 2 in alt origin. 1020 for (let i = 1; i <= 4; ++i) { 1021 await joinCrossOriginInterestGroup( 1022 test, uuid, MAIN_ORIGIN, createIgOverrides(i, fragments)); 1023 } 1024 1025 for (let i = 1; i <= 2; ++i) { 1026 await joinCrossOriginInterestGroup( 1027 test, uuid, ALT_ORIGIN, createIgOverrides(i, fragments, ALT_ORIGIN)); 1028 } 1029 1030 // Both groups in component auction 1, only alt group in component auction 2. 1031 const subAuction1 = createAuctionConfigOverrides( 1032 uuid, fragments, {interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN]}); 1033 const subAuction2 = createAuctionConfigOverrides( 1034 uuid, fragments, {interestGroupBuyers: [ALT_ORIGIN]}); 1035 1036 const topFragments = { 1037 scoreAdFragment: ` 1038 privateAggregation.contributeToHistogramOnEvent( 1039 'reserved.once', 1040 { bucket: 5n, value: 6 });`, 1041 1042 reportResultFragment: ` 1043 privateAggregation.contributeToHistogramOnEvent( 1044 'reserved.always', 1045 { bucket: 6n, value: 7 });` 1046 }; 1047 const mainAuction = createAuctionConfigOverrides( 1048 uuid, topFragments, 1049 {interestGroupBuyers: [], componentAuctions: [subAuction1, subAuction2]}); 1050 1051 await runBasicFledgeAuctionAndNavigate(test, uuid, mainAuction); 1052 let samples = await getDebugSamples(DEBUG_PATH); 1053 let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH); 1054 let expected = [ 1055 '1 => 2', // generateBid only in first component, so happens 1. 1056 '2 => 3', // reportWin once. 1057 '3 => 8', // Once per each component auction (out of total 6 scored). 1058 '4 => 5', // component reportResult once. 1059 '5 => 6', // top-level scoreAd once. 1060 '6 => 7', // top-level reportResult. 1061 ].sort(); 1062 let otherExpected = [ 1063 '1 => 4', // generateBid in each components, so twice, out of 4 executions. 1064 ].sort(); 1065 assert_array_equals(stringifySamples(samples), expected); 1066 assert_array_equals(stringifySamples(otherSamples), otherExpected); 1067 }, 'report.once in a component auction');