Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active January 11, 2024 02:03
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!'))
})
@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