@antelopejs/dms-marketing 0.3.3 → 0.4.1

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 (289) hide show
  1. package/README.md +18 -15
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.js +4 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/data-api/funnels.d.ts +2 -0
  6. package/dist/data-api/funnels.js +8 -1
  7. package/dist/data-api/funnels.js.map +1 -1
  8. package/dist/db/models/funnels.model.d.ts +2 -0
  9. package/dist/db/models/funnels.model.js +9 -0
  10. package/dist/db/models/funnels.model.js.map +1 -1
  11. package/dist/db/models/index.d.ts +1 -0
  12. package/dist/db/models/index.js +1 -0
  13. package/dist/db/models/index.js.map +1 -1
  14. package/dist/db/models/marketing_events.model.d.ts +22 -0
  15. package/dist/db/models/marketing_events.model.js +70 -5
  16. package/dist/db/models/marketing_events.model.js.map +1 -1
  17. package/dist/db/models/marketing_preferences.model.d.ts +25 -0
  18. package/dist/db/models/marketing_preferences.model.js +29 -0
  19. package/dist/db/models/marketing_preferences.model.js.map +1 -0
  20. package/dist/db/models/marketing_sessions.model.d.ts +2 -1
  21. package/dist/db/models/marketing_sessions.model.js +11 -1
  22. package/dist/db/models/marketing_sessions.model.js.map +1 -1
  23. package/dist/db/models/page_snapshots.model.d.ts +2 -1
  24. package/dist/db/models/page_snapshots.model.js +7 -7
  25. package/dist/db/models/page_snapshots.model.js.map +1 -1
  26. package/dist/db/models/website_statistics.model.d.ts +4 -1
  27. package/dist/db/models/website_statistics.model.js +15 -1
  28. package/dist/db/models/website_statistics.model.js.map +1 -1
  29. package/dist/db/tables/index.d.ts +1 -0
  30. package/dist/db/tables/index.js +1 -0
  31. package/dist/db/tables/index.js.map +1 -1
  32. package/dist/db/tables/marketing_preferences.table.d.ts +6 -0
  33. package/dist/db/tables/marketing_preferences.table.js +31 -0
  34. package/dist/db/tables/marketing_preferences.table.js.map +1 -0
  35. package/dist/hooks/index.js +8 -4
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/index.js +18 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/pages/acquisition/page.d.ts +9 -0
  40. package/dist/pages/acquisition/page.js +181 -0
  41. package/dist/pages/acquisition/page.js.map +1 -0
  42. package/dist/pages/all.d.ts +5 -1
  43. package/dist/pages/all.js +5 -1
  44. package/dist/pages/all.js.map +1 -1
  45. package/dist/pages/blocks.d.ts +18 -0
  46. package/dist/pages/blocks.js +56 -0
  47. package/dist/pages/blocks.js.map +1 -0
  48. package/dist/pages/funnel/page.d.ts +9 -0
  49. package/dist/pages/funnel/page.js +36 -0
  50. package/dist/pages/funnel/page.js.map +1 -0
  51. package/dist/pages/funnel-builder/page.d.ts +9 -0
  52. package/dist/pages/funnel-builder/page.js +29 -0
  53. package/dist/pages/funnel-builder/page.js.map +1 -0
  54. package/dist/pages/funnels/page.d.ts +1 -1
  55. package/dist/pages/funnels/page.js +200 -3
  56. package/dist/pages/funnels/page.js.map +1 -1
  57. package/dist/pages/index.d.ts +1 -0
  58. package/dist/pages/index.js +1 -0
  59. package/dist/pages/index.js.map +1 -1
  60. package/dist/pages/{campaigns → install}/page.d.ts +3 -3
  61. package/dist/pages/{campaigns → install}/page.js +14 -12
  62. package/dist/pages/install/page.js.map +1 -0
  63. package/dist/pages/module.d.ts +3 -0
  64. package/dist/pages/module.js +30 -3
  65. package/dist/pages/module.js.map +1 -1
  66. package/dist/pages/overview/page.d.ts +1 -1
  67. package/dist/pages/overview/page.js +122 -3
  68. package/dist/pages/overview/page.js.map +1 -1
  69. package/dist/pages/pages/page.d.ts +1 -1
  70. package/dist/pages/pages/page.js +9 -4
  71. package/dist/pages/pages/page.js.map +1 -1
  72. package/dist/pages/quick-actions.d.ts +1 -0
  73. package/dist/pages/quick-actions.js +25 -0
  74. package/dist/pages/quick-actions.js.map +1 -0
  75. package/dist/pages/settings/form.d.ts +2 -1
  76. package/dist/pages/settings/form.js +62 -20
  77. package/dist/pages/settings/form.js.map +1 -1
  78. package/dist/pages/settings/page.d.ts +4 -1
  79. package/dist/pages/settings/page.js +50 -4
  80. package/dist/pages/settings/page.js.map +1 -1
  81. package/dist/pages/websites/page.d.ts +11 -0
  82. package/dist/pages/websites/page.js +62 -0
  83. package/dist/pages/websites/page.js.map +1 -0
  84. package/dist/routes/blocks.d.ts +50 -0
  85. package/dist/routes/blocks.js +269 -0
  86. package/dist/routes/blocks.js.map +1 -0
  87. package/dist/routes/collect.d.ts +1 -2
  88. package/dist/routes/collect.js +19 -12
  89. package/dist/routes/collect.js.map +1 -1
  90. package/dist/routes/context.d.ts +8 -0
  91. package/dist/routes/context.js +54 -0
  92. package/dist/routes/context.js.map +1 -0
  93. package/dist/routes/experiments.js +2 -1
  94. package/dist/routes/experiments.js.map +1 -1
  95. package/dist/routes/funnels.d.ts +21 -3
  96. package/dist/routes/funnels.js +150 -7
  97. package/dist/routes/funnels.js.map +1 -1
  98. package/dist/routes/index.d.ts +2 -0
  99. package/dist/routes/index.js +2 -0
  100. package/dist/routes/index.js.map +1 -1
  101. package/dist/routes/settings.d.ts +6 -1
  102. package/dist/routes/settings.js +65 -4
  103. package/dist/routes/settings.js.map +1 -1
  104. package/dist/routes/snapshot.d.ts +1 -1
  105. package/dist/routes/snapshot.js +24 -25
  106. package/dist/routes/snapshot.js.map +1 -1
  107. package/dist/routes/stats.d.ts +14 -23
  108. package/dist/routes/stats.js +73 -97
  109. package/dist/routes/stats.js.map +1 -1
  110. package/dist/routes/tracker.js +6 -4
  111. package/dist/routes/tracker.js.map +1 -1
  112. package/dist/routes/websites.d.ts +4 -0
  113. package/dist/routes/websites.js +111 -12
  114. package/dist/routes/websites.js.map +1 -1
  115. package/dist/services/acquisition-report.d.ts +62 -0
  116. package/dist/services/acquisition-report.js +181 -0
  117. package/dist/services/acquisition-report.js.map +1 -0
  118. package/dist/services/acquisition.d.ts +9 -3
  119. package/dist/services/acquisition.js +35 -5
  120. package/dist/services/acquisition.js.map +1 -1
  121. package/dist/services/catalog.d.ts +3 -0
  122. package/dist/services/catalog.js +17 -0
  123. package/dist/services/catalog.js.map +1 -0
  124. package/dist/services/collect.d.ts +1 -2
  125. package/dist/services/collect.js +49 -24
  126. package/dist/services/collect.js.map +1 -1
  127. package/dist/services/composed.d.ts +9 -0
  128. package/dist/services/composed.js +62 -0
  129. package/dist/services/composed.js.map +1 -0
  130. package/dist/services/context.d.ts +20 -0
  131. package/dist/services/context.js +76 -0
  132. package/dist/services/context.js.map +1 -0
  133. package/dist/services/dashboard.d.ts +74 -0
  134. package/dist/services/dashboard.js +343 -0
  135. package/dist/services/dashboard.js.map +1 -0
  136. package/dist/services/experiments/results.d.ts +12 -1
  137. package/dist/services/experiments/results.js +19 -2
  138. package/dist/services/experiments/results.js.map +1 -1
  139. package/dist/services/experiments/stats.d.ts +7 -1
  140. package/dist/services/experiments/stats.js +43 -5
  141. package/dist/services/experiments/stats.js.map +1 -1
  142. package/dist/services/funnel-report.d.ts +64 -0
  143. package/dist/services/funnel-report.js +280 -0
  144. package/dist/services/funnel-report.js.map +1 -0
  145. package/dist/services/funnels/compute.d.ts +2 -0
  146. package/dist/services/funnels/compute.js +25 -13
  147. package/dist/services/funnels/compute.js.map +1 -1
  148. package/dist/services/funnels/results.js +4 -1
  149. package/dist/services/funnels/results.js.map +1 -1
  150. package/dist/services/funnels/step-path.d.ts +4 -0
  151. package/dist/services/funnels/step-path.js +47 -0
  152. package/dist/services/funnels/step-path.js.map +1 -0
  153. package/dist/services/funnels-table.d.ts +31 -0
  154. package/dist/services/funnels-table.js +230 -0
  155. package/dist/services/funnels-table.js.map +1 -0
  156. package/dist/services/heatmap.d.ts +15 -3
  157. package/dist/services/heatmap.js +54 -11
  158. package/dist/services/heatmap.js.map +1 -1
  159. package/dist/services/hourly-traffic.d.ts +4 -0
  160. package/dist/services/hourly-traffic.js +35 -0
  161. package/dist/services/hourly-traffic.js.map +1 -0
  162. package/dist/services/origin-guard.d.ts +9 -1
  163. package/dist/services/origin-guard.js +46 -1
  164. package/dist/services/origin-guard.js.map +1 -1
  165. package/dist/services/period.d.ts +26 -1
  166. package/dist/services/period.js +99 -9
  167. package/dist/services/period.js.map +1 -1
  168. package/dist/services/prune.d.ts +6 -0
  169. package/dist/services/prune.js +35 -0
  170. package/dist/services/prune.js.map +1 -1
  171. package/dist/services/request-body.d.ts +1 -2
  172. package/dist/services/request-body.js +10 -36
  173. package/dist/services/request-body.js.map +1 -1
  174. package/dist/services/request-meta.d.ts +0 -1
  175. package/dist/services/request-meta.js +0 -11
  176. package/dist/services/request-meta.js.map +1 -1
  177. package/dist/services/rollups.d.ts +25 -0
  178. package/dist/services/rollups.js +102 -0
  179. package/dist/services/rollups.js.map +1 -0
  180. package/dist/services/sessionize.d.ts +5 -1
  181. package/dist/services/sessionize.js +52 -10
  182. package/dist/services/sessionize.js.map +1 -1
  183. package/dist/services/settings.d.ts +41 -2
  184. package/dist/services/settings.js +219 -6
  185. package/dist/services/settings.js.map +1 -1
  186. package/dist/services/snapshots.d.ts +3 -2
  187. package/dist/services/snapshots.js +110 -9
  188. package/dist/services/snapshots.js.map +1 -1
  189. package/dist/services/tenant-website.js +1 -0
  190. package/dist/services/tenant-website.js.map +1 -1
  191. package/dist/services/url.js +12 -4
  192. package/dist/services/url.js.map +1 -1
  193. package/dist/services/website-hosts.d.ts +15 -0
  194. package/dist/services/website-hosts.js +67 -0
  195. package/dist/services/website-hosts.js.map +1 -0
  196. package/dist/services/website-report.d.ts +40 -0
  197. package/dist/services/website-report.js +84 -0
  198. package/dist/services/website-report.js.map +1 -0
  199. package/dist/types/constants.d.ts +14 -2
  200. package/dist/types/constants.js +17 -6
  201. package/dist/types/constants.js.map +1 -1
  202. package/dist/types/events.d.ts +221 -68
  203. package/dist/types/events.js +60 -11
  204. package/dist/types/events.js.map +1 -1
  205. package/dist/types/snapshots.d.ts +6 -0
  206. package/dist/types/snapshots.js +2 -0
  207. package/dist/types/snapshots.js.map +1 -1
  208. package/dist/types/statistics.d.ts +2 -1
  209. package/dist/types/statistics.js +9 -2
  210. package/dist/types/statistics.js.map +1 -1
  211. package/dist/utils/experiment-type.js +1 -1
  212. package/dist/utils/experiment-type.js.map +1 -1
  213. package/dist/utils/funnel-steps-type.d.ts +21 -3
  214. package/dist/utils/funnel-steps-type.js +7 -2
  215. package/dist/utils/funnel-steps-type.js.map +1 -1
  216. package/frontend-vue/app/components/ChannelsCard.vue +265 -0
  217. package/frontend-vue/app/components/Context.vue +479 -0
  218. package/frontend-vue/app/components/ExperimentInput.vue +3 -1
  219. package/frontend-vue/app/components/ExperimentReport.vue +736 -0
  220. package/frontend-vue/app/components/FirstRun.vue +60 -0
  221. package/frontend-vue/app/components/FunnelBuilder.vue +1595 -0
  222. package/frontend-vue/app/components/FunnelFigure.vue +640 -0
  223. package/frontend-vue/app/components/FunnelReport.vue +532 -0
  224. package/frontend-vue/app/components/FunnelStepsInput.vue +3 -1
  225. package/frontend-vue/app/components/FunnelTemplates.vue +161 -0
  226. package/frontend-vue/app/components/HeatmapCanvas.vue +11 -4
  227. package/frontend-vue/app/components/InstallGuide.vue +753 -0
  228. package/frontend-vue/app/components/PagePreview.vue +889 -416
  229. package/frontend-vue/app/components/PagesExplorer.vue +552 -0
  230. package/frontend-vue/app/components/ScrollDepthOverlay.vue +48 -20
  231. package/frontend-vue/app/components/TopListTabs.vue +317 -0
  232. package/frontend-vue/app/components/TrackerSnippet.vue +50 -16
  233. package/frontend-vue/app/components/VariationsInput.vue +15 -5
  234. package/frontend-vue/app/components/WebsitesGrid.vue +799 -0
  235. package/frontend-vue/app/composables/useContextLink.ts +16 -0
  236. package/frontend-vue/app/composables/useExperiment.ts +1 -2
  237. package/frontend-vue/app/composables/useFunnelSteps.ts +1 -2
  238. package/frontend-vue/app/composables/useLatestRequest.ts +2 -4
  239. package/frontend-vue/app/composables/useMarketingApi.ts +298 -133
  240. package/frontend-vue/app/composables/useMarketingContext.ts +55 -0
  241. package/frontend-vue/app/composables/useMarketingRoutes.ts +80 -27
  242. package/frontend-vue/app/composables/useSnapshotFrame.ts +26 -4
  243. package/frontend-vue/app/constants.ts +22 -0
  244. package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +2 -1
  245. package/frontend-vue/app/utils/channels.ts +37 -0
  246. package/frontend-vue/app/utils/format.ts +132 -0
  247. package/frontend-vue/app/utils/funnel-draft.ts +264 -0
  248. package/frontend-vue/app/utils/heatmap.ts +82 -0
  249. package/frontend-vue/app/utils/heatmapPalette.ts +10 -0
  250. package/frontend-vue/app/utils/host-conflict.ts +48 -0
  251. package/frontend-vue/app/utils/sample-size.ts +17 -0
  252. package/frontend-vue/app/utils/snapshotViewportUnits.ts +15 -5
  253. package/frontend-vue/app/utils/snippet.ts +53 -0
  254. package/frontend-vue/app/utils/step-path.ts +32 -0
  255. package/frontend-vue/dms.frontend.ts +4 -3
  256. package/frontend-vue/i18n/locales/marketing-en-GB.json +1083 -316
  257. package/frontend-vue/i18n/locales/marketing-fr-FR.json +1087 -320
  258. package/frontend-vue/package.json +1 -2
  259. package/frontend-vue/pnpm-lock.yaml +51 -19
  260. package/frontend-vue/tests/drafts.test.ts +3 -3
  261. package/frontend-vue/tests/format.test.ts +73 -0
  262. package/frontend-vue/tests/funnel-draft.test.ts +194 -0
  263. package/frontend-vue/tests/heatmap.test.ts +32 -0
  264. package/frontend-vue/tests/i18n.test.ts +87 -4
  265. package/frontend-vue/tests/latest-request.test.ts +6 -3
  266. package/frontend-vue/tests/locale-typography.test.ts +115 -0
  267. package/frontend-vue/tests/registration.test.ts +17 -11
  268. package/frontend-vue/tests/routes.test.ts +67 -7
  269. package/frontend-vue/tests/server-parity.test.ts +77 -0
  270. package/frontend-vue/tests/stubs/frontend-module.ts +18 -3
  271. package/frontend-vue/vitest.config.ts +3 -1
  272. package/package.json +10 -8
  273. package/static/snapshot.js +46 -16
  274. package/static/tracker.js +139 -48
  275. package/dist/pages/campaigns/page.js.map +0 -1
  276. package/frontend-vue/app/components/CampaignsView.vue +0 -330
  277. package/frontend-vue/app/components/FunnelDetail.vue +0 -1026
  278. package/frontend-vue/app/components/FunnelsView.vue +0 -493
  279. package/frontend-vue/app/components/NoWebsiteState.vue +0 -18
  280. package/frontend-vue/app/components/OverviewView.vue +0 -493
  281. package/frontend-vue/app/components/PagesView.vue +0 -391
  282. package/frontend-vue/app/components/TextKpiCard.vue +0 -28
  283. package/frontend-vue/app/components/TopListTabsCard.vue +0 -162
  284. package/frontend-vue/app/composables/useChildId.ts +0 -13
  285. package/frontend-vue/app/composables/useMarketingPeriod.ts +0 -65
  286. package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +0 -69
  287. package/frontend-vue/app/composables/useMarketingWebsites.ts +0 -80
  288. package/frontend-vue/app/utils/dmsComponent.ts +0 -18
  289. package/frontend-vue/tests/period.test.ts +0 -35
