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!'))
})
@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