| /** |
| * @license |
| * Copyright Google Inc. All Rights Reserved. |
| * |
| * Use of this source code is governed by an MIT-style license that can be |
| * found in the LICENSE file at https://angular.io/license |
| */ |
| /** |
| * @fileoverview |
| * @suppress {globalThis,undefinedVars} |
| */ |
| |
| /** |
| * Extend the Error with additional fields for rewritten stack frames |
| */ |
| interface Error { |
| /** |
| * Stack trace where extra frames have been removed and zone names added. |
| */ |
| zoneAwareStack?: string; |
| |
| /** |
| * Original stack trace with no modifications |
| */ |
| originalStack?: string; |
| } |
| |
| Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { |
| /* |
| * This code patches Error so that: |
| * - It ignores un-needed stack frames. |
| * - It Shows the associated Zone for reach frame. |
| */ |
| |
| const enum FrameType { |
| /// Skip this frame when printing out stack |
| blackList, |
| /// This frame marks zone transition |
| transition |
| } |
| |
| const blacklistedStackFramesSymbol = api.symbol('blacklistedStackFrames'); |
| const NativeError = global[api.symbol('Error')] = global['Error']; |
| // 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 zoneAwareFrame1: string; |
| let zoneAwareFrame2: string; |
| let zoneAwareFrame1WithoutNew: string; |
| let zoneAwareFrame2WithoutNew: string; |
| let zoneAwareFrame3WithoutNew: string; |
| |
| global['Error'] = ZoneAwareError; |
| const stackRewrite = 'stackRewrite'; |
| |
| type BlackListedStackFramesPolicy = 'default'|'disable'|'lazy'; |
| const blackListedStackFramesPolicy: BlackListedStackFramesPolicy = |
| global['__Zone_Error_BlacklistedStackFrames_policy'] || 'default'; |
| |
| interface ZoneFrameName { |
| zoneName: string; |
| parent?: ZoneFrameName; |
| } |
| |
| function buildZoneFrameNames(zoneFrame: _ZoneFrame) { |
| let zoneFrameName: ZoneFrameName = {zoneName: zoneFrame.zone.name}; |
| let result = zoneFrameName; |
| while (zoneFrame.parent) { |
| zoneFrame = zoneFrame.parent; |
| const parentZoneFrameName = {zoneName: zoneFrame.zone.name}; |
| zoneFrameName.parent = parentZoneFrameName; |
| zoneFrameName = parentZoneFrameName; |
| } |
| return result; |
| } |
| |
| function buildZoneAwareStackFrames( |
| originalStack: string, zoneFrame: _ZoneFrame|ZoneFrameName|null, isZoneFrame = true) { |
| let frames: string[] = originalStack.split('\n'); |
| let i = 0; |
| // Find the first frame |
| while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2 || |
| frames[i] === zoneAwareFrame1WithoutNew || frames[i] === zoneAwareFrame2WithoutNew || |
| frames[i] === zoneAwareFrame3WithoutNew) && |
| i < frames.length) { |
| i++; |
| } |
| for (; i < frames.length && zoneFrame; i++) { |
| let frame = frames[i]; |
| if (frame.trim()) { |
| switch (blackListedStackFrames[frame]) { |
| case FrameType.blackList: |
| frames.splice(i, 1); |
| i--; |
| break; |
| case FrameType.transition: |
| if (zoneFrame.parent) { |
| // This is the special frame where zone changed. Print and process it accordingly |
| zoneFrame = zoneFrame.parent; |
| } else { |
| zoneFrame = null; |
| } |
| frames.splice(i, 1); |
| i--; |
| break; |
| default: |
| frames[i] += isZoneFrame ? ` [${(zoneFrame as _ZoneFrame).zone.name}]` : |
| ` [${(zoneFrame as ZoneFrameName).zoneName}]`; |
| } |
| } |
| } |
| return 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(): Error { |
| // We always have to return native error otherwise the browser console will not work. |
| let error: Error = NativeError.apply(this, arguments); |
| // Save original stack trace |
| const originalStack = (error as any)['originalStack'] = error.stack; |
| |
| // Process the stack trace and rewrite the frames. |
| if ((ZoneAwareError as any)[stackRewrite] && originalStack) { |
| let zoneFrame = api.currentZoneFrame(); |
| if (blackListedStackFramesPolicy === 'lazy') { |
| // don't handle stack trace now |
| (error as any)[api.symbol('zoneFrameNames')] = buildZoneFrameNames(zoneFrame); |
| } else if (blackListedStackFramesPolicy === 'default') { |
| try { |
| error.stack = error.zoneAwareStack = buildZoneAwareStackFrames(originalStack, zoneFrame); |
| } catch (e) { |
| // ignore as some browsers don't allow overriding of stack |
| } |
| } |
| } |
| |
| if (this instanceof NativeError && this.constructor != NativeError) { |
| // We got called with a `new` operator AND we are subclass of ZoneAwareError |
| // in that case we have to copy all of our properties to `this`. |
| Object.keys(error).concat('stack', 'message').forEach((key) => { |
| const value = (error as any)[key]; |
| if (value !== undefined) { |
| try { |
| this[key] = value; |
| } catch (e) { |
| // ignore the assignment in case it is a setter and it throws. |
| } |
| } |
| }); |
| return this; |
| } |
| return error; |
| } |
| |
| // Copy the prototype so that instanceof operator works as expected |
| ZoneAwareError.prototype = NativeError.prototype; |
| (ZoneAwareError as any)[blacklistedStackFramesSymbol] = blackListedStackFrames; |
| (ZoneAwareError as any)[stackRewrite] = false; |
| |
| const zoneAwareStackSymbol = api.symbol('zoneAwareStack'); |
| |
| // try to define zoneAwareStack property when blackListed |
| // policy is delay |
| if (blackListedStackFramesPolicy === 'lazy') { |
| Object.defineProperty(ZoneAwareError.prototype, 'zoneAwareStack', { |
| configurable: true, |
| enumerable: true, |
| get: function() { |
| if (!this[zoneAwareStackSymbol]) { |
| this[zoneAwareStackSymbol] = buildZoneAwareStackFrames( |
| this.originalStack, this[api.symbol('zoneFrameNames')], false); |
| } |
| return this[zoneAwareStackSymbol]; |
| }, |
| set: function(newStack: string) { |
| this.originalStack = newStack; |
| this[zoneAwareStackSymbol] = buildZoneAwareStackFrames( |
| this.originalStack, this[api.symbol('zoneFrameNames')], false); |
| } |
| }); |
| } |
| |
| // those properties need special handling |
| const specialPropertyNames = ['stackTraceLimit', 'captureStackTrace', 'prepareStackTrace']; |
| // those properties of NativeError should be set to ZoneAwareError |
| const nativeErrorProperties = Object.keys(NativeError); |
| if (nativeErrorProperties) { |
| nativeErrorProperties.forEach(prop => { |
| if (specialPropertyNames.filter(sp => sp === prop).length === 0) { |
| Object.defineProperty(ZoneAwareError, prop, { |
| get: function() { |
| return NativeError[prop]; |
| }, |
| set: function(value) { |
| NativeError[prop] = value; |
| } |
| }); |
| } |
| }); |
| } |
| |
| if (NativeError.hasOwnProperty('stackTraceLimit')) { |
| // Extend default stack limit as we will be removing few frames. |
| NativeError.stackTraceLimit = Math.max(NativeError.stackTraceLimit, 15); |
| |
| // make sure that ZoneAwareError has the same property which forwards to NativeError. |
| Object.defineProperty(ZoneAwareError, 'stackTraceLimit', { |
| get: function() { |
| return NativeError.stackTraceLimit; |
| }, |
| set: function(value) { |
| return NativeError.stackTraceLimit = value; |
| } |
| }); |
| } |
| |
| if (NativeError.hasOwnProperty('captureStackTrace')) { |
| Object.defineProperty(ZoneAwareError, 'captureStackTrace', { |
| // add named function here because we need to remove this |
| // stack frame when prepareStackTrace below |
| value: function zoneCaptureStackTrace(targetObject: Object, constructorOpt?: Function) { |
| NativeError.captureStackTrace(targetObject, constructorOpt); |
| } |
| }); |
| } |
| |
| const ZONE_CAPTURESTACKTRACE = 'zoneCaptureStackTrace'; |
| Object.defineProperty(ZoneAwareError, 'prepareStackTrace', { |
| get: function() { |
| return NativeError.prepareStackTrace; |
| }, |
| set: function(value) { |
| if (!value || typeof value !== 'function') { |
| return NativeError.prepareStackTrace = value; |
| } |
| return NativeError.prepareStackTrace = function( |
| error: Error, structuredStackTrace: {getFunctionName: Function}[]) { |
| // remove additional stack information from ZoneAwareError.captureStackTrace |
| if (structuredStackTrace) { |
| for (let i = 0; i < structuredStackTrace.length; i++) { |
| const st = structuredStackTrace[i]; |
| // remove the first function which name is zoneCaptureStackTrace |
| if (st.getFunctionName() === ZONE_CAPTURESTACKTRACE) { |
| structuredStackTrace.splice(i, 1); |
| break; |
| } |
| } |
| } |
| return value.call(this, error, structuredStackTrace); |
| }; |
| } |
| }); |
| |
| if (blackListedStackFramesPolicy === 'disable') { |
| // don't need to run detectZone to populate |
| // blacklisted stack frames |
| return; |
| } |
| // Now we need to populate the `blacklistedStackFrames` as well as find the |
| // 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', |
| onHandleError: function( |
| parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): boolean { |
| if (error.originalStack && Error === ZoneAwareError) { |
| let frames = error.originalStack.split(/\n/); |
| let runFrame = false, runGuardedFrame = false, runTaskFrame = false; |
| 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 |
| // line number or exact equals to `ZoneAwareError`) |
| if (/:\d+:\d+/.test(frame) || frame === 'ZoneAwareError') { |
| // 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 |
| // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24 |
| let fnName: string = frame.split('(')[0].split('@')[0]; |
| let frameType = FrameType.transition; |
| if (fnName.indexOf('ZoneAwareError') !== -1) { |
| if (fnName.indexOf('new ZoneAwareError') !== -1) { |
| zoneAwareFrame1 = frame; |
| zoneAwareFrame2 = frame.replace('new ZoneAwareError', 'new Error.ZoneAwareError'); |
| } else { |
| zoneAwareFrame1WithoutNew = frame; |
| zoneAwareFrame2WithoutNew = frame.replace('Error.', ''); |
| if (frame.indexOf('Error.ZoneAwareError') === -1) { |
| zoneAwareFrame3WithoutNew = |
| frame.replace('ZoneAwareError', 'Error.ZoneAwareError'); |
| } |
| } |
| blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList; |
| } |
| if (fnName.indexOf('runGuarded') !== -1) { |
| runGuardedFrame = true; |
| } else if (fnName.indexOf('runTask') !== -1) { |
| runTaskFrame = true; |
| } else if (fnName.indexOf('run') !== -1) { |
| runFrame = true; |
| } else { |
| frameType = FrameType.blackList; |
| } |
| blackListedStackFrames[frame] = frameType; |
| // Once we find all of the frames we can stop looking. |
| if (runFrame && runGuardedFrame && runTaskFrame) { |
| (ZoneAwareError as any)[stackRewrite] = true; |
| break; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| }) as Zone; |
| // carefully constructor a stack frame which contains all of the frames of interest which |
| // need to be detected and blacklisted. |
| |
| 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 = () => {}; |
| childDetectZone.scheduleEventTask( |
| blacklistedStackFramesSymbol, |
| () => { |
| childDetectZone.scheduleMacroTask( |
| blacklistedStackFramesSymbol, |
| () => { |
| childDetectZone.scheduleMicroTask( |
| blacklistedStackFramesSymbol, |
| () => { |
| throw new Error(); |
| }, |
| undefined, |
| (t: Task) => { |
| (t as any)._transitionTo = fakeTransitionTo; |
| t.invoke(); |
| }); |
| childDetectZone.scheduleMicroTask( |
| blacklistedStackFramesSymbol, |
| () => { |
| throw Error(); |
| }, |
| undefined, |
| (t: Task) => { |
| (t as any)._transitionTo = fakeTransitionTo; |
| t.invoke(); |
| }); |
| }, |
| undefined, |
| (t) => { |
| (t as any)._transitionTo = fakeTransitionTo; |
| t.invoke(); |
| }, |
| () => {}); |
| }, |
| undefined, |
| (t) => { |
| (t as any)._transitionTo = fakeTransitionTo; |
| t.invoke(); |
| }, |
| () => {}); |
| }); |
| }); |
| |
| Error.stackTraceLimit = originalStackTraceLimit; |
| }); |