| // META: script=/resources/testdriver.js |
| // META: script=/resources/testdriver-vendor.js |
| // META: script=/common/utils.js |
| // META: script=resources/fledge-util.sub.js |
| // META: script=/common/subset-tests.js |
| // META: script=third_party/cbor-js/cbor.js |
| // META: timeout=long |
| // META: variant=?1-5 |
| // META: variant=?6-10 |
| // META: variant=?11-15 |
| // META: variant=?16-20 |
| |
| 'use strict'; |
| |
| // To better isolate from private aggregation tests run in parallel, |
| // don't use the usual origin here. |
| const MAIN_ORIGIN = OTHER_ORIGIN1; |
| const ALT_ORIGIN = OTHER_ORIGIN4; |
| |
| const MAIN_PATH = '/.well-known/private-aggregation/report-protected-audience'; |
| const DEBUG_PATH = |
| '/.well-known/private-aggregation/debug/report-protected-audience'; |
| |
| const ADDITIONAL_BID_PUBLIC_KEY = |
| '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='; |
| |
| const enableDebugMode = 'privateAggregation.enableDebugMode();'; |
| |
| // The next 3 methods are for interfacing with the test handler for |
| // Private Aggregation reports; adopted wholesale from Chrome-specific |
| // wpt_internal/private-aggregation/resources/utils.js |
| const resetReports = url => { |
| url = `${url}?clear_stash=true`; |
| const options = { |
| method: 'POST', |
| mode: 'no-cors', |
| }; |
| return fetch(url, options); |
| }; |
| |
| const delay = ms => new Promise(resolve => step_timeout(resolve, ms)); |
| |
| async function pollReports(path, wait_for = 1, timeout = 5000 /*ms*/) { |
| const targetUrl = new URL(path, MAIN_ORIGIN); |
| const endTime = performance.now() + timeout; |
| const outReports = []; |
| |
| do { |
| const response = await fetch(targetUrl); |
| assert_true(response.ok, 'pollReports() fetch response should be OK.'); |
| const reports = await response.json(); |
| outReports.push(...reports); |
| if (outReports.length >= wait_for) { |
| break; |
| } |
| await delay(/*ms=*/ 100); |
| } while (performance.now() < endTime); |
| |
| return outReports.length ? outReports : null; |
| }; |
| |
| function decodeBase64(inStr) { |
| let strBytes = atob(inStr); |
| let arrBytes = new Uint8Array(strBytes.length); |
| for (let i = 0; i < strBytes.length; ++i) { |
| arrBytes[i] = strBytes.codePointAt(i); |
| } |
| return arrBytes.buffer; |
| } |
| |
| function byteArrayToBigInt(inArray) { |
| let out = 0n; |
| for (let byte of inArray) { |
| out = out * 256n + BigInt(byte); |
| } |
| return out; |
| } |
| |
| async function getDebugSamples(path) { |
| const debugReports = await pollReports(path); |
| |
| let samplesDict = new Map(); |
| |
| // Extract samples for debug reports, and aggregate them, so we are not |
| // reliant on how aggregation happens. |
| for (let jsonReport of debugReports) { |
| let report = JSON.parse(jsonReport); |
| for (let payload of report.aggregation_service_payloads) { |
| let decoded = CBOR.decode(decodeBase64(payload.debug_cleartext_payload)); |
| assert_equals(decoded.operation, 'histogram'); |
| for (let sample of decoded.data) { |
| let convertedSample = { |
| bucket: byteArrayToBigInt(sample.bucket), |
| value: byteArrayToBigInt(sample.value) |
| }; |
| if (convertedSample.value !== 0n) { |
| let oldCount = 0n; |
| if (samplesDict.has(convertedSample.bucket)) { |
| oldCount = samplesDict.get(convertedSample.bucket); |
| } |
| |
| samplesDict.set( |
| convertedSample.bucket, oldCount + convertedSample.value); |
| } |
| } |
| } |
| } |
| |
| return samplesDict; |
| } |
| |
| function stringifySamples(samplesDict) { |
| let samplesArray = []; |
| for (let [bucket, value] of samplesDict.entries()) { |
| // Stringify these so we can use assert_array_equals on them. |
| samplesArray.push(bucket + ' => ' + value); |
| } |
| samplesArray.sort(); |
| return samplesArray; |
| } |
| |
| function maybeDelay(delayParam) { |
| if (delayParam) { |
| return `&pipe=trickle(d${delayParam / 1000})` |
| } else { |
| return ''; |
| } |
| } |
| |
| function createIgOverrides(nameAndBid, fragments, originOverride = null) { |
| let originToUse = originOverride ? originOverride : MAIN_ORIGIN; |
| return { |
| name: nameAndBid, |
| biddingLogicURL: createBiddingScriptURL({ |
| origin: originToUse, |
| generateBid: |
| enableDebugMode + fragments.generateBidFragment, |
| reportWin: enableDebugMode + fragments.reportWinFragment, |
| bid: nameAndBid, |
| allowComponentAuction: true |
| }) + |
| maybeDelay(fragments.bidderDelayFactor ? |
| fragments.bidderDelayFactor * nameAndBid : |
| null) |
| }; |
| } |
| |
| function expectAndConsume(samplesDict, bucket, val) { |
| assert_equals(samplesDict.get(bucket), val, 'sample in bucket ' + bucket); |
| samplesDict.delete(bucket); |
| } |
| |
| function createAuctionConfigOverrides( |
| uuid, fragments, moreAuctionConfigOverrides = {}) { |
| return { |
| decisionLogicURL: |
| createDecisionScriptURL(uuid, { |
| origin: MAIN_ORIGIN, |
| scoreAd: enableDebugMode + fragments.scoreAdFragment, |
| reportResult: enableDebugMode + fragments.reportResultFragment |
| }) + |
| maybeDelay(fragments.sellerDelay), |
| seller: MAIN_ORIGIN, |
| interestGroupBuyers: [MAIN_ORIGIN], |
| privateAggregationConfig: |
| { aggregationCoordinatorOrigin: window.location.origin }, |
| ...moreAuctionConfigOverrides |
| }; |
| } |
| |
| // Runs an auction with numGroups interest groups, "1" and "2", etc., with |
| // fragments.generateBidFragment/fragments.reportWinFragment/ |
| // fragments.scoreAdFragment/fragments.reportResultFragment |
| // expected to make some Private Aggregation contributions. |
| // Returns the collected samples. |
| async function runPrivateAggregationTest( |
| test, uuid, fragments, numGroups = 2, moreAuctionConfigOverrides = {}) { |
| await resetReports(MAIN_ORIGIN + MAIN_PATH); |
| await resetReports(MAIN_ORIGIN + DEBUG_PATH); |
| |
| for (let i = 1; i <= numGroups; ++i) { |
| await joinCrossOriginInterestGroup( |
| test, uuid, MAIN_ORIGIN, createIgOverrides(i, fragments)); |
| } |
| |
| const auctionConfigOverrides = |
| createAuctionConfigOverrides(uuid, fragments, moreAuctionConfigOverrides); |
| |
| await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides); |
| return await getDebugSamples(DEBUG_PATH); |
| } |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogram({ bucket: 1n, value: 2 });`, |
| |
| reportWinFragment: |
| `privateAggregation.contributeToHistogram({ bucket: 2n, value: 3 });`, |
| |
| scoreAdFragment: |
| `privateAggregation.contributeToHistogram({ bucket: 3n, value: 4 });`, |
| |
| reportResultFragment: |
| `privateAggregation.contributeToHistogram({ bucket: 4n, value: 5 });` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments); |
| assert_array_equals( |
| stringifySamples(samples), |
| [ |
| '1 => 4', // doubled since it's reported twice. |
| '2 => 3', |
| '3 => 8', // doubled since it's reported twice. |
| '4 => 5' |
| ]); |
| }, 'Basic contributions'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 1n, value: 2 });`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 2n, value: 3 });`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 3n, value: 4 });`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 4n, value: 5 });` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments); |
| assert_array_equals( |
| stringifySamples(samples), |
| [ |
| '1 => 4', // doubled since it's reported twice. |
| '2 => 3', |
| '3 => 8', // doubled since it's reported twice. |
| '4 => 5' |
| ]); |
| }, 'reserved.always'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.win', |
| { bucket: 1n, value: interestGroup.name });`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.win', |
| { bucket: 2n, value: 3 });`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.win', |
| { bucket: 3n, value: bid });`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.win', |
| { bucket: 4n, value: 5 });` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments); |
| assert_array_equals( |
| stringifySamples(samples), |
| [ |
| '1 => 2', // winning IG name |
| '2 => 3', |
| '3 => 2', // winning bid |
| '4 => 5' |
| ]); |
| }, 'reserved.win'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.loss', |
| { bucket: 1n, value: interestGroup.name });`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.loss', |
| { bucket: 2n, value: 3 });`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.loss', |
| { bucket: 3n, value: bid });`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.loss', |
| { bucket: 4n, value: 5 });` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments); |
| |
| // No reserved.loss from reporting since they only run for winners. |
| assert_array_equals( |
| stringifySamples(samples), |
| [ |
| '1 => 1', // losing IG name |
| '3 => 1', // losing bid |
| ]); |
| }, 'reserved.loss'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 1n, value: interestGroup.name });`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 2n, value: 3 });`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 3n, value: bid });`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 4n, value: 5 });` |
| }; |
| |
| const samples = |
| stringifySamples(await runPrivateAggregationTest(test, uuid, fragments)); |
| |
| // No reserved.once from reporting since it throws an exception. |
| // bidder/scorer just pick one. |
| assert_equals(samples.length, 2, 'samples array length'); |
| assert_in_array(samples[0], ['1 => 1', '1 => 2'], 'samples[0]'); |
| assert_in_array(samples[1], ['3 => 1', '3 => 2'], 'samples[1]'); |
| }, 'reserved.once'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 1n, value: 1 });`, |
| |
| reportWinFragment: ` |
| try { |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 2n, value: 2 }); |
| } catch (e) { |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 2n, value: (e instanceof TypeError ? 3 : 4) }); |
| }`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 3n, value: 4 });`, |
| |
| reportResultFragment: ` |
| try { |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 4n, value: 5 }); |
| } catch (e) { |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 4n, value: (e instanceof TypeError ? 6 : 7) }); |
| }` |
| }; |
| |
| const samples = |
| stringifySamples(await runPrivateAggregationTest(test, uuid, fragments)); |
| |
| assert_array_equals(samples, [ |
| '1 => 1', |
| '2 => 3', |
| '3 => 4', |
| '4 => 6', |
| ]); |
| }, 'no reserved.once in reporting'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| await resetReports(ALT_ORIGIN + DEBUG_PATH); |
| await resetReports(ALT_ORIGIN + MAIN_PATH); |
| |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'average-code-fetch-time', offset: 0n}, |
| value: 1});`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'average-code-fetch-time', offset: 100000n}, |
| value: 1});`, |
| |
| bidderDelayFactor: 200, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'average-code-fetch-time', offset: 200000n}, |
| value: 1});`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'average-code-fetch-time', offset: 300000n}, |
| value: 1});`, |
| |
| sellerDelay: 500 |
| }; |
| |
| const altFragments = { |
| generateBidFragment: fragments.generateBidFragment, |
| bidderDelayFactor: 1000 |
| }; |
| |
| await joinCrossOriginInterestGroup( |
| test, uuid, ALT_ORIGIN, createIgOverrides('1', altFragments, ALT_ORIGIN)); |
| const auctionConfigOverrides = { |
| interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN] |
| }; |
| |
| const samples = await runPrivateAggregationTest( |
| test, uuid, fragments, 3, auctionConfigOverrides); |
| |
| let generateBidVal = -1; |
| let reportWinVal = -1; |
| let scoreAdVal = -1; |
| let reportResultVal = -1; |
| assert_equals(samples.size, 4, 'main domain samples'); |
| |
| for (let [bucket, val] of samples.entries()) { |
| assert_equals(val, 1n, 'bucket val'); |
| if (0n <= bucket && bucket < 100000n) { |
| generateBidVal = Number(bucket - 0n); |
| } else if (100000n <= bucket && bucket < 200000n) { |
| reportWinVal = Number(bucket - 100000n); |
| } else if (200000n <= bucket && bucket < 300000n) { |
| scoreAdVal = Number(bucket - 200000n); |
| } else if (300000n <= bucket && bucket < 400000n) { |
| reportResultVal = Number(bucket - 300000n); |
| } else { |
| assert_unreached('Unexpected bucket number ' + bucket); |
| } |
| } |
| |
| assert_greater_than_equal(generateBidVal, 400, 'generateBid code fetch time'); |
| assert_greater_than_equal(reportWinVal, 600, 'reportWin code fetch time'); |
| assert_greater_than_equal(scoreAdVal, 500, 'scoreAd code fetch time'); |
| assert_greater_than_equal( |
| reportResultVal, 500, 'reportResult code fetch time'); |
| |
| let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH); |
| assert_equals(otherSamples.size, 1, 'alt domain samples'); |
| let otherGenerateBidVal = -1; |
| for (let [bucket, val] of otherSamples.entries()) { |
| assert_equals(val, 1n, 'other bucket val'); |
| if (0n <= bucket && bucket < 100000n) { |
| otherGenerateBidVal = Number(bucket - 0n); |
| } else { |
| assert_unreached('Unexpected other bucket number ' + bucket); |
| } |
| } |
| assert_greater_than_equal( |
| otherGenerateBidVal, 1000, 'other generateBid code fetch time'); |
| }, 'average-code-fetch-time'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'percent-scripts-timeout', offset: 0n}, |
| value: 1}); |
| if (interestGroup.name === '1') { |
| while (true) {} |
| } |
| `, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'percent-scripts-timeout', offset: 200n}, |
| value: 1}); |
| while(true) {}`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'percent-scripts-timeout', offset: 400n}, |
| value: 1}); |
| if (bid == 2) { |
| while (true) {} |
| } |
| `, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'percent-scripts-timeout', offset: 600n}, |
| value: 1});` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments, 3); |
| |
| let expected = [ |
| '33 => 1', // 33% of generateBid (base bucket 0) |
| '300 => 1', // 100% of reportWin (base bucket 200) |
| '450 => 1', // 50% of scoreAd (base bucket 400) |
| '600 => 1', // 0% of reportResult (base bucket 600) |
| ].sort(); |
| |
| assert_array_equals(stringifySamples(samples), expected); |
| }, 'percent-scripts-timeout'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| await resetReports(ALT_ORIGIN + DEBUG_PATH); |
| await resetReports(ALT_ORIGIN + MAIN_PATH); |
| |
| const ADDITIONAL_BID_PUBLIC_KEY = |
| '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='; |
| |
| // Join a negative group, one without ads. |
| // These shouldn't count towards participant number. |
| await joinNegativeInterestGroup( |
| test, MAIN_ORIGIN, 'some negative group', ADDITIONAL_BID_PUBLIC_KEY); |
| await joinCrossOriginInterestGroup(test, uuid, MAIN_ORIGIN, {ads: []}); |
| |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'participating-ig-count', offset: 0n}, |
| value: 1});`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'participating-ig-count', offset: 200n}, |
| value: 1});`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'participating-ig-count', offset: 400n}, |
| value: 1});`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'participating-ig-count', offset: 600n}, |
| value: 1});` |
| }; |
| |
| // ... and a different participant should get their own samples. |
| await joinCrossOriginInterestGroup( |
| test, uuid, ALT_ORIGIN, createIgOverrides('1', fragments, ALT_ORIGIN)); |
| await joinCrossOriginInterestGroup( |
| test, uuid, ALT_ORIGIN, createIgOverrides('2', fragments, ALT_ORIGIN)); |
| const auctionConfigOverrides = { |
| interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN] |
| }; |
| |
| const samples = await runPrivateAggregationTest( |
| test, uuid, fragments, 5, auctionConfigOverrides); |
| |
| let expected = [ |
| '5 => 1', // 5 in generateBid (base bucket 0) |
| '205 => 1', // 5 in reportWin (base bucket 200) |
| '400 => 1', // 0 in scoreAd (base bucket 400) |
| '600 => 1', // 0 in reportResult (base bucket 600) |
| ].sort(); |
| |
| assert_array_equals(stringifySamples(samples), expected); |
| |
| let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH); |
| assert_array_equals(stringifySamples(otherSamples), ['2 => 1']); |
| }, 'participating-ig-count'); |
| |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: { |
| baseValue: 'percent-igs-cumulative-timeout', |
| offset: 0n |
| }, |
| value: 1}); |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: { |
| baseValue: 'cumulative-buyer-time', |
| offset: 10000n |
| }, |
| value: 1}); |
| setBid({bid: interestGroup.name, render: interestGroup.ads[0].renderURL}); |
| while (true) {} |
| `, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: { |
| baseValue: 'percent-igs-cumulative-timeout', |
| offset: 200n |
| }, |
| value: 1}); |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: { |
| baseValue: 'cumulative-buyer-time', |
| offset: 20000n |
| }, |
| value: 1}); |
| `, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: { |
| baseValue: 'percent-igs-cumulative-timeout', |
| offset: 400n |
| }, |
| value: 1}); |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: { |
| baseValue: 'cumulative-buyer-time', |
| offset: 40000n |
| }, |
| value: 1}); |
| `, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: { |
| baseValue: 'percent-igs-cumulative-timeout', |
| offset: 600n |
| }, |
| value: 1}); |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: { |
| baseValue: 'cumulative-buyer-time', |
| offset: 60000n |
| }, |
| value: 1});` |
| }; |
| |
| const auctionConfigOverrides = { |
| perBuyerTimeouts: { |
| '*': 500 // max. |
| }, |
| perBuyerCumulativeTimeouts: {'*': 2000} |
| }; |
| |
| const samples = await runPrivateAggregationTest( |
| test, uuid, fragments, 15, auctionConfigOverrides); |
| |
| // Timeout is reported as 3000 (limit + 1000) for generateBid |
| // and reportWin, as 0 for the seller methods. |
| expectAndConsume(samples, 13000n, 1n); // base is 10,000 |
| expectAndConsume(samples, 23000n, 1n); |
| expectAndConsume(samples, 40000n, 1n); |
| expectAndConsume(samples, 60000n, 1n); |
| |
| // percent time is 0 on the seller side. |
| expectAndConsume(samples, 400n, 1n); |
| expectAndConsume(samples, 600n, 1n); |
| |
| assert_equals(samples.size, 2, 'buyer samples'); |
| |
| let percentGenerateBid = -1; |
| let percentReportWin = -1; |
| |
| for (let [bucket, val] of samples.entries()) { |
| assert_equals(val, 1n, 'bucket val'); |
| if (0n <= bucket && bucket <= 110n) { |
| percentGenerateBid = bucket; |
| } else if (200n <= bucket && bucket <= 310n) { |
| percentReportWin = bucket - 200n; |
| } else { |
| assert_unreached('Unexpected bucket number ' + bucket); |
| } |
| } |
| |
| assert_equals( |
| percentGenerateBid, percentReportWin, |
| 'same % in generateBid and reportWin'); |
| |
| // This assumes that at least some time out; which may not be guaranteed with |
| // sufficient level of parallelism. At any rate, the denominator is 15, |
| // however, so only some percentages are possible. |
| assert_in_array( |
| percentGenerateBid, |
| [6n, 13n, 20n, 26n, 33n, 40n, 46n, 53n, 60n, 66n, 73n, 80n, 86n, 93n], |
| 'percent timeout is as expected'); |
| }, 'percent-igs-cumulative-timeout, and cumulative-buyer-time when hit'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 0n}, |
| value: 1});`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 200n}, |
| value: 1});`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 400n}, |
| value: 1});`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 600n}, |
| value: 1});` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments, 5); |
| |
| // 0s for all the bases. |
| let expected = ['0 => 1', '200 => 1', '400 => 1', '600 => 1'].sort(); |
| |
| assert_array_equals(stringifySamples(samples), expected); |
| }, 'cumulative-buyer-time when not configured'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 0n}, |
| value: 1});`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 10000n}, |
| value: 1});`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 20000n}, |
| value: 1});`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: 'cumulative-buyer-time', offset: 30000n}, |
| value: 1});` |
| }; |
| |
| const auctionConfigOverrides = {perBuyerCumulativeTimeouts: {'*': 4000}}; |
| |
| const samples = await runPrivateAggregationTest( |
| test, uuid, fragments, 5, auctionConfigOverrides); |
| |
| // Sellers stuff is just 0s (so 1 to the base bucket offset). |
| expectAndConsume(samples, 20000n, 1n); |
| expectAndConsume(samples, 30000n, 1n); |
| |
| assert_equals(samples.size, 2, 'buyer samples'); |
| |
| let timeGenerateBid = -1; |
| let timeReportWin = -1; |
| |
| for (let [bucket, val] of samples.entries()) { |
| assert_equals(val, 1n, 'bucket val'); |
| if (0n <= bucket && bucket <= 5000n) { |
| timeGenerateBid = bucket; |
| } else if (10000n <= bucket && bucket <= 15000n) { |
| timeReportWin = bucket - 10000n; |
| } else { |
| assert_unreached('Unexpected bucket number'); |
| } |
| } |
| |
| assert_equals( |
| timeGenerateBid, timeReportWin, 'same time in generateBid and reportWin'); |
| |
| // This assume this takes more than 0ms to run; it's not really required to |
| // be the case, but feels like a realistic assumption that makes the test |
| // more useful. |
| assert_true( |
| 1n <= timeGenerateBid && timeGenerateBid <= 4000n, |
| 'time ' + timeGenerateBid + ' is reasonable and non-zero'); |
| }, 'cumulative-buyer-time when configured'); |
| |
| |
| async function testStorageQuotaMetric(test, name) { |
| const uuid = generateUuid(test); |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: '${name}', offset: 0n}, |
| value: 1});`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: '${name}', offset: 10000n}, |
| value: 1});`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: '${name}', offset: 20000n}, |
| value: 1});`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: '${name}', offset: 30000n}, |
| value: 1});` |
| }; |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments, 5); |
| |
| // Sellers stuff is just 0s (so 1 to the base bucket offset). |
| expectAndConsume(samples, 20000n, 1n); |
| expectAndConsume(samples, 30000n, 1n); |
| |
| assert_equals(samples.size, 2, 'buyer samples'); |
| |
| let generateBidVal = -1; |
| let reportWinVal = -1; |
| |
| for (let [bucket, val] of samples.entries()) { |
| assert_equals(val, 1n, 'bucket val'); |
| if (0n <= bucket && bucket < 10000n) { |
| generateBidVal = Number(bucket); |
| } else if (10000n <= bucket && bucket <= 20000n) { |
| reportWinVal = Number(bucket - 10000n); |
| } else { |
| assert_unreached('Unexpected bucket number ' + bucket); |
| } |
| } |
| |
| assert_equals( |
| generateBidVal, reportWinVal, 'same value in generateBid and reportWin'); |
| |
| // We don't know what the impls quota is, or even how much we are using, |
| // but at least make sure it's in range. |
| assert_between_inclusive( |
| generateBidVal, 0, 110, 'reported percent value is in expected range'); |
| } |
| |
| subsetTest(promise_test, async test => { |
| await testStorageQuotaMetric(test, 'percent-regular-ig-count-quota-used'); |
| }, 'percent-regular-ig-count-quota-used'); |
| |
| subsetTest(promise_test, async test => { |
| await testStorageQuotaMetric(test, 'percent-negative-ig-count-quota-used'); |
| }, 'percent-negative-ig-count-quota-used'); |
| |
| subsetTest(promise_test, async test => { |
| await testStorageQuotaMetric(test, 'percent-ig-storage-quota-used'); |
| }, 'percent-ig-storage-quota-used'); |
| |
| |
| async function testStorageUsageMetric(test, name, min) { |
| const uuid = generateUuid(test); |
| const spacing = 1000000000n; |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: '${name}', offset: 0n}, |
| value: 1});`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: '${name}', offset: ${spacing}n}, |
| value: 1});`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', { |
| bucket: {baseValue: '${name}', offset: 2n * ${spacing}n}, |
| value: 1});`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', { |
| bucket: {baseValue: '${name}', offset: 3n * ${spacing}n}, |
| value: 1});` |
| }; |
| |
| await joinNegativeInterestGroup( |
| test, MAIN_ORIGIN, 'some negative group', ADDITIONAL_BID_PUBLIC_KEY); |
| await joinNegativeInterestGroup( |
| test, MAIN_ORIGIN, 'some negative group 2', ADDITIONAL_BID_PUBLIC_KEY); |
| await joinCrossOriginInterestGroup( |
| test, uuid, MAIN_ORIGIN, |
| {ads: [], name: 'Big group w/o ads'.padEnd(50000)}); |
| |
| const samples = await runPrivateAggregationTest(test, uuid, fragments, 5); |
| |
| // Sellers stuff is just 0s (so 1 to the base bucket offset). |
| expectAndConsume(samples, 2n * spacing, 1n); |
| expectAndConsume(samples, 3n * spacing, 1n); |
| |
| assert_equals(samples.size, 2, 'buyer samples'); |
| |
| let generateBidVal = -1; |
| let reportWinVal = -1; |
| |
| for (let [bucket, val] of samples.entries()) { |
| assert_equals(val, 1n, 'bucket val'); |
| if (0n <= bucket && bucket < spacing) { |
| generateBidVal = bucket; |
| } else if (spacing <= bucket && bucket < 2n * spacing) { |
| reportWinVal = bucket - spacing; |
| } else { |
| assert_unreached('Unexpected bucket number ' + bucket); |
| } |
| } |
| |
| assert_equals( |
| generateBidVal, reportWinVal, 'same value in generateBid and reportWin'); |
| |
| assert_true( |
| generateBidVal >= BigInt(min), |
| 'reported value should be at least ' + min + ' but is ' + generateBidVal); |
| } |
| |
| subsetTest(promise_test, async test => { |
| // 5 regular Igs + one ad less. |
| await testStorageUsageMetric(test, 'regular-igs-count', 6); |
| }, 'regular-igs-count'); |
| |
| subsetTest(promise_test, async test => { |
| // 2 negative IGs |
| await testStorageUsageMetric(test, 'negative-igs-count', 2); |
| }, 'negative-igs-count'); |
| |
| subsetTest(promise_test, async test => { |
| // The big group has a 50,000 character name |
| await testStorageUsageMetric(test, 'ig-storage-used', 50000); |
| }, 'ig-storage-used'); |
| |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| await resetReports(MAIN_ORIGIN + MAIN_PATH); |
| await resetReports(MAIN_ORIGIN + DEBUG_PATH); |
| await resetReports(ALT_ORIGIN + MAIN_PATH); |
| await resetReports(ALT_ORIGIN + DEBUG_PATH); |
| |
| const fragments = { |
| generateBidFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 1n, value: 2 });`, |
| |
| reportWinFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 2n, value: 3 });`, |
| |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 3n, value: 4 });`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 4n, value: 5 });` |
| }; |
| |
| // 4 IGs in main origin, 2 in alt origin. |
| for (let i = 1; i <= 4; ++i) { |
| await joinCrossOriginInterestGroup( |
| test, uuid, MAIN_ORIGIN, createIgOverrides(i, fragments)); |
| } |
| |
| for (let i = 1; i <= 2; ++i) { |
| await joinCrossOriginInterestGroup( |
| test, uuid, ALT_ORIGIN, createIgOverrides(i, fragments, ALT_ORIGIN)); |
| } |
| |
| // Both groups in component auction 1, only alt group in component auction 2. |
| const subAuction1 = createAuctionConfigOverrides( |
| uuid, fragments, {interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN]}); |
| const subAuction2 = createAuctionConfigOverrides( |
| uuid, fragments, {interestGroupBuyers: [ALT_ORIGIN]}); |
| |
| const topFragments = { |
| scoreAdFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.once', |
| { bucket: 5n, value: 6 });`, |
| |
| reportResultFragment: ` |
| privateAggregation.contributeToHistogramOnEvent( |
| 'reserved.always', |
| { bucket: 6n, value: 7 });` |
| }; |
| const mainAuction = createAuctionConfigOverrides( |
| uuid, topFragments, |
| {interestGroupBuyers: [], componentAuctions: [subAuction1, subAuction2]}); |
| |
| await runBasicFledgeAuctionAndNavigate(test, uuid, mainAuction); |
| let samples = await getDebugSamples(DEBUG_PATH); |
| let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH); |
| let expected = [ |
| '1 => 2', // generateBid only in first component, so happens 1. |
| '2 => 3', // reportWin once. |
| '3 => 8', // Once per each component auction (out of total 6 scored). |
| '4 => 5', // component reportResult once. |
| '5 => 6', // top-level scoreAd once. |
| '6 => 7', // top-level reportResult. |
| ].sort(); |
| let otherExpected = [ |
| '1 => 4', // generateBid in each components, so twice, out of 4 executions. |
| ].sort(); |
| assert_array_equals(stringifySamples(samples), expected); |
| assert_array_equals(stringifySamples(otherSamples), otherExpected); |
| }, 'report.once in a component auction'); |