diff --git a/lib/zone.ts b/lib/zone.ts index 79c70e11c..c0563e8e2 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -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}]`; @@ -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) { @@ -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); diff --git a/test/common/Error.spec.ts b/test/common/Error.spec.ts index d2f92526b..8204b2add 100644 --- a/test/common/Error.spec.ts +++ b/test/common/Error.spec.ts @@ -258,12 +258,161 @@ describe('ZoneAwareError', () => { expect(outsideFrames[0]).toMatch(/testFn.*[]/); expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/); - expect(insideFrames[2]).toMatch(/testFn.*[]]/); + expect(insideFrames[1]).toMatch(/testFn.*[]]/); expect(outsideWithoutNewFrames[0]).toMatch(/testFn.*[]/); expect(insideWithoutNewFrames[0]).toMatch(/insideRun.*[InnerZone]]/); - expect(insideWithoutNewFrames[2]).toMatch(/testFn.*[]]/); + expect(insideWithoutNewFrames[1]).toMatch(/testFn.*[]]/); } }); + + 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; + } + } + }); + }); });