Attribution Reporting API developer's guide

As you read through the Privacy Sandbox on Android documentation, use the Developer Preview or Beta button to select the program version that you're working with, as instructions may vary.


Provide feedback

The Attribution Reporting API is designed to provide improved user privacy by removing reliance on cross-party user identifiers, and to support key use cases for attribution and conversion measurement across apps. This developer guide describes how to configure and test the Attribution Reporting APIs to register ad clicks, views, and conversions by calling methods that register the relevant triggers and sources for such events.

This guide teaches you how to set up server endpoints and build a client app that calls these services. Learn more about the overall design of Attribution Reporting API in the design proposal.

Key Terms

  • Attribution sources refer to clicks or views.
  • Triggers are events that can be attributed to conversions.
  • Reports contain data about a trigger and the corresponding attribution source. These reports are sent in response to trigger events. The Attribution Reporting API supports event-level reports and aggregatable reports.

Before you begin

To use the Attribution Reporting API, complete the server-side and client-side tasks listed in the following sections.

Set up Attribution Reporting API endpoints

The Attribution Reporting API requires a set of endpoints that you can access from a test device or emulator. Create one endpoint for each of the following server-side tasks:

There are several methods of setting up the required endpoints:

  • The fastest way to get up and running is deploy the OpenAPI v3 service definitions from our sample code repository to a mock or microservices platform. You can use Postman, Prism, or any other mock server platform that accepts this format. Deploy each endpoint and keep track of the URIs for use in your app. To verify report delivery, refer to the calls previously made to the mock or serverless platform.
  • Run your own standalone server using the Spring Boot-based Kotlin sample. Deploy this server on your cloud provider or internal infrastructure.
  • Use the service definitions as examples to integrate the endpoints into your existing system.

Accept source registration

This endpoint should be addressable from a URI similar to the following:

https://adtech.example/attribution_source

When a client app registers an attribution source, it provides the URI for this server endpoint. The Attribution Reporting API then makes a request and includes one of the following headers:

  • For click events:

    Attribution-Reporting-Source-Info: navigation
    
  • For view events:

    Attribution-Reporting-Source-Info: event
    

DP 10 and later source registration requests will contain a Version header as follows:

  Version: 202310

Configure your server endpoint to respond with the following:

// Metadata associated with attribution source.
Attribution-Reporting-Register-Source: {
  "destination": "[app package name]",
  "web_destination": "[eTLD+1]",
  "source_event_id": "[64 bit unsigned integer]",
  "expiry": "[64 bit signed integer]",
  "event_report_window": "[64-bit signed integer]",
  "aggregatable_report_window": "[64-bit signed integer]",
  "priority": "[64 bit signed integer]",
  "filter_data": {
    "[key name 1]": ["key1 value 1", "key1 value 2"],
    "[key name 2]": ["key2 value 1", "key2 value 2"],
    // Note: "source_type" key will be automatically generated as
    // one of {"navigation", "event"}.
  },
  // Attribution source metadata specifying histogram contributions in aggregate
  // report.
  "aggregation_keys": {
    "[key1 name]": "[key1 value]",
    "[key2 name]": "[key2 value]",
  },

    "debug_key": "[64-bit unsigned integer]",
    "debug_reporting": [boolean]
}
// Specify additional ad tech URLs to register this source with.
Attribution-Reporting-Redirect: <Ad Tech Partner URI 1>
Attribution-Reporting-Redirect: <Ad Tech Partner URI 2>

Here's an example with sample values added:

Attribution-Reporting-Register-Source: {
  "destination": "android-app://com.example.advertiser",
  "source_event_id": "234",
  "expiry": "259200",
  "event_report_window": "172800",
  "aggregatable_report_window": "172800",
  "priority": "5",
  "filter_data": {
    "product_id": ["1234"]
  },
  "aggregation_keys": {
  // Generates a "0x159" key piece named (low order bits of the key) for the key
  // named "campaignCounts".
  // User saw an ad from campaign 345 (out of 511).
    "campaignCounts": "0x159",

  // Generates a "0x5" key piece (low order bits of the key) for the key named
  // "geoValue".
  // Source-side geo region = 5 (US), out of a possible ~100 regions.
    "geoValue": "0x5",
  },
  // Opts in to receiving verbose debug reports
  "debug_reporting": true
}

Attribution-Reporting-Redirect:
https://adtechpartner1.example?their_ad_click_id=567
Attribution-Reporting-Redirect:
https://adtechpartner2.example?their_ad_click_id=890

If Attribution-Reporting-Redirects contains URIs of ad tech partners, the Attribution Reporting API then makes a similar request to each URI. Each ad tech partner must configure a server that responds with these headers:

Attribution-Reporting-Register-Source: {
  "destination": "[app package name]",
  "web_destination": "[eTLD+1]",
  "source_event_id": "[64 bit unsigned integer]",
  "expiry": "[64 bit signed integer]",
  "event_report_window": "[64-bit signed integer]",
  "aggregatable_report_window": "[64-bit signed integer]",
  "priority": "[64 bit signed integer]",
  "filter_data": {
    "[key name 1]": ["key1 value 1", "key1 value 2"],
    "[key name 2]": ["key2 value 1", "key2 value 2"],
    // Note: "source_type" key will be automatically generated as
    // one of {"navigation", "event"}.
  },
  "aggregation_keys": {
    "[key1 name]": "[key1 value]",
    "[key2 name]": "[key2 value]",
  }
}
// The Attribution-Reporting-Redirect header is ignored for ad tech partners.

Accept conversion trigger registration

This endpoint should be addressable from a URI similar to the following:

https://adtech.example/attribution_trigger

When a client app registers a trigger event, it provides the URI for this server endpoint. The Attribution Reporting API then makes a request to the provided URI.

DP 10 and later trigger registration requests will contain a Version header as follows:

  Version: 202310

Configure your server endpoint to respond with the following:

