Browser Elements: Part 3: Navigators, Promises, and Beacons

March 25, 2024
Chapter 2: Browser Elements

Today we dive into the last three elements of the main browser frame before moving into browser storage: navigators, promises and beacons.  These elements are not specific to the Privacy Sandbox.  Promises are a standard structure in JavaScript; navigators and beacons are core browser elements in HTML5.  However, they are used extensively by the Sandbox.  As a result, we will need to understand what they are when we explore details of the three main APIs in later posts.

Navigators

What Are Navigators

A navigator is a built-in object of web browsers in the HTML specification that allows the developer to query the state of various elements of the browser itself, the user's environment, and the current webpage. 

Navigators are supported by all major browsers and have been part of HTML since an update to HTML 3.2 in 1999.  They are deeply ingrained in HTML and have evolved substantially over the years.  You will find that navigators are ubiquitous in JavaScript approaches to client-side functionality.

Navigators provide a restricted set of information about the user and their environment. They focus on non-personally identifiable details (like browser name, language) or require user permission for potentially sensitive data (like location).  In that sense, they are considered privacy-preserving.  However despite their intent, some navigator properties (like user agent combined with screen size, etc.) can be used for "fingerprinting." By combining seemingly innocuous details, a website might be able to uniquely identify a user across different websites, raising privacy concerns. Which means their use could violate a core design principle of the Privacy Sandbox.  

Modern browsers and coding practices often sandbox functionalities within navigators, preventing them from accessing more sensitive information about the user's device or browsing history.  This is how the Privacy Sandbox employs them, thus reducing the potential for fingerprinting.  

The reason navigators are so important to the Privacy Sandbox is due to the amount of data stored in the browser and referenced via the user agent header.  Interest group functionality is the most visible example of this.  During an auction, finding interest groups that can bid in the auction involves a call to the user agent header to access that information.  Without navigators, accessing this data would be much more difficult and, I would expect, have higher processing overhead in a situation where scaling Sandbox functionality requires code that minimizes processing time.

Thus the Privacy Sandbox is making a tradeoff between, on the one hand,  coding efficiency as well as control over the user's environment and, on the other hand, protecting user privacy.  

Let me give a few examples of generic code that is easy to understand even if you aren’t a coder and then let me provide an example of how the Privacy Sandbox uses navigators.

Generic Use Case 1

The first simple example is where a web application uses the navigator.userAgent property to identify the user's browser type (e.g., Chrome, Firefox) for basic compatibility checks. 

if (navigator.userAgent.indexOf("Chrome") !== -1) {
	console.log("This user is likely using Chrome.");
}

You can see the structure: navigator + userAgent + indexOf:

  • navigator tells the browser it wants to access browser status
  • userAgent is the read-only property that returns the user agent string for the current browser
  • indexOf() is a function that returns the value of the item queried for in the string that has been returned.  

Generic Use Case 2

Web applications can use the navigator.geolocation endpoint to request the user's location with their permission. This can be helpful for features like weather apps or location-based services.  

navigator.geolocation.getCurrentPosition(
	(position) => {
		console.log("Latitude:", position.coords.latitude);
		console.log("Longitude:", position.coords.longitude);
	},
(error) => {
	console.error("Error getting geolocation:", error.message);
	}
);

I think the code is pretty self-explanatory.

id="#interestgroups"

Interest Group Functions in the Privacy Sandbox

This is our first real, if small, foray into the details of Privacy Sandbox - so welcome to the Sandbox!  We will revisit the topics covered here in greater detail in later posts about interest groups.  For now I’ll do my best to give a short but useful explanation of what you are seeing.

As mentioned previously, one of the major design principles of the Privacy Sandbox is to keep all data in the user’s browser.  A core piece of data are interest groups.  In the most simple description, interest groups represent the audiences that a browser “fits’ into based either on:

  • an advertiser or publisher “telling” the browser that
  • the user being automatically added to an interest group through algorithmic classification based on the user’s behavior in the browser.   

For each interest group, the browser stores information about who owns the group, what ads the group might choose to show, various JavaScript functions and metadata used in bidding and rendering, and what servers to contact to update or supplement that information.

The functions around interest groups that draw upon navigators include:

  • joinAdInterestGroup()
  • leaveAdInterestGroup()
  • clearOriginJoinedAdInterestGroups()
  • runAdAuction()

Let’s look at joinAdInterestGroup() to get a sense of how the Privacy Sandbox uses navigators.

The structure of the navigator for joinAdInterestGroup is very simple:

const joinPromise = navigator.joinAdInterestGroup(myGroup);

The complexity comes with the parameter for joinAdInterestGroup called identified in this code snippet as myGroupmyGroup is a JSON structure that provides all potential information needed to join an auction.  This is not the time to get into that structure.  However, the next partial code snippet  give you a small sense of what the code to invoke the joinAdInterestGroup() function looks like:

