@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,1280 @@
|
|
|
1
|
+
import { n as O, p as S, f as M, b as U, d as o, i as x, m as V } from "./memoryStore-F5I-GQKM.js";
|
|
2
|
+
import { N as F } from "./navigationTracker-BmywA_Bg.js";
|
|
3
|
+
import { N as I } from "./loadingTime-ZkYvFijA.js";
|
|
4
|
+
import { b as z } from "./crashCapture-BW8WuyaU.js";
|
|
5
|
+
class L {
|
|
6
|
+
constructor(e) {
|
|
7
|
+
this.startTimes = /* @__PURE__ */ new Map(), this.telemetry = e;
|
|
8
|
+
}
|
|
9
|
+
// Screen entry → v3 `navigation` (replaces v2 screen_view). Baseline shape follows the
|
|
10
|
+
// reference/Angular keys; iOS reconciliation is OPEN (see additions ledger).
|
|
11
|
+
async startScreen(e) {
|
|
12
|
+
const t = this.lastScreen;
|
|
13
|
+
this.markStart(e), this.lastScreen = e, await this.telemetry.log("navigation", {
|
|
14
|
+
"navigation.from_screen": t ?? null,
|
|
15
|
+
"navigation.to_screen": e,
|
|
16
|
+
"navigation.method": "screen_start",
|
|
17
|
+
"navigation.route_type": "screen"
|
|
18
|
+
}), await this.telemetry.enterView(e, "explicit");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Arm the dwell clock without emitting `navigation`. The route-change path (§4.11) has
|
|
22
|
+
* already emitted its own `navigation` with `method: "push"`, and emitting a second one
|
|
23
|
+
* with `method: "screen_start"` for the same transition would double the table.
|
|
24
|
+
*/
|
|
25
|
+
markStart(e) {
|
|
26
|
+
this.startTimes.set(e, Date.now()), this.telemetry.currentScreen = e, this.lastScreen = e;
|
|
27
|
+
}
|
|
28
|
+
// Screen exit → v3 `screen.duration` with dwell ms (replaces screen_end + performance.screen_duration).
|
|
29
|
+
async endScreen(e) {
|
|
30
|
+
const t = this.startTimes.get(e);
|
|
31
|
+
if (!t) return;
|
|
32
|
+
const s = Date.now() - t;
|
|
33
|
+
this.startTimes.delete(e), await this.telemetry.log("screen.duration", {
|
|
34
|
+
"screen.name": e,
|
|
35
|
+
"screen.duration_ms": s,
|
|
36
|
+
"screen.exit_method": "navigation"
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function u(i = 16) {
|
|
41
|
+
if (typeof globalThis.crypto?.getRandomValues != "function")
|
|
42
|
+
throw new Error(
|
|
43
|
+
"edge-telemetry: crypto.getRandomValues is unavailable, so no id can be minted. On React Native this means react-native-get-random-values failed to load — reinstall it and rebuild."
|
|
44
|
+
);
|
|
45
|
+
const e = new Uint8Array(Math.ceil(i / 2));
|
|
46
|
+
crypto.getRandomValues(e);
|
|
47
|
+
let t = "";
|
|
48
|
+
for (const s of e) t += s.toString(16).padStart(2, "0");
|
|
49
|
+
return t.slice(0, i);
|
|
50
|
+
}
|
|
51
|
+
const g = { none: 0, url: 1, route: 2, explicit: 3 }, _ = "unknown", H = 16;
|
|
52
|
+
function b() {
|
|
53
|
+
return `view_${Date.now()}_${u(16)}`;
|
|
54
|
+
}
|
|
55
|
+
class $ {
|
|
56
|
+
constructor(e) {
|
|
57
|
+
this.telemetry = e, this.activityListeners = /* @__PURE__ */ new Set(), this.boundaryListeners = /* @__PURE__ */ new Set(), this.retiredNames = /* @__PURE__ */ new Map(), this.host = typeof location < "u" && location?.origin ? location.origin : void 0;
|
|
58
|
+
const t = Date.now();
|
|
59
|
+
this.view = {
|
|
60
|
+
id: b(),
|
|
61
|
+
name: _,
|
|
62
|
+
source: "none",
|
|
63
|
+
referrer: "",
|
|
64
|
+
loadType: "initial_load",
|
|
65
|
+
elapsed: 0,
|
|
66
|
+
resumedAt: t,
|
|
67
|
+
errors: 0,
|
|
68
|
+
actions: 0,
|
|
69
|
+
// The launch view is the one `load_type` that seeds from the platform's
|
|
70
|
+
// runtime-ready marker: it is busy until the bundle has evaluated (§4.5.2).
|
|
71
|
+
settle: new I(t, { awaitRuntimeReady: !0 }),
|
|
72
|
+
// The launch root is live here — it is minted in the same `Telemetry` constructor,
|
|
73
|
+
// just above this one — so the initial view is its child and a web hard load
|
|
74
|
+
// reports `launch`, never `navigation` (§6.2).
|
|
75
|
+
span: e.trace?.viewSpan(t) ?? {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The two keys the Context block carries on every row. Name resolves at log time. */
|
|
79
|
+
get id() {
|
|
80
|
+
return this.view.id;
|
|
81
|
+
}
|
|
82
|
+
get name() {
|
|
83
|
+
return this.view.name;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* §3.1 — `view.name` for a **frozen** `view.id`. A span-carrying row pins itself to the
|
|
87
|
+
* view live at span start, and its name is still resolved at log time, so an
|
|
88
|
+
* `http.request` that outlives a route change reports the departing view's *current*
|
|
89
|
+
* best name rather than a stale copy taken at send.
|
|
90
|
+
*
|
|
91
|
+
* ponytail: a bounded ring of retired names, not a full history. A span outliving
|
|
92
|
+
* `MAX_RETIRED_VIEW_NAMES` boundaries is a request the settle cap (30 s) already gave up
|
|
93
|
+
* on; it degrades to `"unknown"`, which is what a never-named view reports anyway.
|
|
94
|
+
*/
|
|
95
|
+
nameOf(e) {
|
|
96
|
+
return e === this.view.id ? this.view.name : this.retiredNames.get(e) ?? _;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The name ladder (§4.5.1), and the only path that mints a view from a *name*.
|
|
100
|
+
*
|
|
101
|
+
* - a **higher** rung re-stamps the name and never changes `view.id` — that is what
|
|
102
|
+
* "rank beats order" means, and it is why a route name arriving after a URL-derived
|
|
103
|
+
* one upgrades the view in place instead of splitting it in two;
|
|
104
|
+
* - a **lower** rung is ignored outright, whenever it arrives — ⚠ *including its
|
|
105
|
+
* boundary*. A host that mixes rung 1 with `attachNavigation` therefore pins the view
|
|
106
|
+
* to the `screenStart` name until the next `screenStart`: route changes stop minting
|
|
107
|
+
* successors, and dwell keeps accruing under the explicit name. That is what §4.5.1's
|
|
108
|
+
* "a lower rung arriving later does not overwrite a higher one" costs, and the two
|
|
109
|
+
* rungs describing one navigation (the upgrade window) is the case it exists for.
|
|
110
|
+
* It needs a contract ruling, not a local invention — see CLAUDE.md's known gaps;
|
|
111
|
+
* - the **same** rung naming a different screen is a genuine navigation, so it ends the
|
|
112
|
+
* view and mints a successor.
|
|
113
|
+
*
|
|
114
|
+
* Rungs 1 and 2 are handed through unnormalized: a host naming a screen "Step 2 of 3"
|
|
115
|
+
* must not receive "Step {id} of {id}". The caller normalizes rung 3, not this method.
|
|
116
|
+
*/
|
|
117
|
+
async navigate(e, t) {
|
|
118
|
+
const s = g[t], r = g[this.view.source];
|
|
119
|
+
if (!(s < r)) {
|
|
120
|
+
if (s > r) {
|
|
121
|
+
this.view.name = e, this.view.source = t;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
e !== this.view.name && await this.exit("route_change", e, t);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** `view.error_count` — `app.crash` rows sharing this `view.id`. A closed enumeration. */
|
|
128
|
+
countError() {
|
|
129
|
+
this.view.errors++;
|
|
130
|
+
}
|
|
131
|
+
/** `view.action_count` — interaction rows sharing this `view.id`. */
|
|
132
|
+
countAction() {
|
|
133
|
+
this.view.actions++;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* An HTTP request started (§4.5.2). Returns its completion callback, **bound to the view
|
|
137
|
+
* that was live at start** — a request that finishes after a route change belongs to the
|
|
138
|
+
* view it started in and does not hold the arriving one open. The collector's own POST
|
|
139
|
+
* never reaches here; the interceptors filter it before calling.
|
|
140
|
+
*/
|
|
141
|
+
requestStarted(e) {
|
|
142
|
+
return this.notifyActivity(), this.view.settle.requestStarted(e);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* §4.6's dead-click gate, in the two places this manager already sees: a request started
|
|
146
|
+
* and a view minted (a route change, a resume, a session rotation). The web tracker adds
|
|
147
|
+
* DOM mutation on top, which it can observe on its own; these two it cannot.
|
|
148
|
+
*
|
|
149
|
+
* Deliberately fires for the background and rotation boundaries too. Those are not
|
|
150
|
+
* navigations the click caused, so counting them as aliveness **under-reports** dead
|
|
151
|
+
* clicks — which is the direction §4.6 requires: `ui.dead` must never falsely accuse.
|
|
152
|
+
*
|
|
153
|
+
* @returns an unsubscribe, so a tracker that is torn down does not keep the manager alive.
|
|
154
|
+
*/
|
|
155
|
+
onActivity(e) {
|
|
156
|
+
return this.activityListeners.add(e), () => {
|
|
157
|
+
this.activityListeners.delete(e);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
notifyActivity() {
|
|
161
|
+
for (const e of this.activityListeners) e();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* §5.1's view-boundary reset. A subscriber is **awaited inside `beginView`, before the
|
|
165
|
+
* successor replaces the current view**, so anything it emits is stamped with the
|
|
166
|
+
* departing `view.id`. That ordering is the whole feature: a fixed 10 s frame window
|
|
167
|
+
* straddling a route change otherwise charges the departing screen's dropped frames to
|
|
168
|
+
* the arriving one — backwards for the one query the metric exists to serve.
|
|
169
|
+
*
|
|
170
|
+
* ⚠ **It does not fire on the `session_rotation` boundary**, and that is deliberate.
|
|
171
|
+
* `newSession()` installs the new `session.id` *before* minting the successor view, so a
|
|
172
|
+
* row emitted there would carry the new `session.id` with the departing `view.id` — the
|
|
173
|
+
* one thing §4.5's "`view.id` never spans a `session.id`" forbids. Emitting *before* the
|
|
174
|
+
* rotation instead is worse: `logMetric` re-checks session expiry, `lastActivity` is
|
|
175
|
+
* still stale at that point, and the emit would rotate the session a second time. The
|
|
176
|
+
* accepted residue is a frame window that carries across a rotation — at most one
|
|
177
|
+
* unflushed window, of a session that ended by idleness.
|
|
178
|
+
*
|
|
179
|
+
* The successor's load type is handed to the subscriber so it can tell the two firing
|
|
180
|
+
* boundaries apart: `frame_render_time` wants both, while §5.3's running CLS/INP values
|
|
181
|
+
* want **`"resume"` — the background boundary — only**. Emitting a page-load-scoped vital
|
|
182
|
+
* at a soft navigation would ship a second row for the same page load and stamp it with
|
|
183
|
+
* a `view.id` that is not the initial view's.
|
|
184
|
+
*
|
|
185
|
+
* @returns an unsubscribe, so a torn-down subscriber does not keep the manager alive.
|
|
186
|
+
*/
|
|
187
|
+
onBoundary(e) {
|
|
188
|
+
return this.boundaryListeners.add(e), () => {
|
|
189
|
+
this.boundaryListeners.delete(e);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The platform's runtime-ready marker — `loadEventEnd` on web,
|
|
194
|
+
* `performance.rnStartupTiming` on native — or `undefined` where the platform has none.
|
|
195
|
+
*
|
|
196
|
+
* Forwarded to whatever view is current, which is safe because a view that was never gated
|
|
197
|
+
* *ignores* the seed — enforced in `NetworkSettle`, not assumed here. On web the `load`
|
|
198
|
+
* event routinely arrives after the first route change, and flooring that view's settle at
|
|
199
|
+
* the page's load time would charge the launch's cost to a route change.
|
|
200
|
+
*/
|
|
201
|
+
seedRuntimeReady(e) {
|
|
202
|
+
this.view.settle.seedRuntimeReady(e);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Emit the `view` event for the current view. Its `view.name` is authoritative and its
|
|
206
|
+
* `view.name_source` reports the *final* rung; the Context-block copy on rows emitted
|
|
207
|
+
* early in the view may still carry a lower rung's name (§4.5.1).
|
|
208
|
+
*
|
|
209
|
+
* `view.loading_time` is **omitted when null**, following this contract's general
|
|
210
|
+
* absent-means-the-SDK-had-nothing discipline (§4.11 names `navigation.from_screen` as the
|
|
211
|
+
* only explicit wire null). `view.loading_time_outcome` always ships: it is what tells the
|
|
212
|
+
* three null causes apart, and reading p75-of-settled beside %-capped is the whole point.
|
|
213
|
+
*/
|
|
214
|
+
async endView() {
|
|
215
|
+
const e = this.view, { loadingTime: t, outcome: s } = e.settle.resolve();
|
|
216
|
+
await this.telemetry.log("view", {
|
|
217
|
+
// A **point span** (§6.3): `span.start_time` from view entry and never
|
|
218
|
+
// `span.duration_ms`. View dwell is `view.time_spent`, not span width — a width
|
|
219
|
+
// here would stretch every tap-that-navigates envelope across the whole visit.
|
|
220
|
+
...e.span,
|
|
221
|
+
...this.host ? { "view.host": this.host } : {},
|
|
222
|
+
"view.referrer": e.referrer,
|
|
223
|
+
"view.load_type": e.loadType,
|
|
224
|
+
"view.name_source": e.source,
|
|
225
|
+
"view.time_spent": this.timeSpent(),
|
|
226
|
+
...t === null ? {} : { "view.loading_time": t },
|
|
227
|
+
"view.loading_time_outcome": s,
|
|
228
|
+
"view.error_count": e.errors,
|
|
229
|
+
"view.action_count": e.actions,
|
|
230
|
+
"view.request_count": e.settle.requestCount
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Mint the successor. Called directly by session rotation, which has to mint *after*
|
|
235
|
+
* the new `session.id` is in place — `view.id` never spans a `session.id` (§4.5).
|
|
236
|
+
* An unnamed successor carries the departing view's name: backgrounding and a session
|
|
237
|
+
* rotation do not move the user off the screen they were on.
|
|
238
|
+
*/
|
|
239
|
+
async beginView(e, t, s) {
|
|
240
|
+
if (this.notifyActivity(), e !== "session_rotation")
|
|
241
|
+
for (const n of this.boundaryListeners)
|
|
242
|
+
try {
|
|
243
|
+
await n(e);
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
const r = this.view, a = Date.now();
|
|
247
|
+
if (this.retiredNames.set(r.id, r.name), this.retiredNames.size > H) {
|
|
248
|
+
const n = this.retiredNames.keys().next();
|
|
249
|
+
n.done || this.retiredNames.delete(n.value);
|
|
250
|
+
}
|
|
251
|
+
this.view = {
|
|
252
|
+
id: b(),
|
|
253
|
+
name: t ?? r.name,
|
|
254
|
+
source: s ?? r.source,
|
|
255
|
+
referrer: r.name,
|
|
256
|
+
loadType: e,
|
|
257
|
+
elapsed: 0,
|
|
258
|
+
resumedAt: a,
|
|
259
|
+
errors: 0,
|
|
260
|
+
actions: 0,
|
|
261
|
+
// No runtime-ready seed: only `initial_load` has a platform marker to wait on.
|
|
262
|
+
settle: new I(a),
|
|
263
|
+
// A view start *extends* the live root; with none live it mints a `navigation`
|
|
264
|
+
// one (§6.2). Its exit does not extend anything (§6.7).
|
|
265
|
+
span: this.telemetry.trace?.viewSpan(a) ?? {}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** A boundary that stays inside one session: emit, then mint the successor. */
|
|
269
|
+
async exit(e, t, s) {
|
|
270
|
+
await this.endView(), await this.beginView(e, t, s);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The background boundary. The successor is minted immediately — `view.id` must never
|
|
274
|
+
* be absent — but its clock starts paused, so a night spent backgrounded does not land
|
|
275
|
+
* as dwell on whatever screen the user left open.
|
|
276
|
+
*/
|
|
277
|
+
async background() {
|
|
278
|
+
await this.exit("resume");
|
|
279
|
+
const e = this.view;
|
|
280
|
+
e.elapsed += this.sinceResume(), e.resumedAt = void 0;
|
|
281
|
+
}
|
|
282
|
+
/** The matching foreground edge: re-arm the paused clock. */
|
|
283
|
+
foreground() {
|
|
284
|
+
this.view.resumedAt ??= Date.now();
|
|
285
|
+
}
|
|
286
|
+
sinceResume() {
|
|
287
|
+
return this.view.resumedAt === void 0 ? 0 : Date.now() - this.view.resumedAt;
|
|
288
|
+
}
|
|
289
|
+
timeSpent() {
|
|
290
|
+
return Math.max(0, this.view.elapsed + this.sinceResume());
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const j = 2e3, B = 1e4, W = () => u(32), N = () => u(16);
|
|
294
|
+
class K {
|
|
295
|
+
/**
|
|
296
|
+
* @param launchStartMs what `app.start` reports as `span.start_time` — and **neither
|
|
297
|
+
* value is a fork time** (§6.2). Web passes `performance.timeOrigin` (true navigation
|
|
298
|
+
* start); native leaves it defaulted to `initialize()`, because everything before the
|
|
299
|
+
* JS bundle loads is invisible and JS cannot see process fork. Do not compare the two
|
|
300
|
+
* platforms' launch envelopes as if they measured the same interval.
|
|
301
|
+
*/
|
|
302
|
+
constructor(e, t) {
|
|
303
|
+
this.allowlist = O(t);
|
|
304
|
+
const s = Date.now();
|
|
305
|
+
this.launch = this.mint("launch", s, e ?? s);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* §4.1's `sdk.trace_allowlist_size` — **count only, never the hosts**, so an operator can
|
|
309
|
+
* tell "nobody opted in" apart from "the header is being stripped" without the SDK
|
|
310
|
+
* shipping a customer's internal hostnames to the collector.
|
|
311
|
+
*/
|
|
312
|
+
allowlistSize() {
|
|
313
|
+
return this.allowlist.size;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* `app.start`'s own row — a root, so `span.id === rum.action.id` and there is no
|
|
317
|
+
* `parent.span.id`. No `span.duration_ms` either: roots derive theirs server-side.
|
|
318
|
+
*/
|
|
319
|
+
launchRootAttributes() {
|
|
320
|
+
return l(this.launch, !0, this.launch.spanStartMs).keys;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Tier 1 for a `view`, captured at view **entry** — the action that opened the screen,
|
|
324
|
+
* not the one that closed it (§6.3). A **point span**: `span.start_time` and never
|
|
325
|
+
* `span.duration_ms`, because view dwell is not span duration and a width here would
|
|
326
|
+
* stretch every tap-that-navigates envelope across the whole time the user sat there.
|
|
327
|
+
*/
|
|
328
|
+
viewSpan(e) {
|
|
329
|
+
const { root: t, isRoot: s } = this.attach("navigation", e);
|
|
330
|
+
return l(t, s, e).keys;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Tier 1 for a `ui.interaction`, captured at the click's **mint** (§4.6/§6.2). The one
|
|
334
|
+
* unconditional mint in the table: a tap is a new user action by definition, so it
|
|
335
|
+
* *replaces* whatever root was live rather than joining it — which is what makes the
|
|
336
|
+
* request a tap fires a child of the tap and not of the route change before it.
|
|
337
|
+
*
|
|
338
|
+
* A root, so `span.id === rum.action.id`, no `parent.span.id`, and no `span.duration_ms`
|
|
339
|
+
* (§6.1 — a root's width is derived server-side).
|
|
340
|
+
*/
|
|
341
|
+
interactionSpan(e) {
|
|
342
|
+
const t = this.mint("interaction", e, e);
|
|
343
|
+
return l(t, !0, e).keys;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Tier 1 for an `http.request`, captured at **send** — the root live then is the parent,
|
|
347
|
+
* and `span.start_time` is the send — plus §6.5's outcome ladder and the one header the
|
|
348
|
+
* SDK ever writes. Returns the finisher, so duration is stamped from the same pair of
|
|
349
|
+
* timestamps the row's `http.duration_ms` uses.
|
|
350
|
+
*
|
|
351
|
+
* ponytail: a `send()` that throws synchronously never calls the finisher, so a root it
|
|
352
|
+
* minted lives on for up to 2 s with no row describing it. Rare enough to leave; give
|
|
353
|
+
* the finisher a `discard()` sibling if the orphan ratio ever shows up.
|
|
354
|
+
*/
|
|
355
|
+
requestTrace(e, t) {
|
|
356
|
+
const s = this.decide(t);
|
|
357
|
+
if (s === "adopted") {
|
|
358
|
+
const d = S(t.consumerTraceparent), C = {
|
|
359
|
+
"trace.id": d.traceId,
|
|
360
|
+
"span.id": d.spanId,
|
|
361
|
+
"rum.action.id": d.spanId,
|
|
362
|
+
"span.start_time": new Date(e).toISOString(),
|
|
363
|
+
"traceparent.outcome": "adopted"
|
|
364
|
+
};
|
|
365
|
+
return { finish: () => C };
|
|
366
|
+
}
|
|
367
|
+
const { root: r, isRoot: a, attribution: n } = this.attach("request", e), h = s === "inject" ? n : s, c = l(r, a, e), w = {
|
|
368
|
+
...c.keys,
|
|
369
|
+
...h ? { "traceparent.outcome": h } : {}
|
|
370
|
+
}, y = a ? () => w : (d) => ({ ...w, "span.duration_ms": Math.max(0, d - e) });
|
|
371
|
+
return s === "inject" ? { finish: y, header: M(r.traceId, c.spanId) } : { finish: y };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* §6.5's ladder above the `injected_*` split, in table order — the precedence *is* the
|
|
375
|
+
* order of these returns. `undefined` means the attribute is omitted entirely: absent
|
|
376
|
+
* means not traced, which is the honest report for a consumer who never opted in and for
|
|
377
|
+
* a sampled-out session (which injects no header at all, not a `flags=00` id).
|
|
378
|
+
*/
|
|
379
|
+
decide(e) {
|
|
380
|
+
if (!(this.allowlist.size === 0 || !e.sampled))
|
|
381
|
+
return U(this.allowlist, e.url) ? e.noCors ? "skipped_no_cors" : e.consumerTraceparent != null ? S(e.consumerTraceparent) ? "adopted" : "skipped_consumer_set" : "inject" : "skipped_off_allowlist";
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Tier 2 — annotation-only (§6.3): join the trace without occupying a span. No
|
|
385
|
+
* `span.id`, no parent, no duration, and **no minting** — §6.2 lists the four events
|
|
386
|
+
* that mint and these are not among them, so an untraced crash carries no trace keys
|
|
387
|
+
* rather than inventing a root nothing else will ever join.
|
|
388
|
+
*/
|
|
389
|
+
annotate(e = Date.now()) {
|
|
390
|
+
const t = this.liveRoot(e);
|
|
391
|
+
return t ? {
|
|
392
|
+
"trace.id": t.traceId,
|
|
393
|
+
"rum.action.id": t.spanId,
|
|
394
|
+
"trace.root_type": t.rootType
|
|
395
|
+
} : {};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Drop the carrier. Background clears it on both builds, so a resumed app's first fetch
|
|
399
|
+
* mints its own root rather than joining an action from before the user left (§6.2);
|
|
400
|
+
* session rotation clears it because `trace.id` never spans a `session.id` (§6.6).
|
|
401
|
+
*/
|
|
402
|
+
clear() {
|
|
403
|
+
this.root = void 0;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Retire the launch root and mint a replacement, keeping the *reported* launch time —
|
|
407
|
+
* the process really did start then. For a session rotation that lands **before**
|
|
408
|
+
* `app.start`, which is the expired-record cold launch and therefore §4.3's most common
|
|
409
|
+
* path of all.
|
|
410
|
+
*
|
|
411
|
+
* Clearing alone would not do: `app.start` reports the launch root at any age, so its
|
|
412
|
+
* root row would ship under the **new** session while the initial `view` it fathered
|
|
413
|
+
* shipped under the **old** one. `trace.id` would span a `session.id` (§6.6, invariant 2)
|
|
414
|
+
* and `GROUP BY rum.action.id` would never reassemble the launch envelope. Re-minting
|
|
415
|
+
* keeps the retired trace wholly inside the retired session.
|
|
416
|
+
*
|
|
417
|
+
* ⚠ The retired trace then has children and no root row — the initial `view` row is a
|
|
418
|
+
* rootless child. That is the same condition process death already produces by design
|
|
419
|
+
* (a killed view emits nothing), and it is the cheaper of the two wrongs.
|
|
420
|
+
*/
|
|
421
|
+
restartLaunchRoot() {
|
|
422
|
+
this.launch = this.mint("launch", Date.now(), this.launch.spanStartMs);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Join the live root, or mint one of `mintAs`. A span's *start* extends the root.
|
|
426
|
+
*
|
|
427
|
+
* `attribution` is read **before** `liveRoot()` drops an expired carrier, which is the
|
|
428
|
+
* only place the two unattributed causes are still distinguishable: a carrier that just
|
|
429
|
+
* aged out is `injected_expired` (context lost), no carrier at all is
|
|
430
|
+
* `injected_unattributed` (no action) — the split §6.5 asks the backend to keep.
|
|
431
|
+
*/
|
|
432
|
+
attach(e, t) {
|
|
433
|
+
const s = this.root !== void 0, r = this.liveRoot(t);
|
|
434
|
+
return r ? (r.lastTouch = t, { root: r, isRoot: !1, attribution: "injected_attributed" }) : {
|
|
435
|
+
root: this.mint(e, t, t),
|
|
436
|
+
isRoot: !0,
|
|
437
|
+
attribution: s ? "injected_expired" : "injected_unattributed"
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
mint(e, t, s) {
|
|
441
|
+
const r = {
|
|
442
|
+
traceId: W(),
|
|
443
|
+
spanId: N(),
|
|
444
|
+
rootType: e,
|
|
445
|
+
startedAt: t,
|
|
446
|
+
spanStartMs: s,
|
|
447
|
+
lastTouch: t
|
|
448
|
+
};
|
|
449
|
+
return this.root = r, r;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The carrier if it is still live, dropping it otherwise. Both boundaries are strict —
|
|
453
|
+
* a root sitting exactly on one has not crossed it — matching the session boundaries.
|
|
454
|
+
*
|
|
455
|
+
* A **negative** delta is clamped to expired (§6.7): `Date.now()` can move backwards on
|
|
456
|
+
* an NTP correction, and reading that as "fresh" would hold one root open indefinitely.
|
|
457
|
+
*/
|
|
458
|
+
liveRoot(e) {
|
|
459
|
+
const t = this.root;
|
|
460
|
+
if (!t) return;
|
|
461
|
+
const s = e - t.lastTouch, r = e - t.startedAt;
|
|
462
|
+
if (s < 0 || r < 0 || s > j || r > B) {
|
|
463
|
+
this.root = void 0;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
return t;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function l(i, e, t) {
|
|
470
|
+
const s = e ? i.spanId : N();
|
|
471
|
+
return {
|
|
472
|
+
spanId: s,
|
|
473
|
+
keys: {
|
|
474
|
+
"trace.id": i.traceId,
|
|
475
|
+
"span.id": s,
|
|
476
|
+
...e ? {} : { "parent.span.id": i.spanId },
|
|
477
|
+
"rum.action.id": i.spanId,
|
|
478
|
+
"trace.root_type": i.rootType,
|
|
479
|
+
"span.start_time": new Date(t).toISOString()
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
class J {
|
|
484
|
+
constructor(e = 20) {
|
|
485
|
+
this.max = e, this.items = [];
|
|
486
|
+
}
|
|
487
|
+
add(e) {
|
|
488
|
+
this.items.push(e), this.items.length > this.max && this.items.shift();
|
|
489
|
+
}
|
|
490
|
+
list() {
|
|
491
|
+
return [...this.items];
|
|
492
|
+
}
|
|
493
|
+
toJSON() {
|
|
494
|
+
return JSON.stringify(this.items);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const X = /* @__PURE__ */ new Set([
|
|
498
|
+
// `session.sequence` is not in the contract's §3.6 list, which names only `event.sequence`.
|
|
499
|
+
// Both are transmission ordinals with no scrubbing use case: (session.id, session.sequence)
|
|
500
|
+
// orders a session's batches (#92) and (session.id, event.sequence) is the backend's dedup
|
|
501
|
+
// key (#94, §2.4) — a hook that could delete either would break the thing it keys.
|
|
502
|
+
//
|
|
503
|
+
// `event.sequence` is stamped in `enqueue()` *after* this hook runs, so on the way in it is
|
|
504
|
+
// absent and the re-stamp is a no-op; it is listed because the hook must not be able to
|
|
505
|
+
// forge one, and because a re-run over an already-stamped event must leave it alone.
|
|
506
|
+
"session.id",
|
|
507
|
+
"session.start_time",
|
|
508
|
+
"session.sequence",
|
|
509
|
+
"event.sequence",
|
|
510
|
+
"device.platform",
|
|
511
|
+
"trace.id",
|
|
512
|
+
"span.id",
|
|
513
|
+
"parent.span.id",
|
|
514
|
+
"rum.action.id",
|
|
515
|
+
"view.id"
|
|
516
|
+
]), k = (i) => X.has(i) || i.startsWith("sdk.") || i.startsWith("app."), m = "device.id";
|
|
517
|
+
function Y(i, e) {
|
|
518
|
+
let t;
|
|
519
|
+
try {
|
|
520
|
+
t = e({ ...i, attributes: { ...i.attributes } });
|
|
521
|
+
} catch (s) {
|
|
522
|
+
return o.warn("Telemetry: beforeSend threw — event dropped", s), { kind: "failed" };
|
|
523
|
+
}
|
|
524
|
+
return t === null ? { kind: "dropped" } : typeof t != "object" ? (o.warn("Telemetry: beforeSend returned a non-event — event dropped", t), { kind: "failed" }) : { kind: "kept", event: Q(i, t) };
|
|
525
|
+
}
|
|
526
|
+
function Q(i, e) {
|
|
527
|
+
const t = {};
|
|
528
|
+
for (const [a, n] of Object.entries(e.attributes ?? {}))
|
|
529
|
+
k(a) || (t[a] = n);
|
|
530
|
+
for (const [a, n] of Object.entries(i.attributes ?? {}))
|
|
531
|
+
k(a) && (t[a] = n);
|
|
532
|
+
const s = t[m];
|
|
533
|
+
if (typeof s != "string" || s === "") {
|
|
534
|
+
const a = i.attributes?.[m];
|
|
535
|
+
a === void 0 ? delete t[m] : t[m] = a;
|
|
536
|
+
}
|
|
537
|
+
const r = {
|
|
538
|
+
type: i.type,
|
|
539
|
+
timestamp: i.timestamp,
|
|
540
|
+
attributes: t
|
|
541
|
+
};
|
|
542
|
+
return i.eventName !== void 0 && (r.eventName = i.eventName), i.metricName !== void 0 && (r.metricName = i.metricName), i.type === "metric" && (r.value = e.value ?? i.value), r;
|
|
543
|
+
}
|
|
544
|
+
function q(i) {
|
|
545
|
+
try {
|
|
546
|
+
return JSON.stringify(i) ?? void 0;
|
|
547
|
+
} catch {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const G = 255, Z = 255, ee = 50, T = 64, A = 64, p = 255;
|
|
552
|
+
let E = !1;
|
|
553
|
+
function f(i, e) {
|
|
554
|
+
if (typeof i != "string") return;
|
|
555
|
+
const t = i.slice(0, e);
|
|
556
|
+
return t.length ? t : void 0;
|
|
557
|
+
}
|
|
558
|
+
function te(i) {
|
|
559
|
+
if (i == null) return;
|
|
560
|
+
if (typeof i == "number" || typeof i == "boolean") return i;
|
|
561
|
+
if (typeof i == "string") return i.slice(0, p) || void 0;
|
|
562
|
+
const e = q(i);
|
|
563
|
+
return e === void 0 ? void 0 : e.slice(0, p) || void 0;
|
|
564
|
+
}
|
|
565
|
+
function se(i) {
|
|
566
|
+
const e = {};
|
|
567
|
+
if (!i) return e;
|
|
568
|
+
const t = f(i.fullName, G), s = f(i.email, Z), r = f(i.phone, ee);
|
|
569
|
+
t !== void 0 && (e["user.name"] = t), s !== void 0 && (e["user.email"] = s), r !== void 0 && (e["user.phone"] = r);
|
|
570
|
+
let a = 0, n = 0;
|
|
571
|
+
for (const h of Object.keys(i.customAttributes || {})) {
|
|
572
|
+
if (h.length > A || n >= T) {
|
|
573
|
+
a++;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const c = te(i.customAttributes[h]);
|
|
577
|
+
if (c === void 0) {
|
|
578
|
+
a++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
e[`user.custom.${h}`] = c, n++;
|
|
582
|
+
}
|
|
583
|
+
return a > 0 && (e["user.custom_dropped"] = a, x() && !E && (E = !0, console.warn(
|
|
584
|
+
`[edge-telemetry] identify(): ${a} custom attribute(s) dropped. user.custom.* is bounded at ${T} keys, ${A}-char keys and ${p}-char values; unserializable values are dropped.`
|
|
585
|
+
))), e;
|
|
586
|
+
}
|
|
587
|
+
const ie = "4.0.0", re = (i) => i ? `react-native-${i}` : "react-native", ne = ie, ae = 1800 * 1e3, oe = 14400 * 1e3, he = 8, ce = 500, v = (i) => i.attributes?.["event.sequence"] ?? -1, P = (i) => i === "app.crash" || i === "app.error";
|
|
588
|
+
function de(i) {
|
|
589
|
+
const e = i.findIndex((t) => t.eventName !== "app.crash");
|
|
590
|
+
return e === -1 ? 0 : e;
|
|
591
|
+
}
|
|
592
|
+
const R = "telemetry_device_id", D = "telemetry_session", ue = 255, le = /* @__PURE__ */ new Set([
|
|
593
|
+
"session.started",
|
|
594
|
+
"session.finalized",
|
|
595
|
+
"app_lifecycle",
|
|
596
|
+
"navigation",
|
|
597
|
+
"screen.duration",
|
|
598
|
+
"http.request",
|
|
599
|
+
"network_change",
|
|
600
|
+
"user.profile.update",
|
|
601
|
+
"custom_event",
|
|
602
|
+
"app.crash",
|
|
603
|
+
"view",
|
|
604
|
+
// ⚠ `app.error` needs backend allowlist sign-off before it ships (#100, §4.7).
|
|
605
|
+
"app.error",
|
|
606
|
+
// §4.6/#102 — replaces `user.interaction`, which is off the list as of v4. The
|
|
607
|
+
// `rum_ui_interactions` columns are already built; the name still needs sign-off.
|
|
608
|
+
"ui.interaction",
|
|
609
|
+
// ⚠ `app.start` needs backend allowlist sign-off before it ships (#98, §4.3/§6.2) — an
|
|
610
|
+
// unlisted eventName is dropped on ingest. It is listed here, so it is being emitted.
|
|
611
|
+
"app.start",
|
|
612
|
+
"frame_render_time",
|
|
613
|
+
"memory_usage",
|
|
614
|
+
"LCP",
|
|
615
|
+
"FCP",
|
|
616
|
+
"CLS",
|
|
617
|
+
"INP",
|
|
618
|
+
"TTFB"
|
|
619
|
+
// ⚠ `page_load`, `resource_timing` and `long_task` are **retired, not deferred**
|
|
620
|
+
// (§10.2, #109) — as `user.interaction` was in #102, four in total for a net-zero v4.
|
|
621
|
+
// Do not add them back: `page_load`'s job is done three ways over (`view` +
|
|
622
|
+
// `view.loading_time` + the vitals), `resource_timing` would blow the 500-event queue on
|
|
623
|
+
// one asset-heavy view for a namespace with no column, and `long_task`'s only APIs are
|
|
624
|
+
// Chromium-only while `frame_render_time` detects jank on every runtime. A name off this
|
|
625
|
+
// list is rewritten to `custom_event`, so all four are unreachable, not merely unused.
|
|
626
|
+
]), me = {
|
|
627
|
+
frame_render_time: "ms",
|
|
628
|
+
memory_usage: "MB",
|
|
629
|
+
LCP: "ms",
|
|
630
|
+
FCP: "ms",
|
|
631
|
+
INP: "ms",
|
|
632
|
+
TTFB: "ms",
|
|
633
|
+
CLS: "score"
|
|
634
|
+
}, fe = /* @__PURE__ */ new Set(["app.crash", "app.error", "custom_event"]);
|
|
635
|
+
function ve(i) {
|
|
636
|
+
return i === void 0 ? 1 : !Number.isFinite(i) || i < 0 || i > 1 ? (o.warn(`Telemetry: sessionSampleRate must be a number in [0,1] — got ${i}; using 1`), 1) : i;
|
|
637
|
+
}
|
|
638
|
+
class Ie {
|
|
639
|
+
constructor(e) {
|
|
640
|
+
this.queue = [], this.intervalId = null, this.userId = void 0, this.deviceIdEphemeral = !1, this.userProfile = void 0, this.eventCount = 0, this.breadcrumbs = new J(20), this.sessionSequence = 0, this.eventSequence = 0, this.sessionEventCount = 0, this.errorCount = 0, this.hookDropped = 0, this.hookFailed = 0, this.eventsDropped = 0, this.crashPersistedThrough = -1, this.appStartEmitted = !1, this.sender = e?.sender, this.batchSize = e?.batchSize ?? 50, this.flushIntervalMs = e?.flushIntervalMs ?? 3e4, this.endpoint = e?.endpoint, this.platform = e?.platform, this.store = e?.store ?? V({ unavailable: !0 }), this.beforeSend = e?.beforeSend, this.buildId = typeof e?.buildId == "string" && e.buildId !== "" ? e.buildId : void 0, this.configuredSampleRate = ve(e?.sessionSampleRate), this.sampleRate = this.configuredSampleRate, this.sampled = this.rollSample(), this.sessionId = e?.sessionId ?? this.generateSessionId(), e?.userId && this.setUserId(e.userId), this.sessionStart = Date.now(), this.sdkVersion = e?.sdkVersion ?? ne, this.sender?.replayFailed && this.sender.replayFailed().catch((t) => {
|
|
641
|
+
o.warn("Telemetry replay failed Telemetry class:", t);
|
|
642
|
+
}), this.flushIntervalMs > 0 && (this.intervalId = setInterval(() => this.flush().catch(() => {
|
|
643
|
+
}), this.flushIntervalMs)), this.navigationTracker = new F(this), this.screens = new L(this), this.deprecatedScreenFeeds = e?.deprecatedScreenFeeds ?? !0, this.trace = new K(e?.traceLaunchStart, e?.traceHostAllowlist), this.views = new $(this), this.deviceInfoHandler = e?.deviceInfoHandler ?? {
|
|
644
|
+
start: async () => Promise.resolve(),
|
|
645
|
+
collect: async () => Promise.resolve({
|
|
646
|
+
app: { name: "", version: "" },
|
|
647
|
+
device: { platform: "" }
|
|
648
|
+
})
|
|
649
|
+
}, this.networkInfoHandler = e?.networkInfoHandler ?? {
|
|
650
|
+
start: async () => Promise.resolve({}),
|
|
651
|
+
collect: async () => Promise.resolve({})
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
// ---------- Session & User APIs ----------
|
|
655
|
+
/**
|
|
656
|
+
* §4.7 — report a handled error. Accepts `unknown` on purpose: half of real `catch`
|
|
657
|
+
* blocks receive a string or an axios rejection object, and a consumer should not have
|
|
658
|
+
* to prove to TypeScript that it is an `Error` before reporting it.
|
|
659
|
+
*
|
|
660
|
+
* Emits `app.error`, **never** `app.crash`. There is deliberately no public path to
|
|
661
|
+
* `app.crash` — a consumer's own code must not be able to manufacture rows in the one
|
|
662
|
+
* table an unfiltered crash-free rate is read from.
|
|
663
|
+
*
|
|
664
|
+
* `source` is SDK-internal (`captureConsole` passes `"console"`); the public wrappers
|
|
665
|
+
* forward two arguments, so a consumer can only ever produce `"reported"`. Routing the
|
|
666
|
+
* console path through here rather than through `log()` directly is what keeps the
|
|
667
|
+
* `error.fatal` rule in one place.
|
|
668
|
+
*/
|
|
669
|
+
captureError(e, t, s = "reported") {
|
|
670
|
+
const r = this.isNativePlatform() ? { fatal: !1 } : {};
|
|
671
|
+
return this.log("app.error", { ...t, ...z(s, e, r) });
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Extend the breadcrumb trail without emitting anything. §4.7 demotes `console.warn` to
|
|
675
|
+
* exactly this — React's dev-mode warnings were the bulk of v3's `app.crash` volume.
|
|
676
|
+
*
|
|
677
|
+
* SDK-internal: `public` only so `adapters/crashCapture.ts` can reach it. Not on
|
|
678
|
+
* `TelemetryBase` and not part of the consumer API — a host app that wants a trail entry
|
|
679
|
+
* should `log()` an event, which extends the trail anyway.
|
|
680
|
+
*/
|
|
681
|
+
addBreadcrumb(e, t) {
|
|
682
|
+
this.breadcrumbs.add({ ...t, name: e, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
683
|
+
}
|
|
684
|
+
trackErrors(e, t) {
|
|
685
|
+
this.crashHandler = e, e.attach(t).catch((s) => {
|
|
686
|
+
o.warn("Telemetry crashHandler attach failed:", s);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
getDeviceInfo(e) {
|
|
690
|
+
this.deviceInfoHandler = e, e.start(this).catch((t) => {
|
|
691
|
+
o.warn("Telemetry deviceInfo tracking start failed:", t);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
getNetworkInfo(e) {
|
|
695
|
+
this.networkInfoHandler = e, e.start(this).catch((t) => {
|
|
696
|
+
o.warn("Telemetry networkInfoHandler start failed:", t);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
trackFrameDrops(e) {
|
|
700
|
+
this.frameDropsHandler = e, e.start().catch((t) => {
|
|
701
|
+
o.warn("Telemetry frameDropsHandler start failed:", t);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
trackNetworkRequests(e) {
|
|
705
|
+
this.networkHandler = e, e.start().catch((t) => {
|
|
706
|
+
o.warn("Telemetry networkHandler start failed:", t);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
trackMemoryUsage(e) {
|
|
710
|
+
this.memoryHandler?.stop?.(), this.memoryHandler = e, e.start().catch((t) => {
|
|
711
|
+
o.warn("Telemetry memoryHandler start failed:", t);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
autoTrackNavigation(e) {
|
|
715
|
+
this.navigationHandler = e, e?.start().catch((t) => {
|
|
716
|
+
o.warn("Telemetry navigationHandler start failed:", t);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* The per-session sample roll (§3.6). Never per-event: per-event sampling punches
|
|
721
|
+
* holes that desynchronise the per-view counters by a random factor per view.
|
|
722
|
+
*
|
|
723
|
+
* `Math.random()` is deliberate here where `randomHex` refuses it — this decides one
|
|
724
|
+
* boolean that dies with the session, not an id that persists forever, and a biased
|
|
725
|
+
* PRNG costs a fraction of a percent of sample accuracy rather than a permanent
|
|
726
|
+
* identity collision.
|
|
727
|
+
*/
|
|
728
|
+
rollSample() {
|
|
729
|
+
return Math.random() < this.sampleRate;
|
|
730
|
+
}
|
|
731
|
+
/** §3.3 / §4.7's platform split, from the entry's declared `platform` opt — never a global. */
|
|
732
|
+
isNativePlatform() {
|
|
733
|
+
return this.platform === "ios" || this.platform === "android";
|
|
734
|
+
}
|
|
735
|
+
generateSessionId() {
|
|
736
|
+
const e = `session_${Date.now()}_${u(16)}`;
|
|
737
|
+
return this.platform ? `${e}_${this.platform}` : e;
|
|
738
|
+
}
|
|
739
|
+
setSessionId(e) {
|
|
740
|
+
this.sessionId = e, this.sessionStart = Date.now();
|
|
741
|
+
}
|
|
742
|
+
getSessionId() {
|
|
743
|
+
return this.sessionId;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Freeze the identity a deferred row must report (§4.6). Read **synchronously at the
|
|
747
|
+
* moment the thing happened**, so a `log()` that lands after a route change or a session
|
|
748
|
+
* rotation still books the click where it occurred.
|
|
749
|
+
*/
|
|
750
|
+
snapshot(e) {
|
|
751
|
+
return {
|
|
752
|
+
at: e,
|
|
753
|
+
viewId: this.views.id,
|
|
754
|
+
sessionId: this.sessionId,
|
|
755
|
+
sessionStart: this.sessionStart
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/** Collector endpoint, so fetch/XHR adapters can skip self-capturing the SDK's own POST. */
|
|
759
|
+
getEndpoint() {
|
|
760
|
+
return this.endpoint;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Sampling stays **session-level** (§6.5): an unsampled session injects no header at
|
|
764
|
+
* all — not a `flags=00` id. Read by the interceptors, which sit outside `log()`'s own
|
|
765
|
+
* sampled-out early return and so need the decision directly.
|
|
766
|
+
*/
|
|
767
|
+
isSampled() {
|
|
768
|
+
return this.sampled;
|
|
769
|
+
}
|
|
770
|
+
// ---------- Session lifecycle (#29) ----------
|
|
771
|
+
/**
|
|
772
|
+
* The entry's one call at init (§4.2): resume the durable session when the gap is
|
|
773
|
+
* inside the idle window, otherwise close the stale one out and start fresh.
|
|
774
|
+
*
|
|
775
|
+
* Process death, tab close, hard reload and bfcache restore all land here, and all
|
|
776
|
+
* four **resume** — which is why a resumed session must not re-emit `session.started`
|
|
777
|
+
* (§4.1), or `COUNT(session.started)` stops equalling session count.
|
|
778
|
+
*/
|
|
779
|
+
async resumeOrStartSession() {
|
|
780
|
+
await this.hydrateAndAnnounce(), await this.emitAppStart();
|
|
781
|
+
}
|
|
782
|
+
async hydrateAndAnnounce() {
|
|
783
|
+
const e = await this.hydrateSession();
|
|
784
|
+
if (e.kind === "resumed") return;
|
|
785
|
+
if (e.kind === "expired") {
|
|
786
|
+
await this.rotateSession(e.reason);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (this.lastActivity === void 0) {
|
|
790
|
+
await this.startSession("launch");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const t = this.expiryReason(Date.now());
|
|
794
|
+
t && await this.rotateSession(t);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* `app.start` (§4.3/§6.2): once per process, a **root** of `trace.root_type = launch`,
|
|
798
|
+
* carrying the launch root minted in the constructor. Idempotent because the web build
|
|
799
|
+
* re-runs `resumeOrStartSession()` on a bfcache restore, which is a resumed process and
|
|
800
|
+
* not a new one.
|
|
801
|
+
*/
|
|
802
|
+
async emitAppStart() {
|
|
803
|
+
this.appStartEmitted || (this.appStartEmitted = !0, await this.log("app.start", this.trace.launchRootAttributes()));
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Read the durable record and adopt it. Adoption happens even when the session has
|
|
807
|
+
* expired: `session.finalized` has to ship under the *old* `session.id` and
|
|
808
|
+
* `session.start_time` before the fresh one takes over.
|
|
809
|
+
*/
|
|
810
|
+
async hydrateSession() {
|
|
811
|
+
const e = await this.store.get(D);
|
|
812
|
+
if (e.status === "unavailable")
|
|
813
|
+
return o.warn("Telemetry: session storage unavailable — this session cannot be resumed"), { kind: "nothing" };
|
|
814
|
+
if (e.status === "miss") return { kind: "nothing" };
|
|
815
|
+
let t;
|
|
816
|
+
try {
|
|
817
|
+
t = JSON.parse(e.value);
|
|
818
|
+
} catch {
|
|
819
|
+
return { kind: "nothing" };
|
|
820
|
+
}
|
|
821
|
+
if (typeof t?.id != "string" || typeof t.start != "number" || typeof t.lastActivity != "number") return { kind: "nothing" };
|
|
822
|
+
this.sessionId = t.id, this.sessionStart = t.start, this.lastActivity = t.lastActivity, this.sessionSequence = t.sequence ?? 0, this.eventSequence = t.eventSequence ?? 0, this.sessionEventCount = t.eventCount ?? 0, this.errorCount = t.errorCount ?? 0, typeof t.sampled == "boolean" && Number.isFinite(t.sampleRate) && t.sampleRate >= 0 && t.sampleRate <= 1 && (this.sampled = t.sampled, this.sampleRate = t.sampleRate);
|
|
823
|
+
const s = this.expiryReason(Date.now());
|
|
824
|
+
return s ? { kind: "expired", reason: s } : { kind: "resumed" };
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Which boundary, if any, the current session has crossed. Idle is checked first:
|
|
828
|
+
* when a long session has also gone quiet, the reason it ended is that the user left.
|
|
829
|
+
*
|
|
830
|
+
* Inert until `lastActivity` is set, so a bare `new Telemetry()` that never started a
|
|
831
|
+
* session never rotates one.
|
|
832
|
+
*/
|
|
833
|
+
expiryReason(e) {
|
|
834
|
+
if (this.lastActivity !== void 0) {
|
|
835
|
+
if (e - this.lastActivity > ae) return "idle";
|
|
836
|
+
if (e - this.sessionStart > oe) return "max_duration";
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Write the session through the `Store` so the next process can resume it.
|
|
841
|
+
*
|
|
842
|
+
* ponytail: one write per event. Web's is a synchronous `localStorage` write; native's
|
|
843
|
+
* is an AsyncStorage round-trip — debounce here if it ever shows up in a trace.
|
|
844
|
+
*/
|
|
845
|
+
async persistSession() {
|
|
846
|
+
const e = {
|
|
847
|
+
id: this.sessionId,
|
|
848
|
+
start: this.sessionStart,
|
|
849
|
+
lastActivity: this.lastActivity ?? this.sessionStart,
|
|
850
|
+
sequence: this.sessionSequence,
|
|
851
|
+
eventSequence: this.eventSequence,
|
|
852
|
+
eventCount: this.sessionEventCount,
|
|
853
|
+
errorCount: this.errorCount,
|
|
854
|
+
sampled: this.sampled,
|
|
855
|
+
sampleRate: this.sampleRate
|
|
856
|
+
};
|
|
857
|
+
(await this.store.set(D, JSON.stringify(e))).status === "unavailable" && o.log("Telemetry: session not persisted (store unavailable)");
|
|
858
|
+
}
|
|
859
|
+
/** Emit session.started for the current session. */
|
|
860
|
+
async startSession(e) {
|
|
861
|
+
this.lastActivity = Date.now(), await this.persistSession(), await this.log("session.started", {
|
|
862
|
+
"session.reason": e,
|
|
863
|
+
// §4.1, count only — never the hosts (#99). Ships on every session so a zero is
|
|
864
|
+
// legible as "nobody opted in" rather than as a stripped header.
|
|
865
|
+
"sdk.trace_allowlist_size": this.trace.allowlistSize()
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Finalize the current session: journey summary + sdk.error_count, then an immediate flush.
|
|
870
|
+
*
|
|
871
|
+
* The backend discards `duration_ms` and `event_count` and derives both (§4.2) — they
|
|
872
|
+
* stay on the wire because the event survives as the carrier for `session.reason` and
|
|
873
|
+
* `sdk.error_count`, neither of which is derivable. Duration is stamped from
|
|
874
|
+
* `lastActivity`, never `now`: a lazily-detected rotation must report 2 minutes of use,
|
|
875
|
+
* not the 6 idle hours that followed it.
|
|
876
|
+
*/
|
|
877
|
+
async finalizeSession(e) {
|
|
878
|
+
await this.log("session.finalized", {
|
|
879
|
+
"session.duration_ms": Math.max(0, (this.lastActivity ?? this.sessionStart) - this.sessionStart),
|
|
880
|
+
"session.event_count": this.sessionEventCount,
|
|
881
|
+
"sdk.error_count": this.errorCount,
|
|
882
|
+
"session.reason": e
|
|
883
|
+
}), await this.flush();
|
|
884
|
+
}
|
|
885
|
+
/** Begin a fresh session: new id/start, reset per-session counters, emit session.started. */
|
|
886
|
+
async newSession(e) {
|
|
887
|
+
this.sessionId = this.generateSessionId(), this.sessionStart = Date.now(), this.sessionSequence = 0, this.eventSequence = 0, this.crashPersistedThrough = -1, this.sessionEventCount = 0, this.errorCount = 0, this.sampleRate = this.configuredSampleRate, this.sampled = this.rollSample(), this.appStartEmitted ? this.trace.clear() : this.trace.restartLaunchRoot(), await this.views.beginView("session_rotation"), await this.startSession(e);
|
|
888
|
+
}
|
|
889
|
+
/** Boundary rotation: finalize the old session then start a fresh one (the pair). */
|
|
890
|
+
async rotateSession(e) {
|
|
891
|
+
await this.views.endView(), await this.finalizeSession(e), await this.newSession(e);
|
|
892
|
+
}
|
|
893
|
+
/** Truncated at source (§3.2): the collector caps at 255 and we must not be the one over. */
|
|
894
|
+
setUserId(e) {
|
|
895
|
+
this.userId = e ? e.slice(0, ue) : void 0;
|
|
896
|
+
}
|
|
897
|
+
getUserId() {
|
|
898
|
+
return this.userId;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* The SDK-owned `device.id` (§3.2/§3.3): self-minted, written through the `Store`,
|
|
902
|
+
* uninstall-scoped, stable across process restarts, and never rotated by login,
|
|
903
|
+
* logout or a profile clear.
|
|
904
|
+
*
|
|
905
|
+
* The platform suffix is the entry's (ios|android|web), not the device-info adapter's:
|
|
906
|
+
* this id is persisted forever, so it must not depend on a call that can throw on first run.
|
|
907
|
+
*
|
|
908
|
+
* `getUniqueId()` is deliberately not used — it already carries two lifetimes on RN
|
|
909
|
+
* alone (ANDROID_ID survives reinstall, identifierForVendor does not), so one identity
|
|
910
|
+
* column would mean two things.
|
|
911
|
+
*
|
|
912
|
+
* When the Store reports `unavailable` — incognito, a partitioned iframe, ITP
|
|
913
|
+
* eviction, a full disk — the id lives for one process only and `device.id_ephemeral`
|
|
914
|
+
* rides the Context block to say so, because that population is otherwise
|
|
915
|
+
* indistinguishable from real installs and reads as traffic growth.
|
|
916
|
+
*/
|
|
917
|
+
async getDeviceId() {
|
|
918
|
+
return this.deviceIdPromise ??= (async () => {
|
|
919
|
+
const e = await this.store.get(R);
|
|
920
|
+
if (e.status === "hit") return e.value;
|
|
921
|
+
const t = this.platform ? `_${this.platform}` : "", s = `device_${Date.now()}_${u(16)}${t}`, r = await this.store.set(R, s);
|
|
922
|
+
return this.deviceIdEphemeral = e.status === "unavailable" || r.status === "unavailable", s;
|
|
923
|
+
})(), this.deviceIdPromise;
|
|
924
|
+
}
|
|
925
|
+
// ---------- User Profile Management ----------
|
|
926
|
+
/**
|
|
927
|
+
* Set complete user profile information
|
|
928
|
+
*/
|
|
929
|
+
setUserProfile(e) {
|
|
930
|
+
const t = Date.now(), s = !this.userProfile || e.userId && e.userId !== this.userProfile.userId;
|
|
931
|
+
this.userProfile = {
|
|
932
|
+
...this.userProfile,
|
|
933
|
+
...e,
|
|
934
|
+
updatedAt: t,
|
|
935
|
+
...s && { createdAt: t }
|
|
936
|
+
}, e.userId ? (this.setUserId(e.userId), this.userProfile.userId = this.userId) : this.userProfile && !this.userProfile.userId && (this.userProfile.userId = this.userId), o.log("Telemetry: User profile updated", this.userProfile);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* EdgeRum-style identify(): record the host-app profile and emit the one event that
|
|
940
|
+
* carries it — `user.profile.update` (§4.10, #107). The PII is no longer on the
|
|
941
|
+
* Context block of every event.
|
|
942
|
+
*
|
|
943
|
+
* `userId` is optional and new in v4: the one call that sends a profile should also
|
|
944
|
+
* set the key that profile attaches to, since `user.id` is consumer-supplied (§3.2)
|
|
945
|
+
* and an unkeyed profile has nothing to upsert on. Omitting it leaves `user.id`
|
|
946
|
+
* exactly as it was — identify never mints one.
|
|
947
|
+
*/
|
|
948
|
+
async identify(e) {
|
|
949
|
+
e.userId !== void 0 && this.setUserId(e.userId), this.setUserProfile({
|
|
950
|
+
...e.name !== void 0 ? { fullName: e.name } : {},
|
|
951
|
+
...e.email !== void 0 ? { email: e.email } : {},
|
|
952
|
+
...e.phone !== void 0 ? { phone: e.phone } : {},
|
|
953
|
+
...e.avatar !== void 0 ? { avatar: e.avatar } : {},
|
|
954
|
+
...e.customAttributes !== void 0 ? { customAttributes: e.customAttributes } : {}
|
|
955
|
+
}), await this.log("user.profile.update", se(this.userProfile));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Set user details with individual parameters
|
|
959
|
+
*/
|
|
960
|
+
setUserDetails(e) {
|
|
961
|
+
this.setUserProfile(e);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Update specific user profile fields
|
|
965
|
+
*/
|
|
966
|
+
updateUserProfile(e) {
|
|
967
|
+
if (!this.userProfile) {
|
|
968
|
+
this.setUserProfile(e);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
this.userProfile = {
|
|
972
|
+
...this.userProfile,
|
|
973
|
+
...e,
|
|
974
|
+
updatedAt: Date.now()
|
|
975
|
+
}, o.log("Telemetry: User profile updated", this.userProfile);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Get current user profile
|
|
979
|
+
*/
|
|
980
|
+
getUserProfile() {
|
|
981
|
+
return this.userProfile;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Clear user profile data
|
|
985
|
+
*/
|
|
986
|
+
clearUserProfile() {
|
|
987
|
+
this.userProfile = void 0, this.userId = void 0, o.log("Telemetry: User profile cleared");
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Set user name (convenience method)
|
|
991
|
+
*/
|
|
992
|
+
setUserName(e, t, s) {
|
|
993
|
+
this.setUserProfile({
|
|
994
|
+
fullName: e,
|
|
995
|
+
firstName: t,
|
|
996
|
+
lastName: s
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Set user contact info (convenience method)
|
|
1001
|
+
*/
|
|
1002
|
+
setUserContact(e, t) {
|
|
1003
|
+
this.setUserProfile({
|
|
1004
|
+
email: e,
|
|
1005
|
+
phone: t
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
// ---------- Logging APIs ----------
|
|
1009
|
+
/**
|
|
1010
|
+
* Log a named event. Keeps existing signature compatibility.
|
|
1011
|
+
* Automatically attaches userId and sessionId to every queued event.
|
|
1012
|
+
*/
|
|
1013
|
+
async log(e, t, s) {
|
|
1014
|
+
this.eventCount++;
|
|
1015
|
+
let r = !1, a = !1;
|
|
1016
|
+
if (!e.startsWith("session.") && e !== "view") {
|
|
1017
|
+
const n = Date.now(), h = this.expiryReason(n);
|
|
1018
|
+
h && await this.rotateSession(h), this.lastActivity = n, this.sessionEventCount++, P(e) && this.errorCount++, r = !0;
|
|
1019
|
+
}
|
|
1020
|
+
if (this.sampled) {
|
|
1021
|
+
const n = le.has(e), h = n ? e : "custom_event", c = await this.collectContext(t);
|
|
1022
|
+
n || (c["event.name"] = e), fe.has(h) && Object.assign(c, this.trace.annotate()), h === "app.crash" ? (c["error.breadcrumbs"] = this.breadcrumbs.toJSON(), a = !0) : this.breadcrumbs.add({ name: h, timestamp: (/* @__PURE__ */ new Date()).toISOString() }), P(h) ? this.views.countError() : h === "ui.interaction" && this.views.countAction(), s && (c["view.id"] = s.viewId, c["view.name"] = this.views.nameOf(s.viewId), c["session.id"] = s.sessionId, c["session.start_time"] = new Date(s.sessionStart).toISOString()), this.enqueue({
|
|
1023
|
+
type: "event",
|
|
1024
|
+
eventName: h,
|
|
1025
|
+
timestamp: new Date(s?.at ?? Date.now()).toISOString(),
|
|
1026
|
+
attributes: c
|
|
1027
|
+
}), o.log("Telemetry queued event:", e, "Queue size:", this.queue.length), o.log("Event attributes:", c);
|
|
1028
|
+
}
|
|
1029
|
+
r && await this.persistSession(), a ? await this.flushCrash() : this.queue.length >= this.batchSize && this.flush();
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The crash path (§2 / §9.4): persist the whole queue, then send **one** batch reordered
|
|
1033
|
+
* so the crash rides in it. Not a drain — a dying process gets one round trip.
|
|
1034
|
+
*
|
|
1035
|
+
* ⚠ The web/native asymmetry is real and is not fixed here. On web the `Store` is
|
|
1036
|
+
* synchronous `localStorage`, so the persist has *landed* by the time the next line runs
|
|
1037
|
+
* and the loss window closes. On native it is an AsyncStorage round-trip that a SIGKILL
|
|
1038
|
+
* can outrun, so the window only narrows. Awaiting harder does not change that; the
|
|
1039
|
+
* asymmetry is the `Store` port's whole point (`core/store.ts`).
|
|
1040
|
+
*
|
|
1041
|
+
* The persisted copy is a safety copy, not a handoff: the queue is left intact, because
|
|
1042
|
+
* an `app.crash` is not always fatal — `window.onerror` fires and the page keeps running
|
|
1043
|
+
* — and the process usually lives on. A successful send therefore
|
|
1044
|
+
* leaves a duplicate on disk to replay next launch — which is what `event.sequence` and
|
|
1045
|
+
* the backend's `(session_id, event_sequence)` dedup exist for (§2.4).
|
|
1046
|
+
*
|
|
1047
|
+
* *One* duplicate. Each row is written at most once by this path, watermarked on
|
|
1048
|
+
* `event.sequence`: a chatty app crash-flushes often, and re-persisting the whole queue
|
|
1049
|
+
* each time would fill the store with copies of
|
|
1050
|
+
* its own backlog and book `store_full` drops that are not loss — corrupting the very
|
|
1051
|
+
* counter this change adds.
|
|
1052
|
+
*/
|
|
1053
|
+
async flushCrash() {
|
|
1054
|
+
if (!this.sender || this.queue.length === 0) return;
|
|
1055
|
+
const e = (r) => r.eventName === "app.crash";
|
|
1056
|
+
this.queue = [...this.queue.filter(e), ...this.queue.filter((r) => !e(r))];
|
|
1057
|
+
const t = this.queue.filter((r) => v(r) > this.crashPersistedThrough);
|
|
1058
|
+
if (t.length > 0 && this.sender.onFailure)
|
|
1059
|
+
try {
|
|
1060
|
+
this.recordDrop("store_full", await this.sender.onFailure(t)), this.crashPersistedThrough = Math.max(this.crashPersistedThrough, ...t.map(v));
|
|
1061
|
+
} catch (r) {
|
|
1062
|
+
o.warn("Telemetry: crash-path persist failed:", r);
|
|
1063
|
+
}
|
|
1064
|
+
const s = this.queue.splice(0, this.batchSize);
|
|
1065
|
+
try {
|
|
1066
|
+
await this.sender.send(s), await this.ackBatch();
|
|
1067
|
+
} catch (r) {
|
|
1068
|
+
const a = s.filter((n) => v(n) > this.crashPersistedThrough);
|
|
1069
|
+
a.length > 0 && this.queue.unshift(...a), o.warn("Telemetry: crash-path send failed:", r);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Build the v3 Context block that rides on every event AND metric: the flattened
|
|
1074
|
+
* device/network snapshot, the caller's data, identity + session + sdk fields, and the
|
|
1075
|
+
* user profile. Shared by log() and logMetric() so both carry the identical iOS-clean set.
|
|
1076
|
+
*/
|
|
1077
|
+
async collectContext(e) {
|
|
1078
|
+
let t = {}, s = {};
|
|
1079
|
+
try {
|
|
1080
|
+
t = await this.deviceInfoHandler?.collect() || {};
|
|
1081
|
+
} catch (n) {
|
|
1082
|
+
o.warn("Telemetry: failed to fetch device info", n);
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
s = await this.networkInfoHandler?.collect() || {};
|
|
1086
|
+
} catch (n) {
|
|
1087
|
+
o.warn("Telemetry: failed to fetch network info", n);
|
|
1088
|
+
}
|
|
1089
|
+
const r = await this.getDeviceId();
|
|
1090
|
+
return {
|
|
1091
|
+
// deviceInfo already namespaces its own keys (app.*, device.*) — flatten flat
|
|
1092
|
+
...this.flattenWithPrefix("", t),
|
|
1093
|
+
...this.flattenWithPrefix("network", s),
|
|
1094
|
+
...this.flattenWithPrefix("", e || {}),
|
|
1095
|
+
// Identity keys land after caller data: `data` may override app./device./network.*
|
|
1096
|
+
// but must never override these (§3.3).
|
|
1097
|
+
// With the identity keys, not with deviceInfo's `app.*`: it is the symbolication
|
|
1098
|
+
// join key, so a stray `log()` payload must not be able to shift which map a
|
|
1099
|
+
// crash resolves against.
|
|
1100
|
+
...this.buildId ? { "app.build_id": this.buildId } : {},
|
|
1101
|
+
"device.id": r,
|
|
1102
|
+
...this.deviceIdEphemeral ? { "device.id_ephemeral": !0 } : {},
|
|
1103
|
+
// Omitted entirely on anonymous traffic (§3.2) — no "", no placeholder.
|
|
1104
|
+
...this.userId ? { "user.id": this.userId } : {},
|
|
1105
|
+
// Denormalized onto every row (§4.5) so "errors by screen" needs no join. The id
|
|
1106
|
+
// is the join key; the name is the view's current best, resolved here at log time
|
|
1107
|
+
// by lookup on that id, so a row can never carry a name that disagrees with it.
|
|
1108
|
+
"view.id": this.views.id,
|
|
1109
|
+
"view.name": this.views.name,
|
|
1110
|
+
"session.id": this.sessionId,
|
|
1111
|
+
"session.start_time": new Date(this.sessionStart).toISOString(),
|
|
1112
|
+
"session.sequence": this.sessionSequence,
|
|
1113
|
+
// Extrapolation is arithmetic when the rate is on the row: it survives a
|
|
1114
|
+
// consumer retuning mid-quarter, which config-in-a-spreadsheet does not.
|
|
1115
|
+
"session.sample_rate": this.sampleRate,
|
|
1116
|
+
// Monotonic and always present; the reason stays omitted until there is one,
|
|
1117
|
+
// so `sdk.drop_reason IS NOT NULL` is a usable filter for "this install lost data".
|
|
1118
|
+
"sdk.events_dropped": this.eventsDropped,
|
|
1119
|
+
...this.dropReason ? { "sdk.drop_reason": this.dropReason } : {},
|
|
1120
|
+
"sdk.hook_dropped": this.hookDropped,
|
|
1121
|
+
"sdk.hook_failed": this.hookFailed,
|
|
1122
|
+
"sdk.platform": re(this.platform),
|
|
1123
|
+
"sdk.version": this.sdkVersion
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Emit a metric on the v3 `type:"metric"` path: { type, metricName, value, timestamp, attributes }.
|
|
1128
|
+
* Carries the same Context block as events. Metrics are samples, not user actions, so they
|
|
1129
|
+
* don't extend the breadcrumb trail and don't count as session activity — a periodic sampler
|
|
1130
|
+
* (memory/frames) must not keep a session alive and defeat the 30-min idle rotation.
|
|
1131
|
+
*/
|
|
1132
|
+
async logMetric(e, t, s) {
|
|
1133
|
+
this.eventCount++;
|
|
1134
|
+
const r = this.expiryReason(Date.now());
|
|
1135
|
+
if (r && await this.rotateSession(r), !this.sampled) return;
|
|
1136
|
+
const a = await this.collectContext(s), n = me[e];
|
|
1137
|
+
n && (a["metric.unit"] = n), this.enqueue({
|
|
1138
|
+
type: "metric",
|
|
1139
|
+
metricName: e,
|
|
1140
|
+
value: t,
|
|
1141
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1142
|
+
attributes: a
|
|
1143
|
+
}), this.queue.length >= this.batchSize && this.flush();
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* The single enqueue point: neither the sample decision nor `beforeSend` can be
|
|
1147
|
+
* bypassed by a future emit path, and the hook runs *before* the queue — which is
|
|
1148
|
+
* what keeps scrubbed fields off disk when a send fails and the batch is persisted.
|
|
1149
|
+
*
|
|
1150
|
+
* The sample check is repeated by log()/logMetric() ahead of `collectContext()`, so
|
|
1151
|
+
* a sampled-out session does no work per event; this one is the backstop that makes
|
|
1152
|
+
* the guarantee structural rather than a convention two call sites happen to follow.
|
|
1153
|
+
*
|
|
1154
|
+
* The two counters are separate on purpose: "my volume is down 40%" has to
|
|
1155
|
+
* distinguish *my rule is too broad* from *my rule is crashing*, and one merged
|
|
1156
|
+
* counter answers neither.
|
|
1157
|
+
*/
|
|
1158
|
+
enqueue(e) {
|
|
1159
|
+
if (!this.sampled) return;
|
|
1160
|
+
let t = e;
|
|
1161
|
+
if (this.beforeSend) {
|
|
1162
|
+
const s = Y(e, this.beforeSend);
|
|
1163
|
+
if (s.kind === "failed") {
|
|
1164
|
+
this.hookFailed++;
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (s.kind === "dropped") {
|
|
1168
|
+
this.hookDropped++;
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
t = s.event;
|
|
1172
|
+
}
|
|
1173
|
+
(t.attributes ??= {})["event.sequence"] = this.eventSequence++, this.queue.push(t), this.capQueue();
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Hold the in-memory queue at its cap (§9.4), booking what it costs.
|
|
1177
|
+
*
|
|
1178
|
+
* The count lands on the *next* event's Context block, not this one's — `collectContext()`
|
|
1179
|
+
* has already run by the time we get here. That is §3.7's stated behaviour, not a bug.
|
|
1180
|
+
*/
|
|
1181
|
+
capQueue() {
|
|
1182
|
+
for (; this.queue.length > ce; )
|
|
1183
|
+
this.queue.splice(de(this.queue), 1), this.recordDrop("queue_full");
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Book dropped rows against the monotonic counter and the reason that shipped last.
|
|
1187
|
+
* Takes `Sender.onFailure`'s return shape as-is — a sender with no cap returns nothing.
|
|
1188
|
+
*/
|
|
1189
|
+
recordDrop(e, t = 1) {
|
|
1190
|
+
!t || t <= 0 || (this.eventsDropped += t, this.dropReason = e);
|
|
1191
|
+
}
|
|
1192
|
+
flattenWithPrefix(e, t, s = 0) {
|
|
1193
|
+
const r = {};
|
|
1194
|
+
for (const a in t) {
|
|
1195
|
+
if (!Object.prototype.hasOwnProperty.call(t, a)) continue;
|
|
1196
|
+
const n = t[a], h = e ? `${e}.${a}` : a;
|
|
1197
|
+
if (n !== void 0)
|
|
1198
|
+
if (typeof n == "object" && n !== null && !Array.isArray(n) && s < he)
|
|
1199
|
+
Object.assign(r, this.flattenWithPrefix(h, n, s + 1));
|
|
1200
|
+
else if (typeof n == "object" && n !== null) {
|
|
1201
|
+
const c = q(n);
|
|
1202
|
+
c !== void 0 && (r[h] = c);
|
|
1203
|
+
} else
|
|
1204
|
+
r[h] = n;
|
|
1205
|
+
}
|
|
1206
|
+
return r;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Explicit metric helper (optional convenience) — routes through the v3 metric path.
|
|
1210
|
+
*/
|
|
1211
|
+
recordMetric(e, t, s) {
|
|
1212
|
+
return this.logMetric(e, t, s);
|
|
1213
|
+
}
|
|
1214
|
+
// ---------- Flush / Persistence ----------
|
|
1215
|
+
// Retry + backoff live in the Sender (see sendWithRetry in web/native senders),
|
|
1216
|
+
// which is also used by the standalone replay paths. flush() does a single send
|
|
1217
|
+
// and, on failure, hands the batch to the sender to persist (or requeues it).
|
|
1218
|
+
async flush() {
|
|
1219
|
+
if (!this.sender || this.queue.length === 0) return;
|
|
1220
|
+
const e = this.queue.splice(0, this.batchSize);
|
|
1221
|
+
try {
|
|
1222
|
+
await this.sender.send(e), await this.ackBatch();
|
|
1223
|
+
} catch (t) {
|
|
1224
|
+
if (this.sender.onFailure)
|
|
1225
|
+
try {
|
|
1226
|
+
this.recordDrop("store_full", await this.sender.onFailure(e));
|
|
1227
|
+
} catch (s) {
|
|
1228
|
+
this.queue.unshift(...e), o.warn("Sender.onFailure failed, requeued events:", s);
|
|
1229
|
+
}
|
|
1230
|
+
else
|
|
1231
|
+
this.queue.unshift(...e);
|
|
1232
|
+
throw o.error("Telemetry flush failed:", t), t;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Book an acknowledged (2xx) batch. `session.sequence` orders a session's batches (#29),
|
|
1237
|
+
* and it is persisted because a resumed session restarting at 0 would emit duplicate
|
|
1238
|
+
* (session.id, session.sequence) pairs and stop ordering anything (#92).
|
|
1239
|
+
*/
|
|
1240
|
+
async ackBatch() {
|
|
1241
|
+
this.sessionSequence++, await this.persistSession();
|
|
1242
|
+
}
|
|
1243
|
+
getQueue() {
|
|
1244
|
+
return [...this.queue];
|
|
1245
|
+
}
|
|
1246
|
+
async shutdown() {
|
|
1247
|
+
this.intervalId && clearInterval(this.intervalId), this.memoryHandler?.stop?.(), await this.flush();
|
|
1248
|
+
}
|
|
1249
|
+
// Screen tracking lives in ScreenTimingTracker (this.screens), the single timed API.
|
|
1250
|
+
// Expose some internal counters (optional)
|
|
1251
|
+
getEventCount() {
|
|
1252
|
+
return this.eventCount;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* A route change: the view boundary (§4.5) plus, on native only, the two deprecated
|
|
1256
|
+
* feeds (§4.11). v3's two native screen paths were disjoint — `attachNavigation` never
|
|
1257
|
+
* touched `inst.screens`, so a React Navigation consumer emitted `navigation` on every
|
|
1258
|
+
* route change and never a single `screen.duration`. Unifying them here is what fixes
|
|
1259
|
+
* that, which is why a *deprecated* event starts firing where it never has.
|
|
1260
|
+
*/
|
|
1261
|
+
async recordRouteChange(e, t) {
|
|
1262
|
+
this.currentScreen = t, this.deprecatedScreenFeeds && (await this.navigationTracker?.recordRouteChange(e, t), await this.screens.endScreen(e), this.screens.markStart(t)), await this.enterView(t, "route");
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Feed the name ladder (§4.5.1). Rungs 1 and 2 arrive unnormalized; rung 3's caller
|
|
1266
|
+
* normalizes before calling. Whether this re-stamps the current view or mints a
|
|
1267
|
+
* successor is the ladder's decision, not the caller's.
|
|
1268
|
+
*/
|
|
1269
|
+
async enterView(e, t) {
|
|
1270
|
+
await this.views.navigate(e, t);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
export {
|
|
1274
|
+
R as DEVICE_ID_KEY,
|
|
1275
|
+
D as SESSION_KEY,
|
|
1276
|
+
Ie as Telemetry,
|
|
1277
|
+
de as evictIndex,
|
|
1278
|
+
P as isErrorName
|
|
1279
|
+
};
|
|
1280
|
+
//# sourceMappingURL=telemetry-ByDJIz_9.js.map
|