Merge pull request #20750 from overleaf/jpa-issue-20743

[logger] sanitize sentry exception context

GitOrigin-RevId: 7f141ad93fc8df807acad9bcf691027efe799150
This commit is contained in:
Jakob Ackermann 2024-10-01 21:39:32 +02:00 committed by Copybot
parent 50aad92eb9
commit 85bc305df2
3 changed files with 126 additions and 33 deletions

View file

@ -1,4 +1,4 @@
const OError = require('@overleaf/o-error') const Serializers = require('./serializers')
const RATE_LIMIT_MAX_ERRORS = 5 const RATE_LIMIT_MAX_ERRORS = 5
const RATE_LIMIT_INTERVAL_MS = 60000 const RATE_LIMIT_INTERVAL_MS = 60000
@ -42,7 +42,7 @@ class SentryManager {
} }
// extract any error object // extract any error object
let error = attributes.err || attributes.error let error = Serializers.err(attributes.err || attributes.error)
// avoid reporting errors twice // avoid reporting errors twice
for (const key in attributes) { for (const key in attributes) {
@ -67,45 +67,21 @@ class SentryManager {
const tags = {} const tags = {}
const extra = {} const extra = {}
for (const key in attributes) { for (const key in attributes) {
const value = attributes[key] let value = attributes[key]
if (Serializers[key]) {
value = Serializers[key](value)
}
if (key.match(/_id/) && typeof value === 'string') { if (key.match(/_id/) && typeof value === 'string') {
tags[key] = value tags[key] = value
} }
extra[key] = value extra[key] = value
} }
// capture req object if available
const { req } = attributes
if (req != null) {
extra.req = {
method: req.method,
url: req.originalUrl,
query: req.query,
headers: req.headers,
ip: req.ip,
}
}
// recreate error objects that have been converted to a normal object
if (!(error instanceof Error) && typeof error === 'object') {
const newError = new Error(error.message)
for (const key of Object.keys(error || {})) {
const value = error[key]
newError[key] = value
}
error = newError
}
// OError integration // OError integration
extra.info = OError.getFullInfo(error) extra.info = error.info
delete error.info
// filter paths from the message to avoid duplicate errors in sentry
// (e.g. errors from `fs` methods which have a path attribute)
try { try {
if (error.path) {
error.message = error.message.replace(` '${error.path}'`, '')
}
// send the error to sentry // send the error to sentry
this.Sentry.captureException(error, { tags, extra, level }) this.Sentry.captureException(error, { tags, extra, level })

View file

@ -4,13 +4,20 @@ function errSerializer(err) {
if (!err) { if (!err) {
return err return err
} }
let message = err.message
if (err.path) {
// filter paths from the message to avoid duplicate errors with different path in message
// (e.g. errors from `fs` methods which have a path attribute)
message = message.replace(` '${err.path}'`, '')
}
return { return {
message: err.message, message,
name: err.name, name: err.name,
stack: OError.getFullStack(err), stack: OError.getFullStack(err),
info: OError.getFullInfo(err), info: OError.getFullInfo(err),
code: err.code, code: err.code,
signal: err.signal, signal: err.signal,
path: err.path,
} }
} }

View file

@ -104,6 +104,116 @@ describe('SentryManager', function () {
) )
}) })
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',
}),
},
})
)
expect(this.Sentry.captureException.args[0][0]).to.deep.equal(expectedErr)
})
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 () { describe('reportedToSentry', function () {
it('should mark the error as reported to sentry', function () { it('should mark the error as reported to sentry', function () {
const err = new Error() const err = new Error()