forked from patcg-individual-drafts/topics
-
Notifications
You must be signed in to change notification settings - Fork 0
/
spec.bs
556 lines (454 loc) · 42.3 KB
/
spec.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
<pre class='metadata'>
Title: Topics API
Status: w3c/CG-DRAFT
ED: https://github.com/patcg-individual-drafts/topics
Shortname: topics
Level: 1
URL: https://github.com/patcg-individual-drafts/topics
Editor: Yao Xiao, Google, yaoxia@chromium.org
Editor: Josh Karlin, Google, jkarlin@chromium.org
Abstract: This specification describes a method that could enable ad-targeting based on a person's general browsing interests without exposing their exact browsing history.
!Participate: <a href="https://github.com/patcg-individual-drafts/topics">GitHub patcg-individual-drafts/topics</a> (<a href="https://github.com/patcg-individual-drafts/topics/issues/new">new issue</a>, <a href="https://github.com/patcg-individual-drafts/topics/issues?state=open">open issues</a>)
Group: patcg
Repository: patcg-individual-drafts/topics
Markup Shorthands: markdown yes
</pre>
<pre class=link-defaults>
spec:html; type:attribute; text:document
spec:webidl; type:dfn; text:resolve
spec:dom; type:dfn; text:origin
spec:fetch; type:dfn; for:/; text:request
spec:dom; type:attribute; text:URL
spec:infra; type:dfn; text:user agent
spec:fetch; type:dfn; text:HTTP-network-or-cache fetch
spec:url; type:dfn; for:url; text:host
spec:url; type:dfn; for:url; text:origin
spec:url; type:dfn; text:domain
spec:fetch; type:dfn; for:/; text:header list
spec:fetch; type:dfn; for:Response; text:response
spec:infra; type:dfn; text:list
</pre>
<pre class="anchors">
spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
type: dfn
text: node navigable; url: document-sequences.html#node-navigable
text: relevant settings object; url: webappapis.html#relevant-settings-object
text: top-level traversable; for:navigable; url: document-sequences.html#nav-top
text: active document; for:navigable; url: document-sequences.html#nav-document
text: navigable; for: Window; url: nav-history-apis.html#window-navigable
spec: html; urlPrefix: https://wicg.github.io/nav-speculation/
type: dfn
text: prerendering navigable; url: prerendering.html#prerendering-navigable
text: post-prerendering activation steps list; url: prerendering.html#platform-object-post-prerendering-activation-steps-list
spec: html; urlPrefix: https://www.rfc-editor.org/rfc/
type: dfn
text: HMAC algorithm; url: rfc6234#section-8.3
</pre>
<style>
/* .XXX from https://resources.whatwg.org/standard.css */
.XXX {
color: #D50606;
background: white;
border: solid #D50606;
}
</style>
<section>
<h2 id="introduction-header">Introduction</h2>
In today's web, people's interests are typically inferred based on observing what sites or pages they visit, which relies on tracking techniques like third-party cookies or less-transparent mechanisms like device fingerprinting. It would be better for privacy if interest-based advertising could be accomplished without needing to collect a particular individual's browsing history.
This specification provides an API to enable ad-targeting based on a person's general browsing interests, without exposing their exact browsing history.
<div class="example">
Creating an ad based on browsing interests, using the {{Document/browsingTopics()|document.browsingTopics()}} JavaScript API:
(Inside an `https://ads.example` iframe)
<pre class="lang-js">
// document.browsingTopics() returns an array of BrowsingTopic objects.
const topics = await document.browsingTopics();
// Get data for an ad creative.
const response = await fetch('https://ads.example/get-creative', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(topics)
});
// Get the JSON from the response.
const creative = await response.json();
// Display the ad.
</pre>
</div>
<div class="example">
Creating an ad based on browsing interests, based on the [:Sec-Browsing-Topics:] HTTP request header sent by this invocation of {{WindowOrWorkerGlobalScope/fetch()}}:
(Inside the top level context)
<pre class="lang-js">
// A 'Sec-Browsing-Topics: [topics header value]' header will be sent in
// the HTTP request.
const response = await fetch('https://ads.example/get-creative', {browsingTopics: true});
const ad_creative = await response.json();
// Display the ad.
</pre>
</div>
</section>
<section>
<h2 id="terminology-and-types-header">Terminology and types</h2>
A <dfn for="browsing topics types">taxonomy</dfn> comprises a list of advertising <dfn for="browsing topics types">topic ids</dfn> as integers. A [=browsing topics types/taxonomy=] is identified by a <dfn for="browsing topics types">taxonomy version</dfn> string. A [=browsing topics types/topic id=] is no smaller than 1.
The taxonomy must be in a tree hierarchy, where an ancestor [=browsing topics types/topic id=] always represents something more general than its descendant [=browsing topics types/topic ids=]. The browser should implement an <dfn>get descendant topics</dfn> algorithm, which takes in a [=browsing topics types/topic id=], and returns its descendants [=browsing topics types/topic ids=] as a [=list=].
The <dfn for="browsing topics types">model version</dfn> is a string that identifies the <dfn for="browsing topics types">model</dfn> used to <dfn>classify</dfn> a string into [=topic ids=]. The meaning may vary across browser vendors. The classification result [=topic ids=] should be relevant to the input string's underlying content.
The <dfn for="browsing topics types">configuration version</dfn> identifies the algorithm (other than the model part) used to calculate the topic. It should take the form of "<code><browser vendor identifier>.<an integer version></code>". The meaning may vary across browser vendors.
Given [=browsing topics types/configuration version=] |configurationVersion|, [=browsing topics types/taxonomy version=] |taxonomyVersion|, and [=browsing topics types/model version=] |modelVersion|, the <dfn for="browsing topics types">version</dfn> is the result of [=string/concatenating=] « |configurationVersion|, |taxonomyVersion|, |modelVersion| » using "<code>:</code>".
A <dfn for="browsing topics types">user topics state</dfn> is a struct with the following fields and default values:
- <dfn for="user topics state">epochs</dfn>: a list of [=epoch=]s, default to an empty list.
- <dfn for="user topics state">hmac key</dfn>: 128 bit number, default to 0.
An <dfn for="browsing topics types">epoch</dfn> is a struct with the following fields:
- <dfn for="epoch">taxonomy</dfn>: a list of integers.
- <dfn for="epoch">taxonomy version</dfn>: a string.
- <dfn for="epoch">model version</dfn>: a string.
- <dfn for="epoch">config version</dfn>: a string.
- <dfn for="epoch">top 5 topics with caller origins</dfn>: a list of [=topic with caller origins=].
- <dfn for="epoch">time</dfn>: a {{DOMHighResTimeStamp}} (from Unix epoch).
A <dfn for="browsing topics types">topic with caller origins</dfn> is a struct with the following fields:
- <dfn for="topic with caller origins">topic id</dfn>: an integer.
- <dfn for="topic with caller origins">caller origins</dfn>: a set of [=origins=].
A <dfn for="browsing topics types">topics history entry</dfn> is a struct with the following fields and default values:
- <dfn for="topics history entry">document id</dfn>: an integer, default to 0.
- <dfn for="topics history entry">topics calculation input data</dfn>: a string, default to an empty string.
- <dfn for="topics history entry">time</dfn>: a {{DOMHighResTimeStamp}} (from Unix epoch).
- <dfn for="topics history entry">topics caller origins</dfn>: an ordered set of [=origins=], default to an empty set.
A <dfn for="browsing topics types">topics caller context</dfn> is a struct with the following fields:
- <dfn for="topics caller context">caller origin</dfn>: an [=origin=].
- <dfn for="topics caller context">top level context domain</dfn>: a [=domain=].
- <dfn for="topics caller context">timestamp</dfn>: a {{DOMHighResTimeStamp}} (from Unix epoch).
<h2 id="user-agent-associated-state-header">User agent associated state</h2>
Each [=user agent=] has an associated [=browsing topics types/user topics state=] <dfn for="user agent">user topics state</dfn> with [=user topics state/epochs=] initially empty, and [=user topics state/hmac key=] initially a randomly generated 128 bit number.
Each [=user agent=] has an associated <dfn for="user agent">topics history storage</dfn> to store the information about the visited pages that are needed for topics calculation. It is a [=list=] of [=topics history entries=], initially empty.
Each [=user agent=] has an associated [=browsing topics types/taxonomy=] <dfn for="user agent">taxonomy</dfn> (identified by [=browsing topics types/taxonomy version=] <dfn for="user agent">taxonomy version</dfn>) and [=browsing topics types/model=] <dfn for="user agent">model</dfn> (identified by [=browsing topics types/model version=] <dfn for="user agent">model version</dfn>).
The [=user agent/taxonomy=] and [=user agent/model=] may be shipped to the browser asynchronously w.r.t. the browser release, and may be unavailable at a given point. They must be updated atomically w.r.t. algorithms that access them (e.g. the [=calculate user topics=] algorithm).
Note: The initial taxonomy used by the API is <a href="https://github.com/patcg-individual-drafts/topics/blob/main/taxonomy_v1.md">taxonomy_v1.md</a> and the expectation is that it will change over time.
Each [=user agent=] has an associated topics algorithm configuration (identified by [=browsing topics types/configuration version=] <dfn for="user agent">configuration version</dfn>). The initial value and meaning is browser defined.
Note: The [=browsing topics types/configuration version=] allows the browser vender to provide algorithms different from the ones specified in this specification. For example, for some of the algorithms in this specification, it may be possible to use a different constant value, while the system overall still has utility and <a href="#privacy-considerations-header">meets the privacy goals</a>.
When [=user agent/configuration version=] is updated, the browser must properly migrate or delete data in [=user agent/user topics state=] and [=user agent/topics history storage=] so that the state and the configuration are consistent.
<h2 id="browsing-topic-dictionary-header">BrowsingTopic dictionary</h2>
The {{BrowsingTopic}} dictionary is used to contain the IDL correspondences of [=browsing topics types/topic id=], [=browsing topics types/version=], [=browsing topics types/configuration version=], [=browsing topics types/taxonomy version=], and [=browsing topics types/model version=].
<pre class="idl">
dictionary BrowsingTopic {
[EnforceRange] unsigned long long topic;
DOMString version;
DOMString configVersion;
DOMString modelVersion;
DOMString taxonomyVersion;
};
</pre>
<div class="example">
An example {{BrowsingTopic}} object from Chrome: <code highlight="js">{ configVersion: "chrome.1", modelVersion: "2206021246", taxonomyVersion: "1", topic: 43, version: "chrome.1:1:2206021246" }</code>.
</div>
<div algorithm>
A {{BrowsingTopic}} dictionary |a| is <dfn id=browsing-topics-dictionary-less-than-comparator for="browsing-topic">code unit less than</dfn> a {{BrowsingTopic}} dictionary |b| if the following steps return true:
1. If |a|["{{BrowsingTopic/topic}}"] < |b|["{{BrowsingTopic/topic}}"], then return true.
1. If |a|["{{BrowsingTopic/version}}"] is [=/code unit less than=] |b|["{{BrowsingTopic/version}}"], then return true.
1. Return false.
</div>
</section>
<section>
<h2 id="document-id-header">document ID</h2>
Each {{Document}} has a <dfn for="document-id-header">document id</dfn>, which is an [=implementation-defined=] unique identifier shared with no other {{Document}} objects.
<h2 id="determine-topics-calculation-input-data-header">Determine topics calculation input data</h2>
Given a {{Document}}, the browser must have a way to determine the <dfn for="determine-topics-calculation-input-data-header">topics calculation input data</dfn>. [=determine-topics-calculation-input-data-header/topics calculation input data=] is a string that encodes the attributes to be used for topics classification. The attributes could be the document's [=Document/URL=], the URL's [=domain=], the document node's [=descendant text content=], etc, as determined by the browser vendor.
Note: In Chrome's experimentation phase, the [=host=] of a {{Document}}'s [=Document/URL=] is used as the [=determine-topics-calculation-input-data-header/topics calculation input data=], and the model is trained with human curated hostnames and topics.
<h2 id="collect-page-topics-calculation-input-data-header">Collect page topics calculation input data</h2>
<div algorithm>
To <dfn>collect page topics calculation input data</dfn>, given a {{Document}} |document|:
1. If |document|'s [=node navigable=] is a [=prerendering navigable=], then append the following steps to |document|'s [=post-prerendering activation steps list=] and return. Else, run the following steps [=in parallel=]:
1. Let |documentId| be |document|'s [=document-id-header/document id=].
1. If user agent's [=user agent/topics history storage=] contains a [=topics history entry=] whose [=topics history entry/document id=] is |documentId|, return.
1. Let |topicsHistoryEntry| be a [=topics history entry=].
1. Set |topicsHistoryEntry|'s [=topics history entry/document id=] to |documentId|.
1. Set |topicsHistoryEntry|'s [=topics history entry/topics calculation input data=] to the [=determine-topics-calculation-input-data-header/topics calculation input data=] for |document|.
1. Let |unsafeMoment| be the [=wall clock=]'s [=wall clock/unsafe current time=].
1. Let |moment| be the result of running [=coarsen time=] algorithm given |unsafeMoment| and [=wall clock=] as input.
1. Let |fromUnixEpochTime| be the [=duration from=] the [=Unix epoch=] to |moment|.
1. Set |topicsHistoryEntry|'s [=topics history entry/time=] to |fromUnixEpochTime|.
1. [=list/Append=] |topicsHistoryEntry| to user agent's [=user agent/topics history storage=].
</div>
<h2 id="collect-topics-caller-origin-header">Collect topics caller origin</h2>
<div algorithm>
To <dfn>collect topics caller origin</dfn>, given a {{Document}} |document| and an [=origin=] |callerOrigin|:
1. Run the following steps [=in parallel=]:
1. Let |documentId| be |document|'s [=document-id-header/document id=].
1. If user agent's [=user agent/topics history storage=] does not contain a [=topics history entry=] whose [=topics history entry/document id=] is |documentId|, return.
1. Let |topicsHistoryEntry| be the [=topics history entry=] in user agent's [=user agent/topics history storage=] whose [=topics history entry/document id=] is |documentId|.
1. [=set/Append=] |callerOrigin| to |topicsHistoryEntry|'s [=topics caller origins=].
</div>
<h2 id="periodically-calculate-user-topics-header">Periodically calculate user topics</h2>
At the start of a browser session, run the [=schedule user topics calculation=] algorithm.
<div algorithm>
To <dfn>schedule user topics calculation</dfn>, perform the following steps:
1. Let |unsafeMoment| be the [=wall clock=]'s [=wall clock/unsafe current time=].
1. Let |moment| be the result of running [=coarsen time=] algorithm given |unsafeMoment| and [=wall clock=] as input.
1. Let |fromUnixEpochTime| be the [=duration from=] the [=Unix epoch=] to |moment|.
1. Let |presumedNextCalculationDelay| be a [=duration=] of 0.
1. If user agent's [=user agent/user topics state=]'s [=user topics state/epochs=] is not empty:
1. Let |numEpochs| be user agent's [=user agent/user topics state=]'s [=user topics state/epochs=]'s [=list/size=].
1. Let |lastTopicsCalculationTime| beuser agent's [=user agent/user topics state=]'s [=user topics state/epochs=][|numEpochs| − 1].
1. Let |presumedNextCalculationDelay| be |lastTopicsCalculationTime| + (a [=duration=] of 7 days) − |fromUnixEpochTime|.
1. If |presumedNextCalculationDelay| < (a [=duration=] of 0), then set |presumedNextCalculationDelay| to (a [=duration=] of 0).
1. Else if |presumedNextCalculationDelay| ≥ (a [=duration=] of 14 days), then set |presumedNextCalculationDelay| to (a [=duration=] of 0).
Note: This could happen if the machine time has gone backward since the last topics calculation. Recalculate immediately to align with the expected schedule rather than potentially stop calculating for a very long time.
1. Schedule the [=calculate user topics=] algorithm to run at [=Unix epoch=] + |fromUnixEpochTime| + |presumedNextCalculationDelay|.
</div>
<div algorithm>
To <dfn>calculate user topics</dfn>, perform the following steps:
1. Let |unsafeMoment| be the [=wall clock=]'s [=wall clock/unsafe current time=].
1. Let |moment| be the result of running [=coarsen time=] algorithm given |unsafeMoment| and [=wall clock=] as input.
1. Let |fromUnixEpochTime| be the [=duration from=] the [=Unix epoch=] to |moment|.
1. If either user agent's [=user agent/model=] or [=user agent/taxonomy=] isn't available:
1. Let |epoch| be an [=epoch=] struct with default initial field values.
1. Set |epoch|'s [=epoch/time=] to |fromUnixEpochTime|.
1. [=list/Append=] |epoch| to user agent's [=user agent/user topics state=]'s [=user topics state/epochs=].
1. If user agent's [=user agent/user topics state=]'s [=user topics state/epochs=] has more than 4 entries, remove the oldest epoch (i.e. the epoch with index 0).
1. Schedule this [=calculate user topics=] algorithm to run at [=Unix epoch=] + |fromUnixEpochTime| + (a [=duration=] of 7 days).
1. Return.
1. Let |topicsCount| be an empty map.
1. Let |topicsCallers| be an empty map.
1. Let |userTopicsDataStartTime| be |fromUnixEpochTime| − (a [=duration=] of 7 days).
1. Let |topicsCallerDataStartTime| be |fromUnixEpochTime| − (a [=duration=] of 21 days).
1. For each [=topics history entry=] |topicsHistoryEntry| in user agent's [=user agent/topics history storage=]:
1. Let |visitTime| be |topicsHistoryEntry|'s [=topics history entry/time=].
1. If |visitTime| is before |topicsCallerDataStartTime|, then continue.
1. [=Classify=] |topicsHistoryEntry|'s [=topics history entry/topics calculation input data=] into |topicIds|.
1. For each |topicId| in |topicIds|:
1. If |topicsCallers|[|topicId|] does not exist:
1. Initialize |topicsCallers|[|topicId|] to be an empty [=list=].
1. If |topicsCount|[|topicId|] does not exist:
1. Initialize |topicsCount|[|topicId|] to 0.
1. For each |callerOrigin| in |topicsHistoryEntry|'s [=topics history entry/topics caller origins=]:
1. [=list/Append=] |callerOrigin| to |topicsCallers|[|topicId|].
1. If |visitTime| is greater than |userTopicsDataStartTime|:
1. Increment |topicsCount|[|topicId|] by 1.
1. Let |top5Topics| be a list containing the top up to 5 |topicId|s in |topicsCount|'s keys set, where the |topicId|s with more count are retrieved first.
1. If |top5Topics| has less than 5 entries:
1. Pad |top5Topics| with random topic ids from user agent's [=user agent/taxonomy=], until |top5Topics| has 5 entries.
1. Let |top5TopicsWithCallerOrigins| be an empty [=list=].
1. For each |topTopicId| in |top5Topics|:
1. Let |topicWithCallerOrigins| be a [=topic with caller origins=] struct with [=topic with caller origins/topic id=] initially 0 and [=topic with caller origins/caller origins=] initially empty.
1. If |topTopicId| is allowed by user preference setting:
1. Set |topicWithCallerOrigins|'s [=topic with caller origins/topic id=] to |topicId|.
1. Let |topicWithDescendantIds| be the result of running [=get descendant topics=] given |topTopicId|.
1. Add |topTopicId| to |topicWithDescendantIds|.
1. For each |topicId| in |topicWithDescendantIds|:
1. If |topicId| is allowed by user preference setting:
1. Insert all elements in |topicsCallers|[|topicId|] to |topicWithCallerOrigins|'s [=topic with caller origins/caller origins=].
1. [=list/Append=] |topicWithCallerOrigins| to |top5TopicsWithCallerOrigins|.
1. Let |epoch| be an [=epoch=] struct with default initial field values.
1. Set |epoch|'s [=epoch/taxonomy=] to user agent's [=user agent/taxonomy=].
1. Set |epoch|'s [=epoch/taxonomy version=] to user agent's [=user agent/taxonomy version=].
1. Set |epoch|'s [=epoch/model version=] to user agent's [=user agent/model version=].
1. Set |epoch|'s [=epoch/config version=] to user agent's [=user agent/configuration version=].
1. Set |epoch|'s [=epoch/top 5 topics with caller origins=] to |top5TopicsWithCallerOrigins|.
1. Set |epoch|'s [=epoch/time=] to |fromUnixEpochTime|.
1. [=list/Append=] |epoch| to user agent's [=user agent/user topics state=]'s [=user topics state/epochs=].
1. If user agent's [=user agent/user topics state=]'s [=user topics state/epochs=] has more than 4 entries, remove the oldest epoch.
1. Schedule this [=calculate user topics=] algorithm to run at [=Unix epoch=] + |fromUnixEpochTime| + (a [=duration=] of 7 days).
</div>
<h2 id="epochs-for-caller-header">Epochs for caller</h2>
<div algorithm>
To <dfn>calculate the epochs for caller</dfn>, given a [=topics caller context=] |callerContext|, perform the following steps. They return a list of [=epoch=].
1. Let |epochs| be user agent's [=user agent/user topics state=]'s [=user topics state/epochs=].
1. If |epochs| is empty, then return an empty [=list=].
1. Let |numEpochs| be |epochs|'s [=list/size=].
1. Let |lastEpochTime| be |epochs|[|numEpochs| − 1]'s [=epoch/time=].
1. Let |epochSwitchTimeDecisionHash| be64 bit truncation of the output of the [=HMAC algorithm=], given input parameters: whichSha=SHA256, key=user agent's [=user agent/user topics state=]'s [=user topics state/hmac key=], and message_array=the concatenation of "epoch-switch-time-decision|" and |callerContext|'s [=topics caller context/top level context domain=].
1. Let |epochSwitchTimeDelayIntroduction| be a [=duration=] of (|epochSwitchTimeDecisionHash| % 172800) seconds (i.e. 172800 is 2 days in seconds).
1. Let |timestamp| be |callerContext|'s [=topics caller context/timestamp=].
1. Let |result| be an empty [=list=].
1. Let |startEpochIndex| be -1.
1. Let |endEpochIndex| be -1.
1. If |timestamp| ≤ |lastEpochTime| + |epochSwitchTimeDelayIntroduction|:
1. Set |startEpochIndex| to max(|numEpochs| − 4, 0).
1. Set |endEpochIndex| to |numEpochs| − 2.
1. Else:
1. Set |startEpochIndex| to max(|numEpochs| − 1, 0).
1. Set |endEpochIndex| to |numEpochs| − 1.
1. If |endEpochIndex| ≥ 0:
1. Let |i| bestartEpochIndex.
1. While |i| ≤ |endEpochIndex|:
1. [=list/Append=] |epochs|[|i|] to |result|.
1. Set |i| to |i| + 1.
1. Return |result|.
</div>
<h2 id="topics-for-caller-header">Topics for caller</h2>
<div algorithm>
To <dfn>calculate the topics for caller</dfn>, given a [=topics caller context=] |callerContext|, perform the following steps. They return a list of {{BrowsingTopic}}s.
1. Let |epochs| be the result of running the [=calculate the epochs for caller=] algorithm given |callerContext| as input.
1. Let |result| be an empty [=list=].
1. For each |epoch| in |epochs|:
1. If |epoch|'s [=epoch/top 5 topics with caller origins=] is empty (implying the topics calculation failed for that epoch), then continue.
1. Let |topic| be null.
1. Let |topTopicIndexDecisionHash| be64 bit truncation of the output of the [=HMAC algorithm=], given input parameters: whichSha=SHA256, key=user agent's [=user agent/user topics state=]'s [=user topics state/hmac key=], and message_array=the concatenation of "top-topic-index-decision|", |epoch|'s [=epoch/time=], and |callerContext|'s [=topics caller context/top level context domain=]
1. Let |topTopicIndex| be |topTopicIndexDecisionHash| % 5.
1. Let |topTopicWithCallerOrigins| be |epoch|'s [=epoch/top 5 topics with caller origins=][|topTopicIndex|].
1. If |topTopicWithCallerOrigins|'s [=topic with caller origins/caller origins=] contains |callerContext|'s [=topics caller context/caller origin=]:
1. Set |topic| to an empty {{BrowsingTopic}} dictionary.
1. Set |topic|["{{BrowsingTopic/topic}}"] to |topTopicWithCallerOrigins|'s [=topic with caller origins/topic id=].
1. If |topic| is null, or if |topic|'s {{BrowsingTopic/topic}} is 0 (i.e. the candidate topic was cleared), then continue.
1. Let |randomOrTopTopicDecisionHash| be 64 bit truncation of the output of the [=HMAC algorithm=], given input parameters: whichSha=SHA256, key=user agent's [=user agent/user topics state=]'s [=user topics state/hmac key=], and message_array=the concatenation of "random-or-top-topic-decision|", |epoch|'s [=epoch/time=], and |callerContext|'s [=topics caller context/top level context domain=].
1. If |randomOrTopTopicDecisionHash| % 100 < 5:
1. Let |randomTopicIndexDecisionHash| be64 bit truncation of the output of the [=HMAC algorithm=], given input parameters: whichSha=SHA256, key=user agent's [=user agent/user topics state=]'s [=user topics state/hmac key=], and message_array=the concatenation of "random-topic-index-decision|", |epoch|'s [=epoch/time=], and |callerContext|'s [=topics caller context/top level context domain=].
1. Let |randomTopicIndex| be |randomTopicIndexDecisionHash| % |epoch|'s [=epoch/taxonomy=]'s [=list/size=].
1. Set |topic|'s {{BrowsingTopic/topic}} to |epoch|'s [=epoch/taxonomy=][|randomTopicIndex|].
1. Set |topic|["{{BrowsingTopic/configVersion}}"] to to |epoch|'s [=epoch/config version=].
1. Set |topic|["{{BrowsingTopic/modelVersion}} to"] |epoch|'s [=epoch/model version=].
1. Set |topic|["{{BrowsingTopic/taxonomyVersion}}"] to |epoch|'s [=epoch/taxonomy version=].
1. Determine the [=browsing topics types/version=] |version|, given |topic|'s {{BrowsingTopic/configVersion}}, {{BrowsingTopic/modelVersion}} and {{BrowsingTopic/taxonomyVersion}} as input.
1. Set |topic|["{{BrowsingTopic/version}}"] to |version|.
1. Add |topic| to |result|.
1. Sort entries in |result| given the <a href="#browsing-topics-dictionary-less-than-comparator">less-than comparator</a> for the {{BrowsingTopic}} dictionary.
1. Remove duplicate entries in |result|. Two {{BrowsingTopic}} dictionaries |a| and |b| are considered equal if |a| is not [=browsing-topic/code unit less than=] |b| and |b| is not [=browsing-topic/code unit less than=] |a|.
1. Return |result|.
</div>
</section>
<section>
<h2 id="the-javascript-api-header">The JavaScript API</h2>
The Topics API lives under the {{Document}} interface, and is only available if the document is in [=secure context=].
<pre class="idl">
dictionary BrowsingTopicsOptions {
boolean skipObservation = false;
};
partial interface Document {
[SecureContext] Promise<sequence<BrowsingTopic>> browsingTopics(optional BrowsingTopicsOptions options = {});
};
</pre>
<div algorithm="browsingTopics(options)">
The <dfn for="Document" method>browsingTopics(options)</dfn> method steps are:
1. Let |document| be [=this=].
1. Let |promise| be [=a new promise=].
1. Let |topicsCallerContext| be a [=topics caller context=].
1. Set |topicsCallerContext|'s [=topics caller context/caller origin=] to |document|'s [=Document/origin=].
1. Set |topicsCallerContext|'s [=topics caller context/top level context domain=] to the result of running |document|'s [=node navigable=]'s [=navigable/top-level traversable=]'s [=navigable/active document=]'s {{Document/domain}} getter steps.
1. Let |unsafeMoment| be the [=wall clock=]'s [=wall clock/unsafe current time=].
1. Let |moment| be the result of running [=coarsen time=] algorithm given |unsafeMoment| and [=wall clock=] as input.
1. Let |fromUnixEpochTime| be the [=duration from=] the [=Unix epoch=] to |moment|.
1. Set |topicsCallerContext|'s [=topics caller context/timestamp=] to |fromUnixEpochTime|.
1. If any of the following is true:
- |document|'s [=Document/origin=] is an [=opaque origin=].
- |document| is not [=allowed to use=] the <code><a href="#browsing-topics-policy-controlled-feature">browsing-topics</a></code> feature.
- |document| is not [=allowed to use=] the <code><a href="#interest-cohort-policy-controlled-feature">interest-cohort</a></code> feature.
- The user preference setting disallows the access to topics from |topicsCallerContext|'s [=topics caller context/caller origin=] or |topicsCallerContext|'s [=topics caller context/top level context domain=].
Note: In Chrome's experimentation phase, it will additionally require a valid <a href="https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/explainer.md">Origin Trial</a> token to exist in |document|.
then:
1. [=Queue a global task=] on the <dfn>browsing topics task source</dfn> given |document|'s [=relevant global object=] to [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}}.
1. Abort these steps.
1. Run the following steps [=in parallel=]:
1. Let |topics| be the result from running the [=calculate the topics for caller=] algorithm, with |topicsCallerContext| as input.
1. If <var ignore=''>options</var>["{{BrowsingTopicsOptions/skipObservation}}"] is false:
1. Run the [=collect page topics calculation input data=] algorithm with |document| as input.
1. Run the [=collect topics caller origin=] algorithm with |document| and |topicsCallerContext|'s [=topics caller context/caller origin=] as input.
1. [=Queue a global task=] on the [=browsing topics task source=] given |document|'s [=relevant global object=] to perform the following steps:
1. [=Resolve=] |promise| with |topics|.
1. Return |promise|.
</div>
</section>
<section>
<h2 id="handle-topics-fetch-request-header">Handle fetch(<url>, {browsingTopics: true}) and <iframe src=url browsingtopics></></h2>
Topics can be sent in the HTTP header for {{WindowOrWorkerGlobalScope/fetch()}} requests and for <a href="https://html.spec.whatwg.org/multipage/iframe-embed-object.html#navigate-an-iframe-or-frame">iframe navigation</a> requests. The response header for a topics related request can specify whether the caller should to be recorded.
<h3 id="send-browsing-topics-header-boolean-associated-with-request-header">send browsing topics header boolean associated with Request</h3>
A [=request=] has an associated <dfn for=request>send browsing topics header boolean</dfn>. Unless stated otherwise it is unset.
<span class=XXX>TODO: make the modification directly to the fetch spec.</span>
<h3 id="browsing-topics-content-attribute-for-iframe-element-header">browsingtopics content attribute for HTMLIframeElement</h3>
The <a href="https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element">iframe</a> element contains a <dfn element-attr for="iframe">browsingtopics</dfn> <a spec=html>content attribute</a>. The IDL attribute <dfn attribute for="HTMLIFrameElement">browsingTopics</dfn> <a spec=html>reflects</a> the <{iframe/browsingtopics}> <a spec=html>content attribute</a>.
<pre class=idl>
partial interface HTMLIFrameElement {
[CEReactions] attribute boolean browsingTopics;
};
</pre>
<span class=XXX>TODO: make the modification directly to the html spec.</span>
<h3 id="browsing-topics-attribute-in-request-init-header">browsingTopics attribute in RequestInit</h3>
The <a href="https://fetch.spec.whatwg.org/#requestinit">RequestInit</a> dictionary contains a browsingTopics attribute:
<pre class=idl>
dictionary RequestInit {
// existing attributes ...
boolean browsingTopics;
};
</pre>
<span class=XXX>TODO: make the modification directly to the fetch spec.</span>
<h3 id="modification-to-request-constructor-steps-header">Modification to request constructor steps</h3>
The following step will be added to the <a href="https://fetch.spec.whatwg.org/#dom-request">new Request(input, init) constructor steps</a>, before step "Set this's [=Request/request=] to |request|":
1. If <var ignore=''>init</var>["{{RequestInit/browsingTopics}}"] <a for=map>exists</a>, then set |request|'s [=request/send browsing topics header boolean=] to it.
<span class=XXX>TODO: make the modification directly to the fetch spec.</span>
<h3 id="modification-to-create-navigation-params-by-fetching-steps-header">Modification to "create navigation params by fetching" steps</h3>
The following step will be added to the <a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching">create navigation params by fetching steps</a>, after step "Let |request| be a new [=Request/request=], with ...":
1. If <var ignore=''>navigable</var>'s [=container=] is an <{iframe}> element, and if it has a <{iframe/browsingtopics}> <a spec=html>content attribute</a>, then set |request|'s [=request/send browsing topics header boolean=] to true.
<span class=XXX>TODO: make the modification directly to the html spec.</span>
<h3 id="the-sec-browsing-topics-http-request-header-header">The \`<code>Sec-Browsing-Topics</code>\` HTTP request header</h3>
This specification defines a \`<dfn export http-header><code>Sec-Browsing-Topics</code></dfn>\` HTTP request header. It is used to send the topics.
<h3 id="modification-to-http-network-or-cache-fetch-algorithm-header">Modification to HTTP-network-or-cache fetch algorithm</h3>
The following step will be added to the <a href="https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch">HTTP-network-or-cache fetch</a> algorithm, before step "Modify |httpRequest|'s [=request/header list=] per HTTP. ...":
1. <a>Append or modify a request \`<code>Sec-Browsing-Topics</code>\` header</a> for |httpRequest|.
<span class=XXX>TODO: make the modification directly to the fetch spec.</span>
<h3 id="append-or-modify-a-request-sec-browsing-topics-header-header">Append or modify a request `Sec-Browsing-Topics` header</h3>
<div algorithm>
To <dfn>append or modify a request \`<code>Sec-Browsing-Topics</code>\` header</dfn>, given a [=request=] |request|, run these steps:
1. If |request|'s [=request/send browsing topics header boolean=] is not true, then return.
1. [=header list/Delete=] [:Sec-Browsing-Topics:] from |request|'s [=header list=].
<p class="note">
The topics a request is allowed to see can change within its redirect chain. For example, different caller origins may receive different topics, as the callers can only get the topics about the sites they were on. Besides, regardless of cross-origin-ness, the timestamp can also affect the candidate epochs where the topics are derived from, thus resulting in different topics across redirects.
</p>
1. Let |initiatorWindow| be |request|'s [=request/window=].
1. Let |requestOrigin| be |request|'s [=request/URL=]'s [=url/origin=].
1. If |requestOrigin| is not a [=potentially trustworthy origin=], then return.
1. If |initiatorWindow| is not an [=environment settings object=], then return.
1. If |initiatorWindow| is not a [=secure context=], then return.
1. For each feature |f| in « "<code>browsing-topic</code>", "<code>interest-cohort</code>" »:
1. Run the <a href="https://www.w3.org/TR/permissions-policy-1/#algo-should-request-be-allowed-to-use-feature">Should request be allowed to use feature?</a> algorithm with <var ignore=''>feature</var> set to |f| and <var ignore=''>request</var> set to |request|. If the algorithm returns false, then return.
Note: the above algorithm should include the <a href="https://github.com/w3c/webappsec-permissions-policy/pull/499">pending update</a>, i.e. the |request| should be considered to contain the equivalent opt-in flags for both "browsing-topic" and the "interest-cohort" feature.
1. Let |topicsCallerContext| be a [=topics caller context=] with default initial field values.
1. Set |topicsCallerContext|'s [=topics caller context/caller origin=] to |requestOrigin|.
1. Set |topicsCallerContext|'s [=topics caller context/top level context domain=] to the result of running |initiatorWindow|'s [=environment settings object/global object=]'s [=Window/navigable=]'s [=navigable/top-level traversable=]'s [=navigable/active document=]'s {{Document/domain}} getter steps.
1. Let |unsafeMoment| be the [=wall clock=]'s [=wall clock/unsafe current time=].
1. Let |moment| be the result of running [=coarsen time=] algorithm given |unsafeMoment| and [=wall clock=] as input.
1. Let |fromUnixEpochTime| be the [=duration from=] the [=Unix epoch=] to |moment|.
1. Set |topicsCallerContext|'s [=topics caller context/timestamp=] to |fromUnixEpochTime|.
1. If the user preference setting disallows the access to topics from |topicsCallerContext|'s [=topics caller context/caller origin=] or |topicsCallerContext|'s [=topics caller context/top level context domain=], then return.
1. Let |topics| be the result from running the [=calculate the topics for caller=] algorithm, with |topicsCallerContext| as input.
1. Let |headerStructuredFields| be an empty Structured Fields <a href="https://www.rfc-editor.org/rfc/rfc8941.html#name-lists">List</a>.
1. For each |topic| in |topics|:
1. Let |topicItem| be a Structured Fields <a href="https://www.rfc-editor.org/rfc/rfc8941.html#name-integers">Integer</a> with value |topic|["{{BrowsingTopic/topic}}"].
1. Let |topicParameters| be an empty Structured Fields <a href="https://www.rfc-editor.org/rfc/rfc8941.html#name-parameters">Parameters</a>.
1. Set |topicParameters|["<code>version</code>"] to |topic|["{{BrowsingTopic/version}}"].
1. Set |topicParameters|["<code>config_version</code>"] to |topic|["{{BrowsingTopic/configVersion}}"].
1. Set |topicParameters|["<code>model_version</code>"] to |topic|["{{BrowsingTopic/modelVersion}}"].
1. Set |topicParameters|["<code>taxonomy_version</code>"] to |topic|["{{BrowsingTopic/taxonomyVersion}}"].
1. Associate |topicParameters| with |topicItem|.
1. Insert |topicItem| to |headerStructuredFields|.
1. [=Set a structured field value=] given ([:Sec-Browsing-Topics:], |headerStructuredFields|) in |request|'s [=request/header list=].
Note: In Chrome's experimentation phase, it will additionally require a valid <a href="https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/explainer.md">Origin Trial</a> token to exist in |initiatorWindow|'s <a data-cite="!HTML#concept-document-window">associated document</a> for the request to be eligible for topics.
</div>
<h3 id="the-observe-browsing-topics-http-response-header-header">The \`<code>Observe-Browsing-Topics</code>\` HTTP response header</h3>
The \`<dfn export http-header><code>Observe-Browsing-Topics</code></dfn>\` HTTP response header can be used to record a caller's topics observation.
<div algorithm>
To <dfn>handle topics response</dfn>, given a [=response=] |response| and a [=request=] request:
1. If |request|'s [=request/header list=] does not [=list/contain=] [:Sec-Browsing-Topics:] (implying the |request|'s [=request/current URL=] is not eligible for topics), then return.
1. Let |topLevelDocument| be |request|'s [=request/window=]'s [=environment settings object/global object=]'s [=Window/navigable=]'s [=navigable/top-level traversable=]'s [=navigable/active document=].
1. Let |callerOrigin| be |request|'s [=request/current URL=]'s [=url/origin=].
1. Let |list| be |response|'s [=response/header list=].
1. Let |headerValues| be the result of [=header list/getting=] [:Observe-Browsing-Topics:] from |list|.
1. If |headerValues|'s [=list/size=] is not 1, then return.
1. Let |headerValue| be |headerValues|[0].
1. Let |observe| be the result of <a href="https://www.rfc-editor.org/rfc/rfc8941.html#name-parsing-a-boolean">parsing</a> |headerValue| into Structured Fields <a href="https://www.rfc-editor.org/rfc/rfc8941.html#name-booleans">Boolean</a>.
1. If |observe| is true:
1. Run the [=collect page topics calculation input data=] algorithm with |topLevelDocument| as input.
1. Run the [=collect topics caller origin=] algorithm with |topLevelDocument| and |callerOrigin| as input.
</div>
<h3 id="modification-to-http-fetch-steps-header">Modification to HTTP fetch steps</h3>
The following step will be added to the [=HTTP fetch=] steps, before checking the redirect status (i.e. "If |actualResponse|'s status is a redirect status, ..."):
1. [=Handle topics response=], given [=response=] |actualResponse| and [=request=] |request| as input.
<span class=XXX>TODO: make the modification directly to the fetch spec.</span>
</section>
<section>
<h2 id="permissions-policy-integration-header">Permissions policy integration</h2>
<p>This specification defines a [=policy-controlled feature=] identified by the string
"<code><dfn id=browsing-topics-policy-controlled-feature>browsing-topics</dfn></code>". Its <a>default allowlist</a> is <code>*</code>.
<p>For backward compatibility, this specification also defines a [=policy-controlled feature=] identified by the string
"<code><dfn id=interest-cohort-policy-controlled-feature>interest-cohort</dfn></code>". Its <a>default allowlist</a> is <code>*</code>.
</section>
<section>
<h2 id="privacy-considerations-header">Privacy considerations</h2>
The Topics API attempts to provide just enough relevant interest information for advertisers to be able to personalize their ads for the user while maintaining user privacy. Some privacy safeguards include: usage in secure contexts only, topic limitation to a human curated taxonomy, different topics given to different sites in the same epoch to prevent cross-site reidentification, noised topics, a limited number of topics provided per epoch, user opt outs, site opt outs, and a suggestion that user agents provide UX to give users choice in which Topics are returned.
</section>