From 4dd4effee0e075af5da3861dbfc1a9effe84d58d Mon Sep 17 00:00:00 2001 From: Nan Lin Date: Wed, 11 Oct 2023 09:47:19 -0400 Subject: [PATCH 1/5] Spec trigger context ID --- index.bs | 39 ++++++++- ts/src/constants.ts | 2 + ts/src/header-validator/trigger.test.ts | 56 ++++++++++++ ts/src/header-validator/validate-json.ts | 107 ++++++++++++++++------- 4 files changed, 170 insertions(+), 34 deletions(-) diff --git a/index.bs b/index.bs index 7538ea9b28..9b8f23e51a 100644 --- a/index.bs +++ b/index.bs @@ -849,6 +849,8 @@ An attribution trigger is a [=struct=] with the following items: :: An [=aggregation coordinator=]. : aggregatable source registration time configuration :: An [=aggregatable source registration time configuration=]. +: trigger context ID +:: Null or a [=string=]. @@ -925,6 +927,8 @@ An aggregatable report is an [=attribution report=] with the following additiona :: An [=aggregatable source registration time configuration=]. : is null report (default false) :: A [=boolean=]. +: trigger context ID +:: Null or a [=string=]. @@ -1172,6 +1176,10 @@ Its value is «[=source type/navigation=] → 8, [=source type/event=] → 2». controls the maximum [=map/size=] of a [=trigger spec map=] for an [=attribution source=]. Its value is 32. +Max length per trigger context ID is a positive integer that controls +the maximum [=string/length=] of an [=attribution trigger=]'s [=attribution trigger/trigger context ID=], +and an [=aggregatable report=]'s [=aggregatable report/trigger context ID=]. Its value is 64. + # Vendor-Specific Values # {#vendor-specific-values} Max pending sources per source origin is a positive integer that @@ -2134,6 +2142,8 @@ a [=trigger state=] |triggerState|: :: «» : [=attribution trigger/aggregatable source registration time configuration=] :: "[=aggregatable source registration time configuration/exclude=]" + : [=attribution trigger/trigger context ID=] + :: null 1. Let |fakeReport| be the result of running [=obtain an event-level report=] with |source|, |fakeTrigger|, and |fakeConfig|. 1. [=Assert=]: |fakeReport| is not null. @@ -2425,6 +2435,13 @@ and a [=moment=] |triggerTime|: 1. If |value|["`aggregatable_source_registration_time`"] is not an [=aggregatable source registration time configuration=], return null. 1. Set |aggregatableSourceRegTimeConfig| to |value|["`aggregatable_source_registration_time`"]. +1. Let |triggerContextID| be null. +1. If |value|["`trigger_context_id`"] [=map/exists=]: + 1. If |value|["`trigger_context_id`"] is not a [=string=], return null. + 1. If |value|["`trigger_context_id`"] is empty or its [=string/length=] is greater than the [=max length per trigger context ID=], + return null. + 1. If |aggregatableSourceRegTimeConfig| is not "[=aggregatable source registration time configuration/exclude=]", return null. + 1. Set |triggerContextID| to |value|["`trigger_context_id`"]. 1. Let |trigger| be a new [=attribution trigger=] with the items: : [=attribution trigger/attribution destination=] :: |destination| @@ -2454,6 +2471,8 @@ and a [=moment=] |triggerTime|: :: |aggregationCoordinator| : [=attribution trigger/aggregatable source registration time configuration=] :: |aggregatableSourceRegTimeConfig| + : [=attribution trigger/trigger context ID=] + :: |triggerContextID| 1. Return |trigger|. Issue: Determine proper charset-handling for the JSON header value. @@ -3036,9 +3055,11 @@ To obtain an event-level report delivery time given an [=attribution |window|'s [=report window/end=]. 1. [=Assert=]: not reached. -To obtain an aggregatable report delivery time given a [=moment=] -|triggerTime|, perform the following steps. They return a [=moment=]. +To obtain an aggregatable report delivery time given an [attribution trigger=] +|trigger|, perform the following steps. They return a [=moment=]. +1. Let |triggerTime| be |trigger|'s [=attribution trigger/trigger time=]. +1. If |trigger|'s [=attribution trigger/trigger context ID=] is not null, return |triggerTime|. 1. Let |r| be a random double between 0 (inclusive) and 1 (exclusive) with uniform probability. 1. Return |triggerTime| + |r| * [=randomized aggregatable report delay=]. @@ -3097,7 +3118,7 @@ required aggregatable budget is the total [=aggregatable contribution/valu To obtain an aggregatable report given an [=attribution source=] |source| and an [=attribution trigger=] |trigger|: -1. Let |reportTime| be the result of running [=obtain an aggregatable report delivery time=] with |trigger|'s [=attribution trigger/trigger time=]. +1. Let |reportTime| be the result of running [=obtain an aggregatable report delivery time=] with |trigger|. 1. Let |report| be a new [=aggregatable report=] struct whose items are: : [=aggregatable report/reporting origin=] @@ -3122,6 +3143,8 @@ an [=attribution trigger=] |trigger|: :: |trigger|'s [=attribution trigger/aggregation coordinator=]. : [=aggregatable report/source registration time configuration=] :: |trigger|'s [=attribution trigger/aggregatable source registration time configuration=]. + : [=aggregatable report/trigger context ID=] + :: |trigger|'s [=attribution trigger/trigger context ID=] 1. Return |report|.

