@antelopejs/dms-marketing 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +143 -0
  3. package/dist/config.d.ts +13 -0
  4. package/dist/config.js +35 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/data-api/body.d.ts +1 -0
  7. package/dist/data-api/body.js +15 -0
  8. package/dist/data-api/body.js.map +1 -0
  9. package/dist/data-api/funnels.d.ts +32 -0
  10. package/dist/data-api/funnels.js +302 -0
  11. package/dist/data-api/funnels.js.map +1 -0
  12. package/dist/data-api/hidden-filter.d.ts +1 -0
  13. package/dist/data-api/hidden-filter.js +11 -0
  14. package/dist/data-api/hidden-filter.js.map +1 -0
  15. package/dist/data-api/index.d.ts +4 -0
  16. package/dist/data-api/index.js +21 -0
  17. package/dist/data-api/index.js.map +1 -0
  18. package/dist/data-api/tenant-scope.d.ts +15 -0
  19. package/dist/data-api/tenant-scope.js +43 -0
  20. package/dist/data-api/tenant-scope.js.map +1 -0
  21. package/dist/data-api/websites.d.ts +25 -0
  22. package/dist/data-api/websites.js +73 -0
  23. package/dist/data-api/websites.js.map +1 -0
  24. package/dist/db/index.d.ts +2 -0
  25. package/dist/db/index.js +19 -0
  26. package/dist/db/index.js.map +1 -0
  27. package/dist/db/models/funnels.model.d.ts +25 -0
  28. package/dist/db/models/funnels.model.js +39 -0
  29. package/dist/db/models/funnels.model.js.map +1 -0
  30. package/dist/db/models/index.d.ts +7 -0
  31. package/dist/db/models/index.js +24 -0
  32. package/dist/db/models/index.js.map +1 -0
  33. package/dist/db/models/marketing_events.model.d.ts +49 -0
  34. package/dist/db/models/marketing_events.model.js +85 -0
  35. package/dist/db/models/marketing_events.model.js.map +1 -0
  36. package/dist/db/models/marketing_sessions.model.d.ts +33 -0
  37. package/dist/db/models/marketing_sessions.model.js +50 -0
  38. package/dist/db/models/marketing_sessions.model.js.map +1 -0
  39. package/dist/db/models/marketing_settings.model.d.ts +23 -0
  40. package/dist/db/models/marketing_settings.model.js +13 -0
  41. package/dist/db/models/marketing_settings.model.js.map +1 -0
  42. package/dist/db/models/page_snapshots.model.d.ts +27 -0
  43. package/dist/db/models/page_snapshots.model.js +32 -0
  44. package/dist/db/models/page_snapshots.model.js.map +1 -0
  45. package/dist/db/models/retention.d.ts +6 -0
  46. package/dist/db/models/retention.js +26 -0
  47. package/dist/db/models/retention.js.map +1 -0
  48. package/dist/db/models/website_statistics.model.d.ts +31 -0
  49. package/dist/db/models/website_statistics.model.js +63 -0
  50. package/dist/db/models/website_statistics.model.js.map +1 -0
  51. package/dist/db/models/websites.model.d.ts +26 -0
  52. package/dist/db/models/websites.model.js +27 -0
  53. package/dist/db/models/websites.model.js.map +1 -0
  54. package/dist/db/tables/funnels.table.d.ts +31 -0
  55. package/dist/db/tables/funnels.table.js +54 -0
  56. package/dist/db/tables/funnels.table.js.map +1 -0
  57. package/dist/db/tables/index.d.ts +7 -0
  58. package/dist/db/tables/index.js +24 -0
  59. package/dist/db/tables/index.js.map +1 -0
  60. package/dist/db/tables/marketing_events.table.d.ts +15 -0
  61. package/dist/db/tables/marketing_events.table.js +66 -0
  62. package/dist/db/tables/marketing_events.table.js.map +1 -0
  63. package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
  64. package/dist/db/tables/marketing_sessions.table.js +115 -0
  65. package/dist/db/tables/marketing_sessions.table.js.map +1 -0
  66. package/dist/db/tables/marketing_settings.table.d.ts +11 -0
  67. package/dist/db/tables/marketing_settings.table.js +51 -0
  68. package/dist/db/tables/marketing_settings.table.js.map +1 -0
  69. package/dist/db/tables/page_snapshots.table.d.ts +17 -0
  70. package/dist/db/tables/page_snapshots.table.js +72 -0
  71. package/dist/db/tables/page_snapshots.table.js.map +1 -0
  72. package/dist/db/tables/website_statistics.table.d.ts +14 -0
  73. package/dist/db/tables/website_statistics.table.js +60 -0
  74. package/dist/db/tables/website_statistics.table.js.map +1 -0
  75. package/dist/db/tables/websites.table.d.ts +13 -0
  76. package/dist/db/tables/websites.table.js +60 -0
  77. package/dist/db/tables/websites.table.js.map +1 -0
  78. package/dist/hooks/index.d.ts +1 -0
  79. package/dist/hooks/index.js +39 -0
  80. package/dist/hooks/index.js.map +1 -0
  81. package/dist/implementations/dms-marketing/index.d.ts +2 -0
  82. package/dist/implementations/dms-marketing/index.js +8 -0
  83. package/dist/implementations/dms-marketing/index.js.map +1 -0
  84. package/dist/index.d.ts +7 -0
  85. package/dist/index.js +89 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/pages/all.d.ts +5 -0
  88. package/dist/pages/all.js +22 -0
  89. package/dist/pages/all.js.map +1 -0
  90. package/dist/pages/campaigns/page.d.ts +9 -0
  91. package/dist/pages/campaigns/page.js +27 -0
  92. package/dist/pages/campaigns/page.js.map +1 -0
  93. package/dist/pages/funnels/page.d.ts +9 -0
  94. package/dist/pages/funnels/page.js +27 -0
  95. package/dist/pages/funnels/page.js.map +1 -0
  96. package/dist/pages/index.d.ts +4 -0
  97. package/dist/pages/index.js +21 -0
  98. package/dist/pages/index.js.map +1 -0
  99. package/dist/pages/module.d.ts +1 -0
  100. package/dist/pages/module.js +13 -0
  101. package/dist/pages/module.js.map +1 -0
  102. package/dist/pages/overview/page.d.ts +9 -0
  103. package/dist/pages/overview/page.js +27 -0
  104. package/dist/pages/overview/page.js.map +1 -0
  105. package/dist/pages/pages/page.d.ts +9 -0
  106. package/dist/pages/pages/page.js +27 -0
  107. package/dist/pages/pages/page.js.map +1 -0
  108. package/dist/pages/settings/form.d.ts +3 -0
  109. package/dist/pages/settings/form.js +35 -0
  110. package/dist/pages/settings/form.js.map +1 -0
  111. package/dist/pages/settings/page.d.ts +9 -0
  112. package/dist/pages/settings/page.js +27 -0
  113. package/dist/pages/settings/page.js.map +1 -0
  114. package/dist/routes/collect.d.ts +7 -0
  115. package/dist/routes/collect.js +62 -0
  116. package/dist/routes/collect.js.map +1 -0
  117. package/dist/routes/experiments.d.ts +6 -0
  118. package/dist/routes/experiments.js +63 -0
  119. package/dist/routes/experiments.js.map +1 -0
  120. package/dist/routes/funnels.d.ts +16 -0
  121. package/dist/routes/funnels.js +44 -0
  122. package/dist/routes/funnels.js.map +1 -0
  123. package/dist/routes/index.d.ts +8 -0
  124. package/dist/routes/index.js +25 -0
  125. package/dist/routes/index.js.map +1 -0
  126. package/dist/routes/settings.d.ts +8 -0
  127. package/dist/routes/settings.js +50 -0
  128. package/dist/routes/settings.js.map +1 -0
  129. package/dist/routes/snapshot.d.ts +10 -0
  130. package/dist/routes/snapshot.js +147 -0
  131. package/dist/routes/snapshot.js.map +1 -0
  132. package/dist/routes/stats.d.ts +44 -0
  133. package/dist/routes/stats.js +278 -0
  134. package/dist/routes/stats.js.map +1 -0
  135. package/dist/routes/tracker.d.ts +6 -0
  136. package/dist/routes/tracker.js +57 -0
  137. package/dist/routes/tracker.js.map +1 -0
  138. package/dist/routes/websites.d.ts +13 -0
  139. package/dist/routes/websites.js +144 -0
  140. package/dist/routes/websites.js.map +1 -0
  141. package/dist/services/acquisition.d.ts +15 -0
  142. package/dist/services/acquisition.js +153 -0
  143. package/dist/services/acquisition.js.map +1 -0
  144. package/dist/services/bounded-cache.d.ts +14 -0
  145. package/dist/services/bounded-cache.js +45 -0
  146. package/dist/services/bounded-cache.js.map +1 -0
  147. package/dist/services/collect.d.ts +11 -0
  148. package/dist/services/collect.js +193 -0
  149. package/dist/services/collect.js.map +1 -0
  150. package/dist/services/experiment-definitions.d.ts +4 -0
  151. package/dist/services/experiment-definitions.js +36 -0
  152. package/dist/services/experiment-definitions.js.map +1 -0
  153. package/dist/services/experiments/assign.d.ts +5 -0
  154. package/dist/services/experiments/assign.js +52 -0
  155. package/dist/services/experiments/assign.js.map +1 -0
  156. package/dist/services/experiments/index.d.ts +3 -0
  157. package/dist/services/experiments/index.js +20 -0
  158. package/dist/services/experiments/index.js.map +1 -0
  159. package/dist/services/experiments/results.d.ts +20 -0
  160. package/dist/services/experiments/results.js +91 -0
  161. package/dist/services/experiments/results.js.map +1 -0
  162. package/dist/services/experiments/stats.d.ts +12 -0
  163. package/dist/services/experiments/stats.js +62 -0
  164. package/dist/services/experiments/stats.js.map +1 -0
  165. package/dist/services/funnels/compute.d.ts +15 -0
  166. package/dist/services/funnels/compute.js +68 -0
  167. package/dist/services/funnels/compute.js.map +1 -0
  168. package/dist/services/funnels/index.d.ts +2 -0
  169. package/dist/services/funnels/index.js +19 -0
  170. package/dist/services/funnels/index.js.map +1 -0
  171. package/dist/services/funnels/results.d.ts +14 -0
  172. package/dist/services/funnels/results.js +72 -0
  173. package/dist/services/funnels/results.js.map +1 -0
  174. package/dist/services/geoip.d.ts +3 -0
  175. package/dist/services/geoip.js +38 -0
  176. package/dist/services/geoip.js.map +1 -0
  177. package/dist/services/heatmap.d.ts +10 -0
  178. package/dist/services/heatmap.js +180 -0
  179. package/dist/services/heatmap.js.map +1 -0
  180. package/dist/services/index.d.ts +26 -0
  181. package/dist/services/index.js +43 -0
  182. package/dist/services/index.js.map +1 -0
  183. package/dist/services/ingest-guard.d.ts +11 -0
  184. package/dist/services/ingest-guard.js +44 -0
  185. package/dist/services/ingest-guard.js.map +1 -0
  186. package/dist/services/keyed-queue.d.ts +4 -0
  187. package/dist/services/keyed-queue.js +19 -0
  188. package/dist/services/keyed-queue.js.map +1 -0
  189. package/dist/services/origin-guard.d.ts +5 -0
  190. package/dist/services/origin-guard.js +43 -0
  191. package/dist/services/origin-guard.js.map +1 -0
  192. package/dist/services/period.d.ts +2 -0
  193. package/dist/services/period.js +19 -0
  194. package/dist/services/period.js.map +1 -0
  195. package/dist/services/prune.d.ts +7 -0
  196. package/dist/services/prune.js +49 -0
  197. package/dist/services/prune.js.map +1 -0
  198. package/dist/services/query-param.d.ts +1 -0
  199. package/dist/services/query-param.js +11 -0
  200. package/dist/services/query-param.js.map +1 -0
  201. package/dist/services/request-body.d.ts +2 -0
  202. package/dist/services/request-body.js +41 -0
  203. package/dist/services/request-body.js.map +1 -0
  204. package/dist/services/request-meta.d.ts +3 -0
  205. package/dist/services/request-meta.js +19 -0
  206. package/dist/services/request-meta.js.map +1 -0
  207. package/dist/services/rollup-write.d.ts +4 -0
  208. package/dist/services/rollup-write.js +84 -0
  209. package/dist/services/rollup-write.js.map +1 -0
  210. package/dist/services/sessionize.d.ts +23 -0
  211. package/dist/services/sessionize.js +78 -0
  212. package/dist/services/sessionize.js.map +1 -0
  213. package/dist/services/settings.d.ts +6 -0
  214. package/dist/services/settings.js +113 -0
  215. package/dist/services/settings.js.map +1 -0
  216. package/dist/services/snapshots.d.ts +24 -0
  217. package/dist/services/snapshots.js +139 -0
  218. package/dist/services/snapshots.js.map +1 -0
  219. package/dist/services/tenant-funnel.d.ts +3 -0
  220. package/dist/services/tenant-funnel.js +17 -0
  221. package/dist/services/tenant-funnel.js.map +1 -0
  222. package/dist/services/tenant-website.d.ts +5 -0
  223. package/dist/services/tenant-website.js +34 -0
  224. package/dist/services/tenant-website.js.map +1 -0
  225. package/dist/services/url.d.ts +10 -0
  226. package/dist/services/url.js +81 -0
  227. package/dist/services/url.js.map +1 -0
  228. package/dist/services/user-agent.d.ts +2 -0
  229. package/dist/services/user-agent.js +17 -0
  230. package/dist/services/user-agent.js.map +1 -0
  231. package/dist/services/visitor-id.d.ts +1 -0
  232. package/dist/services/visitor-id.js +19 -0
  233. package/dist/services/visitor-id.js.map +1 -0
  234. package/dist/services/website-cache.d.ts +3 -0
  235. package/dist/services/website-cache.js +22 -0
  236. package/dist/services/website-cache.js.map +1 -0
  237. package/dist/types/constants.d.ts +74 -0
  238. package/dist/types/constants.js +79 -0
  239. package/dist/types/constants.js.map +1 -0
  240. package/dist/types/events.d.ts +114 -0
  241. package/dist/types/events.js +29 -0
  242. package/dist/types/events.js.map +1 -0
  243. package/dist/types/index.d.ts +5 -0
  244. package/dist/types/index.js +22 -0
  245. package/dist/types/index.js.map +1 -0
  246. package/dist/types/settings.d.ts +67 -0
  247. package/dist/types/settings.js +58 -0
  248. package/dist/types/settings.js.map +1 -0
  249. package/dist/types/snapshots.d.ts +95 -0
  250. package/dist/types/snapshots.js +41 -0
  251. package/dist/types/snapshots.js.map +1 -0
  252. package/dist/types/statistics.d.ts +38 -0
  253. package/dist/types/statistics.js +33 -0
  254. package/dist/types/statistics.js.map +1 -0
  255. package/dist/utils/experiment-type.d.ts +52 -0
  256. package/dist/utils/experiment-type.js +94 -0
  257. package/dist/utils/experiment-type.js.map +1 -0
  258. package/dist/utils/funnel-steps-type.d.ts +42 -0
  259. package/dist/utils/funnel-steps-type.js +62 -0
  260. package/dist/utils/funnel-steps-type.js.map +1 -0
  261. package/frontend-vue/.editorconfig +12 -0
  262. package/frontend-vue/.prettierrc +7 -0
  263. package/frontend-vue/app/components/CampaignsView.vue +330 -0
  264. package/frontend-vue/app/components/ExperimentInput.vue +128 -0
  265. package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
  266. package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
  267. package/frontend-vue/app/components/FunnelsView.vue +493 -0
  268. package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
  269. package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
  270. package/frontend-vue/app/components/OverviewView.vue +493 -0
  271. package/frontend-vue/app/components/PagePreview.vue +915 -0
  272. package/frontend-vue/app/components/PagesView.vue +378 -0
  273. package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
  274. package/frontend-vue/app/components/TextKpiCard.vue +28 -0
  275. package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
  276. package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
  277. package/frontend-vue/app/components/VariationsInput.vue +164 -0
  278. package/frontend-vue/app/composables/useChildId.ts +13 -0
  279. package/frontend-vue/app/composables/useExperiment.ts +59 -0
  280. package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
  281. package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
  282. package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
  283. package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
  284. package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
  285. package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
  286. package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
  287. package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
  288. package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
  289. package/frontend-vue/app/utils/dmsComponent.ts +18 -0
  290. package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
  291. package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
  292. package/frontend-vue/dms.frontend.ts +35 -0
  293. package/frontend-vue/eslint.config.mjs +16 -0
  294. package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
  295. package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
  296. package/frontend-vue/package.json +30 -0
  297. package/frontend-vue/pnpm-lock.yaml +5226 -0
  298. package/frontend-vue/pnpm-workspace.yaml +2 -0
  299. package/frontend-vue/tests/drafts.test.ts +54 -0
  300. package/frontend-vue/tests/i18n.test.ts +86 -0
  301. package/frontend-vue/tests/period.test.ts +35 -0
  302. package/frontend-vue/tests/registration.test.ts +63 -0
  303. package/frontend-vue/tests/routes.test.ts +31 -0
  304. package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
  305. package/frontend-vue/vitest.config.ts +20 -0
  306. package/package.json +91 -0
  307. package/static/snapshot.js +497 -0
  308. package/static/tracker.js +503 -0
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Frontend marketing facade.
3
+ *
4
+ * Single composable wrapping every /api/marketing/* fetch the module pages
5
+ * need — the one file to update when endpoint shapes change (same swap-point
6
+ * convention as dms-api's useApiIntrospection).
7
+ */
8
+
9
+ // --- Shapes mirrored from the backend ---------------------------------------
10
+
11
+ export interface MarketingWebsite {
12
+ _id: string
13
+ name: string
14
+ domain: string
15
+ trackingEnabled: boolean
16
+ /** Opt-in: page snapshots store page content. */
17
+ snapshotsEnabled: boolean
18
+ snapshotMaskText?: boolean
19
+ }
20
+
21
+ /** Editable website fields; only the keys present are touched. */
22
+ export type MarketingWebsitePatch = Partial<
23
+ Pick<MarketingWebsite, 'name' | 'domain' | 'snapshotsEnabled' | 'snapshotMaskText'>
24
+ >
25
+
26
+ export type MarketingSnapshotLayout = 'desktop' | 'tablet' | 'phone'
27
+
28
+ /**
29
+ * One captured rendering of a path: a self-contained HTML document the
30
+ * heatmap is drawn over, plus the geometry it was captured in.
31
+ */
32
+ export interface MarketingPageSnapshot {
33
+ layout: MarketingSnapshotLayout
34
+ /** False when the asked layout has no capture and the nearest stands in. */
35
+ exact: boolean
36
+ capturedAt: number
37
+ origin: string
38
+ viewport: { width: number, height: number }
39
+ document: { width: number, height: number }
40
+ colorScheme?: 'light' | 'dark'
41
+ bytes: number
42
+ html: string | null
43
+ }
44
+
45
+ export interface MarketingPageSnapshotResponse {
46
+ /** Null while no visit has captured the page yet. */
47
+ snapshot: MarketingPageSnapshot | null
48
+ }
49
+
50
+ /** One observed path of a tracked site, over the requested window. */
51
+ export interface MarketingPageEntry {
52
+ path: string
53
+ pageviews: number
54
+ }
55
+
56
+ export interface MarketingPages {
57
+ pages: MarketingPageEntry[]
58
+ /** The inventory hit its cap; narrow the search to see the rest. */
59
+ truncated: boolean
60
+ /** Share of visits capturing clicks — what makes a heatmap sparse. */
61
+ heatmapSampleRate: number
62
+ /** Global collection switch; off means nothing new is being recorded. */
63
+ trackerEnabled: boolean
64
+ }
65
+
66
+ /** The five-way acquisition grouping the backend derives from referrer
67
+ * domain + utm_medium at rollup time. */
68
+ export type MarketingChannel = 'direct' | 'organic' | 'social' | 'referral' | 'paid'
69
+
70
+ export interface MarketingDayStatistics {
71
+ day: number
72
+ pageviews: number
73
+ sessions: number
74
+ newVisitors: number
75
+ customEvents: number
76
+ /** Counted as transitions: a day un-bouncing sessions opened the day
77
+ * before can go negative. Clamp before showing a rate. */
78
+ bouncedSessions: number
79
+ sessionDurationMs: number
80
+ topPages: Record<string, number>
81
+ topEntryPages?: Record<string, number>
82
+ topExitPages?: Record<string, number>
83
+ topReferrers: Record<string, number>
84
+ topUtmSources: Record<string, number>
85
+ topUtmMediums?: Record<string, number>
86
+ topUtmCampaigns?: Record<string, number>
87
+ topUtmTerms?: Record<string, number>
88
+ topUtmContents?: Record<string, number>
89
+ topChannels?: Record<string, number>
90
+ topDevices?: Record<string, number>
91
+ topBrowsers?: Record<string, number>
92
+ topCountries?: Record<string, number>
93
+ topEvents?: Record<string, number>
94
+ topLanguages?: Record<string, number>
95
+ }
96
+
97
+ export interface MarketingOverviewTotals {
98
+ pageviews: number
99
+ sessions: number
100
+ newVisitors: number
101
+ customEvents: number
102
+ bouncedSessions: number
103
+ sessionDurationMs: number
104
+ }
105
+
106
+ export interface MarketingOverview {
107
+ totals: MarketingOverviewTotals
108
+ days: MarketingDayStatistics[]
109
+ topPages: Record<string, number>
110
+ topEntryPages: Record<string, number>
111
+ topExitPages: Record<string, number>
112
+ topReferrers: Record<string, number>
113
+ topUtmSources: Record<string, number>
114
+ topUtmMediums: Record<string, number>
115
+ topUtmCampaigns: Record<string, number>
116
+ topUtmTerms: Record<string, number>
117
+ topUtmContents: Record<string, number>
118
+ topChannels: Record<string, number>
119
+ topDevices: Record<string, number>
120
+ topBrowsers: Record<string, number>
121
+ /** ISO 3166-1 alpha-2 keys; only populated when the deployment configures
122
+ * a GeoIP database — `geoipEnabled` says whether the card should show. */
123
+ topCountries: Record<string, number>
124
+ /** Custom event names by trigger count — which CTA gets clicked. */
125
+ topEvents: Record<string, number>
126
+ /** BCP 47 tags as the visitors' browsers report them (`fr`, `fr-FR`). */
127
+ topLanguages: Record<string, number>
128
+ geoipEnabled: boolean
129
+ }
130
+
131
+ /** One (source, medium, campaign) triple observed on new sessions; a blank
132
+ * slot is a UTM parameter the landing URL did not carry. */
133
+ export interface MarketingCampaignEntry {
134
+ source?: string
135
+ medium?: string
136
+ campaign?: string
137
+ sessions: number
138
+ }
139
+
140
+ export interface MarketingCampaigns {
141
+ /** Exhaustive five-key split, not a top list. */
142
+ channels: Partial<Record<MarketingChannel, number>>
143
+ campaigns: MarketingCampaignEntry[]
144
+ /** The table hit its cap; narrow the search to see the rest. */
145
+ truncated: boolean
146
+ terms: Record<string, number>
147
+ contents: Record<string, number>
148
+ /** Referrer domains across all channels (search and social included). */
149
+ referrers?: Record<string, number>
150
+ }
151
+
152
+ export interface MarketingFunnelStep {
153
+ kind: 'url' | 'custom'
154
+ value: string
155
+ }
156
+
157
+ export type MarketingExperimentStatus = 'draft' | 'running' | 'stopped'
158
+
159
+ export interface MarketingExperimentVariation {
160
+ key: string
161
+ weight: number
162
+ }
163
+
164
+ /** One stretch of time the split served traffic; `stoppedAt` null while it
165
+ * still does. Written by the lifecycle transitions alone — the form sends
166
+ * them back untouched and the backend re-derives them. */
167
+ export interface MarketingExperimentRun {
168
+ startedAt: number
169
+ stoppedAt: number | null
170
+ }
171
+
172
+ /** The A/B facet of a funnel; the first variation is the control. */
173
+ export interface MarketingExperiment {
174
+ key: string
175
+ status: MarketingExperimentStatus
176
+ variations: MarketingExperimentVariation[]
177
+ /** Oldest first; more than one means the split was stopped and resumed.
178
+ * Empty exactly while the split is a draft. */
179
+ runs: MarketingExperimentRun[]
180
+ }
181
+
182
+ export interface MarketingFunnel {
183
+ _id: string
184
+ websiteId: string
185
+ name: string
186
+ steps: MarketingFunnelStep[]
187
+ conversionWindowMs: number
188
+ /** Null on a plain funnel. */
189
+ experiment?: MarketingExperiment | null
190
+ }
191
+
192
+ /** One row of the definitions list — the data-api's Listable fields. Steps
193
+ * and experiment may be JSON strings; read them through parseFunnelStepsValue
194
+ * and parseExperimentValue. The website relation comes back expanded by the
195
+ * list's foreign join — the write path wants the bare id again. */
196
+ export interface MarketingFunnelListItem {
197
+ _id: string
198
+ websiteId: string | { _id: string, name?: string, domain?: string }
199
+ name: string
200
+ steps: MarketingFunnelStep[] | string
201
+ conversionWindowHours: number
202
+ experiment?: MarketingExperiment | string | null
203
+ }
204
+
205
+ /** What the definitions form writes; hours because that is the field the
206
+ * data-api exposes (it owns the ms conversion). `experiment.status` is the
207
+ * transition asked for — it must be legal from the row's current one. */
208
+ export interface MarketingFunnelInput {
209
+ websiteId: string
210
+ name: string
211
+ steps: MarketingFunnelStep[]
212
+ /** Absent keeps the row's current window (edit) or the default (new). */
213
+ conversionWindowHours?: number
214
+ experiment?: MarketingExperiment | null
215
+ }
216
+
217
+ export interface MarketingFunnelStepResult {
218
+ step: MarketingFunnelStep
219
+ sessions: number
220
+ conversionRate: number
221
+ }
222
+
223
+ export interface MarketingFunnelComputation {
224
+ totalSessions: number
225
+ steps: MarketingFunnelStepResult[]
226
+ }
227
+
228
+ /** One arm's results. `computation` is the funnel shape, computed over the
229
+ * arm's exposed sessions; `conversionRate` divides the last step by
230
+ * `exposedSessions` — entering the funnel is itself an outcome. */
231
+ export interface MarketingVariationResult {
232
+ key: string
233
+ exposedSessions: number
234
+ computation: MarketingFunnelComputation
235
+ conversionRate: number
236
+ /** Relative to the control's conversion; null on the control itself. */
237
+ uplift: number | null
238
+ /** Two-sided p-value vs the control; null on the control, when the sample
239
+ * is too small, or when the window is truncated. */
240
+ pValue: number | null
241
+ significant: boolean | null
242
+ }
243
+
244
+ export interface MarketingExperimentSrm {
245
+ expected: number[]
246
+ observed: number[]
247
+ pValue: number | null
248
+ /** True when the observed split is too far from the configured weights —
249
+ * assignment or exposure reporting is suspect. */
250
+ mismatch: boolean
251
+ }
252
+
253
+ export interface MarketingExperimentResults {
254
+ /** Key of the control arm — the experiment's first variation. */
255
+ control: string
256
+ variations: MarketingVariationResult[]
257
+ srm: MarketingExperimentSrm
258
+ }
259
+
260
+ export interface MarketingFunnelResults {
261
+ funnel: MarketingFunnel
262
+ /** Over every session that entered the funnel, arms or not. */
263
+ computation: MarketingFunnelComputation
264
+ /** Present when the funnel carries an experiment past draft. */
265
+ experiment?: MarketingExperimentResults
266
+ /** The raw-event window hit its cap; verdicts are withheld. */
267
+ truncated: boolean
268
+ /** Epoch ms bounds the numbers cover: the selected period on a plain
269
+ * funnel, the experiment's own run once one is started — a running split
270
+ * ends at "now", a stopped one at its stop. */
271
+ window: { since: number, until: number }
272
+ }
273
+
274
+ /**
275
+ * Where the click landed inside the element it hit. This is what the overlay
276
+ * projects from when it can locate the element in the rendered page; `x`/`y`
277
+ * are the fallback, and they depend on the visitor's window on any page
278
+ * shorter than it.
279
+ */
280
+ export interface MarketingHeatmapAnchor {
281
+ selector: string
282
+ ox: number
283
+ oy: number
284
+ nth: number
285
+ }
286
+
287
+ export interface MarketingHeatmapPoint {
288
+ x: number
289
+ y: number
290
+ weight: number
291
+ /** Absent on clicks whose target was the document. */
292
+ anchor?: MarketingHeatmapAnchor
293
+ }
294
+
295
+ export interface MarketingHeatmap {
296
+ points: MarketingHeatmapPoint[]
297
+ maxWeight: number
298
+ totalClicks: number
299
+ truncated: boolean
300
+ }
301
+
302
+ /**
303
+ * Scroll-depth distribution of one path. `views` counts only sampled views
304
+ * that scrolled at all — a view that never scrolls (every page that fits its
305
+ * screen included) emits no depth — so shares read "of the views that
306
+ * scrolled", never "of all views".
307
+ */
308
+ export interface MarketingScrollDepth {
309
+ views: number
310
+ /** reached[d] = views whose max depth is ≥ d percent, d = 0..100. */
311
+ reached: number[]
312
+ truncated: boolean
313
+ }
314
+
315
+ // --- Facade -----------------------------------------------------------------
316
+
317
+ /** Far above any real tenant's funnel count — the data-api list answers 10
318
+ * rows unless told otherwise, and caps at 100 whatever is asked. */
319
+ const FUNNEL_LIST_LIMIT = 100
320
+
321
+ export function useMarketingApi() {
322
+ const { $authFetch } = useAuthFetch()
323
+
324
+ return {
325
+ listWebsites: () => $authFetch<MarketingWebsite[]>('/api/marketing/websites'),
326
+
327
+ createWebsite: (name: string, domain: string) =>
328
+ $authFetch<MarketingWebsite>('/api/marketing/websites', {
329
+ method: 'POST',
330
+ body: { name, domain },
331
+ }),
332
+
333
+ updateWebsite: (id: string, patch: MarketingWebsitePatch) =>
334
+ $authFetch<MarketingWebsite>(
335
+ `/api/marketing/websites/${encodeURIComponent(id)}`,
336
+ { method: 'PUT', body: patch },
337
+ ),
338
+
339
+ // Query strings via ofetch's `query` option — encoding is its job.
340
+ listPages: (website: string, period = '7d', search = '') =>
341
+ $authFetch<MarketingPages>('/api/marketing/stats/pages', {
342
+ query: { website, period, search },
343
+ }),
344
+
345
+ getOverview: (website: string, period = '7d') =>
346
+ $authFetch<MarketingOverview>('/api/marketing/stats/overview', {
347
+ query: { website, period },
348
+ }),
349
+
350
+ getCampaigns: (website: string, period = '7d', search = '') =>
351
+ $authFetch<MarketingCampaigns>('/api/marketing/stats/campaigns', {
352
+ query: { website, period, search },
353
+ }),
354
+
355
+ getHeatmap: (website: string, path: string, period = '7d') =>
356
+ $authFetch<MarketingHeatmap>('/api/marketing/stats/heatmap', {
357
+ query: { website, path, period },
358
+ }),
359
+
360
+ getScrollDepth: (website: string, path: string, period = '7d') =>
361
+ $authFetch<MarketingScrollDepth>('/api/marketing/stats/scroll', {
362
+ query: { website, path, period },
363
+ }),
364
+
365
+ getSnapshot: (website: string, path: string, layout: MarketingSnapshotLayout) =>
366
+ $authFetch<MarketingPageSnapshotResponse>('/api/marketing/stats/snapshot', {
367
+ query: { website, path, layout },
368
+ }),
369
+
370
+ // Whole click and scroll history, every period: without `path` the whole
371
+ // site's.
372
+ resetHeatmap: (website: string, path?: string) =>
373
+ $authFetch<{ deleted: number }>('/api/marketing/stats/heatmap', {
374
+ method: 'DELETE',
375
+ query: { website, path },
376
+ }),
377
+
378
+ getFunnelResults: (id: string, period = '7d') =>
379
+ $authFetch<MarketingFunnelResults>(
380
+ `/api/marketing/funnels/${encodeURIComponent(id)}/results`,
381
+ { query: { period } },
382
+ ),
383
+
384
+ // Definitions CRUD over the funnels data-api routes. `is` is the DMS
385
+ // DataType compare mode — the data-api's own `eq` parses but matches
386
+ // nothing. The list defaults to 10 rows, hence the explicit limit.
387
+ listFunnels: (website: string) =>
388
+ $authFetch<{ results: MarketingFunnelListItem[], total: number }>(
389
+ '/api/marketing/tables/funnels/list',
390
+ {
391
+ query: {
392
+ filter_websiteId: `is:${website}`,
393
+ limit: FUNNEL_LIST_LIMIT,
394
+ sortKey: 'createdAt',
395
+ sortDirection: 'asc',
396
+ },
397
+ },
398
+ ),
399
+
400
+ // The insert answers the generated keys, an array — not the row.
401
+ createFunnel: (input: MarketingFunnelInput) =>
402
+ $authFetch<string[]>('/api/marketing/tables/funnels/new', {
403
+ method: 'POST',
404
+ body: input,
405
+ }),
406
+
407
+ updateFunnel: (id: string, input: MarketingFunnelInput) =>
408
+ $authFetch<unknown>('/api/marketing/tables/funnels/edit', {
409
+ method: 'PUT',
410
+ query: { id },
411
+ body: input,
412
+ }),
413
+
414
+ deleteFunnel: (id: string) =>
415
+ $authFetch<unknown>('/api/marketing/tables/funnels/delete', {
416
+ method: 'DELETE',
417
+ query: { id },
418
+ }),
419
+ }
420
+ }
@@ -0,0 +1,66 @@
1
+ import { computed, type ComputedRef } from 'vue'
2
+
3
+ /**
4
+ * Bridge between the DMS period selector and the marketing endpoints.
5
+ *
6
+ * Every marketing read takes an `Nd` window anchored on now, while the
7
+ * selector publishes a from/to range into a scope registry. The conversion
8
+ * belongs in one place, not least because the deep links between those views
9
+ * carry the window as a query parameter and have to map it back onto a preset.
10
+ */
11
+
12
+ export const MS_PER_DAY = 86_400_000
13
+
14
+ export const DEFAULT_PERIOD_DAYS = 7
15
+ export const DEFAULT_PERIOD = `${DEFAULT_PERIOD_DAYS}d`
16
+
17
+ /** Presets every marketing surface offers, in selector order. */
18
+ export const MARKETING_PERIOD_PRESETS = [
19
+ 'today',
20
+ 'last-7-days',
21
+ 'last-30-days',
22
+ 'last-90-days',
23
+ ]
24
+
25
+ const PRESET_BY_DAYS: Record<number, string> = {
26
+ 1: 'today',
27
+ 7: 'last-7-days',
28
+ 30: 'last-30-days',
29
+ 90: 'last-90-days',
30
+ }
31
+
32
+ export const DEFAULT_PERIOD_PRESET = PRESET_BY_DAYS[DEFAULT_PERIOD_DAYS]
33
+
34
+ /** Day count of an `Nd` string, or the default for anything unparseable. */
35
+ export function periodDays(period: string | undefined): number {
36
+ const days = Number.parseInt(period ?? '', 10)
37
+ return Number.isFinite(days) && days > 0 ? days : DEFAULT_PERIOD_DAYS
38
+ }
39
+
40
+ /**
41
+ * Preset matching an `Nd` window. Only exact matches map back — an arbitrary
42
+ * window (a custom range someone linked from) falls back to the default rather
43
+ * than silently rounding to a neighbouring preset.
44
+ */
45
+ export function ndToPreset(period: string | undefined): string {
46
+ if (!period) {
47
+ return DEFAULT_PERIOD_PRESET
48
+ }
49
+ return PRESET_BY_DAYS[periodDays(period)] ?? DEFAULT_PERIOD_PRESET
50
+ }
51
+
52
+ /** The `Nd` window currently published by a period-selector scope. */
53
+ export function useMarketingPeriod(scope: string): ComputedRef<string> {
54
+ const periodScope = usePeriodScope(scope)
55
+
56
+ return computed(() => {
57
+ const range = periodScope.value?.range
58
+ if (!range) {
59
+ return DEFAULT_PERIOD
60
+ }
61
+ const days = Math.round(
62
+ (range.to.getTime() - range.from.getTime()) / MS_PER_DAY,
63
+ )
64
+ return `${Math.max(1, days)}d`
65
+ })
66
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Links between the module's surfaces.
3
+ *
4
+ * The `/modules/marketing` prefix comes from the page being registered under
5
+ * a DMS module, which also makes it platform-owner-only. Should the surfaces
6
+ * ever move to a tenant-reachable category, this file is the only place that
7
+ * has to know.
8
+ */
9
+
10
+ const MODULE_BASE = '/modules/marketing'
11
+
12
+ interface PagesLink {
13
+ website?: string | null
14
+ path?: string | null
15
+ period?: string | null
16
+ }
17
+
18
+ /** Query without the empty keys — a bare link must stay bare. */
19
+ function withQuery(base: string, query: Record<string, string | null | undefined>) {
20
+ const entries = Object.entries(query).filter(([, value]) => !!value)
21
+ return {
22
+ path: base,
23
+ query: Object.fromEntries(entries) as Record<string, string>,
24
+ }
25
+ }
26
+
27
+ /** The tracked-pages surface, optionally on a given site, path and window. */
28
+ export function pagesLink({ website, path, period }: PagesLink = {}) {
29
+ return withQuery(`${MODULE_BASE}/pages`, { website, path, period })
30
+ }
31
+
32
+ interface CampaignsLink {
33
+ website?: string | null
34
+ period?: string | null
35
+ }
36
+
37
+ /** The acquisition surface, optionally on a given site and window. */
38
+ export function campaignsLink({ website, period }: CampaignsLink = {}) {
39
+ return withQuery(`${MODULE_BASE}/campaigns`, { website, period })
40
+ }
41
+
42
+ interface FunnelsLink {
43
+ website?: string | null
44
+ funnel?: string | null
45
+ period?: string | null
46
+ }
47
+
48
+ /** The funnels surface, optionally on a given site, funnel and window. */
49
+ export function funnelsLink({ website, funnel, period }: FunnelsLink = {}) {
50
+ return withQuery(`${MODULE_BASE}/funnels`, { website, funnel, period })
51
+ }
@@ -0,0 +1,69 @@
1
+ import { useI18n } from '#dms/frontend-module'
2
+ import TrackerSnippet from '../components/TrackerSnippet.vue'
3
+ import { dmsComponent } from '../utils/dmsComponent'
4
+ import type { MarketingWebsite } from './useMarketingApi'
5
+
6
+ const WEBSITES_URL = '/api/marketing/websites'
7
+
8
+ const WEBSITE_FORM_SCHEMA = {
9
+ type: 'object',
10
+ properties: {
11
+ name: { type: 'string', minLength: 1, maxLength: 100 },
12
+ domain: { type: 'string', minLength: 1, maxLength: 253 },
13
+ },
14
+ required: ['name', 'domain'],
15
+ }
16
+
17
+ const WEBSITE_FORM_FIELDS = [
18
+ {
19
+ id: 'name',
20
+ label: '$page.marketing.websites.column.name',
21
+ required: true,
22
+ component: {
23
+ componentName: 'dms-input-text',
24
+ options: { placeholder: '$page.marketing.websites.form.name' },
25
+ },
26
+ },
27
+ {
28
+ id: 'domain',
29
+ label: '$page.marketing.websites.column.domain',
30
+ description: '$page.marketing.websites.form.domain_description',
31
+ required: true,
32
+ component: {
33
+ componentName: 'dms-input-text',
34
+ options: { placeholder: '$page.marketing.websites.form.domain' },
35
+ },
36
+ },
37
+ ]
38
+
39
+ export function useMarketingWebsiteForm() {
40
+ const modal = useModal()
41
+ const { t } = useI18n()
42
+
43
+ function openCreate(): Promise<MarketingWebsite | undefined> {
44
+ const instance = modal.open<MarketingWebsite | undefined>({
45
+ title: t('page.marketing.websites.create_title'),
46
+ component: dmsComponent('dms-form'),
47
+ componentOptions: {
48
+ fields: WEBSITE_FORM_FIELDS,
49
+ schema: WEBSITE_FORM_SCHEMA,
50
+ submitUrl: WEBSITES_URL,
51
+ submitUrlMethod: 'POST',
52
+ onSuccessCallback: (response?: unknown) => {
53
+ instance.close(response as MarketingWebsite)
54
+ },
55
+ },
56
+ })
57
+ return instance.result
58
+ }
59
+
60
+ function openSnippet(website: MarketingWebsite): Promise<void> {
61
+ return modal.open<undefined>({
62
+ title: t('page.marketing.websites.snippet.title'),
63
+ component: TrackerSnippet,
64
+ componentOptions: { website },
65
+ }).result
66
+ }
67
+
68
+ return { openCreate, openSnippet }
69
+ }
@@ -0,0 +1,80 @@
1
+ import { computed, ref, type ComputedRef, type Ref } from 'vue'
2
+ import { useLatestRequest } from './useLatestRequest'
3
+ import { useMarketingApi, type MarketingWebsite } from './useMarketingApi'
4
+ import { useMarketingWebsiteForm } from './useMarketingWebsiteForm'
5
+
6
+ /**
7
+ * The tenant's tracked sites and the one currently being looked at — the
8
+ * preamble of every marketing surface that is not scoped by a TableView.
9
+ *
10
+ * The list is what the endpoint returns, most-recently-active first, so the
11
+ * default selection is a site that has something to show. A selection carried
12
+ * in from a deep link wins over that default, but only while it still names a
13
+ * site that exists: a stale link must land on a populated page, not a blank
14
+ * one.
15
+ */
16
+ export interface MarketingWebsitesState {
17
+ websites: ComputedRef<MarketingWebsite[]>
18
+ /** `{ label, value }` pairs for a USelect. */
19
+ items: ComputedRef<{ label: string, value: string }[]>
20
+ selectedId: Ref<string | undefined>
21
+ selected: ComputedRef<MarketingWebsite | null>
22
+ /** True until the list has answered once, then only while it refetches. */
23
+ loading: ComputedRef<boolean>
24
+ failed: Ref<boolean>
25
+ load: () => Promise<void>
26
+ create: () => Promise<void>
27
+ showSnippet: () => Promise<void>
28
+ }
29
+
30
+ export function useMarketingWebsites(
31
+ initialId?: string,
32
+ ): MarketingWebsitesState {
33
+ const api = useMarketingApi()
34
+ const form = useMarketingWebsiteForm()
35
+ const selectedId = ref<string | undefined>(initialId || undefined)
36
+ const request = useLatestRequest(() => api.listWebsites())
37
+
38
+ const websites = computed(() => request.data.value ?? [])
39
+ const selected = computed(
40
+ () => websites.value.find(site => site._id === selectedId.value) ?? null,
41
+ )
42
+
43
+ async function load(): Promise<void> {
44
+ await request.run()
45
+ const linked = websites.value.find(site => site._id === selectedId.value)
46
+ selectedId.value = linked?._id ?? websites.value[0]?._id
47
+ }
48
+
49
+ async function create(): Promise<void> {
50
+ const created = await form.openCreate()
51
+ if (!created) {
52
+ return
53
+ }
54
+ selectedId.value = created._id
55
+ await load()
56
+ await form.openSnippet(created)
57
+ }
58
+
59
+ async function showSnippet(): Promise<void> {
60
+ if (selected.value) {
61
+ await form.openSnippet(selected.value)
62
+ }
63
+ }
64
+
65
+ return {
66
+ websites,
67
+ items: computed(() =>
68
+ websites.value.map(site => ({ label: site.name, value: site._id })),
69
+ ),
70
+ selectedId,
71
+ selected,
72
+ // Not asked yet reads as loading: a component that told them apart would
73
+ // flash its "no website tracked" state before the first answer lands.
74
+ loading: computed(() => !request.settled.value || request.loading.value),
75
+ failed: request.failed,
76
+ load,
77
+ create,
78
+ showSnippet,
79
+ }
80
+ }