@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
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, watch } from 'vue'
|
|
3
|
-
import { useI18n } from '#dms/frontend-module'
|
|
4
|
-
import { MS_PER_DAY } from '../constants'
|
|
5
|
-
import { useChildId } from '../composables/useChildId'
|
|
6
|
-
import { useLatestRequest } from '../composables/useLatestRequest'
|
|
7
|
-
import {
|
|
8
|
-
useMarketingApi,
|
|
9
|
-
type MarketingOverview,
|
|
10
|
-
} from '../composables/useMarketingApi'
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_PERIOD_PRESET,
|
|
13
|
-
MARKETING_PERIOD_PRESETS,
|
|
14
|
-
periodDays,
|
|
15
|
-
useMarketingPeriod,
|
|
16
|
-
} from '../composables/useMarketingPeriod'
|
|
17
|
-
import {
|
|
18
|
-
campaignsLink,
|
|
19
|
-
funnelsLink,
|
|
20
|
-
pagesLink,
|
|
21
|
-
} from '../composables/useMarketingRoutes'
|
|
22
|
-
import { useMarketingWebsites } from '../composables/useMarketingWebsites'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Overview dashboard: period-scoped traffic area chart, device donut and
|
|
26
|
-
* the top dimensions grouped into four cards — content, acquisition,
|
|
27
|
-
* audience, events — each a tabbed stack of top lists linking out to the
|
|
28
|
-
* surface that owns the detail (pages, campaigns, funnels). All of it is
|
|
29
|
-
* fed by one rollup-backed overview fetch. Chart/KPI primitives come from
|
|
30
|
-
* the dms-ui layer so the look is inherited, not ported; UTM sources have
|
|
31
|
-
* no card here on purpose — the campaigns table is their surface.
|
|
32
|
-
*/
|
|
33
|
-
const PERIOD_SCOPE = 'dms-marketing-overview'
|
|
34
|
-
const TOP_LIST_LIMIT = 8
|
|
35
|
-
|
|
36
|
-
// The DMS page renderer hands every component its identity; keeping it is what
|
|
37
|
-
// lets dms-ui primitives key their events and persisted state per child.
|
|
38
|
-
const props = withDefaults(
|
|
39
|
-
defineProps<{ componentId?: string, pageId?: string }>(),
|
|
40
|
-
{ componentId: 'marketing-overview', pageId: 'marketing' },
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const childId = useChildId(() => props.componentId)
|
|
44
|
-
|
|
45
|
-
const api = useMarketingApi()
|
|
46
|
-
const { t, te, locale } = useI18n()
|
|
47
|
-
|
|
48
|
-
const {
|
|
49
|
-
websites,
|
|
50
|
-
items: websiteItems,
|
|
51
|
-
selectedId,
|
|
52
|
-
loading: loadingWebsites,
|
|
53
|
-
failed: websitesFailed,
|
|
54
|
-
load: loadWebsites,
|
|
55
|
-
create: createWebsite,
|
|
56
|
-
showSnippet,
|
|
57
|
-
} = useMarketingWebsites()
|
|
58
|
-
|
|
59
|
-
// Period comes from the DMS-standard <DmsPeriodSelector>, read back through
|
|
60
|
-
// its scope registry and mapped onto the `Nd` windows the backend accepts.
|
|
61
|
-
const period = useMarketingPeriod(PERIOD_SCOPE)
|
|
62
|
-
|
|
63
|
-
const windowLength = computed(() => periodDays(period.value))
|
|
64
|
-
|
|
65
|
-
const {
|
|
66
|
-
data: overview,
|
|
67
|
-
loading: loadingOverview,
|
|
68
|
-
failed: overviewFailed,
|
|
69
|
-
settled: overviewSettled,
|
|
70
|
-
run: loadOverview,
|
|
71
|
-
} = useLatestRequest<MarketingOverview>(() =>
|
|
72
|
-
// Server-side there is no session to authenticate with: the fetch belongs to
|
|
73
|
-
// the client, like every other read here.
|
|
74
|
-
selectedId.value && !import.meta.env.SSR
|
|
75
|
-
? api.getOverview(selectedId.value, period.value)
|
|
76
|
-
: null,
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
// The watcher owns every overview fetch, including the first: seeding it from
|
|
80
|
-
// onMounted as well would issue the initial request twice. The endpoint lists
|
|
81
|
-
// websites by last activity, so the default selection is the one that has
|
|
82
|
-
// something to show. Immediate costs nothing while the selection starts empty
|
|
83
|
-
// — the fetcher just answers null — and is what keeps this correct the day a
|
|
84
|
-
// deep link seeds it: resolving the site list would then write the same id
|
|
85
|
-
// back, which is no change to watch, and a non-immediate watcher would never
|
|
86
|
-
// fetch at all.
|
|
87
|
-
watch([selectedId, period], () => {
|
|
88
|
-
void loadOverview()
|
|
89
|
-
}, { immediate: true })
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The window between a site being selected and the watcher firing on the next
|
|
93
|
-
* tick counts as loading: dropping the flag there would flash an empty
|
|
94
|
-
* dashboard between the two fetches.
|
|
95
|
-
*/
|
|
96
|
-
const loading = computed(
|
|
97
|
-
() =>
|
|
98
|
-
loadingWebsites.value
|
|
99
|
-
|| loadingOverview.value
|
|
100
|
-
|| (!!selectedId.value && !overviewSettled.value),
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
// Two failures, two flags: retrying the overview must not clear the one that
|
|
104
|
-
// says the site list itself never loaded.
|
|
105
|
-
const failed = computed(() => websitesFailed.value || overviewFailed.value)
|
|
106
|
-
|
|
107
|
-
/** Retry entry point: whichever of the two fetches has nothing to show. */
|
|
108
|
-
function reload() {
|
|
109
|
-
return websites.value.length === 0 ? loadWebsites() : loadOverview()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
onMounted(loadWebsites)
|
|
113
|
-
|
|
114
|
-
// --- KPIs -------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
/** "1m 32s" the way Plausible writes it — locale-neutral abbreviations. */
|
|
117
|
-
function formatDuration(ms: number): string {
|
|
118
|
-
const totalSeconds = Math.round(ms / 1000)
|
|
119
|
-
const hours = Math.floor(totalSeconds / 3600)
|
|
120
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
|
121
|
-
if (hours > 0) {
|
|
122
|
-
return `${hours}h ${minutes}m`
|
|
123
|
-
}
|
|
124
|
-
const seconds = totalSeconds % 60
|
|
125
|
-
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface OverviewKpi {
|
|
129
|
-
id: string
|
|
130
|
-
label: string
|
|
131
|
-
icon: string
|
|
132
|
-
/** Rendered by DmsKpiCard when numeric… */
|
|
133
|
-
value?: number
|
|
134
|
-
format?: 'number' | 'percent'
|
|
135
|
-
/** …and by the local text card when already composed (duration). */
|
|
136
|
-
display?: string
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const kpis = computed<OverviewKpi[]>(() => {
|
|
140
|
-
const totals = overview.value?.totals
|
|
141
|
-
const sessions = totals?.sessions ?? 0
|
|
142
|
-
// Transition-counted bounces can sum below zero on windows cutting a
|
|
143
|
-
// session in two; a negative rate is never worth showing.
|
|
144
|
-
const bounceRate = sessions > 0
|
|
145
|
-
? (Math.max(0, totals?.bouncedSessions ?? 0) / sessions) * 100
|
|
146
|
-
: 0
|
|
147
|
-
const pagesPerSession = sessions > 0
|
|
148
|
-
? Math.round(((totals?.pageviews ?? 0) / sessions) * 10) / 10
|
|
149
|
-
: 0
|
|
150
|
-
const avgDuration = sessions > 0 ? (totals?.sessionDurationMs ?? 0) / sessions : 0
|
|
151
|
-
return [
|
|
152
|
-
{ id: 'pageviews', label: t('page.marketing.overview.kpis.pageviews'), value: totals?.pageviews ?? 0, format: 'number', icon: 'i-ph-eye' },
|
|
153
|
-
{ id: 'sessions', label: t('page.marketing.overview.kpis.sessions'), value: totals?.sessions ?? 0, format: 'number', icon: 'i-ph-users' },
|
|
154
|
-
{ id: 'new-visitors', label: t('page.marketing.overview.kpis.new_visitors'), value: totals?.newVisitors ?? 0, format: 'number', icon: 'i-ph-user-plus' },
|
|
155
|
-
{ id: 'custom-events', label: t('page.marketing.overview.kpis.custom_events'), value: totals?.customEvents ?? 0, format: 'number', icon: 'i-ph-cursor-click' },
|
|
156
|
-
{ id: 'bounce-rate', label: t('page.marketing.overview.kpis.bounce_rate'), value: bounceRate, format: 'percent', icon: 'i-ph-arrow-u-up-left' },
|
|
157
|
-
{ id: 'avg-session-duration', label: t('page.marketing.overview.kpis.avg_session_duration'), display: formatDuration(avgDuration), icon: 'i-ph-timer' },
|
|
158
|
-
{ id: 'pages-per-session', label: t('page.marketing.overview.kpis.pages_per_session'), value: pagesPerSession, format: 'number', icon: 'i-ph-stack' },
|
|
159
|
-
]
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// --- Traffic chart ----------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
function utcMidnightToday(): number {
|
|
165
|
-
const now = new Date()
|
|
166
|
-
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Every day of the window, so days without traffic plot at zero. */
|
|
170
|
-
const windowDays = computed(() => {
|
|
171
|
-
const today = utcMidnightToday()
|
|
172
|
-
const count = windowLength.value
|
|
173
|
-
return Array.from({ length: count }, (_, i) => today - (count - 1 - i) * MS_PER_DAY)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const trafficSeries = computed(() => {
|
|
177
|
-
const days = overview.value?.days ?? []
|
|
178
|
-
const byDay = new Map(days.map(d => [d.day, d]))
|
|
179
|
-
const point = (day: number, pick: (v: { pageviews: number, sessions: number }) => number) => ({
|
|
180
|
-
x: new Date(day),
|
|
181
|
-
y: pick(byDay.get(day) ?? { pageviews: 0, sessions: 0 }),
|
|
182
|
-
})
|
|
183
|
-
return [
|
|
184
|
-
{
|
|
185
|
-
name: t('page.marketing.overview.traffic.pageviews'),
|
|
186
|
-
data: windowDays.value.map(day => point(day, v => v.pageviews)),
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: t('page.marketing.overview.traffic.sessions'),
|
|
190
|
-
data: windowDays.value.map(day => point(day, v => v.sessions)),
|
|
191
|
-
},
|
|
192
|
-
]
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
const hasTraffic = computed(() => (overview.value?.totals.pageviews ?? 0) > 0)
|
|
196
|
-
|
|
197
|
-
// --- Devices donut ----------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
const deviceRecords = computed(() =>
|
|
200
|
-
Object.entries(overview.value?.topDevices ?? {})
|
|
201
|
-
.sort((a, b) => b[1] - a[1])
|
|
202
|
-
.map(([label, value]) => ({ label, value })),
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
// --- Top lists --------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* `linkTo` is only passed for pages: a path has a surface of its own (its
|
|
209
|
-
* heatmap), while a referrer or a browser has nowhere to lead.
|
|
210
|
-
*/
|
|
211
|
-
function toTopItems(
|
|
212
|
-
record: Record<string, number> | undefined,
|
|
213
|
-
linkTo?: (key: string) => ReturnType<typeof pagesLink>,
|
|
214
|
-
) {
|
|
215
|
-
return Object.entries(record ?? {})
|
|
216
|
-
.sort((a, b) => b[1] - a[1])
|
|
217
|
-
.slice(0, TOP_LIST_LIMIT)
|
|
218
|
-
.map(([label, value]) => ({
|
|
219
|
-
id: label,
|
|
220
|
-
title: label,
|
|
221
|
-
value,
|
|
222
|
-
...(linkTo ? { to: linkTo(label) } : {}),
|
|
223
|
-
}))
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const regionNames = computed(() => new Intl.DisplayNames([locale.value], { type: 'region' }))
|
|
227
|
-
|
|
228
|
-
/** Rollup keys are ISO codes but historical map entries can be anything, and
|
|
229
|
-
* `DisplayNames.of` throws on a malformed region code. */
|
|
230
|
-
function countryLabel(code: string): string {
|
|
231
|
-
try {
|
|
232
|
-
return regionNames.value.of(code) ?? code
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
return code
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const languageNames = computed(() => new Intl.DisplayNames([locale.value], { type: 'language' }))
|
|
240
|
-
|
|
241
|
-
/** Same guard as countries: keys are whatever `navigator.language` reported. */
|
|
242
|
-
function languageLabel(tag: string): string {
|
|
243
|
-
try {
|
|
244
|
-
return languageNames.value.of(tag) ?? tag
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return tag
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Channel keys localize on the campaigns page's labels; a key minted outside
|
|
252
|
-
* the known five still shows raw. */
|
|
253
|
-
function channelLabel(channel: string): string {
|
|
254
|
-
const key = `page.marketing.campaigns.channels.${channel}`
|
|
255
|
-
return te(key) ? t(key) : channel
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const groupCards = computed(() => {
|
|
259
|
-
const toPage = (path: string) =>
|
|
260
|
-
pagesLink({ website: selectedId.value, path, period: period.value })
|
|
261
|
-
const label = (key: string) => t(`page.marketing.overview.groups.${key}`)
|
|
262
|
-
return [
|
|
263
|
-
// Entry and exit are paths too, so every row leads to the page surface.
|
|
264
|
-
{
|
|
265
|
-
id: 'content',
|
|
266
|
-
title: label('content.title'),
|
|
267
|
-
stateKey: 'content_tab',
|
|
268
|
-
footerLabel: label('content.view_all'),
|
|
269
|
-
footerTo: pagesLink({ website: selectedId.value, period: period.value }),
|
|
270
|
-
tabs: [
|
|
271
|
-
{ id: 'pages', label: label('content.tabs.pages'), items: toTopItems(overview.value?.topPages, toPage) },
|
|
272
|
-
{ id: 'entry', label: label('content.tabs.entry'), items: toTopItems(overview.value?.topEntryPages, toPage) },
|
|
273
|
-
{ id: 'exit', label: label('content.tabs.exit'), items: toTopItems(overview.value?.topExitPages, toPage) },
|
|
274
|
-
],
|
|
275
|
-
},
|
|
276
|
-
// No UTM tab: the campaigns table owns that detail, the footer leads there.
|
|
277
|
-
{
|
|
278
|
-
id: 'acquisition',
|
|
279
|
-
title: label('acquisition.title'),
|
|
280
|
-
stateKey: 'acquisition_tab',
|
|
281
|
-
footerLabel: label('acquisition.view_all'),
|
|
282
|
-
footerTo: campaignsLink({ website: selectedId.value, period: period.value }),
|
|
283
|
-
tabs: [
|
|
284
|
-
{
|
|
285
|
-
id: 'channels',
|
|
286
|
-
label: label('acquisition.tabs.channels'),
|
|
287
|
-
items: toTopItems(overview.value?.topChannels).map(item =>
|
|
288
|
-
({ ...item, title: channelLabel(item.title) })),
|
|
289
|
-
},
|
|
290
|
-
{ id: 'referrers', label: label('acquisition.tabs.referrers'), items: toTopItems(overview.value?.topReferrers) },
|
|
291
|
-
],
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
id: 'audience',
|
|
295
|
-
title: label('audience.title'),
|
|
296
|
-
stateKey: 'audience_tab',
|
|
297
|
-
tabs: [
|
|
298
|
-
{ id: 'browsers', label: label('audience.tabs.browsers'), items: toTopItems(overview.value?.topBrowsers) },
|
|
299
|
-
{
|
|
300
|
-
id: 'languages',
|
|
301
|
-
label: label('audience.tabs.languages'),
|
|
302
|
-
items: toTopItems(overview.value?.topLanguages).map(item =>
|
|
303
|
-
({ ...item, title: languageLabel(item.title) })),
|
|
304
|
-
},
|
|
305
|
-
// Without a GeoIP database the map can only ever be empty — no tab
|
|
306
|
-
// at all beats a permanently empty one.
|
|
307
|
-
...(overview.value?.geoipEnabled
|
|
308
|
-
? [{
|
|
309
|
-
id: 'countries',
|
|
310
|
-
label: label('audience.tabs.countries'),
|
|
311
|
-
items: toTopItems(overview.value?.topCountries).map(item =>
|
|
312
|
-
({ ...item, title: countryLabel(item.title) })),
|
|
313
|
-
}]
|
|
314
|
-
: []),
|
|
315
|
-
],
|
|
316
|
-
},
|
|
317
|
-
// The names behind the custom-events KPI: which CTA gets clicked.
|
|
318
|
-
{
|
|
319
|
-
id: 'events',
|
|
320
|
-
title: label('events.title'),
|
|
321
|
-
footerLabel: label('events.view_all'),
|
|
322
|
-
footerTo: funnelsLink({ website: selectedId.value, period: period.value }),
|
|
323
|
-
tabs: [
|
|
324
|
-
{ id: 'events', label: label('events.title'), items: toTopItems(overview.value?.topEvents) },
|
|
325
|
-
],
|
|
326
|
-
},
|
|
327
|
-
]
|
|
328
|
-
})
|
|
329
|
-
</script>
|
|
330
|
-
|
|
331
|
-
<template>
|
|
332
|
-
<div class="space-y-6 p-6">
|
|
333
|
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
334
|
-
<div>
|
|
335
|
-
<h1 class="text-xl font-semibold">{{ t('page.marketing.overview.title') }}</h1>
|
|
336
|
-
<p class="text-sm text-muted">{{ t('page.marketing.overview.subtitle') }}</p>
|
|
337
|
-
</div>
|
|
338
|
-
<div class="flex items-center gap-3">
|
|
339
|
-
<USelect
|
|
340
|
-
v-if="websites.length > 1"
|
|
341
|
-
v-model="selectedId"
|
|
342
|
-
:items="websiteItems"
|
|
343
|
-
class="w-56"
|
|
344
|
-
/>
|
|
345
|
-
<UButton
|
|
346
|
-
v-if="selectedId"
|
|
347
|
-
icon="i-ph-code"
|
|
348
|
-
color="neutral"
|
|
349
|
-
variant="ghost"
|
|
350
|
-
size="sm"
|
|
351
|
-
:label="t('page.marketing.websites.snippet.button')"
|
|
352
|
-
@click="showSnippet"
|
|
353
|
-
/>
|
|
354
|
-
<UButton
|
|
355
|
-
v-if="websites.length > 0"
|
|
356
|
-
icon="i-ph-plus"
|
|
357
|
-
color="neutral"
|
|
358
|
-
variant="ghost"
|
|
359
|
-
size="sm"
|
|
360
|
-
:label="t('page.marketing.websites.create')"
|
|
361
|
-
@click="createWebsite"
|
|
362
|
-
/>
|
|
363
|
-
<DmsPeriodSelector
|
|
364
|
-
:id="PERIOD_SCOPE"
|
|
365
|
-
:component-id="childId('period')"
|
|
366
|
-
:page-id="props.pageId"
|
|
367
|
-
:default-preset="DEFAULT_PERIOD_PRESET"
|
|
368
|
-
:presets="MARKETING_PERIOD_PRESETS"
|
|
369
|
-
:comparisons="['none']"
|
|
370
|
-
:show-range-label="false"
|
|
371
|
-
/>
|
|
372
|
-
<UButton
|
|
373
|
-
icon="i-ph-arrow-clockwise"
|
|
374
|
-
color="neutral"
|
|
375
|
-
variant="subtle"
|
|
376
|
-
size="sm"
|
|
377
|
-
:loading="loading"
|
|
378
|
-
@click="loadOverview"
|
|
379
|
-
/>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
|
|
383
|
-
<div v-if="failed" class="flex flex-col items-start gap-3">
|
|
384
|
-
<UAlert
|
|
385
|
-
color="error"
|
|
386
|
-
variant="subtle"
|
|
387
|
-
icon="i-ph-warning-circle"
|
|
388
|
-
:title="t('page.marketing.overview.error.title')"
|
|
389
|
-
:description="t('page.marketing.overview.error.description')"
|
|
390
|
-
/>
|
|
391
|
-
<UButton
|
|
392
|
-
icon="i-ph-arrow-clockwise"
|
|
393
|
-
:label="t('page.marketing.overview.retry')"
|
|
394
|
-
color="neutral"
|
|
395
|
-
variant="subtle"
|
|
396
|
-
size="sm"
|
|
397
|
-
@click="reload"
|
|
398
|
-
/>
|
|
399
|
-
</div>
|
|
400
|
-
|
|
401
|
-
<DmsMarketingNoWebsiteState
|
|
402
|
-
v-else-if="!loading && websites.length === 0"
|
|
403
|
-
icon="i-ph-chart-line-up"
|
|
404
|
-
:title="t('page.marketing.overview.empty.title')"
|
|
405
|
-
:description="t('page.marketing.overview.empty.description')"
|
|
406
|
-
@create="createWebsite"
|
|
407
|
-
/>
|
|
408
|
-
|
|
409
|
-
<!-- Only before the first answer: a refetch keeps the dashboard on screen
|
|
410
|
-
and spins the refresh button instead. -->
|
|
411
|
-
<USkeleton v-else-if="loading && !overview" class="h-96 w-full" />
|
|
412
|
-
|
|
413
|
-
<template v-else>
|
|
414
|
-
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
415
|
-
<template v-for="kpi in kpis" :key="kpi.id">
|
|
416
|
-
<DmsMarketingTextKpiCard
|
|
417
|
-
v-if="kpi.display !== undefined"
|
|
418
|
-
:title="kpi.label"
|
|
419
|
-
:value="kpi.display"
|
|
420
|
-
:icon="kpi.icon"
|
|
421
|
-
/>
|
|
422
|
-
<DmsKpiCard
|
|
423
|
-
v-else
|
|
424
|
-
:component-id="childId(`kpi-${kpi.id}`)"
|
|
425
|
-
:page-id="props.pageId"
|
|
426
|
-
variant="stat"
|
|
427
|
-
:title="kpi.label"
|
|
428
|
-
:icon="kpi.icon"
|
|
429
|
-
:static-value="kpi.value"
|
|
430
|
-
:show-delta="false"
|
|
431
|
-
:value-format="kpi.format"
|
|
432
|
-
/>
|
|
433
|
-
</template>
|
|
434
|
-
</div>
|
|
435
|
-
|
|
436
|
-
<div class="grid gap-4 lg:grid-cols-3">
|
|
437
|
-
<UCard class="lg:col-span-2">
|
|
438
|
-
<template #header>
|
|
439
|
-
<div>
|
|
440
|
-
<p class="font-medium">{{ t('page.marketing.overview.traffic.title') }}</p>
|
|
441
|
-
<p class="text-sm text-muted">{{ t('page.marketing.overview.traffic.subtitle') }}</p>
|
|
442
|
-
</div>
|
|
443
|
-
</template>
|
|
444
|
-
<DmsChart
|
|
445
|
-
v-if="hasTraffic"
|
|
446
|
-
:component-id="childId('traffic')"
|
|
447
|
-
:page-id="props.pageId"
|
|
448
|
-
type="area"
|
|
449
|
-
:static-dataset="trafficSeries"
|
|
450
|
-
:smooth="true"
|
|
451
|
-
:show-legend="true"
|
|
452
|
-
xaxis-type="datetime"
|
|
453
|
-
height="300px"
|
|
454
|
-
/>
|
|
455
|
-
<p v-else class="py-12 text-center text-sm text-muted">
|
|
456
|
-
{{ t('page.marketing.overview.traffic.empty') }}
|
|
457
|
-
</p>
|
|
458
|
-
</UCard>
|
|
459
|
-
|
|
460
|
-
<UCard>
|
|
461
|
-
<template #header>
|
|
462
|
-
<p class="font-medium">{{ t('page.marketing.overview.devices.title') }}</p>
|
|
463
|
-
</template>
|
|
464
|
-
<DmsChart
|
|
465
|
-
v-if="deviceRecords.length > 0"
|
|
466
|
-
:component-id="childId('devices')"
|
|
467
|
-
:page-id="props.pageId"
|
|
468
|
-
type="donut"
|
|
469
|
-
:static-dataset="deviceRecords"
|
|
470
|
-
height="300px"
|
|
471
|
-
:show-total="true"
|
|
472
|
-
/>
|
|
473
|
-
<p v-else class="py-12 text-center text-sm text-muted">
|
|
474
|
-
{{ t('page.marketing.overview.devices.empty') }}
|
|
475
|
-
</p>
|
|
476
|
-
</UCard>
|
|
477
|
-
</div>
|
|
478
|
-
|
|
479
|
-
<div class="grid gap-4 md:grid-cols-2">
|
|
480
|
-
<DmsMarketingTopListTabsCard
|
|
481
|
-
v-for="card in groupCards"
|
|
482
|
-
:key="card.id"
|
|
483
|
-
:title="card.title"
|
|
484
|
-
:tabs="card.tabs"
|
|
485
|
-
:state-key="card.stateKey"
|
|
486
|
-
:empty-label="t('page.marketing.overview.groups.empty')"
|
|
487
|
-
:footer-label="card.footerLabel"
|
|
488
|
-
:footer-to="card.footerTo"
|
|
489
|
-
/>
|
|
490
|
-
</div>
|
|
491
|
-
</template>
|
|
492
|
-
</div>
|
|
493
|
-
</template>
|