@scalebun/web 1.2.3 → 1.3.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.
Files changed (144) hide show
  1. package/dist/bootstrap/SDKBootstrapper.d.ts.map +1 -1
  2. package/dist/bootstrap/SDKBootstrapper.js +37 -1
  3. package/dist/bootstrap/SDKBootstrapper.js.map +1 -1
  4. package/dist/browser.d.ts +2 -2
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +6 -3
  7. package/dist/browser.js.map +1 -1
  8. package/dist/config/schema.d.ts +3 -3
  9. package/dist/config/schema.d.ts.map +1 -1
  10. package/dist/config/schema.js +25 -6
  11. package/dist/config/schema.js.map +1 -1
  12. package/dist/core/pageContext.js +17 -1
  13. package/dist/core/pageContext.js.map +1 -1
  14. package/dist/core/prune.d.ts +28 -4
  15. package/dist/core/prune.d.ts.map +1 -1
  16. package/dist/core/prune.js +92 -11
  17. package/dist/core/prune.js.map +1 -1
  18. package/dist/features/bugreport/BugReportFeature.d.ts +3 -2
  19. package/dist/features/bugreport/BugReportFeature.d.ts.map +1 -1
  20. package/dist/features/bugreport/BugReportFeature.js +2 -1
  21. package/dist/features/bugreport/BugReportFeature.js.map +1 -1
  22. package/dist/features/bugreport/collect.d.ts +13 -1
  23. package/dist/features/bugreport/collect.d.ts.map +1 -1
  24. package/dist/features/bugreport/collect.js +4 -1
  25. package/dist/features/bugreport/collect.js.map +1 -1
  26. package/dist/features/bugreport/widget.d.ts +21 -2
  27. package/dist/features/bugreport/widget.d.ts.map +1 -1
  28. package/dist/features/bugreport/widget.js +37 -4
  29. package/dist/features/bugreport/widget.js.map +1 -1
  30. package/dist/features/coach/overlay.d.ts.map +1 -1
  31. package/dist/features/coach/overlay.js +26 -5
  32. package/dist/features/coach/overlay.js.map +1 -1
  33. package/dist/features/engage/EngageFeature.d.ts +7 -1
  34. package/dist/features/engage/EngageFeature.d.ts.map +1 -1
  35. package/dist/features/engage/EngageFeature.js +32 -10
  36. package/dist/features/engage/EngageFeature.js.map +1 -1
  37. package/dist/features/engage/normalize.d.ts.map +1 -1
  38. package/dist/features/engage/normalize.js +7 -0
  39. package/dist/features/engage/normalize.js.map +1 -1
  40. package/dist/features/engage/runtime.d.ts +11 -0
  41. package/dist/features/engage/runtime.d.ts.map +1 -1
  42. package/dist/features/engage/runtime.js +25 -10
  43. package/dist/features/engage/runtime.js.map +1 -1
  44. package/dist/features/engage/throttle.d.ts +16 -6
  45. package/dist/features/engage/throttle.d.ts.map +1 -1
  46. package/dist/features/engage/throttle.js +96 -6
  47. package/dist/features/engage/throttle.js.map +1 -1
  48. package/dist/features/engage/types.d.ts +6 -0
  49. package/dist/features/engage/types.d.ts.map +1 -1
  50. package/dist/features/engage/types.js.map +1 -1
  51. package/dist/features/inapp/InApp2Feature.d.ts.map +1 -1
  52. package/dist/features/inapp/InApp2Feature.js +1 -0
  53. package/dist/features/inapp/InApp2Feature.js.map +1 -1
  54. package/dist/features/inapp/blocks/heavy/steps.d.ts.map +1 -1
  55. package/dist/features/inapp/blocks/heavy/steps.js +9 -1
  56. package/dist/features/inapp/blocks/heavy/steps.js.map +1 -1
  57. package/dist/features/inapp/blocks/styles.d.ts +1 -1
  58. package/dist/features/inapp/blocks/styles.d.ts.map +1 -1
  59. package/dist/features/inapp/blocks/styles.js +30 -3
  60. package/dist/features/inapp/blocks/styles.js.map +1 -1
  61. package/dist/features/inapp/center/panel.d.ts +1 -1
  62. package/dist/features/inapp/center/panel.d.ts.map +1 -1
  63. package/dist/features/inapp/center/panel.js +40 -6
  64. package/dist/features/inapp/center/panel.js.map +1 -1
  65. package/dist/features/inapp/core/engine.d.ts +2 -0
  66. package/dist/features/inapp/core/engine.d.ts.map +1 -1
  67. package/dist/features/inapp/core/engine.js +1 -1
  68. package/dist/features/inapp/core/engine.js.map +1 -1
  69. package/dist/features/inapp/core/host.d.ts.map +1 -1
  70. package/dist/features/inapp/core/host.js +5 -1
  71. package/dist/features/inapp/core/host.js.map +1 -1
  72. package/dist/features/inapp/core/targeting.d.ts +29 -2
  73. package/dist/features/inapp/core/targeting.d.ts.map +1 -1
  74. package/dist/features/inapp/core/targeting.js +78 -4
  75. package/dist/features/inapp/core/targeting.js.map +1 -1
  76. package/dist/features/inapp/inappContract.web.d.ts.map +1 -1
  77. package/dist/features/inapp/inappContract.web.js +12 -1
  78. package/dist/features/inapp/inappContract.web.js.map +1 -1
  79. package/dist/features/inapp/surfaces/coachmark.d.ts.map +1 -1
  80. package/dist/features/inapp/surfaces/coachmark.js +5 -2
  81. package/dist/features/inapp/surfaces/coachmark.js.map +1 -1
  82. package/dist/features/inapp/surfaces/hotspot.d.ts.map +1 -1
  83. package/dist/features/inapp/surfaces/hotspot.js +5 -2
  84. package/dist/features/inapp/surfaces/hotspot.js.map +1 -1
  85. package/dist/features/inapp/surfaces/inline.d.ts.map +1 -1
  86. package/dist/features/inapp/surfaces/inline.js +7 -1
  87. package/dist/features/inapp/surfaces/inline.js.map +1 -1
  88. package/dist/features/inapp/surfaces/popover.d.ts.map +1 -1
  89. package/dist/features/inapp/surfaces/popover.js +5 -2
  90. package/dist/features/inapp/surfaces/popover.js.map +1 -1
  91. package/dist/features/inapp/surfaces/styles.d.ts +1 -1
  92. package/dist/features/inapp/surfaces/styles.d.ts.map +1 -1
  93. package/dist/features/inapp/surfaces/styles.js +60 -11
  94. package/dist/features/inapp/surfaces/styles.js.map +1 -1
  95. package/dist/features/inapp/surfaces/toolbar.d.ts.map +1 -1
  96. package/dist/features/inapp/surfaces/toolbar.js +5 -2
  97. package/dist/features/inapp/surfaces/toolbar.js.map +1 -1
  98. package/dist/features/inapp/surfaces/tour.d.ts.map +1 -1
  99. package/dist/features/inapp/surfaces/tour.js +5 -2
  100. package/dist/features/inapp/surfaces/tour.js.map +1 -1
  101. package/dist/features/inapp/types.d.ts +2 -0
  102. package/dist/features/inapp/types.d.ts.map +1 -1
  103. package/dist/features/journey/InteractionTracker.d.ts.map +1 -1
  104. package/dist/features/journey/InteractionTracker.js +145 -0
  105. package/dist/features/journey/InteractionTracker.js.map +1 -1
  106. package/dist/features/journey/positionContext.d.ts +34 -0
  107. package/dist/features/journey/positionContext.d.ts.map +1 -0
  108. package/dist/features/journey/positionContext.js +43 -0
  109. package/dist/features/journey/positionContext.js.map +1 -0
  110. package/dist/features/manifest.d.ts +21 -0
  111. package/dist/features/manifest.d.ts.map +1 -1
  112. package/dist/features/manifest.js +27 -10
  113. package/dist/features/manifest.js.map +1 -1
  114. package/dist/features/replay/ReplayFeature.d.ts +20 -0
  115. package/dist/features/replay/ReplayFeature.d.ts.map +1 -1
  116. package/dist/features/replay/ReplayFeature.js +155 -5
  117. package/dist/features/replay/ReplayFeature.js.map +1 -1
  118. package/dist/features/replay/lifecycle/recorder.d.ts +21 -1
  119. package/dist/features/replay/lifecycle/recorder.d.ts.map +1 -1
  120. package/dist/features/replay/lifecycle/recorder.js +20 -4
  121. package/dist/features/replay/lifecycle/recorder.js.map +1 -1
  122. package/dist/features/scroll/index.d.ts +2 -0
  123. package/dist/features/scroll/index.d.ts.map +1 -1
  124. package/dist/features/scroll/index.js +6 -2
  125. package/dist/features/scroll/index.js.map +1 -1
  126. package/dist/features/session/sessionStore.d.ts +17 -1
  127. package/dist/features/session/sessionStore.d.ts.map +1 -1
  128. package/dist/features/session/sessionStore.js +17 -1
  129. package/dist/features/session/sessionStore.js.map +1 -1
  130. package/dist/pipeline/pipeline.d.ts +82 -0
  131. package/dist/pipeline/pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/pipeline.js +307 -14
  133. package/dist/pipeline/pipeline.js.map +1 -1
  134. package/dist/public/ScaleBunFacade.d.ts +31 -0
  135. package/dist/public/ScaleBunFacade.d.ts.map +1 -1
  136. package/dist/public/ScaleBunFacade.js +52 -10
  137. package/dist/public/ScaleBunFacade.js.map +1 -1
  138. package/dist/scalebun.min.js +147 -65
  139. package/dist/scalebun.min.js.map +4 -4
  140. package/dist/transport/httpClient.d.ts +56 -2
  141. package/dist/transport/httpClient.d.ts.map +1 -1
  142. package/dist/transport/httpClient.js +81 -5
  143. package/dist/transport/httpClient.js.map +1 -1
  144. package/package.json +10 -4
