diff --git a/ts/src/flexible-event/main.ts b/ts/src/flexible-event/main.ts index 6acf9f2a90..8388f97292 100644 --- a/ts/src/flexible-event/main.ts +++ b/ts/src/flexible-event/main.ts @@ -117,16 +117,18 @@ config.peek((config) => { console.log(`Number of possible different output states: ${out.numStates}`) console.log(`Information gain: ${out.infoGain.toFixed(2)} bits`) - console.log(`Flip percent: ${(100 * out.flipProb).toFixed(5)}%`) + console.log(`Randomized trigger rate: ${out.flipProb.toFixed(7)}`) if (out.excessive) { const e = out.excessive console.log( `WARNING: info gain > ${infoGainMax.toFixed(2)} for ${ options.source_type - } sources. Would require a ${(100 * e.newFlipProb).toFixed( - 5 - )}% flip chance (effective epsilon = ${e.newEps.toFixed(3)}) to resolve.` + } sources. Would require a ${e.newFlipProb.toFixed( + 7 + )} randomized trigger rate (effective epsilon = ${e.newEps.toFixed( + 3 + )}) to resolve.` ) } }) diff --git a/ts/src/header-validator/context.ts b/ts/src/header-validator/context.ts index e364886b17..0316e6bc3d 100644 --- a/ts/src/header-validator/context.ts +++ b/ts/src/header-validator/context.ts @@ -8,11 +8,16 @@ export type Issue = { export type ValidationResult = { errors: Issue[] warnings: Issue[] + notes: Issue[] } export class Context { private readonly path: PathComponent[] = [] - private readonly result: ValidationResult = { errors: [], warnings: [] } + private readonly result: ValidationResult = { + errors: [], + warnings: [], + notes: [], + } scope(c: PathComponent, f: () => T): T { this.path.push(c) @@ -33,6 +38,10 @@ export class Context { this.result.warnings.push(this.issue(msg)) } + note(msg: string): void { + this.result.notes.push(this.issue(msg)) + } + finish(topLevelError?: string): ValidationResult { if (typeof topLevelError !== 'undefined') { this.result.errors.push({ msg: topLevelError }) diff --git a/ts/src/header-validator/index.html b/ts/src/header-validator/index.html index e5110ff3bb..7266f78f90 100644 --- a/ts/src/header-validator/index.html +++ b/ts/src/header-validator/index.html @@ -85,6 +85,8 @@

Attribution Reporting Header Validation

Warnings:

+

Notes: +

diff --git a/ts/src/header-validator/index.ts b/ts/src/header-validator/index.ts index 488ea4b64b..14aedf09fd 100644 --- a/ts/src/header-validator/index.ts +++ b/ts/src/header-validator/index.ts @@ -13,6 +13,7 @@ const sourceTypeRadios = form.elements.namedItem( )! as RadioNodeList const errorList = document.querySelector('#errors')! const warningList = document.querySelector('#warnings')! +const noteList = document.querySelector('#notes')! const successDiv = document.querySelector('#success')! const sourceTypeFieldset = document.querySelector( '#source-type' @@ -69,7 +70,8 @@ function validate(): void { input.value, vsv.Chromium, sourceType(), - flexCheckbox.checked + flexCheckbox.checked, + /*noteInfoGain=*/ true )[0] break case 'trigger': @@ -103,6 +105,7 @@ function validate(): void { errorList.replaceChildren(...result.errors.map(makeLi)) warningList.replaceChildren(...result.warnings.map(makeLi)) + noteList.replaceChildren(...result.notes.map(makeLi)) } form.addEventListener('input', validate) diff --git a/ts/src/header-validator/source.test.ts b/ts/src/header-validator/source.test.ts index 88044160bd..89edb837ef 100644 --- a/ts/src/header-validator/source.test.ts +++ b/ts/src/header-validator/source.test.ts @@ -11,6 +11,7 @@ import * as jsontest from './validate-json.test' type TestCase = jsontest.TestCase & { sourceType?: SourceType + noteInfoGain?: boolean } const testCases: TestCase[] = [ @@ -1223,6 +1224,7 @@ const testCases: TestCase[] = [ name: 'channel-capacity-default-event', json: `{"destination": "https://a.test"}`, sourceType: SourceType.event, + noteInfoGain: true, vsv: { maxEventLevelChannelCapacityPerSource: { [SourceType.event]: 0, @@ -1233,7 +1235,17 @@ const testCases: TestCase[] = [ expectedErrors: [ { path: [], - msg: 'exceeds max event-level channel capacity per event source (1.58 > 0.00)', + msg: 'information gain: 1.58 exceeds max event-level channel capacity per event source (0.00)', + }, + ], + expectedNotes: [ + { + path: [], + msg: 'number of possible output states: 3', + }, + { + path: [], + msg: 'randomized trigger rate: 0.0000025', }, ], }, @@ -1241,6 +1253,7 @@ const testCases: TestCase[] = [ name: 'channel-capacity-default-navigation', json: `{"destination": "https://a.test"}`, sourceType: SourceType.navigation, + noteInfoGain: true, vsv: { maxEventLevelChannelCapacityPerSource: { [SourceType.event]: Infinity, @@ -1251,7 +1264,17 @@ const testCases: TestCase[] = [ expectedErrors: [ { path: [], - msg: 'exceeds max event-level channel capacity per navigation source (11.46 > 0.00)', + msg: 'information gain: 11.46 exceeds max event-level channel capacity per navigation source (0.00)', + }, + ], + expectedNotes: [ + { + path: [], + msg: 'number of possible output states: 2925', + }, + { + path: [], + msg: 'randomized trigger rate: 0.0024263', }, ], }, @@ -1833,7 +1856,8 @@ testCases.forEach((tc) => tc.json, { ...vsv.Chromium, ...tc.vsv }, tc.sourceType ?? SourceType.navigation, - tc.parseFullFlex ?? false + tc.parseFullFlex ?? false, + tc.noteInfoGain ?? false ) ) ) diff --git a/ts/src/header-validator/util.test.ts b/ts/src/header-validator/util.test.ts index ee1edeba12..3b8d29aba1 100644 --- a/ts/src/header-validator/util.test.ts +++ b/ts/src/header-validator/util.test.ts @@ -5,6 +5,7 @@ import * as context from './context' export type TestCase = { expectedWarnings?: context.Issue[] expectedErrors?: context.Issue[] + expectedNotes?: context.Issue[] } export function run( @@ -17,6 +18,7 @@ export function run( assert.deepEqual(result, { errors: tc.expectedErrors ?? [], warnings: tc.expectedWarnings ?? [], + notes: tc.expectedNotes ?? [], }) }) } diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts index 26c7776408..77f1c43fd6 100644 --- a/ts/src/header-validator/validate-json.ts +++ b/ts/src/header-validator/validate-json.ts @@ -30,7 +30,8 @@ class SourceContext extends RegistrationContext { constructor( vsv: Readonly, parseFullFlex: boolean, - readonly sourceType: SourceType + readonly sourceType: SourceType, + readonly noteInfoGain: boolean ) { super(vsv, parseFullFlex) } @@ -832,18 +833,27 @@ function channelCapacity(ctx: SourceContext, s: Source): void { perTriggerDataConfigs ) - const { infoGain } = config.computeConfigData( + const out = config.computeConfigData( s.eventLevelEpsilon, ctx.vsv.maxEventLevelChannelCapacityPerSource[ctx.sourceType] ) const max = ctx.vsv.maxEventLevelChannelCapacityPerSource[ctx.sourceType] - if (infoGain > max) { + const infoGainMsg = `information gain: ${out.infoGain.toFixed(2)}` + + if (out.infoGain > max) { ctx.error( - `exceeds max event-level channel capacity per ${ + `${infoGainMsg} exceeds max event-level channel capacity per ${ ctx.sourceType - } source (${infoGain.toFixed(2)} > ${max.toFixed(2)})` + } source (${max.toFixed(2)})` ) + } else if (ctx.noteInfoGain) { + ctx.note(infoGainMsg) + } + + if (ctx.noteInfoGain) { + ctx.note(`number of possible output states: ${out.numStates}`) + ctx.note(`randomized trigger rate: ${out.flipProb.toFixed(7)}`) } } @@ -1421,10 +1431,11 @@ export function validateSource( json: string, vsv: Readonly, sourceType: SourceType, - parseFullFlex: boolean = false + parseFullFlex: boolean = false, + noteInfoGain: boolean = false ): [ValidationResult, Maybe] { return validateJSON( - new SourceContext(vsv, parseFullFlex, sourceType), + new SourceContext(vsv, parseFullFlex, sourceType, noteInfoGain), json, source )