@aglyn/plugins-marketing 1.0.0-beta.218 → 1.0.0-beta.220
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 +5 -1
- package/package.json +16 -16
- package/src/lib/analytics-provider.d.ts +177 -0
- package/src/lib/analytics-provider.js +370 -0
- package/src/lib/analytics-provider.js.map +1 -0
- package/src/lib/components/besigner-section-experiments.d.ts +20 -0
- package/src/lib/components/besigner-section-experiments.js +128 -0
- package/src/lib/components/besigner-section-experiments.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.js +1 -1
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +16 -16
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-members-section.js +15 -14
- package/src/lib/components/campaign-members-section.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.js +4 -1
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.js +8 -6
- 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 +1 -1
- package/src/lib/components/conversion-attribution.component.js +2 -2
- package/src/lib/components/conversion-attribution.component.js.map +1 -1
- package/src/lib/components/email-detail.js +12 -7
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-messages-widget.js +1 -1
- package/src/lib/components/email-messages-widget.js.map +1 -1
- package/src/lib/components/emails-list-card.js +11 -5
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +12 -7
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/org-experiments-card.js +1 -1
- package/src/lib/components/org-experiments-card.js.map +1 -1
- package/src/lib/components/record-attribution-widget.js +1 -1
- package/src/lib/components/record-attribution-widget.js.map +1 -1
- package/src/lib/components/record-route-context.d.ts +41 -0
- package/src/lib/components/record-route-context.js +63 -0
- package/src/lib/components/record-route-context.js.map +1 -0
- package/src/lib/components/site-runtime.js +2 -2
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/components/use-marketing-hub-path.d.ts +18 -5
- package/src/lib/components/use-marketing-hub-path.js +36 -6
- package/src/lib/components/use-marketing-hub-path.js.map +1 -1
- package/src/lib/constants/operator-alerts.js +4 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.server.d.ts +3 -1
- package/src/lib/declarations.server.js +17 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-conversions.d.ts +301 -0
- package/src/lib/model/campaign-conversions.js +252 -0
- package/src/lib/model/campaign-conversions.js.map +1 -0
- package/src/lib/model/campaign-kind.d.ts +50 -0
- package/src/lib/model/campaign-kind.js +52 -0
- package/src/lib/model/campaign-kind.js.map +1 -0
- package/src/lib/model/campaign-members-query.js +1 -1
- package/src/lib/model/campaign-members-query.js.map +1 -1
- package/src/lib/model/campaign-revenue.d.ts +327 -0
- package/src/lib/model/campaign-revenue.js +332 -0
- package/src/lib/model/campaign-revenue.js.map +1 -0
- package/src/lib/model/campaign-send-time.d.ts +74 -0
- package/src/lib/model/campaign-send-time.js +120 -0
- package/src/lib/model/campaign-send-time.js.map +1 -0
- package/src/lib/model/experiments.js +1 -1
- package/src/lib/model/experiments.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +4 -1
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/overlay-record-list.d.ts +33 -0
- package/src/lib/model/overlay-record-list.js +53 -0
- package/src/lib/model/overlay-record-list.js.map +1 -0
- package/src/lib/plugin.js +12 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/campaign-batch-plan.d.ts +73 -0
- package/src/lib/server/campaign-batch-plan.js +79 -0
- package/src/lib/server/campaign-batch-plan.js.map +1 -0
- package/src/lib/server/campaign-manage.js +19 -60
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-send.d.ts +1 -1
- package/src/lib/server/campaign-send.js +32 -11
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-events.js +16 -16
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/list-send-time-facts.d.ts +60 -0
- package/src/lib/server/list-send-time-facts.js +108 -0
- package/src/lib/server/list-send-time-facts.js.map +1 -0
- package/src/lib/server/overlay-beacon.d.ts +46 -0
- package/src/lib/server/overlay-beacon.js +76 -0
- package/src/lib/server/overlay-beacon.js.map +1 -0
- package/src/lib/server.js +4 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.d.ts +29 -0
- package/src/lib/tenant-emails.js +41 -0
- package/src/lib/tenant-emails.js.map +1 -0
- package/src/lib/testing/stand-in-emails-page-routes.d.ts +27 -0
- package/src/lib/testing/stand-in-emails-page-routes.js +42 -0
- package/src/lib/testing/stand-in-emails-page-routes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -58
- package/src/lib/components/use-emails-hub-path.js +0 -109
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/experiments.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\n/**\n * A/B experiments (AGL-252): docs at `hosts/{hostId}/experiments/{id}`.\n * Assignment is deterministic — hash(visitorId + experimentId) buckets a\n * visitor into a weighted variant, so the same visitor always sees the\n * same variant with no server state. Exposures/conversions accumulate on\n * per-variant stats counters via the track API. Business tier\n * (`abTesting` flag).\n */\nexport type ExperimentStatus = 'draft' | 'running' | 'paused' | 'done'\n\n/** What the experiment varies. */\nexport type ExperimentTarget = 'screen' | 'section' | 'email'\n\nexport interface ExperimentVariant {\n id: string\n name?: string\n /** Relative traffic weight; defaults to 1. */\n weight?: number\n /** Screen experiments: the screen version this variant serves. */\n versionId?: string\n /** Email experiments (AGL-255): subject/body overrides. */\n subject?: string\n body?: string\n}\n\nexport interface HostExperiment {\n name: string\n status: ExperimentStatus\n target: ExperimentTarget\n /** Screen/section experiments: the screen under test. */\n screenId?: string\n /** Section experiments: canvas node id the variants swap. */\n nodeId?: string\n variants: ExperimentVariant[]\n /** Conversion goal: a host/site event, optionally filtered. */\n goal?: { event: string; filter?: string }\n /** Set when finished; the renderer then serves only the winner. */\n winnerVariantId?: string\n /**\n * Epoch millis (AGL-273): past this a running experiment stops serving\n * variants (visitors get the default) until someone declares a winner.\n */\n endAtMs?: number\n /**\n * Opt-in auto-completion (AGL-273): once every variant has\n * `minExposures` and a challenger clears `confidence` (0..1) against\n * the control, the track pipeline promotes it and finishes the\n * experiment. Control wins when every challenger is confidently worse.\n */\n autoWinner?: { minExposures: number; confidence: number }\n /** Set by the pipeline when `autoWinner` decided the experiment. */\n autoCompleted?: boolean\n}\n\nexport const EXPERIMENT_MAX_VARIANTS = 4\n\n/** FNV-1a over the visitor+experiment key — stable, dependency-free. */\nfunction bucketHash(value: string): number {\n let hash = 0x811c9dc5\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193)\n }\n return hash >>> 0\n}\n\n/**\n * Deterministic weighted assignment: the same visitor always lands in the\n * same variant of an experiment. Finished experiments return the winner;\n * non-running experiments without a winner return null (serve default).\n */\nexport function assignExperimentVariant(\n experiment: Pick<\n HostExperiment,\n 'status' | 'variants' | 'winnerVariantId' | 'endAtMs'\n > & {\n $id?: string\n },\n experimentId: string,\n visitorId: string,\n nowMs: number = Date.now(),\n): ExperimentVariant | null {\n const variants = (experiment.variants ?? []).slice(\n 0,\n EXPERIMENT_MAX_VARIANTS,\n )\n if (!variants.length) return null\n if (experiment.winnerVariantId) {\n return (\n variants.find((variant) => variant.id === experiment.winnerVariantId) ??\n null\n )\n }\n if (experiment.status !== 'running') return null\n // Past the end date (AGL-273) visitors get the default until a winner\n // is declared — stats stop accruing rather than drifting post-test.\n if (experiment.endAtMs && nowMs > experiment.endAtMs) return null\n const totalWeight = variants.reduce(\n (sum, variant) => sum + Math.max(0, variant.weight ?? 1),\n 0,\n )\n if (totalWeight <= 0) return null\n const bucket = bucketHash(`${visitorId}:${experimentId}`) % totalWeight\n let cursor = 0\n for (const variant of variants) {\n cursor += Math.max(0, variant.weight ?? 1)\n if (bucket < cursor) return variant\n }\n return variants[variants.length - 1]\n}\n\n/** Validates an experiment doc shape; human-readable error or null. */\nexport function validateExperiment(\n experiment: HostExperiment,\n): string | null {\n if (!experiment.name?.trim()) return 'Name the experiment'\n if (!['screen', 'section', 'email'].includes(experiment.target)) {\n return 'Pick what the experiment tests'\n }\n const variants = experiment.variants ?? []\n if (variants.length < 2) return 'Add at least two variants'\n if (variants.length > EXPERIMENT_MAX_VARIANTS) {\n return `Experiments are capped at ${EXPERIMENT_MAX_VARIANTS} variants`\n }\n const ids = new Set(variants.map((variant) => variant.id))\n if (ids.size !== variants.length) return 'Variant ids must be unique'\n if (experiment.target === 'section' && !experiment.nodeId?.trim()) {\n return 'Section experiments need the canvas element id'\n }\n if (\n (experiment.target === 'screen' || experiment.target === 'section') &&\n !experiment.screenId?.trim()\n ) {\n return 'Pick the screen under test'\n }\n if (experiment.autoWinner) {\n const { minExposures, confidence } = experiment.autoWinner\n if (!Number.isFinite(minExposures) || minExposures < 1) {\n return 'Auto-winner needs a minimum exposure count of at least 1'\n }\n if (!Number.isFinite(confidence) || confidence <= 0.5 || confidence >= 1) {\n return 'Auto-winner confidence must be between 0.5 and 1'\n }\n }\n return null\n}\n\n/** Naive conversion-rate summary the results table renders. */\nexport function summarizeVariantStats(stats: {\n exposures?: number\n conversions?: number\n}): { exposures: number; conversions: number; rate: number } {\n const exposures = Math.max(0, stats.exposures ?? 0)\n const conversions = Math.max(0, stats.conversions ?? 0)\n return {\n exposures,\n conversions,\n rate: exposures > 0 ? conversions / exposures : 0,\n }\n}\n\nexport interface VariantComparison {\n /** Relative conversion-rate lift vs the control; null when unmeasurable. */\n lift: number | null\n /**\n * One-sided confidence (0..1) that the challenger genuinely beats the\n * control — a two-proportion z-test through the normal CDF. Null until\n * both variants have exposures and at least one conversion exists.\n */\n confidence: number | null\n}\n\n/** Abramowitz–Stegun erf approximation (max error ~1.5e-7). */\nfunction erf(x: number): number {\n const sign = x < 0 ? -1 : 1\n const t = 1 / (1 + 0.3275911 * Math.abs(x))\n const y =\n 1 -\n (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t -\n 0.284496736) *\n t +\n 0.254829592) *\n t *\n Math.exp(-x * x)\n return sign * y\n}\n\n/** Standard normal CDF. */\nfunction normalCdf(z: number): number {\n return 0.5 * (1 + erf(z / Math.SQRT2))\n}\n\n/**\n * Compares a challenger variant against the control (AGL-265): lift =\n * relative rate change; confidence = P(challenger > control) from a\n * pooled two-proportion z-test. Small samples return null confidence\n * rather than a misleading number.\n */\nexport function compareVariants(\n control: { exposures?: number; conversions?: number },\n challenger: { exposures?: number; conversions?: number },\n): VariantComparison {\n const a = summarizeVariantStats(control)\n const b = summarizeVariantStats(challenger)\n const lift = a.rate > 0 ? (b.rate - a.rate) / a.rate : null\n if (\n a.exposures < 1 ||\n b.exposures < 1 ||\n a.conversions + b.conversions < 1\n ) {\n return { lift, confidence: null }\n }\n const pooled =\n (a.conversions + b.conversions) / (a.exposures + b.exposures)\n const standardError = Math.sqrt(\n pooled * (1 - pooled) * (1 / a.exposures + 1 / b.exposures),\n )\n if (!Number.isFinite(standardError) || standardError === 0) {\n return { lift, confidence: null }\n }\n const z = (b.rate - a.rate) / standardError\n return { lift, confidence: normalCdf(z) }\n}\n\n/** Per-variant counters, keyed by variant id, as `experiments/{id}/stats` holds them. */\nexport type ExperimentStatsByVariant = Record<\n string,\n { exposures?: number; conversions?: number } | undefined\n>\n\n/** One variant's line in the results table. */\nexport interface ExperimentResultRow {\n variant: ExperimentVariant\n summary: { exposures: number; conversions: number; rate: number }\n /**\n * Lift and confidence against the control — the FIRST variant, which is\n * compared against nothing and so carries `null` here.\n */\n comparison: VariantComparison | null\n /** It has exposures and no variant converts at a higher rate. Ties all lead. */\n leader: boolean\n /** The variant the experiment finished on. */\n winner: boolean\n}\n\n/**\n * The results table's rows, in variant order: each variant's figures, its\n * comparison with the control (AGL-265), and whether it leads or won.\n */\nexport function experimentResultRows(\n experiment: Pick<HostExperiment, 'variants' | 'winnerVariantId'>,\n stats: ExperimentStatsByVariant,\n): ExperimentResultRow[] {\n const variants = experiment.variants ?? []\n const summaries = variants.map((variant) =>\n summarizeVariantStats(stats[variant.id] ?? {}),\n )\n const control = variants[0]\n return variants.map((variant, index) => {\n const summary = summaries[index]\n return {\n variant,\n summary,\n comparison:\n index > 0 && control\n ? compareVariants(stats[control.id] ?? {}, stats[variant.id] ?? {})\n : null,\n leader:\n summary.exposures > 0 &&\n summaries.every((other) => summary.rate >= other.rate),\n winner: experiment.winnerVariantId === variant.id,\n }\n })\n}\n\n/**\n * A comparison as the results table prints it: `+12% · 97% conf.`, a dash\n * for a lift the control's zero rate cannot express, `needs data` until the\n * z-test has something to test, and `control` for the variant everything is\n * measured against.\n */\nexport function describeVariantComparison(\n comparison: VariantComparison | null,\n): string {\n if (!comparison) return 'control'\n const lift =\n comparison.lift != null\n ? `${comparison.lift >= 0 ? '+' : ''}${(comparison.lift * 100).toFixed(0)}%`\n : '—'\n const confidence =\n comparison.confidence != null\n ? ` · ${(comparison.confidence * 100).toFixed(0)}% conf.`\n : ' · needs data'\n return `${lift}${confidence}`\n}\n\n/**\n * Auto-winner evaluation (AGL-273): decides a running experiment from\n * per-variant stats once thresholds are met. The first variant is the\n * control. Undecidable states return null: any arm still under\n * `minExposures`, or no challenger confidently better while some remain\n * in the uncertain middle. Control wins only when every challenger is\n * confidently worse (its win probability is below `1 - confidence`).\n */\nexport function evaluateAutoWinner(\n experiment: Pick<HostExperiment, 'status' | 'variants' | 'autoWinner'>,\n statsByVariant: Record<\n string,\n { exposures?: number; conversions?: number } | undefined\n >,\n): { variantId: string; confidence: number } | null {\n const config = experiment.autoWinner\n if (!config || experiment.status !== 'running') return null\n const variants = (experiment.variants ?? []).slice(\n 0,\n EXPERIMENT_MAX_VARIANTS,\n )\n if (variants.length < 2) return null\n const controlStats = statsByVariant[variants[0].id] ?? {}\n if ((controlStats.exposures ?? 0) < config.minExposures) return null\n let best: { variantId: string; confidence: number } | null = null\n let everyLoses = true\n for (const challenger of variants.slice(1)) {\n const challengerStats = statsByVariant[challenger.id] ?? {}\n if ((challengerStats.exposures ?? 0) < config.minExposures) return null\n const { confidence } = compareVariants(controlStats, challengerStats)\n if (confidence === null) return null\n if (\n confidence >= config.confidence &&\n (!best || confidence > best.confidence)\n ) {\n best = { variantId: challenger.id, confidence }\n }\n if (confidence > 1 - config.confidence) everyLoses = false\n }\n if (best) return best\n if (everyLoses) {\n return { variantId: variants[0].id, confidence: config.confidence }\n }\n return null\n}\n"],"names":["EXPERIMENT_MAX_VARIANTS","bucketHash","value","hash","index","length","charCodeAt","Math","imul","assignExperimentVariant","experiment","experimentId","visitorId","nowMs","Date","now","variants","slice","winnerVariantId","find","variant","id","status","endAtMs","totalWeight","reduce","sum","max","weight","bucket","cursor","validateExperiment","name","trim","includes","target","ids","Set","map","size","nodeId","screenId","autoWinner","minExposures","confidence","Number","isFinite","summarizeVariantStats","stats","exposures","conversions","rate","erf","x","sign","t","abs","y","exp","normalCdf","z","SQRT2","compareVariants","control","challenger","a","b","lift","pooled","standardError","sqrt","experimentResultRows","summaries","summary","comparison","leader","every","other","winner","describeVariantComparison","toFixed","evaluateAutoWinner","statsByVariant","controlStats","config","best","everyLoses","challengerStats","variantId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/experiments.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\n/**\n * A/B experiments (AGL-252): docs at `hosts/{hostId}/experiments/{id}`.\n * Assignment is deterministic — hash(visitorId + experimentId) buckets a\n * visitor into a weighted variant, so the same visitor always sees the\n * same variant with no server state. Exposures/conversions accumulate on\n * per-variant stats counters via the track API. Business tier\n * (`abTesting` flag).\n */\nexport type ExperimentStatus = 'draft' | 'running' | 'paused' | 'done'\n\n/** What the experiment varies. */\nexport type ExperimentTarget = 'screen' | 'section' | 'email'\n\nexport interface ExperimentVariant {\n id: string\n name?: string\n /** Relative traffic weight; defaults to 1. */\n weight?: number\n /** Screen experiments: the screen version this variant serves. */\n versionId?: string\n /** Email experiments (AGL-255): subject/body overrides. */\n subject?: string\n body?: string\n}\n\nexport interface HostExperiment {\n name: string\n status: ExperimentStatus\n target: ExperimentTarget\n /** Screen/section experiments: the screen under test. */\n screenId?: string\n /** Section experiments: canvas node id the variants swap. */\n nodeId?: string\n variants: ExperimentVariant[]\n /** Conversion goal: a host/site event, optionally filtered. */\n goal?: { event: string; filter?: string }\n /** Set when finished; the renderer then serves only the winner. */\n winnerVariantId?: string\n /**\n * Epoch millis (AGL-273): past this a running experiment stops serving\n * variants (visitors get the default) until someone declares a winner.\n */\n endAtMs?: number\n /**\n * Opt-in auto-completion (AGL-273): once every variant has\n * `minExposures` and a challenger clears `confidence` (0..1) against\n * the control, the track pipeline promotes it and finishes the\n * experiment. Control wins when every challenger is confidently worse.\n */\n autoWinner?: { minExposures: number; confidence: number }\n /** Set by the pipeline when `autoWinner` decided the experiment. */\n autoCompleted?: boolean\n}\n\nexport const EXPERIMENT_MAX_VARIANTS = 4\n\n/** FNV-1a over the visitor+experiment key — stable, dependency-free. */\nfunction bucketHash(value: string): number {\n let hash = 0x811c9dc5\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193)\n }\n return hash >>> 0\n}\n\n/**\n * Deterministic weighted assignment: the same visitor always lands in the\n * same variant of an experiment. Finished experiments return the winner;\n * non-running experiments without a winner return null (serve default).\n */\nexport function assignExperimentVariant(\n experiment: Pick<\n HostExperiment,\n 'status' | 'variants' | 'winnerVariantId' | 'endAtMs'\n > & {\n $id?: string\n },\n experimentId: string,\n visitorId: string,\n nowMs: number = Date.now(),\n): ExperimentVariant | null {\n const variants = (experiment.variants ?? []).slice(\n 0,\n EXPERIMENT_MAX_VARIANTS,\n )\n if (!variants.length) return null\n if (experiment.winnerVariantId) {\n return (\n variants.find((variant) => variant.id === experiment.winnerVariantId) ??\n null\n )\n }\n if (experiment.status !== 'running') return null\n // Past the end date (AGL-273) visitors get the default until a winner\n // is declared — stats stop accruing rather than drifting post-test.\n if (experiment.endAtMs && nowMs > experiment.endAtMs) return null\n const totalWeight = variants.reduce(\n (sum, variant) => sum + Math.max(0, variant.weight ?? 1),\n 0,\n )\n if (totalWeight <= 0) return null\n const bucket = bucketHash(`${visitorId}:${experimentId}`) % totalWeight\n let cursor = 0\n for (const variant of variants) {\n cursor += Math.max(0, variant.weight ?? 1)\n if (bucket < cursor) return variant\n }\n return variants[variants.length - 1]\n}\n\n/** Validates an experiment doc shape; human-readable error or null. */\nexport function validateExperiment(\n experiment: HostExperiment,\n): string | null {\n if (!experiment.name?.trim()) return 'Name the experiment'\n if (!['screen', 'section', 'email'].includes(experiment.target)) {\n return 'Pick what the experiment tests'\n }\n const variants = experiment.variants ?? []\n if (variants.length < 2) return 'Add at least two variants'\n if (variants.length > EXPERIMENT_MAX_VARIANTS) {\n return `Experiments are capped at ${EXPERIMENT_MAX_VARIANTS} variants`\n }\n const ids = new Set(variants.map((variant) => variant.id))\n if (ids.size !== variants.length) return 'Variant ids must be unique'\n if (experiment.target === 'section' && !experiment.nodeId?.trim()) {\n return 'Section experiments need the canvas element id'\n }\n if (\n (experiment.target === 'screen' || experiment.target === 'section') &&\n !experiment.screenId?.trim()\n ) {\n return 'Pick the page under test'\n }\n if (experiment.autoWinner) {\n const { minExposures, confidence } = experiment.autoWinner\n if (!Number.isFinite(minExposures) || minExposures < 1) {\n return 'Auto-winner needs a minimum exposure count of at least 1'\n }\n if (!Number.isFinite(confidence) || confidence <= 0.5 || confidence >= 1) {\n return 'Auto-winner confidence must be between 0.5 and 1'\n }\n }\n return null\n}\n\n/** Naive conversion-rate summary the results table renders. */\nexport function summarizeVariantStats(stats: {\n exposures?: number\n conversions?: number\n}): { exposures: number; conversions: number; rate: number } {\n const exposures = Math.max(0, stats.exposures ?? 0)\n const conversions = Math.max(0, stats.conversions ?? 0)\n return {\n exposures,\n conversions,\n rate: exposures > 0 ? conversions / exposures : 0,\n }\n}\n\nexport interface VariantComparison {\n /** Relative conversion-rate lift vs the control; null when unmeasurable. */\n lift: number | null\n /**\n * One-sided confidence (0..1) that the challenger genuinely beats the\n * control — a two-proportion z-test through the normal CDF. Null until\n * both variants have exposures and at least one conversion exists.\n */\n confidence: number | null\n}\n\n/** Abramowitz–Stegun erf approximation (max error ~1.5e-7). */\nfunction erf(x: number): number {\n const sign = x < 0 ? -1 : 1\n const t = 1 / (1 + 0.3275911 * Math.abs(x))\n const y =\n 1 -\n (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t -\n 0.284496736) *\n t +\n 0.254829592) *\n t *\n Math.exp(-x * x)\n return sign * y\n}\n\n/** Standard normal CDF. */\nfunction normalCdf(z: number): number {\n return 0.5 * (1 + erf(z / Math.SQRT2))\n}\n\n/**\n * Compares a challenger variant against the control (AGL-265): lift =\n * relative rate change; confidence = P(challenger > control) from a\n * pooled two-proportion z-test. Small samples return null confidence\n * rather than a misleading number.\n */\nexport function compareVariants(\n control: { exposures?: number; conversions?: number },\n challenger: { exposures?: number; conversions?: number },\n): VariantComparison {\n const a = summarizeVariantStats(control)\n const b = summarizeVariantStats(challenger)\n const lift = a.rate > 0 ? (b.rate - a.rate) / a.rate : null\n if (\n a.exposures < 1 ||\n b.exposures < 1 ||\n a.conversions + b.conversions < 1\n ) {\n return { lift, confidence: null }\n }\n const pooled =\n (a.conversions + b.conversions) / (a.exposures + b.exposures)\n const standardError = Math.sqrt(\n pooled * (1 - pooled) * (1 / a.exposures + 1 / b.exposures),\n )\n if (!Number.isFinite(standardError) || standardError === 0) {\n return { lift, confidence: null }\n }\n const z = (b.rate - a.rate) / standardError\n return { lift, confidence: normalCdf(z) }\n}\n\n/** Per-variant counters, keyed by variant id, as `experiments/{id}/stats` holds them. */\nexport type ExperimentStatsByVariant = Record<\n string,\n { exposures?: number; conversions?: number } | undefined\n>\n\n/** One variant's line in the results table. */\nexport interface ExperimentResultRow {\n variant: ExperimentVariant\n summary: { exposures: number; conversions: number; rate: number }\n /**\n * Lift and confidence against the control — the FIRST variant, which is\n * compared against nothing and so carries `null` here.\n */\n comparison: VariantComparison | null\n /** It has exposures and no variant converts at a higher rate. Ties all lead. */\n leader: boolean\n /** The variant the experiment finished on. */\n winner: boolean\n}\n\n/**\n * The results table's rows, in variant order: each variant's figures, its\n * comparison with the control (AGL-265), and whether it leads or won.\n */\nexport function experimentResultRows(\n experiment: Pick<HostExperiment, 'variants' | 'winnerVariantId'>,\n stats: ExperimentStatsByVariant,\n): ExperimentResultRow[] {\n const variants = experiment.variants ?? []\n const summaries = variants.map((variant) =>\n summarizeVariantStats(stats[variant.id] ?? {}),\n )\n const control = variants[0]\n return variants.map((variant, index) => {\n const summary = summaries[index]\n return {\n variant,\n summary,\n comparison:\n index > 0 && control\n ? compareVariants(stats[control.id] ?? {}, stats[variant.id] ?? {})\n : null,\n leader:\n summary.exposures > 0 &&\n summaries.every((other) => summary.rate >= other.rate),\n winner: experiment.winnerVariantId === variant.id,\n }\n })\n}\n\n/**\n * A comparison as the results table prints it: `+12% · 97% conf.`, a dash\n * for a lift the control's zero rate cannot express, `needs data` until the\n * z-test has something to test, and `control` for the variant everything is\n * measured against.\n */\nexport function describeVariantComparison(\n comparison: VariantComparison | null,\n): string {\n if (!comparison) return 'control'\n const lift =\n comparison.lift != null\n ? `${comparison.lift >= 0 ? '+' : ''}${(comparison.lift * 100).toFixed(0)}%`\n : '—'\n const confidence =\n comparison.confidence != null\n ? ` · ${(comparison.confidence * 100).toFixed(0)}% conf.`\n : ' · needs data'\n return `${lift}${confidence}`\n}\n\n/**\n * Auto-winner evaluation (AGL-273): decides a running experiment from\n * per-variant stats once thresholds are met. The first variant is the\n * control. Undecidable states return null: any arm still under\n * `minExposures`, or no challenger confidently better while some remain\n * in the uncertain middle. Control wins only when every challenger is\n * confidently worse (its win probability is below `1 - confidence`).\n */\nexport function evaluateAutoWinner(\n experiment: Pick<HostExperiment, 'status' | 'variants' | 'autoWinner'>,\n statsByVariant: Record<\n string,\n { exposures?: number; conversions?: number } | undefined\n >,\n): { variantId: string; confidence: number } | null {\n const config = experiment.autoWinner\n if (!config || experiment.status !== 'running') return null\n const variants = (experiment.variants ?? []).slice(\n 0,\n EXPERIMENT_MAX_VARIANTS,\n )\n if (variants.length < 2) return null\n const controlStats = statsByVariant[variants[0].id] ?? {}\n if ((controlStats.exposures ?? 0) < config.minExposures) return null\n let best: { variantId: string; confidence: number } | null = null\n let everyLoses = true\n for (const challenger of variants.slice(1)) {\n const challengerStats = statsByVariant[challenger.id] ?? {}\n if ((challengerStats.exposures ?? 0) < config.minExposures) return null\n const { confidence } = compareVariants(controlStats, challengerStats)\n if (confidence === null) return null\n if (\n confidence >= config.confidence &&\n (!best || confidence > best.confidence)\n ) {\n best = { variantId: challenger.id, confidence }\n }\n if (confidence > 1 - config.confidence) everyLoses = false\n }\n if (best) return best\n if (everyLoses) {\n return { variantId: variants[0].id, confidence: config.confidence }\n }\n return null\n}\n"],"names":["EXPERIMENT_MAX_VARIANTS","bucketHash","value","hash","index","length","charCodeAt","Math","imul","assignExperimentVariant","experiment","experimentId","visitorId","nowMs","Date","now","variants","slice","winnerVariantId","find","variant","id","status","endAtMs","totalWeight","reduce","sum","max","weight","bucket","cursor","validateExperiment","name","trim","includes","target","ids","Set","map","size","nodeId","screenId","autoWinner","minExposures","confidence","Number","isFinite","summarizeVariantStats","stats","exposures","conversions","rate","erf","x","sign","t","abs","y","exp","normalCdf","z","SQRT2","compareVariants","control","challenger","a","b","lift","pooled","standardError","sqrt","experimentResultRows","summaries","summary","comparison","leader","every","other","winner","describeVariantComparison","toFixed","evaluateAutoWinner","statsByVariant","controlStats","config","best","everyLoses","challengerStats","variantId"],"mappings":"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"}
|
package/src/lib/model/index.d.ts
CHANGED
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
* Context-free — importable by client components, /server handlers, and
|
|
20
20
|
* other plugins/apps via `@aglyn/plugins-marketing/model`.
|
|
21
21
|
*/
|
|
22
|
+
export * from './campaign-kind';
|
|
22
23
|
export * from './campaign-membership-figures';
|
|
24
|
+
export * from './campaign-conversions';
|
|
25
|
+
export * from './campaign-revenue';
|
|
23
26
|
export * from './overlays';
|
|
24
27
|
export * from './experiments';
|
|
25
28
|
export * from './site-contract';
|
package/src/lib/model/index.js
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* Marketing domain model (AGL-412): overlays + experiments relocated from core app-utils.
|
|
18
18
|
* Context-free — importable by client components, /server handlers, and
|
|
19
19
|
* other plugins/apps via `@aglyn/plugins-marketing/model`.
|
|
20
|
-
*/ export * from "./campaign-
|
|
20
|
+
*/ export * from "./campaign-kind.js";
|
|
21
|
+
export * from "./campaign-membership-figures.js";
|
|
22
|
+
export * from "./campaign-conversions.js";
|
|
23
|
+
export * from "./campaign-revenue.js";
|
|
21
24
|
export * from "./overlays.js";
|
|
22
25
|
export * from "./experiments.js";
|
|
23
26
|
export * from "./site-contract.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/index.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\n/**\n * Marketing domain model (AGL-412): overlays + experiments relocated from core app-utils.\n * Context-free — importable by client components, /server handlers, and\n * other plugins/apps via `@aglyn/plugins-marketing/model`.\n */\nexport * from './campaign-membership-figures'\nexport * from './overlays'\nexport * from './experiments'\nexport * from './site-contract'\nexport * from './compile-client-automations'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,mCAA+B;AAC7C,cAAc,gBAAY;AAC1B,cAAc,mBAAe;AAC7B,cAAc,qBAAiB;AAC/B,cAAc,kCAA8B"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/index.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\n/**\n * Marketing domain model (AGL-412): overlays + experiments relocated from core app-utils.\n * Context-free — importable by client components, /server handlers, and\n * other plugins/apps via `@aglyn/plugins-marketing/model`.\n */\nexport * from './campaign-kind'\nexport * from './campaign-membership-figures'\nexport * from './campaign-conversions'\nexport * from './campaign-revenue'\nexport * from './overlays'\nexport * from './experiments'\nexport * from './site-contract'\nexport * from './compile-client-automations'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,qBAAiB;AAC/B,cAAc,mCAA+B;AAC7C,cAAc,4BAAwB;AACtC,cAAc,wBAAoB;AAClC,cAAc,gBAAY;AAC1B,cAAc,mBAAe;AAC7B,cAAc,qBAAiB;AAC/B,cAAc,kCAA8B"}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
|
|
18
|
+
import { type PluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists';
|
|
19
|
+
/**
|
|
20
|
+
* One overlay as another plugin's picker names it: its own name, else the
|
|
21
|
+
* bar's text or the popup's headline, else its id. A retired overlay
|
|
22
|
+
* (`deletedAt`) is left out.
|
|
23
|
+
*/
|
|
24
|
+
export declare function overlayIndexedRecord(id: string, data: Readonly<Record<string, unknown>>): PluginIndexedRecord | null;
|
|
25
|
+
/**
|
|
26
|
+
* A site's announcement bars and popups, listed in the console for another
|
|
27
|
+
* plugin's picker (AGL-3080) — an automation's "Show overlay" step choosing
|
|
28
|
+
* the one it opens. A site's own kind: asked with no site, it lists none.
|
|
29
|
+
* Ordered by document id, so the window is the same one every time.
|
|
30
|
+
*/
|
|
31
|
+
export declare const overlayRecordListSource: PluginRecordListSource;
|
|
32
|
+
/** Called from the console registrar, owner named for a spec that calls it directly. */
|
|
33
|
+
export declare function registerMarketingRecordLists(): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { registerPluginRecordListSource } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
|
|
17
|
+
import { collection, documentId, limit, orderBy, query } from "firebase/firestore";
|
|
18
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
19
|
+
/**
|
|
20
|
+
* One overlay as another plugin's picker names it: its own name, else the
|
|
21
|
+
* bar's text or the popup's headline, else its id. A retired overlay
|
|
22
|
+
* (`deletedAt`) is left out.
|
|
23
|
+
*/ export function overlayIndexedRecord(id, data) {
|
|
24
|
+
var _data_bar, _data_popup;
|
|
25
|
+
if (!id || !data || data['deletedAt'] != null) return null;
|
|
26
|
+
const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
27
|
+
const bar = (_data_bar = data['bar']) != null ? _data_bar : {};
|
|
28
|
+
const popup = (_data_popup = data['popup']) != null ? _data_popup : {};
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
name: text(data['name']) || text(bar['text']) || text(popup['headline']) || id,
|
|
32
|
+
facts: {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A site's announcement bars and popups, listed in the console for another
|
|
37
|
+
* plugin's picker (AGL-3080) — an automation's "Show overlay" step choosing
|
|
38
|
+
* the one it opens. A site's own kind: asked with no site, it lists none.
|
|
39
|
+
* Ordered by document id, so the window is the same one every time.
|
|
40
|
+
*/ export const overlayRecordListSource = {
|
|
41
|
+
query (firestore, request) {
|
|
42
|
+
if (!request.hostId) return null;
|
|
43
|
+
return query(collection(firestore, 'hosts', request.hostId, 'overlays'), orderBy(documentId()), limit(request.limit));
|
|
44
|
+
},
|
|
45
|
+
record: overlayIndexedRecord
|
|
46
|
+
};
|
|
47
|
+
/** Called from the console registrar, owner named for a spec that calls it directly. */ export function registerMarketingRecordLists() {
|
|
48
|
+
registerPluginRecordListSource('overlay', overlayRecordListSource, {
|
|
49
|
+
pluginId: BUNDLE_ID
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=overlay-record-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/overlay-record-list.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 type { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport {\n registerPluginRecordListSource,\n type PluginRecordListSource,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { collection, documentId, limit, orderBy, query } from 'firebase/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * One overlay as another plugin's picker names it: its own name, else the\n * bar's text or the popup's headline, else its id. A retired overlay\n * (`deletedAt`) is left out.\n */\nexport function overlayIndexedRecord(\n id: string,\n data: Readonly<Record<string, unknown>>,\n): PluginIndexedRecord | null {\n if (!id || !data || data['deletedAt'] != null) return null\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const bar = (data['bar'] ?? {}) as Record<string, unknown>\n const popup = (data['popup'] ?? {}) as Record<string, unknown>\n return {\n id,\n name: text(data['name']) || text(bar['text']) || text(popup['headline']) || id,\n facts: {},\n }\n}\n\n/**\n * A site's announcement bars and popups, listed in the console for another\n * plugin's picker (AGL-3080) — an automation's \"Show overlay\" step choosing\n * the one it opens. A site's own kind: asked with no site, it lists none.\n * Ordered by document id, so the window is the same one every time.\n */\nexport const overlayRecordListSource: PluginRecordListSource = {\n query(firestore, request) {\n if (!request.hostId) return null\n return query(\n collection(firestore, 'hosts', request.hostId, 'overlays'),\n orderBy(documentId()),\n limit(request.limit),\n )\n },\n record: overlayIndexedRecord,\n}\n\n/** Called from the console registrar, owner named for a spec that calls it directly. */\nexport function registerMarketingRecordLists(): void {\n registerPluginRecordListSource('overlay', overlayRecordListSource, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordListSource","collection","documentId","limit","orderBy","query","BUNDLE_ID","overlayIndexedRecord","id","data","text","value","trim","bar","popup","name","facts","overlayRecordListSource","firestore","request","hostId","record","registerMarketingRecordLists","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,8BAA8B,QAEzB,kDAAiD;AACxD,SAASC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,OAAO,EAAEC,KAAK,QAAQ,qBAAoB;AAClF,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;CAIC,GACD,OAAO,SAASC,qBACdC,EAAU,EACVC,IAAuC;QAI1BA,WACEA;IAHf,IAAI,CAACD,MAAM,CAACC,QAAQA,IAAI,CAAC,YAAY,IAAI,MAAM,OAAO;IACtD,MAAMC,OAAO,CAACC,QAAoB,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;IAC7E,MAAMC,OAAOJ,YAAAA,IAAI,CAAC,MAAM,YAAXA,YAAe,CAAC;IAC7B,MAAMK,SAASL,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB,CAAC;IACjC,OAAO;QACLD;QACAO,MAAML,KAAKD,IAAI,CAAC,OAAO,KAAKC,KAAKG,GAAG,CAAC,OAAO,KAAKH,KAAKI,KAAK,CAAC,WAAW,KAAKN;QAC5EQ,OAAO,CAAC;IACV;AACF;AAEA;;;;;CAKC,GACD,OAAO,MAAMC,0BAAkD;IAC7DZ,OAAMa,SAAS,EAAEC,OAAO;QACtB,IAAI,CAACA,QAAQC,MAAM,EAAE,OAAO;QAC5B,OAAOf,MACLJ,WAAWiB,WAAW,SAASC,QAAQC,MAAM,EAAE,aAC/ChB,QAAQF,eACRC,MAAMgB,QAAQhB,KAAK;IAEvB;IACAkB,QAAQd;AACV,EAAC;AAED,sFAAsF,GACtF,OAAO,SAASe;IACdtB,+BAA+B,WAAWiB,yBAAyB;QAAEM,UAAUjB;IAAU;AAC3F"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -22,6 +22,7 @@ import { EXPERIMENT_RESULT_ZONE, EXPERIMENT_VARIANTS_ZONE } from "./components/e
|
|
|
22
22
|
import { MARKETING_CONSOLE_SECTIONS, MARKETING_ORG_CONSOLE_SECTIONS } from "./components/marketing-console-sections.js";
|
|
23
23
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
24
24
|
import { StaffOrgEmailCard } from "./components/staff-org-email-card.component.js";
|
|
25
|
+
import { registerMarketingRecordLists } from "./model/overlay-record-list.js";
|
|
25
26
|
import { registerMarketingRecordRoutes } from "./model/marketing-record-routes.js";
|
|
26
27
|
/** Code-split: the Marketing console page only loads when opened. */ const MarketingConsolePage = lazy(()=>import("./components/marketing-console-page.js"));
|
|
27
28
|
/** Dashboard glance card, loaded only where the shell renders the slot. */ const CampaignGlanceCard = lazy(()=>import("./components/campaign-glance-card.component.js"));
|
|
@@ -32,6 +33,7 @@ import { registerMarketingRecordRoutes } from "./model/marketing-record-routes.j
|
|
|
32
33
|
/** The Emails page's Messages section: the list, one message, its composer. */ const EmailMessagesWidget = lazy(()=>import("./components/email-messages-widget.js"));
|
|
33
34
|
/** Who received the sends built from one template, on the template's page. */ const EmailRecipientsCard = lazy(()=>import("./components/email-recipients-card.js"));
|
|
34
35
|
/** Loaded where a record page draws one of the attribution zones. */ const RecordAttributionWidget = lazy(()=>import("./components/record-attribution-widget.js"));
|
|
36
|
+
/** The site's section experiments, reported to the besigner's Interactions section. */ const BesignerSectionExperiments = lazy(()=>import("./components/besigner-section-experiments.js"));
|
|
35
37
|
/**
|
|
36
38
|
* Marketing feature plugin (AGL-395). Console-only — overlays and popups
|
|
37
39
|
* render on published sites through the tenant runtime, not a canvas
|
|
@@ -88,6 +90,8 @@ import { registerMarketingRecordRoutes } from "./model/marketing-record-routes.j
|
|
|
88
90
|
bareZone(CAMPAIGN_DESIGN_CREATE_ZONE, 'A design for one email', 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.');
|
|
89
91
|
bareZone(CAMPAIGN_DESIGN_PREVIEW_ZONE, 'A sent email as an inbox receives it', 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.');
|
|
90
92
|
registerMarketingRecordRoutes();
|
|
93
|
+
// The site's overlays, for another plugin's picker to list (AGL-3080).
|
|
94
|
+
registerMarketingRecordLists();
|
|
91
95
|
Aglyn.registerConsoleExtension({
|
|
92
96
|
pluginId: BUNDLE_ID,
|
|
93
97
|
displayName: 'Marketing',
|
|
@@ -143,6 +147,14 @@ import { registerMarketingRecordRoutes } from "./model/marketing-record-routes.j
|
|
|
143
147
|
title: 'Campaign attribution',
|
|
144
148
|
Component: RecordAttributionWidget
|
|
145
149
|
},
|
|
150
|
+
// The section experiments the besigner's Interactions section badges an
|
|
151
|
+
// element with and starts on a page's element; reported, never drawn.
|
|
152
|
+
{
|
|
153
|
+
slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,
|
|
154
|
+
widgetId: 'marketing-besigner-section-experiments',
|
|
155
|
+
title: 'Section experiments',
|
|
156
|
+
Component: BesignerSectionExperiments
|
|
157
|
+
},
|
|
146
158
|
// The staff organization page's sends and campaigns (AGL-3380), read
|
|
147
159
|
// only: staff see what the organization sent without joining it.
|
|
148
160
|
{
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailRecipientsCard","RecordAttributionWidget","registerMarketingConsole","zone","label","surface","description","pluginId","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordLists } from './model/overlay-record-list'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/** The site's section experiments, reported to the besigner's Interactions section. */\nconst BesignerSectionExperiments = lazy(\n () => import('./components/besigner-section-experiments'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n // The site's overlays, for another plugin's picker to list (AGL-3080).\n registerMarketingRecordLists()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The section experiments the besigner's Interactions section badges an\n // element with and starts on a page's element; reported, never drawn.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,\n widgetId: 'marketing-besigner-section-experiments',\n title: 'Section experiments',\n Component: BesignerSectionExperiments,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordLists","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailRecipientsCard","RecordAttributionWidget","BesignerSectionExperiments","registerMarketingConsole","zone","label","surface","description","pluginId","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","besignerInteractions","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
/** Why a partly-delivered email will not run another batch. */
|
|
18
|
+
export type CampaignBatchStop =
|
|
19
|
+
/** Nothing is left to address. */
|
|
20
|
+
'complete'
|
|
21
|
+
/** {@link EMAIL_MAX_SEND_BATCHES} reached. */
|
|
22
|
+
| 'batch-limit'
|
|
23
|
+
/** The batch settled nobody, so another one would settle nobody either. */
|
|
24
|
+
| 'no-progress';
|
|
25
|
+
/** What happens to an email after one batch of it has been delivered. */
|
|
26
|
+
export interface CampaignBatchPlan {
|
|
27
|
+
/** People this email has resolved and not yet addressed. */
|
|
28
|
+
remaining: number;
|
|
29
|
+
/** Batches this email has now run, including the one just finished. */
|
|
30
|
+
batch: number;
|
|
31
|
+
/** True when another batch will run. */
|
|
32
|
+
resuming: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Why no further batch will run. Null while {@link resuming} is true —
|
|
35
|
+
* a reason for a state that has not happened is a reason a surface would
|
|
36
|
+
* show.
|
|
37
|
+
*/
|
|
38
|
+
stop: CampaignBatchStop | null;
|
|
39
|
+
}
|
|
40
|
+
export interface CampaignBatchPlanInput {
|
|
41
|
+
/** People this batch could have addressed, before the per-send cap. */
|
|
42
|
+
mailable: number;
|
|
43
|
+
/** People it did address — `min(mailable, perSend)`. */
|
|
44
|
+
addressed: number;
|
|
45
|
+
/**
|
|
46
|
+
* Of those it addressed, how many are left for a later batch to try again:
|
|
47
|
+
* the tail an hourly deferral cut off, and nothing else. A recipient the
|
|
48
|
+
* send SETTLED — delivered, suppressed or off-topic — is not retryable and
|
|
49
|
+
* is not counted here.
|
|
50
|
+
*/
|
|
51
|
+
retryable: number;
|
|
52
|
+
/**
|
|
53
|
+
* People this batch settled: delivered plus permanently excluded. Zero is
|
|
54
|
+
* what {@link CampaignBatchStop} `no-progress` is, and it is the reason
|
|
55
|
+
* this is an input rather than a derivation — a batch that addressed 500
|
|
56
|
+
* people and settled none of them will address the same 500 next time.
|
|
57
|
+
*/
|
|
58
|
+
settled: number;
|
|
59
|
+
/** Batches run before this one. */
|
|
60
|
+
batchesSoFar: number;
|
|
61
|
+
/** Injectable for tests; defaults to the shipped guard. */
|
|
62
|
+
maxBatches?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether a partly-delivered email runs again, and how much is left.
|
|
66
|
+
*
|
|
67
|
+
* Pure, so the termination rule can be read and tested without a send. The
|
|
68
|
+
* one property it exists to hold is that a campaign always terminates:
|
|
69
|
+
* `resuming` is false whenever there is nothing left, whenever the batch
|
|
70
|
+
* guard is reached, and whenever a batch settled nobody — the last of which
|
|
71
|
+
* is the only way a self-rescheduling job can loop.
|
|
72
|
+
*/
|
|
73
|
+
export declare function campaignBatchPlan(input: CampaignBatchPlanInput): CampaignBatchPlan;
|
|
@@ -0,0 +1,79 @@
|
|
|
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_MAX_SEND_BATCHES } from "@aglyn/shared-util-email";
|
|
17
|
+
/**
|
|
18
|
+
* Whether a campaign that has delivered one batch runs another.
|
|
19
|
+
*
|
|
20
|
+
* A campaign larger than one send's recipient cap goes out in batches: the
|
|
21
|
+
* sender addresses what it may, and reschedules itself for the rest. This is
|
|
22
|
+
* the rule that decides whether it does, pure so it can be read and tested
|
|
23
|
+
* without a send. The one property it exists to hold is that a campaign always
|
|
24
|
+
* terminates. The ceiling on how many batches any bulk send may take,
|
|
25
|
+
* {@link EMAIL_MAX_SEND_BATCHES}, is the mail rail's; the rule for this sender
|
|
26
|
+
* is the campaign's own.
|
|
27
|
+
*/ /** Clamps a raw count to a positive integer, or returns `fallback`. */ function positiveInt(raw, fallback) {
|
|
28
|
+
const value = Number(raw);
|
|
29
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a partly-delivered email runs again, and how much is left.
|
|
33
|
+
*
|
|
34
|
+
* Pure, so the termination rule can be read and tested without a send. The
|
|
35
|
+
* one property it exists to hold is that a campaign always terminates:
|
|
36
|
+
* `resuming` is false whenever there is nothing left, whenever the batch
|
|
37
|
+
* guard is reached, and whenever a batch settled nobody — the last of which
|
|
38
|
+
* is the only way a self-rescheduling job can loop.
|
|
39
|
+
*/ export function campaignBatchPlan(input) {
|
|
40
|
+
const mailable = positiveInt(input.mailable, 0);
|
|
41
|
+
const addressed = Math.min(mailable, positiveInt(input.addressed, 0));
|
|
42
|
+
const retryable = Math.min(addressed, positiveInt(input.retryable, 0));
|
|
43
|
+
const settled = positiveInt(input.settled, 0);
|
|
44
|
+
const batch = positiveInt(input.batchesSoFar, 0) + 1;
|
|
45
|
+
const maxBatches = positiveInt(input.maxBatches, EMAIL_MAX_SEND_BATCHES);
|
|
46
|
+
const remaining = Math.max(0, mailable - addressed) + retryable;
|
|
47
|
+
if (remaining <= 0) {
|
|
48
|
+
return {
|
|
49
|
+
remaining: 0,
|
|
50
|
+
batch,
|
|
51
|
+
resuming: false,
|
|
52
|
+
stop: 'complete'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (batch >= maxBatches) {
|
|
56
|
+
return {
|
|
57
|
+
remaining,
|
|
58
|
+
batch,
|
|
59
|
+
resuming: false,
|
|
60
|
+
stop: 'batch-limit'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (settled <= 0) {
|
|
64
|
+
return {
|
|
65
|
+
remaining,
|
|
66
|
+
batch,
|
|
67
|
+
resuming: false,
|
|
68
|
+
stop: 'no-progress'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
remaining,
|
|
73
|
+
batch,
|
|
74
|
+
resuming: true,
|
|
75
|
+
stop: null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=campaign-batch-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-batch-plan.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 { EMAIL_MAX_SEND_BATCHES } from '@aglyn/shared-util-email'\n\n/**\n * Whether a campaign that has delivered one batch runs another.\n *\n * A campaign larger than one send's recipient cap goes out in batches: the\n * sender addresses what it may, and reschedules itself for the rest. This is\n * the rule that decides whether it does, pure so it can be read and tested\n * without a send. The one property it exists to hold is that a campaign always\n * terminates. The ceiling on how many batches any bulk send may take,\n * {@link EMAIL_MAX_SEND_BATCHES}, is the mail rail's; the rule for this sender\n * is the campaign's own.\n */\n\n/** Clamps a raw count to a positive integer, or returns `fallback`. */\nfunction positiveInt(raw: unknown, fallback: number): number {\n const value = Number(raw)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback\n}\n\n/** Why a partly-delivered email will not run another batch. */\nexport type CampaignBatchStop =\n /** Nothing is left to address. */\n | 'complete'\n /** {@link EMAIL_MAX_SEND_BATCHES} reached. */\n | 'batch-limit'\n /** The batch settled nobody, so another one would settle nobody either. */\n | 'no-progress'\n\n/** What happens to an email after one batch of it has been delivered. */\nexport interface CampaignBatchPlan {\n /** People this email has resolved and not yet addressed. */\n remaining: number\n /** Batches this email has now run, including the one just finished. */\n batch: number\n /** True when another batch will run. */\n resuming: boolean\n /**\n * Why no further batch will run. Null while {@link resuming} is true —\n * a reason for a state that has not happened is a reason a surface would\n * show.\n */\n stop: CampaignBatchStop | null\n}\n\nexport interface CampaignBatchPlanInput {\n /** People this batch could have addressed, before the per-send cap. */\n mailable: number\n /** People it did address — `min(mailable, perSend)`. */\n addressed: number\n /**\n * Of those it addressed, how many are left for a later batch to try again:\n * the tail an hourly deferral cut off, and nothing else. A recipient the\n * send SETTLED — delivered, suppressed or off-topic — is not retryable and\n * is not counted here.\n */\n retryable: number\n /**\n * People this batch settled: delivered plus permanently excluded. Zero is\n * what {@link CampaignBatchStop} `no-progress` is, and it is the reason\n * this is an input rather than a derivation — a batch that addressed 500\n * people and settled none of them will address the same 500 next time.\n */\n settled: number\n /** Batches run before this one. */\n batchesSoFar: number\n /** Injectable for tests; defaults to the shipped guard. */\n maxBatches?: number\n}\n\n/**\n * Whether a partly-delivered email runs again, and how much is left.\n *\n * Pure, so the termination rule can be read and tested without a send. The\n * one property it exists to hold is that a campaign always terminates:\n * `resuming` is false whenever there is nothing left, whenever the batch\n * guard is reached, and whenever a batch settled nobody — the last of which\n * is the only way a self-rescheduling job can loop.\n */\nexport function campaignBatchPlan(\n input: CampaignBatchPlanInput,\n): CampaignBatchPlan {\n const mailable = positiveInt(input.mailable, 0)\n const addressed = Math.min(mailable, positiveInt(input.addressed, 0))\n const retryable = Math.min(addressed, positiveInt(input.retryable, 0))\n const settled = positiveInt(input.settled, 0)\n const batch = positiveInt(input.batchesSoFar, 0) + 1\n const maxBatches = positiveInt(input.maxBatches, EMAIL_MAX_SEND_BATCHES)\n const remaining = Math.max(0, mailable - addressed) + retryable\n\n if (remaining <= 0) {\n return { remaining: 0, batch, resuming: false, stop: 'complete' }\n }\n if (batch >= maxBatches) {\n return { remaining, batch, resuming: false, stop: 'batch-limit' }\n }\n if (settled <= 0) {\n return { remaining, batch, resuming: false, stop: 'no-progress' }\n }\n return { remaining, batch, resuming: true, stop: null }\n}\n"],"names":["EMAIL_MAX_SEND_BATCHES","positiveInt","raw","fallback","value","Number","isFinite","Math","floor","campaignBatchPlan","input","mailable","addressed","min","retryable","settled","batch","batchesSoFar","maxBatches","remaining","max","resuming","stop"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,2BAA0B;AAEjE;;;;;;;;;;CAUC,GAED,qEAAqE,GACrE,SAASC,YAAYC,GAAY,EAAEC,QAAgB;IACjD,MAAMC,QAAQC,OAAOH;IACrB,OAAOG,OAAOC,QAAQ,CAACF,UAAUA,QAAQ,IAAIG,KAAKC,KAAK,CAACJ,SAASD;AACnE;AAoDA;;;;;;;;CAQC,GACD,OAAO,SAASM,kBACdC,KAA6B;IAE7B,MAAMC,WAAWV,YAAYS,MAAMC,QAAQ,EAAE;IAC7C,MAAMC,YAAYL,KAAKM,GAAG,CAACF,UAAUV,YAAYS,MAAME,SAAS,EAAE;IAClE,MAAME,YAAYP,KAAKM,GAAG,CAACD,WAAWX,YAAYS,MAAMI,SAAS,EAAE;IACnE,MAAMC,UAAUd,YAAYS,MAAMK,OAAO,EAAE;IAC3C,MAAMC,QAAQf,YAAYS,MAAMO,YAAY,EAAE,KAAK;IACnD,MAAMC,aAAajB,YAAYS,MAAMQ,UAAU,EAAElB;IACjD,MAAMmB,YAAYZ,KAAKa,GAAG,CAAC,GAAGT,WAAWC,aAAaE;IAEtD,IAAIK,aAAa,GAAG;QAClB,OAAO;YAAEA,WAAW;YAAGH;YAAOK,UAAU;YAAOC,MAAM;QAAW;IAClE;IACA,IAAIN,SAASE,YAAY;QACvB,OAAO;YAAEC;YAAWH;YAAOK,UAAU;YAAOC,MAAM;QAAc;IAClE;IACA,IAAIP,WAAW,GAAG;QAChB,OAAO;YAAEI;YAAWH;YAAOK,UAAU;YAAOC,MAAM;QAAc;IAClE;IACA,OAAO;QAAEH;QAAWH;QAAOK,UAAU;QAAMC,MAAM;IAAK;AACxD"}
|