Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 58 additions & 13 deletions api.bs
Original file line number Diff line number Diff line change
Expand Up @@ -1426,17 +1426,19 @@ 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}}.
1. If any result in |conversionSites| is failure, return
[=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}}.
Expand Down Expand Up @@ -1598,8 +1600,8 @@ To <dfn>validate {{AttributionConversionOptions}}</dfn> |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=] <dfn>maximum histogram size</dfn>,
or is greater than the <dfn ignore>maximum aggregation-service histogram size</dfn>,
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,
Expand All @@ -1610,27 +1612,31 @@ To <dfn>validate {{AttributionConversionOptions}}</dfn> |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=]
if it is larger than that maximum.
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}}.
Expand Down Expand Up @@ -1880,11 +1886,50 @@ regarding how to best set those values.

An implementation sets an [=implementation-defined=] <dfn>maximum lookback</dfn>
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 <a method for=Attribution>saveImpression()</a>
and <a method for=Attribution>measureConversion()</a>.
The <dfn>maximum number of conversion sites per impression</dfn>
is a limit on the number of values for {{AttributionImpressionOptions/conversionSites}};
implementations [=must=] set this value to at least 5.
The <dfn>maximum number of conversion callers per impression</dfn>
and <dfn>maximum number of impression callers for conversion</dfn>
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 <dfn>maximum number of impression sites for conversion</dfn>
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 <dfn>maximum number of credit values</dfn>,
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 <dfn>maximum number of match values</dfn>,
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 <a method for=Attribution>measureConversion()</a>
is subject to both an [=implementation-define=] <dfn>maximum histogram size</dfn>
and <dfn>maximum aggregation-service histogram size</dfn>.
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.

Expand Down
3 changes: 3 additions & 0 deletions impl/e2e-tests/CONFIG.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
},
"maxConversionSitesPerImpression": 3,
"maxConversionCallersPerImpression": 3,
"maxImpressionSitesForConversion": 3,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will have to update impl/e2e.schema.json as well with the new configuration fields.

"maxImpressionCallersForConversion": 3,
Comment on lines 5 to +8
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know setting lower values here simplifies test construction, but it may be confusing to anyone stumbling across this code for them to be less than the minimums mandated by the specification. I'm OK with keeping them this low for now, but we might want to add a $comment noting this.

"maxCreditSize": 10,
"maxMatchValues": 10,
"maxLookbackDays": 30,
"maxHistogramSize": 5,
"privacyBudgetMicroEpsilons": 1000000,
Expand Down
45 changes: 42 additions & 3 deletions impl/e2e-tests/measure-conversion-errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"options": {
"aggregationService": "https://agg-service.example",
"histogramSize": 1,
"$comment": "Zero credit",
"credit": [0]
},
"expected": "RangeError"
Expand All @@ -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"
Expand All @@ -117,14 +118,28 @@
"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"
},
{
"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,
Expand All @@ -136,7 +151,7 @@
}
},
{
"seconds": 13,
"seconds": 14,
"site": "advertiser.example",
"event": "measureConversion",
"options": {
Expand All @@ -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"
}
]
}
8 changes: 6 additions & 2 deletions impl/e2e-tests/save-impression-errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
"seconds": 1,
"site": "publisher.example",
"event": "saveImpression",
"options": { "histogramIndex": -1 },
"options": {
"histogramIndex": -1
},
"expectedError": "RangeError"
},
{
"seconds": 2,
"site": "publisher.example",
"event": "saveImpression",
"$comment": "Equal to CONFIG.maxHistogramSize",
"options": { "histogramIndex": 5 },
"options": {
"histogramIndex": 5
},
"expectedError": "RangeError"
},
{
Expand Down
72 changes: 51 additions & 21 deletions impl/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,17 @@ function validateSite(input: string): void {
}
}

function parseSites(input: readonly string[]): Set<string> {
function parseSites(
input: readonly string[],
label: string,
limit: number,
): Set<string> {
const parsed = new Set<string>();
if (limit !== null && input.length > limit) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (limit !== null && input.length > limit) {
if (input.length > limit) {

The limit !== null condition is redundant with limit: number above, which makes the value non-nullable.

throw new RangeError(
`number of values in ${label} exceeds limit of ${limit}`,
);
}
for (const site of input) {
parsed.add(parseSite(site));
}
Expand All @@ -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;
Comment on lines +92 to 95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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 conversion sites per impression.
readonly maxConversionSitesPerImpression: number;
/// The maximum number of conversion callers 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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -270,21 +283,38 @@ export class Backend {
lookbackDays = Math.min(lookbackDays, this.#delegate.maxLookbackDays);

const matchValueSet = new Set<number>();
const maxMatchValues = this.#delegate.maxMatchValues;
if (matchValues.length > maxMatchValues) {
throw new RangeError(
`matchValues size must be in the range [0,${maxMatchValues}]`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`matchValues size must be in the range [0,${maxMatchValues}]`,
`number of values in matchValues exceeds limit of ${maxMatchValues}`,

To match the error format for parseSites, and because there can't be fewer than 0 entries in the list.

);
}
for (const value of matchValues) {
if (value < 0 || !Number.isInteger(value)) {
throw new RangeError("match value must be a non-negative integer");
}
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,
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion impl/src/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -132,7 +135,6 @@ function runTest(
event.intermediarySite,
event.options,
);

if (Array.isArray(event.expected)) {
assert.deepEqual(
call().unencryptedHistogram,
Expand Down
Loading