// Metadata associated with trigger.
Attribution-Reporting-Register-Trigger: {
  "event_trigger_data": [{
    // "trigger_data returned" in event reports is truncated to
    // the last 1 or 3 bits, based on conversion type.
    "trigger_data": "[unsigned 64-bit integer]",
    "priority": "[signed 64-bit integer]",
    "deduplication_key": "[signed 64-bit integer]",
    // "filter" and "not_filters" are optional fields which allow configuring
    // event trigger data based on source's filter_data. They consist of a
    // filter set, which is a list of filter maps. An event_trigger_data object
    // is ignored if none of the filter maps in the set match the source's
    // filter data.
    // Note: "source_type" can be used as a key in a filter map to filter based
    // on the source's "navigation" or "event" type. The first
    // Event-Trigger that matches (based on the filters/not_filters) will be
    // used for report generation. If none of the event-triggers match, no
    // event report will be generated.
    "filters": [{
      "[key name 1]": ["key1 value 1", "key1 value 2"],
      // If a key is missing from filters or source's filter_data, it won't be
      // used during matching.
      "[key name 2]": ["key2 value 1", "key2 value 2"],
    }],
    "not_filters":  [{
      "[key name 1]": ["key1 value 1", "key1 value 2"],
      // If a key is missing from not_filters or source's filter_data, it won't
      // be used during matching.
      "[key name 2]": ["key2 value 1", "key2 value 2"],
    }]
  }],
  // Specify a list of dictionaries that generates aggregation keys.
  "aggregatable_trigger_data": [
    // Each dictionary entry independently adds pieces to multiple source keys.
    {
      "key_piece": "[key piece value]",
      "source_keys": ["[key name the key piece value applies to]",
      ["list of IDs in source to match. Non-matching IDs are ignored"]]
      // filters/not_filters are optional fields similar to event trigger data
      // filter fields.
      "filters": [{
        "[key name 1]": ["key1 value 1", "key1 value 2"]
      }],
      "not_filters":  [{
          "[key name 1]": ["key1 value 1", "key1 value 2"],
          "[key name 2]": ["key2 value 1", "key2 value 2"],
      }]
    },
    ..
  ],
  // Specify an amount of an abstract value which can be integers in [1, 2^16]
  // to contribute to each key that is attached to aggregation keys in the
  // order they are generated.
  "aggregatable_values": [
     // Each source event can contribute a maximum of L1 = 2^16 to the
     // aggregate histogram.
    {
     "[key_name]": [value]
    },
    ..
  ],
  aggregatable_deduplication_keys: [{
  deduplication_key": [unsigned 64-bit integer],
    "filters": {
        "category": [filter_1, …, filter_H]
      },
    "not_filters": {
        "category": [filter_1, …, filter_J]
      }
  },
  ...
  {
  "deduplication_key": [unsigned 64-bit integer],
    "filters": {
        "category": [filter_1, …, filter_D]
      },
    "not_filters": {
        "category": [filter_1, …, filter_J]
      }
    }
  ]

  "debug_key": "[64-bit unsigned integer]",
  "debug_reporting": [boolean]

}
// Specify additional ad tech URLs to register this trigger with.
// Repeated Header field "Attribution-Reporting-Redirect"
Attribution-Reporting-Redirect: <Ad Tech Partner URI 1>
Attribution-Reporting-Redirect: <Ad Tech Partner URI 2>

Here's an example with sample values added:

Attribution-Reporting-Register-Trigger: {
  "event_trigger_data": [{
    "trigger_data": "1122", // Returns 010 for CTCs and 0 for VTCs in reports.
    "priority": "3",
    "deduplication_key": "3344"
    "filters": [{ // Filter strings can not exceed 25 characters
      "product_id": ["1234"],
      "source_type": ["event"]
    }]
  },
  {
    "trigger_data": "4", // Returns 100 for CTCs and 0 for VTCs in reports.
    "priority": "3",
    "deduplication_key": "3344"
    "filters": [{ // Filter strings can not exceed 25 characters
      "product_id": ["1234"],
      "source_type": ["navigation"]
    }]
  }],
  "aggregatable_trigger_data": [
    // Each dictionary independently adds pieces to multiple source keys.
    {
      // Conversion type purchase = 2 at a 9-bit offset, i.e. 2 << 9.
      // A 9-bit offset is needed because there are 511 possible campaigns,
      // which takes up 9 bits in the resulting key.
      "key_piece": "0x400",// Conversion type purchase = 2
      // Apply this key piece to:
      "source_keys": ["campaignCounts"]
       // Filter strings can not exceed 25 characters
    },
    {
      // Purchase category shirts = 21 at a 7-bit offset, i.e. 21 << 7.
      // A 7-bit offset is needed because there are ~100 regions for the geo
      // key, which takes up 7 bits of space in the resulting key.
      "key_piece": "0xA80",
      // Apply this key piece to:
      "source_keys": ["geoValue", "nonMatchingIdsAreIgnored"]
      // source_key values must not exceed the limit of 25 characters
    }
  ],
  "aggregatable_values":
    {
      // Privacy budget for each key is L1 / 2 = 2^15 (32768).
      // Conversion count was 1.
      // Scale the count to use the full budget allocated: 1 * 32768 = 32768.
      "campaignCounts": 32768,

      // Purchase price was $52.
      // Purchase values for the app range from $1 to $1,024 (integers only).
      // Scaling factor applied is 32768 / 1024 = 32.
      // For $52 purchase, scale the value by 32 ($52 * 32 = $1,664).
      "geoValue": 1664
    }
  ,
  // aggregatable_deduplication_keys is an optional field. Up to 50 “keys”
  // can be included in the aggregatable_deduplication_keys list. Filters, not
  // filters, and deduplication_key are optional fields. If deduplication_key
  // is omitted, it will be treated as a null value. See
  // https://wicg.github.io/attribution-reporting-api/#triggering-aggregatable-attribution
  aggregatable_deduplication_keys:
  [
    {
    deduplication_key": 3,
        "filters": {
          "category": [A]
        }
    },
    {
    "deduplication_key": 4,
        "filters": {
          "category": [C, D]
        },
        "not_filters": {
          "category": [F]
        }
    }
  ]
  // Opts into receiving verbose debug reports
  "debug_reporting": true
}
Attribution-Reporting-Redirect:https://adtechpartner.example?app_install=567

There is a limit of 25 bytes per aggregation key ID and filter string. This means that your aggregation key IDs and filter strings shouldn't exceed 25 characters. In this example, campaignCounts is 14 characters, so it is a valid aggregation key ID, and 1234 is 4 characters, so it is a valid filter string. If an aggregation key ID or filter string exceeds 25 characters, the trigger is ignored.

If Attribution-Reporting-Redirect contains URIs of ad tech partners, the Attribution Reporting API then makes a similar request to each URI. Each ad tech partner must configure a server that responds with these headers:

