Web Inspector: Remove unnecessary promise rejection handlers now that we use the...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Debug / UncaughtExceptionReporter.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 (function() {
27
28 const windowEvents = ["beforecopy", "copy", "click", "dragover", "focus"];
29 const documentEvents = ["focus", "blur", "resize", "keydown", "keyup", "mousemove", "pagehide", "contextmenu"];
30
31 function stopEventPropagation(event) {
32     if (event.target.classList && event.target.classList.contains("bypass-event-blocking"))
33         return;
34
35     event.stopPropagation();
36 }
37
38 function blockEventHandlers() {
39     // FIXME (151959): text selection on the sheet doesn't work for some reason.
40     for (let name of windowEvents)
41         window.addEventListener(name, stopEventPropagation, true);
42     for (let name of documentEvents)
43         document.addEventListener(name, stopEventPropagation, true);
44 }
45
46 function unblockEventHandlers() {
47     for (let name of windowEvents)
48         window.removeEventListener(name, stopEventPropagation, true);
49     for (let name of documentEvents)
50         document.removeEventListener(name, stopEventPropagation, true);
51 }
52
53 function urlLastPathComponent(url) {
54     if (!url)
55         return "";
56
57     let slashIndex = url.lastIndexOf("/");
58     if (slashIndex === -1)
59         return url;
60
61     return url.slice(slashIndex + 1);
62 }
63
64 function handleError(error) {
65     handleUncaughtExceptionRecord({
66         message: error.message,
67         url: urlLastPathComponent(error.sourceURL),
68         lineNumber: error.line,
69         columnNumber: error.column,
70         stack: error.stack,
71         details: error.details,
72     });
73 }
74
75 function handleUncaughtException(event) {
76     handleUncaughtExceptionRecord({
77         message: event.message,
78         url: urlLastPathComponent(event.filename),
79         lineNumber: event.lineno,
80         columnNumber: event.colno,
81         stack: typeof event.error === "object" && event.error !== null ? event.error.stack : null,
82     });
83 }
84
85 function handleUnhandledPromiseRejection(event) {
86     handleUncaughtExceptionRecord({
87         message: event.reason.message,
88         url: urlLastPathComponent(event.reason.sourceURL),
89         lineNumber: event.reason.line,
90         columnNumber: event.reason.column,
91         stack: event.reason.stack,
92     });
93 }
94
95 function handleUncaughtExceptionRecord(exceptionRecord) {
96     try {
97         if (!WI.settings.enableUncaughtExceptionReporter.value)
98             return;
99     } catch { }
100
101     if (!window.__uncaughtExceptions)
102         window.__uncaughtExceptions = [];
103
104     const loadCompleted = window.__frontendCompletedLoad;
105     const isFirstException = !window.__uncaughtExceptions.length;
106
107     // If an uncaught exception happens after loading is done, only show
108     // the first such exception. Many others may follow if internal
109     // state has been corrupted, but these are unhelpful to report.
110     if (!loadCompleted || isFirstException)
111         window.__uncaughtExceptions.push(exceptionRecord);
112
113     // If WI.contentLoaded throws an uncaught exception, then these
114     // listeners will not work correctly because the UI is not fully loaded.
115     // Prevent any event handlers from running in an inconsistent state.
116     if (isFirstException)
117         blockEventHandlers();
118
119     if (isFirstException && !loadCompleted) {
120         // Signal that loading is done even though we can't guarantee that
121         // evaluating code on the inspector page will do anything useful.
122         // Without this, the frontend host may never show the window.
123         if (window.InspectorFrontendHost)
124             InspectorFrontendHost.loaded();
125
126         // Don't tell InspectorFrontendAPI that loading is done, since it can
127         // clear some of the error boilerplate page by accident.
128     }
129
130     createErrorSheet();
131 }
132
133 function dismissErrorSheet() {
134     unblockEventHandlers();
135
136     window.__sheetElement.remove();
137     window.__sheetElement = null;
138     window.__uncaughtExceptions = [];
139
140     // Do this last in case WebInspector's internal state is corrupted.
141     try {
142         WI.updateWindowTitle();
143     } catch { }
144
145     // FIXME (151959): tell the frontend host to hide a draggable title bar.
146 }
147
148 function createErrorSheet() {
149     // Early errors like parse errors may happen in the <head>, so attach
150     // a body if none exists yet. Code below expects document.body to exist.
151     if (!document.body)
152         document.write("<body></body></html>");
153
154     // FIXME (151959): tell the frontend host to show a draggable title bar.
155     if (window.InspectorFrontendHost)
156         InspectorFrontendHost.inspectedURLChanged("Internal Error");
157
158     // Only allow one sheet element at a time.
159     if (window.__sheetElement) {
160         window.__sheetElement.remove();
161         window.__sheetElement = null;
162     }
163
164     const loadCompleted = window.__frontendCompletedLoad;
165     let firstException = window.__uncaughtExceptions[0];
166
167     // Inlined from Utilities.js, because that file may not have loaded.
168     function insertWordBreakCharacters(text) {
169         return text.replace(/([\/;:\)\]\}&?])/g, "$1\u200b");
170     }
171
172     // This trampoline is necessary since none of our functions will be
173     // in scope of an href="javascript:"-style evaluation.
174     function handleLinkClick(event) {
175         if (event.target.tagName !== "A")
176             return;
177         if (event.target.id === "dismiss-error-sheet")
178             dismissErrorSheet();
179     }
180
181     function formattedEntry(entry) {
182         const indent = "    ";
183         let lines = [`${entry.message} (at ${entry.url}:${entry.lineNumber}:${entry.columnNumber})`];
184         if (entry.stack) {
185             let stackLines = entry.stack.split(/\n/g);
186             for (let stackLine of stackLines) {
187                 let atIndex = stackLine.indexOf("@");
188                 let slashIndex = Math.max(stackLine.lastIndexOf("/"), atIndex);
189                 let functionName = stackLine.substring(0, atIndex) || "?";
190                 let location = stackLine.substring(slashIndex + 1, stackLine.length);
191                 lines.push(`${indent}${functionName} @ ${location}`);
192             }
193         }
194
195         if (entry.details) {
196             lines.push("");
197             lines.push("Additional Details:");
198             for (let key in entry.details) {
199                 let value = entry.details[key];
200                 lines.push(`${indent}${key} --> ${value}`);
201             }
202         }
203
204         return lines.join("\n");
205     }
206
207     let inspectedPageURL = null;
208     try {
209         inspectedPageURL = WI.networkManager.mainFrame.url;
210     } catch { }
211
212     let topLevelItems = [
213         `Inspected URL:        ${inspectedPageURL || "(unknown)"}`,
214         `Loading completed:    ${!!loadCompleted}`,
215         `Frontend User Agent:  ${window.navigator.userAgent}`,
216     ];
217
218     function stringifyAndTruncateObject(object) {
219         let string = JSON.stringify(object);
220         return string.length > 500 ? string.substr(0, 500) + ellipsis : string;
221     }
222
223     if (window.InspectorBackend && InspectorBackend.currentDispatchState) {
224         let state = InspectorBackend.currentDispatchState;
225         if (state.event) {
226             topLevelItems.push("Dispatch Source:      Protocol Event");
227             topLevelItems.push("");
228             topLevelItems.push("Protocol Event:");
229             topLevelItems.push(stringifyAndTruncateObject(state.event));
230         }
231         if (state.response) {
232             topLevelItems.push("Dispatch Source:      Protocol Command Response");
233             topLevelItems.push("");
234             topLevelItems.push("Protocol Command Response:");
235             topLevelItems.push(stringifyAndTruncateObject(state.response));
236         }
237         if (state.request) {
238             topLevelItems.push("");
239             topLevelItems.push("Protocol Command Request:");
240             topLevelItems.push(stringifyAndTruncateObject(state.request));
241         }
242     }
243
244     let formattedErrorDetails = window.__uncaughtExceptions.map((entry) => formattedEntry(entry));
245     let detailsForBugReport = formattedErrorDetails.map((line) => ` - ${line}`).join("\n");
246     topLevelItems.push("");
247     topLevelItems.push("Uncaught Exceptions:");
248     topLevelItems.push(detailsForBugReport);
249
250     let encodedBugDescription = encodeURIComponent(`-------
251 ${topLevelItems.join("\n")}
252 -------
253
254 * STEPS TO REPRODUCE
255 1. What were you doing? Include setup or other preparations to reproduce the exception.
256 2. Include explicit, accurate, and minimal steps taken. Do not include extraneous or irrelevant steps.
257
258 * NOTES
259 Document any additional information that might be useful in resolving the problem, such as screen shots or other included attachments.
260 `);
261     let encodedBugTitle = encodeURIComponent(`Uncaught Exception: ${firstException.message}`);
262     let encodedInspectedURL = encodeURIComponent(inspectedPageURL || "http://");
263     let prefilledBugReportLink = `https://bugs.webkit.org/enter_bug.cgi?alias=&assigned_to=webkit-unassigned%40lists.webkit.org&attach_text=&blocked=&bug_file_loc=${encodedInspectedURL}&bug_severity=Normal&bug_status=NEW&comment=${encodedBugDescription}&component=Web%20Inspector&contenttypeentry=&contenttypemethod=autodetect&contenttypeselection=text%2Fplain&data=&dependson=&description=&flag_type-1=X&flag_type-3=X&form_name=enter_bug&keywords=&op_sys=All&priority=P2&product=WebKit&rep_platform=All&short_desc=${encodedBugTitle}&version=WebKit%20Nightly%20Build`;
264     let detailsForHTML = formattedErrorDetails.map((line) => `<li>${insertWordBreakCharacters(line)}</li>`).join("\n");
265
266     let dismissOptionHTML = !loadCompleted ? "" : `<dt>A frivolous exception will not stop me!</dt>
267         <dd><a class="bypass-event-blocking" id="dismiss-error-sheet">Click to close this view</a> and return
268         to the Web Inspector without reloading. However, some things might not work without reloading if the error corrupted the Inspector's internal state.</dd>`;
269
270     let sheetElement = window.__sheetElement = document.createElement("div");
271     sheetElement.classList.add("sheet-container");
272     sheetElement.innerHTML = `<div class="uncaught-exception-sheet">
273     <h1>
274     <img src="Images/Errors.svg">
275     Web Inspector encountered an internal error.
276     </h1>
277     <dl>
278         <dd>Usually, this is caused by a syntax error while modifying the Web Inspector
279         UI, or running an updated frontend with out-of-date WebKit build.</dt>
280         <dt>I didn't do anything...?</dt>
281         <dd>If you don't think you caused this error to happen,
282         <a href="${prefilledBugReportLink}" target="_blank">click to file a pre-populated
283         bug with this information</a>. It is possible that someone else broke it by accident.</dd>
284         <dt>Oops, can I try again?</dt>
285         <dd><a href="javascript:InspectorFrontendHost.reopen()">Click to reload the Inspector</a>
286         again after making local changes.</dd>
287         ${dismissOptionHTML}
288     </dl>
289     <h2>
290     <img src="Images/Console.svg">
291     These uncaught exceptions caused the problem:
292     </h2>
293     <p><ul>${detailsForHTML}</ul></p>
294     </div>`;
295
296     sheetElement.addEventListener("click", handleLinkClick, true);
297     document.body.appendChild(sheetElement);
298 }
299
300 window.addEventListener("error", handleUncaughtException);
301 window.addEventListener("unhandledrejection", handleUnhandledPromiseRejection);
302 window.handleInternalException = handleError;
303
304 })();