Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

feat(error): an idea for simple stack #693

Merged
merged 2 commits into from
Apr 10, 2017
Merged
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
81 changes: 63 additions & 18 deletions lib/zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,11 +1664,12 @@ const Zone: ZoneType = (function(global: any) {
case FrameType.transition:
if (zoneFrame.parent) {
// This is the special frame where zone changed. Print and process it accordingly
frames[i] += ` [${zoneFrame.parent.zone.name} => ${zoneFrame.zone.name}]`;
zoneFrame = zoneFrame.parent;
} else {
zoneFrame = null;
}
frames.splice(i, 1);
i--;
break;
default:
frames[i] += ` [${zoneFrame.zone.name}]`;
Expand Down Expand Up @@ -1775,17 +1776,11 @@ const Zone: ZoneType = (function(global: any) {
});

// Now we need to populate the `blacklistedStackFrames` as well as find the
// run/runGuraded/runTask frames. This is done by creating a detect zone and then threading
// run/runGuarded/runTask frames. This is done by creating a detect zone and then threading
// the execution through all of the above methods so that we can look at the stack trace and
// find the frames of interest.
let detectZone: Zone = Zone.current.fork({
name: 'detect',
onInvoke: function(
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
applyThis: any, applyArgs: any[], source: string): any {
// Here only so that it will show up in the stack frame so that it can be black listed.
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
},
onHandleError: function(parentZD: ZoneDelegate, current: Zone, target: Zone, error: any):
boolean {
if (error.originalStack && Error === ZoneAwareError) {
Expand Down Expand Up @@ -1834,17 +1829,67 @@ const Zone: ZoneType = (function(global: any) {
// carefully constructor a stack frame which contains all of the frames of interest which
// need to be detected and blacklisted.

// carefully constructor a stack frame which contains all of the frames of interest which
// need to be detected and blacklisted.
let detectRunFn = () => {
detectZone.run(() => {
detectZone.runGuarded(() => {
throw new (ZoneAwareError as any)(ZoneAwareError, NativeError);
});
const childDetectZone = detectZone.fork({
name: 'child',
onScheduleTask: function(delegate, curr, target, task) {
return delegate.scheduleTask(target, task);
},
onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
return delegate.invokeTask(target, task, applyThis, applyArgs);
},
onCancelTask: function(delegate, curr, target, task) {
return delegate.cancelTask(target, task);
},
onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs, source) {
return delegate.invoke(target, callback, applyThis, applyArgs, source);
}
});

// we need to detect all zone related frames, it will
// exceed default stackTraceLimit, so we set it to
// larger number here, and restore it after detect finish.
const originalStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 100;
// we schedule event/micro/macro task, and invoke them
// when onSchedule, so we can get all stack traces for
// all kinds of tasks with one error thrown.
childDetectZone.run(() => {
childDetectZone.runGuarded(() => {
const fakeTransitionTo =
(toState: TaskState, fromState1: TaskState, fromState2: TaskState) => {};
childDetectZone.scheduleEventTask(
blacklistedStackFramesSymbol,
() => {
childDetectZone.scheduleMacroTask(
blacklistedStackFramesSymbol,
() => {
childDetectZone.scheduleMicroTask(
blacklistedStackFramesSymbol,
() => {
throw new (ZoneAwareError as any)(ZoneAwareError, NativeError);
},
null,
(t: Task) => {
(t as any)._transitionTo = fakeTransitionTo;
t.invoke();
});
},
null,
(t) => {
(t as any)._transitionTo = fakeTransitionTo;
t.invoke();
},
() => {});
},
null,
(t) => {
(t as any)._transitionTo = fakeTransitionTo;
t.invoke();
},
() => {});
});
};
// Cause the error to extract the stack frames.
detectZone.runTask(detectZone.scheduleMacroTask('detect', detectRunFn, null, () => null, null));
});
Error.stackTraceLimit = originalStackTraceLimit;

return global['Zone'] = Zone;
})(typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global);
153 changes: 151 additions & 2 deletions test/common/Error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,161 @@ describe('ZoneAwareError', () => {
expect(outsideFrames[0]).toMatch(/testFn.*[<root>]/);

expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
expect(insideFrames[2]).toMatch(/testFn.*[<root>]]/);
expect(insideFrames[1]).toMatch(/testFn.*[<root>]]/);

expect(outsideWithoutNewFrames[0]).toMatch(/testFn.*[<root>]/);

expect(insideWithoutNewFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
expect(insideWithoutNewFrames[2]).toMatch(/testFn.*[<root>]]/);
expect(insideWithoutNewFrames[1]).toMatch(/testFn.*[<root>]]/);
}
});

const zoneAwareFrames = [
'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask',
'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask',
'ZoneDelegate.invokeTask', 'zoneAwareAddListener'
];

function assertStackDoesNotContainZoneFrames(err: Error) {
const frames = err.stack.split('\n');
for (let i = 0; i < frames.length; i++) {
expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1)).toEqual([]);
}
};

