expo-observe 56.0.6 → 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.
Files changed (80) hide show
  1. package/android/src/main/java/expo/modules/observe/OpenTelemetry.kt +5 -0
  2. package/build/ObserveProvider.d.ts.map +1 -1
  3. package/build/ObserveProvider.js +3 -2
  4. package/build/ObserveProvider.js.map +1 -1
  5. package/build/integrations/expo-router/ObserveRouterIntegrationProvider.d.ts +5 -0
  6. package/build/integrations/expo-router/ObserveRouterIntegrationProvider.d.ts.map +1 -0
  7. package/build/integrations/expo-router/ObserveRouterIntegrationProvider.js +21 -0
  8. package/build/integrations/expo-router/ObserveRouterIntegrationProvider.js.map +1 -0
  9. package/build/integrations/expo-router/index.d.ts +1 -0
  10. package/build/integrations/expo-router/index.d.ts.map +1 -1
  11. package/build/integrations/expo-router/index.js +1 -0
  12. package/build/integrations/expo-router/index.js.map +1 -1
  13. package/build/integrations/expo-router/init.d.ts +5 -0
  14. package/build/integrations/expo-router/init.d.ts.map +1 -1
  15. package/build/integrations/expo-router/init.js +83 -0
  16. package/build/integrations/expo-router/init.js.map +1 -1
  17. package/build/integrations/expo-router/storage.d.ts +27 -0
  18. package/build/integrations/expo-router/storage.d.ts.map +1 -0
  19. package/build/integrations/expo-router/storage.js +10 -0
  20. package/build/integrations/expo-router/storage.js.map +1 -0
  21. package/build/integrations/expo-router/useObserveForRouter.d.ts.map +1 -1
  22. package/build/integrations/expo-router/useObserveForRouter.js +46 -3
  23. package/build/integrations/expo-router/useObserveForRouter.js.map +1 -1
  24. package/expo-module.config.json +1 -1
  25. package/ios/CursorRepair.swift +55 -0
  26. package/ios/Event.swift +61 -45
  27. package/ios/Observability.swift +96 -94
  28. package/ios/ObserveUserDefaults.swift +13 -13
  29. package/ios/OpenTelemetry.swift +29 -18
  30. package/ios/Tests/CursorRepairTests.swift +94 -0
  31. package/ios/Tests/OTAnyValueTests.swift +37 -5
  32. package/ios/Tests/OpenTelemetryTests.swift +30 -1
  33. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6-sources.jar → 56.0.8/expo.modules.observe-56.0.8-sources.jar} +0 -0
  34. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.md5 +1 -0
  35. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.sha1 +1 -0
  36. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.sha256 +1 -0
  37. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8-sources.jar.sha512 +1 -0
  38. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6.aar → 56.0.8/expo.modules.observe-56.0.8.aar} +0 -0
  39. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.md5 +1 -0
  40. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.sha1 +1 -0
  41. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.sha256 +1 -0
  42. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.aar.sha512 +1 -0
  43. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6.module → 56.0.8/expo.modules.observe-56.0.8.module} +23 -23
  44. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.md5 +1 -0
  45. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.sha1 +1 -0
  46. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.sha256 +1 -0
  47. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.module.sha512 +1 -0
  48. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6.pom → 56.0.8/expo.modules.observe-56.0.8.pom} +2 -2
  49. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.md5 +1 -0
  50. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.sha1 +1 -0
  51. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.sha256 +1 -0
  52. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.8/expo.modules.observe-56.0.8.pom.sha512 +1 -0
  53. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml +4 -4
  54. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.md5 +1 -1
  55. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha1 +1 -1
  56. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha256 +1 -1
  57. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha512 +1 -1
  58. package/package.json +4 -4
  59. package/src/ObserveProvider.tsx +3 -1
  60. package/src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx +32 -0
  61. package/src/integrations/expo-router/index.ts +1 -0
  62. package/src/integrations/expo-router/init.ts +98 -0
  63. package/src/integrations/expo-router/storage.ts +38 -0
  64. package/src/integrations/expo-router/useObserveForRouter.ts +55 -3
  65. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.md5 +0 -1
  66. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha1 +0 -1
  67. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha256 +0 -1
  68. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha512 +0 -1
  69. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.md5 +0 -1
  70. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha1 +0 -1
  71. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha256 +0 -1
  72. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha512 +0 -1
  73. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.md5 +0 -1
  74. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha1 +0 -1
  75. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha256 +0 -1
  76. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha512 +0 -1
  77. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.md5 +0 -1
  78. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha1 +0 -1
  79. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha256 +0 -1
  80. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha512 +0 -1
@@ -189,6 +189,11 @@ private val metricNameMap = mapOf(
189
189
 
190
190
  // Updates
191
191
  (MetricCategory.Updates.categoryName to "updateDownloadTime") to "expo.updates.download_time",
192
+
193
+ // Navigation
194
+ (MetricCategory.Navigation.categoryName to "cold_ttr") to "expo.navigation.cold_ttr",
195
+ (MetricCategory.Navigation.categoryName to "warm_ttr") to "expo.navigation.warm_ttr",
196
+ (MetricCategory.Navigation.categoryName to "tti") to "expo.navigation.tti"
192
197
  )
193
198
 
194
199
  fun EASMetric.toOTMetric(): OTMetric {
@@ -1 +1 @@
1
- {"version":3,"file":"ObserveProvider.d.ts","sourceRoot":"","sources":["../src/ObserveProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE/C,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CAE9D"}
1
+ {"version":3,"file":"ObserveProvider.d.ts","sourceRoot":"","sources":["../src/ObserveProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAI/C,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CAE9D"}
@@ -1,6 +1,7 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import {} from 'react';
3
+ import { ObserveRouterIntegrationProvider } from './integrations/expo-router/ObserveRouterIntegrationProvider';
3
4
  export function ObserveProvider({ children }) {
4
- return _jsx(_Fragment, { children: children });
5
+ return _jsx(ObserveRouterIntegrationProvider, { children: children });
5
6
  }
6
7
  //# sourceMappingURL=ObserveProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ObserveProvider.js","sourceRoot":"","sources":["../src/ObserveProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAA0B,MAAM,OAAO,CAAC;AAE/C,MAAM,UAAU,eAAe,CAAC,EAAE,QAAQ,EAAqB;IAC7D,OAAO,4BAAG,QAAQ,GAAI,CAAC;AACzB,CAAC","sourcesContent":["import { type PropsWithChildren } from 'react';\n\nexport function ObserveProvider({ children }: PropsWithChildren) {\n return <>{children}</>;\n}\n"]}
1
+ {"version":3,"file":"ObserveProvider.js","sourceRoot":"","sources":["../src/ObserveProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAA0B,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,gCAAgC,EAAE,MAAM,6DAA6D,CAAC;AAE/G,MAAM,UAAU,eAAe,CAAC,EAAE,QAAQ,EAAqB;IAC7D,OAAO,KAAC,gCAAgC,cAAE,QAAQ,GAAoC,CAAC;AACzF,CAAC","sourcesContent":["import { type PropsWithChildren } from 'react';\n\nimport { ObserveRouterIntegrationProvider } from './integrations/expo-router/ObserveRouterIntegrationProvider';\n\nexport function ObserveProvider({ children }: PropsWithChildren) {\n return <ObserveRouterIntegrationProvider>{children}</ObserveRouterIntegrationProvider>;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import { type PropsWithChildren } from 'react';
2
+ import { type RouterIntegrationStorage } from './storage';
3
+ export declare const ObserveRouterIntegrationContext: import("react").Context<RouterIntegrationStorage | null>;
4
+ export declare function ObserveRouterIntegrationProvider({ children }: PropsWithChildren): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=ObserveRouterIntegrationProvider.d.ts.map
@@ -0,0 +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,2CAuB/E"}
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useEffect, useRef, useState } from 'react';
3
+ import { initListeners, isInitialized } from './init';
4
+ import { optionalRouter } from './router';
5
+ import { createRouterIntegrationStorage } from './storage';
6
+ export const ObserveRouterIntegrationContext = createContext(null);
7
+ export function ObserveRouterIntegrationProvider({ children }) {
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
+ });
14
+ const prevInitialized = useRef(isInitialized());
15
+ if (prevInitialized.current !== isInitialized()) {
16
+ throw new Error(`[expo-observe] Router integration was ${isInitialized() ? 'enabled' : 'disabled'} after application mounted. Call ExpoObserve.configure() before mounting AppMetricsRoot.`);
17
+ }
18
+ useEffect(() => listenersCleanup, [listenersCleanup]);
19
+ return (_jsx(ObserveRouterIntegrationContext.Provider, { value: storage, children: children }));
20
+ }
21
+ //# sourceMappingURL=ObserveRouterIntegrationProvider.js.map
@@ -0,0 +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;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,4 +1,5 @@
1
1
  export { isRouterInstalled } from './router';
2
2
  export { useObserveForRouter } from './useObserveForRouter';
3
3
  export { initRouterIntegration } from './init';
4
+ export { ObserveRouterIntegrationProvider } from './ObserveRouterIntegrationProvider';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { isRouterInstalled } from './router';
2
2
  export { useObserveForRouter } from './useObserveForRouter';
3
3
  export { initRouterIntegration } from './init';
4
+ export { ObserveRouterIntegrationProvider } from './ObserveRouterIntegrationProvider';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC","sourcesContent":["export { isRouterInstalled } from './router';\nexport { useObserveForRouter } from './useObserveForRouter';\nexport { initRouterIntegration } from './init';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC","sourcesContent":["export { isRouterInstalled } from './router';\nexport { useObserveForRouter } from './useObserveForRouter';\nexport { initRouterIntegration } from './init';\nexport { ObserveRouterIntegrationProvider } from './ObserveRouterIntegrationProvider';\n"]}
@@ -1,3 +1,8 @@
1
+ import { optionalRouter } from './router';
2
+ import { type RouterIntegrationStorage } from './storage';
1
3
  export declare const isInitialized: () => boolean;
2
4
  export declare function initRouterIntegration(): void;
5
+ type NavigationEvents = NonNullable<typeof optionalRouter>['unstable_navigationEvents'];
6
+ export declare function initListeners(storage: RouterIntegrationStorage, navigationEvents: NavigationEvents): () => void;
7
+ export {};
3
8
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,eAAoB,CAAC;AAE/C,wBAAgB,qBAAqB,SAEpC"}
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"}
@@ -1,6 +1,89 @@
1
+ import AppMetrics from 'expo-app-metrics';
2
+ import { optionalRouter } from './router';
3
+ import {} from './storage';
4
+ // TODO(@ubax): split this module into `.native.ts` / `.web.ts` variants so the
5
+ // web bundle doesn't pull in `expo-app-metrics`' native bridge calls. The web
6
+ // version should be an explicit no-op (return a noop cleanup) rather than
7
+ // relying on the web stubs in `expo-app-metrics/module.web.ts`.
1
8
  let initialized = false;
2
9
  export const isInitialized = () => initialized;
3
10
  export function initRouterIntegration() {
4
11
  initialized = true;
12
+ optionalRouter?.unstable_navigationEvents.enable();
13
+ }
14
+ export function initListeners(storage, navigationEvents) {
15
+ const appLaunchTime = performance.now();
16
+ const cleanup = new Set();
17
+ const unsubscribeAction = navigationEvents.addListener('actionDispatched', (event) => {
18
+ // PRELOAD comes from router.prefetch() — a route warm-up, not a user
19
+ // navigation — so it must not seed dispatchTime.
20
+ if (event.actionType === 'PRELOAD')
21
+ return;
22
+ storage.pendingActions.push({
23
+ actionType: event.actionType,
24
+ dispatchTime: performance.now(),
25
+ });
26
+ });
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);
34
+ const unsubscribeFocus = navigationEvents.addListener('pageFocused', async (e) => {
35
+ // Snapshot both clocks once so every metric written below is stamped with
36
+ // the moment the focus event fired, not the moment `addCustomMetricToSession`
37
+ // happens to run after the awaited `getMainSession()` round-trip.
38
+ const now = performance.now();
39
+ const timestamp = new Date().toISOString();
40
+ const isInitial = !storage.renderedScreensIds.has(e.screenId);
41
+ storage.renderedScreensIds.add(e.screenId);
42
+ const name = isInitial ? 'cold_ttr' : 'warm_ttr';
43
+ const mainSessionId = (await AppMetrics.getMainSession())?.id;
44
+ if (!mainSessionId) {
45
+ return;
46
+ }
47
+ if (!storage.hasRecordedInitialTtr) {
48
+ // Stored in seconds to match the OTel `unit = "s"` convention
49
+ const appLaunchTtrSeconds = (now - appLaunchTime) / 1000;
50
+ storage.hasRecordedInitialTtr = true;
51
+ AppMetrics.addCustomMetricToSession({
52
+ sessionId: mainSessionId,
53
+ timestamp,
54
+ category: 'navigation',
55
+ name,
56
+ routeName: e.pathname,
57
+ value: appLaunchTtrSeconds,
58
+ params: { isAppLaunch: true, routeParams: e.params },
59
+ });
60
+ return;
61
+ }
62
+ if (storage.pendingActions.length === 0)
63
+ return;
64
+ const last = storage.pendingActions[storage.pendingActions.length - 1];
65
+ if (last) {
66
+ const dispatchTime = last.dispatchTime;
67
+ storage.screenTimes[e.screenId] = {
68
+ ...storage.screenTimes[e.screenId],
69
+ dispatchTime,
70
+ };
71
+ AppMetrics.addCustomMetricToSession({
72
+ sessionId: mainSessionId,
73
+ timestamp,
74
+ category: 'navigation',
75
+ name,
76
+ routeName: e.pathname,
77
+ value: (now - dispatchTime) / 1000,
78
+ params: { isAppLaunch: false, routeParams: e.params },
79
+ });
80
+ }
81
+ storage.pendingActions.length = 0;
82
+ });
83
+ cleanup.add(unsubscribeFocus);
84
+ return () => {
85
+ cleanup.forEach((c) => c());
86
+ cleanup.clear();
87
+ };
5
88
  }
6
89
  //# sourceMappingURL=init.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAAA,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;AACrB,CAAC","sourcesContent":["let initialized = false;\n\nexport const isInitialized = () => initialized;\n\nexport function initRouterIntegration() {\n initialized = true;\n}\n"]}
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"]}
@@ -0,0 +1,27 @@
1
+ import type { ActionDispatchedEvent } from 'expo-router';
2
+ export interface ScreenTimes {
3
+ dispatchTime: number;
4
+ lastInteractiveCall?: number;
5
+ }
6
+ export interface PendingAction {
7
+ actionType: ActionDispatchedEvent['actionType'];
8
+ dispatchTime: number;
9
+ }
10
+ export interface RouterIntegrationStorage {
11
+ /**
12
+ * Actions dispatched, but not yet processed by the integration
13
+ */
14
+ pendingActions: PendingAction[];
15
+ renderedScreensIds: Set<string>;
16
+ /**
17
+ * Wether the app had already recorded the first render of the screen
18
+ */
19
+ hasRecordedInitialTtr: boolean;
20
+ /**
21
+ * Times used to calculate spans from dispatch to certain event
22
+ */
23
+ screenTimes: Record<string, ScreenTimes>;
24
+ interactiveScreensIds: Set<string>;
25
+ }
26
+ export declare function createRouterIntegrationStorage(): RouterIntegrationStorage;
27
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAChD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC;;OAEG;IACH,qBAAqB,EAAE,OAAO,CAAC;IAC/B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACzC,qBAAqB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,IAAI,wBAAwB,CAQzE"}
@@ -0,0 +1,10 @@
1
+ export function createRouterIntegrationStorage() {
2
+ return {
3
+ pendingActions: [],
4
+ renderedScreensIds: new Set(),
5
+ hasRecordedInitialTtr: false,
6
+ screenTimes: {},
7
+ interactiveScreensIds: new Set(),
8
+ };
9
+ }
10
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/storage.ts"],"names":[],"mappings":"AA6BA,MAAM,UAAU,8BAA8B;IAC5C,OAAO;QACL,cAAc,EAAE,EAAE;QAClB,kBAAkB,EAAE,IAAI,GAAG,EAAE;QAC7B,qBAAqB,EAAE,KAAK;QAC5B,WAAW,EAAE,EAAE;QACf,qBAAqB,EAAE,IAAI,GAAG,EAAE;KACjC,CAAC;AACJ,CAAC","sourcesContent":["import type { ActionDispatchedEvent } from 'expo-router';\n\nexport interface ScreenTimes {\n dispatchTime: number;\n lastInteractiveCall?: number;\n}\n\nexport interface PendingAction {\n actionType: ActionDispatchedEvent['actionType'];\n dispatchTime: number;\n}\n\nexport interface RouterIntegrationStorage {\n /**\n * Actions dispatched, but not yet processed by the integration\n */\n pendingActions: PendingAction[];\n renderedScreensIds: Set<string>;\n /**\n * Wether the app had already recorded the first render of the screen\n */\n hasRecordedInitialTtr: boolean;\n /**\n * Times used to calculate spans from dispatch to certain event\n */\n screenTimes: Record<string, ScreenTimes>;\n interactiveScreensIds: Set<string>;\n}\n\nexport function createRouterIntegrationStorage(): RouterIntegrationStorage {\n return {\n pendingActions: [],\n renderedScreensIds: new Set(),\n hasRecordedInitialTtr: false,\n screenTimes: {},\n interactiveScreensIds: new Set(),\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useObserveForRouter.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/useObserveForRouter.ts"],"names":[],"mappings":"AAAA,OAAO,UAAqC,MAAM,kBAAkB,CAAC;AAMrE,KAAK,eAAe,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAE9D,wBAAgB,mBAAmB,IAAI,eAAe,GAAG,IAAI,CAwD5D"}
1
+ {"version":3,"file":"useObserveForRouter.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/useObserveForRouter.ts"],"names":[],"mappings":"AAAA,OAAO,UAAqC,MAAM,kBAAkB,CAAC;AAOrE,KAAK,eAAe,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAE9D,wBAAgB,mBAAmB,IAAI,eAAe,GAAG,IAAI,CA2G5D"}
@@ -1,12 +1,15 @@
1
1
  import AppMetrics, {} from 'expo-app-metrics';
2
- import { useCallback, useEffect, useRef } from 'react';
2
+ import { use, useCallback, useEffect, useRef } from 'react';
3
+ import { ObserveRouterIntegrationContext } from './ObserveRouterIntegrationProvider';
3
4
  import { isInitialized } from './init';
4
5
  import { optionalRouter } from './router';
5
6
  export function useObserveForRouter() {
7
+ const storage = use(ObserveRouterIntegrationContext);
6
8
  const isMounted = useRef(true);
7
9
  const route = optionalRouter?.useRoute();
8
10
  const navigation = optionalRouter?.useNavigation();
9
- const pathname = optionalRouter?.useCurrentRouteInfo()?.pathname;
11
+ const routeInfo = optionalRouter?.useCurrentRouteInfo();
12
+ const { pathname, params: routeParams } = routeInfo ?? {};
10
13
  const initializedAtMount = useRef(isInitialized());
11
14
  if (initializedAtMount.current !== isInitialized()) {
12
15
  throw new Error("[expo-observe] Router integration was toggled during a screen's lifecycle. " +
@@ -28,6 +31,8 @@ export function useObserveForRouter() {
28
31
  };
29
32
  }, []);
30
33
  const markInteractive = useCallback(async (attributes) => {
34
+ const now = performance.now();
35
+ const timestamp = new Date().toISOString();
31
36
  if (!isMounted.current) {
32
37
  console.warn('[expo-observe] Calling markInteractive on unmounted screen');
33
38
  return;
@@ -42,7 +47,45 @@ export function useObserveForRouter() {
42
47
  routeName: pathname,
43
48
  });
44
49
  }
45
- }, [screenId, navigation, pathname]);
50
+ if (!storage) {
51
+ throw new Error('[expo-observe] markInteractive was called without an active ObserveProvider. Wrap your app in ObserveRoot from expo-observe.');
52
+ }
53
+ // Snapshot times BEFORE writing the new interactive timestamp so the
54
+ // duplicate-detection logic below sees the *previous* call, not this one.
55
+ const currentScreenData = storage.screenTimes[screenId];
56
+ storage.interactiveScreensIds.add(screenId);
57
+ if (storage.screenTimes[screenId]) {
58
+ storage.screenTimes[screenId] = {
59
+ ...storage.screenTimes[screenId],
60
+ lastInteractiveCall: now,
61
+ };
62
+ }
63
+ if (!currentScreenData?.dispatchTime)
64
+ return;
65
+ const previousInteractiveCall = currentScreenData.lastInteractiveCall;
66
+ const previousWasAfterDispatch = previousInteractiveCall != null && currentScreenData.dispatchTime < previousInteractiveCall;
67
+ if (previousWasAfterDispatch) {
68
+ // We only want to record interactive once per navigation
69
+ return;
70
+ }
71
+ // Stored in seconds to match the OTel `unit = "s"` convention
72
+ const interactiveTimeSeconds = (now - currentScreenData.dispatchTime) / 1000;
73
+ const mainSessionId = (await AppMetrics.getMainSession())?.id;
74
+ // TODO(@ubax): we should count the time against the action which caused the first navigation
75
+ // and add a param stating if during that time there was any navigation
76
+ if (mainSessionId) {
77
+ await AppMetrics.addCustomMetricToSession({
78
+ sessionId: mainSessionId,
79
+ timestamp,
80
+ category: 'navigation',
81
+ // TODO(@ubax): Use segments.join here to get full routeName and pass pathname and params via params
82
+ routeName: pathname,
83
+ name: 'tti',
84
+ value: interactiveTimeSeconds,
85
+ params: { routeParams },
86
+ });
87
+ }
88
+ }, [screenId, navigation, pathname, storage, routeParams]);
46
89
  return initializedAtMount.current ? markInteractive : null;
47
90
  }
