overleaf/libraries/logger/test/unit/sentry-manager-tests.js
Antoine Clausse 7f48c67512 Add prefer-node-protocol ESLint rule (#21532)
* Add `unicorn/prefer-node-protocol`

* Fix `unicorn/prefer-node-protocol` ESLint errors

* Run `npm run format:fix`

* Add sandboxed-module sourceTransformers in mocha setups

Fix `no such file or directory, open 'node:fs'` in `sandboxed-module`

* Remove `node:` in the SandboxedModule requires

* Fix new linting errors with `node:`

GitOrigin-RevId: 68f6e31e2191fcff4cb8058dd0a6914c14f59926
2024-11-11 09:04:51 +00:00

247 lines
7.5 KiB
JavaScript

const Path = require('node:path')
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const sinon = require('sinon')
const MODULE_PATH = Path.join(__dirname, '../../sentry-manager.js')
describe('SentryManager', function () {
beforeEach(function () {
this.clock = sinon.useFakeTimers(Date.now())
this.Sentry = {
init: sinon.stub(),
captureException: sinon.stub(),
}
this.SentryManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'@sentry/node': this.Sentry,
},
})
this.sentryManager = new this.SentryManager('test_dsn')
})
afterEach(function () {
this.clock.restore()
})
describe('captureExceptionRateLimited', function () {
it('should report a single error to sentry', function () {
this.sentryManager.captureExceptionRateLimited({ foo: 'bar' }, 'message')
expect(this.Sentry.captureException).to.have.been.calledOnce
})
it('should report the same error to sentry only once', function () {
const error1 = new Error('this is the error')
this.sentryManager.captureExceptionRateLimited(
{ foo: error1 },
'first message'
)
this.sentryManager.captureExceptionRateLimited(
{ bar: error1 },
'second message'
)
expect(this.Sentry.captureException).to.have.been.calledOnce
})
it('should report two different errors to sentry individually', function () {
const error1 = new Error('this is the error')
const error2 = new Error('this is the error')
this.sentryManager.captureExceptionRateLimited(
{ foo: error1 },
'first message'
)
this.sentryManager.captureExceptionRateLimited(
{ bar: error2 },
'second message'
)
expect(this.Sentry.captureException).to.have.been.calledTwice
})
it('for multiple errors should only report a maximum of 5 errors to sentry', function () {
for (let i = 0; i < 10; i++) {
this.sentryManager.captureExceptionRateLimited(
{ foo: 'bar' },
'message'
)
}
expect(this.Sentry.captureException).to.have.callCount(5)
})
it('for multiple errors with a minute delay should report 10 errors to sentry', function () {
for (let i = 0; i < 10; i++) {
this.sentryManager.captureExceptionRateLimited(
{ foo: 'bar' },
'message'
)
}
expect(this.Sentry.captureException).to.have.callCount(5)
// allow a minute to pass
this.clock.tick(61 * 1000)
for (let i = 0; i < 10; i++) {
this.sentryManager.captureExceptionRateLimited(
{ foo: 'bar' },
'message'
)
}
expect(this.Sentry.captureException).to.have.callCount(10)
})
})
describe('captureException', function () {
it('should remove the path from fs errors', function () {
const fsError = new Error(
"Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'"
)
fsError.path = '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'
this.sentryManager.captureException({ err: fsError }, 'message', 'error')
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match.has(
'message',
'Error: ENOENT: no such file or directory, stat'
)
)
})
it('should sanitize error', function () {
const err = {
name: 'CustomError',
message: 'hello',
_oErrorTags: [{ stack: 'here:1', info: { one: 1 } }],
stack: 'here:0',
info: { key: 'value' },
code: 42,
signal: 9,
path: '/foo',
}
this.sentryManager.captureException({ err }, 'message', 'error')
const expectedErr = {
name: 'CustomError',
message: 'hello',
stack: 'here:0\nhere:1',
code: 42,
signal: 9,
path: '/foo',
}
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match(expectedErr),
sinon.match({
tags: sinon.match.any,
level: sinon.match.any,
extra: {
description: 'message',
info: sinon.match({
one: 1,
key: 'value',
}),
},
})
)
// Chai is very picky with comparing Error instances. Go the long way of comparing all the fields manually.
const gotErr = this.Sentry.captureException.args[0][0]
for (const [key, wanted] of Object.entries(expectedErr)) {
expect(gotErr).to.have.property(key, wanted)
}
})
it('should sanitize request', function () {
const req = {
ip: '1.2.3.4',
method: 'GET',
url: '/foo',
headers: {
referer: 'abc',
'content-length': 1337,
'user-agent': 'curl',
authorization: '42',
},
}
this.sentryManager.captureException({ req }, 'message', 'error')
const expectedReq = {
remoteAddress: '1.2.3.4',
method: 'GET',
url: '/foo',
headers: {
referer: 'abc',
'content-length': 1337,
'user-agent': 'curl',
},
}
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match({
message: 'message',
}),
sinon.match({
tags: sinon.match.any,
level: sinon.match.any,
extra: {
info: sinon.match.any,
req: expectedReq,
},
})
)
expect(this.Sentry.captureException.args[0][1].extra.req).to.deep.equal(
expectedReq
)
})
it('should sanitize response', function () {
const res = {
statusCode: 417,
body: Buffer.from('foo'),
getHeader(key) {
expect(key).to.be.oneOf(['content-length'])
if (key === 'content-length') return 1337
},
}
this.sentryManager.captureException({ res }, 'message', 'error')
const expectedRes = {
statusCode: 417,
headers: {
'content-length': 1337,
},
}
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match({
message: 'message',
}),
sinon.match({
tags: sinon.match.any,
level: sinon.match.any,
extra: {
info: sinon.match.any,
res: expectedRes,
},
})
)
expect(this.Sentry.captureException.args[0][1].extra.res).to.deep.equal(
expectedRes
)
})
describe('reportedToSentry', function () {
it('should mark the error as reported to sentry', function () {
const err = new Error()
this.sentryManager.captureException({ err }, 'message')
expect(this.Sentry.captureException).to.have.been.called
expect(err.reportedToSentry).to.equal(true)
})
it('should mark two errors as reported to sentry', function () {
const err1 = new Error()
const err2 = new Error()
this.sentryManager.captureException({ err: err1, err2 }, 'message')
expect(this.Sentry.captureException).to.have.been.called
expect(err1.reportedToSentry).to.equal(true)
expect(err2.reportedToSentry).to.equal(true)
})
it('should not mark arbitrary objects as reported to sentry', function () {
const err = new Error()
const ctx = { foo: 'bar' }
this.sentryManager.captureException({ err, ctx }, 'message')
expect(this.Sentry.captureException).to.have.been.called
expect(ctx.reportedToSentry).not.to.exist
})
})
})
})