Shared Storage and Private Aggregation Implementation Quickstart

This document is a quickstart guide for using Shared Storage and Private Aggregation. You'll need an understanding of both APIs because Shared Storage stores the values and Private Aggregation creates the aggregatable reports.

Target Audience: Ad techs and measurement providers.

Try the demo

Try out the live demo. Follow the steps in the demo instructions to enable the Privacy Sandbox APIs. Opening Chrome DevTools helps you visualize the results of different use cases. Use cases available in the demo:

  • Private Aggregation
    • Unique Reach Measurement
    • Demographics measurement
    • K+ frequency measurement
  • General Usage
    • Measure hover-over event inside fenced frames
    • Top-level navigation
    • Controlling where third parties can write

How to view Shared Storage

To view what is stored in Shared Storage, use Chrome DevTools. Stored data can be found in Application -> Shared Storage.

View data stored in Shared Storage using Chrome DevTools.

View reports for Private Aggregation

To view the aggregatable reports sent, navigate to chrome://private-aggregation-internals. When debug mode is enabled, a report is sent immediately (without a delay) to [[YOUR_ORIGIN]]/.well-known/private-aggregation/debug/report-shared-storage along with the time-delayed report to be sent to [[YOUR_ORIGIN]]/.well-known/private-aggregation/report-shared-storage.

To enable debugging, follow the instructions in the debugging section.

Viewing reports in chrome://private-aggregation-internals.

Shared Storage API

To prevent cross-site tracking, browsers have started partitioning all forms of storage, including local storage, cookies, and so forth. But there are use cases where unpartitioned storage is required. The Shared Storage API provides unlimited write access across different top-level sites with privacy-preserving read access.

Shared Storage is restricted to the context origin (the caller of sharedStorage).

Shared Storage has a capacity limit per origin, with each entry limited to a maximum number of characters. If the limit is reached, no further inputs are stored. The data storage limits are outlined in the Shared Storage explainer.

Invoking Shared Storage

Ad techs can write to Shared Storage using JavaScript or response headers. Reading from Shared Storage only occurs within an isolated JavaScript environment called a worklet.

  • Using JavaScript Ad techs can perform specific Shared Storage functions such as setting, appending, and deleting values outside of a JavaScript worklet. However, functions such as reading Shared Storage and performing Private Aggregation have to be completed through a JavaScript worklet. Methods that can be used outside of a JavaScript worklet can be found in Proposed API Surface - Outside the worklet.

    Methods that are used in the worklet during an operation can be found in Proposed API Surface - In the worklet.

  • Using response headers

    Similar to JavaScript, only specific functions such as setting, appending, and deleting values in Shared Storage can be done using response headers. To work with Shared Storage in a response header, Shared-Storage-Writable: ?1 has to be included in the request header.

    To initiate a request from the client, run the following code, depending on your chosen method:

    • Using fetch()

      fetch("https://a.example/path/for/updates", {sharedStorageWritable: true});
      
    • Using an iframe or img tag

      <iframe src="https://a.example/path/for/updates" sharedstoragewritable></iframe>
      
    • Using an IDL attribute with an iframe or img tag

      let iframe = document.getElementById("my-iframe");
      iframe.sharedStorageWritable = true;
      iframe.src = "https://a.example/path/for/updates";
      

Further information can be found in Shared Storage: Response Headers.

Writing to Shared Storage

To write to Shared Storage, call sharedStorage.set() from inside or outside a JavaScript worklet. If called from outside the worklet, the data is written to the origin of the browsing context that the call was made from. If called from inside the worklet, the data is written to the origin of the browsing context that loaded the worklet. The keys that are set have an expiration date of 30 days from last update.

The ignoreIfPresent field is optional. If present and set to true, the key is not updated if it already exists. Key expiration is renewed to 30 days from the set() call even if the key is not updated.