// Metadata associated with trigger.
Attribution-Reporting-Register-Trigger: {
  "event_trigger_data": [{
    // "trigger_data" returned in event reports is truncated to
    // the last 1 or 3 bits, based on conversion type.
    "trigger_data": "[unsigned 64-bit integer]",
    "priority": "[signed 64-bit integer]",
    "deduplication_key": "[signed 64-bit integer]",
    // filter and not_filters are optional fields which allow configuring
    // different event trigger data based on source's filter_data. They
    // consist of a filter set, which is a list of filter maps. An
    // event_trigger_data object is ignored if none of the filter maps in the
    // set match the source's filter data. Note: "source_type" can be used as
    // a key in a filter map to filter based on the source's "navigation" or
    // "event" type. The first Event-Trigger that matches (based on the
    // filters/not_filters) will be used for report generation. If none of the
    // event-triggers match, no report will be generated.
    "filters": [{
      "[key name 1]": ["key1 value 1", "key1 value 2"],
      // If a key is missing from filters or source's filter_data, it will not be
      // used during matching.
      "[key name 2]": ["key2 value 1", "key2 value 2"],
    }],
    "not_filters":  [{
      "[key name 1]": ["key1 value 1", "key1 value 2"],
      // If a key is missing from not_filters or source's filter_data, it will not
      // be used during matching.
      "[key name 2]": ["key2 value 1", "key2 value 2"],
    }]
  }],
  "aggregatable_trigger_data": [
    // Each dictionary entry independently adds pieces to multiple source keys.
    {
      "key_piece": "[key piece value]",
      "source_keys": ["[key name the key piece value applies to]",
      ["list of IDs in source to match. Non-matching IDs are ignored"]],
      // filters/not_filters are optional fields similar to event trigger data
      // filter fields.
      "filters": [{
        "[key name 1]": ["key1 value 1", "key1 value 2"]
      }],
      "not_filters":  [{
          "[key name 1]": ["key1 value 1", "key1 value 2"],
          "[key name 2]": ["key2 value 1", "key2 value 2"],
      }]
    },
    ..
  ],
  // Specify an amount of an abstract value which can be integers in [1, 2^16] to
  // contribute to each key that is attached to aggregation keys in the order they
  // are generated.
  "aggregatable_values": [
    // Each source event can contribute a maximum of L1 = 2^16 to the aggregate
    // histogram.
    {
     "[key_name]": [value]
    }
  ]
}
// The Attribution-Reporting-Redirect header is ignored for ad tech partners.

Accept event-level reports

This endpoint should be addressable from a URI. See Enroll for a Privacy Sandbox account for more information about enrolling URIs. (The URI is inferred from the origin of servers used for accepting source registration and trigger registration.) Using the example URIs for endpoints that accept source registration and accept trigger registration, this endpoint's URI is:

https://adtech.example/.well-known/attribution-reporting/report-event-attribution

DP 10 and later event level reports requests will contain a Version header as follows:

  Version: 202310

Configure this server to accept JSON requests that use the following format:

{
  "attribution_destination": "android-app://com.advertiser.example",
  "source_event_id": "12345678",
  "trigger_data": "2",
  "report_id": "12324323",
  "source_type": "navigation",
  "randomized_trigger_rate": "0.02"
   [Optional] "source_debug_key": "[64-bit unsigned integer]",
   [Optional] "trigger_debug_key": "[64-bit unsigned integer]",
}

Debug keys allow for additional insights into your attribution reports; learn more about configuring them.

Accept aggregatable reports

This endpoint should be addressable from a URI. See Enroll for a Privacy Sandbox account for more information about enrolling URIs. (The URI is inferred from the origin of servers used for accepting source registration and trigger registration.) Using the example URIs for endpoints that accept source registration and accept trigger registration, this endpoint's URI is:

https://adtech.example/.well-known/attribution-reporting/report-aggregate-attribution

DP 10 and later aggregatable reports requests will contain a Version header as follows:

  Version: 202310

Both the encrypted and unencrypted fields are populated for aggregatable reports. The encrypted reports enable you to begin testing with the aggregation service, while the unencrypted field provides insight on the way set key-value pairs are structuring the data.

Configure this server to accept JSON requests that use the following format:

{
  // Info that the aggregation services also need encoded in JSON
  // for use with AEAD. Line breaks added for readability.
  "shared_info": "{
     \"api\":\"attribution-reporting\",
     \"attribution_destination\": \"android-app://com.advertiser.example.advertiser\",
     \"scheduled_report_time\":\"[timestamp in seconds]\",
     \"source_registration_time\": \"[timestamp in seconds]\",
     \"version\":\"[api version]\",
     \"report_id\":\"[UUID]\",
     \"reporting_origin\":\"https://reporter.example\" }",

  // In the current Developer Preview release, The "payload" and "key_id" fields
  // are not used because the platform does not yet encrypt aggregate reports.
  // Currently, the "debug_cleartext_payload" field holds unencrypted reports.
  "aggregation_service_payloads": [
    {
      "payload": "[base64 HPKE encrypted data readable only by the aggregation service]",
      "key_id": "[string identifying public key used to encrypt payload]",

      "debug_cleartext_payload": "[unencrypted payload]"
    },
  ],

  "source_debug_key": "[64 bit unsigned integer]",
  "trigger_debug_key": "[64 bit unsigned integer]"
}

Debug keys allow for additional insights into your attribution reports; learn more about configuring them.

Set up Android client

The client app registers attribution sources and triggers, and enables event-level and aggregatable report generation. To prepare an Android client device or emulator for using the Attribution Reporting API, do the following:

  1. Set up your development environment for the Privacy Sandbox on Android.
  2. Install a system image onto a supported device or set up an emulator that includes support for the Privacy Sandbox on Android.
  3. Enable access to the Attribution Reporting API by running the following ADB command. (The API is disabled by default.)

    adb shell device_config put adservices ppapi_app_allow_list \"\*\"
    
  4. If you are locally testing the Attribution Reporting API (such as testing on a device you have access to physically), run this command to disable enrollment:

    adb shell device_config put adservices disable_measurement_enrollment_check "true"
    
  5. Include the ACCESS_ADSERVICES_ATTRIBUTION permission in your Android Manifest file and create an ad services configuration for your app to use the Attribution Reporting APIs:

    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" />
    
  6. (Optional) If you plan to receive debug reports, include the ACCESS_ADSERVICES_AD_ID permission in your Android Manifest file:

    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" />
    
  7. Reference an ad services configuration in the <application> element of your manifest:

    <property android:name="android.adservices.AD_SERVICES_CONFIG"
              android:resource="@xml/ad_services_config" />
    
  8. Specify the ad services XML resource referenced in the manifest, such as res/xml/ad_services_config.xml. Learn more about ad services permissions and SDK access control.

    <ad-services-config>
        <attribution allowAllToAccess="true" />
    </ad-services-config>
    

Register ad events

Your app should register sources and conversions as they occur to ensure that they are properly reported. The MeasurementManager class features methods to help you register attribution source events and conversion triggers.

Register an attribution source event

When an ad is viewed or clicked, a publisher app calls registerSource() to register an attribution source as shown in the code snippet.

The Attribution Reporting API supports the following types of attribution source events:

  • Clicks, which you typically register within a callback method similar to onClick(). The corresponding trigger event usually occurs soon after the click event. This type of event provides more information about user interaction and is therefore a good type of attribution source to give a high priority.
  • Views, which you typically register within a callback method similar to onAdShown(). The corresponding trigger event might occur hours or days after the view event.

Kotlin

companion object {
    private val CALLBACK_EXECUTOR = Executors.newCachedThreadPool()
}

val measurementManager = context.getSystemService(MeasurementManager::class.java)
var exampleClickEvent: InputEvent? = null

// Use the URI of the server-side endpoint that accepts attribution source
// registration.
val attributionSourceUri: Uri =
  Uri.parse("https://adtech.example/attribution_source?AD_TECH_PROVIDED_METADATA")

