diff --git a/index.bs b/index.bs
index 47d6e6e45..9a88b28ad 100644
--- a/index.bs
+++ b/index.bs
@@ -836,7 +836,7 @@ An attribution source is a [=struct=] with the following items:
:: A non-negative integer.
: remaining aggregatable attribution bucket budget
:: A [=map=] whose [=map/key|keys=] are [=strings=] and whose [=map/value|values=] are
- non-negative integers.
+ positive integers.
: aggregatable dedup keys
:: A [=set=] of [=aggregatable dedup key/dedup key|aggregatable dedup key values=] associated with this [=attribution source=].
: debug reporting enabled
@@ -3683,7 +3683,7 @@ To parse aggregatable buckets given a [=map=] |map|:
:: |filterPair|[0]
: [=aggregatable bucket/negated filters=]
:: |filterPair|[1]
- 1. [=set/Append=] |aggregatableBucket| to |aggregatableBuckets|.
+ 1. [=list/Append=] |aggregatableBucket| to |aggregatableBuckets|.
1. Return |aggregatableBuckets|.
To parse attribution scopes for trigger from a [=map=] |map|:
@@ -3972,7 +3972,7 @@ To find matching aggregatable bucket given an [=attribution trigger=]
To get remaining aggregatable budget for matched bucket given an [=attribution trigger=] |trigger| and an [=attribution source=] |sourceToAttribute|:
1. Let |matchedBucket| be the result of running [=find matching aggregatable bucket=] with |trigger| and |sourceToAttribute|.
1. [=Assert=]: |matchedBucket| is not null.
-1. If |sourceToAttribute|'s [=attribution source/remaining aggregatable attribution bucket budget=][|matchedBucket|] does not [=map/exists|exist=]
+1. If |sourceToAttribute|'s [=attribution source/remaining aggregatable attribution bucket budget=][|matchedBucket|] does not [=map/exists|exist=]:
1. Return [=allowed aggregatable budget per source=].
1. Return |sourceToAttribute|'s [=attribution source/remaining aggregatable attribution bucket budget=][|matchedBucket|].
diff --git a/ts/src/header-validator/index.html b/ts/src/header-validator/index.html
index 849479011..1cde11055 100644
--- a/ts/src/header-validator/index.html
+++ b/ts/src/header-validator/index.html
@@ -85,6 +85,7 @@
Attribution Reporting Header Validation
Attribution-Reporting-Info
Use Chromium's vendor-specific values (details)
Enable experimental Flexible Event fields (details)
+
Enable experimental Aggregatable Bucket fields (details)
Validation Result
diff --git a/ts/src/header-validator/index.ts b/ts/src/header-validator/index.ts
index aad868ce6..f971e91e1 100644
--- a/ts/src/header-validator/index.ts
+++ b/ts/src/header-validator/index.ts
@@ -23,6 +23,9 @@ const sourceTypeFieldset =
const effective = document.querySelector('#effective')!
const flexCheckbox = form.elements.namedItem('flex') as HTMLInputElement
+const aggregatableBucketCheckbox = form.elements.namedItem(
+ 'aggregatableBucket'
+) as HTMLInputElement
function sourceType(): SourceType {
return parseSourceType(sourceTypeRadios.value)
@@ -31,6 +34,7 @@ function sourceType(): SourceType {
function validate(): void {
sourceTypeFieldset.disabled = true
flexCheckbox.disabled = true
+ aggregatableBucketCheckbox.disabled = true
let v: validator.Validator
@@ -38,10 +42,12 @@ function validate(): void {
case 'source':
sourceTypeFieldset.disabled = false
flexCheckbox.disabled = false
+ aggregatableBucketCheckbox.disabled = false
v = source.validator({
vsv: vsv.Chromium,
sourceType: sourceType(),
fullFlex: flexCheckbox.checked,
+ aggregatableBucket: aggregatableBucketCheckbox.checked,
noteInfoGain: true,
})
break
@@ -50,6 +56,7 @@ function validate(): void {
v = trigger.validator({
vsv: vsv.Chromium,
fullFlex: flexCheckbox.checked,
+ aggregatableBucket: aggregatableBucketCheckbox.checked,
})
break
case 'os-source':
@@ -100,6 +107,7 @@ document.querySelector('#linkify')!.addEventListener('click', () => {
}
url.searchParams.set('flex', flexCheckbox.checked.toString())
+ url.searchParams.set('aggregatableBucket', flexCheckbox.checked.toString())
void navigator.clipboard.writeText(url.toString())
})
@@ -137,5 +145,6 @@ if (st !== null && st in SourceType) {
sourceTypeRadios.value = st
flexCheckbox.checked = params.get('flex') === 'true'
+aggregatableBucketCheckbox.checked = params.get('aggregatableBucket') === 'true'
validate()
diff --git a/ts/src/header-validator/main.ts b/ts/src/header-validator/main.ts
index 7ce094432..a0b65f711 100644
--- a/ts/src/header-validator/main.ts
+++ b/ts/src/header-validator/main.ts
@@ -12,6 +12,7 @@ interface Arguments {
file?: string
fullFlex: boolean
+ aggregatableBucket: boolean
sourceType?: SourceType
silent: boolean
@@ -48,6 +49,11 @@ const options = parse(
description: 'If true, parse experimental Full Flexible Event fields.',
},
+ aggregatableBucket: {
+ type: Boolean,
+ description: 'If true, parse experimental Aggregatable Bucket fields.',
+ },
+
silent: {
type: Boolean,
description: 'If true, suppress output.',
@@ -95,10 +101,12 @@ const out = validate(
? trigger.validator({
vsv: vsv.Chromium,
fullFlex: options.fullFlex,
+ aggregatableBucket: options.aggregatableBucket,
})
: source.validator({
vsv: vsv.Chromium,
fullFlex: options.fullFlex,
+ aggregatableBucket: options.aggregatableBucket,
sourceType: options.sourceType,
})
)
diff --git a/ts/src/header-validator/source.test.ts b/ts/src/header-validator/source.test.ts
index 01aecbaf1..5f8938dd5 100644
--- a/ts/src/header-validator/source.test.ts
+++ b/ts/src/header-validator/source.test.ts
@@ -51,11 +51,12 @@ const testCases: TestCase[] = [
"max_event_states": 4
},
"aggregatable_bucket_max_budget": {
- "firebase": 32768,
- "third_party": 32768
+ "1": 32768,
+ "2": 32768
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expected: Maybe.some({
aggregatableReportWindow: 3601,
aggregationKeys: new Map([['a', 15n]]),
@@ -100,8 +101,8 @@ const testCases: TestCase[] = [
maxEventStates: 4,
},
aggregatableBucketBudget: new Map([
- ['firebase', 32768],
- ['third_party', 32768],
+ ['1', 32768],
+ ['2', 32768],
]),
}),
},
@@ -3082,6 +3083,7 @@ const testCases: TestCase[] = [
"aggregatable_bucket_max_budget": ["1"]
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expectedErrors: [
{
msg: 'must be an object',
@@ -3098,6 +3100,7 @@ const testCases: TestCase[] = [
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expectedErrors: [
{
msg: 'bucket exceeds max length per aggregatable bucket (51 > 50)',
@@ -3117,6 +3120,7 @@ const testCases: TestCase[] = [
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
},
{
name: 'aggregatable-buckets-too-many',
@@ -3147,6 +3151,7 @@ const testCases: TestCase[] = [
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expectedErrors: [
{
msg: 'exceeds the maximum number of keys (20)',
@@ -3165,6 +3170,7 @@ const testCases: TestCase[] = [
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expectedErrors: [
{
msg: 'must be in the range [1, 65536]',
@@ -3185,6 +3191,7 @@ const testCases: TestCase[] = [
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expectedErrors: [
{
msg: 'must be in the range [1, 65536]',
@@ -3201,6 +3208,7 @@ const testCases: TestCase[] = [
}
}`,
sourceType: SourceType.navigation,
+ parseAggregatableBucket: true,
expectedErrors: [
{
msg: 'must be a number',
@@ -3218,6 +3226,7 @@ testCases.forEach((tc) =>
sourceType: tc.sourceType ?? SourceType.navigation,
fullFlex: tc.parseFullFlex,
noteInfoGain: tc.noteInfoGain,
+ aggregatableBucket: tc.parseAggregatableBucket,
})
)
)
diff --git a/ts/src/header-validator/to-json.ts b/ts/src/header-validator/to-json.ts
index 3ccbbcd47..343963901 100644
--- a/ts/src/header-validator/to-json.ts
+++ b/ts/src/header-validator/to-json.ts
@@ -206,6 +206,7 @@ type Source = CommonDebug &
export interface Options {
fullFlex?: boolean | undefined
+ aggregatableBucket?: boolean | undefined
}
export function serializeSource(
@@ -247,9 +248,9 @@ export function serializeSource(
...ifNotNull('attribution_scopes', s.attributionScopes, (v) =>
serializeAttributionScopes(v)
),
- aggregatable_bucket_max_budget: Object.fromEntries(
- s.aggregatableBucketBudget
- ),
+ aggregatable_bucket_max_budget: opts.aggregatableBucket
+ ? Object.fromEntries(s.aggregatableBucketBudget)
+ : {},
}
return stringify(source)
@@ -411,10 +412,9 @@ export function serializeTrigger(
serializeAggregatableDedupKey
),
- aggregatable_buckets: Array.from(
- t.aggregatableBuckets,
- serializeAggregatableBucket
- ),
+ aggregatable_buckets: opts.aggregatableBucket
+ ? Array.from(t.aggregatableBuckets, serializeAggregatableBucket)
+ : [],
aggregatable_source_registration_time: t.aggregatableSourceRegistrationTime,
diff --git a/ts/src/header-validator/trigger.test.ts b/ts/src/header-validator/trigger.test.ts
index bc8904bf7..e66f25c6f 100644
--- a/ts/src/header-validator/trigger.test.ts
+++ b/ts/src/header-validator/trigger.test.ts
@@ -54,6 +54,7 @@ const testCases: jsontest.TestCase[] = [
},
"attribution_scopes": ["1"]
}`,
+ parseAggregatableBucket: true,
expected: Maybe.some({
aggregatableDedupKeys: [
{
@@ -1743,6 +1744,7 @@ const testCases: jsontest.TestCase[] = [
{
name: 'aggregatable-buckets-wrong-type',
input: `{"aggregatable_buckets": 1}`,
+ parseAggregatableBucket: true,
expectedErrors: [
{
path: ['aggregatable_buckets'],
@@ -1753,6 +1755,7 @@ const testCases: jsontest.TestCase[] = [
{
name: 'aggregatable-buckets-value-wrong-type',
input: `{"aggregatable_buckets": [1]}`,
+ parseAggregatableBucket: true,
expectedErrors: [
{
path: ['aggregatable_buckets', 0],
@@ -1765,6 +1768,7 @@ const testCases: jsontest.TestCase[] = [
input: `{"aggregatable_buckets": [{
"bucket": 1
}]}`,
+ parseAggregatableBucket: true,
expectedErrors: [
{
path: ['aggregatable_buckets', 0, 'bucket'],
@@ -1778,6 +1782,7 @@ const testCases: jsontest.TestCase[] = [
"filters": [],
"not_filters": []
}]}`,
+ parseAggregatableBucket: true,
expectedErrors: [
{
path: ['aggregatable_buckets', 0, 'bucket'],
@@ -1792,6 +1797,7 @@ const testCases: jsontest.TestCase[] = [
"filters": [],
"not_filters": []
}]}`,
+ parseAggregatableBucket: true,
},
]
@@ -1801,6 +1807,7 @@ testCases.forEach((tc) =>
trigger.validator({
vsv: { ...vsv.Chromium, ...tc.vsv },
fullFlex: tc.parseFullFlex,
+ aggregatableBucket: tc.parseAggregatableBucket,
})
)
)
diff --git a/ts/src/header-validator/validate-json.test.ts b/ts/src/header-validator/validate-json.test.ts
index f3cecf87d..d56d2a6c6 100644
--- a/ts/src/header-validator/validate-json.test.ts
+++ b/ts/src/header-validator/validate-json.test.ts
@@ -4,4 +4,5 @@ import * as vsv from '../vendor-specific-values'
export type TestCase = testutil.TestCase & {
vsv?: Readonly>
parseFullFlex?: boolean
+ parseAggregatableBucket?: boolean
}
diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts
index 6d26d934c..ec697b6fa 100644
--- a/ts/src/header-validator/validate-json.ts
+++ b/ts/src/header-validator/validate-json.ts
@@ -38,6 +38,7 @@ export const UINT32_MAX: number = 2 ** 32 - 1
export interface RegistrationOptions {
vsv: VendorSpecificValues
fullFlex?: boolean | undefined
+ aggregatableBucket?: boolean | undefined
}
export class RegistrationContext<
diff --git a/ts/src/header-validator/validate-source.ts b/ts/src/header-validator/validate-source.ts
index 2d7de7b2b..3470fba25 100644
--- a/ts/src/header-validator/validate-source.ts
+++ b/ts/src/header-validator/validate-source.ts
@@ -752,10 +752,12 @@ function source(j: Json, ctx: Context): Maybe {
'attribution_scopes',
withDefault(attributionScopes, null)
),
- aggregatableBucketBudget: field(
- 'aggregatable_bucket_max_budget',
- withDefault(aggregatableBucketBudget, new Map())
- ),
+ aggregatableBucketBudget: ctx.opts.aggregatableBucket
+ ? field(
+ 'aggregatable_bucket_max_budget',
+ withDefault(aggregatableBucketBudget, new Map())
+ )
+ : () => Maybe.some(new Map()),
...commonDebugFields,
...priorityField,
diff --git a/ts/src/header-validator/validate-trigger.ts b/ts/src/header-validator/validate-trigger.ts
index cd5487d63..1b3d6cb9b 100644
--- a/ts/src/header-validator/validate-trigger.ts
+++ b/ts/src/header-validator/validate-trigger.ts
@@ -402,10 +402,9 @@ function trigger(j: Json, ctx: Context): Maybe {
'aggregatable_deduplication_keys',
withDefault(aggregatableDedupKeys, [])
),
- aggregatableBuckets: field(
- 'aggregatable_buckets',
- withDefault(aggregatableBuckets, [])
- ),
+ aggregatableBuckets: ctx.opts.aggregatableBucket
+ ? field('aggregatable_buckets', withDefault(aggregatableBuckets, []))
+ : () => Maybe.some([]),
aggregatableSourceRegistrationTime: () => aggregatableSourceRegTimeVal,
eventTriggerData: field(
'event_trigger_data',