@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.
- package/README.md +4 -1
- package/package.json +15 -15
- package/src/lib/components/campaign-conversions-card.js +53 -64
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +18 -3
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.d.ts +6 -0
- package/src/lib/components/campaign-edit-drawer.js +13 -2
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-queries.d.ts +5 -0
- package/src/lib/components/campaign-queries.js +8 -1
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.d.ts +28 -0
- package/src/lib/components/campaign-reach-sections.js +96 -5
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.js +5 -4
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.js +1 -1
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/conversion-attribution.component.d.ts +18 -0
- package/src/lib/components/conversion-attribution.component.js +177 -39
- package/src/lib/components/conversion-attribution.component.js.map +1 -1
- package/src/lib/components/email-compose-card.js +1 -1
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.js +5 -4
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-template-report-card.d.ts +53 -0
- package/src/lib/components/email-template-report-card.js +469 -0
- package/src/lib/components/email-template-report-card.js.map +1 -0
- package/src/lib/components/emails-list-card.js +2 -2
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +3 -2
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/record-attribution-widget.d.ts +5 -0
- package/src/lib/components/record-attribution-widget.js +6 -3
- package/src/lib/components/record-attribution-widget.js.map +1 -1
- package/src/lib/components/site-runtime.d.ts +5 -0
- package/src/lib/components/site-runtime.js +104 -3
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/declarations.server.d.ts +9 -0
- package/src/lib/declarations.server.js +78 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-container.d.ts +531 -0
- package/src/lib/model/campaign-container.js +532 -0
- package/src/lib/model/campaign-container.js.map +1 -0
- package/src/lib/model/campaign-conversions.d.ts +55 -4
- package/src/lib/model/campaign-conversions.js +60 -4
- package/src/lib/model/campaign-conversions.js.map +1 -1
- package/src/lib/model/campaign-report.d.ts +158 -0
- package/src/lib/model/campaign-report.js +268 -0
- package/src/lib/model/campaign-report.js.map +1 -0
- package/src/lib/model/campaign-revenue.d.ts +3 -3
- package/src/lib/model/campaign-revenue.js +3 -3
- package/src/lib/model/campaign-revenue.js.map +1 -1
- package/src/lib/model/campaign-send-time.js +2 -2
- package/src/lib/model/campaign-send-time.js.map +1 -1
- package/src/lib/model/campaign-visits.d.ts +74 -0
- package/src/lib/model/campaign-visits.js +65 -0
- package/src/lib/model/campaign-visits.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +2 -2
- package/src/lib/model/compile-client-automations.js +3 -3
- package/src/lib/model/compile-client-automations.js.map +1 -1
- package/src/lib/model/email-record.d.ts +175 -0
- package/src/lib/model/email-record.js +206 -0
- package/src/lib/model/email-record.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +2 -2
- package/src/lib/model/site-contract.js.map +1 -1
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.js +16 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/preview-props.js.map +1 -1
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +98 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +314 -0
- package/src/lib/server/campaign-conversion-attribution.js +549 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/campaign-manage.js +1 -1
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-org-refs.js +1 -1
- package/src/lib/server/campaign-org-refs.js.map +1 -1
- package/src/lib/server/campaign-recipients.js +7 -7
- package/src/lib/server/campaign-recipients.js.map +1 -1
- package/src/lib/server/campaign-send.js +25 -19
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/campaign-touch-targets.d.ts +61 -0
- package/src/lib/server/campaign-touch-targets.js +162 -0
- package/src/lib/server/campaign-touch-targets.js.map +1 -0
- package/src/lib/server/campaign-visit-beacon.d.ts +44 -0
- package/src/lib/server/campaign-visit-beacon.js +125 -0
- package/src/lib/server/campaign-visit-beacon.js.map +1 -0
- package/src/lib/server/conversion-creditor.d.ts +18 -0
- package/src/lib/server/conversion-creditor.js +238 -0
- package/src/lib/server/conversion-creditor.js.map +1 -0
- package/src/lib/server/email-campaign-touch.d.ts +81 -0
- package/src/lib/server/email-campaign-touch.js +227 -0
- package/src/lib/server/email-campaign-touch.js.map +1 -0
- package/src/lib/server/email-events.js +32 -31
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/email-revenue-attribution.d.ts +186 -0
- package/src/lib/server/email-revenue-attribution.js +322 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-record-index.d.ts +34 -0
- package/src/lib/server/email-send-record-index.js +90 -0
- package/src/lib/server/email-send-record-index.js.map +1 -0
- package/src/lib/server/get-client-automations.js.map +1 -1
- package/src/lib/server/marketing-figures.js +3 -3
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server/send-tally.d.ts +32 -0
- package/src/lib/server/send-tally.js +54 -0
- package/src/lib/server/send-tally.js.map +1 -0
- package/src/lib/server/site-page-enricher.js +18 -2
- package/src/lib/server/site-page-enricher.js.map +1 -1
- package/src/lib/testing/stand-in-person-records.d.ts +35 -0
- package/src/lib/testing/stand-in-person-records.js +67 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
- package/src/lib/testing/stand-in-record-counts.js +70 -0
- package/src/lib/testing/stand-in-record-counts.js.map +1 -0
- package/src/lib/transfer/campaigns-package.d.ts +72 -0
- package/src/lib/transfer/campaigns-package.js +145 -0
- package/src/lib/transfer/campaigns-package.js.map +1 -0
- package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
- package/src/lib/transfer/campaigns-package.server.js +305 -0
- 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;
|