FLEDGE has been renamed to Protected Audience API. To learn more about the name change, see the blog post
The Private Aggregation API proposes a mechanism for participants of FLEDGE auctions to create summary reports which can measure generic cross-site data.
This proposal seeks to extend this API to support further measurement use-cases for FLEDGE auctions using the same mechanism to aggregate reports from the browser. The new APIs added allow for reporting bidding information and new signals (post-auction signals, latency) at different points in auction (e.g. win, loss, bid, click), while retaining the same privacy and aggregation aspects of the Private Aggregation API proposal.
- Allow all auction participants (both winner and losers) to measure aggregate data about their bids, competing bids, and other auction signals
- “How far off the auction clearing price was my bid?”
- "Why was my bid rejected?"
- "How far off the second highest bid value was my bid?"
- Allow auction participants to measure aggregate data using signals only available during bid generation
- "How many times did my campaign bid?"
- "What is my campaign's win rate?"
- Allow auction winners to measure aggregate data about the auction relative to events which happen with the rendered ad (such as a click on the ad)
- "What is the bid cost per click?"
- "What is the bid cost per render?"
- Allow auction sellers to measure aggregate data on auction latency and other statistics
- "Which participants are contributing the most to auction latency?"
- Allow auction buyers to measure aggregate data on their bidding latency
- “How much latency are my bidding signals fetches and bid generation code contributing?
The Private Aggregation API currently allows auction winners to generate histogram contributions within their reportResult()/reportWin() methods using a new API available in the worklet:
function reportResult(auctionConfig, browserSignals) {
// ...
privateAggregation.contributeToHistogram({
bucket: convertBuyerToBucketId(browserSignals.interestGroupOwner),
value: convertBidToReportingValue(browserSignals.bid)
});
}
While this provides a way to gather important information about a winning creative, for instance, the average bid submitted for a creative, it still lacks some information that can be useful for auction participants as well as sellers. Information which is also not available through event level reporting. Just to mention a few:
- The ability for a seller to track the average number of auction participants
- The ability for a buyer to know the win rate of a campaign
- The ability for a buyer/seller to know the highest other bid (HOB) for training bid shading models
- The ability for a buyer/seller to measure actions contributing to auction latency
- The ability for a buyer to link useful bidding signals (for instance time a user has spent in an interest group) with a click event — this information is essential for model training.
Below, we propose an extension of the Private Aggregation API that tries to address these use cases.
A natural way to address the above use cases is for auction participants and sellers to be able to trigger arbitrary reports at different events. For instance, a buyer may want to count the number of times users that were in an interest group for less than an hour clicked on the winning ad. Or they may want to track auction signals associated with a non-winning bid, etc.
Our proposed API extension allows a bidder to register a set of histogram contributions associated
with an arbitrary event_key
within generateBid
, scoreAd
, reportWin
, and reportResult
.
We consider the scenario where a buyer wants to learn the click-through rate of ads when a user has been in an interest group for a given time.
To generate the bucket that represent interest group age, the buyer may implement getImpressionReportBucket()
and getClickReportBucket()
functions which return buckets that map an interest group and the time the user has spent in that interest group to a 128-bit integer as BigInt
. The browserSignals.recency
value inside generateBid()
specifies the duration in minutes since the user joined the interest group.
Once the buckets have been derived, the buyer can call Private Aggregation inside generateBid()
:
function generateBid(interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, browserSignals) {
// ...
privateAggregation.contributeToHistogramOnEvent("reserved.win", {
bucket: getImpressionReportBucket(), // 128-bit integer as BigInt
value: 1
});
privateAggregation.contributeToHistogramOnEvent("click", {
bucket: getClickReportBuckets(), // 128-bit integer as BigInt
value: 1
});
}
The impression report will be sent if the reserved.win
event is triggered, which is a reserved event for when the bid wins the auction. The click report will be sent if the click
event is triggered by the window.fence.reportEvent("click")
call originating from the fenced frame of the ad.
The buyer can then generate the summary report of the impression count and click count to infer the click-through rate of the users in different interest group age buckets.
A buyer may be interested in understanding how much higher they should have bid in order to win an
auction. To do this, not only do we need to trigger a report at loss time (see Reporting for bids
which do not win), but also have the value of the report depend on the outcome of the auction. To
do this, we introduce a field to the “contributions” object called signalValue
. This field allows
the report to depend on post auction information. A signalValue
object is composed of the following
values:
baseValue
: The name of the auction result value we want to report. For instance, winning-bid.scale
: Optional scale factor by which we want to multiply the auction result value. This is useful for controlling the amount of noise added by the aggregation service. Scale is applied beforeoffset
is added.offset
: Optional offset to add to the auction result value.
After the auction happens, the final value of the generated report is (baseValue
* scale
) + offset
.
The following example shows how to return the gap between an ad bid and the winning bid:
function generateBid(...) {
bid = 100;
privateAggregation.contributeToHistogramOnEvent(
"reserved.loss",
{
bucket: 1596n, // represents a bucket for interest group x winning bid price
value: {
baseValue: "winning-bid",
scale: 2, // Number which will be multiplied by browser value
offset: -bid * 2 // Numbers which will be added to browser value, after scaling
}
});
return bid;
}
In the event this bid does not win the auction, and the winning bid was 200, the histogram contribution would be generated as:
bucket: 1596n
value: 200 // = (winning-bid * scale) + offset = (200 * 2) + (-100 * 2)
This would correspond to the gap by which the advertiser lost the auction, scaled by 2. Scaling is important as it allows bidders to better control the amount of noise which will be added to various aggregation keys.
Similar to the above example, sometimes, the key that we want to aggregate over may depend
on the outcome of an auction. To solve this use case we provide an object called signalBucket
.
The final bucket id of the bucket will depend on the outcome of the auction. The following
example allows the buyer to keep track of how many times their bid was rejected for particular reasons.
function generateBid(...) {
privateAggregation.contributeToHistogramOnEvent(
"reserved.loss",
{
bucket: {
baseValue: "bid-reject-reason",
offset: 500n // Offset buckets
},
value: 1
});
return bid;
}
If the bid is rejected for being below auction floor, this would result in a contribution being generated:
bucket: 502n // 500n + 2n (2n is the value associated with bids below auction floor, see bid-reject-reason below)
value: 1
privateAggregation.contributeToHistogramOnEvent(eventType, contribution)
The parameters consist of:
- an
eventType
which is a string identifying the event type that triggers this report to be sent (see Triggering reports below), and - a
contribution
object which contains:- a
bucket
which is a 128bit ID or asignalBucket
which tells the browser how to calculate the bucket (represented as BigInt), - a
value
which is a non-negative integer or asignalValue
which tells the browser how to calculate the value, and - a
filteringId
which is an optional integer in the range [0, 255] used to allow for separating aggregation service queries. For more detail, please see the flexible filtering explainer.
- a
Where signalBucket
and signalValue
is a dictionary which consists of:
- a
baseValue
field indicating which value the browser should use to calculate the resulting bucket or value. AsignalValue.baseValue
orsignalBucket.baseValue
may be any of the following:winning-bid
: the value used is the winning bid value.highest-scoring-other-bid
: the value used is the bid value that was scored as second highest.script-run-time
: milliseconds of CPU time that the calling function required, when called.signals-fetch-time
: milliseconds required to fetch the trusted bidding or scoring signals, when called fromgenerateBid()
orscoreAd()
respectively.bid-reject-reason
: one of the following values:- 0: indicates seller rejected bid without providing a reason, i.e., bid reject reason not available
- 1: indicates seller rejected bid because “Invalid Bid”
- 2: indicates seller rejected bid because “Bid was Below Auction Floor”
- 3: indicates seller rejected bid because “Creative Filtered - Pending Approval by Exchange”
- 4: indicates seller rejected bid because “Creative Filtered - Disapproved by Exchange”
- 5: indicates seller rejected bid because “Creative Filtered - Blocked by Publisher”
- 6: indicates seller rejected bid because “Creative Filtered - Language Exclusions”
- 7: indicates seller rejected bid because “Creative Filtered - Category Exclusions”
- 8: indicates seller rejected bid because "Creative Filtered - Did Not Meet The K-anonymity Threshold"
- 9: indicates bid produced by
generateBid()
was rejected because it failed a currency check (e.g. the bid returned bygenerateBid()
doesn't match what's specified byperBuyerCurrency
) - 10: indicates bid passed through or altered by
scoreAd()
was rejected because it failed a currency check (e.g. the bid returned or passed through byscoreAd()
in a component auction doesn't match thesellerCurrency
of its auction or theperBuyerCurrency
required by the top-level auction) - Perhaps other values indicating:
- generateBid() hitting timeout
- The auction was aborted (i.e. calling endAdAuction())
- an auction that never rendered the ad
- optional
offset
andscale
that allow the reporter to manipulate the browser signal to customize the buckets and values they will receive in reports:scale
will be multiplied by the browser provided value. Scale is applied beforeoffset
is added. Default value is 1.0.offset
will be added to the browser provided value (allows you to shift buckets, etc). Default value is 0.
A fenced frame can trigger the sending of contributions associated with an arbitrary event by calling into a new API:
window.fence.reportEvent("click");
This will cause any contributions associated with a call to contributeToHistogramOnEvent()
with an event-type of click
to be reported/sent.
In this example, "click"
is an event-name chosen by the auction bidder. There are a number
of event names that are reserved and invoked directly by the browser. All reserved values will
have the "reserved."
prefix, and all non-reserved values cannot have the "reserved."
prefix.
The reserved.win
event-type is a special value that indicates a report should be sent when a
bid wins the auction.
The browser will trigger sending if contributions were registered for a winning bid.
The reserved.loss
event-type is a special value that indicates a report should be sent when a
bid does not win the auction. If the bid does not win, the browser will trigger reporting for
the loss
contributions.
The reserved.always
event-type is a special value that indicates a report should be sent no
matter whether a bid wins the auction or not. The browser will always trigger reporting for the
always
contributions.
The seller may want to collect aggregate statistics on latency and bids for their auctions. Measuring this per buyer will allow the seller to evaluate performance information at a more granular level and give helpful guidance and feedback to their bidders.
In order for the seller to collect this additional information from buyers, we need to give
buyers a way to express which sellers they’re comfortable sharing this information with, so we
add a new mechanism which allows each buyer to declare a set of approved sellers: The interestGroup
provided to navigator.joinAdInterestGroup()
will contain a new field named sellerCapabilities
, a
dict keyed by seller origin (or "*", to set defaults for non-specified seller origins) with lists of
permission strings as values (as described below). Sellers might want to only score bids from
interest groups that will share aggregate statistics, so a field, requiredSellerCapabilities
will
also be added to the auction config. Any interest group that doesn't permit (for the auction's seller)
all the sellerCapabilities
listed in requiredSellerCapabilities
will not participate in the
auction.
For the seller to declare reporting, the auctionConfig
passed to runAdAuction
is amended to
contain a configuration for the seller latency report.
const auctionConfig = {
'seller': 'https://www.example-ssp.com',
// ...
'interestGroupBuyers': ['https://buyer1.com', 'https://buyer2.com', /* ... */],
// An aggregation key for each buyer. This represents the starting contribution bucket
// associated with the corresponding buyer.
'auctionReportBuyerKeys': [100n, // key for buyer1.com
105n, // key for buyer2.com
// ...
],
// Configures what values will be reported for each buyer. The key declares what signal will be
// measured, and the bucket declares the bucket this will be placed relative to the
// buyer key and the scale determines how the value is scaled.
'auctionReportBuyers': {
'interestGroupCount': { 'bucket': 0n, 'scale': 1, },
'bidCount': { 'bucket': 1n, 'scale': 1, },
'totalGenerateBidLatency': { 'bucket': 2n, 'scale': 1, },
'totalSignalsFetchLatency': { 'bucket': 3n, 'scale': .1, },
}
}
The seller is able to measure the following for each buyer, assuming permission is granted via the indicated sellerCapabilities
for that seller:
interestGroupCount
: The number of the interest groups which could participate in the auction (i.e. the number of interest groups on the machine for this buyer -- note the count isn't limited by the auction config'sperBuyerGroupLimits
). This requires theinterest-group-counts
sellerCapabilities
permission.bidCount
: The number of valid bids generated by this buyer. This requires theinterest-group-counts
sellerCapabilities
permission.totalGenerateBidLatency
: The sum of execution time for all generateBids() in milliseconds. This requires thelatency-stats
sellerCapabilities
permission.totalSignalsFetchLatency
: The total time spent fetching trusted buyer signals in milliseconds. If the interest group didn't fetch any trusted signals, then 0 milliseconds is reported. This requires thelatency-stats
sellerCapabilities
permission.
Given the auctionConfig
above, if buyer1.com had two interest groups participate in the auction,
their trusted buyer signals fetch taking 10ms, their generateBid()
scripts running for 2ms and
3ms, and one of the interest groups returning a bid, the following histogram reports would be
generated by the browser:
{ bucket: 100n,// = 100n + 0n = buyer1.com’s key + interestGroupCount’s bucket
value: 2 }, // = 2 interest groups were given the chance to bid.
{ bucket: 101n,// = 100n + 1n = buyer1.com’s key + bidCount’s bucket
value: 1 }, // = 1 interest group returning a bid.
{ bucket: 102n,// = 100n + 2n = buyer1.com’s key + totalGenerateBidLatency’s bucket
value: 5 }, // = 3ms + 2ms generateBid() runtimes.
{ bucket: 103n,// = 100n + 3n = buyer1.com’s key + totalSignalsFetchLatency’s bucket
value: 1 }, // = 10ms total trusted bidding signals fetch time multiplied by .1 scale.
Here's an example of what the sellerCapabilities
interest group field could look like:
'sellerCapabilities': {
'https://seller.com': [ 'interest-group-counts', 'latency-stats' ],
'https://seller2.com': [ 'latency-stats' ],
'*': [ 'interest-group-counts' ]
}
This would grant both interest-group-counts
and latency-stats
permission to https://seller.com, latency-stats
to https://seller2.com, and interest-group-counts
to all other sellers.
NOTE: the permission names interestGroupCounts
and latencyStats
are deprecated and will be removed in a future Chrome release, as they do not follow the documented WebIDL naming conventions.
While third-party cookies are still available, we plan to have a temporary mechanism available that allows for easier debugging. This mechanism is described in detail in the Private Aggregation explainer.
However, one key difference here is that this reporting does not use the
privateAggregation
object and so the method described to use this mechanism
(privateAggregation.enableDebugMode()
) is not available for this reporting.
Instead, we add a new (temporary) parameter to the auctionConfig
passed to
runAdAuction()
:
const auctionConfig = {
'seller': 'https://www.example-ssp.com',
...
'interestGroupBuyers': ['https://buyer1.com', 'https://buyer2.com', ...],
// The API described above
'auctionReportBuyerKeys': [100n, 105n, ...],
'auctionReportBuyers': {
'interestGroupCount': { 'bucket': 0n, 'scale': 1 },
...
}
// Additional parameter for configuring the debug mode
'auctionReportBuyerDebugModeConfig': { 'enabled': true, debugKey: 1234n },
}
Note that enabled
defaults to false if not provided. Also, as with
privateAggregation.enableDebugMode()
, debugKey
is an optional unsigned
64-bit integer that allows sites to associate reports with the contexts that
triggered them.
Otherwise, the details of this debug mode are the same as other uses of Private Aggregation. For example, this debug mode is generally only available to callers eligibile to set third-party cookies and will automatically become deprecated when third-party cookies are.
To control the data volume of aggregatable reports, auction participants may want to subsample reports on the client to avoid high costs or latency associated with aggregation. Auction participants can do this by only utilizing the Private Aggregation API some fraction of the time.
Subsampling on the client side as opposed to the server is ideal to avoid oversampling infrequent events / campaigns.
See Private Aggregation Privacy Consideration
Reports generated with the mechanisms in this proposal will be subject to the same contribution bounding and bucketing for Private Aggregation Reports.
While new data is being used to generate histogram contributions (bidding signals, information from within the fenced frame), the general privacy properties offered by the Aggregation Service still hold.
In the current proposal, the browser limits the overall sensitivity of the contributions made by a single user per reporter per day. It may be desirable to allow reporters/interest group owners to select their histogram contributions knowing the amount of budget which remains. Currently, reports created after the budget has been exhausted are ignored.
We may want to add a reserved.render
event-type or similar which allows a bidder to measure contributions
when the browser shows the ad, or potentially when the browser finishes fetching the document.