expo-observe 56.0.6 → 56.0.7

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 (72) hide show
  1. package/android/src/main/java/expo/modules/observe/OpenTelemetry.kt +4 -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 +20 -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 +77 -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/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6-sources.jar → 56.0.7/expo.modules.observe-56.0.7-sources.jar} +0 -0
  26. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.md5 +1 -0
  27. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.sha1 +1 -0
  28. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.sha256 +1 -0
  29. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7-sources.jar.sha512 +1 -0
  30. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6.aar → 56.0.7/expo.modules.observe-56.0.7.aar} +0 -0
  31. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.md5 +1 -0
  32. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.sha1 +1 -0
  33. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.sha256 +1 -0
  34. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.aar.sha512 +1 -0
  35. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6.module → 56.0.7/expo.modules.observe-56.0.7.module} +23 -23
  36. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.md5 +1 -0
  37. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.sha1 +1 -0
  38. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.sha256 +1 -0
  39. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.module.sha512 +1 -0
  40. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.6/expo.modules.observe-56.0.6.pom → 56.0.7/expo.modules.observe-56.0.7.pom} +2 -2
  41. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.md5 +1 -0
  42. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.sha1 +1 -0
  43. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.sha256 +1 -0
  44. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.7/expo.modules.observe-56.0.7.pom.sha512 +1 -0
  45. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml +4 -4
  46. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.md5 +1 -1
  47. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha1 +1 -1
  48. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha256 +1 -1
  49. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha512 +1 -1
  50. package/package.json +4 -4
  51. package/src/ObserveProvider.tsx +3 -1
  52. package/src/integrations/expo-router/ObserveRouterIntegrationProvider.tsx +31 -0
  53. package/src/integrations/expo-router/index.ts +1 -0
  54. package/src/integrations/expo-router/init.ts +91 -0
  55. package/src/integrations/expo-router/storage.ts +38 -0
  56. package/src/integrations/expo-router/useObserveForRouter.ts +55 -3
  57. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.md5 +0 -1
  58. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha1 +0 -1
  59. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha256 +0 -1
  60. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha512 +0 -1
  61. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.md5 +0 -1
  62. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha1 +0 -1
  63. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha256 +0 -1
  64. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha512 +0 -1
  65. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.md5 +0 -1
  66. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha1 +0 -1
  67. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha256 +0 -1
  68. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha512 +0 -1
  69. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.md5 +0 -1
  70. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha1 +0 -1
  71. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha256 +0 -1
  72. 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,10 @@ 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 "ttr") to "expo.navigation.ttr",
195
+ (MetricCategory.Navigation.categoryName to "tti") to "expo.navigation.tti"
192
196
  )
193
197
 
194
198
  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,2CAsB/E"}
