├── .firebaserc ├── .github └── workflows │ ├── firebase-hosting-merge.yml │ └── firebase-hosting-pull-request.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── firebase.json ├── functions ├── .env.local ├── .env.production ├── .gitignore ├── app │ ├── content-producer │ │ ├── index.js │ │ ├── paa │ │ │ └── scripts │ │ │ │ ├── private-aggregation-test-worklet.js │ │ │ │ ├── private-aggregation-test.html │ │ │ │ └── private-aggregation-test.js │ │ └── setup-paa-test-routes.js │ ├── helpers │ │ └── setup-view.js │ ├── home │ │ └── index.js │ ├── publisher-a │ │ └── index.js │ └── publisher-b │ │ └── index.js ├── index.js ├── package-lock.json ├── package.json └── view │ ├── content-producer │ ├── demographics-survey.hbs │ ├── index.hbs │ └── payment-provider.hbs │ ├── home │ └── index.hbs │ ├── main.hbs │ ├── partials │ └── creativeRotationDemoControls.hbs │ ├── publisher-a │ ├── ab-testing.hbs │ ├── creative-rotation.hbs │ ├── creative-selection-by-frequency.hbs │ ├── demographics-measurement.hbs │ ├── hover-event.hbs │ ├── index.hbs │ ├── k-freq-measurement.hbs │ ├── known-customer.hbs │ ├── reach-measurement.hbs │ ├── third-party-write.hbs │ └── top-level-nav.hbs │ └── publisher-b │ ├── ab-testing.hbs │ ├── creative-rotation.hbs │ ├── creative-selection-by-frequency.hbs │ ├── index.hbs │ ├── k-freq-measurement.hbs │ ├── known-customer.hbs │ └── reach-measurement.hbs ├── package-lock.json ├── package.json ├── prettier.config.js └── sites ├── 404.html ├── content-producer ├── .well-known │ └── privacy-sandbox-attestations.json ├── ads │ ├── ad-1.html │ ├── ad-2.html │ ├── ad-3.html │ ├── ad.css │ ├── buy-now-button.html │ ├── default-ad.html │ ├── example-ad.html │ ├── experiment-ad-a.html │ ├── experiment-ad-b.html │ ├── hover-ad.html │ ├── nav-ad-1.html │ ├── nav-ad-2.html │ ├── nav-ad-3.html │ └── register-button.html ├── assets │ ├── ad-1.png │ ├── ad-2.png │ ├── ad-3.png │ ├── buy-now-button.png │ ├── default-ad.png │ ├── example-ad.png │ ├── experiment-a.png │ ├── experiment-b.png │ └── register-button.png ├── demo.css ├── index.css ├── private-aggregation │ ├── demographics-measurement-worklet.js │ ├── demographics-measurement.html │ ├── demographics-measurement.js │ ├── demographics-survey.css │ ├── demographics-survey.js │ ├── k-freq-measurement-worklet.js │ ├── k-freq-measurement.html │ ├── k-freq-measurement.js │ ├── reach-measurement-worklet.js │ ├── reach-measurement.html │ └── reach-measurement.js ├── url-selection │ ├── ab-testing-worklet.js │ ├── ab-testing.html │ ├── ab-testing.js │ ├── creative-rotation-worklet.js │ ├── creative-rotation.js │ ├── creative-selection-by-frequency-worklet.js │ ├── creative-selection-by-frequency.html │ ├── creative-selection-by-frequency.js │ ├── hover-event-worklet.js │ ├── hover-event.html │ ├── hover-event.js │ ├── known-customer-worklet.js │ ├── known-customer.html │ ├── known-customer.js │ ├── top-level-nav-worklet.js │ ├── top-level-nav.html │ └── top-level-nav.js └── use-cases │ ├── third-party-write.html │ └── third-party-write.js ├── home ├── 404.html ├── assets │ └── privacy-sandbox-logo.png ├── decoder │ ├── decoder.css │ ├── decoder.js │ └── index.html └── index.css ├── index.html ├── publisher-a ├── .well-known │ └── privacy-sandbox-attestations.json └── index.css └── publisher-b ├── .well-known └── privacy-sandbox-attestations.json └── index.css /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "shared-storage-demo" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | on: 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: FirebaseExtended/action-hosting-deploy@v0 15 | with: 16 | repoToken: ${{ secrets.GITHUB_TOKEN }} 17 | firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SHARED_STORAGE_DEMO }} 18 | channelId: live 19 | projectId: shared-storage-demo 20 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | on: pull_request 6 | permissions: 7 | checks: write 8 | contents: read 9 | pull-requests: write 10 | jobs: 11 | build_and_preview: 12 | if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: FirebaseExtended/action-hosting-deploy@v0 17 | with: 18 | repoToken: ${{ secrets.GITHUB_TOKEN }} 19 | firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SHARED_STORAGE_DEMO }} 20 | projectId: shared-storage-demo 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shared storage demo 2 | 3 | This demo shows some [Shared Storage API](https://developer.chrome.com/docs/privacy-sandbox/shared-storage/) use cases. 4 | 5 | The following use-cases have been added: 6 | 7 | - Creative selection by frequency 8 | - A/B testing 9 | - Creative rotation 10 | - Known customer 11 | 12 | ## Live demo 13 | 14 | Visit [https://shared-storage-demo.web.app](https://shared-storage-demo.web.app) to see the demo. 15 | 16 | ## Local development 17 | 18 | ### Setup HTTPS 19 | 20 | The ad rendered in a fenced frame must be served from an HTTPS origin. However the Firebase emulator [does not support HTTPS localhost](https://github.com/firebase/firebase-tools/issues/1908). Therefore, we will use `nginx` to setp up a reverse proxy. 21 | 22 | We need to setup nginx to respond to port 4437 that proxies content from port 3007. 23 | 24 | #### Generate the certs with `mkcert` 25 | 26 | 1. Install `mkcert` by following the [instructions for your operating system](https://github.com/FiloSottile/mkcert#installation). 27 | 1. Run `mkcert -install`. 28 | 1. Create a folder to store the certificates in. In this example, we will use `mkdir ~/certs`. 29 | 1. Navigate to the certificate folder: `cd ~/certs`. 30 | 1. Run `mkcert localhost`. 31 | 32 | > For an in-depth explanation of this section, see the ["How to use HTTPS for local development"](https://web.dev/how-to-use-local-https/) article. 33 | 34 | #### Setup reverse proxy with [nginx](https://www.nginx.com/) 35 | 36 | 1. Install `nginx` ([Mac](https://www.google.com/search?q=install+nginx+mac), [Linux](https://www.google.com/search?q=install+nginx+linux), [Windows](https://www.google.com/search?q=install+nginx+windows)). 37 | 1. Find the `nginx` configuration file location based on your operating system (If you used `homebrew` on Mac, it is under `/Users/[USER-NAME]/homebrew/etc/nginx/nginx.conf`). 38 | 1. Add the 4437 `server` block into the `http` block in the config. Replace `[USER-NAME]` with the path that your certificate is stored in: 39 | 40 | ```nginx 41 | http { 42 | # HTTPS server 43 | server { 44 | listen 4437 ssl; 45 | ssl_certificate /Users/[USER-NAME]/certs/localhost.pem; 46 | ssl_certificate_key /Users/[USER-NAME]/certs/localhost-key.pem; 47 | 48 | location / { 49 | proxy_set_header Host $host; 50 | proxy_set_header X-Real-IP $remote_addr; 51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 52 | proxy_set_header X-Forwarded-Proto $scheme; 53 | 54 | proxy_pass http://localhost:3007/; 55 | proxy_read_timeout 90; 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | 4. Stop the `nginx` server with `nginx -s stop` 62 | 5. Restart the `nginx` server with `nginx` 63 | 64 | The above `nginx` configuration proxies `https://localhost:4437` to `http://localhost:3007` (Content producer server). 65 | 66 | ### Setup Firebase 67 | 68 | - Setup [Firebase tools](https://github.com/firebase/firebase-tools) 69 | 70 | ### Setup repository 71 | 72 | - `git clone https://github.com/googlechromelabs/shared-storage-demo` 73 | - `cd shared-storage-demo` 74 | - `npm install` 75 | 76 | ### Start emulator 77 | 78 | ```bash 79 | npm run dev 80 | ``` 81 | 82 | ### Shared storage 83 | 84 | To run this demo, enable the experiment flag **chrome://flags/#privacy-sandbox-enrollment-overrides**. 85 | 86 | Then, [open Google Chrome from the command line](https://www.chromium.org/developers/how-tos/run-chromium-with-flags/) with the additional flags below to disable the need for attestations when running locally. 87 | 88 | ``` 89 | --enable-privacy-sandbox-ads-apis --disable-features=EnforcePrivacySandboxAttestations 90 | ``` 91 | 92 | And visit `http://localhost:3000` for the main page 93 | 94 | ### Deploy code 95 | 96 | ``` 97 | npm run deploy 98 | ``` 99 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": [ 3 | { 4 | "site": "shared-storage-demo", 5 | "public": "sites/home", 6 | "ignore": [ 7 | "firebase.json", 8 | "**/.*", 9 | "**/node_modules/**" 10 | ], 11 | "rewrites": [ 12 | { 13 | "source": "**", 14 | "function": "home" 15 | } 16 | ] 17 | }, 18 | { 19 | "site": "shared-storage-demo-publisher-a", 20 | "public": "sites/publisher-a", 21 | "ignore": [ 22 | "firebase.json", 23 | "**/.*", 24 | "**/node_modules/**" 25 | ], 26 | "rewrites": [ 27 | { 28 | "source": "**", 29 | "function": "publisherA" 30 | } 31 | ] 32 | }, 33 | { 34 | "site": "shared-storage-demo-publisher-b", 35 | "public": "sites/publisher-b", 36 | "ignore": [ 37 | "firebase.json", 38 | "**/.*", 39 | "**/node_modules/**" 40 | ], 41 | "rewrites": [ 42 | { 43 | "source": "**", 44 | "function": "publisherB" 45 | } 46 | ] 47 | }, 48 | { 49 | "site": "shared-storage-demo-content-producer", 50 | "public": "sites/content-producer", 51 | "ignore": [ 52 | "firebase.json", 53 | "**/.*", 54 | "**/node_modules/**" 55 | ], 56 | "rewrites": [ 57 | { 58 | "source": "**", 59 | "function": "contentProducer" 60 | }, 61 | { 62 | "source": "/.well-known/**", 63 | "function": "contentProducer" 64 | } 65 | ], 66 | "headers": [ 67 | { 68 | "source": "**", 69 | "headers": [ 70 | { 71 | "key": "Supports-Loading-Mode", 72 | "value": "fenced-frame" 73 | }, 74 | { 75 | "key": "Access-Control-Allow-Origin", 76 | "value": "*" 77 | }, 78 | { 79 | "key": "Shared-Storage-Cross-Origin-Worklet-Allowed", 80 | "value": "?1" 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | "emulators": { 88 | "functions": { 89 | "port": "5080" 90 | }, 91 | "hosting": { 92 | "host": "localhost", 93 | "port": "3000" 94 | }, 95 | "ui": { 96 | "enabled": true 97 | } 98 | }, 99 | "functions": { 100 | "source": "functions" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /functions/.env.local: -------------------------------------------------------------------------------- 1 | DEMO_HOME_URL="http://localhost:3000" 2 | CONTENT_PRODUCER_URL="https://localhost:4437" 3 | PUBLISHER_A_URL="http://localhost:3005" 4 | PUBLISHER_B_URL="http://localhost:3006" 5 | -------------------------------------------------------------------------------- /functions/.env.production: -------------------------------------------------------------------------------- 1 | DEMO_HOME_URL="https://shared-storage-demo.web.app" 2 | CONTENT_PRODUCER_URL="https://shared-storage-demo-content-producer.web.app" 3 | PUBLISHER_A_URL="https://shared-storage-demo-publisher-a.web.app" 4 | PUBLISHER_B_URL="https://shared-storage-demo-publisher-b.web.app" 5 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/app/content-producer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const express = require('express'); 17 | const functions = require('firebase-functions'); 18 | const setupView = require('../helpers/setup-view'); 19 | const setupPrivateAggregationTestRoutes = require('./setup-paa-test-routes'); 20 | 21 | // Read the dev or prod URLs to be used 22 | require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` }); 23 | 24 | // Setup app and view 25 | const app = setupView(express(), 'content-producer'); 26 | 27 | // Setup Private Aggregation API test routes 28 | setupPrivateAggregationTestRoutes(app); 29 | 30 | // Setup root route 31 | app.get('/', (req, res) => { 32 | const { DEMO_HOME_URL, PUBLISHER_A_URL, PUBLISHER_B_URL, CONTENT_PRODUCER_URL, PAYMENT_PROVIDER_URL } = process.env; 33 | 34 | res.render('index', { 35 | demoHomeUrl: DEMO_HOME_URL, 36 | publisherAUrl: PUBLISHER_A_URL, 37 | publisherBUrl: PUBLISHER_B_URL, 38 | contentProducerUrl: CONTENT_PRODUCER_URL, 39 | paymentProvider: PAYMENT_PROVIDER_URL, 40 | }); 41 | }); 42 | 43 | app.get('/payment-provider', (req, res) => { 44 | const { DEMO_HOME_URL, PUBLISHER_A_URL, PUBLISHER_B_URL, CONTENT_PRODUCER_URL, PAYMENT_PROVIDER_URL } = process.env; 45 | 46 | res.render('payment-provider', { 47 | demoHomeUrl: DEMO_HOME_URL, 48 | publisherAUrl: PUBLISHER_A_URL, 49 | publisherBUrl: PUBLISHER_B_URL, 50 | contentProducerUrl: CONTENT_PRODUCER_URL, 51 | paymentProvider: PAYMENT_PROVIDER_URL, 52 | }); 53 | }); 54 | 55 | app.get('/demographics-survey', (req, res) => { 56 | const { DEMO_HOME_URL, PUBLISHER_A_URL, PUBLISHER_B_URL, CONTENT_PRODUCER_URL, PAYMENT_PROVIDER_URL } = process.env; 57 | 58 | res.render('demographics-survey', { 59 | demoHomeUrl: DEMO_HOME_URL, 60 | publisherAUrl: PUBLISHER_A_URL, 61 | publisherBUrl: PUBLISHER_B_URL, 62 | contentProducerUrl: CONTENT_PRODUCER_URL, 63 | paymentProvider: PAYMENT_PROVIDER_URL, 64 | }); 65 | }); 66 | 67 | app.post('/report/hover', (req, res) => { 68 | console.log('\x1b[1;31m%s\x1b[0m', `🚀 You have received an event-level report from the browser`); 69 | console.log('QUERY PARAMS RECEIVED (event-level):\n=== \n', req.query, '\n=== \n'); 70 | console.log('PAYLOAD RECEIVED (event-level):\n=== \n', req.body, '\n=== \n'); 71 | res.sendStatus(200); 72 | }); 73 | 74 | exports.contentProducer = functions.https.onRequest(app); 75 | -------------------------------------------------------------------------------- /functions/app/content-producer/paa/scripts/private-aggregation-test-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const DEBUG_MODE_CHANCE = 0.5; 18 | 19 | class PrivateAggregationTest { 20 | async run() { 21 | const testKey = await sharedStorage.get('test-key'); 22 | const testValue = await sharedStorage.get('test-value'); 23 | 24 | if (!testKey || !testValue) { 25 | return; 26 | } 27 | 28 | if (Math.random() < DEBUG_MODE_CHANCE) { 29 | const debugKey = BigInt(54321); 30 | privateAggregation.enableDebugMode({ debugKey }); 31 | } 32 | 33 | privateAggregation.contributeToHistogram({ 34 | bucket: BigInt(testKey), 35 | value: parseInt(testValue), 36 | }); 37 | 38 | sharedStorage.delete('test-key'); 39 | sharedStorage.delete('test-value'); 40 | } 41 | } 42 | 43 | register('private-aggregation-test', PrivateAggregationTest); 44 | -------------------------------------------------------------------------------- /functions/app/content-producer/paa/scripts/private-aggregation-test.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Private Aggregation API testing 24 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /functions/app/content-producer/paa/scripts/private-aggregation-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const PRIVATE_AGGREGATION_TEST_IFRAME_TITLE = 'Private Aggregation API Test' 18 | const PRIVATE_AGGREGATION_TEST_SCRIPT_URL = 'https://shared-storage-demo-content-producer.web.app/paa/scripts/private-aggregation-test.html'; 19 | 20 | function setupIframe() { 21 | const body = document.querySelector('body'); 22 | const iframe = document.createElement('iframe'); 23 | 24 | iframe.style.height = 0; 25 | iframe.style.width = 0; 26 | iframe.style.top = 0; 27 | iframe.style.position = 'absolute'; 28 | iframe.title = PRIVATE_AGGREGATION_TEST_IFRAME_TITLE; 29 | iframe.src = PRIVATE_AGGREGATION_TEST_SCRIPT_URL; 30 | 31 | body.appendChild(iframe); 32 | } 33 | 34 | try { 35 | setupIframe(); 36 | } catch { 37 | // Swallow the error 38 | } 39 | -------------------------------------------------------------------------------- /functions/app/content-producer/setup-paa-test-routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const fs = require('fs'); 17 | const cors = require('cors'); 18 | 19 | function setupPrivateAggregationTestRoutes(app) { 20 | app.get('/paa/get-reports', (req, res) => {}); 21 | 22 | app.get('/paa/scripts/private-aggregation-test.js', cors(), async (req, res) => { 23 | let response; 24 | 25 | try { 26 | response = await fs.promises.readFile(`${__dirname}/paa/scripts/private-aggregation-test.js`, 'utf8'); 27 | } catch (e) { 28 | console.error(e); 29 | res.status(500); 30 | } 31 | 32 | res.type('.js'); 33 | res.send(response); 34 | }); 35 | 36 | app.get('/paa/scripts/private-aggregation-test.html', cors(), async (req, res) => { 37 | let response; 38 | 39 | try { 40 | response = await fs.promises.readFile(`${__dirname}/paa/scripts/private-aggregation-test.html`, 'utf8'); 41 | } catch (e) { 42 | console.error(e); 43 | res.status(500); 44 | } 45 | 46 | res.send(response); 47 | }); 48 | 49 | app.get('/paa/scripts/private-aggregation-test-worklet.js', async (req, res) => { 50 | let response; 51 | 52 | try { 53 | response = await fs.promises.readFile(`${__dirname}/paa/scripts/private-aggregation-test-worklet.js`, 'utf8'); 54 | } catch (e) { 55 | console.error(e); 56 | res.status(500); 57 | } 58 | 59 | res.type('.js'); 60 | res.send(response); 61 | }); 62 | } 63 | 64 | module.exports = setupPrivateAggregationTestRoutes; 65 | -------------------------------------------------------------------------------- /functions/app/helpers/setup-view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | const hbs = require('express-handlebars'); 19 | 20 | // Setup view with Handlebars 21 | // Doc: https://handlebarsjs.com/guide/#block-helpers 22 | module.exports = (app, viewName) => { 23 | app.engine( 24 | 'hbs', 25 | hbs.engine({ 26 | extname: '.hbs', 27 | layoutsDir: path.join(__dirname + '/../../view'), 28 | partialsDir: path.join(__dirname + '/../../view/partials') 29 | }) 30 | ); 31 | 32 | app.set('view engine', 'hbs'); 33 | app.set('views', path.join(__dirname + `/../../view/${viewName}`)); 34 | 35 | return app; 36 | }; 37 | -------------------------------------------------------------------------------- /functions/app/home/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const express = require('express'); 17 | const functions = require('firebase-functions'); 18 | const setupView = require('../helpers/setup-view'); 19 | 20 | // Read the dev or prod URLs to be used 21 | require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` }); 22 | 23 | // Setup app and view 24 | const app = setupView(express(), 'home'); 25 | 26 | // Setup root route 27 | app.get('/', (req, res) => { 28 | const { DEMO_HOME_URL, PUBLISHER_A_URL, PUBLISHER_B_URL, CONTENT_PRODUCER_URL, PAYMENT_PROVIDER_URL } = process.env; 29 | 30 | res.render('index', { 31 | demoHomeUrl: DEMO_HOME_URL, 32 | publisherAUrl: PUBLISHER_A_URL, 33 | publisherBUrl: PUBLISHER_B_URL, 34 | contentProducerUrl: CONTENT_PRODUCER_URL, 35 | paymentProviderUrl: PAYMENT_PROVIDER_URL, 36 | }); 37 | }); 38 | 39 | exports.home = functions.https.onRequest(app); 40 | -------------------------------------------------------------------------------- /functions/app/publisher-a/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const express = require('express'); 17 | const functions = require('firebase-functions'); 18 | const setupView = require('../helpers/setup-view'); 19 | 20 | // Read the dev or prod URLs to be used 21 | require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` }); 22 | 23 | // Setup app and view 24 | const app = setupView(express(), 'publisher-a'); 25 | 26 | // Setup root route 27 | app.get('/:demoName', (req, res) => { 28 | const { DEMO_HOME_URL, PUBLISHER_A_URL, PUBLISHER_B_URL, CONTENT_PRODUCER_URL, PAYMENT_PROVIDER_URL } = process.env; 29 | let { demoName } = req.params; 30 | demoName = demoName ?? 'index'; 31 | console.log({ demoName }); 32 | 33 | res.render(demoName, { 34 | demoHomeUrl: DEMO_HOME_URL, 35 | publisherAUrl: PUBLISHER_A_URL, 36 | publisherBUrl: PUBLISHER_B_URL, 37 | contentProducerUrl: CONTENT_PRODUCER_URL, 38 | paymentProviderUrl: PAYMENT_PROVIDER_URL, 39 | }); 40 | }); 41 | 42 | exports.publisherA = functions.https.onRequest(app); 43 | -------------------------------------------------------------------------------- /functions/app/publisher-b/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const express = require('express'); 17 | const functions = require('firebase-functions'); 18 | const setupView = require('../helpers/setup-view'); 19 | 20 | // Read the dev or prod URLs to be used 21 | require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` }); 22 | 23 | // Setup app and view 24 | const app = setupView(express(), 'publisher-b'); 25 | 26 | // Setup root route 27 | app.get('/:demoName', (req, res) => { 28 | const { DEMO_HOME_URL, PUBLISHER_A_URL, PUBLISHER_B_URL, CONTENT_PRODUCER_URL, PAYMENT_PROVIDER_URL } = process.env; 29 | let { demoName } = req.params; 30 | demoName = demoName ?? 'index'; 31 | console.log({ demoName }); 32 | 33 | res.render(demoName, { 34 | demoHomeUrl: DEMO_HOME_URL, 35 | publisherAUrl: PUBLISHER_A_URL, 36 | publisherBUrl: PUBLISHER_B_URL, 37 | contentProducerUrl: CONTENT_PRODUCER_URL, 38 | paymentProviderUrl: PAYMENT_PROVIDER_URL, 39 | }); 40 | }); 41 | 42 | exports.publisherB = functions.https.onRequest(app); 43 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const home = require('./app/home'); 17 | const publisherA = require('./app/publisher-a'); 18 | const publisherB = require('./app/publisher-b'); 19 | const contentProducer = require('./app/content-producer'); 20 | 21 | exports.home = home.home; 22 | exports.publisherA = publisherA.publisherA; 23 | exports.publisherB = publisherB.publisherB; 24 | exports.contentProducer = contentProducer.contentProducer; 25 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "engines": { 5 | "node": "16" 6 | }, 7 | "main": "index.js", 8 | "dependencies": { 9 | "dotenv": "^16.0.0", 10 | "express": "^4.18.1", 11 | "express-handlebars": "^6.0.5", 12 | "firebase-admin": "^11.11.1", 13 | "firebase-functions": "^3.24.1" 14 | }, 15 | "devDependencies": { 16 | "firebase-functions-test": "^0.2.0" 17 | }, 18 | "private": true 19 | } 20 | -------------------------------------------------------------------------------- /functions/view/content-producer/demographics-survey.hbs: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 24 | 28 |
29 | 36 |
37 | 38 |
Survey
39 |
40 |

Shared storage - Demographics measurement demo

41 |
42 |
43 | 44 |
45 |

Select information to be set in Shared Storage

46 |
47 | Select your age group 48 | 55 | 61 | 67 |
68 |
69 |
70 | Select your continent 71 | 78 | 84 | 90 | 96 | 102 | 108 | 114 |
115 |
116 |
117 | 118 | 123 |
124 |
125 |
126 | 127 |

Description

128 |

129 | You may want to measure the demographics of the users who has seen some content you have embedded across 130 | different sites. 131 |

132 |

133 | For this demo, visit the survey page to enter your demographic data where the information you enter is added 134 | to Shared Storage. Then visit either Publisher A or B sites which will trigger the aggregatable report to be 135 | sent once across both sites. The content ID, age group ID, and the geography ID dimensions are encoded into 136 | the aggregation key (bucket), and the count is used as the aggregatable value. 137 |

138 |

139 | The summary report generated will provide information such as "Approximately 391 users who have seen the 140 | content ID 743 are between the age of 18-25 and are from Europe." 141 |

142 |

Code

143 |
    144 |
  • 145 | Iframe logic 149 | (embedded into the publisher page) 150 |
  • 151 |
  • 152 | Worklet 156 | (loaded and executed by the iframe logic) 157 |
  • 158 |
159 |
160 |
161 | 162 |

163 | Shared Storage API is not enabled in your browser. Follow the 164 | instructions, and enable the the 168 | Privacy Sandbox Ads APIs 169 | experiment flag at 170 | chrome://flags/#privacy-sandbox-ads-apis 171 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 172 |

173 |
174 |
175 | 179 | 180 | -------------------------------------------------------------------------------- /functions/view/content-producer/index.hbs: -------------------------------------------------------------------------------- 1 |

Content producer

2 | -------------------------------------------------------------------------------- /functions/view/content-producer/payment-provider.hbs: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | 33 | 34 |
35 | 41 |
42 | 43 |
Payment provider
44 |
45 |

Shared storage - Known customer demo

46 |
47 |
48 |
49 |
Demo control
50 | 53 | 56 |
57 |
58 |
-------------------------------------------------------------------------------- /functions/view/main.hbs: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Shared storage demo 24 | 28 | 29 | 30 | 31 | 32 | 33 | 45 | 46 | 47 | 48 | {{{body}}} 49 | 50 | -------------------------------------------------------------------------------- /functions/view/partials/creativeRotationDemoControls.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
Demo control
5 |
6 | 12 | 18 | 24 |
25 |
26 |
-------------------------------------------------------------------------------- /functions/view/publisher-a/ab-testing.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher A
12 |
13 |

Shared storage - A/B testing demo

14 |
15 |
16 | 17 | 18 |

Description

19 |

20 | To see if an experiment has the desired effect, you can conduct A/B testing across multiple sites. As an 21 | advertiser, you can choose to render a different ad based on what group the user is assigned to. This group 22 | assignment is stored into Shared Storage and can be used cross-site. 23 |

24 |

25 | In this demo, the user can be assigned to 26 | "Control", 27 | "Experiment A", or 28 | "Experiment B" groups. The same ad will be rendered across different sites based on what group the user 29 | is in. The initial assignment is random, and the demo contains a set of butons to manually add the user to a 30 | group. 31 |

32 |

Code

33 |
    34 |
  • Iframe logic 38 | (embedded into the publisher page)
  • 39 |
  • Worklet 43 | (loaded and executed by the iframe logic)
  • 44 |
45 |
46 |
47 | 48 |

49 | Shared Storage API is not enabled in your browser. Follow the 50 | instructions, and enable the the 54 | Privacy Sandbox Ads APIs 55 | experiment flag at 56 | chrome://flags/#privacy-sandbox-ads-apis 57 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 58 |

59 |
60 |
61 | 64 | -------------------------------------------------------------------------------- /functions/view/publisher-a/creative-rotation.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher A
12 |
13 |

Shared storage - Creative rotation demo

14 |
15 |
16 | {{> creativeRotationDemoControls}} 17 | 18 |

Description

19 |

20 | An advertiser may want to show different ads of the same campaign to the user to increase effectiveness of the 21 | ads. 22 |

23 |

24 | In this demo, the creative can be rotated with different strategies. In sequential rotation, creatives A, B and 25 | C are shown one after another. In even distribution, the creative is selected at random where each creative has 26 | an equal chance of being chosen. In weighted distribution, some creatives can be weighted to be chosen more 27 | often than another creative. 28 |

29 |

Code

30 |
    31 |
  • Iframe logic 35 | (embedded into the publisher page)
  • 36 |
  • Worklet 40 | (loaded and executed by the iframe logic)
  • 41 |
42 |
43 |
44 | 45 |

46 | Shared Storage API is not enabled in your browser. Follow the 47 | instructions, and enable the the 51 | Privacy Sandbox Ads APIs 52 | experiment flag at 53 | chrome://flags/#privacy-sandbox-ads-apis 54 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 55 |

56 |
57 |
58 | 59 | 62 | -------------------------------------------------------------------------------- /functions/view/publisher-a/creative-selection-by-frequency.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher A
12 |
13 |

Shared storage - Creative selection by frequency demo

14 |
15 |
16 | 17 | 18 |

Description

19 |

20 | If an ad creative has been shown to the user too many times, select a different ad. 21 |

22 |

23 | In this demo, when the user sees the same example ad 5 times across both 24 | Publisher A 25 | and 26 | Publisher B 27 | sites, the default ad will be selected. The demo contains a button to reset the frequency back to 5. 28 |

29 |

Code

30 |
    31 |
  • Iframe logic 35 | (embedded into the publisher page)
  • 36 |
  • Worklet 40 | (loaded and executed by the iframe logic)
  • 41 |
42 |
43 |
44 | 45 |

46 | Shared Storage API is not enabled in your browser. Follow the 47 | instructions, and enable the the 51 | Privacy Sandbox Ads APIs 52 | experiment flag at 53 | chrome://flags/#privacy-sandbox-ads-apis 54 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 55 |

56 |
57 |
58 | 61 | -------------------------------------------------------------------------------- /functions/view/publisher-a/demographics-measurement.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher A
12 |
13 |

Shared storage - Demographics measurement

14 |
15 |
16 | 20 | 21 |

Description

22 |

23 | You may want to measure the demographics of the users who has seen some content you have embedded across 24 | different sites. 25 |

26 |

27 | For this demo, visit the survey page to enter your demographic data where the information you enter is added to 28 | Shared Storage. Then visit either Publisher A or B sites which will trigger the aggregatable report to be sent 29 | once across both sites. The content ID, age group ID, and the geography ID dimensions are encoded into the 30 | aggregation key (bucket), and the count is used as the aggregatable value. 31 |

32 |

33 | The summary report generated will provide information such as "Approximately 391 users who have seen the content 34 | ID 743 are between the age of 18-25 and are from Europe." 35 |

36 |

Code

37 |
    38 |
  • Iframe logic 42 | (embedded into the publisher page)
  • 43 |
  • Worklet 47 | (loaded and executed by the iframe logic)
  • 48 |
49 |
50 |
51 | 52 |

53 | Shared Storage API is not enabled in your browser. Follow the 54 | instructions, and enable the the 58 | Privacy Sandbox Ads APIs 59 | experiment flag at 60 | chrome://flags/#privacy-sandbox-ads-apis 61 | with Chrome Canary and Dev 107.0.5292.0 and above to test this Shared Storage API demo. 62 |

63 |
64 |
65 | 68 | -------------------------------------------------------------------------------- /functions/view/publisher-a/hover-event.hbs: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 |
Publisher A
8 |
9 |

Shared storage - Hover event reporting demo

10 |
11 |
12 | 13 | 14 |

Description

15 |

16 | Whenever your mouse enters the demo fenced frame, an event-level report is submitted. Open up DevTools and check the network requests or the console log to see the submitted report. 17 |

18 |

Code

19 | 36 |
37 |
38 | 39 |

40 | Shared Storage API is not enabled in your browser. Follow the 41 | instructions, and enable the the 45 | Privacy Sandbox Ads APIs 46 | experiment flag at 47 | chrome://flags/#privacy-sandbox-ads-apis 48 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 49 |

50 |
51 |
52 | 55 | -------------------------------------------------------------------------------- /functions/view/publisher-a/index.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/functions/view/publisher-a/index.hbs -------------------------------------------------------------------------------- /functions/view/publisher-a/k-freq-measurement.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher A
12 |
13 |

Shared storage - K-frequency measurement

14 |
15 |
16 | 20 | 21 |

Description

22 |

23 | You may want to measure the number of users who have seen your content K or more times a given client across 24 | different sites. 25 |

26 | In this demo, the impression count is added to Shared Storage where it increments by 1 whenever the content is 27 | loaded for both publisher A and B. When the impression count has reached 3, the Private Aggregation API is called. 28 | The content ID dimension is encoded as the aggregation key, and the count is used as the aggregatable value. 29 |

30 | The summary report generated will provide information such as "Approximately 391 users have seen the ad campaign 31 | ID 743 at least 3 times." 32 |

33 |

Code

34 |
    35 |
  • Iframe logic 39 | (embedded into the publisher page)
  • 40 |
  • Worklet 44 | (loaded and executed by the iframe logic)
  • 45 |
46 |
47 |
48 | 49 |

50 | Shared Storage API is not enabled in your browser. Follow the 51 | instructions, and enable the the 55 | Privacy Sandbox Ads APIs 56 | experiment flag at 57 | chrome://flags/#privacy-sandbox-ads-apis 58 | with Chrome Canary and Dev 107.0.5292.0 and above to test this Shared Storage API demo. 59 |

60 |
61 |
62 | 65 | -------------------------------------------------------------------------------- /functions/view/publisher-a/known-customer.hbs: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | 10 |
Publisher A
11 |
12 |

Shared storage - Known customer demo

13 |
14 |
15 | 16 | 17 |

Description

18 |

19 | There may be cases where you want to render a different element based on whether the user has been seen before 20 | at a different site. A payment provider may want to render a 21 | "Register" 22 | or 23 | "Buy now" 24 | button based on whether the user has registered before at the payment provider's site. 25 |

26 |

27 | In this demo, a different button will be rendered for the publishers based on the user status set from the 28 | payment provider page. 29 |

30 |

Code

31 |
    32 |
  • Iframe logic 36 | (embedded into the publisher page)
  • 37 |
  • Worklet 41 | (loaded and executed by the iframe logic)
  • 42 |
43 |
44 |
45 | 46 |

47 | Shared Storage API is not enabled in your browser. Follow the 48 | instructions, and enable the the 52 | Privacy Sandbox Ads APIs 53 | experiment flag at 54 | chrome://flags/#privacy-sandbox-ads-apis 55 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 56 |

57 |
58 |
59 | 62 | -------------------------------------------------------------------------------- /functions/view/publisher-a/reach-measurement.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher A
12 |
13 |

Shared storage - Reach measurement

14 |
15 |
16 | 20 | 21 |

Description

22 |

23 | You may want to keep track of how many unique impressions your content has across different sites. In this demo, 24 | visiting either Publisher A or B sites will send the aggregatable report, but will not send it on subsequent 25 | visits. The ad camaign ID dimension is encoded into the aggregation key (bucket), and the count is used as the 26 | aggregatable value. To reset the demo, hover the "Reset impression flag" button on the demo page. 27 |

28 |

29 | The summary report will contain information such as "Approximately 391 users have seen the ad campaign ID 743." 30 |

31 |

Code

32 |
    33 |
  • Iframe logic 37 | (embedded into the publisher page)
  • 38 |
  • Worklet 42 | (loaded and executed by the iframe logic)
  • 43 |
44 |
45 |
46 | 47 |

48 | Shared Storage API is not enabled in your browser. Follow the 49 | instructions, and enable the the 53 | Privacy Sandbox Ads APIs 54 | experiment flag at 55 | chrome://flags/#privacy-sandbox-ads-apis 56 | with Chrome Canary and Dev 107.0.5292.0 and above to test this Shared Storage API demo. 57 |

58 |
59 |
60 | 63 | -------------------------------------------------------------------------------- /functions/view/publisher-a/third-party-write.hbs: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 |
7 | 8 |
Publisher A
9 |
10 |

Shared storage - Third-party write destination demo

11 |
12 |
13 | 14 | {{!-- This third-party script writes data to the publisher's shared storage --}} 15 | 16 | 17 | {{!-- This third-party iframe's code writes data to its own shared storage --}} 18 | 19 | 20 | 21 |

Description

22 |

23 | When a third-party code is added to the publisher's page, the third-party can write to the shared storage of their own origin, or write to the publisher's origin. 24 |

25 |

26 | Using a script tag allows the third-party code to be injected in the publisher's context, and using an iframe to load third-party code allows third-party to write to its own shared storage. 27 |

28 |

Code

29 | 41 |
42 |
43 | 44 |

45 | Shared Storage API is not enabled in your browser. Follow the 46 | instructions, and enable the the 50 | Privacy Sandbox Ads APIs 51 | experiment flag at 52 | chrome://flags/#privacy-sandbox-ads-apis 53 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 54 |

55 |
56 |
57 | 60 | -------------------------------------------------------------------------------- /functions/view/publisher-a/top-level-nav.hbs: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 |
Publisher A
8 |
9 |

Shared storage - Top-level navigation demo

10 |
11 |
12 | 13 | 14 |

Description

15 |

16 | Whenever the fenced frame navigates the top-level to another page, the remaining entropy budget is decreased for the origin the fenced frame is served from. Clicking on the fenced frame will navigate this demo to another page. Press the back button to return to the page. Check the DevTools / Applications tab and select the origin to see the remaining entropy budget. 17 |

18 |

Code

19 | 36 |
37 |
38 | 39 |

40 | Shared Storage API is not enabled in your browser. Follow the 41 | instructions, and enable the the 45 | Privacy Sandbox Ads APIs 46 | experiment flag at 47 | chrome://flags/#privacy-sandbox-ads-apis 48 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 49 |

50 |
51 |
52 | 55 | -------------------------------------------------------------------------------- /functions/view/publisher-b/ab-testing.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher B
12 |
13 |

Shared storage - A/B testing demo

14 |
15 |
16 | 17 | 18 | 19 |

Description

20 |

21 | To see if an experiment has the desired effect, you can conduct A/B testing across multiple sites. As an 22 | advertiser, you can choose to render a different ad based on what group the user is assigned to. This group 23 | assignment is stored into Shared Storage and can be used cross-site. 24 |

25 |

26 | In this demo, the user can be assigned to 27 | "Control", 28 | "Experiment A", or 29 | "Experiment B" groups. The same ad will be rendered across different sites based on what group the user 30 | is in. The initial assignment is random, and the demo contains a set of butons to manually add the user to a 31 | group. 32 |

33 |

Code

34 |
    35 |
  • Iframe logic 39 | (embedded into the publisher page)
  • 40 |
  • Worklet 44 | (loaded and executed by the iframe logic)
  • 45 |
46 |
47 |
48 | 49 |

50 | Shared Storage API is not enabled in your browser. Follow the 51 | instructions, and enable the the 55 | Privacy Sandbox Ads APIs 56 | experiment flag at 57 | chrome://flags/#privacy-sandbox-ads-apis 58 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 59 |

60 |
61 |
62 | 65 | -------------------------------------------------------------------------------- /functions/view/publisher-b/creative-rotation.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher B
12 |
13 |

Shared storage - Creative rotation demo

14 |
15 |
16 | {{> creativeRotationDemoControls}} 17 | 18 |

Description

19 |

20 | An advertiser may want to show different ads of the same campaign to the user to increase effectiveness of the 21 | ads. 22 |

23 |

24 | In this demo, the creative can be rotated with different strategies. In sequential rotation, creatives A, B and 25 | C are shown one after another. In even distribution, the creative is selected at random where each creative has 26 | an equal chance of being chosen. In weighted distribution, some creatives can be weighted to be chosen more 27 | often than another creative. 28 |

29 |

Code

30 |
    31 |
  • Iframe logic 35 | (embedded into the publisher page)
  • 36 |
  • Worklet 40 | (loaded and executed by the iframe logic)
  • 41 |
42 |
43 |
44 | 45 |

46 | Shared Storage API is not enabled in your browser. Follow the 47 | instructions, and enable the the 51 | Privacy Sandbox Ads APIs 52 | experiment flag at 53 | chrome://flags/#privacy-sandbox-ads-apis 54 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 55 |

56 |
57 |
58 | 59 | 62 | -------------------------------------------------------------------------------- /functions/view/publisher-b/creative-selection-by-frequency.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher B
12 |
13 |

Shared storage - Creative selection by frequency demo

14 |
15 |
16 | 17 | 18 |

Description

19 |

20 | If an ad creative has been shown to the user too many times, select a different ad. 21 |

22 |

23 | In this demo, when the user sees the same example ad 5 times across both 24 | Publisher A 25 | and 26 | Publisher B 27 | sites, the default ad will be selected. The demo contains a button to reset the frequency back to 5. 28 |

29 |

Code

30 |
    31 |
  • Iframe logic 35 | (embedded into the publisher page)
  • 36 |
  • Worklet 40 | (loaded and executed by the iframe logic)
  • 41 |
42 |
43 |
44 | 45 |

46 | Shared Storage API is not enabled in your browser. Follow the 47 | instructions, and enable the the 51 | Privacy Sandbox Ads APIs 52 | experiment flag at 53 | chrome://flags/#privacy-sandbox-ads-apis 54 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 55 |

56 |
57 |
58 | 61 | -------------------------------------------------------------------------------- /functions/view/publisher-b/index.hbs: -------------------------------------------------------------------------------- 1 | 2 |

Publisher B

-------------------------------------------------------------------------------- /functions/view/publisher-b/k-freq-measurement.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher B
12 |
13 |

Shared storage - K-frequency measurement

14 |
15 |
16 | 20 | 21 |

Description

22 |

23 | You may want to measure the number of users who have seen your content K or more times a given client across 24 | different sites. 25 |

26 | In this demo, the impression count is added to Shared Storage where it increments by 1 whenever the content is 27 | loaded for both publisher A and B. When the impression count has reached 3, the Private Aggregation API is called. 28 | The content ID dimension is encoded as the aggregation key, and the count is used as the aggregatable value. 29 |

30 | The summary report generated will provide information such as "Approximately 391 users have seen the ad campaign 31 | ID 743 at least 3 times." 32 |

33 |

Code

34 |
    35 |
  • Iframe logic 39 | (embedded into the publisher page)
  • 40 |
  • Worklet 44 | (loaded and executed by the iframe logic)
  • 45 |
46 |
47 |
48 | 49 |

50 | Shared Storage API is not enabled in your browser. Follow the 51 | instructions, and enable the the 55 | Privacy Sandbox Ads APIs 56 | experiment flag at 57 | chrome://flags/#privacy-sandbox-ads-apis 58 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 59 |

60 |
61 |
62 | 65 | -------------------------------------------------------------------------------- /functions/view/publisher-b/known-customer.hbs: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | 10 |
Publisher B
11 |
12 |

Shared storage - Known customer demo

13 |
14 |
15 | 16 | 17 |

Description

18 |

19 | There may be cases where you want to render a different element based on whether the user has been seen before 20 | at a different site. A payment provider may want to render a 21 | "Register" 22 | or 23 | "Buy now" 24 | button based on whether the user has registered before at the payment provider's site. 25 |

26 |

27 | In this demo, a different button will be rendered for the publishers based on the user status set from the 28 | payment provider page. 29 |

30 |

Code

31 |
    32 |
  • Iframe logic 36 | (embedded into the publisher page)
  • 37 |
  • Worklet 41 | (loaded and executed by the iframe logic)
  • 42 |
43 |
44 |
45 | 46 |

47 | Shared Storage API is not enabled in your browser. Follow the 48 | instructions, and enable the the 52 | Privacy Sandbox Ads APIs 53 | experiment flag at 54 | chrome://flags/#privacy-sandbox-ads-apis 55 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 56 |

57 |
58 |
59 | 62 | -------------------------------------------------------------------------------- /functions/view/publisher-b/reach-measurement.hbs: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 11 |
Publisher B
12 |
13 |

Shared storage - Reach measurement

14 |
15 |
16 | 20 | 21 |

Description

22 |

23 | You may want to keep track of how many unique impressions your content has across different sites. In this demo, 24 | visiting either Publisher A or B sites will send the aggregatable report, but will not send it on subsequent 25 | visits. The ad camaign ID dimension is encoded into the aggregation key (bucket), and the count is used as the 26 | aggregatable value. To reset the demo, hover the "Reset impression flag" button on the demo page. 27 |

28 |

29 | The summary report will contain information such as "Approximately 391 users have seen the ad campaign ID 743." 30 |

31 |

Code

32 |
    33 |
  • Iframe logic 37 | (embedded into the publisher page)
  • 38 |
  • Worklet 42 | (loaded and executed by the iframe logic)
  • 43 |
44 |
45 |
46 | 47 |

48 | Shared Storage API is not enabled in your browser. Follow the 49 | instructions, and enable the the 53 | Privacy Sandbox Ads APIs 54 | experiment flag at 55 | chrome://flags/#privacy-sandbox-ads-apis 56 | with Chrome 104.0.5086.0 and above to test this Shared Storage API demo. 57 |

58 |
59 |
60 | 63 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-storage-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "shared-storage-example", 9 | "version": "1.0.0", 10 | "hasInstallScript": true, 11 | "license": "ISC", 12 | "dependencies": { 13 | "cors": "^2.8.5" 14 | }, 15 | "devDependencies": { 16 | "prettier": "^2.6.2" 17 | } 18 | }, 19 | "node_modules/cors": { 20 | "version": "2.8.5", 21 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 22 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 23 | "dependencies": { 24 | "object-assign": "^4", 25 | "vary": "^1" 26 | }, 27 | "engines": { 28 | "node": ">= 0.10" 29 | } 30 | }, 31 | "node_modules/object-assign": { 32 | "version": "4.1.1", 33 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 34 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 35 | "engines": { 36 | "node": ">=0.10.0" 37 | } 38 | }, 39 | "node_modules/prettier": { 40 | "version": "2.6.2", 41 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", 42 | "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", 43 | "dev": true, 44 | "bin": { 45 | "prettier": "bin-prettier.js" 46 | }, 47 | "engines": { 48 | "node": ">=10.13.0" 49 | }, 50 | "funding": { 51 | "url": "https://github.com/prettier/prettier?sponsor=1" 52 | } 53 | }, 54 | "node_modules/vary": { 55 | "version": "1.1.2", 56 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 57 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 58 | "engines": { 59 | "node": ">= 0.8" 60 | } 61 | } 62 | }, 63 | "dependencies": { 64 | "cors": { 65 | "version": "2.8.5", 66 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 67 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 68 | "requires": { 69 | "object-assign": "^4", 70 | "vary": "^1" 71 | } 72 | }, 73 | "object-assign": { 74 | "version": "4.1.1", 75 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 76 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 77 | }, 78 | "prettier": { 79 | "version": "2.6.2", 80 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", 81 | "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", 82 | "dev": true 83 | }, 84 | "vary": { 85 | "version": "1.1.2", 86 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 87 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-storage-example", 3 | "author": "kevinkiklee", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "echo", 7 | "lint": "npx prettier --write .", 8 | "dev": "firebase emulators:start", 9 | "shell": "firebase functions:shell", 10 | "deploy": "firebase deploy", 11 | "logs": "firebase functions:log", 12 | "postinstall": "cd functions && npm install && cd ..", 13 | "kill-8080": "lsof -ti tcp:8080 | xargs kill -9" 14 | }, 15 | "license": "ISC", 16 | "devDependencies": { 17 | "prettier": "^2.6.2" 18 | }, 19 | "dependencies": { 20 | "cors": "^2.8.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | trailingComma: 'es5', 19 | printWidth: 120, 20 | tabWidth: 2, 21 | semi: true, 22 | singleQuote: true, 23 | }; 24 | -------------------------------------------------------------------------------- /sites/404.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | Page Not Found 23 | 24 | 93 | 94 | 95 |
96 |

404

97 |

Page Not Found

98 |

The specified file was not found on this website. Check the URL for mistakes and try again.

99 |

Why am I seeing this?

100 |

101 | This page was generated by the Firebase Command-Line Interface. To modify it, edit the 102 | 404.html file in your project's configured public directory. 103 |

104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /sites/content-producer/.well-known/privacy-sandbox-attestations.json: -------------------------------------------------------------------------------- 1 | { 2 | "privacy_sandbox_api_attestations": [ 3 | { 4 | "attestation_parser_version": "2", 5 | "attestation_version": "1", 6 | "privacy_policy": [ 7 | "https://policies.google.com/privacy" 8 | ], 9 | "ownership_token": "nLv7az7CCLlmraqX5BBX9yiahJpjE1rdRlRXPAmZ44wCXf4hGS60Y0bsbxI5U09z", 10 | "issued_seconds_since_epoch": 1727275829, 11 | "enrollment_id": "NHT7X", 12 | "enrollment_site": "https://shared-storage-demo-content-producer.web.app/", 13 | "platform_attestations": [ 14 | { 15 | "platform": "chrome", 16 | "attestations": { 17 | "shared_storage_api": { 18 | "ServiceNotUsedForIdentifyingUserAcrossSites": true 19 | }, 20 | "private_aggregation_api": { 21 | "ServiceNotUsedForIdentifyingUserAcrossSites": true 22 | } 23 | } 24 | }, 25 | { 26 | "platform": "android", 27 | "attestations": {} 28 | } 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /sites/content-producer/ads/ad-1.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/ad-2.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/ad-3.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/ad.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | -------------------------------------------------------------------------------- /sites/content-producer/ads/buy-now-button.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /sites/content-producer/ads/default-ad.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/example-ad.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/experiment-ad-a.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/experiment-ad-b.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/content-producer/ads/hover-ad.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

27 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /sites/content-producer/ads/nav-ad-1.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sites/content-producer/ads/nav-ad-2.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sites/content-producer/ads/nav-ad-3.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sites/content-producer/ads/register-button.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /sites/content-producer/assets/ad-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/ad-1.png -------------------------------------------------------------------------------- /sites/content-producer/assets/ad-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/ad-2.png -------------------------------------------------------------------------------- /sites/content-producer/assets/ad-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/ad-3.png -------------------------------------------------------------------------------- /sites/content-producer/assets/buy-now-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/buy-now-button.png -------------------------------------------------------------------------------- /sites/content-producer/assets/default-ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/default-ad.png -------------------------------------------------------------------------------- /sites/content-producer/assets/example-ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/example-ad.png -------------------------------------------------------------------------------- /sites/content-producer/assets/experiment-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/experiment-a.png -------------------------------------------------------------------------------- /sites/content-producer/assets/experiment-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/experiment-b.png -------------------------------------------------------------------------------- /sites/content-producer/assets/register-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/assets/register-button.png -------------------------------------------------------------------------------- /sites/content-producer/demo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | width: 300px; 19 | } 20 | 21 | :where(h1, h2, h3, h4, h5, h6) { 22 | font-weight: var(--font-weight-6); 23 | } 24 | 25 | #ad-slot { 26 | height: 250px; 27 | border: none; 28 | } 29 | 30 | #button-slot { 31 | height: 100px; 32 | border: none; 33 | } 34 | 35 | .demo__card, 36 | .demo__controls { 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | gap: var(--size-2); 41 | background: var(--surface-3); 42 | border: 1px solid var(--surface-1); 43 | padding: var(--size-3); 44 | margin: var(--size-3) 0; 45 | border-radius: var(--radius-3); 46 | box-shadow: var(--shadow-2); 47 | margin-top: 0; 48 | } 49 | 50 | .demo__controls h5 { 51 | margin-bottom: var(--size-2); 52 | } 53 | 54 | .demo__buttons-container { 55 | display: flex; 56 | flex-direction: column; 57 | width: fit-content; 58 | } 59 | 60 | .demo__button { 61 | zoom: 90%; 62 | /* max-width: var(--size-13); */ 63 | --_bg: linear-gradient(var(--indigo-5), var(--indigo-7)); 64 | --_border: var(--indigo-6); 65 | --_text: var(--indigo-0); 66 | --_ink-shadow: 0 1px 0 var(--indigo-9); 67 | margin: 5px; 68 | } 69 | -------------------------------------------------------------------------------- /sites/content-producer/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/content-producer/index.css -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/demographics-measurement-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * The scale factor is multiplied by the aggregatable value to maximize 19 | * the signal-to-noise ratio. See “Noise and scaling” section in the 20 | * Aggregation Fundamentals document to learn more: 21 | * - https://developer.chrome.com/en/docs/privacy-sandbox/aggregation-fundamentals 22 | * 23 | * The scale factor used here has been intentionally reduced to an arbitrary number. 24 | * The maximum aggregatable value used in this demo is 1. The scale factor 25 | * of 65536 would have maximized the signal-to-noise ratio. However, that also 26 | * uses the entire contribution budget for the rolling 24-hour period. 27 | * 28 | * Therefore, the scale factor has been significantly reduced to allow the demo 29 | * to be used repeatedly for testing throughout without hitting the contribution 30 | * budget limit. 31 | */ 32 | const SCALE_FACTOR = 128; 33 | 34 | /** 35 | * The bucket key must be a number, and in this case, it is simply the ad campaign 36 | * ID itself. For more complex buckey key construction, see other use cases in 37 | * this demo. 38 | */ 39 | const AGGREGATION_KEY_MAP = { 40 | ageGroupId: { 41 | '18-39': '1', 42 | '40-64': '2', 43 | '65+': '3', 44 | }, 45 | continentId: { 46 | africa: '1', 47 | antarctica: '2', 48 | asia: '3', 49 | australia: '4', 50 | europe: '5', 51 | 'north-america': '6', 52 | 'south-america': '7', 53 | }, 54 | }; 55 | 56 | /** 57 | * The aggregation key will be in the format of: 58 | * contentId | ageGroupId | continentId 59 | * 60 | * For example, a user from Australia between the age of 40-64, who has 61 | * seen the Content ID 321 will be represented by the key: 62 | * 321 | 2 | 4 or 32124 63 | */ 64 | function generateAggregationKey(contentId, ageGroup, continent) { 65 | const ageGroupId = AGGREGATION_KEY_MAP.ageGroupId[ageGroup]; 66 | const continentId = AGGREGATION_KEY_MAP.continentId[continent]; 67 | const aggregationKey = BigInt(`${contentId}${ageGroupId}${continentId}`); 68 | 69 | console.log(`[PAA] Content ID is ${contentId}`); 70 | console.log(`[PAA] Continent is ${continent}, and the corresponding ID is ${continentId}`); 71 | console.log(`[PAA] Age group is ${ageGroup}, and the corresponding ID is ${ageGroupId}`); 72 | console.log( 73 | `[PAA] The generated aggregation key is ${aggregationKey} which is composed of "${contentId} | ${ageGroupId} | ${continentId}"` 74 | ); 75 | 76 | return aggregationKey; 77 | } 78 | 79 | class DemographicsMeasurementOperation { 80 | async run(data) { 81 | try { 82 | const { contentId, debugKey } = data; 83 | 84 | // Read from Shared Storage 85 | const key = 'has-reported-content'; 86 | const hasReportedContent = (await sharedStorage.get(key)) === 'true'; 87 | const ageGroup = await sharedStorage.get('age-group'); 88 | const continent = await sharedStorage.get('continent'); 89 | 90 | // Do not report if a report has been sent already 91 | if (hasReportedContent) { 92 | console.log( 93 | `[PAA] Demographics measurement report has been submitted already for Content ID ${contentId}. Reset the demo to start over.` 94 | ); 95 | return; 96 | } 97 | 98 | if (!ageGroup || !continent) { 99 | console.log('[PAA] Demographics data is missing. Visit the survey data to enter your data.'); 100 | return; 101 | } 102 | 103 | // Generate the aggregation key and the aggregatable value 104 | const bucket = generateAggregationKey(data.contentId, ageGroup, continent); 105 | const value = 1 * SCALE_FACTOR; 106 | 107 | // Send an aggregatable report via the Private Aggregation API 108 | privateAggregation.enableDebugMode({ debugKey }); 109 | privateAggregation.contributeToHistogram({ bucket, value }); 110 | 111 | // Set the report submission status flag 112 | await sharedStorage.set(key, true); 113 | 114 | console.log(`[PAA] Unique reach measurement report will be submitted for Content ID ${data.contentId}`); 115 | console.log(`[PAA] The aggregation key is ${bucket} and the aggregatable value is ${value}`); 116 | } catch (e) { 117 | console.error(e); 118 | } 119 | } 120 | } 121 | 122 | // Register the operation 123 | register('demographics-measurement', DemographicsMeasurementOperation); 124 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/demographics-measurement.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 41 | 42 | 43 | 44 |
45 |
Demo control
46 |
47 | 50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/demographics-measurement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | async function measureDemographics() { 17 | // Load the Shared Storage worklet 18 | await window.sharedStorage.worklet.addModule('demographics-measurement-worklet.js'); 19 | 20 | // Run the demographics measurement operation 21 | await window.sharedStorage.run('demographics-measurement', { data: { contentId: '123', debugKey: 888n } }); 22 | } 23 | 24 | measureDemographics(); 25 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/demographics-survey.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | :where(h1, h2, h3, h4, h5, h6) { 17 | font-weight: var(--font-weight-6); 18 | } 19 | 20 | main { 21 | margin: 0 auto; 22 | padding: var(--size-5); 23 | } 24 | 25 | p { 26 | margin-bottom: var(--size-3); 27 | max-inline-size: var(--size-content-4); 28 | } 29 | 30 | card { 31 | display: flex; 32 | flex-direction: column; 33 | gap: var(--size-2); 34 | background: var(--surface-3); 35 | border: 1px solid var(--surface-1); 36 | padding: var(--size-4); 37 | border-radius: var(--radius-3); 38 | box-shadow: var(--shadow-2); 39 | } 40 | 41 | .demo__instructions { 42 | display: none; 43 | } 44 | 45 | .demo__header { 46 | padding: var(--size-5) 0; 47 | display: flex; 48 | align-items: center; 49 | } 50 | 51 | .demo__publisher-badge { 52 | display: flex; 53 | margin-right: var(--size-3); 54 | justify-content: center; 55 | align-items: center; 56 | padding: var(--size-3); 57 | } 58 | 59 | .demo__publisher-badge--publisher-a { 60 | background-color: var(--green-5); 61 | } 62 | 63 | .demo__publisher-badge--publisher-b { 64 | background-color: var(--orange-4); 65 | } 66 | 67 | .demo__content { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | } 72 | 73 | .demo__description { 74 | height: fit-content; 75 | } 76 | 77 | .demo__radio-label { 78 | display: block; 79 | } 80 | 81 | .demo__survey-container { 82 | width: 100%; 83 | } 84 | 85 | @media screen and (min-width: 481px) { 86 | main { 87 | max-width: 1000px; 88 | } 89 | 90 | .demo__content { 91 | flex-direction: row; 92 | align-items: flex-start; 93 | } 94 | .demo__description { 95 | margin-left: var(--size-3); 96 | } 97 | } 98 | 99 | @media (prefers-color-scheme: dark) { 100 | .demo__publisher-badge--publisher-a { 101 | background-color: var(--green-9); 102 | } 103 | 104 | .demo__publisher-badge--publisher-b { 105 | background-color: var(--orange-9); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/demographics-survey.js: -------------------------------------------------------------------------------- 1 | const form = document.querySelector('.demo__survey-form'); 2 | 3 | form.addEventListener('submit', async (event) => { 4 | event.preventDefault(); 5 | const ageGroup = form.querySelector('[name=age-group]:checked').value; 6 | const continent = form.querySelector('[name=continent]:checked').value; 7 | 8 | await window.sharedStorage.set('age-group', ageGroup); 9 | await window.sharedStorage.set('continent', continent); 10 | 11 | console.log('The following information has been set in Shared Storage:'); 12 | console.log(`ageGroup = ${ageGroup}`); 13 | console.log(`continent = ${continent}`); 14 | console.log('Visit the publisher page to continue the demo.'); 15 | 16 | const submitButtonEl = form.querySelector('.demo__submit-button'); 17 | const publisherLinkButtonEl = form.querySelector('.demo__continue-button'); 18 | submitButtonEl.style.display = 'none'; 19 | publisherLinkButtonEl.style.display = 'initial'; 20 | }); 21 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/k-freq-measurement-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * The scale factor is multiplied by the aggregatable value to maximize 19 | * the signal-to-noise ratio. See “Noise and scaling” section in the 20 | * Aggregation Fundamentals document to learn more: 21 | * - https://developer.chrome.com/en/docs/privacy-sandbox/aggregation-fundamentals 22 | * 23 | * The scale factor used here has been intentionally reduced to an arbitrary number. 24 | * The maximum aggregatable value used in this demo is 1. The scale factor 25 | * of 65536 would have maximized the signal-to-noise ratio. However, that also 26 | * uses the entire contribution budget for the rolling 24-hour period. 27 | * 28 | * Therefore, the scale factor has been significantly reduced to allow the demo 29 | * to be used repeatedly for testing throughout without hitting the contribution 30 | * budget limit. 31 | */ 32 | const SCALE_FACTOR = 128; 33 | 34 | /** 35 | * The bucket key must be a number, and in this case, it is simply the content 36 | * ID itself. For more complex buckey key construction, see other use cases in 37 | * this demo. 38 | */ 39 | function convertContentIdToBucket(contentId) { 40 | return BigInt(contentId); 41 | } 42 | 43 | class KFreqMeasurementOperation { 44 | async run(data) { 45 | try { 46 | const { kFreq, contentId, debugKey } = data; 47 | 48 | // Read from Shared Storage 49 | const hasReportedContentKey = 'has-reported-content'; 50 | const impressionCountKey = 'impression-count'; 51 | const hasReportedContent = (await sharedStorage.get(hasReportedContentKey)) === 'true'; 52 | const impressionCount = parseInt((await sharedStorage.get(impressionCountKey)) || 0); 53 | 54 | // Do not report if a report has been sent already 55 | if (hasReportedContent) { 56 | console.log( 57 | `[PAA] K-frequency report has been submitted already for Content ID ${contentId}. Reset the demo to start over.` 58 | ); 59 | return; 60 | } 61 | 62 | // Check ipmression count against frequency limit 63 | if (impressionCount < kFreq) { 64 | console.log( 65 | `[PAA] Current impression count is ${impressionCount} and has not met the K threashold of ${kFreq}` 66 | ); 67 | await sharedStorage.set(impressionCountKey, impressionCount + 1); 68 | return; 69 | } 70 | 71 | // Generate the aggregation key and the aggregatable value 72 | const bucket = convertContentIdToBucket(contentId); 73 | const value = 1 * SCALE_FACTOR; 74 | 75 | // Send an aggregatable report via the Private Aggregation API 76 | privateAggregation.enableDebugMode({ debugKey }); 77 | privateAggregation.contributeToHistogram({ bucket, value }); 78 | 79 | // Set the report submission status flag 80 | await sharedStorage.set(hasReportedContentKey, 'true'); 81 | 82 | console.log(`[PAA] Current impression count is ${impressionCount} which meets the K threashold of ${kFreq}`); 83 | console.log('[PAA] K-frequency measurement report will be submitted'); 84 | console.log(`[PAA] The aggregation key is ${bucket} and the aggregatable value is ${value}`); 85 | } catch (e) { 86 | console.log(e); 87 | } 88 | } 89 | } 90 | 91 | // Register the operation 92 | register('k-freq-measurement', KFreqMeasurementOperation); 93 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/k-freq-measurement.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 40 | 41 | 42 | 43 |
44 |
Demo control
45 |
46 | 49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/k-freq-measurement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | async function injectAd() { 18 | // Load the Shared Storage worklet 19 | await window.sharedStorage.worklet.addModule('k-freq-measurement-worklet.js'); 20 | 21 | // Run the K-frequency measurement operation 22 | await window.sharedStorage.run('k-freq-measurement', { data: { kFreq: 3, contentId: 123, debugKey: 999n } }); 23 | } 24 | 25 | injectAd(); 26 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/reach-measurement-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * The scale factor is multiplied by the aggregatable value to maximize 19 | * the signal-to-noise ratio. See “Noise and scaling” section in the 20 | * Aggregation Fundamentals document to learn more: 21 | * - https://developer.chrome.com/en/docs/privacy-sandbox/aggregation-fundamentals 22 | * 23 | * The scale factor used here has been intentionally reduced to an arbitrary number. 24 | * The maximum aggregatable value used in this demo is 1. The scale factor 25 | * of 65536 would have maximized the signal-to-noise ratio. However, that also 26 | * uses the entire contribution budget for the rolling 24-hour period. 27 | * 28 | * Therefore, the scale factor has been significantly reduced to allow the demo 29 | * to be used repeatedly for testing throughout without hitting the contribution 30 | * budget limit. 31 | */ 32 | const SCALE_FACTOR = 128; 33 | 34 | /** 35 | * The bucket key must be a number, and in this case, it is simply the ad campaign 36 | * ID itself. For more complex buckey key construction, see other use cases in this demo. 37 | */ 38 | function convertContentIdToBucket(contentId) { 39 | return BigInt(contentId); 40 | } 41 | 42 | class ReachMeasurementOperation { 43 | async run(data) { 44 | try { 45 | const { contentId, debugKey } = data; 46 | 47 | // Read from Shared Storage 48 | const key = 'has-reported-content'; 49 | const hasReportedContent = (await sharedStorage.get(key)) === 'true'; 50 | 51 | // Do not report if a report has been sent already 52 | if (hasReportedContent) { 53 | console.log( 54 | `[PAA] Unique reach measurement report has been submitted already for Content ID ${data.contentId}. Reset the demo to start over.` 55 | ); 56 | return; 57 | } 58 | 59 | // Generate the aggregation key and the aggregatable value 60 | const bucket = convertContentIdToBucket(contentId); 61 | const value = 1 * SCALE_FACTOR; 62 | 63 | // Send an aggregatable report via the Private Aggregation API 64 | privateAggregation.enableDebugMode({ debugKey }); 65 | privateAggregation.contributeToHistogram({ bucket, value }); 66 | 67 | // Set the report submission status flag 68 | await sharedStorage.set(key, true); 69 | 70 | console.log(`[PAA] Unique reach measurement report will be submitted for Content ID ${contentId}`); 71 | console.log(`[PAA] The aggregation key is ${bucket} and the aggregatable value is ${value}`); 72 | } catch (e) { 73 | console.log(e); 74 | } 75 | } 76 | } 77 | 78 | // Register the operation 79 | register('reach-measurement', ReachMeasurementOperation); 80 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/reach-measurement.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 39 | 40 | 41 | 42 |
43 |
Demo control
44 |
45 | 48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /sites/content-producer/private-aggregation/reach-measurement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | async function measureUniqueReach() { 17 | // Load the Shared Storage worklet 18 | await window.sharedStorage.worklet.addModule('reach-measurement-worklet.js'); 19 | 20 | // Run the reach measurement operation 21 | await window.sharedStorage.run('reach-measurement', { data: { contentId: '1234', debugKey: 777n } }); 22 | } 23 | 24 | measureUniqueReach(); 25 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/ab-testing-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This code is loaded as a Shared Storage worklet 18 | class SelectURLOperation { 19 | async run(urls, data) { 20 | // Read the user's group from shared storage 21 | const experimentGroup = await sharedStorage.get('ab-testing-group'); 22 | 23 | // Log to console for the demo 24 | console.log(`urls = ${JSON.stringify(urls)}`); 25 | console.log(`data = ${JSON.stringify(data)}`); 26 | console.log(`ab-testing-group in shared storage is ${experimentGroup}`); 27 | 28 | // Return the index of the group 29 | return data.indexOf(experimentGroup); 30 | } 31 | } 32 | 33 | function getBucketForTestingGroup(testingGroup) { 34 | switch (testingGroup) { 35 | case 'control': 36 | return 0; 37 | case 'experiment-a': 38 | return 1; 39 | case 'experiment-b': 40 | return 2; 41 | } 42 | } 43 | 44 | class ExperimentGroupReportingOperation { 45 | async run() { 46 | const experimentGroup = await sharedStorage.get('ab-testing-group'); 47 | 48 | const bucket = BigInt(getBucketForTestingGroup(experimentGroup)); 49 | privateAggregation.contributeToHistogram({ bucket, value: 1 }); 50 | } 51 | } 52 | 53 | // Register the operation as 'ab-testing' 54 | register('ab-testing', SelectURLOperation); 55 | register('experiment-group-reporting', ExperimentGroupReportingOperation); 56 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/ab-testing.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 40 | 41 | 42 | 43 | 44 |
45 |
Demo control
46 |
47 | 53 | 59 | 65 |
66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/ab-testing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // For demo purposes. The hostname is used to determine the usage of 18 | // development localhost URL vs production URL 19 | const contentProducerUrl = window.location.host; 20 | 21 | // Map the experiment groups to the URLs 22 | const EXPERIMENT_MAP = [ 23 | { 24 | group: 'control', 25 | url: `https://${contentProducerUrl}/ads/default-ad.html`, 26 | }, 27 | { 28 | group: 'experiment-a', 29 | url: `https://${contentProducerUrl}/ads/experiment-ad-a.html`, 30 | }, 31 | { 32 | group: 'experiment-b', 33 | url: `https://${contentProducerUrl}/ads/experiment-ad-b.html`, 34 | }, 35 | ]; 36 | 37 | // Choose a random group for the initial experiment 38 | function getRandomExperiment() { 39 | const randomIndex = Math.floor(Math.random() * EXPERIMENT_MAP.length); 40 | return EXPERIMENT_MAP[randomIndex].group; 41 | } 42 | 43 | async function injectAd() { 44 | // Load the worklet module 45 | const abTestingWorklet = await window.sharedStorage.createWorklet( 46 | 'ab-testing-worklet.js' 47 | ); 48 | 49 | // Set the initial value in the storage to a random experiment group 50 | window.sharedStorage.set('ab-testing-group', getRandomExperiment(), { 51 | ignoreIfPresent: true, 52 | }); 53 | 54 | const urls = EXPERIMENT_MAP.map(({ url }) => ({ url })); 55 | const groups = EXPERIMENT_MAP.map(({ group }) => group); 56 | 57 | // Resolve the selectURL call to a fenced frame config only when it exists on the page 58 | const resolveToConfig = typeof window.FencedFrameConfig !== 'undefined'; 59 | 60 | // Run the URL selection operation to select an ad based on the experiment group in shared storage 61 | const selectedUrl = await abTestingWorklet.selectURL('ab-testing', urls, { 62 | data: groups, 63 | resolveToConfig, 64 | keepAlive: true, 65 | }); 66 | 67 | const adSlot = document.getElementById('ad-slot'); 68 | 69 | if (resolveToConfig && selectedUrl instanceof FencedFrameConfig) { 70 | adSlot.config = selectedUrl; 71 | } else { 72 | adSlot.src = selectedUrl; 73 | } 74 | 75 | // Run the reporting operation 76 | await abTestingWorklet.run('experiment-group-reporting') 77 | } 78 | 79 | injectAd(); 80 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/creative-rotation-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | class SelectURLOperation { 18 | async run(urls, data) { 19 | // Initially set the storage to sequential mode for the demo 20 | await SelectURLOperation.seedStorage(); 21 | 22 | // Read the rotation mode from Shared Storage 23 | const rotationMode = await sharedStorage.get('creative-rotation-mode'); 24 | 25 | // Generate a random number to be used for rotation 26 | const randomNumber = Math.random(); 27 | 28 | let index; 29 | 30 | switch (rotationMode) { 31 | /** 32 | * Sequential rotation 33 | * - Rotates the creatives in order 34 | * - Example: A -> B -> C -> A ... 35 | */ 36 | case 'sequential': 37 | const currentIndex = await sharedStorage.get('creative-rotation-index'); 38 | index = parseInt(currentIndex, 10); 39 | const nextIndex = (index + 1) % urls.length; 40 | 41 | console.log(`index = ${index} / next index = ${nextIndex}`); 42 | 43 | await sharedStorage.set('creative-rotation-index', nextIndex.toString()); 44 | break; 45 | 46 | /** 47 | * Evenly-distributed rotation 48 | * - Rotates the creatives with equal probability 49 | * - Example: A=33% / B=33% / C=33% 50 | */ 51 | case 'even-distribution': 52 | index = Math.floor(randomNumber * urls.length); 53 | break; 54 | 55 | /** 56 | * Weighted rotation 57 | * - Rotates the creatives with weighted probability 58 | * - Example: A=70% / B=20% / C=10% 59 | */ 60 | case 'weighted-distribution': 61 | console.log('data = ', JSON.stringify(data)); 62 | // Find the first URL where the cumnulative sum of the weights 63 | // exceed the random number. The array is sorted by the weight 64 | // in descending order. 65 | let weightSum = 0; 66 | const { url } = data 67 | .sort((a, b) => b.weight - a.weight) 68 | .find(({ weight }) => { 69 | weightSum += weight; 70 | return weightSum > randomNumber; 71 | }); 72 | 73 | index = urls.indexOf(url); 74 | break; 75 | 76 | default: 77 | index = 0; 78 | } 79 | 80 | console.log(JSON.stringify({ index, randomNumber, rotationMode })); 81 | return index; 82 | } 83 | 84 | // Set the mode to sequential and set the starting index to 0. 85 | static async seedStorage() { 86 | await sharedStorage.set('creative-rotation-mode', 'sequential', { 87 | ignoreIfPresent: true, 88 | }); 89 | 90 | await sharedStorage.set('creative-rotation-index', 0, { 91 | ignoreIfPresent: true, 92 | }); 93 | } 94 | } 95 | 96 | class SetRotationModeOperation { 97 | async run({ rotationMode }) { 98 | await sharedStorage.set('creative-rotation-mode', rotationMode); 99 | } 100 | } 101 | 102 | // Register the operation as 'creative-rotation' 103 | register('creative-rotation', SelectURLOperation); 104 | register('set-rotation-mode', SetRotationModeOperation); 105 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/creative-rotation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // For demo purposes. The hostname is used to determine the usage of 18 | // development localhost URL vs production URL 19 | const contentProducerUrl = window.getContentProducerUrl(); 20 | 21 | // Ad confg with the URL of the ad, a probability weight for rotation, and the clickthrough rate. 22 | const DEMO_AD_CONFIG = [ 23 | { 24 | url: `${contentProducerUrl}/ads/ad-1.html`, 25 | weight: 0.7, 26 | }, 27 | { 28 | url: `${contentProducerUrl}/ads/ad-2.html`, 29 | weight: 0.2, 30 | }, 31 | { 32 | url: `${contentProducerUrl}/ads/ad-3.html`, 33 | weight: 0.1, 34 | }, 35 | ]; 36 | 37 | async function setRotationMode(rotationMode) { 38 | // Load the worklet module 39 | const creativeRotationWorklet = await window.sharedStorage.createWorklet( 40 | `${contentProducerUrl}/url-selection/creative-rotation-worklet.js`, 41 | { dataOrigin: 'script-origin' } 42 | ); 43 | 44 | await creativeRotationWorklet.run('set-rotation-mode', { 45 | data: { rotationMode } 46 | }); 47 | console.log(`creative rotation mode set to ${rotationMode}`); 48 | } 49 | 50 | async function injectAd() { 51 | // Load the worklet module 52 | const creativeRotationWorklet = await window.sharedStorage.createWorklet( 53 | `${contentProducerUrl}/url-selection/creative-rotation-worklet.js`, 54 | { dataOrigin: 'script-origin' } 55 | ); 56 | 57 | const urls = DEMO_AD_CONFIG.map(({ url }) => ({ url })); 58 | 59 | // Resolve the selectURL call to a fenced frame config only when it exists on the page 60 | const resolveToConfig = typeof window.FencedFrameConfig !== 'undefined'; 61 | 62 | // Run the URL selection operation to determine the next ad that should be rendered 63 | const selectedUrl = await creativeRotationWorklet.selectURL('creative-rotation', urls, { 64 | data: DEMO_AD_CONFIG, 65 | resolveToConfig 66 | }); 67 | 68 | const adSlot = document.getElementById('ad-slot'); 69 | 70 | if (resolveToConfig && selectedUrl instanceof FencedFrameConfig) { 71 | adSlot.config = selectedUrl; 72 | } else { 73 | adSlot.src = selectedUrl; 74 | } 75 | } 76 | 77 | injectAd(); 78 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/creative-selection-by-frequency-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const FREQUENCY_LIMIT = 5; 18 | 19 | class CreativeSelectionByFrequencyOperation { 20 | async run(urls, data) { 21 | // Read the current frequency cap in shared storage 22 | const count = parseInt(await sharedStorage.get('frequency-count')); 23 | 24 | // Log to console for the demo 25 | console.log(`urls = ${JSON.stringify(urls)}`); 26 | console.log(`frequency-count in shared storage is ${count}`); 27 | 28 | // If the count is 0, the frequency cap has been reached 29 | if (count === FREQUENCY_LIMIT) { 30 | console.log('frequency limit has been reached, and the default ad will be rendered'); 31 | return 0; 32 | } 33 | 34 | // Increment the frequency 35 | await sharedStorage.set('frequency-count', count + 1); 36 | return 1; 37 | } 38 | } 39 | 40 | // Register the operation 41 | register('creative-selection-by-frequency', CreativeSelectionByFrequencyOperation); 42 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/creative-selection-by-frequency.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 |
44 |
Demo control
45 |
46 | 49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/creative-selection-by-frequency.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // For demo purposes. The hostname is used to determine the usage of 18 | // development localhost URL vs production URL 19 | const contentProducerUrl = window.location.host; 20 | 21 | const AD_URLS = [ 22 | { url: `https://${contentProducerUrl}/ads/default-ad.html` }, 23 | { url: `https://${contentProducerUrl}/ads/example-ad.html` }, 24 | ]; 25 | 26 | async function injectAd() { 27 | // Load the worklet module 28 | await window.sharedStorage.worklet.addModule('creative-selection-by-frequency-worklet.js'); 29 | 30 | // Set the initial frequency cap to 5 31 | window.sharedStorage.set('frequency-count', 0, { 32 | ignoreIfPresent: true, 33 | }); 34 | 35 | // Resolve the selectURL call to a fenced frame config only when it exists on the page 36 | const resolveToConfig = typeof window.FencedFrameConfig !== 'undefined'; 37 | 38 | // Run the URL selection operation to choose an ad based on the frequency cap in shared storage 39 | const selectedUrl = await window.sharedStorage.selectURL('creative-selection-by-frequency', AD_URLS, { 40 | resolveToConfig 41 | }); 42 | 43 | const adSlot = document.getElementById('ad-slot'); 44 | 45 | if (resolveToConfig && selectedUrl instanceof FencedFrameConfig) { 46 | adSlot.config = selectedUrl; 47 | } else { 48 | adSlot.src = selectedUrl; 49 | } 50 | } 51 | 52 | injectAd(); 53 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/hover-event-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This code is loaded as a Shared Storage worklet 18 | class SelectURLOperation { 19 | async run() { 20 | return 0; 21 | } 22 | } 23 | 24 | // Register the operation as 'hover-event' 25 | register('hover-event', SelectURLOperation); 26 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/hover-event.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/hover-event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // For demo purposes. The hostname is used to determine the usage of 18 | // development localhost URL vs production URL 19 | const CONTENT_ID = 1234; 20 | const contentProducerUrl = window.location.host; 21 | const AD_URLS = [ 22 | { 23 | url: `https://${contentProducerUrl}/ads/hover-ad.html`, 24 | reportingMetadata: { 25 | hover: `https://${contentProducerUrl}/report/hover?contentId=${CONTENT_ID}` 26 | } 27 | } 28 | ]; 29 | 30 | async function injectAd() { 31 | // Load the worklet module 32 | await window.sharedStorage.worklet.addModule('hover-event-worklet.js'); 33 | 34 | // Resolve the selectURL call to a fenced frame config only when it exists on the page 35 | const resolveToConfig = typeof window.FencedFrameConfig !== 'undefined'; 36 | 37 | // Run the URL selection operation to select an ad based on the experiment group in shared storage 38 | const selectedUrl = await window.sharedStorage.selectURL('hover-event', AD_URLS, { 39 | resolveToConfig, 40 | }); 41 | 42 | const adSlot = document.getElementById('ad-slot'); 43 | 44 | if (resolveToConfig && selectedUrl instanceof FencedFrameConfig) { 45 | adSlot.config = selectedUrl; 46 | } else { 47 | adSlot.src = selectedUrl; 48 | } 49 | } 50 | 51 | injectAd(); 52 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/known-customer-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | class SelectURLOperation { 18 | async run(urls) { 19 | const knownCustomer = await sharedStorage.get('known-customer'); 20 | 21 | console.log(`urls = ${JSON.stringify(urls)}`); 22 | console.log(`known-customer value is ${knownCustomer}`); 23 | 24 | // '0' is unknown and '1' is known 25 | return parseInt(knownCustomer); 26 | } 27 | } 28 | 29 | // Register the operation as 'known-customer' 30 | register('known-customer', SelectURLOperation); 31 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/known-customer.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Shared storage demo 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/known-customer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // For demo purposes. The hostname is used to determine the usage of 18 | // development localhost URL vs production URL 19 | const contentProducerUrl = window.location.host; 20 | 21 | // The first URL is the "register" button to be rendered if the user is not known 22 | const AD_URLS = [ 23 | { url: `https://${contentProducerUrl}/ads/register-button.html` }, 24 | { url: `https://${contentProducerUrl}/ads/buy-now-button.html` }, 25 | ]; 26 | 27 | async function injectAd() { 28 | // Load the worklet module 29 | await window.sharedStorage.worklet.addModule('known-customer-worklet.js'); 30 | 31 | // Set the initial status to unknown ('0' is unknown and '1' is known) 32 | window.sharedStorage.set('known-customer', 0, { 33 | ignoreIfPresent: true, 34 | }); 35 | 36 | // Resolve the selectURL call to a fenced frame config only when it exists on the page 37 | const resolveToConfig = typeof window.FencedFrameConfig !== 'undefined'; 38 | 39 | // Run the URL selection operation to choose the button based on the user status 40 | const selectedUrl = await window.sharedStorage.selectURL('known-customer', AD_URLS, { 41 | resolveToConfig, 42 | }); 43 | 44 | const adSlot = document.getElementById('button-slot'); 45 | 46 | if (resolveToConfig && selectedUrl instanceof FencedFrameConfig) { 47 | adSlot.config = selectedUrl; 48 | } else { 49 | adSlot.src = selectedUrl; 50 | } 51 | } 52 | 53 | injectAd(); 54 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/top-level-nav-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This code is loaded as a Shared Storage worklet 18 | class SelectURLOperation { 19 | async run() { 20 | return 0; 21 | } 22 | } 23 | 24 | register('top-level-nav', SelectURLOperation); 25 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/top-level-nav.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /sites/content-producer/url-selection/top-level-nav.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // For demo purposes. The hostname is used to determine the usage of 18 | // development localhost URL vs production URL 19 | const contentProducerUrl = window.location.host; 20 | const AD_URLS = [ 21 | { url: `https://${contentProducerUrl}/ads/nav-ad-1.html` }, 22 | { url: `https://${contentProducerUrl}/ads/nav-ad-2.html` }, 23 | { url: `https://${contentProducerUrl}/ads/nav-ad-3.html` }, 24 | ]; 25 | 26 | async function injectAd() { 27 | // Load the worklet module 28 | await window.sharedStorage.worklet.addModule('top-level-nav-worklet.js'); 29 | 30 | // Resolve the selectURL call to a fenced frame config only when it exists on the page 31 | const resolveToConfig = typeof window.FencedFrameConfig !== 'undefined'; 32 | 33 | // Run the URL selection operation to select an ad based on the experiment group in shared storage 34 | const selectedUrl = await window.sharedStorage.selectURL('top-level-nav', AD_URLS, { 35 | resolveToConfig, 36 | }); 37 | 38 | const adSlot = document.getElementById('ad-slot'); 39 | 40 | if (resolveToConfig && selectedUrl instanceof FencedFrameConfig) { 41 | adSlot.config = selectedUrl; 42 | } else { 43 | adSlot.src = selectedUrl; 44 | } 45 | } 46 | 47 | injectAd(); 48 | -------------------------------------------------------------------------------- /sites/content-producer/use-cases/third-party-write.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Shared storage demo 23 | 24 | 25 | 26 | 27 | 31 | 32 | 43 | 44 | 45 | 46 |
47 |
Demo control
48 |
49 | 52 | 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /sites/content-producer/use-cases/third-party-write.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('message', ({ data }) => { 2 | switch (data) { 3 | case 'run-third-party-write-demo': 4 | window.sharedStorage.set('third-party-write-demo', 'written into publisher storage by third-party code'); 5 | break; 6 | case 'clear-publisher-data': 7 | window.sharedStorage.delete('third-party-write-demo'); 8 | break; 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /sites/home/404.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | Page Not Found 23 | 24 | 93 | 94 | 95 |
96 |

404

97 |

Page Not Found

98 |

The specified file was not found on this website. Check the URL for mistakes and try again.

99 |

Why am I seeing this?

100 |

101 | This page was generated by the Firebase Command-Line Interface. To modify it, edit the 102 | 404.html file in your project's configured public directory. 103 |

104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /sites/home/assets/privacy-sandbox-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/shared-storage-demo/3f194de67c956ee421237cc8a402f575993c13de/sites/home/assets/privacy-sandbox-logo.png -------------------------------------------------------------------------------- /sites/home/decoder/decoder.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | .decoder__input { 17 | width: 100%; 18 | } 19 | 20 | .decoder__run { 21 | margin-top: var(--size-4); 22 | } 23 | 24 | .decoder__output-container { 25 | margin-top: var(--size-4); 26 | } 27 | 28 | .decoder__output-table th { 29 | padding: var(--size-2); 30 | /* width: 40%; */ 31 | } 32 | 33 | .decoder__output-table td { 34 | text-align: center; 35 | /* max-width: 150px; */ 36 | overflow-wrap: anywhere; 37 | padding: 0 20px; 38 | } 39 | -------------------------------------------------------------------------------- /sites/home/decoder/decoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Placeholder for 8-bits to be used for padding 18 | const ZEROES_FOR_PADDING = '00000000'; 19 | 20 | function renderOutput({ bucketInBinary, valueInBinary }) { 21 | document.querySelector('.decoder__bucket--binary').innerHTML = bucketInBinary; 22 | document.querySelector('.decoder__bucket--decimal').innerHTML = BigInt(`0b${bucketInBinary}`); 23 | document.querySelector('.decoder__value--binary').innerHTML = valueInBinary; 24 | document.querySelector('.decoder__value--decimal').innerHTML = BigInt(`0b${valueInBinary}`); 25 | } 26 | 27 | // Converts the number to a binary string 28 | function covertToBinary(input) { 29 | return input 30 | // Filter out 0s 31 | .filter((n) => n) 32 | // Converts number to binary, and moves it to an array from a typed array to an array 33 | .reduce((acc, n) => [...acc, n.toString(2)], []) 34 | // Pad the left of the value and make it a length of 8 35 | .map((n) => ZEROES_FOR_PADDING.slice(0, 8 - n.length) + n) 36 | .join(''); 37 | } 38 | 39 | async function decodePayload(payload) { 40 | // Base64 decode 41 | const arr = new Uint8Array([...atob(payload)] 42 | .map((c) => c.charCodeAt(0))); 43 | 44 | // CBOR decode 45 | const { 46 | data: [{ bucket, value }], 47 | } = await cbor.decodeFirst(arr); 48 | 49 | return { 50 | bucketInBinary: covertToBinary(bucket), 51 | valueInBinary: covertToBinary(value), 52 | }; 53 | } 54 | 55 | async function runDecoder() { 56 | const payload = document.querySelector('.decoder__input').value; 57 | 58 | if (!payload) { 59 | return; 60 | } 61 | 62 | const output = await decodePayload(payload); 63 | renderOutput(output); 64 | } 65 | -------------------------------------------------------------------------------- /sites/home/decoder/index.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Debug payload decoder 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | 42 |

Debug payload decoder for the Private Aggregation API

43 |
44 |
45 |
46 |

INPUT

47 |

Copy-paste your debug payload below

48 | 49 |
50 | 51 |
52 |
53 |
54 |
55 |

OUTPUT

56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
 BinaryDecimal
Bucket--
Value--
73 |
74 |
75 |
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /sites/home/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | :where(h1, h2, h3, h4, h5, h6) { 17 | font-weight: var(--font-weight-6); 18 | } 19 | 20 | main { 21 | margin: 0 auto; 22 | padding: var(--size-5); 23 | } 24 | 25 | p, 26 | h3 { 27 | margin-bottom: var(--size-3); 28 | max-inline-size: none; 29 | } 30 | 31 | card { 32 | flex-basis: var(--size-content-1); 33 | display: flex; 34 | flex-direction: column; 35 | gap: var(--size-2); 36 | background: var(--surface-3); 37 | border: 1px solid var(--surface-1); 38 | padding: var(--size-4); 39 | border-radius: var(--radius-3); 40 | box-shadow: var(--shadow-2); 41 | } 42 | 43 | .demo__header { 44 | display: flex; 45 | align-items: center; 46 | } 47 | 48 | .demo__header img { 49 | height: var(--size-9); 50 | margin-right: var(--size-3); 51 | } 52 | 53 | .demo__description p { 54 | max-inline-size: var(--size-content-4); 55 | } 56 | 57 | .demo__description h4 { 58 | margin-bottom: var(--size-4); 59 | } 60 | 61 | .demo__button-container { 62 | display: flex; 63 | margin: var(--size-3) 0; 64 | } 65 | 66 | .demo__button { 67 | zoom: 90%; 68 | max-width: var(--size-13); 69 | --_bg: linear-gradient(var(--indigo-5), var(--indigo-7)); 70 | --_border: var(--indigo-6); 71 | --_text: var(--indigo-0); 72 | --_ink-shadow: 0 1px 0 var(--indigo-9); 73 | margin-right: var(--size-2); 74 | } 75 | 76 | .demo__header { 77 | padding: var(--size-5) 0; 78 | } 79 | 80 | .demo__description { 81 | padding: var(--size-5) 0; 82 | } 83 | 84 | .demo__card-container { 85 | display: grid; 86 | gap: var(--size-5); 87 | } 88 | 89 | .demo__code-inline-break { 90 | display: inline-block; 91 | white-space: normal; 92 | max-width: 100%; 93 | word-break: break-all; 94 | word-wrap: break-word; 95 | } 96 | 97 | .demo__gate-container { 98 | background: var(--surface-2); 99 | border: 1px solid var(--surface-1); 100 | padding: var(--size-4); 101 | border-radius: var(--radius-3); 102 | box-shadow: var(--shadow-2); 103 | display: grid; 104 | grid-template-columns: repeat(2, 1fr); 105 | grid-template-rows: 1fr; 106 | gap: var(--size-5); 107 | } 108 | 109 | .demo__gate-header { 110 | grid-column-start: 1; 111 | grid-column-end: 3; 112 | overflow-wrap: break-word; 113 | } 114 | 115 | @media screen and (max-width: 800px) { 116 | .demo__card-container { 117 | grid-template-columns: 1fr; 118 | } 119 | 120 | card { 121 | max-width: 90%; 122 | } 123 | } 124 | 125 | @media screen and (min-width: 481px) { 126 | main { 127 | max-width: 1000px; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /sites/index.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | Welcome to Firebase Hosting 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 105 | 106 | 107 |
108 |

Welcome

109 |

Firebase Hosting Setup Complete

110 |

111 | You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something 112 | extraordinary! 113 |

114 | Open Hosting Documentation 115 |
116 |

Firebase SDK Loading…

117 | 118 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /sites/publisher-a/.well-known/privacy-sandbox-attestations.json: -------------------------------------------------------------------------------- 1 | { 2 | "privacy_sandbox_api_attestations": [ 3 | { 4 | "attestation_parser_version": "2", 5 | "attestation_version": "1", 6 | "privacy_policy": [ 7 | "https://policies.google.com/privacy" 8 | ], 9 | "ownership_token": "GMALZHbWrzA8rAnaCrTErsuqwncfRAVVentXKgDprUbHVJF3vifABmYZas9QdB3C", 10 | "issued_seconds_since_epoch": 1724335427, 11 | "enrollment_id": "BZBN7", 12 | "enrollment_site": "https://shared-storage-demo-publisher-a.web.app/", 13 | "platform_attestations": [ 14 | { 15 | "platform": "chrome", 16 | "attestations": { 17 | "shared_storage_api": { 18 | "ServiceNotUsedForIdentifyingUserAcrossSites": true 19 | }, 20 | "private_aggregation_api": { 21 | "ServiceNotUsedForIdentifyingUserAcrossSites": true 22 | } 23 | } 24 | }, 25 | { 26 | "platform": "android", 27 | "attestations": {} 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /sites/publisher-a/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | :where(h1, h2, h3, h4, h5, h6) { 17 | font-weight: var(--font-weight-6); 18 | } 19 | 20 | main { 21 | margin: 0 auto; 22 | padding: var(--size-5); 23 | } 24 | 25 | p { 26 | margin-bottom: var(--size-3); 27 | max-inline-size: var(--size-content-4); 28 | } 29 | 30 | .demo__dsp-iframe { 31 | height: 590px; 32 | } 33 | 34 | card { 35 | display: flex; 36 | flex-direction: column; 37 | gap: var(--size-2); 38 | background: var(--surface-3); 39 | border: 1px solid var(--surface-1); 40 | padding: var(--size-4); 41 | border-radius: var(--radius-3); 42 | box-shadow: var(--shadow-2); 43 | } 44 | 45 | .demo__instructions { 46 | display: none; 47 | } 48 | 49 | .demo__header { 50 | padding: var(--size-5) 0; 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | .demo__publisher-badge { 56 | display: flex; 57 | margin-right: var(--size-3); 58 | justify-content: center; 59 | align-items: center; 60 | padding: var(--size-3); 61 | } 62 | 63 | .demo__publisher-badge--publisher-a { 64 | background-color: var(--green-5); 65 | } 66 | 67 | .demo__publisher-badge--publisher-b { 68 | background-color: var(--orange-4); 69 | } 70 | 71 | .demo__content { 72 | display: flex; 73 | flex-direction: column; 74 | align-items: center; 75 | } 76 | 77 | .demo__description { 78 | height: fit-content; 79 | } 80 | 81 | .demo__dsp-iframe--measurement { 82 | height: 350px; 83 | } 84 | 85 | @media screen and (min-width: 481px) { 86 | main { 87 | max-width: 1000px; 88 | } 89 | 90 | .demo__content { 91 | flex-direction: row; 92 | align-items: flex-start; 93 | } 94 | .demo__description { 95 | margin-left: var(--size-3); 96 | } 97 | } 98 | 99 | @media (prefers-color-scheme: dark) { 100 | .demo__publisher-badge--publisher-a { 101 | background-color: var(--green-9); 102 | } 103 | 104 | .demo__publisher-badge--publisher-b { 105 | background-color: var(--orange-9); 106 | } 107 | } 108 | 109 | .demo__controls-section { 110 | width: 300px; 111 | } 112 | #ad-slot { 113 | height: 250px; 114 | border: none; 115 | } 116 | 117 | #button-slot { 118 | height: 100px; 119 | border: none; 120 | } 121 | 122 | .demo__card, 123 | .demo__controls { 124 | display: flex; 125 | flex-direction: column; 126 | align-items: center; 127 | gap: var(--size-2); 128 | background: var(--surface-3); 129 | border: 1px solid var(--surface-1); 130 | padding: var(--size-3); 131 | margin: var(--size-3) 0; 132 | border-radius: var(--radius-3); 133 | box-shadow: var(--shadow-2); 134 | margin-top: 0; 135 | } 136 | 137 | .demo__controls h5 { 138 | margin-bottom: var(--size-2); 139 | } 140 | 141 | .demo__buttons-container { 142 | display: flex; 143 | flex-direction: column; 144 | width: fit-content; 145 | } 146 | 147 | .demo__button { 148 | zoom: 90%; 149 | /* max-width: var(--size-13); */ 150 | --_bg: linear-gradient(var(--indigo-5), var(--indigo-7)); 151 | --_border: var(--indigo-6); 152 | --_text: var(--indigo-0); 153 | --_ink-shadow: 0 1px 0 var(--indigo-9); 154 | margin: 5px; 155 | } 156 | -------------------------------------------------------------------------------- /sites/publisher-b/.well-known/privacy-sandbox-attestations.json: -------------------------------------------------------------------------------- 1 | { 2 | "privacy_sandbox_api_attestations": [ 3 | { 4 | "attestation_parser_version": "2", 5 | "attestation_version": "1", 6 | "privacy_policy": [ 7 | "https://policies.google.com/privacy" 8 | ], 9 | "ownership_token": "jaNybzmPQosoLJSp9OiOlY3UZfFHCMAsKukhACJaEulhPgh8OhlnijY9zKdcUn7P", 10 | "issued_seconds_since_epoch": 1727275740, 11 | "enrollment_id": "G7ZNZ", 12 | "enrollment_site": "https://shared-storage-demo-publisher-b.web.app/", 13 | "platform_attestations": [ 14 | { 15 | "platform": "chrome", 16 | "attestations": { 17 | "shared_storage_api": { 18 | "ServiceNotUsedForIdentifyingUserAcrossSites": true 19 | }, 20 | "private_aggregation_api": { 21 | "ServiceNotUsedForIdentifyingUserAcrossSites": true 22 | } 23 | } 24 | }, 25 | { 26 | "platform": "android", 27 | "attestations": {} 28 | } 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /sites/publisher-b/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | :where(h1, h2, h3, h4, h5, h6) { 17 | font-weight: var(--font-weight-6); 18 | } 19 | 20 | main { 21 | margin: 0 auto; 22 | padding: var(--size-5); 23 | } 24 | 25 | p { 26 | margin-bottom: var(--size-3); 27 | max-inline-size: var(--size-content-4); 28 | } 29 | 30 | .demo__dsp-iframe { 31 | height: 590px; 32 | } 33 | 34 | card { 35 | display: flex; 36 | flex-direction: column; 37 | gap: var(--size-2); 38 | background: var(--surface-3); 39 | border: 1px solid var(--surface-1); 40 | padding: var(--size-4); 41 | border-radius: var(--radius-3); 42 | box-shadow: var(--shadow-2); 43 | } 44 | 45 | .demo__instructions { 46 | display: none; 47 | } 48 | 49 | .demo__header { 50 | padding: var(--size-5) 0; 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | .demo__publisher-badge { 56 | display: flex; 57 | margin-right: var(--size-3); 58 | justify-content: center; 59 | align-items: center; 60 | padding: var(--size-3); 61 | } 62 | 63 | .demo__publisher-badge--publisher-a { 64 | background-color: var(--green-5); 65 | } 66 | 67 | .demo__publisher-badge--publisher-b { 68 | background-color: var(--orange-4); 69 | } 70 | 71 | .demo__content { 72 | display: flex; 73 | flex-direction: column; 74 | align-items: center; 75 | } 76 | 77 | .demo__description { 78 | height: fit-content; 79 | } 80 | 81 | @media screen and (min-width: 481px) { 82 | main { 83 | max-width: 1000px; 84 | } 85 | 86 | .demo__content { 87 | flex-direction: row; 88 | align-items: flex-start; 89 | } 90 | .demo__description { 91 | margin-left: var(--size-3); 92 | } 93 | } 94 | 95 | @media (prefers-color-scheme: dark) { 96 | .demo__publisher-badge--publisher-a { 97 | background-color: var(--green-9); 98 | } 99 | 100 | .demo__publisher-badge--publisher-b { 101 | background-color: var(--orange-9); 102 | } 103 | } 104 | 105 | .demo__controls-section { 106 | width: 300px; 107 | } 108 | 109 | #ad-slot { 110 | height: 250px; 111 | border: none; 112 | } 113 | 114 | #button-slot { 115 | height: 100px; 116 | border: none; 117 | } 118 | 119 | .demo__card, 120 | .demo__controls { 121 | display: flex; 122 | flex-direction: column; 123 | align-items: center; 124 | gap: var(--size-2); 125 | background: var(--surface-3); 126 | border: 1px solid var(--surface-1); 127 | padding: var(--size-3); 128 | margin: var(--size-3) 0; 129 | border-radius: var(--radius-3); 130 | box-shadow: var(--shadow-2); 131 | margin-top: 0; 132 | } 133 | 134 | .demo__controls h5 { 135 | margin-bottom: var(--size-2); 136 | } 137 | 138 | .demo__buttons-container { 139 | display: flex; 140 | flex-direction: column; 141 | width: fit-content; 142 | } 143 | 144 | .demo__button { 145 | zoom: 90%; 146 | /* max-width: var(--size-13); */ 147 | --_bg: linear-gradient(var(--indigo-5), var(--indigo-7)); 148 | --_border: var(--indigo-6); 149 | --_text: var(--indigo-0); 150 | --_ink-shadow: 0 1px 0 var(--indigo-9); 151 | margin: 5px; 152 | } 153 | --------------------------------------------------------------------------------