tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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');