val future = CompletableFuture<Void>()

adView.setOnTouchListener(_: View?, event: MotionEvent?)) ->
    exampleClickEvent = event
    true
}

// Register Click Event
measurementManager.registerSource(
        attributionSourceUri,
        exampleClickEvent,
        CALLBACK_EXECUTOR,
        future::complete)

// Register View Event
measurementManager.registerSource(
        attributionSourceUri,
        null,
        CALLBACK_EXECUTOR,
        future::complete)

Java

private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool();
private InputEvent exampleClickEvent;

MeasurementManager measurementManager =
        context.getSystemService(MeasurementManager.class);

// Use the URI of the server-side endpoint that accepts attribution source
// registration.
Uri attributionSourceUri =
Uri.parse("https://adtech.example/attribution_source?AD_TECH_PROVIDED_METADATA");

CompletableFuture<Void> future = new CompletableFuture<>();

adView.setOnTouchListener(v, event)) -> {
    exampleClickEvent = event;
    return true;
}

// Register Click Event
measurementManager.registerSource(attributionSourceUri, exampleClickEvent,
        CALLBACK_EXECUTOR, future::complete);

// Register View Event
measurementManager.registerSource(attributionSourceUri, null,
        CALLBACK_EXECUTOR, future::complete);

After registration, the API issues an HTTP POST request to the service endpoint at the address specified by attributionSourceUri. The endpoint's response includes values for destination, source_event_id, expiry, and source_priority.

If the originating ad tech wishes to share source registrations, the original attribution source URI can include redirects to other ad tech endpoints. Limits and rules applicable to the redirects are detailed in the technical proposal.

Support has been added for daisy-chain redirects for registerSource and registerTrigger. In addition to the registration header, the API consumer can now provide an HTTP redirect as the server response that includes a 302 status code and a "Location" header with the next URL to visit for an additional registration.

Only the "destination" field provided in the very first visit is used across the daisy-chain. The number of visits has the same limit as the "Attribution-Reporting-Redirect" headers. This redirect support is in addition to the existing "Attribution-Reporting-Redirect" support, and if both are present, "Attribution-Reporting-Redirect" gets preference.

Register a conversion trigger event

To register a conversion trigger event, call registerTrigger() in your app:

Kotlin

companion object {
    private val CALLBACK_EXECUTOR = Executors.newCachedThreadPool()
}

val measurementManager = context.getSystemService(MeasurementManager::class.java)

// Use the URI of the server-side endpoint that accepts trigger registration.
val attributionTriggerUri: Uri =
    Uri.parse("https://adtech.example/trigger?AD_TECH_PROVIDED_METADATA")

val future = CompletableFuture<Void>()

// Register trigger (conversion)
measurementManager.registerTrigger(
        attributionTriggerUri,
        CALLBACK_EXECUTOR,
        future::complete)

Java

private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool();

MeasurementManager measurementManager =
        context.getSystemService(MeasurementManager.class);

// Use the URI of the server-side endpoint that accepts trigger registration.
Uri attributionTriggerUri =
        Uri.parse("https://adtech.example/trigger?AD_TECH_PROVIDED_METADATA");

CompletableFuture<Void> future = new CompletableFuture<>();

// Register trigger (conversion)
measurementManager.registerTrigger(
        attributionTriggerUri,
        CALLBACK_EXECUTOR,
        future::complete)

After registration, the API issues an HTTP POST request to the service endpoint at the address specified by attributionTriggerUri. The endpoint's response includes values for event and aggregate reports.

If the originating ad tech platform allows trigger registrations to be shared, the URI can include redirects to URIs that belong to other ad tech platforms. The limits and rules applicable to the redirects are detailed in the technical proposal.

Register cross app and web measurement

In the case where both an app and browser play a part in the user's journey from source to trigger, there are subtle differences in the implementation of registering ad events. If a user sees an ad on an app and is redirected to a browser for a conversion, the source is registered by the app, and the conversion by the web browser. Similarly, if a user starts on a web browser and is directed to an app for conversion, the browser registers the source, and the app registers the conversion.

Since there are differences in the way ad techs are organized on the web and on Android, we have added new APIs to register sources and triggers when they take place on browsers. The key difference between these APIs and the corresponding app-based APIs is that we expect the browser to follow the redirects, apply any browser-specific filters, and pass on the valid registrations to the platform by calling registerWebSource() or registerWebTrigger().

The following code snippet shows an example of the API call that the browser makes to register an attribution source before directing the user to an app:

Kotlin

companion object {
    private val CALLBACK_EXECUTOR = Executors.newCachedThreadPool()
}

val measurementManager =
        context.getSystemService(MeasurementManager::class.java)
var exampleClickEvent: InputEvent? = null

// Use the URIs of the server-side endpoints that accept attribution source
// registration.
val sourceParam1 = WebSourceParams.Builder(Uri.parse(
        "https://adtech1.example/attribution_source?AD_TECH_PROVIDED_METADATA"))
// True, if debugging is allowed for the ad tech.
    .setDebugKeyAllowed(true)
    .build()

val sourceParam2 = WebSourceParams.Builder(Uri.parse(
        "https://adtech2.example/attribution_source?AD_TECH_PROVIDED_METADATA"))
    .setDebugKeyAllowed(false)
    .build()

val sourceParam3 = WebSourceParams.Builder(Uri.parse(
        "https://adtech3.example/attribution_source?AD_TECH_PROVIDED_METADATA"))
    .build()

val sourceParams = Arrays.asList(sourceParam1, sourceParam2, sourceParam3)
val publisherOrigin = Uri.parse("https://publisher.example")
val appDestination = Uri.parse("android-app://com.example.store")
val webDestination = Uri.parse("https://example.com")

val future = CompletableFuture<Void>()

adView.setOnTouchListener {_: View?, event: MotionEvent? ->
    exampleClickEvent = event
    true
}
val clickRegistrationRequest = WebSourceRegistrationRequest.Builder(
          sourceParams,
          publisherOrigin)
      .setAppDestination(appDestination)
      .setWebDestination(webDestination)
      .setInputEvent(event)
      .build()
val viewRegistrationRequest = WebSourceRegistrationRequest.Builder(
          sourceParams,
          publisherOrigin)
      .setAppDestination(appDestination)
      .setWebDestination(webDestination)
      .setInputEvent(null)
      .build()

// Register a web source for a click event.
measurementManager.registerWebSource(
        clickRegistrationRequest,
        CALLBACK_EXECUTOR,
        future::complete)

// Register a web source for a view event.
measurementManager.registerWebSource(
        viewRegistrationRequest,
        CALLBACK_EXECUTOR,
        future::complete)

Java

private static final Executor CALLBACK_EXECUTOR =
        Executors.newCachedThreadPool();
private InputEvent exampleClickEvent;

MeasurementManager measurementManager =
        context.getSystemService(MeasurementManager.class);

