Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active May 31, 2024 02:49
Show Gist options
  • Save nandorojo/8371475fe9912cb6b8d4f326664f1fc6 to your computer and use it in GitHub Desktop.
Save nandorojo/8371475fe9912cb6b8d4f326664f1fc6 to your computer and use it in GitHub Desktop.
EAS Update + Sentry Source Maps

EAS Update + Sentry Sourcemap upload

  1. Copy the TS file into your app. I put it in scripts/eas-update.ts
  2. Call the script with npx ts-node scripts/eas-update.ts <eas-script-here>
npx ts-node scripts/eas-update.ts npx eas-cli@latest update -p ios

You should provide the following env variables too:

  • EAS_UPDATE_MESSAGE
  • EAS_UPDATE_BRANCH
EAS_UPDATE_MESSAGE="works!" EAS_UPDATE_BRANCH=staging npx ts-node scripts/eas-update.ts npx eas-cli@latest update -p ios

All set. Be sure to call this script from the root of your Expo app.

For instance, you might add the following to your package.json's scripts:

{
  "scripts": {
    "eas-update": "npx ts-node scripts/eas-update.ts npx eas-cli@latest update -p ios"
  }
}

If you do that, just be sure to set EAS_UPDATE_MESSAGE and EAS_UPDATE_BRANCH when calling yarn eas-update.

Context

Meant to solve this issue: expo/sentry-expo#253

Tested with SDK 48 and it's working for me.

import { getConfig } from '@expo/config'
import fs from 'fs'
import spawnAsync from '@expo/spawn-async'
import chalk from 'chalk'
import path from 'path'
const appDir = process.cwd()
console.log()
console.log(chalk.green('Sentry source maps script. Working directory:'))
console.log(appDir)
console.log()
console.log(chalk.green('Importing Expo Config...'))
const config = getConfig(appDir, { skipSDKVersionRequirement: true })
if (!config) {
throw new Error(
'Failed to import Expo config. Are you in your app directory?'
)
}
console.log('Expo Config imported for', chalk.blue(config.exp.name))
const skipUpdate = 'SKIP_EAS_UPDATE' in process.env
const run = async () => {
// read in arguments from the CLI script
const [command, ..._args] = process.argv.slice(2)
const args = [
..._args,
'--message',
`${process.env.EAS_UPDATE_MESSAGE!}`,
'--branch',
`${process.env.EAS_UPDATE_BRANCH!}`,
]
try {
const updateProcess = spawnAsync(command, args, {
stdio: ['inherit', 'pipe', 'pipe'],
env: process.env,
cwd: process.cwd(),
})
const {
child: { stdout, stderr },
} = updateProcess
if (!(stdout && stderr)) {
throw new Error('Failed to spawn eas-cli')
}
let output: string[] = []
console.log()
console.log(
chalk.green('[eas-update-sentry] Running the follwing command:')
)
console.log(command, args.join(' '))
console.log()
stdout.on('data', (data: any) => {
const stringData: string = data.toString('utf8')
console.log(chalk.green('[eas-update-sentry]'), stringData)
output = stringData.split('\n').map((s) => s.trim())
})
await updateProcess
const findUpdateId = (output: string[], platform: 'android' | 'ios') => {
return output
.find((line) => line.toLowerCase().includes(`${platform} update id`))
?.split(' ')
.map((r) => r.trim())
.pop()
?.trim()
}
const iosUpdateId = findUpdateId(output, 'ios')
const androidUpdateId = findUpdateId(output, 'android')
const getBundles = () => {
const bundles = fs.readdirSync(path.resolve(appDir, 'dist/bundles'))
const iosBundle = bundles.find(
(s) => s.startsWith('ios-') && s.endsWith('.js')
)
const iosMap = bundles.find(
(s) => s.startsWith('ios-') && s.endsWith('.map')
)
const androidBundle = bundles.find(
(s) => s.startsWith('android-') && s.endsWith('.js')
)
const androidMap = bundles.find(
(s) => s.startsWith('android-') && s.endsWith('.map')
)
return { iosBundle, iosMap, androidBundle, androidMap }
}
const { iosBundle, iosMap, androidBundle, androidMap } = getBundles()
const uploadSourceMap = async ({
updateId,
buildNumber,
bundleIdentifier,
platform,
}: {
updateId: string
buildNumber: string | number
bundleIdentifier: string
platform: 'android' | 'ios'
}) => {
const sentryConfig = config.exp.hooks?.postPublish?.find((h) =>
h.file?.includes('upload-sourcemaps')
)?.config
const version = config.exp.version || config.exp.runtimeVersion
const result = spawnAsync(
'npx',
[
'@sentry/cli',
'releases',
'files',
`${bundleIdentifier}@${version}+${buildNumber}`,
'upload-sourcemaps',
'--dist',
updateId,
'--rewrite',
platform == 'ios'
? `dist/bundles/main.jsbundle`
: `dist/bundles/index.android.bundle`,
platform == 'ios'
? `dist/bundles/${iosMap}`
: `dist/bundles/${androidMap}`,
],
{
env: {
...process.env,
SENTRY_ORG: sentryConfig?.organization || process.env.SENTRY_ORG,
SENTRY_PROJECT: sentryConfig?.project || process.env.SENTRY_PROJECT,
SENTRY_AUTH_TOKEN:
sentryConfig?.authToken || process.env.SENTRY_AUTH_TOKEN,
SENTRY_URL:
sentryConfig?.url ||
process.env.SENTRY_URL ||
'https://sentry.io/',
},
}
)
result.child.stdout?.on('data', (data) => {
console.log(
chalk.green('[eas-update-sentry]'),
data
.toString('utf8')
.split('\n')
.map((l: string) => `[Upload ${platform} sourcemaps] ${l}`)
.join('\n')
)
})
await result
}
const uploadIosSourceMap = async () => {
if (iosUpdateId && iosBundle && iosMap) {
console.log()
console.log(
chalk.green('[eas-update-sentry] Updating iOS Bundle File...\n'),
console.log('[update-id]', iosUpdateId)
)
fs.renameSync(`dist/bundles/${iosBundle}`, 'dist/bundles/main.jsbundle')
const iOSConfig = {
bundleIdentifier: config.exp.ios?.bundleIdentifier,
buildNumber: config.exp.ios?.buildNumber,
}
if (Object.values(iOSConfig).every(Boolean)) {
await uploadSourceMap({
updateId: iosUpdateId,
buildNumber: iOSConfig.buildNumber!,
bundleIdentifier: iOSConfig.bundleIdentifier!,
platform: 'ios',
})
} else {
console.log(
chalk.yellow(
'[eas-update-sentry] Skipping iOS, missing the following values from your app.config file:',
Object.entries(iOSConfig)
.filter(([key, value]) => !value)
.map(([key]) => key)
.join(' ')
)
)
}
} else {
console.log(
chalk.yellow(
'[eas-update-sentry] Skipping iOS, missing the following values:',
Object.entries({ iosUpdateId, iosBundle, iosMap })
.filter(([key, value]) => !value)
.map(([key]) => key)
.join(' ')
)
)
}
}
const uploadAndroidSourceMap = async () => {
if (androidUpdateId && androidBundle && androidMap) {
console.log()
console.log(
chalk.green('[eas-update-sentry] Updating Android Bundle File...')
)
fs.renameSync(
`dist/bundles/${androidBundle}`,
'dist/bundles/index.android.bundle'
)
const androidConfig = {
package: config.exp.android?.package,
versionCode: config.exp.android?.versionCode,
}
if (Object.values(androidConfig).every(Boolean)) {
await uploadSourceMap({
updateId: androidUpdateId,
buildNumber: androidConfig.versionCode!,
bundleIdentifier: androidConfig.package!,
platform: 'android',
})
} else {
console.log(
chalk.yellow(
'[eas-update-sentry] Skipping Android, missing the following values from your app.config file:',
Object.entries(androidConfig)
.filter(([key, value]) => !value)
.map(([key]) => key)
.join(' ')
)
)
}
} else {
console.log(
chalk.yellow(
'[eas-update-sentry] Skipping Android, missing the following values:',
Object.entries({ androidUpdateId, androidBundle, androidMap })
.filter(([key, value]) => !value)
.map(([key]) => key)
.join(' ')
)
)
}
}
await Promise.all([uploadIosSourceMap(), uploadAndroidSourceMap()])
} catch (error: any) {
process.exit()
}
}
run().then((r) => {
console.log(chalk.yellow('Done!'))
})
@SohelIslamImran
Copy link

