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

Commit 67e17b6

Browse files
committed
feat(error): add a helper method to get non zone aware stack trace
1 parent f3547cc commit 67e17b6

File tree

7 files changed

+252
-5
lines changed

7 files changed

+252
-5
lines changed

Diff for: karma-dist.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ module.exports = function (config) {
1717
config.files.push('dist/sync-test.js');
1818
config.files.push('dist/task-tracking.js');
1919
config.files.push('dist/wtf.js');
20+
config.files.push('dist/zone-helper.js');
2021
config.files.push('build/test/main.js');
2122
};

Diff for: lib/zone-spec/long-stack-trace.ts

+145
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,151 @@ function captureStackTraces(stackTraces: string[][], count: number): void {
136136
}
137137
}
138138

139+
const zoneAwareStackFrames = {};
140+
Zone['__symbol__'][('zoneAwareStackFrames')] = zoneAwareStackFrames;
141+
const zoneAwareFunctionNames = [
142+
'Zone', 'drainMicrotask', 'getStacktraceWithUncaughtError', 'new LongStackTrace',
143+
'Object.onScheduleTask'
144+
];
145+
146+
function handleDetectError(error: Error) {
147+
let frames = error.stack ? error.stack.split(/\n/) : [];
148+
while (frames.length) {
149+
let frame = frames.shift();
150+
// On safari it is possible to have stack frame with no line number.
151+
// This check makes sure that we don't filter frames on name only (must have
152+
// linenumber)
153+
if (/:\d+:\d+/.test(frame)) {
154+
const f = frame.split(' [')[0];
155+
if (zoneAwareFunctionNames.filter(zf => f.toLowerCase().indexOf(f.toLowerCase()) !== -1)
156+
.length > 0) {
157+
zoneAwareStackFrames[f] = f;
158+
}
159+
}
160+
}
161+
}
162+
163+
const detectEmptyZone = Zone.root.fork(Zone['longStackTraceZoneSpec']).fork({
164+
name: 'detectEmptyZone',
165+
onHandleError(parentDelegate, currentZone, targetZone, error) {
166+
parentDelegate.handleError(targetZone, error);
167+
handleDetectError(error);
168+
return false;
169+
}
170+
});
171+
172+
const detectZoneWithCallbacks = Zone.root.fork(Zone['longStackTraceZoneSpec']).fork({
173+
name: 'detectCallbackZone',
174+
onFork: (parentDelegate, currentZone, targetZone, zoneSpec) => {
175+
return parentDelegate.fork(targetZone, zoneSpec);
176+
},
177+
onIntercept: (parentDelegate, currentZone, targetZone, delegate, source) => {
178+
return parentDelegate.intercept(targetZone, delegate, source);
179+
},
180+
onInvoke:
181+
(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) => {
182+
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
183+
},
184+
onScheduleTask: (parentZoneDelegate, currentZone, targetZone, task) => {
185+
return parentZoneDelegate.scheduleTask(targetZone, task);
186+
},
187+
onInvokeTask: (parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) => {
188+
return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
189+
},
190+
onCancelTask: (parentZoneDelegate, currentZone, targetZone, task) => {
191+
return parentZoneDelegate.cancelTask(targetZone, task);
192+
},
193+
194+
onHasTask: (delegate, current, target, hasTaskState) => {
195+
return delegate.hasTask(target, hasTaskState);
196+
},
197+
198+
onHandleError(parentDelegate, currentZone, targetZone, error) {
199+
parentDelegate.handleError(targetZone, error);
200+
handleDetectError(error);
201+
return false;
202+
}
203+
});
204+
205+
let detectFn = () => {
206+
throw new Error('zoneAwareFrames');
207+
};
208+
209+
let detectPromiseFn = () => {
210+
const p = new Promise((resolve, reject) => {
211+
reject(new Error('zoneAwareFrames'));
212+
});
213+
};
214+
215+
let detectPromiseCaughtFn = () => {
216+
const p = new Promise((resolve, reject) => {
217+
reject(new Error('zoneAwareFrames'));
218+
});
219+
p.catch(err => {
220+
throw err;
221+
});
222+
};
223+
224+
// Cause the error to extract the stack frames.
225+
detectEmptyZone.runTask(
226+
detectEmptyZone.scheduleEventTask('detect', detectFn, null, () => null, null));
227+
detectZoneWithCallbacks.runTask(
228+
detectZoneWithCallbacks.scheduleEventTask('detect', detectFn, null, () => null, null));
229+
detectEmptyZone.runTask(
230+
detectEmptyZone.scheduleMacroTask('detect', detectFn, null, () => null, null));
231+
detectZoneWithCallbacks.runTask(
232+
detectZoneWithCallbacks.scheduleMacroTask('detect', detectFn, null, () => null, null));
233+
detectEmptyZone.runTask(detectEmptyZone.scheduleMicroTask('detect', detectFn, null, () => null));
234+
detectZoneWithCallbacks.runTask(
235+
detectZoneWithCallbacks.scheduleMicroTask('detect', detectFn, null, () => null));
236+
237+
detectEmptyZone.runGuarded(() => {
238+
detectEmptyZone.run(detectFn);
239+
});
240+
detectZoneWithCallbacks.runGuarded(() => {
241+
detectEmptyZone.run(detectFn);
242+
});
243+
244+
detectEmptyZone.runGuarded(detectPromiseFn);
245+
detectZoneWithCallbacks.runGuarded(detectPromiseFn);
246+
247+
detectEmptyZone.runGuarded(detectPromiseCaughtFn);
248+
detectZoneWithCallbacks.runGuarded(detectPromiseCaughtFn);
249+
250+
// some functions are not easily to be detected here,
251+
// for example Timeout.ZoneTask.invoke, if we want to detect those functions
252+
// by detect zone, we have to run all patched APIs, it is too risky
253+
// so for those functions, just check whether the stack contains the string or not.
254+
const otherZoneAwareFunctionNames = ['ZoneTask.invoke', 'ZoneAware'];
255+
256+
Object.defineProperty(Error, 'getNonZoneAwareStack', {
257+
value: function(err: Error) {
258+
if (err.stack) {
259+
let frames = err.stack.split('\n');
260+
const simplifiedFrames: string[] = [];
261+
for (let i = 0; i < frames.length; i++) {
262+
const frame = frames[i].split(' [')[0];
263+
const frameWithoutZone = frame.split(' [')[0];
264+
if (zoneAwareStackFrames.hasOwnProperty(frameWithoutZone) &&
265+
zoneAwareStackFrames[frameWithoutZone]) {
266+
frames.splice(i, 1);
267+
i--;
268+
} else if (
269+
otherZoneAwareFunctionNames
270+
.filter(f => frame.toLowerCase().indexOf(f.toLowerCase()) !== -1)
271+
.length > 0) {
272+
frames.splice(i, 1);
273+
i--;
274+
} else {
275+
simplifiedFrames.push(frame);
276+
}
277+
}
278+
return simplifiedFrames.join('\n');
279+
}
280+
return err.stack;
281+
}
282+
});
283+
139284
function computeIgnoreFrames() {
140285
const frames: string[][] = [];
141286
captureStackTraces(frames, 2);

Diff for: lib/zone.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1668,8 +1668,8 @@ const Zone: ZoneType = (function(global: any) {
16681668
// This check makes sure that we don't filter frames on name only (must have
16691669
// linenumber)
16701670
if (/:\d+:\d+/.test(frame)) {
1671-
// Get rid of the path so that we don't accidintely find function name in path.
1672-
// In chrome the seperator is `(` and `@` in FF and safari
1671+
// Get rid of the path so that we don't accidentally find function name in path.
1672+
// In chrome the separator is `(` and `@` in FF and safari
16731673
// Chrome: at Zone.run (zone.js:100)
16741674
// Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24)
16751675
// FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24

Diff for: test/browser-zone-setup.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ import '../lib/zone-spec/long-stack-trace';
1313
import '../lib/zone-spec/proxy';
1414
import '../lib/zone-spec/sync-test';
1515
import '../lib/zone-spec/task-tracking';
16-
import '../lib/zone-spec/wtf';
16+
import '../lib/zone-spec/wtf';

Diff for: test/common_tests.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ import './zone-spec/sync-test.spec';
1919
import './zone-spec/fake-async-test.spec';
2020
import './zone-spec/proxy.spec';
2121
import './zone-spec/task-tracking.spec';
22+
import './helper/zone-helper.spec';
2223

2324
Error.stackTraceLimit = Number.POSITIVE_INFINITY;

Diff for: test/helper/zone-helper.spec.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
describe('zone helper util test', () => {
9+
const getNonZoneAwareStack: (err: Error) => string = Error['getNonZoneAwareStack'];
10+
const zoneAwareFrames = [
11+
'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask',
12+
'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask',
13+
'ZoneDelegate.invokeTask', 'ZoneTask.invoke', 'zoneAwareAddListener', 'drainMicroTaskQueue'
14+
];
15+
16+
const expectStack = function(err: Error) {
17+
const simpleStack = getNonZoneAwareStack(err);
18+
if (!simpleStack) {
19+
return;
20+
}
21+
const frames = simpleStack.split('\n');
22+
for (let i = 0; i < frames.length; i++) {
23+
expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1).length).toBe(0);
24+
}
25+
};
26+
27+
const errorZoneSpec = {
28+
name: 'errorZone',
29+
done: null,
30+
onHandleError: (parentDelegate, currentZone, targetZone, error) => {
31+
expectStack(error);
32+
setTimeout(() => {
33+
errorZoneSpec.done && errorZoneSpec.done();
34+
}, 0);
35+
return false;
36+
}
37+
};
38+
39+
const errorZone = Zone.root.fork(errorZoneSpec);
40+
41+
const errorTest = function(testFn: Function) {
42+
return function(done) {
43+
errorZoneSpec.done = done;
44+
errorZone.run(testFn);
45+
};
46+
};
47+
48+
it('test getNonZoneAwareStack', errorTest(() => {
49+
setTimeout(() => {
50+
throw new Error('test error');
51+
}, 10);
52+
}));
53+
54+
it('test promise getNonZoneAwareStack', (done) => {
55+
const p = new Promise((resolve, reject) => {
56+
reject(new Error('test error'));
57+
});
58+
p.catch(err => {
59+
expectStack(err);
60+
done();
61+
});
62+
});
63+
64+
it('test eventTask getNonZoneAwareStack', errorTest(() => {
65+
const task = Zone.current.scheduleEventTask('errorEvent', () => {
66+
throw new Error('test error');
67+
}, null, () => null, null);
68+
task.invoke();
69+
}));
70+
71+
it('test longStackTrace getNonZoneAwareStack', errorTest(() => {
72+
const task =
73+
Zone.current.fork(Zone['longStackTraceZoneSpec']).scheduleEventTask('errorEvent', () => {
74+
throw new Error('test error');
75+
}, null, () => null, null);
76+
task.invoke();
77+
}));
78+
79+
it('test custom zoneSpec getNonZoneAwareStack', errorTest(() => {
80+
// custom zone spec's stack trace should be keeped
81+
const task = Zone.current.fork(Zone['longStackTraceZoneSpec'])
82+
.fork({
83+
name: 'customZone',
84+
onScheduleTask: (parentDelegate, currentZone, targetZone, task) => {
85+
return parentDelegate.scheduleTask(targetZone, task);
86+
},
87+
onHandleError: (parentDelegate, currentZone, targetZone, error) => {
88+
parentDelegate.handleError(targetZone, error);
89+
const containsCustomZoneSpecStackTrace =
90+
getNonZoneAwareStack(error).indexOf('onScheduleTask') !== -1;
91+
expect(containsCustomZoneSpecStackTrace).toBeTruthy();
92+
return false;
93+
}
94+
})
95+
.scheduleEventTask('errorEvent', () => {
96+
throw new Error('test error');
97+
}, null, () => null, null);
98+
task.invoke();
99+
}));
100+
});

Diff for: test/node/fs.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('nodejs file system', () => {
6161
done();
6262
});
6363
});
64-
writeFile('testfile', 'test new content');
64+
writeFile('testfile', 'test new content', () => {});
6565
});
6666
});
6767
});
@@ -81,7 +81,7 @@ describe('nodejs file system', () => {
8181
done();
8282
});
8383
});
84-
writeFile('testfile', 'test new content');
84+
writeFile('testfile', 'test new content', () => {});
8585
});
8686
});
8787
});

0 commit comments

Comments
 (0)