diff --git a/index.bs b/index.bs
index 7538ea9b28..d1b8c6fdf7 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=].
+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`"]'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`"].
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,14 +3143,15 @@ 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|.
[=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 +3476,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|["`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/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..2ddaa253f9 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