// Use the URIs of the server-side endpoints that accept attribution source
// registration.
WebSourceParams sourceParam1 = WebSourceParams.Builder(Uri.parse(
        "https://adtech1.example/attribution_source?AD_TECH_PROVIDED_METADATA"))
    // True, if debugging is allowed for the ad tech.
    .setDebugKeyAllowed(true)
    .build();

WebSourceParams sourceParam2 = WebSourceParams.Builder(Uri.parse(
        "https://adtech2.example/attribution_source?AD_TECH_PROVIDED_METADATA"))
    .setDebugKeyAllowed(false)
    .build();

WebSourceParams sourceParam3 = WebSourceParams.Builder(Uri.parse(
        "https://adtech3.example/attribution_source?AD_TECH_PROVIDED_METADATA"))
    .build();

List<WebSourceParams> sourceParams =
        Arrays.asList(sourceParam1, sourceParam2, sourceParam3);
Uri publisherOrigin = Uri.parse("https://publisher.example");
Uri appDestination = Uri.parse("android-app://com.example.store");
Uri webDestination = Uri.parse("https://example.com");

CompletableFuture<Void> future = new CompletableFuture<>();

adView.setOnTouchListener(v, event) -> {
    exampleClickEvent = event;
    return true;
}

WebSourceRegistrationRequest clickRegistrationRequest =
        new WebSourceRegistrationRequest.Builder(sourceParams, publisherOrigin)
    .setAppDestination(appDestination)
    .setWebDestination(webDestination)
    .setInputEvent(event)
    .build();
WebSourceRegistrationRequest viewRegistrationRequest =
        new WebSourceRegistrationRequest.Builder(sourceParams, publisherOrigin)
    .setAppDestination(appDestination)
    .setWebDestination(webDestination)
    .setInputEvent(null)
    .build();

// Register a web source for a click event.
measurementManager.registerWebSource(clickRegistrationRequest,
        CALLBACK_EXECUTOR, future::complete);

// Register a web source for a view event.
measurementManager.registerWebSource(viewRegistrationRequest,
        CALLBACK_EXECUTOR, future::complete);

The following code snippet shows an example of the API call that the browser makes to register a conversion after the user has been directed from the app:

Kotlin

companion object {
    private val CALLBACK_EXECUTOR = Executors.newCachedThreadPool()
}

val measurementManager = context.getSystemService(MeasurementManager::class.java)

// Use the URIs of the server-side endpoints that accept trigger registration.
val triggerParam1 = WebTriggerParams.Builder(Uri.parse(
        "https://adtech1.example/trigger?AD_TECH_PROVIDED_METADATA"))
    // True, if debugging is allowed for the ad tech.
    .setDebugKeyAllowed(true)
    .build()

val triggerParam2 = WebTriggerParams.Builder(Uri.parse(
        "https://adtech2.example/trigger?AD_TECH_PROVIDED_METADATA"))
    .setDebugKeyAllowed(false)
    .build()

val triggerParams = Arrays.asList(triggerParam1, triggerParam2)
val advertiserOrigin = Uri.parse("https://advertiser.example")

val future = CompletableFuture<Void>()

val triggerRegistrationRequest = WebTriggerRegistrationRequest.Builder(
        triggerParams,
        advertiserOrigin)
    .build()

// Register the web trigger (conversion).
measurementManager.registerWebTrigger(
    triggerRegistrationRequest,
    CALLBACK_EXECUTOR,
    future::complete)

Java

private static final Executor CALLBACK_EXECUTOR =
        Executors.newCachedThreadPool();

MeasurementManager measurementManager =
        context.getSystemService(MeasurementManager.class);

// Use the URIs of the server-side endpoints that accept trigger registration.
WebTriggerParams triggerParam1 = WebTriggerParams.Builder(Uri.parse(
        "https://adtech1.example/trigger?AD_TECH_PROVIDED_METADATA"))
    // True, if debugging is allowed for the ad tech.
    .setDebugKeyAllowed(true)
    .build();

WebTriggerParams triggerParam2 = WebTriggerParams.Builder(Uri.parse(
        "https://adtech2.example/trigger?AD_TECH_PROVIDED_METADATA"))
    .setDebugKeyAllowed(false)
    .build();

List<WebTriggerParams> triggerParams =
        Arrays.asList(triggerParam1, triggerParam2);
Uri advertiserOrigin = Uri.parse("https://advertiser.example");

CompletableFuture<Void> future = new CompletableFuture<>();

WebTriggerRegistrationRequest triggerRegistrationRequest =
        new WebTriggerRegistrationRequest.Builder(
            triggerParams, advertiserOrigin)
    .build();

// Register the web trigger (conversion).
measurementManager.registerWebTrigger( triggerRegistrationRequest,
        CALLBACK_EXECUTOR, future::complete);

Adding noising for privacy

Event-level reports contain destination, attribution source ID and trigger data. They are sent in the original (unencrypted) format to the reporting origin. To protect user's privacy, noise can be added to make it more difficult to identify an individual user. Noised event-level reports are generated and sent in accordance with the differential-privacy framework. These are the default noise percentage values for different scenarios:

Source type

Source destination value

Noised report probability per source registration

View

Either app or web

0.0000025

View

App and web

0.0000042

Click

Either app or web

0.0024263

Click

App and web

0.0170218

In app-to-web attribution measurement, where sources can drive conversion to both app and web destinations, event-level reports can specify whether the trigger occurred on app or web. To compensate for this additional detail, the noised reports generated are up to ~7x for clicks and ~1.7x for views.

Some ad techs don't require event-level reports to specify whether the trigger occurred on either the app or web destination. Ad techs can use the coarse_event_report_destinations field under the Attribution-Reporting-Register-Source header to reduce noise. If a source with the coarse_event_report_destinations field specified wins attribution, the resulting report includes both app and web destinations without distinction as to where the actual trigger occurred.

In the following examples, a user clicks on an ad, and that source is registered with the API. The user then converts on both the advertiser app and the advertiser's website. Both of these conversions are registered as triggers and attributed to the initial click.

A click-based source registration HTTP header:

Attribution-Reporting-Register-Source: {
    "destination": "android-app://com.advertiser.example",
    "web_destination": "https://advertiser.com",
    "source_event_id": "234",
    "expiry": "60000",
    "priority": "5",
    // Ad tech opts out of receiving app-web destination distinction
    // in event report, avoids additional noise
    "coarse_event_report_destinations": "true"
}

A trigger is registered from the app with the package name com.advertiser.example:

Attribution-Reporting-Register-Trigger: {
    "event_trigger_data": [{
    "trigger_data": "1",
    "priority": "1"
    }],
}

A trigger is registered from a browser from the website with the eTLD+1 domain https://advertiser.com:

Attribution-Reporting-Register-Trigger: {
    "event_trigger_data": [{
    "trigger_data": "2",
    "priority": "2"
    }],
}

