Bug 1520957 - [release 119] Implement Event Listeners Panel (#7715). r=dwalsh
☠☠ backed out by 5b1c54cbac38 ☠ ☠
authorDavid Walsh <davidwalsh83@gmail.com>
Fri, 18 Jan 2019 12:18:50 -0500
changeset 514563 2a0d5bc0699151aa7d4d66b4476090dc17540391
parent 514562 07d417fb91d28f5b9566999ba672660e13f54a77
child 514564 314832e1b78382986a9b70c4c7e4ca8439f38f33
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdwalsh
bugs1520957
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1520957 - [release 119] Implement Event Listeners Panel (#7715). r=dwalsh
devtools/client/debugger/new/dist/debugger.css
devtools/client/debugger/new/src/actions/event-listeners.js
devtools/client/debugger/new/src/actions/index.js
devtools/client/debugger/new/src/actions/moz.build
devtools/client/debugger/new/src/client/firefox/commands.js
devtools/client/debugger/new/src/client/firefox/types.js
devtools/client/debugger/new/src/client/index.js
devtools/client/debugger/new/src/components/SecondaryPanes/EventListeners.css
devtools/client/debugger/new/src/components/SecondaryPanes/EventListeners.js
devtools/client/debugger/new/src/components/SecondaryPanes/index.js
devtools/client/debugger/new/src/components/SecondaryPanes/moz.build
devtools/client/debugger/new/src/reducers/event-listeners.js
devtools/client/debugger/new/src/reducers/index.js
devtools/client/debugger/new/src/reducers/moz.build
devtools/client/debugger/new/src/selectors/index.js
devtools/client/debugger/new/src/types.js
devtools/client/debugger/new/src/utils/prefs.js
devtools/client/preferences/debugger.js
--- a/devtools/client/debugger/new/dist/debugger.css
+++ b/devtools/client/debugger/new/dist/debugger.css
@@ -4106,16 +4106,54 @@ html[dir="rtl"] .command-bar {
   inset-inline-start: auto;
   position: absolute;
   top: 8px;
 }
 /* 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/>. */
 
+.event-listeners-pane ul {
+	padding: 0;
+}
+
+.event-listener-group {
+	user-select: none;
+}
+
+.event-listener-category {
+	font-weight: bold;
+}
+
+.event-listeners-pane .arrow {
+  margin-inline-start: 4px;
+  margin-top: 1px;
+}
+
+html[dir="ltr"] .event-listeners-pane .arrow.expanded {
+  transform: rotate(0deg);
+}
+
+html[dir="rtl"] .event-listeners-pane .arrow.expanded {
+  transform: rotate(90deg);
+}
+
+.event-listener-event {
+	display: flex;
+	align-items: center;
+}
+
+.event-listener-event input {
+	margin-inline-end: 6px;
+	margin-inline-start: 40px;
+}
+/* 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/>. */
+
 .object-node.default-property {
   opacity: 0.6;
 }
 
 .object-node {
   padding-left: 4px;
 }
 
--- a/devtools/client/debugger/new/src/actions/event-listeners.js
+++ b/devtools/client/debugger/new/src/actions/event-listeners.js
@@ -1,168 +1,32 @@
 /* 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/>. */
 
-/* global window gThreadClient setNamedTimeout EVENTS */
-/* eslint no-shadow: 0  */
-
-/**
- * Redux actions for the event listeners state
- * @module actions/event-listeners
- */
-
-import { reportException } from "../utils/DevToolsUtils";
-import { isPaused, getSourceByURL } from "../selectors";
-import { NAME as WAIT_UNTIL } from "./utils/middleware/wait-service";
-
-// delay is in ms
-const FETCH_EVENT_LISTENERS_DELAY = 200;
-let fetchListenersTimerID;
+// @flow
 
-/**
- * @memberof utils/utils
- * @static
- */
-async function asPaused(state: any, client: any, func: any) {
-  if (!isPaused(state)) {
-    await client.interrupt();
-    let result;
+import { asyncStore } from "../utils/prefs";
 
-    try {
-      result = await func(client);
-    } catch (e) {
-      // Try to put the debugger back in a working state by resuming
-      // it
-      await client.resume();
-      throw e;
-    }
-
-    await client.resume();
-    return result;
-  }
+import type { ThunkArgs } from "./types";
+import type { EventListenerBreakpoints } from "../types";
 
-  return func(client);
-}
-
-/**
- * @memberof actions/event-listeners
- * @static
- */
-export function fetchEventListeners() {
-  return ({ dispatch, getState, client }) => {
-    // Make sure we"re not sending a batch of closely repeated requests.
-    // This can easily happen whenever new sources are fetched.
-    if (fetchListenersTimerID) {
-      clearTimeout(fetchListenersTimerID);
-    }
-
-    fetchListenersTimerID = setTimeout(() => {
-      // In case there is still a request of listeners going on (it
-      // takes several RDP round trips right now), make sure we wait
-      // on a currently running request
-      if (getState().eventListeners.fetchingListeners) {
-        dispatch({
-          type: WAIT_UNTIL,
-          predicate: action =>
-            action.type === "FETCH_EVENT_LISTENERS" && action.status === "done",
-          run: dispatch => dispatch(fetchEventListeners())
-        });
-        return;
-      }
-
-      dispatch({
-        type: "FETCH_EVENT_LISTENERS",
-        status: "begin"
-      });
-
-      asPaused(getState(), client, _getEventListeners).then(listeners => {
-        dispatch({
-          type: "FETCH_EVENT_LISTENERS",
-          status: "done",
-          listeners: formatListeners(getState(), listeners)
-        });
-      });
-    }, FETCH_EVENT_LISTENERS_DELAY);
+export function addEventListeners(events: EventListenerBreakpoints) {
+  return async ({ dispatch, client }: ThunkArgs) => {
+    await dispatch({
+      type: "ADD_EVENT_LISTENERS",
+      events
+    });
+    const newList = await asyncStore.eventListenerBreakpoints;
+    client.setEventListenerBreakpoints(newList);
   };
 }
 
-function formatListeners(state, listeners) {
-  return listeners.map(l => {
-    return {
-      selector: l.node.selector,
-      type: l.type,
-      sourceId: getSourceByURL(state, l.function.location.url).id,
-      line: l.function.location.line
-    };
-  });
-}
-
-async function _getEventListeners(threadClient) {
-  const response = await threadClient.eventListeners();
-
-  // Make sure all the listeners are sorted by the event type, since
-  // they"re not guaranteed to be clustered together.
-  response.listeners.sort((a, b) => (a.type > b.type ? 1 : -1));
-
-  // Add all the listeners in the debugger view event linsteners container.
-  const fetchedDefinitions = new Map();
-  const listeners = [];
-  for (const listener of response.listeners) {
-    let definitionSite;
-    if (fetchedDefinitions.has(listener.function.actor)) {
-      definitionSite = fetchedDefinitions.get(listener.function.actor);
-    } else if (listener.function.class == "Function") {
-      definitionSite = await _getDefinitionSite(
-        threadClient,
-        listener.function
-      );
-      if (!definitionSite) {
-        // We don"t know where this listener comes from so don"t show it in
-        // the UI as breaking on it doesn"t work (bug 942899).
-        continue;
-      }
-
-      fetchedDefinitions.set(listener.function.actor, definitionSite);
-    }
-    listener.function.url = definitionSite;
-    listeners.push(listener);
-  }
-  fetchedDefinitions.clear();
-
-  return listeners;
-}
-
-async function _getDefinitionSite(threadClient, func) {
-  const grip = threadClient.pauseGrip(func);
-  let response;
-
-  try {
-    response = await grip.getDefinitionSite();
-  } catch (e) {
-    // Don't make this error fatal, it would break the entire events pane.
-    reportException("_getDefinitionSite", e);
-    return null;
-  }
-
-  return response.source.url;
-}
-
-/**
- * @memberof actions/event-listeners
- * @static
- * @param {string} eventNames
- */
-export function updateEventBreakpoints(eventNames) {
-  return dispatch => {
-    setNamedTimeout("event-breakpoints-update", 0, () => {
-      gThreadClient.pauseOnDOMEvents(eventNames, () => {
-        // Notify that event breakpoints were added/removed on the server.
-        window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
-
-        dispatch({
-          type: "UPDATE_EVENT_BREAKPOINTS",
-          eventNames: eventNames
-        });
-      });
+export function removeEventListeners(events: EventListenerBreakpoints) {
+  return async ({ dispatch, client }: ThunkArgs) => {
+    await dispatch({
+      type: "REMOVE_EVENT_LISTENERS",
+      events
     });
+    const newList = await asyncStore.eventListenerBreakpoints;
+    client.setEventListenerBreakpoints(newList);
   };
 }
--- a/devtools/client/debugger/new/src/actions/index.js
+++ b/devtools/client/debugger/new/src/actions/index.js
@@ -1,16 +1,17 @@
 /* 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/>. */
 
 // @flow
 
 import * as breakpoints from "./breakpoints";
 import * as expressions from "./expressions";
+import * as eventListeners from "./event-listeners";
 import * as pause from "./pause";
 import * as navigation from "./navigation";
 import * as ui from "./ui";
 import * as fileSearch from "./file-search";
 import * as ast from "./ast";
 import * as projectTextSearch from "./project-text-search";
 import * as quickOpen from "./quick-open";
 import * as sourceTree from "./source-tree";
@@ -19,16 +20,17 @@ import * as tabs from "./tabs";
 import * as debuggee from "./debuggee";
 import * as toolbox from "./toolbox";
 import * as preview from "./preview";
 
 export default {
   ...navigation,
   ...breakpoints,
   ...expressions,
+  ...eventListeners,
   ...sources,
   ...tabs,
   ...pause,
   ...ui,
   ...fileSearch,
   ...ast,
   ...projectTextSearch,
   ...quickOpen,
--- a/devtools/client/debugger/new/src/actions/moz.build
+++ b/devtools/client/debugger/new/src/actions/moz.build
@@ -9,16 +9,17 @@ DIRS += [
     'pause',
     'sources',
     'utils',
 ]
 
 DebuggerModules(
     'ast.js',
     'debuggee.js',
+    'event-listeners.js',
     'expressions.js',
     'file-search.js',
     'index.js',
     'navigation.js',
     'preview.js',
     'project-text-search.js',
     'quick-open.js',
     'source-tree.js',
--- a/devtools/client/debugger/new/src/client/firefox/commands.js
+++ b/devtools/client/debugger/new/src/client/firefox/commands.js
@@ -2,16 +2,17 @@
  * 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/>. */
 
 // @flow
 
 import type {
   BreakpointId,
   BreakpointResult,
+  EventListenerBreakpoints,
   Frame,
   FrameId,
   SourceLocation,
   Script,
   Source,
   SourceId,
   Worker
 } from "../../types";
@@ -381,16 +382,20 @@ async function setSkipPausing(thread: st
 function interrupt(thread: string): Promise<*> {
   return lookupThreadClient(thread).interrupt();
 }
 
 function eventListeners(): Promise<*> {
   return threadClient.eventListeners();
 }
 
+function setEventListenerBreakpoints(eventTypes: EventListenerBreakpoints) {
+  // TODO: Figure out what sendpoint we want to hit
+}
+
 function pauseGrip(thread: string, func: Function): ObjectClient {
   return lookupThreadClient(thread).pauseGrip(func);
 }
 
 function registerSource(source: Source) {
   if (isOriginalId(source.id)) {
     throw new Error("registerSource called with original ID");
   }
@@ -478,12 +483,13 @@ const clientCommands = {
   pauseOnExceptions,
   prettyPrint,
   disablePrettyPrint,
   fetchSources,
   fetchWorkers,
   sendPacket,
   setPausePoints,
   setSkipPausing,
+  setEventListenerBreakpoints,
   registerSource
 };
 
 export { setupCommands, clientCommands };
--- a/devtools/client/debugger/new/src/client/firefox/types.js
+++ b/devtools/client/debugger/new/src/client/firefox/types.js
@@ -366,17 +366,18 @@ export type ThreadClient = {
   getEnvironment: (frame: Frame) => Promise<*>,
   addListener: (string, Function) => void,
   getSources: () => Promise<SourcesPacket>,
   reconfigure: ({ observeAsmJS: boolean }) => Promise<*>,
   getLastPausePacket: () => ?PausedPacket,
   _parent: TabClient,
   actor: ActorId,
   request: (payload: Object) => Promise<*>,
-  url: string
+  url: string,
+  setEventListenerBreakpoints: (string[]) => void
 };
 
 /**
  * BreakpointClient
  * @memberof firefox
  * @static
  */
 export type BreakpointClient = {
--- a/devtools/client/debugger/new/src/client/index.js
+++ b/devtools/client/debugger/new/src/client/index.js
@@ -36,20 +36,21 @@ function syncXHRBreakpoints() {
     });
   });
 }
 
 async function loadInitialState() {
   const pendingBreakpoints = await asyncStore.pendingBreakpoints;
   const tabs = await asyncStore.tabs;
   const xhrBreakpoints = await asyncStore.xhrBreakpoints;
+  const eventListenerBreakpoints = await asyncStore.eventListenerBreakpoints;
 
   const breakpoints = initialBreakpointsState(xhrBreakpoints);
 
-  return { pendingBreakpoints, tabs, breakpoints };
+  return { pendingBreakpoints, tabs, breakpoints, eventListenerBreakpoints };
 }
 
 function getClient(connection: any) {
   const {
     tab: { clientType }
   } = connection;
   return clientType == "firefox" ? firefox : chrome;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/EventListeners.css
@@ -0,0 +1,38 @@
+/* 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/>. */
+
+.event-listeners-pane ul {
+	padding: 0;
+}
+
+.event-listener-group {
+	user-select: none;
+}
+
+.event-listener-category {
+	font-weight: bold;
+}
+
+.event-listeners-pane .arrow {
+  margin-inline-start: 4px;
+  margin-top: 1px;
+}
+
+html[dir="ltr"] .event-listeners-pane .arrow.expanded {
+  transform: rotate(0deg);
+}
+
+html[dir="rtl"] .event-listeners-pane .arrow.expanded {
+  transform: rotate(90deg);
+}
+
+.event-listener-event {
+	display: flex;
+	align-items: center;
+}
+
+.event-listener-event input {
+	margin-inline-end: 6px;
+	margin-inline-start: 40px;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/EventListeners.js
@@ -0,0 +1,179 @@
+/* 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/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import classnames from "classnames";
+
+import { connect } from "../../utils/connect";
+import actions from "../../actions";
+import { getActiveEventListeners } from "../../selectors";
+
+import AccessibleImage from "../shared/AccessibleImage";
+
+import type { EventListenerBreakpoints } from "../../types";
+
+import "./EventListeners.css";
+
+const CATEGORIES = {
+  Mouse: ["click", "mouseover", "dblclick"],
+  Keyboard: ["keyup", "keydown"]
+};
+
+type Props = {
+  addEventListeners: typeof actions.addEventListeners,
+  removeEventListeners: typeof actions.removeEventListeners,
+  activeEventListeners: EventListenerBreakpoints
+};
+
+type State = {
+  expandedCategories: string[]
+};
+
+function getKey(category: string, eventType: string) {
+  return `${category}:${eventType}`;
+}
+
+class EventListeners extends Component<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      expandedCategories: []
+    };
+  }
+
+  onCategoryToggle(category, event) {
+    event.preventDefault();
+
+    const { expandedCategories } = this.state;
+
+    if (expandedCategories.includes(category)) {
+      this.setState({
+        expandedCategories: expandedCategories.filter(
+          eventCategory => eventCategory !== category
+        )
+      });
+    } else {
+      this.setState({
+        expandedCategories: [...expandedCategories, category]
+      });
+    }
+  }
+
+  onCategoryClick(category, isChecked) {
+    const { addEventListeners, removeEventListeners } = this.props;
+    const events = CATEGORIES[category].map(eventType =>
+      getKey(category, eventType)
+    );
+
+    if (isChecked) {
+      addEventListeners(events);
+    } else {
+      removeEventListeners(events);
+    }
+  }
+
+  onEventTypeClick(eventType, isChecked) {
+    const { addEventListeners, removeEventListeners } = this.props;
+    if (isChecked) {
+      addEventListeners([eventType]);
+    } else {
+      removeEventListeners([eventType]);
+    }
+  }
+
+  renderCategoryHeading(category) {
+    const { expandedCategories } = this.state;
+    const { activeEventListeners } = this.props;
+
+    const eventTypes = CATEGORIES[category];
+
+    const expanded = expandedCategories.includes(category);
+    const checked = eventTypes.every(eventType =>
+      activeEventListeners.includes(getKey(category, eventType))
+    );
+    const indeterminate =
+      !checked &&
+      eventTypes.some(eventType =>
+        activeEventListeners.includes(getKey(category, eventType))
+      );
+
+    return (
+      <label>
+        <AccessibleImage
+          className={classnames("arrow", { expanded })}
+          onClick={e => this.onCategoryToggle(category, e)}
+        />
+        <input
+          type="checkbox"
+          value={category}
+          onChange={e => this.onCategoryClick(category, e.target.checked)}
+          checked={checked}
+          ref={el => el && (el.indeterminate = indeterminate)}
+        />
+        <span className="event-listener-category">{category}</span>
+      </label>
+    );
+  }
+
+  renderCategoryListing(category) {
+    const { activeEventListeners } = this.props;
+    const { expandedCategories } = this.state;
+
+    const expanded = expandedCategories.includes(category);
+    if (!expanded) {
+      return null;
+    }
+
+    return (
+      <ul>
+        {CATEGORIES[category].map(eventType => {
+          const key = getKey(category, eventType);
+          return (
+            <li className="event-listener-event" key={key}>
+              <label>
+                <input
+                  type="checkbox"
+                  value={key}
+                  onChange={e => this.onEventTypeClick(key, e.target.checked)}
+                  checked={activeEventListeners.includes(key)}
+                />
+                {eventType}
+              </label>
+            </li>
+          );
+        })}
+      </ul>
+    );
+  }
+
+  render() {
+    return (
+      <ul className="event-listeners-list">
+        {Object.keys(CATEGORIES).map(category => {
+          return (
+            <li className="event-listener-group" key={category}>
+              {this.renderCategoryHeading(category)}
+              {this.renderCategoryListing(category)}
+            </li>
+          );
+        })}
+      </ul>
+    );
+  }
+}
+
+const mapStateToProps = state => ({
+  activeEventListeners: getActiveEventListeners(state)
+});
+
+export default connect(
+  mapStateToProps,
+  {
+    addEventListeners: actions.addEventListeners,
+    removeEventListeners: actions.removeEventListeners
+  }
+)(EventListeners);
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/index.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/index.js
@@ -28,16 +28,17 @@ import Breakpoints from "./Breakpoints";
 import Expressions from "./Expressions";
 import SplitBox from "devtools-splitter";
 import Frames from "./Frames";
 import Workers from "./Workers";
 import Accordion from "../shared/Accordion";
 import CommandBar from "./CommandBar";
 import UtilsBar from "./UtilsBar";
 import XHRBreakpoints from "./XHRBreakpoints";
+import EventListeners from "./EventListeners";
 
 import Scopes from "./Scopes";
 
 import "./SecondaryPanes.css";
 
 import type { Expression, WorkerList } from "../../types";
 
 type AccordionPaneItem = {
@@ -294,16 +295,29 @@ class SecondaryPanes extends Component<P
       ),
       opened: prefs.breakpointsVisible,
       onToggle: opened => {
         prefs.breakpointsVisible = opened;
       }
     };
   }
 
+  getEventListenersItem() {
+    return {
+      header: L10N.getStr("eventListenersHeader"),
+      className: "event-listeners-pane",
+      buttons: [],
+      component: <EventListeners />,
+      opened: prefs.eventListenersVisible,
+      onToggle: opened => {
+        prefs.eventListenersVisible = opened;
+      }
+    };
+  }
+
   getStartItems() {
     const { workers } = this.props;
 
     const items: Array<AccordionPaneItem> = [];
     if (this.props.horizontal) {
       if (features.workers && workers.length > 0) {
         items.push(this.getWorkersItem());
       }
@@ -320,16 +334,20 @@ class SecondaryPanes extends Component<P
         items.push(this.getScopeItem());
       }
     }
 
     if (features.xhrBreakpoints) {
       items.push(this.getXHRItem());
     }
 
+    if (features.eventListenersBreakpoints) {
+      items.push(this.getEventListenersItem());
+    }
+
     return items.filter(item => item);
   }
 
   renderHorizontalLayout() {
     return <Accordion items={this.getItems()} />;
   }
 
   getEndItems() {
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/moz.build
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/moz.build
@@ -5,16 +5,17 @@
 
 DIRS += [
     'Breakpoints',
     'Frames',
 ]
 
 DebuggerModules(
     'CommandBar.js',
+    'EventListeners.js',
     'Expressions.js',
     'index.js',
     'Scopes.js',
     'UtilsBar.js',
     'Worker.js',
     'Workers.js',
     'XHRBreakpoints.js',
 )
--- a/devtools/client/debugger/new/src/reducers/event-listeners.js
+++ b/devtools/client/debugger/new/src/reducers/event-listeners.js
@@ -1,41 +1,49 @@
 /* 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/>. */
 
-/**
- * Event listeners reducer
- * @module reducers/event-listeners
- */
+// @flow
+
+import { uniq } from "lodash";
 
-const initialEventListenersState = {
-  activeEventNames: [],
-  listeners: [],
-  fetchingListeners: false
-};
+import { asyncStore } from "../utils/prefs";
+import type { EventListenerBreakpoints } from "../types";
 
-function update(state = initialEventListenersState, action, emit) {
+type OuterState = { eventListenerBreakpoints: EventListenerBreakpoints };
+
+function update(state: EventListenerBreakpoints = [], action: any) {
+  console.log("update; state is: ", state);
   switch (action.type) {
-    case "UPDATE_EVENT_BREAKPOINTS":
-      state.activeEventNames = action.eventNames;
-      // emit("activeEventNames", state.activeEventNames);
-      break;
-    case "FETCH_EVENT_LISTENERS":
-      if (action.status === "begin") {
-        state.fetchingListeners = true;
-      } else if (action.status === "done") {
-        state.fetchingListeners = false;
-        state.listeners = action.listeners;
-      }
-      break;
-    case "NAVIGATE":
-      return initialEventListenersState;
+    case "ADD_EVENT_LISTENERS":
+      return updateEventTypes("add", state, action.events);
+
+    case "REMOVE_EVENT_LISTENERS":
+      return updateEventTypes("remove", state, action.events);
+
+    default:
+      return state;
+  }
+}
+
+function updateEventTypes(
+  addOrRemove: string,
+  currentEvents: EventListenerBreakpoints,
+  events: EventListenerBreakpoints
+): EventListenerBreakpoints {
+  let newEventListeners;
+
+  if (addOrRemove === "add") {
+    newEventListeners = uniq([...currentEvents, ...events]);
+  } else {
+    newEventListeners = currentEvents.filter(event => !events.includes(event));
   }
 
-  return state;
+  asyncStore.eventListenerBreakpoints = newEventListeners;
+  return newEventListeners;
 }
 
-export function getEventListeners(state) {
-  return state.eventListeners.listeners;
+export function getActiveEventListeners(state: OuterState) {
+  return state.eventListenerBreakpoints;
 }
 
 export default update;
--- a/devtools/client/debugger/new/src/reducers/index.js
+++ b/devtools/client/debugger/new/src/reducers/index.js
@@ -17,26 +17,28 @@ import pause from "./pause";
 import ui from "./ui";
 import fileSearch from "./file-search";
 import ast from "./ast";
 import projectTextSearch from "./project-text-search";
 import quickOpen from "./quick-open";
 import sourceTree from "./source-tree";
 import debuggee from "./debuggee";
 import { objectInspector } from "devtools-reps";
+import eventListenerBreakpoints from "./event-listeners";
 
 export default {
   expressions,
   sources,
   tabs,
   breakpoints,
   pendingBreakpoints,
   asyncRequests,
   pause,
   ui,
   fileSearch,
   ast,
   projectTextSearch,
   quickOpen,
   sourceTree,
   debuggee,
-  objectInspector: objectInspector.reducer.default
+  objectInspector: objectInspector.reducer.default,
+  eventListenerBreakpoints
 };
--- a/devtools/client/debugger/new/src/reducers/moz.build
+++ b/devtools/client/debugger/new/src/reducers/moz.build
@@ -7,16 +7,17 @@ DIRS += [
 
 ]
 
 DebuggerModules(
     'ast.js',
     'async-requests.js',
     'breakpoints.js',
     'debuggee.js',
+    'event-listeners.js',
     'expressions.js',
     'file-search.js',
     'index.js',
     'pause.js',
     'pending-breakpoints.js',
     'project-text-search.js',
     'quick-open.js',
     'source-tree.js',
--- a/devtools/client/debugger/new/src/selectors/index.js
+++ b/devtools/client/debugger/new/src/selectors/index.js
@@ -2,16 +2,17 @@
  * 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/>. */
 
 // @flow
 
 export * from "../reducers/expressions";
 export * from "../reducers/sources";
 export * from "../reducers/tabs";
+export * from "../reducers/event-listeners";
 export * from "../reducers/pause";
 export * from "../reducers/debuggee";
 export * from "../reducers/breakpoints";
 export * from "../reducers/pending-breakpoints";
 export * from "../reducers/ui";
 export * from "../reducers/file-search";
 export * from "../reducers/ast";
 export * from "../reducers/project-text-search";
--- a/devtools/client/debugger/new/src/types.js
+++ b/devtools/client/debugger/new/src/types.js
@@ -405,8 +405,10 @@ export type Worker = {
 
 export type Thread = MainThread & Worker;
 export type ThreadList = Array<Thread>;
 export type WorkerList = Array<Worker>;
 
 export type Cancellable = {
   cancel: () => void
 };
+
+export type EventListenerBreakpoints = string[];
--- a/devtools/client/debugger/new/src/utils/prefs.js
+++ b/devtools/client/debugger/new/src/utils/prefs.js
@@ -23,16 +23,17 @@ if (isDevelopment()) {
   pref("devtools.debugger.ignore-caught-exceptions", true);
   pref("devtools.debugger.call-stack-visible", true);
   pref("devtools.debugger.scopes-visible", true);
   pref("devtools.debugger.component-visible", true);
   pref("devtools.debugger.workers-visible", true);
   pref("devtools.debugger.expressions-visible", true);
   pref("devtools.debugger.xhr-breakpoints-visible", true);
   pref("devtools.debugger.breakpoints-visible", true);
+  pref("devtools.debugger.event-listeners-visible", true);
   pref("devtools.debugger.start-panel-collapsed", false);
   pref("devtools.debugger.end-panel-collapsed", false);
   pref("devtools.debugger.start-panel-size", 300);
   pref("devtools.debugger.end-panel-size", 300);
   pref("devtools.debugger.tabs", "[]");
   pref("devtools.debugger.tabsBlackBoxed", "[]");
   pref("devtools.debugger.ui.framework-grouping-on", true);
   pref("devtools.debugger.pending-selected-location", "{}");
@@ -58,16 +59,17 @@ if (isDevelopment()) {
   pref("devtools.debugger.features.skip-pausing", true);
   pref("devtools.debugger.features.component-pane", false);
   pref("devtools.debugger.features.autocomplete-expressions", false);
   pref("devtools.debugger.features.map-expression-bindings", true);
   pref("devtools.debugger.features.map-await-expression", true);
   pref("devtools.debugger.features.xhr-breakpoints", true);
   pref("devtools.debugger.features.original-blackbox", true);
   pref("devtools.debugger.features.windowless-workers", false);
+  pref("devtools.debugger.features.event-listeners-breakpoints", true);
 }
 
 export const prefs = new PrefsHelper("devtools", {
   logging: ["Bool", "debugger.alphabetize-outline"],
   alphabetizeOutline: ["Bool", "debugger.alphabetize-outline"],
   autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
   clientSourceMapsEnabled: ["Bool", "source-map.client-service.enabled"],
   pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
@@ -75,16 +77,17 @@ export const prefs = new PrefsHelper("de
   ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
   callStackVisible: ["Bool", "debugger.call-stack-visible"],
   scopesVisible: ["Bool", "debugger.scopes-visible"],
   componentVisible: ["Bool", "debugger.component-visible"],
   workersVisible: ["Bool", "debugger.workers-visible"],
   breakpointsVisible: ["Bool", "debugger.breakpoints-visible"],
   expressionsVisible: ["Bool", "debugger.expressions-visible"],
   xhrBreakpointsVisible: ["Bool", "debugger.xhr-breakpoints-visible"],
+  eventListenersVisible: ["Bool", "debugger.event-listeners-visible"],
   startPanelCollapsed: ["Bool", "debugger.start-panel-collapsed"],
   endPanelCollapsed: ["Bool", "debugger.end-panel-collapsed"],
   startPanelSize: ["Int", "debugger.start-panel-size"],
   endPanelSize: ["Int", "debugger.end-panel-size"],
   frameworkGroupingOn: ["Bool", "debugger.ui.framework-grouping-on"],
   tabs: ["Json", "debugger.tabs", []],
   tabsBlackBoxed: ["Json", "debugger.tabsBlackBoxed", []],
   pendingSelectedLocation: ["Json", "debugger.pending-selected-location", {}],
@@ -112,23 +115,25 @@ export const features = new PrefsHelper(
   codeFolding: ["Bool", "code-folding"],
   pausePoints: ["Bool", "pause-points"],
   skipPausing: ["Bool", "skip-pausing"],
   autocompleteExpression: ["Bool", "autocomplete-expressions"],
   mapExpressionBindings: ["Bool", "map-expression-bindings"],
   mapAwaitExpression: ["Bool", "map-await-expression"],
   componentPane: ["Bool", "component-pane"],
   xhrBreakpoints: ["Bool", "xhr-breakpoints"],
-  originalBlackbox: ["Bool", "original-blackbox"]
+  originalBlackbox: ["Bool", "original-blackbox"],
+  eventListenersBreakpoints: ["Bool", "event-listeners-breakpoints"]
 });
 
 export const asyncStore = asyncStoreHelper("debugger", {
   pendingBreakpoints: ["pending-breakpoints", {}],
   tabs: ["tabs", []],
-  xhrBreakpoints: ["xhr-breakpoints", []]
+  xhrBreakpoints: ["xhr-breakpoints", []],
+  eventListenerBreakpoints: ["event-listener-breakpoints", []]
 });
 
 if (prefs.debuggerPrefsSchemaVersion !== prefsSchemaVersion) {
   // clear pending Breakpoints
   prefs.pendingBreakpoints = {};
   prefs.tabs = [];
   prefs.xhrBreakpoints = [];
   prefs.debuggerPrefsSchemaVersion = prefsSchemaVersion;
--- a/devtools/client/preferences/debugger.js
+++ b/devtools/client/preferences/debugger.js
@@ -28,25 +28,27 @@ pref("devtools.debugger.ui.variables-sea
 pref("devtools.debugger.ui.framework-grouping-on", true);
 pref("devtools.debugger.call-stack-visible", true);
 pref("devtools.debugger.scopes-visible", true);
 pref("devtools.debugger.component-visible", true);
 pref("devtools.debugger.workers-visible", true);
 pref("devtools.debugger.breakpoints-visible", true);
 pref("devtools.debugger.expressions-visible", true);
 pref("devtools.debugger.xhr-breakpoints-visible", true);
+pref("devtools.debugger.event-listeners-visible", false);
 pref("devtools.debugger.start-panel-collapsed", false);
 pref("devtools.debugger.end-panel-collapsed", false);
 pref("devtools.debugger.start-panel-size", 300);
 pref("devtools.debugger.end-panel-size", 300);
 pref("devtools.debugger.tabs", "[]");
 pref("devtools.debugger.tabsBlackBoxed", "[]");
 pref("devtools.debugger.pending-selected-location", "{}");
 pref("devtools.debugger.pending-breakpoints", "{}");
 pref("devtools.debugger.expressions", "[]");
+pref("devtools.debugger.event-listener-breakpoints", "[]");
 pref("devtools.debugger.file-search-case-sensitive", false);
 pref("devtools.debugger.file-search-whole-word", false);
 pref("devtools.debugger.file-search-regex-match", false);
 pref("devtools.debugger.project-directory-root", "");
 pref("devtools.debugger.skip-pausing", false);
 pref("devtools.debugger.logging", false);
 
 pref("devtools.debugger.features.wasm", true);
@@ -64,8 +66,9 @@ pref("devtools.debugger.features.pause-p
 pref("devtools.debugger.features.component-pane", false);
 pref("devtools.debugger.features.async-stepping", true);
 pref("devtools.debugger.features.skip-pausing", true);
 pref("devtools.debugger.features.autocomplete-expressions", false);
 pref("devtools.debugger.features.map-expression-bindings", true);
 pref("devtools.debugger.features.xhr-breakpoints", true);
 pref("devtools.debugger.features.original-blackbox", true);
 pref("devtools.debugger.features.windowless-workers", false);
+pref("devtools.debugger.features.event-listeners-breakpoints", false);