sentry-manager-tests.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. const Path = require('path')
  2. const SandboxedModule = require('sandboxed-module')
  3. const { expect } = require('chai')
  4. const sinon = require('sinon')
  5. const MODULE_PATH = Path.join(__dirname, '../../sentry-manager.js')
  6. describe('SentryManager', function () {
  7. beforeEach(function () {
  8. this.clock = sinon.useFakeTimers(Date.now())
  9. this.Sentry = {
  10. init: sinon.stub(),
  11. captureException: sinon.stub(),
  12. }
  13. this.SentryManager = SandboxedModule.require(MODULE_PATH, {
  14. requires: {
  15. '@sentry/node': this.Sentry,
  16. },
  17. })
  18. this.sentryManager = new this.SentryManager('test_dsn')
  19. })
  20. afterEach(function () {
  21. this.clock.restore()
  22. })
  23. describe('captureExceptionRateLimited', function () {
  24. it('should report a single error to sentry', function () {
  25. this.sentryManager.captureExceptionRateLimited({ foo: 'bar' }, 'message')
  26. expect(this.Sentry.captureException).to.have.been.calledOnce
  27. })
  28. it('should report the same error to sentry only once', function () {
  29. const error1 = new Error('this is the error')
  30. this.sentryManager.captureExceptionRateLimited(
  31. { foo: error1 },
  32. 'first message'
  33. )
  34. this.sentryManager.captureExceptionRateLimited(
  35. { bar: error1 },
  36. 'second message'
  37. )
  38. expect(this.Sentry.captureException).to.have.been.calledOnce
  39. })
  40. it('should report two different errors to sentry individually', function () {
  41. const error1 = new Error('this is the error')
  42. const error2 = new Error('this is the error')
  43. this.sentryManager.captureExceptionRateLimited(
  44. { foo: error1 },
  45. 'first message'
  46. )
  47. this.sentryManager.captureExceptionRateLimited(
  48. { bar: error2 },
  49. 'second message'
  50. )
  51. expect(this.Sentry.captureException).to.have.been.calledTwice
  52. })
  53. it('for multiple errors should only report a maximum of 5 errors to sentry', function () {
  54. for (let i = 0; i < 10; i++) {
  55. this.sentryManager.captureExceptionRateLimited(
  56. { foo: 'bar' },
  57. 'message'
  58. )
  59. }
  60. expect(this.Sentry.captureException).to.have.callCount(5)
  61. })
  62. it('for multiple errors with a minute delay should report 10 errors to sentry', function () {
  63. for (let i = 0; i < 10; i++) {
  64. this.sentryManager.captureExceptionRateLimited(
  65. { foo: 'bar' },
  66. 'message'
  67. )
  68. }
  69. expect(this.Sentry.captureException).to.have.callCount(5)
  70. // allow a minute to pass
  71. this.clock.tick(61 * 1000)
  72. for (let i = 0; i < 10; i++) {
  73. this.sentryManager.captureExceptionRateLimited(
  74. { foo: 'bar' },
  75. 'message'
  76. )
  77. }
  78. expect(this.Sentry.captureException).to.have.callCount(10)
  79. })
  80. })
  81. describe('captureException', function () {
  82. it('should remove the path from fs errors', function () {
  83. const fsError = new Error(
  84. "Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'"
  85. )
  86. fsError.path = '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'
  87. this.sentryManager.captureException({ err: fsError }, 'message', 'error')
  88. expect(this.Sentry.captureException).to.have.been.calledWith(
  89. sinon.match.has(
  90. 'message',
  91. 'Error: ENOENT: no such file or directory, stat'
  92. )
  93. )
  94. })
  95. it('should sanitize error', function () {
  96. const err = {
  97. name: 'CustomError',
  98. message: 'hello',
  99. _oErrorTags: [{ stack: 'here:1', info: { one: 1 } }],
  100. stack: 'here:0',
  101. info: { key: 'value' },
  102. code: 42,
  103. signal: 9,
  104. path: '/foo',
  105. }
  106. this.sentryManager.captureException({ err }, 'message', 'error')
  107. const expectedErr = {
  108. name: 'CustomError',
  109. message: 'hello',
  110. stack: 'here:0\nhere:1',
  111. code: 42,
  112. signal: 9,
  113. path: '/foo',
  114. }
  115. expect(this.Sentry.captureException).to.have.been.calledWith(
  116. sinon.match(expectedErr),
  117. sinon.match({
  118. tags: sinon.match.any,
  119. level: sinon.match.any,
  120. extra: {
  121. description: 'message',
  122. info: sinon.match({
  123. one: 1,
  124. key: 'value',
  125. }),
  126. },
  127. })
  128. )
  129. // Chai is very picky with comparing Error instances. Go the long way of comparing all the fields manually.
  130. const gotErr = this.Sentry.captureException.args[0][0]
  131. for (const [key, wanted] of Object.entries(expectedErr)) {
  132. expect(gotErr).to.have.property(key, wanted)
  133. }
  134. })
  135. it('should sanitize request', function () {
  136. const req = {
  137. ip: '1.2.3.4',
  138. method: 'GET',
  139. url: '/foo',
  140. headers: {
  141. referer: 'abc',
  142. 'content-length': 1337,
  143. 'user-agent': 'curl',
  144. authorization: '42',
  145. },
  146. }
  147. this.sentryManager.captureException({ req }, 'message', 'error')
  148. const expectedReq = {
  149. remoteAddress: '1.2.3.4',
  150. method: 'GET',
  151. url: '/foo',
  152. headers: {
  153. referer: 'abc',
  154. 'content-length': 1337,
  155. 'user-agent': 'curl',
  156. },
  157. }
  158. expect(this.Sentry.captureException).to.have.been.calledWith(
  159. sinon.match({
  160. message: 'message',
  161. }),
  162. sinon.match({
  163. tags: sinon.match.any,
  164. level: sinon.match.any,
  165. extra: {
  166. info: sinon.match.any,
  167. req: expectedReq,
  168. },
  169. })
  170. )
  171. expect(this.Sentry.captureException.args[0][1].extra.req).to.deep.equal(
  172. expectedReq
  173. )
  174. })
  175. it('should sanitize response', function () {
  176. const res = {
  177. statusCode: 417,
  178. body: Buffer.from('foo'),
  179. getHeader(key) {
  180. expect(key).to.be.oneOf(['content-length'])
  181. if (key === 'content-length') return 1337
  182. },
  183. }
  184. this.sentryManager.captureException({ res }, 'message', 'error')
  185. const expectedRes = {
  186. statusCode: 417,
  187. headers: {
  188. 'content-length': 1337,
  189. },
  190. }
  191. expect(this.Sentry.captureException).to.have.been.calledWith(
  192. sinon.match({
  193. message: 'message',
  194. }),
  195. sinon.match({
  196. tags: sinon.match.any,
  197. level: sinon.match.any,
  198. extra: {
  199. info: sinon.match.any,
  200. res: expectedRes,
  201. },
  202. })
  203. )
  204. expect(this.Sentry.captureException.args[0][1].extra.res).to.deep.equal(
  205. expectedRes
  206. )
  207. })
  208. describe('reportedToSentry', function () {
  209. it('should mark the error as reported to sentry', function () {
  210. const err = new Error()
  211. this.sentryManager.captureException({ err }, 'message')
  212. expect(this.Sentry.captureException).to.have.been.called
  213. expect(err.reportedToSentry).to.equal(true)
  214. })
  215. it('should mark two errors as reported to sentry', function () {
  216. const err1 = new Error()
  217. const err2 = new Error()
  218. this.sentryManager.captureException({ err: err1, err2 }, 'message')
  219. expect(this.Sentry.captureException).to.have.been.called
  220. expect(err1.reportedToSentry).to.equal(true)
  221. expect(err2.reportedToSentry).to.equal(true)
  222. })
  223. it('should not mark arbitrary objects as reported to sentry', function () {
  224. const err = new Error()
  225. const ctx = { foo: 'bar' }
  226. this.sentryManager.captureException({ err, ctx }, 'message')
  227. expect(this.Sentry.captureException).to.have.been.called
  228. expect(ctx.reportedToSentry).not.to.exist
  229. })
  230. })
  231. })
  232. })