@antelopejs/dms-marketing 0.3.3 → 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 +1 -1
- 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,736 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useI18n } from '#dms/frontend-module'
|
|
4
|
+
import type {
|
|
5
|
+
MarketingFunnelResults,
|
|
6
|
+
MarketingVariationResult,
|
|
7
|
+
} from '../composables/useMarketingApi'
|
|
8
|
+
import { pagesLink } from '../composables/useMarketingRoutes'
|
|
9
|
+
import { useContextLink } from '../composables/useContextLink'
|
|
10
|
+
import { formatNumber } from '../utils/format'
|
|
11
|
+
import {
|
|
12
|
+
localizedSnippetComments,
|
|
13
|
+
variationSnippet,
|
|
14
|
+
} from '../utils/funnel-draft'
|
|
15
|
+
import { fractionDigits } from '../constants'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The report of a started A/B test, read over its own runs: the verdict as a
|
|
19
|
+
* sentence with its next action, lift with its interval, p-value and sample
|
|
20
|
+
* size beside it; the warnings that void a reading above the numbers; each
|
|
21
|
+
* arm step by step; the frozen variations and the code that asks for one.
|
|
22
|
+
*
|
|
23
|
+
* A significant reading is only "ready to decide" past the decision rule (a
|
|
24
|
+
* full week served, a conversions floor in every arm): checked every day, a
|
|
25
|
+
* p-value crosses .05 by chance long before. Before that it is "too early".
|
|
26
|
+
*
|
|
27
|
+
* Arms keep an identity colour everywhere (A neutral, B primary, then the
|
|
28
|
+
* other tones): green and red only ever mean a verdict.
|
|
29
|
+
*/
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
results: MarketingFunnelResults
|
|
32
|
+
loading: boolean
|
|
33
|
+
websiteName: string
|
|
34
|
+
}>()
|
|
35
|
+
|
|
36
|
+
const LETTER_A = 65
|
|
37
|
+
const PERCENT = 100
|
|
38
|
+
const MIN_OUTCOMES = 5
|
|
39
|
+
const SMALL_P = 0.001
|
|
40
|
+
|
|
41
|
+
// On a 15% tint the light theme's 700 text falls under 4.5:1: one shade darker
|
|
42
|
+
// there, the theme colour in dark.
|
|
43
|
+
const ARM_STYLES = [
|
|
44
|
+
{ chip: 'bg-elevated text-toned', bar: 'bg-(--ui-text-dimmed)' },
|
|
45
|
+
{
|
|
46
|
+
chip: 'bg-primary/15 text-(--ui-color-primary-800) dark:text-primary',
|
|
47
|
+
bar: 'bg-primary',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
chip: 'bg-warning/15 text-(--ui-color-warning-800) dark:text-warning',
|
|
51
|
+
bar: 'bg-warning',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
chip: 'bg-info/15 text-(--ui-color-info-800) dark:text-info',
|
|
55
|
+
bar: 'bg-info',
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const { t, locale } = useI18n()
|
|
60
|
+
const inContext = useContextLink()
|
|
61
|
+
|
|
62
|
+
const experiment = computed(() => props.results.experiment ?? null)
|
|
63
|
+
const definition = computed(() => props.results.funnel.experiment!)
|
|
64
|
+
const arms = computed(() => experiment.value?.variations ?? [])
|
|
65
|
+
const stopped = computed(() => definition.value.status === 'stopped')
|
|
66
|
+
const decision = computed(() => experiment.value?.decision ?? null)
|
|
67
|
+
|
|
68
|
+
function letter(index: number): string {
|
|
69
|
+
return String.fromCharCode(LETTER_A + index)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function armStyle(index: number) {
|
|
73
|
+
return ARM_STYLES[index % ARM_STYLES.length]!
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function goalSessions(arm: MarketingVariationResult): number {
|
|
77
|
+
return arm.computation.steps.at(-1)?.sessions ?? 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const control = computed(() => arms.value[0] ?? null)
|
|
81
|
+
|
|
82
|
+
/** The server's reading: the challenger it names (best rate among those that
|
|
83
|
+
* got a p-value — the list names the same one) and its verdict. */
|
|
84
|
+
const verdict = computed(() => props.results.verdict ?? null)
|
|
85
|
+
|
|
86
|
+
const bestIndex = computed(() => verdict.value?.leader ?? -1)
|
|
87
|
+
|
|
88
|
+
const best = computed(() =>
|
|
89
|
+
bestIndex.value === -1 ? null : (arms.value[bestIndex.value] ?? null),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const totalExposed = computed(() =>
|
|
93
|
+
arms.value.reduce((sum, arm) => sum + arm.exposedSessions, 0),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
/** Nobody was exposed since the start: the page does not ask for an arm. */
|
|
97
|
+
const noExposure = computed(
|
|
98
|
+
() => arms.value.length > 0 && totalExposed.value === 0,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const insufficient = computed(
|
|
102
|
+
() =>
|
|
103
|
+
!noExposure.value &&
|
|
104
|
+
!props.results.truncated &&
|
|
105
|
+
arms.value.length > 0 &&
|
|
106
|
+
arms.value.slice(1).every((arm) => arm.significant === null),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const srmMismatch = computed(() => experiment.value?.srm.mismatch === true)
|
|
110
|
+
|
|
111
|
+
/** The level each challenger is tested at, in percent (95, or more past two
|
|
112
|
+
* arms). */
|
|
113
|
+
const level = computed(() =>
|
|
114
|
+
formatNumber(
|
|
115
|
+
(1 - (experiment.value?.alpha ?? 0.05)) * PERCENT,
|
|
116
|
+
locale.value,
|
|
117
|
+
1,
|
|
118
|
+
).replace(/[.,]0$/, ''),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const verdictKind = computed(() => verdict.value?.kind ?? 'withheld')
|
|
122
|
+
|
|
123
|
+
const lift = computed(() => (best.value?.uplift ?? 0) * PERCENT)
|
|
124
|
+
|
|
125
|
+
function signed(value: number, digits = 1): string {
|
|
126
|
+
return `${value >= 0 ? '+' : '−'}${formatPercentage(Math.abs(value) / 100, locale.value, fractionDigits(digits))}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const interval = computed(() => {
|
|
130
|
+
const arm = best.value
|
|
131
|
+
if (!arm || arm.upliftLow === null || arm.upliftHigh === null) {
|
|
132
|
+
return ''
|
|
133
|
+
}
|
|
134
|
+
return t('page.marketing.experiment.lift_interval', {
|
|
135
|
+
level: level.value,
|
|
136
|
+
low: signed(arm.upliftLow * PERCENT, 0),
|
|
137
|
+
high: signed(arm.upliftHigh * PERCENT, 0),
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const pValueText = computed(() => {
|
|
142
|
+
const p = best.value?.pValue
|
|
143
|
+
if (p === null || p === undefined) {
|
|
144
|
+
return '—'
|
|
145
|
+
}
|
|
146
|
+
return p < SMALL_P
|
|
147
|
+
? `< ${formatNumber(SMALL_P, locale.value, 3)}`
|
|
148
|
+
: formatNumber(p, locale.value, 3)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const goal = computed(() => props.results.funnel.steps.at(-1)?.value ?? '')
|
|
152
|
+
|
|
153
|
+
const headline = computed(() => {
|
|
154
|
+
const params = {
|
|
155
|
+
letter: letter(bestIndex.value),
|
|
156
|
+
lift: formatPercentage(
|
|
157
|
+
Math.abs(lift.value) / 100,
|
|
158
|
+
locale.value,
|
|
159
|
+
fractionDigits(1),
|
|
160
|
+
),
|
|
161
|
+
}
|
|
162
|
+
return t(`page.marketing.experiment.verdict.${verdictKind.value}`, params)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const explanation = computed(() => {
|
|
166
|
+
if (!best.value || !control.value) {
|
|
167
|
+
return ''
|
|
168
|
+
}
|
|
169
|
+
const kind = verdictKind.value
|
|
170
|
+
const stoppedKind =
|
|
171
|
+
stopped.value && kind !== 'withheld' && kind !== 'early'
|
|
172
|
+
? `${kind}_detail_stopped`
|
|
173
|
+
: `${kind}_detail`
|
|
174
|
+
const days = Math.floor(decision.value?.days ?? 0)
|
|
175
|
+
return t(`page.marketing.experiment.verdict.${stoppedKind}`, {
|
|
176
|
+
goal: goal.value,
|
|
177
|
+
best: formatPercentage(
|
|
178
|
+
best.value.conversionRate,
|
|
179
|
+
locale.value,
|
|
180
|
+
fractionDigits(2),
|
|
181
|
+
),
|
|
182
|
+
control: formatPercentage(
|
|
183
|
+
control.value.conversionRate,
|
|
184
|
+
locale.value,
|
|
185
|
+
fractionDigits(2),
|
|
186
|
+
),
|
|
187
|
+
letter: letter(bestIndex.value),
|
|
188
|
+
key: best.value.key,
|
|
189
|
+
experiment: definition.value.key,
|
|
190
|
+
days,
|
|
191
|
+
minDays: decision.value?.minDays ?? 0,
|
|
192
|
+
minConversions: decision.value?.minConversions ?? 0,
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const maxRate = computed(() =>
|
|
197
|
+
Math.max(0.0001, ...arms.value.map((arm) => arm.conversionRate)),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const weights = computed(() => {
|
|
201
|
+
const total =
|
|
202
|
+
definition.value.variations.reduce(
|
|
203
|
+
(sum, arm) => sum + Math.max(arm.weight, 0),
|
|
204
|
+
0,
|
|
205
|
+
) || 1
|
|
206
|
+
return definition.value.variations.map(
|
|
207
|
+
(arm) => (Math.max(arm.weight, 0) / total) * PERCENT,
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const srmLine = computed(() => {
|
|
212
|
+
const srm = experiment.value?.srm
|
|
213
|
+
if (!srm || totalExposed.value === 0) {
|
|
214
|
+
return ''
|
|
215
|
+
}
|
|
216
|
+
const observed = arms.value
|
|
217
|
+
.map((arm) =>
|
|
218
|
+
formatNumber(
|
|
219
|
+
(arm.exposedSessions / totalExposed.value) * PERCENT,
|
|
220
|
+
locale.value,
|
|
221
|
+
1,
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
.join(' / ')
|
|
225
|
+
const expected = weights.value
|
|
226
|
+
.map((weight) => formatNumber(weight, locale.value, 0))
|
|
227
|
+
.join(' / ')
|
|
228
|
+
return t(
|
|
229
|
+
srm.mismatch
|
|
230
|
+
? 'page.marketing.experiment.srm_failed'
|
|
231
|
+
: 'page.marketing.experiment.srm_passed',
|
|
232
|
+
{ observed, expected },
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
function stepShare(arm: MarketingVariationResult, stepIndex: number): number {
|
|
237
|
+
const sessions = arm.computation.steps[stepIndex]?.sessions ?? 0
|
|
238
|
+
return arm.exposedSessions > 0
|
|
239
|
+
? (sessions / arm.exposedSessions) * PERCENT
|
|
240
|
+
: 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function stepLift(
|
|
244
|
+
arm: MarketingVariationResult,
|
|
245
|
+
stepIndex: number,
|
|
246
|
+
): number | null {
|
|
247
|
+
const base = control.value ? stepShare(control.value, stepIndex) : 0
|
|
248
|
+
return base > 0 ? ((stepShare(arm, stepIndex) - base) / base) * PERCENT : null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The builder's snippet: no top-level await, comments in the reader's language.
|
|
252
|
+
const snippet = computed(() =>
|
|
253
|
+
variationSnippet(
|
|
254
|
+
definition.value.key,
|
|
255
|
+
definition.value.variations[0]?.key ?? 'control',
|
|
256
|
+
definition.value.variations.slice(1).map((arm) => arm.key),
|
|
257
|
+
localizedSnippetComments(t),
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const withheldCounts = computed(() =>
|
|
262
|
+
arms.value.map((arm, index) => ({
|
|
263
|
+
key: arm.key,
|
|
264
|
+
letter: letter(index),
|
|
265
|
+
exposed: arm.exposedSessions,
|
|
266
|
+
goal: goalSessions(arm),
|
|
267
|
+
})),
|
|
268
|
+
)
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<template>
|
|
272
|
+
<div class="flex flex-col gap-4" :class="loading ? 'opacity-80' : ''">
|
|
273
|
+
<DmsBanner
|
|
274
|
+
v-if="srmMismatch"
|
|
275
|
+
tone="error"
|
|
276
|
+
icon="i-ph-scales"
|
|
277
|
+
:title="t('page.marketing.experiment.srm_title')"
|
|
278
|
+
:description="srmLine"
|
|
279
|
+
/>
|
|
280
|
+
<DmsBanner
|
|
281
|
+
v-if="results.truncated"
|
|
282
|
+
tone="warning"
|
|
283
|
+
icon="i-ph-hourglass"
|
|
284
|
+
:title="t('page.marketing.experiment.truncated_title')"
|
|
285
|
+
:description="t('page.marketing.experiment.truncated_description')"
|
|
286
|
+
/>
|
|
287
|
+
|
|
288
|
+
<section v-if="noExposure" class="dms-card flex flex-col gap-3 p-5">
|
|
289
|
+
<DmsStatusPill
|
|
290
|
+
:tone="stopped ? 'neutral' : 'warning'"
|
|
291
|
+
icon="i-ph-eye-slash"
|
|
292
|
+
:mono="false"
|
|
293
|
+
:label="t('page.marketing.experiment.no_exposure_badge')"
|
|
294
|
+
/>
|
|
295
|
+
<h2 class="text-lg font-semibold text-highlighted">
|
|
296
|
+
{{
|
|
297
|
+
t('page.marketing.experiment.no_exposure_title', {
|
|
298
|
+
website: websiteName,
|
|
299
|
+
})
|
|
300
|
+
}}
|
|
301
|
+
</h2>
|
|
302
|
+
<p class="text-sm text-muted">
|
|
303
|
+
{{
|
|
304
|
+
t('page.marketing.experiment.no_exposure_description', {
|
|
305
|
+
key: definition.key,
|
|
306
|
+
})
|
|
307
|
+
}}
|
|
308
|
+
</p>
|
|
309
|
+
</section>
|
|
310
|
+
|
|
311
|
+
<section v-else-if="insufficient" class="dms-card flex flex-col gap-3 p-5">
|
|
312
|
+
<DmsStatusPill
|
|
313
|
+
tone="neutral"
|
|
314
|
+
icon="i-ph-hourglass"
|
|
315
|
+
:mono="false"
|
|
316
|
+
:label="t('page.marketing.experiment.insufficient_badge')"
|
|
317
|
+
/>
|
|
318
|
+
<h2 class="text-lg font-semibold text-highlighted">
|
|
319
|
+
{{ t('page.marketing.experiment.insufficient_title') }}
|
|
320
|
+
</h2>
|
|
321
|
+
<p class="text-sm text-muted">
|
|
322
|
+
{{
|
|
323
|
+
t('page.marketing.experiment.insufficient_description', {
|
|
324
|
+
count: MIN_OUTCOMES,
|
|
325
|
+
})
|
|
326
|
+
}}
|
|
327
|
+
</p>
|
|
328
|
+
<ul class="flex flex-col gap-1.5">
|
|
329
|
+
<li
|
|
330
|
+
v-for="arm in withheldCounts"
|
|
331
|
+
:key="arm.key"
|
|
332
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-0.5 font-mono text-sm"
|
|
333
|
+
>
|
|
334
|
+
<span
|
|
335
|
+
class="grid size-5 place-items-center rounded text-[10px] font-semibold"
|
|
336
|
+
:class="armStyle(arm.letter.charCodeAt(0) - LETTER_A).chip"
|
|
337
|
+
>
|
|
338
|
+
{{ arm.letter }}
|
|
339
|
+
</span>
|
|
340
|
+
<span class="w-28 truncate text-highlighted">{{ arm.key }}</span>
|
|
341
|
+
<span class="text-muted">
|
|
342
|
+
{{
|
|
343
|
+
t('page.marketing.experiment.raw_counts', {
|
|
344
|
+
exposed: formatNumber(arm.exposed, locale),
|
|
345
|
+
goal: formatNumber(arm.goal, locale),
|
|
346
|
+
})
|
|
347
|
+
}}
|
|
348
|
+
</span>
|
|
349
|
+
</li>
|
|
350
|
+
</ul>
|
|
351
|
+
</section>
|
|
352
|
+
|
|
353
|
+
<section
|
|
354
|
+
v-else
|
|
355
|
+
class="dms-card grid overflow-hidden p-0 lg:grid-cols-[minmax(0,1.6fr)_repeat(3,minmax(0,1fr))]"
|
|
356
|
+
>
|
|
357
|
+
<div
|
|
358
|
+
class="flex flex-col gap-3 p-5"
|
|
359
|
+
:class="
|
|
360
|
+
verdictKind === 'winner'
|
|
361
|
+
? 'bg-success/5'
|
|
362
|
+
: verdictKind === 'loser'
|
|
363
|
+
? 'bg-error/5'
|
|
364
|
+
: ''
|
|
365
|
+
"
|
|
366
|
+
>
|
|
367
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
368
|
+
<DmsStatusPill
|
|
369
|
+
:tone="
|
|
370
|
+
verdictKind === 'winner'
|
|
371
|
+
? 'success'
|
|
372
|
+
: verdictKind === 'loser'
|
|
373
|
+
? 'error'
|
|
374
|
+
: 'neutral'
|
|
375
|
+
"
|
|
376
|
+
:icon="
|
|
377
|
+
verdictKind === 'flat' || verdictKind === 'withheld'
|
|
378
|
+
? 'i-ph-minus-circle'
|
|
379
|
+
: 'i-ph-check-circle'
|
|
380
|
+
"
|
|
381
|
+
:mono="false"
|
|
382
|
+
:label="
|
|
383
|
+
t(
|
|
384
|
+
verdictKind === 'flat' || verdictKind === 'withheld'
|
|
385
|
+
? 'page.marketing.experiment.not_significant'
|
|
386
|
+
: 'page.marketing.experiment.significant',
|
|
387
|
+
{ level },
|
|
388
|
+
)
|
|
389
|
+
"
|
|
390
|
+
/>
|
|
391
|
+
<DmsStatusPill
|
|
392
|
+
v-if="verdictKind === 'winner' && definition.status === 'running'"
|
|
393
|
+
tone="neutral"
|
|
394
|
+
:mono="false"
|
|
395
|
+
:label="t('page.marketing.experiment.ready')"
|
|
396
|
+
/>
|
|
397
|
+
<DmsStatusPill
|
|
398
|
+
v-else-if="verdictKind === 'early'"
|
|
399
|
+
tone="info"
|
|
400
|
+
icon="i-ph-hourglass"
|
|
401
|
+
:mono="false"
|
|
402
|
+
:label="t('page.marketing.experiment.too_early')"
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
<h2 class="text-xl font-semibold text-balance text-highlighted">
|
|
406
|
+
{{ headline }}
|
|
407
|
+
</h2>
|
|
408
|
+
<p class="text-sm text-muted">{{ explanation }}</p>
|
|
409
|
+
<ul class="mt-1 flex flex-col gap-2">
|
|
410
|
+
<li
|
|
411
|
+
v-for="(arm, index) in arms"
|
|
412
|
+
:key="arm.key"
|
|
413
|
+
class="grid grid-cols-[1.25rem_minmax(0,6rem)_minmax(0,1fr)_4rem] items-center gap-2 sm:grid-cols-[1.25rem_7rem_minmax(0,1fr)_4rem] sm:gap-3"
|
|
414
|
+
>
|
|
415
|
+
<span
|
|
416
|
+
class="grid size-5 place-items-center rounded font-mono text-[10px] font-semibold"
|
|
417
|
+
:class="armStyle(index).chip"
|
|
418
|
+
>
|
|
419
|
+
{{ letter(index) }}
|
|
420
|
+
</span>
|
|
421
|
+
<span class="truncate font-mono text-[13px] text-highlighted">
|
|
422
|
+
{{ arm.key }}
|
|
423
|
+
</span>
|
|
424
|
+
<span class="h-2 overflow-hidden rounded-full bg-elevated">
|
|
425
|
+
<span
|
|
426
|
+
class="block h-full rounded-full"
|
|
427
|
+
:class="armStyle(index).bar"
|
|
428
|
+
:style="{ width: `${(arm.conversionRate / maxRate) * 100}%` }"
|
|
429
|
+
/>
|
|
430
|
+
</span>
|
|
431
|
+
<span
|
|
432
|
+
class="text-right font-mono text-[13px] font-semibold tabular-nums text-highlighted"
|
|
433
|
+
>
|
|
434
|
+
{{
|
|
435
|
+
formatPercentage(arm.conversionRate, locale, fractionDigits(2))
|
|
436
|
+
}}
|
|
437
|
+
</span>
|
|
438
|
+
</li>
|
|
439
|
+
</ul>
|
|
440
|
+
</div>
|
|
441
|
+
<div
|
|
442
|
+
class="flex flex-col gap-1 border-t border-default p-5 lg:border-t-0 lg:border-l"
|
|
443
|
+
>
|
|
444
|
+
<DmsEyebrow :label="t('page.marketing.experiment.lift')" />
|
|
445
|
+
<span
|
|
446
|
+
class="text-3xl font-semibold tabular-nums"
|
|
447
|
+
:class="
|
|
448
|
+
verdictKind === 'winner'
|
|
449
|
+
? 'text-success'
|
|
450
|
+
: verdictKind === 'loser'
|
|
451
|
+
? 'text-error'
|
|
452
|
+
: 'text-highlighted'
|
|
453
|
+
"
|
|
454
|
+
>
|
|
455
|
+
{{ best ? signed(lift) : '—' }}
|
|
456
|
+
</span>
|
|
457
|
+
<span class="font-mono text-[11px] text-dimmed">
|
|
458
|
+
{{
|
|
459
|
+
t('page.marketing.experiment.lift_hint', {
|
|
460
|
+
letter: letter(bestIndex),
|
|
461
|
+
})
|
|
462
|
+
}}
|
|
463
|
+
</span>
|
|
464
|
+
<span v-if="interval" class="font-mono text-[11px] text-muted">
|
|
465
|
+
{{ interval }}
|
|
466
|
+
</span>
|
|
467
|
+
</div>
|
|
468
|
+
<div
|
|
469
|
+
class="flex flex-col gap-1 border-t border-default p-5 lg:border-t-0 lg:border-l"
|
|
470
|
+
>
|
|
471
|
+
<DmsEyebrow :label="t('page.marketing.experiment.p_value')" />
|
|
472
|
+
<span class="text-3xl font-semibold tabular-nums text-highlighted">
|
|
473
|
+
{{ pValueText }}
|
|
474
|
+
</span>
|
|
475
|
+
<span class="font-mono text-[11px] text-dimmed">
|
|
476
|
+
{{
|
|
477
|
+
t('page.marketing.experiment.p_value_hint', {
|
|
478
|
+
alpha: formatNumber(experiment?.alpha ?? 0.05, locale, 3).replace(
|
|
479
|
+
/0+$/,
|
|
480
|
+
'',
|
|
481
|
+
),
|
|
482
|
+
})
|
|
483
|
+
}}
|
|
484
|
+
</span>
|
|
485
|
+
</div>
|
|
486
|
+
<div
|
|
487
|
+
class="flex flex-col gap-1 border-t border-default p-5 lg:border-t-0 lg:border-l"
|
|
488
|
+
>
|
|
489
|
+
<DmsEyebrow :label="t('page.marketing.experiment.exposed')" />
|
|
490
|
+
<span class="text-3xl font-semibold tabular-nums text-highlighted">
|
|
491
|
+
{{ formatNumber(totalExposed, locale) }}
|
|
492
|
+
</span>
|
|
493
|
+
<span class="font-mono text-[11px] text-dimmed">
|
|
494
|
+
{{
|
|
495
|
+
arms
|
|
496
|
+
.map(
|
|
497
|
+
(arm, index) =>
|
|
498
|
+
`${formatNumber(arm.exposedSessions, locale)} ${letter(index)}`,
|
|
499
|
+
)
|
|
500
|
+
.join(' · ')
|
|
501
|
+
}}
|
|
502
|
+
</span>
|
|
503
|
+
<span v-if="decision" class="font-mono text-[11px] text-muted">
|
|
504
|
+
{{
|
|
505
|
+
t(
|
|
506
|
+
'page.marketing.experiment.days_served',
|
|
507
|
+
{ days: Math.floor(decision.days) },
|
|
508
|
+
Math.floor(decision.days),
|
|
509
|
+
)
|
|
510
|
+
}}
|
|
511
|
+
</span>
|
|
512
|
+
</div>
|
|
513
|
+
<footer
|
|
514
|
+
v-if="srmLine && !srmMismatch"
|
|
515
|
+
class="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-default px-5 py-2.5 text-xs text-dimmed lg:col-span-4"
|
|
516
|
+
>
|
|
517
|
+
<span class="flex items-center gap-1.5">
|
|
518
|
+
<UIcon name="i-ph-scales" class="size-3.5" />
|
|
519
|
+
{{ srmLine }}
|
|
520
|
+
</span>
|
|
521
|
+
</footer>
|
|
522
|
+
</section>
|
|
523
|
+
|
|
524
|
+
<div class="grid gap-4 lg:grid-cols-[minmax(0,1.7fr)_minmax(0,1fr)]">
|
|
525
|
+
<section class="dms-card min-w-0 overflow-hidden p-0">
|
|
526
|
+
<header
|
|
527
|
+
class="flex items-center justify-between gap-3 border-b border-default px-4 py-3"
|
|
528
|
+
>
|
|
529
|
+
<DmsEyebrow :label="t('page.marketing.experiment.steps_title')" />
|
|
530
|
+
<span class="flex gap-1">
|
|
531
|
+
<span
|
|
532
|
+
v-for="(arm, index) in arms"
|
|
533
|
+
:key="arm.key"
|
|
534
|
+
class="grid size-5 place-items-center rounded font-mono text-[10px] font-semibold"
|
|
535
|
+
:class="armStyle(index).chip"
|
|
536
|
+
>
|
|
537
|
+
{{ letter(index) }}
|
|
538
|
+
</span>
|
|
539
|
+
</span>
|
|
540
|
+
</header>
|
|
541
|
+
<ol class="divide-y divide-default">
|
|
542
|
+
<!-- Phone: the step on its own line, the arms under it full width.
|
|
543
|
+
From md: the step beside its arms. -->
|
|
544
|
+
<li
|
|
545
|
+
v-for="(step, stepIndex) in results.funnel.steps"
|
|
546
|
+
:key="stepIndex"
|
|
547
|
+
class="grid grid-cols-[2rem_minmax(0,1fr)] items-center gap-x-3 gap-y-2 px-4 py-3 md:grid-cols-[2rem_minmax(8rem,14rem)_minmax(0,1fr)] md:gap-x-4"
|
|
548
|
+
>
|
|
549
|
+
<span
|
|
550
|
+
class="grid size-7 place-items-center rounded-md font-mono text-xs"
|
|
551
|
+
:class="
|
|
552
|
+
stepIndex === results.funnel.steps.length - 1
|
|
553
|
+
? 'bg-primary text-inverted'
|
|
554
|
+
: 'bg-elevated text-toned'
|
|
555
|
+
"
|
|
556
|
+
>
|
|
557
|
+
<UIcon
|
|
558
|
+
v-if="stepIndex === results.funnel.steps.length - 1"
|
|
559
|
+
name="i-ph-flag-checkered"
|
|
560
|
+
class="size-3.5"
|
|
561
|
+
/>
|
|
562
|
+
<template v-else>{{ stepIndex + 1 }}</template>
|
|
563
|
+
</span>
|
|
564
|
+
<span class="min-w-0">
|
|
565
|
+
<span
|
|
566
|
+
class="block truncate font-mono text-[13px] text-highlighted"
|
|
567
|
+
:title="step.value"
|
|
568
|
+
>
|
|
569
|
+
{{ step.value }}
|
|
570
|
+
</span>
|
|
571
|
+
<span class="flex items-center gap-1 text-xs text-dimmed">
|
|
572
|
+
{{ t(`page.marketing.funnel.kind.${step.kind}`) }}
|
|
573
|
+
<template v-if="step.kind === 'url'">
|
|
574
|
+
·
|
|
575
|
+
<DmsAutoLink
|
|
576
|
+
:to="inContext(pagesLink(step.value))"
|
|
577
|
+
class="text-primary hover:underline"
|
|
578
|
+
>
|
|
579
|
+
{{ t('page.marketing.funnel.see_clicks') }}
|
|
580
|
+
</DmsAutoLink>
|
|
581
|
+
</template>
|
|
582
|
+
</span>
|
|
583
|
+
</span>
|
|
584
|
+
<span class="col-span-2 flex flex-col gap-1.5 md:col-span-1">
|
|
585
|
+
<span
|
|
586
|
+
v-for="(arm, index) in arms"
|
|
587
|
+
:key="arm.key"
|
|
588
|
+
class="grid grid-cols-[1.25rem_minmax(0,1fr)_auto] items-center gap-2"
|
|
589
|
+
>
|
|
590
|
+
<span
|
|
591
|
+
class="grid size-5 place-items-center rounded font-mono text-[10px] font-semibold"
|
|
592
|
+
:class="armStyle(index).chip"
|
|
593
|
+
>
|
|
594
|
+
{{ letter(index) }}
|
|
595
|
+
</span>
|
|
596
|
+
<span class="h-2 overflow-hidden rounded-full bg-elevated">
|
|
597
|
+
<span
|
|
598
|
+
class="block h-full rounded-full"
|
|
599
|
+
:class="armStyle(index).bar"
|
|
600
|
+
:style="{ width: `${stepShare(arm, stepIndex)}%` }"
|
|
601
|
+
/>
|
|
602
|
+
</span>
|
|
603
|
+
<!-- Nobody exposed to the arm: no share to give, not 0 %. -->
|
|
604
|
+
<span
|
|
605
|
+
v-if="arm.exposedSessions === 0"
|
|
606
|
+
class="min-w-[7.5rem] text-right font-mono text-[11px] text-dimmed"
|
|
607
|
+
>
|
|
608
|
+
—
|
|
609
|
+
</span>
|
|
610
|
+
<span
|
|
611
|
+
v-else
|
|
612
|
+
class="min-w-[7.5rem] text-right font-mono text-[11px] whitespace-nowrap tabular-nums"
|
|
613
|
+
>
|
|
614
|
+
<span class="font-semibold text-highlighted">
|
|
615
|
+
{{
|
|
616
|
+
formatNumber(
|
|
617
|
+
arm.computation.steps[stepIndex]?.sessions ?? 0,
|
|
618
|
+
locale,
|
|
619
|
+
)
|
|
620
|
+
}}
|
|
621
|
+
</span>
|
|
622
|
+
<span class="ms-1 text-dimmed">
|
|
623
|
+
{{
|
|
624
|
+
formatPercentage(
|
|
625
|
+
stepShare(arm, stepIndex) / 100,
|
|
626
|
+
locale,
|
|
627
|
+
fractionDigits(stepShare(arm, stepIndex) < 10 ? 2 : 1),
|
|
628
|
+
)
|
|
629
|
+
}}
|
|
630
|
+
</span>
|
|
631
|
+
<span
|
|
632
|
+
v-if="
|
|
633
|
+
index > 0 &&
|
|
634
|
+
stepIndex > 0 &&
|
|
635
|
+
stepLift(arm, stepIndex) !== null
|
|
636
|
+
"
|
|
637
|
+
class="ms-1 text-toned"
|
|
638
|
+
>
|
|
639
|
+
{{ signed(stepLift(arm, stepIndex) ?? 0, 0) }}
|
|
640
|
+
</span>
|
|
641
|
+
</span>
|
|
642
|
+
</span>
|
|
643
|
+
</span>
|
|
644
|
+
</li>
|
|
645
|
+
</ol>
|
|
646
|
+
<footer
|
|
647
|
+
class="flex items-start gap-1.5 border-t border-default px-4 py-2.5 text-xs text-dimmed"
|
|
648
|
+
>
|
|
649
|
+
<UIcon name="i-ph-info" class="mt-0.5 size-3.5 shrink-0" />
|
|
650
|
+
{{ t('page.marketing.experiment.colours_note') }}
|
|
651
|
+
</footer>
|
|
652
|
+
</section>
|
|
653
|
+
|
|
654
|
+
<div class="flex min-w-0 flex-col gap-4">
|
|
655
|
+
<section class="dms-card p-0">
|
|
656
|
+
<header
|
|
657
|
+
class="flex items-center justify-between border-b border-default px-4 py-3"
|
|
658
|
+
>
|
|
659
|
+
<DmsEyebrow :label="t('page.marketing.experiment.variations')" />
|
|
660
|
+
<DmsStatusPill
|
|
661
|
+
size="sm"
|
|
662
|
+
icon="i-ph-lock-simple"
|
|
663
|
+
:mono="false"
|
|
664
|
+
:label="t('page.marketing.experiment.frozen')"
|
|
665
|
+
/>
|
|
666
|
+
</header>
|
|
667
|
+
<ul class="flex flex-col gap-2 px-4 py-3">
|
|
668
|
+
<li
|
|
669
|
+
v-for="(arm, index) in definition.variations"
|
|
670
|
+
:key="arm.key"
|
|
671
|
+
class="flex items-center gap-3"
|
|
672
|
+
>
|
|
673
|
+
<span
|
|
674
|
+
class="grid size-5 place-items-center rounded font-mono text-[10px] font-semibold"
|
|
675
|
+
:class="armStyle(index).chip"
|
|
676
|
+
>
|
|
677
|
+
{{ letter(index) }}
|
|
678
|
+
</span>
|
|
679
|
+
<span class="truncate font-mono text-sm text-highlighted">
|
|
680
|
+
{{ arm.key }}
|
|
681
|
+
</span>
|
|
682
|
+
<span
|
|
683
|
+
v-if="index === 0"
|
|
684
|
+
class="font-mono text-[11px] text-dimmed"
|
|
685
|
+
>
|
|
686
|
+
{{ t('page.marketing.experiment.control_arm') }}
|
|
687
|
+
</span>
|
|
688
|
+
<span class="ms-auto font-mono text-sm text-muted">
|
|
689
|
+
{{
|
|
690
|
+
formatPercentage(
|
|
691
|
+
(weights[index] ?? 0) / 100,
|
|
692
|
+
locale,
|
|
693
|
+
fractionDigits(0),
|
|
694
|
+
)
|
|
695
|
+
}}
|
|
696
|
+
</span>
|
|
697
|
+
</li>
|
|
698
|
+
</ul>
|
|
699
|
+
</section>
|
|
700
|
+
<section class="dms-card p-0">
|
|
701
|
+
<header class="border-b border-default px-4 py-3">
|
|
702
|
+
<DmsEyebrow
|
|
703
|
+
:label="
|
|
704
|
+
t(
|
|
705
|
+
stopped
|
|
706
|
+
? 'page.marketing.experiment.on_site_stopped'
|
|
707
|
+
: 'page.marketing.experiment.on_site',
|
|
708
|
+
)
|
|
709
|
+
"
|
|
710
|
+
/>
|
|
711
|
+
</header>
|
|
712
|
+
<div class="flex flex-col gap-2 p-4">
|
|
713
|
+
<p v-if="stopped" class="text-xs text-muted">
|
|
714
|
+
{{
|
|
715
|
+
t('page.marketing.experiment.on_site_stopped_hint', {
|
|
716
|
+
key: definition.key,
|
|
717
|
+
})
|
|
718
|
+
}}
|
|
719
|
+
</p>
|
|
720
|
+
<div class="overflow-hidden rounded-lg border border-default">
|
|
721
|
+
<div
|
|
722
|
+
class="flex items-center justify-between border-b border-default px-3 py-1.5"
|
|
723
|
+
>
|
|
724
|
+
<DmsEyebrow label="JavaScript" />
|
|
725
|
+
<DmsCopyButton :value="snippet" />
|
|
726
|
+
</div>
|
|
727
|
+
<pre
|
|
728
|
+
class="px-3 py-2.5 font-mono text-xs leading-relaxed break-all whitespace-pre-wrap text-toned"
|
|
729
|
+
>{{ snippet }}</pre>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
</section>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</template>
|