@antelopejs/dms-marketing 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.js +18 -2
- 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 +3 -0
- 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 +21 -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/package.json +9 -7
- 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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { formatNumber } from './format'
|
|
2
|
+
|
|
3
|
+
/** The `t` of useI18n, as far as these helpers call it. */
|
|
4
|
+
type Translate = (
|
|
5
|
+
key: string,
|
|
6
|
+
named?: Record<string, unknown>,
|
|
7
|
+
plural?: number,
|
|
8
|
+
) => string
|
|
9
|
+
|
|
10
|
+
/** Base for reading a typed path the way the backend reads a tracked URL. */
|
|
11
|
+
const RELATIVE_URL_BASE = 'http://relative.invalid'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A typed or pasted path reduced the way the tracker stores it: pathname
|
|
15
|
+
* only, with its leading slash — `demo/pricing`, `/demo/pricing?x=1#top` and
|
|
16
|
+
* `https://site.example/demo/pricing` all read `/demo/pricing`.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizePagePath(typed: string): string | undefined {
|
|
19
|
+
const trimmed = typed.trim()
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return new URL(trimmed, RELATIVE_URL_BASE).pathname
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What a heatmap reset deletes or deleted. */
|
|
31
|
+
export interface HeatmapDataCounts {
|
|
32
|
+
events: number
|
|
33
|
+
snapshots: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** "18 events and 1 snapshot", "18 events", or "nothing". */
|
|
37
|
+
export function heatmapDataLabel(
|
|
38
|
+
counts: HeatmapDataCounts,
|
|
39
|
+
t: Translate,
|
|
40
|
+
locale: string,
|
|
41
|
+
): string {
|
|
42
|
+
const events = t(
|
|
43
|
+
'page.marketing.pages.reset.events',
|
|
44
|
+
{ count: formatNumber(counts.events, locale) },
|
|
45
|
+
counts.events,
|
|
46
|
+
)
|
|
47
|
+
if (counts.snapshots === 0) {
|
|
48
|
+
return events
|
|
49
|
+
}
|
|
50
|
+
return t('page.marketing.pages.reset.and', {
|
|
51
|
+
first: events,
|
|
52
|
+
second: t(
|
|
53
|
+
'page.marketing.pages.reset.snapshots',
|
|
54
|
+
{ count: formatNumber(counts.snapshots, locale) },
|
|
55
|
+
counts.snapshots,
|
|
56
|
+
),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The impact list of a reset dialog, counted when the counts are known. */
|
|
61
|
+
export function heatmapResetImpact(
|
|
62
|
+
counts: HeatmapDataCounts | null,
|
|
63
|
+
t: Translate,
|
|
64
|
+
locale: string,
|
|
65
|
+
) {
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
icon: 'i-ph-cursor-click',
|
|
69
|
+
label: t('page.marketing.pages.reset.impact_events'),
|
|
70
|
+
count: counts ? formatNumber(counts.events, locale) : undefined,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
icon: 'i-ph-camera',
|
|
74
|
+
label: t('page.marketing.pages.reset.impact_snapshots'),
|
|
75
|
+
count: counts ? formatNumber(counts.snapshots, locale) : undefined,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
icon: 'i-ph-check',
|
|
79
|
+
label: t('page.marketing.pages.reset.impact_kept'),
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
}
|
|
@@ -39,3 +39,13 @@ export function heatColorAt(fraction: number, alpha: number): string {
|
|
|
39
39
|
Math.round(low[1][index]! + (high[1][index]! - low[1][index]!) * mix)
|
|
40
40
|
return heatRgba([channel(0), channel(1), channel(2)], alpha)
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/** The ramp as a CSS gradient, for a legend that matches what is drawn:
|
|
44
|
+
* `reversed` runs it from hot to cold. */
|
|
45
|
+
export function heatLegendGradient(reversed = false): string {
|
|
46
|
+
const stops = HEAT_STOPS.map(([stop, rgb]) => {
|
|
47
|
+
const at = reversed ? 1 - stop : stop
|
|
48
|
+
return `${heatRgba(rgb, 1)} ${Math.round(at * 100)}%`
|
|
49
|
+
})
|
|
50
|
+
return `linear-gradient(to right, ${(reversed ? stops.reverse() : stops).join(', ')})`
|
|
51
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host another website of the workspace already declares: the API answers
|
|
3
|
+
* 409 `{ field, message: duplicate_host, values: [host, site] }`. The DMS
|
|
4
|
+
* shows the message alone, so the forms name the site themselves.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const HTTP_CONFLICT = 409
|
|
8
|
+
const DUPLICATE_HOST = '$page.marketing.errors.duplicate_host'
|
|
9
|
+
/** The fields a host is declared in. */
|
|
10
|
+
const HOST_FIELDS = ['domain', 'extraDomains'] as const
|
|
11
|
+
|
|
12
|
+
export interface HostConflict {
|
|
13
|
+
/** `domain` or `extraDomains`: the field that declares the host. */
|
|
14
|
+
field: (typeof HOST_FIELDS)[number]
|
|
15
|
+
host: string
|
|
16
|
+
/** The name of the website that declares it. */
|
|
17
|
+
site: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Translate = (key: string, params?: Record<string, unknown>) => string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The confirmation before "Allow this host": a refused host is only what a
|
|
24
|
+
* beacon claimed, and anyone can send one naming any host.
|
|
25
|
+
*/
|
|
26
|
+
export function allowHostConfirmation(t: Translate, host: string) {
|
|
27
|
+
const prefix = 'page.marketing.install.allow_confirm'
|
|
28
|
+
return {
|
|
29
|
+
title: t(`${prefix}.title`, { host }),
|
|
30
|
+
description: t(`${prefix}.description`),
|
|
31
|
+
confirmLabel: t(`${prefix}.confirm`),
|
|
32
|
+
cancelLabel: t(`${prefix}.cancel`),
|
|
33
|
+
color: 'warning' as const,
|
|
34
|
+
icon: 'i-ph-warning',
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hostConflictOf(error: unknown): HostConflict | null {
|
|
39
|
+
if (apiErrorStatus(error) !== HTTP_CONFLICT) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
const [entry] = resolveFieldErrors(error, { fields: HOST_FIELDS }).fields
|
|
43
|
+
const [host, site] = entry?.values ?? []
|
|
44
|
+
if (entry?.message !== DUPLICATE_HOST || !host || !site) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
return { field: entry.field as HostConflict['field'], host, site }
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** z for a two-sided 95% test and for 80% power, the usual A/B defaults. */
|
|
2
|
+
const Z_ALPHA = 1.96
|
|
3
|
+
const Z_POWER = 0.8416
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sessions each arm needs before a relative lift of `lift` on a conversion
|
|
7
|
+
* rate of `rate` can be told from noise (two-proportion test, 95% / 80%).
|
|
8
|
+
*/
|
|
9
|
+
export function sampleSizePerArm(rate: number, lift: number): number {
|
|
10
|
+
const p1 = rate
|
|
11
|
+
const p2 = Math.min(rate * (1 + lift), 1)
|
|
12
|
+
const variance = p1 * (1 - p1) + p2 * (1 - p2)
|
|
13
|
+
const effect = (p2 - p1) ** 2
|
|
14
|
+
return effect > 0
|
|
15
|
+
? ((Z_ALPHA + Z_POWER) ** 2 * variance) / effect
|
|
16
|
+
: Number.POSITIVE_INFINITY
|
|
17
|
+
}
|
|
@@ -20,12 +20,17 @@ export interface SnapshotViewport {
|
|
|
20
20
|
height: number
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const VIEWPORT_UNIT =
|
|
23
|
+
const VIEWPORT_UNIT =
|
|
24
|
+
/(-?(?:\d+\.?\d*|\.\d+))(dvh|svh|lvh|vh|vmin|vmax)(?![\w-])/g
|
|
24
25
|
const DECLARATION_VALUE = /((?<!\\):)([^;{}]*)/g
|
|
25
26
|
const STYLE_BLOCK = /(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi
|
|
26
27
|
const STYLE_ATTRIBUTE = /(\sstyle=")([^"]*)(")/gi
|
|
27
28
|
|
|
28
|
-
function pixelsOf(
|
|
29
|
+
function pixelsOf(
|
|
30
|
+
amount: string,
|
|
31
|
+
unit: string,
|
|
32
|
+
viewport: SnapshotViewport,
|
|
33
|
+
): number {
|
|
29
34
|
const base = unit.endsWith('vh')
|
|
30
35
|
? viewport.height
|
|
31
36
|
: unit === 'vmin'
|
|
@@ -37,18 +42,23 @@ function pixelsOf(amount: string, unit: string, viewport: SnapshotViewport): num
|
|
|
37
42
|
function rewriteValue(value: string, viewport: SnapshotViewport): string {
|
|
38
43
|
return value.replace(
|
|
39
44
|
VIEWPORT_UNIT,
|
|
40
|
-
(_match, amount: string, unit: string) =>
|
|
45
|
+
(_match, amount: string, unit: string) =>
|
|
46
|
+
`${pixelsOf(amount, unit, viewport)}px`,
|
|
41
47
|
)
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
function rewriteDeclarations(css: string, viewport: SnapshotViewport): string {
|
|
45
51
|
return css.replace(
|
|
46
52
|
DECLARATION_VALUE,
|
|
47
|
-
(_match, colon: string, value: string) =>
|
|
53
|
+
(_match, colon: string, value: string) =>
|
|
54
|
+
colon + rewriteValue(value, viewport),
|
|
48
55
|
)
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
export function rewriteViewportUnits(
|
|
58
|
+
export function rewriteViewportUnits(
|
|
59
|
+
html: string,
|
|
60
|
+
viewport: SnapshotViewport,
|
|
61
|
+
): string {
|
|
52
62
|
return html
|
|
53
63
|
.replace(
|
|
54
64
|
STYLE_BLOCK,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tag a site pastes to load the tracker, in the shapes the install guide
|
|
3
|
+
* offers. Built from the API origin the dashboard talks to, which is where the
|
|
4
|
+
* tracker and the collect endpoint live.
|
|
5
|
+
*/
|
|
6
|
+
export type SnippetFlavor = 'html' | 'nuxt' | 'gtm'
|
|
7
|
+
|
|
8
|
+
// Split so a bundler or a linter never reads a real closing tag here.
|
|
9
|
+
const CLOSING_TAG = '</' + 'script>'
|
|
10
|
+
|
|
11
|
+
export function trackerUrl(apiOrigin: string): string {
|
|
12
|
+
return `${apiOrigin.replace(/\/$/, '')}/api/marketing/tracker.js`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BUILDERS: Record<
|
|
16
|
+
SnippetFlavor,
|
|
17
|
+
(src: string, websiteId: string) => string
|
|
18
|
+
> = {
|
|
19
|
+
html: (src, websiteId) =>
|
|
20
|
+
`<script defer src="${src}"\n data-website-id="${websiteId}">${CLOSING_TAG}`,
|
|
21
|
+
nuxt: (src, websiteId) =>
|
|
22
|
+
[
|
|
23
|
+
'// nuxt.config.ts',
|
|
24
|
+
'export default defineNuxtConfig({',
|
|
25
|
+
' app: {',
|
|
26
|
+
' head: {',
|
|
27
|
+
` script: [{ src: "${src}", defer: true, "data-website-id": "${websiteId}" }],`,
|
|
28
|
+
' },',
|
|
29
|
+
' },',
|
|
30
|
+
'})',
|
|
31
|
+
].join('\n'),
|
|
32
|
+
gtm: (src, websiteId) =>
|
|
33
|
+
[
|
|
34
|
+
'<!-- Custom HTML tag, fired on All Pages -->',
|
|
35
|
+
'<script>',
|
|
36
|
+
' (function () {',
|
|
37
|
+
' var s = document.createElement("script");',
|
|
38
|
+
` s.src = "${src}";`,
|
|
39
|
+
' s.defer = true;',
|
|
40
|
+
` s.setAttribute("data-website-id", "${websiteId}");`,
|
|
41
|
+
' document.head.appendChild(s);',
|
|
42
|
+
' })();',
|
|
43
|
+
CLOSING_TAG,
|
|
44
|
+
].join('\n'),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function trackerSnippet(
|
|
48
|
+
flavor: SnippetFlavor,
|
|
49
|
+
apiOrigin: string,
|
|
50
|
+
websiteId: string,
|
|
51
|
+
): string {
|
|
52
|
+
return BUILDERS[flavor](trackerUrl(apiOrigin), websiteId)
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page step the way the server stores and matches it (mirror of
|
|
3
|
+
* `src/services/funnels/step-path.ts`): a pasted URL, a host glued to the
|
|
4
|
+
* path, a query string, a fragment or a trailing slash come down to the path;
|
|
5
|
+
* a final `*` stays, as a prefix wildcard. The builder shows the result under
|
|
6
|
+
* the step when it differs from what was typed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const STEP_PATH_BASE = 'http://step.invalid'
|
|
10
|
+
const STEP_SCHEME_PATTERN = /^[a-z][a-z\d+.-]*:\/\//i
|
|
11
|
+
const STEP_BARE_HOST_PATTERN = /^[^/\s?#]+\.[^/\s?#]+\//
|
|
12
|
+
|
|
13
|
+
export function normalizeFunnelStepPath(raw: string): string {
|
|
14
|
+
const value = raw.trim()
|
|
15
|
+
const prefix = value.endsWith('*')
|
|
16
|
+
const body = prefix ? value.slice(0, -1) : value
|
|
17
|
+
const absolute = STEP_SCHEME_PATTERN.test(body)
|
|
18
|
+
? body
|
|
19
|
+
: STEP_BARE_HOST_PATTERN.test(body)
|
|
20
|
+
? `https://${body}`
|
|
21
|
+
: `${STEP_PATH_BASE}${body.startsWith('/') ? '' : '/'}${body}`
|
|
22
|
+
let path: string
|
|
23
|
+
try {
|
|
24
|
+
path = new URL(absolute).pathname
|
|
25
|
+
} catch {
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
if (prefix) {
|
|
29
|
+
return `${path}*`
|
|
30
|
+
}
|
|
31
|
+
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
|
|
32
|
+
}
|
|
@@ -9,12 +9,13 @@ interface VueModule {
|
|
|
9
9
|
/**
|
|
10
10
|
* Every component of this module is addressed by name from the backend: the
|
|
11
11
|
* pages declare `CustomComponent('DmsMarketing<Name>')` and the DataTypes
|
|
12
|
-
* resolve their inputs the same way. Registering the whole directory
|
|
13
|
-
*
|
|
12
|
+
* resolve their inputs the same way. Registering the whole directory behind
|
|
13
|
+
* the module's prefix keeps the two sides in sync without a manual list.
|
|
14
14
|
*/
|
|
15
15
|
const components = import.meta.glob<VueModule>('./app/components/**/*.vue')
|
|
16
16
|
|
|
17
17
|
const frontendModule: DmsFrontendModule = {
|
|
18
|
+
componentPrefix: 'DmsMarketing',
|
|
18
19
|
setup(sdk) {
|
|
19
20
|
for (const [path, loader] of Object.entries(components).sort()) {
|
|
20
21
|
const name = path
|
|
@@ -22,7 +23,7 @@ const frontendModule: DmsFrontendModule = {
|
|
|
22
23
|
.at(-1)!
|
|
23
24
|
.replace(/\.vue$/, '')
|
|
24
25
|
sdk.registerComponent(
|
|
25
|
-
|
|
26
|
+
name,
|
|
26
27
|
defineAsyncComponent(async () => (await loader()).default),
|
|
27
28
|
)
|
|
28
29
|
}
|