diff --git a/index.js b/index.js index de078f2..a143e00 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,45 @@ const identity = x => x; const getUndefined = () => {}; const filter = () => true; +// Include a heuristic to remove redux-undo history (https://github.com/omnidan/redux-undo) +// 'past' and 'future' are arrays that can include a large number of copies of the state. +const removeHistoryFromObject = obj => + Object.assign({}, obj, { + past: `redux-undo history was automatically removed. (Entries: ${ + obj.past.length + })`, + future: `redux-undo history was automatically removed. (Entries: ${ + obj.future.length + })` + }); +const isReduxUndoState = state => + state && + state.past && + state.present && + state.future && + typeof state.index === "number" && + typeof state.limit === "number"; +const removeReduxUndoHistoryFromState = state => { + if (!state || typeof state !== "object") return state; + if (isReduxUndoState(state)) { + return removeHistoryFromObject(state); + } + let newState = null; + Object.entries(state).forEach(([key, store]) => { + if (isReduxUndoState(store)) { + if (!newState) newState = Object.assign({}, state); + newState[key] = removeHistoryFromObject(store); + } + }); + return newState || state; +}; + function createRavenMiddleware(Raven, options = {}) { // TODO: Validate options. const { breadcrumbDataFromAction = getUndefined, actionTransformer = identity, - stateTransformer = identity, + stateTransformer = removeReduxUndoHistoryFromState, breadcrumbCategory = "redux-action", filterBreadcrumbActions = filter, getUserContext, diff --git a/index.test.js b/index.test.js index a2eb176..b2b4f62 100644 --- a/index.test.js +++ b/index.test.js @@ -6,7 +6,12 @@ Raven.config("https://5d5bf17b1bed4afc9103b5a09634775e@sentry.io/146969", { allowDuplicates: true }).install(); -const reducer = (previousState = { value: 0 }, action) => { +const defaultState = { value: 0 }; +const context = { + initialState: defaultState +}; + +const reducer = (previousState = context.initialState, action) => { switch (action.type) { case "THROW": // Raven does not seem to be able to capture global exceptions in Jest tests. @@ -23,11 +28,10 @@ const reducer = (previousState = { value: 0 }, action) => { } }; -const context = {}; - describe("raven-for-redux", () => { beforeEach(() => { context.mockTransport = jest.fn(); + context.initialState = defaultState; Raven.setTransport(context.mockTransport); Raven.setDataCallback(undefined); Raven.setBreadcrumbCallback(undefined); @@ -186,6 +190,93 @@ describe("raven-for-redux", () => { userData ); }); + describe("with redux-undo history as top-level state", () => { + beforeEach(() => { + context.initialState = { + past: [{ value: 2 }, { value: 1 }], + present: { value: 0 }, + future: [], + index: 2, + limit: 2 + }; + context.store = createStore( + reducer, + applyMiddleware(context.middleware) + ); + }); + it("replaces the past and future arrays in the state", () => { + expect(() => { + context.store.dispatch({ type: "THROW" }); + }).toThrow(); + + expect(context.mockTransport).toHaveBeenCalledTimes(1); + const { extra } = context.mockTransport.mock.calls[0][0].data; + expect(extra.state).toEqual({ + past: "redux-undo history was automatically removed. (Entries: 2)", + present: { value: 0 }, + future: "redux-undo history was automatically removed. (Entries: 0)", + index: 2, + limit: 2 + }); + }); + }); + describe("with redux-undo history as nested stores", () => { + beforeEach(() => { + context.initialState = { + fooStore: { + past: [{ value: 2 }, { value: 1 }], + present: { value: 0 }, + future: [], + index: 2, + limit: 2 + }, + barStore: { + value: 2 + } + }; + context.store = createStore( + reducer, + applyMiddleware(context.middleware) + ); + }); + it("replaces past and future arrays in any nested stores that use redux-undo", () => { + expect(() => { + context.store.dispatch({ type: "THROW" }); + }).toThrow(); + expect(context.mockTransport).toHaveBeenCalledTimes(1); + const { extra } = context.mockTransport.mock.calls[0][0].data; + expect(extra.state).toEqual({ + fooStore: { + past: "redux-undo history was automatically removed. (Entries: 2)", + present: { value: 0 }, + future: + "redux-undo history was automatically removed. (Entries: 0)", + index: 2, + limit: 2 + }, + barStore: { + value: 2 + } + }); + }); + }); + describe("with state that is not an object", () => { + beforeEach(() => { + context.initialState = 42; + context.store = createStore( + reducer, + applyMiddleware(context.middleware) + ); + }); + it("does not affect the state", () => { + expect(() => { + context.store.dispatch({ type: "THROW" }); + }).toThrow(); + expect(context.mockTransport).toHaveBeenCalledTimes(1); + const { extra } = context.mockTransport.mock.calls[0][0].data; + expect(extra.state).toEqual(42); + }); + }); }); describe("with all the options enabled", () => { beforeEach(() => {