I tried this... But sentry not showing the Stack Trace correctly.. What's wrong? Does this work for you @nandorojo ?
image

image

@nandorojo
Copy link
Author

I think SDK 49 had breaking changes for the output of the dist folder and we need to change it accordingly. Expo needs to fix this themselves ASAP, it’s really a terrible experience

@nandorojo
Copy link
Author

Looks like Expo has renamed the files in the dist/bundles directory. I am updating the code here to reflect this. Please try it once I do and let me know if it's fixed.

Has anyone fixed this themselves btw?

@SohelIslamImran
Copy link

SohelIslamImran commented Oct 23, 2023

Now even with eas build with no OTA updates, the stack trace not showing.🙁

image

@nandorojo
Copy link
Author

Maybe try tweeting for expo to add support for this. I’ve tried my best

@wen-kai
Copy link

wen-kai commented Oct 24, 2023

eas update w/ sdk 49 works great with this script, thank you so much!
i updated getBundles() to look for hermes:

const getBundles = () => {
            const bundles = fs.readdirSync(path.resolve(appDir, 'dist/bundles'))

            let iosBundle = bundles.find(
                (s) => s.startsWith('ios-') && s.endsWith('.js')
            )
            if (!iosBundle) {
                iosBundle = bundles.find(
                    (s) => s.startsWith('ios-') && s.endsWith('.hbc')
                )
            }
            const iosMap = bundles.find(
                (s) => s.startsWith('ios-') && s.endsWith('.map')
            )

            let androidBundle = bundles.find(
                (s) => s.startsWith('android-') && s.endsWith('.js')
            )
            if (!androidBundle) {
                androidBundle = bundles.find(
                    (s) => s.startsWith('android-') && s.endsWith('.hbc')
                )
            }
            const androidMap = bundles.find(
                (s) => s.startsWith('android-') && s.endsWith('.map')
            )

            return { iosBundle, iosMap, androidBundle, androidMap }
        }

@mikevercoelen
Copy link

We've been trying this script, but we're still getting no source maps properly in Sentry with eas updates.

We're using sentry-expo v7.1.1 and Expo v49

@nandorojo Is this script still functional?

@nandorojo
Copy link
Author

I think so, but you need to change the files to .hbc

I’m also facing issues where the CLI output isn’t read properly so it isn’t sending to sentry. I’m also using auto increment build numbers so it’s not uploading properly.

All-in-all, it’s super annoying that sentry hasn’t shipped this yet. Please see my open issue on their repo.

@nandorojo
Copy link
Author

And please comment there asking for it!

@peter-jozsa
Copy link

thank you for creating this little script for us, I managed to make it work with SDK 49. I made the following changes to achieve this:

  • add .hbc file support to getBundles() (like others mentioned it earlier)
    const getBundles = () => {
      const bundles = fs.readdirSync(path.resolve(appDir, 'dist/bundles'))

      const iosBundle = bundles.find(
        (s) => s.startsWith('ios-') && (s.endsWith('.js') || s.endsWith('.hbc')) // <- this line changed
      )
      const iosMap = bundles.find(
        (s) => s.startsWith('ios-') && s.endsWith('.map')
      )

      const androidBundle = bundles.find(
        (s) =>
          s.startsWith('android-') && (s.endsWith('.js') || s.endsWith('.hbc')) // <- this line changed
      )
      const androidMap = bundles.find(
        (s) => s.startsWith('android-') && s.endsWith('.map')
      )

      return { iosBundle, iosMap, androidBundle, androidMap }
    }
  • fix a bug in eas update command output collection that caused "update ID" lookup issues
    stdout.on('data', (data: any) => {
      const stringData: string = data.toString('utf8')
      console.log(chalk.green('[eas-update-sentry]'), stringData)
      output.push(...stringData.split('\n').map((s) => s.trim())) // <- this line changed
    })