const errorZoneSpec = {
name: 'errorZone',
done: <() => void>null,
onHandleError:
(parentDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => {
assertStackDoesNotContainZoneFrames(error);
setTimeout(() => {
errorZoneSpec.done && errorZoneSpec.done();
}, 0);
return false;
}
};

const errorZone = Zone.root.fork(errorZoneSpec);

const assertStackDoesNotContainZoneFramesTest = function(testFn: Function) {
return function(done: () => void) {
errorZoneSpec.done = done;
errorZone.run(testFn);
};
};

describe('Error stack', () => {
it('Error with new which occurs in setTimeout callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
setTimeout(() => {
throw new Error('timeout test error');
}, 10);
}));

it('Error without new which occurs in setTimeout callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
setTimeout(() => {
throw Error('test error');
}, 10);
}));

it('Error with new which cause by promise rejection should not have zone frames visible',
(done) => {
const p = new Promise((resolve, reject) => {
reject(new Error('test error'));
});
p.catch(err => {
assertStackDoesNotContainZoneFrames(err);
done();
});
});

it('Error without new which cause by promise rejection should not have zone frames visible',
(done) => {
const p = new Promise((resolve, reject) => {
reject(Error('test error'));
});
p.catch(err => {
assertStackDoesNotContainZoneFrames(err);
done();
});
});

it('Error with new which occurs in eventTask callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.scheduleEventTask('errorEvent', () => {
throw new Error('test error');
}, null, () => null, null);
task.invoke();
}));

it('Error without new which occurs in eventTask callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.scheduleEventTask('errorEvent', () => {
throw Error('test error');
}, null, () => null, null);
task.invoke();
}));

it('Error with new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
.scheduleEventTask('errorEvent', () => {
throw new Error('test error');
}, null, () => null, null);
task.invoke();
}));

it('Error without new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
.scheduleEventTask('errorEvent', () => {
throw Error('test error');
}, null, () => null, null);
task.invoke();
}));

it('stack frames of the callback in user customized zoneSpec should be kept',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
.fork({
name: 'customZone',
onScheduleTask: (parentDelegate, currentZone, targetZone, task) => {
return parentDelegate.scheduleTask(targetZone, task);
},
onHandleError: (parentDelegate, currentZone, targetZone, error) => {
parentDelegate.handleError(targetZone, error);
const containsCustomZoneSpecStackTrace =
error.stack.indexOf('onScheduleTask') !== -1;
expect(containsCustomZoneSpecStackTrace).toBeTruthy();
return false;
}
})
.scheduleEventTask('errorEvent', () => {
throw new Error('test error');
}, null, () => null, null);
task.invoke();
}));

it('should be able to generate zone free stack even NativeError stack is readonly', function() {
const _global: any =
typeof window === 'object' && window || typeof self === 'object' && self || global;
const NativeError = _global['__zone_symbol__Error'];
const desc = Object.getOwnPropertyDescriptor(NativeError.prototype, 'stack');
if (desc) {
const originalSet: (value: any) => void = desc.set;
// make stack readonly
desc.set = null;

try {
const error = new Error('test error');
expect(error.stack).toBeTruthy();
assertStackDoesNotContainZoneFrames(error);
} finally {
desc.set = originalSet;
}
}
});
});
});