The resulting event-level reports are generated. Assuming both triggers get attributed to the source, the following event-level reports are generated:

  {
    "attribution_destination": ["android-app://com.advertiser.example,https://advertiser.com"],
    "scheduled_report_time": "800176400",
    "source_event_id": "53234",
    "trigger_data": "1",
    // Can be "event" if source were registered by user viewing the ad
    "source_type": "navigation",
    // Would be 0.0170218 without coarse_event_report_destinations as true in the source
    "randomized_trigger_rate": 0.0024263
  }

Generate and deliver reports

The Attribution Reporting API sends reports to the endpoints on your server that accept event-level reports and aggregatable reports.

Force reporting jobs to run

After you register an attribution source event or register a trigger event, the system schedules the reporting job to run. By default, this job runs every 4 hours. For testing purposes, you can force the reporting jobs to run or shorten the intervals between jobs.

Force attribution job to run:

adb shell cmd jobscheduler run -f com.google.android.adservices.api 5

Force event-level reporting job to run:

adb shell cmd jobscheduler run -f com.google.android.adservices.api 3

Force aggregatable reporting job to run:

adb shell cmd jobscheduler run -f com.google.android.adservices.api 7

Check the output in logcat to see when the jobs have run. It should look something like the following:

JobScheduler: executeRunCommand(): com.google.android.adservices.api/0 5 s=false f=true

Force delivery of reports

Even if the reporting job is forced to run, the system still sends out reports according to their scheduled delivery times, which range from a couple of hours to several days. For testing purposes, you can advance the device system time to be after the scheduled delays to initiate the report delivery.

Verify reports on your server

Once reports are sent, verify delivery by checking received reports, applicable server logs such as the mock server history or your custom system.

Decode your aggregate report

When receiving an aggregate report, the debug_cleartext_payload field holds an unencrypted version of your aggregate report. While this version of your report is unencrypted, it still needs to be decoded.

Below is an example of decoding the contents of the debug_cleartext_payload field in two steps: the first using Base 64 decoding, and the second using CBOR decoding.

String base64DebugPayload  = "omRkYXRhgqJldmFsdWVEAAAGgGZidWNrZXRQAAAAAAAAAAAAAAAAAAAKhaJldmFsdWVEAACAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAFWWlvcGVyYXRpb25paGlzdG9ncmFt";
byte[] cborEncoded = Base64.getDecoder().decode(base64DebugPayload);

// CbodDecoder comes from this library https://github.com/c-rack/cbor-java
final List<DataItem> dataItems = new CborDecoder(new ByteArrayInputStream(cborEncoded)).decode();

// In here you can see the contents, but the value will be something like:
// Data items: [{ data: [{ value: co.nstant.in.cbor.model.ByteString@a8b5c07a,
//   bucket: co.nstant.in.cbor.model.ByteString@f812097d },
//   { value: co.nstant.in.cbor.model.ByteString@a8b5dfc0,
//   bucket: co.nstant.in.cbor.model.ByteString@f8120934 }], operation: histogram }]
Log.d("Data items : " + dataItems);

// In order to see the value for bucket and value, you can traverse the data
// and get their values, something like this:
final Map payload = (Map) dataItems.get(0);
final Array payloadArray = (Array) payload.get(new UnicodeString("data"));

payloadArray.getDataItems().forEach(i -> {
    BigInteger value = new BigInteger(((ByteString) ((Map)i).get(new UnicodeString("value"))).getBytes());
    BigInteger bucket = new BigInteger(((ByteString) ((Map)i).get(new UnicodeString("bucket"))).getBytes());
    Log.d("value : " + value + " ;bucket : " + bucket);
});

Testing

To help you get started with the Attribution Reporting API, you can use the MeasurementSampleApp project on GitHub. This sample app demonstrates attribution source registration and trigger registration.

For server endpoints, consider the following reference resources or your custom solution:

  • MeasurementAdTechServerSpec includes OpenAPI service definitions, which can be deployed to a supported mock or microservices platforms.
  • MeasurementAdTechServer includes a reference implementation of a mock server based on Spring Boot app for Google App Engine.

Prerequisites

Deploy mock APIs on remote endpoints accessible from your test device or emulator. For ease of testing, refer to the MeasurementAdTechServerSpec and MeasurementAdTechServer sample projects.

Functionality to test

  • Exercise attribution source and conversion trigger registrations. Check that server-side endpoints respond with the correct format.
  • Execute reporting jobs.
  • Verify delivery of reports on your test server's backend or console.

Upcoming features

Flexible event-level configuration

The default configuration for event level reporting is advised for starting utility testing, but may not be ideal for all use cases. The Attribution Reporting API will support optional, more flexible configurations so that ad techs have increased control over the structure of their event level reports and are able to maximize the utility of the data. This additional flexibility will be introduced into the Attribution Reporting API in two phases:

  • Phase 1: Lite flexible event level configuration; a subset of Phase 2.
  • Phase 2: Full version of flexible event level configuration.

Phase 1: Lite flexible event level

We will add the following two optional parameters to the JSON in Attribution-Reporting-Register-Source:

  • max_event_level_reports
  • event_report_windows
{
  ...
  // Optional. This is a parameter that acts across all trigger types for the
  // lifetime of this source. It restricts the total number of event-level
  // reports that this source can generate. After this maximum is hit, the
  // source is no longer capable of producing any new data. The use of
  // priority in the trigger attribution algorithm in the case of multiple
  // attributable triggers remains unchanged. Defaults to 3 for navigation
  // sources and 1 for event sources
  "max_event_level_reports": <int>,

  // Optional. Represents a series of time windows, starting at 0. Reports
  // for this source will be delivered an hour after the end of each window.
  // Time is encoded as seconds after source registration. If
  // event_report_windows is omitted, will use the default windows. This
  // field is mutually exclusive with the existing `event_report_window` field.
  // // End time is exclusive.
  "event_report_windows": {
    "start_time": <int>,
    "end_times": [<int>, ...]
  }
}

Custom configuration example

This example configuration supports a developer who wants to optimize for receiving reports at earlier reporting windows.

{
  ...
  "max_event_level_reports": 2,
  "event_report_windows": {
    "end_times": [7200, 43200, 86400] // 2 hours, 12 hours, 1 day in seconds
  }
}

Phase 2: Full flexible event level

In addition to the parameters that were added in Phase 1, we will add an additional optional parameter trigger_specs to the JSON in Attribution-Reporting-Register-Source.