modified eas-update.ts
import { getConfig } from '@expo/config'
import fs from 'fs'
import spawnAsync from '@expo/spawn-async'
import chalk from 'chalk'
import path from 'path'

const appDir = process.cwd()

console.log()
console.log(chalk.green('Sentry source maps script. Working directory:'))
console.log(appDir)

console.log()

console.log(chalk.green('Importing Expo Config...'))
const config = getConfig(appDir, { skipSDKVersionRequirement: true })
if (!config) {
  throw new Error(
    'Failed to import Expo config. Are you in your app directory?'
  )
}
console.log('Expo Config imported for', chalk.blue(config.exp.name))

const skipUpdate = 'SKIP_EAS_UPDATE' in process.env

const run = async () => {
  //   read in arguments from the CLI script
  const [command, ..._args] = process.argv.slice(2)

  const args = [
    ..._args,
    '--message',
    `${process.env.EAS_UPDATE_MESSAGE!}`,
    '--branch',
    `${process.env.EAS_UPDATE_BRANCH!}`,
  ]

  try {
    const updateProcess = spawnAsync(command, args, {
      stdio: ['inherit', 'pipe', 'pipe'],
      env: process.env,
      cwd: process.cwd(),
    })
    const {
      child: { stdout, stderr },
    } = updateProcess

    if (!(stdout && stderr)) {
      throw new Error('Failed to spawn eas-cli')
    }

    const output: string[] = []
    console.log()
    console.log(
      chalk.green('[eas-update-sentry] Running the follwing command:')
    )
    console.log(command, args.join(' '))
    console.log()
    stdout.on('data', (data: any) => {
      const stringData: string = data.toString('utf8')
      console.log(chalk.green('[eas-update-sentry]'), stringData)
      output.push(...stringData.split('\n').map((s) => s.trim()))
    })
    await updateProcess

    const findUpdateId = (output: string[], platform: 'android' | 'ios') => {
      return output
        .find((line) => line.toLowerCase().includes(`${platform} update id`))
        ?.split(' ')
        .map((r) => r.trim())
        .pop()
        ?.trim()
    }

    const iosUpdateId = findUpdateId(output, 'ios')
    const androidUpdateId = findUpdateId(output, 'android')

    const getBundles = () => {
      const bundles = fs.readdirSync(path.resolve(appDir, 'dist/bundles'))

      const iosBundle = bundles.find(
        (s) => s.startsWith('ios-') && (s.endsWith('.js') || s.endsWith('.hbc'))
      )
      const iosMap = bundles.find(
        (s) => s.startsWith('ios-') && s.endsWith('.map')
      )

      const androidBundle = bundles.find(
        (s) =>
          s.startsWith('android-') && (s.endsWith('.js') || s.endsWith('.hbc'))
      )
      const androidMap = bundles.find(
        (s) => s.startsWith('android-') && s.endsWith('.map')
      )

      return { iosBundle, iosMap, androidBundle, androidMap }
    }
    const { iosBundle, iosMap, androidBundle, androidMap } = getBundles()

    const uploadSourceMap = async ({
      updateId,
      buildNumber,
      bundleIdentifier,
      platform,
    }: {
      updateId: string

      buildNumber: string | number
      bundleIdentifier: string
      platform: 'android' | 'ios'
    }) => {
      const sentryConfig = config.exp.hooks?.postPublish?.find((h) =>
        h.file?.includes('upload-sourcemaps')
      )?.config

      const version = config.exp.version || config.exp.runtimeVersion

      const result = spawnAsync(
        'npx',
        [
          '@sentry/cli',
          'releases',
          'files',
          `${bundleIdentifier}@${version}+${buildNumber}`,
          'upload-sourcemaps',
          '--dist',
          updateId,
          '--rewrite',
          platform == 'ios'
            ? `dist/bundles/main.jsbundle`
            : `dist/bundles/index.android.bundle`,
          platform == 'ios'
            ? `dist/bundles/${iosMap}`
            : `dist/bundles/${androidMap}`,
        ],
        {
          env: {
            ...process.env,
            SENTRY_ORG: sentryConfig?.organization || process.env.SENTRY_ORG,
            SENTRY_PROJECT: sentryConfig?.project || process.env.SENTRY_PROJECT,
            SENTRY_AUTH_TOKEN:
              sentryConfig?.authToken || process.env.SENTRY_AUTH_TOKEN,
            SENTRY_URL:
              sentryConfig?.url ||
              process.env.SENTRY_URL ||
              'https://sentry.io/',
          },
        }
      )

      result.child.stdout?.on('data', (data) => {
        console.log(
          chalk.green('[eas-update-sentry]'),
          data
            .toString('utf8')
            .split('\n')
            .map((l: string) => `[Upload ${platform} sourcemaps] ${l}`)
            .join('\n')
        )
      })

      await result
    }

    const uploadIosSourceMap = async () => {
      if (iosUpdateId && iosBundle && iosMap) {
        console.log()
        console.log(
          chalk.green('[eas-update-sentry] Updating iOS Bundle File...\n'),
          console.log('[update-id]', iosUpdateId)
        )

        fs.renameSync(`dist/bundles/${iosBundle}`, 'dist/bundles/main.jsbundle')
        const iOSConfig = {
          bundleIdentifier: config.exp.ios?.bundleIdentifier,
          buildNumber: config.exp.ios?.buildNumber,
        }
        if (Object.values(iOSConfig).every(Boolean)) {
          await uploadSourceMap({
            updateId: iosUpdateId,
            buildNumber: iOSConfig.buildNumber!,
            bundleIdentifier: iOSConfig.bundleIdentifier!,
            platform: 'ios',
          })
        } else {
          console.log(
            chalk.yellow(
              '[eas-update-sentry] Skipping iOS, missing the following values from your app.config file:',
              Object.entries(iOSConfig)
                .filter(([key, value]) => !value)
                .map(([key]) => key)
                .join(' ')
            )
          )
        }
      } else {
        console.log(
          chalk.yellow(
            '[eas-update-sentry] Skipping iOS, missing the following values:',
            Object.entries({ iosUpdateId, iosBundle, iosMap })
              .filter(([key, value]) => !value)
              .map(([key]) => key)
              .join(' ')
          )
        )
      }
    }

    const uploadAndroidSourceMap = async () => {
      if (androidUpdateId && androidBundle && androidMap) {
        console.log()
        console.log(
          chalk.green('[eas-update-sentry] Updating Android Bundle File...')
        )

        fs.renameSync(
          `dist/bundles/${androidBundle}`,
          'dist/bundles/index.android.bundle'
        )
        const androidConfig = {
          package: config.exp.android?.package,
          versionCode: config.exp.android?.versionCode,
        }
        if (Object.values(androidConfig).every(Boolean)) {
          await uploadSourceMap({
            updateId: androidUpdateId,
            buildNumber: androidConfig.versionCode!,
            bundleIdentifier: androidConfig.package!,
            platform: 'android',
          })
        } else {
          console.log(
            chalk.yellow(
              '[eas-update-sentry] Skipping Android, missing the following values from your app.config file:',
              Object.entries(androidConfig)
                .filter(([key, value]) => !value)
                .map(([key]) => key)
                .join(' ')
            )
          )
        }
      } else {
        console.log(
          chalk.yellow(
            '[eas-update-sentry] Skipping Android, missing the following values:',
            Object.entries({ androidUpdateId, androidBundle, androidMap })
              .filter(([key, value]) => !value)
              .map(([key]) => key)
              .join(' ')
          )
        )
      }
    }

    await Promise.all([uploadIosSourceMap(), uploadAndroidSourceMap()])
  } catch (error: any) {
    process.exit()
  }
}

run().then((r) => {
  console.log(chalk.yellow('Done!'))
})

@mikevercoelen
Copy link

@peter-jozsa Thanks! How do you initialize Sentry? Are you using sentry-expo or are you using sentry/react-native directly? What are the dist and release params? Do you omit them?

@peter-jozsa
Copy link

Well we use sentry-expo and omit dist and release params. Now i've started reading about these params I feel like our solution is not complete yet...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment