@aglyn/plugins-marketing 1.0.0-beta.143

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 (183) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +56 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
  8. package/src/lib/components/announcement-bar-card.component.js +292 -0
  9. package/src/lib/components/announcement-bar-card.component.js.map +1 -0
  10. package/src/lib/components/campaign-composer.d.ts +84 -0
  11. package/src/lib/components/campaign-composer.js +1643 -0
  12. package/src/lib/components/campaign-composer.js.map +1 -0
  13. package/src/lib/components/campaign-conversions-card.d.ts +22 -0
  14. package/src/lib/components/campaign-conversions-card.js +642 -0
  15. package/src/lib/components/campaign-conversions-card.js.map +1 -0
  16. package/src/lib/components/campaign-detail-card.d.ts +55 -0
  17. package/src/lib/components/campaign-detail-card.js +771 -0
  18. package/src/lib/components/campaign-detail-card.js.map +1 -0
  19. package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
  20. package/src/lib/components/campaign-edit-drawer.js +252 -0
  21. package/src/lib/components/campaign-edit-drawer.js.map +1 -0
  22. package/src/lib/components/campaign-email-zones.d.ts +111 -0
  23. package/src/lib/components/campaign-email-zones.js +54 -0
  24. package/src/lib/components/campaign-email-zones.js.map +1 -0
  25. package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
  26. package/src/lib/components/campaign-glance-card.component.js +151 -0
  27. package/src/lib/components/campaign-glance-card.component.js.map +1 -0
  28. package/src/lib/components/campaign-members-section.d.ts +87 -0
  29. package/src/lib/components/campaign-members-section.js +499 -0
  30. package/src/lib/components/campaign-members-section.js.map +1 -0
  31. package/src/lib/components/campaign-reach-sections.d.ts +137 -0
  32. package/src/lib/components/campaign-reach-sections.js +694 -0
  33. package/src/lib/components/campaign-reach-sections.js.map +1 -0
  34. package/src/lib/components/campaign-report-card.d.ts +91 -0
  35. package/src/lib/components/campaign-report-card.js +667 -0
  36. package/src/lib/components/campaign-report-card.js.map +1 -0
  37. package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
  38. package/src/lib/components/campaign-test-send-drawer.js +238 -0
  39. package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
  40. package/src/lib/components/campaigns-card.d.ts +31 -0
  41. package/src/lib/components/campaigns-card.js +667 -0
  42. package/src/lib/components/campaigns-card.js.map +1 -0
  43. package/src/lib/components/conversion-attribution.component.d.ts +62 -0
  44. package/src/lib/components/conversion-attribution.component.js +127 -0
  45. package/src/lib/components/conversion-attribution.component.js.map +1 -0
  46. package/src/lib/components/email-compose-card.d.ts +12 -0
  47. package/src/lib/components/email-compose-card.js +157 -0
  48. package/src/lib/components/email-compose-card.js.map +1 -0
  49. package/src/lib/components/email-detail.d.ts +40 -0
  50. package/src/lib/components/email-detail.js +1180 -0
  51. package/src/lib/components/email-detail.js.map +1 -0
  52. package/src/lib/components/email-edit-drawer.d.ts +54 -0
  53. package/src/lib/components/email-edit-drawer.js +172 -0
  54. package/src/lib/components/email-edit-drawer.js.map +1 -0
  55. package/src/lib/components/email-messages-widget.d.ts +22 -0
  56. package/src/lib/components/email-messages-widget.js +49 -0
  57. package/src/lib/components/email-messages-widget.js.map +1 -0
  58. package/src/lib/components/email-recipients-card.d.ts +50 -0
  59. package/src/lib/components/email-recipients-card.js +338 -0
  60. package/src/lib/components/email-recipients-card.js.map +1 -0
  61. package/src/lib/components/emails-list-card.d.ts +36 -0
  62. package/src/lib/components/emails-list-card.js +544 -0
  63. package/src/lib/components/emails-list-card.js.map +1 -0
  64. package/src/lib/components/experiment-zones.d.ts +78 -0
  65. package/src/lib/components/experiment-zones.js +65 -0
  66. package/src/lib/components/experiment-zones.js.map +1 -0
  67. package/src/lib/components/host-experiments-card.component.d.ts +17 -0
  68. package/src/lib/components/host-experiments-card.component.js +887 -0
  69. package/src/lib/components/host-experiments-card.component.js.map +1 -0
  70. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  71. package/src/lib/components/host-marketing-summary-card.component.js +161 -0
  72. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
  73. package/src/lib/components/host-overlays-card.component.d.ts +19 -0
  74. package/src/lib/components/host-overlays-card.component.js +783 -0
  75. package/src/lib/components/host-overlays-card.component.js.map +1 -0
  76. package/src/lib/components/marketing-console-page.d.ts +28 -0
  77. package/src/lib/components/marketing-console-page.js +166 -0
  78. package/src/lib/components/marketing-console-page.js.map +1 -0
  79. package/src/lib/components/marketing-console-sections.d.ts +34 -0
  80. package/src/lib/components/marketing-console-sections.js +77 -0
  81. package/src/lib/components/marketing-console-sections.js.map +1 -0
  82. package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
  83. package/src/lib/components/overlay-stats-row.component.js +78 -0
  84. package/src/lib/components/overlay-stats-row.component.js.map +1 -0
  85. package/src/lib/components/popup-card.component.d.ts +18 -0
  86. package/src/lib/components/popup-card.component.js +462 -0
  87. package/src/lib/components/popup-card.component.js.map +1 -0
  88. package/src/lib/components/record-attribution-widget.d.ts +35 -0
  89. package/src/lib/components/record-attribution-widget.js +33 -0
  90. package/src/lib/components/record-attribution-widget.js.map +1 -0
  91. package/src/lib/components/site-runtime.d.ts +9 -0
  92. package/src/lib/components/site-runtime.js +1081 -0
  93. package/src/lib/components/site-runtime.js.map +1 -0
  94. package/src/lib/components/use-campaign-send-api.d.ts +35 -0
  95. package/src/lib/components/use-campaign-send-api.js +99 -0
  96. package/src/lib/components/use-campaign-send-api.js.map +1 -0
  97. package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
  98. package/src/lib/components/use-campaign-topic-options.js +44 -0
  99. package/src/lib/components/use-campaign-topic-options.js.map +1 -0
  100. package/src/lib/components/use-emails-hub-path.d.ts +39 -0
  101. package/src/lib/components/use-emails-hub-path.js +70 -0
  102. package/src/lib/components/use-emails-hub-path.js.map +1 -0
  103. package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
  104. package/src/lib/components/use-marketing-hub-path.js +50 -0
  105. package/src/lib/components/use-marketing-hub-path.js.map +1 -0
  106. package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
  107. package/src/lib/components/use-overlay-copy-editor.js +110 -0
  108. package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
  109. package/src/lib/constants/bundle-common.d.ts +8 -0
  110. package/src/lib/constants/bundle-common.js +9 -0
  111. package/src/lib/constants/bundle-common.js.map +1 -0
  112. package/src/lib/model/campaign-membership-figures.d.ts +109 -0
  113. package/src/lib/model/campaign-membership-figures.js +113 -0
  114. package/src/lib/model/campaign-membership-figures.js.map +1 -0
  115. package/src/lib/model/compile-client-automations.d.ts +65 -0
  116. package/src/lib/model/compile-client-automations.js +95 -0
  117. package/src/lib/model/compile-client-automations.js.map +1 -0
  118. package/src/lib/model/experiments.d.ts +130 -0
  119. package/src/lib/model/experiments.js +186 -0
  120. package/src/lib/model/experiments.js.map +1 -0
  121. package/src/lib/model/index.d.ts +26 -0
  122. package/src/lib/model/index.js +26 -0
  123. package/src/lib/model/index.js.map +1 -0
  124. package/src/lib/model/marketing-record-routes.d.ts +6 -0
  125. package/src/lib/model/marketing-record-routes.js +46 -0
  126. package/src/lib/model/marketing-record-routes.js.map +1 -0
  127. package/src/lib/model/overlay-copy.d.ts +62 -0
  128. package/src/lib/model/overlay-copy.js +78 -0
  129. package/src/lib/model/overlay-copy.js.map +1 -0
  130. package/src/lib/model/overlays.d.ts +113 -0
  131. package/src/lib/model/overlays.js +93 -0
  132. package/src/lib/model/overlays.js.map +1 -0
  133. package/src/lib/model/site-contract.d.ts +100 -0
  134. package/src/lib/model/site-contract.js +23 -0
  135. package/src/lib/model/site-contract.js.map +1 -0
  136. package/src/lib/plugin.d.ts +27 -0
  137. package/src/lib/plugin.js +168 -0
  138. package/src/lib/plugin.js.map +1 -0
  139. package/src/lib/preview-props.d.ts +31 -0
  140. package/src/lib/preview-props.js +54 -0
  141. package/src/lib/preview-props.js.map +1 -0
  142. package/src/lib/server/campaign-manage.d.ts +62 -0
  143. package/src/lib/server/campaign-manage.js +829 -0
  144. package/src/lib/server/campaign-manage.js.map +1 -0
  145. package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
  146. package/src/lib/server/campaign-process-scheduled.js +198 -0
  147. package/src/lib/server/campaign-process-scheduled.js.map +1 -0
  148. package/src/lib/server/campaign-recipients.d.ts +55 -0
  149. package/src/lib/server/campaign-recipients.js +219 -0
  150. package/src/lib/server/campaign-recipients.js.map +1 -0
  151. package/src/lib/server/campaign-send.d.ts +487 -0
  152. package/src/lib/server/campaign-send.js +3157 -0
  153. package/src/lib/server/campaign-send.js.map +1 -0
  154. package/src/lib/server/email-campaign-reach.d.ts +174 -0
  155. package/src/lib/server/email-campaign-reach.js +254 -0
  156. package/src/lib/server/email-campaign-reach.js.map +1 -0
  157. package/src/lib/server/email-events.d.ts +27 -0
  158. package/src/lib/server/email-events.js +714 -0
  159. package/src/lib/server/email-events.js.map +1 -0
  160. package/src/lib/server/get-client-automations.d.ts +60 -0
  161. package/src/lib/server/get-client-automations.js +87 -0
  162. package/src/lib/server/get-client-automations.js.map +1 -0
  163. package/src/lib/server/get-overlays.d.ts +28 -0
  164. package/src/lib/server/get-overlays.js +65 -0
  165. package/src/lib/server/get-overlays.js.map +1 -0
  166. package/src/lib/server/get-screen-experiments.d.ts +42 -0
  167. package/src/lib/server/get-screen-experiments.js +82 -0
  168. package/src/lib/server/get-screen-experiments.js.map +1 -0
  169. package/src/lib/server/lists-materialize.d.ts +18 -0
  170. package/src/lib/server/lists-materialize.js +130 -0
  171. package/src/lib/server/lists-materialize.js.map +1 -0
  172. package/src/lib/server/marketing-figures.d.ts +37 -0
  173. package/src/lib/server/marketing-figures.js +248 -0
  174. package/src/lib/server/marketing-figures.js.map +1 -0
  175. package/src/lib/server/site-page-enricher.d.ts +26 -0
  176. package/src/lib/server/site-page-enricher.js +210 -0
  177. package/src/lib/server/site-page-enricher.js.map +1 -0
  178. package/src/lib/server.d.ts +20 -0
  179. package/src/lib/server.js +152 -0
  180. package/src/lib/server.js.map +1 -0
  181. package/src/lib/site.d.ts +22 -0
  182. package/src/lib/site.js +48 -0
  183. package/src/lib/site.js.map +1 -0