const myGroup = {
	'owner': 'https://www.example-dsp.com',
    'name': 'womens-running-shoes',
...
(more parameters I have removed to be covered later)
...
	'ads': [{renderURL: shoesAd1, sizeGroup: 'group1', ...},
			{renderURL: shoesAd2, sizeGroup: 'group2', ...},
    		{renderURL: shoesAd3, sizeGroup: 'size3', ...}],
...
(more extensive parameters about the ads I have removed to be covered later)
...
};
const joinPromise = navigator.joinAdInterestGroup(myGroup);

You can see pretty easily what is happening.  The part of the myGroup structure shown defines:

  • the owner of the interest group
  • the name of the interest group
  • The ads to be associated with the interest group that can potentially be shown when this interest group wins a bid

The navigator then calls the joinAdInterestGroup() function which stores the interest group in partitioned storage both individually and in what is called an interest group set.

Hopefully that gives you a pretty good sense of what a navigator is and a simple idea of how  that structure is used in the Privacy Sandbox

Promises

In the prior example, you may have noticed the constant called “joinPromise”.  While this is just a constant name in code, the name is relevant because it refers to a JavaScript element called, not surprisingly, a Promise. Promises run asynchronous operations in Javascript.  These instructions run in the background until they finish processing, and they do not stop the JavaScript engine from accepting and processing more instructions.  

I think it is pretty obvious with multiple bidders bidding on multiple auctions on a single publisher’s page that the Privacy Sandbox would have to run these operations asynchronously to function.  Since the result (success or failure of the operation) isn't immediately available, these functions return a promise object. This promise acts as a placeholder, representing the eventual outcome.

Also, you will see references worded like this in the specifications:

“There is a complementary API navigator.leaveAdInterestGroup(myGroup) which looks only at myGroup.name and myGroup.owner. As with join calls, leaveAdInterestGroup() also returns a promise.”

Saying "returns a promise" is a concise way to convey that the function doesn't provide the immediate outcome but sets up a mechanism (the promise) to handle it later. It avoids cluttering the explanation with details about promise resolution or rejection.

You can think of the phrase “returns a promise” as developer shorthand for “The function initiates the asynchronous operation and returns a promise that will eventually indicate success or failure.”  

Beacons

Web applications often need to issue requests that report events, state updates, and analytics to one or more servers. They do this through web beacons.  A beacon is a tiny, often invisible, image element embedded in a webpage or email that sends a one-way communication to a server . When a user opens the page or email, the beacon makes a request to the server hosting the image, indicating that the content has been accessed and sending back the required data. 

Web beacons primarily serve three purposes:

  • Tracking User Activity. They can record page views, email opens, clicks on specific elements, and user journeys across a website.
  • Campaign Measurement. They help analyze the effectiveness of advertising campaigns by tracking how often ads are displayed and clicked.
  • Content Personalization. In some cases, they might be used to personalize content based on user behavior (though privacy concerns limit this use today).

Programmatic advertising servers are constantly collecting data back from browsers in real-time.  In the case of the Privacy Sandbox, data is collected in the browser, either at an event or aggregate level, and needs to be sent back to the publisher’s or advertiser’s servers (or their adTech partners) in a near real-time basis.  Beacons would be one obvious way to handle this.

However, traditional beacons have limitations around privacy and security that make them inappropriate for use in the Privacy Sandbox.  As a result, the Protected Audiences API has defined a new function called registerAdBeacon() which is called in the reporting worklet (read “script runner”) that provides the same functionality as a beacon but in a secure manner.  The registerAdBeacon() function is only available in the reporting functions, and is not available in the buyer's bidding logic or the seller's scoring logic.

While registerAdBeacon() shares some functionalities with web beacons, it's not a direct equivalent. The key differences between beacons and registerAdBeacon() include:

  • Consent and Privacy. Unlike traditional web beacons, registerAdBeacon() operates within the framework of the Protected Audiences API, which emphasizes user consent and privacy-preserving mechanisms.
  • Structured Data. The data reported through registerAdBeacon() is more structured and informative than the basic information a web beacon transmits.
  • Security Context. The data that is available to be reported comes from within a fenced frame, meaning it is limited by the privacy restrictions of that environment.  When an ad is rendered in a fenced frame, the developer triggers a custom event by calling a specific function window.fence.reportEvent(). Data available within the fenced frame is added as a payload and sent to the reporting worklet.  The reporting worklet can then create a beacon and call it in the key reporting functions of the Privacy Sandbox we will discuss in a later post - reportwin() and reportresult().  Here is a simple example of what that code might look like, taken from Google’s documentation:

// Protected Audience API buyer win reporting worklet
function reportWin(auctionSignals) {
	const { campaignId } = auctionSignals
    
	registerAdBeacon({
		click: `https://buyer-server.example/report/click?campaignId=${campaignId}`
	})
}

So there you have it.  Three key elements of web browsers that we will need to understand the inner workings of the Privacy Sandbox.