@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
package/README.md
CHANGED
|
@@ -4,47 +4,66 @@
|
|
|
4
4
|
[](https://reactnative.dev/)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
backend — the same backend the web, Android (Ionic) and iOS SDKs feed.
|
|
7
|
+
Real User Monitoring for **React Native** and **React Native Web**. Add it once and it reports
|
|
8
|
+
what your users actually experience — crashes, slow screens, failing requests, dead taps — to
|
|
9
|
+
your EdgeTelemetryProcessor collector.
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
You import from one package name. The bundler picks the native or the web build for you.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createTelemetry } from "@nathanclaire/edge-telemetry-sdk";
|
|
15
|
+
|
|
16
|
+
const telemetry = createTelemetry({
|
|
17
|
+
apiKey: "edge_xxxxxxxx",
|
|
18
|
+
endpoint: "https://collector.example.com/telemetry",
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That's the whole setup. Crash capture, HTTP interception, screen tracking, frame and memory
|
|
23
|
+
sampling and session management all start on their own — there is nothing else to wire up.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
| | Minimum |
|
|
28
|
+
|---|---|
|
|
29
|
+
| React Native | 0.64 |
|
|
30
|
+
| React / React DOM | 17 |
|
|
31
|
+
| Platforms | iOS, Android, and any modern browser via React Native Web |
|
|
32
|
+
| TypeScript | Optional — types ship with the package |
|
|
14
33
|
|
|
15
34
|
---
|
|
16
35
|
|
|
17
36
|
## Contents
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
**Getting started**
|
|
20
39
|
- [Install](#install)
|
|
21
40
|
- [Quick start](#quick-start)
|
|
41
|
+
- [Verify it's working](#verify-its-working)
|
|
22
42
|
- [Configuration](#configuration)
|
|
43
|
+
|
|
44
|
+
**How-to guides**
|
|
45
|
+
- [Send a custom event](#send-a-custom-event)
|
|
46
|
+
- [Identify the signed-in user](#identify-the-signed-in-user)
|
|
47
|
+
- [Track screens](#track-screens)
|
|
48
|
+
- [Track taps](#track-taps)
|
|
49
|
+
- [Report handled errors](#report-handled-errors)
|
|
50
|
+
- [Make crash stacks readable](#make-crash-stacks-readable)
|
|
51
|
+
- [Strip PII before it leaves the device](#strip-pii-before-it-leaves-the-device)
|
|
52
|
+
- [Sample a percentage of sessions](#sample-a-percentage-of-sessions)
|
|
53
|
+
- [Connect your traces to your backend](#connect-your-traces-to-your-backend)
|
|
54
|
+
|
|
55
|
+
**Reference**
|
|
56
|
+
- [What the SDK captures](#what-the-sdk-captures)
|
|
23
57
|
- [API](#api)
|
|
24
|
-
- [
|
|
58
|
+
- [The Context block](#the-context-block)
|
|
25
59
|
- [Wire format](#wire-format)
|
|
26
|
-
- [Identity
|
|
27
|
-
- [
|
|
28
|
-
- [Debugging](#debugging)
|
|
29
|
-
- [Development](#development)
|
|
30
|
-
|
|
31
|
-
---
|
|
60
|
+
- [Identity and IDs](#identity-and-ids)
|
|
61
|
+
- [Delivery and reliability](#delivery-and-reliability)
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `createTelemetry({ apiKey, endpoint, ... })` — **`apiKey` is now required** (must start with
|
|
39
|
-
`edge_`; sent as `X-API-Key`; init throws otherwise).
|
|
40
|
-
- The POST body is the `telemetry_batch` envelope; timestamps are ISO-8601; device/network data
|
|
41
|
-
rides as a Context block on **every** event (the standalone `device_info`/`network_info`
|
|
42
|
-
events are gone).
|
|
43
|
-
- Several event names changed and only the allowlist ships (`network_request` → `http.request`,
|
|
44
|
-
`screen_view` → `navigation`, …); other `log()` names arrive as `custom_event`.
|
|
45
|
-
- New `debug` option (default `false`) silences all SDK console output.
|
|
46
|
-
|
|
47
|
-
Full breaking-change list and migration checklist: **[CHANGELOG.md](./CHANGELOG.md)**.
|
|
63
|
+
**Help**
|
|
64
|
+
- [Troubleshooting](#troubleshooting)
|
|
65
|
+
- [Upgrading](#upgrading)
|
|
66
|
+
- [Development](#development)
|
|
48
67
|
|
|
49
68
|
---
|
|
50
69
|
|
|
@@ -54,279 +73,895 @@ Full breaking-change list and migration checklist: **[CHANGELOG.md](./CHANGELOG.
|
|
|
54
73
|
npm install @nathanclaire/edge-telemetry-sdk
|
|
55
74
|
```
|
|
56
75
|
|
|
57
|
-
### React Native
|
|
76
|
+
### React Native — one extra step
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
The native modules the SDK reads device and network state from ship as dependencies, so npm
|
|
79
|
+
installs them for you. On iOS, link them:
|
|
60
80
|
|
|
61
81
|
```bash
|
|
62
|
-
npm install @react-native-async-storage/async-storage react-native-device-info @react-native-community/netinfo
|
|
63
82
|
cd ios && pod install
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
|
|
|
67
|
-
|
|
68
|
-
| `@react-native-async-storage/async-storage` |
|
|
69
|
-
|
|
|
70
|
-
|
|
|
85
|
+
| Module | What it provides | If it's missing |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `@react-native-async-storage/async-storage` | Saves failed batches for replay | Telemetry is lost on a network outage instead of retried |
|
|
88
|
+
| `@react-native-community/netinfo` | `network.*` context, `network_change` events | No connectivity data |
|
|
89
|
+
| `react-native-device-info` | `device.*` detail, the `memory_usage` metric | Degrades gracefully — this one is an **optional** peer |
|
|
90
|
+
|
|
91
|
+
`react` and `react-dom` (>= 17) are required peers and will already be in your app.
|
|
92
|
+
|
|
93
|
+
### React Native Web
|
|
71
94
|
|
|
72
|
-
The
|
|
95
|
+
The web build uses `fetch`, `localStorage` and `navigator` only. Nothing to link, nothing to
|
|
96
|
+
configure.
|
|
73
97
|
|
|
74
98
|
---
|
|
75
99
|
|
|
76
100
|
## Quick start
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
**Every public method is `async`** (each awaits a lazily-built core), so `await` your calls.
|
|
80
|
-
|
|
81
|
-
### React Native
|
|
102
|
+
Create the instance once, as early in your app's startup as you can, and export it.
|
|
82
103
|
|
|
83
104
|
```typescript
|
|
105
|
+
// telemetry.ts
|
|
84
106
|
import { createTelemetry } from "@nathanclaire/edge-telemetry-sdk";
|
|
85
107
|
|
|
86
|
-
const telemetry = createTelemetry({
|
|
87
|
-
apiKey: "edge_xxxxxxxx",
|
|
88
|
-
endpoint: "https://collector.example.com/
|
|
89
|
-
|
|
90
|
-
flushIntervalMs: 10000,
|
|
108
|
+
export const telemetry = createTelemetry({
|
|
109
|
+
apiKey: "edge_xxxxxxxx", // required, must start with "edge_"
|
|
110
|
+
endpoint: "https://collector.example.com/telemetry", // your collector's POST URL
|
|
111
|
+
buildId: process.env.GIT_SHA, // optional, makes crashes symbolicatable
|
|
91
112
|
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// App.tsx
|
|
117
|
+
import { telemetry } from "./telemetry";
|
|
92
118
|
|
|
93
119
|
await telemetry.log("checkout_started", { cart_value: 42 });
|
|
94
120
|
```
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
**Create it early.** Everything before `createTelemetry()` runs is invisible to the SDK — a crash
|
|
123
|
+
during startup, the first screen's load time, the requests your splash screen fires. The first
|
|
124
|
+
line of your entry file is the right place.
|
|
125
|
+
|
|
126
|
+
> **Every public method returns a Promise.** Each one waits on a lazily-built core, so `await`
|
|
127
|
+
> your calls — or at minimum attach a `.catch()`. They never throw at you; the SDK swallows its
|
|
128
|
+
> own failures rather than taking your app down with it.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Verify it's working
|
|
133
|
+
|
|
134
|
+
Turn on debug logging, send one event, and force a flush:
|
|
97
135
|
|
|
98
136
|
```typescript
|
|
99
|
-
|
|
137
|
+
const telemetry = createTelemetry({
|
|
138
|
+
apiKey: "edge_xxxxxxxx",
|
|
139
|
+
endpoint: "https://collector.example.com/telemetry",
|
|
140
|
+
debug: true, // routes the SDK's internal diagnostics to the console
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await telemetry.log("hello_world");
|
|
144
|
+
await telemetry.flush(); // don't wait for the 30-second timer
|
|
145
|
+
```
|
|
100
146
|
|
|
147
|
+
You should see the batch POST in your console and a `200` from the collector. If you don't, start
|
|
148
|
+
at [Troubleshooting](#troubleshooting).
|
|
149
|
+
|
|
150
|
+
**The SDK is silent by default.** With `debug` off — the default — it writes nothing to your
|
|
151
|
+
console, including when its own sends fail. That is deliberate: a monitoring tool should not be
|
|
152
|
+
the noisiest thing in your log. Turn it on while integrating, then turn it off.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
Everything is passed to `createTelemetry()`. There are no runtime setters for the options marked
|
|
158
|
+
**constructor-only** — see the note below the table.
|
|
159
|
+
|
|
160
|
+
| Option | Type | Default | What it does |
|
|
161
|
+
|---|---|---|---|
|
|
162
|
+
| `apiKey` | `string` | — | **Required.** Must start with `edge_`. Sent as both `X-API-Key` and `Authorization: Bearer`. |
|
|
163
|
+
| `endpoint` | `string` | placeholder | Your collector's full POST URL. **Always set this** — the default resolves nowhere. |
|
|
164
|
+
| `batchSize` | `number` | `50` | Events per flush. |
|
|
165
|
+
| `flushIntervalMs` | `number` | `30000` | How often to flush. `<= 0` disables the timer. |
|
|
166
|
+
| `captureConsole` | `boolean` | `false` | Turns `console.error` into `app.error` and `console.warn` into a breadcrumb. |
|
|
167
|
+
| `debug` | `boolean` | `false` | Prints the SDK's internal diagnostics. Off means completely silent. |
|
|
168
|
+
| `buildId` | `string` | omitted | Your build's identifier, for [symbolicating crashes](#make-crash-stacks-readable). |
|
|
169
|
+
| `beforeSend` | `(event) => event \| null` | none | [Scrubbing hook](#strip-pii-before-it-leaves-the-device). Constructor-only. |
|
|
170
|
+
| `sessionSampleRate` | `number` | `1` | [Fraction of sessions to keep](#sample-a-percentage-of-sessions), `0`–`1`. Constructor-only. |
|
|
171
|
+
| `traceHostAllowlist` | `string[]` | `[]` (empty) | [Hosts allowed to receive `traceparent`](#connect-your-traces-to-your-backend). Constructor-only. |
|
|
172
|
+
| `store` | `Store` | platform default | Replaces `localStorage` / `AsyncStorage`. Mostly for tests. |
|
|
173
|
+
| `sender` | `Sender` | platform default | Replaces the HTTP transport. Mostly for tests. |
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
101
176
|
const telemetry = createTelemetry({
|
|
102
177
|
apiKey: "edge_xxxxxxxx",
|
|
103
|
-
endpoint: "https://collector.example.com/
|
|
104
|
-
batchSize:
|
|
105
|
-
flushIntervalMs:
|
|
178
|
+
endpoint: "https://collector.example.com/telemetry",
|
|
179
|
+
batchSize: 50,
|
|
180
|
+
flushIntervalMs: 30_000,
|
|
181
|
+
buildId: process.env.GIT_SHA,
|
|
182
|
+
sessionSampleRate: 1,
|
|
183
|
+
traceHostAllowlist: ["api.example.com"],
|
|
106
184
|
});
|
|
185
|
+
```
|
|
107
186
|
|
|
108
|
-
|
|
187
|
+
**Why some options are constructor-only.** `beforeSend`, `sessionSampleRate` and
|
|
188
|
+
`traceHostAllowlist` cannot be set after init, because the gap between the two is not empty: by
|
|
189
|
+
the time you called a setter, `session.started`, the launch trace root and your first HTTP
|
|
190
|
+
requests are already queued. A scrubber that misses the first events is not a scrubber.
|
|
191
|
+
|
|
192
|
+
### Two things to get right
|
|
193
|
+
|
|
194
|
+
**`apiKey` is validated immediately.** A key that doesn't start with `edge_` throws from
|
|
195
|
+
`createTelemetry()`, so a typo fails at startup instead of silently dropping data for a week. The
|
|
196
|
+
value may be an API key *or* an `edge_`-prefixed JWT; the collector accepts both. You never send a
|
|
197
|
+
tenant ID — the backend resolves your tenant from the key.
|
|
198
|
+
|
|
199
|
+
**`endpoint` is used verbatim.** The collector terminates `POST /telemetry`. If you leave it
|
|
200
|
+
unset, every send fails against a placeholder host.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Send a custom event
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
await telemetry.log("checkout_started", { cart_value: 42, currency: "KES" });
|
|
109
208
|
```
|
|
110
209
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
210
|
+
Your `data` is flattened into dot-notation attributes and must hold **primitive values** — string,
|
|
211
|
+
number, boolean. Objects are flattened; arrays and anything nested deeper than 8 levels are
|
|
212
|
+
JSON-stringified.
|
|
213
|
+
|
|
214
|
+
> **Names outside the allowlist still arrive.** Only [allowlisted event names](#what-the-sdk-captures)
|
|
215
|
+
> travel under their own name. Any other name — `checkout_started` above — is sent as
|
|
216
|
+
> `custom_event` with your name preserved in `attributes["event.name"]`. Nothing is dropped, so
|
|
217
|
+
> query custom events by `event.name`, not by `event_name`.
|
|
114
218
|
|
|
115
219
|
---
|
|
116
220
|
|
|
117
|
-
##
|
|
221
|
+
## Identify the signed-in user
|
|
118
222
|
|
|
119
223
|
```typescript
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
sender?: Sender; // override the transport (mainly for tests)
|
|
128
|
-
};
|
|
224
|
+
await telemetry.identify({
|
|
225
|
+
userId: "u-42",
|
|
226
|
+
name: "Ada Lovelace",
|
|
227
|
+
email: "ada@example.com",
|
|
228
|
+
phone: "+254700000000",
|
|
229
|
+
customAttributes: { plan: "pro", org: "acme" },
|
|
230
|
+
});
|
|
129
231
|
```
|
|
130
232
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
233
|
+
`identify()` does two things: it sets `user.id` on every subsequent event, and it emits one
|
|
234
|
+
`user.profile.update` event carrying the profile.
|
|
235
|
+
|
|
236
|
+
> **Warning — if you are upgrading from v3, read this.** `setUserProfile()`, `setUserDetails()`,
|
|
237
|
+
> `updateUserProfile()`, `setUserName()` and `setUserContact()` now record state **only**. They no
|
|
238
|
+
> longer put anything on the wire. If your app used one of them and never called `identify()`,
|
|
239
|
+
> your profile data stopped arriving at the v4 upgrade. Add one `identify()` call.
|
|
240
|
+
|
|
241
|
+
**Why the change:** `user.name`, `user.email` and `user.phone` used to ride the Context block of
|
|
242
|
+
*every* event, so a 10,000-event session put 10,000 copies of an email address on the wire and at
|
|
243
|
+
rest, to populate a table that needs it once. They now ride `user.profile.update` and no other
|
|
244
|
+
event. Because the profile is held in memory, `identify()` re-fires on every launch and the table
|
|
245
|
+
stays current.
|
|
246
|
+
|
|
247
|
+
**Limits.** `name` and `email` truncate at 255 characters, `phone` at **50** (the column is
|
|
248
|
+
`VARCHAR(50)`, and an over-long value loses the whole profile behind a `200`).
|
|
249
|
+
`customAttributes` is capped at 64 keys, 64-character keys and 255-character values;
|
|
250
|
+
non-primitive values are JSON-stringified then truncated. Anything dropped is counted in
|
|
251
|
+
`user.custom_dropped` and warned about once, in development. A bad payload is never a throw.
|
|
252
|
+
|
|
253
|
+
**`user.id` is yours.** The SDK never mints one. Omit `userId` and your traffic stays anonymous —
|
|
254
|
+
still fully joinable on `device.id`, just not attributed to a person.
|
|
135
255
|
|
|
136
256
|
---
|
|
137
257
|
|
|
138
|
-
##
|
|
258
|
+
## Track screens
|
|
259
|
+
|
|
260
|
+
The SDK opens a **view** at startup and closes it at each of four boundaries: a route change,
|
|
261
|
+
backgrounding, a session rotation, and process death. Each closed view emits one `view` event
|
|
262
|
+
with its name, dwell time, request count and load time.
|
|
263
|
+
|
|
264
|
+
### React Navigation (recommended, both platforms)
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
|
|
268
|
+
import { telemetry } from "./telemetry";
|
|
269
|
+
|
|
270
|
+
export default function App() {
|
|
271
|
+
const navRef = useNavigationContainerRef();
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<NavigationContainer
|
|
275
|
+
ref={navRef}
|
|
276
|
+
onReady={() => { telemetry.attachNavigation(navRef); }}
|
|
277
|
+
>
|
|
278
|
+
{/* your screens */}
|
|
279
|
+
</NavigationContainer>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
139
283
|
|
|
140
|
-
|
|
284
|
+
One call covers native **and** web — `getCurrentRoute()` is a navigation-tree API, not a native
|
|
285
|
+
one.
|
|
141
286
|
|
|
142
|
-
###
|
|
287
|
+
### Naming screens manually
|
|
143
288
|
|
|
144
289
|
```typescript
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
290
|
+
await telemetry.screenStart("Checkout"); // native only
|
|
291
|
+
await telemetry.screenEnd("Checkout"); // native only
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
> **Warning — don't mix `screenStart()` with `attachNavigation()`.** Screen names have ranks:
|
|
295
|
+
> an explicit name outranks a route name, which outranks a URL. A lower-ranked name never
|
|
296
|
+
> overwrites a higher-ranked one — so a single `screenStart()` call pins the view to that name
|
|
297
|
+
> and later route changes stop opening new views. Pick one approach per screen.
|
|
298
|
+
|
|
299
|
+
### Reading the results
|
|
300
|
+
|
|
301
|
+
`view` events carry `view.loading_time`, measured as **network settle**: the point where the
|
|
302
|
+
screen's in-flight requests go quiet for a second. It is deliberately `null`, never `0`, for a
|
|
303
|
+
screen that fetched nothing — a `0` would make your p75 track the cache-hit rate, so a backend
|
|
304
|
+
caching win would show up as a frontend regression.
|
|
305
|
+
|
|
306
|
+
`view.loading_time_outcome` always ships and tells the nulls apart:
|
|
307
|
+
|
|
308
|
+
| Outcome | Meaning |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `settled` | Requests finished. `view.loading_time` has a number. |
|
|
311
|
+
| `no_activity` | The screen started no requests. Null. |
|
|
312
|
+
| `capped` | Still loading after 30 seconds. Null — **not** the cap, so 30s and 90s stay distinguishable. |
|
|
313
|
+
| `abandoned` | The user left while it was still loading. Null. |
|
|
314
|
+
|
|
315
|
+
Query **p75 where `outcome = 'settled'`**, alongside **% `capped`**. A plain
|
|
316
|
+
`AVG(loading_time)` averages four different populations.
|
|
317
|
+
|
|
318
|
+
> One screen visit can produce several `view` rows — backgrounding and returning splits it. Sum by
|
|
319
|
+
> `view.name`.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
## Track taps
|
|
323
|
+
|
|
324
|
+
### Web — automatic
|
|
325
|
+
|
|
326
|
+
Every click emits a `ui.interaction` event. No wiring: the SDK attaches one capture-phase
|
|
327
|
+
listener to `document`.
|
|
328
|
+
|
|
329
|
+
**Names are read only from elements that are actionable by role** — `<button>`, `<a href>`,
|
|
330
|
+
`<input type="submit|button|reset">`, `<summary>`, `<option>`, or an explicit
|
|
331
|
+
`role="button|link|tab|checkbox|radio|switch|menuitem|option"`. That gate is what stops a
|
|
332
|
+
clickable `<div>` full of customer data from being auto-named. On React Native Web, a `Pressable`
|
|
333
|
+
lands on a role-bearing element automatically.
|
|
334
|
+
|
|
335
|
+
To name anything yourself — including a role-less element — use `data-edge-action-name`:
|
|
336
|
+
|
|
337
|
+
```html
|
|
338
|
+
<div data-edge-action-name="Checkout — Step 2 of 3">…</div>
|
|
148
339
|
```
|
|
149
340
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
341
|
+
It wins over everything else and ships **exactly as written**, uncapped and unnormalized, because
|
|
342
|
+
you should be able to predict the value you just set. Below it the SDK tries `data-testid`,
|
|
343
|
+
`aria-label`, `title`, then `textContent` — each lowercased, with non-alphanumerics folded to `_`
|
|
344
|
+
and capped at 64 characters, so `"Add to cart!"` becomes `add_to_cart`.
|
|
345
|
+
|
|
346
|
+
When nothing matched, `ui.target` tells you why:
|
|
347
|
+
|
|
348
|
+
| The click landed on | `ui.target` | Read it as |
|
|
349
|
+
|---|---|---|
|
|
350
|
+
| A role-bearing element, a name was found | the name | — |
|
|
351
|
+
| A role-bearing element, nothing readable | `unnamed` | An instrumentation gap. Add `data-edge-action-name`. |
|
|
352
|
+
| A role-less element with `cursor: pointer` | `unnamed` | Same gap. |
|
|
353
|
+
| A role-less element, default cursor | `surface` | Someone tapped padding. Nothing to fix. |
|
|
354
|
+
|
|
355
|
+
Two frustration signals ride along automatically: **`ui.rage`** (3+ clicks within a second on the
|
|
356
|
+
same element) and **`ui.dead`** (no DOM change, no request and no navigation within a second of an
|
|
357
|
+
actionable click).
|
|
358
|
+
|
|
359
|
+
> **Warning — the role gate is not a privacy control.** `<button>Delete John Kamau</button>` still
|
|
360
|
+
> ships that text as the event name. Use [`beforeSend`](#strip-pii-before-it-leaves-the-device)
|
|
361
|
+
> to scrub it. There is deliberately no per-element masking attribute.
|
|
153
362
|
|
|
154
|
-
###
|
|
363
|
+
### Native — name the taps you care about
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
<Pressable
|
|
367
|
+
onPress={() => {
|
|
368
|
+
telemetry.trackTap("checkout");
|
|
369
|
+
navigation.navigate("Cart");
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Native taps are explicit-only. React Native gives a press handler a numeric node tag with no
|
|
375
|
+
public API to resolve it, so a root-level listener genuinely cannot tell a button from padding —
|
|
376
|
+
anything auto-derived would be wrong, or a PII leak. Name the controls that matter.
|
|
377
|
+
|
|
378
|
+
The name ships uncapped and unnormalized, exactly as you wrote it.
|
|
379
|
+
|
|
380
|
+
> **Your interaction coverage equals your instrumentation coverage.** An uninstrumented screen
|
|
381
|
+
> looks identical to an unused one. `ui.rage` on native is gated to named taps too, so a low rage
|
|
382
|
+
> count means *few named taps*, not happy users. And `ui.dead` never appears on native at all —
|
|
383
|
+
> there is no DOM, so there is no mutation signal — so any dead-click rate must filter to web.
|
|
384
|
+
|
|
385
|
+
**The tap's identity is captured at the tap, not at send.** A tap that navigates is still
|
|
386
|
+
attributed to the screen it happened on, not the one it opened.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Report handled errors
|
|
391
|
+
|
|
392
|
+
Unhandled errors are captured for you. For the ones you catch, call `captureError()`:
|
|
155
393
|
|
|
156
394
|
```typescript
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
395
|
+
try {
|
|
396
|
+
await pay();
|
|
397
|
+
} catch (err) {
|
|
398
|
+
await telemetry.captureError(err, { "checkout.step": "pay" });
|
|
399
|
+
}
|
|
400
|
+
```
|
|
162
401
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
402
|
+
It takes `unknown` on purpose — real `catch` blocks receive strings and axios rejection objects,
|
|
403
|
+
and you shouldn't have to prove to TypeScript that it's an `Error` first.
|
|
404
|
+
|
|
405
|
+
### Two event names, and why it matters
|
|
406
|
+
|
|
407
|
+
| | `app.crash` | `app.error` |
|
|
408
|
+
|---|---|---|
|
|
409
|
+
| Means | Unhandled | Handled |
|
|
410
|
+
| Comes from | `ErrorUtils`, `window.onerror`, unhandled rejections | `captureError()`, opted-in `console.error` |
|
|
411
|
+
| Carries breadcrumbs | Yes — the last 20 actions | No |
|
|
412
|
+
|
|
413
|
+
The split exists for one query: crash-free rate is
|
|
414
|
+
`COUNT(event_name = 'app.crash') / sessions`, with no `WHERE` clause anyone can forget.
|
|
415
|
+
|
|
416
|
+
> **`app.crash` is SDK-owned.** `log("app.crash", …)` is routed to `app.error` rather than
|
|
417
|
+
> manufacturing a crash row. A reported error is data — it just isn't a crash.
|
|
418
|
+
|
|
419
|
+
> **Warning — `error.fatal` is a trap on web.** Nothing is fatal in a browser: `window.onerror`
|
|
420
|
+
> fires and the page keeps running, so `error.fatal` is native-only. A cross-platform
|
|
421
|
+
> `1 − COUNT(fatal = true) / sessions` scores web at 100% forever. Use the unfiltered
|
|
422
|
+
> `COUNT(app.crash)`, or exclude web.
|
|
423
|
+
|
|
424
|
+
### Capturing console output
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
createTelemetry({ apiKey: "edge_xxxxxxxx", endpoint: "…", captureConsole: true });
|
|
172
428
|
```
|
|
173
429
|
|
|
430
|
+
Off by default. With it on, `console.error` becomes an `app.error` and `console.warn` becomes a
|
|
431
|
+
breadcrumb. It defaults off because React's own development-mode warnings were the single largest
|
|
432
|
+
contributor to v3 crash counts, which made "is this release crashing more?" unanswerable.
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Make crash stacks readable
|
|
437
|
+
|
|
438
|
+
React Native ships every crash stack minified, so each frame reads as a single letter. Two lines
|
|
439
|
+
fix that, and the SDK writes neither on your behalf.
|
|
440
|
+
|
|
174
441
|
```typescript
|
|
175
|
-
|
|
442
|
+
// 1. In your entry file, before anything else runs:
|
|
443
|
+
Error.stackTraceLimit = 50;
|
|
444
|
+
|
|
445
|
+
// 2. In your telemetry config:
|
|
446
|
+
const telemetry = createTelemetry({
|
|
447
|
+
apiKey: "edge_xxxxxxxx",
|
|
448
|
+
endpoint: "https://collector.example.com/telemetry",
|
|
449
|
+
buildId: process.env.GIT_SHA, // ships as app.build_id
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**`Error.stackTraceLimit`** defaults to **10 frames** — well short of what the SDK can carry, and
|
|
454
|
+
ten frames of a rejected promise can be entirely library internals. The SDK never assigns it,
|
|
455
|
+
because raising it globally would make every `new Error()` in *your* app more expensive,
|
|
456
|
+
invisibly. It warns once in development and leaves the line to you.
|
|
457
|
+
|
|
458
|
+
**`buildId`** is the symbolication join key. Use a git SHA or a CI run number. Crashes resolve
|
|
459
|
+
against `(app, device.platform, app.build_id)` — three parts, because one commit produces
|
|
460
|
+
different Metro output for iOS and Android, and a one-part key would resolve an Android crash
|
|
461
|
+
against the iOS source map and give you frames that are plausible and wrong.
|
|
462
|
+
|
|
463
|
+
> **Warning — never derive `buildId` from your app version.** `${version}-${buildNumber}` is
|
|
464
|
+
> correct for most apps, which is exactly what makes it dangerous: under Expo Updates or CodePush
|
|
465
|
+
> the native binary is unchanged, so the derived key fetches the **wrong** source map and resolves
|
|
466
|
+
> to plausible-wrong line numbers, with nothing on the row marking them untrustworthy. The SDK has
|
|
467
|
+
> no OTA awareness and will not guess. Leave `buildId` unset and crashes stay unsymbolicated —
|
|
468
|
+
> which is at least honest.
|
|
469
|
+
|
|
470
|
+
Stack traces travel raw and byte-for-byte so `metro-symbolicate` can consume them. Over the
|
|
471
|
+
2000-character cap the tail is dropped on a frame boundary and marked `… [truncated]`.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
## Strip PII before it leaves the device
|
|
475
|
+
|
|
476
|
+
`beforeSend` runs on every event and metric, synchronously, **at enqueue** — before anything can
|
|
477
|
+
reach the network *or* the offline store. Return the event to keep it, or `null` to drop it.
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { createTelemetry, type BeforeSend } from "@nathanclaire/edge-telemetry-sdk";
|
|
481
|
+
|
|
482
|
+
const scrub: BeforeSend = (event) => {
|
|
483
|
+
const attrs = event.attributes;
|
|
484
|
+
if (!attrs) return event;
|
|
485
|
+
|
|
486
|
+
// Drop a key outright
|
|
487
|
+
delete attrs["ui.target"];
|
|
488
|
+
|
|
489
|
+
// Or rewrite one
|
|
490
|
+
if (typeof attrs["user.email"] === "string") {
|
|
491
|
+
attrs["user.email"] = hash(attrs["user.email"]);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Or drop the whole event
|
|
495
|
+
if (event.eventName === "custom_event" && attrs["event.name"] === "internal_debug") {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return event;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const telemetry = createTelemetry({ apiKey: "edge_xxxxxxxx", endpoint: "…", beforeSend: scrub });
|
|
176
503
|
```
|
|
177
504
|
|
|
178
|
-
|
|
505
|
+
**It runs at enqueue, not at flush,** because a failed send persists the batch to disk — and on
|
|
506
|
+
native that disk outlives the process. A flush-time hook would let unscrubbed PII land there
|
|
507
|
+
first. **It is synchronous** because crashes flush during teardown, and a Promise-returning hook
|
|
508
|
+
would put your `await` in a dying app's path.
|
|
509
|
+
|
|
510
|
+
**It covers metrics too.** `vital.target` is a raw CSS selector and the least sanitized value the
|
|
511
|
+
SDK sends.
|
|
512
|
+
|
|
513
|
+
### What you can and cannot change
|
|
514
|
+
|
|
515
|
+
Your hook gets a **copy**. Three tiers are re-stamped from the original after it returns:
|
|
516
|
+
|
|
517
|
+
| Tier | Keys | What happens |
|
|
518
|
+
|---|---|---|
|
|
519
|
+
| **A — immutable** | `type`, `eventName`/`metricName`, `timestamp`, `session.*`, `event.sequence`, everything under `sdk.` and `app.`, `device.platform`, `trace.id`, `span.id`, `parent.span.id`, `rum.action.id`, `view.id` | Restored — covers deletion *and* forgery |
|
|
520
|
+
| **B — rewritable, not deletable** | `device.id` | Hash it if you like; delete it and the collector rejects the batch |
|
|
521
|
+
| **C — yours** | Everything else: `user.*`, `http.*`, `error.*`, `ui.target`, `vital.target`, your own `log()` data | Untouched. This is where the PII lives. |
|
|
522
|
+
|
|
523
|
+
The tiers are enforced by re-stamping, **never by throwing**. The realistic hook is a `delete`
|
|
524
|
+
loop and the realistic bug is over-deletion — an over-broad hook must not be able to get your
|
|
525
|
+
whole feed silently discarded behind a `200`.
|
|
526
|
+
|
|
527
|
+
**A hook that throws fails closed:** the event is dropped, never sent in its original form,
|
|
528
|
+
because a bug in a scrubber must not ship the exact field the scrubber existed to remove. Only an
|
|
529
|
+
explicit `null` counts as a deliberate drop. The two outcomes are counted separately on the wire
|
|
530
|
+
— `sdk.hook_dropped` and `sdk.hook_failed` — because "my volume is down 40%" needs to distinguish
|
|
531
|
+
a working scrubber from a broken one.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Sample a percentage of sessions
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
createTelemetry({ apiKey: "edge_xxxxxxxx", endpoint: "…", sessionSampleRate: 0.1 });
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
The dice are rolled **once per session**, persisted with the session record and re-rolled when
|
|
542
|
+
the session rotates — never per event, which would desynchronise the per-view counters. A
|
|
543
|
+
sampled-out session sends **nothing at all**.
|
|
544
|
+
|
|
545
|
+
> **Warning — crashes are not exempt.** Sampling out a session drops its crashes too. It has to:
|
|
546
|
+
> keeping 100% of crashes over 10% of sessions makes the unfiltered crash-free query read ten
|
|
547
|
+
> times too healthy, with no `WHERE` clause available to repair it.
|
|
548
|
+
|
|
549
|
+
Every row carries `session.sample_rate`, so extrapolating back up is arithmetic. A rate outside
|
|
550
|
+
`0`–`1`, or a `NaN`, warns and falls back to `1` — `Math.random() < NaN` is always false and
|
|
551
|
+
would silently mute your entire deployment.
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Connect your traces to your backend
|
|
556
|
+
|
|
557
|
+
The SDK can attach a W3C `traceparent` header to your API calls, so a slow screen in your RUM
|
|
558
|
+
data links to the exact backend trace that caused it.
|
|
559
|
+
|
|
560
|
+
**This is off by default.** The allowlist starts empty, so upgrading can never break your network
|
|
561
|
+
calls on day one. Opt hosts in explicitly:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
createTelemetry({
|
|
565
|
+
apiKey: "edge_xxxxxxxx",
|
|
566
|
+
endpoint: "https://collector.example.com/telemetry",
|
|
567
|
+
traceHostAllowlist: ["api.example.com", "checkout.example.com"],
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
> **Warning — do this first.** Add `traceparent` to `Access-Control-Allow-Headers` on every host
|
|
572
|
+
> you list, *before* you list it. Missing that header is what *"our app broke when we turned on
|
|
573
|
+
> tracing"* actually means. Adding a header also turns previously-simple cross-origin GETs into
|
|
574
|
+
> preflighted ones, so set `Access-Control-Max-Age` while you're there.
|
|
575
|
+
|
|
576
|
+
### The rules, in full
|
|
577
|
+
|
|
578
|
+
- **Bare hosts, exact match.** `api.example.com`, not `https://api.example.com/v2` and not
|
|
579
|
+
`*.example.com`. Listing a host is your assertion that *that host's* CORS allows the header,
|
|
580
|
+
and nobody can make that assertion over a wildcard.
|
|
581
|
+
- **Ports are ignored** when matching. Note that `http.host` on the wire *keeps* the port — the
|
|
582
|
+
two are deliberately different, so don't join on them.
|
|
583
|
+
- **`traceparent` is the only header the SDK writes, and the only one it reads.** No `b3`, no
|
|
584
|
+
`tracestate`, ever. That is a sentence you should be able to state in a security review.
|
|
585
|
+
- **The SDK never strips or rewrites a header it didn't add.** Set your own `traceparent` and it
|
|
586
|
+
adopts yours and steps aside.
|
|
587
|
+
- **No retry without the header.** A rejected preflight and a dead server are the same
|
|
588
|
+
`TypeError`, so retrying would double-send a non-idempotent POST. A tracing feature must not be
|
|
589
|
+
able to double-charge a card.
|
|
590
|
+
- **A malformed allowlist entry throws in development and is dropped in production.** A RUM SDK
|
|
591
|
+
crashing a shipped banking app over a config typo is the one failure worse than no tracing.
|
|
592
|
+
|
|
593
|
+
### Debugging a missing join
|
|
594
|
+
|
|
595
|
+
Every request stamps `traceparent.outcome`, so "the trace isn't joining" is a query rather than a
|
|
596
|
+
mystery:
|
|
597
|
+
|
|
598
|
+
| Value | What happened |
|
|
599
|
+
|---|---|
|
|
600
|
+
| *(absent)* | Not traced — you have no allowlist |
|
|
601
|
+
| `skipped_off_allowlist` | You have an allowlist and this host isn't on it |
|
|
602
|
+
| `skipped_no_cors` | The request's `mode` ruled it out (fetch only) |
|
|
603
|
+
| `skipped_consumer_set` | You set a `traceparent` and it couldn't be parsed |
|
|
604
|
+
| `adopted` | You set a valid one; the SDK used yours |
|
|
605
|
+
| `injected_attributed` | Injected and joined to a live user action — the healthy case |
|
|
606
|
+
| `injected_expired` | Injected, but the action it belonged to had aged out |
|
|
607
|
+
| `injected_unattributed` | Injected with no user action in flight at all |
|
|
608
|
+
|
|
609
|
+
Every skip still stamps local IDs even though no header goes out. That is what makes *"the row is
|
|
610
|
+
here but the backend join is missing"* mean **the header was stripped in transit** rather than
|
|
611
|
+
something ambiguous.
|
|
612
|
+
|
|
613
|
+
> An unsampled session injects no header at all — not a `flags=00` one.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
## What the SDK captures
|
|
617
|
+
|
|
618
|
+
All of this starts in the constructor. You don't call anything to enable it.
|
|
619
|
+
|
|
620
|
+
### Events
|
|
621
|
+
|
|
622
|
+
| Event | Fires when | Platform |
|
|
623
|
+
|---|---|---|
|
|
624
|
+
| `app.start` | Once per process, at init | Both |
|
|
625
|
+
| `session.started` / `session.finalized` | Session begins / rotates | Both |
|
|
626
|
+
| `app_lifecycle` | Foreground ⇄ background | Both |
|
|
627
|
+
| `view` | A screen visit ends (route change, background, session rotation) | Both |
|
|
628
|
+
| `http.request` | Every `fetch` / `XHR` completes | Both |
|
|
629
|
+
| `app.crash` | Unhandled error or promise rejection | Both |
|
|
630
|
+
| `app.error` | `captureError()`, or an opted-in `console.error` | Both |
|
|
631
|
+
| `ui.interaction` | Every click (web) / every `trackTap()` (native) | Both |
|
|
632
|
+
| `network_change` | Connectivity type changes | Both |
|
|
633
|
+
| `user.profile.update` | `identify()` | Both |
|
|
634
|
+
| `navigation` | Route change or `screenStart()` | Both *(deprecated)* |
|
|
635
|
+
| `screen.duration` | `screenEnd()` | Native *(deprecated)* |
|
|
636
|
+
| `custom_event` | Any `log()` name not in this table | Both |
|
|
637
|
+
|
|
638
|
+
### Metrics
|
|
639
|
+
|
|
640
|
+
| Metric | What it measures | Platform |
|
|
641
|
+
|---|---|---|
|
|
642
|
+
| `frame_render_time` | p95 frame time per window; closes every 10s **or** at a screen change | Both |
|
|
643
|
+
| `memory_usage` | Resident memory (RSS) in MB, sampled every 30s | **Native only** |
|
|
644
|
+
| `LCP` `FCP` `TTFB` | Core Web Vitals, once per page load | **Web only** |
|
|
645
|
+
| `CLS` `INP` | Running values, sent when the tab is hidden | **Web only** |
|
|
646
|
+
|
|
647
|
+
Metrics carry `metric.unit` — `ms`, `MB`, or `score` for CLS.
|
|
648
|
+
|
|
649
|
+
> **Web-only metrics are web-only on purpose.** Native emits no Core Web Vitals, and web emits no
|
|
650
|
+
> `memory_usage` — `performance.memory` is Chromium-only, so the metric's mere presence was a
|
|
651
|
+
> browser-detection signal wearing a memory label.
|
|
652
|
+
|
|
653
|
+
> **Warning — Core Web Vitals are page-load-scoped, not screen-scoped.** `LCP`, `FCP` and `TTFB`
|
|
654
|
+
> physically cannot recur on a soft navigation, so `view.id` on a vital row is always the
|
|
655
|
+
> **initial** view's. `GROUP BY view.name` over vitals reads as *"by entry point"* — a useful
|
|
656
|
+
> dashboard, as long as you label it that way.
|
|
657
|
+
|
|
658
|
+
### Retired names
|
|
659
|
+
|
|
660
|
+
`page_load`, `resource_timing`, `long_task` and `user.interaction` were removed in v4. They are
|
|
661
|
+
unreachable — `log("page_load")` now arrives as `custom_event`. Their jobs are covered by `view`,
|
|
662
|
+
`view.loading_time`, the Core Web Vitals and `frame_render_time`.
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## API
|
|
667
|
+
|
|
668
|
+
Every method returns a Promise.
|
|
669
|
+
|
|
670
|
+
### Core — both platforms
|
|
179
671
|
|
|
180
672
|
```typescript
|
|
673
|
+
log(event: string, data?: Record<string, any>): Promise<void>
|
|
674
|
+
captureError(error: unknown, context?: Record<string, any>): Promise<void>
|
|
675
|
+
flush(): Promise<void> // send the queue now
|
|
676
|
+
shutdown(): Promise<void> // stop timers and do a final flush
|
|
677
|
+
attachNavigation(navigationRef: any): Promise<void>
|
|
678
|
+
trackErrors(options?: { captureConsole?: boolean }): Promise<void>
|
|
181
679
|
getDeviceInfo(): Promise<DeviceInfo>
|
|
182
680
|
getNetworkInfo(): Promise<NetworkInfo>
|
|
183
681
|
```
|
|
184
682
|
|
|
185
|
-
###
|
|
683
|
+
### Identity — both platforms
|
|
186
684
|
|
|
187
685
|
```typescript
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
686
|
+
identify(profile: {
|
|
687
|
+
userId?: string;
|
|
688
|
+
name?: string;
|
|
689
|
+
email?: string;
|
|
690
|
+
phone?: string;
|
|
691
|
+
avatar?: string;
|
|
692
|
+
customAttributes?: Record<string, any>;
|
|
693
|
+
}): Promise<void> // the only method that puts a profile on the wire
|
|
694
|
+
|
|
695
|
+
setUserId(id: string): Promise<void> // "" clears it — no empty string is ever sent
|
|
696
|
+
getUserProfile(): Promise<UserProfile | undefined>
|
|
697
|
+
clearUserProfile(): Promise<void> // clears user.id; leaves device.id alone
|
|
698
|
+
|
|
699
|
+
// State-only. These record a profile but emit nothing — call identify() to send it.
|
|
700
|
+
setUserProfile(profile): Promise<void>
|
|
701
|
+
setUserDetails(details): Promise<void>
|
|
702
|
+
updateUserProfile(updates): Promise<void>
|
|
703
|
+
setUserName(fullName: string, firstName?: string, lastName?: string): Promise<void>
|
|
704
|
+
setUserContact(email?: string, phone?: string): Promise<void>
|
|
192
705
|
```
|
|
193
706
|
|
|
194
|
-
|
|
195
|
-
|
|
707
|
+
> `fullName`, `firstName`, `lastName`, `avatar`, `createdAt` and `updatedAt` are `@deprecated` and
|
|
708
|
+
> go away in v5. `fullName` still works — it's what `user.name` is built from. The other five
|
|
709
|
+
> reach no wire key at all.
|
|
196
710
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
711
|
+
### Native only
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
trackTap(name: string): Promise<void> // → ui.interaction. Never rejects.
|
|
715
|
+
trackMemoryUsage(): Promise<void> // auto-started
|
|
716
|
+
screenStart(name: string): Promise<void> // → navigation (deprecated feed)
|
|
717
|
+
screenEnd(name: string): Promise<void> // → screen.duration (deprecated feed)
|
|
718
|
+
trackRoute(from: string, to: string): Promise<void> // (deprecated feed)
|
|
200
719
|
```
|
|
201
720
|
|
|
202
|
-
|
|
721
|
+
Prefer `attachNavigation()` over `screenStart` / `screenEnd` / `trackRoute` — those three feed
|
|
722
|
+
`navigation` and `screen.duration`, both superseded by the `view` event.
|
|
723
|
+
|
|
724
|
+
### Web only
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
trackInteractions(): Promise<void> // auto-started
|
|
728
|
+
autoTrackNavigation(): Promise<void> // auto-started
|
|
729
|
+
```
|
|
203
730
|
|
|
204
|
-
|
|
731
|
+
`trackFrameDrops()`, `trackNetworkRequests()` and `attachAppLifecycle()` exist on both classes and
|
|
732
|
+
are auto-started too. Core Web Vitals capture is intentionally private — there is nothing to
|
|
733
|
+
configure, and a vital that only fires when someone remembers to call a method is a vital nobody
|
|
734
|
+
has.
|
|
205
735
|
|
|
206
|
-
|
|
207
|
-
Each tap emits `user.interaction` with `interaction.type` and the current screen when known. It
|
|
208
|
-
observes the gesture without stealing it (no DOM `target_tag`/`target_class` — those are web-only).
|
|
736
|
+
### Exports
|
|
209
737
|
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
738
|
+
```typescript
|
|
739
|
+
import {
|
|
740
|
+
createTelemetry, // the factory — start here
|
|
741
|
+
memoryStore, // an in-memory Store, for tests
|
|
742
|
+
type TelemetryOpts,
|
|
743
|
+
type BeforeSend,
|
|
744
|
+
type Store, type SyncStore, type AsyncStore, type StoreRead, type StoreWrite,
|
|
745
|
+
type MemoryStoreOpts,
|
|
746
|
+
} from "@nathanclaire/edge-telemetry-sdk";
|
|
216
747
|
```
|
|
217
748
|
|
|
749
|
+
`TelemetryWeb` and `TelemetryNative` are also exported, but **use `createTelemetry()`** — it is
|
|
750
|
+
the only path that validates your `apiKey` and your trace allowlist. Constructing a class
|
|
751
|
+
directly skips both checks.
|
|
752
|
+
|
|
753
|
+
> `TelemetryEvent`, `DeviceInfo`, `NetworkInfo` and `UserProfile` are documented in this README as
|
|
754
|
+
> wire and return shapes, but are **not currently exported as types**. For a `beforeSend` hook,
|
|
755
|
+
> annotate with the exported `BeforeSend` and let the event type infer.
|
|
756
|
+
|
|
218
757
|
---
|
|
219
758
|
|
|
220
|
-
##
|
|
759
|
+
## The Context block
|
|
221
760
|
|
|
222
|
-
|
|
761
|
+
Every event and metric carries a flat, dot-namespaced set of attributes describing *who, where and
|
|
762
|
+
when*. It means each row is self-describing — you never join against a separate device or session
|
|
763
|
+
event.
|
|
223
764
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
LCP/FCP/CLS/INP/TTFB) are emitted only by the web build — native never reports metrics it can't
|
|
245
|
-
honestly measure.
|
|
765
|
+
```
|
|
766
|
+
session.id session.start_time session.sequence session.sample_rate event.sequence
|
|
767
|
+
user.id device.id device.id_ephemeral
|
|
768
|
+
view.id view.name
|
|
769
|
+
sdk.platform sdk.version sdk.events_dropped sdk.drop_reason sdk.hook_dropped sdk.hook_failed
|
|
770
|
+
app.name app.version app.build_number app.package_name app.build_id
|
|
771
|
+
device.platform device.platform_version device.model device.manufacturer device.brand
|
|
772
|
+
device.screen_density device.screen_width_px device.screen_height_px device.orientation
|
|
773
|
+
device.cpu_abi device.low_ram ← native only
|
|
774
|
+
network.type network.is_connected
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
It is **frozen at 39 keys**. A key the SDK has nothing for is **omitted entirely** — never sent as
|
|
778
|
+
`null` or `undefined`. Absence means "we had nothing", consistently, everywhere.
|
|
779
|
+
|
|
780
|
+
Your `log()` data is flattened on top, so it can override any `app.*`, `device.*` or `network.*`
|
|
781
|
+
key — but **not** the identity keys, which are stamped afterwards.
|
|
782
|
+
|
|
783
|
+
**Network and device state are read at log time**, not at init: a request that failed when the
|
|
784
|
+
Wi-Fi dropped is better described by the network at the moment it completed.
|
|
246
785
|
|
|
247
786
|
---
|
|
248
787
|
|
|
249
788
|
## Wire format
|
|
250
789
|
|
|
251
|
-
```
|
|
252
|
-
POST
|
|
790
|
+
```http
|
|
791
|
+
POST /telemetry HTTP/1.1
|
|
253
792
|
Content-Type: application/json
|
|
254
793
|
X-API-Key: edge_xxxxxxxx
|
|
794
|
+
Authorization: Bearer edge_xxxxxxxx
|
|
795
|
+
```
|
|
255
796
|
|
|
797
|
+
```json
|
|
256
798
|
{
|
|
257
799
|
"type": "telemetry_batch",
|
|
258
|
-
"timestamp": "2026-07-02T06:00:00.000Z",
|
|
800
|
+
"timestamp": "2026-07-02T06:00:00.000Z",
|
|
259
801
|
"batch_size": 3,
|
|
260
|
-
"events": [ /*
|
|
802
|
+
"events": [ /* … */ ]
|
|
261
803
|
}
|
|
262
804
|
```
|
|
263
805
|
|
|
264
|
-
Each
|
|
806
|
+
Each entry:
|
|
265
807
|
|
|
266
808
|
```typescript
|
|
267
809
|
type TelemetryEvent = {
|
|
268
810
|
type: "event" | "metric";
|
|
269
|
-
eventName?: string;
|
|
270
|
-
metricName?: string;
|
|
271
|
-
value?: number;
|
|
272
|
-
timestamp: string;
|
|
273
|
-
attributes?: Record<string, any>;
|
|
811
|
+
eventName?: string; // events
|
|
812
|
+
metricName?: string; // metrics
|
|
813
|
+
value?: number; // metrics
|
|
814
|
+
timestamp: string; // ISO 8601, never a millisecond epoch
|
|
815
|
+
attributes?: Record<string, any>; // flat, dot-namespaced, includes the Context block
|
|
274
816
|
};
|
|
275
817
|
```
|
|
276
818
|
|
|
277
|
-
|
|
278
|
-
`
|
|
279
|
-
|
|
280
|
-
separate context events.
|
|
819
|
+
**Both credential headers are always sent, with the same value.** A shared collector reads
|
|
820
|
+
`X-API-Key`; a segmented one running `AUTH_MODE=jwt` reads `Authorization`. One build serves
|
|
821
|
+
both, so nothing has to sniff the credential's shape.
|
|
281
822
|
|
|
282
823
|
---
|
|
283
824
|
|
|
284
|
-
## Identity
|
|
285
|
-
|
|
286
|
-
IDs use 16 hex chars of entropy; device/session are suffixed with the device OS, user is not:
|
|
825
|
+
## Identity and IDs
|
|
287
826
|
|
|
288
827
|
```
|
|
289
|
-
device_{ms}_{
|
|
290
|
-
session_{ms}_{
|
|
291
|
-
user_{ms}_{16hex}
|
|
828
|
+
device_{ms}_{16 hex}_{ios|android|web}
|
|
829
|
+
session_{ms}_{16 hex}_{ios|android|web}
|
|
292
830
|
```
|
|
293
831
|
|
|
294
|
-
|
|
295
|
-
|
|
832
|
+
Entropy comes from `crypto.getRandomValues`. There is no `Math.random()` fallback — a weak ID
|
|
833
|
+
that persists forever is worse than refusing to mint one.
|
|
296
834
|
|
|
297
|
-
`
|
|
298
|
-
|
|
835
|
+
**`device.id` is the SDK's. `user.id` is yours.**
|
|
836
|
+
|
|
837
|
+
| | `device.id` | `user.id` |
|
|
838
|
+
|---|---|---|
|
|
839
|
+
| Minted by | The SDK, once | You, via `setUserId()` / `identify({ userId })` |
|
|
840
|
+
| Rotates | **Never** — not on `identify()`, not on logout | Whenever you set it |
|
|
841
|
+
| Present | Always | Only after you set one |
|
|
842
|
+
| Answers | Anonymous reach | Known-user reach |
|
|
843
|
+
|
|
844
|
+
Because `device.id` survives the login transition, `GROUP BY device.id` stitches a user's
|
|
845
|
+
anonymous and signed-in sessions together.
|
|
846
|
+
|
|
847
|
+
**When storage is unavailable** — incognito, a partitioned iframe, Safari ITP eviction, a full
|
|
848
|
+
disk — `device.id` lives for one process only and the row carries `device.id_ephemeral: true`.
|
|
849
|
+
Exclude that population from device counts; without the flag, a set of users who never return
|
|
850
|
+
inflates `COUNT(DISTINCT device.id)` and reads as growth.
|
|
851
|
+
|
|
852
|
+
### Sessions
|
|
853
|
+
|
|
854
|
+
A session ends after **30 minutes of inactivity** or **4 hours** total, whichever comes first.
|
|
855
|
+
|
|
856
|
+
The session record is persisted, so **process death, a tab close, a hard reload and a bfcache
|
|
857
|
+
restore all resume the same session** if the gap is inside the idle window. A resumed session
|
|
858
|
+
deliberately does *not* re-emit `session.started` — otherwise `COUNT(session.started)` would stop
|
|
859
|
+
equalling your session count.
|
|
299
860
|
|
|
300
861
|
---
|
|
301
862
|
|
|
302
|
-
##
|
|
863
|
+
## Delivery and reliability
|
|
864
|
+
|
|
865
|
+
| | Behaviour |
|
|
866
|
+
|---|---|
|
|
867
|
+
| **Batching** | Flushes at `batchSize` events or every `flushIntervalMs`, whichever comes first |
|
|
868
|
+
| **Retry** | 3 attempts — exponential backoff with jitter on native, linear on web |
|
|
869
|
+
| **Offline queue** | After the last retry, the batch is written to `telemetry_failed_events` and replayed on the next init |
|
|
870
|
+
| **Caps** | 500 events in memory, 500 events / 1 MB on disk. Both drop oldest first, and **`app.crash` is evicted last** |
|
|
871
|
+
| **Crashes** | Don't wait for the batch — the queue is persisted and one batch sent immediately, crash first |
|
|
872
|
+
| **Page unload** | The web sender uses `fetch({ keepalive: true })`, so in-flight batches outlive the document |
|
|
873
|
+
|
|
874
|
+
Anything dropped is reported on the wire as `sdk.events_dropped` and `sdk.drop_reason`, so loss is
|
|
875
|
+
visible rather than silent.
|
|
303
876
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
- **Web unload:** the web sender uses `fetch({ keepalive: true })` (not `sendBeacon`, which can't
|
|
310
|
-
set the required `X-API-Key` header) so in-flight batches survive page unload.
|
|
877
|
+
**Crashes are evicted last on purpose.** Evicting them first would make your crash-free rate look
|
|
878
|
+
*better* the worse the user's network was.
|
|
879
|
+
|
|
880
|
+
> **Note — `flush()` sends one batch per call.** It does not loop. A large backlog still drains
|
|
881
|
+
> one batch per interval.
|
|
311
882
|
|
|
312
883
|
---
|
|
884
|
+
## Troubleshooting
|
|
313
885
|
|
|
314
|
-
|
|
886
|
+
Start by setting `debug: true` and calling `await telemetry.flush()`. Almost everything below
|
|
887
|
+
shows up in that output.
|
|
315
888
|
|
|
316
|
-
|
|
889
|
+
### Nothing arrives at all
|
|
317
890
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
891
|
+
| Check | How |
|
|
892
|
+
|---|---|
|
|
893
|
+
| Is the endpoint right? | The collector terminates **`POST /telemetry`**. Leave `endpoint` unset and every send goes to a placeholder host that resolves nowhere. |
|
|
894
|
+
| Did `createTelemetry()` throw? | An `apiKey` not starting with `edge_` throws synchronously, at startup. |
|
|
895
|
+
| Is the session sampled out? | With `sessionSampleRate < 1`, a sampled-out session sends **nothing**, crashes included. |
|
|
896
|
+
| Is `beforeSend` eating events? | A hook that throws drops the event. Check `sdk.hook_failed` on the rows that do arrive. |
|
|
897
|
+
| Did you flush? | Without `flush()` the first batch waits for 50 events or 30 seconds. |
|
|
898
|
+
|
|
899
|
+
### Events arrive but a dashboard is empty
|
|
900
|
+
|
|
901
|
+
Your event name is probably being rewritten. Only [allowlisted names](#what-the-sdk-captures)
|
|
902
|
+
travel as themselves; everything else arrives as `custom_event` with your name in
|
|
903
|
+
`attributes["event.name"]`. Query on `event.name` for custom events.
|
|
321
904
|
|
|
322
|
-
|
|
323
|
-
|
|
905
|
+
If the name *is* allowlisted and the column is still empty, the backend may not have the name
|
|
906
|
+
enabled on its own ingest allowlist yet — unlisted names are dropped on arrival. That is a
|
|
907
|
+
backend configuration question, not an SDK one.
|
|
908
|
+
|
|
909
|
+
### Common surprises
|
|
910
|
+
|
|
911
|
+
| Symptom | Cause |
|
|
912
|
+
|---|---|
|
|
913
|
+
| Profile data stopped arriving after upgrading to v4 | `setUserProfile()` and friends are state-only now. Call `identify()`. |
|
|
914
|
+
| "Our app broke when we enabled tracing" | The host is missing `Access-Control-Allow-Headers: traceparent`. |
|
|
915
|
+
| Crash stacks are single letters | Set `buildId` and `Error.stackTraceLimit = 50`. See [Make crash stacks readable](#make-crash-stacks-readable). |
|
|
916
|
+
| `device.id` changes every launch | Storage is unavailable. Look for `device.id_ephemeral: true` on the rows. |
|
|
917
|
+
| No `http.request` events from axios | Should work — axios rides XHR, and XHR is the SDK's interception point on both builds. If it doesn't, check that the request isn't going to your collector's own endpoint, which is excluded by design. |
|
|
918
|
+
| No Core Web Vitals on native | Correct — they're web-only, by design. |
|
|
919
|
+
| No `memory_usage` on web | Correct — it's native-only, by design. |
|
|
920
|
+
| No dead-click data on native | Correct — no DOM, so no mutation signal. Filter dead-click rates to web. |
|
|
921
|
+
| `screen.duration` never fires | You're using `attachNavigation()`. Use the `view` event instead — it supersedes both deprecated feeds. |
|
|
922
|
+
| A screen's name is stuck on an old value | You mixed `screenStart()` with `attachNavigation()`. An explicit name outranks a route name and pins the view. |
|
|
923
|
+
| `view.loading_time` is null | Read `view.loading_time_outcome` — it tells you which of the three reasons applies. |
|
|
924
|
+
|
|
925
|
+
### Requests the SDK can't see
|
|
926
|
+
|
|
927
|
+
Interception covers JavaScript `fetch` and `XHR`. It does **not** see `rn-fetch-blob`, React
|
|
928
|
+
Native Firebase, native Apollo links, `expo-file-system`, `Image` loading or WebSockets. A screen
|
|
929
|
+
whose real wait is one of those reports `no_activity`.
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## Upgrading
|
|
934
|
+
|
|
935
|
+
### To v4
|
|
936
|
+
|
|
937
|
+
**Read [`docs/migration-v4.md`](docs/migration-v4.md) before you deploy.** It ships inside the npm
|
|
938
|
+
package, not just in the repo.
|
|
939
|
+
|
|
940
|
+
Of the twenty behavioural changes between 3.1.0 and v4, **exactly one breaks a build** — the
|
|
941
|
+
`getDeviceInfo()` / `getNetworkInfo()` return shapes. The other nineteen are silent: no compile
|
|
942
|
+
error, no runtime error, no log line. They surface as a saved filter returning zero rows, or a
|
|
943
|
+
chart stepping at the release boundary. That is precisely why the note exists.
|
|
944
|
+
|
|
945
|
+
**The one action item most apps have:** if you call `setUserProfile()`, `setUserDetails()`,
|
|
946
|
+
`updateUserProfile()`, `setUserName()` or `setUserContact()` and never call `identify()`, your
|
|
947
|
+
profile data no longer reaches the wire. Add one `identify()` call.
|
|
948
|
+
|
|
949
|
+
The migration note also lists **the columns React Native will never write** — ANR, hangs,
|
|
950
|
+
battery, `app.exit`, native crashes — so nobody builds a panel that stays empty forever.
|
|
951
|
+
|
|
952
|
+
### To v3
|
|
953
|
+
|
|
954
|
+
v3 was a single clean break onto the EdgeRum wire contract: `apiKey` became required, the POST
|
|
955
|
+
body became the `telemetry_batch` envelope, timestamps became ISO-8601, device and network data
|
|
956
|
+
moved onto every event as the Context block, and several event names changed. Full list in
|
|
957
|
+
[CHANGELOG.md](./CHANGELOG.md).
|
|
324
958
|
|
|
325
959
|
---
|
|
326
960
|
|
|
327
961
|
## Development
|
|
328
962
|
|
|
329
963
|
```bash
|
|
964
|
+
npm install
|
|
330
965
|
npm run build # vite build → dist/ (web + native entries, ESM + CJS)
|
|
331
966
|
npm run dev # vite in watch mode
|
|
332
967
|
npm test # vitest run
|
|
@@ -334,15 +969,25 @@ npm run typecheck # tsc --noEmit
|
|
|
334
969
|
npm run clean # remove dist/
|
|
335
970
|
```
|
|
336
971
|
|
|
337
|
-
`
|
|
338
|
-
|
|
339
|
-
|
|
972
|
+
`prepare` runs the build, so installing this as a git dependency builds it automatically. `dist/`
|
|
973
|
+
and `*.tgz` are git-ignored.
|
|
974
|
+
|
|
975
|
+
**Platform code is split by filename**, not by runtime branching: `*.native.ts` and `*.web.ts`,
|
|
976
|
+
with the bundler choosing. Shared logic lives in a plain `.ts` and is tested once, which is how
|
|
977
|
+
the two builds stay in lockstep. When you add a capability, add both halves.
|
|
978
|
+
|
|
979
|
+
> One test (`src/vitals.native.test.ts`) reads `dist/` to prove the web-vitals dependency
|
|
980
|
+
> contributes zero bytes to the native bundle. It is red after `npm run clean` until the next
|
|
981
|
+
> build. That's deliberate — a source-graph proxy would pass a bundler change that a real chunk
|
|
982
|
+
> graph catches.
|
|
340
983
|
|
|
341
|
-
|
|
342
|
-
|
|
984
|
+
Architecture, conventions and the reasoning behind the design decisions live in
|
|
985
|
+
[`CLAUDE.md`](./CLAUDE.md). The wire contract — every key, its type and its null discipline — is
|
|
986
|
+
[`docs/backend-wire-contract.md`](./docs/backend-wire-contract.md), and it outranks everything
|
|
987
|
+
else where they disagree.
|
|
343
988
|
|
|
344
989
|
---
|
|
345
990
|
|
|
346
991
|
## License
|
|
347
992
|
|
|
348
|
-
|
|
993
|
+
MIT
|