@aglyn/plugins-marketing 1.0.0-beta.261 → 1.0.0-beta.262

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketing",
3
- "version": "1.0.0-beta.261",
3
+ "version": "1.0.0-beta.262",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,21 +25,21 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.261",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.261",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.261",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.261",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.261",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.261",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.261",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.261",
36
- "@aglyn/shared-util-email": "1.0.0-beta.261",
37
- "@aglyn/shared-util-http": "1.0.0-beta.261",
38
- "@aglyn/shared-util-timestamp": "1.0.0-beta.261",
39
- "@aglyn/shared-util-tools": "1.0.0-beta.261",
40
- "@aglyn/tenant-data-admin": "1.0.0-beta.261",
41
- "@aglyn/tenant-feature-instance": "1.0.0-beta.261",
42
- "@aglyn/tenant-runtime": "1.0.0-beta.261",
28
+ "@aglyn/aglyn": "1.0.0-beta.262",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.262",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.262",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.262",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.262",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.262",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.262",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.262",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.262",
37
+ "@aglyn/shared-util-http": "1.0.0-beta.262",
38
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.262",
39
+ "@aglyn/shared-util-tools": "1.0.0-beta.262",
40
+ "@aglyn/tenant-data-admin": "1.0.0-beta.262",
41
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.262",
42
+ "@aglyn/tenant-runtime": "1.0.0-beta.262",
43
43
  "@swc/helpers": "0.5.23"
44
44
  },
