@uniweb/core 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/page.js +18 -7
- package/src/tracker.js +63 -4
package/package.json
CHANGED
package/src/page.js
CHANGED
|
@@ -48,13 +48,24 @@ export default class Page {
|
|
|
48
48
|
// Rewrite target (if set, this route is served by an external site)
|
|
49
49
|
this.rewrite = pageData.rewrite || null
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
// page.yml)
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
51
|
+
// This page's answer on section-level instrumentation (`trackSections` in
|
|
52
|
+
// page.yml) — an OVERRIDE, not a switch.
|
|
53
|
+
//
|
|
54
|
+
// ⭐ **Three states, and the third is the point.** `undefined` means the page
|
|
55
|
+
// said nothing and the site's own `tracking.emit` decides; `true` and
|
|
56
|
+
// `false` override it in either direction, so an owner can instrument one
|
|
57
|
+
// page of a quiet site or exempt a noisy page of a loud one.
|
|
58
|
+
//
|
|
59
|
+
// ⛔ **`|| false` was wrong here and collapsed the tri-state**, which made
|
|
60
|
+
// page-level the ONLY spelling and forced every owner to tick pages
|
|
61
|
+
// individually. The wire already carried all three — `content-collector`
|
|
62
|
+
// emits the key only when it is `!= null` — so nothing but this line had to
|
|
63
|
+
// change. ⇒ Read `?? undefined`, never a boolean coercion.
|
|
64
|
+
//
|
|
65
|
+
// The runtime passes it to `Tracker.arms('section_view', …)`, which is where
|
|
66
|
+
// the precedence against the host's own list is resolved. It means nothing
|
|
67
|
+
// without a tracking destination.
|
|
68
|
+
this.trackSections = pageData.trackSections ?? undefined
|
|
58
69
|
|
|
59
70
|
// Two orthogonal visibility axes:
|
|
60
71
|
// • `hidden` — REACHABILITY. When true the page is excluded from the published
|
package/src/tracker.js
CHANGED
|
@@ -171,13 +171,28 @@ export default class Tracker {
|
|
|
171
171
|
* @param {Object} options
|
|
172
172
|
* @param {string} [options.endpoint] - destination; **required to enable**
|
|
173
173
|
* @param {boolean} [options.consentRequired=false] - hold everything until granted
|
|
174
|
-
* @param {number} [options.
|
|
174
|
+
* @param {number} [options.flushIntervalMs=5000] - batch window, MILLISECONDS
|
|
175
175
|
* @param {number} [options.maxQueueSize=10]
|
|
176
176
|
* @param {boolean} [options.debug=false]
|
|
177
177
|
*/
|
|
178
178
|
constructor(options = {}) {
|
|
179
179
|
this.endpoint = options.endpoint || null
|
|
180
|
-
|
|
180
|
+
// ⭐ **`Ms` is in the name because the unit cannot be inferred from the
|
|
181
|
+
// value, and the failure is silent and inverted.** A bare `flushInterval:
|
|
182
|
+
// 30` meaning *thirty seconds* reads here as 30 **milliseconds** — flushing
|
|
183
|
+
// ~33x a second, the opposite of the intent, indistinguishable from working
|
|
184
|
+
// config, and paid for by the host rather than by whoever typed it. The wire
|
|
185
|
+
// field the host emits is `flushIntervalMs` for the same reason; this option
|
|
186
|
+
// is spelled to match so nothing has to translate between them.
|
|
187
|
+
//
|
|
188
|
+
// ⛔ **No floor is imposed, deliberately.** A minimum here would be a second
|
|
189
|
+
// copy of a policy the host already owns, able only to disagree with theirs —
|
|
190
|
+
// the same reason this framework never co-owns a serve location. What is
|
|
191
|
+
// rejected is not a *small* value but an *invalid* one: anything non-finite
|
|
192
|
+
// or <= 0 would arm a spinning or never-firing timer, so it falls back to the
|
|
193
|
+
// default rather than being honoured.
|
|
194
|
+
const interval = options.flushIntervalMs
|
|
195
|
+
this.flushIntervalMs = Number.isFinite(interval) && interval > 0 ? interval : 5000
|
|
181
196
|
this.maxQueueSize = options.maxQueueSize || 10
|
|
182
197
|
this.debug = options.debug || false
|
|
183
198
|
|
|
@@ -207,6 +222,17 @@ export default class Tracker {
|
|
|
207
222
|
// reports three times.
|
|
208
223
|
this.currentPath = null
|
|
209
224
|
|
|
225
|
+
// What the runtime may ARM, as two independent narrowings. Both are `null`
|
|
226
|
+
// when nothing narrows, which is the common case and the cheap one.
|
|
227
|
+
//
|
|
228
|
+
// ⛔ **Neither ever filters `track()`.** The registry is open by design, and
|
|
229
|
+
// a client-side allowlist over a foundation's own events would export one
|
|
230
|
+
// host's policy onto every host — including hosts that sent no list, whose
|
|
231
|
+
// sites would silently drop everything their foundation emits. These gate
|
|
232
|
+
// the runtime's OWN emissions and nothing else. See `arms()`.
|
|
233
|
+
this.hostEvents = options.hostEvents ? new Set(options.hostEvents) : null
|
|
234
|
+
this.siteEmit = options.siteEmit ? new Set(options.siteEmit) : null
|
|
235
|
+
|
|
210
236
|
this.framed = detectFramed()
|
|
211
237
|
|
|
212
238
|
// Called once when consent moves to granted, and never otherwise. The
|
|
@@ -259,6 +285,39 @@ export default class Tracker {
|
|
|
259
285
|
return !!this.endpoint && this.isLiveDocument()
|
|
260
286
|
}
|
|
261
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Whether the runtime should ARM one of its own automatic emitters.
|
|
290
|
+
*
|
|
291
|
+
* ⭐ **Three questions, in the order that makes each one's absence safe:**
|
|
292
|
+
*
|
|
293
|
+
* 1. **Is there anywhere to send, in a live document?** — `isEnabled()`.
|
|
294
|
+
* 2. **Will the host consume this?** `hostEvents` is the host's cost switch:
|
|
295
|
+
* no point arming an observer for a row nobody stores. ⛔ **Absent means NO
|
|
296
|
+
* NARROWING, never an empty set** — a host that sends no list is an older
|
|
297
|
+
* or simpler one, and reading absence as "consume nothing" would take
|
|
298
|
+
* every site on that host dark with every gate reading yes.
|
|
299
|
+
* 3. **Did the site ask for it?** `siteEmit` is the operator's own selection.
|
|
300
|
+
* Absent means everything.
|
|
301
|
+
*
|
|
302
|
+
* `override` is the per-page answer where one exists (`page.trackSections`).
|
|
303
|
+
* It replaces the SITE's answer and cannot escape the host's: a page may
|
|
304
|
+
* widen what its own site configured, and may not conjure a row the host
|
|
305
|
+
* declined to store.
|
|
306
|
+
*
|
|
307
|
+
* ⛔ **Not consulted by `track()`.** A foundation's events are never gated —
|
|
308
|
+
* see the constructor.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} event
|
|
311
|
+
* @param {boolean} [override] - the per-page decision, when the caller has one
|
|
312
|
+
* @returns {boolean}
|
|
313
|
+
*/
|
|
314
|
+
arms(event, override) {
|
|
315
|
+
if (!this.isEnabled()) return false
|
|
316
|
+
if (this.hostEvents && !this.hostEvents.has(event)) return false
|
|
317
|
+
if (override != null) return !!override
|
|
318
|
+
return !this.siteEmit || this.siteEmit.has(event)
|
|
319
|
+
}
|
|
320
|
+
|
|
262
321
|
/** @returns {'granted'|'denied'|'pending'} */
|
|
263
322
|
consentStatus() {
|
|
264
323
|
return this.consent
|
|
@@ -333,7 +392,7 @@ export default class Tracker {
|
|
|
333
392
|
* @param {string} path
|
|
334
393
|
*/
|
|
335
394
|
trackPageView(path) {
|
|
336
|
-
if (!this.
|
|
395
|
+
if (!this.arms('page_view') || !path) return
|
|
337
396
|
if (path === this.currentPath) return
|
|
338
397
|
this.currentPath = path
|
|
339
398
|
// Promptly, rather than waiting out the batch window: a page view is the
|
|
@@ -428,7 +487,7 @@ export default class Tracker {
|
|
|
428
487
|
* @private
|
|
429
488
|
*/
|
|
430
489
|
armFlushInterval() {
|
|
431
|
-
setInterval(() => this.flush(), this.
|
|
490
|
+
setInterval(() => this.flush(), this.flushIntervalMs)
|
|
432
491
|
}
|
|
433
492
|
|
|
434
493
|
/** @private */
|