Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/pages/eas/ai/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Expo MCP Server supports integration with various AI-assisted tools. Use the gen

<Collapsible summary="Claude Code setup">

<Terminal cmd={['$ claude mcp add --transport http expo-mcp https://mcp.expo.dev/mcp']} />
<Terminal cmd={['$ claude mcp add --transport http expo https://mcp.expo.dev/mcp']} />

After installation, run `/mcp` in your Claude Code session to authenticate.

Expand All @@ -96,7 +96,7 @@ After installation, run `/mcp` in your Claude Code session to authenticate.

Click the following link to install the MCP server for Cursor:

<a href="cursor://anysphere.cursor-deeplink/mcp/install?name=expo-mcp&config=eyJ1cmwiOiJodHRwczovL21jcC5leHBvLmRldi9tY3AifQ%3D%3D">
<a href="cursor://anysphere.cursor-deeplink/mcp/install?name=expo&config=eyJ1cmwiOiJodHRwczovL21jcC5leHBvLmRldi9tY3AifQ%3D%3D">
<img
src="https://cursor.com/deeplink/mcp-install-light.svg"
alt="Install MCP Server"
Expand All @@ -118,13 +118,13 @@ Click the following link to install the MCP server for Cursor:
3. Select **HTTP**
4. Enter the server details:
- **URL**: `https://mcp.expo.dev/mcp`
- **Name**: expo-mcp
- **Name**: expo

</Collapsible>

<Collapsible summary="Codex setup">

<Terminal cmd={['$ codex mcp add expo-mcp --url https://mcp.expo.dev/mcp']} />
<Terminal cmd={['$ codex mcp add expo --url https://mcp.expo.dev/mcp']} />

The above command adds the MCP server to your Codex configuration file and prompts you to authenticate with your Expo account.

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/versions/unversioned/sdk/splash-screen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control

> From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.**

Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/).
Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen).

## Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/versions/v54.0.0/sdk/splash-screen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control

> From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.**

Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/).
Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen).

## Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/versions/v55.0.0/sdk/splash-screen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control

> From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.**

Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/).
Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen).

## Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/versions/v56.0.0/sdk/splash-screen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control

> From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.**

Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/).
Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen).

