@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.
- package/README.md +18 -15
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/funnels.d.ts +2 -0
- package/dist/data-api/funnels.js +8 -1
- package/dist/data-api/funnels.js.map +1 -1
- package/dist/db/models/funnels.model.d.ts +2 -0
- package/dist/db/models/funnels.model.js +9 -0
- package/dist/db/models/funnels.model.js.map +1 -1
- package/dist/db/models/index.d.ts +1 -0
- package/dist/db/models/index.js +1 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/marketing_events.model.d.ts +22 -0
- package/dist/db/models/marketing_events.model.js +70 -5
- package/dist/db/models/marketing_events.model.js.map +1 -1
- package/dist/db/models/marketing_preferences.model.d.ts +25 -0
- package/dist/db/models/marketing_preferences.model.js +29 -0
- package/dist/db/models/marketing_preferences.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +2 -1
- package/dist/db/models/marketing_sessions.model.js +11 -1
- package/dist/db/models/marketing_sessions.model.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -1
- package/dist/db/models/page_snapshots.model.js +7 -7
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/website_statistics.model.d.ts +4 -1
- package/dist/db/models/website_statistics.model.js +15 -1
- package/dist/db/models/website_statistics.model.js.map +1 -1
- package/dist/db/tables/index.d.ts +1 -0
- package/dist/db/tables/index.js +1 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/marketing_preferences.table.d.ts +6 -0
- package/dist/db/tables/marketing_preferences.table.js +31 -0
- package/dist/db/tables/marketing_preferences.table.js.map +1 -0
- package/dist/hooks/index.js +8 -4
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +9 -0
- package/dist/pages/acquisition/page.js +181 -0
- package/dist/pages/acquisition/page.js.map +1 -0
- package/dist/pages/all.d.ts +5 -1
- package/dist/pages/all.js +5 -1
- package/dist/pages/all.js.map +1 -1
- package/dist/pages/blocks.d.ts +18 -0
- package/dist/pages/blocks.js +56 -0
- package/dist/pages/blocks.js.map +1 -0
- package/dist/pages/funnel/page.d.ts +9 -0
- package/dist/pages/funnel/page.js +36 -0
- package/dist/pages/funnel/page.js.map +1 -0
- package/dist/pages/funnel-builder/page.d.ts +9 -0
- package/dist/pages/funnel-builder/page.js +29 -0
- package/dist/pages/funnel-builder/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +1 -1
- package/dist/pages/funnels/page.js +200 -3
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/{campaigns → install}/page.d.ts +3 -3
- package/dist/pages/{campaigns → install}/page.js +14 -12
- package/dist/pages/install/page.js.map +1 -0
- package/dist/pages/module.d.ts +3 -0
- package/dist/pages/module.js +30 -3
- package/dist/pages/module.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -1
- package/dist/pages/overview/page.js +122 -3
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -1
- package/dist/pages/pages/page.js +9 -4
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/quick-actions.d.ts +1 -0
- package/dist/pages/quick-actions.js +25 -0
- package/dist/pages/quick-actions.js.map +1 -0
- package/dist/pages/settings/form.d.ts +2 -1
- package/dist/pages/settings/form.js +62 -20
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/settings/page.d.ts +4 -1
- package/dist/pages/settings/page.js +50 -4
- package/dist/pages/settings/page.js.map +1 -1
- package/dist/pages/websites/page.d.ts +11 -0
- package/dist/pages/websites/page.js +62 -0
- package/dist/pages/websites/page.js.map +1 -0
- package/dist/routes/blocks.d.ts +50 -0
- package/dist/routes/blocks.js +269 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/collect.d.ts +1 -2
- package/dist/routes/collect.js +19 -12
- package/dist/routes/collect.js.map +1 -1
- package/dist/routes/context.d.ts +8 -0
- package/dist/routes/context.js +54 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/experiments.js +2 -1
- package/dist/routes/experiments.js.map +1 -1
- package/dist/routes/funnels.d.ts +21 -3
- package/dist/routes/funnels.js +150 -7
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/settings.d.ts +6 -1
- package/dist/routes/settings.js +65 -4
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +24 -25
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +14 -23
- package/dist/routes/stats.js +73 -97
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/tracker.js +6 -4
- package/dist/routes/tracker.js.map +1 -1
- package/dist/routes/websites.d.ts +4 -0
- package/dist/routes/websites.js +111 -12
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +62 -0
- package/dist/services/acquisition-report.js +181 -0
- package/dist/services/acquisition-report.js.map +1 -0
- package/dist/services/acquisition.d.ts +9 -3
- package/dist/services/acquisition.js +35 -5
- package/dist/services/acquisition.js.map +1 -1
- package/dist/services/catalog.d.ts +3 -0
- package/dist/services/catalog.js +17 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/collect.d.ts +1 -2
- package/dist/services/collect.js +49 -24
- package/dist/services/collect.js.map +1 -1
- package/dist/services/composed.d.ts +9 -0
- package/dist/services/composed.js +62 -0
- package/dist/services/composed.js.map +1 -0
- package/dist/services/context.d.ts +20 -0
- package/dist/services/context.js +76 -0
- package/dist/services/context.js.map +1 -0
- package/dist/services/dashboard.d.ts +74 -0
- package/dist/services/dashboard.js +343 -0
- package/dist/services/dashboard.js.map +1 -0
- package/dist/services/experiments/results.d.ts +12 -1
- package/dist/services/experiments/results.js +19 -2
- package/dist/services/experiments/results.js.map +1 -1
- package/dist/services/experiments/stats.d.ts +7 -1
- package/dist/services/experiments/stats.js +43 -5
- package/dist/services/experiments/stats.js.map +1 -1
- package/dist/services/funnel-report.d.ts +64 -0
- package/dist/services/funnel-report.js +280 -0
- package/dist/services/funnel-report.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +2 -0
- package/dist/services/funnels/compute.js +25 -13
- package/dist/services/funnels/compute.js.map +1 -1
- package/dist/services/funnels/results.js +4 -1
- package/dist/services/funnels/results.js.map +1 -1
- package/dist/services/funnels/step-path.d.ts +4 -0
- package/dist/services/funnels/step-path.js +47 -0
- package/dist/services/funnels/step-path.js.map +1 -0
- package/dist/services/funnels-table.d.ts +31 -0
- package/dist/services/funnels-table.js +230 -0
- package/dist/services/funnels-table.js.map +1 -0
- package/dist/services/heatmap.d.ts +15 -3
- package/dist/services/heatmap.js +54 -11
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/hourly-traffic.d.ts +4 -0
- package/dist/services/hourly-traffic.js +35 -0
- package/dist/services/hourly-traffic.js.map +1 -0
- package/dist/services/origin-guard.d.ts +9 -1
- package/dist/services/origin-guard.js +46 -1
- package/dist/services/origin-guard.js.map +1 -1
- package/dist/services/period.d.ts +26 -1
- package/dist/services/period.js +99 -9
- package/dist/services/period.js.map +1 -1
- package/dist/services/prune.d.ts +6 -0
- package/dist/services/prune.js +35 -0
- package/dist/services/prune.js.map +1 -1
- package/dist/services/request-body.d.ts +1 -2
- package/dist/services/request-body.js +10 -36
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/request-meta.d.ts +0 -1
- package/dist/services/request-meta.js +0 -11
- package/dist/services/request-meta.js.map +1 -1
- package/dist/services/rollups.d.ts +25 -0
- package/dist/services/rollups.js +102 -0
- package/dist/services/rollups.js.map +1 -0
- package/dist/services/sessionize.d.ts +5 -1
- package/dist/services/sessionize.js +52 -10
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/settings.d.ts +41 -2
- package/dist/services/settings.js +219 -6
- package/dist/services/settings.js.map +1 -1
- package/dist/services/snapshots.d.ts +3 -2
- package/dist/services/snapshots.js +110 -9
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.js +1 -0
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/url.js +12 -4
- package/dist/services/url.js.map +1 -1
- package/dist/services/website-hosts.d.ts +15 -0
- package/dist/services/website-hosts.js +67 -0
- package/dist/services/website-hosts.js.map +1 -0
- package/dist/services/website-report.d.ts +40 -0
- package/dist/services/website-report.js +84 -0
- package/dist/services/website-report.js.map +1 -0
- package/dist/types/constants.d.ts +14 -2
- package/dist/types/constants.js +17 -6
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +221 -68
- package/dist/types/events.js +60 -11
- package/dist/types/events.js.map +1 -1
- package/dist/types/snapshots.d.ts +6 -0
- package/dist/types/snapshots.js +2 -0
- package/dist/types/snapshots.js.map +1 -1
- package/dist/types/statistics.d.ts +2 -1
- package/dist/types/statistics.js +9 -2
- package/dist/types/statistics.js.map +1 -1
- package/dist/utils/experiment-type.js +1 -1
- package/dist/utils/experiment-type.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +21 -3
- package/dist/utils/funnel-steps-type.js +7 -2
- package/dist/utils/funnel-steps-type.js.map +1 -1
- package/frontend-vue/app/components/ChannelsCard.vue +265 -0
- package/frontend-vue/app/components/Context.vue +479 -0
- package/frontend-vue/app/components/ExperimentInput.vue +3 -1
- package/frontend-vue/app/components/ExperimentReport.vue +736 -0
- package/frontend-vue/app/components/FirstRun.vue +60 -0
- package/frontend-vue/app/components/FunnelBuilder.vue +1595 -0
- package/frontend-vue/app/components/FunnelFigure.vue +640 -0
- package/frontend-vue/app/components/FunnelReport.vue +532 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +3 -1
- package/frontend-vue/app/components/FunnelTemplates.vue +161 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +11 -4
- package/frontend-vue/app/components/InstallGuide.vue +753 -0
- package/frontend-vue/app/components/PagePreview.vue +889 -416
- package/frontend-vue/app/components/PagesExplorer.vue +552 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +48 -20
- package/frontend-vue/app/components/TopListTabs.vue +317 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +50 -16
- package/frontend-vue/app/components/VariationsInput.vue +15 -5
- package/frontend-vue/app/components/WebsitesGrid.vue +799 -0
- package/frontend-vue/app/composables/useContextLink.ts +16 -0
- package/frontend-vue/app/composables/useExperiment.ts +1 -2
- package/frontend-vue/app/composables/useFunnelSteps.ts +1 -2
- package/frontend-vue/app/composables/useLatestRequest.ts +2 -4
- package/frontend-vue/app/composables/useMarketingApi.ts +298 -133
- package/frontend-vue/app/composables/useMarketingContext.ts +55 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +80 -27
- package/frontend-vue/app/composables/useSnapshotFrame.ts +26 -4
- package/frontend-vue/app/constants.ts +22 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +2 -1
- package/frontend-vue/app/utils/channels.ts +37 -0
- package/frontend-vue/app/utils/format.ts +132 -0
- package/frontend-vue/app/utils/funnel-draft.ts +264 -0
- package/frontend-vue/app/utils/heatmap.ts +82 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +10 -0
- package/frontend-vue/app/utils/host-conflict.ts +48 -0
- package/frontend-vue/app/utils/sample-size.ts +17 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +15 -5
- package/frontend-vue/app/utils/snippet.ts +53 -0
- package/frontend-vue/app/utils/step-path.ts +32 -0
- package/frontend-vue/dms.frontend.ts +4 -3
- package/frontend-vue/i18n/locales/marketing-en-GB.json +1083 -316
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +1087 -320
- package/frontend-vue/package.json +1 -2
- package/frontend-vue/pnpm-lock.yaml +51 -19
- package/frontend-vue/tests/drafts.test.ts +3 -3
- package/frontend-vue/tests/format.test.ts +73 -0
- package/frontend-vue/tests/funnel-draft.test.ts +194 -0
- package/frontend-vue/tests/heatmap.test.ts +32 -0
- package/frontend-vue/tests/i18n.test.ts +87 -4
- package/frontend-vue/tests/latest-request.test.ts +6 -3
- package/frontend-vue/tests/locale-typography.test.ts +115 -0
- package/frontend-vue/tests/registration.test.ts +17 -11
- package/frontend-vue/tests/routes.test.ts +67 -7
- package/frontend-vue/tests/server-parity.test.ts +77 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +18 -3
- package/frontend-vue/vitest.config.ts +3 -1
- package/package.json +10 -8
- package/static/snapshot.js +46 -16
- package/static/tracker.js +139 -48
- package/dist/pages/campaigns/page.js.map +0 -1
- package/frontend-vue/app/components/CampaignsView.vue +0 -330
- package/frontend-vue/app/components/FunnelDetail.vue +0 -1026
- package/frontend-vue/app/components/FunnelsView.vue +0 -493
- package/frontend-vue/app/components/NoWebsiteState.vue +0 -18
- package/frontend-vue/app/components/OverviewView.vue +0 -493
- package/frontend-vue/app/components/PagesView.vue +0 -391
- package/frontend-vue/app/components/TextKpiCard.vue +0 -28
- package/frontend-vue/app/components/TopListTabsCard.vue +0 -162
- package/frontend-vue/app/composables/useChildId.ts +0 -13
- package/frontend-vue/app/composables/useMarketingPeriod.ts +0 -65
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +0 -69
- package/frontend-vue/app/composables/useMarketingWebsites.ts +0 -80
- package/frontend-vue/app/utils/dmsComponent.ts +0 -18
- package/frontend-vue/tests/period.test.ts +0 -35
package/static/snapshot.js
CHANGED
|
@@ -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)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
110
|
-
|
|
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
|
-
|
|
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 =
|
|
192
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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((
|
|
314
|
-
oy: rounded((
|
|
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
|
-
|
|
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(
|
|
336
|
-
y: normalized(
|
|
337
|
-
dx: normalized(
|
|
338
|
-
dy: normalized(
|
|
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,
|
|
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(
|
|
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"}
|