@aglyn/aglyn 1.0.0-beta.246 → 1.0.0-beta.249

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.
Files changed (55) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/contact-custom-fields.d.ts +23 -1
  3. package/src/lib/app-utils/contact-custom-fields.js +84 -2
  4. package/src/lib/app-utils/contact-custom-fields.js.map +1 -1
  5. package/src/lib/app-utils/crm.d.ts +7 -1
  6. package/src/lib/app-utils/crm.js.map +1 -1
  7. package/src/lib/app-utils/default-site.js +21 -33
  8. package/src/lib/app-utils/default-site.js.map +1 -1
  9. package/src/lib/app-utils/docs-help-sections.generated.js +8 -8
  10. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-help.generated.d.ts +8 -8
  12. package/src/lib/app-utils/docs-help.generated.js +9 -5
  13. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  14. package/src/lib/app-utils/docs-index.generated.js +129 -59
  15. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  16. package/src/lib/app-utils/element-pick-protocol.d.ts +54 -0
  17. package/src/lib/app-utils/element-pick-protocol.js +41 -0
  18. package/src/lib/app-utils/element-pick-protocol.js.map +1 -0
  19. package/src/lib/app-utils/element-pick-target.d.ts +32 -0
  20. package/src/lib/app-utils/element-pick-target.js +47 -0
  21. package/src/lib/app-utils/element-pick-target.js.map +1 -0
  22. package/src/lib/app-utils/error-beacon.d.ts +19 -0
  23. package/src/lib/app-utils/error-beacon.js +31 -5
  24. package/src/lib/app-utils/error-beacon.js.map +1 -1
  25. package/src/lib/app-utils/form-contract.js +13 -6
  26. package/src/lib/app-utils/form-contract.js.map +1 -1
  27. package/src/lib/app-utils/forms.d.ts +31 -0
  28. package/src/lib/app-utils/forms.js +213 -16
  29. package/src/lib/app-utils/forms.js.map +1 -1
  30. package/src/lib/app-utils/node-definition-sanitizer.js +12 -0
  31. package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
  32. package/src/lib/app-utils/seo-locale.d.ts +14 -0
  33. package/src/lib/app-utils/seo-locale.js +15 -0
  34. package/src/lib/app-utils/seo-locale.js.map +1 -1
  35. package/src/lib/app-utils/seo-site-scan.js +18 -3
  36. package/src/lib/app-utils/seo-site-scan.js.map +1 -1
  37. package/src/lib/app-utils/site-live-url.d.ts +24 -0
  38. package/src/lib/app-utils/site-live-url.js +60 -0
  39. package/src/lib/app-utils/site-live-url.js.map +1 -0
  40. package/src/lib/app-utils/starter-template-nodes.d.ts +13 -0
  41. package/src/lib/app-utils/starter-template-nodes.js +40 -0
  42. package/src/lib/app-utils/starter-template-nodes.js.map +1 -1
  43. package/src/lib/app-utils/starter-templates.d.ts +1 -1
  44. package/src/lib/app-utils/starter-templates.js +19 -31
  45. package/src/lib/app-utils/starter-templates.js.map +1 -1
  46. package/src/lib/plugin-manager/feature-plugins.d.ts +74 -3
  47. package/src/lib/plugin-manager/feature-plugins.js +9 -0
  48. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  49. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +11 -0
  50. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  51. package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +23 -0
  52. package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
  53. package/src/lib/plugin-manager/stock-photo-provider.d.ts +67 -0
  54. package/src/lib/plugin-manager/stock-photo-provider.js +14 -1
  55. package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