Generating randomized null reports

@@ -3156,6 +3179,8 @@ To obtain a null report given an [=attribution trigger=] |trigger| an :: |trigger|'s [=attribution trigger/aggregatable source registration time configuration=] : [=aggregatable report/is null report=] :: true + : [=aggregatable report/trigger context ID=] + :: |trigger|'s [=attribution trigger/trigger context ID=] 1. Return |report|. To obtain rounded source time given a [=moment=] |sourceTime|, return |sourceTime| in seconds @@ -3172,13 +3197,17 @@ To generate null reports given an [=attribution trigger=] |trigger| a 1. Let |nullReports| be a new [=list/is empty|empty=] [=list=]. 1. If |trigger|'s [=attribution trigger/aggregatable source registration time configuration=] is "[=aggregatable source registration time configuration/exclude=]": + 1. Let |randomizedNullReportRate| be [=randomized null report rate excluding source registration time=]. + 1. If |trigger|'s [=attribution trigger|trigger context ID=] is not null, set + |randomizedNullReportRate| to 1. 1. If |report| is null and the result of [=determining if a randomized null report is generated=] with - [=randomized null report rate excluding source registration time=] is true: + |randomizedNullReportRate| is true: 1. Let |nullReport| be the result of [=obtaining a null report=] with |trigger| and |trigger|'s [=attribution trigger/trigger time=]. 1. [=set/Append=] |nullReport| to the [=aggregatable report cache=]. 1. [=list/Append=] |nullReport| to |nullReports|. 1. Otherwise: + 1. [=Assert=]: |trigger|'s [=attribution trigger/trigger context ID=] is null. 1. Let |maxSourceExpiry| be [=valid source expiry range=][1]. 1. Round |maxSourceExpiry| away from zero to the nearest day (86400 seconds). 1. Let |roundedAttributedSourceTime| be null. @@ -3448,6 +3477,8 @@ To serialize an [=aggregatable report=] |report|, run the following 1. If |report|'s [=aggregatable report/trigger debug key=] is not null, [=map/set=] |data|["`trigger_debug_key`"] to |report|'s [=aggregatable report/trigger debug key=], [=serialize an integer|serialized=]. +1. If |report|'s [=aggregatable report/trigger context ID=] is not null, [=map/set=] + |data|["`context_id`"] to |report|'s [=aggregatable report/trigger context ID=]. 1. Return the [=byte sequence=] resulting from executing [=serialize an infra value to JSON bytes=] on |data|. To serialize an [=attribution report=] |report|, run the following steps: diff --git a/ts/src/constants.ts b/ts/src/constants.ts index c38935c009..868324c078 100644 --- a/ts/src/constants.ts +++ b/ts/src/constants.ts @@ -17,6 +17,8 @@ export const maxAggregationKeysPerSource: number = 20 export const maxLengthPerAggregationKeyIdentifier: number = 25 +export const maxLengthPerTriggerContextID: number = 64 + export const minReportWindow: number = 1 * SECONDS_PER_HOUR export const validSourceExpiryRange: Readonly<[min: number, max: number]> = [ diff --git a/ts/src/header-validator/trigger.test.ts b/ts/src/header-validator/trigger.test.ts index c8e8c8f6ca..45a6b3be69 100644 --- a/ts/src/header-validator/trigger.test.ts +++ b/ts/src/header-validator/trigger.test.ts @@ -62,6 +62,7 @@ const testCases: jsontest.TestCase[] = [ aggregatableSourceRegistrationTime: AggregatableSourceRegistrationTime.include, aggregationCoordinatorOrigin: null, + contextId: null, aggregatableTriggerData: [ { positive: [ @@ -966,6 +967,61 @@ const testCases: jsontest.TestCase[] = [ ], }, + { + name: 'context-id-wrong-type', + json: `{"context_id": 1}`, + expectedErrors: [ + { + path: ['context_id'], + msg: 'must be a string', + }, + ], + }, + { + name: 'context-id-empty', + json: `{"context_id": ""}`, + expectedErrors: [ + { + path: ['context_id'], + msg: 'cannot be empty', + }, + ], + }, + { + name: 'context-id-too-long', + json: `{"context_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`, + expectedErrors: [ + { + path: ['context_id'], + msg: 'exceeds max length per trigger context ID (65 > 64)', + }, + ], + }, + { + name: 'context-id-invalid-aggregatable-source-registration-time', + json: `{"context_id": "a", "aggregatable_source_registration_time": 1}`, + expectedErrors: [ + { + path: ['aggregatable_source_registration_time'], + msg: 'must be a string', + }, + { + path: ['context_id'], + msg: 'cannot be fully validated without a valid aggregatable_source_registration_time', + }, + ], + }, + { + name: 'context-id-prohibited-aggregatable-source-registration-time-include', + json: `{"aggregatable_source_registration_time": "include", "context_id": "123"}`, + expectedErrors: [ + { + path: ['context_id'], + msg: 'is prohibited for aggregatable_source_registration_time include', + }, + ], + }, + // Full Flex { diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts index e3d22ec4de..dac24bc206 100644 --- a/ts/src/header-validator/validate-json.ts +++ b/ts/src/header-validator/validate-json.ts @@ -1252,6 +1252,41 @@ function warnInconsistentAggregatableKeys(ctx: Context, t: Trigger): void { }) } +function triggerContextID( + ctx: Context, + j: Json, + aggregatableSourceRegTime: Maybe +): Maybe { + return string(ctx, j).filter((s) => { + if (s.length == 0) { + ctx.error(`cannot be empty`) + return false + } + if (s.length > constants.maxLengthPerTriggerContextID) { + ctx.error( + `exceeds max length per trigger context ID (${s.length} > ${constants.maxLengthPerTriggerContextID})` + ) + return false + } + if (aggregatableSourceRegTime.value === undefined) { + ctx.error( + `cannot be fully validated without a valid aggregatable_source_registration_time` + ) + return false + } + if ( + aggregatableSourceRegTime.value !== + AggregatableSourceRegistrationTime.exclude + ) { + ctx.error( + `is prohibited for aggregatable_source_registration_time ${aggregatableSourceRegTime.value}` + ) + return false + } + return true + }) +} + export type Trigger = CommonDebug & FilterPair & { aggregatableDedupKeys: AggregatableDedupKey[] @@ -1260,39 +1295,51 @@ export type Trigger = CommonDebug & aggregatableValues: Map aggregationCoordinatorOrigin: string | null eventTriggerData: EventTriggerDatum[] + contextId: string | null } function trigger(ctx: Context, j: Json): Maybe { - return struct(ctx, j, { - aggregatableTriggerData: field( - 'aggregatable_trigger_data', - aggregatableTriggerData, - [] - ), - aggregatableValues: field( - 'aggregatable_values', - aggregatableValues, - new Map() - ), - aggregatableDedupKeys: field( - 'aggregatable_deduplication_keys', - aggregatableDedupKeys, - [] - ), - aggregatableSourceRegistrationTime: field( - 'aggregatable_source_registration_time', - aggregatableSourceRegistrationTime, - AggregatableSourceRegistrationTime.exclude - ), - aggregationCoordinatorOrigin: field( - 'aggregation_coordinator_origin', - suitableOrigin, - null - ), - eventTriggerData: field('event_trigger_data', eventTriggerData, []), - ...commonDebugFields, - ...filterFields, - }).peek((t) => warnInconsistentAggregatableKeys(ctx, t)) + return object(ctx, j) + .map((j) => { + const aggregatableSourceRegTimeVal = field( + 'aggregatable_source_registration_time', + aggregatableSourceRegistrationTime, + AggregatableSourceRegistrationTime.exclude + )(ctx, j) + + return struct(ctx, j, { + aggregatableTriggerData: field( + 'aggregatable_trigger_data', + aggregatableTriggerData, + [] + ), + aggregatableValues: field( + 'aggregatable_values', + aggregatableValues, + new Map() + ), + aggregatableDedupKeys: field( + 'aggregatable_deduplication_keys', + aggregatableDedupKeys, + [] + ), + aggregatableSourceRegistrationTime: () => aggregatableSourceRegTimeVal, + aggregationCoordinatorOrigin: field( + 'aggregation_coordinator_origin', + suitableOrigin, + null + ), + eventTriggerData: field('event_trigger_data', eventTriggerData, []), + contextId: field( + 'context_id', + (ctx, j) => triggerContextID(ctx, j, aggregatableSourceRegTimeVal), + null + ), + ...commonDebugFields, + ...filterFields, + }) + }) + .peek((t) => warnInconsistentAggregatableKeys(ctx, t)) } function validateJSON( From eebba9a6e760cac2ae80e63b4c5533cf196cbdcb Mon Sep 17 00:00:00 2001 From: Nan Lin Date: Mon, 20 Nov 2023 18:04:54 -0500 Subject: [PATCH 2/5] fix name --- index.bs | 4 +-- ts/src/header-validator/trigger.test.ts | 32 ++++++++++++------------ ts/src/header-validator/validate-json.ts | 6 ++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/index.bs b/index.bs index 9b8f23e51a..317f3c0f39 100644 --- a/index.bs +++ b/index.bs @@ -3055,7 +3055,7 @@ To obtain an event-level report delivery time given an [=attribution |window|'s [=report window/end=]. 1. [=Assert=]: not reached. -To obtain an aggregatable report delivery time given an [attribution trigger=] +To obtain an aggregatable report delivery time given an [=attribution trigger=] |trigger|, perform the following steps. They return a [=moment=]. 1. Let |triggerTime| be |trigger|'s [=attribution trigger/trigger time=]. @@ -3478,7 +3478,7 @@ To serialize an [=aggregatable report=] |report|, run the following |data|["`trigger_debug_key`"] to |report|'s [=aggregatable report/trigger debug key=], [=serialize an integer|serialized=]. 1. If |report|'s [=aggregatable report/trigger context ID=] is not null, [=map/set=] - |data|["`context_id`"] to |report|'s [=aggregatable report/trigger context ID=]. + |data|["`trigger_context_id`"] to |report|'s [=aggregatable report/trigger context ID=]. 1. Return the [=byte sequence=] resulting from executing [=serialize an infra value to JSON bytes=] on |data|. To serialize an [=attribution report=] |report|, run the following steps: diff --git a/ts/src/header-validator/trigger.test.ts b/ts/src/header-validator/trigger.test.ts index 45a6b3be69..2ddaa253f9 100644 --- a/ts/src/header-validator/trigger.test.ts +++ b/ts/src/header-validator/trigger.test.ts @@ -62,7 +62,7 @@ const testCases: jsontest.TestCase[] = [ aggregatableSourceRegistrationTime: AggregatableSourceRegistrationTime.include, aggregationCoordinatorOrigin: null, - contextId: null, + triggerContextID: null, aggregatableTriggerData: [ { positive: [ @@ -968,55 +968,55 @@ const testCases: jsontest.TestCase[] = [ }, { - name: 'context-id-wrong-type', - json: `{"context_id": 1}`, + name: 'trigger-context-id-wrong-type', + json: `{"trigger_context_id": 1}`, expectedErrors: [ { - path: ['context_id'], + path: ['trigger_context_id'], msg: 'must be a string', }, ], }, { - name: 'context-id-empty', - json: `{"context_id": ""}`, + name: 'trigger-context-id-empty', + json: `{"trigger_context_id": ""}`, expectedErrors: [ { - path: ['context_id'], + path: ['trigger_context_id'], msg: 'cannot be empty', }, ], }, { - name: 'context-id-too-long', - json: `{"context_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`, + name: 'trigger-context-id-too-long', + json: `{"trigger_context_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`, expectedErrors: [ { - path: ['context_id'], + path: ['trigger_context_id'], msg: 'exceeds max length per trigger context ID (65 > 64)', }, ], }, { - name: 'context-id-invalid-aggregatable-source-registration-time', - json: `{"context_id": "a", "aggregatable_source_registration_time": 1}`, + name: 'trigger-context-id-invalid-aggregatable-source-registration-time', + json: `{"trigger_context_id": "a", "aggregatable_source_registration_time": 1}`, expectedErrors: [ { path: ['aggregatable_source_registration_time'], msg: 'must be a string', }, { - path: ['context_id'], + path: ['trigger_context_id'], msg: 'cannot be fully validated without a valid aggregatable_source_registration_time', }, ], }, { - name: 'context-id-prohibited-aggregatable-source-registration-time-include', - json: `{"aggregatable_source_registration_time": "include", "context_id": "123"}`, + name: 'trigger-context-id-prohibited-aggregatable-source-registration-time-include', + json: `{"aggregatable_source_registration_time": "include", "trigger_context_id": "123"}`, expectedErrors: [ { - path: ['context_id'], + path: ['trigger_context_id'], msg: 'is prohibited for aggregatable_source_registration_time include', }, ], diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts index dac24bc206..83d97550f6 100644 --- a/ts/src/header-validator/validate-json.ts +++ b/ts/src/header-validator/validate-json.ts @@ -1295,7 +1295,7 @@ export type Trigger = CommonDebug & aggregatableValues: Map aggregationCoordinatorOrigin: string | null eventTriggerData: EventTriggerDatum[] - contextId: string | null + triggerContextID: string | null } function trigger(ctx: Context, j: Json): Maybe { @@ -1330,8 +1330,8 @@ function trigger(ctx: Context, j: Json): Maybe { null ), eventTriggerData: field('event_trigger_data', eventTriggerData, []), - contextId: field( - 'context_id', + triggerContextID: field( + 'trigger_context_id', (ctx, j) => triggerContextID(ctx, j, aggregatableSourceRegTimeVal), null ), From 864293249d462aa27f56f2531ff13c580cb9459d Mon Sep 17 00:00:00 2001 From: Nan Lin Date: Mon, 20 Nov 2023 18:07:15 -0500 Subject: [PATCH 3/5] fix --- index.bs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 317f3c0f39..28cc28df85 100644 --- a/index.bs +++ b/index.bs @@ -3151,8 +3151,7 @@ an [=attribution trigger=] |trigger|: To obtain a null report given an [=attribution trigger=] |trigger| and a [=moment=] |sourceTime|: -1. Let |reportTime| be the result of running [=obtain an aggregatable report delivery time=] with |trigger|'s - [=attribution trigger/trigger time=]. +1. Let |reportTime| be the result of running [=obtain an aggregatable report delivery time=] with |trigger|. 1. Let |report| be a new [=aggregatable report=] struct whose items are: : [=aggregatable report/reporting origin=] From 291e0bcdfec74276d74b548573919a7ffc412b30 Mon Sep 17 00:00:00 2001 From: Nan Lin <80365263+linnan-github@users.noreply.github.com> Date: Tue, 21 Nov 2023 08:48:28 -0500 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Andrew Paseltiner --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 28cc28df85..9079f9d91f 100644 --- a/index.bs +++ b/index.bs @@ -2438,7 +2438,7 @@ and a [=moment=] |triggerTime|: 1. Let |triggerContextID| be null. 1. If |value|["`trigger_context_id`"] [=map/exists=]: 1. If |value|["`trigger_context_id`"] is not a [=string=], return null. - 1. If |value|["`trigger_context_id`"] is empty or its [=string/length=] is greater than the [=max length per trigger context ID=], + 1. If |value|["`trigger_context_id`"]'s [=string/length=] is 0 or is greater than the [=max length per trigger context ID=], return null. 1. If |aggregatableSourceRegTimeConfig| is not "[=aggregatable source registration time configuration/exclude=]", return null. 1. Set |triggerContextID| to |value|["`trigger_context_id`"]. @@ -3197,7 +3197,7 @@ To generate null reports given an [=attribution trigger=] |trigger| a 1. Let |nullReports| be a new [=list/is empty|empty=] [=list=]. 1. If |trigger|'s [=attribution trigger/aggregatable source registration time configuration=] is "[=aggregatable source registration time configuration/exclude=]": 1. Let |randomizedNullReportRate| be [=randomized null report rate excluding source registration time=]. - 1. If |trigger|'s [=attribution trigger|trigger context ID=] is not null, set + 1. If |trigger|'s [=attribution trigger/trigger context ID=] is not null, set |randomizedNullReportRate| to 1. 1. If |report| is null and the result of [=determining if a randomized null report is generated=] with |randomizedNullReportRate| is true: From 909ebee92ea69d6e13d49052cc1d15817516f161 Mon Sep 17 00:00:00 2001 From: Nan Lin <80365263+linnan-github@users.noreply.github.com> Date: Tue, 21 Nov 2023 08:49:05 -0500 Subject: [PATCH 5/5] Update index.bs --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 9079f9d91f..d1b8c6fdf7 100644 --- a/index.bs +++ b/index.bs @@ -1177,8 +1177,8 @@ controls the maximum [=map/size=] of a [=trigger spec map=] for an [=attribution source=]. Its value is 32. Max length per trigger context ID is a positive integer that controls -the maximum [=string/length=] of an [=attribution trigger=]'s [=attribution trigger/trigger context ID=], -and an [=aggregatable report=]'s [=aggregatable report/trigger context ID=]. Its value is 64. +the maximum [=string/length=] of an [=attribution trigger=]'s [=attribution trigger/trigger context ID=]. +Its value is 64. # Vendor-Specific Values # {#vendor-specific-values}