diff --git a/EVENT.md b/EVENT.md index 55774ad23d..9c4432ee68 100644 --- a/EVENT.md +++ b/EVENT.md @@ -284,8 +284,8 @@ about how to treat the trigger: ``` - `trigger_data`: Optional. Coarse-grained data to identify the trigger. - The value will be limited [depending on the - attributed source type](#data-limits-and-noise). Defaults to 0. + The value will be used [according to the attributed source's allowed trigger + data and matching mode](#data-limits-and-noise). Defaults to 0. - `priority`: Optional. A signed 64-bit integer representing the priority of this trigger compared to other triggers for the same source. Defaults to 0. - `deduplication_key`: Optional. An unsigned 64-bit integer that will be used to @@ -335,9 +335,64 @@ The `source_event_id` is limited to 64 bits of information to enable uniquely identifying an ad click. The trigger-side data must therefore be limited quite strictly, by limiting -the amount of data and by applying noise to the data. `navigation` sources will -be limited to only 3 bits of `trigger_data`, while `event` sources will be -limited to only 1 bit. +the amount of data and by applying noise to the data. By default, `navigation` +sources will be limited to only 3 bits of `trigger_data` (the values 0 through +7), while `event` sources will be limited to only 1 bit (the values 0 through +1). + +Sources can be configured to allow non-default `trigger_data` (values and/or +cardinality): + +```jsonc +{ + // Specifies how the 64-bit unsigned trigger_data from the trigger is matched + // against the source's trigger_data, which is 32-bit. Defaults to "modulus". + // + // If "exact", the trigger_data must exactly match a value contained in the + // source's trigger_data; if there is no such match, no event-level + // attribution takes place. + // + // If "modulus", the source's trigger_data must form a contiguous sequence of + // integers starting at 0. The trigger's trigger_data is taken modulus the + // cardinality of this sequence and then matched against the trigger data. + // See below for an example. It is an error to use "modulus" if the trigger + // data does not form such a sequence. + "trigger_data_matching": , + + // Size must be in the range [0, 32], inclusive. + // If omitted, defaults to [0, 1, 2, 3, 4, 5, 6, 7] for navigation sources and + // [0, 1] for event sources. + "trigger_data": [<32-bit unsigned integer>, ...] +} +``` + +For example, the following configuration can be used to *reduce* noise for a +navigation source (by limiting the number of distinct trigger data values) or +*increase* noise for an event source (by increasing the number): + +```jsonc +{ + ..., + // The effective cardinality is 5, so the trigger's trigger_data will be taken + // modulus 5. Likewise, noise will be applied using the effective cardinality + // of 5, instead of the default for the source type. + "trigger_data": [0, 1, 2, 3, 4] +} +``` + +If `"trigger_data_matching": "exact"` is used, then the values themselves need +not form a contiguous sequence beginning at zero, and any generated report will +contain the exact value, e.g. + +```jsonc +{ + "trigger_data_matching": "exact", + // If this list does not contain the trigger's trigger_data value, attribution + // will fail and no report will be generated. Noise will be applied using an + // effective cardinality of 2. + "trigger_data": [123, 456] +} +``` Noise will be applied to whether a source will be reported truthfully. When an attribution source is registered, the browser will perform one of the @@ -350,16 +405,17 @@ all, or potentially reporting multiple fake reports for the event. Note that this scheme is an instantiation of k-randomized response, see [Differential privacy](#differential-privacy). -Strawman: we can set `p` such that each source is protected with randomized -response that satisfies an epsilon value of 14. This would entail: +`p` is set such that each source is protected with randomized response that +satisfies an epsilon value of 14. This would entail (for default configured +sources): * `p = .24%` for `navigation` sources * `p = .00025%` for `event` sources Note that correcting for this noise addition is straightforward in most cases, please see . Reports will be -updated to include `p` so that noise correction can work correctly in the event -that `p` changes over time, or if different browsers apply different -probabilities: +updated to include `p` so that noise correction can work correctly for +configurations that have different values of `p`, or if different browsers apply +different probabilities: ```jsonc { diff --git a/flexible_event_config.md b/flexible_event_config.md index 19cc4b08f5..6cf7f23e2a 100644 --- a/flexible_event_config.md +++ b/flexible_event_config.md @@ -104,26 +104,16 @@ In addition to the parameters that were added in Phase 1, we will add one additi // Next trigger_spec }, ...], - // Specifies how the 64-bit unsigned trigger_data from the trigger is matched - // against the source's trigger_specs trigger_data, which is 32-bit. Defaults - // to "modulus". - // - // If "exact", the trigger_data must exactly match a value contained in the - // source's trigger_specs; if there is no such match, no event-level - // attribution takes place. - // - // If "modulus", the set of all trigger_data values across all trigger_specs - // for the source must be a contiguous sequence of integers starting at 0. - // The trigger's trigger_data is taken modulus the cardinality of this - // sequence and then matched against the trigger specs. See below for an - // example. It is an error to use "modulus" if the trigger specs do not - // contain such a sequence. + // See description in + // https://github.com/WICG/attribution-reporting-api/blob/main/EVENT.md#data-limits-and-noise "trigger_data_matching": , - // See description in phase 1. + // See description in + // https://github.com/WICG/attribution-reporting-api/blob/main/EVENT.md#optional-varying-frequency-and-number-of-reports "max_event_level_reports": , - // See description in phase 1. + // See description in + // https://github.com/WICG/attribution-reporting-api/blob/main/EVENT.md#optional-varying-frequency-and-number-of-reports "event_report_windows": { "start_time": , "end_times": [, ...] diff --git a/index.bs b/index.bs index 7859c37092..33dafe36a7 100644 --- a/index.bs +++ b/index.bs @@ -715,6 +715,26 @@ A trigger spec is a [=struct=] with the following items: A trigger spec map is a [=map=] whose keys are unsigned 32-bit integers and values are [=trigger specs=]. +To find a matching trigger spec given an [=attribution source=] +|source| and an unsigned 64-bit integer |triggerData|: + +1. Let |specs| be |source|'s [=attribution source/trigger specs=]. +1. If |source|'s [=attribution source/trigger-data matching mode=] is: +
+ : "[=trigger-data matching mode/exact=]" + :: Run the following steps: + 1. If |specs|[|triggerData|] [=map/exists=], return its [=map/entry=]. + 1. Return null. + : "[=trigger-data matching mode/modulus=]" + :: Run the following steps: + 1. If |specs| [=map/is empty=], return null. + 1. Let |keys| be |specs|'s [=map/get the keys|keys=]. + 1. Let |index| be the remainder when dividing |triggerData| by |keys|'s + [=set/size=]. + 1. Return the [=map/entry=] for |specs|[|keys|[|index|]]. + +
+

Attribution source

An attribution source is a [=struct=] with the following items: @@ -734,8 +754,8 @@ An attribution source is a [=struct=] with the following items: :: A [=source type=]. : expiry :: A [=duration=]. -: event-level report windows -:: A [=report window list=]. +: trigger specs +:: A [=trigger spec map=]. : aggregatable report window :: A [=report window=]. : priority @@ -1151,7 +1171,7 @@ Its value is (1 day, 30 days). Min report window is a positive [=duration=] that controls the minimum [=duration from=] an [=attribution source's=] [=attribution source/source time=] and any [=report window/end=] in [=attribution source/aggregatable report window=] or -[=attribution source/event-level report windows=]. +[=trigger spec/event-level report windows=]. Its value is 1 hour. Max entries per filter data is a positive integer that controls the @@ -1179,7 +1199,7 @@ controls the maximum value of [=attribution source/max number of event-level rep Its value is 20. Max settable event-level report windows is a positive integer that -controls the maximum [=list/size=] of [=attribution source/event-level report windows=]. +controls the maximum [=list/size=] of [=trigger spec/event-level report windows=]. Its value is 5. Default event-level attributions per source is a [=map=] that @@ -1903,6 +1923,12 @@ To parse report windows given a |value|, a 1. Set |startDuration| to |endDuration|. 1. Return |windows|. +The user-agent has an associated boolean +experimental Flexible Event support (default false) that exposes +non-normative behavior described in the +Flexible event-level configurations +proposal. + To parse summary window operator given a [=map=] |map|: 1. Let |value| be "[=summary window operator/count=]". @@ -1939,48 +1965,71 @@ To parse summary buckets given a [=map=] |map| and an integer |maxEve 1. Set |prev| to |item|. 1. Return |summaryBuckets|. +To parse trigger data into a trigger spec map given a +|triggerDataList|, a [=trigger spec=] |spec|, a [=trigger spec map=] +|specs|, and a [=boolean=] |allowEmpty|: + +1. If |triggerDataList| is not a [=list=] or its [=list/size=] is greater than + [=max distinct trigger data per source=], return false. +1. If |allowEmpty| is false and |triggerDataList| [=list/is empty=], return + false. +1. [=list/iterate|For each=] |triggerData| of |triggerDataList|: + 1. If |triggerData| is not an integer or cannot be represented by an + unsigned 32-bit integer, or |specs|[|triggerData|] [=map/exists=], + return false. + 1. [=map/Set=] |specs|[|triggerData|] to |spec|. + 1. If |specs|'s [=map/size=] is greater than + [=max distinct trigger data per source=], return false. +1. Return true. + To parse trigger specs given a [=map=] |map|, a [=moment=] -|sourceTime|, a [=duration=] |expiry|, a [=report window list=] -|defaultReportWindows|, an unsigned 32-bit integer -|defaultTriggerDataCardinality|, and a [=trigger-data matching mode=] -|matchingMode|: - -1. [=Assert=]: |defaultTriggerDataCardinality| is greater than 0 and less than - or equal to [=max distinct trigger data per source=]. -1. Let |specs| be a new [=map=]. -1. If |map|["`trigger_specs`"] does not [=map/exist=]: - 1. Let |spec| be a new [=trigger spec=] with the following items: - : [=trigger spec/event-level report windows=] - :: |defaultReportWindows| - 1. [=set/iterate|For each=] integer |triggerData| of [=the exclusive range|the range=] 0 to - |defaultTriggerDataCardinality|, exclusive: - 1. [=map/Set=] |specs|[|triggerData|] to |spec|. - 1. Return |specs|. -1. If |map|["`trigger_specs`"] is not a [=list=] or its - [=list/size=] is greater than [=max distinct trigger data per source=], - return an error. -1. [=list/iterate|For each=] |item| of |map|["`trigger_specs`"]: - 1. If |item| is not a [=map=], return an error. +|sourceTime|, a [=source type=] |sourceType|, a [=duration=] |expiry|, and a +[=trigger-data matching mode=] |matchingMode|: + +1. Let |defaultReportWindows| be the result of + [=parsing top-level report windows=] with |map|, |sourceTime|, |sourceType|, + and |expiry|. +1. If |defaultReportWindows| is an error, return an error. +1. Let |specs| be a new [=trigger spec map=]. +1. If [=experimental Flexible Event support=] is true and + |map|["`trigger_specs`"] [=map/exists=]: + 1. If |map|["`trigger_data`"] [=map/exists=], return an error. + 1. If |map|["`trigger_specs`"] is not a [=list=] or its + [=list/size=] is greater than [=max distinct trigger data per source=], + return an error. + 1. [=list/iterate|For each=] |item| of |map|["`trigger_specs`"]: + 1. If |item| is not a [=map=], return an error. + 1. Let |spec| be a new [=trigger spec=] with the following items: + : [=trigger spec/event-level report windows=] + :: |defaultReportWindows| + 1. If |item|["`event_report_windows`"] [=map/exists=]: + 1. Let |reportWindows| be the result of + [=parsing report windows=] with |item|["`event_report_windows`"], + |sourceTime|, and |expiry|. + 1. If |reportWindows| is an error, return it. + 1. Set |spec|'s [=trigger spec/event-level report windows=] to + |reportWindows|. + 1. If |item|["`trigger_data`"] does not [=map/exist=], return an error. + 1. Let |allowEmpty| be false. + 1. If the result of running + [=parse trigger data into a trigger spec map=] with + |item|["`trigger_data`"], |spec|, |specs|, and |allowEmpty| is + false, return an error. +1. Otherwise: 1. Let |spec| be a new [=trigger spec=] with the following items: : [=trigger spec/event-level report windows=] :: |defaultReportWindows| - 1. If |item|["`event_report_windows`"] [=map/exists=]: - 1. Let |reportWindows| be the result of - [=parsing report windows=] with |item|["`event_report_windows`"], - |sourceTime|, and |expiry|. - 1. If |reportWindows| is an error, return it. - 1. Set |spec|'s [=trigger spec/event-level report windows=] to - |reportWindows|. - 1. If |item|["`trigger_data`"] does not [=map/exist=], is not a [=list=], or - [=list/is empty=], or its [=list/size=] is greater than - [=max distinct trigger data per source=], return an error. - 1. [=list/iterate|For each=] |triggerData| of |item|["`trigger_data`"]: - 1. If |triggerData| is not an integer or cannot be represented by an - unsigned 32-bit integer, or |specs|[|triggerData|] [=map/exists=], + 1. If |map|["`trigger_data`"] [=map/exists=]: + 1. Let |allowEmpty| be true. + 1. If the result of running + [=parse trigger data into a trigger spec map=] with + |map|["`trigger_data`"], |spec|, |specs|, and |allowEmpty| is false, return an error. - 1. [=map/Set=] |specs|[|triggerData|] to |spec|. - 1. If |specs|'s [=map/size=] is greater than - [=max distinct trigger data per source=], return an error. + 1. Otherwise: + 1. [=set/iterate|For each=] integer |triggerData| of + [=the exclusive range|the range=] 0 to + [=default trigger data cardinality=][|sourceType|], exclusive: + 1. [=map/Set=] |specs|[|triggerData|] to |spec|. 1. If |matchingMode| is "[=trigger-data matching mode/modulus=]": 1. Let |i| be 0. 1. [=map/iterate|For each=] |triggerData| of |specs|'s [=map/get the keys|keys=]: @@ -2032,7 +2081,6 @@ To parse source-registration JSON given a [=byte sequence=] 1. If |debugCookieSet| is false, set |debugKey| to null. 1. Let |aggregationKeys| be the result of running [=parse aggregation keys=] with |value|. 1. If |aggregationKeys| is null, return null. -1. Let |triggerDataCardinality| be [=default trigger data cardinality=][|sourceType|]. 1. Let |maxAttributionsPerSource| be [=default event-level attributions per source=][|sourceType|]. 1. Set |maxAttributionsPerSource| to |value|["`max_event_level_reports`"] if it [=map/exists=]. 1. If |maxAttributionsPerSource| is not a non-negative integer, or is greater than [=max settable event-level attributions per source=], return null. @@ -2042,8 +2090,6 @@ To parse source-registration JSON given a [=byte sequence=] 1. Let |debugReportingEnabled| be false. 1. If |value|["`debug_reporting`"] [=map/exists=] and is a [=boolean=], set |debugReportingEnabled| to |value|["`debug_reporting`"]. -1. Let |eventReportWindows| be the result of [=parsing top-level report windows=] with |value|, |sourceTime|, |sourceType|, and |expiry|. -1. If |eventReportWindows| is an error, return null. 1. Let |aggregatableReportWindow| be a new [=report window=] with the following items: : [=report window/start=] @@ -2051,12 +2097,14 @@ To parse source-registration JSON given a [=byte sequence=] : [=report window/end=] :: |sourceTime| + |aggregatableReportWindowEnd| -1. Let |triggerSpecs| be a new [=trigger spec map=]. -1. [=set/iterate|For each=] integer |triggerData| of [=the exclusive range|the range=] 0 to |triggerDataCardinality|, exclusive: - 1. Let |spec| be a new [=trigger spec=] struct whose items are: - : [=trigger spec/event-level report windows=] - :: |eventReportWindows| - 1. [=map/Set=] |triggerSpecs|[|triggerData|] to |spec|. +1. Let |triggerDataMatchingMode| be "[=trigger-data matching mode/modulus=]". +1. If |value|["`trigger_data_matching`"] [=map/exists=]: + 1. If |value|["`trigger_data_matching`"] is not a [=string=], return null. + 1. If |value|["`trigger_data_matching`"] is not a [=trigger-data matching mode=], return null. + 1. Set |triggerDataMatchingMode| to |value|["`trigger_data_matching`"]. +1. Let |triggerSpecs| be the result of [=parsing trigger specs=] with |value|, + |sourceTime|, |sourceType|, |expiry|, and |triggerDataMatchingMode|. +1. If |triggerSpecs| is an error, return null. 1. Let |randomizedResponseConfig| be a new [=randomized response output configuration=] whose items are: : [=randomized response output configuration/max attributions per source=] @@ -2070,11 +2118,6 @@ To parse source-registration JSON given a [=byte sequence=] 1. If [=automation local testing mode=] is true, set |epsilon| to `∞`. 1. If the result of [=computing the channel capacity of a source=] with |randomizedResponseConfig| and |epsilon| is greater than [=max event-level channel capacity per source=][|sourceType|], return null. -1. Let |triggerDataMatchingMode| be "[=trigger-data matching mode/modulus=]". -1. If |value|["`trigger_data_matching`"] [=map/exists=]: - 1. If |value|["`trigger_data_matching`"] is not a [=string=], return null. - 1. If |value|["`trigger_data_matching`"] is not a [=trigger-data matching mode=], return null. - 1. Set |triggerDataMatchingMode| to |value|["`trigger_data_matching`"]. 1. Let |source| be a new [=attribution source=] struct whose items are: : [=attribution source/source identifier=] @@ -2089,8 +2132,8 @@ To parse source-registration JSON given a [=byte sequence=] :: |reportingOrigin| : [=attribution source/expiry=] :: |expiry| - : [=attribution source/event-level report windows=] - :: |eventReportWindows| + : [=attribution source/trigger specs=] + :: |triggerSpecs| : [=attribution source/aggregatable report window=] :: |aggregatableReportWindow| : [=attribution source/priority=] @@ -2177,20 +2220,14 @@ To check if an [=attribution source=] exceeds the unexpired destination lim To obtain a fake report given an [=attribution source=] |source| and a [=trigger state=] |triggerState|: -1. Let |fakeConfig| be a new [=event-level trigger configuration=] with the items: - : [=event-level trigger configuration/trigger data=] - :: |triggerState|'s [=trigger state/trigger data=] - : [=event-level trigger configuration/dedup key=] - :: null - : [=event-level trigger configuration/priority=] - :: 0 - : [=event-level trigger configuration/filters=] - :: «[ "`source_type`" → « |source|'s [=attribution source/source type=] » ]» +1. Let |specEntry| be the [=map/entry=] for + |source|'s [=attribution source/trigger specs=][|triggerState|'s [=trigger state/trigger data=]]. 1. Let |triggerTime| be the greatest [=moment=] that is strictly less than |triggerState|'s [=trigger state/report window=]'s [=report window/end=]. +1. Let |priority| be 0. 1. Let |fakeReport| be the result of running [=obtain an event-level report=] with |source|, |triggerTime|, [=obtain an event-level report/triggerDebugKey=] set to null, - |fakeConfig|, and |triggerState|'s [=trigger state/trigger data=]. + |priority|, and |specEntry|. 1. [=Assert=]: |fakeReport|'s [=event-level report/report time=] is equal to |triggerState|'s [=trigger state/report window=]'s [=report window/end=]. 1. Return |fakeReport|. @@ -2798,6 +2835,10 @@ To maybe replace event-level report given an [=attribution source=] 1. Decrement |sourceToAttribute|'s [=attribution source/number of event-level reports=] value by 1. 1. Return "[=event-level-report-replacement result/add-new-report=]". +Issue: This algorithm is not compatible with the behavior proposed for +[=experimental Flexible Event support=] with differing +[=trigger spec/event-level report windows=] for a given source. + To trigger event-level attribution given an [=attribution trigger=] |trigger|, an [=attribution source=] |sourceToAttribute|, and an [=attribution rate-limit record=] |rateLimitRecord|, run the following steps: @@ -2832,24 +2873,17 @@ To trigger event-level attribution given an [=attribution trigger=] | with "[=trigger debug data type/trigger-event-deduplicated=]", |trigger|, |sourceToAttribute| and [=obtain debug data on trigger registration/report=] set to null. 1. Return the [=triggering result=] ("[=triggering status/dropped=]", |debugData|). -1. Let |triggerData| be |matchedConfig|'s [=event-level trigger configuration/trigger data=]. -1. Let |triggerDataCardinality| be [=default trigger data cardinality=][|attributedSource|'s [=attribution source/source type=]]. -1. If |attributedSource|'s [=attribution source/trigger-data matching mode=] is: -
- : "[=trigger-data matching mode/exact=]": - :: Do nothing. - : "[=trigger-data matching mode/modulus=]": - :: Set |triggerData| to the remainder when dividing |triggerData| by |triggerDataCardinality|. - -
-1. If |triggerData| is greater than or equal to |triggerDataCardinality|: +1. Let |specEntry| be the result of [=finding a matching trigger spec=] with + |sourceToAttribute| and |matchedConfig|'s + [=event-level trigger configuration/trigger data=]. +1. If |specEntry| is null: 1. Let |debugData| be the result of running [=obtain debug data on trigger registration=] with "[=trigger debug data type/trigger-event-no-matching-trigger-data=]", |trigger|, |sourceToAttribute| and [=obtain debug data on trigger registration/report=] set to null. 1. Return the [=triggering result=] ("[=triggering status/dropped=]", |debugData|). 1. Let |windowResult| be the result of [=check whether a moment falls within a window=] with |trigger|'s [=attribution trigger/trigger time=] and - |sourceToAttribute|'s [=attribution source/event-level report windows=]'s + |specEntry|'s [=map/value=]'s [=trigger spec/event-level report windows=]'s [=report window list/total window=]. 1. If |windowResult| is falls before: 1. Let |debugData| be the result of running [=obtain debug data on trigger registration=] @@ -2874,7 +2908,7 @@ To trigger event-level attribution given an [=attribution trigger=] | return it. 1. Let |report| be the result of running [=obtain an event-level report=] with |sourceToAttribute|, |trigger|'s [=attribution trigger/trigger time=], |trigger|'s [=attribution trigger/debug key=], - |matchedConfig|, and |triggerData|. + |matchedConfig|'s [=event-level trigger configuration/priority=], and |specEntry|. 1. If |sourceToAttribute|'s [=attribution source/event-level attributable=] value is false: 1. Let |debugData| be the result of running [=obtain debug data on trigger registration=] @@ -3099,11 +3133,10 @@ a [=report window=] |window|: return falls after. 1. Return falls within. -To obtain an event-level report delivery time given an [=attribution source=] -|source| and a [=moment=] |triggerTime|: +To obtain an event-level report delivery time given a +[=report window list=] |windows| and a [=moment=] |triggerTime|: 1. If [=automation local testing mode=] is true, return |triggerTime|. -1. Let |windows| be |source|'s [=attribution source/event-level report windows=]. 1. [=list/iterate|For each=] |window| of |windows|: 1. If the result of [=check whether a moment falls within a window=] with |triggerTime| and |window| is falls within, return @@ -3123,15 +3156,19 @@ To obtain an aggregatable report delivery time given an [=attribution To obtain an event-level report given an [=attribution source=] |source|, a [=moment=] |triggerTime|, an optional non-negative 64-bit integer triggerDebugKey, -an [=event-level trigger configuration=] |config|, and a non-negative 64-bit integer |triggerData|: +a 64-bit integer priority |priority|, and a [=trigger spec map=] [=map/entry=] +|specEntry|: -1. Let |reportTime| be the result of running [=obtain an event-level report delivery time=] with |source| and |triggerTime|. +1. Let |reportTime| be the result of running + [=obtain an event-level report delivery time=] with |specEntry|'s + [=map/value=]'s [=trigger spec/event-level report windows=] and + |triggerTime|. 1. Let |report| be a new [=event-level report=] struct whose items are: : [=event-level report/event ID=] :: |source|'s [=attribution source/event ID=]. : [=event-level report/trigger data=] - :: |triggerData| + :: |specEntry|'s [=map/key=] : [=event-level report/randomized trigger rate=] :: |source|'s [=attribution source/randomized trigger rate=]. : [=event-level report/reporting origin=] @@ -3141,7 +3178,7 @@ an [=event-level trigger configuration=] |config|, and a non-negative 64-bit int : [=event-level report/report time=] :: |reportTime| : [=event-level report/trigger priority=] - :: |config|'s [=event-level trigger configuration/priority=]. + :: |priority|. : [=event-level report/trigger time=] :: |triggerTime|. : [=event-level report/source identifier=] diff --git a/ts/src/header-validator/source.test.ts b/ts/src/header-validator/source.test.ts index 89edb837ef..f0143924e8 100644 --- a/ts/src/header-validator/source.test.ts +++ b/ts/src/header-validator/source.test.ts @@ -76,11 +76,11 @@ const testCases: TestCase[] = [ name: 'unknown-field', json: `{ "destination": "https://a.test", - "trigger_specs": [] + "x": [] }`, expectedWarnings: [ { - path: ['trigger_specs'], + path: ['x'], msg: 'unknown field', }, ], @@ -1326,7 +1326,7 @@ const testCases: TestCase[] = [ ], }, - // Full Flex + // Flex // TODO: compare returned trigger specs against expected values { @@ -1373,6 +1373,36 @@ const testCases: TestCase[] = [ }, ], }, + { + name: 'top-level-trigger-data-and-trigger-specs', + json: `{ + "destination": "https://a.test", + "trigger_data": [], + "trigger_specs": [] + }`, + parseFullFlex: true, + expectedErrors: [ + { + path: [], + msg: 'mutually exclusive fields: trigger_data, trigger_specs', + }, + ], + }, + { + name: 'top-level-trigger-data-and-trigger-specs-ignored', + json: `{ + "destination": "https://a.test", + "max_event_level_reports": 0, + "trigger_data": [], + "trigger_specs": [] + }`, + expectedWarnings: [ + { + path: ['trigger_specs'], + msg: 'unknown field', + }, + ], + }, { name: 'trigger-data-missing', json: `{ @@ -1391,12 +1421,11 @@ const testCases: TestCase[] = [ name: 'trigger-data-wrong-type', json: `{ "destination": "https://a.test", - "trigger_specs": [{"trigger_data": 1}] + "trigger_data": 1 }`, - parseFullFlex: true, expectedErrors: [ { - path: ['trigger_specs', 0, 'trigger_data'], + path: ['trigger_data'], msg: 'must be a list', }, ], @@ -1405,12 +1434,11 @@ const testCases: TestCase[] = [ name: 'trigger-data-value-wrong-type', json: `{ "destination": "https://a.test", - "trigger_specs": [{"trigger_data": ["1"]}] + "trigger_data": ["1"] }`, - parseFullFlex: true, expectedErrors: [ { - path: ['trigger_specs', 0, 'trigger_data', 0], + path: ['trigger_data', 0], msg: 'must be a number', }, ], @@ -1419,12 +1447,11 @@ const testCases: TestCase[] = [ name: 'trigger-data-value-not-integer', json: `{ "destination": "https://a.test", - "trigger_specs": [{"trigger_data": [1.5]}] + "trigger_data": [1.5] }`, - parseFullFlex: true, expectedErrors: [ { - path: ['trigger_specs', 0, 'trigger_data', 0], + path: ['trigger_data', 0], msg: 'must be an integer', }, ], @@ -1433,12 +1460,11 @@ const testCases: TestCase[] = [ name: 'trigger-data-value-negative', json: `{ "destination": "https://a.test", - "trigger_specs": [{"trigger_data": [-1]}] + "trigger_data": [-1] }`, - parseFullFlex: true, expectedErrors: [ { - path: ['trigger_specs', 0, 'trigger_data', 0], + path: ['trigger_data', 0], msg: 'must be in the range [0, 4294967295]', }, ], @@ -1447,12 +1473,11 @@ const testCases: TestCase[] = [ name: 'trigger-data-value-exceeds-max', json: `{ "destination": "https://a.test", - "trigger_specs": [{"trigger_data": [4294967296]}] + "trigger_data": [4294967296] }`, - parseFullFlex: true, expectedErrors: [ { - path: ['trigger_specs', 0, 'trigger_data', 0], + path: ['trigger_data', 0], msg: 'must be in the range [0, 4294967295]', }, ], @@ -1461,14 +1486,11 @@ const testCases: TestCase[] = [ name: 'trigger-data-duplicated-within', json: `{ "destination": "https://a.test", - "trigger_specs": [ - { "trigger_data": [1, 2, 1] } - ] + "trigger_data": [1, 2, 1] }`, - parseFullFlex: true, expectedErrors: [ { - path: ['trigger_specs', 0, 'trigger_data', 2], + path: ['trigger_data', 2], msg: 'duplicate value 1', }, ], @@ -1491,17 +1513,11 @@ const testCases: TestCase[] = [ ], }, { - name: 'trigger-data-too-many-within', - json: JSON.stringify({ - destination: 'https://a.test', - trigger_specs: [ - { - trigger_data: Array(33) - .fill(0) - .map((_, i) => i), - }, - ], - }), + name: 'trigger-spec-trigger-data-empty', + json: `{ + "destination": "https://a.test", + "trigger_specs": [{"trigger_data": []}] + }`, parseFullFlex: true, expectedErrors: [ { @@ -1749,7 +1765,6 @@ const testCases: TestCase[] = [ "destination": "https://a.test", "trigger_data_matching": 3 }`, - parseFullFlex: true, expectedErrors: [ { path: ['trigger_data_matching'], @@ -1763,7 +1778,6 @@ const testCases: TestCase[] = [ "destination": "https://a.test", "trigger_data_matching": "EXACT" }`, - parseFullFlex: true, expectedErrors: [ { path: ['trigger_data_matching'], @@ -1776,9 +1790,8 @@ const testCases: TestCase[] = [ json: `{ "destination": "https://a.test", "trigger_data_matching": "modulus", - "trigger_specs": [{"trigger_data": [1]}] + "trigger_data": [1] }`, - parseFullFlex: true, expectedErrors: [ { path: ['trigger_data_matching'], @@ -1787,7 +1800,7 @@ const testCases: TestCase[] = [ ], }, { - name: 'trigger-data-matching-modulus-trigger-data-not-contiguous', + name: 'trigger-data-matching-modulus-trigger-data-not-contiguous-across', json: `{ "destination": "https://a.test", "trigger_data_matching": "modulus", @@ -1805,7 +1818,21 @@ const testCases: TestCase[] = [ ], }, { - name: 'trigger-data-matching-modulus-valid', + name: 'trigger-data-matching-modulus-trigger-data-not-contiguous-within', + json: `{ + "destination": "https://a.test", + "trigger_data_matching": "modulus", + "trigger_data": [0, 1, 3] + }`, + expectedErrors: [ + { + path: ['trigger_data_matching'], + msg: 'trigger_data must form a contiguous sequence of integers starting at 0 for modulus', + }, + ], + }, + { + name: 'trigger-data-matching-modulus-valid-across', json: `{ "destination": "https://a.test", "trigger_data_matching": "modulus", @@ -1817,15 +1844,22 @@ const testCases: TestCase[] = [ }`, parseFullFlex: true, }, + { + name: 'trigger-data-matching-modulus-valid-within', + json: `{ + "destination": "https://a.test", + "trigger_data_matching": "modulus", + "trigger_data": [1, 0, 2, 3] + }`, + }, { name: 'no-reports-but-specs', json: `{ "destination": "https://a.test", "max_event_level_reports": 0, - "trigger_specs": [{"trigger_data": [1]}] + "trigger_data": [1] }`, - parseFullFlex: true, expectedWarnings: [ { path: [], @@ -1838,9 +1872,8 @@ const testCases: TestCase[] = [ json: `{ "destination": "https://a.test", "max_event_level_reports": 1, - "trigger_specs": [] + "trigger_data": [] }`, - parseFullFlex: true, expectedWarnings: [ { path: [], diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts index 77f1c43fd6..bae26df862 100644 --- a/ts/src/header-validator/validate-json.ts +++ b/ts/src/header-validator/validate-json.ts @@ -918,9 +918,13 @@ function fullFlexTriggerDatum(ctx: Context, j: Json): Maybe { .filter((n) => isInRange(ctx, n, 0, UINT32_MAX)) } -function triggerDataSet(ctx: Context, j: Json): Maybe> { +function triggerDataSet( + ctx: Context, + j: Json, + allowEmpty: boolean = false +): Maybe> { return set(ctx, j, fullFlexTriggerDatum, { - minLength: 1, + minLength: allowEmpty ? 0 : 1, maxLength: constants.maxTriggerDataPerSource, requireDistinct: true, }) @@ -932,13 +936,17 @@ type TriggerSpecDeps = { maxEventLevelReports: Maybe } +function makeDefaultSummaryBuckets(maxEventLevelReports: number): number[] { + return Array.from({ length: maxEventLevelReports }, (_, i) => i + 1) +} + function triggerSpec( ctx: SourceContext, j: Json, deps: TriggerSpecDeps ): Maybe { - const defaultSummaryBuckets = deps.maxEventLevelReports.map((n) => - Array.from({ length: n }, (_, i) => i + 1) + const defaultSummaryBuckets = deps.maxEventLevelReports.map( + makeDefaultSummaryBuckets ) return struct(ctx, j, { @@ -1001,6 +1009,33 @@ function triggerSpecs( }) } +function triggerSpecsFromTriggerData( + ctx: Context, + j: Json, + deps: TriggerSpecDeps +): Maybe { + return triggerDataSet(ctx, j, /*allowEmpty=*/ true).map((triggerData) => { + if ( + triggerData.size === 0 || + deps.eventReportWindows.value === undefined || + deps.maxEventLevelReports.value === undefined + ) { + return [] + } + + return [ + { + eventReportWindows: deps.eventReportWindows.value, + summaryBuckets: makeDefaultSummaryBuckets( + deps.maxEventLevelReports.value + ), + summaryWindowOperator: SummaryWindowOperator.count, + triggerData: triggerData, + }, + ] + }) +} + function defaultTriggerSpecs( ctx: SourceContext, eventReportWindows: Maybe, @@ -1131,18 +1166,25 @@ function source(ctx: SourceContext, j: Json): Maybe { maxEventLevelReportsVal ) - const triggerSpecsVal = ctx.parseFullFlex - ? field( - 'trigger_specs', - (ctx: SourceContext, j) => - triggerSpecs(ctx, j, { - expiry: expiryVal, - eventReportWindows: eventReportWindowsVal, - maxEventLevelReports: maxEventLevelReportsVal, - }), - defaultTriggerSpecsVal - )(ctx, j) - : defaultTriggerSpecsVal + const triggerSpecsDeps = { + expiry: expiryVal, + eventReportWindows: eventReportWindowsVal, + maxEventLevelReports: maxEventLevelReportsVal, + } + + const triggerSpecsVal = exclusive( + { + trigger_data: (ctx, j) => + triggerSpecsFromTriggerData(ctx, j, triggerSpecsDeps), + ...(ctx.parseFullFlex + ? { + trigger_specs: (ctx: SourceContext, j) => + triggerSpecs(ctx, j, triggerSpecsDeps), + } + : {}), + }, + defaultTriggerSpecsVal + )(ctx, j) return struct(ctx, j, { aggregatableReportWindow: field( @@ -1164,13 +1206,11 @@ function source(ctx: SourceContext, j: Json): Maybe { sourceEventId: field('source_event_id', uint64, 0n), triggerSpecs: () => triggerSpecsVal, - triggerDataMatching: ctx.parseFullFlex - ? field( - 'trigger_data_matching', - (ctx, j) => triggerDataMatching(ctx, j, triggerSpecsVal), - TriggerDataMatching.modulus - ) - : () => some(TriggerDataMatching.modulus), + triggerDataMatching: field( + 'trigger_data_matching', + (ctx, j) => triggerDataMatching(ctx, j, triggerSpecsVal), + TriggerDataMatching.modulus + ), ...commonDebugFields, ...priorityField,