@@ -0,0 +1,54 @@
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
+ * The live-page element picker's wire protocol (AGL-3758).
19
+ *
20
+ * The console's Actions card opens a site page in a frame with
21
+ * {@link ELEMENT_PICK_PARAM}; the page's pick mode highlights elements under
22
+ * the pointer and, on a click, posts the element's selector to the console —
23
+ * the same `[data-aglyn="leaf:<id>"]` selector the Besigner's Interactions
24
+ * "Pick element" stores, which the site runtime resolves through
25
+ * `expandLeafSelector`. Import-free on purpose: the tenant's admin-bar stub,
26
+ * on every published page, reads the param from here.
27
+ */
28
+ /** Query param that arms pick mode on a framed published page. */
29
+ export declare const ELEMENT_PICK_PARAM = "aglyn-pick";
30
+ /** `postMessage` type every pick-mode message carries. */
31
+ export declare const ELEMENT_PICK_MESSAGE_TYPE = "aglyn-pick-element";
32
+ export type ElementPickMessage =
33
+ /** Pick mode loaded and is waiting for a click. */
34
+ {
35
+ type: typeof ELEMENT_PICK_MESSAGE_TYPE;
36
+ event: 'ready';
37
+ }
38
+ /** The builder clicked an element. */
39
+ | {
40
+ type: typeof ELEMENT_PICK_MESSAGE_TYPE;
41
+ event: 'picked';
42
+ selector: string;
43
+ /** What the element reads as, for the console's chip. */
44
+ label: string;
45
+ }
46
+ /** Escape on the page. */
47
+ | {
48
+ type: typeof ELEMENT_PICK_MESSAGE_TYPE;
49
+ event: 'cancel';
50
+ };
51
+ /** Whether a `message` event's data is one of ours, shaped as promised. */
52
+ export declare function isElementPickMessage(data: unknown): data is ElementPickMessage;
53
+ /** `pageUrl` with pick mode armed, keeping any query it already has. */
54
+ export declare function elementPickUrl(pageUrl: string): string;
@@ -0,0 +1,41 @@
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
+ * The live-page element picker's wire protocol (AGL-3758).
18
+ *
19
+ * The console's Actions card opens a site page in a frame with
20
+ * {@link ELEMENT_PICK_PARAM}; the page's pick mode highlights elements under
21
+ * the pointer and, on a click, posts the element's selector to the console —
22
+ * the same `[data-aglyn="leaf:<id>"]` selector the Besigner's Interactions
23
+ * "Pick element" stores, which the site runtime resolves through
24
+ * `expandLeafSelector`. Import-free on purpose: the tenant's admin-bar stub,
25
+ * on every published page, reads the param from here.
26
+ */ /** Query param that arms pick mode on a framed published page. */ export const ELEMENT_PICK_PARAM = 'aglyn-pick';
27
+ /** `postMessage` type every pick-mode message carries. */ export const ELEMENT_PICK_MESSAGE_TYPE = 'aglyn-pick-element';
28
+ /** Whether a `message` event's data is one of ours, shaped as promised. */ export function isElementPickMessage(data) {
29
+ if (!data || typeof data !== 'object') return false;
30
+ const message = data;
31
+ if (message['type'] !== ELEMENT_PICK_MESSAGE_TYPE) return false;
32
+ if (message['event'] === 'ready' || message['event'] === 'cancel') return true;
33
+ return message['event'] === 'picked' && typeof message['selector'] === 'string' && message['selector'] !== '' && typeof message['label'] === 'string';
34
+ }
35
+ /** `pageUrl` with pick mode armed, keeping any query it already has. */ export function elementPickUrl(pageUrl) {
36
+ const url = new URL(pageUrl);
37
+ url.searchParams.set(ELEMENT_PICK_PARAM, '1');
38
+ return url.toString();
39
+ }
40
+
41
+ //# sourceMappingURL=element-pick-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-pick-protocol.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 * The live-page element picker's wire protocol (AGL-3758).\n *\n * The console's Actions card opens a site page in a frame with\n * {@link ELEMENT_PICK_PARAM}; the page's pick mode highlights elements under\n * the pointer and, on a click, posts the element's selector to the console —\n * the same `[data-aglyn=\"leaf:<id>\"]` selector the Besigner's Interactions\n * \"Pick element\" stores, which the site runtime resolves through\n * `expandLeafSelector`. Import-free on purpose: the tenant's admin-bar stub,\n * on every published page, reads the param from here.\n */\n\n/** Query param that arms pick mode on a framed published page. */\nexport const ELEMENT_PICK_PARAM = 'aglyn-pick'\n\n/** `postMessage` type every pick-mode message carries. */\nexport const ELEMENT_PICK_MESSAGE_TYPE = 'aglyn-pick-element'\n\nexport type ElementPickMessage =\n /** Pick mode loaded and is waiting for a click. */\n | { type: typeof ELEMENT_PICK_MESSAGE_TYPE; event: 'ready' }\n /** The builder clicked an element. */\n | {\n type: typeof ELEMENT_PICK_MESSAGE_TYPE\n event: 'picked'\n selector: string\n /** What the element reads as, for the console's chip. */\n label: string\n }\n /** Escape on the page. */\n | { type: typeof ELEMENT_PICK_MESSAGE_TYPE; event: 'cancel' }\n\n/** Whether a `message` event's data is one of ours, shaped as promised. */\nexport function isElementPickMessage(data: unknown): data is ElementPickMessage {\n if (!data || typeof data !== 'object') return false\n const message = data as Record<string, unknown>\n if (message['type'] !== ELEMENT_PICK_MESSAGE_TYPE) return false\n if (message['event'] === 'ready' || message['event'] === 'cancel') return true\n return (\n message['event'] === 'picked' &&\n typeof message['selector'] === 'string' &&\n message['selector'] !== '' &&\n typeof message['label'] === 'string'\n )\n}\n\n/** `pageUrl` with pick mode armed, keeping any query it already has. */\nexport function elementPickUrl(pageUrl: string): string {\n const url = new URL(pageUrl)\n url.searchParams.set(ELEMENT_PICK_PARAM, '1')\n return url.toString()\n}\n"],"names":["ELEMENT_PICK_PARAM","ELEMENT_PICK_MESSAGE_TYPE","isElementPickMessage","data","message","elementPickUrl","pageUrl","url","URL","searchParams","set","toString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,gEAAgE,GAChE,OAAO,MAAMA,qBAAqB,aAAY;AAE9C,wDAAwD,GACxD,OAAO,MAAMC,4BAA4B,qBAAoB;AAgB7D,yEAAyE,GACzE,OAAO,SAASC,qBAAqBC,IAAa;IAChD,IAAI,CAACA,QAAQ,OAAOA,SAAS,UAAU,OAAO;IAC9C,MAAMC,UAAUD;IAChB,IAAIC,OAAO,CAAC,OAAO,KAAKH,2BAA2B,OAAO;IAC1D,IAAIG,OAAO,CAAC,QAAQ,KAAK,WAAWA,OAAO,CAAC,QAAQ,KAAK,UAAU,OAAO;IAC1E,OACEA,OAAO,CAAC,QAAQ,KAAK,YACrB,OAAOA,OAAO,CAAC,WAAW,KAAK,YAC/BA,OAAO,CAAC,WAAW,KAAK,MACxB,OAAOA,OAAO,CAAC,QAAQ,KAAK;AAEhC;AAEA,sEAAsE,GACtE,OAAO,SAASC,eAAeC,OAAe;IAC5C,MAAMC,MAAM,IAAIC,IAAIF;IACpBC,IAAIE,YAAY,CAACC,GAAG,CAACV,oBAAoB;IACzC,OAAOO,IAAII,QAAQ;AACrB"}
@@ -0,0 +1,32 @@
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
+ export interface ElementPickTarget {
18
+ /** The stamped element the click resolves to. */
19
+ element: Element;
20
+ /** The selector stored for it — the Besigner picker's form. */
21
+ selector: string;
22
+ /** What it reads as: its accessible name or text, else its tag. */
23
+ label: string;
24
+ }
25
+ /**
26
+ * The element a pick resolves to (AGL-3758): the nearest ancestor-or-self the
27
+ * renderer stamped, its stored selector, and a label. A layout's namespace is
28
+ * stripped off the id, exactly as the Besigner picker stores the raw canvas
29
+ * id; the runtime's `expandLeafSelector` matches it on every page again.
30
+ * `null` for a point on nothing Aglyn drew.
31
+ */
32
+ export declare function elementPickTarget(from: Element | null): ElementPickTarget | null;
@@ -0,0 +1,47 @@
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 { normalizeLeafId } from "./element-ui.js";
17
+ /** The stamp the node renderer's `Leaf` puts on every element it draws. */ const LEAF_STAMP_SELECTOR = '[data-aglyn^="leaf:"]';
18
+ function readableLabel(element) {
19
+ var _element_textContent;
20
+ const named = element.getAttribute('aria-label') || element.getAttribute('alt') || element.getAttribute('title') || ((_element_textContent = element.textContent) != null ? _element_textContent : '');
21
+ const text = named.replace(/\s+/g, ' ').trim();
22
+ const tag = element.tagName.toLowerCase();
23
+ if (!text) return tag === 'img' ? 'Image' : `Element (${tag})`;
24
+ return text.length > 40 ? `“${text.slice(0, 39)}…”` : `“${text}”`;
25
+ }
26
+ /**
27
+ * The element a pick resolves to (AGL-3758): the nearest ancestor-or-self the
28
+ * renderer stamped, its stored selector, and a label. A layout's namespace is
29
+ * stripped off the id, exactly as the Besigner picker stores the raw canvas
30
+ * id; the runtime's `expandLeafSelector` matches it on every page again.
31
+ * `null` for a point on nothing Aglyn drew.
32
+ */ export function elementPickTarget(from) {
33
+ var _ref, _element_getAttribute;
34
+ var _from_closest;
35
+ const element = (_ref = from == null ? void 0 : (_from_closest = from.closest) == null ? void 0 : _from_closest.call(from, LEAF_STAMP_SELECTOR)) != null ? _ref : null;
36
+ if (!element) return null;
37
+ const stamp = (_element_getAttribute = element.getAttribute('data-aglyn')) != null ? _element_getAttribute : '';
38
+ const id = normalizeLeafId(stamp.slice('leaf:'.length));
39
+ if (!id || id.includes('"') || id.includes('\\')) return null;
40
+ return {
41
+ element,
42
+ selector: `[data-aglyn="leaf:${id}"]`,
43
+ label: readableLabel(element)
44
+ };
45
+ }
46
+
47
+ //# sourceMappingURL=element-pick-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-pick-target.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 { normalizeLeafId } from './element-ui'\n\n/** The stamp the node renderer's `Leaf` puts on every element it draws. */\nconst LEAF_STAMP_SELECTOR = '[data-aglyn^=\"leaf:\"]'\n\nexport interface ElementPickTarget {\n /** The stamped element the click resolves to. */\n element: Element\n /** The selector stored for it — the Besigner picker's form. */\n selector: string\n /** What it reads as: its accessible name or text, else its tag. */\n label: string\n}\n\nfunction readableLabel(element: Element): string {\n const named =\n element.getAttribute('aria-label') ||\n element.getAttribute('alt') ||\n element.getAttribute('title') ||\n (element.textContent ?? '')\n const text = named.replace(/\\s+/g, ' ').trim()\n const tag = element.tagName.toLowerCase()\n if (!text) return tag === 'img' ? 'Image' : `Element (${tag})`\n return text.length > 40 ? `“${text.slice(0, 39)}…”` : `“${text}”`\n}\n\n/**\n * The element a pick resolves to (AGL-3758): the nearest ancestor-or-self the\n * renderer stamped, its stored selector, and a label. A layout's namespace is\n * stripped off the id, exactly as the Besigner picker stores the raw canvas\n * id; the runtime's `expandLeafSelector` matches it on every page again.\n * `null` for a point on nothing Aglyn drew.\n */\nexport function elementPickTarget(from: Element | null): ElementPickTarget | null {\n const element = from?.closest?.(LEAF_STAMP_SELECTOR) ?? null\n if (!element) return null\n const stamp = element.getAttribute('data-aglyn') ?? ''\n const id = normalizeLeafId(stamp.slice('leaf:'.length))\n if (!id || id.includes('\"') || id.includes('\\\\')) return null\n return {\n element,\n selector: `[data-aglyn=\"leaf:${id}\"]`,\n label: readableLabel(element),\n }\n}\n"],"names":["normalizeLeafId","LEAF_STAMP_SELECTOR","readableLabel","element","named","getAttribute","textContent","text","replace","trim","tag","tagName","toLowerCase","length","slice","elementPickTarget","from","closest","stamp","id","includes","selector","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,eAAe,QAAQ,kBAAc;AAE9C,yEAAyE,GACzE,MAAMC,sBAAsB;AAW5B,SAASC,cAAcC,OAAgB;QAKlCA;IAJH,MAAMC,QACJD,QAAQE,YAAY,CAAC,iBACrBF,QAAQE,YAAY,CAAC,UACrBF,QAAQE,YAAY,CAAC,cACpBF,uBAAAA,QAAQG,WAAW,YAAnBH,uBAAuB;IAC1B,MAAMI,OAAOH,MAAMI,OAAO,CAAC,QAAQ,KAAKC,IAAI;IAC5C,MAAMC,MAAMP,QAAQQ,OAAO,CAACC,WAAW;IACvC,IAAI,CAACL,MAAM,OAAOG,QAAQ,QAAQ,UAAU,CAAC,SAAS,EAAEA,IAAI,CAAC,CAAC;IAC9D,OAAOH,KAAKM,MAAM,GAAG,KAAK,CAAC,CAAC,EAAEN,KAAKO,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAEP,KAAK,CAAC,CAAC;AACnE;AAEA;;;;;;CAMC,GACD,OAAO,SAASQ,kBAAkBC,IAAoB;cAGtCb;QAFEa;IAAhB,MAAMb,kBAAUa,yBAAAA,gBAAAA,KAAMC,OAAO,qBAAbD,mBAAAA,MAAgBf,uCAAwB;IACxD,IAAI,CAACE,SAAS,OAAO;IACrB,MAAMe,SAAQf,wBAAAA,QAAQE,YAAY,CAAC,yBAArBF,wBAAsC;IACpD,MAAMgB,KAAKnB,gBAAgBkB,MAAMJ,KAAK,CAAC,QAAQD,MAAM;IACrD,IAAI,CAACM,MAAMA,GAAGC,QAAQ,CAAC,QAAQD,GAAGC,QAAQ,CAAC,OAAO,OAAO;IACzD,OAAO;QACLjB;QACAkB,UAAU,CAAC,kBAAkB,EAAEF,GAAG,EAAE,CAAC;QACrCG,OAAOpB,cAAcC;IACvB;AACF"}
@@ -146,6 +146,25 @@ export declare function isInjectedThirdPartyFrame(stack: string, documentUrl: st
146
146
  /** How many frames of this stack are in a script with no URL at all. */
147
147
  export declare function anonymousScriptFrameCount(stack: string): number;
148
148
  export declare function isBenignBrowserNotice(message: string): boolean;
149
+ /**
150
+ * Whether the document is a page a visitor navigated to over the web
151
+ * (AGL-3759).
152
+ *
153
+ * Only `http:` and `https:` qualify. A document at `about:blank` or
154
+ * `about:srcdoc` is a COPY of our page: a translator extension or a site that
155
+ * frames our markup renders it into a fresh document with no URL of its own,
156
+ * and our scripts run there too. Next.js's app router then throws
157
+ * `Failed to construct 'URL': Invalid URL` resolving its canonical URL against
158
+ * `window.location.href`, which no code of ours can prevent and no visitor on
159
+ * the real page ever sees. `file:` (a saved page) and an extension's own
160
+ * scheme are copies in the same sense.
161
+ *
162
+ * A missing or unreadable location is not a web document either: nothing
163
+ * from it can be traced to a page we serve.
164
+ */
165
+ export declare function isWebDocument(location: {
166
+ protocol?: string;
167
+ } | null | undefined): boolean;
149
168
  /**
150
169
  * A rejection that says only "this was cancelled" (AGL-3734).
151
170
  *
@@ -165,6 +165,29 @@ function clamp(value, max) {
165
165
  export function isBenignBrowserNotice(message) {
166
166
  return BENIGN_BROWSER_NOTICE.test(message);
167
167
  }
168
+ /**
169
+ * Whether the document is a page a visitor navigated to over the web
170
+ * (AGL-3759).
171
+ *
172
+ * Only `http:` and `https:` qualify. A document at `about:blank` or
173
+ * `about:srcdoc` is a COPY of our page: a translator extension or a site that
174
+ * frames our markup renders it into a fresh document with no URL of its own,
175
+ * and our scripts run there too. Next.js's app router then throws
176
+ * `Failed to construct 'URL': Invalid URL` resolving its canonical URL against
177
+ * `window.location.href`, which no code of ours can prevent and no visitor on
178
+ * the real page ever sees. `file:` (a saved page) and an extension's own
179
+ * scheme are copies in the same sense.
180
+ *
181
+ * A missing or unreadable location is not a web document either: nothing
182
+ * from it can be traced to a page we serve.
183
+ */ export function isWebDocument(location) {
184
+ try {
185
+ const protocol = location == null ? void 0 : location.protocol;
186
+ return protocol === 'http:' || protocol === 'https:';
187
+ } catch (unused) {
188
+ return false;
189
+ }
190
+ }
168
191
  /**
169
192
  * A rejection that says only "this was cancelled" (AGL-3734).
170
193
  *
@@ -530,13 +553,16 @@ let installed = false;
530
553
  // (the rejection handler below), because a tab left on a dead deploy is a
531
554
  // visitor's problem whether or not its errors are counted.
532
555
  const sampled = Math.random() < sampleRate;
556
+ // A copy of the page in a document that is not on the web reports nothing
557
+ // by either door, whatever it throws: see `isWebDocument`.
558
+ const reporting = sampled && isWebDocument(window.location);
533
559
  // The document's language as SERVED, read before hydration and before any
534
560
  // translator has had a chance to rewrite it (`pageTranslationSignal`).
535
561
  const servedLang = (_ref3 = (_document_documentElement = document.documentElement) == null ? void 0 : _document_documentElement.getAttribute('lang')) != null ? _ref3 : null;
536
562
  // Watched from now, because React's recovery erases the translator's marks
537
563
  // before a mismatch is reported (`watchPageTranslation`, AGL-3738).
538
564
  const translation = watchPageTranslation(document, servedLang);
539
- if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT);
565
+ if (reporting) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT);
540
566
  // Classified once, here, and only the family is kept: the user-agent
541
567
  // string itself never reaches the queue (AGL-3732).
542
568
  let browser = 'other';
@@ -576,7 +602,7 @@ let installed = false;
576
602
  };
577
603
  const enqueue = (event)=>{
578
604
  var _event_stack;
579
- if (!sampled || sent >= maxPerPage) return;
605
+ if (!reporting || sent >= maxPerPage) return;
580
606
  // Message + first stack line: enough identity to collapse a render loop
581
607
  // without collapsing distinct errors that share a message.
582
608
  const key = `${event.message}\x00${((_event_stack = event.stack) != null ? _event_stack : '').split('\n', 2).join('\n')}`;
@@ -588,9 +614,9 @@ let installed = false;
588
614
  }));
589
615
  if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS);
590
616
  };
591
- // Null on an unsampled pageview, so it reports nothing by either door
592
- // rather than one by one and none by the other.
593
- publishEvent = sampled ? enqueue : null;
617
+ // Null on a pageview that does not report, so it reports nothing by either
618
+ // door rather than one by one and none by the other.
619
+ publishEvent = reporting ? enqueue : null;
594
620
  window.addEventListener('error', (event)=>{
595
621
  try {
596
622
  var _ref, _recoveredErrorKind;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/error-beacon.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 * First-party browser error beacon (AGL-1538).\n *\n * Crashlytics does not exist for web, and adding a third-party error vendor\n * is a subprocessor-list decision that was explicitly deferred. This is the\n * first-party alternative: `window.onerror` / `unhandledrejection` handlers\n * that batch uncaught failures to the app's own `/api/errors` route, which\n * forwards them to Google Cloud Error Reporting — a processor already on the\n * list, alerting through the GCP stack that already pages (AGL-1502).\n *\n * Design constraints, in order:\n *\n * - **It must never become the outage.** Everything is wrapped; a failure to\n * report is silence, not a thrown error. Batches are capped per page so an\n * error thrown in a loop cannot DDoS our own API — after `maxPerPage`\n * events the beacon disarms for the rest of the pageview.\n * - **No PII.** URLs are scrubbed to origin + pathname (query strings and\n * fragments carry tokens and search terms), no user id, no cookies —\n * the payload is the error text and where in the CODE it happened.\n * Cross-origin scripts surface as the browser's opaque \"Script error.\"\n * with no stack; those are dropped since they cannot be acted on.\n * - **Dedupe.** One page throwing the same error on every frame reports it\n * once.\n *\n * Framework-free on purpose: the console mounts it from its root layout, the\n * tenant runtime from its own, and neither needs React here. Deliberately NO\n * 'use client' directive — inside this shared lib the directive forks the\n * module graph (AGL-52); every importer is already a client module.\n */\n\nimport { browserFamily, type BrowserFamily } from './browser-family'\nimport { isCrossPoolDesync } from './cross-pool-desync'\nimport { isForeignScriptUrl, stackFrameUrls } from './foreign-script'\nimport { isStaleBuildError, recoverStaleBuildRejection } from './stale-build-error'\n\nexport interface ErrorBeaconEvent {\n /**\n * Which handler caught it — 'error' | 'unhandledrejection' — or the\n * family it was labeled with: 'hydration', {@link AUTH_DESYNC_KIND},\n * {@link CHUNK_LOAD_KIND}, or a caller's own kind for a handled error.\n * Lands at `jsonPayload.kind`, where the alert policies filter on it.\n */\n kind: string\n /** Error message, clamped. */\n message: string\n /** Stack trace when the thrown value carried one, clamped. */\n stack?: string\n /** Script URL for stackless `window.onerror` events, scrubbed. */\n source?: string\n line?: number\n col?: number\n /** Page URL, scrubbed to origin + pathname. */\n url: string\n /**\n * On a `hydration` event from builds before AGL-3738 only: the page\n * translator whose rewrite was visible in the document when React gave up\n * — see {@link pageTranslationSignal}. Today's beacon drops a mismatch on a\n * translated page instead of sending it, so the field stays only for what\n * older tabs still report.\n */\n translated?: string\n /** On a `hydration` event only: `navigator.language`, clamped. */\n language?: string\n /**\n * On every event: the coarse browser family — one of seven, see\n * {@link browserFamily} (AGL-3732). Never the user agent, never a version.\n * Stamped by the queue, so a caller never sets it.\n */\n browser?: BrowserFamily\n}\n\nexport interface ErrorBeaconOptions {\n /** POST target; same-origin. Default `/api/errors`. */\n endpoint?: string\n /** 0..1 — fraction of pageviews that report at all. Default 1. */\n sampleRate?: number\n /** Max events reported per pageview before the beacon disarms. */\n maxPerPage?: number\n}\n\nconst MAX_MESSAGE = 1_024\nconst MAX_STACK = 8_192\nconst FLUSH_DELAY_MS = 2_000\n\n/** Origin + pathname only — query strings and fragments never leave. */\nfunction scrubUrl(raw: string | null | undefined): string {\n if (!raw) return ''\n try {\n const url = new URL(String(raw), window.location.href)\n return `${url.origin}${url.pathname}`\n } catch {\n return ''\n }\n}\n\nfunction clamp(value: unknown, max: number): string {\n return String(value ?? '').slice(0, max)\n}\n\n/**\n * Was this thrown by a script somebody ELSE evaluated into our page?\n *\n * Three tells, all read off the frames' positions and none off a function\n * name — a name would be a denylist needing a new entry per vendor.\n *\n * **Every frame is the DOCUMENT.** Measured 2026-09-02 (AGL-2523) on\n * aglyn.com/pricing, reported as an error of ours:\n *\n * sendDataToNative@https://aglyn.com/pricing:1:1325\n * sendPageHideMessage@https://aglyn.com/pricing:1:4139\n * @https://aglyn.com/pricing:1:6257\n *\n * `sendDataToNative` is the Meta in-app browser's native bridge, injected by\n * the Facebook/Instagram webview. In WebKit's format the code was evaluated\n * inline, so every frame is the document rather than a script we served.\n *\n * **Every frame is under a FOREIGN scheme.** Measured 2026-09-10 (AGL-2786) on\n * aglyn.com, the same bridge in the Android webview:\n *\n * at sendDataToNative (iabjs://navigation_performance_logger_android:1:10632)\n *\n * That webview names its scripts under its own `iabjs://` scheme, and\n * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`.\n *\n * **Every frame is a script with NO URL** (AGL-3423). Measured on the\n * sign-up page and on aglyn.com/solutions/agencies:\n *\n * at eval (<anonymous>)\n * at predicate (eval at evaluate (:234:30), <anonymous>:11:37)\n *\n * at <anonymous>:1:54\n * at <anonymous>:1:64\n *\n * An automation driver's `evaluate` and a snippet pasted into devtools. Every\n * script we serve has a URL — a chunk, a CDN asset, or the document for an\n * inline one — so a frame whose position is `<anonymous>:line:col`, or code\n * `eval`'d by such a frame, was not served by us. See\n * {@link anonymousScriptFrameCount} for exactly which frames count.\n *\n * ⚑ Deliberately `every`, and deliberately compared against the document\n * rather than against our asset path. A rule like \"no frame under\n * `/_next/static/`\" would delete EVERY error from a self-hosted deployment\n * that serves its assets from a CDN, because none of its frames would match.\n * Comparing to the document cannot fail that way: a CDN frame is not the\n * document either, so it keeps the report.\n *\n * A stack may mix the tells frame by frame. Nothing we ship can be fixed in\n * response to any of them, which is the same test the opaque `Script error.`\n * cut already applies.\n *\n * The cost is honest and small: a throw from one of our own inline\n * bootstrap scripts looks the same and is dropped with it. That trade buys a\n * rule that needs no maintenance as new webviews and extensions appear.\n */\nexport function isInjectedThirdPartyFrame(\n stack: string,\n documentUrl: string,\n): boolean {\n const urls = stackFrameUrls(stack)\n // No frames parsed is not evidence of anything — keep the report. A stack\n // with neither a URL nor an anonymous-script frame is exactly that: an\n // empty frame list must never read as \"every frame is injected\".\n if (!urls.length && !anonymousScriptFrameCount(stack)) return false\n // Anonymous frames need no test of their own — they are injected by\n // definition — so `every` over the URLs is `every` over the whole stack.\n // One URL of ours, even as the origin of an `eval`, keeps the report.\n return urls.every(\n (url) => isForeignScriptUrl(url) || scrubUrl(url) === documentUrl,\n )\n}\n\n/** A frame line, in V8's `at …` format or WebKit/Firefox's `fn@location`. */\nconst STACK_FRAME_LINE = /^\\s*at\\s|@/\n\n/**\n * A position in a script that has no URL.\n *\n * - `<anonymous>:line:col` — V8's name for such a script, both for a top-level\n * snippet and as the position of code run through `eval`/`new Function`.\n * - `eval at …` — V8's origin of `eval`'d code. When OUR script ran the eval\n * its URL is inside this origin, `stackFrameUrls` reads it, and the report\n * is kept; that is how the marketing plugin's `runJs` step stays reported.\n * - `debugger eval code:line:col` — Firefox's devtools console, in the\n * `fn@location` format WebKit also uses.\n *\n * ⚑ A BARE `(<anonymous>)` with no line and column is NOT one of these. It is\n * how V8 prints a builtin — `at Array.forEach (<anonymous>)`,\n * `at JSON.parse (<anonymous>)` — and a builtin is called by whoever called\n * it, including us.\n */\nconst ANONYMOUS_SCRIPT_POSITION =\n /<anonymous>:\\d+:\\d+|\\beval at\\s|\\bdebugger eval code:\\d+:\\d+/\n\n/** How many frames of this stack are in a script with no URL at all. */\nexport function anonymousScriptFrameCount(stack: string): number {\n let count = 0\n for (const line of stack.split('\\n')) {\n if (STACK_FRAME_LINE.test(line) && ANONYMOUS_SCRIPT_POSITION.test(line)) {\n count += 1\n }\n }\n return count\n}\n\n/**\n * Browser notices that are not failures, and that no fix of ours could act on.\n *\n * `ResizeObserver loop completed with undelivered notifications` (and its older\n * wording, `ResizeObserver loop limit exceeded`) is the browser saying it\n * deferred some resize callbacks to the next frame. Nothing threw: the event\n * carries no error object and no stack, and every observer is still\n * delivered. It arrived from the Besigner on 2026-09-26 and paged. Dropped, on\n * the browser's exact wording.\n */\nconst BENIGN_BROWSER_NOTICE =\n /^(?:Uncaught )?(?:Error: )?ResizeObserver loop (?:completed with undelivered notifications|limit exceeded)/\n\nexport function isBenignBrowserNotice(message: string): boolean {\n return BENIGN_BROWSER_NOTICE.test(message)\n}\n\n/**\n * A rejection that says only \"this was cancelled\" (AGL-3734).\n *\n * `AbortError` is what a request rejects with when its `AbortSignal` fires —\n * a deadline we set, a component that unmounted, a search the author typed\n * past — and what the browser rejects every in-flight fetch with when the\n * document goes away under it. On 2026-10-10 one arrived from\n * `/edit-access`, the page a published site opens in a popup or a hidden\n * iframe and then CLOSES itself the moment it has its answer. The report had\n * no stack and no message past the name, so there was no call site to fix:\n * the page's own mint was already caught, and whatever else the console shell\n * had in flight was cut off by the site ending the page, which is the flow\n * working.\n *\n * Why the classifier rather than each call site: every request on every\n * console page can be the one in flight when a page is torn down, and the\n * abort is the intended outcome in every case. There is nothing a fix of ours\n * could change, and nothing in the report to locate it by.\n *\n * Narrow on purpose. Only `name === 'AbortError'`, and only for an UNHANDLED\n * REJECTION — not `TimeoutError`, which is `AbortSignal.timeout` saying a\n * deadline was MISSED (a real slowness), and not an uncaught `error` event or\n * a `reportHandledError` call, where somebody chose to report it.\n */\nexport function isAbortRejection(reason: unknown): boolean {\n try {\n return (\n typeof reason === 'object' &&\n reason !== null &&\n (reason as { name?: unknown }).name === 'AbortError'\n )\n } catch {\n // A getter that throws leaves the rejection reported, never dropped.\n return false\n }\n}\n\n/**\n * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to\n * another account pool (AGL-3280). The console recovers the tab by reloading\n * it once it is hidden and deliberately leaves the rejection unhandled so it\n * is still seen; this label is what lets it be seen by RATE instead of paging\n * on each of the several rejections one storage event raises.\n */\nexport const AUTH_DESYNC_KIND = 'auth-desync'\n\n/**\n * `kind` for a chunk the document names and the origin no longer serves\n * (AGL-3279). Every boundary reloads once for it; what still arrives is the\n * failure the reload did not cure. One of those is a visitor on a bad\n * connection; a burst of them is a broken deploy, which only a rate can tell.\n */\nexport const CHUNK_LOAD_KIND = 'chunk-load'\n\n/**\n * The label for a fault we RECOVER from and still want to see, or undefined.\n *\n * These could be ours, so unlike an injected stack they are never dropped:\n * they are written at full severity under their own `kind`, the per-entry\n * policy excludes that kind, and a rate policy watches it — the same split\n * `hydration` already has.\n */\nexport function recoveredErrorKind(error: unknown): string | undefined {\n try {\n if (isCrossPoolDesync(error)) return AUTH_DESYNC_KIND\n if (isStaleBuildError(error)) return CHUNK_LOAD_KIND\n } catch {\n // A getter that throws leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * React's hydration-mismatch family, which a page TRANSLATOR causes and a\n * real render divergence also causes.\n *\n * These are `onRecoverableError` reports, not crashes: React has already\n * re-rendered on the client and the visitor has a working page. They arrive\n * here as uncaught errors only because React re-throws them to `reportError`.\n *\n * They are marked rather than dropped, and the distinction is the whole\n * design. Dropping would hide a real hydration regression, which is a genuine\n * and expensive bug; the entries stay in `client-errors` at full severity and\n * still group in Error Reporting. What the mark buys is that the log-match\n * policy can stop paging on a single report, while a rate-based policy on the\n * same mark still catches a systemic one.\n *\n * ⚠️ THE MARK IS NOT A VERDICT, and the one cluster this repo has measured is\n * why. Eight #418 reports arrived inside a nine-minute window on 2026-09-01,\n * on one build, and the three pages they came from were exactly the three\n * carrying a form — `/`, `/pricing` and the solutions pages reported nothing\n * in the same window, which no browser-side rewrite would respect. A placed\n * form was expanding inside its own grafted subtree, so every page carrying\n * one shipped six nested `<form>` elements; nested forms are invalid HTML, the\n * parser dropped the inner ones, and the client tree disagreed with the server\n * tree. The visible cost was a submission the route never received, because\n * the native submit came from an unhandled ancestor and the browser fell back\n * to a GET with the visitor's name, email and message in the address bar. The\n * reports stopped because the nesting was fixed, roughly a quarter of an hour\n * after the last one.\n *\n * So a rate low enough not to page is a reason to READ the entry — the page\n * it names, and whether the set of pages has a shape — never a reason to\n * assume somebody's extension.\n */\nconst HYDRATION_MINIFIED = /Minified React error #(?:418|423|425)\\b/\nconst HYDRATION_TEXT =\n /Hydration failed because|Text content does not match server-rendered HTML|There was an error while hydrating/\n\nexport function isHydrationMismatch(message: string): boolean {\n return HYDRATION_MINIFIED.test(message) || HYDRATION_TEXT.test(message)\n}\n\n/**\n * WHICH TRANSLATOR rewrote this page's text, read off the document, or\n * undefined when none visibly did.\n *\n * A hydration report on its own cannot say whether React disagreed with our\n * server render or with a translator that rewrote the server's text before\n * React reached it, and the two look identical in `client-errors`: the\n * minified #418, a stack inside the react-dom chunk, the page URL, nothing\n * else. aglyn.com/ carried eight of them between 2026-09-22 and 2026-10-10.\n * The visitor at 2026-10-09 22:28Z went on to sign up two minutes later with\n * Chrome Translate rewriting the console (the `<font>` crash #1342 fixed); the\n * two on 2026-09-22/23 were desktop Chrome OS in Hyderabad, while every other\n * Indian, Pakistani and Turkish home-page visit in GA that fortnight hydrated\n * without a report — so neither the build nor the time zone was the variable,\n * and the beacon had nothing to say which visitor it was.\n *\n * The signatures are the ones each translator leaves on the DOM it rewrote:\n *\n * - Chrome (and every Chromium that ships Google Translate) sets\n * `translated-ltr` / `translated-rtl` on `<html>` and wraps each rewritten\n * run in `<font style=\"vertical-align: inherit;\">`.\n * - Microsoft Translator (Edge) stamps `_msttexthash` / `_msthash` on every\n * element whose text it replaced.\n * - Any translator that does the job properly rewrites `<html lang>`, which is\n * compared against the `servedLang` the beacon read at install, before\n * hydration began.\n *\n * Never throws: it runs inside the error handler.\n */\nexport function pageTranslationSignal(\n doc: Document,\n servedLang: string | null | undefined,\n): string | undefined {\n try {\n const root = doc.documentElement\n if (!root) return undefined\n if (\n root.classList.contains('translated-ltr') ||\n root.classList.contains('translated-rtl') ||\n doc.querySelector('font[style*=\"vertical-align: inherit\"]')\n ) {\n return 'chrome'\n }\n if (doc.querySelector('[_msttexthash], [_msthash]')) return 'microsoft'\n const lang = root.getAttribute('lang')\n if (servedLang && lang && lang.toLowerCase() !== servedLang.toLowerCase()) {\n return `lang:${clamp(lang, 16)}`\n }\n } catch {\n // An unreadable document leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * A TRANSLATOR'S MARK, REMEMBERED FROM THE MOMENT IT APPEARS (AGL-3738).\n *\n * {@link pageTranslationSignal} read at the moment React reports a mismatch\n * is reading a document the translator no longer owns. React reports a\n * hydration mismatch only after it has client-rendered the root to recover,\n * and that render resets `<html>`'s attributes to the ones it rendered —\n * Chrome's `translated-ltr` class goes, `lang` returns to the served value —\n * and replaces every `<font>` wrapper and hash-stamped element with fresh\n * nodes. Measured 2026-10-12 by hydrating aglyn.com/ with a Chrome-style\n * rewrite of one heading: the report fired with `<html lang=\"en\">`, no class\n * and no `<font>` left anywhere. Six es-US #418s arrived that day on\n * aglyn.com/ and orbita-digital.aglyn.app/ (a Spanish site served as `en`);\n * one carried `lang:en` — the beacon had been installed after the translator\n * set `lang=\"es\"`, so React's reset was the only rewrite left to see, and the\n * same simulation reproduces exactly that label — and the other five carried\n * nothing. The same two pages hydrated cleanly under es-US, en-GB and ru\n * locales and time zones: nothing on them formats by the visitor's locale.\n *\n * So the mark is read when the translator LEAVES it: once at install, for a\n * translator that ran first, and then from a `MutationObserver` until one is\n * seen. A translator rewrites the page before React reaches it — that is what\n * makes it a mismatch — so its mutations are delivered before the report.\n *\n * Cheap on purpose: one observer on `<html>`'s own `class`/`lang`, and one on\n * the tree that looks only at added `<font>` elements and Microsoft's hash\n * attributes, both disconnected as soon as a mark is found. Never throws, and\n * a browser without `MutationObserver` gets the read-at-report behavior.\n */\nexport function watchPageTranslation(\n doc: Document,\n servedLang: string | null | undefined,\n): () => string | undefined {\n let seen = pageTranslationSignal(doc, servedLang)\n const observers: MutationObserver[] = []\n\n const stop = () => {\n for (const observer of observers.splice(0)) observer.disconnect()\n }\n\n const inspect = (records: MutationRecord[]) => {\n if (seen) return\n let relevant = false\n for (const record of records) {\n if (record.type === 'attributes') {\n relevant = true\n break\n }\n for (const node of Array.from(record.addedNodes)) {\n if (node.nodeName === 'FONT') {\n relevant = true\n break\n }\n }\n if (relevant) break\n }\n if (!relevant) return\n seen = pageTranslationSignal(doc, servedLang)\n if (seen) stop()\n }\n\n try {\n const root = doc.documentElement\n if (!seen && root && typeof MutationObserver === 'function') {\n const onRoot = new MutationObserver(inspect)\n onRoot.observe(root, { attributes: true, attributeFilter: ['class', 'lang'] })\n const onTree = new MutationObserver(inspect)\n onTree.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['_msttexthash', '_msthash'],\n })\n observers.push(onRoot, onTree)\n }\n } catch {\n // No observer: the report falls back to reading the document as it is.\n }\n\n return () => {\n try {\n // A rewrite in the same task as the report has not been delivered yet.\n for (const observer of observers) inspect(observer.takeRecords())\n return seen ?? pageTranslationSignal(doc, servedLang)\n } catch {\n return undefined\n }\n }\n}\n\n/**\n * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).\n *\n * `unhandledrejection` hands over whatever was passed to `reject`, and only\n * an `Error` brings a message and a stack. A rejection with anything else —\n * an API's `{ code, message }` object, a `DOMException` from an aborted\n * fetch, a bare string, a `Response` — used to be reported as the literal\n * words \"Unhandled promise rejection\" with no stack and no other field,\n * which names the handler rather than the failure. One such report arrived\n * on 2026-09-21 from the Besigner editor and there is nothing in it to act\n * on: no message, no stack, no code, nothing but the page.\n *\n * So the value is DESCRIBED rather than defaulted. In order of how much a\n * reader can do with it: the error's own message; a string as itself; a\n * `name`/`code`/`message` shape assembled from whatever of those it has —\n * the shape every Firebase, DOM and fetch rejection takes; the value's own\n * `toString` when it says more than `[object Object]`; and last its own\n * keys, which at least name what kind of thing rejected.\n *\n * Nothing here may throw: a getter on the reason can, `toString` can, and\n * a describer that throws inside the rejection handler loses the report it\n * was called to improve.\n */\nexport function describeRejectionReason(reason: unknown): string {\n try {\n if (reason instanceof Error && reason.message) return reason.message\n if (typeof reason === 'string') return reason\n if (reason === null || reason === undefined) {\n return `Unhandled promise rejection (${String(reason)})`\n }\n if (typeof reason !== 'object') {\n return `Unhandled promise rejection (${typeof reason}: ${String(reason)})`\n }\n const shape = reason as Record<string, unknown>\n const named = [\n shape['name'] ?? shape['code'] ?? '',\n shape['message'] ?? shape['statusText'] ?? '',\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n if (named.length) return named.join(': ')\n const printed = String(reason)\n if (printed && printed !== '[object Object]') return printed\n const keys = Object.keys(shape).slice(0, 12).join(', ')\n return keys\n ? `Unhandled promise rejection (object with ${keys})`\n : 'Unhandled promise rejection (empty object)'\n } catch {\n return 'Unhandled promise rejection (undescribable)'\n }\n}\n\n/**\n * How many frames a thrown error keeps, on a pageview that reports (AGL-3423).\n *\n * V8 keeps ten by default, and ten is not enough to find where a render loop\n * STARTED. The Besigner's one React #185 report (2026-09-24) was a\n * react-final-form field's `setState` inside final-form's `notify` loop, and\n * every one of its ten frames was inside those two libraries: the component\n * whose update fed the loop was cut off below them. Fifty reaches past a\n * form library's own frames to ours, and still fits the {@link MAX_STACK}\n * clamp at the length a minified frame line has.\n */\nexport const BEACON_STACK_TRACE_LIMIT = 50\n\n/**\n * Raises the engine's frame limit to `limit`, never lowers it. V8 and\n * JavaScriptCore read `Error.stackTraceLimit`; an engine that does not has no\n * such number, and is left without one rather than handed a property it\n * ignores. It applies to errors created after the call, which is why the\n * beacon sets it at install, at module scope of the app's first client chunk.\n */\nexport function raiseStackTraceLimit(limit: number): void {\n try {\n const engine = Error as ErrorConstructor & { stackTraceLimit?: number }\n const current = engine.stackTraceLimit\n if (typeof current !== 'number' || current >= limit) return\n engine.stackTraceLimit = limit\n } catch {\n // A frozen or exotic `Error` keeps its own limit.\n }\n}\n\nlet installed = false\n\n/**\n * The live beacon's `enqueue`, published for {@link reportHandledError}.\n *\n * Null until `installErrorBeacon` runs, and null forever on a surface that\n * never installs one — so a caught error reported from a page with no beacon\n * is silently dropped rather than throwing inside somebody's error handler.\n */\nlet publishEvent: ((event: ErrorBeaconEvent) => void) | null = null\n\n/**\n * Re-export so `error-beacon` stays the one name a caller has to know for\n * browser error reporting. The definition lives in its own module because a\n * boundary that imports it must not pull the installer below into a chunk\n * every visitor downloads.\n */\nexport { redispatchCaughtError } from './redispatch-caught-error'\n\n/**\n * Report an error the code ALREADY CAUGHT.\n *\n * The two window handlers below see only what nothing caught. A `catch` that\n * swallows its error is invisible to them by construction, which is the whole\n * failure mode this exists for: a background write that can only fail\n * silently is one that stays broken for as long as nobody thinks to look.\n *\n * It rides the same queue as an uncaught error, which is the point — the\n * dedupe collapses a failure that repeats on every edit into one report, and\n * `maxPerPage` bounds it, so a call site in a loop cannot turn a broken\n * feature into a flood. Reporting is never worth an exception of its own, so\n * every path here returns rather than throws.\n */\nexport function reportHandledError(\n error: unknown,\n options?: { kind?: string },\n): void {\n try {\n if (!publishEvent) return\n const thrown = error as Error | undefined\n const message = clamp(\n thrown?.message ?? (typeof error === 'string' ? error : ''),\n MAX_MESSAGE,\n )\n if (!message) return\n publishEvent({\n kind: clamp(options?.kind ?? 'handled', 32),\n message,\n stack: thrown?.stack ? clamp(thrown.stack, MAX_STACK) : undefined,\n url: scrubUrl(window.location.href),\n })\n } catch {\n // An observer that throws is worse than one that misses an event.\n }\n}\n\n/**\n * Installs the handlers once per page. Safe to call from module scope of a\n * client bundle: it no-ops during SSR and on repeat calls.\n */\nexport function installErrorBeacon(options?: ErrorBeaconOptions): void {\n if (typeof window === 'undefined' || installed) return\n installed = true\n\n const endpoint = options?.endpoint ?? '/api/errors'\n const sampleRate = options?.sampleRate ?? 1\n const maxPerPage = options?.maxPerPage ?? 10\n // Only a sampled pageview REPORTS. Every pageview recovers a stale build\n // (the rejection handler below), because a tab left on a dead deploy is a\n // visitor's problem whether or not its errors are counted.\n const sampled = Math.random() < sampleRate\n // The document's language as SERVED, read before hydration and before any\n // translator has had a chance to rewrite it (`pageTranslationSignal`).\n const servedLang = document.documentElement?.getAttribute('lang') ?? null\n // Watched from now, because React's recovery erases the translator's marks\n // before a mismatch is reported (`watchPageTranslation`, AGL-3738).\n const translation = watchPageTranslation(document, servedLang)\n if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT)\n // Classified once, here, and only the family is kept: the user-agent\n // string itself never reaches the queue (AGL-3732).\n let browser: BrowserFamily = 'other'\n try {\n browser = browserFamily(navigator.userAgent)\n } catch {\n // An unreadable navigator is just `other`.\n }\n\n const seen = new Set<string>()\n let queued: ErrorBeaconEvent[] = []\n let sent = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const flush = () => {\n timer = null\n if (!queued.length) return\n const batch = queued\n queued = []\n try {\n const body = JSON.stringify({ events: batch })\n // sendBeacon survives unloads and never blocks; keepalive fetch is the\n // fallback for browsers that refuse the payload.\n if (!navigator.sendBeacon?.(endpoint, body)) {\n void fetch(endpoint, {\n method: 'POST',\n body,\n keepalive: true,\n headers: { 'Content-Type': 'application/json' },\n }).catch((): undefined => undefined)\n }\n } catch {\n // Reporting never breaks the page.\n }\n }\n\n const enqueue = (event: ErrorBeaconEvent) => {\n if (!sampled || sent >= maxPerPage) return\n // Message + first stack line: enough identity to collapse a render loop\n // without collapsing distinct errors that share a message.\n const key = `${event.message}\\x00${(event.stack ?? '').split('\\n', 2).join('\\n')}`\n if (seen.has(key)) return\n seen.add(key)\n sent += 1\n queued.push({ ...event, browser })\n if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS)\n }\n // Null on an unsampled pageview, so it reports nothing by either door\n // rather than one by one and none by the other.\n publishEvent = sampled ? enqueue : null\n\n window.addEventListener('error', (event) => {\n try {\n const error = event.error as Error | undefined\n const message = clamp(error?.message ?? event.message, MAX_MESSAGE)\n // Cross-origin scripts yield an opaque \"Script error.\" with nothing\n // actionable attached; reporting it would only create a noisy group.\n if (!message || message === 'Script error.') return\n if (isBenignBrowserNotice(message)) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = error?.stack ? String(error.stack) : undefined\n // A webview or extension that evaluated its own code into our page —\n // see `isInjectedThirdPartyFrame`. Nothing we ship can be changed in\n // response, so it is dropped on the same test as `Script error.`.\n // Judged on the WHOLE stack, before the clamp: a frame of ours past the\n // clamp still makes the error ours.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n const stack = fullStack ? clamp(fullStack, MAX_STACK) : undefined\n // MARKED, not dropped: a translator causes a hydration mismatch and so\n // does a real render divergence, and a recovered fault is still a\n // fault. Only a rate tells either apart from noise — and the\n // translator's own mark on the DOM, when it left one.\n const kind =\n recoveredErrorKind(error ?? { message }) ??\n (isHydrationMismatch(message) ? 'hydration' : 'error')\n const translated = kind === 'hydration' ? translation() : undefined\n // A mismatch on a page a translator rewrote is the translator's, not\n // ours (AGL-3738): React compared our render with text we did not\n // write, recovered, and the visitor has a working page. Nothing we ship\n // can change in response, so it goes the way of an injected stack. The\n // mark is evidence on the document, not a guess about the visitor — an\n // unmarked mismatch is still reported, and still the one to read.\n if (translated) return\n enqueue({\n kind,\n message,\n stack,\n source: scrubUrl(event.filename) || undefined,\n line: typeof event.lineno === 'number' ? event.lineno : undefined,\n col: typeof event.colno === 'number' ? event.colno : undefined,\n url: pageUrl,\n ...(kind === 'hydration' && navigator.language\n ? { language: clamp(navigator.language, 16) }\n : {}),\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n window.addEventListener('unhandledrejection', (event) => {\n try {\n // An `import()` of a chunk this document names and the origin dropped,\n // which no boundary saw. Recovered by a reload once the tab is hidden,\n // and, like a boundary's reload, not reported: see\n // `recoverStaleBuildRejection`. A tab whose recovery is spent still\n // reports it, as `chunk-load`.\n if (recoverStaleBuildRejection(event.reason)) return\n // A cancelled request, ours or the browser's on teardown: see\n // `isAbortRejection` for why this is dropped and how narrowly.\n if (isAbortRejection(event.reason)) return\n const reason = event.reason as Error | undefined\n const message = clamp(describeRejectionReason(event.reason), MAX_MESSAGE)\n if (!message) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = reason?.stack ? String(reason.stack) : undefined\n // The same injected-code drop as an uncaught error: a rejection raised\n // by a devtools snippet or an automation driver's `evaluate` is no more\n // ours for having been a promise.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n enqueue({\n kind: recoveredErrorKind(event.reason) ?? 'unhandledrejection',\n message,\n stack: fullStack ? clamp(fullStack, MAX_STACK) : undefined,\n url: pageUrl,\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n // A batch waiting out its debounce when the page hides would be lost;\n // `pagehide`/`visibilitychange` are the last reliable moments to send.\n window.addEventListener('pagehide', flush)\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush()\n })\n}\n"],"names":["browserFamily","isCrossPoolDesync","isForeignScriptUrl","stackFrameUrls","isStaleBuildError","recoverStaleBuildRejection","MAX_MESSAGE","MAX_STACK","FLUSH_DELAY_MS","scrubUrl","raw","url","URL","String","window","location","href","origin","pathname","clamp","value","max","slice","isInjectedThirdPartyFrame","stack","documentUrl","urls","length","anonymousScriptFrameCount","every","STACK_FRAME_LINE","ANONYMOUS_SCRIPT_POSITION","count","line","split","test","BENIGN_BROWSER_NOTICE","isBenignBrowserNotice","message","isAbortRejection","reason","name","AUTH_DESYNC_KIND","CHUNK_LOAD_KIND","recoveredErrorKind","error","undefined","HYDRATION_MINIFIED","HYDRATION_TEXT","isHydrationMismatch","pageTranslationSignal","doc","servedLang","root","documentElement","classList","contains","querySelector","lang","getAttribute","toLowerCase","watchPageTranslation","seen","observers","stop","observer","splice","disconnect","inspect","records","relevant","record","type","node","Array","from","addedNodes","nodeName","MutationObserver","onRoot","observe","attributes","attributeFilter","onTree","childList","subtree","push","takeRecords","describeRejectionReason","shape","Error","named","map","part","trim","filter","Boolean","join","printed","keys","Object","BEACON_STACK_TRACE_LIMIT","raiseStackTraceLimit","limit","engine","current","stackTraceLimit","installed","publishEvent","redispatchCaughtError","reportHandledError","options","thrown","kind","installErrorBeacon","document","endpoint","sampleRate","maxPerPage","sampled","Math","random","translation","browser","navigator","userAgent","Set","queued","sent","timer","flush","batch","body","JSON","stringify","events","sendBeacon","fetch","method","keepalive","headers","catch","enqueue","event","key","has","add","setTimeout","addEventListener","pageUrl","fullStack","translated","source","filename","lineno","col","colno","language","visibilityState"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/error-beacon.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 * First-party browser error beacon (AGL-1538).\n *\n * Crashlytics does not exist for web, and adding a third-party error vendor\n * is a subprocessor-list decision that was explicitly deferred. This is the\n * first-party alternative: `window.onerror` / `unhandledrejection` handlers\n * that batch uncaught failures to the app's own `/api/errors` route, which\n * forwards them to Google Cloud Error Reporting — a processor already on the\n * list, alerting through the GCP stack that already pages (AGL-1502).\n *\n * Design constraints, in order:\n *\n * - **It must never become the outage.** Everything is wrapped; a failure to\n * report is silence, not a thrown error. Batches are capped per page so an\n * error thrown in a loop cannot DDoS our own API — after `maxPerPage`\n * events the beacon disarms for the rest of the pageview.\n * - **No PII.** URLs are scrubbed to origin + pathname (query strings and\n * fragments carry tokens and search terms), no user id, no cookies —\n * the payload is the error text and where in the CODE it happened.\n * Cross-origin scripts surface as the browser's opaque \"Script error.\"\n * with no stack; those are dropped since they cannot be acted on.\n * - **Dedupe.** One page throwing the same error on every frame reports it\n * once.\n *\n * Framework-free on purpose: the console mounts it from its root layout, the\n * tenant runtime from its own, and neither needs React here. Deliberately NO\n * 'use client' directive — inside this shared lib the directive forks the\n * module graph (AGL-52); every importer is already a client module.\n */\n\nimport { browserFamily, type BrowserFamily } from './browser-family'\nimport { isCrossPoolDesync } from './cross-pool-desync'\nimport { isForeignScriptUrl, stackFrameUrls } from './foreign-script'\nimport { isStaleBuildError, recoverStaleBuildRejection } from './stale-build-error'\n\nexport interface ErrorBeaconEvent {\n /**\n * Which handler caught it — 'error' | 'unhandledrejection' — or the\n * family it was labeled with: 'hydration', {@link AUTH_DESYNC_KIND},\n * {@link CHUNK_LOAD_KIND}, or a caller's own kind for a handled error.\n * Lands at `jsonPayload.kind`, where the alert policies filter on it.\n */\n kind: string\n /** Error message, clamped. */\n message: string\n /** Stack trace when the thrown value carried one, clamped. */\n stack?: string\n /** Script URL for stackless `window.onerror` events, scrubbed. */\n source?: string\n line?: number\n col?: number\n /** Page URL, scrubbed to origin + pathname. */\n url: string\n /**\n * On a `hydration` event from builds before AGL-3738 only: the page\n * translator whose rewrite was visible in the document when React gave up\n * — see {@link pageTranslationSignal}. Today's beacon drops a mismatch on a\n * translated page instead of sending it, so the field stays only for what\n * older tabs still report.\n */\n translated?: string\n /** On a `hydration` event only: `navigator.language`, clamped. */\n language?: string\n /**\n * On every event: the coarse browser family — one of seven, see\n * {@link browserFamily} (AGL-3732). Never the user agent, never a version.\n * Stamped by the queue, so a caller never sets it.\n */\n browser?: BrowserFamily\n}\n\nexport interface ErrorBeaconOptions {\n /** POST target; same-origin. Default `/api/errors`. */\n endpoint?: string\n /** 0..1 — fraction of pageviews that report at all. Default 1. */\n sampleRate?: number\n /** Max events reported per pageview before the beacon disarms. */\n maxPerPage?: number\n}\n\nconst MAX_MESSAGE = 1_024\nconst MAX_STACK = 8_192\nconst FLUSH_DELAY_MS = 2_000\n\n/** Origin + pathname only — query strings and fragments never leave. */\nfunction scrubUrl(raw: string | null | undefined): string {\n if (!raw) return ''\n try {\n const url = new URL(String(raw), window.location.href)\n return `${url.origin}${url.pathname}`\n } catch {\n return ''\n }\n}\n\nfunction clamp(value: unknown, max: number): string {\n return String(value ?? '').slice(0, max)\n}\n\n/**\n * Was this thrown by a script somebody ELSE evaluated into our page?\n *\n * Three tells, all read off the frames' positions and none off a function\n * name — a name would be a denylist needing a new entry per vendor.\n *\n * **Every frame is the DOCUMENT.** Measured 2026-09-02 (AGL-2523) on\n * aglyn.com/pricing, reported as an error of ours:\n *\n * sendDataToNative@https://aglyn.com/pricing:1:1325\n * sendPageHideMessage@https://aglyn.com/pricing:1:4139\n * @https://aglyn.com/pricing:1:6257\n *\n * `sendDataToNative` is the Meta in-app browser's native bridge, injected by\n * the Facebook/Instagram webview. In WebKit's format the code was evaluated\n * inline, so every frame is the document rather than a script we served.\n *\n * **Every frame is under a FOREIGN scheme.** Measured 2026-09-10 (AGL-2786) on\n * aglyn.com, the same bridge in the Android webview:\n *\n * at sendDataToNative (iabjs://navigation_performance_logger_android:1:10632)\n *\n * That webview names its scripts under its own `iabjs://` scheme, and\n * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`.\n *\n * **Every frame is a script with NO URL** (AGL-3423). Measured on the\n * sign-up page and on aglyn.com/solutions/agencies:\n *\n * at eval (<anonymous>)\n * at predicate (eval at evaluate (:234:30), <anonymous>:11:37)\n *\n * at <anonymous>:1:54\n * at <anonymous>:1:64\n *\n * An automation driver's `evaluate` and a snippet pasted into devtools. Every\n * script we serve has a URL — a chunk, a CDN asset, or the document for an\n * inline one — so a frame whose position is `<anonymous>:line:col`, or code\n * `eval`'d by such a frame, was not served by us. See\n * {@link anonymousScriptFrameCount} for exactly which frames count.\n *\n * ⚑ Deliberately `every`, and deliberately compared against the document\n * rather than against our asset path. A rule like \"no frame under\n * `/_next/static/`\" would delete EVERY error from a self-hosted deployment\n * that serves its assets from a CDN, because none of its frames would match.\n * Comparing to the document cannot fail that way: a CDN frame is not the\n * document either, so it keeps the report.\n *\n * A stack may mix the tells frame by frame. Nothing we ship can be fixed in\n * response to any of them, which is the same test the opaque `Script error.`\n * cut already applies.\n *\n * The cost is honest and small: a throw from one of our own inline\n * bootstrap scripts looks the same and is dropped with it. That trade buys a\n * rule that needs no maintenance as new webviews and extensions appear.\n */\nexport function isInjectedThirdPartyFrame(\n stack: string,\n documentUrl: string,\n): boolean {\n const urls = stackFrameUrls(stack)\n // No frames parsed is not evidence of anything — keep the report. A stack\n // with neither a URL nor an anonymous-script frame is exactly that: an\n // empty frame list must never read as \"every frame is injected\".\n if (!urls.length && !anonymousScriptFrameCount(stack)) return false\n // Anonymous frames need no test of their own — they are injected by\n // definition — so `every` over the URLs is `every` over the whole stack.\n // One URL of ours, even as the origin of an `eval`, keeps the report.\n return urls.every(\n (url) => isForeignScriptUrl(url) || scrubUrl(url) === documentUrl,\n )\n}\n\n/** A frame line, in V8's `at …` format or WebKit/Firefox's `fn@location`. */\nconst STACK_FRAME_LINE = /^\\s*at\\s|@/\n\n/**\n * A position in a script that has no URL.\n *\n * - `<anonymous>:line:col` — V8's name for such a script, both for a top-level\n * snippet and as the position of code run through `eval`/`new Function`.\n * - `eval at …` — V8's origin of `eval`'d code. When OUR script ran the eval\n * its URL is inside this origin, `stackFrameUrls` reads it, and the report\n * is kept; that is how the marketing plugin's `runJs` step stays reported.\n * - `debugger eval code:line:col` — Firefox's devtools console, in the\n * `fn@location` format WebKit also uses.\n *\n * ⚑ A BARE `(<anonymous>)` with no line and column is NOT one of these. It is\n * how V8 prints a builtin — `at Array.forEach (<anonymous>)`,\n * `at JSON.parse (<anonymous>)` — and a builtin is called by whoever called\n * it, including us.\n */\nconst ANONYMOUS_SCRIPT_POSITION =\n /<anonymous>:\\d+:\\d+|\\beval at\\s|\\bdebugger eval code:\\d+:\\d+/\n\n/** How many frames of this stack are in a script with no URL at all. */\nexport function anonymousScriptFrameCount(stack: string): number {\n let count = 0\n for (const line of stack.split('\\n')) {\n if (STACK_FRAME_LINE.test(line) && ANONYMOUS_SCRIPT_POSITION.test(line)) {\n count += 1\n }\n }\n return count\n}\n\n/**\n * Browser notices that are not failures, and that no fix of ours could act on.\n *\n * `ResizeObserver loop completed with undelivered notifications` (and its older\n * wording, `ResizeObserver loop limit exceeded`) is the browser saying it\n * deferred some resize callbacks to the next frame. Nothing threw: the event\n * carries no error object and no stack, and every observer is still\n * delivered. It arrived from the Besigner on 2026-09-26 and paged. Dropped, on\n * the browser's exact wording.\n */\nconst BENIGN_BROWSER_NOTICE =\n /^(?:Uncaught )?(?:Error: )?ResizeObserver loop (?:completed with undelivered notifications|limit exceeded)/\n\nexport function isBenignBrowserNotice(message: string): boolean {\n return BENIGN_BROWSER_NOTICE.test(message)\n}\n\n/**\n * Whether the document is a page a visitor navigated to over the web\n * (AGL-3759).\n *\n * Only `http:` and `https:` qualify. A document at `about:blank` or\n * `about:srcdoc` is a COPY of our page: a translator extension or a site that\n * frames our markup renders it into a fresh document with no URL of its own,\n * and our scripts run there too. Next.js's app router then throws\n * `Failed to construct 'URL': Invalid URL` resolving its canonical URL against\n * `window.location.href`, which no code of ours can prevent and no visitor on\n * the real page ever sees. `file:` (a saved page) and an extension's own\n * scheme are copies in the same sense.\n *\n * A missing or unreadable location is not a web document either: nothing\n * from it can be traced to a page we serve.\n */\nexport function isWebDocument(\n location: { protocol?: string } | null | undefined,\n): boolean {\n try {\n const protocol = location?.protocol\n return protocol === 'http:' || protocol === 'https:'\n } catch {\n return false\n }\n}\n\n/**\n * A rejection that says only \"this was cancelled\" (AGL-3734).\n *\n * `AbortError` is what a request rejects with when its `AbortSignal` fires —\n * a deadline we set, a component that unmounted, a search the author typed\n * past — and what the browser rejects every in-flight fetch with when the\n * document goes away under it. On 2026-10-10 one arrived from\n * `/edit-access`, the page a published site opens in a popup or a hidden\n * iframe and then CLOSES itself the moment it has its answer. The report had\n * no stack and no message past the name, so there was no call site to fix:\n * the page's own mint was already caught, and whatever else the console shell\n * had in flight was cut off by the site ending the page, which is the flow\n * working.\n *\n * Why the classifier rather than each call site: every request on every\n * console page can be the one in flight when a page is torn down, and the\n * abort is the intended outcome in every case. There is nothing a fix of ours\n * could change, and nothing in the report to locate it by.\n *\n * Narrow on purpose. Only `name === 'AbortError'`, and only for an UNHANDLED\n * REJECTION — not `TimeoutError`, which is `AbortSignal.timeout` saying a\n * deadline was MISSED (a real slowness), and not an uncaught `error` event or\n * a `reportHandledError` call, where somebody chose to report it.\n */\nexport function isAbortRejection(reason: unknown): boolean {\n try {\n return (\n typeof reason === 'object' &&\n reason !== null &&\n (reason as { name?: unknown }).name === 'AbortError'\n )\n } catch {\n // A getter that throws leaves the rejection reported, never dropped.\n return false\n }\n}\n\n/**\n * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to\n * another account pool (AGL-3280). The console recovers the tab by reloading\n * it once it is hidden and deliberately leaves the rejection unhandled so it\n * is still seen; this label is what lets it be seen by RATE instead of paging\n * on each of the several rejections one storage event raises.\n */\nexport const AUTH_DESYNC_KIND = 'auth-desync'\n\n/**\n * `kind` for a chunk the document names and the origin no longer serves\n * (AGL-3279). Every boundary reloads once for it; what still arrives is the\n * failure the reload did not cure. One of those is a visitor on a bad\n * connection; a burst of them is a broken deploy, which only a rate can tell.\n */\nexport const CHUNK_LOAD_KIND = 'chunk-load'\n\n/**\n * The label for a fault we RECOVER from and still want to see, or undefined.\n *\n * These could be ours, so unlike an injected stack they are never dropped:\n * they are written at full severity under their own `kind`, the per-entry\n * policy excludes that kind, and a rate policy watches it — the same split\n * `hydration` already has.\n */\nexport function recoveredErrorKind(error: unknown): string | undefined {\n try {\n if (isCrossPoolDesync(error)) return AUTH_DESYNC_KIND\n if (isStaleBuildError(error)) return CHUNK_LOAD_KIND\n } catch {\n // A getter that throws leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * React's hydration-mismatch family, which a page TRANSLATOR causes and a\n * real render divergence also causes.\n *\n * These are `onRecoverableError` reports, not crashes: React has already\n * re-rendered on the client and the visitor has a working page. They arrive\n * here as uncaught errors only because React re-throws them to `reportError`.\n *\n * They are marked rather than dropped, and the distinction is the whole\n * design. Dropping would hide a real hydration regression, which is a genuine\n * and expensive bug; the entries stay in `client-errors` at full severity and\n * still group in Error Reporting. What the mark buys is that the log-match\n * policy can stop paging on a single report, while a rate-based policy on the\n * same mark still catches a systemic one.\n *\n * ⚠️ THE MARK IS NOT A VERDICT, and the one cluster this repo has measured is\n * why. Eight #418 reports arrived inside a nine-minute window on 2026-09-01,\n * on one build, and the three pages they came from were exactly the three\n * carrying a form — `/`, `/pricing` and the solutions pages reported nothing\n * in the same window, which no browser-side rewrite would respect. A placed\n * form was expanding inside its own grafted subtree, so every page carrying\n * one shipped six nested `<form>` elements; nested forms are invalid HTML, the\n * parser dropped the inner ones, and the client tree disagreed with the server\n * tree. The visible cost was a submission the route never received, because\n * the native submit came from an unhandled ancestor and the browser fell back\n * to a GET with the visitor's name, email and message in the address bar. The\n * reports stopped because the nesting was fixed, roughly a quarter of an hour\n * after the last one.\n *\n * So a rate low enough not to page is a reason to READ the entry — the page\n * it names, and whether the set of pages has a shape — never a reason to\n * assume somebody's extension.\n */\nconst HYDRATION_MINIFIED = /Minified React error #(?:418|423|425)\\b/\nconst HYDRATION_TEXT =\n /Hydration failed because|Text content does not match server-rendered HTML|There was an error while hydrating/\n\nexport function isHydrationMismatch(message: string): boolean {\n return HYDRATION_MINIFIED.test(message) || HYDRATION_TEXT.test(message)\n}\n\n/**\n * WHICH TRANSLATOR rewrote this page's text, read off the document, or\n * undefined when none visibly did.\n *\n * A hydration report on its own cannot say whether React disagreed with our\n * server render or with a translator that rewrote the server's text before\n * React reached it, and the two look identical in `client-errors`: the\n * minified #418, a stack inside the react-dom chunk, the page URL, nothing\n * else. aglyn.com/ carried eight of them between 2026-09-22 and 2026-10-10.\n * The visitor at 2026-10-09 22:28Z went on to sign up two minutes later with\n * Chrome Translate rewriting the console (the `<font>` crash #1342 fixed); the\n * two on 2026-09-22/23 were desktop Chrome OS in Hyderabad, while every other\n * Indian, Pakistani and Turkish home-page visit in GA that fortnight hydrated\n * without a report — so neither the build nor the time zone was the variable,\n * and the beacon had nothing to say which visitor it was.\n *\n * The signatures are the ones each translator leaves on the DOM it rewrote:\n *\n * - Chrome (and every Chromium that ships Google Translate) sets\n * `translated-ltr` / `translated-rtl` on `<html>` and wraps each rewritten\n * run in `<font style=\"vertical-align: inherit;\">`.\n * - Microsoft Translator (Edge) stamps `_msttexthash` / `_msthash` on every\n * element whose text it replaced.\n * - Any translator that does the job properly rewrites `<html lang>`, which is\n * compared against the `servedLang` the beacon read at install, before\n * hydration began.\n *\n * Never throws: it runs inside the error handler.\n */\nexport function pageTranslationSignal(\n doc: Document,\n servedLang: string | null | undefined,\n): string | undefined {\n try {\n const root = doc.documentElement\n if (!root) return undefined\n if (\n root.classList.contains('translated-ltr') ||\n root.classList.contains('translated-rtl') ||\n doc.querySelector('font[style*=\"vertical-align: inherit\"]')\n ) {\n return 'chrome'\n }\n if (doc.querySelector('[_msttexthash], [_msthash]')) return 'microsoft'\n const lang = root.getAttribute('lang')\n if (servedLang && lang && lang.toLowerCase() !== servedLang.toLowerCase()) {\n return `lang:${clamp(lang, 16)}`\n }\n } catch {\n // An unreadable document leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * A TRANSLATOR'S MARK, REMEMBERED FROM THE MOMENT IT APPEARS (AGL-3738).\n *\n * {@link pageTranslationSignal} read at the moment React reports a mismatch\n * is reading a document the translator no longer owns. React reports a\n * hydration mismatch only after it has client-rendered the root to recover,\n * and that render resets `<html>`'s attributes to the ones it rendered —\n * Chrome's `translated-ltr` class goes, `lang` returns to the served value —\n * and replaces every `<font>` wrapper and hash-stamped element with fresh\n * nodes. Measured 2026-10-12 by hydrating aglyn.com/ with a Chrome-style\n * rewrite of one heading: the report fired with `<html lang=\"en\">`, no class\n * and no `<font>` left anywhere. Six es-US #418s arrived that day on\n * aglyn.com/ and orbita-digital.aglyn.app/ (a Spanish site served as `en`);\n * one carried `lang:en` — the beacon had been installed after the translator\n * set `lang=\"es\"`, so React's reset was the only rewrite left to see, and the\n * same simulation reproduces exactly that label — and the other five carried\n * nothing. The same two pages hydrated cleanly under es-US, en-GB and ru\n * locales and time zones: nothing on them formats by the visitor's locale.\n *\n * So the mark is read when the translator LEAVES it: once at install, for a\n * translator that ran first, and then from a `MutationObserver` until one is\n * seen. A translator rewrites the page before React reaches it — that is what\n * makes it a mismatch — so its mutations are delivered before the report.\n *\n * Cheap on purpose: one observer on `<html>`'s own `class`/`lang`, and one on\n * the tree that looks only at added `<font>` elements and Microsoft's hash\n * attributes, both disconnected as soon as a mark is found. Never throws, and\n * a browser without `MutationObserver` gets the read-at-report behavior.\n */\nexport function watchPageTranslation(\n doc: Document,\n servedLang: string | null | undefined,\n): () => string | undefined {\n let seen = pageTranslationSignal(doc, servedLang)\n const observers: MutationObserver[] = []\n\n const stop = () => {\n for (const observer of observers.splice(0)) observer.disconnect()\n }\n\n const inspect = (records: MutationRecord[]) => {\n if (seen) return\n let relevant = false\n for (const record of records) {\n if (record.type === 'attributes') {\n relevant = true\n break\n }\n for (const node of Array.from(record.addedNodes)) {\n if (node.nodeName === 'FONT') {\n relevant = true\n break\n }\n }\n if (relevant) break\n }\n if (!relevant) return\n seen = pageTranslationSignal(doc, servedLang)\n if (seen) stop()\n }\n\n try {\n const root = doc.documentElement\n if (!seen && root && typeof MutationObserver === 'function') {\n const onRoot = new MutationObserver(inspect)\n onRoot.observe(root, { attributes: true, attributeFilter: ['class', 'lang'] })\n const onTree = new MutationObserver(inspect)\n onTree.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['_msttexthash', '_msthash'],\n })\n observers.push(onRoot, onTree)\n }\n } catch {\n // No observer: the report falls back to reading the document as it is.\n }\n\n return () => {\n try {\n // A rewrite in the same task as the report has not been delivered yet.\n for (const observer of observers) inspect(observer.takeRecords())\n return seen ?? pageTranslationSignal(doc, servedLang)\n } catch {\n return undefined\n }\n }\n}\n\n/**\n * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).\n *\n * `unhandledrejection` hands over whatever was passed to `reject`, and only\n * an `Error` brings a message and a stack. A rejection with anything else —\n * an API's `{ code, message }` object, a `DOMException` from an aborted\n * fetch, a bare string, a `Response` — used to be reported as the literal\n * words \"Unhandled promise rejection\" with no stack and no other field,\n * which names the handler rather than the failure. One such report arrived\n * on 2026-09-21 from the Besigner editor and there is nothing in it to act\n * on: no message, no stack, no code, nothing but the page.\n *\n * So the value is DESCRIBED rather than defaulted. In order of how much a\n * reader can do with it: the error's own message; a string as itself; a\n * `name`/`code`/`message` shape assembled from whatever of those it has —\n * the shape every Firebase, DOM and fetch rejection takes; the value's own\n * `toString` when it says more than `[object Object]`; and last its own\n * keys, which at least name what kind of thing rejected.\n *\n * Nothing here may throw: a getter on the reason can, `toString` can, and\n * a describer that throws inside the rejection handler loses the report it\n * was called to improve.\n */\nexport function describeRejectionReason(reason: unknown): string {\n try {\n if (reason instanceof Error && reason.message) return reason.message\n if (typeof reason === 'string') return reason\n if (reason === null || reason === undefined) {\n return `Unhandled promise rejection (${String(reason)})`\n }\n if (typeof reason !== 'object') {\n return `Unhandled promise rejection (${typeof reason}: ${String(reason)})`\n }\n const shape = reason as Record<string, unknown>\n const named = [\n shape['name'] ?? shape['code'] ?? '',\n shape['message'] ?? shape['statusText'] ?? '',\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n if (named.length) return named.join(': ')\n const printed = String(reason)\n if (printed && printed !== '[object Object]') return printed\n const keys = Object.keys(shape).slice(0, 12).join(', ')\n return keys\n ? `Unhandled promise rejection (object with ${keys})`\n : 'Unhandled promise rejection (empty object)'\n } catch {\n return 'Unhandled promise rejection (undescribable)'\n }\n}\n\n/**\n * How many frames a thrown error keeps, on a pageview that reports (AGL-3423).\n *\n * V8 keeps ten by default, and ten is not enough to find where a render loop\n * STARTED. The Besigner's one React #185 report (2026-09-24) was a\n * react-final-form field's `setState` inside final-form's `notify` loop, and\n * every one of its ten frames was inside those two libraries: the component\n * whose update fed the loop was cut off below them. Fifty reaches past a\n * form library's own frames to ours, and still fits the {@link MAX_STACK}\n * clamp at the length a minified frame line has.\n */\nexport const BEACON_STACK_TRACE_LIMIT = 50\n\n/**\n * Raises the engine's frame limit to `limit`, never lowers it. V8 and\n * JavaScriptCore read `Error.stackTraceLimit`; an engine that does not has no\n * such number, and is left without one rather than handed a property it\n * ignores. It applies to errors created after the call, which is why the\n * beacon sets it at install, at module scope of the app's first client chunk.\n */\nexport function raiseStackTraceLimit(limit: number): void {\n try {\n const engine = Error as ErrorConstructor & { stackTraceLimit?: number }\n const current = engine.stackTraceLimit\n if (typeof current !== 'number' || current >= limit) return\n engine.stackTraceLimit = limit\n } catch {\n // A frozen or exotic `Error` keeps its own limit.\n }\n}\n\nlet installed = false\n\n/**\n * The live beacon's `enqueue`, published for {@link reportHandledError}.\n *\n * Null until `installErrorBeacon` runs, and null forever on a surface that\n * never installs one — so a caught error reported from a page with no beacon\n * is silently dropped rather than throwing inside somebody's error handler.\n */\nlet publishEvent: ((event: ErrorBeaconEvent) => void) | null = null\n\n/**\n * Re-export so `error-beacon` stays the one name a caller has to know for\n * browser error reporting. The definition lives in its own module because a\n * boundary that imports it must not pull the installer below into a chunk\n * every visitor downloads.\n */\nexport { redispatchCaughtError } from './redispatch-caught-error'\n\n/**\n * Report an error the code ALREADY CAUGHT.\n *\n * The two window handlers below see only what nothing caught. A `catch` that\n * swallows its error is invisible to them by construction, which is the whole\n * failure mode this exists for: a background write that can only fail\n * silently is one that stays broken for as long as nobody thinks to look.\n *\n * It rides the same queue as an uncaught error, which is the point — the\n * dedupe collapses a failure that repeats on every edit into one report, and\n * `maxPerPage` bounds it, so a call site in a loop cannot turn a broken\n * feature into a flood. Reporting is never worth an exception of its own, so\n * every path here returns rather than throws.\n */\nexport function reportHandledError(\n error: unknown,\n options?: { kind?: string },\n): void {\n try {\n if (!publishEvent) return\n const thrown = error as Error | undefined\n const message = clamp(\n thrown?.message ?? (typeof error === 'string' ? error : ''),\n MAX_MESSAGE,\n )\n if (!message) return\n publishEvent({\n kind: clamp(options?.kind ?? 'handled', 32),\n message,\n stack: thrown?.stack ? clamp(thrown.stack, MAX_STACK) : undefined,\n url: scrubUrl(window.location.href),\n })\n } catch {\n // An observer that throws is worse than one that misses an event.\n }\n}\n\n/**\n * Installs the handlers once per page. Safe to call from module scope of a\n * client bundle: it no-ops during SSR and on repeat calls.\n */\nexport function installErrorBeacon(options?: ErrorBeaconOptions): void {\n if (typeof window === 'undefined' || installed) return\n installed = true\n\n const endpoint = options?.endpoint ?? '/api/errors'\n const sampleRate = options?.sampleRate ?? 1\n const maxPerPage = options?.maxPerPage ?? 10\n // Only a sampled pageview REPORTS. Every pageview recovers a stale build\n // (the rejection handler below), because a tab left on a dead deploy is a\n // visitor's problem whether or not its errors are counted.\n const sampled = Math.random() < sampleRate\n // A copy of the page in a document that is not on the web reports nothing\n // by either door, whatever it throws: see `isWebDocument`.\n const reporting = sampled && isWebDocument(window.location)\n // The document's language as SERVED, read before hydration and before any\n // translator has had a chance to rewrite it (`pageTranslationSignal`).\n const servedLang = document.documentElement?.getAttribute('lang') ?? null\n // Watched from now, because React's recovery erases the translator's marks\n // before a mismatch is reported (`watchPageTranslation`, AGL-3738).\n const translation = watchPageTranslation(document, servedLang)\n if (reporting) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT)\n // Classified once, here, and only the family is kept: the user-agent\n // string itself never reaches the queue (AGL-3732).\n let browser: BrowserFamily = 'other'\n try {\n browser = browserFamily(navigator.userAgent)\n } catch {\n // An unreadable navigator is just `other`.\n }\n\n const seen = new Set<string>()\n let queued: ErrorBeaconEvent[] = []\n let sent = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const flush = () => {\n timer = null\n if (!queued.length) return\n const batch = queued\n queued = []\n try {\n const body = JSON.stringify({ events: batch })\n // sendBeacon survives unloads and never blocks; keepalive fetch is the\n // fallback for browsers that refuse the payload.\n if (!navigator.sendBeacon?.(endpoint, body)) {\n void fetch(endpoint, {\n method: 'POST',\n body,\n keepalive: true,\n headers: { 'Content-Type': 'application/json' },\n }).catch((): undefined => undefined)\n }\n } catch {\n // Reporting never breaks the page.\n }\n }\n\n const enqueue = (event: ErrorBeaconEvent) => {\n if (!reporting || sent >= maxPerPage) return\n // Message + first stack line: enough identity to collapse a render loop\n // without collapsing distinct errors that share a message.\n const key = `${event.message}\\x00${(event.stack ?? '').split('\\n', 2).join('\\n')}`\n if (seen.has(key)) return\n seen.add(key)\n sent += 1\n queued.push({ ...event, browser })\n if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS)\n }\n // Null on a pageview that does not report, so it reports nothing by either\n // door rather than one by one and none by the other.\n publishEvent = reporting ? enqueue : null\n\n window.addEventListener('error', (event) => {\n try {\n const error = event.error as Error | undefined\n const message = clamp(error?.message ?? event.message, MAX_MESSAGE)\n // Cross-origin scripts yield an opaque \"Script error.\" with nothing\n // actionable attached; reporting it would only create a noisy group.\n if (!message || message === 'Script error.') return\n if (isBenignBrowserNotice(message)) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = error?.stack ? String(error.stack) : undefined\n // A webview or extension that evaluated its own code into our page —\n // see `isInjectedThirdPartyFrame`. Nothing we ship can be changed in\n // response, so it is dropped on the same test as `Script error.`.\n // Judged on the WHOLE stack, before the clamp: a frame of ours past the\n // clamp still makes the error ours.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n const stack = fullStack ? clamp(fullStack, MAX_STACK) : undefined\n // MARKED, not dropped: a translator causes a hydration mismatch and so\n // does a real render divergence, and a recovered fault is still a\n // fault. Only a rate tells either apart from noise — and the\n // translator's own mark on the DOM, when it left one.\n const kind =\n recoveredErrorKind(error ?? { message }) ??\n (isHydrationMismatch(message) ? 'hydration' : 'error')\n const translated = kind === 'hydration' ? translation() : undefined\n // A mismatch on a page a translator rewrote is the translator's, not\n // ours (AGL-3738): React compared our render with text we did not\n // write, recovered, and the visitor has a working page. Nothing we ship\n // can change in response, so it goes the way of an injected stack. The\n // mark is evidence on the document, not a guess about the visitor — an\n // unmarked mismatch is still reported, and still the one to read.\n if (translated) return\n enqueue({\n kind,\n message,\n stack,\n source: scrubUrl(event.filename) || undefined,\n line: typeof event.lineno === 'number' ? event.lineno : undefined,\n col: typeof event.colno === 'number' ? event.colno : undefined,\n url: pageUrl,\n ...(kind === 'hydration' && navigator.language\n ? { language: clamp(navigator.language, 16) }\n : {}),\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n window.addEventListener('unhandledrejection', (event) => {\n try {\n // An `import()` of a chunk this document names and the origin dropped,\n // which no boundary saw. Recovered by a reload once the tab is hidden,\n // and, like a boundary's reload, not reported: see\n // `recoverStaleBuildRejection`. A tab whose recovery is spent still\n // reports it, as `chunk-load`.\n if (recoverStaleBuildRejection(event.reason)) return\n // A cancelled request, ours or the browser's on teardown: see\n // `isAbortRejection` for why this is dropped and how narrowly.\n if (isAbortRejection(event.reason)) return\n const reason = event.reason as Error | undefined\n const message = clamp(describeRejectionReason(event.reason), MAX_MESSAGE)\n if (!message) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = reason?.stack ? String(reason.stack) : undefined\n // The same injected-code drop as an uncaught error: a rejection raised\n // by a devtools snippet or an automation driver's `evaluate` is no more\n // ours for having been a promise.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n enqueue({\n kind: recoveredErrorKind(event.reason) ?? 'unhandledrejection',\n message,\n stack: fullStack ? clamp(fullStack, MAX_STACK) : undefined,\n url: pageUrl,\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n // A batch waiting out its debounce when the page hides would be lost;\n // `pagehide`/`visibilitychange` are the last reliable moments to send.\n window.addEventListener('pagehide', flush)\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush()\n })\n}\n"],"names":["browserFamily","isCrossPoolDesync","isForeignScriptUrl","stackFrameUrls","isStaleBuildError","recoverStaleBuildRejection","MAX_MESSAGE","MAX_STACK","FLUSH_DELAY_MS","scrubUrl","raw","url","URL","String","window","location","href","origin","pathname","clamp","value","max","slice","isInjectedThirdPartyFrame","stack","documentUrl","urls","length","anonymousScriptFrameCount","every","STACK_FRAME_LINE","ANONYMOUS_SCRIPT_POSITION","count","line","split","test","BENIGN_BROWSER_NOTICE","isBenignBrowserNotice","message","isWebDocument","protocol","isAbortRejection","reason","name","AUTH_DESYNC_KIND","CHUNK_LOAD_KIND","recoveredErrorKind","error","undefined","HYDRATION_MINIFIED","HYDRATION_TEXT","isHydrationMismatch","pageTranslationSignal","doc","servedLang","root","documentElement","classList","contains","querySelector","lang","getAttribute","toLowerCase","watchPageTranslation","seen","observers","stop","observer","splice","disconnect","inspect","records","relevant","record","type","node","Array","from","addedNodes","nodeName","MutationObserver","onRoot","observe","attributes","attributeFilter","onTree","childList","subtree","push","takeRecords","describeRejectionReason","shape","Error","named","map","part","trim","filter","Boolean","join","printed","keys","Object","BEACON_STACK_TRACE_LIMIT","raiseStackTraceLimit","limit","engine","current","stackTraceLimit","installed","publishEvent","redispatchCaughtError","reportHandledError","options","thrown","kind","installErrorBeacon","document","endpoint","sampleRate","maxPerPage","sampled","Math","random","reporting","translation","browser","navigator","userAgent","Set","queued","sent","timer","flush","batch","body","JSON","stringify","events","sendBeacon","fetch","method","keepalive","headers","catch","enqueue","event","key","has","add","setTimeout","addEventListener","pageUrl","fullStack","translated","source","filename","lineno","col","colno","language","visibilityState"],"mappings":"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"}
@@ -75,7 +75,7 @@
75
75
  * Pure and dependency-free, like `forms.ts` beside it: it reaches client
76
76
  * bundles through `app-utils/index`, so nothing here may import a Node
77
77
  * builtin.
78
- */ import { collectFormFieldNodeIds, FORM_COMPONENT_ID, isMarketingConsentFieldName } from "./forms.js";
78
+ */ import { collectFormFieldNodeIds, FORM_INPUT_ELEMENT_IDS, formInputName, formInputType, FORM_COMPONENT_ID, isMarketingConsentFieldName } from "./forms.js";
79
79
  /**
80
80
  * `extractEmailFromFields` prefers a key matching this before falling back to
81
81
  * any email-shaped value. Restated rather than imported so this module stays
@@ -92,13 +92,12 @@
92
92
  * be asking a question whose answer has the evidence removed from it.
93
93
  */ function drawnFields(nodes, formNodeId, formFieldComponentId) {
94
94
  return collectFormFieldNodeIds(nodes, formNodeId, formFieldComponentId).map((nodeId)=>{
95
- var _ref, _props_fieldName, _props_fieldType;
96
- var _nodes_nodeId;
97
- const props = (_ref = nodes == null ? void 0 : (_nodes_nodeId = nodes[nodeId]) == null ? void 0 : _nodes_nodeId.props) != null ? _ref : {};
95
+ const node = nodes == null ? void 0 : nodes[nodeId];
98
96
  return {
99
97
  nodeId,
100
- fieldName: String((_props_fieldName = props['fieldName']) != null ? _props_fieldName : '').trim(),
101
- fieldType: String((_props_fieldType = props['fieldType']) != null ? _props_fieldType : 'text')
98
+ fieldName: formInputName(node),
99
+ fieldType: formInputType(node),
100
+ isCheckboxElement: (node == null ? void 0 : node.componentId) === FORM_INPUT_ELEMENT_IDS.checkbox
102
101
  };
103
102
  });
104
103
  }
@@ -218,6 +217,7 @@
218
217
  }
219
218
  const fields = drawnFields(nodes, formNodeId, formFieldComponentId);
220
219
  const seen = new Map();
220
+ const checkboxNames = new Set();
221
221
  for (const field of fields){
222
222
  if (!field.fieldName) {
223
223
  violations.push({
@@ -228,6 +228,12 @@
228
228
  continue;
229
229
  }
230
230
  const first = seen.get(field.fieldName);
231
+ // Checkboxes sharing a name are ONE question with several boxes — how a
232
+ // multi-choice question is built from MUI's separate Checkbox elements.
233
+ // Their ticks are joined into one answer, so nothing is lost.
234
+ if (first !== undefined && field.isCheckboxElement && checkboxNames.has(field.fieldName)) {
235
+ continue;
236
+ }
231
237
  if (first !== undefined) {
232
238
  violations.push({
233
239
  code: 'field-name-duplicated',
@@ -238,6 +244,7 @@
238
244
  continue;
239
245
  }
240
246
  seen.set(field.fieldName, field.nodeId);
247
+ if (field.isCheckboxElement) checkboxNames.add(field.fieldName);
241
248
  }
242
249
  const consentFieldName = String((_ref3 = form == null ? void 0 : form.consentFieldName) != null ? _ref3 : '').trim();
243
250
  /*