@nathanclaire/edge-telemetry-sdk 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +831 -186
- package/dist/appLifecycle-CECgRVu_.cjs +2 -0
- package/dist/appLifecycle-CECgRVu_.cjs.map +1 -0
- package/dist/appLifecycle-DQErzJIR.js +22 -0
- package/dist/appLifecycle-DQErzJIR.js.map +1 -0
- package/dist/batch-CwoO7noL.js +56 -0
- package/dist/batch-CwoO7noL.js.map +1 -0
- package/dist/batch-DNu8Xr9T.cjs +2 -0
- package/dist/batch-DNu8Xr9T.cjs.map +1 -0
- package/dist/crashCapture-BOJ8nSAZ.cjs +4 -0
- package/dist/crashCapture-BOJ8nSAZ.cjs.map +1 -0
- package/dist/crashCapture-BW8WuyaU.js +70 -0
- package/dist/crashCapture-BW8WuyaU.js.map +1 -0
- package/dist/crashHandler.web-8F_k2veo.js +29 -0
- package/dist/crashHandler.web-8F_k2veo.js.map +1 -0
- package/dist/crashHandler.web-CWmF56mJ.cjs +2 -0
- package/dist/crashHandler.web-CWmF56mJ.cjs.map +1 -0
- package/dist/crashHandlerNative.native-C2kSikDt.cjs +2 -0
- package/dist/crashHandlerNative.native-C2kSikDt.cjs.map +1 -0
- package/dist/crashHandlerNative.native-FupMnr1U.js +39 -0
- package/dist/crashHandlerNative.native-FupMnr1U.js.map +1 -0
- package/dist/deviceInfo.native-BJ-3Jj1T.js +53 -0
- package/dist/deviceInfo.native-BJ-3Jj1T.js.map +1 -0
- package/dist/deviceInfo.native-BoeTZcD6.cjs +2 -0
- package/dist/deviceInfo.native-BoeTZcD6.cjs.map +1 -0
- package/dist/deviceInfo.web-3L3IDQQA.cjs +2 -0
- package/dist/deviceInfo.web-3L3IDQQA.cjs.map +1 -0
- package/dist/deviceInfo.web-BFB1-pcA.js +41 -0
- package/dist/deviceInfo.web-BFB1-pcA.js.map +1 -0
- package/dist/frameTracker-CHG6sIx9.cjs +2 -0
- package/dist/frameTracker-CHG6sIx9.cjs.map +1 -0
- package/dist/frameTracker-DdB8W_kC.js +54 -0
- package/dist/frameTracker-DdB8W_kC.js.map +1 -0
- package/dist/httpAttributes-BrrtM8zh.js +70 -0
- package/dist/httpAttributes-BrrtM8zh.js.map +1 -0
- package/dist/httpAttributes-D0m6UID7.cjs +2 -0
- package/dist/httpAttributes-D0m6UID7.cjs.map +1 -0
- package/dist/index-BtLlV1HA.js +57 -0
- package/dist/index-BtLlV1HA.js.map +1 -0
- package/dist/{deviceInfo.native-D2k0LDhI.js → index-C2XQ-EYw.js} +362 -411
- package/dist/index-C2XQ-EYw.js.map +1 -0
- package/dist/index-CXsVZY35.cjs +2 -0
- package/dist/index-CXsVZY35.cjs.map +1 -0
- package/dist/index-DLquF88M.cjs +2 -0
- package/dist/index-DLquF88M.cjs.map +1 -0
- package/dist/index.native.cjs +1 -1
- package/dist/index.native.cjs.map +1 -1
- package/dist/index.native.js +94 -59
- package/dist/index.native.js.map +1 -1
- package/dist/index.web.cjs +1 -1
- package/dist/index.web.cjs.map +1 -1
- package/dist/index.web.js +88 -33
- package/dist/index.web.js.map +1 -1
- package/dist/interactionWeb.web-Bl_cZlgh.cjs +2 -0
- package/dist/interactionWeb.web-Bl_cZlgh.cjs.map +1 -0
- package/dist/interactionWeb.web-CJAviVQ7.js +109 -0
- package/dist/interactionWeb.web-CJAviVQ7.js.map +1 -0
- package/dist/interceptFetchWeb.web-T88kO-bE.cjs +2 -0
- package/dist/interceptFetchWeb.web-T88kO-bE.cjs.map +1 -0
- package/dist/interceptFetchWeb.web-UXC2Lb6X.js +61 -0
- package/dist/interceptFetchWeb.web-UXC2Lb6X.js.map +1 -0
- package/dist/interceptHttpNative.native-BVpFuoNB.cjs +2 -0
- package/dist/interceptHttpNative.native-BVpFuoNB.cjs.map +1 -0
- package/dist/interceptHttpNative.native-rX-lPNST.js +18 -0
- package/dist/interceptHttpNative.native-rX-lPNST.js.map +1 -0
- package/dist/loadingTime-Bhomd9qq.cjs +2 -0
- package/dist/loadingTime-Bhomd9qq.cjs.map +1 -0
- package/dist/loadingTime-ZkYvFijA.js +70 -0
- package/dist/loadingTime-ZkYvFijA.js.map +1 -0
- package/dist/memoryNative.native-C42h9SjK.js +54 -0
- package/dist/memoryNative.native-C42h9SjK.js.map +1 -0
- package/dist/memoryNative.native-CA-7w9w1.cjs +2 -0
- package/dist/memoryNative.native-CA-7w9w1.cjs.map +1 -0
- package/dist/memoryStore-BIGZmmfD.cjs +2 -0
- package/dist/memoryStore-BIGZmmfD.cjs.map +1 -0
- package/dist/memoryStore-F5I-GQKM.js +193 -0
- package/dist/memoryStore-F5I-GQKM.js.map +1 -0
- package/dist/nativeSender-CfQaRuDK.cjs +2 -0
- package/dist/nativeSender-CfQaRuDK.cjs.map +1 -0
- package/dist/nativeSender-pgVSIv-X.js +59 -0
- package/dist/nativeSender-pgVSIv-X.js.map +1 -0
- package/dist/navigationRef-BzYbs8cm.js +28 -0
- package/dist/navigationRef-BzYbs8cm.js.map +1 -0
- package/dist/navigationRef-C-_qhb43.cjs +2 -0
- package/dist/navigationRef-C-_qhb43.cjs.map +1 -0
- package/dist/{navigationTracker-Cts68dgG.js → navigationTracker-BmywA_Bg.js} +4 -4
- package/dist/navigationTracker-BmywA_Bg.js.map +1 -0
- package/dist/navigationTracker-HkwVyywz.cjs +2 -0
- package/dist/navigationTracker-HkwVyywz.cjs.map +1 -0
- package/dist/navigationWeb.web-CW_z7Ca4.cjs +2 -0
- package/dist/navigationWeb.web-CW_z7Ca4.cjs.map +1 -0
- package/dist/navigationWeb.web-CcwgKuIZ.js +55 -0
- package/dist/navigationWeb.web-CcwgKuIZ.js.map +1 -0
- package/dist/{networkInfo.native-BK5fpAOL.js → networkInfo.native-CGrx9cnH.js} +15 -15
- package/dist/{networkInfo.native-BK5fpAOL.js.map → networkInfo.native-CGrx9cnH.js.map} +1 -1
- package/dist/{networkInfo.native-cxuw93Uy.cjs → networkInfo.native-YVySdv3S.cjs} +2 -2
- package/dist/{networkInfo.native-cxuw93Uy.cjs.map → networkInfo.native-YVySdv3S.cjs.map} +1 -1
- package/dist/{networkInfo.web-twcNn_hc.js → networkInfo.web-B2YlLNsI.js} +3 -3
- package/dist/networkInfo.web-B2YlLNsI.js.map +1 -0
- package/dist/networkInfo.web-PltJTHsy.cjs +2 -0
- package/dist/networkInfo.web-PltJTHsy.cjs.map +1 -0
- package/dist/runtimeReady.native-Bh7EcYmK.js +14 -0
- package/dist/runtimeReady.native-Bh7EcYmK.js.map +1 -0
- package/dist/runtimeReady.native-jUmuE1lV.cjs +2 -0
- package/dist/runtimeReady.native-jUmuE1lV.cjs.map +1 -0
- package/dist/runtimeReady.web-C8fdIbVR.js +25 -0
- package/dist/runtimeReady.web-C8fdIbVR.js.map +1 -0
- package/dist/runtimeReady.web-DHW682RN.cjs +2 -0
- package/dist/runtimeReady.web-DHW682RN.cjs.map +1 -0
- package/dist/seedRuntimeReady-BKwkBEAF.cjs +2 -0
- package/dist/seedRuntimeReady-BKwkBEAF.cjs.map +1 -0
- package/dist/seedRuntimeReady-Cz8nOKoo.js +10 -0
- package/dist/seedRuntimeReady-Cz8nOKoo.js.map +1 -0
- package/dist/src/adapters/appLifecycle.d.ts +8 -1
- package/dist/src/adapters/batch.d.ts +2 -0
- package/dist/src/adapters/crashCapture.d.ts +38 -9
- package/dist/src/adapters/failedEvents.d.ts +43 -0
- package/dist/src/adapters/frameAggregate.d.ts +8 -3
- package/dist/src/adapters/frameTracker.d.ts +30 -0
- package/dist/src/adapters/httpAttributes.d.ts +18 -0
- package/dist/src/adapters/loadingTime.d.ts +67 -0
- package/dist/src/adapters/native/interceptHttpNative.native.d.ts +18 -0
- package/dist/src/adapters/native/memoryNative.native.d.ts +15 -11
- package/dist/src/adapters/native/runtimeReady.native.d.ts +1 -0
- package/dist/src/adapters/native/store.native.d.ts +10 -0
- package/dist/src/adapters/nativeSender.d.ts +2 -2
- package/dist/src/adapters/navigationRef.d.ts +21 -0
- package/dist/src/adapters/navigationTracker.d.ts +1 -1
- package/dist/src/adapters/screenTiming.d.ts +8 -2
- package/dist/src/adapters/seedRuntimeReady.d.ts +8 -0
- package/dist/src/adapters/traceHeader.d.ts +33 -0
- package/dist/src/adapters/traceManager.d.ts +131 -0
- package/dist/src/adapters/uiInteraction.d.ts +111 -0
- package/dist/src/adapters/viewManager.d.ts +161 -0
- package/dist/src/adapters/viewport.d.ts +15 -0
- package/dist/src/adapters/web/interactionWeb.web.d.ts +36 -0
- package/dist/src/adapters/web/interceptFetchWeb.web.d.ts +3 -11
- package/dist/src/adapters/web/navigationWeb.web.d.ts +10 -0
- package/dist/src/adapters/web/runtimeReady.web.d.ts +5 -0
- package/dist/src/adapters/web/store.web.d.ts +12 -0
- package/dist/src/adapters/web/webVitals.web.d.ts +49 -0
- package/dist/src/adapters/webSender.d.ts +2 -2
- package/dist/src/adapters/webVitals.d.ts +25 -0
- package/dist/src/adapters/xhrIntercept.d.ts +53 -0
- package/dist/src/core/beforeSend.d.ts +20 -0
- package/dist/src/core/debug.d.ts +7 -0
- package/dist/src/core/memoryStore.d.ts +23 -0
- package/dist/src/core/store.d.ts +41 -0
- package/dist/src/core/telemetry.d.ts +305 -35
- package/dist/src/core/userProfile.d.ts +12 -0
- package/dist/src/core/utils/json.d.ts +7 -0
- package/dist/src/core/utils/uuid.d.ts +10 -1
- package/dist/src/createTelemetry.native.d.ts +7 -0
- package/dist/src/createTelemetry.web.d.ts +7 -0
- package/dist/src/index.base.d.ts +17 -1
- package/dist/src/index.native.d.ts +46 -9
- package/dist/src/index.web.d.ts +33 -2
- package/dist/store.native-BYBJcy6o.js +32 -0
- package/dist/store.native-BYBJcy6o.js.map +1 -0
- package/dist/store.native-TPqXngmF.cjs +2 -0
- package/dist/store.native-TPqXngmF.cjs.map +1 -0
- package/dist/store.web-CvqqN-_Z.cjs +2 -0
- package/dist/store.web-CvqqN-_Z.cjs.map +1 -0
- package/dist/store.web-DJAlN0nc.js +31 -0
- package/dist/store.web-DJAlN0nc.js.map +1 -0
- package/dist/telemetry-ByDJIz_9.js +1280 -0
- package/dist/telemetry-ByDJIz_9.js.map +1 -0
- package/dist/telemetry-CrU7i1Jc.cjs +2 -0
- package/dist/telemetry-CrU7i1Jc.cjs.map +1 -0
- package/dist/uiInteraction-CdzvEI1p.cjs +2 -0
- package/dist/uiInteraction-CdzvEI1p.cjs.map +1 -0
- package/dist/uiInteraction-DAqxxz0q.js +104 -0
- package/dist/uiInteraction-DAqxxz0q.js.map +1 -0
- package/dist/viewport-Dd85GUNO.cjs +2 -0
- package/dist/viewport-Dd85GUNO.cjs.map +1 -0
- package/dist/viewport-Dzmt5QwK.js +15 -0
- package/dist/viewport-Dzmt5QwK.js.map +1 -0
- package/dist/web-vitals.attribution-CVCPiX-h.cjs +2 -0
- package/dist/web-vitals.attribution-CVCPiX-h.cjs.map +1 -0
- package/dist/web-vitals.attribution-xw9N5MKQ.js +488 -0
- package/dist/web-vitals.attribution-xw9N5MKQ.js.map +1 -0
- package/dist/webSender-BIwslfpT.js +63 -0
- package/dist/webSender-BIwslfpT.js.map +1 -0
- package/dist/webSender-CVtE4sOF.cjs +2 -0
- package/dist/webSender-CVtE4sOF.cjs.map +1 -0
- package/dist/webVitals.web-CNIpsKlA.js +85 -0
- package/dist/webVitals.web-CNIpsKlA.js.map +1 -0
- package/dist/webVitals.web-DpWJ-iYp.cjs +2 -0
- package/dist/webVitals.web-DpWJ-iYp.cjs.map +1 -0
- package/dist/xhrIntercept-BADex6Cq.cjs +2 -0
- package/dist/xhrIntercept-BADex6Cq.cjs.map +1 -0
- package/dist/xhrIntercept-BNiA41Mj.js +53 -0
- package/dist/xhrIntercept-BNiA41Mj.js.map +1 -0
- package/docs/migration-v4.md +190 -0
- package/package.json +12 -4
- package/dist/appLifecycle-Cynw0kAv.js +0 -14
- package/dist/appLifecycle-Cynw0kAv.js.map +0 -1
- package/dist/appLifecycle-DvJIt7ym.cjs +0 -2
- package/dist/appLifecycle-DvJIt7ym.cjs.map +0 -1
- package/dist/batch-CLBFgYPC.cjs +0 -2
- package/dist/batch-CLBFgYPC.cjs.map +0 -1
- package/dist/batch-DLG_gtOm.js +0 -12
- package/dist/batch-DLG_gtOm.js.map +0 -1
- package/dist/crashCapture-BYTM4FLC.cjs +0 -2
- package/dist/crashCapture-BYTM4FLC.cjs.map +0 -1
- package/dist/crashCapture-D_rkkBVa.js +0 -31
- package/dist/crashCapture-D_rkkBVa.js.map +0 -1
- package/dist/crashHandler.web-BSxDpFTo.cjs +0 -2
- package/dist/crashHandler.web-BSxDpFTo.cjs.map +0 -1
- package/dist/crashHandler.web-D59bmhg9.js +0 -32
- package/dist/crashHandler.web-D59bmhg9.js.map +0 -1
- package/dist/crashHandlerNative.native-DeAFze4E.cjs +0 -2
- package/dist/crashHandlerNative.native-DeAFze4E.cjs.map +0 -1
- package/dist/crashHandlerNative.native-d7qJkzA6.js +0 -41
- package/dist/crashHandlerNative.native-d7qJkzA6.js.map +0 -1
- package/dist/debug-DsvQ9y3W.js +0 -68
- package/dist/debug-DsvQ9y3W.js.map +0 -1
- package/dist/debug-rnOOxFpN.cjs +0 -2
- package/dist/debug-rnOOxFpN.cjs.map +0 -1
- package/dist/deviceInfo.native-7hbRYixR.cjs +0 -2
- package/dist/deviceInfo.native-7hbRYixR.cjs.map +0 -1
- package/dist/deviceInfo.native-D2k0LDhI.js.map +0 -1
- package/dist/deviceInfo.web-ByNAlEJh.cjs +0 -2
- package/dist/deviceInfo.web-ByNAlEJh.cjs.map +0 -1
- package/dist/deviceInfo.web-DzQyEku6.js +0 -40
- package/dist/deviceInfo.web-DzQyEku6.js.map +0 -1
- package/dist/frameAggregate-C_HoVFcK.cjs +0 -2
- package/dist/frameAggregate-C_HoVFcK.cjs.map +0 -1
- package/dist/frameAggregate-Cwh1juw1.js +0 -17
- package/dist/frameAggregate-Cwh1juw1.js.map +0 -1
- package/dist/frameDropsNative.native-CP7zrXDf.js +0 -23
- package/dist/frameDropsNative.native-CP7zrXDf.js.map +0 -1
- package/dist/frameDropsNative.native-DKqEDqip.cjs +0 -2
- package/dist/frameDropsNative.native-DKqEDqip.cjs.map +0 -1
- package/dist/frameDropsWeb.web-C9Lt3Fuj.js +0 -22
- package/dist/frameDropsWeb.web-C9Lt3Fuj.js.map +0 -1
- package/dist/frameDropsWeb.web-azpwQCUz.cjs +0 -2
- package/dist/frameDropsWeb.web-azpwQCUz.cjs.map +0 -1
- package/dist/httpAttributes-BV50UKQU.js +0 -19
- package/dist/httpAttributes-BV50UKQU.js.map +0 -1
- package/dist/httpAttributes-xXOD9trQ.cjs +0 -2
- package/dist/httpAttributes-xXOD9trQ.cjs.map +0 -1
- package/dist/interaction-CH8yoY5-.js +0 -23
- package/dist/interaction-CH8yoY5-.js.map +0 -1
- package/dist/interaction-DcNFBW7a.cjs +0 -2
- package/dist/interaction-DcNFBW7a.cjs.map +0 -1
- package/dist/interceptFetchNative.native-VlhtMG-U.js +0 -49
- package/dist/interceptFetchNative.native-VlhtMG-U.js.map +0 -1
- package/dist/interceptFetchNative.native-jEAldH14.cjs +0 -2
- package/dist/interceptFetchNative.native-jEAldH14.cjs.map +0 -1
- package/dist/interceptFetchWeb.web-DFzZliik.cjs +0 -2
- package/dist/interceptFetchWeb.web-DFzZliik.cjs.map +0 -1
- package/dist/interceptFetchWeb.web-DfsMfoRP.js +0 -77
- package/dist/interceptFetchWeb.web-DfsMfoRP.js.map +0 -1
- package/dist/memoryNative.native-BWLh2S9L.js +0 -48
- package/dist/memoryNative.native-BWLh2S9L.js.map +0 -1
- package/dist/memoryNative.native-DCXSOd4Q.cjs +0 -2
- package/dist/memoryNative.native-DCXSOd4Q.cjs.map +0 -1
- package/dist/memoryWeb.web-B3YuV-cB.js +0 -48
- package/dist/memoryWeb.web-B3YuV-cB.js.map +0 -1
- package/dist/memoryWeb.web-CheXAcJn.cjs +0 -2
- package/dist/memoryWeb.web-CheXAcJn.cjs.map +0 -1
- package/dist/nativeSender-BE_mzw_S.js +0 -70
- package/dist/nativeSender-BE_mzw_S.js.map +0 -1
- package/dist/nativeSender-DyAUGY7c.cjs +0 -2
- package/dist/nativeSender-DyAUGY7c.cjs.map +0 -1
- package/dist/navigationNative.native-3tO5JxSd.cjs +0 -2
- package/dist/navigationNative.native-3tO5JxSd.cjs.map +0 -1
- package/dist/navigationNative.native-BUCGPQX2.js +0 -25
- package/dist/navigationNative.native-BUCGPQX2.js.map +0 -1
- package/dist/navigationTracker-B2GMwyKk.cjs +0 -2
- package/dist/navigationTracker-B2GMwyKk.cjs.map +0 -1
- package/dist/navigationTracker-Cts68dgG.js.map +0 -1
- package/dist/navigationWeb.web-B0necr5j.cjs +0 -2
- package/dist/navigationWeb.web-B0necr5j.cjs.map +0 -1
- package/dist/navigationWeb.web-CORzE_Id.js +0 -46
- package/dist/navigationWeb.web-CORzE_Id.js.map +0 -1
- package/dist/networkInfo.web-DJmw1sNK.cjs +0 -2
- package/dist/networkInfo.web-DJmw1sNK.cjs.map +0 -1
- package/dist/networkInfo.web-twcNn_hc.js.map +0 -1
- package/dist/src/adapters/interaction.d.ts +0 -17
- package/dist/src/adapters/native/frameDropsNative.native.d.ts +0 -16
- package/dist/src/adapters/native/interceptFetchNative.native.d.ts +0 -23
- package/dist/src/adapters/native/navigationNative.native.d.ts +0 -12
- package/dist/src/adapters/web/frameDropsWeb.web.d.ts +0 -16
- package/dist/src/adapters/web/memoryWeb.web.d.ts +0 -26
- package/dist/telemetry-Co4MYdwa.cjs +0 -2
- package/dist/telemetry-Co4MYdwa.cjs.map +0 -1
- package/dist/telemetry-KI0x4gzC.js +0 -389
- package/dist/telemetry-KI0x4gzC.js.map +0 -1
- package/dist/v4-BysszJq8.js +0 -31
- package/dist/v4-BysszJq8.js.map +0 -1
- package/dist/v4-CqdK_b8Y.cjs +0 -2
- package/dist/v4-CqdK_b8Y.cjs.map +0 -1
- package/dist/webSender-B27tmT1w.js +0 -42
- package/dist/webSender-B27tmT1w.js.map +0 -1
- package/dist/webSender-BWi4x1df.cjs +0 -2
- package/dist/webSender-BWi4x1df.cjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-CrU7i1Jc.cjs","sources":["../src/adapters/screenTiming.ts","../src/core/utils/uuid.ts","../src/adapters/viewManager.ts","../src/adapters/traceManager.ts","../src/core/breadcrumbs.ts","../src/core/beforeSend.ts","../src/core/utils/json.ts","../src/core/userProfile.ts","../src/core/telemetry.ts"],"sourcesContent":["import { Telemetry } from \"../core/telemetry\";\n\nexport class ScreenTimingTracker {\n private telemetry: Telemetry;\n private startTimes: Map<string, number> = new Map();\n private lastScreen?: string;\n\n constructor(telemetry: Telemetry) {\n this.telemetry = telemetry;\n }\n\n // Screen entry → v3 `navigation` (replaces v2 screen_view). Baseline shape follows the\n // reference/Angular keys; iOS reconciliation is OPEN (see additions ledger).\n async startScreen(screen: string) {\n const from = this.lastScreen;\n this.markStart(screen); // emits nothing; synchronous so a tap right after this call sees it\n this.lastScreen = screen;\n // The deprecated row first, awaited: it describes the transition, so it belongs to\n // the view being left. Then the boundary — otherwise the two race and the\n // `navigation` row lands under whichever view the microtask queue happened to pick.\n await this.telemetry.log(\"navigation\", {\n \"navigation.from_screen\": from ?? null,\n \"navigation.to_screen\": screen,\n \"navigation.method\": \"screen_start\",\n \"navigation.route_type\": \"screen\",\n });\n // Rung 1 of the name ladder (§4.5.1) — `explicit`, and never normalized: a host\n // naming a screen \"Step 2 of 3\" must not receive \"Step {id} of {id}\".\n await this.telemetry.enterView(screen, \"explicit\");\n }\n\n /**\n * Arm the dwell clock without emitting `navigation`. The route-change path (§4.11) has\n * already emitted its own `navigation` with `method: \"push\"`, and emitting a second one\n * with `method: \"screen_start\"` for the same transition would double the table.\n */\n markStart(screen: string) {\n this.startTimes.set(screen, Date.now());\n this.telemetry.currentScreen = screen; // best-effort screen for the deprecated feeds (#33)\n // Advanced here too, not only in startScreen: the route path calls this one, and a\n // consumer mixing both APIs would otherwise get a stale `navigation.from_screen`\n // naming a screen they left several route changes ago.\n this.lastScreen = screen;\n }\n\n // Screen exit → v3 `screen.duration` with dwell ms (replaces screen_end + performance.screen_duration).\n async endScreen(screen: string) {\n const start = this.startTimes.get(screen);\n if (!start) return;\n\n const duration = Date.now() - start;\n this.startTimes.delete(screen);\n\n await this.telemetry.log(\"screen.duration\", {\n \"screen.name\": screen,\n \"screen.duration_ms\": duration,\n \"screen.exit_method\": \"navigation\",\n });\n }\n}\n","// Platform-agnostic id generator. The web/native split was byte-identical, so it's gone.\n\n/**\n * Wire-contract ids carry a 16-hex-char (64-bit) random segment (§3.3) — except W3C\n * `trace.id`, which is 32 (§6.1). The length is the caller's; the entropy source is not.\n *\n * Entropy is `crypto.getRandomValues`, not `Math.random()` (#91): `device.id` is now\n * persisted, so a collision is *permanent* — two handsets merge into one device row and\n * one rate-limit bucket forever — where a session collision was transient. RN has no\n * WebCrypto of its own; the native entry imports `react-native-get-random-values`\n * (already a dependency) to install it before the first id is minted.\n */\nexport function randomHex(length = 16): string {\n // Guarded, because the failure is otherwise a bare TypeError thrown from the Telemetry\n // constructor with nothing pointing at the cause. There is no Math.random() fallback on\n // purpose (#91): silently minting weak ids for a value that persists forever is worse\n // than refusing, and the fix is a reinstall, not a retry.\n if (typeof globalThis.crypto?.getRandomValues !== \"function\") {\n throw new Error(\n \"edge-telemetry: crypto.getRandomValues is unavailable, so no id can be minted. \" +\n \"On React Native this means react-native-get-random-values failed to load — \" +\n \"reinstall it and rebuild.\",\n );\n }\n const bytes = new Uint8Array(Math.ceil(length / 2));\n crypto.getRandomValues(bytes);\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out.slice(0, length);\n}\n","// The View entity (§4.5, #96) — shared by both builds, because everything in it is\n// platform-agnostic bookkeeping. It owns three things:\n//\n// 1. `view.id` + `view.name`, denormalized onto the Context block of every event and\n// metric, so \"errors by screen\" and \"p95 by screen\" need no join.\n// 2. the `view` event emitted at exit, carrying dwell, the three counters and the\n// network-settle verdict from `loadingTime.ts` (#97).\n// 3. the name ladder, and the four lifetime boundaries.\n//\n// It never imports a platform API. The entries feed it: route changes via\n// `Telemetry.recordRouteChange`, background via `AppLifecycleEmitter`, session rotation\n// via `Telemetry.newSession`. Process death needs no feed — a new process opens a fresh\n// initial view, and the killed view's dwell is lost by design (no `view` event was emitted).\n\nimport { randomHex } from \"../core/utils/uuid\";\nimport { NetworkSettle } from \"./loadingTime\";\nimport type { TraceAttributes } from \"./traceManager\";\n\n/** §4.5.1's ladder. `none` is the literal `\"unknown\"` name, not an absent one. */\nexport type ViewNameSource = \"explicit\" | \"route\" | \"url\" | \"none\";\n/** §4.5's four `view.load_type` values — one per lifetime boundary that mints a successor. */\nexport type ViewLoadType = \"initial_load\" | \"route_change\" | \"resume\" | \"session_rotation\";\n\n/** Rank beats order (§4.5.1): explicit > route > url > none. */\nconst RANK: Record<ViewNameSource, number> = { none: 0, url: 1, route: 2, explicit: 3 };\n\n/** `view.name` before anything has named the view. A literal, never null (§4.5.1). */\nexport const UNKNOWN_VIEW_NAME = \"unknown\";\n\n/**\n * How many retired views `nameOf()` can still name (§3.1). Bounded because an unbounded\n * map is a leak on a long session; 16 because a span outliving that many boundaries is a\n * request §4.5.2's 30 s settle cap has already given up on.\n */\nexport const MAX_RETIRED_VIEW_NAMES = 16;\n\n/**\n * All ViewManager needs of core. Structural, so the manager stays unit-testable alone.\n * `trace` is optional for that reason and for that reason only — every real `Telemetry`\n * has one, and a view with no trace source simply carries no §6 keys.\n */\ntype Emitter = {\n log(name: string, data?: Record<string, any>): unknown;\n trace?: { viewSpan(entryAt: number): TraceAttributes };\n};\n\ntype View = {\n id: string;\n name: string;\n source: ViewNameSource;\n referrer: string;\n loadType: ViewLoadType;\n // Dwell is foreground-only (§4.5), so it banks on background and re-arms on foreground\n // rather than running off a single start timestamp.\n elapsed: number;\n resumedAt?: number; // undefined = the clock is paused (app backgrounded)\n errors: number;\n actions: number;\n // `view.request_count` and `view.loading_time` both live here — both count requests\n // *started* in this view, so one object owns both and they cannot disagree (§4.5.2).\n settle: NetworkSettle;\n // §6.3's Tier 1 keys, captured at view **entry**: a `view` row parents to the root that\n // was live when the screen opened, not the one live when the user left it. Held here\n // rather than read at exit precisely so the later root cannot claim it.\n span: TraceAttributes;\n};\n\n/** `view_{ms}_{16hex}` (§3.3). No platform suffix — a view never leaves its process. */\nfunction mintViewId(): string {\n return `view_${Date.now()}_${randomHex(16)}`;\n}\n\nexport class ViewManager {\n private view: View;\n /** Origin only, and only where there is one — omitted on native (§4.5). */\n private readonly host?: string;\n /** §4.6's aliveness subscribers. Empty on native and on a web build with no DOM. */\n private readonly activityListeners = new Set<() => void>();\n /** §5.1's boundary subscribers — the frame window, awaited before the successor mints. */\n private readonly boundaryListeners = new Set<(successorLoadType: ViewLoadType) => unknown>();\n /** Retired `view.id` → final `view.name`, for `nameOf()`. Bounded; oldest evicted. */\n private readonly retiredNames = new Map<string, string>();\n\n constructor(private telemetry: Emitter) {\n // A capability check, not a platform branch: RN has no `location`, and a\n // white-label web bundle serving two banks needs the origin to tell them apart.\n this.host = typeof location !== \"undefined\" && location?.origin ? location.origin : undefined;\n // The initial view opens at SDK init, so `view.id` is never absent (§4.5).\n const now = Date.now();\n this.view = {\n id: mintViewId(), name: UNKNOWN_VIEW_NAME, source: \"none\",\n referrer: \"\", loadType: \"initial_load\",\n elapsed: 0, resumedAt: now, errors: 0, actions: 0,\n // The launch view is the one `load_type` that seeds from the platform's\n // runtime-ready marker: it is busy until the bundle has evaluated (§4.5.2).\n settle: new NetworkSettle(now, { awaitRuntimeReady: true }),\n // The launch root is live here — it is minted in the same `Telemetry` constructor,\n // just above this one — so the initial view is its child and a web hard load\n // reports `launch`, never `navigation` (§6.2).\n span: telemetry.trace?.viewSpan(now) ?? {},\n };\n }\n\n /** The two keys the Context block carries on every row. Name resolves at log time. */\n get id(): string { return this.view.id; }\n get name(): string { return this.view.name; }\n\n /**\n * §3.1 — `view.name` for a **frozen** `view.id`. A span-carrying row pins itself to the\n * view live at span start, and its name is still resolved at log time, so an\n * `http.request` that outlives a route change reports the departing view's *current*\n * best name rather than a stale copy taken at send.\n *\n * ponytail: a bounded ring of retired names, not a full history. A span outliving\n * `MAX_RETIRED_VIEW_NAMES` boundaries is a request the settle cap (30 s) already gave up\n * on; it degrades to `\"unknown\"`, which is what a never-named view reports anyway.\n */\n nameOf(id: string): string {\n if (id === this.view.id) return this.view.name;\n return this.retiredNames.get(id) ?? UNKNOWN_VIEW_NAME;\n }\n\n /**\n * The name ladder (§4.5.1), and the only path that mints a view from a *name*.\n *\n * - a **higher** rung re-stamps the name and never changes `view.id` — that is what\n * \"rank beats order\" means, and it is why a route name arriving after a URL-derived\n * one upgrades the view in place instead of splitting it in two;\n * - a **lower** rung is ignored outright, whenever it arrives — ⚠ *including its\n * boundary*. A host that mixes rung 1 with `attachNavigation` therefore pins the view\n * to the `screenStart` name until the next `screenStart`: route changes stop minting\n * successors, and dwell keeps accruing under the explicit name. That is what §4.5.1's\n * \"a lower rung arriving later does not overwrite a higher one\" costs, and the two\n * rungs describing one navigation (the upgrade window) is the case it exists for.\n * It needs a contract ruling, not a local invention — see CLAUDE.md's known gaps;\n * - the **same** rung naming a different screen is a genuine navigation, so it ends the\n * view and mints a successor.\n *\n * Rungs 1 and 2 are handed through unnormalized: a host naming a screen \"Step 2 of 3\"\n * must not receive \"Step {id} of {id}\". The caller normalizes rung 3, not this method.\n */\n async navigate(name: string, source: ViewNameSource): Promise<void> {\n const rank = RANK[source], current = RANK[this.view.source];\n if (rank < current) return;\n if (rank > current) { this.view.name = name; this.view.source = source; return; }\n if (name === this.view.name) return;\n await this.exit(\"route_change\", name, source);\n }\n\n /** `view.error_count` — `app.crash` rows sharing this `view.id`. A closed enumeration. */\n countError(): void { this.view.errors++; }\n\n /** `view.action_count` — interaction rows sharing this `view.id`. */\n countAction(): void { this.view.actions++; }\n\n /**\n * An HTTP request started (§4.5.2). Returns its completion callback, **bound to the view\n * that was live at start** — a request that finishes after a route change belongs to the\n * view it started in and does not hold the arriving one open. The collector's own POST\n * never reaches here; the interceptors filter it before calling.\n */\n requestStarted(now?: number): (endedAt?: number) => void {\n this.notifyActivity();\n return this.view.settle.requestStarted(now);\n }\n\n /**\n * §4.6's dead-click gate, in the two places this manager already sees: a request started\n * and a view minted (a route change, a resume, a session rotation). The web tracker adds\n * DOM mutation on top, which it can observe on its own; these two it cannot.\n *\n * Deliberately fires for the background and rotation boundaries too. Those are not\n * navigations the click caused, so counting them as aliveness **under-reports** dead\n * clicks — which is the direction §4.6 requires: `ui.dead` must never falsely accuse.\n *\n * @returns an unsubscribe, so a tracker that is torn down does not keep the manager alive.\n */\n onActivity(fn: () => void): () => void {\n this.activityListeners.add(fn);\n return () => { this.activityListeners.delete(fn); };\n }\n\n private notifyActivity(): void {\n for (const fn of this.activityListeners) fn();\n }\n\n /**\n * §5.1's view-boundary reset. A subscriber is **awaited inside `beginView`, before the\n * successor replaces the current view**, so anything it emits is stamped with the\n * departing `view.id`. That ordering is the whole feature: a fixed 10 s frame window\n * straddling a route change otherwise charges the departing screen's dropped frames to\n * the arriving one — backwards for the one query the metric exists to serve.\n *\n * ⚠ **It does not fire on the `session_rotation` boundary**, and that is deliberate.\n * `newSession()` installs the new `session.id` *before* minting the successor view, so a\n * row emitted there would carry the new `session.id` with the departing `view.id` — the\n * one thing §4.5's \"`view.id` never spans a `session.id`\" forbids. Emitting *before* the\n * rotation instead is worse: `logMetric` re-checks session expiry, `lastActivity` is\n * still stale at that point, and the emit would rotate the session a second time. The\n * accepted residue is a frame window that carries across a rotation — at most one\n * unflushed window, of a session that ended by idleness.\n *\n * The successor's load type is handed to the subscriber so it can tell the two firing\n * boundaries apart: `frame_render_time` wants both, while §5.3's running CLS/INP values\n * want **`\"resume\"` — the background boundary — only**. Emitting a page-load-scoped vital\n * at a soft navigation would ship a second row for the same page load and stamp it with\n * a `view.id` that is not the initial view's.\n *\n * @returns an unsubscribe, so a torn-down subscriber does not keep the manager alive.\n */\n onBoundary(fn: (successorLoadType: ViewLoadType) => unknown): () => void {\n this.boundaryListeners.add(fn);\n return () => { this.boundaryListeners.delete(fn); };\n }\n\n /**\n * The platform's runtime-ready marker — `loadEventEnd` on web,\n * `performance.rnStartupTiming` on native — or `undefined` where the platform has none.\n *\n * Forwarded to whatever view is current, which is safe because a view that was never gated\n * *ignores* the seed — enforced in `NetworkSettle`, not assumed here. On web the `load`\n * event routinely arrives after the first route change, and flooring that view's settle at\n * the page's load time would charge the launch's cost to a route change.\n */\n seedRuntimeReady(at?: number): void {\n this.view.settle.seedRuntimeReady(at);\n }\n\n /**\n * Emit the `view` event for the current view. Its `view.name` is authoritative and its\n * `view.name_source` reports the *final* rung; the Context-block copy on rows emitted\n * early in the view may still carry a lower rung's name (§4.5.1).\n *\n * `view.loading_time` is **omitted when null**, following this contract's general\n * absent-means-the-SDK-had-nothing discipline (§4.11 names `navigation.from_screen` as the\n * only explicit wire null). `view.loading_time_outcome` always ships: it is what tells the\n * three null causes apart, and reading p75-of-settled beside %-capped is the whole point.\n */\n async endView(): Promise<void> {\n const v = this.view;\n const { loadingTime, outcome } = v.settle.resolve();\n await this.telemetry.log(\"view\", {\n // A **point span** (§6.3): `span.start_time` from view entry and never\n // `span.duration_ms`. View dwell is `view.time_spent`, not span width — a width\n // here would stretch every tap-that-navigates envelope across the whole visit.\n ...v.span,\n ...(this.host ? { \"view.host\": this.host } : {}),\n \"view.referrer\": v.referrer,\n \"view.load_type\": v.loadType,\n \"view.name_source\": v.source,\n \"view.time_spent\": this.timeSpent(),\n ...(loadingTime === null ? {} : { \"view.loading_time\": loadingTime }),\n \"view.loading_time_outcome\": outcome,\n \"view.error_count\": v.errors,\n \"view.action_count\": v.actions,\n \"view.request_count\": v.settle.requestCount,\n });\n }\n\n /**\n * Mint the successor. Called directly by session rotation, which has to mint *after*\n * the new `session.id` is in place — `view.id` never spans a `session.id` (§4.5).\n * An unnamed successor carries the departing view's name: backgrounding and a session\n * rotation do not move the user off the screen they were on.\n */\n async beginView(successorLoadType: ViewLoadType, name?: string, source?: ViewNameSource): Promise<void> {\n this.notifyActivity();\n // Awaited while `this.view` is still the departing one — see `onBoundary` for why\n // the session-rotation boundary is the one that does not fire. A subscriber that\n // throws is swallowed: a broken frame window must not be able to abort view minting.\n if (successorLoadType !== \"session_rotation\") {\n for (const fn of this.boundaryListeners) {\n try { await fn(successorLoadType); } catch { /* a subscriber's failure is not this view's */ }\n }\n }\n const prev = this.view;\n const now = Date.now();\n this.retiredNames.set(prev.id, prev.name);\n if (this.retiredNames.size > MAX_RETIRED_VIEW_NAMES) {\n // Map iteration is insertion-ordered, so the first key is the oldest retired view.\n const oldest = this.retiredNames.keys().next();\n if (!oldest.done) this.retiredNames.delete(oldest.value);\n }\n this.view = {\n id: mintViewId(),\n name: name ?? prev.name,\n source: source ?? prev.source,\n referrer: prev.name,\n loadType: successorLoadType,\n elapsed: 0, resumedAt: now, errors: 0, actions: 0,\n // No runtime-ready seed: only `initial_load` has a platform marker to wait on.\n settle: new NetworkSettle(now),\n // A view start *extends* the live root; with none live it mints a `navigation`\n // one (§6.2). Its exit does not extend anything (§6.7).\n span: this.telemetry.trace?.viewSpan(now) ?? {},\n };\n }\n\n /** A boundary that stays inside one session: emit, then mint the successor. */\n async exit(successorLoadType: ViewLoadType, name?: string, source?: ViewNameSource): Promise<void> {\n await this.endView();\n await this.beginView(successorLoadType, name, source);\n }\n\n /**\n * The background boundary. The successor is minted immediately — `view.id` must never\n * be absent — but its clock starts paused, so a night spent backgrounded does not land\n * as dwell on whatever screen the user left open.\n */\n async background(): Promise<void> {\n await this.exit(\"resume\");\n const v = this.view;\n v.elapsed += this.sinceResume();\n v.resumedAt = undefined;\n }\n\n /** The matching foreground edge: re-arm the paused clock. */\n foreground(): void {\n this.view.resumedAt ??= Date.now();\n }\n\n private sinceResume(): number {\n return this.view.resumedAt === undefined ? 0 : Date.now() - this.view.resumedAt;\n }\n\n private timeSpent(): number {\n return Math.max(0, this.view.elapsed + this.sinceResume());\n }\n}\n","// Trace and span core (§6, #98) — shared by both builds, because none of it is\n// platform-specific. It owns three things:\n//\n// 1. the carrier: the one root that is currently live, plus its 2 s idle / 10 s cap expiry;\n// 2. root minting — `launch` at construction, `navigation` at a view entry with no live\n// root, `request` at a send with no live root, and `interaction` at every click/tap.\n// 3. the key builders for the three tiers (§6.3), so no call site hand-assembles them.\n//\n// ⚠ **No request tag and no thread-local.** Android needs both because OkHttp runs its\n// interceptors on a dispatcher pool thread; both RN builds patch a single-threaded JS\n// transport, so a plain field read synchronously inside the patch is correct *by\n// construction* (§6.7). Android's process-global `lastRoot` is unnecessary for the same\n// reason: the crash and unhandled-rejection handlers read this carrier directly.\n//\n// The carrier is a field on a `Telemetry`-owned instance rather than a module-level `let`,\n// which is the one deliberate departure from §6.7's wording. The substance it asks for —\n// synchronous plain read, no tag, no thread-local — is unchanged; a module singleton would\n// instead let one `Telemetry` inherit another's live root inside a single process, which is\n// the hazard that actually exists here. `ViewManager` is owned the same way, for the same\n// reason.\n\nimport { randomHex } from \"../core/utils/uuid\";\nimport {\n allowsHost,\n formatTraceparent,\n normalizeAllowlist,\n parseTraceparent,\n type TraceOutcome,\n} from \"./traceHeader\";\n\n/** §6.1's four root types. */\nexport type TraceRootType = \"launch\" | \"interaction\" | \"navigation\" | \"request\";\n\n/**\n * Android's numbers unchanged (§6.7), and **internal constants, never `TelemetryOpts`\n * surface**. The case for raising the cap is chained requests on poor networks; it was\n * rejected on Android's own logic — `injected_expired` exists so the numbers are\n * falsifiable. Ship these, watch the expired ratio, tune on evidence.\n */\nconst ROOT_IDLE_MS = 2000;\nconst ROOT_CAP_MS = 10000;\n\ntype Root = {\n traceId: string;\n spanId: string;\n rootType: TraceRootType;\n /** The liveness clock's origin — always `Date.now()` at mint, never a reported value. */\n startedAt: number;\n /** The reported `span.start_time`. Differs from `startedAt` only for `launch` on web. */\n spanStartMs: number;\n lastTouch: number;\n};\n\n/** A flat bag of wire keys. An absent key means the SDK had nothing (§6.1). */\nexport type TraceAttributes = Record<string, string | number>;\n\n/** What the interceptor observed at `send()`, and nothing it had to interpret. */\nexport type RequestTraceCtx = {\n url: string;\n /** An unsampled session injects no header at all (§6.5). Sampling stays session-level. */\n sampled: boolean;\n /**\n * The consumer's own `traceparent`, captured by the interceptor **at their write time**.\n * Absent (not empty-string) means they set none — never sniffed back off the request.\n */\n consumerTraceparent?: string | null;\n /** fetch-only. `mode` is a fetch concept XHR cannot express, so native never sets it. */\n noCors?: boolean;\n};\n\nexport type RequestTrace = {\n /** The one header value to write, or nothing at all. Never a `flags=00` placeholder. */\n header?: string;\n finish: (endedAt: number) => TraceAttributes;\n};\n\n/** 32 lowercase hex, W3C (§6.1). */\nconst mintTraceId = () => randomHex(32);\n/** 16 lowercase hex, W3C (§6.1). */\nconst mintSpanId = () => randomHex(16);\n\nexport class TraceManager {\n private root?: Root;\n /** Bare hosts, punycode-normalized, ports ignored. Empty by default: v4 is dark on upgrade. */\n private readonly allowlist: ReadonlySet<string>;\n /** Kept whether or not it is still the carrier: `app.start` reports it at any age. */\n private launch: Root;\n\n /**\n * @param launchStartMs what `app.start` reports as `span.start_time` — and **neither\n * value is a fork time** (§6.2). Web passes `performance.timeOrigin` (true navigation\n * start); native leaves it defaulted to `initialize()`, because everything before the\n * JS bundle loads is invisible and JS cannot see process fork. Do not compare the two\n * platforms' launch envelopes as if they measured the same interval.\n */\n constructor(launchStartMs?: number, traceHostAllowlist?: string[]) {\n // Constructor-only (§6.4). Normalized once, here, so a malformed entry throws at the\n // one moment a developer is looking — not on the first request to a typo'd host.\n this.allowlist = normalizeAllowlist(traceHostAllowlist);\n const now = Date.now();\n // Minted here and not at `app.start`'s emit, because the initial view opens in the\n // same `Telemetry` constructor and has to parent to it: a view minting its own root\n // there would make a web hard load report `navigation` (§6.2).\n this.launch = this.mint(\"launch\", now, launchStartMs ?? now);\n }\n\n /**\n * §4.1's `sdk.trace_allowlist_size` — **count only, never the hosts**, so an operator can\n * tell \"nobody opted in\" apart from \"the header is being stripped\" without the SDK\n * shipping a customer's internal hostnames to the collector.\n */\n allowlistSize(): number {\n return this.allowlist.size;\n }\n\n /**\n * `app.start`'s own row — a root, so `span.id === rum.action.id` and there is no\n * `parent.span.id`. No `span.duration_ms` either: roots derive theirs server-side.\n */\n launchRootAttributes(): TraceAttributes {\n return spanKeys(this.launch, true, this.launch.spanStartMs).keys;\n }\n\n /**\n * Tier 1 for a `view`, captured at view **entry** — the action that opened the screen,\n * not the one that closed it (§6.3). A **point span**: `span.start_time` and never\n * `span.duration_ms`, because view dwell is not span duration and a width here would\n * stretch every tap-that-navigates envelope across the whole time the user sat there.\n */\n viewSpan(entryAt: number): TraceAttributes {\n const { root, isRoot } = this.attach(\"navigation\", entryAt);\n return spanKeys(root, isRoot, entryAt).keys;\n }\n\n /**\n * Tier 1 for a `ui.interaction`, captured at the click's **mint** (§4.6/§6.2). The one\n * unconditional mint in the table: a tap is a new user action by definition, so it\n * *replaces* whatever root was live rather than joining it — which is what makes the\n * request a tap fires a child of the tap and not of the route change before it.\n *\n * A root, so `span.id === rum.action.id`, no `parent.span.id`, and no `span.duration_ms`\n * (§6.1 — a root's width is derived server-side).\n */\n interactionSpan(mintedAt: number): TraceAttributes {\n const root = this.mint(\"interaction\", mintedAt, mintedAt);\n return spanKeys(root, true, mintedAt).keys;\n }\n\n /**\n * Tier 1 for an `http.request`, captured at **send** — the root live then is the parent,\n * and `span.start_time` is the send — plus §6.5's outcome ladder and the one header the\n * SDK ever writes. Returns the finisher, so duration is stamped from the same pair of\n * timestamps the row's `http.duration_ms` uses.\n *\n * ponytail: a `send()` that throws synchronously never calls the finisher, so a root it\n * minted lives on for up to 2 s with no row describing it. Rare enough to leave; give\n * the finisher a `discard()` sibling if the orphan ratio ever shows up.\n */\n requestTrace(startedAt: number, ctx: RequestTraceCtx): RequestTrace {\n const decision = this.decide(ctx);\n\n // `adopted` is the consumer's trace, not ours: §6.1 says `span.id` mirrors the\n // foreign parent-id and `parent.span.id` is omitted, so the row is root-shaped inside\n // *their* trace. The carrier is deliberately left untouched — extending a local root\n // from a request that reports foreign ids would attribute their action to ours.\n if (decision === \"adopted\") {\n const foreign = parseTraceparent(ctx.consumerTraceparent)!;\n const keys: TraceAttributes = {\n \"trace.id\": foreign.traceId,\n \"span.id\": foreign.spanId,\n \"rum.action.id\": foreign.spanId,\n \"span.start_time\": new Date(startedAt).toISOString(),\n \"traceparent.outcome\": \"adopted\",\n };\n return { finish: () => keys };\n }\n\n // **Every skip still stamps local ids with no wire header** (§6.5) — that is what\n // makes \"missing DB join ⇒ header stripped in transit\" computable, so attribution\n // runs before the ladder is resolved and regardless of what it says.\n const { root, isRoot, attribution } = this.attach(\"request\", startedAt);\n const outcome: TraceOutcome | undefined =\n decision === \"inject\" ? attribution : decision;\n const span = spanKeys(root, isRoot, startedAt);\n const keys: TraceAttributes = {\n ...span.keys,\n ...(outcome ? { \"traceparent.outcome\": outcome } : {}),\n };\n // `span.duration_ms` is Tier 1 **children only** (§6.1) — a root's is derived.\n const finish = isRoot\n ? () => keys\n : (endedAt: number) => ({ ...keys, \"span.duration_ms\": Math.max(0, endedAt - startedAt) });\n\n return decision === \"inject\"\n ? { finish, header: formatTraceparent(root.traceId, span.spanId) }\n : { finish };\n }\n\n /**\n * §6.5's ladder above the `injected_*` split, in table order — the precedence *is* the\n * order of these returns. `undefined` means the attribute is omitted entirely: absent\n * means not traced, which is the honest report for a consumer who never opted in and for\n * a sampled-out session (which injects no header at all, not a `flags=00` id).\n */\n private decide(ctx: RequestTraceCtx): TraceOutcome | \"inject\" | undefined {\n if (this.allowlist.size === 0 || !ctx.sampled) return undefined;\n if (!allowsHost(this.allowlist, ctx.url)) return \"skipped_off_allowlist\";\n // Outranks `skipped_consumer_set` because a `no-cors` request's headers guard drops\n // the *consumer's* traceparent too — mirroring its ids would manufacture a false\n // correlation against a header no server ever saw.\n if (ctx.noCors) return \"skipped_no_cors\";\n // Never-strip: ownership is the consumer's **write-time presence flag**, recorded by\n // the interceptor when they set it — never inferred from the value's shape, because\n // an SDK-minted and a consumer-minted traceparent are byte-identical by construction.\n if (ctx.consumerTraceparent != null) {\n return parseTraceparent(ctx.consumerTraceparent) ? \"adopted\" : \"skipped_consumer_set\";\n }\n return \"inject\";\n }\n\n /**\n * Tier 2 — annotation-only (§6.3): join the trace without occupying a span. No\n * `span.id`, no parent, no duration, and **no minting** — §6.2 lists the four events\n * that mint and these are not among them, so an untraced crash carries no trace keys\n * rather than inventing a root nothing else will ever join.\n */\n annotate(now: number = Date.now()): TraceAttributes {\n const live = this.liveRoot(now);\n if (!live) return {};\n return {\n \"trace.id\": live.traceId,\n \"rum.action.id\": live.spanId,\n \"trace.root_type\": live.rootType,\n };\n }\n\n /**\n * Drop the carrier. Background clears it on both builds, so a resumed app's first fetch\n * mints its own root rather than joining an action from before the user left (§6.2);\n * session rotation clears it because `trace.id` never spans a `session.id` (§6.6).\n */\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Retire the launch root and mint a replacement, keeping the *reported* launch time —\n * the process really did start then. For a session rotation that lands **before**\n * `app.start`, which is the expired-record cold launch and therefore §4.3's most common\n * path of all.\n *\n * Clearing alone would not do: `app.start` reports the launch root at any age, so its\n * root row would ship under the **new** session while the initial `view` it fathered\n * shipped under the **old** one. `trace.id` would span a `session.id` (§6.6, invariant 2)\n * and `GROUP BY rum.action.id` would never reassemble the launch envelope. Re-minting\n * keeps the retired trace wholly inside the retired session.\n *\n * ⚠ The retired trace then has children and no root row — the initial `view` row is a\n * rootless child. That is the same condition process death already produces by design\n * (a killed view emits nothing), and it is the cheaper of the two wrongs.\n */\n restartLaunchRoot(): void {\n this.launch = this.mint(\"launch\", Date.now(), this.launch.spanStartMs);\n }\n\n /**\n * Join the live root, or mint one of `mintAs`. A span's *start* extends the root.\n *\n * `attribution` is read **before** `liveRoot()` drops an expired carrier, which is the\n * only place the two unattributed causes are still distinguishable: a carrier that just\n * aged out is `injected_expired` (context lost), no carrier at all is\n * `injected_unattributed` (no action) — the split §6.5 asks the backend to keep.\n */\n private attach(mintAs: TraceRootType, now: number): {\n root: Root;\n isRoot: boolean;\n attribution: \"injected_attributed\" | \"injected_expired\" | \"injected_unattributed\";\n } {\n const hadCarrier = this.root !== undefined;\n const live = this.liveRoot(now);\n if (live) {\n live.lastTouch = now;\n return { root: live, isRoot: false, attribution: \"injected_attributed\" };\n }\n return {\n root: this.mint(mintAs, now, now),\n isRoot: true,\n attribution: hadCarrier ? \"injected_expired\" : \"injected_unattributed\",\n };\n }\n\n private mint(rootType: TraceRootType, now: number, spanStartMs: number): Root {\n const root: Root = {\n traceId: mintTraceId(),\n spanId: mintSpanId(),\n rootType,\n startedAt: now,\n spanStartMs,\n lastTouch: now,\n };\n this.root = root;\n return root;\n }\n\n /**\n * The carrier if it is still live, dropping it otherwise. Both boundaries are strict —\n * a root sitting exactly on one has not crossed it — matching the session boundaries.\n *\n * A **negative** delta is clamped to expired (§6.7): `Date.now()` can move backwards on\n * an NTP correction, and reading that as \"fresh\" would hold one root open indefinitely.\n */\n private liveRoot(now: number): Root | undefined {\n const root = this.root;\n if (!root) return undefined;\n const idle = now - root.lastTouch;\n const age = now - root.startedAt;\n if (idle < 0 || age < 0 || idle > ROOT_IDLE_MS || age > ROOT_CAP_MS) {\n this.root = undefined;\n return undefined;\n }\n return root;\n }\n}\n\n/**\n * §6.1's identity, in one place so no call site can get it wrong: `rum.action.id` is the\n * root's `span.id` — **equal to `span.id` on a root and to `parent.span.id` on a child**.\n * That is the single thing that makes an action's envelope one `GROUP BY` and not a\n * self-join. `trace.root_type` is denormalized onto every child so launch traffic separates\n * from tap traffic without joining back to the root.\n */\nfunction spanKeys(root: Root, isRoot: boolean, spanStartMs: number): { keys: TraceAttributes; spanId: string } {\n const spanId = isRoot ? root.spanId : mintSpanId();\n return {\n spanId,\n keys: {\n \"trace.id\": root.traceId,\n \"span.id\": spanId,\n ...(isRoot ? {} : { \"parent.span.id\": root.spanId }),\n \"rum.action.id\": root.spanId,\n \"trace.root_type\": root.rootType,\n \"span.start_time\": new Date(spanStartMs).toISOString(),\n },\n };\n}\n","/**\n * Fixed-size ring buffer of recent actions, attached to `app.crash` as the\n * JSON-stringified `crash.breadcrumbs` (last N, oldest evicted first). See #28.\n */\nexport class BreadcrumbBuffer {\n private items: any[] = [];\n\n constructor(private max = 20) {}\n\n add(item: any): void {\n this.items.push(item);\n if (this.items.length > this.max) this.items.shift();\n }\n\n list(): any[] {\n return [...this.items];\n }\n\n toJSON(): string {\n return JSON.stringify(this.items);\n }\n}\n","import { debug } from \"./debug\";\nimport type { TelemetryEvent } from \"./telemetry\";\n\n/**\n * The consumer's scrubbing hook (§3.6). One event, **synchronous**, run at **enqueue**.\n *\n * Enqueue and not flush: a failed send persists the batch through the `Store`, so a\n * flush-time hook would let unscrubbed PII hit disk — and on native that disk outlives\n * the process. Synchronous because `app.crash` flushes during teardown and a\n * Promise-returning hook would put a host `await` in a dying app's path.\n */\nexport type BeforeSend = (event: TelemetryEvent) => TelemetryEvent | null;\n\nexport type HookOutcome =\n | { kind: \"kept\"; event: TelemetryEvent }\n | { kind: \"dropped\" } // returned null — the hook working as intended (sdk.hook_dropped)\n | { kind: \"failed\" }; // threw — fail closed (sdk.hook_failed)\n\n/**\n * Tier A — immutable. Re-stamped from the original after the hook returns, never\n * defended by throwing: the realistic hook is `delete attrs[k]` in a loop and the\n * realistic failure is over-deletion, so an over-broad hook must not be able to get a\n * consumer's whole feed silently discarded behind a 2xx.\n *\n * A writable `eventName` would also restore the public path to `app.crash` that the\n * allowlist removes on purpose.\n */\nconst TIER_A_KEYS = new Set([\n // `session.sequence` is not in the contract's §3.6 list, which names only `event.sequence`.\n // Both are transmission ordinals with no scrubbing use case: (session.id, session.sequence)\n // orders a session's batches (#92) and (session.id, event.sequence) is the backend's dedup\n // key (#94, §2.4) — a hook that could delete either would break the thing it keys.\n //\n // `event.sequence` is stamped in `enqueue()` *after* this hook runs, so on the way in it is\n // absent and the re-stamp is a no-op; it is listed because the hook must not be able to\n // forge one, and because a re-run over an already-stamped event must leave it alone.\n \"session.id\", \"session.start_time\", \"session.sequence\", \"event.sequence\",\n \"device.platform\", \"trace.id\", \"span.id\", \"parent.span.id\",\n \"rum.action.id\", \"view.id\",\n]);\nconst isTierA = (key: string) =>\n TIER_A_KEYS.has(key) || key.startsWith(\"sdk.\") || key.startsWith(\"app.\");\n\n/**\n * Tier B — rewritable, not deletable. Hashing `device.id` to a tenant-local id is a\n * legitimate ask; deleting it 400s the entire batch at the collector (§2.3).\n * Everything else is Tier C and untouched — that is where the PII actually lives.\n */\nconst TIER_B_KEY = \"device.id\";\n\n/** Run the hook and enforce the tiers on what comes back. */\nexport function applyBeforeSend(original: TelemetryEvent, hook: BeforeSend): HookOutcome {\n let returned: TelemetryEvent | null;\n try {\n // The hook gets a copy, never `original`. The realistic hook is `delete\n // attrs[k]` in a loop, and re-stamping from an object the hook already mutated\n // would restore nothing — the tier table would be decorative.\n returned = hook({ ...original, attributes: { ...original.attributes } });\n } catch (err) {\n // Fail closed. Sending the original would ship the exact field the hook existed\n // to remove, which is the one outcome a broken scrubber must never produce.\n debug.warn(\"Telemetry: beforeSend threw — event dropped\", err);\n return { kind: \"failed\" };\n }\n // Only an explicit `null` is the documented \"drop this row\". Anything else the hook\n // hands back that isn't an event — `undefined` from a forgotten `return`, a string,\n // a number — is a *bug*, and counting it as dropped would inflate the \"my rule is\n // working\" counter with the exact case §3.6 says the two counters exist to separate.\n if (returned === null) return { kind: \"dropped\" };\n if (typeof returned !== \"object\") {\n debug.warn(\"Telemetry: beforeSend returned a non-event — event dropped\", returned);\n return { kind: \"failed\" };\n }\n return { kind: \"kept\", event: restamp(original, returned) };\n}\n\n/** Replace the Tier A slice wholesale — that covers deletion and forgery in one pass. */\nfunction restamp(original: TelemetryEvent, returned: TelemetryEvent): TelemetryEvent {\n const attributes: Record<string, any> = {};\n for (const [k, v] of Object.entries(returned.attributes ?? {})) {\n if (!isTierA(k)) attributes[k] = v;\n }\n for (const [k, v] of Object.entries(original.attributes ?? {})) {\n if (isTierA(k)) attributes[k] = v;\n }\n // Not just `undefined`: a hook writing `null` or `\"\"` here 400s the batch exactly the\n // same way `delete` does, so anything that isn't a usable id is treated as a deletion.\n const rewritten = attributes[TIER_B_KEY];\n if (typeof rewritten !== \"string\" || rewritten === \"\") {\n const originalId = original.attributes?.[TIER_B_KEY];\n if (originalId === undefined) delete attributes[TIER_B_KEY];\n else attributes[TIER_B_KEY] = originalId;\n }\n\n // Built key by key rather than spread: a hook that bolts `metricName` onto an event\n // must not be able to move a row onto the metric path.\n const kept: TelemetryEvent = {\n type: original.type,\n timestamp: original.timestamp,\n attributes,\n };\n if (original.eventName !== undefined) kept.eventName = original.eventName;\n if (original.metricName !== undefined) kept.metricName = original.metricName;\n // `value` is Tier C — rewritable — but a metric without one is not a metric.\n if (original.type === \"metric\") kept.value = returned.value ?? original.value;\n return kept;\n}\n","/**\n * `JSON.stringify` that returns `undefined` instead of throwing — a cycle, a `toJSON` that\n * threw, a `BigInt`. Shared by `user.custom.*`'s bag rule (§4.10) and `flattenWithPrefix`'s\n * depth cap: both exist so a bad consumer payload is a dropped key rather than an exception\n * raised inside the SDK, in the host app's own call stack.\n */\nexport function stringifyOrDrop(value: unknown): string | undefined {\n try {\n return JSON.stringify(value) ?? undefined;\n } catch {\n return undefined;\n }\n}\n","// §4.10, #107 — the profile attributes for `user.profile.update`, and nothing else.\n//\n// PII used to ride the Context block of every event: a 10,000-event session put 10,000\n// copies of an email address on the wire and at rest. It now appears on the one event\n// that needs it, keyed by `user.id` (which `identify({ userId })` sets).\n//\n// The bag rule here retires three v3 defects at once — `flattenWithPrefix` had no depth\n// guard, so a cyclic `customAttributes` value recursed to a `RangeError` *inside the SDK*\n// and crashed the host app on a bad `identify()`; arrays passed through raw against the\n// primitive-values rule; and nested objects recursed into the bag. A bad payload is a\n// dropped key, never a throw.\n\nimport { isDev } from \"./debug\";\nimport { stringifyOrDrop } from \"./utils/json\";\nimport type { UserProfile } from \"./telemetry\";\n\n// Backend column widths (§4.10, work-list item 14). `rum_users.phone` is VARCHAR(50).\nexport const NAME_CAP = 255;\nexport const EMAIL_CAP = 255;\nexport const PHONE_CAP = 50;\nexport const CUSTOM_MAX_KEYS = 64;\nexport const CUSTOM_KEY_CAP = 64;\nexport const CUSTOM_VALUE_CAP = 255;\n\nlet warnedOnce = false;\n\nfunction capString(value: unknown, max: number): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.slice(0, max);\n return trimmed.length ? trimmed : undefined;\n}\n\n/**\n * A primitive rides as itself; anything else is stringified and truncated. A value that\n * cannot be stringified — a cycle, a throwing `toJSON` — drops the key rather than the\n * process. Consumer keys pass through **verbatim, casing included** (§4.10): the SDK\n * imposes no normalization here, so an over-long key is dropped, never truncated.\n */\nfunction customValue(value: unknown): string | number | boolean | undefined {\n if (value === null || value === undefined) return undefined;\n if (typeof value === \"number\" || typeof value === \"boolean\") return value;\n if (typeof value === \"string\") return value.slice(0, CUSTOM_VALUE_CAP) || undefined;\n const flat = stringifyOrDrop(value);\n return flat === undefined ? undefined : flat.slice(0, CUSTOM_VALUE_CAP) || undefined;\n}\n\n/**\n * The wire keys for `user.profile.update`. `user.id` is not here — it rides the Context\n * block, where it is present on this event by construction.\n */\nexport function buildProfileAttributes(profile: UserProfile | undefined): Record<string, any> {\n const attrs: Record<string, any> = {};\n if (!profile) return attrs;\n\n const name = capString(profile.fullName, NAME_CAP);\n const email = capString(profile.email, EMAIL_CAP);\n const phone = capString(profile.phone, PHONE_CAP);\n if (name !== undefined) attrs[\"user.name\"] = name;\n if (email !== undefined) attrs[\"user.email\"] = email;\n if (phone !== undefined) attrs[\"user.phone\"] = phone;\n\n let dropped = 0;\n let kept = 0;\n for (const key of Object.keys(profile.customAttributes || {})) {\n if (key.length > CUSTOM_KEY_CAP || kept >= CUSTOM_MAX_KEYS) { dropped++; continue; }\n const value = customValue((profile.customAttributes as any)[key]);\n if (value === undefined) { dropped++; continue; }\n attrs[`user.custom.${key}`] = value;\n kept++;\n }\n\n if (dropped > 0) {\n attrs[\"user.custom_dropped\"] = dropped;\n if (isDev() && !warnedOnce) {\n warnedOnce = true;\n console.warn(\n `[edge-telemetry] identify(): ${dropped} custom attribute(s) dropped. ` +\n `user.custom.* is bounded at ${CUSTOM_MAX_KEYS} keys, ${CUSTOM_KEY_CAP}-char keys ` +\n `and ${CUSTOM_VALUE_CAP}-char values; unserializable values are dropped.`\n );\n }\n }\n\n return attrs;\n}\n","import { debug } from \"./debug\";\nimport { NavigationTracker } from \"../adapters/navigationTracker\";\nimport { ScreenTimingTracker } from \"../adapters/screenTiming\";\nimport { ViewManager, type ViewNameSource } from \"../adapters/viewManager\";\nimport { TraceManager } from \"../adapters/traceManager\";\nimport { BreadcrumbBuffer } from \"./breadcrumbs\";\nimport { buildErrorAttributes, type ErrorSource } from \"../adapters/crashCapture\";\nimport { randomHex } from \"./utils/uuid\";\nimport type { Store } from \"./store\";\nimport { memoryStore } from \"./memoryStore\";\nimport { applyBeforeSend, type BeforeSend } from \"./beforeSend\";\nimport { buildProfileAttributes } from \"./userProfile\";\nimport { stringifyOrDrop } from \"./utils/json\";\nimport { version as PKG_VERSION } from \"../../package.json\";\n\nexport type { BeforeSend } from \"./beforeSend\";\nimport type { Orientation } from \"../adapters/viewport\";\nexport type { Orientation };\n\n// v3 wire contract constants\n// §3.3 / §12's item 10 — `react-native-{Platform.OS}`, joining Flutter's `flutter-{os}` shape.\n// ⚠ NOT a closed three-value enum: RN-Windows emits `react-native-windows`. The bare\n// framework name survives only for a direct `new Telemetry()` with no entry-declared\n// platform, which is the same gap `apiKey` already has.\nconst sdkPlatform = (platform?: string) => platform ? `react-native-${platform}` : \"react-native\";\nconst SDK_VERSION = PKG_VERSION; // sdk.version follows the published package version\nconst SESSION_IDLE_MS = 30 * 60 * 1000; // rotate the session after 30 min of inactivity (iOS ADR-004)\n// New in v4 (§4.2). Removing the process-death boundary lets a backgrounded app's\n// http.request traffic hold one session open forever; this bounds length, not count.\nconst SESSION_MAX_MS = 4 * 60 * 60 * 1000;\n\n// A cycle needs *a* bound, not a visited-set. Nothing on this wire nests past deviceInfo's\n// two levels, so 8 is unreachable for a real payload and a cycle stops here (§4.10).\nconst FLATTEN_MAX_DEPTH = 8;\n\n/**\n * The in-memory queue's cap (§9.4). Drop-oldest, `app.crash` evicted last: a long\n * offline period on an unbounded queue grows inside the host app's heap, and evicting\n * crashes first would make the crash-free rate read *better* the worse the network is.\n */\nconst QUEUE_MAX_EVENTS = 500;\n\n/** `sdk.drop_reason` (§11). `rejected` has no producer until 4xx-drops-the-batch lands (#113). */\nexport type DropReason = \"queue_full\" | \"store_full\" | \"rejected\";\n\n/**\n * Which row to evict to get back under a cap: the oldest non-crash, or — when every\n * row is a crash — the oldest, because growing past the cap is not an option either.\n *\n * Shared with the offline store (`adapters/failedEvents.ts`) so the in-memory queue and\n * the disk queue can never disagree about what survives.\n *\n * ponytail: O(n) scan per eviction, n <= 500. Track the first non-crash index if a\n * profile ever shows this on a hot path.\n */\n/** An event's `event.sequence`, or -1 before `enqueue()` has stamped one. */\nconst seqOf = (e: TelemetryEvent): number => e.attributes?.['event.sequence'] ?? -1;\n\n/**\n * §4.7's two error names. Both book `sdk.error_count` and `view.error_count` — a closed\n * enumeration, not resource failures and not `console.warn`. Only `app.crash` gets the\n * dedicated crash path and the eviction reprieve, which is the whole point of the split:\n * `COUNT(event_name='app.crash')` must stay a crash count nobody can forget to filter.\n */\nexport const isErrorName = (name?: string): boolean =>\n name === \"app.crash\" || name === \"app.error\";\n\nexport function evictIndex(events: TelemetryEvent[]): number {\n const i = events.findIndex(e => e.eventName !== \"app.crash\");\n return i === -1 ? 0 : i;\n}\n\n/** Store key for the self-minted, persisted `device.id` (#91). */\nexport const DEVICE_ID_KEY = \"telemetry_device_id\";\n/** Store key for the resumable session record (#92). */\nexport const SESSION_KEY = \"telemetry_session\";\n\n/** `session.reason` domains: 3 on session.started, the last 2 on session.finalized (§4.1/§4.2). */\nexport type SessionReason = \"launch\" | \"idle\" | \"max_duration\";\nexport type SessionEndReason = Exclude<SessionReason, \"launch\">;\n\n/**\n * What survives process death, tab close, hard reload and bfcache restore (§4.2).\n * On web the Store is `localStorage`, so this record is browser-wide and shared\n * across tabs — intended, not a leak.\n */\ntype PersistedSession = {\n id: string;\n start: number;\n lastActivity: number;\n sequence: number;\n // Per-session, per-event ordinal (§2.4). Persisted for the same reason `sequence` is:\n // a resumed session restarting at 0 emits duplicate (session.id, event.sequence) pairs,\n // which is exactly the key the backend's dedup index is built on.\n eventSequence: number;\n eventCount: number;\n errorCount: number;\n // The sticky sample decision and the rate it was rolled at (§3.6). Both travel with\n // the session, because a resume is not a rotation — re-rolling on relaunch produces\n // a half-sampled session, and shipping the *current* config rate on a session rolled\n // at the old one makes every extrapolation off it wrong.\n sampled: boolean;\n sampleRate: number;\n};\n\n/** What hydration decided, so the emitting half stays free of storage concerns. */\ntype SessionHydration =\n | { kind: \"resumed\" } // inside the idle window: no session.started (§4.1)\n | { kind: \"nothing\" } // miss or unavailable — storage told us nothing\n | { kind: \"expired\"; reason: SessionEndReason }; // adopted, now owed a finalize + a fresh session\n/** The collector caps `user.id` at 255; truncate at source rather than be rejected (§3.2). */\nconst USER_ID_MAX = 255;\n\n// Names the backend routes. Anything else is remapped to `custom_event` with the\n// original name carried as `event.name`. Includes metric names so the metric path\n// (slice: native metrics) isn't remapped.\nconst ALLOWED_NAMES = new Set<string>([\n \"session.started\", \"session.finalized\", \"app_lifecycle\", \"navigation\",\n \"screen.duration\", \"http.request\", \"network_change\",\n \"user.profile.update\", \"custom_event\", \"app.crash\", \"view\",\n // ⚠ `app.error` needs backend allowlist sign-off before it ships (#100, §4.7).\n \"app.error\",\n // §4.6/#102 — replaces `user.interaction`, which is off the list as of v4. The\n // `rum_ui_interactions` columns are already built; the name still needs sign-off.\n \"ui.interaction\",\n // ⚠ `app.start` needs backend allowlist sign-off before it ships (#98, §4.3/§6.2) — an\n // unlisted eventName is dropped on ingest. It is listed here, so it is being emitted.\n \"app.start\",\n \"frame_render_time\", \"memory_usage\",\n \"LCP\", \"FCP\", \"CLS\", \"INP\", \"TTFB\",\n // ⚠ `page_load`, `resource_timing` and `long_task` are **retired, not deferred**\n // (§10.2, #109) — as `user.interaction` was in #102, four in total for a net-zero v4.\n // Do not add them back: `page_load`'s job is done three ways over (`view` +\n // `view.loading_time` + the vitals), `resource_timing` would blow the 500-event queue on\n // one asset-heavy view for a namespace with no column, and `long_task`'s only APIs are\n // Chromium-only while `frame_render_time` detects jank on every runtime. A name off this\n // list is rewritten to `custom_event`, so all four are unreachable, not merely unused.\n]);\n\n/**\n * §5.1's `metric.unit` — NULL on every RN row ever landed until v4. Without it a CLS of\n * `0.08` renders as a flat zero line beside an LCP of `4000` in the same `value` column,\n * so `score` for CLS is the key entry, not `ms`.\n *\n * A name absent from this map ships no unit: absent means the SDK had nothing, and a\n * consumer's own `recordMetric()` name has no unit we can honestly claim.\n */\nconst METRIC_UNIT: Record<string, string> = {\n frame_render_time: \"ms\",\n memory_usage: \"MB\",\n LCP: \"ms\", FCP: \"ms\", INP: \"ms\", TTFB: \"ms\",\n CLS: \"score\",\n};\n\n/**\n * §6.3's Tier 2 — annotation-only: `trace.id`, `rum.action.id` and `trace.root_type`, no\n * span. Stamped here because these rows have no earlier capture point; Tier 1 rows\n * (`app.start`, `view`, `http.request`) carry keys captured at launch / view entry / send\n * and hand them in as `data`, and everything else is Tier 3 — **trace-free, all metrics\n * included**. A windowed aggregate belongs to no single action, and a `rum.action.id` on\n * one would invite a `GROUP BY` over a number that was never attributable.\n *\n * ⚠ \"% of errors attributed to an action\" is bounded well below 100% by design: a consumer\n * calling `captureError()` from a background retry has no live root and gets no trace keys at\n * all. `ui.interaction` is Tier 1: its keys are captured at the click's\n * mint and handed in as `data`, like the other three.\n */\nconst TIER_2_NAMES = new Set<string>([\"app.crash\", \"app.error\", \"custom_event\"]);\n\n/**\n * §4.6's mint/emit split, for the one event that has one. A `ui.interaction` is emitted\n * after its dead-click window, so its wire `timestamp` and its `view.id` / `view.name` /\n * `session.id` are snapshotted at the click and replayed here — otherwise a tap that\n * navigates is booked against the view it opened.\n */\nexport type LogSnapshot = {\n /**\n * Epoch ms of the click. Becomes the row's `timestamp`. **Omitted by a span-carrying row\n * that is not deferred** — an `http.request` freezes its *attribution* at send but still\n * reports the completion time §4.4 gives it.\n */\n at?: number;\n viewId: string;\n sessionId: string;\n /** Epoch ms — `session.start_time` freezes with the id it belongs to (§3.1). */\n sessionStart: number;\n};\n\n// Events carry `eventName`; metrics carry `metricName` + numeric `value` (v3 §\"Event vs Metric\").\n// Kept as one loose shape (not a strict union) so callers can read `.eventName` without narrowing;\n// the emit helpers (log / logMetric) set the right fields, and JSON.stringify drops the undefined ones.\nexport type TelemetryEvent = {\n type: 'event' | 'metric';\n eventName?: string;\n metricName?: string;\n value?: number;\n timestamp: string; // ISO 8601 (v3 wire contract) — never ms epoch\n attributes?: Record<string, any>;\n};\n\nexport interface Sender {\n send(events: TelemetryEvent[]): Promise<void>;\n /**\n * Persist a batch that could not be sent. Returns how many rows the offline store's\n * cap cost, so core can book them as `sdk.drop_reason = \"store_full\"` (§9.4) — the\n * counter lives on the Context block, which only core assembles. `void` is still a\n * valid return: a consumer's own sender is not obliged to have a cap.\n */\n onFailure?(events: TelemetryEvent[]): Promise<number | void>;\n replayFailed?(): Promise<void>;\n}\n\n\n\nexport interface CrashHandlerOptions {\n // console.error -> app.error, console.warn -> a breadcrumb. Default OFF (§4.7, #100):\n // React's own dev-mode warnings dominated v3's crash count.\n captureConsole?: boolean;\n}\n\nexport interface CrashHandler {\n attach(options?: CrashHandlerOptions): Promise<void>;\n}\nexport interface NetworkInfo {\n type?: string; // \"wifi\", \"cellular\", \"ethernet\", \"unknown\", etc.\n is_connected?: boolean;\n}\n\nexport interface NetworkInfoHandler {\n start(telemetry: Telemetry): Promise<NetworkInfo>;\n collect(): Promise<NetworkInfo>;\n}\n\nexport interface DeviceInfoHandler {\n start(telemetry: Telemetry): Promise<void>;\n collect(): Promise<DeviceInfo>;\n}\nexport interface FrameDropsHandler {\n start(): Promise<void>;\n}\n\nexport interface NetworkHandler {\n start(): Promise<void>;\n}\n\nexport interface MemoryHandler {\n start(): Promise<void>;\n /** Torn down by `shutdown()`: unlike the rAF-driven trackers this one owns a timer. */\n stop?(): void;\n}\n\nexport interface NavigationHandler {\n start(): Promise<void>;\n}\n\n\nexport interface DeviceInfo {\n app: {\n name: string;\n version: string;\n build_number?: string;\n package_name?: string;\n };\n device: {\n // No `id` here (#91): core self-mints and persists device.id, and stamps it onto the\n // Context block *after* this block is flattened — an adapter-set id would be dead.\n platform: string;\n platform_version?: string;\n model?: string;\n manufacturer?: string;\n brand?: string;\n android_sdk?: string;\n android_release?: string;\n hardware?: string;\n product?: string;\n ios_system_name?: string;\n // §3.3 ✱ — native only. \"Does this crash only on cheap devices?\"\n cpu_abi?: string;\n low_ram?: boolean;\n // §3.3 ✱ — both builds, never null: CLS and LCP scale with the viewport, and\n // `orientation` splits CLS, dwell and interactions. Read at log time, like\n // `network.*`, because a device rotates mid-session (§3.1).\n screen_density?: number;\n screen_width_px?: number;\n screen_height_px?: number;\n orientation?: Orientation;\n };\n}\n\nexport interface UserProfile {\n userId?: string;\n /** @deprecated Removed in v5 — use `identify({ name })`. Still the source of `user.name`. */\n fullName?: string;\n /** @deprecated Removed in v5 — never reached the wire; `identify()` could not set it. */\n firstName?: string;\n /** @deprecated Removed in v5 — never reached the wire; `identify()` could not set it. */\n lastName?: string;\n email?: string;\n phone?: string;\n /** @deprecated Removed in v5 — no column and no reader; `user.avatar` is off the wire (§3.4). */\n avatar?: string;\n customAttributes?: Record<string, any>;\n /** @deprecated Removed in v5 — in-memory only, so it meant \"first identify() this process\" (§3.4). */\n createdAt?: number;\n /** @deprecated Removed in v5 — byte-identical to `createdAt` for any one-identify() app (§3.4). */\n updatedAt?: number;\n}\n\n\ntype Opts = {\n sender?: Sender;\n batchSize?: number;\n flushIntervalMs?: number;\n endpoint?: string;\n sessionId?: string;\n userId?: string;\n sdkVersion?: string;\n platform?: string; // device OS (ios|android|web); forms the device/session id suffix\n // What `app.start` reports as `span.start_time` (§6.2). Web passes\n // `performance.timeOrigin`; native leaves it defaulted to `initialize()`. Neither is a\n // fork time, and the two are not comparable — see TraceManager.\n traceLaunchStart?: number;\n deviceInfoHandler?: DeviceInfoHandler;\n networkInfoHandler?: NetworkInfoHandler;\n store?: Store; // persisted state port (#89); defaulted per build by the entry\n // Constructor-only (§3.6). A runtime setter leaves a window between init and\n // registration where session.started, the launch root and the early http.requests\n // all land — that window is the reason, and it is not negotiable.\n beforeSend?: BeforeSend;\n sessionSampleRate?: number; // 0.0-1.0, sticky per session; default 1 (send everything)\n // §6.4, #99. Bare hosts, exact match, ports ignored, **empty by default** — v4 is dark\n // on upgrade, so nobody's CORS breaks until they opt in. Listing a host is the\n // consumer's assertion that that host's CORS config allows the `traceparent` header.\n traceHostAllowlist?: string[];\n // §4.8's symbolication resolve key, third part of `(app_id, device.platform, app.build_id)`.\n // Consumer-supplied — the SDK has zero OTA awareness and cannot derive one — omitted when\n // unset, and **never** falls back to version + build_number: under Expo Updates or\n // CodePush the binary is unchanged, so that fallback resolves against the wrong map and\n // produces frames that are plausible and wrong, with nothing on the row marking them.\n buildId?: string;\n // The deprecated native screen feeds — `navigation` and `screen.duration` (§4.11) — on\n // the ROUTE path. Defaults on, because shared core's v3 behaviour *is* the native one;\n // the web entry opts out, having never emitted `screen.duration` at all. It does not\n // silence web's own history path, which still emits `navigation`. Config from the entry,\n // which is the platform split point — not a branch inside shared code.\n deprecatedScreenFeeds?: boolean;\n};\n\n/**\n * A bad rate must not silently mute a deployment: `Math.random() < NaN` is always false,\n * so an unvalidated `undefined`-shaped value would sample every session out and look\n * exactly like a dead collector. Out of range means \"the consumer meant something\", and\n * the only safe reading of that is 1.\n */\nfunction normalizeSampleRate(rate: number | undefined): number {\n if (rate === undefined) return 1;\n // Number.isFinite is false for a non-number too, so this covers a JS caller's \"0.5\".\n if (!Number.isFinite(rate) || rate < 0 || rate > 1) {\n debug.warn(`Telemetry: sessionSampleRate must be a number in [0,1] — got ${rate}; using 1`);\n return 1;\n }\n return rate;\n}\n\n/**\n * Telemetry core: queueing, batching, retries, auto-replay and session/user management.\n * Keeps the external API you already use: log(name, data?), flush(), shutdown().\n */\nexport class Telemetry {\n private queue: TelemetryEvent[] = [];\n private sender?: Sender;\n private batchSize: number;\n private flushIntervalMs: number;\n private intervalId: any = null;\n private endpoint?: string;\n private crashHandler?: CrashHandler;\n private navigationTracker?: NavigationTracker;\n // single screen-tracking API (timed); used by the native screenStart/screenEnd\n public screens: ScreenTimingTracker;\n // The View entity (§4.5, #96): view.id/view.name on every row, the `view` event at each\n // of the four exit boundaries, and the name ladder. Public because the route, lifecycle\n // and interaction adapters all feed it.\n public readonly views: ViewManager;\n // Trace and span core (§6, #98): the live-root carrier, the three roots this ticket\n // mints and the tier key builders. Public for the same reason `views` is — the\n // interceptors, the lifecycle adapter and ViewManager all read it, synchronously.\n public readonly trace: TraceManager;\n // The web click tracker, parked here so `trackInteractions()` is idempotent: a second\n // call must reuse this tracker rather than add a second capture-phase listener (#102).\n // Untyped and unset on native, whose producer is the public `trackTap(name)` (#103) and\n // needs no tracker here — its only state is a rage window, held by TelemetryNative.\n public webInteractions?: { start(): void };\n // The web vitals tracker, parked here for the same reason (§5.3, #106): `trackWebVitals()`\n // must reuse it, or a second call double-subscribes and doubles every vital row. Unset on\n // native, which emits no vitals ever.\n public webVitals?: { start(): Promise<void> };\n private readonly deprecatedScreenFeeds: boolean;\n // last-known screen; best-effort context for the deprecated screen feeds (#33)\n public currentScreen?: string;\n\n private networkInfoHandler: NetworkInfoHandler;\n private deviceInfoHandler: DeviceInfoHandler;\n\n // Persisted state (#89). The entry injects webStore()/nativeStore(); shared core\n // never imports either. Both entries always inject, so the fallback below only\n // covers a direct `new Telemetry()` — it reports `unavailable`, putting such a\n // caller on the same branch as incognito rather than crashing.\n // Public because it is this seam's only observation point until v4's device.id,\n // session resume, sticky sample rate and capped offline store read it.\n public readonly store: Store;\n\n private frameDropsHandler?: FrameDropsHandler;\n private networkHandler?: NetworkHandler;\n private memoryHandler?: MemoryHandler;\n private navigationHandler?: NavigationHandler;\n\n\n // session / user state\n // Consumer-owned (#91). No anonymous mint: absent until the host app supplies one,\n // so COUNT(DISTINCT user.id) is known-user reach and not a visitor count.\n private userId?: string = undefined;\n // SDK-owned, persisted, uninstall-scoped, never rotates — not on login, not on logout.\n private deviceIdEphemeral = false;\n private deviceIdPromise?: Promise<string>;\n private userProfile?: UserProfile = undefined;\n private sessionId: string;\n private sessionStart: number;\n private sdkVersion: string;\n private platform?: string;\n private eventCount = 0;\n // last-20 action trail, attached to app.crash as error.breadcrumbs (#28, §4.7)\n private breadcrumbs = new BreadcrumbBuffer(20);\n // session lifecycle (#29)\n private lastActivity?: number; // last non-session event time; drives 30-min idle rotation\n private sessionSequence = 0; // increments per acknowledged (2xx) batch\n private eventSequence = 0; // event.sequence — per-session ordinal, stamped at enqueue\n private sessionEventCount = 0; // events this session (journey summary)\n private errorCount = 0; // app.crash + app.error this session (sdk.error_count)\n\n // Sampling and scrubbing (#93, §3.6). `configuredSampleRate` is what the constructor\n // was given and what every rotation re-rolls at; `sampleRate` is what *this* session\n // was rolled at and what ships as session.sample_rate, so a consumer retuning\n // mid-quarter can't retroactively mis-scale a session already in flight.\n private readonly configuredSampleRate: number;\n private sampleRate: number;\n private sampled: boolean;\n private readonly beforeSend?: BeforeSend;\n /** `app.build_id` (§4.8). Empty string is *not* a build id — it is an unset one. */\n private readonly buildId?: string;\n private hookDropped = 0; // sdk.hook_dropped — the hook working\n private hookFailed = 0; // sdk.hook_failed — the hook broken\n\n // Drop accounting (§3.7). Process-lifetime and monotonic — deliberately not reset by a\n // rotation, so \"how much did this install lose\" is one subtraction and not a sum.\n // These are lossy about their own loss by construction: they only arrive if a *later*\n // event gets through, so the tab that closes and never returns reports nothing.\n // `event.sequence`'s gaps are what actually covers that.\n private eventsDropped = 0;\n private dropReason?: DropReason;\n // Highest `event.sequence` the crash path has already written to the offline store, so a\n // second crash persists only the rows the first one did not. -1 means \"nothing yet\".\n private crashPersistedThrough = -1;\n // `app.start` is once per *process* (§6.2), and web re-enters resumeOrStartSession() on a\n // bfcache restore — which resumes a process rather than starting one.\n private appStartEmitted = false;\n\n constructor(opts?: Opts) {\n this.sender = opts?.sender;\n // Android's numbers, deliberately (§9.4). These SDKs feed shared tables, so a\n // per-SDK cadence makes cross-platform arrival comparisons quietly wrong. POSTs get\n // ~25x larger and 3x rarer; the collector clears a 50-event batch ~20x over (§2.3).\n this.batchSize = opts?.batchSize ?? 50;\n this.flushIntervalMs = opts?.flushIntervalMs ?? 30000;\n this.endpoint = opts?.endpoint;\n this.platform = opts?.platform; // set before id generation (suffix source)\n this.store = opts?.store ?? memoryStore({ unavailable: true });\n this.beforeSend = opts?.beforeSend;\n // \"Omitted when unset, never `\\\"\\\"`\" (§4.8): an empty-string build id would reproduce\n // §9.3's `rum_apps` collapse inside symbolication, where absence is itself the signal.\n this.buildId = typeof opts?.buildId === \"string\" && opts.buildId !== \"\" ? opts.buildId : undefined;\n this.configuredSampleRate = normalizeSampleRate(opts?.sessionSampleRate);\n this.sampleRate = this.configuredSampleRate;\n // Rolled here so a bare `new Telemetry()` — no resumeOrStartSession() — is decided\n // too. hydrateSession() overwrites it when a durable record says otherwise.\n this.sampled = this.rollSample();\n\n // start a session\n this.sessionId = opts?.sessionId ?? this.generateSessionId();\n if (opts?.userId) this.setUserId(opts.userId);\n this.sessionStart = Date.now();\n this.sdkVersion = opts?.sdkVersion ?? SDK_VERSION;\n\n // auto replay if supported\n if (this.sender?.replayFailed) {\n // don't block constructor; best-effort\n this.sender.replayFailed().catch((err: any) => {\n debug.warn(\"Telemetry replay failed Telemetry class:\", err);\n });\n }\n\n if (this.flushIntervalMs > 0) {\n this.intervalId = setInterval(() => this.flush().catch(() => { }), this.flushIntervalMs);\n }\n\n this.navigationTracker = new NavigationTracker(this);\n this.screens = new ScreenTimingTracker(this);\n this.deprecatedScreenFeeds = opts?.deprecatedScreenFeeds ?? true;\n // Before the ViewManager, not after: the launch root is minted in this constructor so\n // the initial view can parent to it. A view that minted its own root instead would\n // make a web hard load report `trace.root_type = navigation` (§6.2).\n this.trace = new TraceManager(opts?.traceLaunchStart, opts?.traceHostAllowlist);\n // The initial view opens here, at SDK init — so no row can ever precede a view (§4.5).\n this.views = new ViewManager(this);\n\n this.deviceInfoHandler = opts?.deviceInfoHandler ?? {\n start: async () => Promise.resolve(),\n collect: async () => Promise.resolve({\n app: { name: '', version: '' },\n device: { platform: '' }\n })\n };\n this.networkInfoHandler = opts?.networkInfoHandler ?? {\n start: async () => Promise.resolve({}),\n collect: async () => Promise.resolve({})\n };\n\n }\n\n // ---------- Session & User APIs ----------\n\n /**\n * §4.7 — report a handled error. Accepts `unknown` on purpose: half of real `catch`\n * blocks receive a string or an axios rejection object, and a consumer should not have\n * to prove to TypeScript that it is an `Error` before reporting it.\n *\n * Emits `app.error`, **never** `app.crash`. There is deliberately no public path to\n * `app.crash` — a consumer's own code must not be able to manufacture rows in the one\n * table an unfiltered crash-free rate is read from.\n *\n * `source` is SDK-internal (`captureConsole` passes `\"console\"`); the public wrappers\n * forward two arguments, so a consumer can only ever produce `\"reported\"`. Routing the\n * console path through here rather than through `log()` directly is what keeps the\n * `error.fatal` rule in one place.\n */\n public captureError(error: unknown, context?: Record<string, any>, source: ErrorSource = \"reported\") {\n // `error.fatal` is native-only (§4.7): on web nothing is fatal, so the key is omitted\n // rather than shipped as a permanent `false` that scores web at a perfect crash-free\n // rate forever. Nothing *reported* is fatal, so on native it is always `false`. The\n // platform opt is the same one that suffixes the ids — the entry's declaration, not a\n // branch on a runtime global.\n const fatal = this.isNativePlatform() ? { fatal: false } : {};\n // Context first: the SDK's own `error.*` keys win a collision.\n return this.log(\"app.error\", { ...context, ...buildErrorAttributes(source, error, fatal) });\n }\n\n /**\n * Extend the breadcrumb trail without emitting anything. §4.7 demotes `console.warn` to\n * exactly this — React's dev-mode warnings were the bulk of v3's `app.crash` volume.\n *\n * SDK-internal: `public` only so `adapters/crashCapture.ts` can reach it. Not on\n * `TelemetryBase` and not part of the consumer API — a host app that wants a trail entry\n * should `log()` an event, which extends the trail anyway.\n */\n public addBreadcrumb(name: string, data?: Record<string, any>) {\n // `name` and `timestamp` last: a caller's `data` annotates the crumb, it does not\n // rename it out from under the trail.\n this.breadcrumbs.add({ ...data, name, timestamp: new Date().toISOString() });\n }\n\n public trackErrors(crashHandler: CrashHandler, options?: CrashHandlerOptions) {\n this.crashHandler = crashHandler;\n void crashHandler.attach(options).catch((err) => {\n debug.warn(\"Telemetry crashHandler attach failed:\", err);\n });\n }\n\n getDeviceInfo(deviceInfoHandler: DeviceInfoHandler) {\n this.deviceInfoHandler = deviceInfoHandler;\n void deviceInfoHandler.start(this).catch((err) => {\n debug.warn(\"Telemetry deviceInfo tracking start failed:\", err);\n });\n }\n\n getNetworkInfo(networkInfoHandler: NetworkInfoHandler) {\n this.networkInfoHandler = networkInfoHandler;\n void networkInfoHandler.start(this).catch((err) => {\n debug.warn(\"Telemetry networkInfoHandler start failed:\", err);\n });\n }\n\n public trackFrameDrops(frameDropsHandler: FrameDropsHandler) {\n this.frameDropsHandler = frameDropsHandler;\n void frameDropsHandler.start().catch((err) => {\n debug.warn(\"Telemetry frameDropsHandler start failed:\", err);\n });\n }\n\n public trackNetworkRequests(networkHandler: NetworkHandler) {\n this.networkHandler = networkHandler;\n void networkHandler.start().catch((err) => {\n debug.warn(\"Telemetry networkHandler start failed:\", err);\n });\n }\n\n public trackMemoryUsage(memoryHandler: MemoryHandler) {\n // Re-registration retires the incumbent's timer. `trackMemoryUsage()` is auto-started\n // in the native constructor *and* public, so a consumer calling it hands core a second\n // tracker — without this, two 30-second loops and double the sample volume.\n this.memoryHandler?.stop?.();\n this.memoryHandler = memoryHandler;\n void memoryHandler.start().catch((err) => {\n debug.warn(\"Telemetry memoryHandler start failed:\", err);\n });\n }\n\n public autoTrackNavigation(navigationHandler?: NavigationHandler) {\n this.navigationHandler = navigationHandler;\n void navigationHandler?.start().catch((err) => {\n debug.warn(\"Telemetry navigationHandler start failed:\", err);\n });\n }\n\n\n /**\n * The per-session sample roll (§3.6). Never per-event: per-event sampling punches\n * holes that desynchronise the per-view counters by a random factor per view.\n *\n * `Math.random()` is deliberate here where `randomHex` refuses it — this decides one\n * boolean that dies with the session, not an id that persists forever, and a biased\n * PRNG costs a fraction of a percent of sample accuracy rather than a permanent\n * identity collision.\n */\n private rollSample(): boolean {\n return Math.random() < this.sampleRate;\n }\n\n /** §3.3 / §4.7's platform split, from the entry's declared `platform` opt — never a global. */\n private isNativePlatform(): boolean {\n return this.platform === \"ios\" || this.platform === \"android\";\n }\n\n private generateSessionId(): string {\n const base = `session_${Date.now()}_${randomHex(16)}`;\n // §3.3 / §12's item 13 — suffixed on all three platforms in v4; the web build gained\n // `_web` here, so this is now the same rule `device.id` follows. Nothing parses it.\n return this.platform ? `${base}_${this.platform}` : base;\n }\n\n public setSessionId(id: string) {\n this.sessionId = id;\n this.sessionStart = Date.now();\n }\n\n public getSessionId(): string {\n return this.sessionId;\n }\n\n /**\n * Freeze the identity a deferred row must report (§4.6). Read **synchronously at the\n * moment the thing happened**, so a `log()` that lands after a route change or a session\n * rotation still books the click where it occurred.\n */\n public snapshot(at?: number): LogSnapshot {\n return {\n at,\n viewId: this.views.id,\n sessionId: this.sessionId,\n sessionStart: this.sessionStart,\n };\n }\n\n /** Collector endpoint, so fetch/XHR adapters can skip self-capturing the SDK's own POST. */\n public getEndpoint(): string | undefined {\n return this.endpoint;\n }\n\n /**\n * Sampling stays **session-level** (§6.5): an unsampled session injects no header at\n * all — not a `flags=00` id. Read by the interceptors, which sit outside `log()`'s own\n * sampled-out early return and so need the decision directly.\n */\n isSampled(): boolean {\n return this.sampled;\n }\n\n // ---------- Session lifecycle (#29) ----------\n\n /**\n * The entry's one call at init (§4.2): resume the durable session when the gap is\n * inside the idle window, otherwise close the stale one out and start fresh.\n *\n * Process death, tab close, hard reload and bfcache restore all land here, and all\n * four **resume** — which is why a resumed session must not re-emit `session.started`\n * (§4.1), or `COUNT(session.started)` stops equalling session count.\n */\n public async resumeOrStartSession() {\n await this.hydrateAndAnnounce();\n // Once per process and on every path — a resume emits no `session.started` at all\n // (§4.1), so `session.reason: \"launch\"` undercounts launches and `app.start` is the\n // compensator (§4.3). It is also the launch root's own row (§6.2).\n await this.emitAppStart();\n }\n\n private async hydrateAndAnnounce() {\n const decision = await this.hydrateSession();\n if (decision.kind === \"resumed\") return;\n // Expired: hydration adopted the old id/start so the finalize ships under them.\n if (decision.kind === \"expired\") { await this.rotateSession(decision.reason); return; }\n\n // Storage told us nothing. At init that means launch. On a bfcache restore this method\n // runs a second time with a session already going — storage having nothing to say is no\n // reason to discard it and re-announce a session that never ended. Just re-check the\n // boundaries, which is the whole point of re-running after a long freeze.\n if (this.lastActivity === undefined) { await this.startSession(\"launch\"); return; }\n const reason = this.expiryReason(Date.now());\n if (reason) await this.rotateSession(reason);\n }\n\n /**\n * `app.start` (§4.3/§6.2): once per process, a **root** of `trace.root_type = launch`,\n * carrying the launch root minted in the constructor. Idempotent because the web build\n * re-runs `resumeOrStartSession()` on a bfcache restore, which is a resumed process and\n * not a new one.\n */\n private async emitAppStart() {\n if (this.appStartEmitted) return;\n this.appStartEmitted = true;\n await this.log(\"app.start\", this.trace.launchRootAttributes());\n }\n\n /**\n * Read the durable record and adopt it. Adoption happens even when the session has\n * expired: `session.finalized` has to ship under the *old* `session.id` and\n * `session.start_time` before the fresh one takes over.\n */\n private async hydrateSession(): Promise<SessionHydration> {\n const read = await this.store.get(SESSION_KEY);\n // `unavailable` is not `miss` — incognito, a partitioned iframe, ITP eviction or a full\n // disk means this process can never resume and can never be resumed FROM, so every\n // launch mints a session and COUNT(session.started) over-counts. There is no\n // `session.id_ephemeral` on the contract to say so, and inventing one needs backend\n // sign-off — but the same store failed the device.id round-trip, so every event from\n // this population already carries `device.id_ephemeral: true`. Filter on that.\n if (read.status === \"unavailable\") {\n debug.warn(\"Telemetry: session storage unavailable — this session cannot be resumed\");\n return { kind: \"nothing\" };\n }\n if (read.status === \"miss\") return { kind: \"nothing\" };\n\n let saved: Partial<PersistedSession>;\n try { saved = JSON.parse(read.value); } catch { return { kind: \"nothing\" }; }\n // A record we can't trust is not a session to resume; mint rather than guess.\n if (typeof saved?.id !== \"string\"\n || typeof saved.start !== \"number\"\n || typeof saved.lastActivity !== \"number\") return { kind: \"nothing\" };\n\n this.sessionId = saved.id;\n this.sessionStart = saved.start;\n this.lastActivity = saved.lastActivity;\n this.sessionSequence = saved.sequence ?? 0;\n this.eventSequence = saved.eventSequence ?? 0;\n this.sessionEventCount = saved.eventCount ?? 0;\n this.errorCount = saved.errorCount ?? 0;\n // A resume is not a rotation: the decision and the rate it was rolled at are\n // adopted as-is, so a relaunch can't half-sample a session. A record written\n // before this field existed reads as `undefined` and keeps the fresh roll.\n // The rate is stamped on every row of the resumed session, and on web the record is\n // browser-wide localStorage — so an out-of-range value would ship as a divisor and\n // silently mis-scale every count taken off it. A record we can't trust here loses\n // its decision too, and the fresh constructor roll stands.\n if (typeof saved.sampled === \"boolean\"\n && Number.isFinite(saved.sampleRate) && saved.sampleRate! >= 0 && saved.sampleRate! <= 1) {\n this.sampled = saved.sampled;\n this.sampleRate = saved.sampleRate!;\n }\n\n const reason = this.expiryReason(Date.now());\n return reason ? { kind: \"expired\", reason } : { kind: \"resumed\" };\n }\n\n /**\n * Which boundary, if any, the current session has crossed. Idle is checked first:\n * when a long session has also gone quiet, the reason it ended is that the user left.\n *\n * Inert until `lastActivity` is set, so a bare `new Telemetry()` that never started a\n * session never rotates one.\n */\n private expiryReason(now: number): SessionEndReason | undefined {\n if (this.lastActivity === undefined) return undefined;\n // Both boundaries are strict: a session sitting exactly on one has not crossed it.\n if (now - this.lastActivity > SESSION_IDLE_MS) return \"idle\";\n if (now - this.sessionStart > SESSION_MAX_MS) return \"max_duration\";\n return undefined;\n }\n\n /**\n * Write the session through the `Store` so the next process can resume it.\n *\n * ponytail: one write per event. Web's is a synchronous `localStorage` write; native's\n * is an AsyncStorage round-trip — debounce here if it ever shows up in a trace.\n */\n private async persistSession() {\n const record: PersistedSession = {\n id: this.sessionId,\n start: this.sessionStart,\n lastActivity: this.lastActivity ?? this.sessionStart,\n sequence: this.sessionSequence,\n eventSequence: this.eventSequence,\n eventCount: this.sessionEventCount,\n errorCount: this.errorCount,\n sampled: this.sampled,\n sampleRate: this.sampleRate,\n };\n const write = await this.store.set(SESSION_KEY, JSON.stringify(record));\n // Same first-class `unavailable` path as the read: nothing to retry and nothing to\n // throw, but it must not read as a successful write. See hydrateSession().\n if (write.status === \"unavailable\") debug.log(\"Telemetry: session not persisted (store unavailable)\");\n }\n\n /** Emit session.started for the current session. */\n public async startSession(reason: SessionReason) {\n this.lastActivity = Date.now();\n await this.persistSession(); // durable before it is announced\n await this.log(\"session.started\", {\n \"session.reason\": reason,\n // §4.1, count only — never the hosts (#99). Ships on every session so a zero is\n // legible as \"nobody opted in\" rather than as a stripped header.\n \"sdk.trace_allowlist_size\": this.trace.allowlistSize(),\n });\n }\n\n /**\n * Finalize the current session: journey summary + sdk.error_count, then an immediate flush.\n *\n * The backend discards `duration_ms` and `event_count` and derives both (§4.2) — they\n * stay on the wire because the event survives as the carrier for `session.reason` and\n * `sdk.error_count`, neither of which is derivable. Duration is stamped from\n * `lastActivity`, never `now`: a lazily-detected rotation must report 2 minutes of use,\n * not the 6 idle hours that followed it.\n */\n public async finalizeSession(reason: SessionEndReason) {\n await this.log(\"session.finalized\", {\n \"session.duration_ms\": Math.max(0, (this.lastActivity ?? this.sessionStart) - this.sessionStart),\n \"session.event_count\": this.sessionEventCount,\n \"sdk.error_count\": this.errorCount,\n \"session.reason\": reason,\n });\n await this.flush();\n }\n\n /** Begin a fresh session: new id/start, reset per-session counters, emit session.started. */\n public async newSession(reason: SessionReason) {\n this.sessionId = this.generateSessionId();\n this.sessionStart = Date.now();\n this.sessionSequence = 0;\n this.eventSequence = 0; // the ordinal is per session, and this is a new one\n this.crashPersistedThrough = -1; // the watermark is an event.sequence, so it resets with it\n this.sessionEventCount = 0;\n this.errorCount = 0;\n // Re-rolled at the *configured* rate, not the retired session's — the previous\n // session may have been resumed from a record written under an older config.\n this.sampleRate = this.configuredSampleRate;\n this.sampled = this.rollSample();\n // `trace.id` never spans a `session.id` (§6.6, invariant 2) — dropped before the\n // successor view mints, so that view starts a fresh `navigation` root rather than\n // extending an action from the retired session.\n //\n // Before `app.start` has shipped, dropping is not enough: it reports the launch root\n // at any age, so its root row would land in the new session while the initial `view`\n // it fathered landed in the old one. That is the expired-record cold launch — §4.3's\n // most common path — so the launch root is re-minted instead of merely cleared.\n if (this.appStartEmitted) this.trace.clear();\n else this.trace.restartLaunchRoot();\n // After the new session.id is in place and before the first row of it is emitted:\n // `view.id` never spans a `session.id` (§4.5). rotateSession() has already emitted\n // the departing view's `view` event under the *old* id.\n await this.views.beginView(\"session_rotation\");\n await this.startSession(reason);\n }\n\n /** Boundary rotation: finalize the old session then start a fresh one (the pair). */\n public async rotateSession(reason: SessionEndReason) {\n // The session-rotation view boundary (§4.5), emitted first so the `view` row lands\n // under the session it belongs to. newSession() mints the successor.\n await this.views.endView();\n await this.finalizeSession(reason);\n await this.newSession(reason);\n }\n\n /** Truncated at source (§3.2): the collector caps at 255 and we must not be the one over. */\n public setUserId(id: string) {\n this.userId = id ? id.slice(0, USER_ID_MAX) : undefined;\n }\n\n public getUserId(): string | undefined {\n return this.userId;\n }\n\n /**\n * The SDK-owned `device.id` (§3.2/§3.3): self-minted, written through the `Store`,\n * uninstall-scoped, stable across process restarts, and never rotated by login,\n * logout or a profile clear.\n *\n * The platform suffix is the entry's (ios|android|web), not the device-info adapter's:\n * this id is persisted forever, so it must not depend on a call that can throw on first run.\n *\n * `getUniqueId()` is deliberately not used — it already carries two lifetimes on RN\n * alone (ANDROID_ID survives reinstall, identifierForVendor does not), so one identity\n * column would mean two things.\n *\n * When the Store reports `unavailable` — incognito, a partitioned iframe, ITP\n * eviction, a full disk — the id lives for one process only and `device.id_ephemeral`\n * rides the Context block to say so, because that population is otherwise\n * indistinguishable from real installs and reads as traffic growth.\n */\n private async getDeviceId(): Promise<string> {\n this.deviceIdPromise ??= (async () => {\n const read = await this.store.get(DEVICE_ID_KEY);\n if (read.status === \"hit\") return read.value;\n const suffix = this.platform ? `_${this.platform}` : \"\";\n const id = `device_${Date.now()}_${randomHex(16)}${suffix}`;\n const write = await this.store.set(DEVICE_ID_KEY, id);\n // Either end of the round-trip failing means this id never comes back.\n this.deviceIdEphemeral = read.status === \"unavailable\" || write.status === \"unavailable\";\n return id;\n })();\n return this.deviceIdPromise;\n }\n\n // ---------- User Profile Management ----------\n\n /**\n * Set complete user profile information\n */\n public setUserProfile(profile: Partial<UserProfile>): void {\n const now = Date.now();\n \n // If this is a new profile or userId changed, set createdAt\n const isNewProfile = !this.userProfile || (profile.userId && profile.userId !== this.userProfile.userId);\n \n this.userProfile = {\n ...this.userProfile,\n ...profile,\n updatedAt: now,\n ...(isNewProfile && { createdAt: now })\n };\n\n // Update userId if provided in profile (truncated at source, §3.2)\n if (profile.userId) {\n this.setUserId(profile.userId);\n this.userProfile.userId = this.userId;\n } else if (this.userProfile && !this.userProfile.userId) {\n // Mirror the current id into the profile — undefined while still anonymous\n this.userProfile.userId = this.userId;\n }\n\n debug.log(\"Telemetry: User profile updated\", this.userProfile);\n }\n\n /**\n * EdgeRum-style identify(): record the host-app profile and emit the one event that\n * carries it — `user.profile.update` (§4.10, #107). The PII is no longer on the\n * Context block of every event.\n *\n * `userId` is optional and new in v4: the one call that sends a profile should also\n * set the key that profile attaches to, since `user.id` is consumer-supplied (§3.2)\n * and an unkeyed profile has nothing to upsert on. Omitting it leaves `user.id`\n * exactly as it was — identify never mints one.\n */\n public async identify(profile: {\n userId?: string;\n name?: string;\n email?: string;\n phone?: string;\n avatar?: string;\n customAttributes?: Record<string, any>;\n }) {\n if (profile.userId !== undefined) this.setUserId(profile.userId);\n // Only what this call actually supplied — `setUserProfile` merges by spread, so\n // passing `email: undefined` would wipe an email a prior setUserContact() set.\n this.setUserProfile({\n ...(profile.name !== undefined ? { fullName: profile.name } : {}),\n ...(profile.email !== undefined ? { email: profile.email } : {}),\n ...(profile.phone !== undefined ? { phone: profile.phone } : {}),\n ...(profile.avatar !== undefined ? { avatar: profile.avatar } : {}),\n ...(profile.customAttributes !== undefined ? { customAttributes: profile.customAttributes } : {}),\n });\n // Built from the live profile, not from this call's arguments, so a preceding\n // setUserContact()/setUserName() ships too.\n await this.log(\"user.profile.update\", buildProfileAttributes(this.userProfile));\n }\n\n /**\n * Set user details with individual parameters\n */\n public setUserDetails(details: {\n fullName?: string;\n firstName?: string;\n lastName?: string;\n email?: string;\n phone?: string;\n avatar?: string;\n customAttributes?: Record<string, any>;\n }): void {\n this.setUserProfile(details);\n }\n\n /**\n * Update specific user profile fields\n */\n public updateUserProfile(updates: Partial<UserProfile>): void {\n if (!this.userProfile) {\n // If no profile exists, create one\n this.setUserProfile(updates);\n return;\n }\n\n this.userProfile = {\n ...this.userProfile,\n ...updates,\n updatedAt: Date.now()\n };\n\n debug.log(\"Telemetry: User profile updated\", this.userProfile);\n }\n\n /**\n * Get current user profile\n */\n public getUserProfile(): UserProfile | undefined {\n return this.userProfile;\n }\n\n /**\n * Clear user profile data\n */\n public clearUserProfile(): void {\n this.userProfile = undefined;\n this.userId = undefined; // consumer-owned (§3.2): cleared here, unlike device.id\n debug.log(\"Telemetry: User profile cleared\");\n }\n\n /**\n * Set user name (convenience method)\n */\n public setUserName(fullName: string, firstName?: string, lastName?: string): void {\n this.setUserProfile({\n fullName,\n firstName,\n lastName\n });\n }\n\n /**\n * Set user contact info (convenience method)\n */\n public setUserContact(email?: string, phone?: string): void {\n this.setUserProfile({\n email,\n phone\n });\n }\n\n // ---------- Logging APIs ----------\n\n /**\n * Log a named event. Keeps existing signature compatibility.\n * Automatically attaches userId and sessionId to every queued event.\n */\n async log(name: string, data?: Record<string, any>, snapshot?: LogSnapshot) {\n this.eventCount++;\n let activity = false;\n let crashed = false;\n\n // Session activity and the two boundaries — 30-min idle and the 4-hour cap (§4.2).\n // Session events don't count as activity (they're emitted *by* the lifecycle), so\n // they never re-trigger a rotation.\n // `view` joins the session events here: it is emitted *by* a boundary, from inside\n // rotateSession() among others, so letting it re-enter the expiry check would recurse\n // forever on an already-expired session. It is bookkeeping, not user activity.\n if (!name.startsWith('session.') && name !== 'view') {\n const now = Date.now();\n const reason = this.expiryReason(now);\n if (reason) await this.rotateSession(reason);\n this.lastActivity = now;\n this.sessionEventCount++;\n if (isErrorName(name)) this.errorCount++;\n activity = true;\n }\n\n // A sampled-out session sends nothing at all — not a skeleton record, and\n // crashes are no exception (§3.6): 100% of crashes over 10% of sessions makes\n // the unfiltered crash-free query read 10x too high with no WHERE to repair it.\n // The boundary bookkeeping above still runs, so the rotation that re-rolls the\n // decision still happens on schedule.\n if (this.sampled) {\n // v3 allowlist: unknown names ship as custom_event, original kept as event.name\n const isAllowed = ALLOWED_NAMES.has(name);\n const eventName = isAllowed ? name : 'custom_event';\n\n const attributes = await this.collectContext(data);\n if (!isAllowed) attributes['event.name'] = name;\n\n // §6.3's Tier 2. Tier 1's keys were captured earlier — at launch, at view entry,\n // at request send — and arrived through `data`; Tier 3 gets nothing at all.\n // Assigned after `collectContext`, so a caller's `data` cannot forge them.\n if (TIER_2_NAMES.has(eventName)) Object.assign(attributes, this.trace.annotate());\n\n // `app.crash` carries the trail of prior actions; other events extend the trail.\n // ⚠ Breadcrumbs ride `app.crash` **only** (§4.7): `app.error` volume is\n // consumer-controlled, and a 1-2 KB blob on a high-volume event is how the\n // transport budget gets spent by the SDK's own doing. Stringified, because\n // `stringAttr` renders a real array through `fmt.Sprint` as Go map syntax.\n if (eventName === 'app.crash') {\n attributes['error.breadcrumbs'] = this.breadcrumbs.toJSON();\n crashed = true;\n } else {\n this.breadcrumbs.add({ name: eventName, timestamp: new Date().toISOString() });\n }\n\n // Two of §4.5's three counters, booked against the view this row is pinned to.\n // `view.error_count` is a closed enumeration — crashes only, not failed requests\n // and not console.warn — but `app.error` counts too (§4.5). `view.request_count`\n // is NOT booked here: §4.5.2 counts\n // requests *started* in the view, and this row is emitted at completion, which\n // can be a route change later. The interceptors book it at send time instead.\n if (isErrorName(eventName)) this.views.countError();\n else if (eventName === 'ui.interaction') this.views.countAction();\n\n // §3.1's attribution freeze, and §4.6's mint/emit split — one mechanism. A\n // span-carrying row reports the identity live at **span start**, not at emit:\n // §4.2's 4-hour cap can rotate a session while a request is in flight, and a tap\n // that navigates would otherwise be booked against the view it opened, silently\n // inverting every \"which screen frustrates users\" query. Applied after\n // `collectContext`, with the identity keys, so it is the SDK's snapshot and never\n // a caller's `data`. Point events pass none and are unaffected.\n if (snapshot) {\n attributes['view.id'] = snapshot.viewId;\n // Resolved at log time by lookup on the *frozen* id (§3.1), never replayed\n // from the snapshot: a rung upgrade between mint and emit renames the view\n // in place, and a row must never carry a name that disagrees with its own id.\n attributes['view.name'] = this.views.nameOf(snapshot.viewId);\n attributes['session.id'] = snapshot.sessionId;\n attributes['session.start_time'] = new Date(snapshot.sessionStart).toISOString();\n }\n\n this.enqueue({\n type: 'event',\n eventName,\n timestamp: new Date(snapshot?.at ?? Date.now()).toISOString(),\n attributes,\n });\n\n debug.log(\"Telemetry queued event:\", name, \"Queue size:\", this.queue.length);\n debug.log(\"Event attributes:\", attributes);\n }\n\n // After the enqueue, never before it: the durable record is for the *next* process,\n // and on native this is an AsyncStorage round-trip that must not sit in front of the\n // event it describes.\n if (activity) await this.persistSession();\n\n // A crash is the row the process may not survive to send twice, so it does not wait\n // for the batch to fill. Awaited, unlike the batch-full flush: `app.crash` is emitted\n // from a dying process's teardown, and a fire-and-forget send there is a send that\n // never happens.\n if (crashed) {\n await this.flushCrash();\n } else if (this.queue.length >= this.batchSize) {\n void this.flush();\n }\n }\n\n /**\n * The crash path (§2 / §9.4): persist the whole queue, then send **one** batch reordered\n * so the crash rides in it. Not a drain — a dying process gets one round trip.\n *\n * ⚠ The web/native asymmetry is real and is not fixed here. On web the `Store` is\n * synchronous `localStorage`, so the persist has *landed* by the time the next line runs\n * and the loss window closes. On native it is an AsyncStorage round-trip that a SIGKILL\n * can outrun, so the window only narrows. Awaiting harder does not change that; the\n * asymmetry is the `Store` port's whole point (`core/store.ts`).\n *\n * The persisted copy is a safety copy, not a handoff: the queue is left intact, because\n * an `app.crash` is not always fatal — `window.onerror` fires and the page keeps running\n * — and the process usually lives on. A successful send therefore\n * leaves a duplicate on disk to replay next launch — which is what `event.sequence` and\n * the backend's `(session_id, event_sequence)` dedup exist for (§2.4).\n *\n * *One* duplicate. Each row is written at most once by this path, watermarked on\n * `event.sequence`: a chatty app crash-flushes often, and re-persisting the whole queue\n * each time would fill the store with copies of\n * its own backlog and book `store_full` drops that are not loss — corrupting the very\n * counter this change adds.\n */\n private async flushCrash() {\n if (!this.sender || this.queue.length === 0) return;\n\n // Move crashes to the front, relative order intact: at batchSize 50 a crash enqueued\n // behind 49 older events would otherwise miss the only batch this process gets.\n const isCrash = (e: TelemetryEvent) => e.eventName === 'app.crash';\n this.queue = [...this.queue.filter(isCrash), ...this.queue.filter(e => !isCrash(e))];\n\n const pending = this.queue.filter(e => seqOf(e) > this.crashPersistedThrough);\n if (pending.length > 0 && this.sender.onFailure) {\n try {\n this.recordDrop(\"store_full\", await this.sender.onFailure(pending));\n // Advanced only on a write that returned: a throw leaves the watermark where\n // it was, so the next crash retries these rows rather than abandoning them.\n this.crashPersistedThrough = Math.max(this.crashPersistedThrough, ...pending.map(seqOf));\n } catch (err) {\n debug.warn(\"Telemetry: crash-path persist failed:\", err);\n }\n }\n\n // Spliced *before* the send, not after: both the interval and the batch-full trigger\n // fire flush() unawaited, so a concurrent flush() splices this same front — and a\n // post-send splice would then delete rows this batch never carried, silently and\n // without booking them. Sent directly rather than through flush() because the rows\n // are already on disk and flush()'s failure path would persist them a second time.\n const batch = this.queue.splice(0, this.batchSize);\n try {\n await this.sender.send(batch);\n await this.ackBatch();\n } catch (err) {\n // Requeue only what the store does not already hold — the rest is safe on disk\n // and requeueing it would send it twice for no gain.\n const unsaved = batch.filter(e => seqOf(e) > this.crashPersistedThrough);\n if (unsaved.length > 0) this.queue.unshift(...unsaved);\n debug.warn(\"Telemetry: crash-path send failed:\", err);\n }\n }\n\n /**\n * Build the v3 Context block that rides on every event AND metric: the flattened\n * device/network snapshot, the caller's data, identity + session + sdk fields, and the\n * user profile. Shared by log() and logMetric() so both carry the identical iOS-clean set.\n */\n private async collectContext(data?: Record<string, any>): Promise<Record<string, any>> {\n let deviceInfo: Record<string, any> = {};\n let networkInfo: Record<string, any> = {};\n\n try {\n deviceInfo = (await this.deviceInfoHandler?.collect()) || {};\n } catch (err) {\n debug.warn(\"Telemetry: failed to fetch device info\", err);\n }\n\n try {\n networkInfo = (await this.networkInfoHandler?.collect()) || {};\n } catch (err) {\n debug.warn(\"Telemetry: failed to fetch network info\", err);\n }\n\n // Mint/read before assembling. device.id is persisted forever, so its suffix comes\n // from the entry-supplied platform and never from collect(), which can throw.\n const deviceId = await this.getDeviceId();\n\n const attributes: Record<string, any> = {\n // deviceInfo already namespaces its own keys (app.*, device.*) — flatten flat\n ...this.flattenWithPrefix('', deviceInfo),\n ...this.flattenWithPrefix('network', networkInfo),\n ...this.flattenWithPrefix('', data || {}),\n // Identity keys land after caller data: `data` may override app./device./network.*\n // but must never override these (§3.3).\n // With the identity keys, not with deviceInfo's `app.*`: it is the symbolication\n // join key, so a stray `log()` payload must not be able to shift which map a\n // crash resolves against.\n ...(this.buildId ? { 'app.build_id': this.buildId } : {}),\n 'device.id': deviceId,\n ...(this.deviceIdEphemeral ? { 'device.id_ephemeral': true } : {}),\n // Omitted entirely on anonymous traffic (§3.2) — no \"\", no placeholder.\n ...(this.userId ? { 'user.id': this.userId } : {}),\n // Denormalized onto every row (§4.5) so \"errors by screen\" needs no join. The id\n // is the join key; the name is the view's current best, resolved here at log time\n // by lookup on that id, so a row can never carry a name that disagrees with it.\n 'view.id': this.views.id,\n 'view.name': this.views.name,\n 'session.id': this.sessionId,\n 'session.start_time': new Date(this.sessionStart).toISOString(),\n 'session.sequence': this.sessionSequence,\n // Extrapolation is arithmetic when the rate is on the row: it survives a\n // consumer retuning mid-quarter, which config-in-a-spreadsheet does not.\n 'session.sample_rate': this.sampleRate,\n // Monotonic and always present; the reason stays omitted until there is one,\n // so `sdk.drop_reason IS NOT NULL` is a usable filter for \"this install lost data\".\n 'sdk.events_dropped': this.eventsDropped,\n ...(this.dropReason ? { 'sdk.drop_reason': this.dropReason } : {}),\n 'sdk.hook_dropped': this.hookDropped,\n 'sdk.hook_failed': this.hookFailed,\n 'sdk.platform': sdkPlatform(this.platform),\n 'sdk.version': this.sdkVersion,\n };\n\n // §4.10, #107 — the profile keys are deliberately NOT here; they ride\n // `user.profile.update` alone. `user.id` above is the join key and stays.\n\n return attributes;\n }\n\n /**\n * Emit a metric on the v3 `type:\"metric\"` path: { type, metricName, value, timestamp, attributes }.\n * Carries the same Context block as events. Metrics are samples, not user actions, so they\n * don't extend the breadcrumb trail and don't count as session activity — a periodic sampler\n * (memory/frames) must not keep a session alive and defeat the 30-min idle rotation.\n */\n async logMetric(metricName: string, value: number, data?: Record<string, any>) {\n this.eventCount++;\n // A metric is a sample, not a user action: it must not refresh `lastActivity` and\n // keep a dead session alive. It is still *checked* against the boundaries, or a\n // metric-only stream (a backgrounded app sampling memory) would ship forever under\n // a session that expired hours ago and never hit the 4-hour cap (§4.2).\n const reason = this.expiryReason(Date.now());\n if (reason) await this.rotateSession(reason);\n\n if (!this.sampled) return;\n\n const attributes = await this.collectContext(data);\n // §5.1 — assembled after caller `data`, so a stray attribute cannot mislabel the\n // unit of the column it lands in. Omitted for a name we have no unit for.\n const unit = METRIC_UNIT[metricName];\n if (unit) attributes['metric.unit'] = unit;\n\n this.enqueue({\n type: 'metric',\n metricName,\n value,\n timestamp: new Date().toISOString(),\n attributes,\n });\n if (this.queue.length >= this.batchSize) {\n void this.flush();\n }\n }\n\n /**\n * The single enqueue point: neither the sample decision nor `beforeSend` can be\n * bypassed by a future emit path, and the hook runs *before* the queue — which is\n * what keeps scrubbed fields off disk when a send fails and the batch is persisted.\n *\n * The sample check is repeated by log()/logMetric() ahead of `collectContext()`, so\n * a sampled-out session does no work per event; this one is the backstop that makes\n * the guarantee structural rather than a convention two call sites happen to follow.\n *\n * The two counters are separate on purpose: \"my volume is down 40%\" has to\n * distinguish *my rule is too broad* from *my rule is crashing*, and one merged\n * counter answers neither.\n */\n private enqueue(e: TelemetryEvent) {\n if (!this.sampled) return;\n\n let kept = e;\n if (this.beforeSend) {\n const outcome = applyBeforeSend(e, this.beforeSend);\n if (outcome.kind === \"failed\") { this.hookFailed++; return; }\n if (outcome.kind === \"dropped\") { this.hookDropped++; return; }\n kept = outcome.event;\n }\n\n // After the hook, never before (§2.4): an event the hook drops must not consume an\n // ordinal, or every scrubbed row would read as a gap — and gaps are precisely how\n // the backend tells real loss from a replay. Tier A already protects the key, so a\n // hook cannot forge or delete one once stamped.\n (kept.attributes ??= {})['event.sequence'] = this.eventSequence++;\n\n this.queue.push(kept);\n this.capQueue();\n }\n\n /**\n * Hold the in-memory queue at its cap (§9.4), booking what it costs.\n *\n * The count lands on the *next* event's Context block, not this one's — `collectContext()`\n * has already run by the time we get here. That is §3.7's stated behaviour, not a bug.\n */\n private capQueue() {\n while (this.queue.length > QUEUE_MAX_EVENTS) {\n this.queue.splice(evictIndex(this.queue), 1);\n this.recordDrop(\"queue_full\");\n }\n }\n\n /**\n * Book dropped rows against the monotonic counter and the reason that shipped last.\n * Takes `Sender.onFailure`'s return shape as-is — a sender with no cap returns nothing.\n */\n private recordDrop(reason: DropReason, count: number | void = 1) {\n if (!count || count <= 0) return;\n this.eventsDropped += count;\n this.dropReason = reason;\n }\n\n private flattenWithPrefix(prefix: string, obj: Record<string, any>, depth = 0): Record<string, any> {\n const result: Record<string, any> = {};\n\n for (const key in obj) {\n if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;\n\n const value = obj[key];\n const prefixedKey = prefix ? `${prefix}.${key}` : key;\n\n // §3.3's null discipline — \"absent means the SDK had nothing\" (#108). An\n // `undefined` value was already dropped by the sender's `JSON.stringify`, so\n // this is wire-neutral; what it fixes is the in-memory bag, where\n // `device.android_sdk: undefined` on a web row made the 39-key shape\n // unassertable and handed `beforeSend` keys that never ship.\n if (value === undefined) continue;\n\n // §4.10, #107 — the depth guard. Without it a cyclic value recursed until the\n // stack blew, *inside* the SDK, taking the host app's render tree with it. A\n // bad payload is a dropped key, never a RangeError.\n if (typeof value === 'object' && value !== null && !Array.isArray(value) && depth < FLATTEN_MAX_DEPTH) {\n Object.assign(result, this.flattenWithPrefix(prefixedKey, value, depth + 1));\n } else if (typeof value === 'object' && value !== null) {\n // An array, or an object at the depth cap. Both ride as a JSON string rather\n // than as themselves — the primitive-values rule, and the only way to close\n // the hole a cycle *inside an array* leaves: the array branch never recurses,\n // so the depth guard cannot see it and the throw would land in the sender's\n // `JSON.stringify`, where `flush()` swallows it and the whole batch is lost\n // silently. Stringifying is also what the collector's `fmt.Sprint` would do\n // to a raw array, only in Go map syntax rather than JSON.\n const flat = stringifyOrDrop(value);\n if (flat !== undefined) result[prefixedKey] = flat;\n } else {\n result[prefixedKey] = value;\n }\n }\n\n return result;\n }\n\n\n\n /**\n * Explicit metric helper (optional convenience) — routes through the v3 metric path.\n */\n recordMetric(name: string, value: number, data?: Record<string, any>) {\n return this.logMetric(name, value, data);\n }\n\n // ---------- Flush / Persistence ----------\n\n // Retry + backoff live in the Sender (see sendWithRetry in web/native senders),\n // which is also used by the standalone replay paths. flush() does a single send\n // and, on failure, hands the batch to the sender to persist (or requeues it).\n async flush() {\n if (!this.sender || this.queue.length === 0) return;\n\n // build a batch of up to batchSize\n const toSend = this.queue.splice(0, this.batchSize);\n\n try {\n await this.sender.send(toSend);\n await this.ackBatch();\n } catch (lastError) {\n if (this.sender.onFailure) {\n try {\n this.recordDrop(\"store_full\", await this.sender.onFailure(toSend));\n } catch (persistErr) {\n // If persistence fails, requeue to avoid data loss\n this.queue.unshift(...toSend);\n debug.warn(\"Sender.onFailure failed, requeued events:\", persistErr);\n }\n } else {\n this.queue.unshift(...toSend);\n }\n\n debug.error(\"Telemetry flush failed:\", lastError);\n throw lastError;\n }\n }\n\n /**\n * Book an acknowledged (2xx) batch. `session.sequence` orders a session's batches (#29),\n * and it is persisted because a resumed session restarting at 0 would emit duplicate\n * (session.id, session.sequence) pairs and stop ordering anything (#92).\n */\n private async ackBatch() {\n this.sessionSequence++;\n await this.persistSession();\n }\n\n getQueue() {\n return [...this.queue];\n }\n\n async shutdown() {\n if (this.intervalId) clearInterval(this.intervalId);\n this.memoryHandler?.stop?.();\n await this.flush();\n }\n\n // Screen tracking lives in ScreenTimingTracker (this.screens), the single timed API.\n\n // Expose some internal counters (optional)\n getEventCount() {\n return this.eventCount;\n }\n\n /**\n * A route change: the view boundary (§4.5) plus, on native only, the two deprecated\n * feeds (§4.11). v3's two native screen paths were disjoint — `attachNavigation` never\n * touched `inst.screens`, so a React Navigation consumer emitted `navigation` on every\n * route change and never a single `screen.duration`. Unifying them here is what fixes\n * that, which is why a *deprecated* event starts firing where it never has.\n */\n async recordRouteChange(from: string, to: string) {\n this.currentScreen = to; // best-effort screen for the deprecated feeds (#33)\n if (this.deprecatedScreenFeeds) {\n // Both rows describe the transition, so both are emitted — and awaited — before\n // the view boundary: they belong to the view being left.\n await this.navigationTracker?.recordRouteChange(from, to);\n await this.screens.endScreen(from);\n // Arm the dwell clock for the arriving screen. `markStart`, not `startScreen`:\n // the latter emits its own `navigation`, and the line above already emitted this\n // transition's. This is the join that was missing — v3's `attachNavigation` never\n // touched `screens`, so `screen.duration` never fired for a React Navigation app.\n this.screens.markStart(to);\n }\n await this.enterView(to, \"route\");\n }\n\n /**\n * Feed the name ladder (§4.5.1). Rungs 1 and 2 arrive unnormalized; rung 3's caller\n * normalizes before calling. Whether this re-stamps the current view or mints a\n * successor is the ladder's decision, not the caller's.\n */\n async enterView(name: string, source: ViewNameSource) {\n await this.views.navigate(name, source);\n }\n\n\n}\n\n"],"names":["ScreenTimingTracker","telemetry","screen","from","start","duration","randomHex","length","bytes","out","b","RANK","UNKNOWN_VIEW_NAME","MAX_RETIRED_VIEW_NAMES","mintViewId","ViewManager","now","NetworkSettle","id","name","source","rank","current","fn","at","v","loadingTime","outcome","successorLoadType","prev","oldest","ROOT_IDLE_MS","ROOT_CAP_MS","mintTraceId","mintSpanId","TraceManager","launchStartMs","traceHostAllowlist","normalizeAllowlist","spanKeys","entryAt","root","isRoot","mintedAt","startedAt","ctx","decision","foreign","parseTraceparent","keys","attribution","span","finish","endedAt","formatTraceparent","allowsHost","live","mintAs","hadCarrier","rootType","spanStartMs","idle","age","spanId","BreadcrumbBuffer","max","item","TIER_A_KEYS","isTierA","key","TIER_B_KEY","applyBeforeSend","original","hook","returned","err","debug","restamp","attributes","k","rewritten","originalId","kept","stringifyOrDrop","value","NAME_CAP","EMAIL_CAP","PHONE_CAP","CUSTOM_MAX_KEYS","CUSTOM_KEY_CAP","CUSTOM_VALUE_CAP","warnedOnce","capString","trimmed","customValue","flat","buildProfileAttributes","profile","attrs","email","phone","dropped","isDev","sdkPlatform","platform","SDK_VERSION","PKG_VERSION","SESSION_IDLE_MS","SESSION_MAX_MS","FLATTEN_MAX_DEPTH","QUEUE_MAX_EVENTS","seqOf","e","isErrorName","evictIndex","events","i","DEVICE_ID_KEY","SESSION_KEY","USER_ID_MAX","ALLOWED_NAMES","METRIC_UNIT","TIER_2_NAMES","normalizeSampleRate","rate","Telemetry","opts","memoryStore","NavigationTracker","error","context","fatal","buildErrorAttributes","data","crashHandler","options","deviceInfoHandler","networkInfoHandler","frameDropsHandler","networkHandler","memoryHandler","navigationHandler","base","reason","read","saved","record","suffix","write","isNewProfile","details","updates","fullName","firstName","lastName","snapshot","activity","crashed","isAllowed","eventName","isCrash","pending","batch","unsaved","deviceInfo","networkInfo","deviceId","metricName","unit","count","prefix","obj","depth","result","prefixedKey","toSend","lastError","persistErr","to"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const o="unnamed",d="surface",h=new Set(["button","link","tab","checkbox","radio","switch","menuitem","option"]),l=new Set(["submit","button","reset"]);function m(t){if(!t)return!1;const e=i(t,"role")?.trim().toLowerCase();if(e&&h.has(e))return!0;switch(a(t)){case"button":case"summary":case"option":return!0;case"a":return i(t,"href")!=null;case"input":return l.has(f(t));default:return!1}}function _(t){if(!t)return!0;const e=i(t,"contenteditable")?.trim().toLowerCase();if(e!=null&&e!=="false")return!0;const n=a(t);return n==="textarea"||n==="select"||n==="input"&&!l.has(f(t))||n==="a"&&(i(t,"download")!=null||i(t,"target")==="_blank")}function b(t){return t?t.trim().toLowerCase().replace(/[^a-z0-9]+/g,"_").slice(0,64).replace(/^_+|_+$/g,""):""}function A(t,e={}){const n=t[0],r=t.find(m);for(const s of t){const u=i(s,"data-edge-action-name");if(u&&u.trim()!=="")return{target:u,nameSource:"edge_action",tag:a(s),node:s,actionable:r!==void 0}}if(!r)return{target:e.pointerCursor?o:d,nameSource:"none",tag:a(n),node:n,actionable:!1};const g=[["test_id",i(r,"data-testid")],["aria_label",i(r,"aria-label")],["title",i(r,"title")],["text",r.textContent??null]];for(const[s,u]of g){const c=b(u);if(c)return{target:c,nameSource:s,tag:a(r),node:r,actionable:!0}}return{target:o,nameSource:"none",tag:a(r),node:r,actionable:!0}}function E(t){return{"ui.type":t.type,"ui.target":t.target,"ui.name_source":t.nameSource,"ui.tag":t.tag,"ui.x":Math.round(t.x??0),"ui.y":Math.round(t.y??0),...t.rage?{"ui.rage":!0}:{}}}const N=3,p=1e3;class C{constructor(e){this.windowMs=e,this.hits=[],this.flagged=!1}record(e,n){return e!==this.node&&(this.node=e,this.hits=[],this.flagged=!1),this.hits=this.hits.filter(r=>n-r<this.windowMs),this.hits.push(n),this.hits.length<N?(this.flagged=!1,!1):this.flagged?!1:(this.flagged=!0,!0)}}function f(t){return(i(t,"type")??"text").trim().toLowerCase()}function i(t,e){return t?.getAttribute?t.getAttribute(e):null}function a(t){return(t?.tagName??"").toLowerCase()}exports.RAGE_WINDOW_MS=p;exports.RageTracker=C;exports.UI_UNNAMED=o;exports.isDeadClickExempt=_;exports.resolveUiName=A;exports.uiAttributes=E;
|
|
2
|
+
//# sourceMappingURL=uiInteraction-CdzvEI1p.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uiInteraction-CdzvEI1p.cjs","sources":["../src/adapters/uiInteraction.ts"],"sourcesContent":["// `ui.interaction`'s name ladder, role gate and rage window (§4.6, #102) — the platform-free\n// half, so the web wiring and #103's native `trackTap` cannot drift on the rules.\n//\n// Elements are duck-typed (`tagName`, `getAttribute`, `textContent`) rather than typed as\n// `Element`: the native build compiles this file too and has no DOM lib, and it makes the\n// ladder testable against plain objects instead of a headless browser.\n\n/** Rungs 2-5 are capped at 64 (§4.6). Rung 1 is not — see `resolveUiName`. */\nexport const UI_NAME_MAX = 64;\n\n/**\n * §4.6's three unnamed values, two of which live here. `unnamed` is an **instrumentation\n * gap someone should close**; `surface` is whitespace with nothing to fix. Collapsing them\n * into one recreates the original disease in miniature — `40% unnamed` with no way to tell\n * a missing `data-edge-action-name` from people tapping padding.\n */\nexport const UI_UNNAMED = \"unnamed\";\nexport const UI_SURFACE = \"surface\";\n\n/** Six values on web, two (`edge_action | none`) on native (§4.6). */\nexport type UiNameSource =\n | \"edge_action\"\n | \"test_id\"\n | \"aria_label\"\n | \"title\"\n | \"text\"\n | \"none\";\n\n/** The bare minimum of an element this module reads. */\nexport type UiElement = {\n tagName?: string;\n getAttribute?(name: string): string | null;\n textContent?: string | null;\n};\n\n/**\n * §4.6's role gate: `<button>`, `<a href>`, `<input type=submit|button|reset>`, `<summary>`,\n * `<option>`, or an explicit ARIA role.\n *\n * ⚠ **A proxy, not a privacy guarantee.** `<button>Delete John Kamau</button>` still ships\n * that text; `beforeSend` is the answer and there is deliberately **no second per-element\n * masking mechanism** (§4.6). What the gate buys is that the PII-carrying clickable `<div>`\n * — which sets no role — is never auto-named. On RN-Web `createDOMProps` maps the `role`\n * prop to an analogous semantic element, so a `Pressable` lands here automatically.\n */\nconst ACTIONABLE_ROLES = new Set([\n \"button\", \"link\", \"tab\", \"checkbox\", \"radio\", \"switch\", \"menuitem\", \"option\",\n]);\nconst ACTIONABLE_INPUT_TYPES = new Set([\"submit\", \"button\", \"reset\"]);\n\nexport function isActionable(el: UiElement | undefined | null): boolean {\n if (!el) return false;\n const role = attr(el, \"role\")?.trim().toLowerCase();\n if (role && ACTIONABLE_ROLES.has(role)) return true;\n switch (tagOf(el)) {\n case \"button\":\n case \"summary\":\n case \"option\":\n return true;\n case \"a\":\n return attr(el, \"href\") != null;\n case \"input\":\n return ACTIONABLE_INPUT_TYPES.has(inputType(el));\n default:\n return false;\n }\n}\n\n/**\n * Text entry and downloads are excluded from dead-click judgement (§4.6): typing mutates\n * nothing the observer can see, and a download or a `_blank` anchor does its work outside\n * this document entirely. Both would be false accusations.\n */\nexport function isDeadClickExempt(el: UiElement | undefined | null): boolean {\n if (!el) return true;\n // `contenteditable=\"false\"` is an ordinary element and must stay judgeable; the empty\n // string and `\"true\"` are both the editable form.\n const editable = attr(el, \"contenteditable\")?.trim().toLowerCase();\n if (editable != null && editable !== \"false\") return true;\n const tag = tagOf(el);\n if (tag === \"textarea\" || tag === \"select\") return true;\n if (tag === \"input\" && !ACTIONABLE_INPUT_TYPES.has(inputType(el))) return true;\n if (tag === \"a\" && (attr(el, \"download\") != null || attr(el, \"target\") === \"_blank\")) return true;\n return false;\n}\n\n/**\n * Rungs 2-5's normalization (§4.6): trim, lowercase, non-alphanumeric → `_`, collapse runs,\n * cap 64. Edge underscores are dropped too, so `\"Add to cart!\"` is `add_to_cart` and not\n * `add_to_cart_` — a cap or a trailing `!` must not change the name a consumer sees.\n * Returns `\"\"` when nothing survived, which the ladder reads as \"this rung had nothing\".\n */\nexport function normalizeUiName(raw: string | null | undefined): string {\n if (!raw) return \"\";\n return raw\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .slice(0, UI_NAME_MAX)\n .replace(/^_+|_+$/g, \"\");\n}\n\n/** What one click resolved to, before the trace and view snapshot are attached. */\nexport type UiNameResolution = {\n /** `ui.target` — a derived name, or `unnamed` / `surface` (§4.6). */\n target: string;\n nameSource: UiNameSource;\n /** `ui.tag` — the *resolved* element's tag, lowercased; the click target when none resolved. */\n tag: string;\n /**\n * The node rage identity is keyed on — the resolved element, or the raw target when\n * nothing resolved. **Never the derived name** (§4.6): three clicks on three different\n * `unnamed` divs are indistinguishable on the wire, which is the whole reason rage runs\n * client-side.\n */\n node: unknown;\n /** Whether the click landed on something role-actionable — the `ui.dead` gate. */\n actionable: boolean;\n};\n\n/**\n * §4.6's five-rung ladder, gated on element role.\n *\n * Rung 1 (`data-edge-action-name`) is scanned over the **whole** path, works on role-less\n * elements, always wins, and passes through **unnormalized and uncapped** — it is explicit\n * author intent and a consumer must be able to predict the value they just set. Rungs 2-5\n * are read off the nearest role-actionable ancestor *only*, and normalized.\n *\n * @param path `composedPath()`, innermost first.\n * @param pointerCursor whether the click target's computed cursor is `pointer` — the one\n * signal that separates `unnamed` (a real control nobody instrumented) from `surface`.\n */\nexport function resolveUiName(\n path: readonly UiElement[],\n opts: { pointerCursor?: boolean } = {},\n): UiNameResolution {\n const target = path[0];\n const host = path.find(isActionable);\n\n for (const el of path) {\n const explicit = attr(el, \"data-edge-action-name\");\n if (explicit && explicit.trim() !== \"\") {\n // `actionable` reports the **click**, not the attribute-bearing element: a\n // `data-edge-action-name` on a role-less wrapper around a real `<button>` must\n // still be judged for `ui.dead`. That is the population §4.6 most wants judged.\n return { target: explicit, nameSource: \"edge_action\", tag: tagOf(el), node: el, actionable: host !== undefined };\n }\n }\n\n if (!host) {\n // Role-less. A pointer cursor still says \"someone built a control here\" (§4.6).\n return {\n target: opts.pointerCursor ? UI_UNNAMED : UI_SURFACE,\n nameSource: \"none\",\n tag: tagOf(target),\n node: target,\n actionable: false,\n };\n }\n\n const rungs: Array<[UiNameSource, string | null]> = [\n [\"test_id\", attr(host, \"data-testid\")], // read, never written (§4.6)\n [\"aria_label\", attr(host, \"aria-label\")],\n [\"title\", attr(host, \"title\")],\n [\"text\", host.textContent ?? null],\n ];\n for (const [nameSource, raw] of rungs) {\n const name = normalizeUiName(raw);\n if (name) return { target: name, nameSource, tag: tagOf(host), node: host, actionable: true };\n }\n\n // Actionable but nothing survived: someone should add `data-edge-action-name`.\n return { target: UI_UNNAMED, nameSource: \"none\", tag: tagOf(host), node: host, actionable: true };\n}\n\n/**\n * §4.6's key block, shared so a web click and a native tap cannot drift on a key name.\n * Everything platform-specific is decided by the caller: web resolves the ladder against the\n * DOM, native is handed the name outright.\n *\n * `ui.x`/`ui.y` default to `0` — the key is never-null in §4.6 and a keyboard activation\n * (web) or a `trackTap(name)` (native) genuinely has no viewport coordinate. `ui.rage` is\n * **omitted when false**, deliberately asymmetric with `ui.dead`, which the web caller adds\n * later or not at all.\n */\nexport function uiAttributes(a: {\n type: \"click\" | \"tap\";\n target: string;\n nameSource: UiNameSource;\n tag: string;\n x?: number;\n y?: number;\n rage?: boolean;\n}): Record<string, any> {\n return {\n \"ui.type\": a.type,\n \"ui.target\": a.target,\n \"ui.name_source\": a.nameSource,\n \"ui.tag\": a.tag,\n \"ui.x\": Math.round(a.x ?? 0),\n \"ui.y\": Math.round(a.y ?? 0),\n ...(a.rage ? { \"ui.rage\": true } : {}),\n };\n}\n\n/** §4.6's threshold: **≥3** clicks inside the window. */\nexport const RAGE_CLICK_THRESHOLD = 3;\n\n/**\n * §4.6 states the same 1000 ms as the dead-click window, but the two are **not the same\n * constant** and must not be bound: the dead-click window is §4.5.2's `QUIET_WINDOW_MS`\n * because the contract says so, and retuning network settle must not silently retune what\n * counts as user frustration.\n */\nexport const RAGE_WINDOW_MS = 1000;\n\n/**\n * ≥3 clicks within a sliding window **on the same live element node**, flagged **once per\n * burst** on the crossing click — so `rage bursts = count of flagged rows` and a fourth and\n * fifth angry click do not each mint their own burst.\n *\n * ⚠ Identity is the node reference the caller hands in, never `ui.target`. The tracker is\n * per-`Telemetry`, so the node references it holds die with the page; it keeps at most\n * `RAGE_CLICK_THRESHOLD` timestamps for one node at a time and drops the node the moment a\n * click lands elsewhere, which is why there is no eviction policy to get wrong.\n */\nexport class RageTracker {\n private node: unknown;\n private hits: number[] = [];\n private flagged = false;\n\n constructor(private readonly windowMs: number) {}\n\n /** @returns `true` on the click that crosses the threshold, and only that one. */\n record(node: unknown, now: number): boolean {\n if (node !== this.node) {\n this.node = node;\n this.hits = [];\n this.flagged = false;\n }\n // Strictly inside the window, matching every other boundary in this SDK.\n this.hits = this.hits.filter(t => now - t < this.windowMs);\n this.hits.push(now);\n if (this.hits.length < RAGE_CLICK_THRESHOLD) {\n // The burst decayed below the threshold — the next crossing is a new burst.\n this.flagged = false;\n return false;\n }\n if (this.flagged) return false;\n this.flagged = true;\n return true;\n }\n}\n\n/** `<input>` with no `type` is a text field — the attribute defaults, the DOM does not. */\nfunction inputType(el: UiElement): string {\n return (attr(el, \"type\") ?? \"text\").trim().toLowerCase();\n}\n\nfunction attr(el: UiElement | undefined | null, name: string): string | null {\n return el?.getAttribute ? el.getAttribute(name) : null;\n}\n\nfunction tagOf(el: UiElement | undefined | null): string {\n return (el?.tagName ?? \"\").toLowerCase();\n}\n"],"names":["UI_UNNAMED","UI_SURFACE","ACTIONABLE_ROLES","ACTIONABLE_INPUT_TYPES","isActionable","el","role","attr","tagOf","inputType","isDeadClickExempt","editable","tag","normalizeUiName","raw","resolveUiName","path","opts","target","host","explicit","rungs","nameSource","name","uiAttributes","a","RAGE_CLICK_THRESHOLD","RAGE_WINDOW_MS","RageTracker","windowMs","node","now","t"],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const c = "unnamed", d = "surface", h = /* @__PURE__ */ new Set([
|
|
2
|
+
"button",
|
|
3
|
+
"link",
|
|
4
|
+
"tab",
|
|
5
|
+
"checkbox",
|
|
6
|
+
"radio",
|
|
7
|
+
"switch",
|
|
8
|
+
"menuitem",
|
|
9
|
+
"option"
|
|
10
|
+
]), f = /* @__PURE__ */ new Set(["submit", "button", "reset"]);
|
|
11
|
+
function m(t) {
|
|
12
|
+
if (!t) return !1;
|
|
13
|
+
const e = a(t, "role")?.trim().toLowerCase();
|
|
14
|
+
if (e && h.has(e)) return !0;
|
|
15
|
+
switch (i(t)) {
|
|
16
|
+
case "button":
|
|
17
|
+
case "summary":
|
|
18
|
+
case "option":
|
|
19
|
+
return !0;
|
|
20
|
+
case "a":
|
|
21
|
+
return a(t, "href") != null;
|
|
22
|
+
case "input":
|
|
23
|
+
return f.has(l(t));
|
|
24
|
+
default:
|
|
25
|
+
return !1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function A(t) {
|
|
29
|
+
if (!t) return !0;
|
|
30
|
+
const e = a(t, "contenteditable")?.trim().toLowerCase();
|
|
31
|
+
if (e != null && e !== "false") return !0;
|
|
32
|
+
const n = i(t);
|
|
33
|
+
return n === "textarea" || n === "select" || n === "input" && !f.has(l(t)) || n === "a" && (a(t, "download") != null || a(t, "target") === "_blank");
|
|
34
|
+
}
|
|
35
|
+
function _(t) {
|
|
36
|
+
return t ? t.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 64).replace(/^_+|_+$/g, "") : "";
|
|
37
|
+
}
|
|
38
|
+
function p(t, e = {}) {
|
|
39
|
+
const n = t[0], r = t.find(m);
|
|
40
|
+
for (const s of t) {
|
|
41
|
+
const o = a(s, "data-edge-action-name");
|
|
42
|
+
if (o && o.trim() !== "")
|
|
43
|
+
return { target: o, nameSource: "edge_action", tag: i(s), node: s, actionable: r !== void 0 };
|
|
44
|
+
}
|
|
45
|
+
if (!r)
|
|
46
|
+
return {
|
|
47
|
+
target: e.pointerCursor ? c : d,
|
|
48
|
+
nameSource: "none",
|
|
49
|
+
tag: i(n),
|
|
50
|
+
node: n,
|
|
51
|
+
actionable: !1
|
|
52
|
+
};
|
|
53
|
+
const g = [
|
|
54
|
+
["test_id", a(r, "data-testid")],
|
|
55
|
+
// read, never written (§4.6)
|
|
56
|
+
["aria_label", a(r, "aria-label")],
|
|
57
|
+
["title", a(r, "title")],
|
|
58
|
+
["text", r.textContent ?? null]
|
|
59
|
+
];
|
|
60
|
+
for (const [s, o] of g) {
|
|
61
|
+
const u = _(o);
|
|
62
|
+
if (u) return { target: u, nameSource: s, tag: i(r), node: r, actionable: !0 };
|
|
63
|
+
}
|
|
64
|
+
return { target: c, nameSource: "none", tag: i(r), node: r, actionable: !0 };
|
|
65
|
+
}
|
|
66
|
+
function w(t) {
|
|
67
|
+
return {
|
|
68
|
+
"ui.type": t.type,
|
|
69
|
+
"ui.target": t.target,
|
|
70
|
+
"ui.name_source": t.nameSource,
|
|
71
|
+
"ui.tag": t.tag,
|
|
72
|
+
"ui.x": Math.round(t.x ?? 0),
|
|
73
|
+
"ui.y": Math.round(t.y ?? 0),
|
|
74
|
+
...t.rage ? { "ui.rage": !0 } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const b = 3, C = 1e3;
|
|
78
|
+
class E {
|
|
79
|
+
constructor(e) {
|
|
80
|
+
this.windowMs = e, this.hits = [], this.flagged = !1;
|
|
81
|
+
}
|
|
82
|
+
/** @returns `true` on the click that crosses the threshold, and only that one. */
|
|
83
|
+
record(e, n) {
|
|
84
|
+
return e !== this.node && (this.node = e, this.hits = [], this.flagged = !1), this.hits = this.hits.filter((r) => n - r < this.windowMs), this.hits.push(n), this.hits.length < b ? (this.flagged = !1, !1) : this.flagged ? !1 : (this.flagged = !0, !0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function l(t) {
|
|
88
|
+
return (a(t, "type") ?? "text").trim().toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
function a(t, e) {
|
|
91
|
+
return t?.getAttribute ? t.getAttribute(e) : null;
|
|
92
|
+
}
|
|
93
|
+
function i(t) {
|
|
94
|
+
return (t?.tagName ?? "").toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
E as R,
|
|
98
|
+
c as U,
|
|
99
|
+
C as a,
|
|
100
|
+
A as i,
|
|
101
|
+
p as r,
|
|
102
|
+
w as u
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=uiInteraction-DAqxxz0q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uiInteraction-DAqxxz0q.js","sources":["../src/adapters/uiInteraction.ts"],"sourcesContent":["// `ui.interaction`'s name ladder, role gate and rage window (§4.6, #102) — the platform-free\n// half, so the web wiring and #103's native `trackTap` cannot drift on the rules.\n//\n// Elements are duck-typed (`tagName`, `getAttribute`, `textContent`) rather than typed as\n// `Element`: the native build compiles this file too and has no DOM lib, and it makes the\n// ladder testable against plain objects instead of a headless browser.\n\n/** Rungs 2-5 are capped at 64 (§4.6). Rung 1 is not — see `resolveUiName`. */\nexport const UI_NAME_MAX = 64;\n\n/**\n * §4.6's three unnamed values, two of which live here. `unnamed` is an **instrumentation\n * gap someone should close**; `surface` is whitespace with nothing to fix. Collapsing them\n * into one recreates the original disease in miniature — `40% unnamed` with no way to tell\n * a missing `data-edge-action-name` from people tapping padding.\n */\nexport const UI_UNNAMED = \"unnamed\";\nexport const UI_SURFACE = \"surface\";\n\n/** Six values on web, two (`edge_action | none`) on native (§4.6). */\nexport type UiNameSource =\n | \"edge_action\"\n | \"test_id\"\n | \"aria_label\"\n | \"title\"\n | \"text\"\n | \"none\";\n\n/** The bare minimum of an element this module reads. */\nexport type UiElement = {\n tagName?: string;\n getAttribute?(name: string): string | null;\n textContent?: string | null;\n};\n\n/**\n * §4.6's role gate: `<button>`, `<a href>`, `<input type=submit|button|reset>`, `<summary>`,\n * `<option>`, or an explicit ARIA role.\n *\n * ⚠ **A proxy, not a privacy guarantee.** `<button>Delete John Kamau</button>` still ships\n * that text; `beforeSend` is the answer and there is deliberately **no second per-element\n * masking mechanism** (§4.6). What the gate buys is that the PII-carrying clickable `<div>`\n * — which sets no role — is never auto-named. On RN-Web `createDOMProps` maps the `role`\n * prop to an analogous semantic element, so a `Pressable` lands here automatically.\n */\nconst ACTIONABLE_ROLES = new Set([\n \"button\", \"link\", \"tab\", \"checkbox\", \"radio\", \"switch\", \"menuitem\", \"option\",\n]);\nconst ACTIONABLE_INPUT_TYPES = new Set([\"submit\", \"button\", \"reset\"]);\n\nexport function isActionable(el: UiElement | undefined | null): boolean {\n if (!el) return false;\n const role = attr(el, \"role\")?.trim().toLowerCase();\n if (role && ACTIONABLE_ROLES.has(role)) return true;\n switch (tagOf(el)) {\n case \"button\":\n case \"summary\":\n case \"option\":\n return true;\n case \"a\":\n return attr(el, \"href\") != null;\n case \"input\":\n return ACTIONABLE_INPUT_TYPES.has(inputType(el));\n default:\n return false;\n }\n}\n\n/**\n * Text entry and downloads are excluded from dead-click judgement (§4.6): typing mutates\n * nothing the observer can see, and a download or a `_blank` anchor does its work outside\n * this document entirely. Both would be false accusations.\n */\nexport function isDeadClickExempt(el: UiElement | undefined | null): boolean {\n if (!el) return true;\n // `contenteditable=\"false\"` is an ordinary element and must stay judgeable; the empty\n // string and `\"true\"` are both the editable form.\n const editable = attr(el, \"contenteditable\")?.trim().toLowerCase();\n if (editable != null && editable !== \"false\") return true;\n const tag = tagOf(el);\n if (tag === \"textarea\" || tag === \"select\") return true;\n if (tag === \"input\" && !ACTIONABLE_INPUT_TYPES.has(inputType(el))) return true;\n if (tag === \"a\" && (attr(el, \"download\") != null || attr(el, \"target\") === \"_blank\")) return true;\n return false;\n}\n\n/**\n * Rungs 2-5's normalization (§4.6): trim, lowercase, non-alphanumeric → `_`, collapse runs,\n * cap 64. Edge underscores are dropped too, so `\"Add to cart!\"` is `add_to_cart` and not\n * `add_to_cart_` — a cap or a trailing `!` must not change the name a consumer sees.\n * Returns `\"\"` when nothing survived, which the ladder reads as \"this rung had nothing\".\n */\nexport function normalizeUiName(raw: string | null | undefined): string {\n if (!raw) return \"\";\n return raw\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .slice(0, UI_NAME_MAX)\n .replace(/^_+|_+$/g, \"\");\n}\n\n/** What one click resolved to, before the trace and view snapshot are attached. */\nexport type UiNameResolution = {\n /** `ui.target` — a derived name, or `unnamed` / `surface` (§4.6). */\n target: string;\n nameSource: UiNameSource;\n /** `ui.tag` — the *resolved* element's tag, lowercased; the click target when none resolved. */\n tag: string;\n /**\n * The node rage identity is keyed on — the resolved element, or the raw target when\n * nothing resolved. **Never the derived name** (§4.6): three clicks on three different\n * `unnamed` divs are indistinguishable on the wire, which is the whole reason rage runs\n * client-side.\n */\n node: unknown;\n /** Whether the click landed on something role-actionable — the `ui.dead` gate. */\n actionable: boolean;\n};\n\n/**\n * §4.6's five-rung ladder, gated on element role.\n *\n * Rung 1 (`data-edge-action-name`) is scanned over the **whole** path, works on role-less\n * elements, always wins, and passes through **unnormalized and uncapped** — it is explicit\n * author intent and a consumer must be able to predict the value they just set. Rungs 2-5\n * are read off the nearest role-actionable ancestor *only*, and normalized.\n *\n * @param path `composedPath()`, innermost first.\n * @param pointerCursor whether the click target's computed cursor is `pointer` — the one\n * signal that separates `unnamed` (a real control nobody instrumented) from `surface`.\n */\nexport function resolveUiName(\n path: readonly UiElement[],\n opts: { pointerCursor?: boolean } = {},\n): UiNameResolution {\n const target = path[0];\n const host = path.find(isActionable);\n\n for (const el of path) {\n const explicit = attr(el, \"data-edge-action-name\");\n if (explicit && explicit.trim() !== \"\") {\n // `actionable` reports the **click**, not the attribute-bearing element: a\n // `data-edge-action-name` on a role-less wrapper around a real `<button>` must\n // still be judged for `ui.dead`. That is the population §4.6 most wants judged.\n return { target: explicit, nameSource: \"edge_action\", tag: tagOf(el), node: el, actionable: host !== undefined };\n }\n }\n\n if (!host) {\n // Role-less. A pointer cursor still says \"someone built a control here\" (§4.6).\n return {\n target: opts.pointerCursor ? UI_UNNAMED : UI_SURFACE,\n nameSource: \"none\",\n tag: tagOf(target),\n node: target,\n actionable: false,\n };\n }\n\n const rungs: Array<[UiNameSource, string | null]> = [\n [\"test_id\", attr(host, \"data-testid\")], // read, never written (§4.6)\n [\"aria_label\", attr(host, \"aria-label\")],\n [\"title\", attr(host, \"title\")],\n [\"text\", host.textContent ?? null],\n ];\n for (const [nameSource, raw] of rungs) {\n const name = normalizeUiName(raw);\n if (name) return { target: name, nameSource, tag: tagOf(host), node: host, actionable: true };\n }\n\n // Actionable but nothing survived: someone should add `data-edge-action-name`.\n return { target: UI_UNNAMED, nameSource: \"none\", tag: tagOf(host), node: host, actionable: true };\n}\n\n/**\n * §4.6's key block, shared so a web click and a native tap cannot drift on a key name.\n * Everything platform-specific is decided by the caller: web resolves the ladder against the\n * DOM, native is handed the name outright.\n *\n * `ui.x`/`ui.y` default to `0` — the key is never-null in §4.6 and a keyboard activation\n * (web) or a `trackTap(name)` (native) genuinely has no viewport coordinate. `ui.rage` is\n * **omitted when false**, deliberately asymmetric with `ui.dead`, which the web caller adds\n * later or not at all.\n */\nexport function uiAttributes(a: {\n type: \"click\" | \"tap\";\n target: string;\n nameSource: UiNameSource;\n tag: string;\n x?: number;\n y?: number;\n rage?: boolean;\n}): Record<string, any> {\n return {\n \"ui.type\": a.type,\n \"ui.target\": a.target,\n \"ui.name_source\": a.nameSource,\n \"ui.tag\": a.tag,\n \"ui.x\": Math.round(a.x ?? 0),\n \"ui.y\": Math.round(a.y ?? 0),\n ...(a.rage ? { \"ui.rage\": true } : {}),\n };\n}\n\n/** §4.6's threshold: **≥3** clicks inside the window. */\nexport const RAGE_CLICK_THRESHOLD = 3;\n\n/**\n * §4.6 states the same 1000 ms as the dead-click window, but the two are **not the same\n * constant** and must not be bound: the dead-click window is §4.5.2's `QUIET_WINDOW_MS`\n * because the contract says so, and retuning network settle must not silently retune what\n * counts as user frustration.\n */\nexport const RAGE_WINDOW_MS = 1000;\n\n/**\n * ≥3 clicks within a sliding window **on the same live element node**, flagged **once per\n * burst** on the crossing click — so `rage bursts = count of flagged rows` and a fourth and\n * fifth angry click do not each mint their own burst.\n *\n * ⚠ Identity is the node reference the caller hands in, never `ui.target`. The tracker is\n * per-`Telemetry`, so the node references it holds die with the page; it keeps at most\n * `RAGE_CLICK_THRESHOLD` timestamps for one node at a time and drops the node the moment a\n * click lands elsewhere, which is why there is no eviction policy to get wrong.\n */\nexport class RageTracker {\n private node: unknown;\n private hits: number[] = [];\n private flagged = false;\n\n constructor(private readonly windowMs: number) {}\n\n /** @returns `true` on the click that crosses the threshold, and only that one. */\n record(node: unknown, now: number): boolean {\n if (node !== this.node) {\n this.node = node;\n this.hits = [];\n this.flagged = false;\n }\n // Strictly inside the window, matching every other boundary in this SDK.\n this.hits = this.hits.filter(t => now - t < this.windowMs);\n this.hits.push(now);\n if (this.hits.length < RAGE_CLICK_THRESHOLD) {\n // The burst decayed below the threshold — the next crossing is a new burst.\n this.flagged = false;\n return false;\n }\n if (this.flagged) return false;\n this.flagged = true;\n return true;\n }\n}\n\n/** `<input>` with no `type` is a text field — the attribute defaults, the DOM does not. */\nfunction inputType(el: UiElement): string {\n return (attr(el, \"type\") ?? \"text\").trim().toLowerCase();\n}\n\nfunction attr(el: UiElement | undefined | null, name: string): string | null {\n return el?.getAttribute ? el.getAttribute(name) : null;\n}\n\nfunction tagOf(el: UiElement | undefined | null): string {\n return (el?.tagName ?? \"\").toLowerCase();\n}\n"],"names":["UI_UNNAMED","UI_SURFACE","ACTIONABLE_ROLES","ACTIONABLE_INPUT_TYPES","isActionable","el","role","attr","tagOf","inputType","isDeadClickExempt","editable","tag","normalizeUiName","raw","resolveUiName","path","opts","target","host","explicit","rungs","nameSource","name","uiAttributes","a","RAGE_CLICK_THRESHOLD","RAGE_WINDOW_MS","RageTracker","windowMs","node","now","t"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport-Dd85GUNO.cjs","sources":["../src/adapters/viewport.ts"],"sourcesContent":["// §3.3's four viewport keys, shaped once for both builds (#108).\n//\n// The *reads* are platform APIs and stay in `deviceInfo.native.ts` / `deviceInfo.web.ts`\n// — RN's `Dimensions` + `PixelRatio`, the DOM's `innerWidth`/`innerHeight` +\n// `devicePixelRatio`. The *derivation* is not, and a second copy of it is how the two\n// builds drift: `screen_width_px` meaning physical pixels on one and CSS pixels on the\n// other would make a single column mean two things.\n\n/** §3.3 types all four never-null, so this returns all four or none of it is honest. */\nexport type ViewportKeys = {\n screen_density: number;\n screen_width_px: number;\n screen_height_px: number;\n orientation: Orientation;\n};\n\n/** §3.3's cardinality is 2. A square window reports `portrait`. */\nexport type Orientation = \"portrait\" | \"landscape\";\n\n/**\n * @param cssWidth logical width — dp on native, CSS px on web\n * @param cssHeight logical height, same units\n * @param density the pixel ratio, so `cssWidth * density` is physical pixels on both\n */\nexport function viewportKeys(cssWidth: number, cssHeight: number, density: number): ViewportKeys {\n return {\n screen_density: density,\n screen_width_px: Math.round(cssWidth * density),\n screen_height_px: Math.round(cssHeight * density),\n // Derived from the two values the other keys already ship rather than from\n // `screen.orientation` / RN's orientation modules: no feature check, no new\n // dependency, and it cannot disagree with the dimensions on the same row.\n orientation: cssWidth > cssHeight ? \"landscape\" : \"portrait\",\n };\n}\n"],"names":["viewportKeys","cssWidth","cssHeight","density"],"mappings":"aAwBO,SAASA,EAAaC,EAAkBC,EAAmBC,EAA+B,CAC7F,MAAO,CACH,eAAgBA,EAChB,gBAAiB,KAAK,MAAMF,EAAWE,CAAO,EAC9C,iBAAkB,KAAK,MAAMD,EAAYC,CAAO,EAIhD,YAAaF,EAAWC,EAAY,YAAc,UAAA,CAE1D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function t(e, n, r) {
|
|
2
|
+
return {
|
|
3
|
+
screen_density: r,
|
|
4
|
+
screen_width_px: Math.round(e * r),
|
|
5
|
+
screen_height_px: Math.round(n * r),
|
|
6
|
+
// Derived from the two values the other keys already ship rather than from
|
|
7
|
+
// `screen.orientation` / RN's orientation modules: no feature check, no new
|
|
8
|
+
// dependency, and it cannot disagree with the dimensions on the same row.
|
|
9
|
+
orientation: e > n ? "landscape" : "portrait"
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
t as v
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=viewport-Dzmt5QwK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport-Dzmt5QwK.js","sources":["../src/adapters/viewport.ts"],"sourcesContent":["// §3.3's four viewport keys, shaped once for both builds (#108).\n//\n// The *reads* are platform APIs and stay in `deviceInfo.native.ts` / `deviceInfo.web.ts`\n// — RN's `Dimensions` + `PixelRatio`, the DOM's `innerWidth`/`innerHeight` +\n// `devicePixelRatio`. The *derivation* is not, and a second copy of it is how the two\n// builds drift: `screen_width_px` meaning physical pixels on one and CSS pixels on the\n// other would make a single column mean two things.\n\n/** §3.3 types all four never-null, so this returns all four or none of it is honest. */\nexport type ViewportKeys = {\n screen_density: number;\n screen_width_px: number;\n screen_height_px: number;\n orientation: Orientation;\n};\n\n/** §3.3's cardinality is 2. A square window reports `portrait`. */\nexport type Orientation = \"portrait\" | \"landscape\";\n\n/**\n * @param cssWidth logical width — dp on native, CSS px on web\n * @param cssHeight logical height, same units\n * @param density the pixel ratio, so `cssWidth * density` is physical pixels on both\n */\nexport function viewportKeys(cssWidth: number, cssHeight: number, density: number): ViewportKeys {\n return {\n screen_density: density,\n screen_width_px: Math.round(cssWidth * density),\n screen_height_px: Math.round(cssHeight * density),\n // Derived from the two values the other keys already ship rather than from\n // `screen.orientation` / RN's orientation modules: no feature check, no new\n // dependency, and it cannot disagree with the dimensions on the same row.\n orientation: cssWidth > cssHeight ? \"landscape\" : \"portrait\",\n };\n}\n"],"names":["viewportKeys","cssWidth","cssHeight","density"],"mappings":"AAwBO,SAASA,EAAaC,GAAkBC,GAAmBC,GAA+B;AAC7F,SAAO;AAAA,IACH,gBAAgBA;AAAA,IAChB,iBAAiB,KAAK,MAAMF,IAAWE,CAAO;AAAA,IAC9C,kBAAkB,KAAK,MAAMD,IAAYC,CAAO;AAAA;AAAA;AAAA;AAAA,IAIhD,aAAaF,IAAWC,IAAY,cAAc;AAAA,EAAA;AAE1D;"}
|