├── .editorconfig ├── .gitignore ├── .travis.yml ├── cli.js ├── index.js ├── lib ├── create-issue.js ├── find-issues.js ├── generate-issue-body.js └── normalize-error.js ├── media └── logo.svg ├── package.json ├── readme.md └── test ├── app.js ├── create-issue.js ├── find-issues.js ├── generate-issue-body.js └── helpers ├── mock-create-issue.js └── mock-find-issues.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '7' 4 | - '6' 5 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {spawn} = require('child_process'); 4 | const updateNotifier = require('update-notifier'); 5 | const pkg = require('./package.json'); 6 | 7 | updateNotifier({pkg}).notify(); 8 | 9 | const args = process.argv.slice(2); 10 | spawn('npm', ['start', '--'].concat(args), { 11 | cwd: __dirname, 12 | stdio: 'inherit' 13 | }); 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {json} = require('micro'); 4 | const generateIssueBody = require('./lib/generate-issue-body'); 5 | const normalizeError = require('./lib/normalize-error'); 6 | const findIssues = require('./lib/find-issues'); 7 | const createIssue = require('./lib/create-issue'); 8 | 9 | const token = process.env.GITHUB_TOKEN; 10 | const user = process.env.GITHUB_USER; 11 | const repo = process.env.GITHUB_REPO; 12 | 13 | if (!token) { 14 | console.log('GitHub token is required. Set `GITHUB_TOKEN` environment variable.'); 15 | process.exit(1); // eslint-disable-line unicorn/no-process-exit 16 | } 17 | 18 | if (!user) { 19 | console.log('GitHub user name is required. Set `GITHUB_USER` environment variable.'); 20 | process.exit(1); // eslint-disable-line unicorn/no-process-exit 21 | } 22 | 23 | if (!repo) { 24 | console.log('GitHub repository name is required. Set `GITHUB_REPO` environment variable.'); 25 | process.exit(1); // eslint-disable-line unicorn/no-process-exit 26 | } 27 | 28 | module.exports = async req => { 29 | const data = await json(req); 30 | const err = normalizeError(data); 31 | 32 | const title = `${err.name}: ${err.message}`; 33 | const body = generateIssueBody(err); 34 | 35 | const issues = await findIssues({token, user, repo}); 36 | const isDuplicate = issues.some(issue => { 37 | return issue.title === title && issue.state === 'open'; 38 | }); 39 | 40 | if (!isDuplicate) { 41 | const labels = err.props.labels; 42 | await createIssue({token, user, repo, title, labels, body}); 43 | } 44 | 45 | return null; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/create-issue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const got = require('got'); 4 | 5 | module.exports = ({token, user, repo, title, body, labels}) => { 6 | return got(`https://api.github.com/repos/${user}/${repo}/issues`, { 7 | method: 'post', 8 | headers: { 9 | authorization: `token ${token}`, 10 | 'content-type': 'application/json' 11 | }, 12 | body: JSON.stringify({title, body, labels}) 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/find-issues.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parseLinkHeader = require('parse-link-header'); 4 | const got = require('got'); 5 | 6 | const loadUrl = (url, token) => { 7 | return got(url, { 8 | headers: {authorization: `token ${token}`}, 9 | json: true 10 | }); 11 | }; 12 | 13 | module.exports = ({token, user, repo}) => { 14 | const issues = []; 15 | 16 | const next = url => { 17 | return loadUrl(url, token).then(res => { 18 | issues.push.apply(issues, res.body); 19 | 20 | if (res.headers.link) { 21 | const link = parseLinkHeader(res.headers.link); 22 | 23 | if (link.next) { 24 | return next(link.next.url); 25 | } 26 | } 27 | }); 28 | }; 29 | 30 | return next(`https://api.github.com/repos/${user}/${repo}/issues`) 31 | .then(() => issues); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/generate-issue-body.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const StackUtils = require('stack-utils'); 4 | const stripIndent = require('strip-indent'); 5 | const objectToMap = require('object-to-map'); 6 | const humanize = require('humanize-string'); 7 | 8 | const stackUtils = new StackUtils({ 9 | internals: StackUtils.nodeInternals() 10 | }); 11 | 12 | module.exports = err => { 13 | const stack = err.stack ? stackUtils.clean(err.stack) : 'No stack trace'; 14 | const props = []; 15 | 16 | // labels are handled by `lib/create-issue`, 17 | // no need to display them in issue body 18 | delete err.props.labels; 19 | 20 | for (let [key, value] of objectToMap(err.props)) { 21 | props.push(`**${humanize(key)}**: \`${value}\``); 22 | } 23 | 24 | return stripIndent(` 25 | Hey, [OhCrash](https://github.com/vadimdemedes/ohcrash) just reported an error 🔥! 26 | Here are the relevant details: 27 | 28 | **Error name:** \`${err.name}\` 29 | **Message:** \`${err.message}\` 30 | 31 | ${props.join('\n\t\t')} 32 | 33 | **Stack trace:** 34 | 35 | \`\`\`js 36 | ${stack} 37 | \`\`\` 38 | 39 | Best of luck! 40 | `).trim(); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/normalize-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = err => { 4 | if (!err.name) { 5 | err.name = 'Error'; 6 | } 7 | 8 | if (!err.message) { 9 | err.message = 'No error message'; 10 | } 11 | 12 | if (!err.props) { 13 | err.props = {}; 14 | } 15 | 16 | return err; 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ohcrash", 3 | "version": "2.0.1", 4 | "description": "Report errors directly to GitHub Issues", 5 | "author": "Vadim Demedes ", 6 | "repository": "vadimdemedes/ohcrash", 7 | "scripts": { 8 | "start": "micro", 9 | "test": "xo && ava" 10 | }, 11 | "engines": { 12 | "node": ">= 6" 13 | }, 14 | "files": [ 15 | "lib", 16 | "index.js", 17 | "cli.js" 18 | ], 19 | "bin": "cli.js", 20 | "main": "index.js", 21 | "dependencies": { 22 | "got": "^6.7.1", 23 | "humanize-string": "^1.0.1", 24 | "micro": "^7.0.0", 25 | "object-to-map": "^2.0.0", 26 | "parse-link-header": "^0.4.1", 27 | "stack-utils": "^1.0.0", 28 | "strip-indent": "^2.0.0", 29 | "update-notifier": "^1.0.3" 30 | }, 31 | "devDependencies": { 32 | "async-to-gen": "^1.3.2", 33 | "ava": "^0.18.1", 34 | "nock": "^9.0.2", 35 | "test-listen": "^1.0.1", 36 | "xo": "^0.17.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | OhCrash 4 |
5 |
6 |

7 | 8 | [![Build Status](https://travis-ci.org/vadimdemedes/ohcrash.svg?branch=master)](https://travis-ci.org/vadimdemedes/ohcrash) 9 | 10 | A microservice, which creates issues in a GitHub repository for each reported error. 11 | Think of it as barebones [BugSnag](https://bugsnag.com), but errors are reported straight to GitHub Issues. 12 | 13 | You can effortlessly deploy your own instance of OhCrash using [now](https://zeit.co/now). 14 | 15 | [![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/vadimdemedes/ohcrash&env=GITHUB_TOKEN&env=GITHUB_USER&env=GITHUB_REPO) 16 | 17 | 18 | ## Usage 19 | 20 | OhCrash microservice requires a GitHub token, username and a repository name. 21 | You can obtain your personal access token [here](https://github.com/settings/tokens). 22 | Make sure to select `public_repo` scope to create issues in a public repository or `repo` for private repositories. 23 | 24 | If you want to run OhCrash locally: 25 | 26 | ```bash 27 | $ npm install --global ohcrash 28 | $ export GITHUB_TOKEN="your token" 29 | $ export GITHUB_USER="your username" 30 | $ export GITHUB_REPO="target repository name" 31 | $ ohcrash 32 | ``` 33 | 34 | `ohcrash` command accepts the same options as [micro](https://github.com/zeit/micro). 35 | 36 | After OhCrash instance is up, use [ohcrash-client](https://github.com/vadimdemedes/ohcrash-client) module to start reporting errors! 37 | It catches uncaught exceptions and unhandled rejections out-of-the-box. 38 | Errors can also be reported manually, using a `report()` method. 39 | 40 | ```js 41 | const ohcrash = require('ohcrash-client').register('http://localhost:3000'); 42 | 43 | const err = new Error('Custom error handling'); 44 | ohcrash.report(err); 45 | ``` 46 | 47 | Learn more about the client at [ohcrash-client](https://github.com/vadimdemedes/ohcrash-client) repository. 48 | 49 | 50 | ## Deployment 51 | 52 | OhCrash can (and should 😄) be easily deployed to [now](https://github.com/zeit/now) by Zeit. 53 | Assuming you've got `now` all set up: 54 | 55 | ``` 56 | $ now -e GITHUB_TOKEN=token -e GITHUB_USER=user -e GITHUB_REPO=repo vadimdemedes/ohcrash 57 | ``` 58 | 59 | Alternatively, deploy `ohcrash` without even leaving the browser: 60 | 61 | [![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/vadimdemedes/ohcrash&env=GITHUB_TOKEN&env=GITHUB_USER&env=GITHUB_REPO) 62 | 63 | Make sure to set a persistent alias using `now alias` for your deployment. 64 | Execute `now help alias` for information on how to do this. 65 | Later, use that URL as an endpoint for [ohcrash-client](https://github.com/vadimdemedes/ohcrash-client). 66 | 67 | ```js 68 | require('ohcrash-client').register('https://my-ohcrash.now.sh'); 69 | ``` 70 | 71 | ## License 72 | 73 | MIT © [Vadim Demedes](https://vadimdemedes.com) 74 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const listen = require('test-listen'); 4 | const micro = require('micro'); 5 | const test = require('ava'); 6 | const got = require('got'); 7 | const generateIssueBody = require('../lib/generate-issue-body'); 8 | const mockCreateIssue = require('./helpers/mock-create-issue'); 9 | const mockFindIssues = require('./helpers/mock-find-issues'); 10 | 11 | const token = process.env.GITHUB_TOKEN = 'token'; 12 | const user = process.env.GITHUB_USER = 'user'; 13 | const repo = process.env.GITHUB_REPO = 'repo'; 14 | 15 | require('async-to-gen/register')({includes: /index\.js$/}); 16 | const app = require('../'); // eslint-disable-line import/order 17 | 18 | test('report error', async t => { 19 | const service = micro(app); 20 | const url = await listen(service); 21 | 22 | const err = new Error('Error message'); 23 | err.props = {runtime: 'Node.js'}; 24 | 25 | const findReq = mockFindIssues({ 26 | token, 27 | user, 28 | repo, 29 | issues: [] 30 | }); 31 | 32 | const createReq = mockCreateIssue({ 33 | token, 34 | user, 35 | repo, 36 | title: 'Error: Error message', 37 | body: generateIssueBody(err) 38 | }); 39 | 40 | const res = await got(url, { 41 | method: 'post', 42 | headers: {'content-type': 'application/json'}, 43 | body: JSON.stringify({ 44 | name: err.name, 45 | message: err.message, 46 | stack: err.stack, 47 | props: err.props 48 | }) 49 | }); 50 | 51 | t.true(findReq.isDone()); 52 | t.true(createReq.isDone()); 53 | t.is(res.body, ''); 54 | }); 55 | 56 | test('report error if issue exists but closed', async t => { 57 | const service = micro(app); 58 | const url = await listen(service); 59 | 60 | const err = new Error('Error message'); 61 | err.props = {runtime: 'Node.js'}; 62 | 63 | const findReq = mockFindIssues({ 64 | token, 65 | user, 66 | repo, 67 | issues: [{title: 'Error: Error message', state: 'closed'}] 68 | }); 69 | 70 | const createReq = mockCreateIssue({ 71 | token, 72 | user, 73 | repo, 74 | title: 'Error: Error message', 75 | body: generateIssueBody(err) 76 | }); 77 | 78 | const res = await got(url, { 79 | method: 'post', 80 | headers: {'content-type': 'application/json'}, 81 | body: JSON.stringify({ 82 | name: err.name, 83 | message: err.message, 84 | stack: err.stack, 85 | props: err.props 86 | }) 87 | }); 88 | 89 | t.true(findReq.isDone()); 90 | t.true(createReq.isDone()); 91 | t.is(res.body, ''); 92 | }); 93 | 94 | test('ignore error if issue is already open', async t => { 95 | const service = micro(app); 96 | const url = await listen(service); 97 | 98 | const err = new Error('Error message'); 99 | err.props = {runtime: 'Node.js'}; 100 | 101 | const findReq = mockFindIssues({ 102 | token, 103 | user, 104 | repo, 105 | issues: [{title: 'Error: Error message', state: 'open'}] 106 | }); 107 | 108 | const res = await got(url, { 109 | method: 'post', 110 | headers: {'content-type': 'application/json'}, 111 | body: JSON.stringify({ 112 | name: err.name, 113 | message: err.message, 114 | stack: err.stack, 115 | props: err.props 116 | }) 117 | }); 118 | 119 | t.true(findReq.isDone()); 120 | t.is(res.body, ''); 121 | }); 122 | 123 | test('report error without name', async t => { 124 | const service = micro(app); 125 | const url = await listen(service); 126 | 127 | const err = new Error('Error message'); 128 | err.name = undefined; 129 | err.props = {runtime: 'Node.js'}; 130 | 131 | const findReq = mockFindIssues({ 132 | token, 133 | user, 134 | repo, 135 | issues: [] 136 | }); 137 | 138 | const createReq = mockCreateIssue({ 139 | token, 140 | user, 141 | repo, 142 | title: 'Error: Error message', 143 | body: generateIssueBody(Object.assign(err, {name: 'Error'})) 144 | }); 145 | 146 | const res = await got(url, { 147 | method: 'post', 148 | headers: {'content-type': 'application/json'}, 149 | body: JSON.stringify({ 150 | message: err.message, 151 | stack: err.stack, 152 | props: err.props 153 | }) 154 | }); 155 | 156 | t.true(findReq.isDone()); 157 | t.true(createReq.isDone()); 158 | t.is(res.body, ''); 159 | }); 160 | 161 | test('report error without message', async t => { 162 | const service = micro(app); 163 | const url = await listen(service); 164 | 165 | const err = new Error(); 166 | err.props = {runtime: 'Node.js'}; 167 | 168 | const findReq = mockFindIssues({ 169 | token, 170 | user, 171 | repo, 172 | issues: [] 173 | }); 174 | 175 | const createReq = mockCreateIssue({ 176 | token, 177 | user, 178 | repo, 179 | title: 'Error: No error message', 180 | body: generateIssueBody(Object.assign(err, {message: 'No error message'})) 181 | }); 182 | 183 | const res = await got(url, { 184 | method: 'post', 185 | headers: {'content-type': 'application/json'}, 186 | body: JSON.stringify({ 187 | name: err.name, 188 | stack: err.stack, 189 | props: err.props 190 | }) 191 | }); 192 | 193 | t.true(findReq.isDone()); 194 | t.true(createReq.isDone()); 195 | t.is(res.body, ''); 196 | }); 197 | 198 | test('report error without props', async t => { 199 | const service = micro(app); 200 | const url = await listen(service); 201 | 202 | const err = new Error('Error message'); 203 | 204 | const findReq = mockFindIssues({ 205 | token, 206 | user, 207 | repo, 208 | issues: [] 209 | }); 210 | 211 | const createReq = mockCreateIssue({ 212 | token, 213 | user, 214 | repo, 215 | title: 'Error: Error message', 216 | body: generateIssueBody(Object.assign(err, {props: {}})) 217 | }); 218 | 219 | const res = await got(url, { 220 | method: 'post', 221 | headers: {'content-type': 'application/json'}, 222 | body: JSON.stringify({ 223 | name: err.name, 224 | message: err.message, 225 | stack: err.stack 226 | }) 227 | }); 228 | 229 | t.true(findReq.isDone()); 230 | t.true(createReq.isDone()); 231 | t.is(res.body, ''); 232 | }); 233 | -------------------------------------------------------------------------------- /test/create-issue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const createIssue = require('../lib/create-issue'); 5 | const mockCreateIssue = require('./helpers/mock-create-issue'); 6 | 7 | test('send request to create issue', async t => { 8 | const options = { 9 | token: 'token', 10 | user: 'avajs', 11 | repo: 'ava', 12 | title: 'title', 13 | body: 'body', 14 | labels: ['priotity', 'bug'] 15 | }; 16 | 17 | const req = mockCreateIssue(options); 18 | await createIssue(options); 19 | t.true(req.isDone()); 20 | }); 21 | -------------------------------------------------------------------------------- /test/find-issues.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const findIssues = require('../lib/find-issues'); 5 | const mockFindIssues = require('./helpers/mock-find-issues'); 6 | 7 | test('find issues', async t => { 8 | const token = 'token'; 9 | const user = 'avajs'; 10 | const repo = 'ava'; 11 | 12 | const ghIssues = ['a', 'b', 'c']; 13 | const req = mockFindIssues({token, user, repo, issues: ghIssues}); 14 | 15 | const issues = await findIssues({token, user, repo}); 16 | t.true(req.isDone()); 17 | t.deepEqual(issues, ghIssues); 18 | }); 19 | 20 | test('fetch all pages', async t => { 21 | const token = 'token'; 22 | const user = 'avajs'; 23 | const repo = 'ava'; 24 | 25 | const firstPage = mockFindIssues({ 26 | token, 27 | user, 28 | repo, 29 | issues: ['a', 'b'], 30 | headers: {link: `; rel="next"`} 31 | }); 32 | 33 | const secondPage = mockFindIssues({ 34 | token, 35 | user, 36 | repo, 37 | query: { 38 | page: 2, 39 | per_page: 100 // eslint-disable-line camelcase 40 | }, 41 | issues: ['c', 'd'] 42 | }); 43 | 44 | const issues = await findIssues({token, user, repo}); 45 | t.true(firstPage.isDone()); 46 | t.true(secondPage.isDone()); 47 | t.deepEqual(issues, ['a', 'b', 'c', 'd']); 48 | }); 49 | -------------------------------------------------------------------------------- /test/generate-issue-body.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stripIndent = require('strip-indent'); 4 | const StackUtils = require('stack-utils'); 5 | const test = require('ava'); 6 | const generateIssueBody = require('../lib/generate-issue-body'); 7 | const normalizeError = require('../lib/normalize-error'); 8 | 9 | const stackUtils = new StackUtils({ 10 | internals: StackUtils.nodeInternals() 11 | }); 12 | 13 | test('essential info', t => { 14 | const err = normalizeError(new Error('Error message')); 15 | err.props.runtime = 'runtime'; 16 | 17 | const issue = generateIssueBody(err); 18 | 19 | t.is(issue, stripIndent(` 20 | Hey, [OhCrash](https://github.com/vadimdemedes/ohcrash) just reported an error 🔥! 21 | Here are the relevant details: 22 | 23 | **Error name:** \`Error\` 24 | **Message:** \`Error message\` 25 | 26 | **Runtime**: \`runtime\` 27 | 28 | **Stack trace:** 29 | 30 | \`\`\`js 31 | ${stackUtils.clean(err.stack)} 32 | \`\`\` 33 | 34 | Best of luck! 35 | `).trim()); 36 | }); 37 | 38 | test('ignore labels', t => { 39 | const err = normalizeError(new Error('Error message')); 40 | err.props.runtime = 'runtime'; 41 | err.props.labels = ['bug', 'priority']; 42 | 43 | const issue = generateIssueBody(err); 44 | 45 | t.is(issue, stripIndent(` 46 | Hey, [OhCrash](https://github.com/vadimdemedes/ohcrash) just reported an error 🔥! 47 | Here are the relevant details: 48 | 49 | **Error name:** \`Error\` 50 | **Message:** \`Error message\` 51 | 52 | **Runtime**: \`runtime\` 53 | 54 | **Stack trace:** 55 | 56 | \`\`\`js 57 | ${stackUtils.clean(err.stack)} 58 | \`\`\` 59 | 60 | Best of luck! 61 | `).trim()); 62 | }); 63 | 64 | test('missing stack trace', t => { 65 | const err = normalizeError(new Error('Error message')); 66 | err.props.runtime = 'runtime'; 67 | err.stack = undefined; 68 | 69 | const issue = generateIssueBody(err); 70 | 71 | t.is(issue, stripIndent(` 72 | Hey, [OhCrash](https://github.com/vadimdemedes/ohcrash) just reported an error 🔥! 73 | Here are the relevant details: 74 | 75 | **Error name:** \`Error\` 76 | **Message:** \`Error message\` 77 | 78 | **Runtime**: \`runtime\` 79 | 80 | **Stack trace:** 81 | 82 | \`\`\`js 83 | No stack trace 84 | \`\`\` 85 | 86 | Best of luck! 87 | `).trim()); 88 | }); 89 | 90 | test('custom props', t => { 91 | const err = normalizeError(new Error('Error message')); 92 | err.props = { 93 | env: 'development', 94 | version: '1.0.0' 95 | }; 96 | 97 | const issue = generateIssueBody(err); 98 | 99 | t.is(issue, stripIndent(` 100 | Hey, [OhCrash](https://github.com/vadimdemedes/ohcrash) just reported an error 🔥! 101 | Here are the relevant details: 102 | 103 | **Error name:** \`Error\` 104 | **Message:** \`Error message\` 105 | 106 | **Env**: \`development\` 107 | **Version**: \`1.0.0\` 108 | 109 | **Stack trace:** 110 | 111 | \`\`\`js 112 | ${stackUtils.clean(err.stack)} 113 | \`\`\` 114 | 115 | Best of luck! 116 | `).trim()); 117 | }); 118 | -------------------------------------------------------------------------------- /test/helpers/mock-create-issue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nock = require('nock'); 4 | 5 | module.exports = ({token, user, repo, title, body, labels}) => { 6 | const headers = { 7 | authorization: `token ${token}`, 8 | 'content-type': 'application/json' 9 | }; 10 | 11 | return nock('https://api.github.com', {reqheaders: headers}) 12 | .post(`/repos/${user}/${repo}/issues`, {title, body, labels}) 13 | .reply(201); 14 | }; 15 | -------------------------------------------------------------------------------- /test/helpers/mock-find-issues.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nock = require('nock'); 4 | 5 | module.exports = ({token, user, repo, query, headers, issues}) => { 6 | return nock('https://api.github.com', { 7 | reqheaders: {authorization: `token ${token}`} 8 | }) 9 | .get(`/repos/${user}/${repo}/issues`) 10 | .query(query) 11 | .reply(200, issues, headers); 12 | }; 13 | --------------------------------------------------------------------------------