@@ -0,0 +1,20 @@
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 prevInitialized = useRef(isInitialized());
10
+ if (prevInitialized.current !== isInitialized()) {
11
+ throw new Error(`[expo-observe] Router integration was ${isInitialized() ? 'enabled' : 'disabled'} after application mounted. Call ExpoObserve.configure() before mounting AppMetricsRoot.`);
12
+ }
13
+ useEffect(() => {
14
+ if (!storage || !optionalRouter)
15
+ return;
16
+ return initListeners(storage, optionalRouter.unstable_navigationEvents);
17
+ }, [storage]);
18
+ return (_jsx(ObserveRouterIntegrationContext.Provider, { value: storage, children: children }));
19
+ }
20
+ //# 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;IAEF,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;QACb,IAAI,CAAC,OAAO,IAAI,CAAC,cAAc;YAAE,OAAO;QACxC,OAAO,aAAa,CAAC,OAAO,EAAE,cAAc,CAAC,yBAAyB,CAAC,CAAC;IAC1E,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,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\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(() => {\n if (!storage || !optionalRouter) return;\n return initListeners(storage, optionalRouter.unstable_navigationEvents);\n }, [storage]);\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,CAyEZ"}
@@ -1,6 +1,83 @@
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
+ // TODO(@ubax): Handle screen preloading
19
+ // PRELOAD comes from router.prefetch() — a route warm-up, not a user
20
+ // navigation — so it must not seed dispatchTime.
21
+ if (event.actionType === 'PRELOAD')
22
+ return;
23
+ storage.pendingActions.push({
24
+ actionType: event.actionType,
25
+ dispatchTime: performance.now(),
26
+ });
27
+ });
28
+ cleanup.add(unsubscribeAction);
29
+ const unsubscribeFocus = navigationEvents.addListener('pageFocused', async (e) => {
30
+ // Snapshot both clocks once so every metric written below is stamped with
31
+ // the moment the focus event fired, not the moment `addCustomMetricToSession`
32
+ // happens to run after the awaited `getMainSession()` round-trip.
33
+ const now = performance.now();
34
+ const timestamp = new Date().toISOString();
35
+ const isInitial = !storage.renderedScreensIds.has(e.screenId);
36
+ storage.renderedScreensIds.add(e.screenId);
37
+ const mainSessionId = (await AppMetrics.getMainSession())?.id;
38
+ if (!mainSessionId) {
39
+ return;
40
+ }
41
+ if (!storage.hasRecordedInitialTtr) {
42
+ // Stored in seconds to match the OTel `unit = "s"` convention
43
+ const appLaunchTtrSeconds = (now - appLaunchTime) / 1000;
44
+ storage.hasRecordedInitialTtr = true;
45
+ AppMetrics.addCustomMetricToSession({
46
+ sessionId: mainSessionId,
47
+ timestamp,
48
+ category: 'navigation',
49
+ name: 'ttr',
50
+ routeName: e.pathname,
51
+ value: appLaunchTtrSeconds,
52
+ params: { isInitial, isAppLaunch: true, routeParams: e.params },
53
+ });
54
+ return;
55
+ }
56
+ if (storage.pendingActions.length === 0)
57
+ return;
58
+ const last = storage.pendingActions[storage.pendingActions.length - 1];
59
+ if (last) {
60
+ const dispatchTime = last.dispatchTime;
61
+ storage.screenTimes[e.screenId] = {
62
+ ...storage.screenTimes[e.screenId],
63
+ dispatchTime,
64
+ };
65
+ AppMetrics.addCustomMetricToSession({
66
+ sessionId: mainSessionId,
67
+ timestamp,
68
+ category: 'navigation',
69
+ name: 'ttr',
70
+ routeName: e.pathname,
71
+ value: (now - dispatchTime) / 1000,
72
+ params: { isInitial, isAppLaunch: false, routeParams: e.params },
73
+ });
74
+ }
75
+ storage.pendingActions.length = 0;
76
+ });
77
+ cleanup.add(unsubscribeFocus);
78
+ return () => {
79
+ cleanup.forEach((c) => c());
80
+ cleanup.clear();
81
+ };
5
82
  }
6
83
  //# 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 // TODO(@ubax): Handle screen preloading\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 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 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: 'ttr',\n routeName: e.pathname,\n value: appLaunchTtrSeconds,\n params: { isInitial, 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: 'ttr',\n routeName: e.pathname,\n value: (now - dispatchTime) / 1000,\n params: { isInitial, 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.7",
13
13
  "repository": "local-maven-repo"
14
14
  }
15
15
  }
@@ -0,0 +1 @@
1
+ 23c19756a5d59cad42ce48628d3487e0f32a9b5ec2621649e514564020676a8b
@@ -0,0 +1 @@
1
+ 4c663a5d08c5c762bdb6802fd1218799cf0687f116a3515e09eaa19d1fead4f64cedc52eec67272cf1fc8e49605d21b479d3be67ff7accd19bdc325ccedea730
@@ -0,0 +1 @@
1
+ 8608df161cd61ebbe4545743b35a10e3a490a9aa133be6189a4a8a23ed950bb1
@@ -0,0 +1 @@
1
+ da746a4158b84fc77c4d35f60be2ba470e6c763c412f382ad3a8315c97a77cc9e5faed5695ef00a37b69a71bbec4c6ec52f0393822caeca47e551755f9136ef1
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "expo.modules.observe",
5
5
  "module": "expo.modules.observe",
