This blog post covers a new feature called Private Click Measurement, or PCM, for measuring ad clicks across websites and from iOS apps to websites. It is part of iOS and iPadOS 14.5 betas.
Motivation and Goals
Classic ad attribution on the web is done with cookies carrying user or device IDs. Such attribution constitutes cross-site tracking which WebKit is committed to preventing. Websites should not be able to attribute data of an ad click and a conversion to a single user as part of large scale tracking.
At the same time, we want to support measurement of online advertising. PCM achieves this tradeoff by sending attribution reports with limited data in a dedicated Private Browsing mode without any cookies, delaying reports randomly between 24 and 48 hours to disassociate events in time, and handling data on-device.
The Feature in a Nutshell
- A new, on-by-default feature called Private Click Measurement, or PCM, for privacy-preserving measurement of ad clicks across websites and from iOS apps to websites in iOS and iPadOS 14.5 betas.
- An 8-bit identifier on the click source side, which means 256 parallel ad campaigns can be measured per website or app.
- A 4-bit identifier on the conversion side, which means 16 different conversion events can be distinguished.
- Fraud prevention via unlinkable tokens will be coming.
A Proposed Standard
We first proposed privacy-preserving measurement of ad clicks in May 2019. Since then the proposal has changed name to Private Click Measurement and been discussed extensively in the W3C Privacy Community group, both through meetings and on GitHub.
A proposal needs two independent implementations to be on track to become a web standard. This means another browser such as Firefox, Brave, Chrome, or Edge needs to independently implement PCM before it can move further along the standards track. We are working with them to get there.
Nevertheless, we are happy to be the first browser to enable a proposed web standard for measuring advertising!
On By Default
You may ask why we are enabling PCM by default before there is a second independent implementation and before we’ve added the fraud prevention mechanism discussed in W3C Privacy CG. The reasons are:
- Early access. We recognize the need for early access so that advertisers, websites, and apps can adopt the technology, analyze real data, tune their measurement, and report any issues to us.
- Equal access. We want to provide everyone with the opportunity to test and use this technology from the get-go. An alternative would be to only run it with selected partners but we have opted for an open approach.
- Attribution data is stable. Fraud prevention tokens will be added and naming of data labels might change, but the functionality and attribution data is stable, namely 8 bits on the click source side and 4 bits on the attribute-on side. Thus, full scale tests of PCM are meaningful and useful at this point.
Web-to-Web Click Measurement
PCM web-to-web is the case covered by the proposed standard, i.e. a user clicks a link on a webpage, is navigated cross-site, and up to seven days later, there’s a signal on the destination website saying it would like attribution for any previous clicks that took the user here.
For the purposes of the examples below, we assume the click happens on a website called social.example
and the click navigates the user to shop.example
.
The Click Side
Links that want to store click measurement data should look like this:
<!-- Link on social.example -->
<a href="https://shop.example/product.html"
attributionsourceid="[8-bit source ID]"
attributeon="https://shop.example">
Markup
</a>
The two mandatory attributes are:
attributionsourceid
: The 8-bit attribution source ID, allowed to be between 0 and 255. This was earlier referred to as the ad campaign ID but since PCM is not technically tied to advertising, it was decided in the standards discussion that its attributes and key names should not use advertising terms.
attributeon
. The click destination website which wants to attribute incoming navigations to clicks. Note that PCM only uses the registrable domain or eTLD+1, i.e. there is no separation based on subdomains. This is so that the destination cannot be set up as https://janeDoeTracking.shop.example
to track user Jane Doe.
If the click indeed navigated the user to the attributeon
website, the attributionsourceid
is stored as a click from social.example
to shop.example
for 7 days.
Note that this data is not accessible to websites. It’s silently stored in the browser.
The Triggering Event
To trigger click attribution, the “attribute on” website has to make an HTTP GET request to the website(s) where it is running click-through ads. This way of doing it is intended to support existing “tracking pixels” and make adoption easy. In our example this would be the shop.example
site making an HTTP GET request to social.example
. For a more modern way of triggering attribution, see the Future Enhancements section.
The HTTP GET request to social.example
triggers attribution if it is redirected to https://social.example/.well-known/private-click-measurement/trigger-attribution/
[``4-bit`` trigger data]/[optional 6-bit priority]
. Note: The first beta lacks the /trigger-attribution
path component since this was a very recent decision in the standards group.
The two URL path parameters are:
- Trigger data. This is a 4-bit value between 00 and 15 that encodes the triggering event (note the mandatory two digits). This was earlier referred to as the conversion value but again, PCM is not technically tied to advertising so it doesn’t use advertising terms.
- Optional priority. This is a 6-bit value between 00 and 63 which allows multiple triggering events to result in a single attribution report for the event with the highest priority (again, note the two digits). For instance, there might be multiple steps in a sales funnel where each step triggers attribution but steps further down the funnel have higher priority. This value only controls which trigger data goes into the attribution report and is not part of the attribution report itself. You may ask why this isn’t a 4-bit value like the trigger data. The reason is to support easy changes to what’s being measured without having to remap several trigger-data-to-priority pairs. Triggering events 00-15 may start out as mapped to priority 00-15 but then the shop owner wants to drill into events 5-7. With the extra bits, it’s easy to assign triggering events 05-07 to priority 20-22 so as to focus attribution reports to those.
Once a triggering event matches a stored click, a single attribution report is scheduled by the browser to be sent out randomly between 24 and 48 hours later, or the earliest time thereafter when the browser is running. As long as an attribution report has not yet been sent, it can be rescheduled based on a triggering event with higher priority.
The Attribution Report
PCM attribution reports are sent as HTTP POST requests to /.well-known/private-click-measurement/report-attribution/
on the website where the click happened, in our example https://social.example/.well-known/private-click-measurement/report-attribution/
. Note: The first beta lacks the /report-attribution
path component since this was a very recent decision in the standards group.
The report is in JSON and looks like this:
{
"source_engagement_type" : "click",
"source_site" : "social.example",
"source_id" : [8-bit source ID],
"attributed_on_site" : "shop.example",
"attribution_trigger_data" : [4-bit trigger data],
"version": 1
}
Notes on the non-obvious key-values above:
source_engagement_type
is always “click” for PCM. This field allows for future use of this report mechanism for other types of attribution such as view-through.
version
signals to the receiving end which version of the attribution feature this is. You should expect this number to be increased when fraud prevention tokens are added or something else about the mechanism is changed. This allows concurrent versions to work in parallel and provides a signal to developers that there may be things they need to change or adopt on their side.
App-to-Web Click Measurement
This is exciting – we’re adding the capability to measure ad clicks from iOS and iPadOS apps to Safari!
Many advertisers in apps want to take the user to their website where the user can buy a product or sign up for a service. This is exactly the kind of ad PCM app-to-web allows them to measure.
The Click Side
The only thing that differs from PCM web-to-web is on the click side which is in an iOS app. To adopt this technology you need to do this:
- Add a URL to where you want PCM’s ad attribution reports to be sent when ads are clicked in your app. You do this under the key
NSAdvertisingAttributionReportEndpoint
in your Info.plist. The naming of this endpoint is deliberately not tied to PCM. Potential future ad measurement reports associated with apps may use this URL with a differing well-known location if appropriate. Note that the subsequent HTTP redirect to trigger attribution needs to go to this website.
- Populate and add the new
UIEventAttribution
to the options of your call to openURL:
. See below for what fields you need to enter in UIEventAttribution
.
- Overlay the parts of the click-through ad that will trigger navigations to websites with the new
UIEventAttributionView
. This view only serves as a checkpoint for Apple’s code on-device to check that a user gesture happened before the navigation. The view does not consume the gesture and you are free to decide whether or not to navigate to a website even if the gesture happened on one of these views. A user gesture is required for your UIEventAttribution
object to be forwarded to the browser as part if the call to openURL:
. Note that PCM app-to-web is so far only supported in Safari and only on iOS and iPadOS. We intend to add WebKit API to enable other default browsers to be the destination of PCM app-to-web too.
UIEventAttribution
This is the optional data structure you submit in your call to openURL:
when you want to measure clicks:
open class UIEventAttribution : NSObject, NSCopying {
open var sourceIdentifier: UInt8 { get }
open var destinationURL: URL { get }
open var reportEndpoint: String? { get }
open var sourceDescription: String { get }
open var purchaser: String { get }
public init(sourceIdentifier: UInt8,
destinationURL: URL,
sourceDescription: String,
purchaser: String)
}
sourceIdentifier
is the same as PCM’s attributionsourceid
attribute for links. Allowed values are 0-255.
destinationURL
is the same as PCM’s attributeon
attribute for links but it should be a full URL with protocol. The report will be sent to the URL’s registrable domain (eTLD+1) and over HTTPS.
reportEndpoint
will be picked up by Apple code from your info.plist’s NSAdvertisingAttributionReportEndpoint
. As you can see, the init
function does not take this parameter. This is where PCM will send any subsequent ad attribution reports. The reason why it need to be stated in the static Info.plist is so that it cannot be used as a tracking vector by dynamically submitting user-specific reporting URLs such as janeDoeTracking.example
.
sourceDescription
is a human-readable description of the content that was tapped. This string should be no longer than roughly 100 characters and can be localized according to the context. It will not be seen by Apple or the destination website. Instead it’s intended to be able to show to users what ad click data they have stored.
purchaser
is a human-readable name or description of the purchaser of the content that was tapped, typically the ad buyer. This string should be no longer than roughly 100 characters and can be localized according to the context. It will not be seen by Apple or the destination website. Instead it’s intended to be able to show to users what ad click data they have stored.
UIEventAttribution
Sample Code
func openAdLink() {
let adURL = URL(string: "https://shop.example/tabletStandDeluxe.html")!
let eventAttribution =
UIEventAttribution(sourceIdentifier: 4,
destinationURL: adURL,
sourceDescription: "Banner ad for Tablet Stand Deluxe.",
purchaser: "Shop Example, Inc.")
// If using scene lifecycle.
let sceneOpenURLOptions = UIScene.OpenExternalURLOptions()
sceneOpenURLOptions.eventAttribution = eventAttribution
self.view.window?.windowScene?.open(adURL,
options: sceneOpenURLOptions,
completionHandler: nil)
// If using application lifecycle.
let appOpenURLOptions: [UIApplication.OpenExternalURLOptionsKey : Any] = [
.eventAttribution: eventAttribution
]
UIApplication.shared.open(adURL,
options: appOpenURLOptions,
completionHandler: nil)
}
UIEventAttributionView
UIEventAttributionView
is the view that is placed over the tappable content, typically an ad. It’s used by the system to verify that a user gesture has occurred.
open class UIEventAttributionView : UIView {
}
The view is invisible and very lightweight. The simplest use case is to create one of these views and stretch it over your entire tappable content. You can also place multiple over a single piece of content if you for instance want to create specific tappable areas.
To ensure your UIEventAttributionView
works correctly:
- Ensure
isUserInteractionEnabled
is false
. This is the default value for this view and ensures the view doesn’t consume events which would otherwise go to the content beneath it.
- Ensure there are no views placed on top of the event attribution view. The user should be tapping this view for it to count as a user gesture for the purposes of PCM app-to-web.
- Ensure your tap handling occurs on a touch up event. This automatically occurs if your content is tapped in response to a
UITapGestureRecognizer
firing or at the .ended
state of a UILongPressGestureRecognizer
.
UIEventAttributionView
Sample Code
func addEventAttributionView() {
// Create an event attribution view.
let eventAttributionView = UIEventAttributionView()
// Place it over your ad however you'd like.
eventAttributionView.translatesAutoresizingMaskIntoConstraints = false
adView.addSubview(eventAttributionView)
NSLayoutConstraint.activate([
adView.topAnchor.constraint(equalTo: eventAttributionView.topAnchor),
adView.leadingAnchor.constraint(equalTo: eventAttributionView.leadingAnchor),
adView.trailingAnchor.constraint(equalTo: eventAttributionView.trailingAnchor),
adView.bottomAnchor.constraint(equalTo: eventAttributionView.bottomAnchor)
])
}
Testing and Debugging
WebKit has an experimental feature called Private Click Measurement Debug Mode. You’ll find it under Develop–>Experimental Features on macOS and under Settings–>Safari–>Advanced–>Experimental Features on iOS and iPadOS. When you enable this mode and restart Safari, reports go out a mere 10 seconds after the triggering event instead of 24-48 hours later. This allows quick turnaround in testing and debugging.
The debug mode also enables debug output in Web Inspector’s console. This output will show up by default in a later beta.
Remember to disable debug mode once you’re done testing.
Future Enhancements
As is always the case with web standards, proposed or established, there are enhancement requests, corner cases, and a need to evolve the specification as the platform progresses. Below is a list of prominent and relevant issues that may show up as changes to our implementation of PCM in upcoming releases. Please take part on GitHub if you have input.
- Fraud prevention with unlinkable tokens, GitHub issue #27. A proposed solution was presented to W3C Privacy CG in May 2020. It will use what is traditionally called blinded signatures (we call them unlinkable tokens). The intention is to offer websites to cryptographically sign tokens which will be included in attribution reports in a format that makes it impossible to link them back to the event when they were signed. These tokens serve as proof to the report recipient that they trusted the events involved (link click and attribution trigger) without telling them which events.
- Modern JavaScript API for triggering event instead of legacy tracking pixels, GitHub issue #31. The intent here is to let a JavaScript call serve as the triggering event instead of redirected tracking pixels. This will remove the requirement for making third-party requests all together.
- Attribution reports to advertisers too, GitHub issue #53. We have expressed that we’d like the attribution report to be sent to both the click source and the advertiser site. However, this sparked a conversation on sending reports to designated third-parties and you can read and join that conversation in GitHub issue #57.
- Support PCM links in nested iframes, GitHub issue #7. This is about measuring click-through ads served in cross-site iframes. Since subsequent attribution reports will be sent to the first-party click source site, it’s not clear how that first party should control click measurement requested on its behalf. Part of this conversation covers not just serving of ads by third parties but also reporting to such third-parties. The privacy risk of such a scheme is explored in GitHub issue #57.
Misuse or Use Together With Tracking May Lead To Blocking
PCM is intended to support privacy-preserving measurement of clicks across websites or from apps to websites. It is not intended to be used to track users, events, or devices across those contexts.
If PCM is being misused for tracking purposes or being used in conjunction with unrelated means of tracking users, events, or devices, we may block the offending party from using PCM and potential future measurement features.
FAQ
- What about PCM web-to-app? We are interested in this but don’t have a solution yet.
- What about view-through ad attribution? We are interested in this but don’t have a privacy-preserving solution yet.
- Is there a reason why the click has to take the user to the device’s browser? Yes. Stored clicks are valid for 7 days. Let’s assume that the user doesn’t trigger attribution right after they click but want to think about it first. When they choose to re-engage a few hours or days later they will most likely go to their browser and either look up the tab where they left off, use a bookmark they might have saved, use their search provider to find the right webpage, or enter the website’s address directly in the URL bar. For the stored click data to be readily available when the user re-engages in this fashion, the initial click needs to take the user to their browser since PCM data just like other website data is not shared between browsers and WebViews. In short: The user’s browser is the most likely place where delayed click-through attribution will happen.
- Does use of PCM app-to-web require the app to be granted permission to track according to AppTrackingTransparency? No.
- How do users delete stored clicks? Stored clicks are deleted when they delete website data.
- Can users opt out of PCM? Yes. There is a new Safari privacy setting for privacy-preserving ad measurement. If the user has opted out, no click metadata will be stored and no attribution reports will be sent out.
- Is PCM enabled in Private Browsing Mode? No.
- What is the maximum number of parallel ad campaigns per source website or source app? 256, with the actual value being between 0 and 255.
- What is the maximum number of triggering events I can distinguish? 16, with the actual value being between 0 and 15.
- What is the maximum time between a click and a triggering event to still get attribution? 7 days.
- Can I use PCM app-to-web with WebViews? No. Apps have too much control over WebViews for a feature like PCM to be able to protect the data.
- Can I use PCM app-to-web with SFSafariViewController? We are interested in this but don’t have a solution yet.
- Can other default browsers on iOS and iPadOS participate in PCM app-to-web? It is our intention to add such an API at a later point. Please let us know if you are interested.
- Where can I provide feedback? Please file any web-facing issues or issues with the attribution report mechanism directly to WebKit: https://bugs.webkit.org. Please use Feedback Assistant for any issues with UIKit APIs or the Info.plist integration: https://developer.apple.com/bug-reporting/.
Thank You
We’d like to thank the W3C Privacy Community Group for all the work filing issues, suggesting changes, and engaging with us on this work. Please continue to do so as we move forward. Also, a big thank you to the engineers who’ve helped implement this feature – Anant, Kate, Jon, Chris, Jonathan, Chris, and Glen.