From ae6d825511a64957a87fc96dd9286b0b27310854 Mon Sep 17 00:00:00 2001 From: Andy Leiserson Date: Tue, 10 Sep 2024 15:38:04 -0700 Subject: [PATCH 1/3] Continue work on API --- api.bs | 237 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 166 insertions(+), 71 deletions(-) diff --git a/api.bs b/api.bs index 7dab622..e2b941e 100644 --- a/api.bs +++ b/api.bs @@ -33,7 +33,7 @@ and allocating value to those [=actions=]. For advertising, actions that are of interest are primarily the showing of advertisements -(also referred to as impressions). +(also referred to as impressions). Other actions include ad clicks (or other interactions) and opportunities to show ads that were not taken. @@ -194,7 +194,7 @@ A conversion value might also be split between multiple impressions to split credit, though this capability is not presently supported in the API. -* Compatibility with privacy-preserving aggregation systems +* Compatibility with privacy-preserving aggregation services * Flexibility to assign buckets * As histogram size increases, noise becomes a problem @@ -261,7 +261,7 @@ As a result, sites learn nothing about what happened on other sites from this in The site can collect the encrypted histograms it receives from calls to this API and submit them to the aggregation service. -The aggregation service: +Upon receiving a set of encrypted histograms from a site, the aggregation service: 1. confirms that it has not previously computed an aggregate @@ -277,78 +277,138 @@ The aggregation service: # API Details # {#api} -Open questions: -* Filter/query language -* Reports are sent to aggregation system directly, or via conversion site? Or - option of either? => via conversion site -* Epochs +Before using the other Private Attribution APIs, a page must call the +[[#list-aggregation-services-api]] to query the supported aggregation services. +The page may select any of the supported systems returned by +listAggregationServices(). +The name of the selected system must be supplied as +the `aggregator` member of the {{PrivateAttributionImpressionOptions}} or +{{PrivateAttributionConversionOptions}} dictionaries when calling the +saveImpression() or +measureConversion() APIs, respectively. +## Finding a Supported Aggregation Service ## {#list-aggregation-services-api} -## ListAggregationSystems API ## {#list-aggregation-systems-api} +

Is any additional information required in the +{{PrivateAttributionAggregationService}} dictionary? -navigator.privateAttribution.listAggregationSystems() - -TODO: add whatever else is needed in this struct +The listAggregationServices() method +returns a list of aggregation services supported by the [=user agent=]. The page +must select and specify one of these services when calling the +saveImpression() and +measureConversion() APIs.