6
- "version": "56.0.6",
6
+ "version": "56.0.7",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -24,13 +24,13 @@
24
24
  },
25
25
  "files": [
26
26
  {
27
- "name": "expo.modules.observe-56.0.6.aar",
28
- "url": "expo.modules.observe-56.0.6.aar",
29
- "size": 249370,
30
- "sha512": "08e1da2bd10daf59e6735744f55363c2124d5d3a96643e0a3d2b82f525541f7340ddbd266faaa62b9372d140d2c8d4101f959a607fc286ca1be350d66adde104",
31
- "sha256": "769b7da82956bd4f8b4db8bf6f5e050495c3f5f4f8d97fdf1ca45bb924def584",
32
- "sha1": "d9893c3574bfcd4036a6031f4fbfbbf5a60cbd2f",
33
- "md5": "f036b5972d135b9eb16c44980a4ec3fd"
27
+ "name": "expo.modules.observe-56.0.7.aar",
28
+ "url": "expo.modules.observe-56.0.7.aar",
29
+ "size": 249447,
30
+ "sha512": "da746a4158b84fc77c4d35f60be2ba470e6c763c412f382ad3a8315c97a77cc9e5faed5695ef00a37b69a71bbec4c6ec52f0393822caeca47e551755f9136ef1",
31
+ "sha256": "8608df161cd61ebbe4545743b35a10e3a490a9aa133be6189a4a8a23ed950bb1",
32
+ "sha1": "9e107b5ab2fa23705fde5c73b8dd42b86ba676d1",
33
+ "md5": "7ef55e8cba36b799428754e0007b57aa"
34
34
  }
35
35
  ]
36
36
  },
@@ -61,7 +61,7 @@
61
61
  "group": "expo.modules.appmetrics",
62
62
  "module": "expo.modules.appmetrics",
63
63
  "version": {
64
- "requires": "56.0.6"
64
+ "requires": "56.0.7"
65
65
  }
66
66
  },
67
67
  {
@@ -134,13 +134,13 @@
134
134
  ],
135
135
  "files": [
136
136
  {
137
- "name": "expo.modules.observe-56.0.6.aar",
138
- "url": "expo.modules.observe-56.0.6.aar",
139
- "size": 249370,
140
- "sha512": "08e1da2bd10daf59e6735744f55363c2124d5d3a96643e0a3d2b82f525541f7340ddbd266faaa62b9372d140d2c8d4101f959a607fc286ca1be350d66adde104",
141
- "sha256": "769b7da82956bd4f8b4db8bf6f5e050495c3f5f4f8d97fdf1ca45bb924def584",
142
- "sha1": "d9893c3574bfcd4036a6031f4fbfbbf5a60cbd2f",
143
- "md5": "f036b5972d135b9eb16c44980a4ec3fd"
137
+ "name": "expo.modules.observe-56.0.7.aar",
138
+ "url": "expo.modules.observe-56.0.7.aar",
139
+ "size": 249447,
140
+ "sha512": "da746a4158b84fc77c4d35f60be2ba470e6c763c412f382ad3a8315c97a77cc9e5faed5695ef00a37b69a71bbec4c6ec52f0393822caeca47e551755f9136ef1",
141
+ "sha256": "8608df161cd61ebbe4545743b35a10e3a490a9aa133be6189a4a8a23ed950bb1",
142
+ "sha1": "9e107b5ab2fa23705fde5c73b8dd42b86ba676d1",
143
+ "md5": "7ef55e8cba36b799428754e0007b57aa"
144
144
  }
145
145
  ]
146
146
  },
@@ -154,13 +154,13 @@
154
154
  },
