@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,52 @@
1
+ import { DataType } from "@antelopejs/interface-dms/base/data-types";
2
+ import { z } from "zod";
3
+ declare const experimentSchema: z.ZodObject<{
4
+ key: z.ZodString;
5
+ status: z.ZodOptional<z.ZodEnum<["draft", "running", "stopped"]>>;
6
+ variations: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodObject<{
7
+ key: z.ZodString;
8
+ weight: z.ZodNumber;
9
+ }, "strip", z.ZodTypeAny, {
10
+ key: string;
11
+ weight: number;
12
+ }, {
13
+ key: string;
14
+ weight: number;
15
+ }>, "many">, {
16
+ key: string;
17
+ weight: number;
18
+ }[], {
19
+ key: string;
20
+ weight: number;
21
+ }[]>, {
22
+ key: string;
23
+ weight: number;
24
+ }[], {
25
+ key: string;
26
+ weight: number;
27
+ }[]>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ key: string;
30
+ variations: {
31
+ key: string;
32
+ weight: number;
33
+ }[];
34
+ status?: "stopped" | "running" | "draft" | undefined;
35
+ }, {
36
+ key: string;
37
+ variations: {
38
+ key: string;
39
+ weight: number;
40
+ }[];
41
+ status?: "stopped" | "running" | "draft" | undefined;
42
+ }>;
43
+ export type ExperimentInput = z.output<typeof experimentSchema>;
44
+ export declare function parseExperimentInput(raw: unknown): ExperimentInput | null;
45
+ export declare function experimentIssueField(error: unknown): "key" | "variations" | "status" | undefined;
46
+ export declare class ExperimentType extends DataType {
47
+ readonly options: Record<string, unknown>;
48
+ constructor(options?: Record<string, unknown>);
49
+ protected defaultInputComponent(): import("@antelopejs/interface-dms").ComponentInfoSerialized<unknown>;
50
+ getValidation(): z.ZodEffects<z.ZodUnknown, unknown, unknown>;
51
+ }
52
+ export {};
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ExperimentType = void 0;
13
+ exports.parseExperimentInput = parseExperimentInput;
14
+ exports.experimentIssueField = experimentIssueField;
15
+ const custom_1 = require("@antelopejs/interface-dms/base/custom");
16
+ const data_types_1 = require("@antelopejs/interface-dms/base/data-types");
17
+ const zod_1 = require("zod");
18
+ const funnels_table_1 = require("@/db/tables/funnels.table");
19
+ const constants_1 = require("@/types/constants");
20
+ const experimentKeySchema = zod_1.z
21
+ .string()
22
+ .trim()
23
+ .min(1)
24
+ .max(constants_1.MAX_EXPERIMENT_KEY_LENGTH)
25
+ .regex(/^[a-z0-9][a-z0-9_-]*$/);
26
+ const variationSchema = zod_1.z.object({
27
+ key: experimentKeySchema,
28
+ weight: zod_1.z.number().finite().nonnegative(),
29
+ });
30
+ const variationsSchema = zod_1.z
31
+ .array(variationSchema)
32
+ .min(constants_1.MIN_EXPERIMENT_VARIATIONS)
33
+ .max(constants_1.MAX_EXPERIMENT_VARIATIONS)
34
+ .refine((variations) => new Set(variations.map((v) => v.key)).size === variations.length)
35
+ .refine((variations) => variations.some((v) => v.weight > 0));
36
+ const experimentSchema = zod_1.z.object({
37
+ key: experimentKeySchema,
38
+ status: zod_1.z.enum(funnels_table_1.EXPERIMENT_STATUSES).optional(),
39
+ variations: variationsSchema,
40
+ });
41
+ function isBlankKey(value) {
42
+ return typeof value !== "string" || value.trim() === "";
43
+ }
44
+ function parseExperimentInput(raw) {
45
+ const value = typeof raw === "string" && raw !== "" ? JSON.parse(raw) : raw;
46
+ if (value === null || value === undefined || value === "") {
47
+ return null;
48
+ }
49
+ const candidate = value;
50
+ const noArms = !Array.isArray(candidate.variations) || candidate.variations.length === 0;
51
+ if (isBlankKey(candidate.key) && noArms) {
52
+ return null;
53
+ }
54
+ return experimentSchema.parse(value);
55
+ }
56
+ function experimentIssueField(error) {
57
+ if (!(error instanceof zod_1.z.ZodError)) {
58
+ return undefined;
59
+ }
60
+ const field = error.issues[0]?.path[0];
61
+ return field === "key" || field === "variations" || field === "status"
62
+ ? field
63
+ : undefined;
64
+ }
65
+ const experimentValueSchema = zod_1.z.unknown().refine((value) => {
66
+ try {
67
+ parseExperimentInput(value);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ });
74
+ let ExperimentType = class ExperimentType extends data_types_1.DataType {
75
+ options;
76
+ constructor(options = {}) {
77
+ super([], undefined, options);
78
+ this.options = options;
79
+ }
80
+ defaultInputComponent() {
81
+ return (0, custom_1.CustomComponent)("DmsMarketingExperimentInput")
82
+ .options(this.options)
83
+ .serializeSync();
84
+ }
85
+ getValidation() {
86
+ return experimentValueSchema;
87
+ }
88
+ };
89
+ exports.ExperimentType = ExperimentType;
90
+ exports.ExperimentType = ExperimentType = __decorate([
91
+ (0, data_types_1.RegisterDataType)("marketing_experiment"),
92
+ __metadata("design:paramtypes", [Object])
93
+ ], ExperimentType);
94
+ //# sourceMappingURL=experiment-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment-type.js","sourceRoot":"","sources":["../../src/utils/experiment-type.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { DataType } from "@antelopejs/interface-dms/base/data-types";
2
+ import { z } from "zod";
3
+ declare const stepSchema: z.ZodObject<{
4
+ kind: z.ZodEnum<["url", "custom"]>;
5
+ value: z.ZodString;
6
+ }, "strip", z.ZodTypeAny, {
7
+ value: string;
8
+ kind: "custom" | "url";
9
+ }, {
10
+ value: string;
11
+ kind: "custom" | "url";
12
+ }>;
13
+ export type FunnelStepInput = z.output<typeof stepSchema>;
14
+ export declare function parseFunnelSteps(raw: unknown): FunnelStepInput[];
15
+ export declare class FunnelStepsType extends DataType {
16
+ readonly options: Record<string, unknown>;
17
+ constructor(options?: Record<string, unknown>);
18
+ protected defaultInputComponent(): import("@antelopejs/interface-dms").ComponentInfoSerialized<unknown>;
19
+ getValidation(): z.ZodPipeline<z.ZodEffects<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodObject<{
20
+ kind: z.ZodEnum<["url", "custom"]>;
21
+ value: z.ZodString;
22
+ }, "strip", z.ZodTypeAny, {
23
+ value: string;
24
+ kind: "custom" | "url";
25
+ }, {
26
+ value: string;
27
+ kind: "custom" | "url";
28
+ }>, "many">]>, any, string | {
29
+ value: string;
30
+ kind: "custom" | "url";
31
+ }[]>, z.ZodArray<z.ZodObject<{
32
+ kind: z.ZodEnum<["url", "custom"]>;
33
+ value: z.ZodString;
34
+ }, "strip", z.ZodTypeAny, {
35
+ value: string;
36
+ kind: "custom" | "url";
37
+ }, {
38
+ value: string;
39
+ kind: "custom" | "url";
40
+ }>, "many">>;
41
+ }
42
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FunnelStepsType = void 0;
13
+ exports.parseFunnelSteps = parseFunnelSteps;
14
+ const custom_1 = require("@antelopejs/interface-dms/base/custom");
15
+ const data_types_1 = require("@antelopejs/interface-dms/base/data-types");
16
+ const zod_1 = require("zod");
17
+ const constants_1 = require("@/types/constants");
18
+ const stepSchema = zod_1.z.object({
19
+ kind: zod_1.z.enum(["url", "custom"]),
20
+ value: zod_1.z.string().trim().min(1).max(constants_1.MAX_FUNNEL_STEP_VALUE_LENGTH),
21
+ });
22
+ const funnelStepsSchema = zod_1.z
23
+ .union([zod_1.z.string(), zod_1.z.array(stepSchema)])
24
+ .transform((value) => {
25
+ if (typeof value !== "string") {
26
+ return value;
27
+ }
28
+ if (value === "") {
29
+ return [];
30
+ }
31
+ try {
32
+ return JSON.parse(value);
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ })
38
+ .pipe(zod_1.z.array(stepSchema).min(constants_1.MIN_FUNNEL_STEPS).max(constants_1.MAX_FUNNEL_STEPS));
39
+ function parseFunnelSteps(raw) {
40
+ return funnelStepsSchema.parse(raw);
41
+ }
42
+ let FunnelStepsType = class FunnelStepsType extends data_types_1.DataType {
43
+ options;
44
+ constructor(options = {}) {
45
+ super([], undefined, options);
46
+ this.options = options;
47
+ }
48
+ defaultInputComponent() {
49
+ return (0, custom_1.CustomComponent)("DmsMarketingFunnelStepsInput")
50
+ .options(this.options)
51
+ .serializeSync();
52
+ }
53
+ getValidation() {
54
+ return funnelStepsSchema;
55
+ }
56
+ };
57
+ exports.FunnelStepsType = FunnelStepsType;
58
+ exports.FunnelStepsType = FunnelStepsType = __decorate([
59
+ (0, data_types_1.RegisterDataType)("marketing_funnel_steps"),
60
+ __metadata("design:paramtypes", [Object])
61
+ ], FunnelStepsType);
62
+ //# sourceMappingURL=funnel-steps-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"funnel-steps-type.js","sourceRoot":"","sources":["../../src/utils/funnel-steps-type.ts"],"names":[],"mappings":";;;;;;;;;;;;AA2CA,4CAEC;AA7CD,kEAAwE;AACxE,0EAGmD;AACnD,6BAAwB;AACxB,iDAI2B;AAE3B,MAAM,UAAU,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1B,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC/B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,wCAA4B,CAAC;CAClE,CAAC,CAAC;AAGH,MAAM,iBAAiB,GAAG,OAAC;KACxB,KAAK,CAAC,CAAC,OAAC,CAAC,MAAM,EAAE,EAAE,OAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;KACxC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;IACnB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC;KACD,IAAI,CAAC,OAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,4BAAgB,CAAC,CAAC,GAAG,CAAC,4BAAgB,CAAC,CAAC,CAAC;AAUzE,SAAgB,gBAAgB,CAAC,GAAY;IAC3C,OAAO,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACtC,CAAC;AAOM,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,qBAAQ;IACf;IAA5B,YAA4B,UAAmC,EAAE;QAC/D,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QADJ,YAAO,GAAP,OAAO,CAA8B;IAEjE,CAAC;IAES,qBAAqB;QAC7B,OAAO,IAAA,wBAAe,EAAC,8BAA8B,CAAC;aACnD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;aACrB,aAAa,EAAE,CAAC;IACrB,CAAC;IAED,aAAa;QACX,OAAO,iBAAiB,CAAC;IAC3B,CAAC;CACF,CAAA;AAdY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,6BAAgB,EAAC,wBAAwB,CAAC;;GAC9B,eAAe,CAc3B"}
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_size = 2
5
+ indent_style = space
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "endOfLine": "auto",
6
+ "htmlWhitespaceSensitivity": "ignore"
7
+ }
@@ -0,0 +1,330 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ import { useDmsRoute, useI18n } from '#dms/frontend-module'
4
+ import { useChildId } from '../composables/useChildId'
5
+ import { useLatestRequest } from '../composables/useLatestRequest'
6
+ import {
7
+ useMarketingApi,
8
+ type MarketingCampaignEntry,
9
+ type MarketingCampaigns,
10
+ type MarketingChannel,
11
+ } from '../composables/useMarketingApi'
12
+ import {
13
+ MARKETING_PERIOD_PRESETS,
14
+ ndToPreset,
15
+ useMarketingPeriod,
16
+ } from '../composables/useMarketingPeriod'
17
+ import { useMarketingWebsites } from '../composables/useMarketingWebsites'
18
+
19
+ import type { TableColumn } from '@nuxt/ui'
20
+
21
+ /**
22
+ * Acquisition surface: the five-channel split of sessions (donut) next to the
23
+ * UTM campaign table — one row per (source, medium, campaign) triple observed
24
+ * on new sessions — with UTM terms and contents as top lists below.
25
+ *
26
+ * Everything reads the daily rollups through one campaigns fetch; the search
27
+ * box filters the table server-side because a day's map caps its entries and
28
+ * the merged table can exceed what one response carries.
29
+ */
30
+ const PERIOD_SCOPE = 'dms-marketing-campaigns'
31
+ const SEARCH_DEBOUNCE_MS = 300
32
+
33
+ // The DMS page renderer hands every component its identity; keeping it is what
34
+ // lets dms-ui primitives key their events and persisted state per child.
35
+ const props = withDefaults(
36
+ defineProps<{ componentId?: string, pageId?: string }>(),
37
+ { componentId: 'marketing-campaigns', pageId: 'marketing' },
38
+ )
39
+
40
+ const childId = useChildId(() => props.componentId)
41
+
42
+ const api = useMarketingApi()
43
+ const { t, te } = useI18n()
44
+ const route = useDmsRoute()
45
+
46
+ const queryParam = (key: string): string => {
47
+ const value = route.query[key]
48
+ return typeof value === 'string' ? value : ''
49
+ }
50
+
51
+ // Read once: the selector seeds its preset at init and never reactively, so a
52
+ // deep link's window has to be resolved before it renders.
53
+ const initialPreset = ndToPreset(queryParam('period'))
54
+
55
+ const {
56
+ websites,
57
+ items: websiteItems,
58
+ selectedId,
59
+ loading: loadingWebsites,
60
+ failed: websitesFailed,
61
+ load: loadWebsites,
62
+ create: createWebsite,
63
+ } = useMarketingWebsites(queryParam('website'))
64
+
65
+ const period = useMarketingPeriod(PERIOD_SCOPE)
66
+ const search = ref('')
67
+
68
+ let searchTimer: ReturnType<typeof setTimeout> | null = null
69
+
70
+ const {
71
+ data: acquisition,
72
+ loading: loadingCampaigns,
73
+ failed: campaignsFailed,
74
+ settled: campaignsSettled,
75
+ run: loadCampaigns,
76
+ } = useLatestRequest<MarketingCampaigns>(() =>
77
+ // Server-side there is no session to authenticate with: the fetch belongs to
78
+ // the client, like every other read here.
79
+ selectedId.value && !import.meta.env.SSR
80
+ ? api.getCampaigns(selectedId.value, period.value, search.value.trim())
81
+ : null,
82
+ )
83
+
84
+ // The watcher owns every fetch, including the first: seeding it from onMounted
85
+ // as well would issue the initial request twice on a bare link, and not at all
86
+ // on a link that already names the site the list would have picked — resolving
87
+ // it writes back the same id, which is no change to watch.
88
+ watch([selectedId, period], () => {
89
+ void loadCampaigns()
90
+ }, { immediate: true })
91
+
92
+ watch(search, () => {
93
+ if (searchTimer) {
94
+ clearTimeout(searchTimer)
95
+ }
96
+ searchTimer = setTimeout(() => void loadCampaigns(), SEARCH_DEBOUNCE_MS)
97
+ })
98
+
99
+ onBeforeUnmount(() => {
100
+ if (searchTimer) {
101
+ clearTimeout(searchTimer)
102
+ }
103
+ })
104
+
105
+ const loading = computed(
106
+ () =>
107
+ loadingWebsites.value
108
+ || loadingCampaigns.value
109
+ || (!!selectedId.value && !campaignsSettled.value),
110
+ )
111
+
112
+ // Two failures, two flags: retrying the campaigns fetch must not clear the
113
+ // one that says the site list itself never loaded.
114
+ const failed = computed(() => websitesFailed.value || campaignsFailed.value)
115
+
116
+ /** Retry entry point: whichever of the two fetches has nothing to show. */
117
+ function reload() {
118
+ return websites.value.length === 0 ? loadWebsites() : loadCampaigns()
119
+ }
120
+
121
+ onMounted(loadWebsites)
122
+
123
+ // --- Channels ---------------------------------------------------------------
124
+
125
+ /** Display order mirrors the backend's MARKETING_CHANNELS. */
126
+ const CHANNEL_ORDER: MarketingChannel[] = ['direct', 'organic', 'social', 'referral', 'paid']
127
+
128
+ /** Known channels localize; a key minted by a future classifier still shows. */
129
+ function channelLabel(channel: string): string {
130
+ const key = `page.marketing.campaigns.channels.${channel}`
131
+ return te(key) ? t(key) : channel
132
+ }
133
+
134
+ const channelRecords = computed(() => {
135
+ const channels = acquisition.value?.channels ?? {}
136
+ return CHANNEL_ORDER
137
+ .map(channel => ({ label: channelLabel(channel), value: channels[channel] ?? 0 }))
138
+ .filter(record => record.value > 0)
139
+ })
140
+
141
+ // --- Campaign table ---------------------------------------------------------
142
+
143
+ const campaignRows = computed(() => acquisition.value?.campaigns ?? [])
144
+ const truncated = computed(() => acquisition.value?.truncated ?? false)
145
+
146
+ const columns = computed<TableColumn<MarketingCampaignEntry>[]>(() => [
147
+ { accessorKey: 'source', header: t('page.marketing.campaigns.table.source') },
148
+ { accessorKey: 'medium', header: t('page.marketing.campaigns.table.medium') },
149
+ { accessorKey: 'campaign', header: t('page.marketing.campaigns.table.campaign') },
150
+ {
151
+ accessorKey: 'sessions',
152
+ header: t('page.marketing.campaigns.table.sessions'),
153
+ meta: { class: { th: 'text-right', td: 'text-right' } },
154
+ },
155
+ ])
156
+
157
+ // --- Referrer / term / content top lists ------------------------------------
158
+
159
+ const TOP_LIST_LIMIT = 8
160
+
161
+ function toTopItems(record: Record<string, number> | undefined) {
162
+ return Object.entries(record ?? {})
163
+ .sort((a, b) => b[1] - a[1])
164
+ .slice(0, TOP_LIST_LIMIT)
165
+ .map(([label, value]) => ({ id: label, title: label, value }))
166
+ }
167
+
168
+ const topLists = computed(() => [
169
+ // Referrer domains across all channels — the overview's acquisition card
170
+ // links here for this detail.
171
+ {
172
+ id: 'referrers',
173
+ title: t('page.marketing.campaigns.top.referrers'),
174
+ items: toTopItems(acquisition.value?.referrers),
175
+ },
176
+ {
177
+ id: 'terms',
178
+ title: t('page.marketing.campaigns.top.terms'),
179
+ items: toTopItems(acquisition.value?.terms),
180
+ },
181
+ {
182
+ id: 'contents',
183
+ title: t('page.marketing.campaigns.top.contents'),
184
+ items: toTopItems(acquisition.value?.contents),
185
+ },
186
+ ])
187
+ </script>
188
+
189
+ <template>
190
+ <div class="space-y-6 p-6">
191
+ <div class="flex flex-wrap items-center justify-between gap-4">
192
+ <div>
193
+ <h1 class="text-xl font-semibold">{{ t('page.marketing.campaigns.title') }}</h1>
194
+ <p class="text-sm text-muted">{{ t('page.marketing.campaigns.subtitle') }}</p>
195
+ </div>
196
+ <div class="flex items-center gap-3">
197
+ <USelect
198
+ v-if="websites.length > 1"
199
+ v-model="selectedId"
200
+ :items="websiteItems"
201
+ class="w-56"
202
+ />
203
+ <DmsPeriodSelector
204
+ :id="PERIOD_SCOPE"
205
+ :component-id="childId('period')"
206
+ :page-id="props.pageId"
207
+ :default-preset="initialPreset"
208
+ :presets="MARKETING_PERIOD_PRESETS"
209
+ :comparisons="['none']"
210
+ :show-range-label="false"
211
+ />
212
+ <UButton
213
+ icon="i-ph-arrow-clockwise"
214
+ color="neutral"
215
+ variant="subtle"
216
+ size="sm"
217
+ :loading="loading"
218
+ @click="loadCampaigns"
219
+ />
220
+ </div>
221
+ </div>
222
+
223
+ <div v-if="failed" class="flex flex-col items-start gap-3">
224
+ <UAlert
225
+ color="error"
226
+ variant="subtle"
227
+ icon="i-ph-warning-circle"
228
+ :title="t('page.marketing.campaigns.error.title')"
229
+ :description="t('page.marketing.campaigns.error.description')"
230
+ />
231
+ <UButton
232
+ icon="i-ph-arrow-clockwise"
233
+ :label="t('page.marketing.campaigns.retry')"
234
+ color="neutral"
235
+ variant="subtle"
236
+ size="sm"
237
+ @click="reload"
238
+ />
239
+ </div>
240
+
241
+ <DmsMarketingNoWebsiteState
242
+ v-else-if="!loading && websites.length === 0"
243
+ icon="i-ph-megaphone"
244
+ :title="t('page.marketing.campaigns.empty.title')"
245
+ :description="t('page.marketing.campaigns.empty.description')"
246
+ @create="createWebsite"
247
+ />
248
+
249
+ <!-- Only before the first answer: a refetch keeps the tables on screen
250
+ and spins the refresh button instead. -->
251
+ <USkeleton v-else-if="loading && !acquisition" class="h-96 w-full" />
252
+
253
+ <template v-else>
254
+ <div class="grid gap-4 lg:grid-cols-3">
255
+ <UCard>
256
+ <template #header>
257
+ <div>
258
+ <p class="font-medium">{{ t('page.marketing.campaigns.channels.title') }}</p>
259
+ <p class="text-sm text-muted">{{ t('page.marketing.campaigns.channels.subtitle') }}</p>
260
+ </div>
261
+ </template>
262
+ <DmsChart
263
+ v-if="channelRecords.length > 0"
264
+ :component-id="childId('channels')"
265
+ :page-id="props.pageId"
266
+ type="donut"
267
+ :static-dataset="channelRecords"
268
+ height="300px"
269
+ :show-total="true"
270
+ />
271
+ <p v-else class="py-12 text-center text-sm text-muted">
272
+ {{ t('page.marketing.campaigns.channels.empty') }}
273
+ </p>
274
+ </UCard>
275
+
276
+ <UCard class="lg:col-span-2">
277
+ <template #header>
278
+ <div class="flex flex-wrap items-center justify-between gap-3">
279
+ <div>
280
+ <p class="font-medium">{{ t('page.marketing.campaigns.table.title') }}</p>
281
+ <p class="text-sm text-muted">{{ t('page.marketing.campaigns.table.subtitle') }}</p>
282
+ </div>
283
+ <UInput
284
+ v-model="search"
285
+ icon="i-ph-magnifying-glass"
286
+ :placeholder="t('page.marketing.campaigns.table.search_placeholder')"
287
+ class="w-56"
288
+ />
289
+ </div>
290
+ </template>
291
+ <UTable :data="campaignRows" :columns="columns">
292
+ <template #source-cell="{ row }">
293
+ <span v-if="row.original.source">{{ row.original.source }}</span>
294
+ <span v-else class="text-muted">—</span>
295
+ </template>
296
+ <template #medium-cell="{ row }">
297
+ <span v-if="row.original.medium">{{ row.original.medium }}</span>
298
+ <span v-else class="text-muted">—</span>
299
+ </template>
300
+ <template #campaign-cell="{ row }">
301
+ <span v-if="row.original.campaign">{{ row.original.campaign }}</span>
302
+ <span v-else class="text-muted">—</span>
303
+ </template>
304
+ <template #empty>
305
+ <p class="py-8 text-center text-sm text-muted">
306
+ {{ t('page.marketing.campaigns.table.empty') }}
307
+ </p>
308
+ </template>
309
+ </UTable>
310
+ <p v-if="truncated" class="mt-2 text-sm text-muted">
311
+ {{ t('page.marketing.campaigns.table.truncated') }}
312
+ </p>
313
+ </UCard>
314
+ </div>
315
+
316
+ <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
317
+ <DmsTopListCard
318
+ v-for="list in topLists"
319
+ :key="list.id"
320
+ :component-id="childId(`top-${list.id}`)"
321
+ :page-id="props.pageId"
322
+ :title="list.title"
323
+ :static-items="list.items"
324
+ :show-delta="false"
325
+ :empty-label="t('page.marketing.campaigns.top.empty')"
326
+ />
327
+ </div>
328
+ </template>
329
+ </div>
330
+ </template>