error-rewrite.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. * @fileoverview
  10. * @suppress {globalThis,undefinedVars}
  11. */
  12. /**
  13. * Extend the Error with additional fields for rewritten stack frames
  14. */
  15. interface Error {
  16. /**
  17. * Stack trace where extra frames have been removed and zone names added.
  18. */
  19. zoneAwareStack?: string;
  20. /**
  21. * Original stack trace with no modifications
  22. */
  23. originalStack?: string;
  24. }
  25. Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
  26. /*
  27. * This code patches Error so that:
  28. * - It ignores un-needed stack frames.
  29. * - It Shows the associated Zone for reach frame.
  30. */
  31. const enum FrameType {
  32. /// Skip this frame when printing out stack
  33. blackList,
  34. /// This frame marks zone transition
  35. transition
  36. }
  37. const blacklistedStackFramesSymbol = api.symbol('blacklistedStackFrames');
  38. const NativeError = global[api.symbol('Error')] = global['Error'];
  39. // Store the frames which should be removed from the stack frames
  40. const blackListedStackFrames: {[frame: string]: FrameType} = {};
  41. // We must find the frame where Error was created, otherwise we assume we don't understand stack
  42. let zoneAwareFrame1: string;
  43. let zoneAwareFrame2: string;
  44. let zoneAwareFrame1WithoutNew: string;
  45. let zoneAwareFrame2WithoutNew: string;
  46. let zoneAwareFrame3WithoutNew: string;
  47. global['Error'] = ZoneAwareError;
  48. const stackRewrite = 'stackRewrite';
  49. type BlackListedStackFramesPolicy = 'default'|'disable'|'lazy';
  50. const blackListedStackFramesPolicy: BlackListedStackFramesPolicy =
  51. global['__Zone_Error_BlacklistedStackFrames_policy'] || 'default';
  52. interface ZoneFrameName {
  53. zoneName: string;
  54. parent?: ZoneFrameName;
  55. }
  56. function buildZoneFrameNames(zoneFrame: _ZoneFrame) {
  57. let zoneFrameName: ZoneFrameName = {zoneName: zoneFrame.zone.name};
  58. let result = zoneFrameName;
  59. while (zoneFrame.parent) {
  60. zoneFrame = zoneFrame.parent;
  61. const parentZoneFrameName = {zoneName: zoneFrame.zone.name};
  62. zoneFrameName.parent = parentZoneFrameName;
  63. zoneFrameName = parentZoneFrameName;
  64. }
  65. return result;
  66. }
  67. function buildZoneAwareStackFrames(
  68. originalStack: string, zoneFrame: _ZoneFrame|ZoneFrameName|null, isZoneFrame = true) {
  69. let frames: string[] = originalStack.split('\n');
  70. let i = 0;
  71. // Find the first frame
  72. while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2 ||
  73. frames[i] === zoneAwareFrame1WithoutNew || frames[i] === zoneAwareFrame2WithoutNew ||
  74. frames[i] === zoneAwareFrame3WithoutNew) &&
  75. i < frames.length) {
  76. i++;
  77. }
  78. for (; i < frames.length && zoneFrame; i++) {
  79. let frame = frames[i];
  80. if (frame.trim()) {
  81. switch (blackListedStackFrames[frame]) {
  82. case FrameType.blackList:
  83. frames.splice(i, 1);
  84. i--;
  85. break;
  86. case FrameType.transition:
  87. if (zoneFrame.parent) {
  88. // This is the special frame where zone changed. Print and process it accordingly
  89. zoneFrame = zoneFrame.parent;
  90. } else {
  91. zoneFrame = null;
  92. }
  93. frames.splice(i, 1);
  94. i--;
  95. break;
  96. default:
  97. frames[i] += isZoneFrame ? ` [${(zoneFrame as _ZoneFrame).zone.name}]` :
  98. ` [${(zoneFrame as ZoneFrameName).zoneName}]`;
  99. }
  100. }
  101. }
  102. return frames.join('\n');
  103. }
  104. /**
  105. * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as
  106. * adds zone information to it.
  107. */
  108. function ZoneAwareError(): Error {
  109. // We always have to return native error otherwise the browser console will not work.
  110. let error: Error = NativeError.apply(this, arguments);
  111. // Save original stack trace
  112. const originalStack = (error as any)['originalStack'] = error.stack;
  113. // Process the stack trace and rewrite the frames.
  114. if ((ZoneAwareError as any)[stackRewrite] && originalStack) {
  115. let zoneFrame = api.currentZoneFrame();
  116. if (blackListedStackFramesPolicy === 'lazy') {
  117. // don't handle stack trace now
  118. (error as any)[api.symbol('zoneFrameNames')] = buildZoneFrameNames(zoneFrame);
  119. } else if (blackListedStackFramesPolicy === 'default') {
  120. try {
  121. error.stack = error.zoneAwareStack = buildZoneAwareStackFrames(originalStack, zoneFrame);
  122. } catch (e) {
  123. // ignore as some browsers don't allow overriding of stack
  124. }
  125. }
  126. }
  127. if (this instanceof NativeError && this.constructor != NativeError) {
  128. // We got called with a `new` operator AND we are subclass of ZoneAwareError
  129. // in that case we have to copy all of our properties to `this`.
  130. Object.keys(error).concat('stack', 'message').forEach((key) => {
  131. const value = (error as any)[key];
  132. if (value !== undefined) {
  133. try {
  134. this[key] = value;
  135. } catch (e) {
  136. // ignore the assignment in case it is a setter and it throws.
  137. }
  138. }
  139. });
  140. return this;
  141. }
  142. return error;
  143. }
  144. // Copy the prototype so that instanceof operator works as expected
  145. ZoneAwareError.prototype = NativeError.prototype;
  146. (ZoneAwareError as any)[blacklistedStackFramesSymbol] = blackListedStackFrames;
  147. (ZoneAwareError as any)[stackRewrite] = false;
  148. const zoneAwareStackSymbol = api.symbol('zoneAwareStack');
  149. // try to define zoneAwareStack property when blackListed
  150. // policy is delay
  151. if (blackListedStackFramesPolicy === 'lazy') {
  152. Object.defineProperty(ZoneAwareError.prototype, 'zoneAwareStack', {
  153. configurable: true,
  154. enumerable: true,
  155. get: function() {
  156. if (!this[zoneAwareStackSymbol]) {
  157. this[zoneAwareStackSymbol] = buildZoneAwareStackFrames(
  158. this.originalStack, this[api.symbol('zoneFrameNames')], false);
  159. }
  160. return this[zoneAwareStackSymbol];
  161. },
  162. set: function(newStack: string) {
  163. this.originalStack = newStack;
  164. this[zoneAwareStackSymbol] = buildZoneAwareStackFrames(
  165. this.originalStack, this[api.symbol('zoneFrameNames')], false);
  166. }
  167. });
  168. }
  169. // those properties need special handling
  170. const specialPropertyNames = ['stackTraceLimit', 'captureStackTrace', 'prepareStackTrace'];
  171. // those properties of NativeError should be set to ZoneAwareError
  172. const nativeErrorProperties = Object.keys(NativeError);
  173. if (nativeErrorProperties) {
  174. nativeErrorProperties.forEach(prop => {
  175. if (specialPropertyNames.filter(sp => sp === prop).length === 0) {
  176. Object.defineProperty(ZoneAwareError, prop, {
  177. get: function() {
  178. return NativeError[prop];
  179. },
  180. set: function(value) {
  181. NativeError[prop] = value;
  182. }
  183. });
  184. }
  185. });
  186. }
  187. if (NativeError.hasOwnProperty('stackTraceLimit')) {
  188. // Extend default stack limit as we will be removing few frames.
  189. NativeError.stackTraceLimit = Math.max(NativeError.stackTraceLimit, 15);
  190. // make sure that ZoneAwareError has the same property which forwards to NativeError.
  191. Object.defineProperty(ZoneAwareError, 'stackTraceLimit', {
  192. get: function() {
  193. return NativeError.stackTraceLimit;
  194. },
  195. set: function(value) {
  196. return NativeError.stackTraceLimit = value;
  197. }
  198. });
  199. }
  200. if (NativeError.hasOwnProperty('captureStackTrace')) {
  201. Object.defineProperty(ZoneAwareError, 'captureStackTrace', {
  202. // add named function here because we need to remove this
  203. // stack frame when prepareStackTrace below
  204. value: function zoneCaptureStackTrace(targetObject: Object, constructorOpt?: Function) {
  205. NativeError.captureStackTrace(targetObject, constructorOpt);
  206. }
  207. });
  208. }
  209. const ZONE_CAPTURESTACKTRACE = 'zoneCaptureStackTrace';
  210. Object.defineProperty(ZoneAwareError, 'prepareStackTrace', {
  211. get: function() {
  212. return NativeError.prepareStackTrace;
  213. },
  214. set: function(value) {
  215. if (!value || typeof value !== 'function') {
  216. return NativeError.prepareStackTrace = value;
  217. }
  218. return NativeError.prepareStackTrace = function(
  219. error: Error, structuredStackTrace: {getFunctionName: Function}[]) {
  220. // remove additional stack information from ZoneAwareError.captureStackTrace
  221. if (structuredStackTrace) {
  222. for (let i = 0; i < structuredStackTrace.length; i++) {
  223. const st = structuredStackTrace[i];
  224. // remove the first function which name is zoneCaptureStackTrace
  225. if (st.getFunctionName() === ZONE_CAPTURESTACKTRACE) {
  226. structuredStackTrace.splice(i, 1);
  227. break;
  228. }
  229. }
  230. }
  231. return value.call(this, error, structuredStackTrace);
  232. };
  233. }
  234. });
  235. if (blackListedStackFramesPolicy === 'disable') {
  236. // don't need to run detectZone to populate
  237. // blacklisted stack frames
  238. return;
  239. }
  240. // Now we need to populate the `blacklistedStackFrames` as well as find the
  241. // run/runGuarded/runTask frames. This is done by creating a detect zone and then threading
  242. // the execution through all of the above methods so that we can look at the stack trace and
  243. // find the frames of interest.
  244. let detectZone: Zone = Zone.current.fork({
  245. name: 'detect',
  246. onHandleError: function(
  247. parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): boolean {
  248. if (error.originalStack && Error === ZoneAwareError) {
  249. let frames = error.originalStack.split(/\n/);
  250. let runFrame = false, runGuardedFrame = false, runTaskFrame = false;
  251. while (frames.length) {
  252. let frame = frames.shift();
  253. // On safari it is possible to have stack frame with no line number.
  254. // This check makes sure that we don't filter frames on name only (must have
  255. // line number or exact equals to `ZoneAwareError`)
  256. if (/:\d+:\d+/.test(frame) || frame === 'ZoneAwareError') {
  257. // Get rid of the path so that we don't accidentally find function name in path.
  258. // In chrome the separator is `(` and `@` in FF and safari
  259. // Chrome: at Zone.run (zone.js:100)
  260. // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24)
  261. // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24
  262. // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24
  263. let fnName: string = frame.split('(')[0].split('@')[0];
  264. let frameType = FrameType.transition;
  265. if (fnName.indexOf('ZoneAwareError') !== -1) {
  266. if (fnName.indexOf('new ZoneAwareError') !== -1) {
  267. zoneAwareFrame1 = frame;
  268. zoneAwareFrame2 = frame.replace('new ZoneAwareError', 'new Error.ZoneAwareError');
  269. } else {
  270. zoneAwareFrame1WithoutNew = frame;
  271. zoneAwareFrame2WithoutNew = frame.replace('Error.', '');
  272. if (frame.indexOf('Error.ZoneAwareError') === -1) {
  273. zoneAwareFrame3WithoutNew =
  274. frame.replace('ZoneAwareError', 'Error.ZoneAwareError');
  275. }
  276. }
  277. blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList;
  278. }
  279. if (fnName.indexOf('runGuarded') !== -1) {
  280. runGuardedFrame = true;
  281. } else if (fnName.indexOf('runTask') !== -1) {
  282. runTaskFrame = true;
  283. } else if (fnName.indexOf('run') !== -1) {
  284. runFrame = true;
  285. } else {
  286. frameType = FrameType.blackList;
  287. }
  288. blackListedStackFrames[frame] = frameType;
  289. // Once we find all of the frames we can stop looking.
  290. if (runFrame && runGuardedFrame && runTaskFrame) {
  291. (ZoneAwareError as any)[stackRewrite] = true;
  292. break;
  293. }
  294. }
  295. }
  296. }
  297. return false;
  298. }
  299. }) as Zone;
  300. // carefully constructor a stack frame which contains all of the frames of interest which
  301. // need to be detected and blacklisted.
  302. const childDetectZone = detectZone.fork({
  303. name: 'child',
  304. onScheduleTask: function(delegate, curr, target, task) {
  305. return delegate.scheduleTask(target, task);
  306. },
  307. onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
  308. return delegate.invokeTask(target, task, applyThis, applyArgs);
  309. },
  310. onCancelTask: function(delegate, curr, target, task) {
  311. return delegate.cancelTask(target, task);
  312. },
  313. onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs, source) {
  314. return delegate.invoke(target, callback, applyThis, applyArgs, source);
  315. }
  316. });
  317. // we need to detect all zone related frames, it will
  318. // exceed default stackTraceLimit, so we set it to
  319. // larger number here, and restore it after detect finish.
  320. const originalStackTraceLimit = Error.stackTraceLimit;
  321. Error.stackTraceLimit = 100;
  322. // we schedule event/micro/macro task, and invoke them
  323. // when onSchedule, so we can get all stack traces for
  324. // all kinds of tasks with one error thrown.
  325. childDetectZone.run(() => {
  326. childDetectZone.runGuarded(() => {
  327. const fakeTransitionTo = () => {};
  328. childDetectZone.scheduleEventTask(
  329. blacklistedStackFramesSymbol,
  330. () => {
  331. childDetectZone.scheduleMacroTask(
  332. blacklistedStackFramesSymbol,
  333. () => {
  334. childDetectZone.scheduleMicroTask(
  335. blacklistedStackFramesSymbol,
  336. () => {
  337. throw new Error();
  338. },
  339. undefined,
  340. (t: Task) => {
  341. (t as any)._transitionTo = fakeTransitionTo;
  342. t.invoke();
  343. });
  344. childDetectZone.scheduleMicroTask(
  345. blacklistedStackFramesSymbol,
  346. () => {
  347. throw Error();
  348. },
  349. undefined,
  350. (t: Task) => {
  351. (t as any)._transitionTo = fakeTransitionTo;
  352. t.invoke();
  353. });
  354. },
  355. undefined,
  356. (t) => {
  357. (t as any)._transitionTo = fakeTransitionTo;
  358. t.invoke();
  359. },
  360. () => {});
  361. },
  362. undefined,
  363. (t) => {
  364. (t as any)._transitionTo = fakeTransitionTo;
  365. t.invoke();
  366. },
  367. () => {});
  368. });
  369. });
  370. Error.stackTraceLimit = originalStackTraceLimit;
  371. });