Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ Those configuration options are documented below:
sampleRate: 0.5 // send 50% of events, drop the other half
}

.. describe:: sanitizeKeys

An array of strings representing keys that should be scrubbed from the payload sent to Sentry.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(worth being explicit here that this doesn't scrub or match on string values or query encoded data)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also worth mentioning that sentry itself can do do server side sanitizing and this is different

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically i think people are really gonna want to know what guarantees they get from this feature so it's worth writing a longer explanation


.. code-block:: javascript

{
sanitizeKeys: ['token', 'userPassword', 'csrf_token']
}


.. describe:: dataCallback

A function that allows mutation of the data payload right before being
Expand Down
11 changes: 9 additions & 2 deletions src/raven.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var supportsFetch = utils.supportsFetch;
var supportsReferrerPolicy = utils.supportsReferrerPolicy;
var serializeKeysForMessage = utils.serializeKeysForMessage;
var serializeException = utils.serializeException;
var sanitize = utils.sanitize;

var wrapConsoleMethod = require('./console').wrapMethod;

Expand Down Expand Up @@ -85,13 +86,13 @@ function Raven() {
collectWindowErrors: true,
captureUnhandledRejections: true,
maxMessageLength: 0,

// By default, truncates URL values to 250 chars
maxUrlLength: 250,
stackTraceLimit: 50,
autoBreadcrumbs: true,
instrument: true,
sampleRate: 1
sampleRate: 1,
sanitizeKeys: []
};
this._fetchDefaults = {
method: 'POST',
Expand Down Expand Up @@ -1865,6 +1866,8 @@ Raven.prototype = {
// Include server_name if it's defined in globalOptions
if (globalOptions.serverName) data.server_name = globalOptions.serverName;

data = this._sanitizeData(data);

// Cleanup empty properties before sending them to the server
Object.keys(data).forEach(function(key) {
if (data[key] == null || data[key] === '' || isEmptyObject(data[key])) {
Expand Down Expand Up @@ -1905,6 +1908,10 @@ Raven.prototype = {
}
},

_sanitizeData: function(data) {
return sanitize(data, this._globalOptions.sanitizeKeys);
},

_getUuid: function() {
return uuid4();
},
Expand Down
27 changes: 26 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,30 @@ function serializeKeysForMessage(keys, maxLength) {
return '';
}

function sanitize(input, sanitizeKeys) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this deal with cycles? or will the data be guaranteed by this point not to have any? how strong is that guarantee? (afraid of freezing a browser in some weird scenario)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't. Thanks for catching that!

sanitizeKeys = isArray(sanitizeKeys) ? sanitizeKeys : [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's not an array, maybe just bail out here and return array? to reduce the risk involved in shipping this code?

var sanitizeMask = '********';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know we use asterisks elsewhere, but i think something explicit like [value sanitized by raven.js]could be better.

probably not worth breaking with the precedent though


if (isArray(input)) {
return input.map(function(val) {
return sanitize(val, sanitizeKeys);
});
}

if (isPlainObject(input)) {
return Object.keys(input).reduce(function(acc, k) {
if (sanitizeKeys.indexOf(k) > -1) {
acc[k] = sanitizeMask;
} else {
acc[k] = sanitize(input[k], sanitizeKeys);
}
return acc;
}, {});
}

return input;
}

module.exports = {
isObject: isObject,
isError: isError,
Expand Down Expand Up @@ -566,5 +590,6 @@ module.exports = {
fill: fill,
safeJoin: safeJoin,
serializeException: serializeException,
serializeKeysForMessage: serializeKeysForMessage
serializeKeysForMessage: serializeKeysForMessage,
sanitize: sanitize
};
27 changes: 27 additions & 0 deletions test/raven.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,33 @@ describe('globals', function() {
assert.isTrue(Raven._send.calledOnce);
});

it('should respect `sanitizeKeys`', function() {
this.sinon.stub(Raven, '_sendProcessedPayload');
Raven._globalOptions.sanitizeKeys = ['password', 'token'];
Raven.captureMessage('hello', {
extra: {
password: 'foo',
token: 'abc',
user: 'rick'
},
user: {
password: 'foo'
}
});

// It's not the main thing we test here and it's a variable with every run
delete Raven._sendProcessedPayload.lastCall.args[0].extra['session:duration'];

assert.deepEqual(Raven._sendProcessedPayload.lastCall.args[0].extra, {
password: '********',
token: '********',
user: 'rick'
});
assert.deepEqual(Raven._sendProcessedPayload.lastCall.args[0].user, {
password: '********'
});
});

it('should send a proper payload with frames', function() {
this.sinon.stub(Raven, '_send');

Expand Down
63 changes: 63 additions & 0 deletions test/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var parseUrl = utils.parseUrl;
var safeJoin = utils.safeJoin;
var serializeException = utils.serializeException;
var serializeKeysForMessage = utils.serializeKeysForMessage;
var sanitize = utils.sanitize;

describe('utils', function() {
describe('isUndefined', function() {
Expand Down Expand Up @@ -696,4 +697,66 @@ describe('utils', function() {
assert.equal(serializeKeysForMessage('foo'), 'foo');
});
});

describe('sanitize', function() {
var sanitizeMask = '********';

it('should return simple values directly', function() {
var actual = sanitize('foo');
var expected = 'foo';
assert.deepEqual(actual, expected);
});

it('should return same value when no sanitizeKeys passed', function() {
var actual = sanitize({foo: 42});
var expected = {foo: 42};
assert.deepEqual(actual, expected);
});

it('should return same value when empty sanitizeKeys array passed', function() {
var actual = sanitize({foo: 42}, []);
var expected = {foo: 42};
assert.deepEqual(actual, expected);
});

it('should sanitize flat objects', function() {
var actual = sanitize({foo: 42}, ['foo']);
var expected = {foo: sanitizeMask};
assert.deepEqual(actual, expected);
});

it('should sanitize flat objects with multiple keys', function() {
var actual = sanitize({foo: 42, bar: 'abc', baz: 1337}, ['foo', 'baz']);
var expected = {foo: sanitizeMask, bar: 'abc', baz: sanitizeMask};
assert.deepEqual(actual, expected);
});

it('should sanitize flat objects when value is a plain object or array', function() {
var actual = sanitize({foo: {bar: 42}}, ['foo']);
var expected = {foo: sanitizeMask};
assert.deepEqual(actual, expected);

actual = sanitize({foo: [42, 'abc']}, ['foo']);
expected = {foo: sanitizeMask};
assert.deepEqual(actual, expected);
});

it('should sanitize nested objects keys', function() {
var actual = sanitize({foo: {bar: 42}}, ['bar']);
var expected = {foo: {bar: sanitizeMask}};
assert.deepEqual(actual, expected);
});

it('should sanitize objects nested in arrays', function() {
var actual = sanitize({foo: [{bar: 42}, 42]}, ['bar']);
var expected = {foo: [{bar: sanitizeMask}, 42]};
assert.deepEqual(actual, expected);
});

it('should sanitize every object when array provided as input', function() {
var actual = sanitize([{foo: 42}, {bar: 42}, 42], ['foo', 'bar']);
var expected = [{foo: sanitizeMask}, {bar: sanitizeMask}, 42];
assert.deepEqual(actual, expected);
});
});
});
105 changes: 49 additions & 56 deletions typescript/raven-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,107 +3,100 @@ import Raven = require('..'); // or import * as Raven from '..'
Raven.config('https://[email protected]/1').install();

var options: Raven.RavenOptions = {
logger: 'my-logger',
ignoreUrls: [
/graph\.facebook\.com/i,
'graph.facebook.com'
],
ignoreErrors: [
/fb_xd_fragment/,
'fb_xd_fragment'
],
includePaths: [
/https?:\/\/(www\.)?getsentry\.com/,
'https://www.sentry.io'
],
whitelistUrls: [
/https?:\/\/google\.com/,
'https://www.google.com'
],
autoBreadcrumbs: {
xhr: false,
console: false,
dom: true,
location: false,
sentry: true
},
breadcrumbCallback: function (data) {
return data
}
logger: 'my-logger',
ignoreUrls: [/graph\.facebook\.com/i, 'graph.facebook.com'],
ignoreErrors: [/fb_xd_fragment/, 'fb_xd_fragment'],
includePaths: [/https?:\/\/(www\.)?getsentry\.com/, 'https://www.sentry.io'],
whitelistUrls: [/https?:\/\/google\.com/, 'https://www.google.com'],
autoBreadcrumbs: {
xhr: false,
console: false,
dom: true,
location: false,
sentry: true
},
breadcrumbCallback: function(data) {
return data;
},
sanitizeKeys: ['token', 'userPassword', 'csrf_token']
};

Raven.config('https://[email protected]/1', options).install();

var throwsError = () => {
throw new Error('broken');
throw new Error('broken');
};

try {
throwsError();
} catch(e) {
Raven.captureException(e);
Raven.captureException(e, {tags: { key: "value" }});
throwsError();
} catch (e) {
Raven.captureException(e);
Raven.captureException(e, {tags: {key: 'value'}});
}

// ErrorEvent requires at least Typescript 2.3.0 due to incorrect ErrorEvent definitions
// prior to that version.
var throwsErrorEvent = () => {
throw new ErrorEvent('oops', {error: new Error('Oops')});
throw new ErrorEvent('oops', {error: new Error('Oops')});
};

try {
throwsErrorEvent();
} catch(ee) {
Raven.captureException(ee);
Raven.captureException(ee, {tags: { key: "value" }});
throwsErrorEvent();
} catch (ee) {
Raven.captureException(ee);
Raven.captureException(ee, {tags: {key: 'value'}});
}

Raven.captureException('Something broke');
Raven.captureException('Something broke', {tags: { key: "value" }});
Raven.captureException('Something broke', {tags: {key: 'value'}});

Raven.context(throwsError);
Raven.context({tags: { key: "value" }}, throwsError);
Raven.context({tags: {key: 'value'}}, throwsError);
Raven.context(throwsErrorEvent);
Raven.context({tags: { key: "value" }}, throwsErrorEvent);
Raven.context({tags: {key: 'value'}}, throwsErrorEvent);

setTimeout(Raven.wrap(throwsError), 1000);
Raven.wrap({logger: "my.module"}, throwsError)();
Raven.wrap({logger: 'my.module'}, throwsError)();
Raven.wrap(throwsErrorEvent)();
Raven.wrap({logger: "my.module"}, throwsErrorEvent)();
Raven.wrap({logger: 'my.module'}, throwsErrorEvent)();

Raven.setUserContext();
Raven.setUserContext({
email: '[email protected]',
id: '123'
email: '[email protected]',
id: '123'
});

Raven.setExtraContext({foo: 'bar'});
Raven.setExtraContext();
Raven.setTagsContext({env: 'prod'});
Raven.setTagsContext();
Raven.clearContext();
var obj:Object = Raven.getContext();
var err:Error = Raven.lastException();
var obj: Object = Raven.getContext();
var err: Error = Raven.lastException();

Raven.captureMessage('Broken!');
Raven.captureMessage('Broken!', {tags: { key: "value" }});
Raven.captureMessage('Broken!', { stacktrace: true });
Raven.captureMessage('Warning', { level: 'warning' });
Raven.captureMessage('Broken!', {tags: {key: 'value'}});
Raven.captureMessage('Broken!', {stacktrace: true});
Raven.captureMessage('Warning', {level: 'warning'});
Raven.captureBreadcrumb({
message: "This is a breadcrumb message."
message: 'This is a breadcrumb message.'
});
Raven.captureBreadcrumb({
category: 'console',
level: 'log',
message: 'A console.log() message'
});
Raven.captureBreadcrumb({category: "console", level: "log", message: "A console.log() message"});

Raven.setRelease('abc123');
Raven.setEnvironment('production');

Raven.setDataCallback(function (data: any) {});
Raven.setDataCallback(function (data: any, original: any) {});
Raven.setShouldSendCallback(function (data: any) {});
Raven.setShouldSendCallback(function (data: any, original: any) {});
Raven.setDataCallback(function(data: any) {});
Raven.setDataCallback(function(data: any, original: any) {});
Raven.setShouldSendCallback(function(data: any) {});
Raven.setShouldSendCallback(function(data: any, original: any) {});

Raven.showReportDialog({
eventId: 'abcdef123456'
eventId: 'abcdef123456'
});

Raven.setDSN('https://[email protected]/2');
3 changes: 3 additions & 0 deletions typescript/raven.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ declare namespace Raven {
/** In some cases you may see issues where Sentry groups multiple events together when they should be separate entities. In other cases, Sentry simply doesn’t group events together because they’re so sporadic that they never look the same. */
fingerprint?: string[];

/** An array of strings representing keys that should be scrubbed from the payload sent to Sentry */
sanitizeKeys?: string[];

/** A function which allows mutation of the data payload right before being sent to Sentry */
dataCallback?: (data: any) => any;

Expand Down