Bug 1041895 - Add support for different flags with text filtering. r=rickychien draft
authorTim Nguyen <ntim.bugs@gmail.com>
Sun, 09 Apr 2017 17:42:00 +0200
changeset 559294 91e217701e43cc78fd638c09f889ca16e81af35f
parent 557359 0895c01064811d6b78aadc7bcae3ff1509a29bab
child 623355 351bb70947a488cef22875f3fa7620242c7d56ea
push id53049
push userbmo:ntim.bugs@gmail.com
push dateSun, 09 Apr 2017 15:42:29 +0000
reviewersrickychien
bugs1041895
milestone55.0a1
Bug 1041895 - Add support for different flags with text filtering. r=rickychien MozReview-Commit-ID: GvHXYIxwFrM
devtools/client/netmonitor/src/components/request-list-header.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/request-list-header-context-menu.js
devtools/client/netmonitor/src/utils/filter-predicates.js
devtools/client/netmonitor/src/utils/filter-text-utils.js
devtools/client/netmonitor/src/utils/moz.build
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_filter-flags.js
devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
toolkit/content/license.html
--- a/devtools/client/netmonitor/src/components/request-list-header.js
+++ b/devtools/client/netmonitor/src/components/request-list-header.js
@@ -8,37 +8,26 @@ const {
   createClass,
   PropTypes,
   DOM,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const Actions = require("../actions/index");
 const { getWaterfallScale } = require("../selectors/index");
 const { getFormattedTime } = require("../utils/format-utils");
+const { HEADERS } = require("../constants");
 const { L10N } = require("../utils/l10n");
 const WaterfallBackground = require("../waterfall-background");
 const RequestListHeaderContextMenu = require("../request-list-header-context-menu");
 
 const { div, button } = DOM;
 
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
 
-const HEADERS = [
-  { name: "status", label: "status3" },
-  { name: "method" },
-  { name: "file", boxName: "icon-and-file" },
-  { name: "domain", boxName: "security-and-domain" },
-  { name: "cause" },
-  { name: "type" },
-  { name: "transferred" },
-  { name: "contentSize", boxName: "size" },
-  { name: "waterfall" }
-];
-
 /**
  * Render the request list header with sorting arrows for columns.
  * Displays tick marks in the waterfall column header.
  * Also draws the waterfall background canvas and updates it when needed.
  */
 const RequestListHeader = createClass({
   displayName: "RequestListHeader",
 
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -105,18 +105,64 @@ const EVENTS = {
   PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed",
   EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed",
 
   // Fired once the NetMonitorController establishes a connection to the debug
   // target.
   CONNECTED: "connected",
 };
 
+const HEADERS = [
+  {
+    name: "status",
+    label: "status3",
+    canFilter: true,
+    filterKey: "status-code"
+  },
+  {
+    name: "method",
+    canFilter: true,
+  },
+  {
+    name: "file",
+    boxName: "icon-and-file",
+    canFilter: false,
+  },
+  {
+    name: "domain",
+    boxName: "security-and-domain",
+    canFilter: true,
+  },
+  {
+    name: "cause",
+    canFilter: true,
+  },
+  {
+    name: "type",
+    canFilter: false,
+  },
+  {
+    name: "transferred",
+    canFilter: true,
+  },
+  {
+    name: "contentSize",
+    boxName: "size",
+    filterKey: "size",
+    canFilter: true,
+  },
+  {
+    name: "waterfall",
+    canFilter: false,
+  }
+];
+
 const general = {
   ACTIVITY_TYPE,
   EVENTS,
   FILTER_SEARCH_DELAY: 200,
+  HEADERS,
   // 100 KB in bytes
   SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE: 102400,
 };
 
 // flatten constants
 module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/src/request-list-header-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-header-context-menu.js
@@ -1,21 +1,22 @@
 /* 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 Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
+const { HEADERS } = require("./constants");
 const { L10N } = require("./utils/l10n");
 
-const stringMap = {
-  status: "status3"
-};
+const stringMap = HEADERS
+  .filter((header) => header.hasOwnProperty("label"))
+  .reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
 
 class RequestListHeaderContextMenu {
   constructor({ toggleColumn, resetColumns }) {
     this.toggleColumn = toggleColumn;
     this.resetColumns = resetColumns;
   }
 
   get columns() {
--- a/devtools/client/netmonitor/src/utils/filter-predicates.js
+++ b/devtools/client/netmonitor/src/utils/filter-predicates.js
@@ -1,14 +1,16 @@
 /* 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 { isFreetextMatch } = require("./filter-text-utils");
+
 /**
  * Predicates used when filtering items.
  *
  * @param object item
  *        The filtered item.
  * @return boolean
  *         True if the item should be visible, false otherwise.
  */
@@ -97,37 +99,24 @@ function isWS({ requestHeaders, response
   return true;
 }
 
 function isOther(item) {
   let tests = [isHtml, isCss, isJs, isXHR, isFont, isImage, isMedia, isFlash, isWS];
   return tests.every(is => !is(item));
 }
 
-function isFreetextMatch({ url }, text) {
-  let lowerCaseUrl = url.toLowerCase();
-  let lowerCaseText = text.toLowerCase();
-  let textLength = text.length;
-  // Support negative filtering
-  if (text.startsWith("-") && textLength > 1) {
-    lowerCaseText = lowerCaseText.substring(1, textLength);
-    return !lowerCaseUrl.includes(lowerCaseText);
-  }
-
-  // no text is a positive match
-  return !text || lowerCaseUrl.includes(lowerCaseText);
-}
-
-exports.Filters = {
-  all: all,
-  html: isHtml,
-  css: isCss,
-  js: isJs,
-  xhr: isXHR,
-  fonts: isFont,
-  images: isImage,
-  media: isMedia,
-  flash: isFlash,
-  ws: isWS,
-  other: isOther,
+module.exports = {
+  Filters: {
+    all: all,
+    html: isHtml,
+    css: isCss,
+    js: isJs,
+    xhr: isXHR,
+    fonts: isFont,
+    images: isImage,
+    media: isMedia,
+    flash: isFlash,
+    ws: isWS,
+    other: isOther,
+  },
+  isFreetextMatch,
 };
-
-exports.isFreetextMatch = isFreetextMatch;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/filter-text-utils.js
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2013 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+"use strict";
+
+const { HEADERS } = require("../constants");
+const HEADER_FILTERS = HEADERS
+  .filter(h => h.canFilter)
+  .map(h => h.filterKey || h.name);
+
+const FILTER_FLAGS = [
+  ...HEADER_FILTERS,
+  "scheme",
+  "mime-type",
+  "larger-than",
+  "is",
+];
+
+/*
+  The function `parseFilters` is from:
+  https://github.com/ChromeDevTools/devtools-frontend/
+
+  front_end/network/FilterSuggestionBuilder.js#L138-L163
+  Commit f340aefd7ec9b702de9366a812288cfb12111fce
+*/
+
+function parseFilters(query) {
+  let flags = [];
+  let text = [];
+  let parts = query.split(/\s+/);
+
+  for (let part of parts) {
+    if (!part) {
+      continue;
+    }
+    let colonIndex = part.indexOf(":");
+    if (colonIndex === -1) {
+      text.push(part);
+      continue;
+    }
+    let key = part.substring(0, colonIndex);
+    let negative = key.startsWith("-");
+    if (negative) {
+      key = key.substring(1);
+    }
+    if (!FILTER_FLAGS.includes(key)) {
+      text.push(part);
+      continue;
+    }
+    let value = part.substring(colonIndex + 1);
+    value = processFlagFilter(key, value);
+    flags.push({
+      type: key,
+      value,
+      negative,
+    });
+  }
+
+  return { text, flags };
+}
+
+function processFlagFilter(type, value) {
+  switch (type) {
+    case "size":
+    case "transferred":
+    case "larger-than":
+      let multiplier = 1;
+      if (value.endsWith("k")) {
+        multiplier = 1024;
+        value = value.substring(0, value.length - 1);
+      } else if (value.endsWith("m")) {
+        multiplier = 1024 * 1024;
+        value = value.substring(0, value.length - 1);
+      }
+      let quantity = Number(value);
+      if (isNaN(quantity)) {
+        return null;
+      }
+      return quantity * multiplier;
+    default:
+      return value.toLowerCase();
+  }
+}
+
+function getSizeOrder(size) {
+  return Math.round(Math.log10(size));
+}
+
+function isFlagFilterMatch(item, { type, value, negative }) {
+  let match = true;
+  switch (type) {
+    case "status-code":
+      match = item.status === value;
+      break;
+    case "method":
+      match = item.method.toLowerCase() === value;
+      break;
+    case "domain":
+      match = item.urlDetails.host.toLowerCase().includes(value);
+      break;
+    case "cause":
+      let causeType = item.cause.type;
+      match = typeof causeType === "string" ?
+                causeType.toLowerCase().includes(value) : false;
+      break;
+    case "transferred":
+      if (item.fromCache) {
+        match = false;
+      } else {
+        match = getSizeOrder(value) === getSizeOrder(item.transferredSize);
+      }
+      break;
+    case "size":
+      match = getSizeOrder(value) === getSizeOrder(item.contentSize);
+      break;
+    case "larger-than":
+      match = item.contentSize > value;
+      break;
+    case "mime-type":
+      match = item.mimeType.includes(value);
+      break;
+    case "is":
+      if (value === "from-cache" ||
+          value === "cached") {
+        match = item.fromCache || item.status === "304";
+      } else if (value === "running") {
+        match = !item.status;
+      }
+      break;
+    case "scheme":
+      let scheme = new URL(item.url).protocol.replace(":", "").toLowerCase();
+      match = scheme === value;
+      break;
+  }
+  if (negative) {
+    return !match;
+  }
+  return match;
+}
+
+function isTextFilterMatch({ url }, text) {
+  let lowerCaseUrl = url.toLowerCase();
+  let lowerCaseText = text.toLowerCase();
+  let textLength = text.length;
+  // Support negative filtering
+  if (text.startsWith("-") && textLength > 1) {
+    lowerCaseText = lowerCaseText.substring(1, textLength);
+    return !lowerCaseUrl.includes(lowerCaseText);
+  }
+
+  // no text is a positive match
+  return !text || lowerCaseUrl.includes(lowerCaseText);
+}
+
+function isFreetextMatch(item, text) {
+  if (!text) {
+    return true;
+  }
+
+  let filters = parseFilters(text);
+  let match = true;
+
+  for (let textFilter of filters.text) {
+    match = match && isTextFilterMatch(item, textFilter);
+  }
+
+  for (let flagFilter of filters.flags) {
+    match = match && isFlagFilterMatch(item, flagFilter);
+  }
+
+  return match;
+}
+
+module.exports = {
+  isFreetextMatch,
+};
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -2,15 +2,16 @@
 # 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/.
 
 DevToolsModules(
     'client.js',
     'create-store.js',
     'filter-predicates.js',
+    'filter-text-utils.js',
     'format-utils.js',
     'l10n.js',
     'mdn-utils.js',
     'prefs.js',
     'request-utils.js',
     'sort-predicates.js',
 )
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -103,16 +103,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_net_frame.js]
 [browser_net_header-docs.js]
 skip-if = (os == 'linux' && debug && bits == 32) # Bug 1321434
 [browser_net_filter-01.js]
 skip-if = (os == 'linux' && debug && bits == 32) # Bug 1303439
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
+[browser_net_filter-flags.js]
 [browser_net_footer-summary.js]
 [browser_net_icon-preview.js]
 [browser_net_image-tooltip.js]
 [browser_net_json-b64.js]
 [browser_net_json-null.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
 [browser_net_json_custom_mime.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-flags.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test different text filtering flags
+ */
+const REQUESTS = [
+  { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" },
+  { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" },
+  { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" },
+  { url: "sjs_content-type-test-server.sjs?fmt=font" },
+  { url: "sjs_content-type-test-server.sjs?fmt=image" },
+  { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+  { url: "sjs_content-type-test-server.sjs?fmt=video" },
+  { url: "sjs_status-codes-test-server.sjs?sts=304" },
+];
+
+const EXPECTED_REQUESTS = [
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=html",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "html",
+      fullMimeType: "text/html; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=css",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "css",
+      fullMimeType: "text/css; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=js",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "js",
+      fullMimeType: "application/javascript; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=font",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "woff",
+      fullMimeType: "font/woff"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=image",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "png",
+      fullMimeType: "image/png"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=audio",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "ogg",
+      fullMimeType: "audio/ogg"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=video",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "webm",
+      fullMimeType: "video/webm"
+    },
+  },
+  {
+    method: "GET",
+    url: STATUS_CODES_SJS + "?sts=304",
+    data: {
+      status: 304,
+      statusText: "Not Modified",
+      displayedStatus: "304",
+      type: "plain",
+      fullMimeType: "text/plain; charset=utf-8"
+    }
+  },
+];
+
+add_task(function* () {
+  let { monitor } = yield initNetMonitor(FILTERING_URL);
+  let { document, gStore, windowRequire } = monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  let {
+    getDisplayedRequests,
+    getSortedRequests,
+  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+
+  gStore.dispatch(Actions.batchEnable(false));
+
+  function setFreetextFilter(value) {
+    gStore.dispatch(Actions.setRequestFilterText(value));
+  }
+
+  info("Starting test... ");
+
+  let waitNetwork = waitForNetworkEvents(monitor, 8);
+  loadCommonFrameScript();
+  yield performRequestsInContent(REQUESTS);
+  yield waitNetwork;
+
+  // Test running flag once requests finish running
+  setFreetextFilter("is:running");
+  testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+
+  // Test cached flag
+  setFreetextFilter("is:from-cache");
+  testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+
+  setFreetextFilter("is:cached");
+  testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+
+  // Test negative cached flag
+  setFreetextFilter("-is:from-cache");
+  testContents([1, 1, 1, 1, 1, 1, 1, 0]);
+
+  setFreetextFilter("-is:cached");
+  testContents([1, 1, 1, 1, 1, 1, 1, 0]);
+
+  // Test status-code flag
+  setFreetextFilter("status-code:200");
+  testContents([1, 1, 1, 1, 1, 1, 1, 0]);
+
+  // Test status-code negative flag
+  setFreetextFilter("-status-code:200");
+  testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+
+  // Test mime-type flag
+  setFreetextFilter("mime-type:HtmL");
+  testContents([1, 0, 0, 0, 0, 0, 0, 0]);
+
+  // Test mime-type negative flag
+  setFreetextFilter("-mime-type:HtmL");
+  testContents([0, 1, 1, 1, 1, 1, 1, 1]);
+
+  // Test method flag
+  setFreetextFilter("method:get");
+  testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+
+  // Test unmatched method flag
+  setFreetextFilter("method:post");
+  testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+
+  // Test scheme flag (all requests are http)
+  setFreetextFilter("scheme:http");
+  testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+
+  setFreetextFilter("scheme:https");
+  testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+
+  // Test mixing flags
+  setFreetextFilter("-mime-type:HtmL status-code:200");
+  testContents([0, 1, 1, 1, 1, 1, 1, 0]);
+
+  yield teardown(monitor);
+
+  function testContents(visibility) {
+    const items = getSortedRequests(gStore.getState());
+    const visibleItems = getDisplayedRequests(gStore.getState());
+
+    is(items.size, visibility.length,
+      "There should be a specific amount of items in the requests menu.");
+    is(visibleItems.size, visibility.filter(e => e).length,
+      "There should be a specific amount of visible items in the requests menu.");
+
+    for (let i = 0; i < visibility.length; i++) {
+      let itemId = items.get(i).id;
+      let shouldBeVisible = !!visibility[i];
+      let isThere = visibleItems.some(r => r.id == itemId);
+      is(isThere, shouldBeVisible,
+        `The item at index ${i} has visibility=${shouldBeVisible}`);
+
+      if (shouldBeVisible) {
+        let { method, url, data } = EXPECTED_REQUESTS[i];
+        verifyRequestItemTarget(
+          document,
+          getDisplayedRequests(gStore.getState()),
+          getSortedRequests(gStore.getState()).get(i),
+          method,
+          url,
+          data
+        );
+      }
+    }
+  }
+});
--- a/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
+++ b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -19,16 +19,19 @@ function handleRequest(request, response
         response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
         break;
       case "200":
         response.setStatusLine(request.httpVersion, 202, "Created");
         break;
       case "300":
         response.setStatusLine(request.httpVersion, 303, "See Other");
         break;
+      case "304":
+        response.setStatusLine(request.httpVersion, 304, "Not Modified");
+        break;
       case "400":
         response.setStatusLine(request.httpVersion, 404, "Not Found");
         break;
       case "500":
         response.setStatusLine(request.httpVersion, 501, "Not Implemented");
         break;
       case "ok":
         response.setStatusLine(request.httpVersion, 200, "OK");
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -3252,18 +3252,19 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE PO
 
 
 
     <hr>
 
     <h1><a id="google-bsd"></a>Google BSD License</h1>
 
     <p>This license applies to files in the directories
-    <span class="path">toolkit/crashreporter/google-breakpad/</span> and
-    <span class="path">toolkit/components/protobuf/</span>.</p>
+    <span class="path">toolkit/crashreporter/google-breakpad/</span>,
+    <span class="path">toolkit/components/protobuf/</span> and
+    <span class="path">devtools/client/netmonitor/src/utils/filter-text-utils.js.</p>
 
 <pre>
 Copyright (c) 2006, Google Inc.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met: