Skip to content

If Sentry responds with 413 request too large, try to send the exception again without any state #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
built/
coverage/
vendor/
5 changes: 3 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
],
"rules": {
"prettier/prettier": "error",
"prefer-arrow-callback": "error"
"prefer-arrow-callback": "error",
"prefer-reflect": "off",
},
"parserOptions": {
"ecmaVersion": 8,
Expand All @@ -20,4 +21,4 @@
"node": true,
"es6": true
}
}
}
59 changes: 59 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const stringify = require("./vendor/json-stringify-safe/stringify");

const identity = x => x;
const getUndefined = () => {};
const filter = () => true;
Expand Down Expand Up @@ -32,6 +34,63 @@ function createRavenMiddleware(Raven, options = {}) {
return original ? original(data) : data;
});

const retryCaptureWithoutReduxState = (errorMessage, captureFn) => {
Raven.setDataCallback((data, originalCallback) => {
Raven.setDataCallback(originalCallback);
const reduxExtra = {
lastAction: actionTransformer(lastAction),
state: errorMessage
};
data.extra = Object.assign(reduxExtra, data.extra);
data.breadcrumbs.values = [];
return data;
});
// Raven has an internal check for duplicate errors that we need to disable.
const originalAllowDuplicates = Raven._globalOptions.allowDuplicates;
Raven._globalOptions.allowDuplicates = true;
captureFn();
Raven._globalOptions.allowDuplicates = originalAllowDuplicates;
};

const retryWithoutStateIfRequestTooLarge = originalFn => {
return (...captureArguments) => {
const originalTransport = Raven._globalOptions.transport;
Raven.setTransport(opts => {
Raven.setTransport(originalTransport);
const requestBody = stringify(opts.data);
if (requestBody.length > 200000) {
// We know the request is too large, so don't try sending it to Sentry.
// Retry the capture function, and don't include the state this time.
const errorMessage =
"Could not send state because request would be larger than 200KB. " +
`(Was: ${requestBody.length}B)`;
retryCaptureWithoutReduxState(errorMessage, () => {
originalFn.apply(Raven, captureArguments);
});
return;
}
opts.onError = error => {
if (error.request && error.request.status === 413) {
const errorMessage =
"Failed to submit state to Sentry: 413 request too large.";
retryCaptureWithoutReduxState(errorMessage, () => {
originalFn.apply(Raven, captureArguments);
});
}
};
(originalTransport || Raven._makeRequest).call(Raven, opts);
});
originalFn.apply(Raven, captureArguments);
};
};

Raven.captureException = retryWithoutStateIfRequestTooLarge(
Raven.captureException
);
Raven.captureMessage = retryWithoutStateIfRequestTooLarge(
Raven.captureMessage
);

return next => action => {
// Log the action taken to Raven so that we have narrative context in our
// error report.
Expand Down
76 changes: 72 additions & 4 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ const Raven = require("raven-js");
const createRavenMiddleware = require("./index");
const { createStore, applyMiddleware } = require("redux");

Raven.config("https://[email protected]/146969", {
allowDuplicates: true
}).install();
const stringify = require.requireActual(
"./vendor/json-stringify-safe/stringify"
);
const stringifyMocked = require("./vendor/json-stringify-safe/stringify");
jest.mock("./vendor/json-stringify-safe/stringify");

Raven.config(
"https://[email protected]/146969"
).install();

const reducer = (previousState = { value: 0 }, action) => {
switch (action.type) {
Expand All @@ -27,12 +33,13 @@ const context = {};

describe("raven-for-redux", () => {
beforeEach(() => {
stringifyMocked.mockImplementation(obj => stringify(obj));
context.mockTransport = jest.fn();
Raven.setTransport(context.mockTransport);
Raven.setDataCallback(undefined);
Raven.setBreadcrumbCallback(undefined);
Raven.setUserContext(undefined);

Raven._globalOptions.allowDuplicates = true;
Raven._breadcrumbs = [];
Raven._globalContext = {};
});
Expand Down Expand Up @@ -186,6 +193,67 @@ describe("raven-for-redux", () => {
userData
);
});

["captureException", "captureMessage"].forEach(fnName => {
it(`skips state for ${fnName} if request would be larger than 200000B`, () => {
expect(Raven._globalOptions.transport).toEqual(context.mockTransport);
stringifyMocked
.mockClear()
.mockImplementationOnce(() => ({ length: 200001 }))
.mockImplementationOnce(() => ({ length: 500 }));
// Test that allowDuplicates is set to true inside our handler and reset afterwards
// (Error message needs to be unique for each test, because we set allowDuplicates to null)
Raven._globalOptions.allowDuplicates = null;
Raven[fnName].call(
Raven,
fnName === "captureException"
? new Error("Test skip state")
: "Test skip state"
);

// Ensure transport and allowDuplicates have been reset
expect(Raven._globalOptions.transport).toEqual(context.mockTransport);
expect(Raven._globalOptions.allowDuplicates).toEqual(null);
expect(context.mockTransport).toHaveBeenCalledTimes(1);
const { extra } = context.mockTransport.mock.calls[0][0].data;
expect(extra).toMatchObject({
state:
"Could not send state because request would be larger than 200KB. (Was: 200001B)",
lastAction: undefined
});
});

it(`retries ${fnName} without any state if Sentry returns 413 request too large`, () => {
expect(Raven._globalOptions.transport).toEqual(context.mockTransport);
context.mockTransport.mockImplementationOnce(options => {
options.onError({ request: { status: 413 } });
});
// Test that allowDuplicates is set to true inside our handler and reset afterwards
// (Error message needs to be unique for each test, because we set allowDuplicates to null)
Raven._globalOptions.allowDuplicates = null;
Raven[fnName].call(
Raven,
fnName === "captureException"
? new Error("Test retry on 413 error")
: "Test retry on 413 error"
);

// Ensure transport and allowDuplicates have been reset
expect(Raven._globalOptions.transport).toEqual(context.mockTransport);
expect(Raven._globalOptions.allowDuplicates).toEqual(null);
expect(context.mockTransport).toHaveBeenCalledTimes(2);
const { extra } = context.mockTransport.mock.calls[0][0].data;
expect(extra).toMatchObject({
state: { value: 0 },
lastAction: undefined
});
const { extra: extra2 } = context.mockTransport.mock.calls[1][0].data;
expect(extra2).toMatchObject({
state: "Failed to submit state to Sentry: 413 request too large.",
lastAction: undefined
});
});
});
});
describe("with all the options enabled", () => {
beforeEach(() => {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"scripts": {
"fix": "eslint --fix .",
"tdd": "jest --watch",
"test": "jest --coverage && npm run lint",
"test": "jest --coverage --collectCoverageFrom=index.js && npm run lint",
"lint": "eslint .",
"build": "babel index.js --out-dir built/",
"build": "babel index.js vendor/json-stringify-safe/stringify.js --out-dir built/",
"prepublishOnly": "npm run test && npm run build",
"serve-example": "webpack-dev-server --watch --config=example/webpack.config.js"
},
Expand Down
76 changes: 76 additions & 0 deletions vendor/json-stringify-safe/stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
json-stringify-safe
Like JSON.stringify, but doesn't throw on circular references.

Copied from sentry-javascript/packages/raven-js/vendor/json-stringify-safe/stringify.js

Originally forked from https://github.com/isaacs/json-stringify-safe
version 5.0.1 on 3/8/2017 and modified to handle Errors serialization
and IE8 compatibility. Tests for this are in test/vendor.

ISC license: https://github.com/isaacs/json-stringify-safe/blob/master/LICENSE
*/

exports = module.exports = stringify;
exports.getSerialize = serializer;

function indexOf(haystack, needle) {
for (var i = 0; i < haystack.length; ++i) {
if (haystack[i] === needle) return i;
}
return -1;
}

function stringify(obj, replacer, spaces, cycleReplacer) {
return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces);
}

// https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106
function stringifyError(value) {
var err = {
// These properties are implemented as magical getters and don't show up in for in
stack: value.stack,
message: value.message,
name: value.name
};

for (var i in value) {
if (Object.prototype.hasOwnProperty.call(value, i)) {
err[i] = value[i];
}
}

return err;
}

function serializer(replacer, cycleReplacer) {
var stack = [];
var keys = [];

if (cycleReplacer == null) {
cycleReplacer = function(key, value) {
if (stack[0] === value) {
return '[Circular ~]';
}
return '[Circular ~.' + keys.slice(0, indexOf(stack, value)).join('.') + ']';
};
}

return function(key, value) {
if (stack.length > 0) {
var thisPos = indexOf(stack, this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);

if (~indexOf(stack, value)) {
value = cycleReplacer.call(this, key, value);
}
} else {
stack.push(value);
}

return replacer == null
? value instanceof Error ? stringifyError(value) : value
: replacer.call(this, key, value);
};
}