Skip to content

Commit

Permalink
Add trigger_data_matching field to Flexible Event explainer
Browse files Browse the repository at this point in the history
  • Loading branch information
apasel422 committed Oct 3, 2023
1 parent e1dc946 commit 066ed0c
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 12 deletions.
69 changes: 68 additions & 1 deletion flexible_event_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ _Note: This document describes possible new functionality in the Attribution Rep
- [Goals](#goals)
- [Phase 2: Full Flexible Event-Level](#phase-2-full-flexible-event-level)
- [API changes](#api-changes)
- [Trigger-data modulus matching example](#trigger-data-modulus-matching-example)
- [Configurations that are equivalent to the current version](#configurations-that-are-equivalent-to-the-current-version)
- [Equivalent event sources](#equivalent-event-sources)
- [Equivalent navigation sources](#equivalent-navigation-sources)
Expand Down Expand Up @@ -101,6 +102,21 @@ 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 source's trigger specs must contain trigger_data forming
// a contiguous sequence of integers starting at 0: the 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.
"trigger_data_matching": <one of "exact" or "modulus">,

// See description in phase 1.
"max_event_level_reports": <int>,

Expand All @@ -114,7 +130,7 @@ In addition to the parameters that were added in Phase 1, we will add one additi

This configuration fully specifies the output space of the event-level reports, per source registration. For every trigger spec, we fully specify:
* A set of matching criteria:
* Which specific trigger data this spec applies to. This source is eligible to be matched only with triggers that have one of the specified `trigger_data` values in the `trigger_specs`. In other words, if the trigger would have matched this source but its `trigger_data` is not one of the values in the source's configuration, the trigger is ignored.
* Which specific trigger data this spec applies to. This source is eligible to be matched only with triggers that have one of the specified `trigger_data` values in the `trigger_specs` according to the `trigger_data_matching` field. In other words, if the trigger would have matched this source but its `trigger_data` is not one of the values in the source's configuration, the trigger is ignored.
* When a specific trigger matches this spec (via `event_report_windows`).
Note that the trigger could still be matched with a source for aggregatable reports despite failing the above two match criteria.
* A specific algorithm for summarizing and bucketizing all the triggers within an attribution window. This allows triggers to specify a `value` parameter that gets summed up for a particular spec, but reported as a bucketized value
Expand Down Expand Up @@ -153,6 +169,50 @@ When the `event_report_window` for a spec completes, we will map its summary val
}
```

### Trigger-data modulus matching example

Given a source with the following registration:

```jsonc
{
"trigger_data_matching": "modulus",
"trigger_specs": [
// Spec A
{
"trigger_data": [0, 3, 5],
...
},
// Spec B
{
"trigger_data": [1, 2],
...
},
// Spec C
{
"trigger_data": [4],
...
},
]
}
```

The trigger-data cardinality is 6, so all triggers' `trigger_data` will be taken
modulus 6 before determining the matching `trigger_spec`:

- `{"trigger_data": "0"}` will match Spec A because `0 % 6 = 0`
- `{"trigger_data": "1"}` will match Spec B because `1 % 6 = 1`
- `{"trigger_data": "2"}` will match Spec B because `2 % 6 = 2`
- `{"trigger_data": "3"}` will match Spec A because `3 % 6 = 3`
- `{"trigger_data": "4"}` will match Spec C because `4 % 6 = 4`
- `{"trigger_data": "5"}` will match Spec A because `5 % 6 = 5`
- `{"trigger_data": "6"}` will match Spec A because `6 % 6 = 0`
- `{"trigger_data": "7"}` will match Spec B because `7 % 6 = 1`
- `{"trigger_data": "8"}` will match Spec B because `8 % 6 = 2`
- `{"trigger_data": "9"}` will match Spec A because `9 % 6 = 3`
- `{"trigger_data": "10"}` will match Spec C because `10 % 6 = 4`
- `{"trigger_data": "11"}` will match Spec A because `11 % 6 = 5`
- ...

## Configurations that are equivalent to the current version

The following are equivalent configurations for the API's current event and navigation sources, respectively. Especially for navigation sources, this illustrates why the noise levels are so high relative to event sources to maintain the same epsilon values: navigation sources have a much larger output space.
Expand All @@ -165,6 +225,7 @@ It is possible that there are multiple configurations that are equivalent, given
// Note: most of the fields here are not required to be explicitly listed.
// Here we list them explicitly just for clarity.
{
"trigger_data_matching": "modulus",
"trigger_specs": [{
"trigger_data": [0, 1],
"event_report_windows": {
Expand All @@ -185,6 +246,7 @@ It is possible that there are multiple configurations that are equivalent, given
// Note: most of the fields here are not required to be explicitly listed.
// Here we list them explicitly just for clarity.
{
"trigger_data_matching": "modulus",
"trigger_specs": [{
"trigger_data": [0, 1, 2, 3, 4, 5, 6, 7],
"event_report_windows": {
Expand All @@ -211,6 +273,7 @@ This example configuration supports a developer who wants to optimize for value

```jsonc
{
"trigger_data_matching": "exact",
"trigger_specs": [{
"trigger_data": [0],
"event_report_windows": {
Expand Down Expand Up @@ -269,6 +332,7 @@ This example shows how a developer can configure a source to get a count of trig

```jsonc
{
"trigger_data_matching": "exact",
"trigger_specs": [{
"trigger_data": [0],
"event_report_windows": {
Expand Down Expand Up @@ -312,6 +376,7 @@ This example configuration supports a developer who wants to learn whether at le

```jsonc
{
"trigger_data_matching": "exact",
"trigger_specs": [{
"trigger_data": [0],
"event_report_windows": {
Expand All @@ -331,6 +396,7 @@ Note that the `trigger_specs` registration can differ from source to source. Thi

```jsonc
{
"trigger_data_matching": "exact",
"trigger_specs": [{
"trigger_data": [0, 1, 2, 3],
"event_report_windows": {
Expand All @@ -343,6 +409,7 @@ Note that the `trigger_specs` registration can differ from source to source. Thi

```jsonc
{
"trigger_data_matching": "exact",
"trigger_specs": [{
"trigger_data": [4, 5, 6, 7],
"event_report_windows": {
Expand Down
82 changes: 81 additions & 1 deletion ts/src/header-validator/source.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { SourceType } from '../source-type'
import * as vsv from '../vendor-specific-values'
import { Maybe } from './maybe'
import { Source, SummaryWindowOperator, validateSource } from './validate-json'
import {
Source,
SummaryWindowOperator,
TriggerDataMatching,
validateSource,
} from './validate-json'
import * as jsontest from './validate-json.test'

type TestCase = jsontest.TestCase<Source> & {
Expand Down Expand Up @@ -60,6 +65,7 @@ const testCases: TestCase[] = [
triggerData: new Set([0, 1, 2, 3, 4, 5, 6, 7]),
},
],
triggerDataMatching: TriggerDataMatching.modulus,
}),
},

Expand Down Expand Up @@ -1518,6 +1524,80 @@ const testCases: TestCase[] = [
},
],
},
{
name: 'trigger-data-matching-wrong-type',
json: `{
"destination": "https://a.test",
"trigger_data_matching": 3
}`,
parseFullFlex: true,
expectedErrors: [
{
path: ['trigger_data_matching'],
msg: 'must be a string',
},
],
},
{
name: 'trigger-data-matching-wrong-value',
json: `{
"destination": "https://a.test",
"trigger_data_matching": "EXACT"
}`,
parseFullFlex: true,
expectedErrors: [
{
path: ['trigger_data_matching'],
msg: 'must be one of the following (case-sensitive): exact, modulus',
},
],
},
{
name: 'trigger-data-matching-modulus-trigger-data-start-not-0',
json: `{
"destination": "https://a.test",
"trigger_data_matching": "modulus",
"trigger_specs": [{"trigger_data": [1]}]
}`,
parseFullFlex: true,
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-trigger-data-not-contiguous',
json: `{
"destination": "https://a.test",
"trigger_data_matching": "modulus",
"trigger_specs": [
{"trigger_data": [0, 1]},
{"trigger_data": [3]}
]
}`,
parseFullFlex: true,
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',
json: `{
"destination": "https://a.test",
"trigger_data_matching": "modulus",
"trigger_specs": [
{"trigger_data": [1, 0]},
{"trigger_data": [3]},
{"trigger_data": [2]}
]
}`,
parseFullFlex: true,
},
]

testCases.forEach((tc) =>
Expand Down
65 changes: 55 additions & 10 deletions ts/src/header-validator/validate-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,41 @@ function defaultTriggerSpecs(
)
}

export enum TriggerDataMatching {
exact = 'exact',
modulus = 'modulus',
}

function triggerDataMatching(
ctx: Context,
j: Json,
specs: Maybe<TriggerSpec[]>
): Maybe<TriggerDataMatching> {
return enumerated(ctx, j, TriggerDataMatching).filter((v) => {
if (v !== TriggerDataMatching.modulus) {
return true
}

if (specs.value === undefined) {
ctx.error('cannot be fully validated without valid trigger specs')
return false
}

const triggerData: number[] = specs.value
.flatMap((spec) => Array.from(spec.triggerData))
.sort()

if (triggerData.some((triggerDatum, i) => triggerDatum !== i)) {
ctx.error(
'trigger_data must form a contiguous sequence of integers starting at 0 for modulus'
)
return false
}

return true
})
}

export type Source = CommonDebug &
Priority & {
aggregatableReportWindow: number
Expand All @@ -965,6 +1000,7 @@ export type Source = CommonDebug &
sourceEventId: bigint

triggerSpecs: TriggerSpec[]
triggerDataMatching: TriggerDataMatching
}

function source(ctx: Context, j: Json): Maybe<Source> {
Expand Down Expand Up @@ -997,6 +1033,19 @@ function source(ctx: Context, j: Json): Maybe<Source> {
maxEventLevelReportsVal
)

const triggerSpecsVal = ctx.parseFullFlex
? field(
'trigger_specs',
(ctx, j) =>
triggerSpecs(ctx, j, {
expiry: expiryVal,
eventReportWindows: eventReportWindowsVal,
maxEventLevelReports: maxEventLevelReportsVal,
}),
defaultTriggerSpecsVal
)(ctx, j)
: defaultTriggerSpecsVal

return struct(ctx, j, {
aggregatableReportWindow: field(
'aggregatable_report_window',
Expand All @@ -1010,19 +1059,15 @@ function source(ctx: Context, j: Json): Maybe<Source> {
filterData: field('filter_data', filterData, new Map()),
maxEventLevelReports: () => maxEventLevelReportsVal,
sourceEventId: field('source_event_id', uint64, 0n),
triggerSpecs: () => triggerSpecsVal,

triggerSpecs: ctx.parseFullFlex
triggerDataMatching: ctx.parseFullFlex
? field(
'trigger_specs',
(ctx, j) =>
triggerSpecs(ctx, j, {
expiry: expiryVal,
eventReportWindows: eventReportWindowsVal,
maxEventLevelReports: maxEventLevelReportsVal,
}),
defaultTriggerSpecsVal
'trigger_data_matching',
(ctx, j) => triggerDataMatching(ctx, j, triggerSpecsVal),
TriggerDataMatching.modulus
)
: () => defaultTriggerSpecsVal,
: () => some(TriggerDataMatching.modulus),

...commonDebugFields,
...priorityField,
Expand Down

0 comments on commit 066ed0c

Please sign in to comment.