{
  // A trigger spec is a set of matching criteria, along with a scheme to
  // generate bucketized output based on accumulated values across multiple
  // triggers within the specified event_report_window. There will be a limit on
  // the number of specs possible to define for a source.
  "trigger_specs": [{
    // This spec will only apply to registrations that set one of the given
    // trigger data values (non-negative integers) in the list.
    // trigger_data will still appear in the event-level report.
    "trigger_data": [<int>, ...]

    // Represents a series of time windows, starting at the source registration
    // time. Reports for this spec will be delivered an hour after the end of
    // each window. Time is encoded as seconds after source registration.
    // end_times must consist of strictly increasing positive integers.
    //
    // Note: specs with identical trigger_data cannot have overlapping windows;
    // this ensures that triggers match at most one spec. If
    // event_report_windows is omitted, will use the "event_report_window" or
    // "event_report_windows" field specified at the global level for the source
    // (or the default windows if none are specified). End time is exclusive.
    "event_report_windows": {
      "start_time": <int>,
      "end_times": [<int>, ...],
    }

    // Represents an operator that summarizes the triggers within a window
    // count: number of triggers attributed within a window
    // value_sum: sum of the value of triggers within a window
    // The summary is reported as an index into a bucketization scheme. Defaults
    // to "count"
    "summary_window_operator": <one of "count" or "value_sum">,

    // Represents a bucketization of the integers from [0, MAX_INT], encoded as
    // a list of integers where new buckets begin (excluding 0 which is
    // implicitly included).
    // It must consist of strictly increasing positive integers.
    //
    // e.g. [5, 10, 100] encodes the following ranges:
    // [[0, 4], [5, 9], [10, 99], [100, MAX_INT]]
    //
    // At the end of each reporting window, triggers will be summarized into an
    // integer which slots into one of these ranges. Reports will be sent for
    // every new range boundary that is crossed. Reports will never be sent for
    // the range that includes 0, as every source is initialized in this range.
    //
    // If omitted, then represents a trivial mapping
    // [1, 2, ... , MAX_INT]
    // With MAX_INT being the maximum int value defined by the browser.
    "summary_buckets": [<bucket start>, ...]
  }, {
    // Next trigger_spec
  } ...],

  // See description in phase 1.
  "max_event_level_reports": <int>
  // See description in phase 1.
  "event_report_windows": {
    "start_time": <int>,
    "end_times": [<int>, ...]
  }
}

This configuration fully specifies the output space of the event-level reports, per source registration. For every trigger spec, we fully specify:

  • A set of matching criteria:
    • Which specific trigger data this spec applies to. This source is eligible to be matched only with triggers that have one of the specified trigger_data values in the trigger_specs. In other words, if the trigger would have matched this source but its trigger_data is not one of the values in the source's configuration, the trigger is ignored.
    • When a specific trigger matches this spec (using event_report_windows). Note that the trigger could still be matched with a source for aggregatable reports despite failing the two match criteria mentioned earlier.
  • A specific algorithm for summarizing and bucketizing all the triggers within an attribution window. This allows triggers to specify a value parameter that gets summed up for a particular spec, but reported as a bucketed value.

Triggers will also support adding an optional value parameter in the dictionaries within event_trigger_data.

{
  "event_trigger_data": [
    {
      "trigger_data": "2",
      "value": 100,  // Defaults to 1
      "filters": ...
    },
    ...
  ]
}

Every trigger registration will match with at most one trigger spec and update its associated summary value. At a high level, at trigger time we will:

  • Apply global attribution filters.
  • For every trigger spec, evaluate the event_trigger_data on the spec to find a match, using the spec's event_reporting_window. The top level event_reporting_windows acts as a default value in case any trigger spec is the missing event_report_windows sub-field.
  • The first matched spec is chosen for attribution, and the summary value is incremented by value.

When the event_report_window for a spec completes, we will map its summary value to a bucket, and send an event-level report for every increment in the summary bucket caused by attributed trigger values. Reports will come with one extra field, trigger_summary_bucket.

{
  ...
  "trigger_summary_bucket": [<bucket start>, <bucket end>],
}

Configurations that are equivalent to the current version

The following are equivalent configurations for the APIs current event and navigation sources, respectively. Especially for navigation sources, this illustrates why the noise levels are so high relative to event sources to maintain the same epsilon values: navigation sources have a much larger output space.

It is possible that there are multiple configurations that are equivalent, given that some parameters can be set as default or pruned.

Equivalent event sources
// Note: most of the fields here are not required to be explicitly listed.
// Here we list them explicitly just for clarity.
{
  "trigger_specs": [
  {
    "trigger_data": [0, 1],
    "event_report_windows": {
      "end_times": [<30 days>]
    },
    "summary_window_operator": "count",
    "summary_buckets": [1],
  }],
  "max_event_level_reports": 1,
  ...
  // expiry must be greater than or equal to the last element of the end_times
  "expiry": <30 days>,
}
Equivalent navigation sources
// Note: most of the fields here are not required to be explicitly listed.
// Here we list them explicitly just for clarity.
{
  "trigger_specs": [
  {
    "trigger_data": [0, 1, 2, 3, 4, 5, 6, 7],
    "event_report_windows": {
      "end_times": [<2 days>, <7 days>, <30 days>]
    },
    "summary_window_operator": "count",
    "summary_buckets": [1, 2, 3],
  }],
  "max_event_level_reports": 3,
  ...
  // expiry must be greater than or equal to the last element of the end_times
  "expiry": <30 days>,
}

Example custom configurations