45
45
  "peerDependencies": {
@@ -13,7 +13,8 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
16
+ */ import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureCellText, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
17
+ import { figureCount, figurePlural, figureRow } from "@aglyn/aglyn/app-utils/weekly-summary";
17
18
  import { campaignReport } from "../model/campaign-report.js";
18
19
  import { EMAIL_CREATED_AT_FIELD, emailSendTimeMs } from "../model/email-record.js";
19
20
  import { CAMPAIGN_SEND_HOST_FIELD } from "../model/campaign-container.js";
@@ -63,6 +64,18 @@ export function marketingFigureReaders(firestore) {
63
64
  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.',
64
65
  scope: 'site',
65
66
  windows: WINDOWS,
67
+ weeklySummary: {
68
+ order: 90,
69
+ lines: (table)=>{
70
+ var _all_delivered;
71
+ const all = figureRow(table, 'campaign', 'All campaigns');
72
+ if (!all || !figureCount(all['delivered'])) return [];
73
+ const sent = table.rows.filter((row)=>row !== all && figureCount(row['delivered']) > 0).length;
74
+ return [
75
+ `Campaigns: ${figurePlural(sent, 'email', 'emails')} sent, reaching ` + pluginFigureCellText(table, 'delivered', (_all_delivered = all['delivered']) != null ? _all_delivered : 0) + (typeof all['openRate'] === 'number' ? `, ${pluginFigureCellText(table, 'openRate', all['openRate'])} opened` : '') + (typeof all['clickRate'] === 'number' ? `, ${pluginFigureCellText(table, 'clickRate', all['clickRate'])} clicked` : '')
76
+ ];
77
+ }
78
+ },
66
79
  read: async (request)=>{
67
80
  if (!request.hostId) return {
68
81
  ok: false,
@@ -1 +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 type SendRate,\n type SendStats,\n} from '@aglyn/shared-ui-email-campaigns/model/send-report'\nimport { campaignReport } from '../model/campaign-report'\nimport {\n EMAIL_CREATED_AT_FIELD,\n emailSendTimeMs,\n} from '../model/email-record'\nimport { CAMPAIGN_SEND_HOST_FIELD } from '../model/campaign-container'\nimport { orgCampaignSends } from './campaign-org-refs'\nimport { compareVariants, summarizeVariantStats, type HostExperiment } from '../model/experiments'\nimport {\n CAMPAIGN_CONVERSION_KINDS,\n type CampaignConversionKind,\n type CampaignTouchChannel,\n} from '../model/campaign-conversions'\nimport {\n EMAIL_ATTRIBUTION_WINDOW_DAYS,\n type CampaignRevenueRollup,\n} from '../model/campaign-revenue'\nimport { CAMPAIGN_ATTRIBUTIONS_COLLECTION } from './campaign-attribution-store'\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\n/**\n * The channels a conversion is counted by, in reading order. Each is an\n * aggregation count over the attribution records — `kind`, `channel` and the\n * window on `convertedAtMs` — so the table costs a read per thousand index\n * entries rather than a read per record, and never loads one.\n */\nexport const CONVERSION_FIGURE_CHANNELS: readonly CampaignTouchChannel[] = ['email', 'page', 'web', 'sequence']\n\n/** How a conversions table names each kind: the record it credits, in the console's words. */\nconst CONVERSION_KIND_LABELS: Readonly<Record<CampaignConversionKind, string>> = {\n form: 'Form submissions',\n lead: 'Leads',\n contact: 'Contacts',\n booking: 'Bookings',\n}\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nconst percent = (rate: SendRate | 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<SendRate | null>): number | null {\n const known = rates.filter((rate): rate is SendRate => 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 if (!request.orgId) return { ok: false, status: 400, error: 'This site is not part of an organization' }\n const { current } = pluginFigureWindows(request.now, request.days)\n /*\n * The organization's sends, narrowed to the ones sent as this site.\n * Served by the composite index on the site field and the creation\n * stamp — the equality and the ordering together are one index.\n */\n const snapshot = await orgCampaignSends(firestore(), request.orgId)\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', request.hostId)\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 SendStats | 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/** A stored count as a non-negative integer. */\nconst whole = (raw: unknown): number => {\n const value = Number(raw)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\n/**\n * What the site's campaigns CAUSED and EARNED, as two more tables (AGL-3603):\n * the conversions credited to a campaign touch in the window, and the revenue\n * credited to the campaign emails sent in it. Same rules the Conversions\n * section and a campaign's report print — the kinds are never added together,\n * currencies are never added together, net is gross less refunded — and the\n * same honesty about what is NOT here: a conversion credited to nothing has no\n * record, so it is not counted, and the notes say so.\n */\nexport function marketingOutcomeFigureReaders(firestore: () => Firestore): PluginFigureReader[] {\n return [\n {\n id: 'marketing.conversions',\n label: 'Campaign conversions',\n description:\n 'Conversions credited to a campaign touch on the site in the window — form submissions, leads, contacts and bookings, one row each — counted by the touch that earned the credit: a campaign email, a page filed under a campaign, a link labeled for a campaign, or a sales sequence email.',\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 conversions' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one conversions are read over' }\n }\n const { current } = pluginFigureWindows(request.now, request.days)\n /*\n * Aggregation counts, never the records: per kind, one over every\n * channel and one per channel, each an equality on `kind` (and\n * `channel`) with the window on `convertedAtMs`. Served by the two\n * composite indexes the index file declares for them.\n */\n const inWindow = (kind: CampaignConversionKind, channel: CampaignTouchChannel | null) => {\n let query = firestore()\n .collection('hosts')\n .doc(request.hostId as string)\n .collection(CAMPAIGN_ATTRIBUTIONS_COLLECTION)\n .where('kind', '==', kind)\n if (channel) query = query.where('channel', '==', channel)\n return query\n .where('convertedAtMs', '>=', current.startMs)\n .where('convertedAtMs', '<', current.endMs)\n .count()\n .get()\n .then((snapshot) => whole(snapshot.data().count))\n }\n const rows: PluginFigureRow[] = await Promise.all(\n CAMPAIGN_CONVERSION_KINDS.map(async (kind) => {\n const [credited, ...byChannel] = await Promise.all([\n inWindow(kind, null),\n ...CONVERSION_FIGURE_CHANNELS.map((channel) => inWindow(kind, channel)),\n ])\n const row: PluginFigureRow = { kind: CONVERSION_KIND_LABELS[kind], credited }\n CONVERSION_FIGURE_CHANNELS.forEach((channel, index) => {\n row[channel] = byChannel[index]\n })\n return row\n }),\n )\n return {\n ok: true,\n table: {\n title: 'Campaign conversions',\n source: { label: 'Conversions', path: 'marketing/conversions' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'kind', label: 'Conversion', kind: 'text' },\n { key: 'credited', label: 'Credited to a campaign', kind: 'count' },\n { key: 'email', label: 'From a campaign email', kind: 'count' },\n { key: 'page', label: 'From a campaign page', kind: 'count' },\n { key: 'web', label: 'From a labeled link', kind: 'count' },\n { key: 'sequence', label: 'From a sequence email', kind: 'count' },\n ],\n rows,\n omitted: 0,\n notes: [\n 'Each row counts a different record of the same visits — one form submission can also make a lead and a contact — so the rows are never added together.',\n 'Only conversions credited to a campaign touch are counted; a conversion credited to nothing has no record and is not in this table.',\n ],\n },\n }\n },\n },\n {\n id: 'marketing.revenue',\n label: 'Campaign revenue',\n description:\n 'Revenue credited to each campaign email sent from the site in the window: orders credited, gross, refunded and net, one row per email and currency, with amounts in each row’s currency.',\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 campaign revenue' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one campaign revenue is read over' }\n }\n if (!request.orgId) return { ok: false, status: 400, error: 'This site is not part of an organization' }\n const { current } = pluginFigureWindows(request.now, request.days)\n // The campaigns table's own read: the site's sends, newest first.\n const sendsRef = orgCampaignSends(firestore(), request.orgId)\n const snapshot = await sendsRef\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', request.hostId)\n .orderBy(EMAIL_CREATED_AT_FIELD, 'desc')\n .limit(CAMPAIGN_FIGURES_READ_LIMIT)\n .get()\n const sends = snapshot.docs\n .filter((doc) => {\n const record = doc.data() ?? {}\n const sentAt = emailSendTimeMs(record)\n return Boolean(record['sentAt']) && sentAt >= current.startMs && sentAt < current.endMs\n })\n .slice(0, PLUGIN_FIGURE_MAX_ROWS)\n /*\n * One keyed read per send for its revenue rollup, bounded by the rows\n * a table may carry — the rollup is its own document so the history\n * list never pays for it, and this is the one place that does.\n */\n const rollups = sends.length\n ? await firestore().getAll(\n ...sends.map((doc) => sendsRef.doc(doc.id).collection('reports').doc('revenue')),\n )\n : []\n const rows: PluginFigureRow[] = []\n const currencies = new Set<string>()\n sends.forEach((doc, index) => {\n const rollup = (rollups[index]?.data() ?? {}) as CampaignRevenueRollup\n for (const [currency, totals] of Object.entries(rollup.byCurrency ?? {})) {\n const grossCents = whole(totals?.grossCents)\n const refundedCents = whole(totals?.refundedCents)\n const orders = whole(totals?.orders)\n if (!orders && !grossCents) continue\n currencies.add(currency.toUpperCase())\n rows.push({\n campaign: String(doc.get('subject') ?? '').trim() || 'A campaign with no subject',\n currency: currency.toUpperCase(),\n orders,\n gross: grossCents / 100,\n refunded: refundedCents / 100,\n // Clamped where it is shown, as the campaign report clamps it.\n net: Math.max(0, grossCents - refundedCents) / 100,\n })\n }\n })\n const [only] = currencies\n const money = currencies.size === 1 && /^[A-Z]{3}$/.test(only ?? '')\n const amount = (key: string, label: string) =>\n money\n ? { key, label, kind: 'money' as const, currency: only }\n : { key, label: `${label} (in the row’s currency)`, kind: 'number' as const }\n return {\n ok: true,\n table: {\n title: 'Campaign revenue',\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 email', kind: 'text' },\n { key: 'currency', label: 'Currency', kind: 'text' },\n { key: 'orders', label: 'Orders credited', kind: 'count' },\n amount('gross', 'Gross'),\n amount('refunded', 'Refunded'),\n amount('net', 'Net'),\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n `Revenue is credited to the last campaign email whose link the buyer clicked within ${EMAIL_ATTRIBUTION_WINDOW_DAYS} days, for the emails sent in the window; amounts in different currencies are never added together.`,\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}\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), ...marketingOutcomeFigureReaders(firestore)]) {\n registerPluginFigureReader(reader, { pluginId: MARKETING_PLUGIN_ID })\n }\n}\n"],"names":["PLUGIN_FIGURE_MAX_ROWS","pluginFigureWindows","registerPluginFigureReader","campaignReport","EMAIL_CREATED_AT_FIELD","emailSendTimeMs","CAMPAIGN_SEND_HOST_FIELD","orgCampaignSends","compareVariants","summarizeVariantStats","CAMPAIGN_CONVERSION_KINDS","EMAIL_ATTRIBUTION_WINDOW_DAYS","CAMPAIGN_ATTRIBUTIONS_COLLECTION","MARKETING_PLUGIN_ID","CAMPAIGN_FIGURES_READ_LIMIT","EXPERIMENT_FIGURES_READ_LIMIT","CONVERSION_FIGURE_CHANNELS","CONVERSION_KIND_LABELS","form","lead","contact","booking","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","orgId","current","now","snapshot","where","orderBy","limit","get","sends","docs","map","doc","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","collection","experiment","byVariant","variants","stats","ref","Map","entry","control","forEach","variant","index","own","summary","comparison","push","test","name","shown","exposures","conversions","lift","confidence","whole","raw","Number","isFinite","floor","marketingOutcomeFigureReaders","inWindow","channel","query","count","then","Promise","all","credited","byChannel","row","sendsRef","rollups","getAll","currencies","Set","rollup","currency","totals","Object","entries","byCurrency","grossCents","refundedCents","orders","add","toUpperCase","gross","refunded","net","only","money","size","amount","registerMarketingFigureReaders","reader","pluginId"],"mappings":"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"}
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 pluginFigureCellText,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { figureCount, figurePlural, figureRow } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport {\n type SendRate,\n type SendStats,\n} from '@aglyn/shared-ui-email-campaigns/model/send-report'\nimport { campaignReport } from '../model/campaign-report'\nimport {\n EMAIL_CREATED_AT_FIELD,\n emailSendTimeMs,\n} from '../model/email-record'\nimport { CAMPAIGN_SEND_HOST_FIELD } from '../model/campaign-container'\nimport { orgCampaignSends } from './campaign-org-refs'\nimport { compareVariants, summarizeVariantStats, type HostExperiment } from '../model/experiments'\nimport {\n CAMPAIGN_CONVERSION_KINDS,\n type CampaignConversionKind,\n type CampaignTouchChannel,\n} from '../model/campaign-conversions'\nimport {\n EMAIL_ATTRIBUTION_WINDOW_DAYS,\n type CampaignRevenueRollup,\n} from '../model/campaign-revenue'\nimport { CAMPAIGN_ATTRIBUTIONS_COLLECTION } from './campaign-attribution-store'\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\n/**\n * The channels a conversion is counted by, in reading order. Each is an\n * aggregation count over the attribution records — `kind`, `channel` and the\n * window on `convertedAtMs` — so the table costs a read per thousand index\n * entries rather than a read per record, and never loads one.\n */\nexport const CONVERSION_FIGURE_CHANNELS: readonly CampaignTouchChannel[] = ['email', 'page', 'web', 'sequence']\n\n/** How a conversions table names each kind: the record it credits, in the console's words. */\nconst CONVERSION_KIND_LABELS: Readonly<Record<CampaignConversionKind, string>> = {\n form: 'Form submissions',\n lead: 'Leads',\n contact: 'Contacts',\n booking: 'Bookings',\n}\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nconst percent = (rate: SendRate | 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<SendRate | null>): number | null {\n const known = rates.filter((rate): rate is SendRate => 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 weeklySummary: {\n order: 90,\n lines: (table) => {\n const all = figureRow(table, 'campaign', 'All campaigns')\n if (!all || !figureCount(all['delivered'])) return []\n const sent = table.rows.filter((row) => row !== all && figureCount(row['delivered']) > 0).length\n return [\n `Campaigns: ${figurePlural(sent, 'email', 'emails')} sent, reaching ` +\n pluginFigureCellText(table, 'delivered', all['delivered'] ?? 0) +\n (typeof all['openRate'] === 'number'\n ? `, ${pluginFigureCellText(table, 'openRate', all['openRate'])} opened`\n : '') +\n (typeof all['clickRate'] === 'number'\n ? `, ${pluginFigureCellText(table, 'clickRate', all['clickRate'])} clicked`\n : ''),\n ]\n },\n },\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 if (!request.orgId) return { ok: false, status: 400, error: 'This site is not part of an organization' }\n const { current } = pluginFigureWindows(request.now, request.days)\n /*\n * The organization's sends, narrowed to the ones sent as this site.\n * Served by the composite index on the site field and the creation\n * stamp — the equality and the ordering together are one index.\n */\n const snapshot = await orgCampaignSends(firestore(), request.orgId)\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', request.hostId)\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 SendStats | 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/** A stored count as a non-negative integer. */\nconst whole = (raw: unknown): number => {\n const value = Number(raw)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\n/**\n * What the site's campaigns CAUSED and EARNED, as two more tables (AGL-3603):\n * the conversions credited to a campaign touch in the window, and the revenue\n * credited to the campaign emails sent in it. Same rules the Conversions\n * section and a campaign's report print — the kinds are never added together,\n * currencies are never added together, net is gross less refunded — and the\n * same honesty about what is NOT here: a conversion credited to nothing has no\n * record, so it is not counted, and the notes say so.\n */\nexport function marketingOutcomeFigureReaders(firestore: () => Firestore): PluginFigureReader[] {\n return [\n {\n id: 'marketing.conversions',\n label: 'Campaign conversions',\n description:\n 'Conversions credited to a campaign touch on the site in the window — form submissions, leads, contacts and bookings, one row each — counted by the touch that earned the credit: a campaign email, a page filed under a campaign, a link labeled for a campaign, or a sales sequence email.',\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 conversions' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one conversions are read over' }\n }\n const { current } = pluginFigureWindows(request.now, request.days)\n /*\n * Aggregation counts, never the records: per kind, one over every\n * channel and one per channel, each an equality on `kind` (and\n * `channel`) with the window on `convertedAtMs`. Served by the two\n * composite indexes the index file declares for them.\n */\n const inWindow = (kind: CampaignConversionKind, channel: CampaignTouchChannel | null) => {\n let query = firestore()\n .collection('hosts')\n .doc(request.hostId as string)\n .collection(CAMPAIGN_ATTRIBUTIONS_COLLECTION)\n .where('kind', '==', kind)\n if (channel) query = query.where('channel', '==', channel)\n return query\n .where('convertedAtMs', '>=', current.startMs)\n .where('convertedAtMs', '<', current.endMs)\n .count()\n .get()\n .then((snapshot) => whole(snapshot.data().count))\n }\n const rows: PluginFigureRow[] = await Promise.all(\n CAMPAIGN_CONVERSION_KINDS.map(async (kind) => {\n const [credited, ...byChannel] = await Promise.all([\n inWindow(kind, null),\n ...CONVERSION_FIGURE_CHANNELS.map((channel) => inWindow(kind, channel)),\n ])\n const row: PluginFigureRow = { kind: CONVERSION_KIND_LABELS[kind], credited }\n CONVERSION_FIGURE_CHANNELS.forEach((channel, index) => {\n row[channel] = byChannel[index]\n })\n return row\n }),\n )\n return {\n ok: true,\n table: {\n title: 'Campaign conversions',\n source: { label: 'Conversions', path: 'marketing/conversions' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'kind', label: 'Conversion', kind: 'text' },\n { key: 'credited', label: 'Credited to a campaign', kind: 'count' },\n { key: 'email', label: 'From a campaign email', kind: 'count' },\n { key: 'page', label: 'From a campaign page', kind: 'count' },\n { key: 'web', label: 'From a labeled link', kind: 'count' },\n { key: 'sequence', label: 'From a sequence email', kind: 'count' },\n ],\n rows,\n omitted: 0,\n notes: [\n 'Each row counts a different record of the same visits — one form submission can also make a lead and a contact — so the rows are never added together.',\n 'Only conversions credited to a campaign touch are counted; a conversion credited to nothing has no record and is not in this table.',\n ],\n },\n }\n },\n },\n {\n id: 'marketing.revenue',\n label: 'Campaign revenue',\n description:\n 'Revenue credited to each campaign email sent from the site in the window: orders credited, gross, refunded and net, one row per email and currency, with amounts in each row’s currency.',\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 campaign revenue' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one campaign revenue is read over' }\n }\n if (!request.orgId) return { ok: false, status: 400, error: 'This site is not part of an organization' }\n const { current } = pluginFigureWindows(request.now, request.days)\n // The campaigns table's own read: the site's sends, newest first.\n const sendsRef = orgCampaignSends(firestore(), request.orgId)\n const snapshot = await sendsRef\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', request.hostId)\n .orderBy(EMAIL_CREATED_AT_FIELD, 'desc')\n .limit(CAMPAIGN_FIGURES_READ_LIMIT)\n .get()\n const sends = snapshot.docs\n .filter((doc) => {\n const record = doc.data() ?? {}\n const sentAt = emailSendTimeMs(record)\n return Boolean(record['sentAt']) && sentAt >= current.startMs && sentAt < current.endMs\n })\n .slice(0, PLUGIN_FIGURE_MAX_ROWS)\n /*\n * One keyed read per send for its revenue rollup, bounded by the rows\n * a table may carry — the rollup is its own document so the history\n * list never pays for it, and this is the one place that does.\n */\n const rollups = sends.length\n ? await firestore().getAll(\n ...sends.map((doc) => sendsRef.doc(doc.id).collection('reports').doc('revenue')),\n )\n : []\n const rows: PluginFigureRow[] = []\n const currencies = new Set<string>()\n sends.forEach((doc, index) => {\n const rollup = (rollups[index]?.data() ?? {}) as CampaignRevenueRollup\n for (const [currency, totals] of Object.entries(rollup.byCurrency ?? {})) {\n const grossCents = whole(totals?.grossCents)\n const refundedCents = whole(totals?.refundedCents)\n const orders = whole(totals?.orders)\n if (!orders && !grossCents) continue\n currencies.add(currency.toUpperCase())\n rows.push({\n campaign: String(doc.get('subject') ?? '').trim() || 'A campaign with no subject',\n currency: currency.toUpperCase(),\n orders,\n gross: grossCents / 100,\n refunded: refundedCents / 100,\n // Clamped where it is shown, as the campaign report clamps it.\n net: Math.max(0, grossCents - refundedCents) / 100,\n })\n }\n })\n const [only] = currencies\n const money = currencies.size === 1 && /^[A-Z]{3}$/.test(only ?? '')\n const amount = (key: string, label: string) =>\n money\n ? { key, label, kind: 'money' as const, currency: only }\n : { key, label: `${label} (in the row’s currency)`, kind: 'number' as const }\n return {\n ok: true,\n table: {\n title: 'Campaign revenue',\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 email', kind: 'text' },\n { key: 'currency', label: 'Currency', kind: 'text' },\n { key: 'orders', label: 'Orders credited', kind: 'count' },\n amount('gross', 'Gross'),\n amount('refunded', 'Refunded'),\n amount('net', 'Net'),\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n `Revenue is credited to the last campaign email whose link the buyer clicked within ${EMAIL_ATTRIBUTION_WINDOW_DAYS} days, for the emails sent in the window; amounts in different currencies are never added together.`,\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}\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), ...marketingOutcomeFigureReaders(firestore)]) {\n registerPluginFigureReader(reader, { pluginId: MARKETING_PLUGIN_ID })\n }\n}\n"],"names":["PLUGIN_FIGURE_MAX_ROWS","pluginFigureCellText","pluginFigureWindows","registerPluginFigureReader","figureCount","figurePlural","figureRow","campaignReport","EMAIL_CREATED_AT_FIELD","emailSendTimeMs","CAMPAIGN_SEND_HOST_FIELD","orgCampaignSends","compareVariants","summarizeVariantStats","CAMPAIGN_CONVERSION_KINDS","EMAIL_ATTRIBUTION_WINDOW_DAYS","CAMPAIGN_ATTRIBUTIONS_COLLECTION","MARKETING_PLUGIN_ID","CAMPAIGN_FIGURES_READ_LIMIT","EXPERIMENT_FIGURES_READ_LIMIT","CONVERSION_FIGURE_CHANNELS","CONVERSION_KIND_LABELS","form","lead","contact","booking","WINDOWS","percent","rate","Math","round","value","pooled","rates","known","filter","denominator","reduce","sum","numerator","marketingFigureReaders","firestore","id","label","description","scope","windows","weeklySummary","order","lines","table","all","sent","rows","row","length","read","request","hostId","ok","status","error","includes","days","orgId","current","now","snapshot","where","orderBy","limit","get","sends","docs","map","doc","data","record","sentAt","Boolean","startMs","endMs","subject","String","trim","report","campaign","delivered","every","send","openRate","open","clickRate","click","slice","title","source","path","period","from","to","columns","key","kind","omitted","max","notes","feature","experimentsRef","collection","experiment","byVariant","variants","stats","ref","Map","entry","control","forEach","variant","index","own","summary","comparison","push","test","name","shown","exposures","conversions","lift","confidence","whole","raw","Number","isFinite","floor","marketingOutcomeFigureReaders","inWindow","channel","query","count","then","Promise","credited","byChannel","sendsRef","rollups","getAll","currencies","Set","rollup","currency","totals","Object","entries","byCurrency","grossCents","refundedCents","orders","add","toUpperCase","gross","refunded","net","only","money","size","amount","registerMarketingFigureReaders","reader","pluginId"],"mappings":"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"}