Compare commits

...
5 Commits
Author SHA1 Message Date
Expo CI 08a9b5ff7f v18.13.0 2026-05-14 20:13:13 +00:00
Douglas Lowder 501af5213e [eas-cli] Extract shared observe flag groups into observe/flags.ts (#3731)
# Why

It was found that observe commands were using common flag definitions with duplicate code.

# How

Refactored the observe commands to extract flag definitions.

# Test Plan

Commands tested locally.

Unit tests pass.
2026-05-14 13:10:13 -07:00
Douglas Lowder d48f931d77 [eas-cli] Add observe:routes for navigation metrics by route name (#3730)
# Why

New command `observe:routes` to get navigation metrics by route.

```
USAGE
  $ eas observe:routes [--platform android|ios] [--metric cold_ttr|warm_ttr|nav_tti...] [--stat
    median|med|p90|count|event_count|eventCount...] [--after <value>] [--limit <value>] [--start <value> | --days
    <value>] [--end <value> | ] [--app-version <value>] [--update-id <value>] [--build-number <value>] [--project-id
    <value>] [--json] [--non-interactive]

FLAGS
  --after=<value>         Cursor for pagination. Use the endCursor from a previous query to fetch the next page.
  --app-version=<value>   Filter by app version
  --build-number=<value>  Filter by app build number
  --days=<value>          Show results from the last N days (mutually exclusive with --start/--end)
  --end=<value>           End of time range (ISO date)
  --json                  Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
  --limit=<value>         The number of items to fetch each query. Defaults to 50 and is capped at 200.
  --metric=<option>...    Navigation metric to display (can be specified multiple times). Defaults to all three.
                          <options: cold_ttr|warm_ttr|nav_tti>
  --non-interactive       Run the command in non-interactive mode.
  --platform=<option>     Filter by platform
                          <options: android|ios>
  --project-id=<value>    EAS project ID (defaults to the project ID of the current directory)
  --start=<value>         Start of time range (ISO date)
  --stat=<option>...      Statistic to display per metric (can be specified multiple times)
                          <options: median|med|p90|count|event_count|eventCount>
  --update-id=<value>     Filter by EAS update ID

DESCRIPTION
  display app navigation route metrics (Cold TTR, Warm TTR, TTI) grouped by route name

```

# How

Added a new command that that queries the new navigationRoutes GraphQL field to display Cold TTR, Warm TTR, and TTI, broken down per route name and platform.

# Test Plan

Tested with staging data in `@expokadi/cadence` project.

New unit tests added.
2026-05-14 12:11:38 -07:00
Mustafa Abban 99b18a2189 [worker] - remove logging errors to datadog (#3729)
* [worker] - remove logging errors to datadog

* [worker] - remove logging errors to datadog

* rem tests

* rem tests
2026-05-13 14:09:38 -04:00
Expo CI 9ece20157f update CHANGELOG.md 2026-05-13 14:21:35 +00:00
26 changed files with 1415 additions and 485 deletions

No files matched your search

+6
View File
@@ -8,10 +8,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- [eas-cli] New command `observe:routes` for navigation metrics. ([#3730](https://github.com/expo/eas-cli/pull/3730) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
### 🧹 Chores
- [eas-cli] Refactor observe commands to extract common flag definitions. ([#3731](https://github.com/expo/eas-cli/pull/3731) by [@douglowder](https://github.com/douglowder))
## [18.12.3](https://github.com/expo/eas-cli/releases/tag/v18.12.3) - 2026-05-13
## [18.12.2](https://github.com/expo/eas-cli/releases/tag/v18.12.2) - 2026-05-12
### 🐛 Bug fixes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "18.12.3",
"version": "18.13.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+100 -100
View File
@@ -190,7 +190,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -207,7 +207,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:usage [ACCOUNT_NAME]`
@@ -229,7 +229,7 @@ DESCRIPTION
view account usage and billing for the current cycle
```
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/account/usage.ts)_
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/account/usage.ts)_
## `eas account:view`
@@ -246,7 +246,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -260,7 +260,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -312,7 +312,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -333,7 +333,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -353,7 +353,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -373,7 +373,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -396,7 +396,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -434,7 +434,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -454,7 +454,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -472,7 +472,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -492,7 +492,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -517,7 +517,7 @@ DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/dev.ts)_
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
@@ -544,7 +544,7 @@ DESCRIPTION
download a simulator/emulator build by build ID or fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/download.ts)_
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -582,7 +582,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -622,7 +622,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -652,7 +652,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -680,7 +680,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -732,7 +732,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -750,7 +750,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -768,7 +768,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -785,7 +785,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -806,7 +806,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:delete [NAME]`
@@ -827,7 +827,7 @@ DESCRIPTION
Delete a channel
```
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/delete.ts)_
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/delete.ts)_
## `eas channel:edit [NAME]`
@@ -849,7 +849,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:insights`
@@ -873,7 +873,7 @@ DESCRIPTION
display adoption, crash, and unique-user insights for a channel + runtime version
```
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/insights.ts)_
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/insights.ts)_
## `eas channel:list`
@@ -893,7 +893,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -915,7 +915,7 @@ DESCRIPTION
pause a channel to stop it from sending updates
```
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -937,7 +937,7 @@ DESCRIPTION
resume a channel to start sending updates
```
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -972,7 +972,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -995,7 +995,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -1016,7 +1016,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -1033,7 +1033,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -1051,7 +1051,7 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -1080,7 +1080,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/deploy/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -1105,7 +1105,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/deploy/alias/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
## `eas deploy:alias:delete [ALIAS_NAME]`
@@ -1126,7 +1126,7 @@ ALIASES
$ eas worker:alias:delete
```
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
## `eas deploy:delete [DEPLOYMENT_ID]`
@@ -1147,7 +1147,7 @@ ALIASES
$ eas worker:delete
```
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/deploy/delete.ts)_
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/deploy/delete.ts)_
## `eas deploy:promote`
@@ -1184,7 +1184,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1204,7 +1204,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1225,7 +1225,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1246,7 +1246,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1260,7 +1260,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1274,7 +1274,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1306,7 +1306,7 @@ DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/create.ts)_
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1332,7 +1332,7 @@ DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/delete.ts)_
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1354,7 +1354,7 @@ DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/exec.ts)_
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1382,7 +1382,7 @@ DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/get.ts)_
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1410,7 +1410,7 @@ DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/list.ts)_
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1433,7 +1433,7 @@ DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/pull.ts)_
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1455,7 +1455,7 @@ DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/push.ts)_
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1489,7 +1489,7 @@ DESCRIPTION
update an environment variable on the current project or account
```
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/env/update.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
@@ -1533,7 +1533,7 @@ EXAMPLES
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/fingerprint/compare.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
@@ -1564,7 +1564,7 @@ EXAMPLES
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
```
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/fingerprint/generate.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1643,7 +1643,7 @@ DESCRIPTION
connect a project to an App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
## `eas integrations:asc:disconnect`
@@ -1662,7 +1662,7 @@ DESCRIPTION
disconnect the current project from its App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
## `eas integrations:asc:status`
@@ -1680,7 +1680,7 @@ DESCRIPTION
show the App Store Connect app link status for the current project
```
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/asc/status.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
## `eas integrations:convex:connect`
@@ -1702,7 +1702,7 @@ DESCRIPTION
connect Convex to your Expo project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
## `eas integrations:convex:dashboard`
@@ -1716,7 +1716,7 @@ DESCRIPTION
open the Convex dashboard for the linked Convex project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
## `eas integrations:convex:project`
@@ -1730,7 +1730,7 @@ DESCRIPTION
display the Convex project linked to the current Expo app
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/project.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/project.ts)_
## `eas integrations:convex:project:delete`
@@ -1748,7 +1748,7 @@ DESCRIPTION
remove the Convex project link for the current Expo app from EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
## `eas integrations:convex:team`
@@ -1762,7 +1762,7 @@ DESCRIPTION
display Convex teams linked to the current Expo app's owner account
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/team.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/team.ts)_
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
@@ -1783,7 +1783,7 @@ DESCRIPTION
remove a Convex team link from the current Expo app owner account's EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
@@ -1803,7 +1803,7 @@ DESCRIPTION
send a Convex team invitation to your verified email address
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
## `eas login`
@@ -1855,7 +1855,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1873,7 +1873,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1891,7 +1891,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas new [PATH]`
@@ -1943,7 +1943,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1957,7 +1957,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1980,7 +1980,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:new [PATH]`
@@ -2004,7 +2004,7 @@ ALIASES
$ eas new
```
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/project/new.ts)_
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/project/new.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -2022,7 +2022,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -2055,7 +2055,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -2098,7 +2098,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -2119,7 +2119,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -2140,7 +2140,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -2165,7 +2165,7 @@ DESCRIPTION
edit all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:insights GROUPID`
@@ -2192,7 +2192,7 @@ DESCRIPTION
display launch, crash, unique-user, and size insights for an update group
```
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/insights.ts)_
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/insights.ts)_
## `eas update:list`
@@ -2219,7 +2219,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -2257,7 +2257,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:revert-update-rollout`
@@ -2285,7 +2285,7 @@ DESCRIPTION
revert a rollout update for a project
```
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
## `eas update:roll-back-to-embedded`
@@ -2315,7 +2315,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -2336,7 +2336,7 @@ DESCRIPTION
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -2360,7 +2360,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas upload`
@@ -2381,7 +2381,7 @@ DESCRIPTION
upload a local build and generate a sharable link
```
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/upload.ts)_
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/upload.ts)_
## `eas webhook:create`
@@ -2403,7 +2403,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -2423,7 +2423,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -2442,7 +2442,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -2465,7 +2465,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -2482,7 +2482,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2603,7 +2603,7 @@ DESCRIPTION
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/cancel.ts)_
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
## `eas workflow:create [NAME]`
@@ -2623,7 +2623,7 @@ DESCRIPTION
create a new workflow configuration YAML file
```
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/create.ts)_
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:logs [ID]`
@@ -2647,7 +2647,7 @@ DESCRIPTION
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
```
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/logs.ts)_
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/logs.ts)_
## `eas workflow:run FILE`
@@ -2687,7 +2687,7 @@ FLAG DESCRIPTIONS
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/run.ts)_
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:runs`
@@ -2709,7 +2709,7 @@ DESCRIPTION
list recent workflow runs for this project, with their IDs, statuses, and timestamps
```
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/runs.ts)_
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/runs.ts)_
## `eas workflow:status [WORKFLOW_RUN_ID]`
@@ -2737,7 +2737,7 @@ FLAG DESCRIPTIONS
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/status.ts)_
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/status.ts)_
## `eas workflow:validate PATH`
@@ -2757,7 +2757,7 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/validate.ts)_
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/validate.ts)_
## `eas workflow:view [ID]`
@@ -2779,5 +2779,5 @@ DESCRIPTION
workflow runs for the current project.
```
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.12.3/packages/eas-cli/src/commands/workflow/view.ts)_
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.13.0/packages/eas-cli/src/commands/workflow/view.ts)_
<!-- commandsstop -->
+12
View File
@@ -28667,6 +28667,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "expoPackageVersion",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fingerprintHash",
"description": null,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli",
"version": "18.12.3",
"version": "18.13.0",
"description": "EAS command line tool",
"keywords": [
"cli",
@@ -0,0 +1,174 @@
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
import { AppPlatform } from '../../../graphql/generated';
import { fetchObserveNavigationRoutesAsync } from '../../../observe/fetchNavigationRoutes';
import {
buildObserveNavigationRoutesJson,
buildObserveNavigationRoutesTable,
} from '../../../observe/formatNavigationRoutes';
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
import ObserveRoutes from '../routes';
jest.mock('../../../observe/fetchNavigationRoutes');
jest.mock('../../../observe/formatNavigationRoutes', () => {
const actual = jest.requireActual('../../../observe/formatNavigationRoutes');
return {
...actual,
buildObserveNavigationRoutesTable: jest.fn().mockReturnValue('table'),
buildObserveNavigationRoutesJson: jest.fn().mockReturnValue({}),
};
});
jest.mock('../../../log');
jest.mock('../../../utils/json');
const mockFetchObserveNavigationRoutesAsync = jest.mocked(fetchObserveNavigationRoutesAsync);
const mockBuildObserveNavigationRoutesTable = jest.mocked(buildObserveNavigationRoutesTable);
const mockBuildObserveNavigationRoutesJson = jest.mocked(buildObserveNavigationRoutesJson);
const mockEnableJsonOutput = jest.mocked(enableJsonOutput);
const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput);
describe(ObserveRoutes, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const projectId = 'test-project-id';
beforeEach(() => {
jest.clearAllMocks();
mockFetchObserveNavigationRoutesAsync.mockResolvedValue({
routes: [],
pageInfoByPlatform: new Map(),
});
});
function createCommand(argv: string[]): ObserveRoutes {
const command = new ObserveRoutes(argv, mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
projectId,
loggedIn: { graphqlClient },
});
return command;
}
it('queries both platforms by default with all three navigation metric full names', async () => {
const command = createCommand([]);
await command.runAsync();
expect(mockFetchObserveNavigationRoutesAsync).toHaveBeenCalledTimes(1);
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.platforms).toEqual([AppPlatform.Android, AppPlatform.Ios]);
expect(options.limit).toBe(50);
const tableCall = mockBuildObserveNavigationRoutesTable.mock.calls[0];
expect(tableCall[1]).toEqual([
'expo.navigation.cold_ttr',
'expo.navigation.warm_ttr',
'expo.navigation.tti',
]);
expect(tableCall[2]).toEqual(['median', 'count']);
});
it('filters platform when --platform ios is provided', async () => {
const command = createCommand(['--platform', 'ios']);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.platforms).toEqual([AppPlatform.Ios]);
});
it('resolves --metric short aliases to navigation metric full names and deduplicates', async () => {
const command = createCommand([
'--metric',
'cold_ttr',
'--metric',
'cold_ttr',
'--metric',
'nav_tti',
]);
await command.runAsync();
expect(mockBuildObserveNavigationRoutesTable.mock.calls[0][1]).toEqual([
'expo.navigation.cold_ttr',
'expo.navigation.tti',
]);
});
it('passes --limit and --after through to the fetcher', async () => {
const command = createCommand(['--limit', '25', '--after', 'cursor-abc']);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.limit).toBe(25);
expect(options.after).toBe('cursor-abc');
});
it('uses --days to compute start/end time range', async () => {
const now = new Date('2025-06-15T12:00:00.000Z');
jest.useFakeTimers({ now });
const command = createCommand(['--days', '7']);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.endTime).toBe('2025-06-15T12:00:00.000Z');
expect(options.startTime).toBe('2025-06-08T12:00:00.000Z');
jest.useRealTimers();
});
it('uses explicit --start and --end when provided', async () => {
const command = createCommand([
'--start',
'2025-01-01T00:00:00.000Z',
'--end',
'2025-02-01T00:00:00.000Z',
]);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.startTime).toBe('2025-01-01T00:00:00.000Z');
expect(options.endTime).toBe('2025-02-01T00:00:00.000Z');
});
it('rejects --days combined with --start', async () => {
const command = createCommand(['--days', '7', '--start', '2025-01-01T00:00:00.000Z']);
await expect(command.runAsync()).rejects.toThrow();
});
it('passes app-version, update-id, and build-number filters', async () => {
const command = createCommand([
'--app-version',
'2.1.0',
'--update-id',
'update-xyz',
'--build-number',
'42',
]);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.appVersion).toBe('2.1.0');
expect(options.updateId).toBe('update-xyz');
expect(options.buildNumber).toBe('42');
});
it('resolves --stat aliases and passes them through', async () => {
const command = createCommand(['--stat', 'p90', '--stat', 'eventCount']);
await command.runAsync();
expect(mockBuildObserveNavigationRoutesTable.mock.calls[0][2]).toEqual(['p90', 'count']);
});
it('uses default JSON stats (median, p90, count) when --json is set without --stat', async () => {
const command = createCommand(['--json', '--non-interactive']);
await command.runAsync();
expect(mockEnableJsonOutput).toHaveBeenCalled();
expect(mockBuildObserveNavigationRoutesJson.mock.calls[0][2]).toEqual([
'median',
'p90',
'count',
]);
expect(mockPrintJsonOnlyOutput).toHaveBeenCalled();
});
});
+15 -35
View File
@@ -11,13 +11,17 @@ import {
fetchTotalEventCountAsync,
resolveOrderBy,
} from '../../observe/fetchEvents';
import {
ObserveAfterFlag,
ObserveAppVersionFlag,
ObservePlatformFlag,
ObserveProjectIdFlag,
ObserveTimeRangeFlags,
ObserveUpdateIdFlag,
} from '../../observe/flags';
import { METRIC_ALIASES, METRIC_SHORT_NAMES, resolveMetricName } from '../../observe/metricNames';
import { buildObserveEventsJson, buildObserveEventsTable } from '../../observe/formatEvents';
import {
allowedPlatformFlagValues,
appObservePlatformFromFlag,
appPlatformsFromFlag,
} from '../../observe/platforms';
import { appObservePlatformFromFlag, appPlatformsFromFlag } from '../../observe/platforms';
import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext';
import { resolveTimeRange } from '../../observe/startAndEndTime';
import { selectAsync } from '../../prompts';
@@ -44,40 +48,16 @@ export default class ObserveEvents extends EasCommand {
required: false,
default: EventsOrderPreset.Oldest.valueOf().toLowerCase(),
})(),
platform: Flags.option({
description: 'Filter by platform',
options: allowedPlatformFlagValues,
})(),
after: Flags.string({
description:
'Cursor for pagination. Use the endCursor from a previous query to fetch the next page.',
}),
...ObservePlatformFlag,
...ObserveAfterFlag,
limit: getLimitFlagWithCustomValues({
defaultTo: DEFAULT_EVENTS_LIMIT,
limit: 100,
}),
start: Flags.string({
description: 'Start of time range (ISO date)',
exclusive: ['days'],
}),
end: Flags.string({
description: 'End of time range (ISO date)',
exclusive: ['days'],
}),
days: Flags.integer({
description: 'Show events from the last N days (mutually exclusive with --start/--end)',
min: 1,
exclusive: ['start', 'end'],
}),
'app-version': Flags.string({
description: 'Filter by app version',
}),
'update-id': Flags.string({
description: 'Filter by EAS update ID',
}),
'project-id': Flags.string({
description: 'EAS project ID (defaults to the project ID of the current directory)',
}),
...ObserveTimeRangeFlags,
...ObserveAppVersionFlag,
...ObserveUpdateIdFlag,
...ObserveProjectIdFlag,
...EasNonInteractiveAndJsonFlags,
};
+15 -31
View File
@@ -6,6 +6,14 @@ import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
import Log from '../../log';
import { ObserveQuery } from '../../graphql/queries/ObserveQuery';
import { fetchObserveCustomEventsAsync } from '../../observe/fetchCustomEvents';
import {
ObserveAfterFlag,
ObserveAppVersionFlag,
ObservePlatformFlag,
ObserveProjectIdFlag,
ObserveTimeRangeFlags,
ObserveUpdateIdFlag,
} from '../../observe/flags';
import {
buildObserveCustomEventNamesJson,
buildObserveCustomEventNamesTable,
@@ -14,7 +22,7 @@ import {
buildObserveCustomEventsJson,
buildObserveCustomEventsTable,
} from '../../observe/formatCustomEvents';
import { allowedPlatformFlagValues, appObservePlatformFromFlag } from '../../observe/platforms';
import { appObservePlatformFromFlag } from '../../observe/platforms';
import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext';
import { resolveTimeRange } from '../../observe/startAndEndTime';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -34,37 +42,15 @@ export default class ObserveLogs extends EasCommand {
};
static override flags = {
platform: Flags.option({
description: 'Filter by platform',
options: allowedPlatformFlagValues,
})(),
after: Flags.string({
description:
'Cursor for pagination. Use the endCursor from a previous query to fetch the next page.',
}),
...ObservePlatformFlag,
...ObserveAfterFlag,
limit: getLimitFlagWithCustomValues({
defaultTo: DEFAULT_EVENTS_LIMIT,
limit: 100,
}),
start: Flags.string({
description: 'Start of time range (ISO date)',
exclusive: ['days'],
}),
end: Flags.string({
description: 'End of time range (ISO date)',
exclusive: ['days'],
}),
days: Flags.integer({
description: 'Show events from the last N days (mutually exclusive with --start/--end)',
min: 1,
exclusive: ['start', 'end'],
}),
'app-version': Flags.string({
description: 'Filter by app version',
}),
'update-id': Flags.string({
description: 'Filter by EAS update ID',
}),
...ObserveTimeRangeFlags,
...ObserveAppVersionFlag,
...ObserveUpdateIdFlag,
'session-id': Flags.string({
description: 'Filter by session ID',
}),
@@ -73,9 +59,7 @@ export default class ObserveLogs extends EasCommand {
'When no event name argument is provided, list all events across all event names instead of a summary of event names + counts.',
default: false,
}),
'project-id': Flags.string({
description: 'EAS project ID (defaults to the project ID of the current directory)',
}),
...ObserveProjectIdFlag,
...EasNonInteractiveAndJsonFlags,
};
@@ -4,6 +4,11 @@ import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import Log from '../../log';
import { fetchObserveMetricsAsync } from '../../observe/fetchMetrics';
import {
ObservePlatformFlag,
ObserveProjectIdFlag,
ObserveTimeRangeFlags,
} from '../../observe/flags';
import {
StatisticKey,
buildObserveMetricsJson,
@@ -11,7 +16,7 @@ import {
resolveStatKey,
} from '../../observe/formatMetrics';
import { METRIC_ALIASES, resolveMetricName } from '../../observe/metricNames';
import { allowedPlatformFlagValues, appPlatformsFromFlag } from '../../observe/platforms';
import { appPlatformsFromFlag } from '../../observe/platforms';
import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext';
import { resolveTimeRange } from '../../observe/startAndEndTime';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -41,10 +46,7 @@ export default class ObserveMetrics extends EasCommand {
static override description = 'display app performance metrics grouped by app version';
static override flags = {
platform: Flags.option({
description: 'Filter by platform',
options: allowedPlatformFlagValues,
})(),
...ObservePlatformFlag,
metric: Flags.option({
description: 'Metric name to display (can be specified multiple times).',
multiple: true,
@@ -55,22 +57,8 @@ export default class ObserveMetrics extends EasCommand {
multiple: true,
options: DEFAULT_STATS_JSON,
})(),
start: Flags.string({
description: 'Start of time range for metrics data (ISO date).',
exclusive: ['days'],
}),
end: Flags.string({
description: 'End of time range for metrics data (ISO date).',
exclusive: ['days'],
}),
days: Flags.integer({
description: 'Show metrics from the last N days (mutually exclusive with --start/--end)',
min: 1,
exclusive: ['start', 'end'],
}),
'project-id': Flags.string({
description: 'EAS project ID (defaults to the project ID of the current directory)',
}),
...ObserveTimeRangeFlags,
...ObserveProjectIdFlag,
...EasNonInteractiveAndJsonFlags,
};
@@ -0,0 +1,139 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
import Log from '../../log';
import { fetchObserveNavigationRoutesAsync } from '../../observe/fetchNavigationRoutes';
import {
ObserveAfterFlag,
ObserveAppVersionFlag,
ObservePlatformFlag,
ObserveProjectIdFlag,
ObserveTimeRangeFlags,
ObserveUpdateIdFlag,
} from '../../observe/flags';
import {
NAVIGATION_METRIC_NAMES,
NavigationStatKey,
buildObserveNavigationRoutesJson,
buildObserveNavigationRoutesTable,
resolveNavigationStatKey,
} from '../../observe/formatNavigationRoutes';
import { NAVIGATION_METRIC_ALIASES, resolveNavigationMetricName } from '../../observe/metricNames';
import { appPlatformsFromFlag } from '../../observe/platforms';
import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext';
import { resolveTimeRange } from '../../observe/startAndEndTime';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
const DEFAULT_ROUTES_LIMIT = 50;
const STAT_OPTIONS = ['median', 'med', 'p90', 'count', 'event_count', 'eventCount'];
const DEFAULT_STATS_TABLE: NavigationStatKey[] = ['median', 'count'];
const DEFAULT_STATS_JSON: NavigationStatKey[] = ['median', 'p90', 'count'];
export default class ObserveRoutes extends EasCommand {
static override hidden = true;
static override description =
'display app navigation route metrics (Cold TTR, Warm TTR, TTI) grouped by route name';
static override flags = {
...ObservePlatformFlag,
metric: Flags.option({
description:
'Navigation metric to display (can be specified multiple times). Defaults to all three.',
multiple: true,
options: Object.keys(NAVIGATION_METRIC_ALIASES),
})(),
stat: Flags.option({
description: 'Statistic to display per metric (can be specified multiple times)',
multiple: true,
options: STAT_OPTIONS,
})(),
...ObserveAfterFlag,
limit: getLimitFlagWithCustomValues({
defaultTo: DEFAULT_ROUTES_LIMIT,
limit: 200,
}),
...ObserveTimeRangeFlags,
...ObserveAppVersionFlag,
...ObserveUpdateIdFlag,
'build-number': Flags.string({
description: 'Filter by app build number',
}),
...ObserveProjectIdFlag,
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
private static loggedInOnlyContextDefinition = {
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(ObserveRoutes);
const { projectId, graphqlClient } = await resolveObserveCommandContextAsync({
command: this,
commandClass: ObserveRoutes,
loggedInOnlyContextDefinition: ObserveRoutes.loggedInOnlyContextDefinition,
projectIdOverride: flags['project-id'],
nonInteractive: flags['non-interactive'],
});
if (flags.json) {
enableJsonOutput();
} else {
Log.warn('EAS Observe is in preview and subject to breaking changes.');
}
const metricNames = flags.metric?.length
? Array.from(new Set(flags.metric.map(resolveNavigationMetricName)))
: NAVIGATION_METRIC_NAMES;
const argumentsStat = flags.stat?.length
? Array.from(new Set(flags.stat.map(resolveNavigationStatKey)))
: undefined;
const { daysBack, startTime, endTime } = resolveTimeRange(flags);
const platforms = appPlatformsFromFlag(flags.platform);
const { routes, pageInfoByPlatform } = await fetchObserveNavigationRoutesAsync(
graphqlClient,
projectId,
{
startTime,
endTime,
platforms,
limit: flags.limit ?? DEFAULT_ROUTES_LIMIT,
...(flags.after && { after: flags.after }),
appVersion: flags['app-version'],
updateId: flags['update-id'],
buildNumber: flags['build-number'],
}
);
if (flags.json) {
const stats = argumentsStat ?? DEFAULT_STATS_JSON;
printJsonOnlyOutput(
buildObserveNavigationRoutesJson(routes, metricNames, stats, pageInfoByPlatform)
);
} else {
const stats = argumentsStat ?? DEFAULT_STATS_TABLE;
Log.addNewLineIfNone();
Log.log(
buildObserveNavigationRoutesTable(routes, metricNames, stats, {
daysBack,
startTime,
endTime,
pageInfoByPlatform,
})
);
}
}
}
@@ -1,11 +1,14 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import Log from '../../log';
import { fetchObserveVersionsAsync } from '../../observe/fetchVersions';
import {
ObservePlatformFlag,
ObserveProjectIdFlag,
ObserveTimeRangeFlags,
} from '../../observe/flags';
import { buildObserveVersionsJson, buildObserveVersionsTable } from '../../observe/formatVersions';
import { allowedPlatformFlagValues, appPlatformsFromFlag } from '../../observe/platforms';
import { appPlatformsFromFlag } from '../../observe/platforms';
import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext';
import { resolveTimeRange } from '../../observe/startAndEndTime';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -15,26 +18,9 @@ export default class ObserveVersions extends EasCommand {
static override description = 'display app versions with build and update details';
static override flags = {
platform: Flags.option({
description: 'Filter by platform',
options: allowedPlatformFlagValues,
})(),
start: Flags.string({
description: 'Start of time range (ISO date)',
exclusive: ['days'],
}),
end: Flags.string({
description: 'End of time range (ISO date)',
exclusive: ['days'],
}),
days: Flags.integer({
description: 'Show versions from the last N days (mutually exclusive with --start/--end)',
min: 1,
exclusive: ['start', 'end'],
}),
'project-id': Flags.string({
description: 'EAS project ID (defaults to the project ID of the current directory)',
}),
...ObservePlatformFlag,
...ObserveTimeRangeFlags,
...ObserveProjectIdFlag,
...EasNonInteractiveAndJsonFlags,
};
+12
View File
@@ -4029,6 +4029,7 @@ export type BuildMetadataInput = {
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
distribution?: InputMaybe<DistributionType>;
environment?: InputMaybe<Scalars['String']['input']>;
expoPackageVersion?: InputMaybe<Scalars['String']['input']>;
fingerprintHash?: InputMaybe<Scalars['String']['input']>;
fingerprintSource?: InputMaybe<FingerprintSourceInput>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
@@ -12903,6 +12904,17 @@ export type AppObserveCustomEventNamesQueryVariables = Exact<{
export type AppObserveCustomEventNamesQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, observe: { __typename?: 'AppObserve', customEventNames: { __typename?: 'AppObserveCustomEventNames', isTruncated: boolean, names: Array<{ __typename?: 'AppObserveCustomEventName', eventName: string, count: number }> } } } } };
export type AppObserveNavigationRoutesQueryVariables = Exact<{
appId: Scalars['String']['input'];
filter: AppObserveNavigationRoutesFilter;
first?: InputMaybe<Scalars['Int']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
orderBy?: InputMaybe<AppObserveNavigationRoutesOrderBy>;
}>;
export type AppObserveNavigationRoutesQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, observe: { __typename?: 'AppObserve', navigationRoutes: { __typename?: 'AppObserveNavigationRoutesConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: string | null }, edges: Array<{ __typename?: 'AppObserveNavigationRouteEdge', cursor: string, node: { __typename?: 'AppObserveNavigationRoute', routeName: string, coldTtr: { __typename?: 'AppObserveNavigationStat', count: number, median?: number | null, p90?: number | null }, warmTtr: { __typename?: 'AppObserveNavigationStat', count: number, median?: number | null, p90?: number | null }, tti: { __typename?: 'AppObserveNavigationStat', count: number, median?: number | null, p90?: number | null } } }> } } } } };
export type GetAssetMetadataQueryVariables = Exact<{
storageKeys: Array<Scalars['String']['input']> | Scalars['String']['input'];
}>;
@@ -10,6 +10,9 @@ import {
AppObserveEvent,
AppObserveEventsFilter,
AppObserveEventsOrderBy,
AppObserveNavigationRoute,
AppObserveNavigationRoutesFilter,
AppObserveNavigationRoutesOrderBy,
AppObservePlatform,
AppObserveReleasesInput,
AppObserveTimeSeriesInput,
@@ -133,6 +136,31 @@ type AppObserveCustomEventNamesQueryVariables = {
environment?: string;
};
type AppObserveNavigationRoutesQuery = {
app: {
byId: {
id: string;
observe: {
navigationRoutes: {
pageInfo: PageInfo;
edges: Array<{
cursor: string;
node: AppObserveNavigationRoute;
}>;
};
};
};
};
};
type AppObserveNavigationRoutesQueryVariables = {
appId: string;
filter: AppObserveNavigationRoutesFilter;
first?: number;
after?: string;
orderBy?: AppObserveNavigationRoutesOrderBy;
};
export const ObserveQuery = {
async timeSeriesAsync(
graphqlClient: ExpoGraphqlClient,
@@ -398,4 +426,68 @@ export const ObserveQuery = {
return data.app.byId.observe.customEventNames;
},
async navigationRoutesAsync(
graphqlClient: ExpoGraphqlClient,
variables: AppObserveNavigationRoutesQueryVariables
): Promise<{ routes: AppObserveNavigationRoute[]; pageInfo: PageInfo }> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppObserveNavigationRoutesQuery, AppObserveNavigationRoutesQueryVariables>(
gql`
query AppObserveNavigationRoutes(
$appId: String!
$filter: AppObserveNavigationRoutesFilter!
$first: Int
$after: String
$orderBy: AppObserveNavigationRoutesOrderBy
) {
app {
byId(appId: $appId) {
id
observe {
navigationRoutes(filter: $filter, first: $first, after: $after, orderBy: $orderBy) {
pageInfo {
hasNextPage
hasPreviousPage
endCursor
}
edges {
cursor
node {
routeName
coldTtr {
count
median
p90
}
warmTtr {
count
median
p90
}
tti {
count
median
p90
}
}
}
}
}
}
}
}
`,
variables
)
.toPromise()
);
const { edges, pageInfo } = data.app.byId.observe.navigationRoutes;
return {
routes: edges.map(edge => edge.node),
pageInfo,
};
},
};
@@ -0,0 +1,162 @@
import {
AppObserveEventsOrderByDirection,
AppObserveNavigationRoute,
AppObserveNavigationRoutesOrderByField,
AppObservePlatform,
AppPlatform,
} from '../../graphql/generated';
import { ObserveQuery } from '../../graphql/queries/ObserveQuery';
import { fetchObserveNavigationRoutesAsync } from '../fetchNavigationRoutes';
jest.mock('../../graphql/queries/ObserveQuery');
jest.mock('../../log');
function makeRoute(routeName: string): AppObserveNavigationRoute {
return {
__typename: 'AppObserveNavigationRoute' as const,
routeName,
coldTtr: { __typename: 'AppObserveNavigationStat' as const, count: 10, median: 0.5, p90: 0.9 },
warmTtr: { __typename: 'AppObserveNavigationStat' as const, count: 8, median: 0.3, p90: 0.6 },
tti: { __typename: 'AppObserveNavigationStat' as const, count: 12, median: 0.45, p90: 0.7 },
};
}
describe('fetchObserveNavigationRoutesAsync', () => {
const mockNavigationRoutesAsync = jest.mocked(ObserveQuery.navigationRoutesAsync);
const mockGraphqlClient = {} as any;
beforeEach(() => {
mockNavigationRoutesAsync.mockClear();
mockNavigationRoutesAsync.mockResolvedValue({
routes: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
});
});
it('passes appId, time range, limit, and filter values for each platform', async () => {
await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', {
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios],
limit: 25,
appVersion: '2.1.0',
updateId: 'update-xyz',
buildNumber: '42',
});
expect(mockNavigationRoutesAsync).toHaveBeenCalledTimes(1);
const call = mockNavigationRoutesAsync.mock.calls[0][1];
expect(call.appId).toBe('project-123');
expect(call.first).toBe(25);
expect(call.filter).toEqual({
platform: AppObservePlatform.Ios,
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
appVersion: '2.1.0',
appUpdateId: 'update-xyz',
appBuildNumber: '42',
});
});
it('defaults orderBy to NAVIGATION_COUNT DESC', async () => {
await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', {
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios],
limit: 50,
});
const call = mockNavigationRoutesAsync.mock.calls[0][1];
expect(call.orderBy).toEqual({
field: AppObserveNavigationRoutesOrderByField.NavigationCount,
direction: AppObserveEventsOrderByDirection.Desc,
});
});
it('does not send optional filters when not provided', async () => {
await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', {
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios],
limit: 50,
});
const filter = mockNavigationRoutesAsync.mock.calls[0][1].filter;
expect(filter).not.toHaveProperty('appVersion');
expect(filter).not.toHaveProperty('appUpdateId');
expect(filter).not.toHaveProperty('appBuildNumber');
});
it('forwards after cursor when provided', async () => {
await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', {
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios],
limit: 50,
after: 'cursor-abc',
});
expect(mockNavigationRoutesAsync.mock.calls[0][1].after).toBe('cursor-abc');
});
it('fans out across multiple platforms and tags each route with its platform', async () => {
mockNavigationRoutesAsync.mockImplementation(async (_client, vars) => {
if (vars.filter.platform === AppObservePlatform.Android) {
return {
routes: [makeRoute('/android-home')],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
};
}
return {
routes: [makeRoute('/ios-home')],
pageInfo: { hasNextPage: true, hasPreviousPage: false, endCursor: 'ios-cursor' },
};
});
const { routes, pageInfoByPlatform } = await fetchObserveNavigationRoutesAsync(
mockGraphqlClient,
'project-123',
{
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios, AppPlatform.Android],
limit: 50,
}
);
expect(mockNavigationRoutesAsync).toHaveBeenCalledTimes(2);
expect(routes).toHaveLength(2);
const byName = new Map(routes.map(r => [r.route.routeName, r.platform]));
expect(byName.get('/android-home')).toBe(AppPlatform.Android);
expect(byName.get('/ios-home')).toBe(AppPlatform.Ios);
expect(pageInfoByPlatform.get(AppPlatform.Ios)?.hasNextPage).toBe(true);
expect(pageInfoByPlatform.get(AppPlatform.Android)?.hasNextPage).toBe(false);
});
it('handles partial failures gracefully', async () => {
mockNavigationRoutesAsync.mockImplementation(async (_client, vars) => {
if (vars.filter.platform === AppObservePlatform.Android) {
throw new Error('Network error');
}
return {
routes: [makeRoute('/home')],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
};
});
const { routes, pageInfoByPlatform } = await fetchObserveNavigationRoutesAsync(
mockGraphqlClient,
'project-123',
{
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios, AppPlatform.Android],
limit: 50,
}
);
expect(routes).toHaveLength(1);
expect(routes[0].platform).toBe(AppPlatform.Ios);
expect(pageInfoByPlatform.has(AppPlatform.Android)).toBe(false);
});
});
@@ -71,12 +71,12 @@ describe(buildObserveMetricsTable, () => {
"Med values (event count)
iOS
App Version Cold Launch TTI 
----------- ----------- ----------
1.2.0 0.35s (110) 1.32s (90)
App Version Cold Launch Startup TTI
----------- ----------- -----------
1.2.0 0.35s (110) 1.32s (90)
Android
App Version Cold Launch TTI 
App Version Cold Launch Startup TTI
----------- ----------- -----------
1.1.0 0.25s (120) 1.12s (100)"
`);
@@ -93,9 +93,9 @@ describe(buildObserveMetricsTable, () => {
"Med values (event count)
iOS
App Version Cold Launch TTI 
----------- ----------- -----
2.0.0 - (-) - (-)"
App Version Cold Launch Startup TTI
----------- ----------- -----------
2.0.0 - (-) - (-) "
`);
});
@@ -0,0 +1,210 @@
import { AppPlatform } from '../../graphql/generated';
import { NavigationRouteWithPlatform } from '../fetchNavigationRoutes';
import {
buildObserveNavigationRoutesJson,
buildObserveNavigationRoutesTable,
isNavigationMetric,
resolveNavigationStatKey,
} from '../formatNavigationRoutes';
function makeRoute(
routeName: string,
platform: AppPlatform,
overrides?: Partial<{
coldTtr: { count: number; median: number | null; p90: number | null };
warmTtr: { count: number; median: number | null; p90: number | null };
tti: { count: number; median: number | null; p90: number | null };
}>
): NavigationRouteWithPlatform {
return {
platform,
route: {
__typename: 'AppObserveNavigationRoute' as const,
routeName,
coldTtr: {
__typename: 'AppObserveNavigationStat' as const,
count: 10,
median: 0.5,
p90: 0.9,
...overrides?.coldTtr,
},
warmTtr: {
__typename: 'AppObserveNavigationStat' as const,
count: 8,
median: 0.3,
p90: 0.6,
...overrides?.warmTtr,
},
tti: {
__typename: 'AppObserveNavigationStat' as const,
count: 12,
median: 0.45,
p90: 0.7,
...overrides?.tti,
},
},
};
}
describe(isNavigationMetric, () => {
it('returns true for each known navigation metric full name', () => {
expect(isNavigationMetric('expo.navigation.cold_ttr')).toBe(true);
expect(isNavigationMetric('expo.navigation.warm_ttr')).toBe(true);
expect(isNavigationMetric('expo.navigation.tti')).toBe(true);
});
it('returns false for app-startup metrics or unknown names', () => {
expect(isNavigationMetric('expo.app_startup.tti')).toBe(false);
expect(isNavigationMetric('cold_ttr')).toBe(false);
expect(isNavigationMetric('unknown')).toBe(false);
});
});
describe(resolveNavigationStatKey, () => {
it('resolves aliases', () => {
expect(resolveNavigationStatKey('med')).toBe('median');
expect(resolveNavigationStatKey('median')).toBe('median');
expect(resolveNavigationStatKey('p90')).toBe('p90');
expect(resolveNavigationStatKey('count')).toBe('count');
expect(resolveNavigationStatKey('eventCount')).toBe('count');
expect(resolveNavigationStatKey('event_count')).toBe('count');
});
it('throws on unknown stat', () => {
expect(() => resolveNavigationStatKey('min')).toThrow('Unknown statistic');
});
});
describe(buildObserveNavigationRoutesTable, () => {
it('returns a yellow warning when no routes are present', () => {
const output = buildObserveNavigationRoutesTable(
[],
['expo.navigation.cold_ttr'],
['median', 'count']
);
expect(output).toContain('No navigation routes found.');
});
it('formats routes grouped by platform with merged (med + count) cells', () => {
const routes = [
makeRoute('/home', AppPlatform.Ios),
makeRoute('/profile', AppPlatform.Ios, {
coldTtr: { count: 5, median: 0.7, p90: 1.0 },
}),
makeRoute('/home', AppPlatform.Android),
];
const output = buildObserveNavigationRoutesTable(
routes,
['expo.navigation.cold_ttr', 'expo.navigation.warm_ttr', 'expo.navigation.tti'],
['median', 'count'],
{ daysBack: 7 }
);
expect(output).toContain('Med values (navigation count) for the last 7 days');
expect(output).toContain('Cold TTR');
expect(output).toContain('Warm TTR');
expect(output).toContain('Nav TTI');
expect(output).toContain('/home');
expect(output).toContain('/profile');
expect(output).toContain('0.50s (10)');
expect(output).toContain('0.70s (5)');
expect(output).toContain('iOS');
expect(output).toContain('Android');
});
it('renders separate columns when count is omitted from stats', () => {
const routes = [makeRoute('/home', AppPlatform.Ios)];
const output = buildObserveNavigationRoutesTable(
routes,
['expo.navigation.cold_ttr'],
['median', 'p90']
);
expect(output).toContain('Med, P90 values');
expect(output).toContain('Cold TTR Med');
expect(output).toContain('Cold TTR P90');
expect(output).toContain('0.50s');
expect(output).toContain('0.90s');
});
it('renders count-only column when only count stat is requested', () => {
const routes = [makeRoute('/home', AppPlatform.Ios)];
const output = buildObserveNavigationRoutesTable(
routes,
['expo.navigation.cold_ttr'],
['count']
);
expect(output).toContain('Navigation count values');
expect(output).toContain('Cold TTR Count');
expect(output).toContain('10');
});
it('shows a next-page hint per platform when hasNextPage is true', () => {
const routes = [makeRoute('/home', AppPlatform.Ios)];
const pageInfoByPlatform = new Map([
[AppPlatform.Ios, { hasNextPage: true, endCursor: 'cursor-ios' }],
]);
const output = buildObserveNavigationRoutesTable(
routes,
['expo.navigation.cold_ttr'],
['median', 'count'],
{ pageInfoByPlatform }
);
expect(output).toContain('Next page (iOS): --after cursor-ios');
});
});
describe(buildObserveNavigationRoutesJson, () => {
it('maps routes to the requested metrics and stats, including pageInfoByPlatform', () => {
const routes = [makeRoute('/home', AppPlatform.Ios)];
const pageInfoByPlatform = new Map([
[AppPlatform.Ios, { hasNextPage: true, endCursor: 'cursor-ios' }],
]);
const result = buildObserveNavigationRoutesJson(
routes,
['expo.navigation.cold_ttr', 'expo.navigation.tti'],
['median', 'p90', 'count'],
pageInfoByPlatform
);
expect(result.routes).toEqual([
{
routeName: '/home',
platform: AppPlatform.Ios,
metrics: {
'expo.navigation.cold_ttr': { median: 0.5, p90: 0.9, count: 10 },
'expo.navigation.tti': { median: 0.45, p90: 0.7, count: 12 },
},
},
]);
expect(result.pageInfoByPlatform).toEqual({
[AppPlatform.Ios]: { hasNextPage: true, endCursor: 'cursor-ios' },
});
});
it('returns null for stats when the underlying value is null', () => {
const routes = [
makeRoute('/home', AppPlatform.Ios, {
coldTtr: { count: 0, median: null, p90: null },
}),
];
const result = buildObserveNavigationRoutesJson(
routes,
['expo.navigation.cold_ttr'],
['median', 'p90', 'count'],
new Map()
);
expect(result.routes[0].metrics['expo.navigation.cold_ttr']).toEqual({
median: null,
p90: null,
count: 0,
});
});
});
@@ -1,4 +1,8 @@
import { getMetricDisplayName, resolveMetricName } from '../metricNames';
import {
getMetricDisplayName,
resolveMetricName,
resolveNavigationMetricName,
} from '../metricNames';
describe(resolveMetricName, () => {
it('resolves short alias "tti" to full metric name', () => {
@@ -37,15 +41,46 @@ describe(resolveMetricName, () => {
});
});
describe(resolveNavigationMetricName, () => {
it('resolves short aliases to navigation metric full names', () => {
expect(resolveNavigationMetricName('cold_ttr')).toBe('expo.navigation.cold_ttr');
expect(resolveNavigationMetricName('warm_ttr')).toBe('expo.navigation.warm_ttr');
expect(resolveNavigationMetricName('nav_tti')).toBe('expo.navigation.tti');
});
it('passes through full navigation metric names unchanged', () => {
expect(resolveNavigationMetricName('expo.navigation.cold_ttr')).toBe(
'expo.navigation.cold_ttr'
);
expect(resolveNavigationMetricName('expo.navigation.warm_ttr')).toBe(
'expo.navigation.warm_ttr'
);
expect(resolveNavigationMetricName('expo.navigation.tti')).toBe('expo.navigation.tti');
});
it('throws on unknown alias or non-navigation metric', () => {
expect(() => resolveNavigationMetricName('unknown')).toThrow('Unknown navigation metric');
expect(() => resolveNavigationMetricName('expo.app_startup.tti')).toThrow(
'Unknown navigation metric'
);
});
});
describe(getMetricDisplayName, () => {
it('returns short display name for known metrics', () => {
it('returns short display name for known app-startup metrics', () => {
expect(getMetricDisplayName('expo.app_startup.cold_launch_time')).toBe('Cold Launch');
expect(getMetricDisplayName('expo.app_startup.warm_launch_time')).toBe('Warm Launch');
expect(getMetricDisplayName('expo.app_startup.tti')).toBe('TTI');
expect(getMetricDisplayName('expo.app_startup.ttr')).toBe('TTR');
expect(getMetricDisplayName('expo.app_startup.tti')).toBe('Startup TTI');
expect(getMetricDisplayName('expo.app_startup.ttr')).toBe('Startup TTR');
expect(getMetricDisplayName('expo.app_startup.bundle_load_time')).toBe('Bundle Load');
});
it('returns short display name for known navigation metrics', () => {
expect(getMetricDisplayName('expo.navigation.cold_ttr')).toBe('Nav Cold TTR');
expect(getMetricDisplayName('expo.navigation.warm_ttr')).toBe('Nav Warm TTR');
expect(getMetricDisplayName('expo.navigation.tti')).toBe('Nav TTI');
});
it('returns the full metric name for unknown metrics', () => {
expect(getMetricDisplayName('custom.metric.name')).toBe('custom.metric.name');
});
+2 -6
View File
@@ -1,6 +1,6 @@
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import { AppObservePlatform, AppPlatform } from '../graphql/generated';
import { AppPlatform } from '../graphql/generated';
import { ObserveQuery } from '../graphql/queries/ObserveQuery';
import Log from '../log';
import {
@@ -10,11 +10,7 @@ import {
UpdateIdsMap,
makeMetricsKey,
} from './formatMetrics';
const appPlatformToObservePlatform: Record<AppPlatform, AppObservePlatform> = {
[AppPlatform.Android]: AppObservePlatform.Android,
[AppPlatform.Ios]: AppObservePlatform.Ios,
};
import { appPlatformToObservePlatform } from './platforms';
export function validateDateFlag(value: string, flagName: string): void {
const parsed = new Date(value);
@@ -0,0 +1,86 @@
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
AppObserveEventsOrderByDirection,
AppObserveNavigationRoute,
AppObserveNavigationRoutesOrderBy,
AppObserveNavigationRoutesOrderByField,
AppPlatform,
PageInfo,
} from '../graphql/generated';
import { ObserveQuery } from '../graphql/queries/ObserveQuery';
import Log from '../log';
import { appPlatformToObservePlatform } from './platforms';
export interface NavigationRouteWithPlatform {
platform: AppPlatform;
route: AppObserveNavigationRoute;
}
export interface FetchNavigationRoutesOptions {
startTime: string;
endTime: string;
platforms: AppPlatform[];
limit: number;
after?: string;
appVersion?: string;
updateId?: string;
buildNumber?: string;
orderBy?: AppObserveNavigationRoutesOrderBy;
}
export interface FetchNavigationRoutesResult {
routes: NavigationRouteWithPlatform[];
pageInfoByPlatform: Map<AppPlatform, PageInfo>;
}
export async function fetchObserveNavigationRoutesAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
options: FetchNavigationRoutesOptions
): Promise<FetchNavigationRoutesResult> {
const orderBy: AppObserveNavigationRoutesOrderBy = options.orderBy ?? {
field: AppObserveNavigationRoutesOrderByField.NavigationCount,
direction: AppObserveEventsOrderByDirection.Desc,
};
const queries = options.platforms.map(async appPlatform => {
const observePlatform = appPlatformToObservePlatform[appPlatform];
try {
const result = await ObserveQuery.navigationRoutesAsync(graphqlClient, {
appId,
filter: {
platform: observePlatform,
startTime: options.startTime,
endTime: options.endTime,
...(options.appVersion && { appVersion: options.appVersion }),
...(options.updateId && { appUpdateId: options.updateId }),
...(options.buildNumber && { appBuildNumber: options.buildNumber }),
},
first: options.limit,
...(options.after && { after: options.after }),
orderBy,
});
return { appPlatform, ...result };
} catch (error: any) {
Log.warn(`Failed to fetch navigation routes on ${observePlatform}: ${error.message}`);
return null;
}
});
const results = await Promise.all(queries);
const routes: NavigationRouteWithPlatform[] = [];
const pageInfoByPlatform = new Map<AppPlatform, PageInfo>();
for (const result of results) {
if (!result) {
continue;
}
pageInfoByPlatform.set(result.appPlatform, result.pageInfo);
for (const route of result.routes) {
routes.push({ platform: result.appPlatform, route });
}
}
return { routes, pageInfoByPlatform };
}
+51
View File
@@ -0,0 +1,51 @@
import { Flags } from '@oclif/core';
import { allowedPlatformFlagValues } from './platforms';
export const ObserveProjectIdFlag = {
'project-id': Flags.string({
description: 'EAS project ID (defaults to the project ID of the current directory)',
}),
};
export const ObservePlatformFlag = {
platform: Flags.option({
description: 'Filter by platform',
options: allowedPlatformFlagValues,
})(),
};
export const ObserveTimeRangeFlags = {
start: Flags.string({
description: 'Start of time range (ISO date)',
exclusive: ['days'],
}),
end: Flags.string({
description: 'End of time range (ISO date)',
exclusive: ['days'],
}),
days: Flags.integer({
description: 'Show results from the last N days (mutually exclusive with --start/--end)',
min: 1,
exclusive: ['start', 'end'],
}),
};
export const ObserveAfterFlag = {
after: Flags.string({
description:
'Cursor for pagination. Use the endCursor from a previous query to fetch the next page.',
}),
};
export const ObserveAppVersionFlag = {
'app-version': Flags.string({
description: 'Filter by app version',
}),
};
export const ObserveUpdateIdFlag = {
'update-id': Flags.string({
description: 'Filter by EAS update ID',
}),
};
@@ -0,0 +1,239 @@
import chalk from 'chalk';
import { EasCommandError } from '../commandUtils/errors';
import { AppPlatform } from '../graphql/generated';
import { appPlatformDisplayNames } from '../platform';
import renderTextTable from '../utils/renderTextTable';
import { NavigationRouteWithPlatform } from './fetchNavigationRoutes';
import { buildTimeRangeDescription } from './formatUtils';
import { getMetricDisplayName } from './metricNames';
export type NavigationStatKey = 'median' | 'p90' | 'count';
export const NAVIGATION_STAT_ALIASES: Record<string, NavigationStatKey> = {
med: 'median',
median: 'median',
p90: 'p90',
count: 'count',
event_count: 'count',
eventCount: 'count',
};
export const NAVIGATION_STAT_DISPLAY_NAMES: Record<NavigationStatKey, string> = {
median: 'Med',
p90: 'P90',
count: 'Count',
};
export function resolveNavigationStatKey(input: string): NavigationStatKey {
const resolved = NAVIGATION_STAT_ALIASES[input];
if (resolved) {
return resolved;
}
throw new EasCommandError(
`Unknown statistic: "${input}". Valid options: ${Object.keys(NAVIGATION_STAT_ALIASES).join(
', '
)}`
);
}
type NavigationRouteField = 'coldTtr' | 'warmTtr' | 'tti';
const FULL_METRIC_NAME_TO_FIELD: Record<string, NavigationRouteField> = {
'expo.navigation.cold_ttr': 'coldTtr',
'expo.navigation.warm_ttr': 'warmTtr',
'expo.navigation.tti': 'tti',
};
export const NAVIGATION_METRIC_NAMES = Object.keys(FULL_METRIC_NAME_TO_FIELD);
export function isNavigationMetric(metricName: string): boolean {
return metricName in FULL_METRIC_NAME_TO_FIELD;
}
function formatStatValue(stat: NavigationStatKey, value: number | null | undefined): string {
if (value == null) {
return '-';
}
if (stat === 'count') {
return String(value);
}
return `${value.toFixed(2)}s`;
}
function formatMergedCell(
stat: NavigationStatKey,
statValue: number | null | undefined,
count: number | null | undefined
): string {
const formatted = formatStatValue(stat, statValue);
const countStr = count != null ? String(count) : '-';
return `${formatted} (${countStr})`;
}
export interface NavigationRouteValuesJson {
routeName: string;
platform: AppPlatform;
metrics: Record<string, Partial<Record<NavigationStatKey, number | null>>>;
}
export interface ObserveNavigationRoutesJsonOutput {
routes: NavigationRouteValuesJson[];
pageInfoByPlatform: Record<string, { hasNextPage: boolean; endCursor: string | null }>;
}
function metricStat(
node: NavigationRouteWithPlatform,
metricName: string
): { count: number; median: number | null; p90: number | null } {
const field = FULL_METRIC_NAME_TO_FIELD[metricName];
if (!field) {
throw new EasCommandError(`Unknown navigation metric: "${metricName}"`);
}
const stat = node.route[field];
return {
count: stat.count,
median: stat.median ?? null,
p90: stat.p90 ?? null,
};
}
export function buildObserveNavigationRoutesJson(
routes: NavigationRouteWithPlatform[],
metricNames: string[],
stats: NavigationStatKey[],
pageInfoByPlatform: Map<AppPlatform, { hasNextPage: boolean; endCursor?: string | null }>
): ObserveNavigationRoutesJsonOutput {
const jsonRoutes: NavigationRouteValuesJson[] = routes.map(node => {
const metrics: Record<string, Partial<Record<NavigationStatKey, number | null>>> = {};
for (const metricName of metricNames) {
const values = metricStat(node, metricName);
const statValues: Partial<Record<NavigationStatKey, number | null>> = {};
for (const stat of stats) {
statValues[stat] = values[stat] ?? null;
}
metrics[metricName] = statValues;
}
return {
routeName: node.route.routeName,
platform: node.platform,
metrics,
};
});
const pageInfoByPlatformOutput: Record<
string,
{ hasNextPage: boolean; endCursor: string | null }
> = {};
for (const [platform, pageInfo] of pageInfoByPlatform) {
pageInfoByPlatformOutput[platform] = {
hasNextPage: pageInfo.hasNextPage,
endCursor: pageInfo.endCursor ?? null,
};
}
return { routes: jsonRoutes, pageInfoByPlatform: pageInfoByPlatformOutput };
}
export interface BuildNavigationRoutesTableOptions {
daysBack?: number;
startTime?: string;
endTime?: string;
pageInfoByPlatform?: Map<AppPlatform, { hasNextPage: boolean; endCursor?: string | null }>;
}
export function buildObserveNavigationRoutesTable(
routes: NavigationRouteWithPlatform[],
metricNames: string[],
stats: NavigationStatKey[],
options?: BuildNavigationRoutesTableOptions
): string {
if (routes.length === 0) {
return chalk.yellow('No navigation routes found.');
}
const displayStats = stats.filter(s => s !== 'count');
const hasCount = stats.includes('count');
const statsDesc =
displayStats.length > 0
? displayStats.map(s => NAVIGATION_STAT_DISPLAY_NAMES[s]).join(', ')
: 'Navigation count';
const timeDesc = buildTimeRangeDescription({
daysBack: options?.daysBack,
startTime: options?.startTime,
endTime: options?.endTime,
});
const countSuffix = hasCount && displayStats.length > 0 ? ' (navigation count)' : '';
const summaryLine = `${statsDesc} values${countSuffix}${timeDesc ? ` ${timeDesc}` : ''}`;
const byPlatform = new Map<AppPlatform, NavigationRouteWithPlatform[]>();
for (const node of routes) {
if (!byPlatform.has(node.platform)) {
byPlatform.set(node.platform, []);
}
byPlatform.get(node.platform)!.push(node);
}
const metricHeaders: string[] = [];
for (const metricName of metricNames) {
const displayName = getMetricDisplayName(metricName);
if (displayStats.length > 0 && hasCount) {
for (const stat of displayStats) {
metricHeaders.push(
displayStats.length > 1
? `${displayName} ${NAVIGATION_STAT_DISPLAY_NAMES[stat]}`
: displayName
);
}
} else {
for (const stat of displayStats.length > 0
? displayStats
: (['count'] as NavigationStatKey[])) {
metricHeaders.push(
displayStats.length === 0
? `${displayName} Count`
: `${displayName} ${NAVIGATION_STAT_DISPLAY_NAMES[stat]}`
);
}
}
}
const headers = ['Route', ...metricHeaders];
const sections: string[] = [chalk.bold(summaryLine)];
for (const [platform, platformRoutes] of byPlatform) {
sections.push('');
sections.push(chalk.bold(appPlatformDisplayNames[platform]));
const rows: string[][] = platformRoutes.map(node => {
const cells: string[] = [];
for (const metricName of metricNames) {
const values = metricStat(node, metricName);
if (displayStats.length > 0 && hasCount) {
for (const stat of displayStats) {
cells.push(formatMergedCell(stat, values[stat], values.count));
}
} else {
for (const stat of displayStats.length > 0
? displayStats
: (['count'] as NavigationStatKey[])) {
cells.push(formatStatValue(stat, values[stat]));
}
}
}
return [node.route.routeName, ...cells];
});
sections.push(renderTextTable(headers, rows));
const pageInfo = options?.pageInfoByPlatform?.get(platform);
if (pageInfo?.hasNextPage && pageInfo.endCursor) {
sections.push(
`Next page (${appPlatformDisplayNames[platform]}): --after ${pageInfo.endCursor}`
);
}
}
return sections.join('\n');
}
+26 -2
View File
@@ -8,14 +8,24 @@ export const METRIC_ALIASES: Record<string, string> = {
bundle_load: 'expo.app_startup.bundle_load_time',
};
export const NAVIGATION_METRIC_ALIASES: Record<string, string> = {
cold_ttr: 'expo.navigation.cold_ttr',
warm_ttr: 'expo.navigation.warm_ttr',
nav_tti: 'expo.navigation.tti',
};
const KNOWN_FULL_NAMES = new Set(Object.values(METRIC_ALIASES));
const KNOWN_FULL_NAVIGATION_NAMES = new Set(Object.values(NAVIGATION_METRIC_ALIASES));
export const METRIC_SHORT_NAMES: Record<string, string> = {
'expo.app_startup.cold_launch_time': 'Cold Launch',
'expo.app_startup.warm_launch_time': 'Warm Launch',
'expo.app_startup.tti': 'TTI',
'expo.app_startup.ttr': 'TTR',
'expo.app_startup.tti': 'Startup TTI',
'expo.app_startup.ttr': 'Startup TTR',
'expo.app_startup.bundle_load_time': 'Bundle Load',
'expo.navigation.cold_ttr': 'Nav Cold TTR',
'expo.navigation.warm_ttr': 'Nav Warm TTR',
'expo.navigation.tti': 'Nav TTI',
};
export function resolveMetricName(input: string): string {
@@ -30,6 +40,20 @@ export function resolveMetricName(input: string): string {
);
}
export function resolveNavigationMetricName(input: string): string {
if (NAVIGATION_METRIC_ALIASES[input]) {
return NAVIGATION_METRIC_ALIASES[input];
}
if (KNOWN_FULL_NAVIGATION_NAMES.has(input)) {
return input;
}
throw new EasCommandError(
`Unknown navigation metric: "${input}". Use a full metric name (e.g. expo.navigation.cold_ttr) or a short alias: ${Object.keys(
NAVIGATION_METRIC_ALIASES
).join(', ')}`
);
}
export function getMetricDisplayName(metricName: string): string {
return METRIC_SHORT_NAMES[metricName] ?? metricName;
}
@@ -51,3 +51,8 @@ export function appPlatformsFromFlag(flag: PlatformFlagValue | undefined): AppPl
}
return [defaultAppPlatform];
}
export const appPlatformToObservePlatform: Record<AppPlatform, AppObservePlatform> = {
[AppPlatform.Android]: AppObservePlatform.Android,
[AppPlatform.Ios]: AppObservePlatform.Ios,
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/worker",
"version": "18.12.3",
"version": "18.13.0",
"private": true,
"scripts": {
"start": "yarn watch",
+2 -198
View File
@@ -1,75 +1,12 @@
import {
ArchiveSourceType,
BuildMode,
BuildTrigger,
Platform,
Workflow,
errors,
} from '@expo/eas-build-job';
import { errors } from '@expo/eas-build-job';
import spawn from '@expo/turtle-spawn';
import { vol } from 'memfs';
import path from 'path';
import { build } from '../build';
import { createBuildContext } from '../context';
import BuildService, { getExpoPackageVersionAsync } from '../service';
import { turtleFetch } from '../utils/turtleFetch';
import { getExpoPackageVersionAsync } from '../service';
jest.mock('fs');
jest.mock('@expo/turtle-spawn', () => jest.fn());
jest.mock('../build', () => ({
build: jest.fn(),
}));
jest.mock('../context', () => ({
createBuildContext: jest.fn(),
}));
jest.mock('../external/analytics', () => ({
Analytics: jest.fn().mockImplementation(() => ({
flushEventsAsync: jest.fn(),
logEvent: jest.fn(),
})),
}));
jest.mock('../logger', () => {
const logger = {
child: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
level: jest.fn(),
warn: jest.fn(),
};
logger.child.mockReturnValue(logger);
return {
__esModule: true,
default: logger,
createBuildLoggerWithSecretsFilter: jest.fn(async () => ({
cleanUp: jest.fn(),
logBuffer: { getLogs: jest.fn(() => []), getPhaseLogs: jest.fn(() => []) },
logger,
})),
};
});
jest.mock('../sentry', () => ({
__esModule: true,
default: {
capture: jest.fn(),
},
}));
jest.mock('../utils/turtleFetch', () => ({
turtleFetch: jest.fn(),
}));
jest.mock('../config', () => {
const config = jest.requireActual('../config').default;
return {
__esModule: true,
default: {
...config,
buildId: 'build-id',
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
},
};
});
describe(getExpoPackageVersionAsync, () => {
const projectRoot = '/test-project';
@@ -122,136 +59,3 @@ describe(getExpoPackageVersionAsync, () => {
await expect(getExpoPackageVersionAsync(buildContext)).rejects.toBeInstanceOf(errors.UserError);
});
});
describe(BuildService, () => {
const projectRoot = '/test-project';
const job = {
appId: 'app-id',
cache: {
clear: false,
disabled: false,
paths: [],
},
mode: BuildMode.BUILD,
platform: Platform.IOS,
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://example.com/project.tar.gz',
},
projectRootDirectory: '.',
secrets: {
robotAccessToken: 'robot-token',
},
triggeredBy: BuildTrigger.EAS_CLI,
type: Workflow.GENERIC,
};
beforeEach(() => {
vol.reset();
jest.clearAllMocks();
jest.mocked(spawn).mockResolvedValue({
stdout: Buffer.from(path.join(projectRoot, 'node_modules/expo/package.json')),
} as any);
jest.mocked(turtleFetch).mockResolvedValue({ ok: true } as any);
jest.mocked(build).mockRejectedValue(new Error('raw build failure'));
jest.mocked(createBuildContext).mockReturnValue({
env: { PATH: '/usr/bin' },
getReactNativeProjectDirectory: () => projectRoot,
job,
logger: {},
} as any);
});
it('sends expo_package_version to Datadog error logs without changing sdk_version', async () => {
vol.fromJSON({
[path.join(projectRoot, 'node_modules/expo/package.json')]: JSON.stringify({
version: '55.0.17',
}),
});
const service = new BuildService();
jest.spyOn(service, 'finishError').mockResolvedValue(undefined);
await (service as any).startBuildInternal({
initiatingUserId: 'user-id',
job,
metadata: {
buildProfile: 'production',
reactNativeVersion: '0.83.0',
sdkVersion: '55.0.0',
},
projectId: 'project-id',
});
expect(turtleFetch).toHaveBeenCalledWith(
'http://api.expo.test/v2/turtle-builds/logs',
'POST',
expect.objectContaining({
json: expect.objectContaining({
tags: expect.objectContaining({
expo_package_version: '55.0.17',
sdk_version: '55.0.0',
}),
}),
})
);
});
it('sends null expo_package_version to Datadog error logs when expo package version resolution fails', async () => {
jest.mocked(spawn).mockRejectedValue(new Error('Cannot find module expo/package.json'));
const service = new BuildService();
jest.spyOn(service, 'finishError').mockResolvedValue(undefined);
await (service as any).startBuildInternal({
initiatingUserId: 'user-id',
job,
metadata: {
sdkVersion: '55.0.0',
},
projectId: 'project-id',
});
expect(turtleFetch).toHaveBeenCalledWith(
'http://api.expo.test/v2/turtle-builds/logs',
'POST',
expect.objectContaining({
json: expect.objectContaining({
tags: expect.objectContaining({
expo_package_version: null,
sdk_version: '55.0.0',
}),
}),
})
);
});
it('does not try to resolve expo_package_version when build context setup fails', async () => {
jest.mocked(createBuildContext).mockImplementation(() => {
throw new Error('context setup failure');
});
const service = new BuildService();
jest.spyOn(service, 'finishError').mockResolvedValue(undefined);
await (service as any).startBuildInternal({
initiatingUserId: 'user-id',
job,
metadata: {
sdkVersion: '55.0.0',
},
projectId: 'project-id',
});
expect(spawn).not.toHaveBeenCalled();
expect(turtleFetch).toHaveBeenCalledWith(
'http://api.expo.test/v2/turtle-builds/logs',
'POST',
expect.objectContaining({
json: expect.objectContaining({
tags: expect.objectContaining({
expo_package_version: null,
sdk_version: '55.0.0',
}),
}),
})
);
});
});
-55
View File
@@ -34,7 +34,6 @@ import logger, { createBuildLoggerWithSecretsFilter } from './logger';
import sentry from './sentry';
import State from './state';
import { WebSocketServer } from './utils/WebSocketServer';
import { turtleFetch } from './utils/turtleFetch';
export const HANGING_WORKER_CHECK_TIMEOUT_MS = 5 * 60 * 1000;
@@ -334,60 +333,6 @@ export default class BuildService {
},
});
const robotAccessToken = job.secrets?.robotAccessToken;
if (robotAccessToken) {
const expoPackageVersionResult = this.buildContext
? await asyncResult(getExpoPackageVersionAsync(this.buildContext))
: null;
const expoPackageVersion =
expoPackageVersionResult?.ok === true ? expoPackageVersionResult.value : null;
let rawErrorMessage: string = '';
if (maybeRawError?.stderr) {
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stderr);
}
if (maybeRawError?.stdout) {
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stdout);
}
if (!rawErrorMessage) {
rawErrorMessage = maybeRawError?.message ?? err.message;
}
try {
await turtleFetch(
new URL('turtle-builds/logs', config.wwwApiV2BaseUrl).toString(),
'POST',
{
json: {
buildId: this.buildId,
message: rawErrorMessage,
level: 'error',
tags: {
build_phase: err.buildPhase ?? null,
error_code: err.errorCode,
platform: job.platform,
workflow: job.type,
sdk_version: metadata?.sdkVersion ?? null,
expo_package_version: expoPackageVersion,
react_native_version: metadata?.reactNativeVersion ?? null,
app_id: job.appId ?? null,
build_profile: metadata?.buildProfile ?? null,
app_name: metadata?.appName ?? null,
app_identifier: metadata?.appIdentifier ?? null,
distribution: metadata?.distribution ?? null,
cli_version: metadata?.cliVersion ?? null,
},
},
headers: {
Authorization: `Bearer ${robotAccessToken}`,
},
shouldThrowOnNotOk: false,
}
);
} catch (fetchError: any) {
logger.warn({ err: fetchError }, 'Failed to send build error log');
}
}
await this.finishError(err, maybeArtifacts);
}
}