@aglyn/plugins-marketing 1.0.0-beta.221 → 1.0.0-beta.223

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 (127) hide show
  1. package/README.md +4 -1
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +53 -64
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +18 -3
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-edit-drawer.d.ts +6 -0
  8. package/src/lib/components/campaign-edit-drawer.js +13 -2
  9. package/src/lib/components/campaign-edit-drawer.js.map +1 -1
  10. package/src/lib/components/campaign-queries.d.ts +5 -0
  11. package/src/lib/components/campaign-queries.js +8 -1
  12. package/src/lib/components/campaign-queries.js.map +1 -1
  13. package/src/lib/components/campaign-reach-sections.d.ts +28 -0
  14. package/src/lib/components/campaign-reach-sections.js +96 -5
  15. package/src/lib/components/campaign-reach-sections.js.map +1 -1
  16. package/src/lib/components/campaign-report-card.js +5 -4
  17. package/src/lib/components/campaign-report-card.js.map +1 -1
  18. package/src/lib/components/campaigns-card.js +1 -1
  19. package/src/lib/components/campaigns-card.js.map +1 -1
  20. package/src/lib/components/conversion-attribution.component.d.ts +18 -0
  21. package/src/lib/components/conversion-attribution.component.js +177 -39
  22. package/src/lib/components/conversion-attribution.component.js.map +1 -1
  23. package/src/lib/components/email-compose-card.js +1 -1
  24. package/src/lib/components/email-compose-card.js.map +1 -1
  25. package/src/lib/components/email-detail.js +5 -4
  26. package/src/lib/components/email-detail.js.map +1 -1
  27. package/src/lib/components/email-template-report-card.d.ts +53 -0
  28. package/src/lib/components/email-template-report-card.js +469 -0
  29. package/src/lib/components/email-template-report-card.js.map +1 -0
  30. package/src/lib/components/emails-list-card.js +2 -2
  31. package/src/lib/components/emails-list-card.js.map +1 -1
  32. package/src/lib/components/host-experiments-card.component.js +3 -2
  33. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  34. package/src/lib/components/record-attribution-widget.d.ts +5 -0
  35. package/src/lib/components/record-attribution-widget.js +6 -3
  36. package/src/lib/components/record-attribution-widget.js.map +1 -1
  37. package/src/lib/components/site-runtime.d.ts +5 -0
  38. package/src/lib/components/site-runtime.js +104 -3
  39. package/src/lib/components/site-runtime.js.map +1 -1
  40. package/src/lib/declarations.server.d.ts +9 -0
  41. package/src/lib/declarations.server.js +78 -0
  42. package/src/lib/declarations.server.js.map +1 -1
  43. package/src/lib/model/campaign-container.d.ts +531 -0
  44. package/src/lib/model/campaign-container.js +532 -0
  45. package/src/lib/model/campaign-container.js.map +1 -0
  46. package/src/lib/model/campaign-conversions.d.ts +55 -4
  47. package/src/lib/model/campaign-conversions.js +60 -4
  48. package/src/lib/model/campaign-conversions.js.map +1 -1
  49. package/src/lib/model/campaign-report.d.ts +158 -0
  50. package/src/lib/model/campaign-report.js +268 -0
  51. package/src/lib/model/campaign-report.js.map +1 -0
  52. package/src/lib/model/campaign-revenue.d.ts +3 -3
  53. package/src/lib/model/campaign-revenue.js +3 -3
  54. package/src/lib/model/campaign-revenue.js.map +1 -1
  55. package/src/lib/model/campaign-send-time.js +2 -2
  56. package/src/lib/model/campaign-send-time.js.map +1 -1
  57. package/src/lib/model/campaign-visits.d.ts +74 -0
  58. package/src/lib/model/campaign-visits.js +65 -0
  59. package/src/lib/model/campaign-visits.js.map +1 -0
  60. package/src/lib/model/compile-client-automations.d.ts +2 -2
  61. package/src/lib/model/compile-client-automations.js +3 -3
  62. package/src/lib/model/compile-client-automations.js.map +1 -1
  63. package/src/lib/model/email-record.d.ts +175 -0
  64. package/src/lib/model/email-record.js +206 -0
  65. package/src/lib/model/email-record.js.map +1 -0
  66. package/src/lib/model/site-contract.d.ts +2 -2
  67. package/src/lib/model/site-contract.js.map +1 -1
  68. package/src/lib/model/template-report.d.ts +158 -0
  69. package/src/lib/model/template-report.js +249 -0
  70. package/src/lib/model/template-report.js.map +1 -0
  71. package/src/lib/plugin.js +16 -0
  72. package/src/lib/plugin.js.map +1 -1
  73. package/src/lib/preview-props.js.map +1 -1
  74. package/src/lib/server/campaign-attribution-store.d.ts +55 -0
  75. package/src/lib/server/campaign-attribution-store.js +98 -0
  76. package/src/lib/server/campaign-attribution-store.js.map +1 -0
  77. package/src/lib/server/campaign-conversion-attribution.d.ts +314 -0
  78. package/src/lib/server/campaign-conversion-attribution.js +549 -0
  79. package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
  80. package/src/lib/server/campaign-manage.js +1 -1
  81. package/src/lib/server/campaign-manage.js.map +1 -1
  82. package/src/lib/server/campaign-org-refs.js +1 -1
  83. package/src/lib/server/campaign-org-refs.js.map +1 -1
  84. package/src/lib/server/campaign-recipients.js +7 -7
  85. package/src/lib/server/campaign-recipients.js.map +1 -1
  86. package/src/lib/server/campaign-send.js +25 -19
  87. package/src/lib/server/campaign-send.js.map +1 -1
  88. package/src/lib/server/campaign-touch-targets.d.ts +61 -0
  89. package/src/lib/server/campaign-touch-targets.js +162 -0
  90. package/src/lib/server/campaign-touch-targets.js.map +1 -0
  91. package/src/lib/server/campaign-visit-beacon.d.ts +44 -0
  92. package/src/lib/server/campaign-visit-beacon.js +125 -0
  93. package/src/lib/server/campaign-visit-beacon.js.map +1 -0
  94. package/src/lib/server/conversion-creditor.d.ts +18 -0
  95. package/src/lib/server/conversion-creditor.js +238 -0
  96. package/src/lib/server/conversion-creditor.js.map +1 -0
  97. package/src/lib/server/email-campaign-touch.d.ts +81 -0
  98. package/src/lib/server/email-campaign-touch.js +227 -0
  99. package/src/lib/server/email-campaign-touch.js.map +1 -0
  100. package/src/lib/server/email-events.js +32 -31
  101. package/src/lib/server/email-events.js.map +1 -1
  102. package/src/lib/server/email-revenue-attribution.d.ts +186 -0
  103. package/src/lib/server/email-revenue-attribution.js +322 -0
  104. package/src/lib/server/email-revenue-attribution.js.map +1 -0
  105. package/src/lib/server/email-send-record-index.d.ts +34 -0
  106. package/src/lib/server/email-send-record-index.js +90 -0
  107. package/src/lib/server/email-send-record-index.js.map +1 -0
  108. package/src/lib/server/get-client-automations.js.map +1 -1
  109. package/src/lib/server/marketing-figures.js +3 -3
  110. package/src/lib/server/marketing-figures.js.map +1 -1
  111. package/src/lib/server/send-tally.d.ts +32 -0
  112. package/src/lib/server/send-tally.js +54 -0
  113. package/src/lib/server/send-tally.js.map +1 -0
  114. package/src/lib/server/site-page-enricher.js +18 -2
  115. package/src/lib/server/site-page-enricher.js.map +1 -1
  116. package/src/lib/testing/stand-in-person-records.d.ts +35 -0
  117. package/src/lib/testing/stand-in-person-records.js +67 -0
  118. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  119. package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
  120. package/src/lib/testing/stand-in-record-counts.js +70 -0
  121. package/src/lib/testing/stand-in-record-counts.js.map +1 -0
  122. package/src/lib/transfer/campaigns-package.d.ts +72 -0
  123. package/src/lib/transfer/campaigns-package.js +145 -0
  124. package/src/lib/transfer/campaigns-package.js.map +1 -0
  125. package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
  126. package/src/lib/transfer/campaigns-package.server.js +305 -0
  127. package/src/lib/transfer/campaigns-package.server.js.map +1 -0
@@ -0,0 +1,61 @@
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
+ /** A campaign container, as much of it as a touch's credit needs. */
18
+ export interface LiveCampaign {
19
+ id: string;
20
+ name: string;
21
+ }
22
+ /**
23
+ * The campaign at `orgs/{orgId}/emailCampaigns/{campaignId}`, or `null` when
24
+ * it does not exist or its owner retired it.
25
+ */
26
+ export declare function readLiveCampaign(options: {
27
+ orgId: string;
28
+ campaignId: string;
29
+ }, firestore: any): Promise<LiveCampaign | null>;
30
+ /**
31
+ * The campaigns a site's screen is filed under right now, or `null` when the
32
+ * screen does not exist.
33
+ */
34
+ export declare function readScreenCampaignIds(options: {
35
+ hostId: string;
36
+ screenId: string;
37
+ }, firestore: any): Promise<string[] | null>;
38
+ /**
39
+ * Of the campaigns a page touch carries, the first that the page is STILL
40
+ * filed under and that still exists — or `null`.
41
+ *
42
+ * The touch carries the screen's campaigns in the screen's own order, so a
43
+ * page filed under two campaigns credits the one it names first. One
44
+ * outcome, one campaign: crediting both would count one visitor's form twice
45
+ * across the org's campaigns.
46
+ */
47
+ export declare function filedCampaignForPage(options: {
48
+ orgId: string;
49
+ hostId: string;
50
+ screenId: string;
51
+ campaignIds: readonly string[];
52
+ }, firestore: any): Promise<LiveCampaign | null>;
53
+ /**
54
+ * The one live campaign placed on this site that declares `label` among its
55
+ * `utmCampaigns`, or `null` — for no such campaign, and for two.
56
+ */
57
+ export declare function campaignForUtmLabel(options: {
58
+ orgId: string;
59
+ hostId: string;
60
+ label: string | null | undefined;
61
+ }, firestore: any): Promise<LiveCampaign | null>;
@@ -0,0 +1,162 @@
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 { EMAIL_CAMPAIGNS_COLLECTION, campaignPlacedOnHost, normalizeCampaignUtmLabel } from "../model/campaign-container.js";
17
+ import { readContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
18
+ import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
19
+ import { CAMPAIGN_KIND } from "../model/campaign-kind.js";
20
+ /**
21
+ * WHAT A VISITOR'S TOUCH NAMES, CHECKED AGAINST WHAT IS STORED (AGL-3461).
22
+ *
23
+ * Two touches name a campaign DOCUMENT without a click on its mail: a page
24
+ * the visitor viewed that is filed under a campaign, and a `utm_campaign`
25
+ * label a campaign declares as its own. Both arrive from the visitor's
26
+ * browser, so neither is trusted as sent:
27
+ *
28
+ * - a page touch credits a campaign only while the campaign exists and the
29
+ * page is still filed under it — one screen read and one campaign read;
30
+ * - a label credits a campaign only when exactly one live campaign placed
31
+ * on the site declares it — one `array-contains` query on the org's
32
+ * campaigns, served by the automatic single-field index. A label two
33
+ * campaigns claim credits neither, rather than whichever the index
34
+ * happens to return first.
35
+ *
36
+ * The conversion join (`campaign-conversion-attribution.ts`) asks at an
37
+ * identify moment and the first-visit beacon (`campaign-visit-beacon.ts`) on
38
+ * a pageview. Each answer is memoized for a minute per Firestore instance —
39
+ * the beacon is per pageview and a landing page's visitors all ask the same
40
+ * question — so a campaign deleted or a page refiled is believed within the
41
+ * minute, which is the staleness the site collector's own caches accept.
42
+ *
43
+ * Never throws: a lookup that fails answers "names nothing", and the touch is
44
+ * then treated as the label or the page it is, credited to no document.
45
+ */ /** How long one answer is believed, per Firestore instance. */ const MEMO_MS = 60000;
46
+ /** Size cap on each memo; cleared wholesale like every visitor-keyed map. */ const MEMO_MAX = 2000;
47
+ const memos = new WeakMap();
48
+ function memoFor(firestore) {
49
+ let memo = memos.get(firestore);
50
+ if (!memo) {
51
+ memo = new Map();
52
+ memos.set(firestore, memo);
53
+ }
54
+ return memo;
55
+ }
56
+ async function remembered(firestore, key, read) {
57
+ const memo = memoFor(firestore);
58
+ const hit = memo.get(key);
59
+ const now = Date.now();
60
+ if (hit && now - hit.at < MEMO_MS) return hit.value;
61
+ const value = await read();
62
+ if (memo.size > MEMO_MAX) memo.clear();
63
+ memo.set(key, {
64
+ value,
65
+ at: now
66
+ });
67
+ return value;
68
+ }
69
+ /**
70
+ * The campaign at `orgs/{orgId}/emailCampaigns/{campaignId}`, or `null` when
71
+ * it does not exist or its owner retired it.
72
+ */ export async function readLiveCampaign(options, firestore) {
73
+ const { orgId, campaignId } = options;
74
+ if (!isDocumentId(orgId) || !isDocumentId(campaignId)) return null;
75
+ try {
76
+ return await remembered(firestore, `campaign:${orgId}:${campaignId}`, async ()=>{
77
+ var _snapshot_data, _data_name;
78
+ const snapshot = await firestore.collection('orgs').doc(orgId).collection(EMAIL_CAMPAIGNS_COLLECTION).doc(campaignId).get();
79
+ if (!snapshot.exists) return null;
80
+ const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
81
+ if (data.deletedAt) return null;
82
+ return {
83
+ id: campaignId,
84
+ name: String((_data_name = data.name) != null ? _data_name : '').trim() || campaignId
85
+ };
86
+ });
87
+ } catch (error) {
88
+ console.error('[campaign-touch] campaign read failed', error);
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * The campaigns a site's screen is filed under right now, or `null` when the
94
+ * screen does not exist.
95
+ */ export async function readScreenCampaignIds(options, firestore) {
96
+ const { hostId, screenId } = options;
97
+ if (!isDocumentId(hostId) || !isDocumentId(screenId)) return null;
98
+ try {
99
+ return await remembered(firestore, `screen:${hostId}:${screenId}`, async ()=>{
100
+ var _snapshot_data;
101
+ const snapshot = await firestore.collection('hosts').doc(hostId).collection('screens').doc(screenId).get();
102
+ if (!snapshot.exists) return null;
103
+ return readContainerIds((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {}, CAMPAIGN_KIND);
104
+ });
105
+ } catch (error) {
106
+ console.error('[campaign-touch] screen read failed', error);
107
+ return null;
108
+ }
109
+ }
110
+ /**
111
+ * Of the campaigns a page touch carries, the first that the page is STILL
112
+ * filed under and that still exists — or `null`.
113
+ *
114
+ * The touch carries the screen's campaigns in the screen's own order, so a
115
+ * page filed under two campaigns credits the one it names first. One
116
+ * outcome, one campaign: crediting both would count one visitor's form twice
117
+ * across the org's campaigns.
118
+ */ export async function filedCampaignForPage(options, firestore) {
119
+ const filed = await readScreenCampaignIds(options, firestore);
120
+ if (!(filed == null ? void 0 : filed.length)) return null;
121
+ for (const campaignId of options.campaignIds){
122
+ if (!filed.includes(campaignId)) continue;
123
+ const campaign = await readLiveCampaign({
124
+ orgId: options.orgId,
125
+ campaignId
126
+ }, firestore);
127
+ if (campaign) return campaign;
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * The one live campaign placed on this site that declares `label` among its
133
+ * `utmCampaigns`, or `null` — for no such campaign, and for two.
134
+ */ export async function campaignForUtmLabel(options, firestore) {
135
+ const { orgId, hostId } = options;
136
+ const label = normalizeCampaignUtmLabel(options.label);
137
+ if (!label || !isDocumentId(orgId)) return null;
138
+ try {
139
+ return await remembered(firestore, `label:${orgId}:${hostId}:${label}`, async ()=>{
140
+ var _snapshot_docs, _match_data_name;
141
+ const snapshot = await firestore.collection('orgs').doc(orgId).collection(EMAIL_CAMPAIGNS_COLLECTION).where('utmCampaigns', 'array-contains', label).limit(5).get();
142
+ const matches = ((_snapshot_docs = snapshot.docs) != null ? _snapshot_docs : []).map((doc)=>{
143
+ var _doc_data;
144
+ return {
145
+ id: String(doc.id),
146
+ data: (_doc_data = doc.data()) != null ? _doc_data : {}
147
+ };
148
+ }).filter(({ data })=>!data.deletedAt).filter(({ data })=>campaignPlacedOnHost(data, hostId));
149
+ if (matches.length !== 1) return null;
150
+ const [match] = matches;
151
+ return {
152
+ id: match.id,
153
+ name: String((_match_data_name = match.data.name) != null ? _match_data_name : '').trim() || match.id
154
+ };
155
+ });
156
+ } catch (error) {
157
+ console.error('[campaign-touch] label lookup failed', error);
158
+ return null;
159
+ }
160
+ }
161
+
162
+ //# sourceMappingURL=campaign-touch-targets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-touch-targets.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 EMAIL_CAMPAIGNS_COLLECTION,\n campaignPlacedOnHost,\n normalizeCampaignUtmLabel,\n type EmailCampaign,\n} from '../model/campaign-container'\nimport { readContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport { CAMPAIGN_KIND } from '../model/campaign-kind'\n\n/**\n * WHAT A VISITOR'S TOUCH NAMES, CHECKED AGAINST WHAT IS STORED (AGL-3461).\n *\n * Two touches name a campaign DOCUMENT without a click on its mail: a page\n * the visitor viewed that is filed under a campaign, and a `utm_campaign`\n * label a campaign declares as its own. Both arrive from the visitor's\n * browser, so neither is trusted as sent:\n *\n * - a page touch credits a campaign only while the campaign exists and the\n * page is still filed under it — one screen read and one campaign read;\n * - a label credits a campaign only when exactly one live campaign placed\n * on the site declares it — one `array-contains` query on the org's\n * campaigns, served by the automatic single-field index. A label two\n * campaigns claim credits neither, rather than whichever the index\n * happens to return first.\n *\n * The conversion join (`campaign-conversion-attribution.ts`) asks at an\n * identify moment and the first-visit beacon (`campaign-visit-beacon.ts`) on\n * a pageview. Each answer is memoized for a minute per Firestore instance —\n * the beacon is per pageview and a landing page's visitors all ask the same\n * question — so a campaign deleted or a page refiled is believed within the\n * minute, which is the staleness the site collector's own caches accept.\n *\n * Never throws: a lookup that fails answers \"names nothing\", and the touch is\n * then treated as the label or the page it is, credited to no document.\n */\n\n/** How long one answer is believed, per Firestore instance. */\nconst MEMO_MS = 60_000\n\n/** Size cap on each memo; cleared wholesale like every visitor-keyed map. */\nconst MEMO_MAX = 2_000\n\ntype Memo = Map<string, { value: unknown; at: number }>\n\nconst memos = new WeakMap<object, Memo>()\n\nfunction memoFor(firestore: object): Memo {\n let memo = memos.get(firestore)\n if (!memo) {\n memo = new Map()\n memos.set(firestore, memo)\n }\n return memo\n}\n\nasync function remembered<T>(\n firestore: object,\n key: string,\n read: () => Promise<T>,\n): Promise<T> {\n const memo = memoFor(firestore)\n const hit = memo.get(key)\n const now = Date.now()\n if (hit && now - hit.at < MEMO_MS) return hit.value as T\n const value = await read()\n if (memo.size > MEMO_MAX) memo.clear()\n memo.set(key, { value, at: now })\n return value\n}\n\n/** A campaign container, as much of it as a touch's credit needs. */\nexport interface LiveCampaign {\n id: string\n name: string\n}\n\n/**\n * The campaign at `orgs/{orgId}/emailCampaigns/{campaignId}`, or `null` when\n * it does not exist or its owner retired it.\n */\nexport async function readLiveCampaign(\n options: { orgId: string; campaignId: string },\n firestore: any,\n): Promise<LiveCampaign | null> {\n const { orgId, campaignId } = options\n if (!isDocumentId(orgId) || !isDocumentId(campaignId)) return null\n try {\n return await remembered(firestore, `campaign:${orgId}:${campaignId}`, async () => {\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(EMAIL_CAMPAIGNS_COLLECTION)\n .doc(campaignId)\n .get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as Partial<EmailCampaign>\n if (data.deletedAt) return null\n return { id: campaignId, name: String(data.name ?? '').trim() || campaignId }\n })\n } catch (error) {\n console.error('[campaign-touch] campaign read failed', error)\n return null\n }\n}\n\n/**\n * The campaigns a site's screen is filed under right now, or `null` when the\n * screen does not exist.\n */\nexport async function readScreenCampaignIds(\n options: { hostId: string; screenId: string },\n firestore: any,\n): Promise<string[] | null> {\n const { hostId, screenId } = options\n if (!isDocumentId(hostId) || !isDocumentId(screenId)) return null\n try {\n return await remembered(firestore, `screen:${hostId}:${screenId}`, async () => {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('screens')\n .doc(screenId)\n .get()\n if (!snapshot.exists) return null\n return readContainerIds(\n (snapshot.data() ?? {}) as Record<string, unknown>,\n CAMPAIGN_KIND,\n )\n })\n } catch (error) {\n console.error('[campaign-touch] screen read failed', error)\n return null\n }\n}\n\n/**\n * Of the campaigns a page touch carries, the first that the page is STILL\n * filed under and that still exists — or `null`.\n *\n * The touch carries the screen's campaigns in the screen's own order, so a\n * page filed under two campaigns credits the one it names first. One\n * outcome, one campaign: crediting both would count one visitor's form twice\n * across the org's campaigns.\n */\nexport async function filedCampaignForPage(\n options: {\n orgId: string\n hostId: string\n screenId: string\n campaignIds: readonly string[]\n },\n firestore: any,\n): Promise<LiveCampaign | null> {\n const filed = await readScreenCampaignIds(options, firestore)\n if (!filed?.length) return null\n for (const campaignId of options.campaignIds) {\n if (!filed.includes(campaignId)) continue\n const campaign = await readLiveCampaign(\n { orgId: options.orgId, campaignId },\n firestore,\n )\n if (campaign) return campaign\n }\n return null\n}\n\n/**\n * The one live campaign placed on this site that declares `label` among its\n * `utmCampaigns`, or `null` — for no such campaign, and for two.\n */\nexport async function campaignForUtmLabel(\n options: { orgId: string; hostId: string; label: string | null | undefined },\n firestore: any,\n): Promise<LiveCampaign | null> {\n const { orgId, hostId } = options\n const label = normalizeCampaignUtmLabel(options.label)\n if (!label || !isDocumentId(orgId)) return null\n try {\n return await remembered(firestore, `label:${orgId}:${hostId}:${label}`, async () => {\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(EMAIL_CAMPAIGNS_COLLECTION)\n .where('utmCampaigns', 'array-contains', label)\n .limit(5)\n .get()\n const matches = (snapshot.docs ?? [])\n .map((doc: any) => ({ id: String(doc.id), data: (doc.data() ?? {}) as Partial<EmailCampaign> }))\n .filter(({ data }: { data: Partial<EmailCampaign> }) => !data.deletedAt)\n .filter(({ data }: { data: Partial<EmailCampaign> }) => campaignPlacedOnHost(data, hostId))\n if (matches.length !== 1) return null\n const [match] = matches\n return { id: match.id, name: String(match.data.name ?? '').trim() || match.id }\n })\n } catch (error) {\n console.error('[campaign-touch] label lookup failed', error)\n return null\n }\n}\n"],"names":["EMAIL_CAMPAIGNS_COLLECTION","campaignPlacedOnHost","normalizeCampaignUtmLabel","readContainerIds","isDocumentId","CAMPAIGN_KIND","MEMO_MS","MEMO_MAX","memos","WeakMap","memoFor","firestore","memo","get","Map","set","remembered","key","read","hit","now","Date","at","value","size","clear","readLiveCampaign","options","orgId","campaignId","snapshot","data","collection","doc","exists","deletedAt","id","name","String","trim","error","console","readScreenCampaignIds","hostId","screenId","filedCampaignForPage","filed","length","campaignIds","includes","campaign","campaignForUtmLabel","label","match","where","limit","matches","docs","map","filter"],"mappings":"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"}
@@ -0,0 +1,44 @@
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 { PluginSiteBeaconRequest } from '@aglyn/aglyn/plugin-manager/plugin-site-beacons';
18
+ /**
19
+ * The body field that marks a beacon as a campaign visit. The site runtime
20
+ * sends it; the collector hands the beacon here after its host, lockdown and
21
+ * rate gates.
22
+ */
23
+ export declare const CAMPAIGN_VISIT_BEACON_FIELD = "campaignVisit";
24
+ /**
25
+ * Counts one campaign visit beacon (AGL-3461) into
26
+ * `orgs/{orgId}/campaignVisitReports/{campaignId}`.
27
+ *
28
+ * The body says what the visitor's browser saw — the screen and the
29
+ * campaigns its page was filed under, the `utm_campaign` label on the
30
+ * address, and which of those the device claimed as a FIRST visit — and none
31
+ * of it is believed as sent:
32
+ *
33
+ * - a page's campaign is counted only while the screen is still filed under
34
+ * it and the campaign still exists;
35
+ * - a label is counted only for the one live campaign on the site that
36
+ * declares it;
37
+ * - a campaign named by both the page and the label in one beacon is one
38
+ * visit, and counts once.
39
+ *
40
+ * `views` moves for the page's campaigns only — a label is an arrival, not a
41
+ * view of the campaign's pages. Never throws past the collector, which logs
42
+ * and answers 204 either way.
43
+ */
44
+ export declare function countCampaignVisitBeacon(request: PluginSiteBeaconRequest, firestore?: any): Promise<number>;
@@ -0,0 +1,125 @@
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 { FieldValue } from "firebase-admin/firestore";
18
+ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
19
+ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
20
+ import { CAMPAIGN_VISIT_REPORTS_COLLECTION } from "../model/campaign-visits.js";
21
+ import { campaignForUtmLabel, readLiveCampaign, readScreenCampaignIds } from "./campaign-touch-targets.js";
22
+ /**
23
+ * The body field that marks a beacon as a campaign visit. The site runtime
24
+ * sends it; the collector hands the beacon here after its host, lockdown and
25
+ * rate gates.
26
+ */ export const CAMPAIGN_VISIT_BEACON_FIELD = 'campaignVisit';
27
+ /** The most campaigns one beacon may name, matching the page touch. */ const MAX_CAMPAIGNS = 5;
28
+ const DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
29
+ function idsOf(raw) {
30
+ if (!Array.isArray(raw)) return [];
31
+ const ids = [];
32
+ for (const entry of raw){
33
+ const id = typeof entry === 'string' ? entry.trim() : '';
34
+ if (!DOCUMENT_ID.test(id) || ids.includes(id)) continue;
35
+ ids.push(id);
36
+ if (ids.length >= MAX_CAMPAIGNS) break;
37
+ }
38
+ return ids;
39
+ }
40
+ /**
41
+ * Counts one campaign visit beacon (AGL-3461) into
42
+ * `orgs/{orgId}/campaignVisitReports/{campaignId}`.
43
+ *
44
+ * The body says what the visitor's browser saw — the screen and the
45
+ * campaigns its page was filed under, the `utm_campaign` label on the
46
+ * address, and which of those the device claimed as a FIRST visit — and none
47
+ * of it is believed as sent:
48
+ *
49
+ * - a page's campaign is counted only while the screen is still filed under
50
+ * it and the campaign still exists;
51
+ * - a label is counted only for the one live campaign on the site that
52
+ * declares it;
53
+ * - a campaign named by both the page and the label in one beacon is one
54
+ * visit, and counts once.
55
+ *
56
+ * `views` moves for the page's campaigns only — a label is an arrival, not a
57
+ * view of the campaign's pages. Never throws past the collector, which logs
58
+ * and answers 204 either way.
59
+ */ export async function countCampaignVisitBeacon(request, firestore = firebaseAdmin.app().firestore()) {
60
+ const { hostId, day, body } = request;
61
+ const orgId = await resolveOrgIdForHost(hostId);
62
+ if (!orgId) return 0;
63
+ const month = String(day != null ? day : '').slice(0, 7);
64
+ /** Per campaign: whether this pageview viewed its page, and whether it was a first visit. */ const counted = new Map();
65
+ const note = (id, view, first)=>{
66
+ var _counted_get;
67
+ const held = (_counted_get = counted.get(id)) != null ? _counted_get : {
68
+ view: false,
69
+ first: false
70
+ };
71
+ counted.set(id, {
72
+ view: held.view || view,
73
+ first: held.first || first
74
+ });
75
+ };
76
+ const screenId = typeof body['screenId'] === 'string' ? body['screenId'].trim() : '';
77
+ const carried = idsOf(body['campaignIds']);
78
+ const firsts = new Set(idsOf(body['first']));
79
+ if (DOCUMENT_ID.test(screenId) && carried.length) {
80
+ const filed = await readScreenCampaignIds({
81
+ hostId,
82
+ screenId
83
+ }, firestore);
84
+ for (const id of carried){
85
+ if (!(filed == null ? void 0 : filed.includes(id))) continue;
86
+ if (!await readLiveCampaign({
87
+ orgId,
88
+ campaignId: id
89
+ }, firestore)) continue;
90
+ note(id, true, firsts.has(id));
91
+ }
92
+ }
93
+ const label = typeof body['utmCampaign'] === 'string' ? body['utmCampaign'] : '';
94
+ if (label && body['firstLabel'] === true) {
95
+ const campaign = await campaignForUtmLabel({
96
+ orgId,
97
+ hostId,
98
+ label
99
+ }, firestore);
100
+ if (campaign) note(campaign.id, false, true);
101
+ }
102
+ const reports = firestore.collection('orgs').doc(orgId).collection(CAMPAIGN_VISIT_REPORTS_COLLECTION);
103
+ await Promise.all([
104
+ ...counted.entries()
105
+ ].map(([campaignId, { view, first }])=>{
106
+ const fields = _extends({}, view ? {
107
+ views: FieldValue.increment(1)
108
+ } : {}, first ? {
109
+ firstVisits: FieldValue.increment(1)
110
+ } : {});
111
+ if (!Object.keys(fields).length) return Promise.resolve();
112
+ return reports.doc(campaignId).set(_extends({}, fields, /^\d{4}-\d{2}$/.test(month) ? {
113
+ byMonth: {
114
+ [month]: fields
115
+ }
116
+ } : {}, {
117
+ updatedAt: FieldValue.serverTimestamp()
118
+ }), {
119
+ merge: true
120
+ });
121
+ }));
122
+ return counted.size;
123
+ }
124
+
125
+ //# sourceMappingURL=campaign-visit-beacon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-visit-beacon.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 { FieldValue } from 'firebase-admin/firestore'\nimport type { PluginSiteBeaconRequest } from '@aglyn/aglyn/plugin-manager/plugin-site-beacons'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { CAMPAIGN_VISIT_REPORTS_COLLECTION } from '../model/campaign-visits'\nimport {\n campaignForUtmLabel,\n readLiveCampaign,\n readScreenCampaignIds,\n} from './campaign-touch-targets'\n\n/**\n * The body field that marks a beacon as a campaign visit. The site runtime\n * sends it; the collector hands the beacon here after its host, lockdown and\n * rate gates.\n */\nexport const CAMPAIGN_VISIT_BEACON_FIELD = 'campaignVisit'\n\n/** The most campaigns one beacon may name, matching the page touch. */\nconst MAX_CAMPAIGNS = 5\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nfunction idsOf(raw: unknown): string[] {\n if (!Array.isArray(raw)) return []\n const ids: string[] = []\n for (const entry of raw) {\n const id = typeof entry === 'string' ? entry.trim() : ''\n if (!DOCUMENT_ID.test(id) || ids.includes(id)) continue\n ids.push(id)\n if (ids.length >= MAX_CAMPAIGNS) break\n }\n return ids\n}\n\n/**\n * Counts one campaign visit beacon (AGL-3461) into\n * `orgs/{orgId}/campaignVisitReports/{campaignId}`.\n *\n * The body says what the visitor's browser saw — the screen and the\n * campaigns its page was filed under, the `utm_campaign` label on the\n * address, and which of those the device claimed as a FIRST visit — and none\n * of it is believed as sent:\n *\n * - a page's campaign is counted only while the screen is still filed under\n * it and the campaign still exists;\n * - a label is counted only for the one live campaign on the site that\n * declares it;\n * - a campaign named by both the page and the label in one beacon is one\n * visit, and counts once.\n *\n * `views` moves for the page's campaigns only — a label is an arrival, not a\n * view of the campaign's pages. Never throws past the collector, which logs\n * and answers 204 either way.\n */\nexport async function countCampaignVisitBeacon(\n request: PluginSiteBeaconRequest,\n firestore: any = firebaseAdmin.app().firestore(),\n): Promise<number> {\n const { hostId, day, body } = request\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) return 0\n const month = String(day ?? '').slice(0, 7)\n\n /** Per campaign: whether this pageview viewed its page, and whether it was a first visit. */\n const counted = new Map<string, { view: boolean; first: boolean }>()\n const note = (id: string, view: boolean, first: boolean) => {\n const held = counted.get(id) ?? { view: false, first: false }\n counted.set(id, { view: held.view || view, first: held.first || first })\n }\n\n const screenId = typeof body['screenId'] === 'string' ? body['screenId'].trim() : ''\n const carried = idsOf(body['campaignIds'])\n const firsts = new Set(idsOf(body['first']))\n if (DOCUMENT_ID.test(screenId) && carried.length) {\n const filed = await readScreenCampaignIds({ hostId, screenId }, firestore)\n for (const id of carried) {\n if (!filed?.includes(id)) continue\n if (!(await readLiveCampaign({ orgId, campaignId: id }, firestore))) continue\n note(id, true, firsts.has(id))\n }\n }\n\n const label = typeof body['utmCampaign'] === 'string' ? body['utmCampaign'] : ''\n if (label && body['firstLabel'] === true) {\n const campaign = await campaignForUtmLabel({ orgId, hostId, label }, firestore)\n if (campaign) note(campaign.id, false, true)\n }\n\n const reports = firestore\n .collection('orgs')\n .doc(orgId)\n .collection(CAMPAIGN_VISIT_REPORTS_COLLECTION)\n await Promise.all(\n [...counted.entries()].map(([campaignId, { view, first }]) => {\n const fields: Record<string, unknown> = {\n ...(view ? { views: FieldValue.increment(1) } : {}),\n ...(first ? { firstVisits: FieldValue.increment(1) } : {}),\n }\n if (!Object.keys(fields).length) return Promise.resolve()\n return reports.doc(campaignId).set(\n {\n ...fields,\n ...(/^\\d{4}-\\d{2}$/.test(month) ? { byMonth: { [month]: fields } } : {}),\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n }),\n )\n return counted.size\n}\n"],"names":["FieldValue","firebaseAdmin","resolveOrgIdForHost","CAMPAIGN_VISIT_REPORTS_COLLECTION","campaignForUtmLabel","readLiveCampaign","readScreenCampaignIds","CAMPAIGN_VISIT_BEACON_FIELD","MAX_CAMPAIGNS","DOCUMENT_ID","idsOf","raw","Array","isArray","ids","entry","id","trim","test","includes","push","length","countCampaignVisitBeacon","request","firestore","app","hostId","day","body","orgId","month","String","slice","counted","Map","note","view","first","held","get","set","screenId","carried","firsts","Set","filed","campaignId","has","label","campaign","reports","collection","doc","Promise","all","entries","map","fields","views","increment","firstVisits","Object","keys","resolve","byMonth","updatedAt","serverTimestamp","merge","size"],"mappings":"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"}
@@ -0,0 +1,18 @@
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 { PluginConversionCreditor } from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit';
18
+ export declare const marketingConversionCreditor: PluginConversionCreditor;