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

Commit e1c2a02

Browse files
committed
feat(Error): Rewrite Error stack frames to include zone
The errors caused by the Zone contain extra stack frames. This feature removes the confusing stack frames as well as appends the zone for each stack frame to get a better understanding of the error messages. Improved Error: ``` Error.spec.js:54 Error: Inside [InnerZone] at insideRun (Error.spec.js:31) [InnerZone] at Zone.run (zone.js:100) [<root> => InnerZone] at testFn (Error.spec.js:29) [<root>] at Zone.run (zone.js:100) [ProxyZone => <root>] at Object.eval (Error.spec.js:19) [ProxyZone] ``` Original Error: ``` Error.spec.js:53 Error: Inside at ZoneAwareError (zone.js:652) at insideRun (Error.spec.js:31) at ZoneDelegate.invoke (zone.js:216) at Zone.run (zone.js:100) at testFn (Error.spec.js:29) at ZoneDelegate.invoke (zone.js:216) at Zone.run (zone.js:100) at Object.eval (Error.spec.js:19) ```
1 parent dd8aa15 commit e1c2a02

File tree

3 files changed

+252
-11
lines changed

3 files changed

+252
-11
lines changed

Diff for: lib/zone.ts

+180-11
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,21 @@ interface EventTask extends Task {
519519
/* TS v1.8 => type: 'eventTask'; */
520520
}
521521

522+
/**
523+
* Extend the Error with additional fields for rewritten stack frames
524+
*/
525+
interface Error {
526+
/**
527+
* Stack trace where extra frames have been removed and zone names added.
528+
*/
529+
zoneAwareStack?: string;
530+
531+
/**
532+
* Original stack trace with no modiffications
533+
*/
534+
originalStack?: string;
535+
}
536+
522537
/** @internal */
523538
type AmbientZone = Zone;
524539
/** @internal */
@@ -545,7 +560,7 @@ const Zone: ZoneType = (function(global: any) {
545560

546561

547562
static get current(): AmbientZone {
548-
return _currentZone;
563+
return _currentZoneFrame.zone;
549564
};
550565
static get currentTask(): Task {
551566
return _currentTask;
@@ -606,19 +621,17 @@ const Zone: ZoneType = (function(global: any) {
606621

607622
public run(
608623
callback: Function, applyThis: any = null, applyArgs: any[] = null, source: string = null) {
609-
const oldZone = _currentZone;
610-
_currentZone = this;
624+
_currentZoneFrame = new ZoneFrame(_currentZoneFrame, this);
611625
try {
612626
return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
613627
} finally {
614-
_currentZone = oldZone;
628+
_currentZoneFrame = _currentZoneFrame.parent;
615629
}
616630
}
617631

618632
public runGuarded(
619633
callback: Function, applyThis: any = null, applyArgs: any[] = null, source: string = null) {
620-
const oldZone = _currentZone;
621-
_currentZone = this;
634+
_currentZoneFrame = new ZoneFrame(_currentZoneFrame, this);
622635
try {
623636
try {
624637
return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
@@ -628,7 +641,7 @@ const Zone: ZoneType = (function(global: any) {
628641
}
629642
}
630643
} finally {
631-
_currentZone = oldZone;
644+
_currentZoneFrame = _currentZoneFrame.parent;
632645
}
633646
}
634647

@@ -641,8 +654,7 @@ const Zone: ZoneType = (function(global: any) {
641654
'; Execution: ' + this.name + ')');
642655
const previousTask = _currentTask;
643656
_currentTask = task;
644-
const oldZone = _currentZone;
645-
_currentZone = this;
657+
_currentZoneFrame = new ZoneFrame(_currentZoneFrame, this);
646658
try {
647659
if (task.type == 'macroTask' && task.data && !task.data.isPeriodic) {
648660
task.cancelFn = null;
@@ -655,7 +667,7 @@ const Zone: ZoneType = (function(global: any) {
655667
}
656668
}
657669
} finally {
658-
_currentZone = oldZone;
670+
_currentZoneFrame = _currentZoneFrame.parent;
659671
_currentTask = previousTask;
660672
}
661673
}
@@ -924,14 +936,23 @@ const Zone: ZoneType = (function(global: any) {
924936
rejection: any;
925937
}
926938

939+
class ZoneFrame {
940+
public parent: ZoneFrame;
941+
public zone: Zone;
942+
constructor(parent: ZoneFrame, zone: Zone) {
943+
this.parent = parent;
944+
this.zone = zone;
945+
}
946+
}
947+
927948
function __symbol__(name: string) {
928949
return '__zone_symbol__' + name;
929950
};
930951
const symbolSetTimeout = __symbol__('setTimeout');
931952
const symbolPromise = __symbol__('Promise');
932953
const symbolThen = __symbol__('then');
933954

934-
let _currentZone: Zone = new Zone(null, null);
955+
let _currentZoneFrame = new ZoneFrame(null, new Zone(null, null));
935956
let _currentTask: Task = null;
936957
let _microTaskQueue: Task[] = [];
937958
let _isDrainingMicrotaskQueue: boolean = false;
@@ -1232,5 +1253,153 @@ const Zone: ZoneType = (function(global: any) {
12321253

12331254
// This is not part of public API, but it is usefull for tests, so we expose it.
12341255
Promise[Zone.__symbol__('uncaughtPromiseErrors')] = _uncaughtPromiseErrors;
1256+
1257+
/*
1258+
* This code patches Error so that:
1259+
* - It ignores un-needed stack frames.
1260+
* - It Shows the associated Zone for reach frame.
1261+
*/
1262+
1263+
enum FrameType {
1264+
/// Skip this frame when printing out stack
1265+
blackList,
1266+
/// This frame marks zone transition
1267+
trasition
1268+
};
1269+
const NativeError = global[__symbol__('Error')] = global.Error;
1270+
// Store the frames which should be removed from the stack frames
1271+
const blackListedStackFrames: {[frame: string]:FrameType} = {};
1272+
// We must find the frame where Error was created, otherwise we assume we don't understand stack
1273+
let zoneAwareFrame: string;
1274+
global.Error = ZoneAwareError;
1275+
// How should the stack frames be parsed.
1276+
let frameParserStrategy = null;
1277+
const stackRewrite = 'stackRewrite';
1278+
1279+
1280+
/**
1281+
* This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as
1282+
* adds zone information to it.
1283+
*/
1284+
function ZoneAwareError() {
1285+
// Create an Error.
1286+
let error: Error = NativeError.apply(this, arguments);
1287+
1288+
// Save original stack trace
1289+
error.originalStack = error.stack;
1290+
1291+
// Process the stack trace and rewrite the frames.
1292+
if (ZoneAwareError[stackRewrite] && error.originalStack) {
1293+
let frames: string[] = error.originalStack.split('\n');
1294+
let zoneFrame = _currentZoneFrame;
1295+
let i = 0;
1296+
// Find the first frame
1297+
while (frames[i] !== zoneAwareFrame && i < frames.length) {
1298+
i++;
1299+
}
1300+
for(;i < frames.length && zoneFrame; i++) {
1301+
let frame = frames[i];
1302+
if (frame.trim()) {
1303+
let frameType = blackListedStackFrames.hasOwnProperty(frame) && blackListedStackFrames[frame];
1304+
if (frameType === FrameType.blackList) {
1305+
frames.splice(i, 1);
1306+
i--;
1307+
} else if (frameType === FrameType.trasition) {
1308+
if (zoneFrame.parent) {
1309+
// This is the special frame where zone changed. Print and process it accordingly
1310+
frames[i] += ` [${zoneFrame.parent.zone.name} => ${zoneFrame.zone.name}]`;
1311+
zoneFrame = zoneFrame.parent;
1312+
} else {
1313+
zoneFrame == null;
1314+
}
1315+
} else {
1316+
frames[i] += ` [${zoneFrame.zone.name}]`;
1317+
}
1318+
}
1319+
}
1320+
error.stack = error.zoneAwareStack = frames.join('\n');
1321+
}
1322+
return error;
1323+
};
1324+
// Copy the prototype so that instanceof operator works as expected
1325+
ZoneAwareError.prototype = NativeError.prototype;
1326+
ZoneAwareError[Zone.__symbol__('blacklistedStackFrames')] = blackListedStackFrames;
1327+
ZoneAwareError[stackRewrite] = false;
1328+
1329+
if (NativeError.hasOwnProperty('stackTraceLimit')) {
1330+
// Extend default stack limit as we will be removing few frames.
1331+
NativeError.stackTraceLimit = Math.max(NativeError.stackTraceLimit, 15);
1332+
1333+
// make sure that ZoneAwareError has the same property which forwards to NativeError.
1334+
Object.defineProperty(ZoneAwareError, 'stackTraceLimit', {
1335+
get: function() { return NativeError.stackTraceLimit; },
1336+
set: function(value) { return NativeError.stackTraceLimit = value; }
1337+
});
1338+
}
1339+
1340+
// Now we need to populet the `blacklistedStackFrames` as well as find the
1341+
// run/runGuraded/runTask frames. This is done by creating a detect zone and then threading
1342+
// the execution through all of the above methods so that we can look at the stack trace and
1343+
// find the frames of interest.
1344+
let detectZone: Zone = Zone.current.fork({
1345+
name: 'detect',
1346+
onInvoke: function(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
1347+
delegate: Function, applyThis: any, applyArgs: any[], source: string): any {
1348+
// Here only so that it will show up in the stack frame so that it can be black listed.
1349+
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
1350+
},
1351+
onHandleError: function(parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): boolean {
1352+
if (error.originalStack && Error === ZoneAwareError) {
1353+
let frames = error.originalStack.split(/\n/);
1354+
let runFrame = false, runGuardedFrame = false, runTaskFrame = false;
1355+
while (frames.length) {
1356+
let frame = frames.shift();
1357+
// On safari it is possible to have stack frame with no line number.
1358+
// This check makes sure that we don't filter frames on name only (must have linenumber)
1359+
if (/:\d+:\d+/.test(frame)) {
1360+
// Get rid of the path so that we don't accidintely find function name in path.
1361+
// In chrome the seperator is `(` and `@` in FF and safari
1362+
// Chrome: at Zone.run (zone.js:100)
1363+
// Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24)
1364+
// FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24
1365+
// Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24
1366+
let fnName: string = frame.split('(')[0].split('@')[0];
1367+
let frameType = FrameType.trasition;
1368+
if (fnName.indexOf('ZoneAwareError') !== -1) {
1369+
zoneAwareFrame = frame;
1370+
}
1371+
if (fnName.indexOf('runGuarded') !== -1) {
1372+
runGuardedFrame = true;
1373+
} else if (fnName.indexOf('runTask') !== -1) {
1374+
runTaskFrame = true;
1375+
} else if (fnName.indexOf('run') !== -1) {
1376+
runFrame = true;
1377+
} else {
1378+
frameType = FrameType.blackList;
1379+
}
1380+
blackListedStackFrames[frame] = frameType;
1381+
// Once we find all of the frames we can stop looking.
1382+
if (runFrame && runGuardedFrame && runTaskFrame) {
1383+
ZoneAwareError[stackRewrite] = true;
1384+
break;
1385+
}
1386+
}
1387+
}
1388+
}
1389+
return false;
1390+
}
1391+
}) as Zone;
1392+
// carefully constructor a stack frame which contains all of the frames of interest which
1393+
// need to be detected and blacklisted.
1394+
let detectRunFn = () => {
1395+
detectZone.run(() => {
1396+
detectZone.runGuarded(() => {
1397+
throw new Error('blacklistStackFrames');
1398+
});
1399+
});
1400+
};
1401+
// Cause the error to extract the stack frames.
1402+
detectZone.runTask(detectZone.scheduleMacroTask('detect', detectRunFn, null, () => null, null));
1403+
12351404
return global.Zone = Zone;
12361405
})(typeof window === 'object' && window || typeof self === 'object' && self || global);

Diff for: test/common/Error.spec.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
describe('ZoneAwareError', () => {
10+
// If the environment does not supports stack rewrites, then these tests will fail
11+
// and there is no point in running them.
12+
if (!Error['stackRewrite']) return;
13+
14+
it('should show zone names in stack frames and remove extra frames', () => {
15+
const rootZone = getRootZone();
16+
const innerZone = rootZone.fork({name: 'InnerZone'});
17+
18+
rootZone.run(testFn);
19+
function testFn() {
20+
let outside: Error;
21+
let inside: Error;
22+
try {
23+
throw new Error('Outside');
24+
} catch(e) {
25+
outside = e;
26+
}
27+
innerZone.run(function insideRun () {
28+
try {
29+
throw new Error('Inside');
30+
} catch(e) {
31+
inside = e;
32+
}
33+
});
34+
35+
expect(outside.stack).toEqual(outside.zoneAwareStack);
36+
expect(inside.stack).toEqual(inside.zoneAwareStack);
37+
expect(typeof inside.originalStack).toEqual('string');
38+
const outsideFrames = outside.stack.split(/\n/);
39+
const insideFrames = inside.stack.split(/\n/);
40+
// throw away first line if it contains the error
41+
if (/Outside/.test(outsideFrames[0])) {
42+
outsideFrames.shift();
43+
}
44+
if (/new Error/.test(outsideFrames[0])) {
45+
outsideFrames.shift();
46+
}
47+
if (/Inside/.test(insideFrames[0])) {
48+
insideFrames.shift();
49+
}
50+
if (/new Error/.test(insideFrames[0])) {
51+
insideFrames.shift();
52+
}
53+
54+
expect(outsideFrames[0]).toMatch(/testFn.*[<root>]/);
55+
56+
expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
57+
expect(insideFrames[1]).toMatch(/run.*[<root> => InnerZone]]/);
58+
expect(insideFrames[2]).toMatch(/testFn.*[<root>]]/);
59+
}
60+
});
61+
});
62+
63+
function getRootZone() {
64+
let zone = Zone.current;
65+
while(zone.parent) {
66+
zone = zone.parent;
67+
}
68+
return zone;
69+
}

Diff for: test/common_tests.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import './common/microtasks.spec';
1010
import './common/zone.spec';
1111
import './common/util.spec';
1212
import './common/Promise.spec';
13+
import './common/Error.spec';
1314
import './common/setInterval.spec';
1415
import './common/setTimeout.spec';
1516
import './zone-spec/long-stack-trace-zone.spec';
@@ -18,3 +19,5 @@ import './zone-spec/sync-test.spec';
1819
import './zone-spec/fake-async-test.spec';
1920
import './zone-spec/proxy.spec';
2021
import './zone-spec/task-tracking.spec';
22+
23+
Error.stackTraceLimit = Number.POSITIVE_INFINITY;

0 commit comments

Comments
 (0)