48
91
  //# sourceMappingURL=useObserveForRouter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useObserveForRouter.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/useObserveForRouter.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,EAAE,EAAyB,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAI1C,MAAM,UAAU,mBAAmB;IACjC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,cAAc,EAAE,QAAQ,EAAE,CAAC;IACzC,MAAM,UAAU,GAAG,cAAc,EAAE,aAAa,EAAE,CAAC;IACnD,MAAM,QAAQ,GAAG,cAAc,EAAE,mBAAmB,EAAE,EAAE,QAAQ,CAAC;IAEjE,MAAM,kBAAkB,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IACnD,IAAI,kBAAkB,CAAC,OAAO,KAAK,aAAa,EAAE,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,6EAA6E;YAC3E,sGAAsG,CACzG,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,EAAE,GAAG,CAAC;IAC5B,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,YAAY,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CACV,2FAA2F,CAC5F,CAAC;QACF,YAAY,CAAC,OAAO,GAAG,QAAQ,CAAC;IAClC,CAAC;IAED,SAAS,CAAC,GAAG,EAAE;QACb,4EAA4E;QAC5E,8EAA8E;QAC9E,gEAAgE;QAChE,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;QACzB,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,OAAO,GAAG,KAAK,CAAC;QAC5B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,eAAe,GAAG,WAAW,CACjC,KAAK,EAAE,UAA6B,EAAE,EAAE;QACtC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CACV,sHAAsH,CACvH,CAAC;YACF,OAAO;QACT,CAAC;QACD,IAAI,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC;YAC5B,UAAU,CAAC,eAAe,CAAC;gBACzB,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC;gBACrB,SAAS,EAAE,QAAQ;aACpB,CAAC,CAAC;QACL,CAAC;IACH,CAAC,EACD,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CACjC,CAAC;IAEF,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC","sourcesContent":["import AppMetrics, { type MetricAttributes } from 'expo-app-metrics';\nimport { useCallback, useEffect, useRef } from 'react';\n\nimport { isInitialized } from './init';\nimport { optionalRouter } from './router';\n\ntype MarkInteractive = (typeof AppMetrics)['markInteractive'];\n\nexport function useObserveForRouter(): MarkInteractive | null {\n const isMounted = useRef(true);\n const route = optionalRouter?.useRoute();\n const navigation = optionalRouter?.useNavigation();\n const pathname = optionalRouter?.useCurrentRouteInfo()?.pathname;\n\n const initializedAtMount = useRef(isInitialized());\n if (initializedAtMount.current !== isInitialized()) {\n throw new Error(\n \"[expo-observe] Router integration was toggled during a screen's lifecycle. \" +\n 'Call `ExpoObserve.configure({ disableRouterIntegration })` once at startup before any screen mounts.'\n );\n }\n\n const screenId = route?.key;\n const prevScreenId = useRef(screenId);\n if (prevScreenId.current !== screenId) {\n console.warn(\n '[expo-observe] Screen ID changed between renders. This is most likely an expo-router bug.'\n );\n prevScreenId.current = screenId;\n }\n\n useEffect(() => {\n // Strict-mode mounts the effect twice (mount → cleanup → re-mount). Without\n // restoring isMounted here, the second mount would leave it permanently false\n // and every markInteractive call would warn \"unmounted screen\".\n isMounted.current = true;\n return () => {\n isMounted.current = false;\n };\n }, []);\n\n const markInteractive = useCallback(\n async (attributes?: MetricAttributes) => {\n if (!isMounted.current) {\n console.warn('[expo-observe] Calling markInteractive on unmounted screen');\n return;\n }\n if (!screenId) {\n console.warn(\n '[expo-observe] No metadata available for the current screen. Make sure to call useObserve inside a screen component.'\n );\n return;\n }\n if (navigation?.isFocused()) {\n AppMetrics.markInteractive({\n ...(attributes ?? {}),\n routeName: pathname,\n });\n }\n },\n [screenId, navigation, pathname]\n );\n\n return initializedAtMount.current ? markInteractive : null;\n}\n"]}