@@ -11,9 +11,12 @@
11
11
  * Privacy, unconditionally: no form value, hidden input, script, event
12
12
  * handler or framed document leaves the page. `contenteditable` text is
13
13
  * masked, as is everything under `data-dms-marketing-mask` (or the
14
- * `dms-marketing-mask` class); `data-dms-marketing-block` (or the
15
- * `dms-marketing-block` class) replaces an element with an empty box of its
16
- * size. `maskText` masks every text node.
14
+ * `dms-marketing-mask` class) — text, the attributes that carry text
15
+ * (title, alt, labels, placeholders, data-*) and the path of its links;
16
+ * `data-dms-marketing-block` (or the `dms-marketing-block` class) replaces
17
+ * an element with an empty box of its size. `maskText` masks every text
18
+ * node and text attribute of the page; data-* and link paths stay, since
19
+ * styles select on them.
17
20
  */
18
21
  (() => {
19
22
  const CONFIG_GLOBAL = "__dmsMarketingSnapshotConfig";
@@ -81,6 +84,17 @@
81
84
  "background",
82
85
  ]);
83
86
  const LINK_TAGS = new Set(["a", "area"]);
87
+ /** Attributes that carry text a visitor could read, masked with it. */
88
+ const TEXT_ATTRIBUTES = new Set([
89
+ "title",
90
+ "alt",
91
+ "label",
92
+ "placeholder",
93
+ "aria-label",
94
+ "aria-description",
95
+ "aria-placeholder",
96
+ "aria-valuetext",
97
+ ]);
84
98
  const OPAQUE_URL_PATTERN = /^(#|data:|blob:|about:)/i;
85
99
  const SCRIPT_URL_PATTERN = /^javascript:/i;
86
100
  const CSS_URL_PATTERN = /url\(\s*(['"]?)([^'")]*)\1\s*\)/g;
@@ -93,10 +107,12 @@
93
107
  }
94
108
 
95
109
  const config = readGlobal(CONFIG_GLOBAL);
110
+ const maskAllText = config?.maskText === true;
96
111
  const context = readGlobal(CONTEXT_GLOBAL);
97
112
  if (
98
113
  !config ||
99
114
  !context ||
115
+ typeof config.token !== "string" ||
100
116
  typeof context.endpoint !== "string" ||
101
117
  window.top !== window
102
118
  ) {
@@ -144,10 +160,14 @@
144
160
  }
145
161
 
146
162
  /** Links keep their target, never a query or fragment: tokens and
147
- * addresses ride there, and nothing in the backdrop is clickable. */
148
- function withoutQuery(url) {
163
+ * addresses ride there, and nothing in the backdrop is clickable. A masked
164
+ * link keeps its origin alone — a profile path names its person. */
165
+ function withoutQuery(url, masked) {
149
166
  try {
150
167
  const parsed = new URL(url);
168
+ if (masked) {
169
+ return `${parsed.origin}/`;
170
+ }
151
171
  parsed.search = "";
152
172
  parsed.hash = "";
153
173
  return parsed.href;
@@ -269,7 +289,7 @@
269
289
  return element.dataset.src || current || value;
270
290
  }
271
291
 
272
- function serializeAttributes(element) {
292
+ function serializeAttributes(element, masked) {
273
293
  const tag = element.localName;
274
294
  let out = "";
275
295
  for (const attribute of element.attributes) {
@@ -278,6 +298,14 @@
278
298
  if (name.startsWith("on") || DROPPED_ATTRIBUTES.has(name)) {
279
299
  continue;
280
300
  }
301
+ if (
302
+ ((masked || maskAllText) && TEXT_ATTRIBUTES.has(name)) ||
303
+ (masked &&
304
+ name.startsWith("data-") &&
305
+ !name.startsWith(`data-${MASK_MARKER}`))
306
+ ) {
307
+ value = maskText(value);
308
+ }
281
309
  if (name === "value" && (tag === "input" || tag === "textarea")) {
282
310
  continue;
283
311
  }
@@ -309,7 +337,7 @@
309
337
  if (absolute === null) {
310
338
  continue;
311
339
  }
312
- value = LINK_TAGS.has(tag) ? withoutQuery(absolute) : absolute;
340
+ value = LINK_TAGS.has(tag) ? withoutQuery(absolute, masked) : absolute;
313
341
  }
314
342
  out += ` ${name}="${escapeAttribute(value)}"`;
315
343
  }
@@ -334,7 +362,7 @@
334
362
  return `<${tag}${attributes} style="${size};overflow:hidden"></${tag}>`;
335
363
  }
336
364
 
337
- function serializeCanvas(element) {
365
+ function serializeCanvas(element, masked) {
338
366
  let dataUrl = "";
339
367
  try {
340
368
  dataUrl = element.toDataURL();
@@ -342,9 +370,9 @@
342
370
  // Tainted by cross-origin drawing: the box stays, the pixels do not.
343
371
  }
344
372
  if (dataUrl && dataUrl.length <= MAX_CANVAS_DATA_URL_LENGTH) {
345
- return `<img${serializeAttributes(element)} src="${dataUrl}">`;
373
+ return `<img${serializeAttributes(element, masked)} src="${dataUrl}">`;
346
374
  }
347
- return `<canvas${serializeAttributes(element)}></canvas>`;
375
+ return `<canvas${serializeAttributes(element, masked)}></canvas>`;
348
376
  }
349
377
 
350
378
  function serializeChildren(node, masked) {
@@ -353,7 +381,8 @@
353
381
  if (child.nodeType === Node.ELEMENT_NODE) {
354
382
  out += serializeElement(child, masked);
355
383
  } else if (child.nodeType === Node.TEXT_NODE) {
356
- const text = masked ? maskText(child.data) : child.data;
384
+ const text =
385
+ masked || maskAllText ? maskText(child.data) : child.data;
357
386
  out += escapeText(text);
358
387
  }
359
388
  }
@@ -377,12 +406,12 @@
377
406
  if (isMarked(element, BLOCK_MARKER)) {
378
407
  return serializeBlocked(element);
379
408
  }
380
- if (tag === "canvas") {
381
- return serializeCanvas(element);
382
- }
383
409
  const mask =
384
410
  masked || isMarked(element, MASK_MARKER) || element.isContentEditable;
385
- let out = `<${tag}${serializeAttributes(element)}>`;
411
+ if (tag === "canvas") {
412
+ return serializeCanvas(element, mask);
413
+ }
414
+ let out = `<${tag}${serializeAttributes(element, mask)}>`;
386
415
  if (VOID_TAGS.has(tag)) {
387
416
  return out;
388
417
  }
@@ -403,7 +432,7 @@
403
432
  }
404
433
 
405
434
  function serializeDocument() {
406
- return `<!DOCTYPE html>${serializeElement(document.documentElement, config.maskText === true)}`;
435
+ return `<!DOCTYPE html>${serializeElement(document.documentElement, false)}`;
407
436
  }
408
437
 
409
438
  // --- Timing ---------------------------------------------------------------
@@ -466,6 +495,7 @@
466
495
  const root = document.documentElement;
467
496
  const page = {
468
497
  website: context.website,
498
+ token: config.token,
469
499
  url: (location.origin + location.pathname + location.search).slice(
470
500
  0,
471
501
  MAX_URL_LENGTH,
package/static/tracker.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * Optional attributes:
9
9
  * data-host-url override the collect origin (defaults to the script origin)
10
10
  * data-heatmap-sample 0-1 click/scroll sampling rate; overrides the module setting
11
- * data-do-not-track "false" to ignore the browser DNT signal
11
+ * data-do-not-track "false" to ignore the browser DNT and GPC signals
12
12
  *
13
13
  * Cookieless by design: no identifier is generated or stored client-side —
14
14
  * the backend derives an anonymous visitor hash per request. Payload shape
@@ -36,12 +36,17 @@
36
36
  const CONFIG_GLOBAL = "__dmsMarketingTrackerConfig";
37
37
  /** Global the capture script (snapshot.js) reads its page context from. */
38
38
  const SNAPSHOT_CONTEXT_GLOBAL = "__dmsMarketingSnapshotContext";
39
+ /** Set by the first tracker that starts collecting on the page. */
40
+ const INSTALLED_GLOBAL = "__dmsMarketingInstalled";
39
41
 
40
42
  const DEFAULT_SAMPLE_RATE = 0.1;
41
43
  const FLUSH_INTERVAL_MS = 5000;
42
44
  const MAX_BATCH = 25;
43
- /** Mirrors the backend MAX_URL_LENGTH: a longer field fails validation. */
45
+ /** Mirror the backend caps (MAX_URL_LENGTH, MAX_TITLE_LENGTH,
46
+ * MAX_EVENT_NAME_LENGTH): cut here, nothing is lost to validation. */
44
47
  const MAX_URL_LENGTH = 500;
48
+ const MAX_TITLE_LENGTH = 200;
49
+ const MAX_EVENT_NAME_LENGTH = 50;
45
50
  const MAX_SELECTOR_LENGTH = 120;
46
51
  const SELECTOR_DEPTH = 3;
47
52
  const COORDINATE_PRECISION = 4;
@@ -51,6 +56,11 @@
51
56
  const SNAPSHOT_DELAY_MS = 3000;
52
57
  /** SPA navigations may ask again, this many times per page load. */
53
58
  const MAX_SNAPSHOTS_PER_LOAD = 3;
59
+ /** After a SPA navigation, time for the new view to render before its
60
+ * unscrolled depth is measured. */
61
+ const SCROLL_MEASURE_DELAY_MS = 1000;
62
+ /** Where a keyboard-activated click is anchored inside its element. */
63
+ const ELEMENT_CENTER = 0.5;
54
64
 
55
65
  // --- Public API -----------------------------------------------------------
56
66
 
@@ -72,11 +82,6 @@
72
82
 
73
83
  // --- Configuration --------------------------------------------------------
74
84
 
75
- const script = document.currentScript;
76
- if (!script) {
77
- return;
78
- }
79
-
80
85
  /** Read once and removed: the injected settings are not a page-facing API. */
81
86
  function servedConfig() {
82
87
  const injected = window[CONFIG_GLOBAL];
@@ -84,6 +89,21 @@
84
89
  return injected && typeof injected === "object" ? injected : {};
85
90
  }
86
91
 
92
+ const config = servedConfig();
93
+
94
+ // Collection paused (Settings): the page API without the collection, so
95
+ // `variation()` answers the control. A page already running the tracker
96
+ // keeps it.
97
+ if (config.paused === true) {
98
+ window.dmsMarketing = window.dmsMarketing || NOOP_API;
99
+ return;
100
+ }
101
+
102
+ const script = document.currentScript;
103
+ if (!script) {
104
+ return;
105
+ }
106
+
87
107
  function firstFiniteNumber(values) {
88
108
  return values.find((value) => Number.isFinite(value));
89
109
  }
@@ -98,16 +118,27 @@
98
118
  return Math.min(Math.max(rate, 0), 1);
99
119
  }
100
120
 
101
- const config = servedConfig();
102
121
  const websiteId = script.getAttribute("data-website-id");
103
122
  if (!websiteId) {
104
123
  publishApi();
105
124
  return;
106
125
  }
107
126
 
127
+ // A tag included twice (theme and tag manager) would count everything twice.
128
+ if (window[INSTALLED_GLOBAL]) {
129
+ console.warn("[dms-marketing] tracker included twice; ignoring the copy.");
130
+ return;
131
+ }
132
+ window[INSTALLED_GLOBAL] = true;
133
+
134
+ /** Global Privacy Control is the opt-out signal browsers ship now that DNT
135
+ * is deprecated; both are honoured alike. */
108
136
  const respectDnt = script.getAttribute("data-do-not-track") !== "false";
109
- const dntEnabled = navigator.doNotTrack === "1" || window.doNotTrack === "1";
110
- if (respectDnt && dntEnabled) {
137
+ const optedOut =
138
+ navigator.doNotTrack === "1" ||
139
+ window.doNotTrack === "1" ||
140
+ navigator.globalPrivacyControl === true;
141
+ if (respectDnt && optedOut) {
111
142
  publishApi();
112
143
  return;
113
144
  }
@@ -135,7 +166,7 @@
135
166
  const base = () => ({
136
167
  url: currentUrl(),
137
168
  referrer: document.referrer.slice(0, MAX_URL_LENGTH) || undefined,
138
- title: document.title || undefined,
169
+ title: document.title.slice(0, MAX_TITLE_LENGTH) || undefined,
139
170
  screen: `${screen.width}x${screen.height}`,
140
171
  language: navigator.language || undefined,
141
172
  at: Date.now(),
@@ -179,7 +210,8 @@
179
210
  * navigation rewrites `location` before the depth of the page being left
180
211
  * is flushed. */
181
212
  let scrolledUrl = null;
182
- let maxScrollDepth = 0;
213
+ /** -1 until the page has been measured once. */
214
+ let maxScrollDepth = -1;
183
215
 
184
216
  function trackPageview() {
185
217
  if (location.pathname === lastPath) {
@@ -188,17 +220,34 @@
188
220
  flushScrollDepth();
189
221
  lastPath = location.pathname;
190
222
  scrolledUrl = currentUrl();
191
- maxScrollDepth = 0;
192
- enqueue(Object.assign(base(), { kind: "pageview" }));
223
+ maxScrollDepth = -1;
224
+ // The rate it applies, for the server's "recorded on N % of page loads":
225
+ // counting scroll events against pageviews undercounts the views left
226
+ // before their depth is sent.
227
+ enqueue(
228
+ Object.assign(base(), { kind: "pageview", data: { sample: sampleRate } }),
229
+ );
230
+ scheduleScrollMeasure();
193
231
  scheduleSnapshot();
194
232
  }
195
233
 
196
- const originalPushState = history.pushState;
197
- history.pushState = function pushState(...args) {
198
- originalPushState.apply(this, args);
199
- trackPageview();
200
- };
234
+ /** pushState and replaceState (a client-side redirect lands on a page
235
+ * too); a same-path call — a query or hash update — is no navigation. */
236
+ for (const method of ["pushState", "replaceState"]) {
237
+ const original = history[method];
238
+ history[method] = function navigate(...args) {
239
+ original.apply(this, args);
240
+ trackPageview();
241
+ };
242
+ }
201
243
  window.addEventListener("popstate", trackPageview);
244
+ // A page restored from the back/forward cache runs no script again.
245
+ window.addEventListener("pageshow", (event) => {
246
+ if (event.persisted) {
247
+ lastPath = null;
248
+ trackPageview();
249
+ }
250
+ });
202
251
 
203
252
  // --- Scroll depth (max reached per page, flushed on leave) ----------------
204
253
 
@@ -215,19 +264,30 @@
215
264
  );
216
265
  }
217
266
 
218
- window.addEventListener(
219
- "scroll",
220
- () => {
221
- const depth = currentScrollDepth();
222
- if (depth > maxScrollDepth) {
223
- maxScrollDepth = depth;
224
- }
225
- },
226
- { passive: true },
227
- );
267
+ function measureScrollDepth() {
268
+ const depth = currentScrollDepth();
269
+ if (depth > maxScrollDepth) {
270
+ maxScrollDepth = depth;
271
+ }
272
+ }
273
+
274
+ window.addEventListener("scroll", measureScrollDepth, { passive: true });
275
+
276
+ /**
277
+ * Every sampled view reports a depth, scrolled or not: what shows without
278
+ * scrolling is reached too (all of a page shorter than the window), and a
279
+ * view that never scrolls belongs in the denominator.
280
+ */
281
+ function scheduleScrollMeasure() {
282
+ if (document.readyState === "complete") {
283
+ setTimeout(measureScrollDepth, SCROLL_MEASURE_DELAY_MS);
284
+ return;
285
+ }
286
+ window.addEventListener("load", measureScrollDepth, { once: true });
287
+ }
228
288
 
229
289
  function flushScrollDepth() {
230
- if (!heatmapSampled || maxScrollDepth <= 0) {
290
+ if (!heatmapSampled || maxScrollDepth < 0) {
231
291
  return;
232
292
  }
233
293
  enqueue(
@@ -237,7 +297,7 @@
237
297
  data: { depth: maxScrollDepth },
238
298
  }),
239
299
  );
240
- maxScrollDepth = 0;
300
+ maxScrollDepth = -1;
241
301
  }
242
302
 
243
303
  // --- Click capture (sampled, viewport-normalized) -------------------------
@@ -257,19 +317,16 @@
257
317
  return parts.join(">").slice(0, MAX_SELECTOR_LENGTH);
258
318
  }
259
319
 
260
- function normalized(value, total) {
261
- if (total <= 0) {
262
- return 0;
263
- }
264
- return Number(Math.min(value / total, 1).toFixed(COORDINATE_PRECISION));
265
- }
266
-
267
320
  function rounded(value) {
268
321
  return Number(
269
322
  Math.min(Math.max(value, 0), 1).toFixed(COORDINATE_PRECISION),
270
323
  );
271
324
  }
272
325
 
326
+ function normalized(value, total) {
327
+ return total <= 0 ? 0 : rounded(value / total);
328
+ }
329
+
273
330
  /**
274
331
  * Where the click landed inside the element it hit, so the point survives
275
332
  * being read back at another window size. Document fractions do not: they
@@ -284,7 +341,7 @@
284
341
  * several elements, and the embedder resolving it would otherwise land on
285
342
  * whichever comes first in the document.
286
343
  */
287
- function elementAnchor(target, event) {
344
+ function elementAnchor(target, point) {
288
345
  if (target === document.body || target === document.documentElement) {
289
346
  return null;
290
347
  }
@@ -310,16 +367,37 @@
310
367
  }
311
368
  return {
312
369
  selector: selector,
313
- ox: rounded((event.clientX - box.left) / box.width),
314
- oy: rounded((event.clientY - box.top) / box.height),
370
+ ox: rounded((point.clientX - box.left) / box.width),
371
+ oy: rounded((point.clientY - box.top) / box.height),
315
372
  nth: nth,
316
373
  };
317
374
  }
318
375
 
376
+ /**
377
+ * Where the click happened. A click from the keyboard (Enter, Space) has no
378
+ * pointer — detail 0, coordinates 0,0 — and is placed at the centre of the
379
+ * element it activated instead of the window's corner.
380
+ */
381
+ function clickPoint(target, event) {
382
+ if (event.detail !== 0 || event.clientX !== 0 || event.clientY !== 0) {
383
+ return event;
384
+ }
385
+ const box = target.getBoundingClientRect();
386
+ const clientX = box.left + box.width * ELEMENT_CENTER;
387
+ const clientY = box.top + box.height * ELEMENT_CENTER;
388
+ return {
389
+ clientX: clientX,
390
+ clientY: clientY,
391
+ pageX: clientX + scrollX,
392
+ pageY: clientY + scrollY,
393
+ };
394
+ }
395
+
319
396
  document.addEventListener(
320
397
  "click",
321
398
  (event) => {
322
- if (!heatmapSampled) {
399
+ // Script-dispatched clicks (element.click()) are no visitor's.
400
+ if (!heatmapSampled || !event.isTrusted) {
323
401
  return;
324
402
  }
325
403
  const target = event.target;
@@ -327,18 +405,19 @@
327
405
  return;
328
406
  }
329
407
  const doc = document.documentElement;
408
+ const point = clickPoint(target, event);
330
409
  enqueue(
331
410
  Object.assign(base(), {
332
411
  kind: "click",
333
412
  data: Object.assign(
334
413
  {
335
- x: normalized(event.clientX, innerWidth),
336
- y: normalized(event.clientY, innerHeight),
337
- dx: normalized(event.pageX, doc.scrollWidth),
338
- dy: normalized(event.pageY, doc.scrollHeight),
414
+ x: normalized(point.clientX, innerWidth),
415
+ y: normalized(point.clientY, innerHeight),
416
+ dx: normalized(point.pageX, doc.scrollWidth),
417
+ dy: normalized(point.pageY, doc.scrollHeight),
339
418
  selector: shortSelector(target),
340
419
  },
341
- elementAnchor(target, event),
420
+ elementAnchor(target, point),
342
421
  ),
343
422
  }),
344
423
  );
@@ -369,6 +448,9 @@
369
448
  return;
370
449
  }
371
450
  snapshotsRequested++;
451
+ // The upload is only accepted for the page the visit's last pageview
452
+ // named: that pageview must be on its way before the capture is.
453
+ flush();
372
454
  const path = location.pathname;
373
455
  window[SNAPSHOT_CONTEXT_GLOBAL] = {
374
456
  website: websiteId,
@@ -466,7 +548,16 @@
466
548
 
467
549
  publishApi({
468
550
  track(name, data) {
469
- enqueue(Object.assign(base(), { kind: "custom", name, data }));
551
+ enqueue(
552
+ Object.assign(base(), {
553
+ kind: "custom",
554
+ name:
555
+ typeof name === "string"
556
+ ? name.slice(0, MAX_EVENT_NAME_LENGTH)
557
+ : name,
558
+ data,
559
+ }),
560
+ );
470
561
  },
471
562
  /** Assigned variation of a running experiment, or null. Reports the
472
563
  * exposure, so only call it where the variation is actually applied. */
@@ -1 +0,0 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../../../src/pages/campaigns/page.ts"],"names":[],"mappings":";;;;;;;;;AAAA,kEAAwE;AACxE,oEAAuE;AACvE,yDAA8E;AAC9E,iDAAwD;AASjD,IAAM,sBAAsB,GAA5B,MAAM,sBAAuB,SAAQ,IAAA,qBAAc,EACxD,WAAW,EACX;IACE,WAAW,EAAE,iCAAiC;IAC9C,WAAW,EAAE,uCAAuC;IACpD,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,+BAAmB;IAC3B,KAAK,EAAE,CAAC;CACT,EACD,IAAA,uBAAa,EAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CACnC;IACC,MAAM,CAAC,OAAO,GAAG,IAAA,wBAAe,EAAC,2BAA2B,CAAC,CAAC;;AAXnD,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,mBAAY,GAAE;GACF,sBAAsB,CAYlC"}