Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec supporting trigger context ID for aggregate reporting #1114

Merged
merged 5 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,8 @@ An attribution trigger is a [=struct=] with the following items:
:: An [=aggregation coordinator=].
: <dfn>aggregatable source registration time configuration</dfn>
:: An [=aggregatable source registration time configuration=].
: <dfn>trigger context ID</dfn>
:: Null or a [=string=].

</dl>

Expand Down Expand Up @@ -925,6 +927,8 @@ An aggregatable report is an [=attribution report=] with the following additiona
:: An [=aggregatable source registration time configuration=].
: <dfn>is null report</dfn> (default false)
:: A [=boolean=].
: <dfn>trigger context ID</dfn>
:: Null or a [=string=].

</dl>

Expand Down Expand Up @@ -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.

<dfn>Max length per trigger context ID</dfn> 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}

<dfn>Max pending sources per source origin</dfn> is a positive integer that
Expand Down Expand Up @@ -2134,6 +2142,8 @@ a [=trigger state=] |triggerState|:
:: «»
: [=attribution trigger/aggregatable source registration time configuration=]
:: "<code>[=aggregatable source registration time configuration/exclude=]</code>"
: [=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.
Expand Down Expand Up @@ -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 "<code>[=aggregatable source registration time configuration/exclude=]</code>", 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|
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -3036,9 +3055,11 @@ To <dfn>obtain an event-level report delivery time</dfn> given an [=attribution
|window|'s [=report window/end=].
1. [=Assert=]: not reached.

To <dfn>obtain an aggregatable report delivery time</dfn> given a [=moment=]
|triggerTime|, perform the following steps. They return a [=moment=].
To <dfn>obtain an aggregatable report delivery time</dfn> 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=].

Expand Down Expand Up @@ -3097,7 +3118,7 @@ required aggregatable budget</dfn> is the total [=aggregatable contribution/valu
To <dfn>obtain an aggregatable report</dfn> 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=]
Expand All @@ -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|.

<h3 id="generating-randomized-null-reports">Generating randomized null reports</h3>

To <dfn>obtain a null report</dfn> 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=]
Expand All @@ -3156,6 +3178,8 @@ To <dfn>obtain a null report</dfn> 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 <dfn>obtain rounded source time</dfn> given a [=moment=] |sourceTime|, return |sourceTime| in seconds
Expand All @@ -3172,13 +3196,17 @@ To <dfn>generate null reports</dfn> 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 "<code>[=aggregatable source registration time configuration/exclude=]</code>":
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.
Expand Down Expand Up @@ -3448,6 +3476,8 @@ To <dfn>serialize an [=aggregatable report=] </dfn> |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 <dfn>serialize an [=attribution report=]</dfn> |report|, run the following steps:
Expand Down
2 changes: 2 additions & 0 deletions ts/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]> = [
Expand Down
56 changes: 56 additions & 0 deletions ts/src/header-validator/trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const testCases: jsontest.TestCase<Trigger>[] = [
aggregatableSourceRegistrationTime:
AggregatableSourceRegistrationTime.include,
aggregationCoordinatorOrigin: null,
triggerContextID: null,
aggregatableTriggerData: [
{
positive: [
Expand Down Expand Up @@ -966,6 +967,61 @@ const testCases: jsontest.TestCase<Trigger>[] = [
],
},

{
name: 'trigger-context-id-wrong-type',
json: `{"trigger_context_id": 1}`,
expectedErrors: [
{
path: ['trigger_context_id'],
msg: 'must be a string',
},
],
},
{
name: 'trigger-context-id-empty',
json: `{"trigger_context_id": ""}`,
expectedErrors: [
{
path: ['trigger_context_id'],
msg: 'cannot be empty',
},
],
},
{
name: 'trigger-context-id-too-long',
json: `{"trigger_context_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`,
expectedErrors: [
{
path: ['trigger_context_id'],
msg: 'exceeds max length per trigger context ID (65 > 64)',
},
],
},
{
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: ['trigger_context_id'],
msg: 'cannot be fully validated without a valid aggregatable_source_registration_time',
},
],
},
{
name: 'trigger-context-id-prohibited-aggregatable-source-registration-time-include',
json: `{"aggregatable_source_registration_time": "include", "trigger_context_id": "123"}`,
expectedErrors: [
{
path: ['trigger_context_id'],
msg: 'is prohibited for aggregatable_source_registration_time include',
},
],
},

// Full Flex

{
Expand Down
107 changes: 77 additions & 30 deletions ts/src/header-validator/validate-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,41 @@ function warnInconsistentAggregatableKeys(ctx: Context, t: Trigger): void {
})
}

function triggerContextID(
ctx: Context,
j: Json,
aggregatableSourceRegTime: Maybe<AggregatableSourceRegistrationTime>
): Maybe<string> {
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[]
Expand All @@ -1260,39 +1295,51 @@ export type Trigger = CommonDebug &
aggregatableValues: Map<string, number>
aggregationCoordinatorOrigin: string | null
eventTriggerData: EventTriggerDatum[]
triggerContextID: string | null
}

function trigger(ctx: Context, j: Json): Maybe<Trigger> {
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, []),
triggerContextID: field(
'trigger_context_id',
(ctx, j) => triggerContextID(ctx, j, aggregatableSourceRegTimeVal),
null
),
...commonDebugFields,
...filterFields,
})
})
.peek((t) => warnInconsistentAggregatableKeys(ctx, t))
}

function validateJSON<T>(
Expand Down