1
+ {"version":3,"file":"useObserveForRouter.js","sourceRoot":"","sources":["../../../src/integrations/expo-router/useObserveForRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import AppMetrics, { type MetricAttributes } from 'expo-app-metrics';\nimport { use, useCallback, useEffect, useRef } from 'react';\n\nimport { ObserveRouterIntegrationContext } from './ObserveRouterIntegrationProvider';\nimport { isInitialized } from './init';\nimport { optionalRouter } from './router';\n\ntype MarkInteractive = (typeof AppMetrics)['markInteractive'];\n\nexport function useObserveForRouter(): MarkInteractive | null {\n const storage = use(ObserveRouterIntegrationContext);\n const isMounted = useRef(true);\n const route = optionalRouter?.useRoute();\n const navigation = optionalRouter?.useNavigation();\n const routeInfo = optionalRouter?.useCurrentRouteInfo();\n const { pathname, params: routeParams } = routeInfo ?? {};\n\n const initializedAtMount = useRef(isInitialized());\n if (initializedAtMount.current !== isInitialized()) {\n throw new Error(\n \"[expo-observe] Router integration was toggled during a screen's lifecycle. \" +\n 'Call `ExpoObserve.configure({ disableRouterIntegration })` once at startup before any screen mounts.'\n );\n }\n\n const screenId = route?.key;\n const prevScreenId = useRef(screenId);\n if (prevScreenId.current !== screenId) {\n console.warn(\n '[expo-observe] Screen ID changed between renders. This is most likely an expo-router bug.'\n );\n prevScreenId.current = screenId;\n }\n\n useEffect(() => {\n // Strict-mode mounts the effect twice (mount → cleanup → re-mount). Without\n // restoring isMounted here, the second mount would leave it permanently false\n // and every markInteractive call would warn \"unmounted screen\".\n isMounted.current = true;\n return () => {\n isMounted.current = false;\n };\n }, []);\n\n const markInteractive = useCallback(\n async (attributes?: MetricAttributes) => {\n const now = performance.now();\n const timestamp = new Date().toISOString();\n if (!isMounted.current) {\n console.warn('[expo-observe] Calling markInteractive on unmounted screen');\n return;\n }\n if (!screenId) {\n console.warn(\n '[expo-observe] No metadata available for the current screen. Make sure to call useObserve inside a screen component.'\n );\n return;\n }\n if (navigation?.isFocused()) {\n AppMetrics.markInteractive({\n ...(attributes ?? {}),\n routeName: pathname,\n });\n }\n\n if (!storage) {\n throw new Error(\n '[expo-observe] markInteractive was called without an active ObserveProvider. Wrap your app in ObserveRoot from expo-observe.'\n );\n }\n\n // Snapshot times BEFORE writing the new interactive timestamp so the\n // duplicate-detection logic below sees the *previous* call, not this one.\n const currentScreenData = storage.screenTimes[screenId];\n\n storage.interactiveScreensIds.add(screenId);\n if (storage.screenTimes[screenId]) {\n storage.screenTimes[screenId] = {\n ...storage.screenTimes[screenId],\n lastInteractiveCall: now,\n };\n }\n\n if (!currentScreenData?.dispatchTime) return;\n\n const previousInteractiveCall = currentScreenData.lastInteractiveCall;\n const previousWasAfterDispatch =\n previousInteractiveCall != null && currentScreenData.dispatchTime < previousInteractiveCall;\n\n if (previousWasAfterDispatch) {\n // We only want to record interactive once per navigation\n return;\n }\n\n // Stored in seconds to match the OTel `unit = \"s\"` convention\n const interactiveTimeSeconds = (now - currentScreenData.dispatchTime) / 1000;\n const mainSessionId = (await AppMetrics.getMainSession())?.id;\n // TODO(@ubax): we should count the time against the action which caused the first navigation\n // and add a param stating if during that time there was any navigation\n if (mainSessionId) {\n await AppMetrics.addCustomMetricToSession({\n sessionId: mainSessionId,\n timestamp,\n category: 'navigation',\n // TODO(@ubax): Use segments.join here to get full routeName and pass pathname and params via params\n routeName: pathname,\n name: 'tti',\n value: interactiveTimeSeconds,\n params: { routeParams },\n });\n }\n },\n [screenId, navigation, pathname, storage, routeParams]\n );\n\n return initializedAtMount.current ? markInteractive : null;\n}\n"]}
@@ -9,7 +9,7 @@
9
9
  "publication": {
10
10
  "groupId": "expo.modules.observe",
11
11
  "artifactId": "expo.modules.observe",
12
- "version": "56.0.6",
12
+ "version": "56.0.8",
13
13
  "repository": "local-maven-repo"
14
14
  }
