forked from getsentry/sentry-javascript
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add an integration scrubbing event parameters
relates getsentry#2391
- Loading branch information
Showing
3 changed files
with
244 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; | ||
import { isPlainObject, isRegExp } from '@sentry/utils'; | ||
|
||
/** JSDoc */ | ||
interface ScrubOptions { | ||
sanitizeKeys: Array<string | RegExp>; | ||
} | ||
|
||
/** JSDoc */ | ||
export class Scrub implements Integration { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
public name: string = Scrub.id; | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public static id: string = 'Scrub'; | ||
|
||
/** JSDoc */ | ||
private readonly _options: ScrubOptions; | ||
private readonly _sanitizeMask: string; | ||
private _lazySanitizeRegExp?: RegExp; | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public constructor(options: ScrubOptions) { | ||
this._options = { | ||
sanitizeKeys: [], | ||
...options, | ||
}; | ||
this._sanitizeMask = '********'; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { | ||
addGlobalEventProcessor((event: Event, _hint?: EventHint) => { | ||
const self = getCurrentHub().getIntegration(Scrub); | ||
if (self) { | ||
return self.process(event); | ||
} | ||
return event; | ||
}); | ||
} | ||
|
||
/** JSDoc */ | ||
public process(event: Event): Event { | ||
if (this._options.sanitizeKeys.length === 0) { | ||
// nothing to sanitize | ||
return event; | ||
} | ||
|
||
return this._sanitize(event) as Event; | ||
} | ||
|
||
/** | ||
* lazily generate regexp | ||
*/ | ||
private _sanitizeRegExp(): RegExp { | ||
if (this._lazySanitizeRegExp) { | ||
return this._lazySanitizeRegExp; | ||
} | ||
|
||
const sources = this._options.sanitizeKeys.reduce( | ||
(acc, key) => { | ||
if (typeof key === 'string') { | ||
// escape string value | ||
// see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping | ||
acc.push(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); | ||
} else if (isRegExp(key)) { | ||
acc.push(key.source); | ||
} | ||
return acc; | ||
}, | ||
[] as string[], | ||
); | ||
|
||
return (this._lazySanitizeRegExp = RegExp(sources.join('|'), 'i')); | ||
} | ||
|
||
/** | ||
* sanitize event data recursively | ||
*/ | ||
private _sanitize(input: unknown): unknown { | ||
if (Array.isArray(input)) { | ||
return input.map(value => this._sanitize(value)); | ||
} | ||
|
||
if (isPlainObject(input)) { | ||
const inputVal = input as { [key: string]: unknown }; | ||
return Object.keys(inputVal).reduce<Record<string, unknown>>((acc, key) => { | ||
acc[key] = this._sanitizeRegExp().test(key) ? this._sanitizeMask : this._sanitize(inputVal[key]); | ||
return acc; | ||
}, {}); | ||
} | ||
return input; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { Scrub } from '../src/scrub'; | ||
|
||
/** JSDoc */ | ||
function clone<T>(data: T): T { | ||
return JSON.parse(JSON.stringify(data)); | ||
} | ||
|
||
let scrub: Scrub; | ||
const sanitizeMask = '********'; | ||
const messageEvent = { | ||
fingerprint: ['MrSnuffles'], | ||
message: 'PickleRick', | ||
stacktrace: { | ||
frames: [ | ||
{ | ||
colno: 1, | ||
filename: 'filename.js', | ||
function: 'function', | ||
lineno: 1, | ||
}, | ||
{ | ||
colno: 2, | ||
filename: 'filename.js', | ||
function: 'function', | ||
lineno: 2, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
describe('Scrub', () => { | ||
describe('sanitizeKeys option is empty', () => { | ||
beforeEach(() => { | ||
scrub = new Scrub({ | ||
sanitizeKeys: [], | ||
}); | ||
}); | ||
|
||
it('should not affect any changes', () => { | ||
const event = clone(messageEvent); | ||
const processedEvent = scrub.process(event); | ||
expect(processedEvent).toEqual(event); | ||
}); | ||
}); | ||
|
||
describe('sanitizeKeys option has type of string', () => { | ||
beforeEach(() => { | ||
scrub = new Scrub({ | ||
sanitizeKeys: ['message', 'filename'], | ||
}); | ||
}); | ||
|
||
it('should mask matched value in object', () => { | ||
const event = scrub.process(clone(messageEvent)); | ||
expect(event.message).toEqual(sanitizeMask); | ||
}); | ||
|
||
it('should not mask unmatched value in object', () => { | ||
const event = scrub.process(clone(messageEvent)); | ||
expect(event.fingerprint).toEqual(messageEvent.fingerprint); | ||
}); | ||
|
||
it('should mask matched value in Array', () => { | ||
const event: any = scrub.process(clone(messageEvent)); | ||
expect(event.stacktrace.frames[0].filename).toEqual(sanitizeMask); | ||
expect(event.stacktrace.frames[1].filename).toEqual(sanitizeMask); | ||
}); | ||
|
||
it('should not mask unmatched value in Array', () => { | ||
const event: any = scrub.process(clone(messageEvent)); | ||
expect(event.stacktrace.frames[0].function).toEqual(messageEvent.stacktrace.frames[0].function); | ||
expect(event.stacktrace.frames[1].function).toEqual(messageEvent.stacktrace.frames[1].function); | ||
}); | ||
}); | ||
|
||
describe('sanitizeKeys option has type of RegExp', () => { | ||
beforeEach(() => { | ||
scrub = new Scrub({ | ||
sanitizeKeys: [/^name$/], | ||
}); | ||
}); | ||
|
||
it('should mask only matched value', () => { | ||
const testEvent: any = { | ||
filename: 'to be show', | ||
name: 'do not show', | ||
}; | ||
const event: any = scrub.process(testEvent); | ||
expect(event.filename).toEqual(testEvent.filename); | ||
expect(event.name).toEqual(sanitizeMask); | ||
}); | ||
}); | ||
|
||
describe('sanitizeKeys option has mixed type of RegExp and string', () => { | ||
beforeEach(() => { | ||
scrub = new Scrub({ | ||
sanitizeKeys: [/^filename$/, 'function'], | ||
}); | ||
}); | ||
|
||
it('should mask only matched value', () => { | ||
const event: any = scrub.process(clone(messageEvent)); | ||
expect(event.stacktrace.frames[0].function).toEqual(sanitizeMask); | ||
expect(event.stacktrace.frames[1].function).toEqual(sanitizeMask); | ||
expect(event.stacktrace.frames[0].filename).toEqual(sanitizeMask); | ||
expect(event.stacktrace.frames[1].filename).toEqual(sanitizeMask); | ||
}); | ||
|
||
it('should not mask unmatched value', () => { | ||
const event: any = scrub.process(clone(messageEvent)); | ||
expect(event.stacktrace.frames[0].colno).toEqual(messageEvent.stacktrace.frames[0].colno); | ||
expect(event.stacktrace.frames[1].colno).toEqual(messageEvent.stacktrace.frames[1].colno); | ||
expect(event.stacktrace.frames[0].lineno).toEqual(messageEvent.stacktrace.frames[0].lineno); | ||
expect(event.stacktrace.frames[1].lineno).toEqual(messageEvent.stacktrace.frames[1].lineno); | ||
}); | ||
}); | ||
|
||
describe('event has circular objects', () => { | ||
beforeEach(() => { | ||
scrub = new Scrub({ | ||
sanitizeKeys: [/message/], | ||
}); | ||
}); | ||
|
||
it('should not show call stack size exceeded', () => { | ||
const event: any = { | ||
contexts: {}, | ||
extra: { | ||
message: 'do not show', | ||
}, | ||
}; | ||
event.contexts.circular = event.contexts; | ||
|
||
const actual = scrub.process(event); | ||
expect(actual).toEqual({ | ||
contexts: { circular: '[Circular ~]' }, | ||
extra: { message: sanitizeMask }, | ||
}); | ||
}); | ||
}); | ||
}); |