155
155
  "files": [
156
156
  {
157
- "name": "expo.modules.observe-56.0.6-sources.jar",
158
- "url": "expo.modules.observe-56.0.6-sources.jar",
159
- "size": 18794,
160
- "sha512": "bd4748de4b9dca3a6bcdef11bb3e8c0947af8c7bae941bfcf965b8667826e16dc2ba49c482213fafde8452c9524b7131a4551ab9e37f3675823f1ecc2b38fd10",
161
- "sha256": "1c46128e736c61eb22b8f15c5f63d994d88bb996224450adf476885200604ec3",
162
- "sha1": "60f98dcd27468ed24aaf453903b5474d4abb2929",
163
- "md5": "31b0b3276f00d248e8b49ee056f17d7c"
157
+ "name": "expo.modules.observe-56.0.7-sources.jar",
158
+ "url": "expo.modules.observe-56.0.7-sources.jar",
159
+ "size": 18825,
160
+ "sha512": "4c663a5d08c5c762bdb6802fd1218799cf0687f116a3515e09eaa19d1fead4f64cedc52eec67272cf1fc8e49605d21b479d3be67ff7accd19bdc325ccedea730",
161
+ "sha256": "23c19756a5d59cad42ce48628d3487e0f32a9b5ec2621649e514564020676a8b",
162
+ "sha1": "493fddc02b0a9f719d056e023369811756e18afc",
163
+ "md5": "8f3298ccb20d077bf8b224af4aecc57b"
164
164
  }
165
165
  ]
166
166
  }
@@ -0,0 +1 @@
1
+ 25f25a0e9169f62078983759116e7199a8aa66024f6a4a220dff7d8d2143c8e6
@@ -0,0 +1 @@
1
+ 673f7bbfb0de0497c480a81db38bf912413d3e840f2f0df6b57a132fecaa96c24bd24cac8925f69aa25547a4153be6823f1d277c47237f39ad0690e238d205e5
@@ -9,7 +9,7 @@
9
9
  <modelVersion>4.0.0</modelVersion>
10
10
  <groupId>expo.modules.observe</groupId>
11
11
  <artifactId>expo.modules.observe</artifactId>
12
- <version>56.0.6</version>
12
+ <version>56.0.7</version>
13
13
  <packaging>aar</packaging>
14
14
  <name>expo.modules.observe</name>
15
15
  <url>https://github.com/expo/expo</url>
@@ -52,7 +52,7 @@
52
52
  <dependency>
53
53
  <groupId>expo.modules.appmetrics</groupId>
54
54
  <artifactId>expo.modules.appmetrics</artifactId>
55
- <version>56.0.6</version>
55
+ <version>56.0.7</version>
56
56
  <scope>runtime</scope>
57
57
  </dependency>
58
58
  <dependency>
@@ -0,0 +1 @@
1
+ a922ed23df3d2043d7cf868e13a96f441fd2f1574a2e0f7328bd1c473d0a0817
@@ -0,0 +1 @@
1
+ 0c8e85e42d623e20aeb0d31ecbe3e72846d97a552f915e5755435bd2dd5c137fbe60def827aeafb8bd64f91af9319debd53d6b99598d6cff577234098d0340b2
@@ -3,11 +3,11 @@
3
3
  <groupId>expo.modules.observe</groupId>
4
4
  <artifactId>expo.modules.observe</artifactId>
5
5
  <versioning>
6
- <latest>56.0.6</latest>
7
- <release>56.0.6</release>
6
+ <latest>56.0.7</latest>
7
+ <release>56.0.7</release>
8
8
  <versions>
9
- <version>56.0.6</version>
9
+ <version>56.0.7</version>
10
10
  </versions>