@@ -0,0 +1,130 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { writeCronBeat } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin, materializeDynamicList } from "@aglyn/tenant-data-admin";
19
+ /**
20
+ * The scheduled sweep that keeps dynamic lists fresh
21
+ * (`docs/specs/email-overhaul.md` §3e).
22
+ *
23
+ * A dynamic list stores a rule and materializes into its `members`
24
+ * subcollection; without something re-running the rule, "dynamic" means
25
+ * "evaluated once at creation". This is that something: it rides the existing
26
+ * fifteen-minute Cloud Scheduler fast-cron beside the scheduled-campaign
27
+ * processor, so a dynamic list is never more than about fifteen minutes stale
28
+ * and no new scheduler job is provisioned.
29
+ *
30
+ * ## It chunks, because `sweepConsoleCron` will follow a cursor
31
+ *
32
+ * The caller in `cloud/functions` drives a route to completion by following
33
+ * `nextCursor`, so this answers `{ done, nextCursor }` rather than trying to
34
+ * sweep every list in one invocation. Two cursors are in play and they are
35
+ * different things: `nextCursor` here is a position in the LIST of lists, and
36
+ * the `evaluationCursor` stored on each list document is a position inside
37
+ * one list's own scan. Both resume; neither is allowed to shorten a
38
+ * membership.
39
+ *
40
+ * ## A failing list must not stop the sweep
41
+ *
42
+ * Each list is evaluated in its own try. One org's malformed rule, deleted
43
+ * segment or missing host would otherwise stop every list after it in
44
+ * document order from ever being evaluated — the failure would be invisible
45
+ * and would land on whichever customers sort last.
46
+ */ /** Dynamic lists evaluated per invocation. */ const LISTS_PER_CHUNK = 20;
47
+ export const listsMaterializeHandler = async (req, res)=>{
48
+ var _ref;
49
+ var _req_body;
50
+ if (req.method !== 'POST') {
51
+ return res.status(405).json({
52
+ error: 'Method not allowed'
53
+ });
54
+ }
55
+ const cronSecret = process.env.CRON_SECRET;
56
+ if (!cronSecret) {
57
+ return res.status(501).json({
58
+ error: 'Dynamic lists are not configured (CRON_SECRET).'
59
+ });
60
+ }
61
+ if (req.headers['x-cron-secret'] !== cronSecret) {
62
+ return res.status(401).json({
63
+ error: 'Unauthenticated'
64
+ });
65
+ }
66
+ const firestore = firebaseAdmin.app().firestore();
67
+ /*
68
+ * The mark `/api/health/crons` reads to notice this job going away
69
+ * (AGL-1955). Nothing downstream ages when a dynamic list stops being
70
+ * re-evaluated — a stale membership looks exactly like a fresh one whose
71
+ * population happens not to have changed — so the invocation itself is the
72
+ * only honest thing to watch.
73
+ */ await writeCronBeat(firestore, 'lists-materialize');
74
+ const cursor = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body['cursor']) != null ? _ref : '');
75
+ let query = firestore.collectionGroup('lists').where('kind', '==', 'dynamic').orderBy(firebaseAdmin.firestore.FieldPath.documentId()).limit(LISTS_PER_CHUNK);
76
+ /*
77
+ * A DocumentReference, not the raw string. A collection-group cursor on
78
+ * `documentId()` is compared against the FULL resource path, and a bare id
79
+ * would either be rejected or silently compared against the wrong thing —
80
+ * so the cursor travels as a path and is turned back into a reference here.
81
+ */ if (cursor) query = query.startAfter(firestore.doc(cursor));
82
+ const lists = await query.get();
83
+ const results = [];
84
+ let lastPath = '';
85
+ for (const listDoc of lists.docs){
86
+ var _listDoc_get;
87
+ lastPath = listDoc.ref.path;
88
+ const hostId = String((_listDoc_get = listDoc.get('hostId')) != null ? _listDoc_get : '');
89
+ if (!hostId) {
90
+ // A dynamic list with no host has no silos to draw from. Reported
91
+ // rather than thrown: it is a data problem on one list, and the sweep
92
+ // has other lists to get to.
93
+ results.push({
94
+ list: listDoc.ref.path,
95
+ error: 'no hostId'
96
+ });
97
+ continue;
98
+ }
99
+ try {
100
+ var _listDoc_get1;
101
+ const outcome = await materializeDynamicList({
102
+ listRef: listDoc.ref,
103
+ hostId,
104
+ rule: listDoc.get('rule'),
105
+ resume: (_listDoc_get1 = listDoc.get('evaluationCursor')) != null ? _listDoc_get1 : null
106
+ });
107
+ results.push(_extends({
108
+ list: listDoc.ref.path
109
+ }, outcome));
110
+ } catch (error) {
111
+ console.error('dynamic list materialize failed', listDoc.ref.path, error);
112
+ results.push({
113
+ list: listDoc.ref.path,
114
+ error: 'evaluation failed'
115
+ });
116
+ }
117
+ }
118
+ const done = lists.size < LISTS_PER_CHUNK;
119
+ return res.status(200).json({
120
+ ok: true,
121
+ evaluated: results.length,
122
+ results,
123
+ done,
124
+ // The caller refuses to loop on `done:false` with no cursor, which is the
125
+ // one shape that would re-read the same chunk for ever.
126
+ nextCursor: done ? null : lastPath
127
+ });
128
+ };
129
+
130
+ //# sourceMappingURL=lists-materialize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/lists-materialize.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { writeCronBeat, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n materializeDynamicList,\n type DynamicListCursor,\n} from '@aglyn/tenant-data-admin'\n\n/**\n * The scheduled sweep that keeps dynamic lists fresh\n * (`docs/specs/email-overhaul.md` §3e).\n *\n * A dynamic list stores a rule and materializes into its `members`\n * subcollection; without something re-running the rule, \"dynamic\" means\n * \"evaluated once at creation\". This is that something: it rides the existing\n * fifteen-minute Cloud Scheduler fast-cron beside the scheduled-campaign\n * processor, so a dynamic list is never more than about fifteen minutes stale\n * and no new scheduler job is provisioned.\n *\n * ## It chunks, because `sweepConsoleCron` will follow a cursor\n *\n * The caller in `cloud/functions` drives a route to completion by following\n * `nextCursor`, so this answers `{ done, nextCursor }` rather than trying to\n * sweep every list in one invocation. Two cursors are in play and they are\n * different things: `nextCursor` here is a position in the LIST of lists, and\n * the `evaluationCursor` stored on each list document is a position inside\n * one list's own scan. Both resume; neither is allowed to shorten a\n * membership.\n *\n * ## A failing list must not stop the sweep\n *\n * Each list is evaluated in its own try. One org's malformed rule, deleted\n * segment or missing host would otherwise stop every list after it in\n * document order from ever being evaluated — the failure would be invisible\n * and would land on whichever customers sort last.\n */\n\n/** Dynamic lists evaluated per invocation. */\nconst LISTS_PER_CHUNK = 20\n\nexport const listsMaterializeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret) {\n return res\n .status(501)\n .json({ error: 'Dynamic lists are not configured (CRON_SECRET).' })\n }\n if (req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n\n const firestore = firebaseAdmin.app().firestore()\n /*\n * The mark `/api/health/crons` reads to notice this job going away\n * (AGL-1955). Nothing downstream ages when a dynamic list stops being\n * re-evaluated — a stale membership looks exactly like a fresh one whose\n * population happens not to have changed — so the invocation itself is the\n * only honest thing to watch.\n */\n await writeCronBeat(firestore, 'lists-materialize')\n\n const cursor = String((req.body as Record<string, unknown>)?.['cursor'] ?? '')\n let query = firestore\n .collectionGroup('lists')\n .where('kind', '==', 'dynamic')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(LISTS_PER_CHUNK)\n /*\n * A DocumentReference, not the raw string. A collection-group cursor on\n * `documentId()` is compared against the FULL resource path, and a bare id\n * would either be rejected or silently compared against the wrong thing —\n * so the cursor travels as a path and is turned back into a reference here.\n */\n if (cursor) query = query.startAfter(firestore.doc(cursor))\n\n const lists = await query.get()\n const results: Array<Record<string, unknown>> = []\n let lastPath = ''\n for (const listDoc of lists.docs) {\n lastPath = listDoc.ref.path\n const hostId = String(listDoc.get('hostId') ?? '')\n if (!hostId) {\n // A dynamic list with no host has no silos to draw from. Reported\n // rather than thrown: it is a data problem on one list, and the sweep\n // has other lists to get to.\n results.push({ list: listDoc.ref.path, error: 'no hostId' })\n continue\n }\n try {\n const outcome = await materializeDynamicList({\n listRef: listDoc.ref,\n hostId,\n rule: listDoc.get('rule'),\n resume: (listDoc.get('evaluationCursor') as DynamicListCursor) ?? null,\n })\n results.push({ list: listDoc.ref.path, ...outcome })\n } catch (error) {\n console.error('dynamic list materialize failed', listDoc.ref.path, error)\n results.push({ list: listDoc.ref.path, error: 'evaluation failed' })\n }\n }\n\n const done = lists.size < LISTS_PER_CHUNK\n return res.status(200).json({\n ok: true,\n evaluated: results.length,\n results,\n done,\n // The caller refuses to loop on `done:false` with no cursor, which is the\n // one shape that would re-read the same chunk for ever.\n nextCursor: done ? null : lastPath,\n })\n}\n"],"names":["writeCronBeat","firebaseAdmin","materializeDynamicList","LISTS_PER_CHUNK","listsMaterializeHandler","req","res","method","status","json","error","cronSecret","process","env","CRON_SECRET","headers","firestore","app","cursor","String","body","query","collectionGroup","where","orderBy","FieldPath","documentId","limit","startAfter","doc","lists","get","results","lastPath","listDoc","docs","ref","path","hostId","push","list","outcome","listRef","rule","resume","console","done","size","ok","evaluated","length","nextCursor"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAA+B,sBAAqB;AAC1E,SACEC,aAAa,EACbC,sBAAsB,QAEjB,2BAA0B;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,4CAA4C,GAC5C,MAAMC,kBAAkB;AAExB,OAAO,MAAMC,0BAA4C,OAAOC,KAAKC;;QAwB5CD;IAvBvB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,aAAaC,QAAQC,GAAG,CAACC,WAAW;IAC1C,IAAI,CAACH,YAAY;QACf,OAAOL,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;YAAEC,OAAO;QAAkD;IACrE;IACA,IAAIL,IAAIU,OAAO,CAAC,gBAAgB,KAAKJ,YAAY;QAC/C,OAAOL,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAkB;IACzD;IAEA,MAAMM,YAAYf,cAAcgB,GAAG,GAAGD,SAAS;IAC/C;;;;;;GAMC,GACD,MAAMhB,cAAcgB,WAAW;IAE/B,MAAME,SAASC,gBAAQd,YAAAA,IAAIe,IAAI,qBAAT,AAACf,SAAsC,CAAC,SAAS,mBAAI;IAC3E,IAAIgB,QAAQL,UACTM,eAAe,CAAC,SAChBC,KAAK,CAAC,QAAQ,MAAM,WACpBC,OAAO,CAACvB,cAAce,SAAS,CAACS,SAAS,CAACC,UAAU,IACpDC,KAAK,CAACxB;IACT;;;;;GAKC,GACD,IAAIe,QAAQG,QAAQA,MAAMO,UAAU,CAACZ,UAAUa,GAAG,CAACX;IAEnD,MAAMY,QAAQ,MAAMT,MAAMU,GAAG;IAC7B,MAAMC,UAA0C,EAAE;IAClD,IAAIC,WAAW;IACf,KAAK,MAAMC,WAAWJ,MAAMK,IAAI,CAAE;YAEVD;QADtBD,WAAWC,QAAQE,GAAG,CAACC,IAAI;QAC3B,MAAMC,SAASnB,QAAOe,eAAAA,QAAQH,GAAG,CAAC,qBAAZG,eAAyB;QAC/C,IAAI,CAACI,QAAQ;YACX,kEAAkE;YAClE,sEAAsE;YACtE,6BAA6B;YAC7BN,QAAQO,IAAI,CAAC;gBAAEC,MAAMN,QAAQE,GAAG,CAACC,IAAI;gBAAE3B,OAAO;YAAY;YAC1D;QACF;QACA,IAAI;gBAKSwB;YAJX,MAAMO,UAAU,MAAMvC,uBAAuB;gBAC3CwC,SAASR,QAAQE,GAAG;gBACpBE;gBACAK,MAAMT,QAAQH,GAAG,CAAC;gBAClBa,MAAM,GAAGV,gBAAAA,QAAQH,GAAG,CAAC,+BAAZG,gBAAyD;YACpE;YACAF,QAAQO,IAAI,CAAC;gBAAEC,MAAMN,QAAQE,GAAG,CAACC,IAAI;eAAKI;QAC5C,EAAE,OAAO/B,OAAO;YACdmC,QAAQnC,KAAK,CAAC,mCAAmCwB,QAAQE,GAAG,CAACC,IAAI,EAAE3B;YACnEsB,QAAQO,IAAI,CAAC;gBAAEC,MAAMN,QAAQE,GAAG,CAACC,IAAI;gBAAE3B,OAAO;YAAoB;QACpE;IACF;IAEA,MAAMoC,OAAOhB,MAAMiB,IAAI,GAAG5C;IAC1B,OAAOG,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAC1BuC,IAAI;QACJC,WAAWjB,QAAQkB,MAAM;QACzBlB;QACAc;QACA,0EAA0E;QAC1E,wDAAwD;QACxDK,YAAYL,OAAO,OAAOb;IAC5B;AACF,EAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
+ /**
19
+ * A site's campaign and A/B testing results as figure tables another plugin
20
+ * reads by id (AGL-2915).
21
+ *
22
+ * The campaign table is what each send's report shows — delivered, and the
23
+ * open and click rates over delivered — through `campaignReport`, the one
24
+ * reader of a send's counters. The experiments table is what the A/B testing
25
+ * section shows, through the same variant comparison. No recipient, address
26
+ * or visitor reaches either: a campaign is named by its subject, a variant by
27
+ * its name.
28
+ */
29
+ type Firestore = FirebaseFirestore.Firestore;
30
+ /** Sends a campaign table reads, newest first, before it keeps those sent in the window. */
31
+ export declare const CAMPAIGN_FIGURES_READ_LIMIT = 100;
32
+ /** Experiments an experiments table reads. */
33
+ export declare const EXPERIMENT_FIGURES_READ_LIMIT = 20;
34
+ export declare function marketingFigureReaders(firestore: () => Firestore): PluginFigureReader[];
35
+ /** Registers the campaign and A/B testing readers from the console surface, where insight jobs run. */
36
+ export declare function registerMarketingFigureReaders(firestore: () => Firestore): void;
37
+ export {};
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
17
+ import { campaignReport } from "@aglyn/shared-ui-email-campaigns/model/campaign-report";
18
+ import { EMAIL_CREATED_AT_FIELD, emailSendTimeMs } from "@aglyn/shared-ui-email-campaigns/model/email-record";
19
+ import { compareVariants, summarizeVariantStats } from "../model/experiments.js";
20
+ const MARKETING_PLUGIN_ID = 'marketing';
21
+ /** Sends a campaign table reads, newest first, before it keeps those sent in the window. */ export const CAMPAIGN_FIGURES_READ_LIMIT = 100;
22
+ /** Experiments an experiments table reads. */ export const EXPERIMENT_FIGURES_READ_LIMIT = 20;
23
+ const WINDOWS = [
24
+ 7,
25
+ 14,
26
+ 30,
27
+ 90
28
+ ];
29
+ const percent = (rate)=>rate ? Math.round(rate.value * 1000) / 10 : null;
30
+ /** Rates pooled across sends: every numerator over every denominator, never an average of rates. */ function pooled(rates) {
31
+ const known = rates.filter((rate)=>rate !== null);
32
+ const denominator = known.reduce((sum, rate)=>sum + rate.denominator, 0);
33
+ if (!denominator) return null;
34
+ return Math.round(known.reduce((sum, rate)=>sum + rate.numerator, 0) / denominator * 1000) / 10;
35
+ }
36
+ export function marketingFigureReaders(firestore) {
37
+ return [
38
+ {
39
+ id: 'marketing.campaigns',
40
+ label: 'Campaigns',
41
+ description: 'Each campaign email sent from the site in the window: how many were delivered and the share of delivered messages opened and clicked, with every campaign together on the first row.',
42
+ scope: 'site',
43
+ windows: WINDOWS,
44
+ read: async (request)=>{
45
+ if (!request.hostId) return {
46
+ ok: false,
47
+ status: 400,
48
+ error: 'Open a site to read its campaigns'
49
+ };
50
+ if (!WINDOWS.includes(request.days)) {
51
+ return {
52
+ ok: false,
53
+ status: 400,
54
+ error: 'That window is not one campaigns are read over'
55
+ };
56
+ }
57
+ const { current } = pluginFigureWindows(request.now, request.days);
58
+ const snapshot = await firestore().collection('hosts').doc(request.hostId).collection('campaigns').orderBy(EMAIL_CREATED_AT_FIELD, 'desc').limit(CAMPAIGN_FIGURES_READ_LIMIT).get();
59
+ const sends = snapshot.docs.map((doc)=>{
60
+ var _doc_data;
61
+ return (_doc_data = doc.data()) != null ? _doc_data : {};
62
+ }).filter((record)=>{
63
+ const sentAt = emailSendTimeMs(record);
64
+ return Boolean(record['sentAt']) && sentAt >= current.startMs && sentAt < current.endMs;
65
+ }).map((record)=>{
66
+ var _record_subject;
67
+ return {
68
+ subject: String((_record_subject = record['subject']) != null ? _record_subject : '').trim() || 'A campaign with no subject',
69
+ report: campaignReport(record['stats'])
70
+ };
71
+ });
72
+ const rows = [
73
+ {
74
+ campaign: 'All campaigns',
75
+ delivered: sends.every((send)=>send.report.delivered === null) ? null : sends.reduce((sum, send)=>{
76
+ var _send_report_delivered;
77
+ return sum + ((_send_report_delivered = send.report.delivered) != null ? _send_report_delivered : 0);
78
+ }, 0),
79
+ openRate: pooled(sends.map((send)=>send.report.rates.open)),
80
+ clickRate: pooled(sends.map((send)=>send.report.rates.click))
81
+ },
82
+ ...sends.slice(0, PLUGIN_FIGURE_MAX_ROWS - 1).map((send)=>({
83
+ campaign: send.subject,
84
+ delivered: send.report.delivered,
85
+ openRate: percent(send.report.rates.open),
86
+ clickRate: percent(send.report.rates.click)
87
+ }))
88
+ ];
89
+ return {
90
+ ok: true,
91
+ table: {
92
+ title: 'Campaigns',
93
+ source: {
94
+ label: 'Campaigns',
95
+ path: 'marketing/campaigns'
96
+ },
97
+ period: {
98
+ from: current.from,
99
+ to: current.to,
100
+ days: request.days
101
+ },
102
+ columns: [
103
+ {
104
+ key: 'campaign',
105
+ label: 'Campaign',
106
+ kind: 'text'
107
+ },
108
+ {
109
+ key: 'delivered',
110
+ label: 'Delivered',
111
+ kind: 'count'
112
+ },
113
+ {
114
+ key: 'openRate',
115
+ label: 'Opened',
116
+ kind: 'percent'
117
+ },
118
+ {
119
+ key: 'clickRate',
120
+ label: 'Clicked',
121
+ kind: 'percent'
122
+ }
123
+ ],
124
+ rows,
125
+ omitted: Math.max(0, sends.length - (PLUGIN_FIGURE_MAX_ROWS - 1)),
126
+ notes: [
127
+ 'Rates are over delivered messages, and a campaign with no delivery recorded has none.',
128
+ ...snapshot.docs.length === CAMPAIGN_FIGURES_READ_LIMIT ? [
129
+ `Only the ${CAMPAIGN_FIGURES_READ_LIMIT} most recent emails were looked at.`
130
+ ] : []
131
+ ]
132
+ }
133
+ };
134
+ }
135
+ },
136
+ {
137
+ id: 'marketing.experiments',
138
+ label: 'A/B tests',
139
+ description: 'Each running or finished A/B test on the site: every variant’s visitors shown it, conversions and conversion rate, with the lift over the first variant and how confident that lift is.',
140
+ scope: 'site',
141
+ windows: [],
142
+ feature: 'abTesting',
143
+ read: async (request)=>{
144
+ if (!request.hostId) return {
145
+ ok: false,
146
+ status: 400,
147
+ error: 'Open a site to read its A/B tests'
148
+ };
149
+ const experimentsRef = firestore().collection('hosts').doc(request.hostId).collection('experiments');
150
+ const snapshot = await experimentsRef.limit(EXPERIMENT_FIGURES_READ_LIMIT).get();
151
+ const rows = [];
152
+ for (const doc of snapshot.docs){
153
+ var _doc_data, _experiment_variants, _byVariant_get;
154
+ const experiment = (_doc_data = doc.data()) != null ? _doc_data : {};
155
+ if (experiment.status !== 'running' && experiment.status !== 'done') continue;
156
+ const variants = ((_experiment_variants = experiment.variants) != null ? _experiment_variants : []).slice(0, 4);
157
+ if (!variants.length) continue;
158
+ const stats = await doc.ref.collection('stats').get();
159
+ const byVariant = new Map(stats.docs.map((entry)=>{
160
+ var _entry_data;
161
+ return [
162
+ entry.id,
163
+ (_entry_data = entry.data()) != null ? _entry_data : {}
164
+ ];
165
+ }));
166
+ const control = (_byVariant_get = byVariant.get(variants[0].id)) != null ? _byVariant_get : {};
167
+ variants.forEach((variant, index)=>{
168
+ var _byVariant_get, _experiment_name, _variant_name;
169
+ const own = (_byVariant_get = byVariant.get(variant.id)) != null ? _byVariant_get : {};
170
+ const summary = summarizeVariantStats(own);
171
+ const comparison = index === 0 ? null : compareVariants(control, own);
172
+ rows.push({
173
+ test: String((_experiment_name = experiment.name) != null ? _experiment_name : '').trim() || doc.id,
174
+ variant: String((_variant_name = variant.name) != null ? _variant_name : '').trim() || variant.id,
175
+ shown: summary.exposures,
176
+ conversions: summary.conversions,
177
+ rate: summary.exposures ? Math.round(summary.rate * 1000) / 10 : null,
178
+ lift: (comparison == null ? void 0 : comparison.lift) === null || comparison === null ? null : Math.round(comparison.lift * 1000) / 10,
179
+ confidence: (comparison == null ? void 0 : comparison.confidence) === null || comparison === null ? null : Math.round(comparison.confidence * 1000) / 10
180
+ });
181
+ });
182
+ }
183
+ return {
184
+ ok: true,
185
+ table: {
186
+ title: 'A/B tests',
187
+ source: {
188
+ label: 'A/B testing',
189
+ path: 'marketing/experiments'
190
+ },
191
+ period: null,
192
+ columns: [
193
+ {
194
+ key: 'test',
195
+ label: 'Test',
196
+ kind: 'text'
197
+ },
198
+ {
199
+ key: 'variant',
200
+ label: 'Variant',
201
+ kind: 'text'
202
+ },
203
+ {
204
+ key: 'shown',
205
+ label: 'Shown',
206
+ kind: 'count'
207
+ },
208
+ {
209
+ key: 'conversions',
210
+ label: 'Conversions',
211
+ kind: 'count'
212
+ },
213
+ {
214
+ key: 'rate',
215
+ label: 'Conversion rate',
216
+ kind: 'percent'
217
+ },
218
+ {
219
+ key: 'lift',
220
+ label: 'Lift over the first variant',
221
+ kind: 'change'
222
+ },
223
+ {
224
+ key: 'confidence',
225
+ label: 'Confidence in the lift',
226
+ kind: 'percent'
227
+ }
228
+ ],
229
+ rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),
230
+ omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),
231
+ notes: [
232
+ 'Figures are totals since each test started; the first variant of a test is its control.'
233
+ ]
234
+ }
235
+ };
236
+ }
237
+ }
238
+ ];
239
+ }
240
+ /** Registers the campaign and A/B testing readers from the console surface, where insight jobs run. */ export function registerMarketingFigureReaders(firestore) {
241
+ for (const reader of marketingFigureReaders(firestore)){
242
+ registerPluginFigureReader(reader, {
243
+ pluginId: MARKETING_PLUGIN_ID
244
+ });
245
+ }
246
+ }
247
+
248
+ //# sourceMappingURL=marketing-figures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/marketing-figures.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n campaignReport,\n type CampaignRate,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n EMAIL_CREATED_AT_FIELD,\n emailSendTimeMs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport { compareVariants, summarizeVariantStats, type HostExperiment } from '../model/experiments'\n\n/**\n * A site's campaign and A/B testing results as figure tables another plugin\n * reads by id (AGL-2915).\n *\n * The campaign table is what each send's report shows — delivered, and the\n * open and click rates over delivered — through `campaignReport`, the one\n * reader of a send's counters. The experiments table is what the A/B testing\n * section shows, through the same variant comparison. No recipient, address\n * or visitor reaches either: a campaign is named by its subject, a variant by\n * its name.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst MARKETING_PLUGIN_ID = 'marketing'\n\n/** Sends a campaign table reads, newest first, before it keeps those sent in the window. */\nexport const CAMPAIGN_FIGURES_READ_LIMIT = 100\n\n/** Experiments an experiments table reads. */\nexport const EXPERIMENT_FIGURES_READ_LIMIT = 20\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nconst percent = (rate: CampaignRate | null): number | null =>\n rate ? Math.round(rate.value * 1_000) / 10 : null\n\n/** Rates pooled across sends: every numerator over every denominator, never an average of rates. */\nfunction pooled(rates: ReadonlyArray<CampaignRate | null>): number | null {\n const known = rates.filter((rate): rate is CampaignRate => rate !== null)\n const denominator = known.reduce((sum, rate) => sum + rate.denominator, 0)\n if (!denominator) return null\n return Math.round((known.reduce((sum, rate) => sum + rate.numerator, 0) / denominator) * 1_000) / 10\n}\n\nexport function marketingFigureReaders(firestore: () => Firestore): PluginFigureReader[] {\n return [\n {\n id: 'marketing.campaigns',\n label: 'Campaigns',\n description:\n 'Each campaign email sent from the site in the window: how many were delivered and the share of delivered messages opened and clicked, with every campaign together on the first row.',\n scope: 'site',\n windows: WINDOWS,\n read: async (request) => {\n if (!request.hostId) return { ok: false, status: 400, error: 'Open a site to read its campaigns' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one campaigns are read over' }\n }\n const { current } = pluginFigureWindows(request.now, request.days)\n const snapshot = await firestore()\n .collection('hosts')\n .doc(request.hostId)\n .collection('campaigns')\n .orderBy(EMAIL_CREATED_AT_FIELD, 'desc')\n .limit(CAMPAIGN_FIGURES_READ_LIMIT)\n .get()\n const sends = snapshot.docs\n .map((doc) => doc.data() ?? {})\n .filter((record) => {\n const sentAt = emailSendTimeMs(record)\n return Boolean(record['sentAt']) && sentAt >= current.startMs && sentAt < current.endMs\n })\n .map((record) => ({\n subject: String(record['subject'] ?? '').trim() || 'A campaign with no subject',\n report: campaignReport(record['stats'] as CampaignStats | undefined),\n }))\n const rows: PluginFigureRow[] = [\n {\n campaign: 'All campaigns',\n delivered: sends.every((send) => send.report.delivered === null)\n ? null\n : sends.reduce((sum, send) => sum + (send.report.delivered ?? 0), 0),\n openRate: pooled(sends.map((send) => send.report.rates.open)),\n clickRate: pooled(sends.map((send) => send.report.rates.click)),\n },\n ...sends.slice(0, PLUGIN_FIGURE_MAX_ROWS - 1).map((send) => ({\n campaign: send.subject,\n delivered: send.report.delivered,\n openRate: percent(send.report.rates.open),\n clickRate: percent(send.report.rates.click),\n })),\n ]\n return {\n ok: true,\n table: {\n title: 'Campaigns',\n source: { label: 'Campaigns', path: 'marketing/campaigns' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'campaign', label: 'Campaign', kind: 'text' },\n { key: 'delivered', label: 'Delivered', kind: 'count' },\n { key: 'openRate', label: 'Opened', kind: 'percent' },\n { key: 'clickRate', label: 'Clicked', kind: 'percent' },\n ],\n rows,\n omitted: Math.max(0, sends.length - (PLUGIN_FIGURE_MAX_ROWS - 1)),\n notes: [\n 'Rates are over delivered messages, and a campaign with no delivery recorded has none.',\n ...(snapshot.docs.length === CAMPAIGN_FIGURES_READ_LIMIT\n ? [`Only the ${CAMPAIGN_FIGURES_READ_LIMIT} most recent emails were looked at.`]\n : []),\n ],\n },\n }\n },\n },\n {\n id: 'marketing.experiments',\n label: 'A/B tests',\n description:\n 'Each running or finished A/B test on the site: every variant’s visitors shown it, conversions and conversion rate, with the lift over the first variant and how confident that lift is.',\n scope: 'site',\n windows: [],\n feature: 'abTesting',\n read: async (request) => {\n if (!request.hostId) return { ok: false, status: 400, error: 'Open a site to read its A/B tests' }\n const experimentsRef = firestore().collection('hosts').doc(request.hostId).collection('experiments')\n const snapshot = await experimentsRef.limit(EXPERIMENT_FIGURES_READ_LIMIT).get()\n const rows: PluginFigureRow[] = []\n for (const doc of snapshot.docs) {\n const experiment = (doc.data() ?? {}) as Partial<HostExperiment>\n if (experiment.status !== 'running' && experiment.status !== 'done') continue\n const variants = (experiment.variants ?? []).slice(0, 4)\n if (!variants.length) continue\n const stats = await doc.ref.collection('stats').get()\n const byVariant = new Map(stats.docs.map((entry) => [entry.id, entry.data() ?? {}]))\n const control = byVariant.get(variants[0].id) ?? {}\n variants.forEach((variant, index) => {\n const own = byVariant.get(variant.id) ?? {}\n const summary = summarizeVariantStats(own)\n const comparison = index === 0 ? null : compareVariants(control, own)\n rows.push({\n test: String(experiment.name ?? '').trim() || doc.id,\n variant: String(variant.name ?? '').trim() || variant.id,\n shown: summary.exposures,\n conversions: summary.conversions,\n rate: summary.exposures ? Math.round(summary.rate * 1_000) / 10 : null,\n lift: comparison?.lift === null || comparison === null ? null : Math.round(comparison.lift * 1_000) / 10,\n confidence:\n comparison?.confidence === null || comparison === null\n ? null\n : Math.round(comparison.confidence * 1_000) / 10,\n })\n })\n }\n return {\n ok: true,\n table: {\n title: 'A/B tests',\n source: { label: 'A/B testing', path: 'marketing/experiments' },\n period: null,\n columns: [\n { key: 'test', label: 'Test', kind: 'text' },\n { key: 'variant', label: 'Variant', kind: 'text' },\n { key: 'shown', label: 'Shown', kind: 'count' },\n { key: 'conversions', label: 'Conversions', kind: 'count' },\n { key: 'rate', label: 'Conversion rate', kind: 'percent' },\n { key: 'lift', label: 'Lift over the first variant', kind: 'change' },\n { key: 'confidence', label: 'Confidence in the lift', kind: 'percent' },\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: ['Figures are totals since each test started; the first variant of a test is its control.'],\n },\n }\n },\n },\n ]\n}\n\n/** Registers the campaign and A/B testing readers from the console surface, where insight jobs run. */\nexport function registerMarketingFigureReaders(firestore: () => Firestore): void {\n for (const reader of marketingFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: MARKETING_PLUGIN_ID })\n }\n}\n"],"names":["PLUGIN_FIGURE_MAX_ROWS","pluginFigureWindows","registerPluginFigureReader","campaignReport","EMAIL_CREATED_AT_FIELD","emailSendTimeMs","compareVariants","summarizeVariantStats","MARKETING_PLUGIN_ID","CAMPAIGN_FIGURES_READ_LIMIT","EXPERIMENT_FIGURES_READ_LIMIT","WINDOWS","percent","rate","Math","round","value","pooled","rates","known","filter","denominator","reduce","sum","numerator","marketingFigureReaders","firestore","id","label","description","scope","windows","read","request","hostId","ok","status","error","includes","days","current","now","snapshot","collection","doc","orderBy","limit","get","sends","docs","map","data","record","sentAt","Boolean","startMs","endMs","subject","String","trim","report","rows","campaign","delivered","every","send","openRate","open","clickRate","click","slice","table","title","source","path","period","from","to","columns","key","kind","omitted","max","length","notes","feature","experimentsRef","experiment","byVariant","variants","stats","ref","Map","entry","control","forEach","variant","index","own","summary","comparison","push","test","name","shown","exposures","conversions","lift","confidence","registerMarketingFigureReaders","reader","pluginId"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { SitePageEnricher } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Marketing contributions to a rendered site page (AGL-418), relocated
20
+ * verbatim from the tenant loader: overlays (announcement bar + popup with
21
+ * server-resolved binding tokens, AGL-195/196/251), site-event automations
22
+ * (AGL-256/257), and screen/section experiments (AGL-253). Every block is
23
+ * entitlement-gated on the owning org's plan exactly as before; the shapes
24
+ * written here are the wire contract the tenant page runtime consumes.
25
+ */
26
+ export declare const marketingSitePageEnricher: SitePageEnricher;