@@ -10,6 +10,7 @@
10
10
  * unmasked, so this never was the SDK's privacy boundary anyway.
11
11
  */
12
12
  import { RageDetector, DeadClickWatcher } from './friction.js';
13
+ import { getPositionContext } from './positionContext.js';
13
14
  export class InteractionTracker {
14
15
  constructor(emit, opts = {},
15
16
  /** Raise a capture signal ('rage'|'dead') onto the bus — arms friction-triggered features
@@ -89,11 +90,155 @@ function pointer(me) {
89
90
  function anchor(el) {
90
91
  const r = el.getBoundingClientRect();
91
92
  const out = { ex: r.x | 0, ey: r.y | 0, ew: r.width | 0, eh: r.height | 0 };
93
+ /**
94
+ * VIEWPORT ANCHORING — `fx` is now 1 for fixed and 2 for STICKY.
95
+ *
96
+ * Additive by widening the value rather than by adding a field. Every deployed reader tests `fx`
97
+ * for truthiness, and 2 is as truthy as 1, so an old backend treats a sticky click as
98
+ * element-owned — exactly the intended behaviour — while a new one can tell them apart. Nothing
99
+ * changes meaning and no deploy ordering is required.
100
+ *
101
+ * A separate `pc: 'sticky'` string was written first and reverted: it cost 60 bytes against 14
102
+ * bytes of core headroom. See positionContext.ts.
103
+ *
104
+ * STICKY IS WHY THIS CHANGED AT ALL. The old check looked for `position: fixed` and nothing else,
105
+ * so a sticky header read as ordinary content and its clicks went to the document-space lane —
106
+ * which must choose one page coordinate for a control clicked at every scroll depth.
107
+ */
108
+ const pc = getPositionContext(el);
109
+ // Emitted only when there is something to say, so the common case still adds no bytes.
110
+ if (pc)
111
+ out.fx = pc;
112
+ out.path = structuralPath(el);
113
+ const ui = openOverlays();
114
+ if (ui)
115
+ out.ui = ui;
92
116
  const comp = el.closest('[data-scalebun-component]')?.getAttribute('data-scalebun-component');
93
117
  if (comp)
94
118
  out.component = comp;
95
119
  return out;
96
120
  }
121
+ /**
122
+ * S1 — WHICH OVERLAYS WERE OPEN when this click happened.
123
+ *
124
+ * A click on a menu item only means something if the menu was open. Nothing stored today says so, and
125
+ * it is not derivable afterwards from anything else on the event — the same lesson as `fx` and
126
+ * `path`: capture has to lead use, and history cannot be backfilled. Without this the dashboard can
127
+ * SHOW a state (the viewer can open and close the reconstruction) but cannot COUNT one, because every
128
+ * click on the route is aggregated into a single picture regardless of what was on screen.
129
+ *
130
+ * PLATFORM-DEFINED STATE ONLY: `aria-expanded="true"` and `<dialog open>`. Deliberately no guessing at
131
+ * `.menu-open` or `.is-active` — a class heuristic is right on some sites and silently wrong on
132
+ * others, and a wrong state key partitions clicks into buckets that do not correspond to anything.
133
+ *
134
+ * SORTED, so the same set of open overlays produces the same key whatever order the DOM happens to
135
+ * return them in — otherwise one state would fragment into several and the counts would be
136
+ * meaningless.
137
+ *
138
+ * Emitted ONLY when something is open, exactly as `fx` is. The overwhelming majority of clicks happen
139
+ * with nothing open and pay nothing at all; the length cap bounds the rest.
140
+ */
141
+ function openOverlays() {
142
+ const out = [];
143
+ document.querySelectorAll('[aria-expanded="true"],dialog[open]').forEach((e) => {
144
+ const k = e.getAttribute('aria-controls') || e.getAttribute('aria-label') || e.id;
145
+ if (k)
146
+ out.push(k);
147
+ });
148
+ return out.length ? out.sort().join('|').slice(0, 64) : undefined;
149
+ }
150
+ /**
151
+ * True when this click landed on a VIEWPORT-anchored element — one that does not move as the page
152
+ * scrolls, because it or an ancestor computes to `position: fixed`.
153
+ *
154
+ * WHY THE DASHBOARD NEEDS THIS. A click's page position is derived as `y + sy`: viewport coordinate
155
+ * plus scroll offset. For a fixed element that arithmetic is meaningless — it never moved, so adding
156
+ * how far the visitor happened to have scrolled scatters one physical button down the whole document.
157
+ * Measured on real data before this field existed: a single fixed host produced clicks at 22 distinct
158
+ * page depths between 0.5% and 50.8%, which is 10% of all placeable clicks laid down as a false
159
+ * vertical streak. A structured artefact like that reads as a finding; noise does not.
160
+ *
161
+ * It was worked around by excluding one selector we own by name. That only ever covered OUR widget —
162
+ * the customer's sticky header, cookie banner and third-party chat have the identical defect and
163
+ * cannot be recognised from a selector. This field is the general fix, and it is the ELEMENT that
164
+ * knows: only capture can answer it, which is why it belongs here and not in the query.
165
+ *
166
+ * ONLY `fixed`, DELIBERATELY NOT `sticky`. A sticky element behaves like a fixed one *while stuck*
167
+ * and like normal content otherwise, and which of the two it is at click time cannot be decided from
168
+ * the element alone without reconstructing its natural flow position. Reporting a guess would put
169
+ * clicks in a confidently wrong place, which is the failure this field exists to remove. Sticky
170
+ * therefore keeps today's behaviour until it can be answered rather than estimated.
171
+ *
172
+ * Emitted only when TRUE (`fx: 1`), so the common case adds no bytes to the payload.
173
+ *
174
+ * Cost: `getComputedStyle` per ancestor, on click only. Layout is already flushed by the
175
+ * `getBoundingClientRect` above, so these reads hit a clean style tree rather than forcing a
176
+ * recalculation. Uncapped depth on purpose — a cap would silently misreport exactly the deeply
177
+ * nested content inside a fixed modal that most needs this to be right.
178
+ */
179
+ /**
180
+ * A STRUCTURAL path to the clicked element — `main>div:nth-of-type(2)>button`.
181
+ *
182
+ * WHY, given `selector` already exists. `selector` is the element's own tag and classes, which turns
183
+ * out to identify almost nothing: measured across 849 real clicks, the single commonest value is a
184
+ * bare `div` (159 of them), followed by Tailwind utility soups like `div.flex.flex-col`. None of
185
+ * those distinguish one element from thousands of others, so anything built on them — anchoring a
186
+ * click to the control it hit, following a control across a redesign — is no more reliable than raw
187
+ * coordinates while LOOKING far more precise. That combination is the worst of the two.
188
+ *
189
+ * A path is stable where classes are not: utility classes change whenever the design does, but a
190
+ * button's position in the document structure usually survives a restyle.
191
+ *
192
+ * ADDED ALONGSIDE `selector`, NOT REPLACING IT. The component heatmap aggregates on `selector`
193
+ * today; changing its meaning underneath would silently re-bucket every existing aggregate against
194
+ * history captured under the old rule.
195
+ *
196
+ * Captured now although nothing consumes it yet, which is a deliberate exception to not shipping
197
+ * unused fields: a path cannot be reconstructed after the fact from a stored click, so the data has
198
+ * to start accumulating before the feature that needs it can be built at all. The alternative is
199
+ * building it later and having no history to run it against.
200
+ *
201
+ * An `id` ENDS the walk — an id is unique in the document, so everything above it is noise, and
202
+ * stopping keeps the common case short. Depth is capped for the same reason: bytes on every click
203
+ * are real, and the segments nearest the element carry nearly all of the identifying power.
204
+ *
205
+ * `nth-of-type` is emitted only when a parent holds more than one child of that tag, so a page whose
206
+ * structure is already unambiguous pays nothing for it.
207
+ */
208
+ function structuralPath(el) {
209
+ const parts = [];
210
+ let n = el;
211
+ for (let depth = 0; n && depth < 6; depth++) {
212
+ const tag = n.tagName.toLowerCase();
213
+ if (tag === 'html' || tag === 'body')
214
+ break;
215
+ if (n.id) {
216
+ parts.unshift(`${tag}#${n.id}`);
217
+ break;
218
+ }
219
+ const up = n.parentElement;
220
+ if (up) {
221
+ let index = 0;
222
+ let total = 0;
223
+ const kids = up.children;
224
+ const self = n;
225
+ for (let i = 0; i < kids.length; i++) {
226
+ const kid = kids[i];
227
+ if (!kid || kid.tagName !== self.tagName)
228
+ continue;
229
+ total++;
230
+ if (kid === self)
231
+ index = total;
232
+ }
233
+ parts.unshift(total > 1 ? `${tag}:nth-of-type(${index})` : tag);
234
+ }
235
+ else {
236
+ parts.unshift(tag);
237
+ }
238
+ n = up;
239
+ }
240
+ return parts.join('>');
241
+ }
97
242
  const mask = (s) => s.replace(/\S/g, '*');
98
243
  /**
99
244
  * A human-ish label: tag + short text, e.g. `button: Buy now`.
@@ -1 +1 @@
1
- {"version":3,"file":"InteractionTracker.js","sourceRoot":"","sources":["../../../src/features/journey/InteractionTracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"InteractionTracker.js","sourceRoot":"","sources":["../../../src/features/journey/InteractionTracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAavD,MAAM,OAAO,kBAAkB;IAM7B,YACmB,IAAgB,EACjC,OAAkC,EAAE;IACpC;mGAC+F;IAC9E,MAAiC;QAJjC,SAAI,GAAJ,IAAI,CAAY;QAIhB,WAAM,GAAN,MAAM,CAA2B;QAElD,IAAI,CAAC,IAAI,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,YAAY,IAAI,GAAG,EAAE,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;QACjG,IAAI,CAAC,IAAI,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC;QACrD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC;IACzC,CAAC;IAED,KAAK;QACH,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO;QAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC5B,IAAI,CAAC,SAAS,GAAG,CAAC,CAAQ,EAAE,EAAE;YAC5B,MAAM,EAAE,GAAG,CAAe,CAAC;YAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAwB,CAAC;YAC1C,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,OAAO;YAEvC,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC9C,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;YACpC,2FAA2F;YAC3F,uDAAuD;YACvD,MAAM,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;YACtB,wGAAwG;YACxG,4GAA4G;YAC5G,6FAA6F;YAC7F,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACvD,MAAM,IAAI,GAA4B,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,CAAC;YACjF,IAAI,IAAI;gBAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACtC,IAAI,IAAI;gBAAE,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,kDAAkD;YAEnF,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE;gBACxB,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;gBACxF,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,yDAAyD;YAClF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QACF,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,SAAS,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;YACtD,QAAQ,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;CACF;AAED;;;;;;;;;uEASuE;AACvE,SAAS,OAAO,CAAC,EAAc;IAC7B,MAAM,CAAC,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7D,OAAO;QACL,CAAC,EAAE,EAAE,CAAC,OAAO;QACb,CAAC,EAAE,EAAE,CAAC,OAAO;QACb,EAAE,EAAE,CAAC,EAAE,UAAU,IAAI,CAAC;QACtB,EAAE,EAAE,CAAC,EAAE,WAAW,IAAI,CAAC;QACvB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,EAAE,EAAE,QAAQ,CAAC,eAAe,CAAC,YAAY;QACzC,gGAAgG;QAChG,GAAG,EAAE,CAAC,EAAE,gBAAgB,IAAI,CAAC;KAC9B,CAAC;AACJ,CAAC;AAED,iHAAiH;AACjH,SAAS,MAAM,CAAC,EAAW;IACzB,MAAM,CAAC,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC;IACrC,MAAM,GAAG,GAA4B,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACrG;;;;;;;;;;;;;;OAcG;IACH,MAAM,EAAE,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IAClC,uFAAuF;IACvF,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC;IACpB,GAAG,CAAC,IAAI,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,YAAY,EAAE,CAAC;IAC1B,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC;IACpB,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,2BAA2B,CAAC,EAAE,YAAY,CAAC,yBAAyB,CAAC,CAAC;IAC9F,IAAI,IAAI;QAAE,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC;IAC/B,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,YAAY;IACnB,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,QAAQ,CAAC,gBAAgB,CAAC,qCAAqC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7E,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAClF,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,SAAS,cAAc,CAAC,EAAW;IACjC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,GAAmB,EAAE,CAAC;IAC3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACpC,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM;YAAE,MAAM;QAC5C,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC;YACT,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAChC,MAAM;QACR,CAAC;QACD,MAAM,EAAE,GAAmB,CAAC,CAAC,aAAa,CAAC;QAC3C,IAAI,EAAE,EAAE,CAAC;YACP,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,MAAM,IAAI,GAAmB,EAAE,CAAC,QAAQ,CAAC;YACzC,MAAM,IAAI,GAAY,CAAC,CAAC;YACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO;oBAAE,SAAS;gBACnD,KAAK,EAAE,CAAC;gBACR,IAAI,GAAG,KAAK,IAAI;oBAAE,KAAK,GAAG,KAAK,CAAC;YAClC,CAAC;YACD,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,gBAAgB,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,CAAC,GAAG,EAAE,CAAC;IACT,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAW,EAAE,QAAQ,GAAG,KAAK;IACpD,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,CAAC,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACxD,IAAI,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC;IACtB,OAAO,GAAG,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACnD,CAAC;AAED,+EAA+E;AAC/E,SAAS,UAAU,CAAC,EAAW;IAC7B,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,EAAE,CAAC,EAAE;QAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;SACzC,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,OAAO,CAAC;SACf,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACf,IAAI,GAAG,CAAC,MAAM;QAAE,CAAC,IAAI,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * How an element is positioned relative to the page — the one fact that decides whether a click on
3
+ * it can be placed in document space at all.
4
+ *
5
+ * WHY CHECKING `fixed` WAS NOT ENOUGH. The previous rule walked ancestors for `position: fixed` and
6
+ * nothing else, so a `position: sticky` header read as ordinary content. Its clicks then went into
7
+ * the document-space lane, which must choose ONE page coordinate for them — and a sticky header is
8
+ * clicked at every scroll depth, so no single coordinate is right for more than a fraction of them.
9
+ * The heat sat where the header had been while the header was somewhere else.
10
+ *
11
+ * STICKY COUNTS EVEN WHEN NOT CURRENTLY STUCK. An element that will pin at some offset cannot be
12
+ * described by one document coordinate either, and the consumer positions it from its current rect,
13
+ * which is correct in both phases. Requiring it to be stuck at click time would make the answer
14
+ * depend on when the visitor happened to click.
15
+ *
16
+ * ANCESTORS, NOT JUST THE TARGET. The thing clicked is usually a link or an icon inside the
17
+ * positioned container, so the target's own `position` is almost always `static`.
18
+ *
19
+ * Nothing here names a tag, a class or a framework — it asks the browser what it computed.
20
+ *
21
+ * NUMERIC, AND IT SHIPS ON `fx`. The core bundle gate had 14 bytes of headroom; a `pc: 'sticky'`
22
+ * string field costs 60. Widening the value of the field that already exists is additive in the way
23
+ * that matters — every deployed reader tests `fx` for truthiness, and 2 is as truthy as 1, so an old
24
+ * backend treats a sticky click as element-owned, which is exactly the intended behaviour. A new
25
+ * reader can tell the two apart. No field changes meaning, and nothing has to be deployed in order.
26
+ */
27
+ /** 0 = normal document flow · 1 = fixed ancestor · 2 = sticky ancestor. */
28
+ export type PositionContext = 0 | 1 | 2;
29
+ /**
30
+ * `fixed` wins over `sticky` when both appear: the nearest fixed ancestor is what actually detaches
31
+ * the subtree from the document, and a sticky element inside a fixed one travels with the fixed one.
32
+ */
33
+ export declare function getPositionContext(el: Element): PositionContext;
34
+ //# sourceMappingURL=positionContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"positionContext.d.ts","sourceRoot":"","sources":["../../../src/features/journey/positionContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,2EAA2E;AAC3E,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExC;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG,eAAe,CAS/D"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * How an element is positioned relative to the page — the one fact that decides whether a click on
3
+ * it can be placed in document space at all.
4
+ *
5
+ * WHY CHECKING `fixed` WAS NOT ENOUGH. The previous rule walked ancestors for `position: fixed` and
6
+ * nothing else, so a `position: sticky` header read as ordinary content. Its clicks then went into
7
+ * the document-space lane, which must choose ONE page coordinate for them — and a sticky header is
8
+ * clicked at every scroll depth, so no single coordinate is right for more than a fraction of them.
9
+ * The heat sat where the header had been while the header was somewhere else.
10
+ *
11
+ * STICKY COUNTS EVEN WHEN NOT CURRENTLY STUCK. An element that will pin at some offset cannot be
12
+ * described by one document coordinate either, and the consumer positions it from its current rect,
13
+ * which is correct in both phases. Requiring it to be stuck at click time would make the answer
14
+ * depend on when the visitor happened to click.
15
+ *
16
+ * ANCESTORS, NOT JUST THE TARGET. The thing clicked is usually a link or an icon inside the
17
+ * positioned container, so the target's own `position` is almost always `static`.
18
+ *
19
+ * Nothing here names a tag, a class or a framework — it asks the browser what it computed.
20
+ *
21
+ * NUMERIC, AND IT SHIPS ON `fx`. The core bundle gate had 14 bytes of headroom; a `pc: 'sticky'`
22
+ * string field costs 60. Widening the value of the field that already exists is additive in the way
23
+ * that matters — every deployed reader tests `fx` for truthiness, and 2 is as truthy as 1, so an old
24
+ * backend treats a sticky click as element-owned, which is exactly the intended behaviour. A new
25
+ * reader can tell the two apart. No field changes meaning, and nothing has to be deployed in order.
26
+ */
27
+ /**
28
+ * `fixed` wins over `sticky` when both appear: the nearest fixed ancestor is what actually detaches
29
+ * the subtree from the document, and a sticky element inside a fixed one travels with the fixed one.
30
+ */
31
+ export function getPositionContext(el) {
32
+ // No `typeof window` guard: this runs only inside DOM event handlers, which never fire server-side.
33
+ let sticky = 0;
34
+ for (let n = el; n; n = n.parentElement) {
35
+ const p = getComputedStyle(n).position;
36
+ if (p === 'fixed')
37
+ return 1;
38
+ if (p === 'sticky')
39
+ sticky = 2;
40
+ }
41
+ return sticky;
42
+ }
43
+ //# sourceMappingURL=positionContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"positionContext.js","sourceRoot":"","sources":["../../../src/features/journey/positionContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAW;IAC5C,oGAAoG;IACpG,IAAI,MAAM,GAAoB,CAAC,CAAC;IAChC,KAAK,IAAI,CAAC,GAAmB,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC;QACxD,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACvC,IAAI,CAAC,KAAK,OAAO;YAAE,OAAO,CAAC,CAAC;QAC5B,IAAI,CAAC,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -6,6 +6,27 @@ import type { Capabilities } from '../core/capabilities.js';
6
6
  * only registers a feature when config.features[name] !== false (replay is off by default).
7
7
  */
8
8
  export declare const FEATURES: IFeature[];
9
+ /**
10
+ * P5a — features that are OFF BY DEFAULT, loaded only when a host turns one on.
11
+ *
12
+ * WHY THESE FIVE AND NOT OTHERS: `resolveConfig` defaults `replay`, `inbox`, `profiler`, `watchdog`
13
+ * and `inapp2` to `false`. Every other feature defaults to `true`, so lazy-loading it would trade
14
+ * eager bytes for a round trip on the path virtually every site takes — a bad exchange. These five are
15
+ * the only ones the typical site never uses, and they were nonetheless constructed and bundled into
16
+ * the eager core on every page load.
17
+ *
18
+ * MEASURED: removing their shells takes the eager core from 33,602 B to 32,219 B gzipped — **1,383 B**,
19
+ * which is 7× the headroom that existed before this change (190 B). That budget is what a minimal
20
+ * eager keyframe needs, and it did not exist until now.
21
+ *
22
+ * A dynamic `import()` here is what actually moves the bytes; a `() => new X()` factory would not,
23
+ * because the static import would still pull the module into the eager chunk.
24
+ *
25
+ * The cost is one round trip when a host opts in, paid during `activate()` before `setupAll` — so an
26
+ * enabled feature still sets up before the pipeline starts, exactly as an eager one does. The ordering
27
+ * F-VIT depends on is unchanged.
28
+ */
29
+ export declare const OPTIONAL_FEATURES: Record<string, () => Promise<IFeature>>;
9
30
  /**
10
31
  * The MINIMAL capability each feature fundamentally needs — bootstrap skips a feature whose required
11
32
  * capabilities are absent in the current runtime (e.g. DOM features on the edge). CONSERVATIVE by
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/features/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAoBzD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,EAmB9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,YAAY,CAAC,CAmBlE,CAAC;AAEF,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,YAAY,CAAC,CAE3E"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/features/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAezD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,EAc9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAMrE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,YAAY,CAAC,CAmBlE,CAAC;AAEF,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,YAAY,CAAC,CAE3E"}
@@ -9,13 +9,8 @@ import { ConsoleFeature } from './console/ConsoleFeature.js';
9
9
  import { BugReportFeature } from './bugreport/BugReportFeature.js';
10
10
  import { CrashFeature } from './crash/CrashFeature.js';
11
11
  import { EngageFeature } from './engage/EngageFeature.js';
12
- import { InboxFeature } from './inbox/InboxFeature.js';
13
12
  import { WebPerfFeature } from './webperf/WebPerfFeature.js';
14
- import { ProfilerFeature } from './profiler/ProfilerFeature.js';
15
- import { WatchdogFeature } from './watchdog/WatchdogFeature.js';
16
13
  import { ExitIntentFeature } from './exit-intent/ExitIntentFeature.js';
17
- import { InApp2Feature } from './inapp/InApp2Feature.js';
18
- import { ReplayFeature } from './replay/ReplayFeature.js';
19
14
  /**
20
15
  * The feature registry source. Each capture feature vertical (errors, network, webVitals, replay,
21
16
  * …) appends its instance here — this is the append-only seam from WEB-SDK-PLAN.md §1. Bootstrap
@@ -34,13 +29,35 @@ export const FEATURES = [
34
29
  new BugReportFeature(), // bug-report widget (F-BUG)
35
30
  new CrashFeature(), // crash detection: ReportingObserver + session-liveness recovery (F-CRASH)
36
31
  new EngageFeature(), // surveys/NPS — shell in core, runtime lazy-loaded when a campaign is active (F-ENG)
37
- new InboxFeature(), // persistent message inbox — eager shell, runtime lazy-loaded on first inbox() use (F-INBOX, opt-in)
38
- new ProfilerFeature(), // JS Self-Profiling flame graphs (F-PROF, opt-in/off by default)
39
- new WatchdogFeature(), // resource watchdog: continuous sampling → replay-linked incidents (F-WDOG, opt-in)
40
32
  new ExitIntentFeature(), // exit-intent CRO detector → $exit_intent + signal (F-EXIT, opt-in)
41
- new InApp2Feature(), // IAM 2.0 engine — schema-driven in-app surfaces (F-IAM2, opt-in/off by default)
42
- new ReplayFeature(), // F-RPL (lazy, opt-in)
43
33
  ];
34
+ /**
35
+ * P5a — features that are OFF BY DEFAULT, loaded only when a host turns one on.
36
+ *
37
+ * WHY THESE FIVE AND NOT OTHERS: `resolveConfig` defaults `replay`, `inbox`, `profiler`, `watchdog`
38
+ * and `inapp2` to `false`. Every other feature defaults to `true`, so lazy-loading it would trade
39
+ * eager bytes for a round trip on the path virtually every site takes — a bad exchange. These five are
40
+ * the only ones the typical site never uses, and they were nonetheless constructed and bundled into
41
+ * the eager core on every page load.
42
+ *
43
+ * MEASURED: removing their shells takes the eager core from 33,602 B to 32,219 B gzipped — **1,383 B**,
44
+ * which is 7× the headroom that existed before this change (190 B). That budget is what a minimal
45
+ * eager keyframe needs, and it did not exist until now.
46
+ *
47
+ * A dynamic `import()` here is what actually moves the bytes; a `() => new X()` factory would not,
48
+ * because the static import would still pull the module into the eager chunk.
49
+ *
50
+ * The cost is one round trip when a host opts in, paid during `activate()` before `setupAll` — so an
51
+ * enabled feature still sets up before the pipeline starts, exactly as an eager one does. The ordering
52
+ * F-VIT depends on is unchanged.
53
+ */
54
+ export const OPTIONAL_FEATURES = {
55
+ replay: async () => new (await import('./replay/ReplayFeature.js')).ReplayFeature(),
56
+ inbox: async () => new (await import('./inbox/InboxFeature.js')).InboxFeature(),
57
+ profiler: async () => new (await import('./profiler/ProfilerFeature.js')).ProfilerFeature(),
58
+ watchdog: async () => new (await import('./watchdog/WatchdogFeature.js')).WatchdogFeature(),
59
+ inapp2: async () => new (await import('./inapp/InApp2Feature.js')).InApp2Feature(),
60
+ };
44
61
  /**
45
62
  * The MINIMAL capability each feature fundamentally needs — bootstrap skips a feature whose required
46
63
  * capabilities are absent in the current runtime (e.g. DOM features on the edge). CONSERVATIVE by
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/features/manifest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/features/manifest.ts"],"names":[],"mappings":"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"}
@@ -11,8 +11,28 @@ export declare class ReplayFeature implements IFeature {
11
11
  readonly name = "replay";
12
12
  private controller?;
13
13
  private uninstallWebGL?;
14
+ private watchdog?;
14
15
  get isActive(): boolean;
15
16
  setup(ctx: FeatureContext): Promise<void>;
17
+ /**
18
+ * Resolves when arming has finished — succeeded OR reported its failure. Never rejects (`arm` owns
19
+ * its own error handling), so awaiting it is always safe.
20
+ *
21
+ * Deliberately not on `IFeature`: nothing in the SDK awaits it, and adding it to the interface would
22
+ * imply every feature has an async tail. It exists because `setup()` returning early made "the
23
+ * recorder is now live" unobservable, which the replay suites legitimately need to assert — the
24
+ * alternative was weakening eleven behavioural tests into timing guesses, which would trade a real
25
+ * contract for a green tick.
26
+ */
27
+ armed?: Promise<void>;
28
+ /**
29
+ * Load the recorder and start capturing. Runs detached from `setup()` — see the note above.
30
+ *
31
+ * Every failure here is terminal for replay and silent everywhere else, so this method owns its own
32
+ * reporting: nothing upstream is awaiting it to notice a rejection.
33
+ */
34
+ private arm;
35
+ private load;
16
36
  /** Capture-on-signal flush (promote pre-roll → live). No-op until a recorder is active. */
17
37
  flush(): void;
18
38
  /** Live diagnostics (null when not recording — off, or a non-sampled session). */
@@ -1 +1 @@
1
- {"version":3,"file":"ReplayFeature.d.ts","sourceRoot":"","sources":["../../../src/features/replay/ReplayFeature.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAS5E,qBAAa,aAAc,YAAW,QAAQ;IAC5C,QAAQ,CAAC,IAAI,YAAY;IACzB,OAAO,CAAC,UAAU,CAAC,CAAqB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAa;IAEpC,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAEK,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA2F/C,2FAA2F;IAC3F,KAAK,IAAI,IAAI;IAIb,kFAAkF;IAClF,KAAK,IAAI,WAAW,GAAG,IAAI;IAI3B,6FAA6F;IAC7F,UAAU,CAAC,MAAM,SAAW,GAAG,IAAI;IAInC,8FAA8F;IAC9F,KAAK,IAAI,IAAI;IAIb,+EAA+E;IAC/E,MAAM,IAAI,IAAI;IAId,uFAAuF;IACvF,UAAU,CAAC,MAAM,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAInD,QAAQ,IAAI,IAAI;CAMjB"}
1
+ {"version":3,"file":"ReplayFeature.d.ts","sourceRoot":"","sources":["../../../src/features/replay/ReplayFeature.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAsD5E,qBAAa,aAAc,YAAW,QAAQ;IAC5C,QAAQ,CAAC,IAAI,YAAY;IACzB,OAAO,CAAC,UAAU,CAAC,CAAqB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,CAAgC;IAEjD,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAEK,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAgF/C;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtB;;;;;OAKG;YACW,GAAG;YAgBH,IAAI;IA+FlB,2FAA2F;IAC3F,KAAK,IAAI,IAAI;IAIb,kFAAkF;IAClF,KAAK,IAAI,WAAW,GAAG,IAAI;IAI3B,6FAA6F;IAC7F,UAAU,CAAC,MAAM,SAAW,GAAG,IAAI;IAInC,8FAA8F;IAC9F,KAAK,IAAI,IAAI;IAIb,+EAA+E;IAC/E,MAAM,IAAI,IAAI;IAId,uFAAuF;IACvF,UAAU,CAAC,MAAM,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAInD,QAAQ,IAAI,IAAI;CAUjB"}
@@ -1,6 +1,49 @@
1
+ /**
2
+ * P5 — `decideSampling` is imported STATICALLY, not fetched.
3
+ *
4
+ * It used to be `await import('./lifecycle/sampling.js')`, which put a whole round trip in front of the
5
+ * recorder import: init → fetch sampling → decide → fetch recorder → create. Measured, that waterfall
6
+ * is what sets the capture floor, and the floor is set by LATENCY rather than by session duration —
7
+ * with instant chunks a 100 ms visit captures, at 300 ms per chunk the floor rises to 500 ms and at
8
+ * 600 ms it rises to 2 s. Time-to-first-keyframe scales at roughly 8× per-chunk latency because the
9
+ * graph is deep, so removing a hop from it is worth more than shaving bytes anywhere else.
10
+ *
11
+ * This costs the EAGER core nothing. Since P5a moved `ReplayFeature` itself into a lazy chunk, its
12
+ * static imports are bundled into that chunk — which is already being fetched by the time this runs.
13
+ * The module is ~40 lines and its only runtime dependency, `bucketOf`, is in eager core already for
14
+ * flags and experiments.
15
+ *
16
+ * The guarantee the dynamic import was protecting is preserved and in fact strengthened: a
17
+ * non-sampled session still installs nothing beyond the decision, and now fetches nothing extra to
18
+ * make it.
19
+ */
20
+ import { decideSampling } from './lifecycle/sampling.js';
21
+ // Already in the eager core (FeatureRegistry), so reusing it here costs the identifier and nothing
22
+ // more — and keeps ONE definition of "was this a chunk that never arrived" across both reporters.
23
+ import { isModuleLoadFailure } from '../../bootstrap/FeatureRegistry.js';
1
24
  /** Gap before the single recorder-import retry (A3). Long enough to outlast a blip, short enough that a
2
25
  * bouncing visitor is not what decides the outcome. */
3
26
  const RECORDER_RETRY_MS = 400;
27
+ /**
28
+ * P1b — the capture-failure report. SAME NAME as the React Native SDK, with platform-specific
29
+ * `reason` values, so one dashboard breakdown reads both without a concept-map alias.
30
+ *
31
+ * Hoisted to a const because it appears three times: repeating the literal costs more gzipped bytes
32
+ * than the identifier, and the eager budget it comes out of is 468 B.
33
+ */
34
+ const FAIL = 'sdk_replay_capture_failed';
35
+ /**
36
+ * How long a session may be ARMED with zero frames before that is a defect rather than latency.
37
+ *
38
+ * Measured, not chosen. Of sessions that produced a keyframe, the capture offset is p50 0 ms, p95
39
+ * 2,453 ms, p99 6,821 ms. 5 s sits above p95 and below p99: late enough that a merely slow session is
40
+ * not accused, early enough to still fire inside the 5–10 s bucket — which is where readiness collapses
41
+ * to 31.4% against 77% at 10–30 s, i.e. the one bucket this watchdog exists to explain.
42
+ *
43
+ * A page that unloads before this fires reports nothing, and that is correct: the session ended, it did
44
+ * not fail. Those are `bounced`, and they are counted from duration, not from here.
45
+ */
46
+ const FIRST_FRAME_WATCHDOG_MS = 5000;
4
47
  /** Extracted so the retry awaits the SAME specifier — two spellings would defeat the module cache. */
5
48
  const loadRecorder = () => import('./lifecycle/recorder.js');
6
49
  export class ReplayFeature {
@@ -11,11 +54,31 @@ export class ReplayFeature {
11
54
  return this.controller?.isActive ?? false;
12
55
  }
13
56
  async setup(ctx) {
14
- if (typeof document === 'undefined' || typeof MutationObserver === 'undefined')
57
+ /**
58
+ * P1b — the three exits below used to be SILENT, and that silence is the whole diagnostic gap.
59
+ *
60
+ * Measured on 275 live web sessions: 64 lasted a second or more and stored zero frames, and 57 of
61
+ * those 64 uploaded events, web vitals and `first_open` in the same session — so the SDK
62
+ * demonstrably ran, authenticated and delivered. Yet `sdk_feature_setup_failed` appears zero times
63
+ * across 2,736 analytics events and 2,317 replay events. The reason is now known: that event only
64
+ * fires when `setup()` THROWS, and these paths `return`. A failure that returns normally is
65
+ * indistinguishable from a healthy session that simply had nothing to say.
66
+ *
67
+ * Sampling is deliberately NOT reported. It is a legitimate decision rather than a failure, and it
68
+ * is already recoverable server-side — the bucket is `fnv1a32(sessionId + ':wzb-replay-v1') % 100`,
69
+ * byte-identical in the backend, so "should this session have recorded" is re-derivable without
70
+ * spending a single eager byte or trusting the client. Verified against this dataset: `missing`
71
+ * sessions are spread evenly across all 100 buckets (33 below 50, 31 above), which is what ruled
72
+ * sampling out as the cause. Calling a sampled-out session a capture failure would also poison the
73
+ * one metric this event exists to feed.
74
+ */
75
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') {
76
+ ctx.trackEvent(FAIL, { stage: 'init', reason: 'ENV_UNSUPPORTED' });
15
77
  return;
78
+ }
16
79
  // Deterministic sampling gate (server-reconcilable fnv1a) — decide BEFORE loading the recorder,
17
- // so a non-sampled session installs nothing beyond this tiny decision.
18
- const { decideSampling } = await import('./lifecycle/sampling.js');
80
+ // so a non-sampled session installs nothing beyond this tiny decision. Statically imported: see
81
+ // the note on the import.
19
82
  const decision = decideSampling(ctx.replaySampling, ctx.sessionId ?? '');
20
83
  ctx.log('[replay] sampling:', decision.reason, '(bucket', decision.bucket + ')');
21
84
  if (!decision.record)
@@ -26,6 +89,9 @@ export class ReplayFeature {
26
89
  const { userOptedOutOfTracking } = await import('./lifecycle/deviceSignals.js');
27
90
  if (userOptedOutOfTracking()) {
28
91
  ctx.log('[replay] dnt: skip');
92
+ // Not a defect — a respected opt-out. Reported anyway so it lands in the ATTRIBUTED column
93
+ // instead of the residual; an unexplained gap is what this phase exists to remove.
94
+ ctx.trackEvent(FAIL, { stage: 'init', reason: 'DNT_OPTED_OUT' });
29
95
  return;
30
96
  }
31
97
  }
@@ -38,6 +104,54 @@ export class ReplayFeature {
38
104
  const { installWebGLPreserve } = await import('./engine/webglPatch.js');
39
105
  this.uninstallWebGL = installWebGLPreserve();
40
106
  }
107
+ /**
108
+ * P3 — ARM WITHOUT BLOCKING THE BOOTSTRAP. Everything below this line is deliberately NOT awaited.
109
+ *
110
+ * `SDKBootstrapper.activate()` runs `await registry.setupAll(ctx)` and only then
111
+ * `await pipeline.start()` — and `setupAll` is a sequential for-loop. So every millisecond replay
112
+ * spends fetching its recorder chunk is a millisecond before the SESSION IS OPENED. A slow chunk
113
+ * therefore does not merely lose replay: it loses the whole visit, including analytics, errors and
114
+ * performance, because nothing has a session to attach to yet.
115
+ *
116
+ * MEASURED, Phase 2 browser matrix: with the recorder chunk stalled 6 s, visits of 3 s and 5 s
117
+ * produced NO SESSION ROW AT ALL — not a session missing replay, no session. At 8 s and 15 s (long
118
+ * enough to outlast the stall) the same scenario reached PLAYBACK_READY, which is what identifies
119
+ * the stall as the cause rather than anything about capture itself.
120
+ *
121
+ * The `setupAll` ordering it relies on cannot simply be reversed: F-VIT documents a dependency on
122
+ * setup running before `pipeline.start`. Making replay's own tail non-blocking fixes the defect
123
+ * without touching that contract or any other feature.
124
+ *
125
+ * The cost of not awaiting is that a rejection no longer reaches `FeatureRegistry.setupAll`, which
126
+ * is what produced `sdk_feature_setup_failed` for replay. `arm()` therefore reports its own failure
127
+ * with a MORE specific reason than the registry could, so attribution improves rather than
128
+ * regresses.
129
+ */
130
+ this.armed = this.arm(ctx, decision);
131
+ }
132
+ /**
133
+ * Load the recorder and start capturing. Runs detached from `setup()` — see the note above.
134
+ *
135
+ * Every failure here is terminal for replay and silent everywhere else, so this method owns its own
136
+ * reporting: nothing upstream is awaiting it to notice a rejection.
137
+ */
138
+ async arm(ctx, decision) {
139
+ try {
140
+ await this.load(ctx, decision);
141
+ }
142
+ catch (err) {
143
+ const message = String(err?.message ?? err).slice(0, 300);
144
+ ctx.trackEvent(FAIL, {
145
+ stage: 'init',
146
+ // Separates "the chunk never arrived" (CSP, 404 after a deploy, chunk-hash skew, dropped
147
+ // request) from "the recorder itself threw" — completely different fixes, and the message is
148
+ // the only signal the three engines agree on.
149
+ reason: isModuleLoadFailure(message) ? 'ENGINE_CHUNK_LOAD_FAILED' : 'RECORDER_INIT_FAILED',
150
+ message,
151
+ });
152
+ }
153
+ }
154
+ async load(ctx, decision) {
41
155
  /**
42
156
  * R3 — WARM the engine chunks concurrently instead of letting them waterfall.
43
157
  *
@@ -81,8 +195,8 @@ export class ReplayFeature {
81
195
  *
82
196
  * When the retry SUCCEEDS it says so. Without that event a retry is an unfalsifiable fix: coverage
83
197
  * might improve and nobody could tell whether this was why. `sdk_replay_recovered` makes it
84
- * measurable, and pairs with `sdk_feature_setup_failed` (which the throw below still produces via
85
- * FeatureRegistry) so both outcomes are visible instead of neither.
198
+ * measurable, and pairs with the `sdk_replay_capture_failed` its caller emits when the second
199
+ * attempt fails too, so both outcomes are visible instead of neither.
86
200
  */
87
201
  let createRecorder;
88
202
  try {
@@ -97,6 +211,38 @@ export class ReplayFeature {
97
211
  });
98
212
  }
99
213
  this.controller = await createRecorder(ctx, decision);
214
+ /**
215
+ * P1b — the ARMED-BUT-SILENT watchdog. This is the case nothing else can see.
216
+ *
217
+ * Every gate above reports by returning or throwing. Once `createRecorder` resolves the feature is
218
+ * live and, as far as any existing signal is concerned, healthy — so a recorder that attaches and
219
+ * then never produces a frame (root never resolved, snapshot threw inside an observer callback,
220
+ * serialization died on a hostile DOM) is invisible on the client AND on the server, because
221
+ * nothing is ever sent. That is the exact residue this phase has to attribute.
222
+ *
223
+ * Tests `captured`, NOT `frames`. `frames` is the segment-buffer occupancy: while streaming, frames
224
+ * leave the buffer as they ship, so a healthy session reports 0 and an earlier revision of this
225
+ * watchdog accused every one of them — measured in the Phase 2 browser matrix, `normal` at 5 s, 8 s
226
+ * and 15 s all reached PLAYBACK_READY while reporting NO_FRAME_AFTER_ARM. A false positive here is
227
+ * worse than no watchdog: it would fill the attribution table this phase exists to build with
228
+ * sessions that captured perfectly.
229
+ *
230
+ * `recording` is still required: a paused or torn-down recorder has a legitimate reason to hold no
231
+ * keyframe and must not be accused either.
232
+ */
233
+ this.watchdog = setTimeout(() => {
234
+ const s = this.controller?.stats();
235
+ if (s?.recording && !s.captured) {
236
+ ctx.trackEvent(FAIL, {
237
+ stage: 'first-frame',
238
+ reason: 'NO_FRAME_AFTER_ARM',
239
+ // Carried so the server can separate "armed and streaming" from "armed and buffering the
240
+ // pre-roll", which is a legitimate zero-frame state under onError mode.
241
+ streaming: s.streaming,
242
+ bucket: s.sampleBucket,
243
+ });
244
+ }
245
+ }, FIRST_FRAME_WATCHDOG_MS);
100
246
  }
101
247
  /** Capture-on-signal flush (promote pre-roll → live). No-op until a recorder is active. */
102
248
  flush() {
@@ -123,6 +269,10 @@ export class ReplayFeature {
123
269
  this.controller?.setPrivacy(policy);
124
270
  }
125
271
  teardown() {
272
+ // Before the controller goes: a fired-after-teardown watchdog would read `stats()` as undefined and
273
+ // report nothing, but the timer would still hold this instance alive until it elapsed.
274
+ clearTimeout(this.watchdog);
275
+ this.watchdog = undefined;
126
276
  this.controller?.teardown();
127
277
  this.controller = undefined;
128
278
  this.uninstallWebGL?.();