1067 lines
34 KiB
JavaScript
1067 lines
34 KiB
JavaScript
// 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');
|