If Shared Storage is accessed multiple times in the same page load with the same key, the value for the key is overwritten. It's a good idea to use sharedStorage.append() if the key needs to maintain the previous value.

  • Using JavaScript

    Outside the worklet:

    window.sharedStorage.set('myKey', 'myValue1', { ignoreIfPresent: true });
    // Shared Storage: {'myKey': 'myValue1'}
    window.sharedStorage.set('myKey', 'myValue2', { ignoreIfPresent: true });
    // Shared Storage: {'myKey': 'myValue1'}
    window.sharedStorage.set('myKey', 'myValue2', { ignoreIfPresent: false });
    // Shared Storage: {'myKey': 'myValue2'}
    

    Similarly, inside the worklet:

    sharedStorage.set('myKey', 'myValue1', { ignoreIfPresent: true });
    
  • Using response headers

    You can also write to Shared Storage using response headers. To do so, use Shared-Storage-Write in the response header along with the following commands:

    Shared-Storage-Write : set;key="myKey";value="myValue";ignore_if_present
    
    Shared-Storage-Write : set;key="myKey";value="myValue";ignore_if_present=?0
    

    Multiple items can be comma-separated and can combine set, append, delete, and clear.

    Shared-Storage-Write :
    set;key="hello";value="world";ignore_if_present, set;key="good";value="bye"
    

Appending a value

You can append a value to an existing key using the append method. If the key does not exist, calling append() creates the key and sets the value. This can be accomplished using JavaScript or response headers.

  • Using JavaScript

    To update values of existing keys, use sharedStorage.append() from either inside or outside the worklet.

    window.sharedStorage.append('myKey', 'myValue1');
    // Shared Storage: {'myKey': 'myValue1'}
    window.sharedStorage.append('myKey', 'myValue2');
    // Shared Storage: {'myKey': 'myValue1myValue2'}
    window.sharedStorage.append('anotherKey', 'hello');
    // Shared Storage: {'myKey': 'myValue1myValue2', 'anotherKey': 'hello'}
    

    To append inside the worklet:

    sharedStorage.append('myKey', 'myValue1');
    
  • Using response headers

    Similar to setting a value in Shared Storage, you can use the Shared-Storage-Write in the response header to pass in the key-value pair.

    Shared-Storage-Write : append;key="myKey";value="myValue2"
    

Reading from Shared Storage

You can read from Shared Storage only from within a worklet.

await sharedStorage.get('mykey');

The origin of the browsing context that the worklet module was loaded from determines whose Shared Storage is read.

Deleting from Shared Storage

You can perform deletes from Shared Storage using JavaScript from either inside or outside the worklet or by using response headers with delete(). To delete all keys at once, use clear() from either.

  • Using JavaScript

    To delete from Shared Storage from outside the worklet:

    window.sharedStorage.delete('myKey');
    

    To delete from Shared Storage from inside the worklet:

    sharedStorage.delete('myKey');
    

    To delete all keys at once from outside the worklet:

    window.sharedStorage.clear();
    

    To delete all keys at once from inside the worklet:

    sharedStorage.clear();
    
  • Using response headers

    To delete values using response headers, you can also use Shared-Storage-Write in the response header to pass the key to be deleted.

    delete;key="myKey"
    

    To delete all keys using response headers:

    clear;
    

Context switching