## Installation

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 47 additions & 69 deletions packages/expo-observe/src/integrations/expo-router/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,88 +64,66 @@ export function initListeners(
const isInitial = !storage.renderedScreensIds.has(e.screenId);
storage.renderedScreensIds.add(e.screenId);
const name = isInitial ? 'cold_ttr' : 'warm_ttr';
const mainSessionId = (await AppMetrics.getMainSession())?.id;
if (!mainSessionId) {
return;
}

const routePattern = buildRoutePattern(e.segments);

// Resolve which clock anchors the TTR span. Initial focus diffs against
// app launch; subsequent focuses diff against the last dispatched action.
let dispatchTime: number;
let isAppLaunch: boolean;
if (!storage.hasRecordedInitialTtr) {
// Stored in seconds to match the OTel `unit = "s"` convention
const appLaunchTtrSeconds = (now - appLaunchTime) / 1000;
storage.hasRecordedInitialTtr = true;
// Seed dispatchTime so a later markInteractive on the initial screen can
// diff against app launch — symmetric with the navigated-focus branch
// below, which seeds dispatchTime from the last pending action.
storage.screenTimes[e.screenId] = {
...storage.screenTimes[e.screenId],
dispatchTime: appLaunchTime,
isAppLaunch: true,
...(hasPendingInteractive ? { lastInteractiveCall: now } : {}),
};
AppMetrics.addCustomMetricToSession({
sessionId: mainSessionId,
timestamp,
category: 'navigation',
name,
routeName: routePattern,
value: appLaunchTtrSeconds,
params: { isAppLaunch: true, routeParams: e.params, url: e.pathname },
});
if (hasPendingInteractive) {
// `markInteractive` ran before this focus, so emit TTI = TTR — the
// screen wasn't interactive any earlier than it was focused.
storage.interactiveScreensIds.add(e.screenId);
await emitTTI({
sessionId: mainSessionId,
timestamp,
routeName: routePattern,
value: appLaunchTtrSeconds,
isAppLaunch: true,
routeParams: e.params,
url: e.pathname,
});
}
return;
dispatchTime = appLaunchTime;
isAppLaunch = true;
} else {
const last = storage.pendingActions[storage.pendingActions.length - 1];
if (!last) return;
dispatchTime = last.dispatchTime;
isAppLaunch = false;
storage.pendingActions.length = 0;
}

if (storage.pendingActions.length === 0) return;

const last = storage.pendingActions[storage.pendingActions.length - 1];
if (last) {
const dispatchTime = last.dispatchTime;
const ttrSeconds = (now - dispatchTime) / 1000;
storage.screenTimes[e.screenId] = {
...storage.screenTimes[e.screenId],
dispatchTime,
isAppLaunch: false,
...(hasPendingInteractive ? { lastInteractiveCall: now } : {}),
};
// Stored in seconds to match the OTel `unit = "s"` convention
const ttrSeconds = (now - dispatchTime) / 1000;

// Write all storage updates BEFORE awaiting anything so a concurrent
// markInteractive sees the seeded dispatchTime and emits its own TTI
// instead of racing the deferred-TTI branch below.
storage.screenTimes[e.screenId] = {
...storage.screenTimes[e.screenId],
dispatchTime,
isAppLaunch,
...(hasPendingInteractive ? { lastInteractiveCall: now } : {}),
};
if (hasPendingInteractive) {
// `markInteractive` ran before this focus, so emit TTI = TTR — the
// screen wasn't interactive any earlier than it was focused.
storage.interactiveScreensIds.add(e.screenId);
}

AppMetrics.addCustomMetricToSession({
// All async work happens after storage is updated.
const mainSessionId = (await AppMetrics.getMainSession())?.id;
if (!mainSessionId) return;

AppMetrics.addCustomMetricToSession({
sessionId: mainSessionId,
timestamp,
category: 'navigation',
name,
routeName: routePattern,
value: ttrSeconds,
params: { isAppLaunch, routeParams: e.params, url: e.pathname },
});
if (hasPendingInteractive) {
await emitTTI({
sessionId: mainSessionId,
timestamp,
category: 'navigation',
name,
routeName: routePattern,
value: ttrSeconds,
params: { isAppLaunch: false, routeParams: e.params, url: e.pathname },
isAppLaunch,
routeParams: e.params,
url: e.pathname,
});
if (hasPendingInteractive) {
storage.interactiveScreensIds.add(e.screenId);
await emitTTI({
sessionId: mainSessionId,
timestamp,
routeName: routePattern,
value: ttrSeconds,
isAppLaunch: false,
routeParams: e.params,
url: e.pathname,
});
}
}
storage.pendingActions.length = 0;
});
cleanup.add(unsubscribeFocus);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,20 @@ export function useObserveForRouter(): MarkInteractive | null {
return;
}

// TTI is recorded once per screen ID for the lifetime of the storage —
// re-focusing the same screen (A → B → A) must not produce a second metric
const wasAlreadyInteractive = storage.interactiveScreensIds.has(screenId);
storage.interactiveScreensIds.add(screenId);

// All async work happens after storage is updated, so a concurrent
// pageFocused observes our writes before its own awaited writes.
AppMetrics.markInteractive({
...(attributes ?? {}),
routeName: routePattern,
params: { ...(attributes?.params ?? {}), url: pathname },
});

// TTI is recorded once per screen ID for the lifetime of the storage —
// re-focusing the same screen (A → B → A) must not produce a second metric
if (storage.interactiveScreensIds.has(screenId)) return;
storage.interactiveScreensIds.add(screenId);

if (wasAlreadyInteractive) return;
if (!currentScreenData?.dispatchTime) {
// `pageFocused` hasn't recorded the dispatch yet. The focus listener
// will see `lastInteractiveCall` set with no `dispatchTime` and emit
Expand Down
1 change: 1 addition & 0 deletions packages/expo-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- [jetpack-compose] Added `NavigationBar` and `NavigationBarItem` components.
- [iOS] Added support for custom SF Symbols in the SwiftUI `Image` component. ([#46183](https://github.com/expo/expo/pull/46183) by [@cinques](https://github.com/cinques))
- [iOS] Added `presentationBackground` SwiftUI modifier and applied it in `community/bottom-sheet`. ([#46285](https://github.com/expo/expo/pull/46285) by [@duyanhv](https://github.com/duyanhv))
- [swift-ui] Added `<DisclosureGroup.Label>` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo))
- [universal] Added `<Collapsible.labelStyle>` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo))

Expand Down
Loading