diff --git a/api.bs b/api.bs
index cce0efb..2efec04 100644
--- a/api.bs
+++ b/api.bs
@@ -1426,8 +1426,9 @@ and [=implicit API inputs=] |implicitInputs|:
the [=maximum lookback=].
1. If the [=list/size=] of
|options|.{{AttributionImpressionOptions/conversionSites}} is
- greater than an [=implementation-defined=] maximum value, return
- [=a promise rejected with=] a {{RangeError}} in |realm|.
+ greater than the [=implementation-defined=]
+ [=maximum number of conversion sites per impression=],
+ return [=a promise rejected with=] a {{RangeError}} in |realm|.
1. Let |conversionSites| be the [=set=] that is the result
of invoking [=parse a site=]
for each value in |options|.{{AttributionImpressionOptions/conversionSites}}.
@@ -1435,8 +1436,9 @@ and [=implicit API inputs=] |implicitInputs|:
[=a promise rejected with=] a {{"SyntaxError"}} {{DOMException}} in |realm|.
1. If the [=list/size=] of
|options|.{{AttributionImpressionOptions/conversionCallers}} is
- greater than an [=implementation-defined=] maximum value, return
- [=a promise rejected with=] a {{RangeError}} in |realm|.
+ greater than the [=implementation-defined=]
+ [=maximum number of conversion callers per impression=],
+ return [=a promise rejected with=] a {{RangeError}} in |realm|.
1. Let |conversionCallers| be the [=set=] that is the result
of invoking [=parse a site=]
for each value in |options|.{{AttributionImpressionOptions/conversionCallers}}.
@@ -1598,8 +1600,8 @@ To validate {{AttributionConversionOptions}} |options|:
is less than or equal to 0 or is greater than [=maximum epsilon=],
throw a {{RangeError}}.
1. If |options|.{{AttributionConversionOptions/histogramSize}}
- is 0 or greater than the [=implementation-defined=] maximum histogram size,
- or is greater than the maximum aggregation-service histogram size,
+ is 0 or greater than the [=implementation-defined=] [=maximum histogram size=],
+ or is greater than the [=maximum aggregation-service histogram size=],
if any, for |options|.{{AttributionConversionOptions/aggregationService}},
throw a {{RangeError}}.
1. If |options|.{{AttributionConversionOptions/value}} is 0,
@@ -1610,7 +1612,8 @@ To validate {{AttributionConversionOptions}} |options|:
1. Let |credit| be |options|.{{AttributionConversionOptions/credit}} if it [=map/exists=], «1» otherwise.
1. If |credit| [=list/is empty=], throw a {{RangeError}}.
1. If any of the [=list/items=] of |credit| are less than or equal to 0, throw a {{RangeError}}.
-1. If the [=list/size=] of |credit| exceeds an [=implementation-defined=] maximum, throw a {{RangeError}}.
+1. If the [=list/size=] of |credit| exceeds the [=implementation-defined=]
+ [=maximum number of credit values=], throw a {{RangeError}}.
1. Let |lookback| be |options|.{{AttributionConversionOptions/lookbackDays}} [=days=]
if it [=map/exists=], the [=maximum lookback=] [=days=] otherwise.
1. Set |lookback| to the [=maximum lookback=] [=days=]
@@ -1618,19 +1621,22 @@ To validate {{AttributionConversionOptions}} |options|:
1. If |lookback| is 0 [=days=], throw a {{RangeError}}.
1. If the [=list/size=] of
|options|.{{AttributionConversionOptions/matchValues}} is
- greater than an [=implementation-defined=] maximum value, throw a {{RangeError}}.
+ greater than the [=implementation-defined=] [=maximum number of match values=],
+ throw a {{RangeError}}.
1. Let |matchValues| be the result of running [=set/create|creating a set=] with
|options|.{{AttributionConversionOptions/matchValues}}.
1. If the [=list/size=] of
- |options|.{{AttributionConversionOptions/impressionSites}} is
- greater than an [=implementation-defined=] maximum value, throw a {{RangeError}}.
+ |options|.{{AttributionConversionOptions/impressionSites}} is greater than
+ the [=implementation-defined=] [=maximum number of impression sites for conversion=],
+ throw a {{RangeError}}.
1. Let |impressionSites| be the [=set=] that is the result
of invoking [=parse a site=]
for each value in |options|.{{AttributionConversionOptions/impressionSites}}.
1. If any result in |impressionSites| is failure, throw a {{"SyntaxError"}} {{DOMException}}.
1. If the [=list/size=] of
- |options|.{{AttributionConversionOptions/impressionCallers}} is
- greater than an [=implementation-defined=] maximum value, throw a {{RangeError}}.
+ |options|.{{AttributionConversionOptions/impressionCallers}} is greater than
+ the [=implementation-defined=] [=maximum number of impression callers for conversion=],
+ throw a {{RangeError}}.
1. Let |impressionCallers| be the [=set=] that is the result
of invoking [=parse a site=]
for each value in |options|.{{AttributionConversionOptions/impressionCallers}}.
@@ -1880,11 +1886,50 @@ regarding how to best set those values.
An implementation sets an [=implementation-defined=] maximum lookback
for {{AttributionImpressionOptions/lifetimeDays}} and
-{{AttributionConversionOptions/lookbackDays}}. It is a positive integer number of [=days=].
+{{AttributionConversionOptions/lookbackDays}}.
+[=Maximum lookback=] is a positive integer number of [=days=].
There is no point to having different maximum values for
these values as the smaller of the two values
will determine which saved [=impressions=] are available for conversions.
+Implementations [=must=] set a [=maximum lookback=] of at least 90 days.
+
+An [=implementation-defined=] value is chosen for the lists of sites
+passed to saveImpression()
+and measureConversion().
+The maximum number of conversion sites per impression
+is a limit on the number of values for {{AttributionImpressionOptions/conversionSites}};
+implementations [=must=] set this value to at least 5.
+The maximum number of conversion callers per impression
+and maximum number of impression callers for conversion
+are limits on the number of values for {{AttributionImpressionOptions/conversionCallers}}
+and {{AttributionConversionOptions/impressionCallers}} respectively;
+implementations [=must=] set each of these values to at least 10.
+The maximum number of impression sites for conversion
+is a limit on the number of values for {{AttributionConversionOptions/impressionSites}};
+implementations [=must=] set this value to at least 30.
+
+An [=implementation-defined=] value is chosen for
+the maximum number of credit values,
+which is a limit on the number of values for {{AttributionConversionOptions/credit}};
+implementations [=must=] set this value to at least 10.
+An [=implementation-defined=] value is chosen for
+the maximum number of match values,
+which is a limit on the number of values for {{AttributionConversionOptions/matchValues}};
+implementations [=must=] set this value to at least 30.
+
+The size of histograms
+produced by measureConversion()
+is subject to both an [=implementation-define=] maximum histogram size
+and maximum aggregation-service histogram size.
+No minimum value for the [=maximum histogram size=] is set
+as the expectation is that the choice of aggregation technology
+will set a limit.
+The [=maximum aggregation-service histogram size=]
+will be a fixed value that is determined
+by the choice of technology used by the [=aggregation service=];
+it is not [=implementation-defined=].
+
Deciding on a value for differential privacy parameters
is hard and therefore TBD.
diff --git a/impl/e2e-tests/CONFIG.json b/impl/e2e-tests/CONFIG.json
index 9ef3398..940001a 100644
--- a/impl/e2e-tests/CONFIG.json
+++ b/impl/e2e-tests/CONFIG.json
@@ -4,7 +4,10 @@
},
"maxConversionSitesPerImpression": 3,
"maxConversionCallersPerImpression": 3,
+ "maxImpressionSitesForConversion": 3,
+ "maxImpressionCallersForConversion": 3,
"maxCreditSize": 10,
+ "maxMatchValues": 10,
"maxLookbackDays": 30,
"maxHistogramSize": 5,
"privacyBudgetMicroEpsilons": 1000000,
diff --git a/impl/e2e-tests/measure-conversion-errors.json b/impl/e2e-tests/measure-conversion-errors.json
index f9195d2..e79ffe0 100644
--- a/impl/e2e-tests/measure-conversion-errors.json
+++ b/impl/e2e-tests/measure-conversion-errors.json
@@ -94,6 +94,7 @@
"options": {
"aggregationService": "https://agg-service.example",
"histogramSize": 1,
+ "$comment": "Zero credit",
"credit": [0]
},
"expected": "RangeError"
@@ -102,10 +103,10 @@
"seconds": 10,
"site": "advertiser.example",
"event": "measureConversion",
- "$comment": "Greater than CONFIG.maxCreditSize",
"options": {
"aggregationService": "https://agg-service.example",
"histogramSize": 1,
+ "$comment": "Greater than CONFIG.maxCreditSize",
"credit": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
},
"expected": "RangeError"
@@ -117,7 +118,9 @@
"options": {
"aggregationService": "https://agg-service.example",
"histogramSize": 1,
- "lookbackDays": 0
+ "credit": [1],
+ "$comment": "Greater than CONFIG.maxMatchValues",
+ "matchValues": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
},
"expected": "RangeError"
},
@@ -125,6 +128,18 @@
"seconds": 12,
"site": "advertiser.example",
"event": "measureConversion",
+ "options": {
+ "aggregationService": "https://agg-service.example",
+ "histogramSize": 1,
+ "$comment": "lookback cannot be zero",
+ "lookbackDays": 0
+ },
+ "expected": "RangeError"
+ },
+ {
+ "seconds": 13,
+ "site": "advertiser.example",
+ "event": "measureConversion",
"options": {
"aggregationService": "https://agg-service.example",
"histogramSize": 1,
@@ -136,7 +151,7 @@
}
},
{
- "seconds": 13,
+ "seconds": 14,
"site": "advertiser.example",
"event": "measureConversion",
"options": {
@@ -148,6 +163,30 @@
"error": "DOMException",
"name": "SyntaxError"
}
+ },
+ {
+ "seconds": 15,
+ "site": "advertiser.example",
+ "event": "measureConversion",
+ "options": {
+ "aggregationService": "https://agg-service.example",
+ "histogramSize": 1,
+ "$comment": "Greater than CONFIG.maxImpressionSitesForConversion",
+ "impressionSites": ["a", "b", "c", "d"]
+ },
+ "expected": "RangeError"
+ },
+ {
+ "seconds": 16,
+ "site": "advertiser.example",
+ "event": "measureConversion",
+ "options": {
+ "aggregationService": "https://agg-service.example",
+ "histogramSize": 1,
+ "$comment": "Greater than CONFIG.maxImpressionCallersForConversion",
+ "impressionCallers": ["a", "b", "c", "d"]
+ },
+ "expected": "RangeError"
}
]
}
diff --git a/impl/e2e-tests/save-impression-errors.json b/impl/e2e-tests/save-impression-errors.json
index 88673c3..122dacb 100644
--- a/impl/e2e-tests/save-impression-errors.json
+++ b/impl/e2e-tests/save-impression-errors.json
@@ -4,7 +4,9 @@
"seconds": 1,
"site": "publisher.example",
"event": "saveImpression",
- "options": { "histogramIndex": -1 },
+ "options": {
+ "histogramIndex": -1
+ },
"expectedError": "RangeError"
},
{
@@ -12,7 +14,9 @@
"site": "publisher.example",
"event": "saveImpression",
"$comment": "Equal to CONFIG.maxHistogramSize",
- "options": { "histogramIndex": 5 },
+ "options": {
+ "histogramIndex": 5
+ },
"expectedError": "RangeError"
},
{
diff --git a/impl/src/backend.ts b/impl/src/backend.ts
index 42c5e18..d96dc9e 100644
--- a/impl/src/backend.ts
+++ b/impl/src/backend.ts
@@ -68,8 +68,17 @@ function validateSite(input: string): void {
}
}
-function parseSites(input: readonly string[]): Set {
+function parseSites(
+ input: readonly string[],
+ label: string,
+ limit: number,
+): Set {
const parsed = new Set();
+ if (limit !== null && input.length > limit) {
+ throw new RangeError(
+ `number of values in ${label} exceeds limit of ${limit}`,
+ );
+ }
for (const site of input) {
parsed.add(parseSite(site));
}
@@ -80,10 +89,21 @@ export interface Delegate {
readonly aggregationServices: AttributionAggregationServices;
readonly includeUnencryptedHistogram?: boolean;
+ /// The maximum number of conversion callers per impression.
readonly maxConversionSitesPerImpression: number;
+ /// The maximum number of conversion sites per impression.
readonly maxConversionCallersPerImpression: number;
+ /// The maximum number of impression sites for conversion.
+ readonly maxImpressionSitesForConversion: number;
+ /// The maximum number of impression callers for conversion.
+ readonly maxImpressionCallersForConversion: number;
+ /// The maximum number of credit values.
readonly maxCreditSize: number;
+ /// The maximum number of match values.
+ readonly maxMatchValues: number;
+ /// The maximum lookback in days.
readonly maxLookbackDays: number;
+ /// The maximum size of histograms.
readonly maxHistogramSize: number;
readonly privacyBudgetMicroEpsilons: number;
readonly privacyBudgetEpoch: Temporal.Duration;
@@ -162,23 +182,16 @@ export class Backend {
}
lifetimeDays = Math.min(lifetimeDays, this.#delegate.maxLookbackDays);
- const maxConversionSitesPerImpression =
- this.#delegate.maxConversionSitesPerImpression;
- if (conversionSites.length > maxConversionSitesPerImpression) {
- throw new RangeError(
- `conversionSites.length must be <= ${maxConversionSitesPerImpression}`,
- );
- }
- const parsedConversionSites = parseSites(conversionSites);
-
- const maxConversionCallersPerImpression =
- this.#delegate.maxConversionCallersPerImpression;
- if (conversionCallers.length > maxConversionCallersPerImpression) {
- throw new RangeError(
- `conversionCallers.length must be <= ${maxConversionCallersPerImpression}`,
- );
- }
- const parsedConversionCallers = parseSites(conversionCallers);
+ const parsedConversionSites = parseSites(
+ conversionSites,
+ "conversionSites",
+ this.#delegate.maxConversionSitesPerImpression,
+ );
+ const parsedConversionCallers = parseSites(
+ conversionCallers,
+ "conversionCallers",
+ this.#delegate.maxConversionCallersPerImpression,
+ );
if (matchValue < 0 || !Number.isInteger(matchValue)) {
throw new RangeError("matchValue must be a non-negative integer");
@@ -270,6 +283,12 @@ export class Backend {
lookbackDays = Math.min(lookbackDays, this.#delegate.maxLookbackDays);
const matchValueSet = new Set();
+ const maxMatchValues = this.#delegate.maxMatchValues;
+ if (matchValues.length > maxMatchValues) {
+ throw new RangeError(
+ `matchValues size must be in the range [0,${maxMatchValues}]`,
+ );
+ }
for (const value of matchValues) {
if (value < 0 || !Number.isInteger(value)) {
throw new RangeError("match value must be a non-negative integer");
@@ -277,14 +296,25 @@ export class Backend {
matchValueSet.add(value);
}
+ const parsedImpressionSites = parseSites(
+ impressionSites,
+ "impressionSites",
+ this.#delegate.maxImpressionSitesForConversion,
+ );
+ const parsedImpressionCallers = parseSites(
+ impressionCallers,
+ "impressionCallers",
+ this.#delegate.maxImpressionCallersForConversion,
+ );
+
return {
aggregationService: aggregationServiceEntry,
epsilon,
histogramSize,
lookback: days(lookbackDays),
matchValues: matchValueSet,
- impressionSites: parseSites(impressionSites),
- impressionCallers: parseSites(impressionCallers),
+ impressionSites: parsedImpressionSites,
+ impressionCallers: parsedImpressionCallers,
credit,
value,
maxValue,
@@ -661,7 +691,7 @@ export class Backend {
}
clearState(sites: readonly string[], forgetVisits: boolean): void {
- const parsedSites = parseSites(sites);
+ const parsedSites = parseSites(sites, "sites", Infinity);
if (!forgetVisits) {
this.#zeroBudgetForSites(parsedSites);
return;
diff --git a/impl/src/e2e.test.ts b/impl/src/e2e.test.ts
index c727c9a..abe3629 100644
--- a/impl/src/e2e.test.ts
+++ b/impl/src/e2e.test.ts
@@ -89,7 +89,10 @@ function runTest(
maxConversionSitesPerImpression: config.maxConversionSitesPerImpression,
maxConversionCallersPerImpression: config.maxConversionCallersPerImpression,
+ maxImpressionSitesForConversion: config.maxImpressionSitesForConversion,
+ maxImpressionCallersForConversion: config.maxImpressionCallersForConversion,
maxCreditSize: config.maxCreditSize,
+ maxMatchValues: config.maxMatchValues,
maxLookbackDays: config.maxLookbackDays,
maxHistogramSize: config.maxHistogramSize,
privacyBudgetMicroEpsilons: config.privacyBudgetMicroEpsilons,
@@ -132,7 +135,6 @@ function runTest(
event.intermediarySite,
event.options,
);
-
if (Array.isArray(event.expected)) {
assert.deepEqual(
call().unencryptedHistogram,
diff --git a/impl/src/fixture.ts b/impl/src/fixture.ts
index ab24cde..6ec4024 100644
--- a/impl/src/fixture.ts
+++ b/impl/src/fixture.ts
@@ -10,7 +10,10 @@ export interface TestConfig {
aggregationServices: Record;
maxConversionSitesPerImpression: number;
maxConversionCallersPerImpression: number;
+ maxImpressionSitesForConversion: number;
+ maxImpressionCallersForConversion: number;
maxCreditSize: number;
+ maxMatchValues: number;
maxLookbackDays: number;
maxHistogramSize: number;
privacyBudgetMicroEpsilons: number;
@@ -35,7 +38,10 @@ export function makeBackend(
maxConversionSitesPerImpression: config.maxConversionSitesPerImpression,
maxConversionCallersPerImpression: config.maxConversionCallersPerImpression,
+ maxImpressionSitesForConversion: config.maxImpressionSitesForConversion,
+ maxImpressionCallersForConversion: config.maxImpressionCallersForConversion,
maxCreditSize: config.maxCreditSize,
+ maxMatchValues: config.maxMatchValues,
maxLookbackDays: config.maxLookbackDays,
maxHistogramSize: config.maxHistogramSize,
privacyBudgetMicroEpsilons: config.privacyBudgetMicroEpsilons,
diff --git a/impl/src/simulator.ts b/impl/src/simulator.ts
index eea63b4..98c51f4 100644
--- a/impl/src/simulator.ts
+++ b/impl/src/simulator.ts
@@ -18,7 +18,10 @@ const backend = new Backend({
// TODO: Allow these values to be configured in the UI.
maxConversionSitesPerImpression: 10,
maxConversionCallersPerImpression: 10,
+ maxImpressionSitesForConversion: 10,
+ maxImpressionCallersForConversion: 10,
maxCreditSize: Infinity,
+ maxMatchValues: Infinity,
maxLookbackDays: 30,
maxHistogramSize: 100,
privacyBudgetMicroEpsilons: 1000000,