@antelopejs/dms-marketing 0.2.2
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/LICENSE +190 -0
- package/README.md +143 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +35 -0
- package/dist/config.js.map +1 -0
- package/dist/data-api/body.d.ts +1 -0
- package/dist/data-api/body.js +15 -0
- package/dist/data-api/body.js.map +1 -0
- package/dist/data-api/funnels.d.ts +32 -0
- package/dist/data-api/funnels.js +302 -0
- package/dist/data-api/funnels.js.map +1 -0
- package/dist/data-api/hidden-filter.d.ts +1 -0
- package/dist/data-api/hidden-filter.js +11 -0
- package/dist/data-api/hidden-filter.js.map +1 -0
- package/dist/data-api/index.d.ts +4 -0
- package/dist/data-api/index.js +21 -0
- package/dist/data-api/index.js.map +1 -0
- package/dist/data-api/tenant-scope.d.ts +15 -0
- package/dist/data-api/tenant-scope.js +43 -0
- package/dist/data-api/tenant-scope.js.map +1 -0
- package/dist/data-api/websites.d.ts +25 -0
- package/dist/data-api/websites.js +73 -0
- package/dist/data-api/websites.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/funnels.model.d.ts +25 -0
- package/dist/db/models/funnels.model.js +39 -0
- package/dist/db/models/funnels.model.js.map +1 -0
- package/dist/db/models/index.d.ts +7 -0
- package/dist/db/models/index.js +24 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/marketing_events.model.d.ts +49 -0
- package/dist/db/models/marketing_events.model.js +85 -0
- package/dist/db/models/marketing_events.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +33 -0
- package/dist/db/models/marketing_sessions.model.js +50 -0
- package/dist/db/models/marketing_sessions.model.js.map +1 -0
- package/dist/db/models/marketing_settings.model.d.ts +23 -0
- package/dist/db/models/marketing_settings.model.js +13 -0
- package/dist/db/models/marketing_settings.model.js.map +1 -0
- package/dist/db/models/page_snapshots.model.d.ts +27 -0
- package/dist/db/models/page_snapshots.model.js +32 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -0
- package/dist/db/models/retention.d.ts +6 -0
- package/dist/db/models/retention.js +26 -0
- package/dist/db/models/retention.js.map +1 -0
- package/dist/db/models/website_statistics.model.d.ts +31 -0
- package/dist/db/models/website_statistics.model.js +63 -0
- package/dist/db/models/website_statistics.model.js.map +1 -0
- package/dist/db/models/websites.model.d.ts +26 -0
- package/dist/db/models/websites.model.js +27 -0
- package/dist/db/models/websites.model.js.map +1 -0
- package/dist/db/tables/funnels.table.d.ts +31 -0
- package/dist/db/tables/funnels.table.js +54 -0
- package/dist/db/tables/funnels.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +7 -0
- package/dist/db/tables/index.js +24 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/marketing_events.table.d.ts +15 -0
- package/dist/db/tables/marketing_events.table.js +66 -0
- package/dist/db/tables/marketing_events.table.js.map +1 -0
- package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
- package/dist/db/tables/marketing_sessions.table.js +115 -0
- package/dist/db/tables/marketing_sessions.table.js.map +1 -0
- package/dist/db/tables/marketing_settings.table.d.ts +11 -0
- package/dist/db/tables/marketing_settings.table.js +51 -0
- package/dist/db/tables/marketing_settings.table.js.map +1 -0
- package/dist/db/tables/page_snapshots.table.d.ts +17 -0
- package/dist/db/tables/page_snapshots.table.js +72 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -0
- package/dist/db/tables/website_statistics.table.d.ts +14 -0
- package/dist/db/tables/website_statistics.table.js +60 -0
- package/dist/db/tables/website_statistics.table.js.map +1 -0
- package/dist/db/tables/websites.table.d.ts +13 -0
- package/dist/db/tables/websites.table.js +60 -0
- package/dist/db/tables/websites.table.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +39 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/implementations/dms-marketing/index.d.ts +2 -0
- package/dist/implementations/dms-marketing/index.js +8 -0
- package/dist/implementations/dms-marketing/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/all.d.ts +5 -0
- package/dist/pages/all.js +22 -0
- package/dist/pages/all.js.map +1 -0
- package/dist/pages/campaigns/page.d.ts +9 -0
- package/dist/pages/campaigns/page.js +27 -0
- package/dist/pages/campaigns/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +9 -0
- package/dist/pages/funnels/page.js +27 -0
- package/dist/pages/funnels/page.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/module.d.ts +1 -0
- package/dist/pages/module.js +13 -0
- package/dist/pages/module.js.map +1 -0
- package/dist/pages/overview/page.d.ts +9 -0
- package/dist/pages/overview/page.js +27 -0
- package/dist/pages/overview/page.js.map +1 -0
- package/dist/pages/pages/page.d.ts +9 -0
- package/dist/pages/pages/page.js +27 -0
- package/dist/pages/pages/page.js.map +1 -0
- package/dist/pages/settings/form.d.ts +3 -0
- package/dist/pages/settings/form.js +35 -0
- package/dist/pages/settings/form.js.map +1 -0
- package/dist/pages/settings/page.d.ts +9 -0
- package/dist/pages/settings/page.js +27 -0
- package/dist/pages/settings/page.js.map +1 -0
- package/dist/routes/collect.d.ts +7 -0
- package/dist/routes/collect.js +62 -0
- package/dist/routes/collect.js.map +1 -0
- package/dist/routes/experiments.d.ts +6 -0
- package/dist/routes/experiments.js +63 -0
- package/dist/routes/experiments.js.map +1 -0
- package/dist/routes/funnels.d.ts +16 -0
- package/dist/routes/funnels.js +44 -0
- package/dist/routes/funnels.js.map +1 -0
- package/dist/routes/index.d.ts +8 -0
- package/dist/routes/index.js +25 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/settings.d.ts +8 -0
- package/dist/routes/settings.js +50 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/snapshot.d.ts +10 -0
- package/dist/routes/snapshot.js +147 -0
- package/dist/routes/snapshot.js.map +1 -0
- package/dist/routes/stats.d.ts +44 -0
- package/dist/routes/stats.js +278 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/tracker.d.ts +6 -0
- package/dist/routes/tracker.js +57 -0
- package/dist/routes/tracker.js.map +1 -0
- package/dist/routes/websites.d.ts +13 -0
- package/dist/routes/websites.js +144 -0
- package/dist/routes/websites.js.map +1 -0
- package/dist/services/acquisition.d.ts +15 -0
- package/dist/services/acquisition.js +153 -0
- package/dist/services/acquisition.js.map +1 -0
- package/dist/services/bounded-cache.d.ts +14 -0
- package/dist/services/bounded-cache.js +45 -0
- package/dist/services/bounded-cache.js.map +1 -0
- package/dist/services/collect.d.ts +11 -0
- package/dist/services/collect.js +193 -0
- package/dist/services/collect.js.map +1 -0
- package/dist/services/experiment-definitions.d.ts +4 -0
- package/dist/services/experiment-definitions.js +36 -0
- package/dist/services/experiment-definitions.js.map +1 -0
- package/dist/services/experiments/assign.d.ts +5 -0
- package/dist/services/experiments/assign.js +52 -0
- package/dist/services/experiments/assign.js.map +1 -0
- package/dist/services/experiments/index.d.ts +3 -0
- package/dist/services/experiments/index.js +20 -0
- package/dist/services/experiments/index.js.map +1 -0
- package/dist/services/experiments/results.d.ts +20 -0
- package/dist/services/experiments/results.js +91 -0
- package/dist/services/experiments/results.js.map +1 -0
- package/dist/services/experiments/stats.d.ts +12 -0
- package/dist/services/experiments/stats.js +62 -0
- package/dist/services/experiments/stats.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +15 -0
- package/dist/services/funnels/compute.js +68 -0
- package/dist/services/funnels/compute.js.map +1 -0
- package/dist/services/funnels/index.d.ts +2 -0
- package/dist/services/funnels/index.js +19 -0
- package/dist/services/funnels/index.js.map +1 -0
- package/dist/services/funnels/results.d.ts +14 -0
- package/dist/services/funnels/results.js +72 -0
- package/dist/services/funnels/results.js.map +1 -0
- package/dist/services/geoip.d.ts +3 -0
- package/dist/services/geoip.js +38 -0
- package/dist/services/geoip.js.map +1 -0
- package/dist/services/heatmap.d.ts +10 -0
- package/dist/services/heatmap.js +180 -0
- package/dist/services/heatmap.js.map +1 -0
- package/dist/services/index.d.ts +26 -0
- package/dist/services/index.js +43 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/ingest-guard.d.ts +11 -0
- package/dist/services/ingest-guard.js +44 -0
- package/dist/services/ingest-guard.js.map +1 -0
- package/dist/services/keyed-queue.d.ts +4 -0
- package/dist/services/keyed-queue.js +19 -0
- package/dist/services/keyed-queue.js.map +1 -0
- package/dist/services/origin-guard.d.ts +5 -0
- package/dist/services/origin-guard.js +43 -0
- package/dist/services/origin-guard.js.map +1 -0
- package/dist/services/period.d.ts +2 -0
- package/dist/services/period.js +19 -0
- package/dist/services/period.js.map +1 -0
- package/dist/services/prune.d.ts +7 -0
- package/dist/services/prune.js +49 -0
- package/dist/services/prune.js.map +1 -0
- package/dist/services/query-param.d.ts +1 -0
- package/dist/services/query-param.js +11 -0
- package/dist/services/query-param.js.map +1 -0
- package/dist/services/request-body.d.ts +2 -0
- package/dist/services/request-body.js +41 -0
- package/dist/services/request-body.js.map +1 -0
- package/dist/services/request-meta.d.ts +3 -0
- package/dist/services/request-meta.js +19 -0
- package/dist/services/request-meta.js.map +1 -0
- package/dist/services/rollup-write.d.ts +4 -0
- package/dist/services/rollup-write.js +84 -0
- package/dist/services/rollup-write.js.map +1 -0
- package/dist/services/sessionize.d.ts +23 -0
- package/dist/services/sessionize.js +78 -0
- package/dist/services/sessionize.js.map +1 -0
- package/dist/services/settings.d.ts +6 -0
- package/dist/services/settings.js +113 -0
- package/dist/services/settings.js.map +1 -0
- package/dist/services/snapshots.d.ts +24 -0
- package/dist/services/snapshots.js +139 -0
- package/dist/services/snapshots.js.map +1 -0
- package/dist/services/tenant-funnel.d.ts +3 -0
- package/dist/services/tenant-funnel.js +17 -0
- package/dist/services/tenant-funnel.js.map +1 -0
- package/dist/services/tenant-website.d.ts +5 -0
- package/dist/services/tenant-website.js +34 -0
- package/dist/services/tenant-website.js.map +1 -0
- package/dist/services/url.d.ts +10 -0
- package/dist/services/url.js +81 -0
- package/dist/services/url.js.map +1 -0
- package/dist/services/user-agent.d.ts +2 -0
- package/dist/services/user-agent.js +17 -0
- package/dist/services/user-agent.js.map +1 -0
- package/dist/services/visitor-id.d.ts +1 -0
- package/dist/services/visitor-id.js +19 -0
- package/dist/services/visitor-id.js.map +1 -0
- package/dist/services/website-cache.d.ts +3 -0
- package/dist/services/website-cache.js +22 -0
- package/dist/services/website-cache.js.map +1 -0
- package/dist/types/constants.d.ts +74 -0
- package/dist/types/constants.js +79 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/events.d.ts +114 -0
- package/dist/types/events.js +29 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/settings.d.ts +67 -0
- package/dist/types/settings.js +58 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/snapshots.d.ts +95 -0
- package/dist/types/snapshots.js +41 -0
- package/dist/types/snapshots.js.map +1 -0
- package/dist/types/statistics.d.ts +38 -0
- package/dist/types/statistics.js +33 -0
- package/dist/types/statistics.js.map +1 -0
- package/dist/utils/experiment-type.d.ts +52 -0
- package/dist/utils/experiment-type.js +94 -0
- package/dist/utils/experiment-type.js.map +1 -0
- package/dist/utils/funnel-steps-type.d.ts +42 -0
- package/dist/utils/funnel-steps-type.js +62 -0
- package/dist/utils/funnel-steps-type.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +7 -0
- package/frontend-vue/app/components/CampaignsView.vue +330 -0
- package/frontend-vue/app/components/ExperimentInput.vue +128 -0
- package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
- package/frontend-vue/app/components/FunnelsView.vue +493 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
- package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
- package/frontend-vue/app/components/OverviewView.vue +493 -0
- package/frontend-vue/app/components/PagePreview.vue +915 -0
- package/frontend-vue/app/components/PagesView.vue +378 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
- package/frontend-vue/app/components/TextKpiCard.vue +28 -0
- package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
- package/frontend-vue/app/components/VariationsInput.vue +164 -0
- package/frontend-vue/app/composables/useChildId.ts +13 -0
- package/frontend-vue/app/composables/useExperiment.ts +59 -0
- package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
- package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
- package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
- package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
- package/frontend-vue/app/utils/dmsComponent.ts +18 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
- package/frontend-vue/dms.frontend.ts +35 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
- package/frontend-vue/package.json +30 -0
- package/frontend-vue/pnpm-lock.yaml +5226 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/drafts.test.ts +54 -0
- package/frontend-vue/tests/i18n.test.ts +86 -0
- package/frontend-vue/tests/period.test.ts +35 -0
- package/frontend-vue/tests/registration.test.ts +63 -0
- package/frontend-vue/tests/routes.test.ts +31 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
- package/frontend-vue/vitest.config.ts +20 -0
- package/package.json +91 -0
- package/static/snapshot.js +497 -0
- package/static/tracker.js +503 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dms-marketing page snapshot capture.
|
|
3
|
+
*
|
|
4
|
+
* Served by GET /api/marketing/snapshot.js to a sampled visit the backend
|
|
5
|
+
* wants a capture from (see static/tracker.js), with the site's settings
|
|
6
|
+
* prepended as `window.__dmsMarketingSnapshotConfig` and the tracker's
|
|
7
|
+
* context waiting in `window.__dmsMarketingSnapshotContext`. Serializes the
|
|
8
|
+
* rendered DOM — what the visitor sees, whatever built it — into one
|
|
9
|
+
* self-contained HTML document and uploads it as the heatmap's backdrop.
|
|
10
|
+
*
|
|
11
|
+
* Privacy, unconditionally: no form value, hidden input, script, event
|
|
12
|
+
* handler or framed document leaves the page. `contenteditable` text is
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
(() => {
|
|
19
|
+
const CONFIG_GLOBAL = "__dmsMarketingSnapshotConfig";
|
|
20
|
+
const CONTEXT_GLOBAL = "__dmsMarketingSnapshotContext";
|
|
21
|
+
const MASK_MARKER = "dms-marketing-mask";
|
|
22
|
+
const BLOCK_MARKER = "dms-marketing-block";
|
|
23
|
+
const MASK_CHAR = "x";
|
|
24
|
+
/** Mirrors the backend MAX_URL_LENGTH. */
|
|
25
|
+
const MAX_URL_LENGTH = 500;
|
|
26
|
+
const MAX_REPORTED_BYTES = 256 * 1024 * 1024;
|
|
27
|
+
/** A canvas past this many data-URL characters ships empty. */
|
|
28
|
+
const MAX_CANVAS_DATA_URL_LENGTH = 200000;
|
|
29
|
+
/** Time without a DOM mutation before the page counts as settled. */
|
|
30
|
+
const QUIET_MS = 500;
|
|
31
|
+
const QUIET_DEADLINE_MS = 10000;
|
|
32
|
+
const IDLE_TIMEOUT_MS = 2000;
|
|
33
|
+
|
|
34
|
+
const VOID_TAGS = new Set([
|
|
35
|
+
"area",
|
|
36
|
+
"base",
|
|
37
|
+
"br",
|
|
38
|
+
"col",
|
|
39
|
+
"embed",
|
|
40
|
+
"hr",
|
|
41
|
+
"img",
|
|
42
|
+
"input",
|
|
43
|
+
"link",
|
|
44
|
+
"meta",
|
|
45
|
+
"param",
|
|
46
|
+
"source",
|
|
47
|
+
"track",
|
|
48
|
+
"wbr",
|
|
49
|
+
]);
|
|
50
|
+
/** Nothing of theirs renders in a script-less document — noscript
|
|
51
|
+
* included, whose content WOULD render there — or their content is another
|
|
52
|
+
* document, plugin or stream. */
|
|
53
|
+
const DROPPED_TAGS = new Set([
|
|
54
|
+
"script",
|
|
55
|
+
"noscript",
|
|
56
|
+
"template",
|
|
57
|
+
"meta",
|
|
58
|
+
"base",
|
|
59
|
+
"object",
|
|
60
|
+
"embed",
|
|
61
|
+
"applet",
|
|
62
|
+
"portal",
|
|
63
|
+
"source",
|
|
64
|
+
"track",
|
|
65
|
+
]);
|
|
66
|
+
const DROPPED_ATTRIBUTES = new Set([
|
|
67
|
+
"srcdoc",
|
|
68
|
+
"nonce",
|
|
69
|
+
"integrity",
|
|
70
|
+
"action",
|
|
71
|
+
"formaction",
|
|
72
|
+
"ping",
|
|
73
|
+
]);
|
|
74
|
+
/** Media and frames keep their box, never their content. */
|
|
75
|
+
const SOURCELESS_TAGS = new Set(["iframe", "frame", "video", "audio"]);
|
|
76
|
+
const URL_ATTRIBUTES = new Set([
|
|
77
|
+
"src",
|
|
78
|
+
"href",
|
|
79
|
+
"poster",
|
|
80
|
+
"xlink:href",
|
|
81
|
+
"background",
|
|
82
|
+
]);
|
|
83
|
+
const LINK_TAGS = new Set(["a", "area"]);
|
|
84
|
+
const OPAQUE_URL_PATTERN = /^(#|data:|blob:|about:)/i;
|
|
85
|
+
const SCRIPT_URL_PATTERN = /^javascript:/i;
|
|
86
|
+
const CSS_URL_PATTERN = /url\(\s*(['"]?)([^'")]*)\1\s*\)/g;
|
|
87
|
+
const CSS_IMPORT_PATTERN = /@import\s+(['"])([^'"]+)\1/g;
|
|
88
|
+
|
|
89
|
+
function readGlobal(name) {
|
|
90
|
+
const value = window[name];
|
|
91
|
+
delete window[name];
|
|
92
|
+
return value && typeof value === "object" ? value : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const config = readGlobal(CONFIG_GLOBAL);
|
|
96
|
+
const context = readGlobal(CONTEXT_GLOBAL);
|
|
97
|
+
if (
|
|
98
|
+
!config ||
|
|
99
|
+
!context ||
|
|
100
|
+
typeof context.endpoint !== "string" ||
|
|
101
|
+
window.top !== window
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Text -----------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function escapeText(text) {
|
|
109
|
+
return text
|
|
110
|
+
.replace(/&/g, "&")
|
|
111
|
+
.replace(/</g, "<")
|
|
112
|
+
.replace(/>/g, ">");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function escapeAttribute(value) {
|
|
116
|
+
return value
|
|
117
|
+
.replace(/&/g, "&")
|
|
118
|
+
.replace(/"/g, """)
|
|
119
|
+
.replace(/</g, "<");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whitespace survives so lines still wrap where they did. */
|
|
123
|
+
function maskText(text) {
|
|
124
|
+
return text.replace(/\S/g, MASK_CHAR);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- URLs -----------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** Absolute form, untouched for fragments and inline data, null for what
|
|
130
|
+
* must not be carried (script URLs, garbage). */
|
|
131
|
+
function absoluteUrl(value, base) {
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
if (!trimmed || OPAQUE_URL_PATTERN.test(trimmed)) {
|
|
134
|
+
return trimmed;
|
|
135
|
+
}
|
|
136
|
+
if (SCRIPT_URL_PATTERN.test(trimmed)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return new URL(trimmed, base).href;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 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) {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = new URL(url);
|
|
151
|
+
parsed.search = "";
|
|
152
|
+
parsed.hash = "";
|
|
153
|
+
return parsed.href;
|
|
154
|
+
} catch {
|
|
155
|
+
return url;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function absolutizeSrcset(value, base) {
|
|
160
|
+
return value
|
|
161
|
+
.split(",")
|
|
162
|
+
.map((candidate) => {
|
|
163
|
+
const [url, ...descriptors] = candidate.trim().split(/\s+/);
|
|
164
|
+
const absolute = url ? absoluteUrl(url, base) : null;
|
|
165
|
+
return absolute ? [absolute, ...descriptors].join(" ") : "";
|
|
166
|
+
})
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(", ");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function absolutizeCss(css, base) {
|
|
172
|
+
return css
|
|
173
|
+
.replace(CSS_URL_PATTERN, (_match, _quote, url) => {
|
|
174
|
+
const absolute = absoluteUrl(url, base);
|
|
175
|
+
return absolute === null ? "url()" : `url(${JSON.stringify(absolute)})`;
|
|
176
|
+
})
|
|
177
|
+
.replace(CSS_IMPORT_PATTERN, (match, _quote, url) => {
|
|
178
|
+
const absolute = absoluteUrl(url, base);
|
|
179
|
+
return absolute === null
|
|
180
|
+
? match
|
|
181
|
+
: `@import ${JSON.stringify(absolute)}`;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Stylesheets ----------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Rules read from the CSSOM, not from the element's text: CSS-in-JS
|
|
189
|
+
* libraries insert theirs with `insertRule`, leaving the `<style>` empty.
|
|
190
|
+
* Null when the browser refuses (cross-origin sheet); the caller keeps a
|
|
191
|
+
* link then. Readable imports are inlined in place, under their media.
|
|
192
|
+
*/
|
|
193
|
+
function rulesText(sheet, base) {
|
|
194
|
+
let rules;
|
|
195
|
+
try {
|
|
196
|
+
rules = sheet.cssRules;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
let out = "";
|
|
201
|
+
for (const rule of rules) {
|
|
202
|
+
const imported =
|
|
203
|
+
rule.type === CSSRule.IMPORT_RULE && rule.styleSheet
|
|
204
|
+
? rulesText(rule.styleSheet, rule.styleSheet.href || base)
|
|
205
|
+
: null;
|
|
206
|
+
if (imported === null) {
|
|
207
|
+
out += `${absolutizeCss(rule.cssText, base)}\n`;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const media = rule.media?.mediaText;
|
|
211
|
+
out += media ? `@media ${media}{${imported}}\n` : `${imported}\n`;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function mediaAttribute(element) {
|
|
217
|
+
const media = element.getAttribute("media");
|
|
218
|
+
return media ? ` media="${escapeAttribute(media)}"` : "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function serializeStyle(element) {
|
|
222
|
+
const sheet = element.sheet;
|
|
223
|
+
if (sheet?.disabled) {
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
const css = sheet ? rulesText(sheet, location.href) : null;
|
|
227
|
+
const text = css === null ? element.textContent : css;
|
|
228
|
+
return `<style${mediaAttribute(element)}>${text}</style>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Only stylesheets render; preloads, icons and manifests are requests
|
|
232
|
+
* the backdrop has no use for. */
|
|
233
|
+
function serializeLink(element) {
|
|
234
|
+
const rel = (element.getAttribute("rel") || "").toLowerCase().split(/\s+/);
|
|
235
|
+
const sheet = element.sheet;
|
|
236
|
+
if (!rel.includes("stylesheet") || sheet?.disabled) {
|
|
237
|
+
return "";
|
|
238
|
+
}
|
|
239
|
+
const href = element.href;
|
|
240
|
+
const css = sheet ? rulesText(sheet, href) : null;
|
|
241
|
+
if (css !== null) {
|
|
242
|
+
return `<style${mediaAttribute(element)}>${css}</style>`;
|
|
243
|
+
}
|
|
244
|
+
return `<link rel="stylesheet" href="${escapeAttribute(href)}"${mediaAttribute(element)}>`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function adoptedStyles(root) {
|
|
248
|
+
let out = "";
|
|
249
|
+
for (const sheet of root.adoptedStyleSheets || []) {
|
|
250
|
+
out += `<style>${rulesText(sheet, location.href) || ""}</style>`;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- Elements -------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
function isMarked(element, marker) {
|
|
258
|
+
return (
|
|
259
|
+
element.hasAttribute(`data-${marker}`) ||
|
|
260
|
+
element.classList.contains(marker)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function imageSource(element, value) {
|
|
265
|
+
const current = element.currentSrc;
|
|
266
|
+
if (current && !current.startsWith("data:")) {
|
|
267
|
+
return current;
|
|
268
|
+
}
|
|
269
|
+
return element.dataset.src || current || value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function serializeAttributes(element) {
|
|
273
|
+
const tag = element.localName;
|
|
274
|
+
let out = "";
|
|
275
|
+
for (const attribute of element.attributes) {
|
|
276
|
+
const name = attribute.name;
|
|
277
|
+
let value = attribute.value;
|
|
278
|
+
if (name.startsWith("on") || DROPPED_ATTRIBUTES.has(name)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (name === "value" && (tag === "input" || tag === "textarea")) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (
|
|
285
|
+
(name === "checked" && tag === "input") ||
|
|
286
|
+
(name === "selected" && tag === "option")
|
|
287
|
+
) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (name === "src" && SOURCELESS_TAGS.has(tag)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
tag === "img" &&
|
|
295
|
+
element.currentSrc &&
|
|
296
|
+
(name === "srcset" || name === "sizes")
|
|
297
|
+
) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (name === "src" && tag === "img") {
|
|
301
|
+
value = imageSource(element, value);
|
|
302
|
+
}
|
|
303
|
+
if (name === "srcset") {
|
|
304
|
+
value = absolutizeSrcset(value, location.href);
|
|
305
|
+
} else if (name === "style") {
|
|
306
|
+
value = absolutizeCss(value, location.href);
|
|
307
|
+
} else if (URL_ATTRIBUTES.has(name)) {
|
|
308
|
+
const absolute = absoluteUrl(value, location.href);
|
|
309
|
+
if (absolute === null) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
value = LINK_TAGS.has(tag) ? withoutQuery(absolute) : absolute;
|
|
313
|
+
}
|
|
314
|
+
out += ` ${name}="${escapeAttribute(value)}"`;
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The element's own box, nothing inside it. Id and class stay so the
|
|
320
|
+
* page's CSS still lays it out — and click anchors still resolve to it.
|
|
321
|
+
* The box is the border box the visitor saw, capped at its container so a
|
|
322
|
+
* narrower layout reflows it instead of overflowing. */
|
|
323
|
+
function serializeBlocked(element) {
|
|
324
|
+
const box = element.getBoundingClientRect();
|
|
325
|
+
const tag = VOID_TAGS.has(element.localName) ? "span" : element.localName;
|
|
326
|
+
let attributes = "";
|
|
327
|
+
for (const name of ["id", "class"]) {
|
|
328
|
+
const value = element.getAttribute(name);
|
|
329
|
+
if (value) {
|
|
330
|
+
attributes += ` ${name}="${escapeAttribute(value)}"`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const size = `box-sizing:border-box;width:${Math.round(box.width)}px;max-width:100%;height:${Math.round(box.height)}px`;
|
|
334
|
+
return `<${tag}${attributes} style="${size};overflow:hidden"></${tag}>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function serializeCanvas(element) {
|
|
338
|
+
let dataUrl = "";
|
|
339
|
+
try {
|
|
340
|
+
dataUrl = element.toDataURL();
|
|
341
|
+
} catch {
|
|
342
|
+
// Tainted by cross-origin drawing: the box stays, the pixels do not.
|
|
343
|
+
}
|
|
344
|
+
if (dataUrl && dataUrl.length <= MAX_CANVAS_DATA_URL_LENGTH) {
|
|
345
|
+
return `<img${serializeAttributes(element)} src="${dataUrl}">`;
|
|
346
|
+
}
|
|
347
|
+
return `<canvas${serializeAttributes(element)}></canvas>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function serializeChildren(node, masked) {
|
|
351
|
+
let out = "";
|
|
352
|
+
for (const child of node.childNodes) {
|
|
353
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
354
|
+
out += serializeElement(child, masked);
|
|
355
|
+
} else if (child.nodeType === Node.TEXT_NODE) {
|
|
356
|
+
const text = masked ? maskText(child.data) : child.data;
|
|
357
|
+
out += escapeText(text);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function serializeElement(element, masked) {
|
|
364
|
+
const tag = element.localName;
|
|
365
|
+
if (
|
|
366
|
+
DROPPED_TAGS.has(tag) ||
|
|
367
|
+
(tag === "input" && element.type === "hidden")
|
|
368
|
+
) {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
if (tag === "style") {
|
|
372
|
+
return serializeStyle(element);
|
|
373
|
+
}
|
|
374
|
+
if (tag === "link") {
|
|
375
|
+
return serializeLink(element);
|
|
376
|
+
}
|
|
377
|
+
if (isMarked(element, BLOCK_MARKER)) {
|
|
378
|
+
return serializeBlocked(element);
|
|
379
|
+
}
|
|
380
|
+
if (tag === "canvas") {
|
|
381
|
+
return serializeCanvas(element);
|
|
382
|
+
}
|
|
383
|
+
const mask =
|
|
384
|
+
masked || isMarked(element, MASK_MARKER) || element.isContentEditable;
|
|
385
|
+
let out = `<${tag}${serializeAttributes(element)}>`;
|
|
386
|
+
if (VOID_TAGS.has(tag)) {
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
// A textarea's text IS its value.
|
|
390
|
+
if (tag !== "textarea") {
|
|
391
|
+
if (element.shadowRoot) {
|
|
392
|
+
out += `<template shadowrootmode="open">${adoptedStyles(element.shadowRoot)}${serializeChildren(element.shadowRoot, mask)}</template>`;
|
|
393
|
+
}
|
|
394
|
+
if (tag === "head") {
|
|
395
|
+
out += '<meta charset="utf-8">';
|
|
396
|
+
}
|
|
397
|
+
out += serializeChildren(element, mask);
|
|
398
|
+
if (tag === "head") {
|
|
399
|
+
out += adoptedStyles(document);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return `${out}</${tag}>`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function serializeDocument() {
|
|
406
|
+
return `<!DOCTYPE html>${serializeElement(document.documentElement, config.maskText === true)}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// --- Timing ---------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
/** Calls back once the DOM has stopped mutating, or at the deadline on a
|
|
412
|
+
* page that never stops (a ticking clock, a looping animation). */
|
|
413
|
+
function whenSettled(callback) {
|
|
414
|
+
let done = false;
|
|
415
|
+
let timer = null;
|
|
416
|
+
const observer = new MutationObserver(arm);
|
|
417
|
+
function finish() {
|
|
418
|
+
if (done) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
done = true;
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
observer.disconnect();
|
|
424
|
+
callback();
|
|
425
|
+
}
|
|
426
|
+
function arm() {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
timer = setTimeout(finish, QUIET_MS);
|
|
429
|
+
}
|
|
430
|
+
observer.observe(document.documentElement, {
|
|
431
|
+
subtree: true,
|
|
432
|
+
childList: true,
|
|
433
|
+
attributes: true,
|
|
434
|
+
characterData: true,
|
|
435
|
+
});
|
|
436
|
+
arm();
|
|
437
|
+
setTimeout(finish, QUIET_DEADLINE_MS);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function whenIdle(callback) {
|
|
441
|
+
if (typeof requestIdleCallback === "function") {
|
|
442
|
+
requestIdleCallback(callback, { timeout: IDLE_TIMEOUT_MS });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
setTimeout(callback, 0);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// --- Upload ---------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
// text/plain is CORS-safelisted (no preflight); no-cors keeps the
|
|
451
|
+
// unreadable answer from logging an error on the visitor's console.
|
|
452
|
+
function post(body) {
|
|
453
|
+
fetch(context.endpoint, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
body,
|
|
456
|
+
mode: "no-cors",
|
|
457
|
+
credentials: "omit",
|
|
458
|
+
}).catch(() => {});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function capture() {
|
|
462
|
+
// The visitor navigated on since the negotiation: this is another page.
|
|
463
|
+
if (location.pathname !== context.path) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const root = document.documentElement;
|
|
467
|
+
const page = {
|
|
468
|
+
website: context.website,
|
|
469
|
+
url: (location.origin + location.pathname + location.search).slice(
|
|
470
|
+
0,
|
|
471
|
+
MAX_URL_LENGTH,
|
|
472
|
+
),
|
|
473
|
+
viewport: { width: innerWidth, height: innerHeight },
|
|
474
|
+
colorScheme: matchMedia("(prefers-color-scheme: dark)").matches
|
|
475
|
+
? "dark"
|
|
476
|
+
: "light",
|
|
477
|
+
};
|
|
478
|
+
const html = serializeDocument();
|
|
479
|
+
const body = JSON.stringify({
|
|
480
|
+
...page,
|
|
481
|
+
document: { width: root.scrollWidth, height: root.scrollHeight },
|
|
482
|
+
html,
|
|
483
|
+
});
|
|
484
|
+
if (new Blob([body]).size <= config.maxBytes) {
|
|
485
|
+
post(body);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
post(
|
|
489
|
+
JSON.stringify({
|
|
490
|
+
...page,
|
|
491
|
+
oversizeBytes: Math.min(new Blob([html]).size, MAX_REPORTED_BYTES),
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
whenSettled(() => whenIdle(capture));
|
|
497
|
+
})();
|