PrivacySandbox
The browser-side halves of Topics, Attribution Reporting, Private State Tokens and fenced frames - the APIs proposed to do what third-party cookies used to, with the browser holding the data instead of the site.
@inject Bit.Butil.PrivacySandbox privacySandboxMDN reference
A topic is an id in a public taxonomy - never a profile, an identifier or a history. At most three come back, and only ones the browser has already observed this caller present for, so an empty array is the ordinary answer. Reading with skipObservation records nothing; observing should follow from a real ad request rather than from a debug read.
@inject Bit.Butil.PrivacySandbox privacySandbox
var topics = await privacySandbox.GetTopics(skipObservation: true);
foreach (var topic in topics)
{
// topic.Topic indexes into topic.TaxonomyVersion's taxonomy
}Attribution Reporting
IsAttributionReportingSupported / RegisterAttribution / RegisterAttributionImageThe registration lives in the response headers of a URL you fetch, not in its body - the browser reads them and stores what it read, and nothing comes back to your code. Which is the design: measurement without the site learning who converted. The image form is what an ad creative uses; the fetch form suits an app that is already making the request.
// what an ad creative does - the image itself is never displayed:
await privacySandbox.RegisterAttributionImage("https://reporter.example/pixel.png");
// a source - an impression or a click that may later convert:
await privacySandbox.RegisterAttribution("https://reporter.example/source",
eventSourceEligible: true);
// the conversion itself:
await privacySandbox.RegisterAttribution("https://reporter.example/trigger",
eventSourceEligible: false, triggerEligible: true);Private State Tokens
IsPrivateStateTokensSupported / HasPrivateToken / HasRedemptionRecord / RequestTokenAn anti-fraud signal that carries nothing else: a site the user has already proved themselves on issues tokens, and another site spends one to learn 'trusted' and no more. The two Has… calls are the cheap checks - they answer whether the challenge needs running again at all.
if (await privacySandbox.HasRedemptionRecord(issuer) is false)
{
await privacySandbox.RequestToken(issuerUrl, PrivateStateTokenOperation.TokenRedemption);
}
// Attaching the record needs the issuers it may come from - nothing else says whose:
await privacySandbox.RequestToken(apiUrl, PrivateStateTokenOperation.SendRedemptionRecord,
issuers: [issuer]);Worth knowing rather than assuming: inside a fenced frame storage is partitioned differently, there is no access to the embedder, navigation is restricted and the referrer is gone - so code that takes any of those for granted has to behave differently there.
if (await privacySandbox.IsInFencedFrame())
{
// no embedder, partitioned storage, no referrer
}API reference
ValueTask<bool> IsTopicsSupported()ValueTask<BrowsingTopic[]> GetTopics(bool skipObservation = false)ValueTask<bool> IsAttributionReportingSupported()ValueTask<bool> RegisterAttribution(string url, bool eventSourceEligible = true, bool triggerEligible = false)ValueTask<bool> RegisterAttributionImage(string url)ValueTask<bool> IsPrivateStateTokensSupported()ValueTask<bool> HasPrivateToken(string issuer)ValueTask<bool> HasRedemptionRecord(string issuer)ValueTask<bool> RequestToken(string url, PrivateStateTokenOperation operation, int version = 1, string[]? issuers = null)ValueTask<bool> IsFencedFrameSupported()ValueTask<bool> IsInFencedFrame()