@aglyn/aglyn 1.0.0-beta.229 → 1.0.0-beta.230
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/analytics-events.d.ts +18 -0
- package/src/lib/app-utils/analytics-events.js +2 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +31 -7
- package/src/lib/app-utils/docs-help.generated.js +63 -2
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +295 -43
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.js +20 -0
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.js +15 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
- package/src/lib/app-utils/site-journey.d.ts +143 -0
- package/src/lib/app-utils/site-journey.js +282 -0
- package/src/lib/app-utils/site-journey.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +16 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +85 -0
- package/src/lib/plugin-manager/feature-plugins.js +18 -1
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +29 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +192 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js +157 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -0
- package/src/lib/plugin-manager/plugin-events.d.ts +21 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { HostEventDeclaration } from './host-events';
|
|
10
10
|
/** Every host event a plugin declares. */
|
|
11
|
-
export type PluginHostEventType = 'formSubmission' | 'memberSignUp' | 'memberSignIn' | 'memberSignOut' | 'lead' | 'booking' | 'taskCompleted' | 'contactCreated' | 'contactStageChanged' | 'dealStageChanged' | 'dealWon' | 'dealLost';
|
|
11
|
+
export type PluginHostEventType = 'formSubmission' | 'memberSignUp' | 'memberSignIn' | 'memberSignOut' | 'lead' | 'booking' | 'taskCompleted' | 'contactCreated' | 'contactStageChanged' | 'dealStageChanged' | 'dealWon' | 'dealLost' | 'funnelLeft';
|
|
12
12
|
export declare const PLUGIN_HOST_EVENTS: readonly HostEventDeclaration[];
|
|
@@ -172,6 +172,21 @@
|
|
|
172
172
|
"companyId",
|
|
173
173
|
"lostReason"
|
|
174
174
|
]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"pluginId": "funnels",
|
|
178
|
+
"type": "funnelLeft",
|
|
179
|
+
"order": 150,
|
|
180
|
+
"label": "Left a funnel",
|
|
181
|
+
"payloadKeys": [
|
|
182
|
+
"funnelId",
|
|
183
|
+
"funnelName",
|
|
184
|
+
"step",
|
|
185
|
+
"stepLabel",
|
|
186
|
+
"nextStepLabel",
|
|
187
|
+
"afterHours",
|
|
188
|
+
"email"
|
|
189
|
+
]
|
|
175
190
|
}
|
|
176
191
|
];
|
|
177
192
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/plugin-host-events.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The host events plugins declare (AGL-3080): each plugin's `hostEvents`\n * in plugins.config.json, in `order`. Core's `host-events.ts` folds them\n * in beside the platform's own.\n */\n\nimport type { HostEventDeclaration } from './host-events'\n\n/** Every host event a plugin declares. */\nexport type PluginHostEventType =\n | 'formSubmission'\n | 'memberSignUp'\n | 'memberSignIn'\n | 'memberSignOut'\n | 'lead'\n | 'booking'\n | 'taskCompleted'\n | 'contactCreated'\n | 'contactStageChanged'\n | 'dealStageChanged'\n | 'dealWon'\n | 'dealLost'\n\nexport const PLUGIN_HOST_EVENTS: readonly HostEventDeclaration[] = [\n {\n \"pluginId\": \"forms\",\n \"type\": \"formSubmission\",\n \"order\": 10,\n \"label\": \"Form submitted\",\n \"payloadKeys\": [\n \"formId\",\n \"formName\",\n \"path\",\n \"and every submitted field by name\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignUp\",\n \"order\": 30,\n \"label\": \"Member signed up\",\n \"payloadKeys\": [\n \"email\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignIn\",\n \"order\": 40,\n \"label\": \"Member signed in\",\n \"payloadKeys\": [\n \"email\"\n ]\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignOut\",\n \"order\": 50,\n \"label\": \"Member signed out\"\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"lead\",\n \"order\": 60,\n \"label\": \"New lead\",\n \"payloadKeys\": [\n \"leadId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"formId\",\n \"campaignIds\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"bookings\",\n \"type\": \"booking\",\n \"order\": 70,\n \"label\": \"New booking\",\n \"payloadKeys\": [\n \"serviceName\",\n \"email\",\n \"startsAtMs\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"taskCompleted\",\n \"order\": 80,\n \"label\": \"CRM task completed\",\n \"payloadKeys\": [\n \"taskId\",\n \"title\",\n \"kind\",\n \"priority\",\n \"dueAtMs\",\n \"completedAtMs\",\n \"completedByUid\",\n \"assigneeUid\",\n \"createdByUid\",\n \"contactId\",\n \"companyId\",\n \"dealId\",\n \"taskHostId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactCreated\",\n \"order\": 90,\n \"label\": \"Contact created\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"lifecycleStage\",\n \"campaignIds\",\n \"formId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactStageChanged\",\n \"order\": 100,\n \"label\": \"Contact changed stage\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"lifecycleStage\",\n \"previousStage\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealStageChanged\",\n \"order\": 110,\n \"label\": \"Deal moved\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealWon\",\n \"order\": 120,\n \"label\": \"Deal won\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealLost\",\n \"order\": 130,\n \"label\": \"Deal lost\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\",\n \"lostReason\"\n ]\n }\n]\n"],"names":["PLUGIN_HOST_EVENTS"],"mappings":"AAAA;;;;;;;CAOC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/plugin-host-events.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The host events plugins declare (AGL-3080): each plugin's `hostEvents`\n * in plugins.config.json, in `order`. Core's `host-events.ts` folds them\n * in beside the platform's own.\n */\n\nimport type { HostEventDeclaration } from './host-events'\n\n/** Every host event a plugin declares. */\nexport type PluginHostEventType =\n | 'formSubmission'\n | 'memberSignUp'\n | 'memberSignIn'\n | 'memberSignOut'\n | 'lead'\n | 'booking'\n | 'taskCompleted'\n | 'contactCreated'\n | 'contactStageChanged'\n | 'dealStageChanged'\n | 'dealWon'\n | 'dealLost'\n | 'funnelLeft'\n\nexport const PLUGIN_HOST_EVENTS: readonly HostEventDeclaration[] = [\n {\n \"pluginId\": \"forms\",\n \"type\": \"formSubmission\",\n \"order\": 10,\n \"label\": \"Form submitted\",\n \"payloadKeys\": [\n \"formId\",\n \"formName\",\n \"path\",\n \"and every submitted field by name\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignUp\",\n \"order\": 30,\n \"label\": \"Member signed up\",\n \"payloadKeys\": [\n \"email\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignIn\",\n \"order\": 40,\n \"label\": \"Member signed in\",\n \"payloadKeys\": [\n \"email\"\n ]\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignOut\",\n \"order\": 50,\n \"label\": \"Member signed out\"\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"lead\",\n \"order\": 60,\n \"label\": \"New lead\",\n \"payloadKeys\": [\n \"leadId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"formId\",\n \"campaignIds\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"bookings\",\n \"type\": \"booking\",\n \"order\": 70,\n \"label\": \"New booking\",\n \"payloadKeys\": [\n \"serviceName\",\n \"email\",\n \"startsAtMs\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"taskCompleted\",\n \"order\": 80,\n \"label\": \"CRM task completed\",\n \"payloadKeys\": [\n \"taskId\",\n \"title\",\n \"kind\",\n \"priority\",\n \"dueAtMs\",\n \"completedAtMs\",\n \"completedByUid\",\n \"assigneeUid\",\n \"createdByUid\",\n \"contactId\",\n \"companyId\",\n \"dealId\",\n \"taskHostId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactCreated\",\n \"order\": 90,\n \"label\": \"Contact created\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"lifecycleStage\",\n \"campaignIds\",\n \"formId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactStageChanged\",\n \"order\": 100,\n \"label\": \"Contact changed stage\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"lifecycleStage\",\n \"previousStage\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealStageChanged\",\n \"order\": 110,\n \"label\": \"Deal moved\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealWon\",\n \"order\": 120,\n \"label\": \"Deal won\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealLost\",\n \"order\": 130,\n \"label\": \"Deal lost\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\",\n \"lostReason\"\n ]\n },\n {\n \"pluginId\": \"funnels\",\n \"type\": \"funnelLeft\",\n \"order\": 150,\n \"label\": \"Left a funnel\",\n \"payloadKeys\": [\n \"funnelId\",\n \"funnelName\",\n \"step\",\n \"stepLabel\",\n \"nextStepLabel\",\n \"afterHours\",\n \"email\"\n ]\n }\n]\n"],"names":["PLUGIN_HOST_EVENTS"],"mappings":"AAAA;;;;;;;CAOC,GAoBD,OAAO,MAAMA,qBAAsD;IACjE;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;IACX;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;CACD,CAAA"}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 VISIT'S STEPS, IN ORDER (AGL-3605): the one per-visit record the site
|
|
19
|
+
* collector keeps, so a funnel can ask "of the visits that did A, how many
|
|
20
|
+
* then did B".
|
|
21
|
+
*
|
|
22
|
+
* Every other first-party counter is a daily aggregate with no identifier at
|
|
23
|
+
* all (`visit-claim.ts` says why), and an aggregate cannot be ordered: a day
|
|
24
|
+
* with 100 pricing views and 10 sign-ups says nothing about whether the
|
|
25
|
+
* sign-ups viewed pricing. Ordering needs something that ties two steps to one
|
|
26
|
+
* visit, and this module is that and nothing more.
|
|
27
|
+
*
|
|
28
|
+
* ## What identifies a visit
|
|
29
|
+
*
|
|
30
|
+
* A random id held in `sessionStorage`: one browser tab, until it closes. Not
|
|
31
|
+
* a cookie, not `localStorage`, never derived from the address or the
|
|
32
|
+
* browser, never shared between sites (the stored id names the site it was
|
|
33
|
+
* minted for, and another site's page mints its own). Two tabs are two
|
|
34
|
+
* visits; closing the tab and coming back tomorrow is a new visit. Nothing
|
|
35
|
+
* here links a visit to a person: only a door that already verified who the
|
|
36
|
+
* visitor is — a form they submitted — may hand the id to the server beside
|
|
37
|
+
* the address they typed ({@link siteJourneyField}), so a drop-off follow-up
|
|
38
|
+
* reaches a person who identified themselves and never an anonymous visit.
|
|
39
|
+
*
|
|
40
|
+
* ## Consent
|
|
41
|
+
*
|
|
42
|
+
* The id leaves the browser, so it is `analytics` storage and waits for the
|
|
43
|
+
* same grant the site's analytics tag waits for. The page tells this module
|
|
44
|
+
* the verdict on every render ({@link configureSiteJourney}): until it grants
|
|
45
|
+
* — and for a visitor who declines, opts out or sends GPC — nothing is minted
|
|
46
|
+
* and every step is DROPPED, never queued, for the reason
|
|
47
|
+
* `analytics-events.ts` gives. The one step recorded at the moment of a grant
|
|
48
|
+
* is the page the visitor is on right then, which is a step they are taking
|
|
49
|
+
* now, not a replay. Withdrawing the grant deletes the stored id, so a later
|
|
50
|
+
* grant starts a new visit rather than continuing the old one.
|
|
51
|
+
*
|
|
52
|
+
* ## When it runs at all
|
|
53
|
+
*
|
|
54
|
+
* Only on a site that records journeys (`host.funnelRecording === true`,
|
|
55
|
+
* which the plugin that reads them stamps when a site has something to read
|
|
56
|
+
* them for), only on a production surface without the internal-traffic
|
|
57
|
+
* opt-in (the beacon's own gate), and at most {@link SITE_JOURNEY_MAX_STEPS}
|
|
58
|
+
* steps per visit.
|
|
59
|
+
*
|
|
60
|
+
* Kept free of the `@aglyn/aglyn` barrel and of any plugin: the published
|
|
61
|
+
* page's measurement mount imports it (the AGL-1550 independence rule), and
|
|
62
|
+
* plugins report their steps here without knowing who counts them.
|
|
63
|
+
*/
|
|
64
|
+
/** The body field that marks a collector beacon as a journey step. */
|
|
65
|
+
export declare const SITE_JOURNEY_BEACON_FIELD = "journey";
|
|
66
|
+
/**
|
|
67
|
+
* What a step can be — only what the platform already sees happen.
|
|
68
|
+
*
|
|
69
|
+
* - `page`: a page was viewed; key = the path.
|
|
70
|
+
* - `form`: a form was submitted successfully; key = the form id.
|
|
71
|
+
* - `booking`: a booking was made (a free one confirmed, or a paid one's
|
|
72
|
+
* payment settled); key = the service id.
|
|
73
|
+
* - `cart`: a product was added to the cart; key = the product id.
|
|
74
|
+
* - `order`: a storefront order was placed and paid; no key.
|
|
75
|
+
* - `overlay`: an announcement bar or popup was clicked; key = the overlay id.
|
|
76
|
+
* - `event`: a custom event an interaction fired; key = the event name.
|
|
77
|
+
* - `email`: a person the visit identified opened or clicked an email the
|
|
78
|
+
* site sent them; key = `opened` or `clicked`. Recorded by the SERVER from
|
|
79
|
+
* the delivery log, never by a page: a browser that sends one is refused.
|
|
80
|
+
*/
|
|
81
|
+
export declare const SITE_JOURNEY_STEP_TYPES: readonly ["page", "form", "booking", "cart", "order", "overlay", "event", "email"];
|
|
82
|
+
export type SiteJourneyStepType = (typeof SITE_JOURNEY_STEP_TYPES)[number];
|
|
83
|
+
export declare function isSiteJourneyStepType(value: unknown): value is SiteJourneyStepType;
|
|
84
|
+
/** The step types only the server records; a page's beacon naming one is refused. */
|
|
85
|
+
export declare const SITE_JOURNEY_SERVER_STEP_TYPES: ReadonlySet<SiteJourneyStepType>;
|
|
86
|
+
/** The keys an `email` step carries. */
|
|
87
|
+
export declare const SITE_JOURNEY_EMAIL_KEYS: readonly ["opened", "clicked"];
|
|
88
|
+
/**
|
|
89
|
+
* The body field an identifying door (a form submission) carries the visit
|
|
90
|
+
* id under, so the server can tie the visit to the person the door verified.
|
|
91
|
+
* See {@link siteJourneyField}.
|
|
92
|
+
*/
|
|
93
|
+
export declare const SITE_JOURNEY_SUBMISSION_FIELD = "journey";
|
|
94
|
+
/** The most steps one visit records. A visit past it records nothing more. */
|
|
95
|
+
export declare const SITE_JOURNEY_MAX_STEPS = 60;
|
|
96
|
+
/** The longest key a step carries; longer keys are cut. */
|
|
97
|
+
export declare const SITE_JOURNEY_KEY_MAX = 200;
|
|
98
|
+
/** A visit id's shape: 22 URL-safe characters (128 random bits). */
|
|
99
|
+
export declare const SITE_JOURNEY_ID_PATTERN: RegExp;
|
|
100
|
+
/** Whether a host document asks for journeys to be recorded. */
|
|
101
|
+
export declare function hostRecordsJourneys(host: {
|
|
102
|
+
funnelRecording?: unknown;
|
|
103
|
+
} | null | undefined): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Tells the recorder what the page knows: the site, and whether this visitor
|
|
106
|
+
* may be recorded right now (the site records journeys AND the visitor's
|
|
107
|
+
* analytics consent grants). Called on every render of the page's
|
|
108
|
+
* measurement mount; idempotent.
|
|
109
|
+
*
|
|
110
|
+
* `enabled: null` means "not known yet" — consent still resolving, which a
|
|
111
|
+
* page that remounts goes through on every navigation. Nothing is recorded
|
|
112
|
+
* while it is unknown, and nothing is forgotten either: only a settled `false`
|
|
113
|
+
* (a refusal, or the site no longer recording) deletes the stored visit.
|
|
114
|
+
*
|
|
115
|
+
* `path` is the page the visitor is on. It is recorded as a step when
|
|
116
|
+
* recording turns on and whenever it changes while recording is on — so the
|
|
117
|
+
* page steps are driven from here, once per page the visitor moves to.
|
|
118
|
+
*/
|
|
119
|
+
export declare function configureSiteJourney(options: {
|
|
120
|
+
hostId: string | null | undefined;
|
|
121
|
+
enabled: boolean | null;
|
|
122
|
+
path?: string | null;
|
|
123
|
+
}): void;
|
|
124
|
+
/**
|
|
125
|
+
* Records one step of this visit, or drops it: when the page has not enabled
|
|
126
|
+
* recording, when the visit is full, or when the browser refuses storage.
|
|
127
|
+
* Never throws, never queues, never delays the caller.
|
|
128
|
+
*/
|
|
129
|
+
export declare function recordSiteJourneyStep(type: SiteJourneyStepType, key?: string | null): void;
|
|
130
|
+
/**
|
|
131
|
+
* The visit id for a door that identifies the visitor — a form submission —
|
|
132
|
+
* as `{ journey: id }`, or nothing: when the page has not enabled recording
|
|
133
|
+
* (no funnel on the site, or no analytics consent), or no visit has started.
|
|
134
|
+
*
|
|
135
|
+
* The id is what lets a funnel's drop-off follow-up reach a PERSON (AGL-3605):
|
|
136
|
+
* the door verifies who submitted, and the server ties this visit to that
|
|
137
|
+
* address. Nothing is minted here; a visit with no recorded step has no id.
|
|
138
|
+
*/
|
|
139
|
+
export declare function siteJourneyField(): {
|
|
140
|
+
journey?: string;
|
|
141
|
+
};
|
|
142
|
+
/** Test seam: forget the page's configuration. */
|
|
143
|
+
export declare function resetSiteJourneyForTests(): void;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { sendAnalyticsBeacon } from "./analytics-beacon.js";
|
|
18
|
+
/**
|
|
19
|
+
* A VISIT'S STEPS, IN ORDER (AGL-3605): the one per-visit record the site
|
|
20
|
+
* collector keeps, so a funnel can ask "of the visits that did A, how many
|
|
21
|
+
* then did B".
|
|
22
|
+
*
|
|
23
|
+
* Every other first-party counter is a daily aggregate with no identifier at
|
|
24
|
+
* all (`visit-claim.ts` says why), and an aggregate cannot be ordered: a day
|
|
25
|
+
* with 100 pricing views and 10 sign-ups says nothing about whether the
|
|
26
|
+
* sign-ups viewed pricing. Ordering needs something that ties two steps to one
|
|
27
|
+
* visit, and this module is that and nothing more.
|
|
28
|
+
*
|
|
29
|
+
* ## What identifies a visit
|
|
30
|
+
*
|
|
31
|
+
* A random id held in `sessionStorage`: one browser tab, until it closes. Not
|
|
32
|
+
* a cookie, not `localStorage`, never derived from the address or the
|
|
33
|
+
* browser, never shared between sites (the stored id names the site it was
|
|
34
|
+
* minted for, and another site's page mints its own). Two tabs are two
|
|
35
|
+
* visits; closing the tab and coming back tomorrow is a new visit. Nothing
|
|
36
|
+
* here links a visit to a person: only a door that already verified who the
|
|
37
|
+
* visitor is — a form they submitted — may hand the id to the server beside
|
|
38
|
+
* the address they typed ({@link siteJourneyField}), so a drop-off follow-up
|
|
39
|
+
* reaches a person who identified themselves and never an anonymous visit.
|
|
40
|
+
*
|
|
41
|
+
* ## Consent
|
|
42
|
+
*
|
|
43
|
+
* The id leaves the browser, so it is `analytics` storage and waits for the
|
|
44
|
+
* same grant the site's analytics tag waits for. The page tells this module
|
|
45
|
+
* the verdict on every render ({@link configureSiteJourney}): until it grants
|
|
46
|
+
* — and for a visitor who declines, opts out or sends GPC — nothing is minted
|
|
47
|
+
* and every step is DROPPED, never queued, for the reason
|
|
48
|
+
* `analytics-events.ts` gives. The one step recorded at the moment of a grant
|
|
49
|
+
* is the page the visitor is on right then, which is a step they are taking
|
|
50
|
+
* now, not a replay. Withdrawing the grant deletes the stored id, so a later
|
|
51
|
+
* grant starts a new visit rather than continuing the old one.
|
|
52
|
+
*
|
|
53
|
+
* ## When it runs at all
|
|
54
|
+
*
|
|
55
|
+
* Only on a site that records journeys (`host.funnelRecording === true`,
|
|
56
|
+
* which the plugin that reads them stamps when a site has something to read
|
|
57
|
+
* them for), only on a production surface without the internal-traffic
|
|
58
|
+
* opt-in (the beacon's own gate), and at most {@link SITE_JOURNEY_MAX_STEPS}
|
|
59
|
+
* steps per visit.
|
|
60
|
+
*
|
|
61
|
+
* Kept free of the `@aglyn/aglyn` barrel and of any plugin: the published
|
|
62
|
+
* page's measurement mount imports it (the AGL-1550 independence rule), and
|
|
63
|
+
* plugins report their steps here without knowing who counts them.
|
|
64
|
+
*/ /** The body field that marks a collector beacon as a journey step. */ export const SITE_JOURNEY_BEACON_FIELD = 'journey';
|
|
65
|
+
/**
|
|
66
|
+
* What a step can be — only what the platform already sees happen.
|
|
67
|
+
*
|
|
68
|
+
* - `page`: a page was viewed; key = the path.
|
|
69
|
+
* - `form`: a form was submitted successfully; key = the form id.
|
|
70
|
+
* - `booking`: a booking was made (a free one confirmed, or a paid one's
|
|
71
|
+
* payment settled); key = the service id.
|
|
72
|
+
* - `cart`: a product was added to the cart; key = the product id.
|
|
73
|
+
* - `order`: a storefront order was placed and paid; no key.
|
|
74
|
+
* - `overlay`: an announcement bar or popup was clicked; key = the overlay id.
|
|
75
|
+
* - `event`: a custom event an interaction fired; key = the event name.
|
|
76
|
+
* - `email`: a person the visit identified opened or clicked an email the
|
|
77
|
+
* site sent them; key = `opened` or `clicked`. Recorded by the SERVER from
|
|
78
|
+
* the delivery log, never by a page: a browser that sends one is refused.
|
|
79
|
+
*/ export const SITE_JOURNEY_STEP_TYPES = [
|
|
80
|
+
'page',
|
|
81
|
+
'form',
|
|
82
|
+
'booking',
|
|
83
|
+
'cart',
|
|
84
|
+
'order',
|
|
85
|
+
'overlay',
|
|
86
|
+
'event',
|
|
87
|
+
'email'
|
|
88
|
+
];
|
|
89
|
+
export function isSiteJourneyStepType(value) {
|
|
90
|
+
return typeof value === 'string' && SITE_JOURNEY_STEP_TYPES.includes(value);
|
|
91
|
+
}
|
|
92
|
+
/** The step types only the server records; a page's beacon naming one is refused. */ export const SITE_JOURNEY_SERVER_STEP_TYPES = new Set([
|
|
93
|
+
'email'
|
|
94
|
+
]);
|
|
95
|
+
/** The keys an `email` step carries. */ export const SITE_JOURNEY_EMAIL_KEYS = [
|
|
96
|
+
'opened',
|
|
97
|
+
'clicked'
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* The body field an identifying door (a form submission) carries the visit
|
|
101
|
+
* id under, so the server can tie the visit to the person the door verified.
|
|
102
|
+
* See {@link siteJourneyField}.
|
|
103
|
+
*/ export const SITE_JOURNEY_SUBMISSION_FIELD = 'journey';
|
|
104
|
+
/** The most steps one visit records. A visit past it records nothing more. */ export const SITE_JOURNEY_MAX_STEPS = 60;
|
|
105
|
+
/** The longest key a step carries; longer keys are cut. */ export const SITE_JOURNEY_KEY_MAX = 200;
|
|
106
|
+
/** A visit id's shape: 22 URL-safe characters (128 random bits). */ export const SITE_JOURNEY_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
107
|
+
const STORAGE_KEY = 'aglyn-journey';
|
|
108
|
+
/** Whether a host document asks for journeys to be recorded. */ export function hostRecordsJourneys(host) {
|
|
109
|
+
return (host == null ? void 0 : host.funnelRecording) === true;
|
|
110
|
+
}
|
|
111
|
+
const state = {
|
|
112
|
+
hostId: null,
|
|
113
|
+
enabled: false,
|
|
114
|
+
path: null
|
|
115
|
+
};
|
|
116
|
+
function readStored() {
|
|
117
|
+
try {
|
|
118
|
+
const raw = window.sessionStorage.getItem(STORAGE_KEY);
|
|
119
|
+
if (!raw) return null;
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
if (!parsed || !SITE_JOURNEY_ID_PATTERN.test(String(parsed.id))) return null;
|
|
122
|
+
return parsed;
|
|
123
|
+
} catch (unused) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function writeStored(journey) {
|
|
128
|
+
try {
|
|
129
|
+
window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(journey));
|
|
130
|
+
return true;
|
|
131
|
+
} catch (unused) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function forgetStored() {
|
|
136
|
+
try {
|
|
137
|
+
window.sessionStorage.removeItem(STORAGE_KEY);
|
|
138
|
+
} catch (unused) {
|
|
139
|
+
// Nothing stored, or storage refused: either way nothing is held.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 128 random bits as 22 URL-safe characters, or null without a CSPRNG. */ function mintJourneyId() {
|
|
143
|
+
try {
|
|
144
|
+
const bytes = new Uint8Array(16);
|
|
145
|
+
window.crypto.getRandomValues(bytes);
|
|
146
|
+
let binary = '';
|
|
147
|
+
for (const byte of bytes)binary += String.fromCharCode(byte);
|
|
148
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
149
|
+
} catch (unused) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Where this visit came from, sent once with its first step. */ function arrivalSource() {
|
|
154
|
+
const source = {};
|
|
155
|
+
try {
|
|
156
|
+
const params = new URLSearchParams(window.location.search);
|
|
157
|
+
for (const [param, field] of [
|
|
158
|
+
[
|
|
159
|
+
'utm_source',
|
|
160
|
+
'utmSource'
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
'utm_medium',
|
|
164
|
+
'utmMedium'
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
'utm_campaign',
|
|
168
|
+
'utmCampaign'
|
|
169
|
+
]
|
|
170
|
+
]){
|
|
171
|
+
const value = params.get(param);
|
|
172
|
+
if (value) source[field] = value.slice(0, 100);
|
|
173
|
+
}
|
|
174
|
+
const referrer = document.referrer ? new URL(document.referrer) : null;
|
|
175
|
+
if (referrer && referrer.host && referrer.host !== window.location.host) {
|
|
176
|
+
source['referrerHost'] = referrer.host.slice(0, 100);
|
|
177
|
+
}
|
|
178
|
+
} catch (unused) {
|
|
179
|
+
// An unreadable referrer is a direct visit.
|
|
180
|
+
}
|
|
181
|
+
return source;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Tells the recorder what the page knows: the site, and whether this visitor
|
|
185
|
+
* may be recorded right now (the site records journeys AND the visitor's
|
|
186
|
+
* analytics consent grants). Called on every render of the page's
|
|
187
|
+
* measurement mount; idempotent.
|
|
188
|
+
*
|
|
189
|
+
* `enabled: null` means "not known yet" — consent still resolving, which a
|
|
190
|
+
* page that remounts goes through on every navigation. Nothing is recorded
|
|
191
|
+
* while it is unknown, and nothing is forgotten either: only a settled `false`
|
|
192
|
+
* (a refusal, or the site no longer recording) deletes the stored visit.
|
|
193
|
+
*
|
|
194
|
+
* `path` is the page the visitor is on. It is recorded as a step when
|
|
195
|
+
* recording turns on and whenever it changes while recording is on — so the
|
|
196
|
+
* page steps are driven from here, once per page the visitor moves to.
|
|
197
|
+
*/ export function configureSiteJourney(options) {
|
|
198
|
+
if (typeof window === 'undefined') return;
|
|
199
|
+
const hostId = options.hostId || null;
|
|
200
|
+
const enabled = Boolean(hostId) && options.enabled === true;
|
|
201
|
+
const turnedOn = enabled && (!state.enabled || state.hostId !== hostId);
|
|
202
|
+
state.hostId = hostId;
|
|
203
|
+
state.enabled = enabled;
|
|
204
|
+
if (options.enabled === false) forgetStored();
|
|
205
|
+
const path = options.path || null;
|
|
206
|
+
if (enabled && path && (turnedOn || path !== state.path)) {
|
|
207
|
+
state.path = path;
|
|
208
|
+
recordSiteJourneyStep('page', path);
|
|
209
|
+
}
|
|
210
|
+
if (!enabled) state.path = null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Records one step of this visit, or drops it: when the page has not enabled
|
|
214
|
+
* recording, when the visit is full, or when the browser refuses storage.
|
|
215
|
+
* Never throws, never queues, never delays the caller.
|
|
216
|
+
*/ export function recordSiteJourneyStep(type, key) {
|
|
217
|
+
try {
|
|
218
|
+
if (typeof window === 'undefined' || !state.enabled || !state.hostId) return;
|
|
219
|
+
if (!isSiteJourneyStepType(type) || SITE_JOURNEY_SERVER_STEP_TYPES.has(type)) return;
|
|
220
|
+
const hostId = state.hostId;
|
|
221
|
+
const cleanKey = String(key != null ? key : '').trim().slice(0, SITE_JOURNEY_KEY_MAX);
|
|
222
|
+
if (type === 'page' && !cleanKey.startsWith('/')) return;
|
|
223
|
+
if (type === 'event' && !cleanKey) return;
|
|
224
|
+
const stepId = `${type}\u0000${cleanKey}`;
|
|
225
|
+
let journey = readStored();
|
|
226
|
+
let first = false;
|
|
227
|
+
if (!journey || journey.hostId !== hostId) {
|
|
228
|
+
const id = mintJourneyId();
|
|
229
|
+
if (!id) return;
|
|
230
|
+
journey = {
|
|
231
|
+
id,
|
|
232
|
+
hostId,
|
|
233
|
+
steps: 0
|
|
234
|
+
};
|
|
235
|
+
first = true;
|
|
236
|
+
}
|
|
237
|
+
if (journey.steps >= SITE_JOURNEY_MAX_STEPS) return;
|
|
238
|
+
if (journey.last === stepId) return;
|
|
239
|
+
journey = _extends({}, journey, {
|
|
240
|
+
steps: journey.steps + 1,
|
|
241
|
+
last: stepId
|
|
242
|
+
});
|
|
243
|
+
if (!writeStored(journey)) return;
|
|
244
|
+
sendAnalyticsBeacon(_extends({
|
|
245
|
+
hostId,
|
|
246
|
+
[SITE_JOURNEY_BEACON_FIELD]: journey.id,
|
|
247
|
+
stepType: type
|
|
248
|
+
}, cleanKey ? {
|
|
249
|
+
stepKey: cleanKey
|
|
250
|
+
} : {}, first ? _extends({
|
|
251
|
+
journeyStart: true
|
|
252
|
+
}, arrivalSource()) : {}));
|
|
253
|
+
} catch (unused) {
|
|
254
|
+
// A journey step never breaks the page.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The visit id for a door that identifies the visitor — a form submission —
|
|
259
|
+
* as `{ journey: id }`, or nothing: when the page has not enabled recording
|
|
260
|
+
* (no funnel on the site, or no analytics consent), or no visit has started.
|
|
261
|
+
*
|
|
262
|
+
* The id is what lets a funnel's drop-off follow-up reach a PERSON (AGL-3605):
|
|
263
|
+
* the door verifies who submitted, and the server ties this visit to that
|
|
264
|
+
* address. Nothing is minted here; a visit with no recorded step has no id.
|
|
265
|
+
*/ export function siteJourneyField() {
|
|
266
|
+
try {
|
|
267
|
+
if (typeof window === 'undefined' || !state.enabled || !state.hostId) return {};
|
|
268
|
+
const journey = readStored();
|
|
269
|
+
return journey && journey.hostId === state.hostId ? {
|
|
270
|
+
[SITE_JOURNEY_SUBMISSION_FIELD]: journey.id
|
|
271
|
+
} : {};
|
|
272
|
+
} catch (unused) {
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Test seam: forget the page's configuration. */ export function resetSiteJourneyForTests() {
|
|
277
|
+
state.hostId = null;
|
|
278
|
+
state.enabled = false;
|
|
279
|
+
state.path = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=site-journey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-journey.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 { sendAnalyticsBeacon } from './analytics-beacon'\n\n/**\n * A VISIT'S STEPS, IN ORDER (AGL-3605): the one per-visit record the site\n * collector keeps, so a funnel can ask \"of the visits that did A, how many\n * then did B\".\n *\n * Every other first-party counter is a daily aggregate with no identifier at\n * all (`visit-claim.ts` says why), and an aggregate cannot be ordered: a day\n * with 100 pricing views and 10 sign-ups says nothing about whether the\n * sign-ups viewed pricing. Ordering needs something that ties two steps to one\n * visit, and this module is that and nothing more.\n *\n * ## What identifies a visit\n *\n * A random id held in `sessionStorage`: one browser tab, until it closes. Not\n * a cookie, not `localStorage`, never derived from the address or the\n * browser, never shared between sites (the stored id names the site it was\n * minted for, and another site's page mints its own). Two tabs are two\n * visits; closing the tab and coming back tomorrow is a new visit. Nothing\n * here links a visit to a person: only a door that already verified who the\n * visitor is — a form they submitted — may hand the id to the server beside\n * the address they typed ({@link siteJourneyField}), so a drop-off follow-up\n * reaches a person who identified themselves and never an anonymous visit.\n *\n * ## Consent\n *\n * The id leaves the browser, so it is `analytics` storage and waits for the\n * same grant the site's analytics tag waits for. The page tells this module\n * the verdict on every render ({@link configureSiteJourney}): until it grants\n * — and for a visitor who declines, opts out or sends GPC — nothing is minted\n * and every step is DROPPED, never queued, for the reason\n * `analytics-events.ts` gives. The one step recorded at the moment of a grant\n * is the page the visitor is on right then, which is a step they are taking\n * now, not a replay. Withdrawing the grant deletes the stored id, so a later\n * grant starts a new visit rather than continuing the old one.\n *\n * ## When it runs at all\n *\n * Only on a site that records journeys (`host.funnelRecording === true`,\n * which the plugin that reads them stamps when a site has something to read\n * them for), only on a production surface without the internal-traffic\n * opt-in (the beacon's own gate), and at most {@link SITE_JOURNEY_MAX_STEPS}\n * steps per visit.\n *\n * Kept free of the `@aglyn/aglyn` barrel and of any plugin: the published\n * page's measurement mount imports it (the AGL-1550 independence rule), and\n * plugins report their steps here without knowing who counts them.\n */\n\n/** The body field that marks a collector beacon as a journey step. */\nexport const SITE_JOURNEY_BEACON_FIELD = 'journey'\n\n/**\n * What a step can be — only what the platform already sees happen.\n *\n * - `page`: a page was viewed; key = the path.\n * - `form`: a form was submitted successfully; key = the form id.\n * - `booking`: a booking was made (a free one confirmed, or a paid one's\n * payment settled); key = the service id.\n * - `cart`: a product was added to the cart; key = the product id.\n * - `order`: a storefront order was placed and paid; no key.\n * - `overlay`: an announcement bar or popup was clicked; key = the overlay id.\n * - `event`: a custom event an interaction fired; key = the event name.\n * - `email`: a person the visit identified opened or clicked an email the\n * site sent them; key = `opened` or `clicked`. Recorded by the SERVER from\n * the delivery log, never by a page: a browser that sends one is refused.\n */\nexport const SITE_JOURNEY_STEP_TYPES = [\n 'page',\n 'form',\n 'booking',\n 'cart',\n 'order',\n 'overlay',\n 'event',\n 'email',\n] as const\n\nexport type SiteJourneyStepType = (typeof SITE_JOURNEY_STEP_TYPES)[number]\n\nexport function isSiteJourneyStepType(value: unknown): value is SiteJourneyStepType {\n return (\n typeof value === 'string' &&\n (SITE_JOURNEY_STEP_TYPES as readonly string[]).includes(value)\n )\n}\n\n/** The step types only the server records; a page's beacon naming one is refused. */\nexport const SITE_JOURNEY_SERVER_STEP_TYPES: ReadonlySet<SiteJourneyStepType> = new Set(['email'])\n\n/** The keys an `email` step carries. */\nexport const SITE_JOURNEY_EMAIL_KEYS = ['opened', 'clicked'] as const\n\n/**\n * The body field an identifying door (a form submission) carries the visit\n * id under, so the server can tie the visit to the person the door verified.\n * See {@link siteJourneyField}.\n */\nexport const SITE_JOURNEY_SUBMISSION_FIELD = 'journey'\n\n/** The most steps one visit records. A visit past it records nothing more. */\nexport const SITE_JOURNEY_MAX_STEPS = 60\n\n/** The longest key a step carries; longer keys are cut. */\nexport const SITE_JOURNEY_KEY_MAX = 200\n\n/** A visit id's shape: 22 URL-safe characters (128 random bits). */\nexport const SITE_JOURNEY_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/\n\nconst STORAGE_KEY = 'aglyn-journey'\n\n/** Whether a host document asks for journeys to be recorded. */\nexport function hostRecordsJourneys(\n host: { funnelRecording?: unknown } | null | undefined,\n): boolean {\n return host?.funnelRecording === true\n}\n\ninterface StoredJourney {\n id: string\n hostId: string\n steps: number\n /** The last step sent, so a reload of the same page is one step. */\n last?: string\n}\n\ninterface JourneyState {\n hostId: string | null\n enabled: boolean\n /** The page last recorded, so a re-render records nothing new. */\n path: string | null\n}\n\nconst state: JourneyState = { hostId: null, enabled: false, path: null }\n\nfunction readStored(): StoredJourney | null {\n try {\n const raw = window.sessionStorage.getItem(STORAGE_KEY)\n if (!raw) return null\n const parsed = JSON.parse(raw) as StoredJourney\n if (!parsed || !SITE_JOURNEY_ID_PATTERN.test(String(parsed.id))) return null\n return parsed\n } catch {\n return null\n }\n}\n\nfunction writeStored(journey: StoredJourney): boolean {\n try {\n window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(journey))\n return true\n } catch {\n return false\n }\n}\n\nfunction forgetStored(): void {\n try {\n window.sessionStorage.removeItem(STORAGE_KEY)\n } catch {\n // Nothing stored, or storage refused: either way nothing is held.\n }\n}\n\n/** 128 random bits as 22 URL-safe characters, or null without a CSPRNG. */\nfunction mintJourneyId(): string | null {\n try {\n const bytes = new Uint8Array(16)\n window.crypto.getRandomValues(bytes)\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n } catch {\n return null\n }\n}\n\n/** Where this visit came from, sent once with its first step. */\nfunction arrivalSource(): Record<string, string> {\n const source: Record<string, string> = {}\n try {\n const params = new URLSearchParams(window.location.search)\n for (const [param, field] of [\n ['utm_source', 'utmSource'],\n ['utm_medium', 'utmMedium'],\n ['utm_campaign', 'utmCampaign'],\n ] as const) {\n const value = params.get(param)\n if (value) source[field] = value.slice(0, 100)\n }\n const referrer = document.referrer ? new URL(document.referrer) : null\n if (referrer && referrer.host && referrer.host !== window.location.host) {\n source['referrerHost'] = referrer.host.slice(0, 100)\n }\n } catch {\n // An unreadable referrer is a direct visit.\n }\n return source\n}\n\n/**\n * Tells the recorder what the page knows: the site, and whether this visitor\n * may be recorded right now (the site records journeys AND the visitor's\n * analytics consent grants). Called on every render of the page's\n * measurement mount; idempotent.\n *\n * `enabled: null` means \"not known yet\" — consent still resolving, which a\n * page that remounts goes through on every navigation. Nothing is recorded\n * while it is unknown, and nothing is forgotten either: only a settled `false`\n * (a refusal, or the site no longer recording) deletes the stored visit.\n *\n * `path` is the page the visitor is on. It is recorded as a step when\n * recording turns on and whenever it changes while recording is on — so the\n * page steps are driven from here, once per page the visitor moves to.\n */\nexport function configureSiteJourney(options: {\n hostId: string | null | undefined\n enabled: boolean | null\n path?: string | null\n}): void {\n if (typeof window === 'undefined') return\n const hostId = options.hostId || null\n const enabled = Boolean(hostId) && options.enabled === true\n const turnedOn = enabled && (!state.enabled || state.hostId !== hostId)\n state.hostId = hostId\n state.enabled = enabled\n if (options.enabled === false) forgetStored()\n const path = options.path || null\n if (enabled && path && (turnedOn || path !== state.path)) {\n state.path = path\n recordSiteJourneyStep('page', path)\n }\n if (!enabled) state.path = null\n}\n\n/**\n * Records one step of this visit, or drops it: when the page has not enabled\n * recording, when the visit is full, or when the browser refuses storage.\n * Never throws, never queues, never delays the caller.\n */\nexport function recordSiteJourneyStep(\n type: SiteJourneyStepType,\n key?: string | null,\n): void {\n try {\n if (typeof window === 'undefined' || !state.enabled || !state.hostId) return\n if (!isSiteJourneyStepType(type) || SITE_JOURNEY_SERVER_STEP_TYPES.has(type)) return\n const hostId = state.hostId\n const cleanKey = String(key ?? '').trim().slice(0, SITE_JOURNEY_KEY_MAX)\n if (type === 'page' && !cleanKey.startsWith('/')) return\n if (type === 'event' && !cleanKey) return\n const stepId = `${type}\\u0000${cleanKey}`\n\n let journey = readStored()\n let first = false\n if (!journey || journey.hostId !== hostId) {\n const id = mintJourneyId()\n if (!id) return\n journey = { id, hostId, steps: 0 }\n first = true\n }\n if (journey.steps >= SITE_JOURNEY_MAX_STEPS) return\n if (journey.last === stepId) return\n journey = { ...journey, steps: journey.steps + 1, last: stepId }\n if (!writeStored(journey)) return\n\n sendAnalyticsBeacon({\n hostId,\n [SITE_JOURNEY_BEACON_FIELD]: journey.id,\n stepType: type,\n ...(cleanKey ? { stepKey: cleanKey } : {}),\n ...(first ? { journeyStart: true, ...arrivalSource() } : {}),\n })\n } catch {\n // A journey step never breaks the page.\n }\n}\n\n/**\n * The visit id for a door that identifies the visitor — a form submission —\n * as `{ journey: id }`, or nothing: when the page has not enabled recording\n * (no funnel on the site, or no analytics consent), or no visit has started.\n *\n * The id is what lets a funnel's drop-off follow-up reach a PERSON (AGL-3605):\n * the door verifies who submitted, and the server ties this visit to that\n * address. Nothing is minted here; a visit with no recorded step has no id.\n */\nexport function siteJourneyField(): { journey?: string } {\n try {\n if (typeof window === 'undefined' || !state.enabled || !state.hostId) return {}\n const journey = readStored()\n return journey && journey.hostId === state.hostId ? { [SITE_JOURNEY_SUBMISSION_FIELD]: journey.id } : {}\n } catch {\n return {}\n }\n}\n\n/** Test seam: forget the page's configuration. */\nexport function resetSiteJourneyForTests(): void {\n state.hostId = null\n state.enabled = false\n state.path = null\n}\n"],"names":["sendAnalyticsBeacon","SITE_JOURNEY_BEACON_FIELD","SITE_JOURNEY_STEP_TYPES","isSiteJourneyStepType","value","includes","SITE_JOURNEY_SERVER_STEP_TYPES","Set","SITE_JOURNEY_EMAIL_KEYS","SITE_JOURNEY_SUBMISSION_FIELD","SITE_JOURNEY_MAX_STEPS","SITE_JOURNEY_KEY_MAX","SITE_JOURNEY_ID_PATTERN","STORAGE_KEY","hostRecordsJourneys","host","funnelRecording","state","hostId","enabled","path","readStored","raw","window","sessionStorage","getItem","parsed","JSON","parse","test","String","id","writeStored","journey","setItem","stringify","forgetStored","removeItem","mintJourneyId","bytes","Uint8Array","crypto","getRandomValues","binary","byte","fromCharCode","btoa","replace","arrivalSource","source","params","URLSearchParams","location","search","param","field","get","slice","referrer","document","URL","configureSiteJourney","options","Boolean","turnedOn","recordSiteJourneyStep","type","key","has","cleanKey","trim","startsWith","stepId","first","steps","last","stepType","stepKey","journeyStart","siteJourneyField","resetSiteJourneyForTests"],"mappings":"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"}
|
|
@@ -60,6 +60,14 @@ export interface CoreOrgFeatureFlags {
|
|
|
60
60
|
/** A/B experiments (AGL-252); Business tier. */
|
|
61
61
|
abTesting?: boolean;
|
|
62
62
|
versioning?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Reusable components WITHOUT a count: Starter and above. Since AGL-3615 a
|
|
65
|
+
* component's create is counted against `componentsPerHost` alone — Free
|
|
66
|
+
* saves one — so this flag no longer decides whether a site may make one.
|
|
67
|
+
* It states that the allowance is unlimited, and a per-org override that
|
|
68
|
+
* turns it on lifts `componentsPerHost` with it, which is how an org
|
|
69
|
+
* granted the feature by contract before the count existed keeps it.
|
|
70
|
+
*/
|
|
63
71
|
reusableComponents?: boolean;
|
|
64
72
|
customDomain?: boolean;
|
|
65
73
|
/**
|
|
@@ -472,6 +480,14 @@ export interface CoreOrgEntitlements {
|
|
|
472
480
|
* is why a plan resolving to 0 still accepts submissions.
|
|
473
481
|
*/
|
|
474
482
|
formsPerHost?: number;
|
|
483
|
+
/**
|
|
484
|
+
* Reusable component DEFINITIONS per host — documents under
|
|
485
|
+
* `hosts/{hostId}/components` (AGL-3615). Free 1; every paid plan
|
|
486
|
+
* `UNLIMITED`, the allowance it had when components were a boolean
|
|
487
|
+
* feature. Refused at the create only, so a site holding more than its
|
|
488
|
+
* plan now includes keeps every component.
|
|
489
|
+
*/
|
|
490
|
+
componentsPerHost?: number;
|
|
475
491
|
/** Component-builder caps (AGL-99): host variables. */
|
|
476
492
|
variablesPerHost?: number;
|
|
477
493
|
/** Component-builder caps (AGL-99): host functions. */
|