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

Commit

Permalink
feat(error): Remove all Zone frames from stack (#693)
Browse files Browse the repository at this point in the history
* feat(error): an idea for simple stack

* merge detectZone
  • Loading branch information
JiaLiPassion authored and mhevery committed Apr 10, 2017
1 parent 06d1ac0 commit 681a017
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 19 deletions.
79 changes: 62 additions & 17 deletions lib/zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1654,11 +1654,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 @@ -1770,12 +1771,6 @@ const Zone: ZoneType = (function(global: any) {
// 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 @@ -1824,17 +1819,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;
}
}
});
});
});

0 comments on commit 681a017

Please sign in to comment.