summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources/error-messages.js
blob: 7628d7fd6dcc834bbddb8f7b141b338ee41b3e58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const nsIConsoleListenerWatcher = require("resource://devtools/server/actors/resources/utils/nsi-console-listener-watcher.js");
const {
  DevToolsServer,
} = require("resource://devtools/server/devtools-server.js");
const ErrorDocs = require("resource://devtools/server/actors/errordocs.js");
const {
  createStringGrip,
  makeDebuggeeValue,
  createValueGripForTarget,
} = require("resource://devtools/server/actors/object/utils.js");
const {
  getActorIdForInternalSourceId,
} = require("resource://devtools/server/actors/utils/dbg-source.js");
const {
  WebConsoleUtils,
} = require("resource://devtools/server/actors/webconsole/utils.js");

const {
  TYPES: { ERROR_MESSAGE },
} = require("resource://devtools/server/actors/resources/index.js");
const Targets = require("resource://devtools/server/actors/targets/index.js");

const { MESSAGE_CATEGORY } = require("resource://devtools/shared/constants.js");

const PLATFORM_SPECIFIC_CATEGORIES = [
  "XPConnect JavaScript",
  "component javascript",
  "chrome javascript",
  "chrome registration",
];

class ErrorMessageWatcher extends nsIConsoleListenerWatcher {
  shouldHandleMessage(targetActor, message, isCachedMessage = false) {
    // The listener we use can be called either with a nsIConsoleMessage or a nsIScriptError.
    // In this file, we only want to handle nsIScriptError.
    if (
      // We only care about nsIScriptError
      !(message instanceof Ci.nsIScriptError) ||
      !this.isCategoryAllowed(targetActor, message.category) ||
      // Block any error that was triggered by eager evaluation
      message.sourceName === "debugger eager eval code"
    ) {
      return false;
    }

    // Filter specific to CONTENT PROCESS targets
    if (this.isProcessTarget(targetActor)) {
      // Don't want to display cached messages from private windows.
      const isCachedFromPrivateWindow =
        isCachedMessage && message.isFromPrivateWindow;
      if (isCachedFromPrivateWindow) {
        return false;
      }

      // `ContentChild` forwards all errors to the parent process (via IPC) all errors up
      // the parent process and sets a `isForwardedFromContentProcess` property on them.
      // Ignore these forwarded messages as the original ones will be logged either in a
      // content process target (if window-less message) or frame target (if related to a window)
      if (message.isForwardedFromContentProcess) {
        return false;
      }

      // Ignore all messages related to a given window for content process targets
      // These messages will be handled by Watchers instantiated for the related frame targets
      if (
        targetActor.targetType == Targets.TYPES.PROCESS &&
        message.innerWindowID
      ) {
        return false;
      }

      return true;
    }

    if (!message.innerWindowID) {
      return false;
    }

    const ids = targetActor.windows.map(window =>
      WebConsoleUtils.getInnerWindowId(window)
    );
    return ids.includes(message.innerWindowID);
  }

  /**
   * Check if the given message category is allowed to be tracked or not.
   * We ignore chrome-originating errors as we only care about content.
   *
   * @param string category
   *        The message category you want to check.
   * @return boolean
   *         True if the category is allowed to be logged, false otherwise.
   */
  isCategoryAllowed(targetActor, category) {
    // CSS Parser errors will be handled by the CSSMessageWatcher.
    if (!category || category === MESSAGE_CATEGORY.CSS_PARSER) {
      return false;
    }

    // We listen for everything on Process targets
    if (this.isProcessTarget(targetActor)) {
      return true;
    }

    // Don't restrict any categories in the Browser Toolbox/Browser Console
    if (targetActor.sessionContext.type == "all") {
      return true;
    }

    // For non-process targets in other toolboxes, we filter-out platform-specific errors.
    return !PLATFORM_SPECIFIC_CATEGORIES.includes(category);
  }

  /**
   * Prepare an nsIScriptError to be sent to the client.
   *
   * @param nsIScriptError error
   *        The page error we need to send to the client.
   * @return object
   *         The object you can send to the remote client.
   */
  buildResource(targetActor, error) {
    const stack = this.prepareStackForRemote(targetActor, error.stack);
    let lineText = error.sourceLine;
    if (
      lineText &&
      lineText.length > DevToolsServer.LONG_STRING_INITIAL_LENGTH
    ) {
      lineText = lineText.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
    }

    const notesArray = this.prepareNotesForRemote(targetActor, error.notes);

    // If there is no location information in the error but we have a stack,
    // fill in the location with the first frame on the stack.
    let { sourceName, sourceId, lineNumber, columnNumber } = error;
    if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) {
      sourceName = stack[0].filename;
      sourceId = stack[0].sourceId;
      lineNumber = stack[0].lineNumber;
      columnNumber = stack[0].columnNumber;
    }

    const pageError = {
      errorMessage: createStringGrip(targetActor, error.errorMessage),
      errorMessageName: error.errorMessageName,
      exceptionDocURL: ErrorDocs.GetURL(error),
      sourceName,
      sourceId: getActorIdForInternalSourceId(targetActor, sourceId),
      lineText,
      lineNumber,
      columnNumber,
      category: error.category,
      innerWindowID: error.innerWindowID,
      timeStamp: error.microSecondTimeStamp / 1000,
      warning: !!(error.flags & error.warningFlag),
      error: !(error.flags & (error.warningFlag | error.infoFlag)),
      info: !!(error.flags & error.infoFlag),
      private: error.isFromPrivateWindow,
      stacktrace: stack,
      notes: notesArray,
      chromeContext: error.isFromChromeContext,
      isPromiseRejection: error.isPromiseRejection,
      isForwardedFromContentProcess: error.isForwardedFromContentProcess,
    };

    // If the pageError does have an exception object, we want to return the grip for it,
    // but only if we do manage to get the grip, as we're checking the property on the
    // client to render things differently.
    if (error.hasException) {
      try {
        const obj = makeDebuggeeValue(targetActor, error.exception);
        if (obj?.class !== "DeadObject") {
          pageError.exception = createValueGripForTarget(targetActor, obj);
          pageError.hasException = true;
        }
      } catch (e) {}
    }

    return {
      pageError,
      resourceType: ERROR_MESSAGE,
    };
  }
}
module.exports = ErrorMessageWatcher;