11
- <lastUpdated>20260511220222</lastUpdated>
11
+ <lastUpdated>20260513103326</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- cd0849a80c6cd62a9a7f462cf71d6658
1
+ 05e5fc6848d89657a8973e8c088bcaa2
@@ -1 +1 @@
1
- 37aded1512caa842f577a749ead89e715550ff26
1
+ df7fbc2f70f15ab653429335cf2f500949439a83
@@ -1 +1 @@
1
- 8282e04b67fc0c740f5a0b722a8f0f9a72b17035d69d514a0284d0d81f3f7e03
1
+ a43ea0c299b993042559fd97a130d316f4a51fae98b252a17b9a2e0f916d3b0e
@@ -1 +1 @@
1
- 9e1b110a9da69bfd87007becc9cd6af9a0d93726f8339f657edadf13756b0d2af0ee77ead3af73b3f75475288f3f4fa21badeb104316b987cdc9df5b3ea2c81b
1
+ 447e7d6e054a3794a65d44518aa3939e162e6dcaf29ba8165772e358fa462ce8bb9bf99aa37ea28146861143c102e9a08442c80da71c674493c027eeb3b0d0f4
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-observe",
3
3
  "title": "Expo Observe",
4
- "version": "56.0.6",
4
+ "version": "56.0.7",
5
5
  "description": "Expo module that dispatches collected app metrics to EAS Observe",
6
6
  "main": "src/index.ts",
7
7
  "types": "build/index.d.ts",
@@ -24,13 +24,13 @@
24
24
  "author": "650 Industries, Inc.",
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
- "expo-app-metrics": "~56.0.6",
27
+ "expo-app-metrics": "~56.0.7",
28
28
  "expo-eas-client": "~56.0.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@testing-library/react-native": "^13.3.0",
32
32
  "@types/react": "~19.2.0",
33
- "expo": "56.0.0-preview.8",
33
+ "expo": "56.0.0-preview.10",
34
34
  "expo-module-scripts": "56.0.2"
35
35
  },
36
36
  "peerDependencies": {
@@ -44,7 +44,7 @@
44
44
  "optional": true
45
45
  }
46
46
  },
47
- "gitHead": "42013232893cb2aa71ab218e9b422d4a8476b3f0",
47
+ "gitHead": "40f0a6f6711d93762e0506b37e6e077e4bd9a541",
48
48
  "scripts": {
49
49
  "build": "expo-module build",
50
50
  "clean": "expo-module clean",
@@ -1,5 +1,7 @@
1
1
  import { type PropsWithChildren } from 'react';
2
2
 
3
+ import { ObserveRouterIntegrationProvider } from './integrations/expo-router/ObserveRouterIntegrationProvider';
4
+
3
5
  export function ObserveProvider({ children }: PropsWithChildren) {
4
- return <>{children}</>;
6
+ return <ObserveRouterIntegrationProvider>{children}</ObserveRouterIntegrationProvider>;
5
7
  }
@@ -0,0 +1,31 @@
1
+ import { createContext, type PropsWithChildren, useEffect, useRef, useState } from 'react';
2
+
3
+ import { initListeners, isInitialized } from './init';
4
+ import { optionalRouter } from './router';
5
+ import { createRouterIntegrationStorage, type RouterIntegrationStorage } from './storage';
6
+
7
+ export const ObserveRouterIntegrationContext = createContext<RouterIntegrationStorage | null>(null);
8
+
9
+ export function ObserveRouterIntegrationProvider({ children }: PropsWithChildren) {
10
+ const [storage] = useState<RouterIntegrationStorage | null>(() =>
11
+ isInitialized() ? createRouterIntegrationStorage() : null
12
+ );
13
+
14
+ const prevInitialized = useRef(isInitialized());
15
+ if (prevInitialized.current !== isInitialized()) {
16
+ throw new Error(
17
+ `[expo-observe] Router integration was ${isInitialized() ? 'enabled' : 'disabled'} after application mounted. Call ExpoObserve.configure() before mounting AppMetricsRoot.`
18
+ );
19
+ }
20
+
21
+ useEffect(() => {
22
+ if (!storage || !optionalRouter) return;
23
+ return initListeners(storage, optionalRouter.unstable_navigationEvents);
24
+ }, [storage]);
25
+
26
+ return (
27
+ <ObserveRouterIntegrationContext.Provider value={storage}>
28
+ {children}
29
+ </ObserveRouterIntegrationContext.Provider>
30
+ );
31
+ }
@@ -1,3 +1,4 @@
1
1
  export { isRouterInstalled } from './router';
2
2
  export { useObserveForRouter } from './useObserveForRouter';
3
3
  export { initRouterIntegration } from './init';
4
+ export { ObserveRouterIntegrationProvider } from './ObserveRouterIntegrationProvider';
@@ -1,7 +1,98 @@
1
+ import AppMetrics from 'expo-app-metrics';
2
+
3
+ import { optionalRouter } from './router';
4
+ import { type RouterIntegrationStorage } from './storage';
5
+
6
+ // TODO(@ubax): split this module into `.native.ts` / `.web.ts` variants so the
7
+ // web bundle doesn't pull in `expo-app-metrics`' native bridge calls. The web
8
+ // version should be an explicit no-op (return a noop cleanup) rather than
9
+ // relying on the web stubs in `expo-app-metrics/module.web.ts`.
10
+
1
11
  let initialized = false;
2
12
 
3
13
  export const isInitialized = () => initialized;
4
14
 
5
15
  export function initRouterIntegration() {
6
16
  initialized = true;
17
+ optionalRouter?.unstable_navigationEvents.enable();
18
+ }
19
+
20
+ type NavigationEvents = NonNullable<typeof optionalRouter>['unstable_navigationEvents'];
21
+
22
+ export function initListeners(
23
+ storage: RouterIntegrationStorage,
24
+ navigationEvents: NavigationEvents
25
+ ): () => void {
26
+ const appLaunchTime = performance.now();
27
+ const cleanup = new Set<() => void>();
28
+
29
+ const unsubscribeAction = navigationEvents.addListener('actionDispatched', (event) => {
30
+ // TODO(@ubax): Handle screen preloading
31
+ // PRELOAD comes from router.prefetch() — a route warm-up, not a user
32
+ // navigation — so it must not seed dispatchTime.
33
+ if (event.actionType === 'PRELOAD') return;
34
+ storage.pendingActions.push({
35
+ actionType: event.actionType,
36
+ dispatchTime: performance.now(),
37
+ });
38
+ });
39
+ cleanup.add(unsubscribeAction);
40
+
41
+ const unsubscribeFocus = navigationEvents.addListener('pageFocused', async (e) => {
42
+ // Snapshot both clocks once so every metric written below is stamped with
43
+ // the moment the focus event fired, not the moment `addCustomMetricToSession`
44
+ // happens to run after the awaited `getMainSession()` round-trip.
45
+ const now = performance.now();
46
+ const timestamp = new Date().toISOString();
47
+ const isInitial = !storage.renderedScreensIds.has(e.screenId);
48
+ storage.renderedScreensIds.add(e.screenId);
49
+ const mainSessionId = (await AppMetrics.getMainSession())?.id;
50
+ if (!mainSessionId) {
51
+ return;
52
+ }
53
+
54
+ if (!storage.hasRecordedInitialTtr) {
55
+ // Stored in seconds to match the OTel `unit = "s"` convention
56
+ const appLaunchTtrSeconds = (now - appLaunchTime) / 1000;
57
+ storage.hasRecordedInitialTtr = true;
58
+ AppMetrics.addCustomMetricToSession({
59
+ sessionId: mainSessionId,
60
+ timestamp,
61
+ category: 'navigation',
62
+ name: 'ttr',
63
+ routeName: e.pathname,
64
+ value: appLaunchTtrSeconds,
65
+ params: { isInitial, isAppLaunch: true, routeParams: e.params },
66
+ });
67
+ return;
68
+ }
69
+
70
+ if (storage.pendingActions.length === 0) return;
71
+
72
+ const last = storage.pendingActions[storage.pendingActions.length - 1];
73
+ if (last) {
74
+ const dispatchTime = last.dispatchTime;
75
+ storage.screenTimes[e.screenId] = {
76
+ ...storage.screenTimes[e.screenId],
77
+ dispatchTime,
78
+ };
79
+
80
+ AppMetrics.addCustomMetricToSession({
81
+ sessionId: mainSessionId,
82
+ timestamp,
83
+ category: 'navigation',
84
+ name: 'ttr',
85
+ routeName: e.pathname,
86
+ value: (now - dispatchTime) / 1000,
87
+ params: { isInitial, isAppLaunch: false, routeParams: e.params },
88
+ });
89
+ }
90
+ storage.pendingActions.length = 0;
91
+ });
92
+ cleanup.add(unsubscribeFocus);
93
+
94
+ return () => {
95
+ cleanup.forEach((c) => c());
96
+ cleanup.clear();
97
+ };
7
98
  }
@@ -0,0 +1,38 @@
1
+ import type { ActionDispatchedEvent } from 'expo-router';
2
+
3
+ export interface ScreenTimes {
4
+ dispatchTime: number;
5
+ lastInteractiveCall?: number;
6
+ }
7
+
8
+ export interface PendingAction {
9
+ actionType: ActionDispatchedEvent['actionType'];
10
+ dispatchTime: number;
11
+ }
12
+
13
+ export interface RouterIntegrationStorage {
14
+ /**
15
+ * Actions dispatched, but not yet processed by the integration
16
+ */
17
+ pendingActions: PendingAction[];
18
+ renderedScreensIds: Set<string>;
19
+ /**
20
+ * Wether the app had already recorded the first render of the screen
21
+ */
22
+ hasRecordedInitialTtr: boolean;
23
+ /**
24
+ * Times used to calculate spans from dispatch to certain event
25
+ */
26
+ screenTimes: Record<string, ScreenTimes>;
27
+ interactiveScreensIds: Set<string>;
28
+ }
29
+
30
+ export function createRouterIntegrationStorage(): RouterIntegrationStorage {
31
+ return {
32
+ pendingActions: [],
33
+ renderedScreensIds: new Set(),
34
+ hasRecordedInitialTtr: false,
35
+ screenTimes: {},
36
+ interactiveScreensIds: new Set(),
37
+ };
38
+ }
@@ -1,16 +1,19 @@
1
1
  import AppMetrics, { type MetricAttributes } from 'expo-app-metrics';
2
- import { useCallback, useEffect, useRef } from 'react';
2
+ import { use, useCallback, useEffect, useRef } from 'react';
3
3
 
4
+ import { ObserveRouterIntegrationContext } from './ObserveRouterIntegrationProvider';
4
5
  import { isInitialized } from './init';
5
6
  import { optionalRouter } from './router';
6
7
 
7
8
  type MarkInteractive = (typeof AppMetrics)['markInteractive'];
8
9
 
9
10
  export function useObserveForRouter(): MarkInteractive | null {
11
+ const storage = use(ObserveRouterIntegrationContext);
10
12
  const isMounted = useRef(true);
11
13
  const route = optionalRouter?.useRoute();
12
14
  const navigation = optionalRouter?.useNavigation();
13
- const pathname = optionalRouter?.useCurrentRouteInfo()?.pathname;
15
+ const routeInfo = optionalRouter?.useCurrentRouteInfo();
16
+ const { pathname, params: routeParams } = routeInfo ?? {};
14
17
 
15
18
  const initializedAtMount = useRef(isInitialized());
16
19
  if (initializedAtMount.current !== isInitialized()) {
@@ -41,6 +44,8 @@ export function useObserveForRouter(): MarkInteractive | null {
41
44
 
42
45
  const markInteractive = useCallback(
43
46
  async (attributes?: MetricAttributes) => {
47
+ const now = performance.now();
48
+ const timestamp = new Date().toISOString();
44
49
  if (!isMounted.current) {
45
50
  console.warn('[expo-observe] Calling markInteractive on unmounted screen');
46
51
  return;
@@ -57,8 +62,55 @@ export function useObserveForRouter(): MarkInteractive | null {
57
62
  routeName: pathname,
58
63
  });
59
64
  }
65
+
66
+ if (!storage) {
67
+ throw new Error(
68
+ '[expo-observe] markInteractive was called without an active ObserveProvider. Wrap your app in ObserveRoot from expo-observe.'
69
+ );
70
+ }
71
+
72
+ // Snapshot times BEFORE writing the new interactive timestamp so the
73
+ // duplicate-detection logic below sees the *previous* call, not this one.
74
+ const currentScreenData = storage.screenTimes[screenId];
75
+
76
+ storage.interactiveScreensIds.add(screenId);
77
+ if (storage.screenTimes[screenId]) {
78
+ storage.screenTimes[screenId] = {
79
+ ...storage.screenTimes[screenId],
80
+ lastInteractiveCall: now,
81
+ };
82
+ }
83
+
84
+ if (!currentScreenData?.dispatchTime) return;
85
+
86
+ const previousInteractiveCall = currentScreenData.lastInteractiveCall;
87
+ const previousWasAfterDispatch =
88
+ previousInteractiveCall != null && currentScreenData.dispatchTime < previousInteractiveCall;
89
+
90
+ if (previousWasAfterDispatch) {
91
+ // We only want to record interactive once per navigation
92
+ return;
93
+ }
94
+
95
+ // Stored in seconds to match the OTel `unit = "s"` convention
96
+ const interactiveTimeSeconds = (now - currentScreenData.dispatchTime) / 1000;
97
+ const mainSessionId = (await AppMetrics.getMainSession())?.id;
98
+ // TODO(@ubax): we should count the time against the action which caused the first navigation
99
+ // and add a param stating if during that time there was any navigation
100
+ if (mainSessionId) {
101
+ await AppMetrics.addCustomMetricToSession({
102
+ sessionId: mainSessionId,
103
+ timestamp,
104
+ category: 'navigation',
105
+ // TODO(@ubax): Use segments.join here to get full routeName and pass pathname and params via params
106
+ routeName: pathname,
107
+ name: 'tti',
108
+ value: interactiveTimeSeconds,
109
+ params: { routeParams },
110
+ });
111
+ }
60
112
  },
61
- [screenId, navigation, pathname]
113
+ [screenId, navigation, pathname, storage, routeParams]
62
114
  );
63
115
 
64
116
  return initializedAtMount.current ? markInteractive : null;
@@ -1 +0,0 @@
1
- 1c46128e736c61eb22b8f15c5f63d994d88bb996224450adf476885200604ec3
@@ -1 +0,0 @@
1
- bd4748de4b9dca3a6bcdef11bb3e8c0947af8c7bae941bfcf965b8667826e16dc2ba49c482213fafde8452c9524b7131a4551ab9e37f3675823f1ecc2b38fd10
@@ -1 +0,0 @@
1
- 769b7da82956bd4f8b4db8bf6f5e050495c3f5f4f8d97fdf1ca45bb924def584
@@ -1 +0,0 @@
1
- 08e1da2bd10daf59e6735744f55363c2124d5d3a96643e0a3d2b82f525541f7340ddbd266faaa62b9372d140d2c8d4101f959a607fc286ca1be350d66adde104
@@ -1 +0,0 @@
1
- d643c58ef9b70813b638fa8a4ba72fc41d5c63afd08f85b02285260c7963a130
@@ -1 +0,0 @@
1
- 6b0d9ae6e5ed40ffb3826209ea7302fe75ac545a24eaab4bfeabc8462b7c526423127093c3a014eb8da7d57bf85dfc8261a43cc2f2a76b3c01867188c0757c6b
@@ -1 +0,0 @@
1
- 10d75b1b5e6ea080ef434272e582a959daf1e7c1ec67d5f8d014ecd0981206ca
@@ -1 +0,0 @@
1
- 17c30ab50f98303f3132a45cd096b90de7a96c7cac09720fc34fd93aa1289fffbd96da5d7cbd9b8eb9ec12f1d777eafaba8b9961c2a5c17cf35d8be6ed9eee4d