diff --git a/AGGREGATE.md b/AGGREGATE.md
index 46f2b423b5..2babeaecca 100644
--- a/AGGREGATE.md
+++ b/AGGREGATE.md
@@ -172,9 +172,6 @@ The scheme above will generate the following abstract histogram contributions:
value: 1664
}]
```
-Note: The `filters` field will still apply to aggregatable reports, and each
-dict in `aggregatable_trigger_data` can still optionally have filters applied
-to it just like for event-level reports.
Note: the above scheme was used to maximize the [contribution
budget](#contribution-bounding-and-budgeting) and optimize utility in the face
@@ -185,6 +182,34 @@ true_agg_campaign_counts = raw_agg_campaign_counts / (L1 / 2)
true_agg_geo_value = 1024 * raw_agg_geo_value / (L1 / 2)
```
+Note: The `filters` field will still apply to aggregatable reports, and each
+dict in `aggregatable_trigger_data` can still optionally have filters applied
+to it just like for event-level reports.
+
+Note: The `aggregatable_values` field may also be specified as a list of
+dictionaries, where each dictionary contains a dictionary `values` of key-value
+pairs as outlined above as well as optional `filters` and `not_filters` fields,
+allowing trigger registrations to customize how values are contributed to keys
+depending on source filter data. If multiple list entries have filters that
+match the source's filters, only the first entry and its corresponding values
+will be used.
+
+For example:
+```jsonc
+{
+ ...,
+ "aggregatable_values": [
+ {
+ "values": {
+ "campaignCounts": 32768,
+ "geoValue": 1664
+ },
+ "filters": {"source_type": ["navigation"]}
+ }
+ ]
+}
+```
+
Trigger registration will accept an optional field
`aggregatable_deduplication_keys` which will be used to deduplicate multiple
triggers containing the same `deduplication_key` for a single source with selective filtering.
diff --git a/index.bs b/index.bs
index f943cd0920..b07bc9940e 100644
--- a/index.bs
+++ b/index.bs
@@ -797,6 +797,20 @@ An aggregatable trigger data is a [=struct=] with the following items:
+
Aggregatable values configuration
+
+An aggregatable values configuration is a [=struct=] with the following items:
+
+
+: values
+:: A [=map=] whose [=map/key|keys=] are [=strings=] and whose [=map/value|values=] are non-negative 32-bit integers.
+: filters
+:: A [=list=] of [=filter configs=].
+: negated filters
+:: A [=list=] of [=filter configs=].
+
+
+
Aggregatable dedup key
An aggregatable dedup key is a [=struct=] with the following items:
@@ -868,9 +882,8 @@ An attribution trigger is a [=struct=] with the following items:
:: A [=set=] of [=event-level trigger configuration=].
: aggregatable trigger data
:: A [=list=] of [=aggregatable trigger data=].
-: aggregatable values
-:: An [=ordered map=] whose [=map/key|keys=] are [=strings=] and whose
- [=map/value|values=] are non-negative 32-bit integers.
+: aggregatable values configurations
+:: A [=list=] of [=aggregatable values configuration=].
: aggregatable dedup keys
:: A [=list=] of [=aggregatable dedup key=].
: verifications
@@ -1196,9 +1209,9 @@ controls the maximum [=map/size=] of an [=attribution source=]'s
Max length per aggregation key identifier is a positive integer that controls
the maximum [=string/length=] of an [=attribution source=]'s [=attribution source/aggregation keys=]'s
-[=map/keys=], an [=attribution trigger=]'s [=attribution trigger/aggregatable values=]'s [=map/keys=],
-and an [=aggregatable trigger data=]'s [=aggregatable trigger data/source keys=]'s [=set/items=].
-Its value is 25.
+[=map/keys=], an [=attribution trigger=]'s [=attribution trigger/aggregatable values configurations=]'s [=list/item=]'s
+[=aggregatable values configuration/values=]'s [=map/keys=], and an [=aggregatable trigger data=]'s
+[=aggregatable trigger data/source keys=]'s [=set/items=]. Its value is 25.
Default trigger data cardinality is a [=map=] that
controls the valid range of [=event-level trigger configuration/trigger data=].
@@ -2399,18 +2412,51 @@ To parse aggregatable trigger data given an [=ordered map=] |map|:
1. [=list/Append=] |aggregatableTrigger| to |aggregatableTriggerData|.
1. Return |aggregatableTriggerData|.
+To parse aggregatable key-values given a [=map=] |map|:
+
+1. [=map/iterate|For each=] |key| → |value| of |map|:
+ 1. If |key|'s [=string/length=] is greater than the [=max length per aggregation key identifier=],
+ return null.
+ 1. If |value| is not an integer, return null.
+ 1. If |value| is less than or equal to 0, return null.
+ 1. If |value| is greater than [=allowed aggregatable budget per source=], return null.
+1. Return |map|.
+
To parse aggregatable values given an [=ordered map=] |map|:
-1. If |map|["`aggregatable_values`"] does not [=map/exist=], return «[]».
+1. If |map|["`aggregatable_values`"] does not [=map/exist=], return a new [=list/is empty|empty=] [=list=].
1. Let |values| be |map|["`aggregatable_values`"].
-1. If |values| is not an [=ordered map=], return null.
-1. [=map/iterate|For each=] |key| → |value| of |values|:
- 1. If |key|'s [=string/length=] is greater than the [=max length per aggregation key identifier=],
- return null.
- 1. If |value| is not an integer, return null.
- 1. If |value| is less than or equal to 0, return null.
- 1. If |value| is greater than [=allowed aggregatable budget per source=], return null.
-1. Return |values|.
+1. If |values| is not an [=ordered map=] or a [=list=], return null.
+1. Let |aggregatableValuesConfigurations| be a [=list=] of [=aggregatable values configurations=], initially empty.
+1. If |values| is a [=map=]:
+ 1. Let |aggregatableKeyValues| be the result of running [=parse aggregatable key-values=] with |values|.
+ 1. If |aggregatableKeyValues| is null, return null.
+ 1. Let |aggregatableValuesConfiguration| be a new [=aggregatable values configuration=] with the items:
+ : [=aggregatable values configuration/values=]
+ :: |aggregatableKeyValues|
+ : [=aggregatable values configuration/filters=]
+ :: «»
+ : [=aggregatable values configuration/negated filters=]
+ :: «»
+ 1. [=list/Append=] |aggregatableValuesConfiguration| to |aggregatableValuesConfigurations|.
+ 1. Return |aggregatableValuesConfigurations|.
+1. [=list/iterate|For each=] |value| of |values|:
+ 1. If |value| is not a [=map=], return null.
+ 1. If |value|["`values`"] does not [=map/exist=], return null.
+ 1. Let |aggregatableKeyValues| be the result of running [=parse aggregatable key-values=] with |value|["`values`"].
+ 1. If |aggregatableKeyValues| is null, return null.
+ 1. Let |filterPair| be the result of running [=parse a filter pair=] with
+ |value|.
+ 1. If |filterPair| is null, return null.
+ 1. Let |aggregatableValuesConfiguration| be a new [=aggregatable values configuration=] with the items:
+ : [=aggregatable values configuration/values=]
+ :: |aggregatableKeyValues|
+ : [=aggregatable values configuration/filters=]
+ :: |filterPair|[0]
+ : [=aggregatable values configuration/negated filters=]
+ :: |filterPair|[1]
+ 1. [=list/Append=] |aggregatableValuesConfiguration| to |aggregatableValuesConfigurations|.
+1. Return |aggregatableValuesConfigurations|.
To parse aggregatable dedup keys given an [=ordered map=] |map|:
@@ -2450,8 +2496,8 @@ and a [=moment=] |triggerTime|:
1. Let |aggregatableTriggerData| be the result of running [=parse aggregatable trigger data=]
with |value|.
1. If |aggregatableTriggerData| is null, return null.
-1. Let |aggregatableValues| be the result of running [=parse aggregatable values=] with |value|.
-1. If |aggregatableValues| is null, return null.
+1. Let |aggregatableValuesConfigurations| be the result of running [=parse aggregatable values=] with |value|.
+1. If |aggregatableValuesConfigurations| is null, return null.
1. Let |aggregatableDedupKeys| be the result of running [=parse aggregatable dedup keys=]
with |value|.
1. If |aggregatableDedupKeys| is null, return null.
@@ -2503,8 +2549,8 @@ and a [=moment=] |triggerTime|:
:: |eventTriggers|
: [=attribution trigger/aggregatable trigger data=]
:: |aggregatableTriggerData|
- : [=attribution trigger/aggregatable values=]
- :: |aggregatableValues|
+ : [=attribution trigger/aggregatable values configurations=]
+ :: |aggregatableValuesConfigurations|
: [=attribution trigger/aggregatable dedup keys=]
:: |aggregatableDedupKeys|
: [=attribution trigger/verifications=]
@@ -2649,6 +2695,21 @@ Given an [=attribution trigger=] |trigger|, an [=attribution source=]
Creating aggregatable contributions
+To create [=aggregatable contributions=] from [=attribution source/aggregation keys=] and aggregatable [=aggregatable values configuration/values=]
+ given a [=map=] |aggregationKeys| and a [=map=] |aggregatableValues|,
+ run the following steps:
+
+1. Let |contributions| be an empty [=list=].
+1. [=map/iterate|For each=] |id| → |key| of |aggregationKeys|:
+ 1. If |aggregatableValues|[|id|] does not [=map/exist=], [=iteration/continue=].
+ 1. Let |contribution| be a new [=aggregatable contribution=] with the items:
+ : [=aggregatable contribution/key=]
+ :: |key|
+ : [=aggregatable contribution/value=]
+ :: |aggregatableValues|[|id|]
+ 1. [=list/Append=] |contribution| to |contributions|.
+1. Return |contributions|.
+
To create [=aggregatable contributions=] given an [=attribution source=] |source| and an
[=attribution trigger=] |trigger|, run the following steps:
@@ -2663,17 +2724,15 @@ To create [=aggregatable contributions=] given an [=attribution sourc
1. If |aggregationKeys|[|sourceKey|] does not [=map/exist=], [=iteration/continue=].
1. [=map/Set=] |aggregationKeys|[|sourceKey|] to |aggregationKeys|[|sourceKey|] bitwise-OR |triggerData|'s
[=aggregatable trigger data/key piece=].
-1. Let |aggregatableValues| be |trigger|'s [=attribution trigger/aggregatable values=].
-1. Let |contributions| be a new empty [=list=].
-1. [=map/iterate|For each=] |id| → |key| of |aggregationKeys|:
- 1. If |aggregatableValues|[|id|] does not [=map/exist=], [=iteration/continue=].
- 1. Let |contribution| be a new [=aggregatable contribution=] with the items:
- : [=aggregatable contribution/key=]
- :: |key|
- : [=aggregatable contribution/value=]
- :: |aggregatableValues|[|id|]
- 1. [=list/Append=] |contribution| to |contributions|.
-1. Return |contributions|.
+1. Let |aggregatableValuesConfigurations| be |trigger|'s [=attribution trigger/aggregatable values configurations=].
+1. [=list/iterate|For each=] |aggregatableValuesConfiguration| of |aggregatableValuesConfigurations|:
+ 1. If the result of running [=match an attribution source against filters and negated filters=] with
+ |source|, |aggregatableValuesConfiguration|'s [=aggregatable values configuration/filters=],
+ |aggregatableValuesConfiguration|'s [=aggregatable values configuration/negated filters=], and
+ |trigger|'s [=attribution trigger/trigger time=] is true:
+ 1. Return the result of running [=create aggregatable contributions from aggregation keys and aggregatable values=] with
+ |aggregationKeys| and |aggregatableValuesConfiguration|'s [=aggregatable values configuration/values=].
+1. Return a new [=list/is empty|empty=] [=list=].
Can source create aggregatable contributions
@@ -3019,7 +3078,7 @@ To check if an [=attribution trigger=] contains aggregatable data giv
run the following steps:
1. If |trigger|'s [=attribution trigger/aggregatable trigger data=] is not [=list/is empty|empty=], return true.
-1. If |trigger|'s [=attribution trigger/aggregatable values=] is not [=map/is empty|empty=], return true.
+1. If any of |trigger|'s [=attribution trigger/aggregatable values configurations=]'s [=aggregatable values configuration/values=] is not [=list/is empty|empty=], return true.
1. Return false.
To trigger attribution given an [=attribution trigger=] |trigger|, run the following steps:
diff --git a/ts/src/header-validator/trigger.test.ts b/ts/src/header-validator/trigger.test.ts
index f8e950d0ba..2442314df1 100644
--- a/ts/src/header-validator/trigger.test.ts
+++ b/ts/src/header-validator/trigger.test.ts
@@ -81,7 +81,13 @@ const testCases: jsontest.TestCase[] = [
sourceKeys: new Set(['x']),
},
],
- aggregatableValues: new Map([['x', 5]]),
+ aggregatableValuesConfigurations: [
+ {
+ values: new Map([['x', 5]]),
+ positive: [],
+ negative: [],
+ },
+ ],
debugKey: 5n,
debugReporting: true,
eventTriggerData: [
@@ -118,6 +124,27 @@ const testCases: jsontest.TestCase[] = [
],
}),
},
+ {
+ name: 'aggregatable-values-list-with-filters',
+ json: `{
+ "aggregatable_values": [
+ {
+ "values": {
+ "a": 1
+ },
+ "filters": [{"g": []}, {"h": []}],
+ "not_filters": [{"g": []}, {"h": []}]
+ },
+ {
+ "values": {
+ "b": 2
+ },
+ "filters": [{"i": []}, {"j": []}],
+ "not_filters": [{"i": []}, {"j": []}]
+ }
+ ]
+ }`,
+ },
{
name: 'or-filters',
json: `{
@@ -320,7 +347,7 @@ const testCases: jsontest.TestCase[] = [
expectedErrors: [
{
path: ['aggregatable_values'],
- msg: 'must be an object',
+ msg: 'must be an object or a list',
},
],
},
@@ -364,7 +391,47 @@ const testCases: jsontest.TestCase[] = [
},
],
},
+ {
+ name: 'aggregatable-values-list-values-field-missing',
+ json: `{
+ "aggregatable_values": [
+ {
+ "a": 1
+ }
+ ]
+ }`,
+ expectedErrors: [
+ {
+ path: ['aggregatable_values', 0, 'values'],
+ msg: 'required',
+ },
+ ],
+ expectedWarnings: [
+ {
+ msg: 'unknown field',
+ path: ['aggregatable_values', 0, 'a'],
+ },
+ ],
+ },
+ {
+ name: 'aggregatable-values-list-wrong-type',
+ json: `{
+ "aggregatable_values": [
+ {
+ "values": []
+ }
+ ]
+ }`,
+ expectedErrors: [
+ {
+ path: ['aggregatable_values', 0, 'values'],
+ msg: 'must be an object',
+ },
+ ],
+ },
+ // TODO(apasel422): Uncomment once respective function is updated.
+ /*
{
name: 'inconsistent-aggregatable-keys',
json: `{
@@ -398,6 +465,7 @@ const testCases: jsontest.TestCase[] = [
},
],
},
+ */
{
name: 'debug-reporting-wrong-type',
diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts
index 26c7776408..46f0033dc9 100644
--- a/ts/src/header-validator/validate-json.ts
+++ b/ts/src/header-validator/validate-json.ts
@@ -1194,6 +1194,10 @@ function aggregatableTriggerData(
)
}
+export type AggregatableValuesConfiguration = FilterPair & {
+ values: Map
+}
+
function aggregatableKeyValue(
ctx: Context,
[key, j]: [string, Json]
@@ -1208,10 +1212,32 @@ function aggregatableKeyValue(
)
}
-function aggregatableValues(ctx: Context, j: Json): Maybe