-dictionary PrivateAttributionAggregationSystem { - // Name of the aggregation system. This is passed as the `aggregator` parameter to - // other APIs. +dictionary PrivateAttributionAggregationService { required DOMString name; + required DOMString apiVersion; +}; + +[SecureContext, Exposed=Window] +interface PrivateAttribution { + sequence<PrivateAttributionAggregationService> listAggregationServices(); }; +The arguments to listAggregationServices() are as follows: + +
+
name
+
+ Name of the aggregation service. This is passed as the `aggregator` + parameter to other APIs. +
+
apiVersion
+
+ Version of the Private Attribution API supported by this aggregator. Even if + an aggregator supports multiple versions of the API, it is expected to + assign a unique aggregation service name for each supported version. + Thus, the API version is implicit in the aggregator selection + and does not need to be passed to other APIs. +
+
+ ## Saving Impressions ## {#save-impression-api} -The saveImpression() method does something or other. +The saveImpression() method requests +that the [=user agent=] record an [=impression=] in the [=impression store=].
 navigator.privateAttribution.saveImpression({
-  aggregator: "aggregator.example", // the name of the aggregation system
-  index: 3,                         // the histogram index for counting this impression
+  aggregator: "aggregator.example",
+  histogramIndex: 3,
   ad: "sample-campaign-eijb",       // a unique identifier for the ad placement
-  target: "advertiser.example",     // the advertiser site where a conversion will occur
+  conversionSite: "advertiser.example",     // the advertiser site where a conversion will occur
 });
 
-Add: -* TTL - dictionary PrivateAttributionImpressionOptions { required DOMString aggregator; required unsigned long histogramIndex; required DOMString ad; - required DOMString target; + required DOMString conversionSite; + unsigned long expiration; }; [SecureContext, Exposed=Window] -interface PrivateAttribution { +partial interface PrivateAttribution { [Throws] undefined saveImpression(PrivateAttributionImpressionOptions options); }; -Implicit saveImpression API inputs: -* Timestamp (epoch?) -* Source site +The arguments to saveImpression() are as follows: +
+
aggregator
+
+ A selection from the [=aggregation services=] that can be listed using listAggregationServices. +
+
histogramIndex
+
+ If measureConversion() matches this + [=impression=] with a subsequent [=conversion=], the [=conversion value=] + will be added to the histogram bucket identified by this index. +
+
expiration
+
+ A "time to live" (in seconds) after which the [=impression=] can no longer + receive attribution. The [=user agent=] should impose an upper limit on the + expiration, and silently reduce the value specified here if it exceeds that + limit. +
+
+ +

In the early-binding attribution model, it is not necessary to +identify the aggregation service when recording impressions. Thus, the aggregator member could +be removed from {{PrivateAttributionImpressionOptions}} and the [=impression store=]. +However, identifying the aggregation service when recording impressions will be +required if we adopt the late-binding attribution model in the future. ### Operation ### {#save-impression-api-operation} -1. Validate inputs -2. If the private attribution API is enabled, save the impression to the store. +1. Validate the page-supplied API inputs +2. Collect the implict API inputs: + 1. The current timestamp + 2. The impression site domain +3. If the private attribution API is enabled, save the impression to the store. -## MeasureConversion API ## {#measure-conversion-api} +## Requesting Attribution for a Conversion ## {#measure-conversion} -The measureConversion() method is used to do stuff. +The measureConversion() method +requests that the [=user agent=] perform [=attribution=] for a [=conversion=], +and return a [=conversion report=]. -TODO: -* Change filter data +The measureConversion() method +always returns a conversion report, +regardless of whether matching [=impression|impression(s)=] were found.

 navigator.privateAttribution.measureConversion({
-  // name of the aggregation system
+  // name of the aggregation service
   aggregator: "aggregator.example",
 
   // the number of buckets in the histogram
@@ -362,12 +422,10 @@ navigator.privateAttribution.measureConversion({
   // a list of possible ad identifiers that can be attributed
   ads: ["sample-campaign-eijb"],
   // a list of sites where impressions might have been registered
-  source: ["publisher.example"]
+  impressionSites: ["publisher.example"]
 });
 
-// TODO clarify "Infinity" - dictionary PrivateAttributionConversionOptions { required DOMString aggregator; @@ -378,9 +436,9 @@ dictionary PrivateAttributionConversionOptions { PrivateAttributionLogic logic = "last-touch"; unsigned long value = 1; - unsigned long lookbackDays = Infinity; + unsigned long lookbackDays; sequence<DOMString> ads = []; - sequence<DOMString> sources = []; + sequence<DOMString> impressionSites = []; }; [SecureContext, Exposed=Window] @@ -393,42 +451,78 @@ The arguments to <a method for=PrivateAttribution>measureConversion()</a> are as <dl dfn-for=PrivateAttributionConversionOptions dfn-type=dict-member> <dt><dfn>aggregator</dfn></dt> - <dd>A selection from the [=aggregation services=] - that can be listed using aggregationServices <!-- TODO link --> - + <dd> + A selection from the [=aggregation services=] that can be listed using <a + method for=PrivateAttribution>listAggregationServices()</a>. + </dd> <dt><dfn>histogramSize</dfn></dt> <dt><dfn>epsilon</dfn></dt> <dt><dfn>logic</dfn></dt> <dt><dfn>value</dfn></dt> - <dd>The conversion value</dd> + <dd>The [=conversion value=]</dd> <dt><dfn>lookbackDays</dfn></dt> + <dd>An integer number of days. Only impressions occurring within the past `lookbackDays` may match this [=conversion=].</dd> <dt><dfn>ads</dfn></dt> - <dt><dfn>sources</dfn></dt> + <dd>A list of [=ad identifiers=]. Only [=impressions=] having one of the listed identifiers may match this [=conversion=].</dd> + <dt><dfn>impressionSites</dfn></dt> + <dd>A list of impression sites. Only [=impressions=] recorded by one of the impression sites are eligible to match this [=conversion=].</dd> </dl> -Implicit MeasureConversion API inputs: -* Timestamp (epoch?) -* Target site - ### Operation ### {#measure-conversion-api-operation} -1. Validate inputs -2. Set |reportedConversionValue| to 0. -3. If the private attribution API is enabled, search for a matching impression. -4. If a matching impression was found: +1. Validate the page-supplied API inputs +2. Collect the implicit API inputs + 1. The current timestamp + 2. The conversion site domain +3. Set |reportedConversionValue| to 0. +4. If the private attribution API is enabled, search for a matching impression using the [[#logic-matching|common matching logic]]. +5. If a matching impression was found: 1. Set |histogramIndex| to the value from the matching impression 2. Set |reportedConversionValue| to the smaller of the following: - 1. The conversion value passed to the MeasureConversion API. - 2. The limit on conversion value determined by the remaining privacy budget. -5. Update the privacy budget store to reflect the reported conversion value. -6. Construct a report from |reportedConversionValue|, |histogramIndex|, and <var ignore=''>histogramSize</var>. -7. Encrypt the report. -8. Return the encrypted report. + 1. The conversion value passed to the MeasureConversion API. + 2. The limit on conversion value determined by the remaining privacy budget. +6. Update the privacy budget store to reflect the reported conversion value. +7. Construct a report from |reportedConversionValue|, |histogramIndex|, and <var ignore=''>histogramSize</var>. +8. Encrypt the report. +9. Return the encrypted report. + + +## Impression store ## {#s-impression-store} + +The <dfn>impression store</dfn> is used by the <a method +for=PrivateAttribution>measureConversion()</a> method to find matching +[=impressions=]. + +### Contents ### {#impression-store-contents} + +The [=impression store=] must store the following information: + +<div link-for=PrivateAttribution> +<pre class=simpledef> +Ad: The [=/ad identifier=] passed to <a>saveImpression()</a>. +Aggregation Service: The [=/aggregation service=] passed to <a>saveImpression()</a>. +Aggregation Service: Attribution can only occur between [=/impressions=] and [=/conversions=] specifying the same [=/aggregation service=]. +Impression Site: The site that called <a>saveImpression()</a>. +Conversion Sites: The conversion site(s) that were passed to <a>saveImpression()</a>. +Timestamp: The time at which <a>saveImpression()</a> was called. +Expiration: The number of seconds an [=/impression=] remains eligible for attribution, +Expiration: either from the call to <a>saveImpression()</a>, or a [=/user agent=]-defined limit. +Histogram Index: The histogram index passed to <a>saveImpression()</a>. +</pre> +</div> +### Periodic Maintenance ### {#impression-store-maintenance} -## Impression database ## {#impression-database} +The [=user agent=] should periodically use +the timestamp and expiration values +to identify and delete any [=impressions=] in the [=impression store=] +that have expired. +It is not necessary to remove [=impressions=] immediately upon expiry, +as long as <a method for=PrivateAttribution>measureConversion()</a> +excludes expired [=impressions=] from [=attribution=]. However, the +[=user agent=] should not retain expired [=impressions=] indefinitely. ## Attribution Logic ## {#s-logic} @@ -450,33 +544,35 @@ how to handle weeks in which the [=privacy budget=] is insufficient, and (optionally) how to process any additional parameters that might be used. -### Last Touch Attribution ## {#logic-last-touch} +### Last Touch Attribution ### {#logic-last-touch} -The <dfn enum-value for=PrivateAttributionLogic>last-touch</dfn> [=attribution logic=] +The <dfn enum-value for=PrivateAttributionLogic>"last-touch"</dfn> [=attribution logic=] indicates that the browser should select -the last impression that matches the [[#logic-matching|common matching logic]]. -The entire [=conversion value=] is allocated to the histogram bucket -that was saved with the impression. +the last (most recent) impression that matches the [[#logic-matching|common matching logic]]. +The entire [=conversion value=] (up to the maximum imposed by the [[#dp-budget|privacy budget]]) +is allocated to the histogram bucket that was saved with the impression. ### Common Matching Logic ### {#logic-matching} -TODO specify how to match using "lookbackDays", "ads" and "sources". +TODO specify how to match using "lookbackDays", "ads" and "impressionSites". +Discuss "infinite" lookbackDays. Clarify when it apples. When field is missing? Zero? +<dfn>ad identifier</dfn> ## User control and visibility ## {#user-control} * Users should be able to opt out. Opt out should be undetectable. -* User ability to view the impression store. +* User ability to view the impression store and past report submissions. # Implementation Considerations # {#implementation-considerations} * Management and distribution of values for the following: * Histogram size - * Target site for impressions - * Source site for conversions + * Conversion site for impressions + * Impression site for conversions * Ad IDs # Aggregation # {#aggregation} @@ -503,8 +599,7 @@ TODO ## Anti-Replay Requirements ## {#anti-replay} -<!-- TODO link to definition of "conversion report" --> -Conversion reports generated by browsers are bound +[=Conversion reports=] generated by browsers are bound to the amount of [=privacy budget=] that was expended by the site that requested the report. @@ -794,7 +889,7 @@ TODO * Interaction with telemetry opt-outs * Timing attacks on APIs -* Aggregation system security +* Aggregation service security * Fraud and abuse From c672e405c8dc29914085e3ad6f17934ec35fc572 Mon Sep 17 00:00:00 2001 From: Andy Leiserson <aleiserson@mozilla.com> Date: Fri, 13 Sep 2024 10:14:51 -0700 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Martin Thomson <mt@lowentropy.net> --- api.bs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/api.bs b/api.bs index e2b941e..caacf53 100644 --- a/api.bs +++ b/api.bs @@ -277,15 +277,15 @@ Upon receiving a set of encrypted histograms from a site, the aggregation servic # API Details # {#api} -Before using the other Private Attribution APIs, a page must call the -[[#list-aggregation-services-api]] to query the supported aggregation services. -The page may select any of the supported systems returned by +Before using the other Private Attribution APIs, a site must +[[#list-aggregation-services-api|list aggregation services]] to discover the aggregation services +that are supported. +The page may select any of the supported services returned by <a method for=PrivateAttribution>listAggregationServices()</a>. -The name of the selected system must be supplied as -the `aggregator` member of the {{PrivateAttributionImpressionOptions}} or +The name of the selected service must be supplied as +the `aggregator` member of the {{PrivateAttributionConversionOptions}} dictionaries when calling the -<a method for=PrivateAttribution>saveImpression()</a> or -<a method for=PrivateAttribution>measureConversion()</a> APIs, respectively. +<a method for=PrivateAttribution>measureConversion()</a> API. ## Finding a Supported Aggregation Service ## {#list-aggregation-services-api} @@ -295,8 +295,7 @@ the `aggregator` member of the {{PrivateAttributionImpressionOptions}} or The <dfn method for=PrivateAttribution>listAggregationServices()</dfn> method returns a list of aggregation services supported by the [=user agent=]. The page must select and specify one of these services when calling the -<a method for=PrivateAttribution>saveImpression()</a> and -<a method for=PrivateAttribution>measureConversion()</a> APIs. +<a method for=PrivateAttribution>measureConversion()</a> API. <xmp class=idl> dictionary PrivateAttributionAggregationService { @@ -306,7 +305,7 @@ dictionary PrivateAttributionAggregationService { [SecureContext, Exposed=Window] interface PrivateAttribution { - sequence<PrivateAttributionAggregationService> listAggregationServices(); + attribute FrozenArray<PrivateAttributionAggregationService> aggregationServices; }; From 19ba9b275f3ad69af483043c77a7368de268c262 Mon Sep 17 00:00:00 2001 From: Andy Leiserson Date: Fri, 13 Sep 2024 10:22:24 -0700 Subject: [PATCH 3/3] Additional review edits * Updates for not passing `aggregator` to `saveImpression`. * Change `expiration` to `lifetimeDays`. --- api.bs | 44 +++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/api.bs b/api.bs index caacf53..4fcc7c1 100644 --- a/api.bs +++ b/api.bs @@ -284,18 +284,20 @@ The page may select any of the supported services returned by listAggregationServices(). The name of the selected service must be supplied as the `aggregator` member of the -{{PrivateAttributionConversionOptions}} dictionaries when calling the -measureConversion() API. +{{PrivateAttributionConversionOptions}} dictionary when calling the +measureConversion() method. ## Finding a Supported Aggregation Service ## {#list-aggregation-services-api}

Is any additional information required in the -{{PrivateAttributionAggregationService}} dictionary? +{{PrivateAttributionAggregationService}} dictionary? Do we want +to rename `apiVersion` to `protocol`? And we should definitely +define an enum for it. The listAggregationServices() method returns a list of aggregation services supported by the [=user agent=]. The page must select and specify one of these services when calling the -measureConversion() API. +measureConversion() method.

dictionary PrivateAttributionAggregationService { @@ -315,7 +317,7 @@ The arguments to <a method for=PrivateAttribution>listAggregationServices()</a> <dt><dfn>name</dfn></dt> <dd> Name of the aggregation service. This is passed as the `aggregator` - parameter to other APIs. + parameter to <a method for=PrivateAttribution>measureConversion()</a>. </dd> <dt><dfn>apiVersion</dfn></dt> <dd> @@ -323,7 +325,7 @@ The arguments to <a method for=PrivateAttribution>listAggregationServices()</a> an aggregator supports multiple versions of the API, it is expected to assign a unique aggregation service name for each supported version. Thus, the API version is implicit in the aggregator selection - and does not need to be passed to other APIs. + and does not need to be passed to <a method for=PrivateAttribution>measureConversion()</a>. </dd> </dl> @@ -334,7 +336,6 @@ that the [=user agent=] record an [=impression=] in the [=impression store=]. <pre> navigator.privateAttribution.saveImpression({ - aggregator: "aggregator.example", histogramIndex: 3, ad: "sample-campaign-eijb", // a unique identifier for the ad placement conversionSite: "advertiser.example", // the advertiser site where a conversion will occur @@ -343,11 +344,10 @@ navigator.privateAttribution.saveImpression({ <xmp class=idl> dictionary PrivateAttributionImpressionOptions { - required DOMString aggregator; required unsigned long histogramIndex; required DOMString ad; required DOMString conversionSite; - unsigned long expiration; + unsigned long lifetimeDays; }; [SecureContext, Exposed=Window] @@ -359,33 +359,21 @@ partial interface PrivateAttribution { The arguments to <a method for=PrivateAttribution>saveImpression()</a> are as follows: <dl dfn-for=PrivateAttributionImpressionOptions dfn-type=dict-member> - <dt><dfn>aggregator</dfn></dt> - <dd> - A selection from the [=aggregation services=] that can be listed using <a - method for=PrivateAttribution>listAggregationServices</a>. - </dd> <dt><dfn>histogramIndex</dfn></dt> <dd> If <a method for=PrivateAttribution>measureConversion()</a> matches this [=impression=] with a subsequent [=conversion=], the [=conversion value=] will be added to the histogram bucket identified by this index. </dd> - <dt><dfn>expiration</dfn></dt> + <dt><dfn>lifetimeDays</dfn></dt> <dd> - A "time to live" (in seconds) after which the [=impression=] can no longer + A "time to live" (in days) after which the [=impression=] can no longer receive attribution. The [=user agent=] should impose an upper limit on the - expiration, and silently reduce the value specified here if it exceeds that + lifetime, and silently reduce the value specified here if it exceeds that limit. </dd> </dl> -<p class=issue>In the early-binding attribution model, it is not necessary to -identify the aggregation service when recording impressions. Thus, the <a -dict-member for=PrivateAttributionImpressionOptions>aggregator</a> member could -be removed from {{PrivateAttributionImpressionOptions}} and the [=impression store=]. -However, identifying the aggregation service when recording impressions will be -required if we adopt the late-binding attribution model in the future. - ### Operation ### {#save-impression-api-operation} 1. Validate the page-supplied API inputs @@ -500,13 +488,11 @@ The [=impression store=] must store the following information: <div link-for=PrivateAttribution> <pre class=simpledef> Ad: The [=/ad identifier=] passed to <a>saveImpression()</a>. -Aggregation Service: The [=/aggregation service=] passed to <a>saveImpression()</a>. -Aggregation Service: Attribution can only occur between [=/impressions=] and [=/conversions=] specifying the same [=/aggregation service=]. Impression Site: The site that called <a>saveImpression()</a>. Conversion Sites: The conversion site(s) that were passed to <a>saveImpression()</a>. Timestamp: The time at which <a>saveImpression()</a> was called. -Expiration: The number of seconds an [=/impression=] remains eligible for attribution, -Expiration: either from the call to <a>saveImpression()</a>, or a [=/user agent=]-defined limit. +Lifetime: The number of days an [=/impression=] remains eligible for attribution, +Lifetime: either from the call to <a>saveImpression()</a>, or a [=/user agent=]-defined limit. Histogram Index: The histogram index passed to <a>saveImpression()</a>. </pre> </div> @@ -514,7 +500,7 @@ Histogram Index: The histogram index passed to <a>saveImpression()</a>. ### Periodic Maintenance ### {#impression-store-maintenance} The [=user agent=] should periodically use -the timestamp and expiration values +the timestamp and lifetime values to identify and delete any [=impressions=] in the [=impression store=] that have expired.