15
15
  }
@@ -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
- Creates a new event for EAS, based on the objects from `expo-app-metrics` package.
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 create(app: AppInfo, device: DeviceInfo, sessions: [Session], environment: String? = nil) -> Event {
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: app.appName,
73
- appIdentifier: app.appId,
74
- appVersion: app.appVersion,
75
- appBuildNumber: app.buildNumber,
76
- appEasBuildId: app.easBuildId,
77
- appUpdatesInfo: app.updatesInfo,
78
-
79
- deviceName: device.modelName,
80
- deviceModel: device.modelIdentifier,
81
- deviceOs: device.systemName,
82
- deviceOsVersion: device.systemVersion,
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
- languageTag: Locale.preferredLanguages.first ?? "en-US",
89
- environment: environment
94
+ languageTag: session.languageTag ?? Locale.preferredLanguages.first ?? "en-US",
95
+ environment: session.environment
90
96
  ),
91
- metrics: sessions.flatMap { session in
92
- return session.metrics.map { metric in
93
- return Metric(
94
- category: metric.category?.rawValue,
95
- name: metric.name,
96
- value: metric.value,
97
- timestamp: metric.timestamp,
98
- sessionId: session.id,
99
- parentSessionId: nil,
100
- routeName: metric.routeName,
101
- updateId: metric.updateId,
102
- customParams: metric.params
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: sessions.flatMap { session in
107
- return session.logs.map { log in
108
- return Log(
109
- name: log.name,
110
- body: log.body,
111
- timestamp: log.timestamp,
112
- severity: log.severity,
113
- attributes: log.attributes,
114
- droppedAttributesCount: log.droppedAttributesCount,
115
- sessionId: session.id
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
+ }