@aglyn/plugins-marketing 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +56 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
- package/src/lib/components/announcement-bar-card.component.js +292 -0
- package/src/lib/components/announcement-bar-card.component.js.map +1 -0
- package/src/lib/components/campaign-composer.d.ts +84 -0
- package/src/lib/components/campaign-composer.js +1643 -0
- package/src/lib/components/campaign-composer.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.d.ts +22 -0
- package/src/lib/components/campaign-conversions-card.js +642 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -0
- package/src/lib/components/campaign-detail-card.d.ts +55 -0
- package/src/lib/components/campaign-detail-card.js +771 -0
- package/src/lib/components/campaign-detail-card.js.map +1 -0
- package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
- package/src/lib/components/campaign-edit-drawer.js +252 -0
- package/src/lib/components/campaign-edit-drawer.js.map +1 -0
- package/src/lib/components/campaign-email-zones.d.ts +111 -0
- package/src/lib/components/campaign-email-zones.js +54 -0
- package/src/lib/components/campaign-email-zones.js.map +1 -0
- package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
- package/src/lib/components/campaign-glance-card.component.js +151 -0
- package/src/lib/components/campaign-glance-card.component.js.map +1 -0
- package/src/lib/components/campaign-members-section.d.ts +87 -0
- package/src/lib/components/campaign-members-section.js +499 -0
- package/src/lib/components/campaign-members-section.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +137 -0
- package/src/lib/components/campaign-reach-sections.js +694 -0
- package/src/lib/components/campaign-reach-sections.js.map +1 -0
- package/src/lib/components/campaign-report-card.d.ts +91 -0
- package/src/lib/components/campaign-report-card.js +667 -0
- package/src/lib/components/campaign-report-card.js.map +1 -0
- package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
- package/src/lib/components/campaign-test-send-drawer.js +238 -0
- package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
- package/src/lib/components/campaigns-card.d.ts +31 -0
- package/src/lib/components/campaigns-card.js +667 -0
- package/src/lib/components/campaigns-card.js.map +1 -0
- package/src/lib/components/conversion-attribution.component.d.ts +62 -0
- package/src/lib/components/conversion-attribution.component.js +127 -0
- package/src/lib/components/conversion-attribution.component.js.map +1 -0
- package/src/lib/components/email-compose-card.d.ts +12 -0
- package/src/lib/components/email-compose-card.js +157 -0
- package/src/lib/components/email-compose-card.js.map +1 -0
- package/src/lib/components/email-detail.d.ts +40 -0
- package/src/lib/components/email-detail.js +1180 -0
- package/src/lib/components/email-detail.js.map +1 -0
- package/src/lib/components/email-edit-drawer.d.ts +54 -0
- package/src/lib/components/email-edit-drawer.js +172 -0
- package/src/lib/components/email-edit-drawer.js.map +1 -0
- package/src/lib/components/email-messages-widget.d.ts +22 -0
- package/src/lib/components/email-messages-widget.js +49 -0
- package/src/lib/components/email-messages-widget.js.map +1 -0
- package/src/lib/components/email-recipients-card.d.ts +50 -0
- package/src/lib/components/email-recipients-card.js +338 -0
- package/src/lib/components/email-recipients-card.js.map +1 -0
- package/src/lib/components/emails-list-card.d.ts +36 -0
- package/src/lib/components/emails-list-card.js +544 -0
- package/src/lib/components/emails-list-card.js.map +1 -0
- package/src/lib/components/experiment-zones.d.ts +78 -0
- package/src/lib/components/experiment-zones.js +65 -0
- package/src/lib/components/experiment-zones.js.map +1 -0
- package/src/lib/components/host-experiments-card.component.d.ts +17 -0
- package/src/lib/components/host-experiments-card.component.js +887 -0
- package/src/lib/components/host-experiments-card.component.js.map +1 -0
- package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
- package/src/lib/components/host-marketing-summary-card.component.js +161 -0
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
- package/src/lib/components/host-overlays-card.component.d.ts +19 -0
- package/src/lib/components/host-overlays-card.component.js +783 -0
- package/src/lib/components/host-overlays-card.component.js.map +1 -0
- package/src/lib/components/marketing-console-page.d.ts +28 -0
- package/src/lib/components/marketing-console-page.js +166 -0
- package/src/lib/components/marketing-console-page.js.map +1 -0
- package/src/lib/components/marketing-console-sections.d.ts +34 -0
- package/src/lib/components/marketing-console-sections.js +77 -0
- package/src/lib/components/marketing-console-sections.js.map +1 -0
- package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
- package/src/lib/components/overlay-stats-row.component.js +78 -0
- package/src/lib/components/overlay-stats-row.component.js.map +1 -0
- package/src/lib/components/popup-card.component.d.ts +18 -0
- package/src/lib/components/popup-card.component.js +462 -0
- package/src/lib/components/popup-card.component.js.map +1 -0
- package/src/lib/components/record-attribution-widget.d.ts +35 -0
- package/src/lib/components/record-attribution-widget.js +33 -0
- package/src/lib/components/record-attribution-widget.js.map +1 -0
- package/src/lib/components/site-runtime.d.ts +9 -0
- package/src/lib/components/site-runtime.js +1081 -0
- package/src/lib/components/site-runtime.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +35 -0
- package/src/lib/components/use-campaign-send-api.js +99 -0
- package/src/lib/components/use-campaign-send-api.js.map +1 -0
- package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
- package/src/lib/components/use-campaign-topic-options.js +44 -0
- package/src/lib/components/use-campaign-topic-options.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +39 -0
- package/src/lib/components/use-emails-hub-path.js +70 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
- package/src/lib/components/use-marketing-hub-path.js +50 -0
- package/src/lib/components/use-marketing-hub-path.js.map +1 -0
- package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
- package/src/lib/components/use-overlay-copy-editor.js +110 -0
- package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/campaign-membership-figures.d.ts +109 -0
- package/src/lib/model/campaign-membership-figures.js +113 -0
- package/src/lib/model/campaign-membership-figures.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +65 -0
- package/src/lib/model/compile-client-automations.js +95 -0
- package/src/lib/model/compile-client-automations.js.map +1 -0
- package/src/lib/model/experiments.d.ts +130 -0
- package/src/lib/model/experiments.js +186 -0
- package/src/lib/model/experiments.js.map +1 -0
- package/src/lib/model/index.d.ts +26 -0
- package/src/lib/model/index.js +26 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketing-record-routes.d.ts +6 -0
- package/src/lib/model/marketing-record-routes.js +46 -0
- package/src/lib/model/marketing-record-routes.js.map +1 -0
- package/src/lib/model/overlay-copy.d.ts +62 -0
- package/src/lib/model/overlay-copy.js +78 -0
- package/src/lib/model/overlay-copy.js.map +1 -0
- package/src/lib/model/overlays.d.ts +113 -0
- package/src/lib/model/overlays.js +93 -0
- package/src/lib/model/overlays.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +100 -0
- package/src/lib/model/site-contract.js +23 -0
- package/src/lib/model/site-contract.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +168 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/preview-props.d.ts +31 -0
- package/src/lib/preview-props.js +54 -0
- package/src/lib/preview-props.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +62 -0
- package/src/lib/server/campaign-manage.js +829 -0
- package/src/lib/server/campaign-manage.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
- package/src/lib/server/campaign-process-scheduled.js +198 -0
- package/src/lib/server/campaign-process-scheduled.js.map +1 -0
- package/src/lib/server/campaign-recipients.d.ts +55 -0
- package/src/lib/server/campaign-recipients.js +219 -0
- package/src/lib/server/campaign-recipients.js.map +1 -0
- package/src/lib/server/campaign-send.d.ts +487 -0
- package/src/lib/server/campaign-send.js +3157 -0
- package/src/lib/server/campaign-send.js.map +1 -0
- package/src/lib/server/email-campaign-reach.d.ts +174 -0
- package/src/lib/server/email-campaign-reach.js +254 -0
- package/src/lib/server/email-campaign-reach.js.map +1 -0
- package/src/lib/server/email-events.d.ts +27 -0
- package/src/lib/server/email-events.js +714 -0
- package/src/lib/server/email-events.js.map +1 -0
- package/src/lib/server/get-client-automations.d.ts +60 -0
- package/src/lib/server/get-client-automations.js +87 -0
- package/src/lib/server/get-client-automations.js.map +1 -0
- package/src/lib/server/get-overlays.d.ts +28 -0
- package/src/lib/server/get-overlays.js +65 -0
- package/src/lib/server/get-overlays.js.map +1 -0
- package/src/lib/server/get-screen-experiments.d.ts +42 -0
- package/src/lib/server/get-screen-experiments.js +82 -0
- package/src/lib/server/get-screen-experiments.js.map +1 -0
- package/src/lib/server/lists-materialize.d.ts +18 -0
- package/src/lib/server/lists-materialize.js +130 -0
- package/src/lib/server/lists-materialize.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +37 -0
- package/src/lib/server/marketing-figures.js +248 -0
- package/src/lib/server/marketing-figures.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +26 -0
- package/src/lib/server/site-page-enricher.js +210 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server.d.ts +20 -0
- package/src/lib/server.js +152 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +22 -0
- package/src/lib/site.js +48 -0
- package/src/lib/site.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/compile-client-automations.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// Import the entitlement/event predicates from the context-free actions\n// subpath, NOT the `@aglyn/aglyn` barrel: this file is in the marketing model,\n// which the plugin's SERVER graph re-exports, and the full barrel pulls client\n// React contexts (createContext) that break the RSC server build (AGL-830).\nimport {\n hostActionStepsForClient,\n isClientActionStep,\n isClientStepEntitled,\n isSiteEventType,\n type HostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport { overlayMatchesPath } from './overlays'\nimport type { ClientAutomation } from './site-contract'\n\n/** A raw `hosts/{hostId}/actions` doc paired with its id. */\nexport interface RawHostAction {\n id: string\n action: HostAction\n}\n\nexport interface CompileClientAutomationsOptions {\n /** Leading-slash page path for pathPattern targeting. */\n path: string\n /**\n * `actions` entitlement (AGL-577). Basic presentational steps\n * (menu/drawer/show-hide/class/nav/alert) always load; advanced client\n * steps (overlay/showHtml/analytics) and server steps drop when false.\n */\n actionsEntitled: boolean\n /** Business-tier gate for `runJs` steps (dropped when false). */\n allowJs: boolean\n /**\n * Ignore each action's `pathPattern` (AGL-830). The besigner Preview shows\n * a single composed screen and scopes relevance by selector match, so it\n * skips path filtering rather than resolve the composed path.\n */\n matchAllPaths?: boolean\n /**\n * Drop every automation that names a `pathPattern` (AGL-2511), for a\n * surface whose own path is unknown — the designed 404 body, served from a\n * per-host cache that no request path belongs to.\n *\n * The opposite of `matchAllPaths` and deliberately so: Preview knows the\n * page and only lacks its composed address, so it keeps path-scoped\n * automations; this caller cannot know the page at all, and running an\n * automation scoped to `/pricing` on whatever URL 404'd would be a guess\n * with a side effect. Interactions authored on nodes carry no pattern, so\n * a nav's hover choreography — the reason this surface is enriched — is\n * unaffected.\n */\n dropPathScoped?: boolean\n}\n\n/**\n * Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):\n * the single place the trigger config + entitlement-trimmed client steps are\n * shaped. Both the server page-enricher (admin fetch) and the editor Preview\n * (client fetch) feed their loaded action docs through this, so live and\n * preview stay byte-for-byte consistent. No I/O, no firebase — safe on the\n * client. Malformed entries are skipped, never thrown.\n */\nexport function compileClientAutomations(\n actions: RawHostAction[],\n options: CompileClientAutomationsOptions,\n): ClientAutomation[] {\n const automations: ClientAutomation[] = []\n for (const { id, action } of actions) {\n if (\n (action as { deletedAt?: unknown }).deletedAt ||\n action.enabled === false\n ) {\n continue\n }\n const event = String(action.trigger?.event ?? '')\n if (!isSiteEventType(event)) continue\n const pathPattern = action.trigger?.pathPattern?.trim()\n if (pathPattern && options.dropPathScoped) continue\n if (\n !options.matchAllPaths &&\n pathPattern &&\n !overlayMatchesPath({ pathPatterns: [pathPattern] }, options.path)\n ) {\n continue\n }\n /*\n * TRUNCATED AT THE FIRST WAIT, before anything else is decided.\n *\n * The browser runs its slice of the list the moment the trigger fires, so\n * a client step sitting after a `wait` would execute at once — the popup\n * an author scheduled for three days later would appear immediately, the\n * delay would look correct on the server, and the two halves of one\n * authored flow would disagree about when they happened. Everything past\n * the first wait belongs to a run the job beat has not started yet.\n */\n const reachableSteps = hostActionStepsForClient(action.steps)\n // Step-level entitlement tiering (AGL-577): basic presentational steps\n // load on every plan; advanced client steps need `actions` and runJs\n // needs `webhooks` — see isClientStepEntitled.\n const clientSteps = reachableSteps.filter((step) =>\n isClientStepEntitled(step, {\n actionsEntitled: options.actionsEntitled,\n allowJs: options.allowJs,\n }),\n )\n // Server steps only dispatch (and are only re-authorized) when the org is\n // actions-entitled — never advertise them otherwise, so basic-tier pages\n // don't POST to /api/events/dispatch for a no-op.\n const hasServerSteps =\n options.actionsEntitled &&\n (action.steps ?? []).some((step) => !isClientActionStep(step))\n if (!clientSteps.length && !hasServerSteps) continue\n automations.push({\n id,\n event,\n ...(action.trigger?.selector\n ? { selector: action.trigger.selector }\n : {}),\n ...(Number(action.trigger?.threshold) > 0\n ? { threshold: Number(action.trigger.threshold) }\n : {}),\n ...(action.trigger?.oncePerVisitor === true\n ? { oncePerVisitor: true }\n : {}),\n ...(action.trigger?.oncePerSession === true\n ? { oncePerSession: true }\n : {}),\n ...(Number(action.trigger?.cooldownMinutes) >= 1\n ? { cooldownMinutes: Number(action.trigger?.cooldownMinutes) }\n : {}),\n // Repeatable UI choreography (AGL-562): menu/drawer interactions fire on\n // every occurrence instead of once per pageview.\n ...(action.trigger?.everyTime === true ? { everyTime: true } : {}),\n steps: clientSteps,\n hasServerSteps,\n })\n }\n return automations\n}\n\nexport default compileClientAutomations\n"],"names":["hostActionStepsForClient","isClientActionStep","isClientStepEntitled","isSiteEventType","overlayMatchesPath","compileClientAutomations","actions","options","automations","id","action","deletedAt","enabled","event","String","trigger","pathPattern","trim","dropPathScoped","matchAllPaths","pathPatterns","path","reachableSteps","steps","clientSteps","filter","step","actionsEntitled","allowJs","hasServerSteps","some","length","push","selector","Number","threshold","oncePerVisitor","oncePerSession","cooldownMinutes","everyTime"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,wEAAwE;AACxE,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,SACEA,wBAAwB,EACxBC,kBAAkB,EAClBC,oBAAoB,EACpBC,eAAe,QAEV,iCAAgC;AACvC,SAASC,kBAAkB,QAAQ,gBAAY;AA0C/C;;;;;;;CAOC,GACD,OAAO,SAASC,yBACdC,OAAwB,EACxBC,OAAwC;IAExC,MAAMC,cAAkC,EAAE;IAC1C,KAAK,MAAM,EAAEC,EAAE,EAAEC,MAAM,EAAE,IAAIJ,QAAS;kBA2CjCI;YApCkBA,iBAEDA,6BAAAA,kBAuCdA,kBAGOA,kBAGPA,kBAGAA,kBAGOA,kBACmBA,kBAI1BA;QAhEN,IACE,AAACA,OAAmCC,SAAS,IAC7CD,OAAOE,OAAO,KAAK,OACnB;YACA;QACF;QACA,MAAMC,QAAQC,gBAAOJ,kBAAAA,OAAOK,OAAO,qBAAdL,gBAAgBG,KAAK,mBAAI;QAC9C,IAAI,CAACV,gBAAgBU,QAAQ;QAC7B,MAAMG,eAAcN,mBAAAA,OAAOK,OAAO,sBAAdL,8BAAAA,iBAAgBM,WAAW,qBAA3BN,4BAA6BO,IAAI;QACrD,IAAID,eAAeT,QAAQW,cAAc,EAAE;QAC3C,IACE,CAACX,QAAQY,aAAa,IACtBH,eACA,CAACZ,mBAAmB;YAAEgB,cAAc;gBAACJ;aAAY;QAAC,GAAGT,QAAQc,IAAI,GACjE;YACA;QACF;QACA;;;;;;;;;KASC,GACD,MAAMC,iBAAiBtB,yBAAyBU,OAAOa,KAAK;QAC5D,uEAAuE;QACvE,qEAAqE;QACrE,+CAA+C;QAC/C,MAAMC,cAAcF,eAAeG,MAAM,CAAC,CAACC,OACzCxB,qBAAqBwB,MAAM;gBACzBC,iBAAiBpB,QAAQoB,eAAe;gBACxCC,SAASrB,QAAQqB,OAAO;YAC1B;QAEF,0EAA0E;QAC1E,yEAAyE;QACzE,kDAAkD;QAClD,MAAMC,iBACJtB,QAAQoB,eAAe,IACvB,EAACjB,gBAAAA,OAAOa,KAAK,YAAZb,gBAAgB,EAAE,EAAEoB,IAAI,CAAC,CAACJ,OAAS,CAACzB,mBAAmByB;QAC1D,IAAI,CAACF,YAAYO,MAAM,IAAI,CAACF,gBAAgB;QAC5CrB,YAAYwB,IAAI,CAAC;YACfvB;YACAI;WACIH,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgBuB,QAAQ,IACxB;YAAEA,UAAUvB,OAAOK,OAAO,CAACkB,QAAQ;QAAC,IACpC,CAAC,GACDC,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgByB,SAAS,IAAI,IACpC;YAAEA,WAAWD,OAAOxB,OAAOK,OAAO,CAACoB,SAAS;QAAE,IAC9C,CAAC,GACDzB,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB0B,cAAc,MAAK,OACnC;YAAEA,gBAAgB;QAAK,IACvB,CAAC,GACD1B,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB2B,cAAc,MAAK,OACnC;YAAEA,gBAAgB;QAAK,IACvB,CAAC,GACDH,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB4B,eAAe,KAAK,IAC3C;YAAEA,iBAAiBJ,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB4B,eAAe;QAAE,IAC3D,CAAC,GAGD5B,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB6B,SAAS,MAAK,OAAO;YAAEA,WAAW;QAAK,IAAI,CAAC;YAChEhB,OAAOC;YACPK;;IAEJ;IACA,OAAOrB;AACT;AAEA,eAAeH,yBAAwB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* A/B experiments (AGL-252): docs at `hosts/{hostId}/experiments/{id}`.
|
|
19
|
+
* Assignment is deterministic — hash(visitorId + experimentId) buckets a
|
|
20
|
+
* visitor into a weighted variant, so the same visitor always sees the
|
|
21
|
+
* same variant with no server state. Exposures/conversions accumulate on
|
|
22
|
+
* per-variant stats counters via the track API. Business tier
|
|
23
|
+
* (`abTesting` flag).
|
|
24
|
+
*/
|
|
25
|
+
export type ExperimentStatus = 'draft' | 'running' | 'paused' | 'done';
|
|
26
|
+
/** What the experiment varies. */
|
|
27
|
+
export type ExperimentTarget = 'screen' | 'section' | 'email';
|
|
28
|
+
export interface ExperimentVariant {
|
|
29
|
+
id: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
/** Relative traffic weight; defaults to 1. */
|
|
32
|
+
weight?: number;
|
|
33
|
+
/** Screen experiments: the screen version this variant serves. */
|
|
34
|
+
versionId?: string;
|
|
35
|
+
/** Email experiments (AGL-255): subject/body overrides. */
|
|
36
|
+
subject?: string;
|
|
37
|
+
body?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface HostExperiment {
|
|
40
|
+
name: string;
|
|
41
|
+
status: ExperimentStatus;
|
|
42
|
+
target: ExperimentTarget;
|
|
43
|
+
/** Screen/section experiments: the screen under test. */
|
|
44
|
+
screenId?: string;
|
|
45
|
+
/** Section experiments: canvas node id the variants swap. */
|
|
46
|
+
nodeId?: string;
|
|
47
|
+
variants: ExperimentVariant[];
|
|
48
|
+
/** Conversion goal: a host/site event, optionally filtered. */
|
|
49
|
+
goal?: {
|
|
50
|
+
event: string;
|
|
51
|
+
filter?: string;
|
|
52
|
+
};
|
|
53
|
+
/** Set when finished; the renderer then serves only the winner. */
|
|
54
|
+
winnerVariantId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Epoch millis (AGL-273): past this a running experiment stops serving
|
|
57
|
+
* variants (visitors get the default) until someone declares a winner.
|
|
58
|
+
*/
|
|
59
|
+
endAtMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Opt-in auto-completion (AGL-273): once every variant has
|
|
62
|
+
* `minExposures` and a challenger clears `confidence` (0..1) against
|
|
63
|
+
* the control, the track pipeline promotes it and finishes the
|
|
64
|
+
* experiment. Control wins when every challenger is confidently worse.
|
|
65
|
+
*/
|
|
66
|
+
autoWinner?: {
|
|
67
|
+
minExposures: number;
|
|
68
|
+
confidence: number;
|
|
69
|
+
};
|
|
70
|
+
/** Set by the pipeline when `autoWinner` decided the experiment. */
|
|
71
|
+
autoCompleted?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare const EXPERIMENT_MAX_VARIANTS = 4;
|
|
74
|
+
/**
|
|
75
|
+
* Deterministic weighted assignment: the same visitor always lands in the
|
|
76
|
+
* same variant of an experiment. Finished experiments return the winner;
|
|
77
|
+
* non-running experiments without a winner return null (serve default).
|
|
78
|
+
*/
|
|
79
|
+
export declare function assignExperimentVariant(experiment: Pick<HostExperiment, 'status' | 'variants' | 'winnerVariantId' | 'endAtMs'> & {
|
|
80
|
+
$id?: string;
|
|
81
|
+
}, experimentId: string, visitorId: string, nowMs?: number): ExperimentVariant | null;
|
|
82
|
+
/** Validates an experiment doc shape; human-readable error or null. */
|
|
83
|
+
export declare function validateExperiment(experiment: HostExperiment): string | null;
|
|
84
|
+
/** Naive conversion-rate summary the results table renders. */
|
|
85
|
+
export declare function summarizeVariantStats(stats: {
|
|
86
|
+
exposures?: number;
|
|
87
|
+
conversions?: number;
|
|
88
|
+
}): {
|
|
89
|
+
exposures: number;
|
|
90
|
+
conversions: number;
|
|
91
|
+
rate: number;
|
|
92
|
+
};
|
|
93
|
+
export interface VariantComparison {
|
|
94
|
+
/** Relative conversion-rate lift vs the control; null when unmeasurable. */
|
|
95
|
+
lift: number | null;
|
|
96
|
+
/**
|
|
97
|
+
* One-sided confidence (0..1) that the challenger genuinely beats the
|
|
98
|
+
* control — a two-proportion z-test through the normal CDF. Null until
|
|
99
|
+
* both variants have exposures and at least one conversion exists.
|
|
100
|
+
*/
|
|
101
|
+
confidence: number | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Compares a challenger variant against the control (AGL-265): lift =
|
|
105
|
+
* relative rate change; confidence = P(challenger > control) from a
|
|
106
|
+
* pooled two-proportion z-test. Small samples return null confidence
|
|
107
|
+
* rather than a misleading number.
|
|
108
|
+
*/
|
|
109
|
+
export declare function compareVariants(control: {
|
|
110
|
+
exposures?: number;
|
|
111
|
+
conversions?: number;
|
|
112
|
+
}, challenger: {
|
|
113
|
+
exposures?: number;
|
|
114
|
+
conversions?: number;
|
|
115
|
+
}): VariantComparison;
|
|
116
|
+
/**
|
|
117
|
+
* Auto-winner evaluation (AGL-273): decides a running experiment from
|
|
118
|
+
* per-variant stats once thresholds are met. The first variant is the
|
|
119
|
+
* control. Undecidable states return null: any arm still under
|
|
120
|
+
* `minExposures`, or no challenger confidently better while some remain
|
|
121
|
+
* in the uncertain middle. Control wins only when every challenger is
|
|
122
|
+
* confidently worse (its win probability is below `1 - confidence`).
|
|
123
|
+
*/
|
|
124
|
+
export declare function evaluateAutoWinner(experiment: Pick<HostExperiment, 'status' | 'variants' | 'autoWinner'>, statsByVariant: Record<string, {
|
|
125
|
+
exposures?: number;
|
|
126
|
+
conversions?: number;
|
|
127
|
+
} | undefined>): {
|
|
128
|
+
variantId: string;
|
|
129
|
+
confidence: number;
|
|
130
|
+
} | null;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A/B experiments (AGL-252): docs at `hosts/{hostId}/experiments/{id}`.
|
|
18
|
+
* Assignment is deterministic — hash(visitorId + experimentId) buckets a
|
|
19
|
+
* visitor into a weighted variant, so the same visitor always sees the
|
|
20
|
+
* same variant with no server state. Exposures/conversions accumulate on
|
|
21
|
+
* per-variant stats counters via the track API. Business tier
|
|
22
|
+
* (`abTesting` flag).
|
|
23
|
+
*/ export const EXPERIMENT_MAX_VARIANTS = 4;
|
|
24
|
+
/** FNV-1a over the visitor+experiment key — stable, dependency-free. */ function bucketHash(value) {
|
|
25
|
+
let hash = 0x811c9dc5;
|
|
26
|
+
for(let index = 0; index < value.length; index += 1){
|
|
27
|
+
hash ^= value.charCodeAt(index);
|
|
28
|
+
hash = Math.imul(hash, 0x01000193);
|
|
29
|
+
}
|
|
30
|
+
return hash >>> 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Deterministic weighted assignment: the same visitor always lands in the
|
|
34
|
+
* same variant of an experiment. Finished experiments return the winner;
|
|
35
|
+
* non-running experiments without a winner return null (serve default).
|
|
36
|
+
*/ export function assignExperimentVariant(experiment, experimentId, visitorId, nowMs = Date.now()) {
|
|
37
|
+
var _experiment_variants;
|
|
38
|
+
const variants = ((_experiment_variants = experiment.variants) != null ? _experiment_variants : []).slice(0, EXPERIMENT_MAX_VARIANTS);
|
|
39
|
+
if (!variants.length) return null;
|
|
40
|
+
if (experiment.winnerVariantId) {
|
|
41
|
+
var _variants_find;
|
|
42
|
+
return (_variants_find = variants.find((variant)=>variant.id === experiment.winnerVariantId)) != null ? _variants_find : null;
|
|
43
|
+
}
|
|
44
|
+
if (experiment.status !== 'running') return null;
|
|
45
|
+
// Past the end date (AGL-273) visitors get the default until a winner
|
|
46
|
+
// is declared — stats stop accruing rather than drifting post-test.
|
|
47
|
+
if (experiment.endAtMs && nowMs > experiment.endAtMs) return null;
|
|
48
|
+
const totalWeight = variants.reduce((sum, variant)=>{
|
|
49
|
+
var _variant_weight;
|
|
50
|
+
return sum + Math.max(0, (_variant_weight = variant.weight) != null ? _variant_weight : 1);
|
|
51
|
+
}, 0);
|
|
52
|
+
if (totalWeight <= 0) return null;
|
|
53
|
+
const bucket = bucketHash(`${visitorId}:${experimentId}`) % totalWeight;
|
|
54
|
+
let cursor = 0;
|
|
55
|
+
for (const variant of variants){
|
|
56
|
+
var _variant_weight;
|
|
57
|
+
cursor += Math.max(0, (_variant_weight = variant.weight) != null ? _variant_weight : 1);
|
|
58
|
+
if (bucket < cursor) return variant;
|
|
59
|
+
}
|
|
60
|
+
return variants[variants.length - 1];
|
|
61
|
+
}
|
|
62
|
+
/** Validates an experiment doc shape; human-readable error or null. */ export function validateExperiment(experiment) {
|
|
63
|
+
var _experiment_variants;
|
|
64
|
+
var _experiment_name, _experiment_nodeId, _experiment_screenId;
|
|
65
|
+
if (!((_experiment_name = experiment.name) == null ? void 0 : _experiment_name.trim())) return 'Name the experiment';
|
|
66
|
+
if (![
|
|
67
|
+
'screen',
|
|
68
|
+
'section',
|
|
69
|
+
'email'
|
|
70
|
+
].includes(experiment.target)) {
|
|
71
|
+
return 'Pick what the experiment tests';
|
|
72
|
+
}
|
|
73
|
+
const variants = (_experiment_variants = experiment.variants) != null ? _experiment_variants : [];
|
|
74
|
+
if (variants.length < 2) return 'Add at least two variants';
|
|
75
|
+
if (variants.length > EXPERIMENT_MAX_VARIANTS) {
|
|
76
|
+
return `Experiments are capped at ${EXPERIMENT_MAX_VARIANTS} variants`;
|
|
77
|
+
}
|
|
78
|
+
const ids = new Set(variants.map((variant)=>variant.id));
|
|
79
|
+
if (ids.size !== variants.length) return 'Variant ids must be unique';
|
|
80
|
+
if (experiment.target === 'section' && !((_experiment_nodeId = experiment.nodeId) == null ? void 0 : _experiment_nodeId.trim())) {
|
|
81
|
+
return 'Section experiments need the canvas element id';
|
|
82
|
+
}
|
|
83
|
+
if ((experiment.target === 'screen' || experiment.target === 'section') && !((_experiment_screenId = experiment.screenId) == null ? void 0 : _experiment_screenId.trim())) {
|
|
84
|
+
return 'Pick the screen under test';
|
|
85
|
+
}
|
|
86
|
+
if (experiment.autoWinner) {
|
|
87
|
+
const { minExposures, confidence } = experiment.autoWinner;
|
|
88
|
+
if (!Number.isFinite(minExposures) || minExposures < 1) {
|
|
89
|
+
return 'Auto-winner needs a minimum exposure count of at least 1';
|
|
90
|
+
}
|
|
91
|
+
if (!Number.isFinite(confidence) || confidence <= 0.5 || confidence >= 1) {
|
|
92
|
+
return 'Auto-winner confidence must be between 0.5 and 1';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/** Naive conversion-rate summary the results table renders. */ export function summarizeVariantStats(stats) {
|
|
98
|
+
var _stats_exposures, _stats_conversions;
|
|
99
|
+
const exposures = Math.max(0, (_stats_exposures = stats.exposures) != null ? _stats_exposures : 0);
|
|
100
|
+
const conversions = Math.max(0, (_stats_conversions = stats.conversions) != null ? _stats_conversions : 0);
|
|
101
|
+
return {
|
|
102
|
+
exposures,
|
|
103
|
+
conversions,
|
|
104
|
+
rate: exposures > 0 ? conversions / exposures : 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Abramowitz–Stegun erf approximation (max error ~1.5e-7). */ function erf(x) {
|
|
108
|
+
const sign = x < 0 ? -1 : 1;
|
|
109
|
+
const t = 1 / (1 + 0.3275911 * Math.abs(x));
|
|
110
|
+
const y = 1 - ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x);
|
|
111
|
+
return sign * y;
|
|
112
|
+
}
|
|
113
|
+
/** Standard normal CDF. */ function normalCdf(z) {
|
|
114
|
+
return 0.5 * (1 + erf(z / Math.SQRT2));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compares a challenger variant against the control (AGL-265): lift =
|
|
118
|
+
* relative rate change; confidence = P(challenger > control) from a
|
|
119
|
+
* pooled two-proportion z-test. Small samples return null confidence
|
|
120
|
+
* rather than a misleading number.
|
|
121
|
+
*/ export function compareVariants(control, challenger) {
|
|
122
|
+
const a = summarizeVariantStats(control);
|
|
123
|
+
const b = summarizeVariantStats(challenger);
|
|
124
|
+
const lift = a.rate > 0 ? (b.rate - a.rate) / a.rate : null;
|
|
125
|
+
if (a.exposures < 1 || b.exposures < 1 || a.conversions + b.conversions < 1) {
|
|
126
|
+
return {
|
|
127
|
+
lift,
|
|
128
|
+
confidence: null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const pooled = (a.conversions + b.conversions) / (a.exposures + b.exposures);
|
|
132
|
+
const standardError = Math.sqrt(pooled * (1 - pooled) * (1 / a.exposures + 1 / b.exposures));
|
|
133
|
+
if (!Number.isFinite(standardError) || standardError === 0) {
|
|
134
|
+
return {
|
|
135
|
+
lift,
|
|
136
|
+
confidence: null
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const z = (b.rate - a.rate) / standardError;
|
|
140
|
+
return {
|
|
141
|
+
lift,
|
|
142
|
+
confidence: normalCdf(z)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Auto-winner evaluation (AGL-273): decides a running experiment from
|
|
147
|
+
* per-variant stats once thresholds are met. The first variant is the
|
|
148
|
+
* control. Undecidable states return null: any arm still under
|
|
149
|
+
* `minExposures`, or no challenger confidently better while some remain
|
|
150
|
+
* in the uncertain middle. Control wins only when every challenger is
|
|
151
|
+
* confidently worse (its win probability is below `1 - confidence`).
|
|
152
|
+
*/ export function evaluateAutoWinner(experiment, statsByVariant) {
|
|
153
|
+
var _experiment_variants, _statsByVariant_variants__id, _controlStats_exposures;
|
|
154
|
+
const config = experiment.autoWinner;
|
|
155
|
+
if (!config || experiment.status !== 'running') return null;
|
|
156
|
+
const variants = ((_experiment_variants = experiment.variants) != null ? _experiment_variants : []).slice(0, EXPERIMENT_MAX_VARIANTS);
|
|
157
|
+
if (variants.length < 2) return null;
|
|
158
|
+
const controlStats = (_statsByVariant_variants__id = statsByVariant[variants[0].id]) != null ? _statsByVariant_variants__id : {};
|
|
159
|
+
if (((_controlStats_exposures = controlStats.exposures) != null ? _controlStats_exposures : 0) < config.minExposures) return null;
|
|
160
|
+
let best = null;
|
|
161
|
+
let everyLoses = true;
|
|
162
|
+
for (const challenger of variants.slice(1)){
|
|
163
|
+
var _statsByVariant_challenger_id, _challengerStats_exposures;
|
|
164
|
+
const challengerStats = (_statsByVariant_challenger_id = statsByVariant[challenger.id]) != null ? _statsByVariant_challenger_id : {};
|
|
165
|
+
if (((_challengerStats_exposures = challengerStats.exposures) != null ? _challengerStats_exposures : 0) < config.minExposures) return null;
|
|
166
|
+
const { confidence } = compareVariants(controlStats, challengerStats);
|
|
167
|
+
if (confidence === null) return null;
|
|
168
|
+
if (confidence >= config.confidence && (!best || confidence > best.confidence)) {
|
|
169
|
+
best = {
|
|
170
|
+
variantId: challenger.id,
|
|
171
|
+
confidence
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (confidence > 1 - config.confidence) everyLoses = false;
|
|
175
|
+
}
|
|
176
|
+
if (best) return best;
|
|
177
|
+
if (everyLoses) {
|
|
178
|
+
return {
|
|
179
|
+
variantId: variants[0].id,
|
|
180
|
+
confidence: config.confidence
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=experiments.js.map
|
|
@@ -0,0 +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/**\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","evaluateAutoWinner","statsByVariant","controlStats","config","best","everyLoses","challengerStats","variantId"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Marketing domain model (AGL-412): overlays + experiments relocated from core app-utils.
|
|
19
|
+
* Context-free — importable by client components, /server handlers, and
|
|
20
|
+
* other plugins/apps via `@aglyn/plugins-marketing/model`.
|
|
21
|
+
*/
|
|
22
|
+
export * from './campaign-membership-figures';
|
|
23
|
+
export * from './overlays';
|
|
24
|
+
export * from './experiments';
|
|
25
|
+
export * from './site-contract';
|
|
26
|
+
export * from './compile-client-automations';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Marketing domain model (AGL-412): overlays + experiments relocated from core app-utils.
|
|
18
|
+
* Context-free — importable by client components, /server handlers, and
|
|
19
|
+
* other plugins/apps via `@aglyn/plugins-marketing/model`.
|
|
20
|
+
*/ export * from "./campaign-membership-figures.js";
|
|
21
|
+
export * from "./overlays.js";
|
|
22
|
+
export * from "./experiments.js";
|
|
23
|
+
export * from "./site-contract.js";
|
|
24
|
+
export * from "./compile-client-automations.js";
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
* See the License for the specific language governing permissions and
|
|
12
|
+
* limitations under the License.
|
|
13
|
+
*/ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
|
|
14
|
+
import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
15
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
16
|
+
/**
|
|
17
|
+
* Where a campaign is read, published for every other surface.
|
|
18
|
+
*
|
|
19
|
+
* A message's template lists the campaigns its sends belonged to, and a
|
|
20
|
+
* campaign's pages are this plugin's. A surface elsewhere asks the
|
|
21
|
+
* record-route registry for `campaign` rather than spelling this plugin's nav
|
|
22
|
+
* slug, and gets `null` — text instead of a link — where this plugin is not
|
|
23
|
+
* loaded.
|
|
24
|
+
*/ /** The nav slug the shell resolves this plugin's hub by. */ const MARKETING_SLUG = 'marketing';
|
|
25
|
+
/**
|
|
26
|
+
* Called from the console registrar. The owner is named rather than read off
|
|
27
|
+
* the loader's marker, so the route registers the same way under a spec that
|
|
28
|
+
* calls the registrar directly.
|
|
29
|
+
*/ export function registerMarketingRecordRoutes() {
|
|
30
|
+
registerPluginRecordRoute('campaign', {
|
|
31
|
+
// Campaigns belong to one site, so the organization has no address.
|
|
32
|
+
list: (context)=>context.host ? `${hub(context.orgSlug, context.host)}/campaigns` : null,
|
|
33
|
+
record: (context, id)=>context.host ? `${hub(context.orgSlug, context.host)}/campaigns/${id}` : null
|
|
34
|
+
}, {
|
|
35
|
+
pluginId: BUNDLE_ID
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function hub(orgSlug, host) {
|
|
39
|
+
return buildRoute(Route.HOST_PLUGIN, {
|
|
40
|
+
orgSlug,
|
|
41
|
+
host,
|
|
42
|
+
pluginSlug: MARKETING_SLUG
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=marketing-record-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/marketing-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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 */\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { registerPluginRecordRoute } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * Where a campaign is read, published for every other surface.\n *\n * A message's template lists the campaigns its sends belonged to, and a\n * campaign's pages are this plugin's. A surface elsewhere asks the\n * record-route registry for `campaign` rather than spelling this plugin's nav\n * slug, and gets `null` — text instead of a link — where this plugin is not\n * loaded.\n */\n\n/** The nav slug the shell resolves this plugin's hub by. */\nconst MARKETING_SLUG = 'marketing'\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the route registers the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerMarketingRecordRoutes(): void {\n registerPluginRecordRoute(\n 'campaign',\n {\n // Campaigns belong to one site, so the organization has no address.\n list: (context) =>\n context.host\n ? `${hub(context.orgSlug, context.host)}/campaigns`\n : null,\n record: (context, id) =>\n context.host\n ? `${hub(context.orgSlug, context.host)}/campaigns/${id}`\n : null,\n },\n { pluginId: BUNDLE_ID },\n )\n}\n\nfunction hub(orgSlug: string, host: string): string {\n return buildRoute(Route.HOST_PLUGIN, {\n orgSlug,\n host,\n pluginSlug: MARKETING_SLUG,\n })\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","MARKETING_SLUG","registerMarketingRecordRoutes","list","context","host","hub","orgSlug","record","id","pluginId","HOST_PLUGIN","pluginSlug"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GACD,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SAASC,yBAAyB,QAAQ,mDAAkD;AAC5F,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;CAQC,GAED,0DAA0D,GAC1D,MAAMC,iBAAiB;AAEvB;;;;CAIC,GACD,OAAO,SAASC;IACdH,0BACE,YACA;QACE,oEAAoE;QACpEI,MAAM,CAACC,UACLA,QAAQC,IAAI,GACR,GAAGC,IAAIF,QAAQG,OAAO,EAAEH,QAAQC,IAAI,EAAE,UAAU,CAAC,GACjD;QACNG,QAAQ,CAACJ,SAASK,KAChBL,QAAQC,IAAI,GACR,GAAGC,IAAIF,QAAQG,OAAO,EAAEH,QAAQC,IAAI,EAAE,WAAW,EAAEI,IAAI,GACvD;IACR,GACA;QAAEC,UAAUV;IAAU;AAE1B;AAEA,SAASM,IAAIC,OAAe,EAAEF,IAAY;IACxC,OAAOR,WAAWC,MAAMa,WAAW,EAAE;QACnCJ;QACAF;QACAO,YAAYX;IACd;AACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens';
|
|
18
|
+
import { type HostVariable } from '@aglyn/aglyn/app-utils/variables';
|
|
19
|
+
/**
|
|
20
|
+
* The fields of a bar or popup that hold copy: a bar's `text`, a popup's
|
|
21
|
+
* `headline` and `body`. Links, labels and colors are not copy and are never
|
|
22
|
+
* resolved.
|
|
23
|
+
*/
|
|
24
|
+
export declare const OVERLAY_COPY_FIELDS: readonly ["text", "headline", "body"];
|
|
25
|
+
/**
|
|
26
|
+
* Overlay copy as a visitor reads it: an announcement bar's text, and a
|
|
27
|
+
* popup's headline and body.
|
|
28
|
+
*
|
|
29
|
+
* Every path that puts overlay copy in front of a visitor goes through this
|
|
30
|
+
* one function — the bar and popup a page shows on load, and a bar or popup an
|
|
31
|
+
* automation shows later — so the same overlay cannot read one way when it
|
|
32
|
+
* matches the page and another way when an automation opens it.
|
|
33
|
+
*
|
|
34
|
+
* The order is the one a page's own copy is resolved in: site variables
|
|
35
|
+
* (`{{var:id}}`) first, then the site's details (`{{host.*}}`). A variable
|
|
36
|
+
* whose document is gone renders as nothing, and so does a detail the site has
|
|
37
|
+
* not set; neither ever reaches a visitor as the token.
|
|
38
|
+
*
|
|
39
|
+
* Deep imports rather than either `@aglyn/aglyn` barrel: the server enricher
|
|
40
|
+
* and the console editors both import this module, and the client barrel
|
|
41
|
+
* carries React contexts a server graph must not load.
|
|
42
|
+
*
|
|
43
|
+
* @param variables - the site's variables keyed by document id, as
|
|
44
|
+
* `getVariables` returns them.
|
|
45
|
+
* @param host - the site document the copy is published on.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveOverlayCopy(text: string, variables: Record<string, HostVariable>, host: HostTokenSource | null | undefined): string;
|
|
48
|
+
/**
|
|
49
|
+
* The tokens in stored overlay copy that a visitor would read exactly as
|
|
50
|
+
* typed: every token {@link resolveOverlayCopy} leaves in place.
|
|
51
|
+
*
|
|
52
|
+
* A bare `{{name}}` is one — nothing on a published page resolves that form,
|
|
53
|
+
* which is why an editor rewrites a name it recognizes to `{{var:id}}` before
|
|
54
|
+
* saving — and so is any token overlay copy has no source for, such as a
|
|
55
|
+
* function call or an entry field.
|
|
56
|
+
*
|
|
57
|
+
* Each token is put through the published page's own resolution rather than
|
|
58
|
+
* matched against a list of forms, so what an editor warns about cannot drift
|
|
59
|
+
* from what the page does. A variable whose document is gone and a detail the
|
|
60
|
+
* site has not set both render as nothing, and neither is listed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function overlayCopyTokensShownAsTyped(text: string | undefined, variables: Record<string, HostVariable>, host: HostTokenSource | null | undefined): string[];
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { resolveHostTokens } from "@aglyn/aglyn/app-utils/host-tokens";
|
|
17
|
+
import { resolveBindings } from "@aglyn/aglyn/app-utils/variables";
|
|
18
|
+
/**
|
|
19
|
+
* The fields of a bar or popup that hold copy: a bar's `text`, a popup's
|
|
20
|
+
* `headline` and `body`. Links, labels and colors are not copy and are never
|
|
21
|
+
* resolved.
|
|
22
|
+
*/ export const OVERLAY_COPY_FIELDS = [
|
|
23
|
+
'text',
|
|
24
|
+
'headline',
|
|
25
|
+
'body'
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Overlay copy as a visitor reads it: an announcement bar's text, and a
|
|
29
|
+
* popup's headline and body.
|
|
30
|
+
*
|
|
31
|
+
* Every path that puts overlay copy in front of a visitor goes through this
|
|
32
|
+
* one function — the bar and popup a page shows on load, and a bar or popup an
|
|
33
|
+
* automation shows later — so the same overlay cannot read one way when it
|
|
34
|
+
* matches the page and another way when an automation opens it.
|
|
35
|
+
*
|
|
36
|
+
* The order is the one a page's own copy is resolved in: site variables
|
|
37
|
+
* (`{{var:id}}`) first, then the site's details (`{{host.*}}`). A variable
|
|
38
|
+
* whose document is gone renders as nothing, and so does a detail the site has
|
|
39
|
+
* not set; neither ever reaches a visitor as the token.
|
|
40
|
+
*
|
|
41
|
+
* Deep imports rather than either `@aglyn/aglyn` barrel: the server enricher
|
|
42
|
+
* and the console editors both import this module, and the client barrel
|
|
43
|
+
* carries React contexts a server graph must not load.
|
|
44
|
+
*
|
|
45
|
+
* @param variables - the site's variables keyed by document id, as
|
|
46
|
+
* `getVariables` returns them.
|
|
47
|
+
* @param host - the site document the copy is published on.
|
|
48
|
+
*/ export function resolveOverlayCopy(text, variables, host) {
|
|
49
|
+
if (typeof text !== 'string' || !text.includes('{{')) return text;
|
|
50
|
+
return resolveHostTokens(resolveBindings(text, variables), host);
|
|
51
|
+
}
|
|
52
|
+
/** Any `{{…}}` token, whatever it names. */ const ANY_TOKEN_PATTERN = /\{\{[^{}]*\}\}/g;
|
|
53
|
+
/**
|
|
54
|
+
* The tokens in stored overlay copy that a visitor would read exactly as
|
|
55
|
+
* typed: every token {@link resolveOverlayCopy} leaves in place.
|
|
56
|
+
*
|
|
57
|
+
* A bare `{{name}}` is one — nothing on a published page resolves that form,
|
|
58
|
+
* which is why an editor rewrites a name it recognizes to `{{var:id}}` before
|
|
59
|
+
* saving — and so is any token overlay copy has no source for, such as a
|
|
60
|
+
* function call or an entry field.
|
|
61
|
+
*
|
|
62
|
+
* Each token is put through the published page's own resolution rather than
|
|
63
|
+
* matched against a list of forms, so what an editor warns about cannot drift
|
|
64
|
+
* from what the page does. A variable whose document is gone and a detail the
|
|
65
|
+
* site has not set both render as nothing, and neither is listed.
|
|
66
|
+
*/ export function overlayCopyTokensShownAsTyped(text, variables, host) {
|
|
67
|
+
var _text_match;
|
|
68
|
+
if (typeof text !== 'string' || !text.includes('{{')) return [];
|
|
69
|
+
const shown = new Set();
|
|
70
|
+
for (const token of (_text_match = text.match(ANY_TOKEN_PATTERN)) != null ? _text_match : []){
|
|
71
|
+
if (resolveOverlayCopy(token, variables, host) === token) shown.add(token);
|
|
72
|
+
}
|
|
73
|
+
return [
|
|
74
|
+
...shown
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=overlay-copy.js.map
|