expo-observe 56.0.7 → 56.0.8
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/android/src/main/java/expo/modules/observe/OpenTelemetry.kt +2 -1
- package/build/integrations/expo-router/ObserveRouterIntegrationProvider.d.ts.map +1 -1
- package/build/integrations/expo-router/ObserveRouterIntegrationProvider.js +6 -5
- package/build/integrations/expo-router/ObserveRouterIntegrationProvider.js.map +1 -1
- package/build/integrations/expo-router/init.d.ts.map +1 -1
- package/build/integrations/expo-router/init.js +11 -5
- package/build/integrations/expo-router/init.js.map +1 -1
- package/expo-module.config.json +1 -1
- package/ios/CursorRepair.swift +55 -0
- package/ios/Event.swift +61 -45
- package/ios/Observability.swift +96 -94
- package/ios/ObserveUserDefaults.swift +13 -13
- package/ios/OpenTelemetry.swift +29 -18
- package/ios/Tests/CursorRepairTests.swift +94 -0
- package/ios/Tests/OTAnyValueTests.swift +37 -5
- package/ios/Tests/OpenTelemetryTests.swift +30 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.7/expo.modules.observe-56.0.7-sources.jar → 56.0.8/expo.modules.observe-56.0.8-sources.jar} +0 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.7/expo.modules.observe-56.0.7.aar → 56.0.8/expo.modules.observe-56.0.8.aar} +0 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.7/expo.modules.observe-56.0.7.module → 56.0.8/expo.modules.observe-56.0.8.module} +23 -23
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.7/expo.modules.observe-56.0.7.pom → 56.0.8/expo.modules.observe-56.0.8.pom} +2 -2
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml +4 -4
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha512 +1 -1
- package/package.json +4 -4
- package/src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx +5 -4
- package/src/integrations/expo-router/init.ts +12 -5
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.sha512 +0 -1
|
@@ -191,7 +191,8 @@ private val metricNameMap = mapOf(
|
|
|
191
191
|
(MetricCategory.Updates.categoryName to "updateDownloadTime") to "expo.updates.download_time",
|
|
192
192
|
|
|
193
193
|
// Navigation
|
|
194
|
-
(MetricCategory.Navigation.categoryName to "
|
|
194
|
+
(MetricCategory.Navigation.categoryName to "cold_ttr") to "expo.navigation.cold_ttr",
|
|
195
|
+
(MetricCategory.Navigation.categoryName to "warm_ttr") to "expo.navigation.warm_ttr",
|
|
195
196
|
(MetricCategory.Navigation.categoryName to "tti") to "expo.navigation.tti"
|
|
196
197
|
)
|
|
197
198
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObserveRouterIntegrationProvider.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,iBAAiB,EAA+B,MAAM,OAAO,CAAC;AAI3F,OAAO,EAAkC,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAE1F,eAAO,MAAM,+BAA+B,0DAAuD,CAAC;AAEpG,wBAAgB,gCAAgC,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"ObserveRouterIntegrationProvider.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,iBAAiB,EAA+B,MAAM,OAAO,CAAC;AAI3F,OAAO,EAAkC,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAE1F,eAAO,MAAM,+BAA+B,0DAAuD,CAAC;AAEpG,wBAAgB,gCAAgC,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CAuB/E"}
|
|
@@ -6,15 +6,16 @@ import { createRouterIntegrationStorage } from './storage';
|
|
|
6
6
|
export const ObserveRouterIntegrationContext = createContext(null);
|
|
7
7
|
export function ObserveRouterIntegrationProvider({ children }) {
|
|
8
8
|
const [storage] = useState(() => isInitialized() ? createRouterIntegrationStorage() : null);
|
|
9
|
+
const [listenersCleanup] = useState(() => {
|
|
10
|
+
if (!storage || !optionalRouter)
|
|
11
|
+
return;
|
|
12
|
+
return initListeners(storage, optionalRouter.unstable_navigationEvents);
|
|
13
|
+
});
|
|
9
14
|
const prevInitialized = useRef(isInitialized());
|
|
10
15
|
if (prevInitialized.current !== isInitialized()) {
|
|
11
16
|
throw new Error(`[expo-observe] Router integration was ${isInitialized() ? 'enabled' : 'disabled'} after application mounted. Call ExpoObserve.configure() before mounting AppMetricsRoot.`);
|
|
12
17
|
}
|
|
13
|
-
useEffect(() =>
|
|
14
|
-
if (!storage || !optionalRouter)
|
|
15
|
-
return;
|
|
16
|
-
return initListeners(storage, optionalRouter.unstable_navigationEvents);
|
|
17
|
-
}, [storage]);
|
|
18
|
+
useEffect(() => listenersCleanup, [listenersCleanup]);
|
|
18
19
|
return (_jsx(ObserveRouterIntegrationContext.Provider, { value: storage, children: children }));
|
|
19
20
|
}
|
|
20
21
|
//# sourceMappingURL=ObserveRouterIntegrationProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObserveRouterIntegrationProvider.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAA0B,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE3F,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,8BAA8B,EAAiC,MAAM,WAAW,CAAC;AAE1F,MAAM,CAAC,MAAM,+BAA+B,GAAG,aAAa,CAAkC,IAAI,CAAC,CAAC;AAEpG,MAAM,UAAU,gCAAgC,CAAC,EAAE,QAAQ,EAAqB;IAC9E,MAAM,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAkC,GAAG,EAAE,CAC/D,aAAa,EAAE,CAAC,CAAC,CAAC,8BAA8B,EAAE,CAAC,CAAC,CAAC,IAAI,CAC1D,CAAC;
|
|
1
|
+
{"version":3,"file":"ObserveRouterIntegrationProvider.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAA0B,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE3F,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,8BAA8B,EAAiC,MAAM,WAAW,CAAC;AAE1F,MAAM,CAAC,MAAM,+BAA+B,GAAG,aAAa,CAAkC,IAAI,CAAC,CAAC;AAEpG,MAAM,UAAU,gCAAgC,CAAC,EAAE,QAAQ,EAAqB;IAC9E,MAAM,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAkC,GAAG,EAAE,CAC/D,aAAa,EAAE,CAAC,CAAC,CAAC,8BAA8B,EAAE,CAAC,CAAC,CAAC,IAAI,CAC1D,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE;QACvC,IAAI,CAAC,OAAO,IAAI,CAAC,cAAc;YAAE,OAAO;QACxC,OAAO,aAAa,CAAC,OAAO,EAAE,cAAc,CAAC,yBAAyB,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IAChD,IAAI,eAAe,CAAC,OAAO,KAAK,aAAa,EAAE,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CACb,yCAAyC,aAAa,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,0FAA0F,CAC5K,CAAC;IACJ,CAAC;IAED,SAAS,CAAC,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAEtD,OAAO,CACL,KAAC,+BAA+B,CAAC,QAAQ,IAAC,KAAK,EAAE,OAAO,YACrD,QAAQ,GACgC,CAC5C,CAAC;AACJ,CAAC","sourcesContent":["import { createContext, type PropsWithChildren, useEffect, useRef, useState } from 'react';\n\nimport { initListeners, isInitialized } from './init';\nimport { optionalRouter } from './router';\nimport { createRouterIntegrationStorage, type RouterIntegrationStorage } from './storage';\n\nexport const ObserveRouterIntegrationContext = createContext<RouterIntegrationStorage | null>(null);\n\nexport function ObserveRouterIntegrationProvider({ children }: PropsWithChildren) {\n const [storage] = useState<RouterIntegrationStorage | null>(() =>\n isInitialized() ? createRouterIntegrationStorage() : null\n );\n const [listenersCleanup] = useState(() => {\n if (!storage || !optionalRouter) return;\n return initListeners(storage, optionalRouter.unstable_navigationEvents);\n });\n\n const prevInitialized = useRef(isInitialized());\n if (prevInitialized.current !== isInitialized()) {\n throw new Error(\n `[expo-observe] Router integration was ${isInitialized() ? 'enabled' : 'disabled'} after application mounted. Call ExpoObserve.configure() before mounting AppMetricsRoot.`\n );\n }\n\n useEffect(() => listenersCleanup, [listenersCleanup]);\n\n return (\n <ObserveRouterIntegrationContext.Provider value={storage}>\n {children}\n </ObserveRouterIntegrationContext.Provider>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAS1D,eAAO,MAAM,aAAa,eAAoB,CAAC;AAE/C,wBAAgB,qBAAqB,SAGpC;AAED,KAAK,gBAAgB,GAAG,WAAW,CAAC,OAAO,cAAc,CAAC,CAAC,2BAA2B,CAAC,CAAC;AAExF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,EAAE,gBAAgB,GACjC,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAS1D,eAAO,MAAM,aAAa,eAAoB,CAAC;AAE/C,wBAAgB,qBAAqB,SAGpC;AAED,KAAK,gBAAgB,GAAG,WAAW,CAAC,OAAO,cAAc,CAAC,CAAC,2BAA2B,CAAC,CAAC;AAExF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,EAAE,gBAAgB,GACjC,MAAM,IAAI,CAgFZ"}
|
|
@@ -15,7 +15,6 @@ export function initListeners(storage, navigationEvents) {
|
|
|
15
15
|
const appLaunchTime = performance.now();
|
|
16
16
|
const cleanup = new Set();
|
|
17
17
|
const unsubscribeAction = navigationEvents.addListener('actionDispatched', (event) => {
|
|
18
|
-
// TODO(@ubax): Handle screen preloading
|
|
19
18
|
// PRELOAD comes from router.prefetch() — a route warm-up, not a user
|
|
20
19
|
// navigation — so it must not seed dispatchTime.
|
|
21
20
|
if (event.actionType === 'PRELOAD')
|
|
@@ -26,6 +25,12 @@ export function initListeners(storage, navigationEvents) {
|
|
|
26
25
|
});
|
|
27
26
|
});
|
|
28
27
|
cleanup.add(unsubscribeAction);
|
|
28
|
+
const unsubscribePreload = navigationEvents.addListener('pagePreloaded', (e) => {
|
|
29
|
+
// The screen rendered as part of a preload. Mark it as already rendered so
|
|
30
|
+
// the eventual `pageFocused` resolves to `warm_ttr` rather than `cold_ttr`.
|
|
31
|
+
storage.renderedScreensIds.add(e.screenId);
|
|
32
|
+
});
|
|
33
|
+
cleanup.add(unsubscribePreload);
|
|
29
34
|
const unsubscribeFocus = navigationEvents.addListener('pageFocused', async (e) => {
|
|
30
35
|
// Snapshot both clocks once so every metric written below is stamped with
|
|
31
36
|
// the moment the focus event fired, not the moment `addCustomMetricToSession`
|
|
@@ -34,6 +39,7 @@ export function initListeners(storage, navigationEvents) {
|
|
|
34
39
|
const timestamp = new Date().toISOString();
|
|
35
40
|
const isInitial = !storage.renderedScreensIds.has(e.screenId);
|
|
36
41
|
storage.renderedScreensIds.add(e.screenId);
|
|
42
|
+
const name = isInitial ? 'cold_ttr' : 'warm_ttr';
|
|
37
43
|
const mainSessionId = (await AppMetrics.getMainSession())?.id;
|
|
38
44
|
if (!mainSessionId) {
|
|
39
45
|
return;
|
|
@@ -46,10 +52,10 @@ export function initListeners(storage, navigationEvents) {
|
|
|
46
52
|
sessionId: mainSessionId,
|
|
47
53
|
timestamp,
|
|
48
54
|
category: 'navigation',
|
|
49
|
-
name
|
|
55
|
+
name,
|
|
50
56
|
routeName: e.pathname,
|
|
51
57
|
value: appLaunchTtrSeconds,
|
|
52
|
-
params: {
|
|
58
|
+
params: { isAppLaunch: true, routeParams: e.params },
|
|
53
59
|
});
|
|
54
60
|
return;
|
|
55
61
|
}
|
|
@@ -66,10 +72,10 @@ export function initListeners(storage, navigationEvents) {
|
|
|
66
72
|
sessionId: mainSessionId,
|
|
67
73
|
timestamp,
|
|
68
74
|
category: 'navigation',
|
|
69
|
-
name
|
|
75
|
+
name,
|
|
70
76
|
routeName: e.pathname,
|
|
71
77
|
value: (now - dispatchTime) / 1000,
|
|
72
|
-
params: {
|
|
78
|
+
params: { isAppLaunch: false, routeParams: e.params },
|
|
73
79
|
});
|
|
74
80
|
}
|
|
75
81
|
storage.pendingActions.length = 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAiC,MAAM,WAAW,CAAC;AAE1D,+EAA+E;AAC/E,8EAA8E;AAC9E,0EAA0E;AAC1E,gEAAgE;AAEhE,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC;AAE/C,MAAM,UAAU,qBAAqB;IACnC,WAAW,GAAG,IAAI,CAAC;IACnB,cAAc,EAAE,yBAAyB,CAAC,MAAM,EAAE,CAAC;AACrD,CAAC;AAID,MAAM,UAAU,aAAa,CAC3B,OAAiC,EACjC,gBAAkC;IAElC,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAc,CAAC;IAEtC,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,WAAW,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,EAAE;QACnF,
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"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","sourcesContent":["import AppMetrics from 'expo-app-metrics';\n\nimport { optionalRouter } from './router';\nimport { type RouterIntegrationStorage } from './storage';\n\n// TODO(@ubax): split this module into `.native.ts` / `.web.ts` variants so the\n// web bundle doesn't pull in `expo-app-metrics`' native bridge calls. The web\n// version should be an explicit no-op (return a noop cleanup) rather than\n// relying on the web stubs in `expo-app-metrics/module.web.ts`.\n\nlet initialized = false;\n\nexport const isInitialized = () => initialized;\n\nexport function initRouterIntegration() {\n initialized = true;\n optionalRouter?.unstable_navigationEvents.enable();\n}\n\ntype NavigationEvents = NonNullable<typeof optionalRouter>['unstable_navigationEvents'];\n\nexport function initListeners(\n storage: RouterIntegrationStorage,\n navigationEvents: NavigationEvents\n): () => void {\n const appLaunchTime = performance.now();\n const cleanup = new Set<() => void>();\n\n const unsubscribeAction = navigationEvents.addListener('actionDispatched', (event) => {\n // PRELOAD comes from router.prefetch() — a route warm-up, not a user\n // navigation — so it must not seed dispatchTime.\n if (event.actionType === 'PRELOAD') return;\n storage.pendingActions.push({\n actionType: event.actionType,\n dispatchTime: performance.now(),\n });\n });\n cleanup.add(unsubscribeAction);\n\n const unsubscribePreload = navigationEvents.addListener('pagePreloaded', (e) => {\n // The screen rendered as part of a preload. Mark it as already rendered so\n // the eventual `pageFocused` resolves to `warm_ttr` rather than `cold_ttr`.\n storage.renderedScreensIds.add(e.screenId);\n });\n cleanup.add(unsubscribePreload);\n\n const unsubscribeFocus = navigationEvents.addListener('pageFocused', async (e) => {\n // Snapshot both clocks once so every metric written below is stamped with\n // the moment the focus event fired, not the moment `addCustomMetricToSession`\n // happens to run after the awaited `getMainSession()` round-trip.\n const now = performance.now();\n const timestamp = new Date().toISOString();\n const isInitial = !storage.renderedScreensIds.has(e.screenId);\n storage.renderedScreensIds.add(e.screenId);\n const name = isInitial ? 'cold_ttr' : 'warm_ttr';\n const mainSessionId = (await AppMetrics.getMainSession())?.id;\n if (!mainSessionId) {\n return;\n }\n\n if (!storage.hasRecordedInitialTtr) {\n // Stored in seconds to match the OTel `unit = \"s\"` convention\n const appLaunchTtrSeconds = (now - appLaunchTime) / 1000;\n storage.hasRecordedInitialTtr = true;\n AppMetrics.addCustomMetricToSession({\n sessionId: mainSessionId,\n timestamp,\n category: 'navigation',\n name,\n routeName: e.pathname,\n value: appLaunchTtrSeconds,\n params: { isAppLaunch: true, routeParams: e.params },\n });\n return;\n }\n\n if (storage.pendingActions.length === 0) return;\n\n const last = storage.pendingActions[storage.pendingActions.length - 1];\n if (last) {\n const dispatchTime = last.dispatchTime;\n storage.screenTimes[e.screenId] = {\n ...storage.screenTimes[e.screenId],\n dispatchTime,\n };\n\n AppMetrics.addCustomMetricToSession({\n sessionId: mainSessionId,\n timestamp,\n category: 'navigation',\n name,\n routeName: e.pathname,\n value: (now - dispatchTime) / 1000,\n params: { isAppLaunch: false, routeParams: e.params },\n });\n }\n storage.pendingActions.length = 0;\n });\n cleanup.add(unsubscribeFocus);\n\n return () => {\n cleanup.forEach((c) => c());\n cleanup.clear();\n };\n}\n"]}
|
package/expo-module.config.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoAppMetrics
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Resets a dispatch cursor to `-1` if it has fallen past the largest id currently in its source
|
|
7
|
+
table. The cursors live in UserDefaults; their source tables can be wiped from underneath them
|
|
8
|
+
(notably on a schema-version mismatch in `expo-app-metrics`). Without this check the cursor would
|
|
9
|
+
skip every new row until enough accumulated to pass the stale value.
|
|
10
|
+
|
|
11
|
+
- `signalName`: short human-readable label ("metric" / "log") for log messages.
|
|
12
|
+
- `readCursor`: returns the persisted cursor value.
|
|
13
|
+
- `writeCursor`: persists a new cursor value.
|
|
14
|
+
- `readMaxId`: returns the largest id in the source table, or nil when empty.
|
|
15
|
+
*/
|
|
16
|
+
@AppMetricsActor
|
|
17
|
+
internal func repairCursorIfStale(
|
|
18
|
+
signalName: String,
|
|
19
|
+
readCursor: () -> Int64,
|
|
20
|
+
writeCursor: (Int64) -> Void,
|
|
21
|
+
readMaxId: () throws -> Int64?
|
|
22
|
+
) {
|
|
23
|
+
let cursor = readCursor()
|
|
24
|
+
let maxId: Int64?
|
|
25
|
+
do {
|
|
26
|
+
maxId = try readMaxId()
|
|
27
|
+
} catch {
|
|
28
|
+
observeLogger.warn("[Observe] Failed to read max \(signalName) id while repairing cursor: \(error.localizedDescription)")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if cursor > (maxId ?? -1) {
|
|
32
|
+
observeLogger.info("[Observe] Resetting stale \(signalName) dispatch cursor (was \(cursor), max id is \(maxId.map(String.init) ?? "<empty>"))")
|
|
33
|
+
writeCursor(-1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@AppMetricsActor
|
|
38
|
+
internal func repairMetricCursorIfStale() {
|
|
39
|
+
repairCursorIfStale(
|
|
40
|
+
signalName: "metric",
|
|
41
|
+
readCursor: { ObserveUserDefaults.lastDispatchedMetricId },
|
|
42
|
+
writeCursor: { ObserveUserDefaults.lastDispatchedMetricId = $0 },
|
|
43
|
+
readMaxId: { try AppMetrics.getMaxMetricId() }
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@AppMetricsActor
|
|
48
|
+
internal func repairLogCursorIfStale() {
|
|
49
|
+
repairCursorIfStale(
|
|
50
|
+
signalName: "log",
|
|
51
|
+
readCursor: { ObserveUserDefaults.lastDispatchedLogId },
|
|
52
|
+
writeCursor: { ObserveUserDefaults.lastDispatchedLogId = $0 },
|
|
53
|
+
readMaxId: { try AppMetrics.getMaxLogId() }
|
|
54
|
+
)
|
|
55
|
+
}
|
package/ios/Event.swift
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import ExpoAppMetrics
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import Foundation
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
An object representing an event providing some app metrics and the information about the app and the device.
|
|
@@ -64,58 +66,72 @@ struct Event: Codable, Sendable {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
|
-
|
|
69
|
+
Builds an `Event` from a session row plus its metric/log batch. The session row carries all the
|
|
70
|
+
metadata that used to live on `Entry`/`AppInfo`/`DeviceInfo`; metrics and logs are passed
|
|
71
|
+
separately so a partial dispatch (only the rows past a cursor) can still produce a valid event.
|
|
68
72
|
*/
|
|
69
|
-
static func
|
|
73
|
+
static func from(session: SessionRow, metrics: [MetricRow], logs: [LogRow]) -> Event {
|
|
74
|
+
let updatesInfo = AppInfo.UpdatesInfo(
|
|
75
|
+
updateId: session.appUpdateId,
|
|
76
|
+
runtimeVersion: session.appUpdateRuntimeVersion,
|
|
77
|
+
requestHeaders: decodeRequestHeaders(session.appUpdateRequestHeaders)
|
|
78
|
+
)
|
|
70
79
|
return Event(
|
|
71
80
|
metadata: Metadata(
|
|
72
|
-
appName:
|
|
73
|
-
appIdentifier:
|
|
74
|
-
appVersion:
|
|
75
|
-
appBuildNumber:
|
|
76
|
-
appEasBuildId:
|
|
77
|
-
appUpdatesInfo:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
reactNativeVersion: app.reactNativeVersion,
|
|
85
|
-
expoSdkVersion: app.expoSdkVersion,
|
|
81
|
+
appName: session.appName,
|
|
82
|
+
appIdentifier: session.appIdentifier,
|
|
83
|
+
appVersion: session.appVersion,
|
|
84
|
+
appBuildNumber: session.appBuildNumber,
|
|
85
|
+
appEasBuildId: session.appEasBuildId,
|
|
86
|
+
appUpdatesInfo: updatesInfo.isEmpty ? nil : updatesInfo,
|
|
87
|
+
deviceName: session.deviceName ?? "",
|
|
88
|
+
deviceModel: session.deviceModel ?? "",
|
|
89
|
+
deviceOs: session.deviceOs ?? "",
|
|
90
|
+
deviceOsVersion: session.deviceOsVersion ?? "",
|
|
91
|
+
reactNativeVersion: session.reactNativeVersion ?? "",
|
|
92
|
+
expoSdkVersion: session.expoSdkVersion ?? "",
|
|
86
93
|
clientVersion: ObserveVersions.clientVersion,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
environment: environment
|
|
94
|
+
languageTag: session.languageTag ?? Locale.preferredLanguages.first ?? "en-US",
|
|
95
|
+
environment: session.environment
|
|
90
96
|
),
|
|
91
|
-
metrics:
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
}
|
|
97
|
+
metrics: metrics.map { metric in
|
|
98
|
+
return Metric(
|
|
99
|
+
category: metric.category,
|
|
100
|
+
name: metric.name,
|
|
101
|
+
value: metric.value,
|
|
102
|
+
timestamp: metric.timestamp,
|
|
103
|
+
sessionId: metric.sessionId,
|
|
104
|
+
parentSessionId: nil,
|
|
105
|
+
routeName: metric.routeName,
|
|
106
|
+
updateId: metric.updateId,
|
|
107
|
+
customParams: decodeCustomParams(metric.params)
|
|
108
|
+
)
|
|
105
109
|
},
|
|
106
|
-
logs:
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
}
|
|
110
|
+
logs: logs.map { log in
|
|
111
|
+
return Log(
|
|
112
|
+
name: log.name,
|
|
113
|
+
body: log.body,
|
|
114
|
+
timestamp: log.timestamp,
|
|
115
|
+
severity: Severity(rawValue: log.severity) ?? .info,
|
|
116
|
+
attributes: decodeCustomParams(log.attributes),
|
|
117
|
+
droppedAttributesCount: log.droppedAttributesCount,
|
|
118
|
+
sessionId: log.sessionId
|
|
119
|
+
)
|
|
118
120
|
}
|
|
119
121
|
)
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
|
|
125
|
+
private func decodeRequestHeaders(_ json: String?) -> [String: String]? {
|
|
126
|
+
guard let json, let data = json.data(using: .utf8) else {
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
return try? JSONSerialization.jsonObject(with: data) as? [String: String]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private func decodeCustomParams(_ json: String?) -> AnyCodable? {
|
|
133
|
+
guard let json, let data = json.data(using: .utf8) else {
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
return try? JSONDecoder().decode(AnyCodable.self, from: data)
|
|
137
|
+
}
|
package/ios/Observability.swift
CHANGED
|
@@ -10,155 +10,156 @@ internal struct ObservabilityManager {
|
|
|
10
10
|
private static var projectId: String? = nil
|
|
11
11
|
private static var useOpenTelemetry = false
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
Returns entries from AppMetrics storage whose id is newer than the supplied
|
|
15
|
-
cursor.
|
|
16
|
-
|
|
17
|
-
The first (current) entry may have a lower id than the cursor when storage
|
|
18
|
-
was wiped, empty, or failed to decode — ids restart from 0 in that case.
|
|
19
|
-
We return all entries so the cursor can be repaired on the next dispatch.
|
|
20
|
-
*/
|
|
21
|
-
private static func entriesNewerThan(cursor: Int) -> [MetricsStorage.Entry] {
|
|
22
|
-
let entries = AppMetrics.storage.getAllEntries()
|
|
23
|
-
if let firstEntry = entries.first, firstEntry.id < cursor {
|
|
24
|
-
return entries
|
|
25
|
-
}
|
|
26
|
-
return entries.filter { $0.id > cursor }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
Entries whose metrics have not been dispatched yet.
|
|
31
|
-
*/
|
|
32
|
-
internal static func getEntriesToDispatch() -> [MetricsStorage.Entry] {
|
|
33
|
-
return entriesNewerThan(cursor: ObserveUserDefaults.lastDispatchedEntryId)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
Entries whose logs have not been dispatched yet. Tracked independently from
|
|
38
|
-
metrics so the two signals can advance in isolation.
|
|
39
|
-
*/
|
|
40
|
-
internal static func getLogEntriesToDispatch() -> [MetricsStorage.Entry] {
|
|
41
|
-
return entriesNewerThan(cursor: ObserveUserDefaults.lastDispatchedLogEntryId)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
13
|
internal static func dispatch() async {
|
|
45
|
-
// Compute once and reuse for both signals — `shouldDispatch()` reads the
|
|
46
|
-
//
|
|
47
|
-
// Both halves of dispatch want the same answer.
|
|
14
|
+
// Compute once and reuse for both signals — `shouldDispatch()` reads the persisted config, the
|
|
15
|
+
// bundle defaults, and computes a sample-rate hash. Both halves of dispatch want the same answer.
|
|
48
16
|
let shouldDispatch = Self.shouldDispatch()
|
|
49
17
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// `Event.create` work for sessions that have both signals pending.
|
|
53
|
-
let allEntries = AppMetrics.storage.getAllEntries()
|
|
54
|
-
let eventsByEntryId: [Int: Event] = Dictionary(
|
|
55
|
-
uniqueKeysWithValues: allEntries.map { entry in
|
|
56
|
-
(
|
|
57
|
-
entry.id,
|
|
58
|
-
Event.create(
|
|
59
|
-
app: entry.app, device: entry.device, sessions: entry.sessions,
|
|
60
|
-
environment: entry.environment
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
await dispatchMetrics(eventsByEntryId: eventsByEntryId, shouldDispatch: shouldDispatch)
|
|
67
|
-
await dispatchLogs(eventsByEntryId: eventsByEntryId, shouldDispatch: shouldDispatch)
|
|
18
|
+
await dispatchMetrics(shouldDispatch: shouldDispatch)
|
|
19
|
+
await dispatchLogs(shouldDispatch: shouldDispatch)
|
|
68
20
|
}
|
|
69
21
|
|
|
70
|
-
private static func dispatchMetrics(
|
|
71
|
-
|
|
72
|
-
shouldDispatch: Bool
|
|
73
|
-
) async {
|
|
74
|
-
let entries = getEntriesToDispatch()
|
|
22
|
+
private static func dispatchMetrics(shouldDispatch: Bool) async {
|
|
23
|
+
repairMetricCursorIfStale()
|
|
75
24
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
25
|
+
let cursor = ObserveUserDefaults.lastDispatchedMetricId
|
|
26
|
+
let pendingMetrics: [MetricRow]
|
|
27
|
+
do {
|
|
28
|
+
pendingMetrics = try AppMetrics.getMetrics(afterId: cursor)
|
|
29
|
+
} catch {
|
|
30
|
+
observeLogger.warn("[EAS Observe] Failed to read pending metrics: \(error.localizedDescription)")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
guard !pendingMetrics.isEmpty, let endpointUrl = metricsEndpointUrl else {
|
|
34
|
+
observeLogger.debug("[EAS Observe] No new metrics to dispatch")
|
|
79
35
|
return
|
|
80
36
|
}
|
|
37
|
+
let highestId = pendingMetrics.last?.id ?? cursor
|
|
81
38
|
if !shouldDispatch {
|
|
82
|
-
ObserveUserDefaults.
|
|
39
|
+
ObserveUserDefaults.lastDispatchedMetricId = highestId
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
let events: [Event]
|
|
43
|
+
do {
|
|
44
|
+
events = try buildEvents(forMetrics: pendingMetrics)
|
|
45
|
+
} catch {
|
|
46
|
+
observeLogger.warn("[EAS Observe] Failed to assemble metric events: \(error.localizedDescription)")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if events.isEmpty {
|
|
50
|
+
ObserveUserDefaults.lastDispatchedMetricId = highestId
|
|
83
51
|
return
|
|
84
52
|
}
|
|
85
53
|
do {
|
|
86
|
-
let events = entries.compactMap { eventsByEntryId[$0.id] }
|
|
87
|
-
|
|
88
|
-
if events.isEmpty {
|
|
89
|
-
ObserveUserDefaults.lastDispatchedEntryId = entries.first?.id ?? -1
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
54
|
let body: any Encodable
|
|
94
55
|
if useOpenTelemetry {
|
|
95
|
-
body = OTRequestBody(resourceMetrics: events.map { $0.toOTEvent(easClientId)})
|
|
56
|
+
body = OTRequestBody(resourceMetrics: events.map { $0.toOTEvent(easClientId) })
|
|
96
57
|
} else {
|
|
97
58
|
body = RequestBody(easClientId: easClientId, events: events)
|
|
98
59
|
}
|
|
99
|
-
|
|
100
60
|
let success = try await sendRequest(to: endpointUrl, body: body)
|
|
101
61
|
if success {
|
|
102
62
|
ObserveUserDefaults.lastDispatchDate = Date.now
|
|
103
|
-
ObserveUserDefaults.
|
|
63
|
+
ObserveUserDefaults.lastDispatchedMetricId = highestId
|
|
104
64
|
}
|
|
105
65
|
} catch {
|
|
106
66
|
observeLogger.warn("[EAS Observe] Dispatching the metrics has thrown an error: \(error)")
|
|
107
67
|
}
|
|
108
68
|
}
|
|
109
69
|
|
|
110
|
-
private static func dispatchLogs(
|
|
111
|
-
eventsByEntryId: [Int: Event],
|
|
112
|
-
shouldDispatch: Bool
|
|
113
|
-
) async {
|
|
70
|
+
private static func dispatchLogs(shouldDispatch: Bool) async {
|
|
114
71
|
// Logs are only sent in OpenTelemetry mode — there is no legacy logs endpoint.
|
|
115
72
|
guard useOpenTelemetry else {
|
|
116
73
|
return
|
|
117
74
|
}
|
|
75
|
+
repairLogCursorIfStale()
|
|
118
76
|
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
77
|
+
let cursor = ObserveUserDefaults.lastDispatchedLogId
|
|
78
|
+
let pendingLogs: [LogRow]
|
|
79
|
+
do {
|
|
80
|
+
pendingLogs = try AppMetrics.getLogs(afterId: cursor)
|
|
81
|
+
} catch {
|
|
82
|
+
observeLogger.warn("[EAS Observe] Failed to read pending logs: \(error.localizedDescription)")
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
guard !pendingLogs.isEmpty, let endpointUrl = logsEndpointUrl else {
|
|
86
|
+
observeLogger.debug("[EAS Observe] No new logs to dispatch")
|
|
123
87
|
return
|
|
124
88
|
}
|
|
89
|
+
let highestId = pendingLogs.last?.id ?? cursor
|
|
125
90
|
if !shouldDispatch {
|
|
126
|
-
ObserveUserDefaults.
|
|
91
|
+
ObserveUserDefaults.lastDispatchedLogId = highestId
|
|
127
92
|
return
|
|
128
93
|
}
|
|
94
|
+
let events: [Event]
|
|
129
95
|
do {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if resourceLogs.isEmpty {
|
|
139
|
-
ObserveUserDefaults.lastDispatchedLogEntryId = entries.first?.id ?? -1
|
|
140
|
-
return
|
|
96
|
+
events = try buildEvents(forLogs: pendingLogs)
|
|
97
|
+
} catch {
|
|
98
|
+
observeLogger.warn("[EAS Observe] Failed to assemble log events: \(error.localizedDescription)")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
let resourceLogs = events.compactMap { event -> OTResourceLogs? in
|
|
102
|
+
guard !event.logs.isEmpty else {
|
|
103
|
+
return nil
|
|
141
104
|
}
|
|
142
|
-
|
|
105
|
+
return event.toOTResourceLogs(easClientId)
|
|
106
|
+
}
|
|
107
|
+
if resourceLogs.isEmpty {
|
|
108
|
+
ObserveUserDefaults.lastDispatchedLogId = highestId
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
do {
|
|
143
112
|
let body = OTLogsRequestBody(resourceLogs: resourceLogs)
|
|
144
113
|
let success = try await sendRequest(to: endpointUrl, body: body)
|
|
145
114
|
if success {
|
|
146
|
-
ObserveUserDefaults.
|
|
115
|
+
ObserveUserDefaults.lastDispatchedLogId = highestId
|
|
147
116
|
}
|
|
148
117
|
} catch {
|
|
149
118
|
observeLogger.warn("[EAS Observe] Dispatching the logs has thrown an error: \(error)")
|
|
150
119
|
}
|
|
151
120
|
}
|
|
152
121
|
|
|
122
|
+
/**
|
|
123
|
+
Groups `metrics` by `sessionId`, hydrates the matching session rows, and emits one `Event` per
|
|
124
|
+
session in the same shape Android dispatches: each event carries the session's metadata and only
|
|
125
|
+
the metrics that belong to it.
|
|
126
|
+
*/
|
|
127
|
+
private static func buildEvents(forMetrics metrics: [MetricRow]) throws -> [Event] {
|
|
128
|
+
let metricsBySession = Dictionary(grouping: metrics, by: \.sessionId)
|
|
129
|
+
let sessionIds = Array(metricsBySession.keys)
|
|
130
|
+
let sessions = try AppMetrics.getSessions(ids: sessionIds)
|
|
131
|
+
return sessions.compactMap { session in
|
|
132
|
+
guard let sessionMetrics = metricsBySession[session.id] else {
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
return Event.from(session: session, metrics: sessionMetrics, logs: [])
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static func buildEvents(forLogs logs: [LogRow]) throws -> [Event] {
|
|
140
|
+
let logsBySession = Dictionary(grouping: logs, by: \.sessionId)
|
|
141
|
+
let sessionIds = Array(logsBySession.keys)
|
|
142
|
+
let sessions = try AppMetrics.getSessions(ids: sessionIds)
|
|
143
|
+
return sessions.compactMap { session in
|
|
144
|
+
guard let sessionLogs = logsBySession[session.id] else {
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
return Event.from(session: session, metrics: [], logs: sessionLogs)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
153
151
|
private static func sendRequest(to endpointUrl: URL, body: any Encodable) async throws -> Bool {
|
|
154
152
|
var request = URLRequest(url: endpointUrl)
|
|
155
153
|
request.httpMethod = "POST"
|
|
156
154
|
request.allHTTPHeaderFields = ["Content-Type": "application/json"]
|
|
157
155
|
request.httpBody = try body.toJSONData([])
|
|
158
156
|
|
|
157
|
+
#if DEBUG
|
|
159
158
|
observeLogger.debug("[EAS Observe] Sending the request to \(endpointUrl) with body:")
|
|
160
|
-
// Use `print` so the JSON can be copied without including the log level emojis.
|
|
159
|
+
// Use `print` so the JSON can be copied without including the log level emojis. Wrapped in
|
|
160
|
+
// `#if DEBUG` so release builds don't pay for a second pretty-printed encode of the payload.
|
|
161
161
|
print(try body.toJSONString(.prettyPrinted))
|
|
162
|
+
#endif
|
|
162
163
|
|
|
163
164
|
let (responseData, urlResponse) = try await URLSession.shared.data(for: request)
|
|
164
165
|
|
|
@@ -224,3 +225,4 @@ internal struct ObservabilityManager {
|
|
|
224
225
|
return EASClientID.deterministicUniformValue(EASClientID.uuid()) < clamped
|
|
225
226
|
}
|
|
226
227
|
}
|
|
228
|
+
|
|
@@ -35,8 +35,8 @@ internal final class ObserveUserDefaults: UserDefaults {
|
|
|
35
35
|
Enum with keys used within this user defaults database.
|
|
36
36
|
*/
|
|
37
37
|
private enum Keys: String {
|
|
38
|
-
case
|
|
39
|
-
case
|
|
38
|
+
case lastDispatchedMetricId
|
|
39
|
+
case lastDispatchedLogId
|
|
40
40
|
case lastDispatchDate
|
|
41
41
|
case config
|
|
42
42
|
case bundleDefaults
|
|
@@ -65,29 +65,29 @@ internal final class ObserveUserDefaults: UserDefaults {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
Id of the last dispatched
|
|
69
|
-
|
|
68
|
+
Id of the last metric row dispatched. Each successful dispatch advances this past the largest id
|
|
69
|
+
in the batch so the next dispatch reads only newer rows. Auto-increment ids are monotonic in
|
|
70
|
+
SQLite, so a date-independent cursor avoids drift when the device clock changes.
|
|
70
71
|
*/
|
|
71
|
-
static var
|
|
72
|
+
static var lastDispatchedMetricId: Int64 {
|
|
72
73
|
get {
|
|
73
|
-
return defaults.object(forKey: Keys.
|
|
74
|
+
return (defaults.object(forKey: Keys.lastDispatchedMetricId.rawValue) as? Int64) ?? -1
|
|
74
75
|
}
|
|
75
76
|
set {
|
|
76
|
-
defaults.set(newValue, forKey: Keys.
|
|
77
|
+
defaults.set(newValue, forKey: Keys.lastDispatchedMetricId.rawValue)
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
Id of the last
|
|
82
|
-
|
|
83
|
-
move forward independently.
|
|
82
|
+
Id of the last log row dispatched. Tracked separately from the metric cursor so a logs request
|
|
83
|
+
failure does not block metrics dispatch (and vice versa) — both signals move forward independently.
|
|
84
84
|
*/
|
|
85
|
-
static var
|
|
85
|
+
static var lastDispatchedLogId: Int64 {
|
|
86
86
|
get {
|
|
87
|
-
return defaults.object(forKey: Keys.
|
|
87
|
+
return (defaults.object(forKey: Keys.lastDispatchedLogId.rawValue) as? Int64) ?? -1
|
|
88
88
|
}
|
|
89
89
|
set {
|
|
90
|
-
defaults.set(newValue, forKey: Keys.
|
|
90
|
+
defaults.set(newValue, forKey: Keys.lastDispatchedLogId.rawValue)
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|