From 858b482f5838e054a04e45af234a767191e70709 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Sun, 12 Feb 2017 02:26:50 +0900 Subject: [PATCH 1/2] feat(error): remove all zone related stack frames from error.stack --- lib/zone.ts | 160 +++++++++++++++++++++++++++++++++++--- test/common/Error.spec.ts | 147 +++++++++++++++++++++++++++++++--- test/node/fs.spec.ts | 4 +- 3 files changed, 290 insertions(+), 21 deletions(-) diff --git a/lib/zone.ts b/lib/zone.ts index aaeb47592..fc9de5608 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -1571,7 +1571,7 @@ const Zone: ZoneType = (function(global: any) { // Store the frames which should be removed from the stack frames const blackListedStackFrames: {[frame: string]: FrameType} = {}; // We must find the frame where Error was created, otherwise we assume we don't understand stack - let zoneAwareFrame: string; + let zoneAwareFrame: string[] = []; global.Error = ZoneAwareError; // How should the stack frames be parsed. let frameParserStrategy = null; @@ -1691,6 +1691,15 @@ const Zone: ZoneType = (function(global: any) { return newProps; }; + // some functions are not easily to be detected here, + // for example Timeout.ZoneTask.invoke, if we want to detect those functions + // by detect zone, we have to run all patched APIs, it is too risky + // so for those functions, just check whether the stack contains the string or not. + const otherZoneAwareFunctionNames = [ + 'ZoneTask.invoke', 'ZoneAware', 'getStacktraceWithUncaughtError', 'new LongStackTrace', + 'long-stack-trace' + ]; + /** * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as * adds zone information to it. @@ -1716,25 +1725,32 @@ const Zone: ZoneType = (function(global: any) { let zoneFrame = _currentZoneFrame; let i = 0; // Find the first frame - while (frames[i] !== zoneAwareFrame && i < frames.length) { + while (i < frames.length && zoneAwareFrame.filter(zf => zf === frames[i]).length === 0) { i++; } for (; i < frames.length && zoneFrame; i++) { - let frame = frames[i]; - if (frame.trim()) { + let frame = frames[i].trim(); + if (frame) { let frameType = blackListedStackFrames.hasOwnProperty(frame) && blackListedStackFrames[frame]; if (frameType === FrameType.blackList) { frames.splice(i, 1); i--; + } else if ( + otherZoneAwareFunctionNames + .filter(f => frame.toLowerCase().indexOf(f.toLowerCase()) !== -1) + .length > 0) { + frames.splice(i, 1); + i--; } else if (frameType === 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--; } else { frames[i] += ` [${zoneFrame.zone.name}]`; } @@ -1823,7 +1839,7 @@ const Zone: ZoneType = (function(global: any) { } }); - // Now we need to populet the `blacklistedStackFrames` as well as find the + // 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 // the execution through all of the above methods so that we can look at the stack trace and // find the frames of interest. @@ -1846,8 +1862,8 @@ const Zone: ZoneType = (function(global: any) { // This check makes sure that we don't filter frames on name only (must have // linenumber) if (/:\d+:\d+/.test(frame)) { - // Get rid of the path so that we don't accidintely find function name in path. - // In chrome the seperator is `(` and `@` in FF and safari + // Get rid of the path so that we don't accidentally find function name in path. + // In chrome the separator is `(` and `@` in FF and safari // Chrome: at Zone.run (zone.js:100) // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24) // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24 @@ -1855,7 +1871,7 @@ const Zone: ZoneType = (function(global: any) { let fnName: string = frame.split('(')[0].split('@')[0]; let frameType = FrameType.transition; if (fnName.indexOf('ZoneAwareError') !== -1) { - zoneAwareFrame = frame; + zoneAwareFrame.push(frame); } if (fnName.indexOf('runGuarded') !== -1) { runGuardedFrame = true; @@ -1866,7 +1882,7 @@ const Zone: ZoneType = (function(global: any) { } else { frameType = FrameType.blackList; } - blackListedStackFrames[frame] = frameType; + blackListedStackFrames[frame.trim()] = frameType; // Once we find all of the frames we can stop looking. if (runFrame && runGuardedFrame && runTaskFrame) { (ZoneAwareError as any)[stackRewrite] = true; @@ -1887,8 +1903,132 @@ const Zone: ZoneType = (function(global: any) { }); }); }; + let detectRunWithoutNewFn = () => { + detectZone.run(() => { + detectZone.runGuarded(() => { + throw Error('blacklistStackFrames'); + }); + }); + }; // Cause the error to extract the stack frames. detectZone.runTask(detectZone.scheduleMacroTask('detect', detectRunFn, null, () => null, null)); + detectZone.runTask( + detectZone.scheduleMacroTask('detect', detectRunWithoutNewFn, null, () => null, null)); + + function handleDetectError(error: Error) { + let frames = error.stack ? error.stack.split(/\n/) : []; + while (frames.length) { + let frame = frames.shift(); + // On safari it is possible to have stack frame with no line number. + // This check makes sure that we don't filter frames on name only (must have + // linenumber) + const trimedFrame = frame.trim().split('[')[0].trim(); + if (/:\d+:\d+/.test(trimedFrame) && !blackListedStackFrames.hasOwnProperty(trimedFrame)) { + blackListedStackFrames[trimedFrame] = FrameType.blackList; + } + + let fnName: string = frame.split('(')[0].split('@')[0]; + if (fnName.indexOf('runGuarded') !== -1) { + break; + } else if (fnName.indexOf('runTask') !== -1) { + break; + } + } + } + + const detectEmptyZone = Zone.root.fork({ + name: 'detectEmptyZone', + onHandleError(parentDelegate, currentZone, targetZone, error) { + parentDelegate.handleError(targetZone, error); + handleDetectError(error); + return false; + } + }); + + const detectZoneWithCallbacks = Zone.root.fork({ + name: 'detectCallbackZone', + onFork: (parentDelegate, currentZone, targetZone, zoneSpec) => { + handleDetectError(Error('onFork')); + return parentDelegate.fork(targetZone, zoneSpec); + }, + onIntercept: (parentDelegate, currentZone, targetZone, delegate, source) => { + handleDetectError(Error('onIntercept')); + return parentDelegate.intercept(targetZone, delegate, source); + }, + onInvoke: + (parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) => { + handleDetectError(Error('onInvoke')); + return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source); + }, + onScheduleTask: (parentZoneDelegate, currentZone, targetZone, task) => { + handleDetectError(Error('onScheduleTask')); + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + onInvokeTask: (parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) => { + handleDetectError(Error('onInvokeTask')); + return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs); + }, + onCancelTask: (parentZoneDelegate, currentZone, targetZone, task) => { + handleDetectError(Error('onCancelTask')); + return parentZoneDelegate.cancelTask(targetZone, task); + }, + + onHasTask: (delegate, current, target, hasTaskState) => { + handleDetectError(Error('onHasTask')); + return delegate.hasTask(target, hasTaskState); + }, + + onHandleError(parentDelegate, currentZone, targetZone, error) { + parentDelegate.handleError(targetZone, error); + handleDetectError(error); + return false; + } + }); + + let detectFn = () => { + throw Error('zoneAwareFrames'); + }; + + let detectPromiseFn = () => { + new Promise((resolve, reject) => { + reject(Error('zoneAwareFrames')); + }); + }; + + let detectPromiseCaughtFn = () => { + const p = new Promise((resolve, reject) => { + reject(Error('zoneAwareFrames')); + }); + p.catch(err => { + throw err; + }); + }; + + // Cause the error to extract the stack frames. + detectEmptyZone.runTask( + detectEmptyZone.scheduleEventTask('detect', detectFn, null, () => null, null)); + detectZoneWithCallbacks.runTask( + detectZoneWithCallbacks.scheduleEventTask('detect', detectFn, null, () => null, null)); + detectEmptyZone.runTask( + detectEmptyZone.scheduleMacroTask('detect', detectFn, null, () => null, null)); + detectZoneWithCallbacks.runTask( + detectZoneWithCallbacks.scheduleMacroTask('detect', detectFn, null, () => null, null)); + detectEmptyZone.runTask(detectEmptyZone.scheduleMicroTask('detect', detectFn, null, () => null)); + detectZoneWithCallbacks.runTask( + detectZoneWithCallbacks.scheduleMicroTask('detect', detectFn, null, () => null)); + + detectEmptyZone.runGuarded(() => { + detectEmptyZone.run(detectFn); + }); + detectZoneWithCallbacks.runGuarded(() => { + detectEmptyZone.run(detectFn); + }); + + detectEmptyZone.runGuarded(detectPromiseFn); + detectZoneWithCallbacks.runGuarded(detectPromiseFn); + + detectEmptyZone.runGuarded(detectPromiseCaughtFn); + detectZoneWithCallbacks.runGuarded(detectPromiseCaughtFn); return global['Zone'] = Zone; })(typeof window === 'object' && window || typeof self === 'object' && self || global); diff --git a/test/common/Error.spec.ts b/test/common/Error.spec.ts index 8614cb5d4..2eaaca812 100644 --- a/test/common/Error.spec.ts +++ b/test/common/Error.spec.ts @@ -170,31 +170,49 @@ describe('ZoneAwareError', () => { }); it('should show zone names in stack frames and remove extra frames', () => { - const rootZone = getRootZone(); + const rootZone = Zone.root; const innerZone = rootZone.fork({name: 'InnerZone'}); rootZone.run(testFn); function testFn() { let outside: Error; let inside: Error; + let outsideWithoutNew: Error; + let insideWithoutNew: Error; try { throw new Error('Outside'); } catch (e) { outside = e; } + try { + throw Error('Outside'); + } catch (e) { + outsideWithoutNew = e; + } innerZone.run(function insideRun() { try { throw new Error('Inside'); } catch (e) { inside = e; } + try { + throw Error('Inside'); + } catch (e) { + insideWithoutNew = e; + } }); expect(outside.stack).toEqual(outside.zoneAwareStack); + expect(outsideWithoutNew.stack).toEqual(outsideWithoutNew.zoneAwareStack); expect(inside.stack).toEqual(inside.zoneAwareStack); + expect(insideWithoutNew.stack).toEqual(insideWithoutNew.zoneAwareStack); expect(typeof inside.originalStack).toEqual('string'); + expect(typeof insideWithoutNew.originalStack).toEqual('string'); const outsideFrames = outside.stack.split(/\n/); const insideFrames = inside.stack.split(/\n/); + const outsideWithoutNewFrames = outsideWithoutNew.stack.split(/\n/); + const insideWithoutNewFrames = insideWithoutNew.stack.split(/\n/); + // throw away first line if it contains the error if (/Outside/.test(outsideFrames[0])) { outsideFrames.shift(); @@ -202,26 +220,137 @@ describe('ZoneAwareError', () => { if (/new Error/.test(outsideFrames[0])) { outsideFrames.shift(); } + if (/Outside/.test(outsideWithoutNewFrames[0])) { + outsideWithoutNewFrames.shift(); + } if (/Inside/.test(insideFrames[0])) { insideFrames.shift(); } if (/new Error/.test(insideFrames[0])) { insideFrames.shift(); } + if (/Inside/.test(insideWithoutNewFrames[0])) { + insideWithoutNewFrames.shift(); + } expect(outsideFrames[0]).toMatch(/testFn.*[]/); expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/); - expect(insideFrames[1]).toMatch(/run.*[ => InnerZone]]/); - expect(insideFrames[2]).toMatch(/testFn.*[]]/); + expect(insideFrames[1]).toMatch(/testFn.*[]]/); + + expect(outsideWithoutNewFrames[0]).toMatch(/testFn.*[]/); + + expect(insideWithoutNewFrames[0]).toMatch(/insideRun.*[InnerZone]]/); + expect(insideWithoutNewFrames[1]).toMatch(/testFn.*[]]/); } }); }); -function getRootZone() { - let zone = Zone.current; - while (zone.parent) { - zone = zone.parent; +const zoneAwareFrames = [ + 'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask', + 'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask', 'ZoneDelegate.invokeTask', + 'ZoneTask.invoke', 'zoneAwareAddListener', 'drainMicroTaskQueue', 'LongStackTrace', + 'getStacktraceWithUncaughtError' +]; + +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([]); } - return zone; -} \ No newline at end of file +}; + +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', () => { + (Zone as any)['debug'] = true; + it('new Error which occurs in setTimeout callback should not have zone frames visible', + assertStackDoesNotContainZoneFramesTest(() => { + setTimeout(() => { + throw new Error('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 which cause by promise rejection should not have zone frames visible', (done) => { + const p = new Promise((resolve, reject) => { + try { + // throw here because in IE + // err.stack will be undefined until throw + throw new Error('test error'); + } catch (err) { + reject(err); + } + }); + p.catch(err => { + assertStackDoesNotContainZoneFrames(err); + done(); + }); + }); + + it('error 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 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('stack frames of the callback in user customized zoneSpec should be kept', + assertStackDoesNotContainZoneFramesTest(() => { + (Zone as any)['debug'] = false; + 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(); + })); +}); diff --git a/test/node/fs.spec.ts b/test/node/fs.spec.ts index 99b9a026c..e52d317ed 100644 --- a/test/node/fs.spec.ts +++ b/test/node/fs.spec.ts @@ -63,7 +63,7 @@ describe('nodejs file system', () => { done(); }); }); - writeFile('testfile', 'test new content'); + writeFile('testfile', 'test new content', () => {}); }); }); }); @@ -83,7 +83,7 @@ describe('nodejs file system', () => { done(); }); }); - writeFile('testfile', 'test new content'); + writeFile('testfile', 'test new content', () => {}); }); }); }); From bbd9ac3b1687f1f84a4b57a1e0561b7d01d37b3a Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Sun, 12 Mar 2017 01:36:13 +0900 Subject: [PATCH 2/2] fix(error): add more handling for IE --- lib/zone.ts | 183 ++++++++++++++++++++++++++++++-------- test/common/Error.spec.ts | 98 +++++++++++++++----- 2 files changed, 220 insertions(+), 61 deletions(-) diff --git a/lib/zone.ts b/lib/zone.ts index fc9de5608..c3d40ca15 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -1571,10 +1571,10 @@ const Zone: ZoneType = (function(global: any) { // Store the frames which should be removed from the stack frames const blackListedStackFrames: {[frame: string]: FrameType} = {}; // We must find the frame where Error was created, otherwise we assume we don't understand stack - let zoneAwareFrame: string[] = []; + // the frame will be an array, because Error with new or without new will + // have different stack frames. + let zoneAwareErrorStartFrames: string[] = []; global.Error = ZoneAwareError; - // How should the stack frames be parsed. - let frameParserStrategy = null; const stackRewrite = 'stackRewrite'; // fix #595, create property descriptor @@ -1657,6 +1657,8 @@ const Zone: ZoneType = (function(global: any) { // in NativeError createProperty(props, 'originalStack'); createProperty(props, 'zoneAwareStack'); + // in IE, stack is not in prototype + createProperty(props, 'stack'); // define toString, toSource as method property createMethodProperty(props, 'toString'); @@ -1700,35 +1702,21 @@ const Zone: ZoneType = (function(global: any) { 'long-stack-trace' ]; - /** - * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as - * adds zone information to it. - */ - function ZoneAwareError() { - // make sure we have a valid this - // if this is undefined(call Error without new) or this is global - // or this is some other objects, we should force to create a - // valid ZoneAwareError by call Object.create() - if (!(this instanceof ZoneAwareError)) { - return ZoneAwareError.apply(Object.create(ZoneAwareError.prototype), arguments); - } - // Create an Error. - let error: Error = NativeError.apply(this, arguments); - this[__symbol__('error')] = error; - + function attachZoneAndRemoveInternalZoneFrames(error: any) { // Save original stack trace error.originalStack = error.stack; - // Process the stack trace and rewrite the frames. if ((ZoneAwareError as any)[stackRewrite] && error.originalStack) { let frames: string[] = error.originalStack.split('\n'); let zoneFrame = _currentZoneFrame; let i = 0; // Find the first frame - while (i < frames.length && zoneAwareFrame.filter(zf => zf === frames[i]).length === 0) { + while (i < frames.length && + zoneAwareErrorStartFrames.filter(zf => zf.trim() === frames[i].trim()).length === 0) { i++; } for (; i < frames.length && zoneFrame; i++) { + // trim here because blackListedStackFrames store the trimmed frames let frame = frames[i].trim(); if (frame) { let frameType = @@ -1758,9 +1746,41 @@ const Zone: ZoneType = (function(global: any) { } error.stack = error.zoneAwareStack = frames.join('\n'); } + } + + /** + * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as + * adds zone information to it. + */ + function ZoneAwareError() { + // make sure we have a valid this + // if this is undefined(call Error without new) or this is global + // or this is some other objects, we should force to create a + // valid ZoneAwareError by call Object.create() + if (!(this instanceof ZoneAwareError)) { + return ZoneAwareError.apply(Object.create(ZoneAwareError.prototype), arguments); + } + // Create an Error. + let error: Error = NativeError.apply(this, arguments); + if (!error.stack) { + // in IE, the error.stack will be undefined + // when error was constructed, it will only + // be available when throw + try { + throw error; + } catch (err) { + error = err; + } + } + this[__symbol__('error')] = error; + // 1. attach zone information to stack frame + // 2. remove zone internal stack frames + attachZoneAndRemoveInternalZoneFrames(error); + // use defineProperties here instead of copy property value // because of issue #595 which will break angular2. - Object.defineProperties(this, getErrorPropertiesForPrototype(Object.getPrototypeOf(this))); + const props = getErrorPropertiesForPrototype(Object.getPrototypeOf(this)); + Object.defineProperties(this, props); return this; } @@ -1871,7 +1891,10 @@ const Zone: ZoneType = (function(global: any) { let fnName: string = frame.split('(')[0].split('@')[0]; let frameType = FrameType.transition; if (fnName.indexOf('ZoneAwareError') !== -1) { - zoneAwareFrame.push(frame); + // we found the ZoneAwareError start frame + // the frame will be different when call Error(...) + // and new Error(...), so we store them both + zoneAwareErrorStartFrames.push(frame); } if (fnName.indexOf('runGuarded') !== -1) { runGuardedFrame = true; @@ -1896,17 +1919,44 @@ const Zone: ZoneType = (function(global: any) { }) as Zone; // carefully constructor a stack frame which contains all of the frames of interest which // need to be detected and blacklisted. + + // use this method to handle + // 1. IE issue, the error.stack can only be not undefined after throw + // 2. handle Error(...) without new options + const throwError = (message: string, withNew: boolean = true) => { + let error; + try { + if (withNew) { + throw new Error(message); + } else { + throw Error(message); + } + } catch (err) { + error = err; + } + return error; + }; + + const nativeStackTraceLimit = NativeError.stackTraceLimit; + // in some system/browser, some additional stack frames + // will be generated (such as inline function) + // so the the stack frame to check ZoneAwareError Start + // maybe ignored because the frame's number will exceed + // stackTraceLimit, so we just set stackTraceLimit to 100 + // and reset after all detect work is done. + NativeError.stackTraceLimit = 100; let detectRunFn = () => { detectZone.run(() => { detectZone.runGuarded(() => { - throw new Error('blacklistStackFrames'); + throw throwError('blacklistStackFrames'); }); }); }; + let detectRunWithoutNewFn = () => { detectZone.run(() => { detectZone.runGuarded(() => { - throw Error('blacklistStackFrames'); + throw throwError('blacklistStackFrames', false); }); }); }; @@ -1922,11 +1972,14 @@ const Zone: ZoneType = (function(global: any) { // On safari it is possible to have stack frame with no line number. // This check makes sure that we don't filter frames on name only (must have // linenumber) - const trimedFrame = frame.trim().split('[')[0].trim(); - if (/:\d+:\d+/.test(trimedFrame) && !blackListedStackFrames.hasOwnProperty(trimedFrame)) { - blackListedStackFrames[trimedFrame] = FrameType.blackList; + const trimmedFrame = frame.trim().split('[')[0].trim(); + if (/:\d+:\d+/.test(trimmedFrame) && !blackListedStackFrames.hasOwnProperty(trimmedFrame)) { + blackListedStackFrames[trimmedFrame] = FrameType.blackList; } + // when we found runGuarded or runTask, we should stop + // otherwise we will store some stack frames like + // module.load, require and something like that let fnName: string = frame.split('(')[0].split('@')[0]; if (fnName.indexOf('runGuarded') !== -1) { break; @@ -1948,33 +2001,41 @@ const Zone: ZoneType = (function(global: any) { const detectZoneWithCallbacks = Zone.root.fork({ name: 'detectCallbackZone', onFork: (parentDelegate, currentZone, targetZone, zoneSpec) => { - handleDetectError(Error('onFork')); + // we need to generate Error with or without new + handleDetectError(throwError('onFork')); + handleDetectError(throwError('onFork', false)); return parentDelegate.fork(targetZone, zoneSpec); }, onIntercept: (parentDelegate, currentZone, targetZone, delegate, source) => { - handleDetectError(Error('onIntercept')); + handleDetectError(throwError('onIntercept')); + handleDetectError(throwError('onIntercept', false)); return parentDelegate.intercept(targetZone, delegate, source); }, onInvoke: (parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) => { - handleDetectError(Error('onInvoke')); + handleDetectError(throwError('onInvoke')); + handleDetectError(throwError('onInvoke', false)); return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source); }, onScheduleTask: (parentZoneDelegate, currentZone, targetZone, task) => { - handleDetectError(Error('onScheduleTask')); + handleDetectError(throwError('onScheduleTask')); + handleDetectError(throwError('onScheduleTask', false)); return parentZoneDelegate.scheduleTask(targetZone, task); }, onInvokeTask: (parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) => { - handleDetectError(Error('onInvokeTask')); + handleDetectError(throwError('onInvokeTask')); + handleDetectError(throwError('onInvokeTask', false)); return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs); }, onCancelTask: (parentZoneDelegate, currentZone, targetZone, task) => { - handleDetectError(Error('onCancelTask')); + handleDetectError(throwError('onCancelTask')); + handleDetectError(throwError('onCancelTask', false)); return parentZoneDelegate.cancelTask(targetZone, task); }, onHasTask: (delegate, current, target, hasTaskState) => { - handleDetectError(Error('onHasTask')); + handleDetectError(throwError('onHasTask')); + handleDetectError(throwError('onHasTask', false)); return delegate.hasTask(target, hasTaskState); }, @@ -1986,18 +2047,37 @@ const Zone: ZoneType = (function(global: any) { }); let detectFn = () => { - throw Error('zoneAwareFrames'); + throw throwError('zoneAwareFrames'); + }; + + let detectWithoutNewFn = () => { + throw throwError('zoneAwareFrames', false); }; let detectPromiseFn = () => { new Promise((resolve, reject) => { - reject(Error('zoneAwareFrames')); + reject(throwError('zoneAwareFrames')); + }); + }; + + let detectPromiseWithoutNewFn = () => { + new Promise((resolve, reject) => { + reject(throwError('zoneAwareFrames', false)); }); }; let detectPromiseCaughtFn = () => { const p = new Promise((resolve, reject) => { - reject(Error('zoneAwareFrames')); + reject(throwError('zoneAwareFrames')); + }); + p.catch(err => { + throw err; + }); + }; + + let detectPromiseCaughtWithoutNewFn = () => { + const p = new Promise((resolve, reject) => { + reject(throwError('zoneAwareFrames', false)); }); p.catch(err => { throw err; @@ -2024,11 +2104,38 @@ const Zone: ZoneType = (function(global: any) { detectEmptyZone.run(detectFn); }); + detectEmptyZone.runTask( + detectEmptyZone.scheduleEventTask('detect', detectWithoutNewFn, null, () => null, null)); + detectZoneWithCallbacks.runTask(detectZoneWithCallbacks.scheduleEventTask( + 'detect', detectWithoutNewFn, null, () => null, null)); + detectEmptyZone.runTask( + detectEmptyZone.scheduleMacroTask('detect', detectWithoutNewFn, null, () => null, null)); + detectZoneWithCallbacks.runTask(detectZoneWithCallbacks.scheduleMacroTask( + 'detect', detectWithoutNewFn, null, () => null, null)); + detectEmptyZone.runTask( + detectEmptyZone.scheduleMicroTask('detect', detectWithoutNewFn, null, () => null)); + detectZoneWithCallbacks.runTask( + detectZoneWithCallbacks.scheduleMicroTask('detect', detectWithoutNewFn, null, () => null)); + + detectEmptyZone.runGuarded(() => { + detectEmptyZone.run(detectWithoutNewFn); + }); + detectZoneWithCallbacks.runGuarded(() => { + detectEmptyZone.run(detectWithoutNewFn); + }); + detectEmptyZone.runGuarded(detectPromiseFn); detectZoneWithCallbacks.runGuarded(detectPromiseFn); + detectEmptyZone.runGuarded(detectPromiseWithoutNewFn); + detectZoneWithCallbacks.runGuarded(detectPromiseWithoutNewFn); + detectEmptyZone.runGuarded(detectPromiseCaughtFn); detectZoneWithCallbacks.runGuarded(detectPromiseCaughtFn); + detectEmptyZone.runGuarded(detectPromiseCaughtWithoutNewFn); + detectZoneWithCallbacks.runGuarded(detectPromiseCaughtWithoutNewFn); + NativeError.stackTraceLimit = nativeStackTraceLimit; + return global['Zone'] = Zone; })(typeof window === 'object' && window || typeof self === 'object' && self || global); diff --git a/test/common/Error.spec.ts b/test/common/Error.spec.ts index 2eaaca812..17f33cd86 100644 --- a/test/common/Error.spec.ts +++ b/test/common/Error.spec.ts @@ -169,6 +169,16 @@ describe('ZoneAwareError', () => { expect(spy).toHaveBeenCalledWith('test'); }); + it('should always have stack property even without throw', () => { + // in IE, the stack will be undefined without throw + // in ZoneAwareError, we will make stack always be + // there event without throw + const error = new Error('test'); + const errorWithoutNew = Error('test'); + expect(error.stack.split('\n').length > 0).toBeTruthy(); + expect(errorWithoutNew.stack.split('\n').length > 0).toBeTruthy(); + }); + it('should show zone names in stack frames and remove extra frames', () => { const rootZone = Zone.root; const innerZone = rootZone.fork({name: 'InnerZone'}); @@ -220,18 +230,39 @@ describe('ZoneAwareError', () => { if (/new Error/.test(outsideFrames[0])) { outsideFrames.shift(); } + if (/Outside/.test(outsideWithoutNewFrames[0])) { outsideWithoutNewFrames.shift(); } + if (/new Error/.test(outsideWithoutNewFrames[0])) { + outsideWithoutNewFrames.shift(); + } + if (/Error.ZoneAwareError/.test(outsideWithoutNewFrames[0])) { + outsideWithoutNewFrames.shift(); + } + if (/ZoneAwareError/.test(outsideWithoutNewFrames[0])) { + outsideWithoutNewFrames.shift(); + } + if (/Inside/.test(insideFrames[0])) { insideFrames.shift(); } if (/new Error/.test(insideFrames[0])) { insideFrames.shift(); } + if (/Inside/.test(insideWithoutNewFrames[0])) { insideWithoutNewFrames.shift(); } + if (/new Error/.test(insideWithoutNewFrames[0])) { + insideWithoutNewFrames.shift(); + } + if (/Error.ZoneAwareError/.test(insideWithoutNewFrames[0])) { + insideWithoutNewFrames.shift(); + } + if (/ZoneAwareError/.test(insideWithoutNewFrames[0])) { + insideWithoutNewFrames.shift(); + } expect(outsideFrames[0]).toMatch(/testFn.*[]/); @@ -249,7 +280,7 @@ describe('ZoneAwareError', () => { const zoneAwareFrames = [ 'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask', 'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask', 'ZoneDelegate.invokeTask', - 'ZoneTask.invoke', 'zoneAwareAddListener', 'drainMicroTaskQueue', 'LongStackTrace', + 'ZoneTask.invoke', 'zoneAwareAddListener', 'drainMicroTaskQueue', 'new LongStackTrace', 'getStacktraceWithUncaughtError' ]; @@ -283,11 +314,10 @@ const assertStackDoesNotContainZoneFramesTest = function(testFn: Function) { }; describe('Error stack', () => { - (Zone as any)['debug'] = true; - it('new Error which occurs in setTimeout callback should not have zone frames visible', + it('Error with new which occurs in setTimeout callback should not have zone frames visible', assertStackDoesNotContainZoneFramesTest(() => { setTimeout(() => { - throw new Error('test error'); + throw new Error('timeout test error'); }, 10); })); @@ -298,23 +328,29 @@ describe('Error stack', () => { }, 10); })); - it('error which cause by promise rejection should not have zone frames visible', (done) => { - const p = new Promise((resolve, reject) => { - try { - // throw here because in IE - // err.stack will be undefined until throw - throw new Error('test error'); - } catch (err) { - reject(err); - } - }); - p.catch(err => { - assertStackDoesNotContainZoneFrames(err); - done(); - }); - }); - - it('error which occurs in eventTask callback should not have zone frames visible', + 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'); @@ -322,7 +358,15 @@ describe('Error stack', () => { task.invoke(); })); - it('error which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible', + 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', () => { @@ -331,9 +375,17 @@ describe('Error stack', () => { 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(() => { - (Zone as any)['debug'] = false; const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec']) .fork({ name: 'customZone',