Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
test/fixtures
25 changes: 25 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 8
},
"rules": {
"camelcase": ["error", {"properties": "never"}],
"quotes": ["error", "single", "avoid-escape"],
"no-underscore-dangle": "off",
"no-useless-escape": "off",
"complexity": ["error", { "max": 35 }],
"no-use-before-define": ["off", { "functions": false }],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-prototype-builtins": "off",
"no-var": "error",
"prefer-const": ["error", { "destructuring": "all" }]
},
"globals": {
"output": true
}
}
28 changes: 28 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node.js CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [10.x, 12.x, 14.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
package-lock.json
coverage
.nyc_output
.DS_Store
*.swp
11 changes: 11 additions & 0 deletions .nycrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
check-coverage: true
lines: 90
branches: 80
functions: 80
statements: 90
watermarks:
lines: [90, 95]
functions: [70, 90]
branches: [70, 90]
statements: [90, 95]
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# rollbar-cli
# rollbar-cli

The Rollbar CLI provides easy command line access to Rollbar's API features,
starting with source map uploads.

## Usage and Reference
Currently the upload-sourcemaps command is supported.

### upload-sourcemaps
Upload source maps recursively from a directory.

```
rollbar-cli upload-sourcemaps <path> [options]

upload sourcemaps

Options:
--version Show version number [boolean]
-v, --verbose Verbose status output [boolean]
-q, --quiet Silent status output [boolean]
--help Show help [boolean]
--access-token Access token for the Rollbar API [string] [required]
--url-prefix Base part of the stack trace URLs [string] [required]
--code-version Code version string must match value in the Rollbar item
[string] [required]
-D, --dry-run Scan and validate source maps without uploading [boolean]
```

Example:
```
rollbar-cli upload-sourcemaps ./dist -access-token 638d... --url-prefix 'http://example.com/' --code-version 123.456
```


## Release History & Changelog

See our [Releases](https://github.com/rollbar/rollbar-cli/releases) page for a list of all releases, including changes.

## Help / Support

If you run into any issues, please email us at [support@rollbar.com](mailto:support@rollbar.com).

For bug reports, please [open an issue on GitHub](https://github.com/rollbar/rollbar-cli/issues/new).

## Developing

To set up a development environment, you'll need Node.js and npm.

1. Install dependencies
`npm install`

2. Link the rollbar-cli command to the local repo
`npm link`

3. Run the tests
`npm test`

## Contributing

1. [Fork it](https://github.com/rollbar/rollbar-cli).
2. Create your feature branch (`git checkout -b my-new-feature`).
3. Commit your changes (`git commit -am 'Added some feature'`).
4. Push to the branch (`git push origin my-new-feature`).
5. Create a new Pull Request.
5 changes: 5 additions & 0 deletions bin/rollbar
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

'use strict';

(require('../src/index'))();
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "rollbar-cli",
"version": "0.1.0",
"license": "MIT",
"bin": {
"rollbar-cli": "bin/rollbar"
},
"scripts": {
"lint": "./node_modules/.bin/eslint . --ext .js",
"test": "nyc --reporter=html --reporter=text mocha './test/{,!(fixtures)/**/}*test.js'"
},
"dependencies": {
"axios": "^0.19.2",
"chalk": "^4.1.0",
"form-data": "^3.0.0",
"glob": "^7.1.6",
"source-map": "^0.7.3",
"yargs": "^15.4.1"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^6.8.0",
"mocha": "^7.2.0",
"nyc": "^15.1.0",
"sinon": "^9.0.3"
}
}
59 changes: 59 additions & 0 deletions src/common/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

const util = require('util');
const chalk = require('chalk');

class Output {
constructor(options = {}) {
this.all = !!options.verbose;
this.enable = !options.quiet;
this.labelSize = options.labelSize || 6;
}

status() {
this.write(chalk.white(this.format(...arguments)));
}

verbose() {
if (this.all) {
this.write(chalk.grey(this.format(...arguments)));
}
}

warn() {
this.write(chalk.yellow(this.format(...arguments)));
}

error() {
this.write(chalk.red(this.format(...arguments)));
}

fail() {
this.write(chalk.red(this.format(...arguments)));
}

success() {
this.write(chalk.green(this.format(...arguments)));
}

format() {
const args = Array.from(arguments);
let paddedLabel = args[0].padEnd(this.labelSize, ' ');

if (args[0].trim().length) {
paddedLabel = `[${paddedLabel}] `;
} else {
paddedLabel = ` ${paddedLabel} `;
}

return paddedLabel + util.format(...args.slice(1)) + '\n';
}

write(str) {
if (this.enable) {
process.stdout.write(str);
}
}
}

module.exports = Output;
63 changes: 63 additions & 0 deletions src/common/rollbar-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const axios = require('axios');
const FormData = require('form-data');

class RollbarAPI {
constructor(accessToken) {
this.accessToken = accessToken;

this.axios = axios.create({
baseURL: 'https://api.rollbar.com/api/1/',
headers: { 'X-Rollbar-Access-Token': accessToken },
// Always resolve, regardless of status code.
// When we let axios reject, we end up with less specific error messages.
validateStatus: function (_status) { return true; },
});
}

async sourcemaps(request) {
output.verbose('', 'minified_url: ' + request.minified_url);

const form = this.convertRequestToForm(request);
const resp = await this.axios.post(
'/sourcemap',
form.getBuffer(), // use buffer to prevent unwanted string escaping.
{ headers: {
// axios needs some help with headers for form data.
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`,
'Content-Length': form.getLengthSync()
}}
);

return this.processResponse(resp);
}

convertRequestToForm(request) {
const form = new FormData();

form.append('version', request.version);
form.append('minified_url', request.minified_url);
form.append('source_map', Buffer.from(request.source_map), { filename: 'source_map' });

if (request.sources) {
for (const filename of Object.keys(request.sources)) {
if (filename && request.sources[filename]) {
form.append(filename, Buffer.from(request.sources[filename]), { filename: filename });
}
}
}
return form;
}

processResponse(resp) {
output.verbose('', 'response:', resp.data, resp.status, resp.statusText);
if (resp.status === 200) {
return null;
}

return resp.data;
}
}

module.exports = RollbarAPI;
28 changes: 28 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

const yargs = require('yargs');

function cli() {
yargs
.option('v', {
alias: 'verbose',
describe: 'Verbose status output',
requiresArg: false,
type: 'boolean',
demandOption: false
})
.option('q', {
alias: 'quiet',
describe: 'Silent status output',
requiresArg: false,
type: 'boolean',
demandOption: false
})
.command(require('./sourcemaps/command'))
.help()
.argv

return 0;
}

module.exports = cli;
62 changes: 62 additions & 0 deletions src/sourcemaps/command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

const Scanner = require('./scanner');
const Uploader = require('./uploader');
const Output = require('../common/output.js');

exports.command = 'upload-sourcemaps <path> [options]'

exports.describe = 'upload sourcemaps'

exports.builder = function (yargs) {
return yargs
.option('access-token', {
describe: 'Access token for the Rollbar API',
requiresArg: true,
type: 'string',
demandOption: true
})
.option('url-prefix', {
describe: 'Base part of the stack trace URLs',
requiresArg: true,
type: 'string',
demandOption: true
})
.option('code-version', {
describe: 'Code version string must match value in the Rollbar item',
requiresArg: true,
type: 'string',
demandOption: true
})
.option('D', {
alias: 'dry-run',
describe: 'Scan and validate source maps without uploading',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the option of dry run.

requiresArg: false,
type: 'boolean',
demandOption: false
})
}

exports.handler = async function (argv) {
global.output = new Output({
verbose: argv['verbose'],
quiet: argv['quiet']
});

const scanner = new Scanner({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this scanner adapt to dsym or other symbol files in future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assume not since the source-map npm package is the standard package for Javascript source maps.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. So looks like we will have to implement separate scanner for scanning dsym files.

targetPath: argv['path'],
sources: argv['sources']
});

await scanner.scan();

const uploader = new Uploader({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this uploader adapt to dsym or other symbol files in future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it depends on how different Rollbar's API is for dsym, and how different the data structures are. It's not something I've evaluated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I was just asking so that in near future if we end up adding dsym or proguard symbol upload functionality we shouldn't have to do majore rewrite of the CLI.

accessToken: argv['access-token'],
baseUrl: argv['url-prefix'],
codeVersion: argv['code-version']
})

uploader.mapFiles(scanner.files);

await uploader.upload(argv['dry-run']);
}
Loading