@aglyn/aglyn 1.0.0-beta.232 → 1.0.0-beta.233
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/admin-audit-index.d.ts +3 -1
- package/src/lib/app-utils/admin-audit-index.js +9 -1
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-summary.d.ts +95 -0
- package/src/lib/app-utils/analytics-summary.js +113 -0
- package/src/lib/app-utils/analytics-summary.js.map +1 -0
- package/src/lib/app-utils/artifact-list-keys.js +12 -0
- package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
- package/src/lib/app-utils/artifact-list-queries.d.ts +16 -0
- package/src/lib/app-utils/artifact-list-queries.js +122 -4
- package/src/lib/app-utils/artifact-list-queries.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +30 -2
- package/src/lib/app-utils/crm.js +77 -15
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +17 -5
- package/src/lib/app-utils/docs-help.generated.js +35 -1
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +139 -6
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/lockdown.js +1 -1
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/message-search.js +5 -2
- package/src/lib/app-utils/message-search.js.map +1 -1
- package/src/lib/app-utils/name-search.js +15 -3
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/app-utils/screen-analytics-aggregate.d.ts +70 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js +82 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -0
- package/src/lib/app-utils/screen-kind.d.ts +25 -0
- package/src/lib/app-utils/screen-kind.js +25 -0
- package/src/lib/app-utils/screen-kind.js.map +1 -0
- package/src/lib/app-utils/screen-route.d.ts +1 -5
- package/src/lib/app-utils/screen-route.js +2 -4
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/site-journey-steps.d.ts +38 -0
- package/src/lib/app-utils/site-journey-steps.js +54 -0
- package/src/lib/app-utils/site-journey-steps.js.map +1 -0
- package/src/lib/app-utils/site-journey.d.ts +2 -22
- package/src/lib/app-utils/site-journey.js +2 -32
- package/src/lib/app-utils/site-journey.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +20 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-checkout-credits.d.ts +288 -0
- package/src/lib/plugin-manager/plugin-checkout-credits.js +196 -0
- package/src/lib/plugin-manager/plugin-checkout-credits.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -1
|
@@ -1 +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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wBAAoB;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CC,GAED,oEAAoE,GACpE,OAAO,MAAMC,4BAA4B,UAAS;AAElD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV,OAAO,SAASC,sBAAsBC,KAAc;IAClD,OACE,OAAOA,UAAU,YACjB,AAACF,wBAA8CG,QAAQ,CAACD;AAE5D;AAEA,mFAAmF,GACnF,OAAO,MAAME,iCAAmE,IAAIC,IAAI;IAAC;CAAQ,EAAC;AAElG,sCAAsC,GACtC,OAAO,MAAMC,0BAA0B;IAAC;IAAU;CAAU,CAAS;AAErE;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,UAAS;AAEtD,4EAA4E,GAC5E,OAAO,MAAMC,yBAAyB,GAAE;AAExC,yDAAyD,GACzD,OAAO,MAAMC,uBAAuB,IAAG;AAEvC,kEAAkE,GAClE,OAAO,MAAMC,0BAA0B,sBAAqB;AAE5D,MAAMC,cAAc;AAEpB,8DAA8D,GAC9D,OAAO,SAASC,oBACdC,IAAsD;IAEtD,OAAOA,CAAAA,wBAAAA,KAAMC,eAAe,MAAK;AACnC;AAiBA,MAAMC,QAAsB;IAAEC,QAAQ;IAAMC,SAAS;IAAOC,MAAM;AAAK;AAEvE,SAASC;IACP,IAAI;QACF,MAAMC,MAAMC,OAAOC,cAAc,CAACC,OAAO,CAACZ;QAC1C,IAAI,CAACS,KAAK,OAAO;QACjB,MAAMI,SAASC,KAAKC,KAAK,CAACN;QAC1B,IAAI,CAACI,UAAU,CAACd,wBAAwBiB,IAAI,CAACC,OAAOJ,OAAOK,EAAE,IAAI,OAAO;QACxE,OAAOL;IACT,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASM,YAAYC,OAAsB;IACzC,IAAI;QACFV,OAAOC,cAAc,CAACU,OAAO,CAACrB,aAAac,KAAKQ,SAAS,CAACF;QAC1D,OAAO;IACT,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASG;IACP,IAAI;QACFb,OAAOC,cAAc,CAACa,UAAU,CAACxB;IACnC,EAAE,eAAM;IACN,kEAAkE;IACpE;AACF;AAEA,yEAAyE,GACzE,SAASyB;IACP,IAAI;QACF,MAAMC,QAAQ,IAAIC,WAAW;QAC7BjB,OAAOkB,MAAM,CAACC,eAAe,CAACH;QAC9B,IAAII,SAAS;QACb,KAAK,MAAMC,QAAQL,MAAOI,UAAUb,OAAOe,YAAY,CAACD;QACxD,OAAOE,KAAKH,QAAQI,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO;IAC7E,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,+DAA+D,GAC/D,SAASC;IACP,MAAMC,SAAiC,CAAC;IACxC,IAAI;QACF,MAAMC,SAAS,IAAIC,gBAAgB5B,OAAO6B,QAAQ,CAACC,MAAM;QACzD,KAAK,MAAM,CAACC,OAAOC,MAAM,IAAI;YAC3B;gBAAC;gBAAc;aAAY;YAC3B;gBAAC;gBAAc;aAAY;YAC3B;gBAAC;gBAAgB;aAAc;SAChC,CAAW;YACV,MAAMnD,QAAQ8C,OAAOM,GAAG,CAACF;YACzB,IAAIlD,OAAO6C,MAAM,CAACM,MAAM,GAAGnD,MAAMqD,KAAK,CAAC,GAAG;QAC5C;QACA,MAAMC,WAAWC,SAASD,QAAQ,GAAG,IAAIE,IAAID,SAASD,QAAQ,IAAI;QAClE,IAAIA,YAAYA,SAAS3C,IAAI,IAAI2C,SAAS3C,IAAI,KAAKQ,OAAO6B,QAAQ,CAACrC,IAAI,EAAE;YACvEkC,MAAM,CAAC,eAAe,GAAGS,SAAS3C,IAAI,CAAC0C,KAAK,CAAC,GAAG;QAClD;IACF,EAAE,eAAM;IACN,4CAA4C;IAC9C;IACA,OAAOR;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASY,qBAAqBC,OAIpC;IACC,IAAI,OAAOvC,WAAW,aAAa;IACnC,MAAML,SAAS4C,QAAQ5C,MAAM,IAAI;IACjC,MAAMC,UAAU4C,QAAQ7C,WAAW4C,QAAQ3C,OAAO,KAAK;IACvD,MAAM6C,WAAW7C,WAAY,CAAA,CAACF,MAAME,OAAO,IAAIF,MAAMC,MAAM,KAAKA,MAAK;IACrED,MAAMC,MAAM,GAAGA;IACfD,MAAME,OAAO,GAAGA;IAChB,IAAI2C,QAAQ3C,OAAO,KAAK,OAAOiB;IAC/B,MAAMhB,OAAO0C,QAAQ1C,IAAI,IAAI;IAC7B,IAAID,WAAWC,QAAS4C,CAAAA,YAAY5C,SAASH,MAAMG,IAAI,AAAD,GAAI;QACxDH,MAAMG,IAAI,GAAGA;QACb6C,sBAAsB,QAAQ7C;IAChC;IACA,IAAI,CAACD,SAASF,MAAMG,IAAI,GAAG;AAC7B;AAEA;;;;CAIC,GACD,OAAO,SAAS6C,sBACdC,IAAyB,EACzBC,GAAmB;IAEnB,IAAI;QACF,IAAI,OAAO5C,WAAW,eAAe,CAACN,MAAME,OAAO,IAAI,CAACF,MAAMC,MAAM,EAAE;QACtE,IAAI,CAACf,sBAAsB+D,SAAS5D,+BAA+B8D,GAAG,CAACF,OAAO;QAC9E,MAAMhD,SAASD,MAAMC,MAAM;QAC3B,MAAMmD,WAAWvC,OAAOqC,cAAAA,MAAO,IAAIG,IAAI,GAAGb,KAAK,CAAC,GAAG9C;QACnD,IAAIuD,SAAS,UAAU,CAACG,SAASE,UAAU,CAAC,MAAM;QAClD,IAAIL,SAAS,WAAW,CAACG,UAAU;QACnC,MAAMG,SAAS,GAAGN,KAAK,MAAM,EAAEG,UAAU;QAEzC,IAAIpC,UAAUZ;QACd,IAAIoD,QAAQ;QACZ,IAAI,CAACxC,WAAWA,QAAQf,MAAM,KAAKA,QAAQ;YACzC,MAAMa,KAAKO;YACX,IAAI,CAACP,IAAI;YACTE,UAAU;gBAAEF;gBAAIb;gBAAQwD,OAAO;YAAE;YACjCD,QAAQ;QACV;QACA,IAAIxC,QAAQyC,KAAK,IAAIhE,wBAAwB;QAC7C,IAAIuB,QAAQ0C,IAAI,KAAKH,QAAQ;QAC7BvC,UAAU,aAAKA;YAASyC,OAAOzC,QAAQyC,KAAK,GAAG;YAAGC,MAAMH;;QACxD,IAAI,CAACxC,YAAYC,UAAU;QAE3BjC,oBAAoB;YAClBkB;YACA,CAACjB,0BAA0B,EAAEgC,QAAQF,EAAE;YACvC6C,UAAUV;WACNG,WAAW;YAAEQ,SAASR;QAAS,IAAI,CAAC,GACpCI,QAAQ;YAAEK,cAAc;WAAS9B,mBAAoB,CAAC;IAE9D,EAAE,eAAM;IACN,wCAAwC;IAC1C;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAAS+B;IACd,IAAI;QACF,IAAI,OAAOxD,WAAW,eAAe,CAACN,MAAME,OAAO,IAAI,CAACF,MAAMC,MAAM,EAAE,OAAO,CAAC;QAC9E,MAAMe,UAAUZ;QAChB,OAAOY,WAAWA,QAAQf,MAAM,KAAKD,MAAMC,MAAM,GAAG;YAAE,CAACT,8BAA8B,EAAEwB,QAAQF,EAAE;QAAC,IAAI,CAAC;IACzG,EAAE,eAAM;QACN,OAAO,CAAC;IACV;AACF;AAEA,gDAAgD,GAChD,OAAO,SAASiD;IACd/D,MAAMC,MAAM,GAAG;IACfD,MAAME,OAAO,GAAG;IAChBF,MAAMG,IAAI,GAAG;AACf"}
|
|
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'\nimport {\n SITE_JOURNEY_KEY_MAX,\n type SiteJourneyStepType,\n isSiteJourneyStepType,\n} from './site-journey-steps'\n\nexport {\n SITE_JOURNEY_EMAIL_KEYS,\n SITE_JOURNEY_KEY_MAX,\n SITE_JOURNEY_STEP_TYPES,\n type SiteJourneyStepType,\n isSiteJourneyStepType,\n} from './site-journey-steps'\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/** 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/**\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\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_KEY_MAX","isSiteJourneyStepType","SITE_JOURNEY_EMAIL_KEYS","SITE_JOURNEY_STEP_TYPES","SITE_JOURNEY_BEACON_FIELD","SITE_JOURNEY_SERVER_STEP_TYPES","Set","SITE_JOURNEY_SUBMISSION_FIELD","SITE_JOURNEY_MAX_STEPS","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","value","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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"}
|
|
@@ -256,6 +256,16 @@
|
|
|
256
256
|
"stops": "Switching Delivery apps off for this site stops its DoorDash, Uber Eats and Grubhub orders reaching the register; the services are told the store is not connected.",
|
|
257
257
|
"keeps": "The linked stores, their item matches and every order already taken are kept, and switching it back on takes new orders again."
|
|
258
258
|
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"id": "loyalty",
|
|
262
|
+
"label": "Rewards",
|
|
263
|
+
"alwaysOnForWorkspace": true,
|
|
264
|
+
"description": "Points, referrals and store credit for your store.",
|
|
265
|
+
"siteOff": {
|
|
266
|
+
"stops": "Stops new points, rewards at checkout and at the register, and referral credit.",
|
|
267
|
+
"keeps": "Members keep their points and store credit."
|
|
268
|
+
}
|
|
259
269
|
}
|
|
260
270
|
];
|
|
261
271
|
export const PUBLISHED_SITE_IMPACT = {
|
|
@@ -289,7 +299,8 @@ export const PUBLISHED_SITE_IMPACT = {
|
|
|
289
299
|
"marketplaces": "console-only",
|
|
290
300
|
"print-on-demand": "console-only",
|
|
291
301
|
"inventory-sync": "console-only",
|
|
292
|
-
"delivery-apps": "console-only"
|
|
302
|
+
"delivery-apps": "console-only",
|
|
303
|
+
"loyalty": "console-only"
|
|
293
304
|
};
|
|
294
305
|
/**
|
|
295
306
|
* The admin edit bar's quick links, in the order they are drawn — each
|
|
@@ -654,6 +665,12 @@ export const PUBLISHED_SITE_IMPACT = {
|
|
|
654
665
|
"mediaScan": "none",
|
|
655
666
|
"mediaScanReason": "Register staff PINs (AGL-3609): a salted hash and a lockout counter per member, written only by the staff PIN route. No content field at all."
|
|
656
667
|
},
|
|
668
|
+
{
|
|
669
|
+
"pluginId": "commerce",
|
|
670
|
+
"name": "posOfflineSales",
|
|
671
|
+
"mediaScan": "none",
|
|
672
|
+
"mediaScanReason": "The offline register's sync records (AGL-3625): one per cash sale rung offline, holding its order id, flags and stock conflicts, written only by the sync route. No content field, and a busy offline register writes one per sale."
|
|
673
|
+
},
|
|
657
674
|
{
|
|
658
675
|
"pluginId": "commerce",
|
|
659
676
|
"name": "terminal",
|
|
@@ -2318,7 +2335,8 @@ export const PUBLISHED_SITE_IMPACT = {
|
|
|
2318
2335
|
"bookings",
|
|
2319
2336
|
"commerce",
|
|
2320
2337
|
"crm",
|
|
2321
|
-
"email"
|
|
2338
|
+
"email",
|
|
2339
|
+
"loyalty"
|
|
2322
2340
|
];
|
|
2323
2341
|
/**
|
|
2324
2342
|
* The plugins whose sales the operator's sales tax return may not be filed
|