Shared Storage data is written to the origin (for example, https://example.adtech.com) of the browsing context that the call originated from.

When you load the third-party code using a <script> tag, the code is executed in the browsing context of the embedder. Therefore, when the third-party code calls sharedStorage.set(), the data is written to the embedder's Shared Storage. When you load the third-party code within an iframe, the code receives a new browsing context, and its origin is the origin of the iframe. Therefore, the sharedStorage.set() call made from the iframe stores the data into the Shared Storage of the iframe origin.

First-party context

If a first-party page has embedded third-party JavaScript code that calls sharedStorage.set() or sharedStorage.delete(), the key-value pair is stored in the first-party context.

Data stored in a first-party page with embedded third-party JavaScript.

Third-party context

The key-value pair can be stored in the ad tech or third-party context by creating an iframe and calling set() or delete() in the JavaScript code from within the iframe.

Data stored in the ad-tech or third-party context.

Private Aggregation API

To measure aggregatable data stored in Shared Storage, you can use the Private Aggregation API.

To create a report, call contributeToHistogram() inside a worklet with a bucket and value. The bucket is represented by an unsigned 128-bit integer which must be passed into the function as a BigInt. The value is a positive integer.

To protect privacy, the report's payload, which contains the bucket and value, is encrypted in transit, and it can only be decrypted and aggregated using the Aggregation Service.

The browser will also limit the contributions a site can make to an output query. Specifically, the contribution budget limits the total of all reports from a single site for a given browser in a given time window across all buckets. If the current budget is exceeded, a report will not be generated.

privateAggregation.contributeToHistogram({
  bucket: BigInt(myBucket),
  value: parseInt(myBucketValue)
});

Executing Shared Storage and Private Aggregation

You must create a worklet to access data from shared storage. To do this, call createWorklet() with the URL of the worklet. By default, when using shared storage with createWorklet(), the data partition origin will be the invoking browsing context's origin, not the origin of the worklet script itself.

To change the default behaviour, set the dataOrigin property when calling createWorklet.

  • dataOrigin: "context-origin": (Default) Data is stored in the shared storage of the invoking browsing context's origin.
  • dataOrigin: "script-origin": Data is stored in the shared storage of the worklet script's origin. Note that an opt-in is required to enable this mode.
sharedStorage.createWorklet(scriptUrl, {dataOrigin: "script-origin"});

To opt-in, when using "script-origin", the script endpoint will have to respond with the header Shared-Storage-Cross-Origin-Worklet-Allowed. Note that CORS should be enabled for cross-origin requests.

Shared-Storage-Cross-Origin-Worklet-Allowed : ?1

Using cross-origin iframe

An iframe is needed to invoke the shared storage worklet.

In the ad's iframe, load the worklet module by calling addModule(). To run the method that is registered in the sharedStorageWorklet.js worklet file, in the same ad iframe JavaScript, call sharedStorage.run().

const sharedStorageWorklet = await window.sharedStorage.createWorklet(
  'https://any-origin.example/modules/sharedStorageWorklet.js'
);
await sharedStorageWorklet.run('shared-storage-report', {
  data: { campaignId: '1234' },
});

In the worklet script, you will need to create a class with an async run method and register it to run in the ad's iframe. Inside sharedStorageWorklet.js:

class SharedStorageReportOperation {
  async run(data) {
    // Other code goes here.
    bucket = getBucket(...);
    value = getValue(...);
    privateAggregation.contributeToHistogram({
      bucket,
      value
    });
  }
}
register('shared-storage-report', SharedStorageReportOperation);

Using cross-origin request

Shared Storage and Private Aggregation allows creation of cross-origin worklets without the need for cross-origin iframes.

The first-party page can also invoke a createWorklet() call to the cross-origin javascript endpoint. You will need to set the data partition origin of the worklet to be that of the script-origin when creating the worklet.

async function crossOriginCall() {
  const privateAggregationWorklet = await sharedStorage.createWorklet(
    'https://cross-origin.example/js/worklet.js',
    { dataOrigin: 'script-origin' }
  );
  await privateAggregationWorklet.run('pa-worklet');
}
crossOriginCall();

The cross-origin javascript endpoint will have to respond with the headers Shared-Storage-Cross-Origin-Worklet-Allowed and note that CORS is enabled for the request.

Shared-Storage-Cross-Origin-Worklet-Allowed : ?1

Worklets created using the createWorklet() will have selectURL and run(). addModule() is not available for this.

class CrossOriginWorklet {
  async run(data){
    // Other code goes here.
    bucket = getBucket(...);
    value = getValue(...);
    privateAggregation.contributeToHistogram({
      bucket,
      value
    });
  }
}

Debugging

To enable debugging, call the enableDebugMode() JavaScript method in the same context where Shared Storage and Private Aggregation is used. This will be applied for future reports in the same context.

privateAggregation.enableDebugMode();

To associate the reports with the contexts that triggered them, you can set a 64-bit unsigned integer debug key which is passed to the JavaScript call. The debugKey is a BigInt.

privateAggregation.enableDebugMode({debugKey: 1234});

Debugging Shared Storage

Shared Storage returns a generic error message:

Promise is rejected without and explicit error message

You can debug Shared Storage by wrapping the calls with try-catch blocks.

try {
  privateAggregation.contributeToHistogram({bucket, value});
} catch (e){
  console.log(e);
}

Debugging Private Aggregation

Reports are sent to /.well-known/private-aggregation/report-shared-storage and /.well-known/private-aggregation/debug/report-shared-storage. Debug reports receive a payload similar to the following JSON. This payload defines the api field as "shared-storage".

{
   "aggregation_coordinator_origin": "https://publickeyservice.msmt.gcp.privacysandboxservices.com",
   "aggregation_service_payloads": [ {
      "debug_cleartext_payload": "omRkYXRhlKJldmFsdWVEAAAAgGZidWNrZXRQAAAAAAAAAAAAAAAAB1vNFaJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAKJldmFsdWVEAAAAAGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAGlvcGVyYXRpb25paGlzdG9ncmFt",
      "key_id": "1569ab37-da44-4a26-80d9-5ed6524ab2a7",
      "payload": "/9nHrWn1MnJWRxFvanbubciWE9mPyIij6uYLi5k351eQCd3/TZpe2knaatUNcniq4a4e61tmKebv50OmMRZFnnCfcAwIdIgLHu1a3en97PojqWJBfO52RiVMIcP7KQTLzMxq2LhvPSdV4zjXo1Teu/JuIK3LIyis3vUMpS+tUAX0QV+I6X5SVmZFiNW9aMb8DwLOtqrBy5JJ/EkOIY0G+1Fi1/3R7UtKsqM1o71A/OzdmlNkwO7EV/VUNinGvWnd19FvDHe/kqkNdTHKbhAnMmbZzHW9bsEQS81leElCla6BTdbdbeeFU/jbTj0AOaoNOIe5r7FU5NG6nW4ULXTCbLLjTQ1mtl3id3IP41Zin1JvABCDC/HUSgLFz8EUqkmbMIOlMfNYA79aURq6FqE0GO0HtICYf0GPNdVv7p4jY3FNn6+JS4l5F3t+3lP9ceo4IpCE+31jzMtYJ+19xFh6C5ufteBR/iknZFcc1w3caQBhgRl5jt8DbaOzYcW4690H8Ul4Oh2wRO+6/njifk+pExLay/O5swLi2lUUph5OUEaaztwwzh2mnhwIBxMkPnfsGihiF+5KDEajVfMZ3NLsIDoZO+l4RTZrkqE+jVkAqaZGBiCIx42Edp/JV0DXfrryypCdQBZr8iEbSzCM9hKsMfLN7S/VkPe5rDwOZbhKCn5XXgfGz5tSx/KbZgsQf4OCEhwAyNPHAh3MHU7xmkQ3pKg4EIUC/WOtKAlVDOtDMmPPoQY1eAwJhw9SxZaYF1kHjUkTm3EnGhgXgOwCRWqeboNenSFaCyp6DbFNI3+ImONMi2oswrrZO+54Tyhca5mnLIiInI+C3SlP4Sv1jFECIUdf/mifJRM5hMj6OChzHf4sEifjqtD4A30c4OzGexWarie2xakdQej9Go4Lm0GZEDBfcAdWLT9HwmpeI2u4HDAblXDvLN8jYFDOOtzOl90oU7AwdhkumUCFLRadXAccXW9SvLfDswRkXMffMJLFqkRKVE1GPonFFtlzaRqp7IgE8L6AOtz6NDcxAjHnEuzDPPMcWMl1AFH0gq7h"
   } ],
   "debug_key": "1234",
   "shared_info": "{\"api\":\"shared-storage\",\"debug_mode\":\"enabled\",\"report_id\":\"80d93c0a-a94e-4ab7-aeb5-a4ecd4bfc598\",\"reporting_origin\":\"https://privacy-sandbox-demos-dsp.dev\",\"scheduled_report_time\":\"1717784740\",\"version\":\"0.1\"}"
}

Debug cleartext payload

The debug_cleartext_payload is Base64 CBOR-encoded. You can view the bucket and value using the decoder or use the JavaScript code found in the Shared Storage decoder.

Next steps

The following pages explain important aspects of the Shared Storage and Private Aggregation APIs.

Once you are acquainted with the APIs, you can start collecting the reports, which are sent as a POST request to the following endpoints as JSON in the request body.

  • Debug Reports - context-origin/.well-known/private-aggregation/debug/report-shared-storage
  • Reports - context-origin/.well-known/private-aggregation/report-shared-storage

Once reports are collected, you can test using the local testing tool or set up the Trusted Execution Environment for Aggregation Service to get the aggregated reports.

Share your feedback

You can share your feedback on the APIs and documentation on GitHub.