-
Notifications
You must be signed in to change notification settings - Fork 34
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
feat(data-point): Add reducer stack when reducers throw errors #230
feat(data-point): Add reducer stack when reducers throw errors #230
Conversation
const value = accumulator.value | ||
const result = Promise.try(() => getResolveFunction(reducer)) | ||
// NOTE: recursive call | ||
.then(resolve => resolve(manager, resolveReducer, accumulator, reducer)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrapping getResolveFunction
so that resolveReducer
will always return a promise, and errors thrown from this function will always have the _stack
property
function resolveRequest (accumulator, resolveReducer) { | ||
inspect(accumulator) | ||
const acc = utils.set(accumulator, 'value', accumulator.options) | ||
return resolveReducer(acc, requestReducer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gets rid of all the custom properties that were added to the error object here. My reasoning for this change is that I'm now adding error._input
and error._stack
properties that have a lot of the same information, and most of the other properties are just repeating data that's already available in the original error. Now that we're no longer doing console.info
with the error message (#210), it seems less important to have this code.
packages/data-point/README.md
Outdated
error._value, | ||
'foo' | ||
) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wondering if this property should be _input
instead of _value
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking the same thing. input
better matches what this actually is (the input
provided to the failing reducer). The description you wrote supports that idea too:
When a reducer throws an error, DataPoint adds two properties to the error object:
`_value:*` - the input value to the reducer that failed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -23,7 +23,8 @@ function resolveErrorReducers (manager, error, accumulator, resolveReducer) { | |||
const reducerResolved = resolveReducer( | |||
manager, | |||
errorAccumulator, | |||
errorReducer | |||
errorReducer, | |||
[['error']] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the nested array forces stringifyReducerStack
to format the output string in a particular way, and I'm using this approach for most of the entity lifecycle reducers. Don't really like it though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming this is kept it might be worth adding a comment about why it's implemented this way.
This allows for easier reducer stack traces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome! And the new tests you wrote for the stack tracing are 💯! 👏 👏 👏
Left a few comments, and I believe most are questions not requests. I would like to have some comment in there about that nested array being used. The other comments I left could just be discussed first.
Fantastic work!
packages/data-point/README.md
Outdated
|
||
When a reducer throws an error, DataPoint adds two properties to the error object: | ||
|
||
`_value:*` - the input value to the reducer that failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-reading this, changing this to _input
makes more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
expect(result).toHaveProperty('_value') | ||
expect(result).toHaveProperty('_stack') | ||
expect(result._value).toMatchSnapshot() | ||
expect(result._stack).toMatchSnapshot() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we want to use toThrowErrorMatchingSnapshot()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that work with promises?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it doesn't.
We can do this though, which should write the whole object:
async function testError (reducer, input) {
await expect(dataPoint.resolve(reducer, input)).rejects.toMatchSnapshot()
}
However this changes the snapshots dramatically, and it looks like we lose some critical information stored in the snapshots. So it might not be a good idea to change this after all.
Related thread: jestjs/jest#3601
} | ||
} | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely see the value of defining these entities here, but it's not the same pattern as the other entity fixtures we have (in the test directory).
I'm not asking this to be changed or moved, just commenting in case there's interest in discussing the inclusion of this in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like defining them in the same file so it's easier to understand the tests, and since these entities are testing very specific cases, I doubt they would ever be used in other files.
If we do use the same entities in multiple tests (which is encouraged by defining them in the test directory), it could also make it harder to make changes to the entities if we ever need to, since any change would affect multiple tests.
Lastly, if every instance of datapoint that we create for the tests contains all if the entities defined in the test directory, that might slow down the tests (since we probably create a lot of instances).
dataPoint.addEntities(schemaA10) | ||
|
||
describe('transform entity stack traces', () => { | ||
test('transform:1', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The descriptions could use a little improvement (even though I expect they'll all say something similar). Just thinking that if this is run with --verbose
there's no additional information from the descriptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went through and added better descriptions; please take a look when you have a chance
racc = utils.set(acc, 'value', _.defaultTo(acc.value, {})) | ||
return resolveReducer(racc, contextTransform) | ||
const racc = utils.set(acc, 'value', _.defaultTo(acc.value, {})) | ||
return resolveReducer(racc, contextTransform, [['value']]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These nested arrays are in a few places. I'm still not exactly sure why, tbh. What's the format that is produced and why is it being done that way? (Sorry if I'm overlooking something obvious.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The format can be seen by looking at the _stack strings in the snapshots.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The values in the nested arrays will be printed with brackets and no preceding whitespace, so that we get things like 'entry:example[value]'
(and the strings are created by the stringifyReducerStack
function)
@@ -1,3 +1,5 @@ | |||
const Promise = require('bluebird') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a required change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added this because I was getting an error when trying to use Promise.tap
in the main reducer resolve function - I'm no longer using tap
, but left this in
stack = castArray(key) | ||
} | ||
|
||
if (reducer.type === 'ReducerFunction') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might this be better as a switch
? I don't know what other types we might have a case for, but at a certain point if..else if..else
gets cumbersome to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably won't need more conditions here, since I don't think we'll be adding many more reducer types. Honestly I just really dislike switch syntax...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly I just really dislike switch syntax...
Ah ha!!!! 😄
But for real, if we're not going to extend it then a switch isn't really necessary I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to change it though if there's a consensus...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be premature to change it now if we only have to account for these two scenarios.
.catch(e => e) | ||
.then(result => { | ||
expect(result).toBeInstanceOf(Error) | ||
expect(result).toMatchSnapshot() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we want to use toThrowErrorMatchingSnapshot()
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for record keeping, we agreed not to do this because it's using a promise instead of a synchronous function.
*/ | ||
function getMatchingCaseStatement (caseStatements, acc, resolveReducer) { | ||
function getMatchingCaseIndex (caseStatements, accumulator, resolveReducer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returning the index instead of the statement, so that we can add the index to the reducer stack
Codecov Report
@@ Coverage Diff @@
## master #230 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 129 130 +1
Lines 1700 1737 +37
Branches 149 160 +11
=====================================
+ Hits 1700 1737 +37
Continue to review full report at Codecov.
|
What: closes #149 and closes #160 . I opened a previous PR for this issue, but that implementation was (probably) less efficient than this one.
Why: Helps with debugging
How: Add
_stack
and_input
properties to error objects. It doesn't log anything though - leaving that to the user.Instead of trying to create a tree data structure, this relies on the fact that
resolveReducer
is called recursively. That function generates a promise with acatch
handler that re-throws the error, so the handler is called for every reducer in a given chain. So, if we have a reducer chaina -> b -> c
that throws an error forc
, each reducer has a chance to add data to the error object:This is the sequence for that chain:
c
-> creates a stack array:error._stack = ['c']
b
-> updates the array:error._stack = ['b', 'c']
a
-> updates the array:error._stack = ['a', 'b', 'c']
This means the final error object has a
_stack
property that records an id for each reducer in the chain that failed. That's the basic idea anyway --resolveReducer
also now takes akey
parameter to add extra metadata about the reducer that's being resolved. For example, if we're resolving aReducerMap
on a given array, we need to know what index of the array was being resolved when the reducer failed. In that example, thekey
parameter would be the current index of the input array, and it would be added to theerror._stack
array.NOTE: The stack traces do not working correctly for entities with the
[]
notation (e.g.hash:thing[]
); specifically, it does not indicate what index of the array was being evaluated when the error was thrown. This would be fixed if we do #130.Checklist: