From ed92d094109a9b4dc412896d6a7db776b87f8681 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner Date: Tue, 30 Sep 2025 09:42:23 -0400 Subject: [PATCH 1/3] Add e2e test for Clear-Site-Data integration --- impl/e2e-tests/clear-site-data.json | 151 ++++++++++++++++++++++++++++ impl/src/e2e.test.ts | 24 ++++- 2 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 impl/e2e-tests/clear-site-data.json diff --git a/impl/e2e-tests/clear-site-data.json b/impl/e2e-tests/clear-site-data.json new file mode 100644 index 0000000..d48f5ed --- /dev/null +++ b/impl/e2e-tests/clear-site-data.json @@ -0,0 +1,151 @@ +{ + "$comment": "use different advertiser sites to avoid depending on budgeting", + "events": [ + { + "seconds": 1, + "site": "a.example", + "event": "saveImpression", + "options": { "histogramIndex": 0 } + }, + { + "seconds": 2, + "site": "b.example", + "event": "saveImpression", + "options": { + "histogramIndex": 1, + "conversionSites": [ + "advertiser-1.example", + "advertiser-2.example", + "advertiser-3.example" + ] + } + }, + { + "seconds": 3, + "site": "c.example", + "intermediarySite": "a.example", + "event": "saveImpression", + "options": { "histogramIndex": 2 } + }, + { + "seconds": 4, + "site": "advertiser-1.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [2, 2, 2] + }, + { + "seconds": 5, + "site": "b.example", + "event": "clearImpressionsForConversionSite", + "$comment": "should remove no impressions" + }, + { + "seconds": 6, + "site": "advertiser-2.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [2, 2, 2] + }, + { + "seconds": 7, + "site": "a.example", + "event": "clearImpressionsForConversionSite", + "$comment": "should remove only the impression with histogramIndex 2" + }, + { + "seconds": 8, + "site": "advertiser-3.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [3, 3, 0] + }, + { + "seconds": 9, + "site": "advertiser-1.example", + "event": "clearImpressionsForConversionSite", + "$comment": "should remove no impressions, but remove this site from the conversionSites of the impression with histogramIndex 1" + }, + { + "seconds": 10, + "site": "advertiser-1.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [6, 0, 0] + }, + { + "seconds": 11, + "site": "advertiser-2.example", + "event": "clearImpressionsForConversionSite", + "$comment": "should remove no impressions, but remove this site from the conversionSites of the impression with histogramIndex 1" + }, + { + "seconds": 12, + "site": "advertiser-2.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [6, 0, 0] + }, + { + "seconds": 13, + "site": "advertiser-3.example", + "event": "clearImpressionsForConversionSite", + "$comment": "should remove the impression with histogramIndex 1, as its conversion sites set should become empty" + }, + { + "seconds": 14, + "site": "advertiser-3.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [6, 0, 0] + } + ] +} diff --git a/impl/src/e2e.test.ts b/impl/src/e2e.test.ts index 49e4350..3838e17 100644 --- a/impl/src/e2e.test.ts +++ b/impl/src/e2e.test.ts @@ -19,11 +19,10 @@ interface TestCase { events: Event[]; } -type Event = { - seconds: number; - site: string; - intermediarySite?: string | undefined; -} & (SaveImpression | MeasureConversion); +type Event = + | SaveImpression + | MeasureConversion + | ClearImpressionsForConversionSite; type ExpectedError = | "RangeError" @@ -35,16 +34,28 @@ type ExpectedError = interface SaveImpression { event: "saveImpression"; + seconds: number; + site: string; + intermediarySite?: string | undefined; options: AttributionImpressionOptions; expectedError?: ExpectedError; } interface MeasureConversion { event: "measureConversion"; + seconds: number; + site: string; + intermediarySite?: string | undefined; options: AttributionConversionOptions; expected: number[] | ExpectedError; } +interface ClearImpressionsForConversionSite { + event: "clearImpressionsForConversionSite"; + seconds: number; + site: string; +} + function assertThrows( call: () => unknown, expectedError: ExpectedError, @@ -137,6 +148,9 @@ function runTest( break; } + case "clearImpressionsForConversionSite": + backend.clearImpressionsForConversionSite(event.site); + break; } } } From 919b2673c5eac4a7923e65abe06aeebce3cf21f3 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner Date: Tue, 30 Sep 2025 10:38:56 -0400 Subject: [PATCH 2/3] Remove scheme check and add impression site check to Clear-Site-Data --- api.bs | 14 ++++++----- impl/e2e-tests/clear-site-data.json | 39 ++++++++++++++++++++++------- impl/src/backend.ts | 5 +++- impl/src/e2e.test.ts | 13 ++++------ 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/api.bs b/api.bs index 3e8b23f..06f0ea7 100644 --- a/api.bs +++ b/api.bs @@ -984,24 +984,26 @@ defined in [[CLEAR-SITE-DATA#header]]. When the [[CLEAR-SITE-DATA#clear-response|clear site data for response]] algorithm is invoked, if the list of types [=set/contains=] \``"impressions"`\`, -the [=clear impressions for a conversion site=] is invoked, +the [=clear impressions for a site=] is invoked, passing the origin.
-To clear impressions for a conversion site, +To clear impressions for a site, given an [=origin=] |origin|, run these steps: -1. If |origin| is not a [=tuple origin=] with a [=scheme=] of `https`, - return. +1. If |origin| is not a [=tuple origin=], return. 1. Let |site| be the value returned by invoking [=registrable domain|obtain a registrable domain=], - passing the [=host=] part of the [=tuple origin|origin tuple=]. + passing |origin|'s [=host=] part. 1. [=set/iterate|For each=] [=impression=] |impression| of the [=impression store=]: + 1. If |impression|'s [=impression/Intermediary Site=] is `undefined` and + its [=impression/Impression Site=] is equal to |site|, + [=set/remove=] |impression| from the [=impression store=] and [=iteration/continue=]. 1. If |impression| has an [=impression/Intermediary Site=] equal to |site|, [=set/remove=] |impression| from the [=impression store=] and [=iteration/continue=]. @@ -2873,7 +2875,7 @@ at the time they are saved. When clearing site data at the request of a [=site=], through the use of the [:Clear-Site-Data:] header, -a [=user agent=] only [=clear impressions for a conversion site|removes impressions=], +a [=user agent=] only [=clear impressions for a site|removes impressions=], without altering either the [=privacy budget store=] or the [=epoch start store=] for affected [=sites=]. diff --git a/impl/e2e-tests/clear-site-data.json b/impl/e2e-tests/clear-site-data.json index d48f5ed..0e3cdfe 100644 --- a/impl/e2e-tests/clear-site-data.json +++ b/impl/e2e-tests/clear-site-data.json @@ -23,7 +23,7 @@ { "seconds": 3, "site": "c.example", - "intermediarySite": "a.example", + "intermediarySite": "d.example", "event": "saveImpression", "options": { "histogramIndex": 2 } }, @@ -44,9 +44,9 @@ }, { "seconds": 5, - "site": "b.example", - "event": "clearImpressionsForConversionSite", - "$comment": "should remove no impressions" + "site": "c.example", + "event": "clearImpressionsForSite", + "$comment": "should remove no impressions, as although there is an impression site for c.example, it has a different intermediary site" }, { "seconds": 6, @@ -65,8 +65,8 @@ }, { "seconds": 7, - "site": "a.example", - "event": "clearImpressionsForConversionSite", + "site": "d.example", + "event": "clearImpressionsForSite", "$comment": "should remove only the impression with histogramIndex 2" }, { @@ -87,7 +87,7 @@ { "seconds": 9, "site": "advertiser-1.example", - "event": "clearImpressionsForConversionSite", + "event": "clearImpressionsForSite", "$comment": "should remove no impressions, but remove this site from the conversionSites of the impression with histogramIndex 1" }, { @@ -108,7 +108,7 @@ { "seconds": 11, "site": "advertiser-2.example", - "event": "clearImpressionsForConversionSite", + "event": "clearImpressionsForSite", "$comment": "should remove no impressions, but remove this site from the conversionSites of the impression with histogramIndex 1" }, { @@ -129,7 +129,7 @@ { "seconds": 13, "site": "advertiser-3.example", - "event": "clearImpressionsForConversionSite", + "event": "clearImpressionsForSite", "$comment": "should remove the impression with histogramIndex 1, as its conversion sites set should become empty" }, { @@ -146,6 +146,27 @@ "credit": [1, 1, 1] }, "expected": [6, 0, 0] + }, + { + "seconds": 15, + "site": "a.example", + "event": "clearImpressionsForSite", + "$comment": "should remove the impression with histogramIndex 0" + }, + { + "seconds": 16, + "site": "advertiser-4.example", + "event": "measureConversion", + "$comment": "try to select 3 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 3, + "value": 6, + "maxValue": 6, + "credit": [1, 1, 1] + }, + "expected": [0, 0, 0] } ] } diff --git a/impl/src/backend.ts b/impl/src/backend.ts index ebb95c2..a491c54 100644 --- a/impl/src/backend.ts +++ b/impl/src/backend.ts @@ -601,8 +601,11 @@ export class Backend { return startEpoch; } - clearImpressionsForConversionSite(site: string): void { + clearImpressionsForSite(site: string): void { function shouldRemoveImpression(i: Impression): boolean { + if (i.intermediarySite === undefined && i.impressionSite === site) { + return true; + } if (i.intermediarySite === site) { return true; } diff --git a/impl/src/e2e.test.ts b/impl/src/e2e.test.ts index 3838e17..c727c9a 100644 --- a/impl/src/e2e.test.ts +++ b/impl/src/e2e.test.ts @@ -19,10 +19,7 @@ interface TestCase { events: Event[]; } -type Event = - | SaveImpression - | MeasureConversion - | ClearImpressionsForConversionSite; +type Event = SaveImpression | MeasureConversion | ClearImpressionsForSite; type ExpectedError = | "RangeError" @@ -50,8 +47,8 @@ interface MeasureConversion { expected: number[] | ExpectedError; } -interface ClearImpressionsForConversionSite { - event: "clearImpressionsForConversionSite"; +interface ClearImpressionsForSite { + event: "clearImpressionsForSite"; seconds: number; site: string; } @@ -148,8 +145,8 @@ function runTest( break; } - case "clearImpressionsForConversionSite": - backend.clearImpressionsForConversionSite(event.site); + case "clearImpressionsForSite": + backend.clearImpressionsForSite(event.site); break; } } From 9f612935d52926e224e5579c0ca838dad6fec2a8 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner Date: Tue, 30 Sep 2025 10:56:21 -0400 Subject: [PATCH 3/3] Allow conversion callers to delete impressions --- api.bs | 21 ++++++--- impl/e2e-tests/clear-site-data.json | 73 +++++++++++++++++++++++++++++ impl/src/backend.ts | 19 +++++--- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/api.bs b/api.bs index 06f0ea7..a4af595 100644 --- a/api.bs +++ b/api.bs @@ -1004,18 +1004,25 @@ run these steps: 1. If |impression|'s [=impression/Intermediary Site=] is `undefined` and its [=impression/Impression Site=] is equal to |site|, [=set/remove=] |impression| from the [=impression store=] and [=iteration/continue=]. + 1. If |impression| has an [=impression/Intermediary Site=] equal to |site|, [=set/remove=] |impression| from the [=impression store=] and [=iteration/continue=]. - 1. If |impression| has a [=impression/Conversion Sites=] [=set=] - that does not [=set/contain=] the value |site|, [=iteration/continue=]. + 1. If |impression|'s [=impression/Conversion Sites=] [=set/contains=] |site|: + + 1. [=set/Remove=] |site| from |impression|'s [=impression/Conversion Sites=]. + + 1. If |impression|'s [=impression/Conversion Sites=] [=set/is empty=], + [=set/remove=] |impression| from the [=impression store=] and + [=iteration/continue=]. + + 1. If |impression|'s [=impression/Conversion Callers=] [=set/contains=] |site|: - 1. If the [=set/size=] of |impression|'s [=impression/Conversion Sites=] - is greater than one, - [=set/remove|remove=] |site| from |impression|'s [=impression/Conversion Sites=] - and [=iteration/continue=]. + 1. [=set/Remove=] |site| from |impression|'s [=impression/Conversion Callers=]. - 1. Otherwise, [=set/remove=] |impression| from the [=impression store=]. + 1. If |impression|'s [=impression/Conversion Callers=] [=set/is empty=], + [=set/remove=] |impression| from the [=impression store=] and + [=iteration/continue=].

This process does not remove impressions that are saved with an empty [=set=] of [=impression/Conversion Sites=]. diff --git a/impl/e2e-tests/clear-site-data.json b/impl/e2e-tests/clear-site-data.json index 0e3cdfe..465204f 100644 --- a/impl/e2e-tests/clear-site-data.json +++ b/impl/e2e-tests/clear-site-data.json @@ -167,6 +167,79 @@ "credit": [1, 1, 1] }, "expected": [0, 0, 0] + }, + { + "seconds": 17, + "site": "e.example", + "event": "saveImpression", + "options": { + "histogramIndex": 3, + "conversionSites": ["advertiser-5.example"], + "conversionCallers": [ + "intermediary-1.example", + "intermediary-2.example" + ] + } + }, + { + "seconds": 18, + "site": "intermediary-1.example", + "event": "clearImpressionsForSite", + "$comment": "should remove no impressions, but remove this site from the conversionCallers of the impression with histogramIndex 3" + }, + { + "seconds": 19, + "site": "advertiser-5.example", + "intermediarySite": "intermediary-1.example", + "event": "measureConversion", + "$comment": "try to select 4 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 4, + "value": 8, + "maxValue": 8, + "credit": [1, 1, 1, 1] + }, + "expected": [0, 0, 0, 0] + }, + { + "seconds": 20, + "site": "advertiser-5.example", + "intermediarySite": "intermediary-2.example", + "event": "measureConversion", + "$comment": "try to select 4 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 4, + "value": 8, + "maxValue": 8, + "credit": [1, 1, 1, 1] + }, + "expected": [0, 0, 0, 8] + }, + { + "seconds": 21, + "site": "intermediary-2.example", + "event": "clearImpressionsForSite", + "$comment": "should remove the impression with histogramIndex 3, as its conversion callers set should become empty" + }, + { + "seconds": 22, + "site": "advertiser-5.example", + "intermediarySite": "intermediary-2.example", + "event": "measureConversion", + "$comment": "try to select 4 impressions to avoid depending on ordering", + "options": { + "aggregationService": "https://agg-service.example", + "epsilon": 0.5, + "histogramSize": 4, + "value": 8, + "maxValue": 8, + "credit": [1, 1, 1, 1] + }, + "expected": [0, 0, 0, 0] } ] } diff --git a/impl/src/backend.ts b/impl/src/backend.ts index a491c54..1f67b03 100644 --- a/impl/src/backend.ts +++ b/impl/src/backend.ts @@ -18,7 +18,7 @@ interface Impression { impressionSite: string; intermediarySite: string | undefined; conversionSites: Set; - conversionCallers: ReadonlySet; + conversionCallers: Set; timestamp: Temporal.Instant; lifetime: Temporal.Duration; histogramIndex: number; @@ -609,14 +609,19 @@ export class Backend { if (i.intermediarySite === site) { return true; } - if (!i.conversionSites.has(site)) { - return false; - } - if (i.conversionSites.size > 1) { + if (i.conversionSites.has(site)) { i.conversionSites.delete(site); - return false; + if (i.conversionSites.size === 0) { + return true; + } } - return true; + if (i.conversionCallers.has(site)) { + i.conversionCallers.delete(site); + if (i.conversionCallers.size === 0) { + return true; + } + } + return false; } this.#impressions = this.#impressions.filter(