Below are some additional configurations outside the defaults. In all of these examples, the developer trade-offs include:

  • reducing some dimension of the default configuration (#triggers, trigger data cardinality, #windows) for increasing another one to preserve the noise level
  • reducing some dimension of the default configuration (#triggers, trigger data cardinality, #windows) for reduced noise level

Report trigger value buckets

This example configuration supports a developer who wants to optimize for value data for only one reporting window (e.g. 7 days), trading fewer reporting windows for less noise. In this example any trigger that sets trigger_data to a value other than 0 is ineligible for attribution.

{
  "trigger_specs": [
  {
    "trigger_data": [0],
    "event_report_windows": {
      "end_times": [604800, 1209600] // 7 days, 14 days represented in seconds
    },
    "summary_window_operator": "value_sum",
    "summary_buckets": [5, 10, 100]
  }],
}

Triggers could be registered with the value field set, which are summed up and bucketed. For example if there are three triggers within 7 days of source registrations with values 1, 3 and 4.

{ "event_trigger_data": [{"trigger_data": "0", "value": 1}] }
{ "event_trigger_data": [{"trigger_data": "0", "value": 3}] }
{ "event_trigger_data": [{"trigger_data": "0", "value": 4}] }

The values are summed to 8 and reported in the following reports after 7 days + 1 hour:

// Report 1
{
  ...
  "trigger_summary_bucket": [5, 9]
}

In the subsequent 7 days, the following triggers are registered:

{ "event_trigger_data": [{"trigger_data": "0", "value": 50}] }
{ "event_trigger_data": [{"trigger_data": "0", "value": 45}] }

The values are summed to 8 + 50 + 45 = 103. This yields the following reports at 14 days + 1 hour:

// Report 2
{
  ...
  "trigger_summary_bucket": [10, 99]
},

// Report 3
{
  ...
  "trigger_summary_bucket": [100, MAX_INT]
}
Report trigger counts

This example shows how a developer can configure a source to get a count of triggers up to 10.

{
  "trigger_specs": [
  {
    "trigger_data": [0],
    "event_report_windows": {
      "end_times": [604800] // 7 days represented in seconds
    },
    // This field could be omitted to save bandwidth since the default is "count"
    "summary_window_operator": "count",
    "summary_buckets": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  }],
}

Attributed triggers with trigger_data set to 0 are counted and capped at 10. The trigger value is ignored since summary_window_operator is set to count. If 4 triggers are registered and attributed to the source, the report would look like this:

// Report 1
{
  ...
  "trigger_summary_bucket": [1, 1]
}
// Report 2
{
  ...
  "trigger_summary_bucket": [2, 2]
}
// Report 3
{
  ...
  "trigger_summary_bucket": [3, 3]
}
// Report 4
{
  ...
  "trigger_summary_bucket": [4, 4]
}
Binary with more frequent reporting

This example configuration supports a developer who wants to learn whether at least one conversion occurred in the first 10 days (regardless of value), but wants to receive reports at more frequent intervals than the default. Again, in this example any trigger that sets trigger_data to a value other than 0 is ineligible for attribution. This is why this use case is referred to as binary.

{
  "trigger_specs": [
  {
    "trigger_data": [0],
    "event_report_windows": {
      // 1 day, 2 days, 3 days, 5 days, 7 days, 10 days represented in seconds
      "end_times": [86400, 172800, 259200, 432000, 604800, 864000]
    },
    // This field could be omitted to save bandwidth since the default is "count"
    "summary_window_operator": "count",
    "summary_buckets": [1]
  }],
}
Vary trigger specs from source to source
{
  "trigger_specs": [
  {
    "trigger_data": [0, 1, 2, 3],
    "event_report_windows": {
      "end_times": [172800, 604800, 2592000] // 2 days, 7 days, 30 days represented in seconds
    }
  }],
  "max_event_level_reports": 3
}
{
  "trigger_specs": [
  {
    "trigger_data": [4, 5, 6, 7],
    "event_report_windows": {
      "end_times": [172800, 604800, 2592000] // 2 days, 7 days, 30 days represented in seconds
    }
  }],
  "max_event_level_reports": 3
}

We encourage developers to suggest different use cases they may have for this API extension, and we will update this explainer with sample configurations for those use cases.

Cross network attribution without redirects

Ad techs should use redirects to register multiple attribution source triggers and to perform cross-network attribution. This feature helps support cross-network attribution where redirects aren't feasible across networks. Learn more.

Ad techs can send configuration in the trigger registration response based on which sources registered by other ad techs are selected to generate derived sources; these derived sources are then used for attribution. Aggregate reports are generated if the trigger gets attributed to a derived source. Event report generation for derived sources is not supported.

Ad techs can choose from the aggregation_keys in their registered sources that they intend to share with partner ad techs. These keys can be declared in the optional shared_aggregation_keys field, located under the source registration header Attribution-Reporting-Register-Source:

"shared_aggregation_keys": ["[key name1]", "[key name2]"]

Derived sources are generated based on the configuration under the trigger registration header Attribution-Reporting-Register-Trigger:

  // Specifies the configuration based on which derived sources should be
  // generated. Those derived sources will be included for source matching at the
  // time of attribution. For example, if adtech2 is registering a trigger with an
  // attribution_config with source_network as adtech1, available sources
  // registered by adtech1 will be considered with additional filtering criteria
  // applied to that set as mentioned in the attribution_config. Derived
  // sources can have different values to priority, post_install_exclusivity_window
  // etc.

  "attribution_config": [
    {
      // Derived sources are created from this adtech's registered sources
      "source_network": "[original source's adtech enrollment ID]",
      //(optional) Filter sources whose priority falls in this range
      "source_priority_range": {
        "start": [priority filter lower bound],
        "end": [priority filter upper bound]
      },
      // (optional) Filter sources whose at least one of filter maps matches these
      // filters
      "source_filters": {
        "key name 1": ["key1 value 1"]
      },
      // (optional) Filter sources whose none of filter map matches these
      // filters
        "source_not_filters": {
          "key name 1": ["key1 value 1"]
        },
      // (optional) Apply this priority to the generated derived sources
      "priority": "[64 bit signed integer]",
      // (optional) The derived source will have expiry set as this or parent
      // source's, whichever is earlier
      "expiry": "[64 bit signed integer]",
      // (optional) set on the derived source
      "filter_data": {
        "key name 1": ["key1 value 1"]
      },
      // (optional) set on the derived source
      "post_install_exclusivity_window": "[64-bit unsigned integer]"
    }
  ]

Here's a version with example values added:

  "attribution_config": [
    {
      "source_network": "adtech1-enrollment-id",
      "source_priority_range": {
        "start": 50,
        "end": 100
      },
      "source_filters": {
        "source_type": ["NAVIGATION"]
      },
      "source_not_filters": {
        "product_id": ["789"]
      },
      "priority": "30",
      "expiry": "78901",
      // (optional) set on the derived source
      "filter_data": {
        "product_id": ["1234"]
        },
      // (optional) set on the derived source
      "post_install_exclusivity_window": "7890"
    }
  ]

Two new optional fields are added to trigger registration header. These fields enable the winning ad tech's identifier in aggregatable report keys:

  • x_network_bit_mapping: Enrollment id to ad tech identifier bit mapping
  • x_network_data: Offset (left shift) for the winning ad tech's x_network_bit_mapping OR operation with the trigger key piece
Example:
"Attribution-Reporting-Register-Trigger": {
  "attribution_config": [...],
  "aggregatable_trigger_data": [
    {
     "key_piece": "0x400",
     "source_keys": ["campaignCounts"]
      "x_network_data" : {
        "key_offset" : 12 // [64 bit unsigned integer]
      }
    }
    …
  ]
  …
  "x_network_bit_mapping": {
   // This mapping is used to generate trigger key pieces with AdTech identifier
   // bits. eg. If AdTechA's sources wins the attribution then 0x1 here will be
   // OR'd with the trigger key pieces to generate the final key piece.
    "AdTechA-enrollment_id": "0x1", // Identifier bits in hex for A
    "AdTechB-enrollment_id": "0x2"  // Identifier bits in hex for B
  }
  …
}

Here's the resulting trigger key piece calculation when generating a report for AdTechB's source:

  • key_piece: 0x400 (010000000000)
  • key_offset: 12
  • AdtechB's enrollment_id value: 2 (010) (from x_network_bit_mapping)
  • Resultant Trigger Key piece: 0x400 | 0x2 << 12 = 0x2400

Limitations

For a list of in-progress capabilities for the SDK Runtime, view the release notes.

Report bugs and issues

Your feedback is a crucial part of the Privacy Sandbox on Android! Let us know of any issues you find or ideas for improving Privacy Sandbox on Android.