diff --git a/.gitignore b/.gitignore index 50fb3439f9..4033aaa3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Distribution dist/ bin/ +upload/* +!upload/package.json # NodeJS node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4221fa8a26..1035a998d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased](https://github.com/Instabug/Instabug-React-Native/compare/v13.2.0...dev) +### Added + +- Export `uploadSourcemaps` and `uploadSoFiles` utilities in the `instabug-reactnative/upload` sub-package for usage in custom Node.js upload scripts ([#1252](https://github.com/Instabug/Instabug-React-Native/pull/1252)). + ### Fixed - Fix APM network logging on Android ([#1253](https://github.com/Instabug/Instabug-React-Native/pull/1253)). diff --git a/cli/UploadSoFiles.ts b/cli/UploadSoFiles.ts deleted file mode 100755 index 6329f4d27f..0000000000 --- a/cli/UploadSoFiles.ts +++ /dev/null @@ -1,88 +0,0 @@ -import axios from 'axios'; -import { Command, Option } from 'commander'; -import FormData from 'form-data'; -import fs from 'fs'; - -interface UploadSoFilesOptions { - arch: 'x86' | 'x86_64' | 'arm64-v8a' | 'armeabi-v7a'; - file: string; - token: string; - name: string; - api_key: string; -} -/** - * This script uploads .so files to the specified endpoint used in NDK crash reporting. - * Usage: node upload-so-files.js --arch --file --api_key --token --name - */ - -export const UploadSoFilesCommand = new Command(); - -UploadSoFilesCommand.name('upload-so-files') - .addOption( - new Option('-arch, --arch ', 'arch') - .choices(['x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a']) - .makeOptionMandatory(), - ) - .addOption( - new Option( - '-f, --file ', - 'The path of the symbol files in Zip format', - ).makeOptionMandatory(), - ) - .addOption(new Option('--api_key ', 'Your App key').makeOptionMandatory()) - .addOption( - new Option('-t, --token ', 'Your App Token') - .env('INSTABUG_APP_TOKEN') - .makeOptionMandatory(), - ) - .addOption( - new Option('-n, --name ', 'The app version name') - .env('INSTABUG_APP_VERSION_NAME') - .makeOptionMandatory(), - ) - .action(function (this: Command) { - const options = this.opts(); - UploadSoFiles(options); - }) - .showHelpAfterError(); - -const UploadSoFiles = async (opts: UploadSoFilesOptions) => { - const fileName = opts.file; - if (fileName == null) { - console.error('Failed to upload So Files: invalid file path'); - process.exit(1); - } - - if (fs.existsSync(fileName) === false) { - console.error('Failed to upload So Files: File not found'); - process.exit(1); - } - var fileExt = fileName.split('.').pop(); - - if (fileExt !== 'zip') { - console.error('Failed to upload So Files: You can only upload ZIP files'); - process.exit(1); - } - - const fileBlob = fs.readFileSync(opts.file); - - const form = new FormData(); - form.append('app_version', opts.name); - form.append('so_file', fileBlob, fileName); - form.append('application_token', opts.token); - form.append('api_key', opts.api_key); - form.append('arch', opts.arch); - - console.log('Uploading So files...'); - const uploadEndpoint = 'https://api.instabug.com/api/web/public/so_files'; - try { - await axios.post(uploadEndpoint, form, { - headers: form.getHeaders(), - }); - - console.log(`Successfully uploaded So Files for version: ${opts.name} with arch ${opts.arch}`); - } catch (err) { - console.error('Failed to upload So Files:', axios.isAxiosError(err) ? err.response?.data : err); - process.exit(1); - } -}; diff --git a/cli/UploadSourcemaps.ts b/cli/UploadSourcemaps.ts deleted file mode 100755 index 939629cb5f..0000000000 --- a/cli/UploadSourcemaps.ts +++ /dev/null @@ -1,89 +0,0 @@ -import axios from 'axios'; -import { Command, Option } from 'commander'; -import FormData from 'form-data'; -import fs from 'fs'; - -interface UploadSourcemapsOptions { - platform: 'android' | 'ios'; - file: string; - token: string; - name: string; - code: string; - label?: string; -} - -export const uploadSourcemapsCommand = new Command(); - -uploadSourcemapsCommand - .name('upload-sourcemaps') - .addOption( - new Option('-p, --platform ', 'Platform') - .choices(['ios', 'android']) - .makeOptionMandatory(), - ) - .addOption( - new Option('-f, --file ', 'The path of the source map file').makeOptionMandatory(), - ) - .addOption( - new Option('-t, --token ', 'Your App Token') - .env('INSTABUG_APP_TOKEN') - .makeOptionMandatory(), - ) - .addOption( - new Option('-n, --name ', 'The app version name') - .env('INSTABUG_APP_VERSION_NAME') - .makeOptionMandatory(), - ) - .addOption( - new Option('-c, --code ', 'The app version code') - .env('INSTABUG_APP_VERSION_CODE') - .makeOptionMandatory(), - ) - .addOption( - new Option('-l, --label ', "The CodePush label if it's a CodePush release").env( - 'INSTABUG_APP_VERSION_LABEL', - ), - ) - .action(function (this: Command) { - const options = this.opts(); - uploadSourcemaps(options); - }) - .showHelpAfterError(); - -const uploadSourcemaps = async (opts: UploadSourcemapsOptions) => { - const fileName = `${opts.platform}-sourcemap.json`; - const fileBlob = fs.readFileSync(opts.file); - - const version = { - code: opts.code, - name: opts.name, - codepush: opts.label, - }; - - const form = new FormData(); - form.append('app_version', JSON.stringify(version)); - form.append('symbols_file', fileBlob, fileName); - form.append('application_token', opts.token); - form.append('platform', 'react_native'); - form.append('os', opts.platform); - - console.log('Uploading Source map file...'); - - try { - const response = await axios.post('https://api.instabug.com/api/sdk/v3/symbols_files', form, { - headers: form.getHeaders(), - }); - - const appVersion = version.codepush - ? `${version.name} (${version.code})+codepush:${version.codepush}` - : `${version.name} (${version.code})`; - - console.log(`Successfully uploaded Source maps for version: ${appVersion}`); - console.log(response.data); - } catch (err) { - console.error( - 'Failed to upload source maps:', - axios.isAxiosError(err) ? err.response?.data : err, - ); - } -}; diff --git a/cli/commands/UploadSoFiles.ts b/cli/commands/UploadSoFiles.ts new file mode 100755 index 0000000000..99b23e3e4c --- /dev/null +++ b/cli/commands/UploadSoFiles.ts @@ -0,0 +1,38 @@ +import { Command, Option } from 'commander'; +import { uploadSoFiles, UploadSoFilesOptions } from '../upload/uploadSoFiles'; + +/** + * This script uploads .so files to the specified endpoint used in NDK crash reporting. + * Usage: node upload-so-files.js --arch --file --api_key --token --name + */ + +export const UploadSoFilesCommand = new Command(); + +UploadSoFilesCommand.name('upload-so-files') + .addOption( + new Option('-arch, --arch ', 'arch') + .choices(['x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a']) + .makeOptionMandatory(), + ) + .addOption( + new Option( + '-f, --file ', + 'The path of the symbol files in Zip format', + ).makeOptionMandatory(), + ) + .addOption(new Option('--api_key ', 'Your App key').makeOptionMandatory()) + .addOption( + new Option('-t, --token ', 'Your App Token') + .env('INSTABUG_APP_TOKEN') + .makeOptionMandatory(), + ) + .addOption( + new Option('-n, --name ', 'The app version name') + .env('INSTABUG_APP_VERSION_NAME') + .makeOptionMandatory(), + ) + .action(function (this: Command) { + const options = this.opts(); + uploadSoFiles(options); + }) + .showHelpAfterError(); diff --git a/cli/commands/UploadSourcemaps.ts b/cli/commands/UploadSourcemaps.ts new file mode 100755 index 0000000000..5199693e77 --- /dev/null +++ b/cli/commands/UploadSourcemaps.ts @@ -0,0 +1,40 @@ +import { Command, Option } from 'commander'; +import { uploadSourcemaps, UploadSourcemapsOptions } from '../upload/uploadSourcemaps'; + +export const uploadSourcemapsCommand = new Command(); + +uploadSourcemapsCommand + .name('upload-sourcemaps') + .addOption( + new Option('-p, --platform ', 'Platform') + .choices(['ios', 'android']) + .makeOptionMandatory(), + ) + .addOption( + new Option('-f, --file ', 'The path of the source map file').makeOptionMandatory(), + ) + .addOption( + new Option('-t, --token ', 'Your App Token') + .env('INSTABUG_APP_TOKEN') + .makeOptionMandatory(), + ) + .addOption( + new Option('-n, --name ', 'The app version name') + .env('INSTABUG_APP_VERSION_NAME') + .makeOptionMandatory(), + ) + .addOption( + new Option('-c, --code ', 'The app version code') + .env('INSTABUG_APP_VERSION_CODE') + .makeOptionMandatory(), + ) + .addOption( + new Option('-l, --label ', "The CodePush label if it's a CodePush release").env( + 'INSTABUG_APP_VERSION_LABEL', + ), + ) + .action(function (this: Command) { + const options = this.opts(); + uploadSourcemaps(options); + }) + .showHelpAfterError(); diff --git a/cli/index.ts b/cli/index.ts index c1731d67a5..8df747e75e 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import { Command } from 'commander'; -import { uploadSourcemapsCommand } from './UploadSourcemaps'; -import { UploadSoFilesCommand } from './UploadSoFiles'; +import { uploadSourcemapsCommand } from './commands/UploadSourcemaps'; +import { UploadSoFilesCommand } from './commands/UploadSoFiles'; const program = new Command(); diff --git a/cli/upload/index.ts b/cli/upload/index.ts new file mode 100644 index 0000000000..b09f4b243b --- /dev/null +++ b/cli/upload/index.ts @@ -0,0 +1,2 @@ +export * from './uploadSourcemaps'; +export * from './uploadSoFiles'; diff --git a/cli/upload/uploadSoFiles.ts b/cli/upload/uploadSoFiles.ts new file mode 100755 index 0000000000..fb25f60632 --- /dev/null +++ b/cli/upload/uploadSoFiles.ts @@ -0,0 +1,112 @@ +import axios from 'axios'; +import FormData from 'form-data'; +import fs from 'fs'; + +export interface UploadSoFilesOptions { + arch: 'x86' | 'x86_64' | 'arm64-v8a' | 'armeabi-v7a'; + file: string; + token: string; + name: string; + api_key: string; + + /** + * Disables logging to the console and prevents process exit on error. + * + * @default false + * */ + silent?: boolean; +} + +/** + * Uploads NDK `.so` files to Instabug. + * + * @param opts Options for the `.so` files upload process. + * @returns A promise that resolves to a boolean indicating whether the upload was successful. + */ +export const uploadSoFiles = async (opts: UploadSoFilesOptions): Promise => { + const fileName = opts.file; + + const validFilePath = assert( + fileName != null, + 'Failed to upload So Files: invalid file path', + opts.silent, + ); + + if (!validFilePath) { + return false; + } + + const fileExists = assert( + fs.existsSync(fileName), + 'Failed to upload So Files: File not found', + opts.silent, + ); + + if (!fileExists) { + return false; + } + + const fileExt = fileName.split('.').pop(); + + const isZipFile = assert( + fileExt === 'zip', + 'Failed to upload So Files: You can only upload ZIP files', + opts.silent, + ); + + if (!isZipFile) { + return false; + } + + const fileBlob = fs.readFileSync(opts.file); + + const form = new FormData(); + form.append('app_version', opts.name); + form.append('so_file', fileBlob, fileName); + form.append('application_token', opts.token); + form.append('api_key', opts.api_key); + form.append('arch', opts.arch); + + if (!opts.silent) { + console.log('Uploading So files...'); + } + + const uploadEndpoint = 'https://api.instabug.com/api/web/public/so_files'; + try { + await axios.post(uploadEndpoint, form, { + headers: form.getHeaders(), + }); + + if (!opts.silent) { + console.log( + `Successfully uploaded So Files for version: ${opts.name} with arch ${opts.arch}`, + ); + } + + return true; + } catch (err) { + if (!opts.silent) { + console.error( + 'Failed to upload So Files:', + axios.isAxiosError(err) ? err.response?.data : err, + ); + + process.exit(1); + } + + return false; + } +}; + +export const assert = (condition: unknown, message: string, silent?: boolean): boolean => { + if (silent) { + return Boolean(condition); + } + + if (!condition) { + console.error(message); + process.exit(1); + } + + return true; +}; diff --git a/cli/upload/uploadSourcemaps.ts b/cli/upload/uploadSourcemaps.ts new file mode 100644 index 0000000000..701ce9ead8 --- /dev/null +++ b/cli/upload/uploadSourcemaps.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; +import FormData from 'form-data'; +import fs from 'fs'; + +export interface UploadSourcemapsOptions { + platform: 'android' | 'ios'; + file: string; + token: string; + name: string; + code: string; + label?: string; + + /** + * Disables logging to the console and prevents process exit on error. + * + * @default false + * */ + silent?: boolean; +} + +/** + * Uploads JavaScript sourcemaps to Instabug. + * + * @param opts Options for the sourcemaps upload process. + * @returns A promise that resolves to a boolean indicating whether the upload was successful. + */ +export const uploadSourcemaps = async (opts: UploadSourcemapsOptions): Promise => { + const fileName = `${opts.platform}-sourcemap.json`; + const fileBlob = fs.readFileSync(opts.file); + + const version = { + code: opts.code, + name: opts.name, + codepush: opts.label, + }; + + const form = new FormData(); + form.append('app_version', JSON.stringify(version)); + form.append('symbols_file', fileBlob, fileName); + form.append('application_token', opts.token); + form.append('platform', 'react_native'); + form.append('os', opts.platform); + + if (!opts.silent) { + console.log('Uploading Source map file...'); + } + + try { + const response = await axios.post('https://api.instabug.com/api/sdk/v3/symbols_files', form, { + headers: form.getHeaders(), + }); + + const appVersion = version.codepush + ? `${version.name} (${version.code})+codepush:${version.codepush}` + : `${version.name} (${version.code})`; + + if (!opts.silent) { + console.log(`Successfully uploaded Source maps for version: ${appVersion}`); + console.log(response.data); + } + + return true; + } catch (err) { + if (!opts.silent) { + console.error( + 'Failed to upload source maps:', + axios.isAxiosError(err) ? err.response?.data : err, + ); + } + + return false; + } +}; diff --git a/rollup.config.js b/rollup.config.js index 9b3068d89e..ae91a89060 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,19 +5,30 @@ import typescript from '@rollup/plugin-typescript'; import cleanup from 'rollup-plugin-cleanup'; import shebang from 'rollup-plugin-preserve-shebang'; +const commonPlugins = [ + shebang(), + json(), + nodeResolve({ preferBuiltins: true }), + commonjs(), + cleanup(), +]; + /** @type import('rollup').RollupOptions */ -export default { - input: ['cli/index.ts'], - output: { - dir: 'bin', - format: 'cjs', +export default [ + { + input: ['cli/index.ts'], + output: { + dir: 'bin', + format: 'cjs', + }, + plugins: [...commonPlugins, typescript({ tsconfig: './tsconfig.cli.json' })], + }, + { + input: ['cli/upload/index.ts'], + output: { + dir: 'upload', + format: 'cjs', + }, + plugins: [...commonPlugins, typescript({ tsconfig: './tsconfig.upload.json' })], }, - plugins: [ - typescript({ tsconfig: './tsconfig.cli.json' }), - shebang(), - json(), - nodeResolve({ preferBuiltins: true }), - commonjs(), - cleanup(), - ], -}; +]; diff --git a/tsconfig.upload.json b/tsconfig.upload.json new file mode 100644 index 0000000000..5ade7ea9a7 --- /dev/null +++ b/tsconfig.upload.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["cli/upload/**/*"], + "exclude": [], + "compilerOptions": { + "lib": ["dom"], + "outDir": "upload", + "module": "esnext" + } +} diff --git a/upload/package.json b/upload/package.json new file mode 100644 index 0000000000..778c7ebf51 --- /dev/null +++ b/upload/package.json @@ -0,0 +1,5 @@ +{ + "main": "index.js", + "module": "index.mjs", + "types": "index.d.ts" +}