From f6b78ec35143d4a1125a3199d77b2b72d1006c4e Mon Sep 17 00:00:00 2001 From: Jakub Tkacz <32908614+Ubax@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:08:32 +0200 Subject: [PATCH 01/17] [expo-app-metrics] add session shared object (#46652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why Until now `getMainSession`/`getForegroundSession` returned eager, serialized snapshots of a session (the `MainSession`/`GenericSession` union), so the data was frozen at the moment of the call and the full metrics/logs collections were marshalled across the bridge every time. We want a single live `Session` representation that reflects the session's current state and only fetches the heavy collections when asked. # How 1. Introduce a native `Session` shared object (`SessionSharedObject` on Android, `Session` on iOS) exposed to JS via `src/Session.ts`. - Scalar fields (`id`, `type`, `startDate`) are snapshots captured when the object is created. - Mutable state (`isActive`, `getEndDate`) and the heavy collections (`getMetrics`, `getLogs`) are fetched lazily so each call reflects live state. - Adds `addMetric(metric)` on the object โ€” the session id is implied by the receiver, so the new `MetricInput` type is `Omit`. 2. Change the module surface: - `getMainSession()` is now synchronous and returns the shared object built from in-memory state, so it never returns `null`. Repeated calls return the same static reference (`getMainSession() === getMainSession()`). - `getForegroundSession()` resolves to the foreground `Session` shared object or `null`. - Export the native `Session` class on the module type so the web module can substitute its own implementation. 3. Split the historic-session shape out into a new eager `DebugSession` type (used by `getInactiveSessions()`), keeping serialized history separate from the live shared object. 4. Android: fold the `SessionCoordinator` (added in [#46702](https://github.com/expo/expo/pull/46702)) into `SessionSharedObject`. The session id and start date are generated synchronously so they are available to readers immediately, while `startSessionWithIdAt` runs in a `by lazy` job; `awaitSessionPersisted()` (`job.join()`) gates `addMetrics`/`addLogs`/`stop` so no insert can race ahead of session creation. 5. iOS: rework `Session`/`StoredSession` and add `JsMetric` to back the shared object and the `addMetric` input. 6. Update `observe-tester` screens and the web module (`module.web.ts`) to the new API. # Test Plan 1. CI 2. Observe tester โ€” verified sessions, metrics, and logs render on both platforms. # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- .../src/screens/AppMetricsScreen.tsx | 14 +- .../app/(tabs)/(sessions)/sessions/[id].tsx | 4 +- .../(tabs)/(sessions)/sessions/foreground.tsx | 4 +- .../app/(tabs)/(sessions)/sessions/index.tsx | 16 +- .../app/(tabs)/(sessions)/sessions/main.tsx | 9 +- .../observe-tester/app/(tabs)/debug/index.tsx | 29 +- .../components/SessionDetail.tsx | 23 +- .../components/SessionHeader.tsx | 7 +- packages/expo-app-metrics/CHANGELOG.md | 1 + .../modules/appmetrics/AppMetricsModule.kt | 108 +++++-- .../appmetrics/storage/MetricsDatabase.kt | 6 + .../appmetrics/storage/SessionCoordinator.kt | 65 ---- .../appmetrics/storage/SessionManager.kt | 8 + .../appmetrics/storage/SessionMappers.kt | 52 ++- .../appmetrics/storage/SessionSharedObject.kt | 57 ++++ .../appmetrics/updates/UpdatesMonitoring.kt | 5 +- .../appmetrics/storage/SessionManagerTest.kt | 103 ++++++ .../appmetrics/storage/SessionMappersTest.kt | 70 +++- .../storage/SessionSharedObjectTest.kt | 301 ++++++++++++++++++ packages/expo-app-metrics/build/Session.d.ts | 54 ++++ .../expo-app-metrics/build/Session.d.ts.map | 1 + packages/expo-app-metrics/build/index.d.ts | 1 + .../expo-app-metrics/build/index.d.ts.map | 2 +- .../expo-app-metrics/build/module.web.d.ts | 5 +- .../build/module.web.d.ts.map | 2 +- packages/expo-app-metrics/build/types.d.ts | 77 +++-- .../expo-app-metrics/build/types.d.ts.map | 2 +- .../ios/AppMetricsModule.swift | 52 ++- .../ios/Sessions/ForegroundSession.swift | 2 +- .../ios/Sessions/JsMetric.swift | 25 ++ .../ios/Sessions/Session.swift | 42 ++- .../ios/Storage/StoredSession.swift | 64 ++-- .../ios/Tests/SessionMetricInputTests.swift | 52 +++ packages/expo-app-metrics/src/Session.ts | 56 ++++ packages/expo-app-metrics/src/index.ts | 1 + packages/expo-app-metrics/src/module.web.ts | 36 ++- packages/expo-app-metrics/src/types.ts | 82 +++-- .../src/__tests__/optionalImport.test.ts | 2 +- .../expo-router/__tests__/init.test.native.ts | 4 +- .../src/integrations/expo-router/init.ts | 6 +- .../expo-router/useObserveForRouter.ts | 2 +- ...ObserveNavigationContainer.test.native.tsx | 2 +- .../ObserveNavigationProvider.test.native.tsx | 2 +- .../handleStateChange.test.native.ts | 2 +- .../__tests__/integration.test.native.tsx | 2 +- .../useObserveForReactNavigation.ts | 2 +- 46 files changed, 1198 insertions(+), 264 deletions(-) delete mode 100644 packages/expo-app-metrics/android/src/main/java/expo/modules/appmetrics/storage/SessionCoordinator.kt create mode 100644 packages/expo-app-metrics/android/src/main/java/expo/modules/appmetrics/storage/SessionSharedObject.kt create mode 100644 packages/expo-app-metrics/android/src/test/java/expo/modules/appmetrics/storage/SessionSharedObjectTest.kt create mode 100644 packages/expo-app-metrics/build/Session.d.ts create mode 100644 packages/expo-app-metrics/build/Session.d.ts.map create mode 100644 packages/expo-app-metrics/ios/Tests/SessionMetricInputTests.swift create mode 100644 packages/expo-app-metrics/src/Session.ts diff --git a/apps/native-component-list/src/screens/AppMetricsScreen.tsx b/apps/native-component-list/src/screens/AppMetricsScreen.tsx index b39061ad171998..811878e3e4ab02 100644 --- a/apps/native-component-list/src/screens/AppMetricsScreen.tsx +++ b/apps/native-component-list/src/screens/AppMetricsScreen.tsx @@ -11,12 +11,14 @@ export default function AppMetricsScreen() { useFocusEffect( React.useCallback(() => { - let cancelled = false; - AppMetrics.getMainSession().then((s) => { - if (!cancelled) setMetrics(s?.metrics ?? []); - }); + let canceled = false; + AppMetrics.getMainSession() + .getMetrics() + .then((m) => { + if (!canceled) setMetrics(m); + }); return () => { - cancelled = true; + canceled = true; }; }, []) ); @@ -24,7 +26,7 @@ export default function AppMetricsScreen() { const onRefresh = React.useCallback(async () => { setRefreshing(true); try { - setMetrics((await AppMetrics.getMainSession())?.metrics ?? []); + setMetrics(await AppMetrics.getMainSession().getMetrics()); } finally { setRefreshing(false); } diff --git a/apps/observe-tester/app/(tabs)/(sessions)/sessions/[id].tsx b/apps/observe-tester/app/(tabs)/(sessions)/sessions/[id].tsx index 1c2d658121b5e7..7d9ac9472152b9 100644 --- a/apps/observe-tester/app/(tabs)/(sessions)/sessions/[id].tsx +++ b/apps/observe-tester/app/(tabs)/(sessions)/sessions/[id].tsx @@ -1,4 +1,4 @@ -import AppMetrics, { type Session } from 'expo-app-metrics'; +import AppMetrics, { type DebugSession } from 'expo-app-metrics'; import { useFocusEffect, useLocalSearchParams } from 'expo-router'; import { useCallback, useState } from 'react'; @@ -6,7 +6,7 @@ import { SessionDetail } from '@/components/SessionDetail'; export default function InactiveSessionScreen() { const { id } = useLocalSearchParams<{ id: string }>(); - const [session, setSession] = useState(null); + const [session, setSession] = useState(null); const [loaded, setLoaded] = useState(false); useFocusEffect( diff --git a/apps/observe-tester/app/(tabs)/(sessions)/sessions/foreground.tsx b/apps/observe-tester/app/(tabs)/(sessions)/sessions/foreground.tsx index c759e20d390810..11d2a5261e85de 100644 --- a/apps/observe-tester/app/(tabs)/(sessions)/sessions/foreground.tsx +++ b/apps/observe-tester/app/(tabs)/(sessions)/sessions/foreground.tsx @@ -1,11 +1,11 @@ -import AppMetrics, { type Session } from 'expo-app-metrics'; +import AppMetrics, { type DebugSession } from 'expo-app-metrics'; import { useFocusEffect } from 'expo-router'; import { useCallback, useState } from 'react'; import { SessionDetail, liveSessionToRecord } from '@/components/SessionDetail'; export default function ForegroundSessionScreen() { - const [session, setSession] = useState(null); + const [session, setSession] = useState(null); const [loaded, setLoaded] = useState(false); useFocusEffect( diff --git a/apps/observe-tester/app/(tabs)/(sessions)/sessions/index.tsx b/apps/observe-tester/app/(tabs)/(sessions)/sessions/index.tsx index 33b5c0805ffc8f..5abc481f785348 100644 --- a/apps/observe-tester/app/(tabs)/(sessions)/sessions/index.tsx +++ b/apps/observe-tester/app/(tabs)/(sessions)/sessions/index.tsx @@ -1,4 +1,4 @@ -import AppMetrics, { type Session, type SessionType } from 'expo-app-metrics'; +import AppMetrics, { type DebugSession, type Session, type SessionType } from 'expo-app-metrics'; import { useObserve } from 'expo-observe'; import { type Href, router, Stack, useFocusEffect } from 'expo-router'; import { useCallback, useEffect, useState } from 'react'; @@ -50,9 +50,9 @@ export default function SessionsList() { AppMetrics.getMainSession(), AppMetrics.getForegroundSession(), ]); - const active = live - .filter((session): session is Session => session != null) - .map(liveSessionToRow); + const active = await Promise.all( + live.filter((session): session is Session => session != null).map(liveSessionToRow) + ); const records = await AppMetrics.getInactiveSessions(); const inactive: SessionRowData[] = records @@ -143,20 +143,20 @@ export default function SessionsList() { ); } -function liveSessionToRow(session: Session): SessionRowData { +async function liveSessionToRow(session: Session): Promise { return { id: session.id, type: session.type, startDate: session.startDate, endDate: null, isActive: true, - metricCount: session.metrics.length, + metricCount: (await session.getMetrics()).length, crashed: false, href: `/sessions/${session.type}`, }; } -function inactiveSessionToRow(session: Session): SessionRowData { +function inactiveSessionToRow(session: DebugSession): SessionRowData { return { id: session.id, type: session.type, @@ -164,7 +164,7 @@ function inactiveSessionToRow(session: Session): SessionRowData { endDate: session.endDate ?? null, isActive: false, metricCount: session.metrics.length, - crashed: 'crashReport' in session ? !!session.crashReport : false, + crashed: !!session.crashReport, href: `/sessions/${session.id}`, }; } diff --git a/apps/observe-tester/app/(tabs)/(sessions)/sessions/main.tsx b/apps/observe-tester/app/(tabs)/(sessions)/sessions/main.tsx index 7b05be5c63c696..1b6e2b23f3ed8c 100644 --- a/apps/observe-tester/app/(tabs)/(sessions)/sessions/main.tsx +++ b/apps/observe-tester/app/(tabs)/(sessions)/sessions/main.tsx @@ -1,19 +1,20 @@ -import AppMetrics, { type Session } from 'expo-app-metrics'; +import AppMetrics, { type DebugSession } from 'expo-app-metrics'; import { useFocusEffect } from 'expo-router'; import { useCallback, useState } from 'react'; import { SessionDetail, liveSessionToRecord } from '@/components/SessionDetail'; export default function MainSessionScreen() { - const [session, setSession] = useState(null); + const [session, setSession] = useState(null); const [loaded, setLoaded] = useState(false); useFocusEffect( useCallback(() => { let cancelled = false; - AppMetrics.getMainSession().then((mainSession) => { + const mainSession = AppMetrics.getMainSession(); + liveSessionToRecord(mainSession).then((record) => { if (cancelled) return; - setSession(mainSession ? liveSessionToRecord(mainSession) : null); + setSession(record); setLoaded(true); }); return () => { diff --git a/apps/observe-tester/app/(tabs)/debug/index.tsx b/apps/observe-tester/app/(tabs)/debug/index.tsx index 7225821f7fd1da..e4722d65d787c1 100644 --- a/apps/observe-tester/app/(tabs)/debug/index.tsx +++ b/apps/observe-tester/app/(tabs)/debug/index.tsx @@ -45,13 +45,28 @@ export default function Debug() {