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> { +function aggregatableKeyValues( + ctx: Context, + j: Json +): Maybe> { return keyValues(ctx, j, aggregatableKeyValue) } +function aggregatableValuesConfigurations( + ctx: Context, + j: Json +): Maybe { + return typeSwitch(ctx, j, { + object: (ctx, j) => + aggregatableKeyValues(ctx, j).map((values) => [ + { values, positive: [], negative: [] }, + ]), + list: (ctx, j) => + array(ctx, j, (ctx, j) => + struct(ctx, j, { + values: field('values', aggregatableKeyValues), + ...filterFields, + }) + ), + }) +} + export type EventTriggerDatum = FilterPair & Priority & DedupKey & { @@ -1276,8 +1302,10 @@ function aggregatableSourceRegistrationTime( return enumerated(ctx, j, AggregatableSourceRegistrationTime) } -function warnInconsistentAggregatableKeys(ctx: Context, t: Trigger): void { - const triggerDataKeys = new Set() +// TODO(apasel422): Update with new AggregatableValuesConfiguration structure. +function warnInconsistentAggregatableKeys(_ctx: Context, _t: Trigger): void { + /* +const triggerDataKeys = new Set() ctx.scope('aggregatable_trigger_data', () => { for (const [index, datum] of t.aggregatableTriggerData.entries()) { @@ -1308,6 +1336,7 @@ function warnInconsistentAggregatableKeys(ctx: Context, t: Trigger): void { } } }) +*/ } function triggerContextID( @@ -1350,7 +1379,7 @@ export type Trigger = CommonDebug & aggregatableDedupKeys: AggregatableDedupKey[] aggregatableTriggerData: AggregatableTriggerDatum[] aggregatableSourceRegistrationTime: AggregatableSourceRegistrationTime - aggregatableValues: Map + aggregatableValuesConfigurations: AggregatableValuesConfiguration[] aggregationCoordinatorOrigin: string | null eventTriggerData: EventTriggerDatum[] triggerContextID: string | null @@ -1371,10 +1400,10 @@ function trigger(ctx: RegistrationContext, j: Json): Maybe { aggregatableTriggerData, [] ), - aggregatableValues: field( + aggregatableValuesConfigurations: field( 'aggregatable_values', - aggregatableValues, - new Map() + aggregatableValuesConfigurations, + [] ), aggregatableDedupKeys: field( 'aggregatable_deduplication_keys',