-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
151 lines (137 loc) · 4.52 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
const crypto = require('crypto')
/**
* Calculate a string representation of the error with as much information as possible
* @param {*} err
*/
const errorToString = (err) => {
return err.stack || err.message || String(err) || 'Unknown error'
}
/**
* Calculate a hash of the string representation of the error in order to
* not create multiple issues for the same problem. The hash will be in the title
* of the issue according to the requirements
* @param {String} string
*/
const hash = (string) => {
return crypto.createHash('sha256')
.update(string)
.digest('hex')
.substring(0, 8)
}
/**
* Core functionality for handling an error
* @param {Context} context
* @param {*} err
*/
const reportError = async (context, err, opts) => {
const options = {
title: 'Probot integration problem',
body: 'An error occurred',
labels: [],
reopen: false,
...opts
}
// I decided not to support passing a milestone or assignees
// since that opens the door for errors when creating the issue if the data provided is wrong
// and for an error reporting tool it is an important requirement not to fail
// when reporting an error :)
const params = context.repo()
const {owner, repo} = params
const errString = errorToString(err)
const errCode = hash(errString)
const title = `[${errCode}] ${options.title}`
// If the webhook event is due to the current issue, ignore it
// Example: an app listens to issues.opened events, it fails, we create an issue
// because of that crash, then the app receives that new event, and crashes again.
// That could potentially create an infinite loop between the app and GitHub
if (context.payload.issue && context.payload.issue.title === title) return
// Look for an existing issue with the same error hash/code
const q = [
'sort:updated-desc',
options.reopen ? '' : 'is:open',
errCode
]
.filter(Boolean)
.join(' ')
const result = await context.github.search.issues({ q })
const issue = result.data.items[0]
if (issue) {
// If the issue exists we update the occurrences counter and also that updates
// the updated_at date. Useful for sorting the issues in the UI or API
const { number } = issue
let { body } = issue
body = body
.replace(/(Occurrences:\s*)(\d+)/, (match, label, value) => label + String(+value + 1))
// If reopen is set to true and the issue is closed, reopen it
const state = issue.state === 'closed' && options.reopen ? 'open' : issue.state
await context.github.issues.edit({ owner, repo, number, body, state })
} else {
const body = [
options.body,
'```\n' + errString + '\n```',
'Occurrences: 1'
].join('\n\n')
await context.github.issues.create({owner, repo, title, body, labels: options.labels})
}
}
class Lifeguard {
constructor (options) {
this.options = options
}
/**
* Common functionality for invoking the original callback
* @param {Context} context
* @param {Function} callback
* @param {Object} that
*/
async invokeCallback (context, callback, that) {
try {
return await callback.apply(that, arguments)
} catch (err) {
await reportError(context, err, this.options)
// Throw it again so it is handled by probot and logs it with bunyan
throw err
}
}
/**
* Use this mehtod to wrap the whole bot application. It overrides the
* robot.on() method to make sure all the event handlers are safely wrapped
* to catch any errors.
*
* This is implemented in a way that "this" is kept even after wrapping the handler.
*
* All the probot examples use arrow functions, but if you pass
* a regular function binded to an object and you invoke the function
* then the "this" reference is kept. The same should happen in our
* library, the ABI should not change when using probot-lifeguard
* @param {Function} handler
*/
guardApp (handler) {
const self = this
return app => {
const original = app.on.bind(app)
app.on = function (event, callback) {
return original(event, async function (context) {
return self.invokeCallback(context, callback, this)
})
}
handler(app)
}
}
/**
* Use this method to wrap just one event handler
* @param {Function} callback
*/
guardHandler (callback) {
const self = this
return async function (context) {
return self.invokeCallback(context, callback, this)
}
}
}
module.exports = {
reportError,
hash,
errorToString,
lifeguard: (options) => new Lifeguard(options)
}