Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 25 Apr 2018 12:38:44 +0300
changeset 469157 6696fd50114a8c105771f1e87e0b76cbf5761c26
parent 469156 aa4185068f2e3ffb1377afa0e0d95b3c5ac0a04c (current diff)
parent 469045 a83a4ef50f6ca754ec451320dfefbffa707bad1a (diff)
child 469158 37320f8b708c6aad8914e0d1da0b90affc1b613f
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.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
Merge mozilla-central to inbound. a=merge CLOSED TREE
layout/style/nsStyleStruct.cpp
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/gradients/shape-outside-radial-gradient-002.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/gradients/shape-outside-radial-gradient-003.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-006.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-007.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-008.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-009.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-010.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-011.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-018.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-019.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-020.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-021.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-022.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/shape-image/shape-image-023.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-017.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-018.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-019.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-020.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-021.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-022.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-024.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-025.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-026.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-027.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-028.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-029.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-015.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-017.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-018.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-019.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-020.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-021.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-022.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-023.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-024.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-025.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-010.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-011.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-012.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-013.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-014.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-015.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/values/shape-margin-000.html.ini
testing/web-platform/meta/css/css-shapes/shape-outside/values/shape-margin-004.html.ini
testing/web-platform/meta/css/css-shapes/spec-examples/shape-outside-013.html.ini
testing/web-platform/meta/css/css-shapes/spec-examples/shape-outside-019.html.ini
testing/web-platform/meta/service-workers/service-worker/worker-interception-redirect.https.html.ini
toolkit/content/widgets/scrollbar.xml
--- a/.taskcluster.yml
+++ b/.taskcluster.yml
@@ -107,27 +107,27 @@ tasks:
                 ACTION_TASK_GROUP_ID: '${ownTaskId}'
                 ACTION_TASK_ID: {$json: {$eval: 'taskId'}}
                 ACTION_TASK: {$json: {$eval: 'task'}}
                 ACTION_INPUT: {$json: {$eval: 'input'}}
                 ACTION_CALLBACK: '${action.cb_name}'
                 ACTION_PARAMETERS: {$json: {$eval: 'parameters'}}
 
         cache:
-          level-${repository.level}-checkouts-sparse-v1: /builds/worker/checkouts
+          level-${repository.level}-checkouts-sparse-v2: /builds/worker/checkouts
 
         features:
           taskclusterProxy: true
           chainOfTrust: true
 
         # Note: This task is built server side without the context or tooling that
         # exist in tree so we must hard code the hash
         # XXX Changing this will break Chain of Trust without an associated puppet and
         # scriptworker patch!
-        image: 'taskcluster/decision:2.0.0@sha256:4039fd878e5700b326d4a636e28c595c053fbcb53909c1db84ad1f513cf644ef'
+        image: 'taskcluster/decision:2.1.0@sha256:6db3b697d7a3c7aba440d72f04199331b872111cefff57206b8b8b1d53230360'
 
         maxRunTime: 1800
 
         command:
           - /builds/worker/bin/run-task
           - '--vcs-checkout=/builds/worker/checkouts/gecko'
           - '--sparse-profile=build/sparse-profiles/taskgraph'
           - '--'
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1423,16 +1423,19 @@ pref("identity.mobilepromo.ios", "https:
 // Migrate any existing Firefox Account data from the default profile to the
 // Developer Edition profile.
 #ifdef MOZ_DEV_EDITION
 pref("identity.fxaccounts.migrateToDevEdition", true);
 #else
 pref("identity.fxaccounts.migrateToDevEdition", false);
 #endif
 
+// If activated, send tab will use the new FxA messages backend.
+pref("identity.fxaccounts.messages.enabled", false);
+
 // On GTK, we now default to showing the menubar only when alt is pressed:
 #ifdef MOZ_WIDGET_GTK
 pref("ui.key.menuAccessKeyFocuses", true);
 #endif
 
 #ifdef NIGHTLY_BUILD
 pref("media.eme.vp9-in-mp4.enabled", true);
 #else
--- a/browser/base/content/browser-sidebar.js
+++ b/browser/base/content/browser-sidebar.js
@@ -184,16 +184,21 @@ var SidebarUI = {
       appcontent.ordinal = boxOrdinal;
       // Indicate we've switched ordering to the box
       this._box.setAttribute("positionend", true);
     } else {
       this._box.removeAttribute("positionend");
     }
 
     this.hideSwitcherPanel();
+
+    let content = SidebarUI.browser.contentWindow;
+    if (content && content.updatePosition) {
+      content.updatePosition();
+    }
   },
 
   /**
    * Try and adopt the status of the sidebar from another window.
    * @param {Window} sourceWindow - Window to use as a source for sidebar status.
    * @return true if we adopted the state, or false if the caller should
    * initialize the state itself.
    */
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -314,20 +314,44 @@ var gSync = {
   },
 
   openSendToDevicePromo() {
     let url = this.PRODUCT_INFO_BASE_URL;
     url += "send-tabs/?utm_source=" + Services.appinfo.name.toLowerCase();
     switchToTabHavingURI(url, true, { replaceQueryString: true });
   },
 
-  sendTabToDevice(url, clientId, title) {
-    Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title).catch(e => {
-      console.error("Could not send tab to device", e);
-    });
+  async sendTabToDevice(url, clients, title) {
+    let devices;
+    try {
+      devices = await fxAccounts.getDeviceList();
+    } catch (e) {
+      console.error("Could not get the FxA device list", e);
+      devices = []; // We can still run in degraded mode.
+    }
+    const toSendMessages = [];
+    for (const client of clients) {
+      const device = devices.find(d => d.id == client.fxaDeviceId);
+      if (device && fxAccounts.messages.canReceiveSendTabMessages(device)) {
+        toSendMessages.push(device);
+      } else {
+        try {
+          await Weave.Service.clientsEngine.sendURIToClientForDisplay(url, client.id, title);
+        } catch (e) {
+          console.error("Could not send tab to device", e);
+        }
+      }
+    }
+    if (toSendMessages.length) {
+      try {
+        await fxAccounts.messages.sendTab(toSendMessages, {url, title});
+      } catch (e) {
+        console.error("Could not send tab to device", e);
+      }
+    }
   },
 
   populateSendTabToDevicesMenu(devicesPopup, url, title, createDeviceNodeFn) {
     if (!createDeviceNodeFn) {
       createDeviceNodeFn = (clientId, name, clientType, lastModified) => {
         let eltName = name ? "menuitem" : "menuseparator";
         return document.createElement(eltName);
       };
@@ -358,28 +382,33 @@ var gSync = {
       this._appendSendTabVerify(fragment, createDeviceNodeFn);
     } else /* status is STATUS_NOT_CONFIGURED */ {
       this._appendSendTabUnconfigured(fragment, createDeviceNodeFn);
     }
 
     devicesPopup.appendChild(fragment);
   },
 
+  // TODO: once our transition from the old-send tab world is complete,
+  // this list should be built using the FxA device list instead of the client
+  // collection.
   _appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title) {
+    const onSendAllCommand = (event) => {
+      this.sendTabToDevice(url, this.remoteClients, title);
+    };
     const onTargetDeviceCommand = (event) => {
-      let clients = event.target.getAttribute("clientId") ?
-        [event.target.getAttribute("clientId")] :
-        this.remoteClients.map(client => client.id);
-
-      clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+      const clientId = event.target.getAttribute("clientId");
+      const client = this.remoteClients.find(c => c.id == clientId);
+      this.sendTabToDevice(url, [client], title);
     };
 
     function addTargetDevice(clientId, name, clientType, lastModified) {
       const targetDevice = createDeviceNodeFn(clientId, name, clientType, lastModified);
-      targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+      targetDevice.addEventListener("command", clientId ? onTargetDeviceCommand :
+                                                          onSendAllCommand, true);
       targetDevice.classList.add("sync-menuitem", "sendtab-target");
       targetDevice.setAttribute("clientId", clientId);
       targetDevice.setAttribute("clientType", clientType);
       targetDevice.setAttribute("label", name);
       fragment.appendChild(targetDevice);
     }
 
     const clients = this.remoteClients;
--- a/browser/base/content/webext-panels.js
+++ b/browser/base/content/webext-panels.js
@@ -88,16 +88,27 @@ var gBrowser = {
   getTabModalPromptBox(browser) {
     if (!browser.tabModalPromptBox) {
       browser.tabModalPromptBox = new TabModalPromptBox(browser);
     }
     return browser.tabModalPromptBox;
   },
 };
 
+function updatePosition() {
+  // We need both of these to make sure we update the position
+  // after any lower level updates have finished.
+  requestAnimationFrame(() => setTimeout(() => {
+    let browser = document.getElementById("webext-panels-browser");
+    if (browser && browser.isRemoteBrowser) {
+      browser.frameLoader.requestUpdatePosition();
+    }
+  }, 0));
+}
+
 function loadPanel(extensionId, extensionUrl, browserStyle) {
   let browserEl = document.getElementById("webext-panels-browser");
   if (browserEl) {
     if (browserEl.currentURI.spec === extensionUrl) {
       return;
     }
     // Forces runtime disconnect.  Remove the stack (parent).
     browserEl.parentNode.remove();
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -439,16 +439,17 @@ BrowserGlue.prototype = {
         this._onVerifyLoginNotification(JSON.parse(data));
         break;
       case "fxaccounts:device_disconnected":
         data = JSON.parse(data);
         if (data.isLocalDevice) {
           this._onDeviceDisconnected();
         }
         break;
+      case "fxaccounts:messages:display-tabs":
       case "weave:engine:clients:display-uris":
         this._onDisplaySyncURIs(subject);
         break;
       case "session-save":
         this._setPrefToSaveSession(true);
         subject.QueryInterface(Ci.nsISupportsPRBool);
         subject.data = true;
         break;
@@ -593,16 +594,17 @@ BrowserGlue.prototype = {
       os.addObserver(this, "browser-lastwindow-close-requested");
       os.addObserver(this, "browser-lastwindow-close-granted");
     }
     os.addObserver(this, "weave:service:ready");
     os.addObserver(this, "fxaccounts:onverified");
     os.addObserver(this, "fxaccounts:device_connected");
     os.addObserver(this, "fxaccounts:verify_login");
     os.addObserver(this, "fxaccounts:device_disconnected");
+    os.addObserver(this, "fxaccounts:messages:display-tabs");
     os.addObserver(this, "weave:engine:clients:display-uris");
     os.addObserver(this, "session-save");
     os.addObserver(this, "places-init-complete");
     os.addObserver(this, "distribution-customization-complete");
     os.addObserver(this, "handle-xul-text-link");
     os.addObserver(this, "profile-before-change");
     os.addObserver(this, "keyword-search");
     os.addObserver(this, "browser-search-engine-modified");
@@ -635,16 +637,17 @@ BrowserGlue.prototype = {
       os.removeObserver(this, "browser-lastwindow-close-requested");
       os.removeObserver(this, "browser-lastwindow-close-granted");
     }
     os.removeObserver(this, "weave:service:ready");
     os.removeObserver(this, "fxaccounts:onverified");
     os.removeObserver(this, "fxaccounts:device_connected");
     os.removeObserver(this, "fxaccounts:verify_login");
     os.removeObserver(this, "fxaccounts:device_disconnected");
+    os.removeObserver(this, "fxaccounts:messages:display-tabs");
     os.removeObserver(this, "weave:engine:clients:display-uris");
     os.removeObserver(this, "session-save");
     if (this._bookmarksBackupIdleTime) {
       this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
       delete this._bookmarksBackupIdleTime;
     }
     if (this._lateTasksIdleObserver) {
       this._idleService.removeIdleObserver(this._lateTasksIdleObserver, LATE_TASKS_IDLE_TIME_SEC);
@@ -2538,17 +2541,17 @@ BrowserGlue.prototype = {
         tab.setAttribute("attention", true);
         return tab;
       };
 
       const firstTab = await openTab(URIs[0]);
       await Promise.all(URIs.slice(1).map(URI => openTab(URI)));
 
       let title, body;
-      const deviceName = Weave.Service.clientsEngine.getClientName(URIs[0].clientId);
+      const deviceName = URIs[0].sender.name;
       const bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
       if (URIs.length == 1) {
         // Due to bug 1305895, tabs from iOS may not have device information, so
         // we have separate strings to handle those cases. (See Also
         // unnamedTabsArrivingNotificationNoDevice.body below)
         if (deviceName) {
           title = bundle.formatStringFromName("tabArrivingNotificationWithDevice.title", [deviceName], 1);
         } else {
@@ -2562,17 +2565,17 @@ BrowserGlue.prototype = {
         if (win.gURLBar) {
           body = win.gURLBar.trimValue(body);
         }
         if (wasTruncated) {
           body = bundle.formatStringFromName("singleTabArrivingWithTruncatedURL.body", [body], 1);
         }
       } else {
         title = bundle.GetStringFromName("multipleTabsArrivingNotification.title");
-        const allSameDevice = URIs.every(URI => URI.clientId == URIs[0].clientId);
+        const allSameDevice = URIs.every(URI => URI.sender.id == URIs[0].sender.id);
         const unknownDevice = allSameDevice && !deviceName;
         let tabArrivingBody;
         if (unknownDevice) {
           tabArrivingBody = "unnamedTabsArrivingNotificationNoDevice.body";
         } else if (allSameDevice) {
           tabArrivingBody = "unnamedTabsArrivingNotification2.body";
         } else {
           tabArrivingBody = "unnamedTabsArrivingNotificationMultiple2.body";
--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -104,17 +104,17 @@ const LibraryButton = {
     // this will be null, and we bail out early.
     if (!libraryViewInsertionPoint) {
       return;
     }
     const parent = libraryViewInsertionPoint.parentNode;
     const {nextSibling} = libraryViewInsertionPoint;
     const item = win.document.createElement("toolbarbutton");
     item.className = "subviewbutton subviewbutton-iconic";
-    item.addEventListener("command", () => win.openTrustedLinkIn(this.PAGE_TO_OPEN, "tab"));
+    item.addEventListener("command", () => win.openWebLinkIn(this.PAGE_TO_OPEN, "tab"));
     item.id = this.ITEM_ID;
     const iconURL = this.ICON_URL;
     item.setAttribute("image", iconURL);
     item.setAttribute("label", this.LABEL);
 
     parent.insertBefore(item, nextSibling);
   },
 };
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>57.0a1</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
       </Description>
     </em:targetApplication>
     <em:type>2</em:type>
-    <em:version>30.1.0</em:version>
+    <em:version>32.1.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
   </Description>
 </RDF>
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -152,16 +152,28 @@ FINAL_TARGET_FILES.features['screenshots
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["fy_NL"] += [
   'webextension/_locales/fy_NL/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ga_IE"] += [
   'webextension/_locales/ga_IE/messages.json'
 ]
 
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["gd"] += [
+  'webextension/_locales/gd/messages.json'
+]
+
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["gl"] += [
+  'webextension/_locales/gl/messages.json'
+]
+
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["gn"] += [
+  'webextension/_locales/gn/messages.json'
+]
+
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["gu_IN"] += [
   'webextension/_locales/gu_IN/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["he"] += [
   'webextension/_locales/he/messages.json'
 ]
 
@@ -240,32 +252,40 @@ FINAL_TARGET_FILES.features['screenshots
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["mk"] += [
   'webextension/_locales/mk/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ml"] += [
   'webextension/_locales/ml/messages.json'
 ]
 
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["mn"] += [
+  'webextension/_locales/mn/messages.json'
+]
+
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["mr"] += [
   'webextension/_locales/mr/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ms"] += [
   'webextension/_locales/ms/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["my"] += [
   'webextension/_locales/my/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["nb_NO"] += [
   'webextension/_locales/nb_NO/messages.json'
 ]
 
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ne_NP"] += [
+  'webextension/_locales/ne_NP/messages.json'
+]
+
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["nl"] += [
   'webextension/_locales/nl/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["nn_NO"] += [
   'webextension/_locales/nn_NO/messages.json'
 ]
 
--- a/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
+++ b/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
@@ -10,62 +10,62 @@ function checkElements(expectPresent, l)
 
 async function togglePageActionPanel() {
   await promiseOpenPageActionPanel();
   EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
   await promisePageActionPanelEvent("popuphidden");
 }
 
 function promiseOpenPageActionPanel() {
-  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                  .getInterface(Ci.nsIDOMWindowUtils);
+  const dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
   return BrowserTestUtils.waitForCondition(() => {
     // Wait for the main page action button to become visible.  It's hidden for
     // some URIs, so depending on when this is called, it may not yet be quite
     // visible.  It's up to the caller to make sure it will be visible.
     info("Waiting for main page action button to have non-0 size");
-    let bounds = dwu.getBoundsWithoutFlushing(BrowserPageActions.mainButtonNode);
+    const bounds = dwu.getBoundsWithoutFlushing(BrowserPageActions.mainButtonNode);
     return bounds.width > 0 && bounds.height > 0;
   }).then(() => {
     // Wait for the panel to become open, by clicking the button if necessary.
     info("Waiting for main page action panel to be open");
-    if (BrowserPageActions.panelNode.state == "open") {
+    if (BrowserPageActions.panelNode.state === "open") {
       return Promise.resolve();
     }
-    let shownPromise = promisePageActionPanelEvent("popupshown");
+    const shownPromise = promisePageActionPanelEvent("popupshown");
     EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
     return shownPromise;
   }).then(() => {
     // Wait for items in the panel to become visible.
     return promisePageActionViewChildrenVisible(BrowserPageActions.mainViewNode);
   });
 }
 
 function promisePageActionPanelEvent(eventType) {
   return new Promise(resolve => {
-    let panel = BrowserPageActions.panelNode;
-    if ((eventType == "popupshown" && panel.state == "open") ||
-        (eventType == "popuphidden" && panel.state == "closed")) {
+    const panel = BrowserPageActions.panelNode;
+    if ((eventType === "popupshown" && panel.state === "open") ||
+        (eventType === "popuphidden" && panel.state === "closed")) {
       executeSoon(resolve);
       return;
     }
     panel.addEventListener(eventType, () => {
       executeSoon(resolve);
     }, { once: true });
   });
 }
 
 function promisePageActionViewChildrenVisible(panelViewNode) {
   info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
-  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                  .getInterface(Ci.nsIDOMWindowUtils);
+  const dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
   return BrowserTestUtils.waitForCondition(() => {
-    let bodyNode = panelViewNode.firstChild;
-    for (let childNode of bodyNode.childNodes) {
-      let bounds = dwu.getBoundsWithoutFlushing(childNode);
+    const bodyNode = panelViewNode.firstChild;
+    for (const childNode of bodyNode.childNodes) {
+      const bounds = dwu.getBoundsWithoutFlushing(childNode);
       if (bounds.width > 0 && bounds.height > 0) {
         return true;
       }
     }
     return false;
   });
 }
 
@@ -73,18 +73,18 @@ add_task(async function() {
   await promiseScreenshotsEnabled();
 
   registerCleanupFunction(async function() {
     await promiseScreenshotsReset();
   });
 
   // Toggle the page action panel to get it to rebuild itself.  An actionable
   // page must be opened first.
-  let url = "http://example.com/browser_screenshots_ui_check";
-  await BrowserTestUtils.withNewTab(url, async () => {
+  const url = "http://example.com/browser_screenshots_ui_check";
+  await BrowserTestUtils.withNewTab(url, async () => { // eslint-disable-line space-before-function-paren
     await togglePageActionPanel();
 
     await BrowserTestUtils.waitForCondition(
       () => document.getElementById(BUTTON_ID),
       "Screenshots button should be present", 100, 100);
 
     checkElements(true, [BUTTON_ID]);
   });
--- a/browser/extensions/screenshots/webextension/_locales/ach/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/ach/messages.json
@@ -24,27 +24,41 @@
     "message": "Gwok potbuk weng"
   },
   "cancelScreenshot": {
     "message": "Juki"
   },
   "downloadScreenshot": {
     "message": "Gam"
   },
+  "downloadOnlyDetailsESR": {
+    "message": "Itye ka tic ki Firefox pi ESR."
+  },
   "notificationLinkCopiedTitle": {
     "message": "Ki loko kakube"
   },
   "notificationLinkCopiedDetails": {
     "message": "Ki loko kakube me cal mamegi i bao me coc. Dii $META_KEY$-V me mwono ne.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "Loki"
+  },
+  "imageCroppedWarning": {
+    "message": "Ki ngolo cal ma odoko $PIXELS$px.",
+    "placeholders": {
+      "pixels": {
+        "content": "$1"
+      }
+    }
+  },
   "requestErrorTitle": {
     "message": "Pe tye katic."
   },
   "requestErrorDetails": {
     "message": "Timwa kica! Pe onongo wa twero gwoko cal mamegi. Tim ber item doki lacen."
   },
   "connectionErrorTitle": {
     "message": "Pe watwero kube ki cal me wang kio mamegi."
--- a/browser/extensions/screenshots/webextension/_locales/bs/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/bs/messages.json
@@ -124,17 +124,17 @@
   },
   "tourBodyIntro": {
     "message": "Snimite, sačuvajte i dijelite snimke bez da napuštate Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Novi način da sačuvate"
   },
   "tourBodyPageAction": {
-    "message": "Proširite meni radnji stranice u adresnoj traci svaki put kada želite snimiti ekran."
+    "message": "Proširite meni s radnjama stranice u adresnoj traci svaki put kada želite snimiti ekran."
   },
   "tourHeaderClickAndDrag": {
     "message": "Uslikajte baš ono što želite"
   },
   "tourBodyClickAndDrag": {
     "message": "Kliknite i povucite ako želite snimiti samo dio stranice. Također možete označiti vaš odabir tako da preko njega stavite pokazivač miša."
   },
   "tourHeaderFullPage": {
--- a/browser/extensions/screenshots/webextension/_locales/ca/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/ca/messages.json
@@ -39,16 +39,22 @@
     "message": "En una finestra de navegació privada."
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "S'han desactivat les galetes de tercers."
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "S'ha activat l'opció «No recordarà mai l'historial»."
   },
+  "downloadOnlyDetailsESR": {
+    "message": "Feu servir el Firefox ESR."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "Les pujades estan inhabilitades."
+  },
   "notificationLinkCopiedTitle": {
     "message": "S'ha copiat l'enllaç"
   },
   "notificationLinkCopiedDetails": {
     "message": "L'enllaç de la captura s'ha copiat al porta-retalls. Premeu $META_KEY$-V per enganxar-lo.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
--- a/browser/extensions/screenshots/webextension/_locales/de/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/de/messages.json
@@ -39,16 +39,22 @@
     "message": "In einem privaten Fenster."
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "Cookies von Drittanbietern sind deaktiviert."
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "„Chronik niemals anlegen“ ist aktiviert."
   },
+  "downloadOnlyDetailsESR": {
+    "message": "Sie verwenden Firefox ESR."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "Uploads wurden deaktiviert."
+  },
   "notificationLinkCopiedTitle": {
     "message": "Link kopiert"
   },
   "notificationLinkCopiedDetails": {
     "message": "Der Link zu Ihrem Bildschirmfoto wurde in die Zwischenablage kopiert. Drücken Sie $META_KEY$-V zum Einfügen.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
--- a/browser/extensions/screenshots/webextension/_locales/el/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/el/messages.json
@@ -24,16 +24,19 @@
     "message": "Αποθήκευση ολόκληρης σελίδας"
   },
   "cancelScreenshot": {
     "message": "Ακύρωση"
   },
   "downloadScreenshot": {
     "message": "Λήψη"
   },
+  "downloadOnlyDetailsPrivate": {
+    "message": "Σε ένα παράθυρο ιδιωτικής περιήγησης."
+  },
   "downloadOnlyDetailsThirdParty": {
     "message": "Τα cookies τρίτων είναι απενεργοποιημένα."
   },
   "downloadOnlyDetailsESR": {
     "message": "Χρησιμοποιείτε το Firefox ESR."
   },
   "downloadOnlyDetailsNoUploadPref": {
     "message": "Οι μεταφορτώσεις έχουν απενεργοποιηθεί."
--- a/browser/extensions/screenshots/webextension/_locales/et/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/et/messages.json
@@ -39,30 +39,47 @@
     "message": "oled privaatse veebilehitsemise aknas"
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "kolmanda osapoole küpsised on keelatud"
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "“ei säilita ajalugu” on lubatud"
   },
+  "downloadOnlyDetailsESR": {
+    "message": "Sa kasutad Firefox ESRi."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "Üleslaadimised on keelatud."
+  },
   "notificationLinkCopiedTitle": {
     "message": "Link kopeeriti"
   },
   "notificationLinkCopiedDetails": {
     "message": "Link sinu pildile kopeeriti lõikepuhvrisse. Asetamiseks vajuta $META_KEY$-V.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Kopeeri"
   },
+  "notificationImageCopiedTitle": {
+    "message": "Pilt kopeeriti"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "Sinu pilt kopeeriti vahemällu. Asetamiseks vajuta $META_KEY$-V.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
   "imageCroppedWarning": {
     "message": "See pilt on vähendatud $PIXELS$-le pikslile.",
     "placeholders": {
       "pixels": {
         "content": "$1"
       }
     }
   },
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/_locales/gd/messages.json
@@ -0,0 +1,184 @@
+{
+  "addonDescription": {
+    "message": "Tog cliopaichean is glacaidhean-sgrìn on lìon is sàbhail iad rè seal no gu buan."
+  },
+  "addonAuthorsList": {
+    "message": "Mozilla <screenshots-feedback@mozilla.com>"
+  },
+  "contextMenuLabel": {
+    "message": "Tog glacadh-sgrìn"
+  },
+  "myShotsLink": {
+    "message": "Na glacaidhean agam"
+  },
+  "screenshotInstructions": {
+    "message": "Dèan briogadh no slaodadh air an duilleag airson raon a thaghadh. Brùth ESC airson sgur dheth."
+  },
+  "saveScreenshotSelectedArea": {
+    "message": "Sàbhail"
+  },
+  "saveScreenshotVisibleArea": {
+    "message": "Sàbhail na tha ri fhaicinn"
+  },
+  "saveScreenshotFullPage": {
+    "message": "Sàbhail an duilleag shlàn"
+  },
+  "cancelScreenshot": {
+    "message": "Sguir dheth"
+  },
+  "downloadScreenshot": {
+    "message": "Luchdaich a-nuas"
+  },
+  "downloadOnlyNotice": {
+    "message": "Tha thu sa mhodh luchdaidh a-nuas a-mhàin."
+  },
+  "downloadOnlyDetails": {
+    "message": "Bidh gleus glacaidhean-sgrìn Firefox sa mhodh luchdaidh a-nuas gu fèin-obrachail sna suidheachaidhean a leanas:"
+  },
+  "downloadOnlyDetailsPrivate": {
+    "message": "Ann an uinneag brabhsaidh phrìobhaidich."
+  },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "Tha briosgaidean threas-phàrtaidhean à comas."
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "Tha “Na cuimhnich an eachdraidh idir” an comas."
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "Tha thu a’ cleachdadh Firefox ESR."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "Chaidh an luchdadh suas a chur à comas."
+  },
+  "notificationLinkCopiedTitle": {
+    "message": "Chaidh lethbhreac a dhèanamh dhen cheangal"
+  },
+  "notificationLinkCopiedDetails": {
+    "message": "Chaidh lethbhreac de cheangal a’ ghlacaidh agad a chur air an stòr-bhòrd. Brùth $META_KEY$-V airson a chur ann.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
+  "copyScreenshot": {
+    "message": "Dèan lethbhreac"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "Chaidh lethbhreac a dhèanamh dhen ghlacadh"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "Chaidh lethbhreac dhen ghlacadh agad a chur air an stòr-bhòrd. Brùth $META_KEY$-V airson a chur ann.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
+  "imageCroppedWarning": {
+    "message": "Chaidh an dealbh a bhearradh is tha e $PIXELS$px a-nis.",
+    "placeholders": {
+      "pixels": {
+        "content": "$1"
+      }
+    }
+  },
+  "requestErrorTitle": {
+    "message": "Tuibriste."
+  },
+  "requestErrorDetails": {
+    "message": "Tha sinn duilich! Cha b’ urrainn dhuinn an glacadh agad a shàbhaladh. Feuch ris a-rithist an ceann greis."
+  },
+  "connectionErrorTitle": {
+    "message": "Cha b’ urrainn dhuinn na glacaidhean-sgrìn agad a shàbhaladh."
+  },
+  "connectionErrorDetails": {
+    "message": "Thoir sùil air a’ cheangal agad ris an eadar-lìon. Ma tha ceangal agad ris, dh’fhaoidte gu bheil duilgheadas sealach aig seirbheis glacaidhean-sgrìn Firefox."
+  },
+  "loginErrorDetails": {
+    "message": "Cha b’ urrainn dhuinn an glacadh agad a shàbhaladh air sgàth duilgheadas le seirbheis glacaidhean-sgrìn Firefox. Feuch ris a-rithist an ceann greis."
+  },
+  "unshootablePageErrorTitle": {
+    "message": "Chan urrainn dhuinn glacadh-sgrìn a dhèanamh dhen duilleag seo."
+  },
+  "unshootablePageErrorDetails": {
+    "message": "Chan eil duilleag-lìn àbhaisteach a tha seo ’s chan urrainn dhut glacadh-sgrìn a dhèanamh dheth."
+  },
+  "selfScreenshotErrorTitle": {
+    "message": "Chan urrainn dhut glacadh a thogail de dhuilleag ghlacaidhean-sgrìn Firefox!"
+  },
+  "emptySelectionErrorTitle": {
+    "message": "Tha na thagh thu ro bheag"
+  },
+  "privateWindowErrorTitle": {
+    "message": "Tha gleus nan glacaidhean-sgrìn à comas ann am modh a’ bhrabhsaidh phrìobhaidich"
+  },
+  "privateWindowErrorDetails": {
+    "message": "Tha sinn duilich mu dhèidhinn. Tha sinn ag obair air agus an dòchas gum bi e ri làimh a dh’aithghearr."
+  },
+  "genericErrorTitle": {
+    "message": "Ìoc! Sin glacaidhean-sgrìn Firefox air feadh na fìdhle."
+  },
+  "genericErrorDetails": {
+    "message": "Chan eil sinn cinnteach dè thachair. A bheil thu airson feuchainn ris a-rithist no glacadh a thogail de dhuilleag eile?"
+  },
+  "tourBodyIntro": {
+    "message": "Tog, sàbhail is co-roinn glacadh-sgrìn gun Firefix fhàgail."
+  },
+  "tourHeaderPageAction": {
+    "message": "Dòigh ùr airson sàbhaladh"
+  },
+  "tourBodyPageAction": {
+    "message": "Leudaich clàr-taice gnìomhan na duilleige ann am bàr an t-seòlaidh uair sam bith a tha thu airson glacadh-sgrìn a thogail."
+  },
+  "tourHeaderClickAndDrag": {
+    "message": "Na glac ach dìreach na tha a dhìth ort"
+  },
+  "tourBodyClickAndDrag": {
+    "message": "Dèan briogadh is slaodadh airson earrann de dhuilleag a ghlacadh. ’S urrainn dhut fantainn os cionn rud cuideachd airson na thagh thu a shoillseachadh."
+  },
+  "tourHeaderFullPage": {
+    "message": "Glac uinneagan no duilleagan slàna"
+  },
+  "tourBodyFullPage": {
+    "message": "Tagh na putanan air an taobh deas gu h-àrd airson na tha ri fhaicinn san uinneag a ghlacadh no airson duilleag shlàn a ghlacadh."
+  },
+  "tourHeaderDownloadUpload": {
+    "message": "Do thoil fhèin"
+  },
+  "tourBodyDownloadUpload": {
+    "message": "Sàbhail na glacaidhean bearrte air an lìon ach am bi e furasta an co-roinneadh no luchdaich a-nuas iad dhan choimpiutair agad. ’S urrainn dhut briogadh air a’ phutan “Na glacaidhean agam” cuideachd is chì thu gach glacadh a thog thu."
+  },
+  "tourSkip": {
+    "message": "LEUM SEACHAD"
+  },
+  "tourNext": {
+    "message": "An ath-shleamhnag"
+  },
+  "tourPrevious": {
+    "message": "An t-sleamhnag roimhe"
+  },
+  "tourDone": {
+    "message": "Dèanta"
+  },
+  "termsAndPrivacyNotice2": {
+    "message": "Ma chleachdas tu gleus nan glacaidhean-sgrìn aig Firefox, bidh thu ag aontachadh ris na $TERMSANDPRIVACYNOTICETERMSLINK$ agus $TERMSANDPRIVACYNOTICEPRIVACYLINK$ againn.",
+    "placeholders": {
+      "termsandprivacynoticetermslink": {
+        "content": "$1"
+      },
+      "termsandprivacynoticeprivacylink": {
+        "content": "$2"
+      }
+    }
+  },
+  "termsAndPrivacyNoticeTermsLink": {
+    "message": "na teirmichean"
+  },
+  "termsAndPrivacyNoticyPrivacyLink": {
+    "message": "an aithris prìobhaideachd"
+  },
+  "libraryLabel": {
+    "message": "Glacaidhean-sgrìn"
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/_locales/gl/messages.json
@@ -0,0 +1,64 @@
+{
+  "myShotsLink": {
+    "message": "As miñas capturas"
+  },
+  "saveScreenshotSelectedArea": {
+    "message": "Gardar"
+  },
+  "saveScreenshotVisibleArea": {
+    "message": "Gardar a parte visible"
+  },
+  "saveScreenshotFullPage": {
+    "message": "Gardar a páxina completa"
+  },
+  "cancelScreenshot": {
+    "message": "Cancelar"
+  },
+  "downloadScreenshot": {
+    "message": "Descargar"
+  },
+  "downloadOnlyDetailsPrivate": {
+    "message": "Nunha xanela de navegación privada."
+  },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "Cando as cookies de terceiros están desactivadas."
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "Cando está activa a opción \"Nunca gardará o historial\"."
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "Está usando Firefox ESR."
+  },
+  "notificationLinkCopiedTitle": {
+    "message": "Copiouse a ligazón"
+  },
+  "copyScreenshot": {
+    "message": "Copiar"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "Copiouse a captura"
+  },
+  "tourDone": {
+    "message": "Feito"
+  },
+  "termsAndPrivacyNotice2": {
+    "message": "Ao usar Firefox Screenshots, vostede acepta os nosos $TERMSANDPRIVACYNOTICETERMSLINK$ e a $TERMSANDPRIVACYNOTICEPRIVACYLINK$.",
+    "placeholders": {
+      "termsandprivacynoticetermslink": {
+        "content": "$1"
+      },
+      "termsandprivacynoticeprivacylink": {
+        "content": "$2"
+      }
+    }
+  },
+  "termsAndPrivacyNoticeTermsLink": {
+    "message": "Termos"
+  },
+  "termsAndPrivacyNoticyPrivacyLink": {
+    "message": "Política de privacidade"
+  },
+  "libraryLabel": {
+    "message": "Capturas de pantalla"
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/_locales/gn/messages.json
@@ -0,0 +1,14 @@
+{
+  "saveScreenshotSelectedArea": {
+    "message": "Ñongatu"
+  },
+  "saveScreenshotVisibleArea": {
+    "message": "Ñongatu hechapy"
+  },
+  "saveScreenshotFullPage": {
+    "message": "Kuatiarogue tuichavéva ñongatu"
+  },
+  "cancelScreenshot": {
+    "message": "Heja"
+  }
+}
\ No newline at end of file
--- a/browser/extensions/screenshots/webextension/_locales/he/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/he/messages.json
@@ -24,27 +24,70 @@
     "message": "שמירת הדף במלואו"
   },
   "cancelScreenshot": {
     "message": "ביטול"
   },
   "downloadScreenshot": {
     "message": "הורדה"
   },
+  "downloadOnlyNotice": {
+    "message": "הינך כרגע במצב הורדה בלבד."
+  },
+  "downloadOnlyDetails": {
+    "message": "‏Firefox Screenshots משתנה אוטומטית למצב הורדה בלבד במקרים הבאים:"
+  },
+  "downloadOnlyDetailsPrivate": {
+    "message": "בחלון גלישה פרטית."
+  },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "כשעוגיות צד שלישי מנוטרלות."
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "„לא לזכור היסטוריה” פעיל."
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "הגרסה שלך היא Firefox ESR."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "ההעלאות הושבתו."
+  },
   "notificationLinkCopiedTitle": {
     "message": "הקישור הועתק"
   },
   "notificationLinkCopiedDetails": {
     "message": "הקישור לתמונה שלך הועתק ללוח. נא ללחוץ על $META_KEY$-V כדי להדביק.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "העתקה"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "צילום המסך הועתק"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "צילום המסך שלך הועתק ללוח העריכה. יש ללחוץ על $META_KEY$-V כדי להדביק.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
+  "imageCroppedWarning": {
+    "message": "תמונה זו נחתכה לגודל של $PIXELS$ פיקסלים.",
+    "placeholders": {
+      "pixels": {
+        "content": "$1"
+      }
+    }
+  },
   "requestErrorTitle": {
     "message": "לא תקין."
   },
   "requestErrorDetails": {
     "message": "אנו מצטערים, אך לא ניתן היה לשמור את התמונה. נא לנסות שוב מאוחר יותר."
   },
   "connectionErrorTitle": {
     "message": "לא ניתן היה להתחבר אל מאגר צילומי המסך שלך."
@@ -80,16 +123,19 @@
     "message": "אנחנו לא בטוחים מה קרה פה הרגע. אכפת לך לנסות שוב או לצלם דף אחר?"
   },
   "tourBodyIntro": {
     "message": "צילום, שמירה ושיתוף של צילומי מסך מבלי לעזוב את Firefox."
   },
   "tourHeaderPageAction": {
     "message": "דרך חדשה לשמירה"
   },
+  "tourBodyPageAction": {
+    "message": "יש לפתוח את תפריט פעולות הדף בסרגל הכתובת בכל פעם שברצונך לצלם את המסך."
+  },
   "tourHeaderClickAndDrag": {
     "message": "לצלם רק את מה שנחוץ לך"
   },
   "tourBodyClickAndDrag": {
     "message": "ניתן ללחוץ ולגרור כדי לצלם רק מקטע מהעמוד. ניתן גם לרחף מעל כדי לסמן את הבחירה שלך."
   },
   "tourHeaderFullPage": {
     "message": "לצלם חלונות או דפים שלמים"
--- a/browser/extensions/screenshots/webextension/_locales/hi_IN/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/hi_IN/messages.json
@@ -53,16 +53,30 @@
   "notificationLinkCopiedDetails": {
     "message": "आपके शॉट के लिंक क्लिपबोर्ड पर कॉपी किए गए हैं. पेस्ट करने के लिए $META_KEY$-V दबाएँ.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "प्रतिलिपि बनाएँ"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "शॉट प्रतिलिपि बनाई गई"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "आपके शॉट के लिंक क्लिपबोर्ड पर कॉपी किए गए हैं. पेस्ट करने के लिए $META_KEY$-V दबाएँ.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
   "imageCroppedWarning": {
     "message": "इस छवि को $PIXELS$px में छोटा किया गया है.",
     "placeholders": {
       "pixels": {
         "content": "$1"
       }
     }
   },
--- a/browser/extensions/screenshots/webextension/_locales/ko/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/ko/messages.json
@@ -39,16 +39,19 @@
     "message": "개인정보 보호 창에 있을 때"
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "제 3자 쿠키가 비활성화되어 있을 때"
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "“히스토리 기억 안함”이 활성화되어 있을 때"
   },
+  "downloadOnlyDetailsESR": {
+    "message": "Firefox ESR을 사용중입니다."
+  },
   "notificationLinkCopiedTitle": {
     "message": "링크 복사됨"
   },
   "notificationLinkCopiedDetails": {
     "message": "방금 찍은 스냅샷의 링크가 클립보드에 저장됐습니다. 붙여넣으려면 $META_KEY$-V를 누르세요.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
--- a/browser/extensions/screenshots/webextension/_locales/lij/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/lij/messages.json
@@ -35,16 +35,19 @@
   "notificationLinkCopiedDetails": {
     "message": "O colegamento a l'inmagine o l'é stæto copiou inti aponti. Sciacca$META_KEY$-V pe incolalo.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "Còpia"
+  },
   "requestErrorTitle": {
     "message": "Feua serviçio."
   },
   "requestErrorDetails": {
     "message": "Ne spiaxe! No poemmo sarvâ l'inmagine. Pe piaxei preuva torna dòppo."
   },
   "connectionErrorTitle": {
     "message": "No poemmo conetise a-e teu föto do schermo."
@@ -68,16 +71,19 @@
     "message": "Ahime mi! Firefox Screeshot o s'é ciantou."
   },
   "genericErrorDetails": {
     "message": "Niatri no emmo ben acapio cöse l'é sucesso. Ti peu miga preuvâ co-ina pagina dispægia?"
   },
   "tourBodyIntro": {
     "message": "Fanni, sarva e condividdi föto do schermo sensa sciortî da Firefox."
   },
+  "tourHeaderPageAction": {
+    "message": "Un neuvo mòddo de sarvâ"
+  },
   "tourHeaderClickAndDrag": {
     "message": "Catua solo quello che t'eu"
   },
   "tourBodyClickAndDrag": {
     "message": "Sciacca e rebela pe catuâ solo 'na porçion de 'na pagina. Ti peu anche anâ co-o ratto sorvia l'area seleçionâ pe evidençiala."
   },
   "tourHeaderFullPage": {
     "message": "Catua 'n barcon ò 'na pagina intrega"
@@ -98,18 +104,18 @@
     "message": "Pròscima schermâ"
   },
   "tourPrevious": {
     "message": "Schermâ de primma"
   },
   "tourDone": {
     "message": "Fæto"
   },
-  "termsAndPrivacyNoticeCloudServices": {
-    "message": "Se ti deuvi Firefox Screenshots, ti e d'acordio con {termsAndPrivacyNoticeTermsLink} e {termsAndPrivacyNoticePrivacyLink} de Firefox Cloud Services."
-  },
   "termsAndPrivacyNoticeTermsLink": {
     "message": "Termini"
   },
   "termsAndPrivacyNoticyPrivacyLink": {
     "message": "Informativa in sciâ privacy"
+  },
+  "libraryLabel": {
+    "message": "Föto do schermo"
   }
 }
\ No newline at end of file
--- a/browser/extensions/screenshots/webextension/_locales/mk/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/mk/messages.json
@@ -24,27 +24,62 @@
     "message": "Сними цела страница"
   },
   "cancelScreenshot": {
     "message": "Откажи"
   },
   "downloadScreenshot": {
     "message": "Преземи"
   },
+  "downloadOnlyNotice": {
+    "message": "Во моментов сте во режим Само Преземање."
+  },
+  "downloadOnlyDetails": {
+    "message": "Firefox Screenshots се префрла во режим Само Преземање во овие случаи:"
+  },
+  "downloadOnlyDetailsPrivate": {
+    "message": "Во приватен прозорец."
+  },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "Колачињата од трети страни се оневозможени."
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "“Никогаш нема да ја памти историјата” е вклучена."
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "Користите Firefox ESR."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "Качувањето на датотеки е оневозможено."
+  },
   "notificationLinkCopiedTitle": {
     "message": "Врската е ископирана"
   },
   "notificationLinkCopiedDetails": {
     "message": "Врската до вашата слика е ископирана во меморија. Притиснете $META_KEY$-V за да ја вметнете.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "Копирај"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "Слика е ископирана"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "Вашата слика беше ископирана во меморија. Притиснете $META_KEY$-V за да ја вметнете.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
   "imageCroppedWarning": {
     "message": "Оваа слика е скратена до $PIXELS$px.",
     "placeholders": {
       "pixels": {
         "content": "$1"
       }
     }
   },
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/_locales/mn/messages.json
@@ -0,0 +1,5 @@
+{
+  "addonDescription": {
+    "message": "Вэбээс авсан клип болон дэлгэцийн зургийг аваад тэдгээрийг түр эсвэл бүрмөсөн хадгал."
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/_locales/ne_NP/messages.json
@@ -0,0 +1,20 @@
+{
+  "addonDescription": {
+    "message": "वेबबाट फोटो र सिक्रिनसट या क्लिपहरू लिएर क्षणिक वा सदाको लागि सङ्ग्रह गर्न ।"
+  },
+  "contextMenuLabel": {
+    "message": "स्क्रिनसट लिनुहोस्"
+  },
+  "myShotsLink": {
+    "message": "मेरा सटहरू"
+  },
+  "saveScreenshotSelectedArea": {
+    "message": "सङ्ग्रह गर्नुहोस्"
+  },
+  "saveScreenshotVisibleArea": {
+    "message": "दृश्यात्मक सङ्ग्रह गर्नुहोस्"
+  },
+  "cancelScreenshot": {
+    "message": "रद्द गर्नुहोस"
+  }
+}
\ No newline at end of file
--- a/browser/extensions/screenshots/webextension/_locales/pa_IN/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/pa_IN/messages.json
@@ -21,30 +21,83 @@
     "message": "ਪੂਰੇ ਸਫ਼ੇ ਨੂੰ ਸੰਭਾਲੋ"
   },
   "cancelScreenshot": {
     "message": "ਰੱਦ ਕਰੋ"
   },
   "downloadScreenshot": {
     "message": "ਡਾਊਨਲੋਡ ਕਰੋ"
   },
+  "downloadOnlyDetailsPrivate": {
+    "message": "ਇੱਕ ਨਿੱਜੀ ਬਰਾਊਜਿੰਗ ਵਿੰਡੋ ਵਿੱਚ"
+  },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "ਤੀਜੀ ਧਿਰ ਦੀਆਂ ਕੂਕੀਜ ਆਯੋਗ ਹਨ।"
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "\"ਅਤੀਤ ਕਦੇ ਵੀ ਯਾਦ ਨਾ ਰੱਖੋ\" ਆਯੋਗ ਕੀਤਾ ਗਿਆ ਹੈ।"
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "ਤੁਸੀਂ ESR ਫਾਇਰਫਾਕਸ ਵਰਤ ਰਹੇ ਹੋ।"
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "ਅੱਪਲ੍ਹੋਡ ਆਯੋਗ ਕੀਤੇ ਗਏ ਹਨ।"
+  },
   "notificationLinkCopiedTitle": {
     "message": "ਲਿੰਕ ਕਾਪੀ ਕੀਤਾ ਗਿਆ"
   },
+  "copyScreenshot": {
+    "message": "ਕਾਪੀ"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "ਸ਼ਾਟ ਕਾਪੀ ਕੀਤਾ ਗਿਆ"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "ਤੁਹਾਡਾ ਸ਼ਾਟ ਕਲਿੱਪਬੋਰਡ ਤੋਂ ਕਾਪੀ ਕੀਤਾ ਗਿਆ ਹੈ। ਪੇਸਟ ਕਰਨ ਲਈ  $META_KEY$-V  ਨੂੰ ਦਬਾਓ।",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
   "requestErrorTitle": {
     "message": "ਖ਼ਰਾਬ ਹੈ।"
   },
+  "connectionErrorTitle": {
+    "message": "ਅਸੀਂ ਤੁਹਾਡੇ ਸਕਰੀਨਸ਼ਾਟ ਨਾਲ ਕੁਨੈੱਕਟ ਨਹੀਂ ਕਰ ਸਕਦੇ।"
+  },
+  "unshootablePageErrorTitle": {
+    "message": "ਅਸੀਂ ਇਸ ਸਫੇ ਦਾ ਸਕਰੀਨਸ਼ਾਟ ਨਹੀਂ ਲੈ ਸਕਦੇ।"
+  },
+  "emptySelectionErrorTitle": {
+    "message": "ਤੁਹਾਡੀ ਚੋਣ ਬਹੁਤ ਛੋਟੀ ਹੈ"
+  },
+  "privateWindowErrorTitle": {
+    "message": "ਨਿੱਜੀ ਬਰਾਊਜਿੰਗ ਮੋਡ ਵਿੱਚ ਸਕਰੀਨਸ਼ਾਟ ਆਯੋਗ ਹੋਇਆ"
+  },
+  "genericErrorTitle": {
+    "message": "ਠਹਿਰੋ! ਫਾਇਰਫਾਕਸ ਸਕਰੀਨਸ਼ਾਟ ਲੈਣ 'ਚ ਸਮੱਸਿਆ ਆਈ"
+  },
+  "tourBodyIntro": {
+    "message": "ਬਿਨਾਂ ਫਾਇਰਫਾਕਸ ਨੂੰ ਛੱਡੇ ਸਕਰੀਨਸ਼ਾਟ ਲਓ, ਸੰਭਾਲੋ, ਅਤੇ ਸਾਂਝਾ ਕਰੋ।"
+  },
   "tourHeaderPageAction": {
     "message": "ਸੰਭਾਲਣ ਦਾ ਨਵਾਂ ਢੰਗ"
   },
+  "tourHeaderClickAndDrag": {
+    "message": "ਕੈਪਚਰ ਕਰੋ ਤੁਸੀਂ ਕੀ ਚਾਹੁੰਦੇ ਹੋ"
+  },
   "tourSkip": {
     "message": "ਛੱਡੋ"
   },
   "tourNext": {
     "message": "ਅਗਲੀ ਸਲਾਈਡ"
   },
   "tourPrevious": {
     "message": "ਪਿਛਲੀ ਸਲਾਈਡ"
   },
   "tourDone": {
     "message": "ਮੁਕੰਮਲ"
+  },
+  "termsAndPrivacyNoticyPrivacyLink": {
+    "message": "ਨਿੱਜੀ ਨੋਟਿਸ"
   }
 }
\ No newline at end of file
--- a/browser/extensions/screenshots/webextension/_locales/ro/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/ro/messages.json
@@ -25,17 +25,17 @@
   },
   "cancelScreenshot": {
     "message": "Renunță"
   },
   "downloadScreenshot": {
     "message": "Descarcă"
   },
   "downloadOnlyNotice": {
-    "message": "Momentan sunteți în modul descărcare."
+    "message": "În prezent ești în modul numai de descărcare."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots se mută automat în modul descărcare în aceste situații:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "În fereastra de navigare privată."
   },
   "downloadOnlyDetailsThirdParty": {
--- a/browser/extensions/screenshots/webextension/_locales/te/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/te/messages.json
@@ -21,25 +21,37 @@
     "message": "రద్దుచేయి"
   },
   "downloadScreenshot": {
     "message": "దింపుకోండి"
   },
   "downloadOnlyNotice": {
     "message": "మీరు ప్రస్తుతం దింపుకోలు-మాత్రమే రీతిలో ఉన్నారు."
   },
+  "downloadOnlyDetailsPrivate": {
+    "message": "అంతరంగిక విహారణ కిటికీలో."
+  },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "మూడవ-పక్ష కుకీలు అచేతనమయ్యాయి."
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "మీరు Firefox ESR ఉపయోగిస్తున్నారు."
+  },
   "downloadOnlyDetailsNoUploadPref": {
     "message": "ఎక్కింపులు అచేతమై ఉన్నాయి."
   },
   "notificationLinkCopiedTitle": {
     "message": "లంకె కాపీ అయింది"
   },
   "copyScreenshot": {
     "message": "కాపీచెయ్యి"
   },
+  "notificationImageCopiedTitle": {
+    "message": "పట్టు కాపీఅయ్యింది"
+  },
   "requestErrorTitle": {
     "message": "పని చెయుట లేదు."
   },
   "requestErrorDetails": {
     "message": "క్షమిచండి! మీ తెరను భద్రపరచలేకపోయాం. దయచేసి కాసేపాగి మళ్ళీ ప్రయత్నించండి."
   },
   "connectionErrorTitle": {
     "message": "మేము మీ స్క్రీన్షాట్లకు కనెక్ట్ చేయలేము."
--- a/browser/extensions/screenshots/webextension/_locales/th/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/th/messages.json
@@ -33,27 +33,36 @@
     "message": "คุณกำลังอยู่ในโหมดดาวน์โหลดเท่านั้น"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "ในหน้าต่างการท่องเว็บแบบส่วนตัว"
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "คุกกี้บุคคลที่สามถูกปิดการใช้งาน"
   },
+  "downloadOnlyDetailsESR": {
+    "message": "คุณกำลังใช้ Firefox ESR"
+  },
   "notificationLinkCopiedTitle": {
     "message": "คัดลอกลิงก์แล้ว"
   },
   "notificationLinkCopiedDetails": {
     "message": "คัดลอกลิงก์ไปยังภาพของคุณไปยังคลิปบอร์ดแล้ว กด $META_KEY$-V เพื่อวาง",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "คัดลอก"
+  },
+  "notificationImageCopiedTitle": {
+    "message": "คัดลอกภาพแล้ว"
+  },
   "imageCroppedWarning": {
     "message": "ภาพนี้ถูกตัดเป็น $PIXELS$ พิกเซล",
     "placeholders": {
       "pixels": {
         "content": "$1"
       }
     }
   },
--- a/browser/extensions/screenshots/webextension/_locales/tr/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/tr/messages.json
@@ -124,35 +124,35 @@
   },
   "tourBodyIntro": {
     "message": "Firefox'tan çıkmadan ekran görüntüleri alın, kaydedin ve paylaşın."
   },
   "tourHeaderPageAction": {
     "message": "Kaydetmenin yeni yolu"
   },
   "tourBodyPageAction": {
-    "message": "Ekran görüntüsü almak istediğiniz zaman adres çubuğundaki sayfa eylemleri menüsünü açabilirsiniz."
+    "message": "Ekran görüntüsü almak istediğiniz zaman adres çubuğundaki sayfa eylemleri menüsünü açın."
   },
   "tourHeaderClickAndDrag": {
     "message": "İstediğini yakala"
   },
   "tourBodyClickAndDrag": {
-    "message": "Sayfanın belli bir kısmını yakalamak için işaretçiyi tıklayıp sürükleyin. Seçiminizi vurgulamak için fareyle üzerine gelebilirsiniz."
+    "message": "Sayfanın belli bir kısmını yakalamak için tıklayıp sürükleyin. Seçiminizi işaretlemek için fareyle üzerine de gidebilirsiniz."
   },
   "tourHeaderFullPage": {
     "message": "Pencereleri veya sayfaların tamamını yakala"
   },
   "tourBodyFullPage": {
     "message": "Yalnızca pencerede gördüğünüz alanı veya sayfanın tamamını yakalamak için sağ üstteki düğmelerden uygun olanı seçin."
   },
   "tourHeaderDownloadUpload": {
     "message": "İstediğin gibi yakala"
   },
   "tourBodyDownloadUpload": {
-    "message": "Ekran görüntülerinizi daha kolay paylaşmak veya bilgisayarınıza indirmek için web’e kaydedin. Kaydettiğiniz tüm görüntüleri bulmak için \"Ekran görüntülerim\" düğmesine tıklayabilirsiniz."
+    "message": "Ekran görüntülerinizi daha kolay paylaşmak web’e kaydedebilir veya bilgisayarınıza indirebilirsiniz. Kaydettiğiniz tüm görüntüleri bulmak için “Ekran görüntülerim” düğmesine tıklayabilirsiniz."
   },
   "tourSkip": {
     "message": "GEÇ"
   },
   "tourNext": {
     "message": "Sonraki slayt"
   },
   "tourPrevious": {
--- a/browser/extensions/screenshots/webextension/_locales/ur/messages.json
+++ b/browser/extensions/screenshots/webextension/_locales/ur/messages.json
@@ -24,27 +24,33 @@
     "message": "پورا صفحہ محفوظ کریں"
   },
   "cancelScreenshot": {
     "message": "منسوخ کریں"
   },
   "downloadScreenshot": {
     "message": "ڈاؤن لوڈ"
   },
+  "downloadOnlyDetailsESR": {
+    "message": "آپ Firefox ESR استعمال کر رہے ہیں۔"
+  },
   "notificationLinkCopiedTitle": {
     "message": "تبط نقل کر دیا گیا"
   },
   "notificationLinkCopiedDetails": {
     "message": "آُپ کی شاٹس کا ربط و تختہ تراشہ پر نقل کر دیا گیا ہے۔ چسپاں کرنے کے لیئے $META_KEY$-V دبائِں۔",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "copyScreenshot": {
+    "message": "نقل کریں"
+  },
   "requestErrorTitle": {
     "message": "خراب ہے۔"
   },
   "requestErrorDetails": {
     "message": "معاف کیجیئے گا! ہم آپ کی شاٹ محفوظ نہیں کر سکے۔ براہ مہربانی کچھ دیر بعد کوشش کریں۔"
   },
   "connectionErrorTitle": {
     "message": "ہم آپ کی اسکرین شاٹس سے نہیں جڑ سکتے۔"
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -6,28 +6,28 @@ this.main = (function() {
   const exports = {};
 
   const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl";
   const { sendEvent } = analytics;
 
   const manifest = browser.runtime.getManifest();
   let backend;
 
-  let hasSeenOnboarding;
-
-  browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
-    hasSeenOnboarding = !!result.hasSeenOnboarding;
-    if (!hasSeenOnboarding) {
+  let hasSeenOnboarding = browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
+    const onboarded = !!result.hasSeenOnboarding;
+    if (!onboarded) {
       setIconActive(false, null);
       // Note that the branded name 'Firefox Screenshots' is not localized:
       startBackground.photonPageActionPort.postMessage({
         type: "setProperties",
         title: "Firefox Screenshots"
       });
     }
+    hasSeenOnboarding = Promise.resolve(onboarded);
+    return hasSeenOnboarding;
   }).catch((error) => {
     log.error("Error getting hasSeenOnboarding:", error);
   });
 
   exports.setBackend = function(newBackend) {
     backend = newBackend;
     backend = backend.replace(/\/*$/, "");
   };
@@ -88,44 +88,46 @@ this.main = (function() {
   }
 
   function shouldOpenMyShots(url) {
     return /^about:(?:newtab|blank|home)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
   }
 
   // This is called by startBackground.js, directly in response to clicks on the Photon page action
   exports.onClicked = catcher.watchFunction((tab) => {
-    if (shouldOpenMyShots(tab.url)) {
-      if (!hasSeenOnboarding) {
+    catcher.watchPromise(hasSeenOnboarding.then(onboarded => {
+      if (shouldOpenMyShots(tab.url)) {
+        if (!onboarded) {
+          catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
+            sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
+            return forceOnboarding();
+          }));
+          return;
+        }
         catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
-          sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
-          return forceOnboarding();
+          sendEvent("goto-myshots", "about-newtab", {incognito: tab.incognito});
         }));
-        return;
+        catcher.watchPromise(
+          auth.authHeaders()
+          .then(() => browser.tabs.update({url: backend + "/shots"})));
+      } else {
+        catcher.watchPromise(
+          toggleSelector(tab)
+            .then(active => {
+              const event = active ? "start-shot" : "cancel-shot";
+              sendEvent(event, "toolbar-button", {incognito: tab.incognito});
+            }, (error) => {
+              if ((!onboarded) && error.popupMessage === "UNSHOOTABLE_PAGE") {
+                sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
+                return forceOnboarding();
+              }
+              throw error;
+            }));
       }
-      catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
-        sendEvent("goto-myshots", "about-newtab", {incognito: tab.incognito});
-      }));
-      catcher.watchPromise(
-        auth.authHeaders()
-        .then(() => browser.tabs.update({url: backend + "/shots"})));
-    } else {
-      catcher.watchPromise(
-        toggleSelector(tab)
-          .then(active => {
-            const event = active ? "start-shot" : "cancel-shot";
-            sendEvent(event, "toolbar-button", {incognito: tab.incognito});
-          }, (error) => {
-            if ((!hasSeenOnboarding) && error.popupMessage === "UNSHOOTABLE_PAGE") {
-              sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
-              return forceOnboarding();
-            }
-            throw error;
-          }));
-    }
+    }));
   });
 
   function forceOnboarding() {
     return browser.tabs.create({url: getOnboardingUrl()});
   }
 
   exports.onClickedContextMenu = catcher.watchFunction((info, tab) => {
     if (!tab) {
@@ -268,18 +270,18 @@ this.main = (function() {
     });
   });
 
   communication.register("closeSelector", (sender) => {
     setIconActive(false, sender.tab.id);
   });
 
   communication.register("hasSeenOnboarding", () => {
-    hasSeenOnboarding = true;
-    catcher.watchPromise(browser.storage.local.set({hasSeenOnboarding}));
+    hasSeenOnboarding = Promise.resolve(true);
+    catcher.watchPromise(browser.storage.local.set({hasSeenOnboarding: true}));
     setIconActive(false, null);
     startBackground.photonPageActionPort.postMessage({
       type: "setProperties",
       title: browser.i18n.getMessage("contextMenuLabel")
     });
   });
 
   communication.register("abortStartShot", () => {
--- a/browser/extensions/screenshots/webextension/background/selectorLoader.js
+++ b/browser/extensions/screenshots/webextension/background/selectorLoader.js
@@ -61,34 +61,36 @@ this.selectorLoader = (function() {
     }).then(result => {
       return result && result[0];
     });
   };
 
   const loadingTabs = new Set();
 
   exports.loadModules = function(tabId, hasSeenOnboarding) {
-    loadingTabs.add(tabId);
-    let promise = downloadOnlyCheck(tabId);
-    if (hasSeenOnboarding) {
-      promise = promise.then(() => {
-        return executeModules(tabId, standardScripts.concat(selectorScripts));
+    catcher.watchPromise(hasSeenOnboarding.then(onboarded => {
+      loadingTabs.add(tabId);
+      let promise = downloadOnlyCheck(tabId);
+      if (onboarded) {
+        promise = promise.then(() => {
+          return executeModules(tabId, standardScripts.concat(selectorScripts));
+        });
+      } else {
+        promise = promise.then(() => {
+          return executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
+        });
+      }
+      return promise.then((result) => {
+        loadingTabs.delete(tabId);
+        return result;
+      }, (error) => {
+        loadingTabs.delete(tabId);
+        throw error;
       });
-    } else {
-      promise = promise.then(() => {
-        return executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
-      });
-    }
-    return promise.then((result) => {
-      loadingTabs.delete(tabId);
-      return result;
-    }, (error) => {
-      loadingTabs.delete(tabId);
-      throw error;
-    });
+    }));
   };
 
   // TODO: since bootstrap communication is now required, would this function
   // make more sense inside background/main?
   function downloadOnlyCheck(tabId) {
     return communication.sendToBootstrap("isHistoryEnabled").then((historyEnabled) => {
       return communication.sendToBootstrap("isUploadDisabled").then((uploadDisabled) => {
         return browser.tabs.get(tabId).then(tab => {
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -186,23 +186,24 @@ this.takeshot = (function() {
     return auth.authHeaders().then((_headers) => {
       headers = _headers;
       if (blob) {
         const files = [ {fieldName: "blob", filename: "screenshot.png", blob} ];
         if (thumbnail) {
           files.push({fieldName: "thumbnail", filename: "thumbnail.png", blob: thumbnail});
         }
         return createMultipart(
-          {shot: JSON.stringify(shot.asJson())},
+          {shot: JSON.stringify(shot)},
+
           files
         );
       }
       return {
         "content-type": "application/json",
-        body: JSON.stringify(shot.asJson())
+        body: JSON.stringify(shot)
       };
 
     }).then((submission) => {
       headers["content-type"] = submission["content-type"];
       sendEvent("upload", "started", {eventValue: Math.floor(submission.body.length / 1000)});
       return fetch(shot.jsonUrl, {
         method: "PUT",
         mode: "cors",
--- a/browser/extensions/screenshots/webextension/build/raven.js
+++ b/browser/extensions/screenshots/webextension/build/raven.js
@@ -1,2587 +1,2942 @@
-/*! Raven.js 3.18.1 (2dca364) | github.com/getsentry/raven-js */
+/*! Raven.js 3.24.1 (f3b3500) | github.com/getsentry/raven-js */
 
 /*
  * Includes TraceKit
  * https://github.com/getsentry/TraceKit
  *
- * Copyright 2017 Matt Robenolt and other contributors
+ * Copyright 2018 Matt Robenolt and other contributors
  * Released under the BSD license
  * https://github.com/getsentry/raven-js/blob/master/LICENSE
  *
  */
 
-(function(f) {
-  if (typeof exports === 'object' && typeof module !== 'undefined') {
-    module.exports = f();
-  } else if (typeof define === 'function' && define.amd) {
-    define([], f);
-  } else {
-    var g;
-    if (typeof window !== 'undefined') {
-      g = window;
-    } else if (typeof global !== 'undefined') {
-      g = global;
-    } else if (typeof self !== 'undefined') {
-      g = self;
-    } else {
-      g = this;
-    }
-    g.Raven = f();
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Raven = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+function RavenConfigError(message) {
+  this.name = 'RavenConfigError';
+  this.message = message;
+}
+RavenConfigError.prototype = new Error();
+RavenConfigError.prototype.constructor = RavenConfigError;
+
+module.exports = RavenConfigError;
+
+},{}],2:[function(_dereq_,module,exports){
+var utils = _dereq_(5);
+
+var wrapMethod = function(console, level, callback) {
+  var originalConsoleLevel = console[level];
+  var originalConsole = console;
+
+  if (!(level in console)) {
+    return;
   }
-})(function() {
-  var define, module, exports;
-  return (function e(t, n, r) {
-    function s(o, u) {
-      if (!n[o]) {
-        if (!t[o]) {
-          var a = typeof require == 'function' && require;
-          if (!u && a) return a(o, !0);
-          if (i) return i(o, !0);
-          var f = new Error("Cannot find module '" + o + "'");
-          throw ((f.code = 'MODULE_NOT_FOUND'), f);
-        }
-        var l = (n[o] = {exports: {}});
-        t[o][0].call(
-          l.exports,
-          function(e) {
-            var n = t[o][1][e];
-            return s(n ? n : e);
-          },
-          l,
-          l.exports,
-          e,
-          t,
-          n,
-          r
-        );
+
+  var sentryLevel = level === 'warn' ? 'warning' : level;
+
+  console[level] = function() {
+    var args = [].slice.call(arguments);
+
+    var msg = utils.safeJoin(args, ' ');
+    var data = {level: sentryLevel, logger: 'console', extra: {arguments: args}};
+
+    if (level === 'assert') {
+      if (args[0] === false) {
+        // Default browsers message
+        msg =
+          'Assertion failed: ' + (utils.safeJoin(args.slice(1), ' ') || 'console.assert');
+        data.extra.arguments = args.slice(1);
+        callback && callback(msg, data);
       }
-      return n[o].exports;
+    } else {
+      callback && callback(msg, data);
+    }
+
+    // this fails for some browsers. :(
+    if (originalConsoleLevel) {
+      // IE9 doesn't allow calling apply on console functions directly
+      // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193
+      Function.prototype.apply.call(originalConsoleLevel, originalConsole, args);
     }
-    var i = typeof require == 'function' && require;
-    for (var o = 0; o < r.length; o++) s(r[o]);
-    return s;
-  })(
-    {
-      1: [
-        function(_dereq_, module, exports) {
-          function RavenConfigError(message) {
-            this.name = 'RavenConfigError';
-            this.message = message;
-          }
-          RavenConfigError.prototype = new Error();
-          RavenConfigError.prototype.constructor = RavenConfigError;
-
-          module.exports = RavenConfigError;
-        },
-        {}
-      ],
-      2: [
-        function(_dereq_, module, exports) {
-          var wrapMethod = function(console, level, callback) {
-            var originalConsoleLevel = console[level];
-            var originalConsole = console;
-
-            if (!(level in console)) {
-              return;
-            }
-
-            var sentryLevel = level === 'warn' ? 'warning' : level;
-
-            console[level] = function() {
-              var args = [].slice.call(arguments);
-
-              var msg = '' + args.join(' ');
-              var data = {
-                level: sentryLevel,
-                logger: 'console',
-                extra: {arguments: args}
-              };
-
-              if (level === 'assert') {
-                if (args[0] === false) {
-                  // Default browsers message
-                  msg =
-                    'Assertion failed: ' + (args.slice(1).join(' ') || 'console.assert');
-                  data.extra.arguments = args.slice(1);
-                  callback && callback(msg, data);
-                }
-              } else {
-                callback && callback(msg, data);
-              }
-
-              // this fails for some browsers. :(
-              if (originalConsoleLevel) {
-                // IE9 doesn't allow calling apply on console functions directly
-                // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193
-                Function.prototype.apply.call(
-                  originalConsoleLevel,
-                  originalConsole,
-                  args
-                );
-              }
-            };
-          };
-
-          module.exports = {
-            wrapMethod: wrapMethod
-          };
-        },
-        {}
-      ],
-      3: [
-        function(_dereq_, module, exports) {
-          (function(global) {
-            /*global XDomainRequest:false, __DEV__:false*/
-
-            var TraceKit = _dereq_(6);
-            var stringify = _dereq_(7);
-            var RavenConfigError = _dereq_(1);
-            var utils = _dereq_(5);
-
-            var isError = utils.isError,
-              isObject = utils.isObject;
-
-            var wrapConsoleMethod = _dereq_(2).wrapMethod;
-
-            var dsnKeys = 'source protocol user pass host port path'.split(' '),
-              dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/;
-
-            function now() {
-              return +new Date();
-            }
-
-            // This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
-            var _window =
-              typeof window !== 'undefined'
-                ? window
-                : typeof global !== 'undefined'
-                  ? global
-                  : typeof self !== 'undefined' ? self : {};
-            var _document = _window.document;
-            var _navigator = _window.navigator;
-
-            function keepOriginalCallback(original, callback) {
-              return isFunction(callback)
-                ? function(data) {
-                    return callback(data, original);
-                  }
-                : callback;
-            }
-
-            // First, check for JSON support
-            // If there is no JSON, we no-op the core features of Raven
-            // since JSON is required to encode the payload
-            function Raven() {
-              this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify);
-              // Raven can run in contexts where there's no document (react-native)
-              this._hasDocument = !isUndefined(_document);
-              this._hasNavigator = !isUndefined(_navigator);
-              this._lastCapturedException = null;
-              this._lastData = null;
-              this._lastEventId = null;
-              this._globalServer = null;
-              this._globalKey = null;
-              this._globalProject = null;
-              this._globalContext = {};
-              this._globalOptions = {
-                logger: 'javascript',
-                ignoreErrors: [],
-                ignoreUrls: [],
-                whitelistUrls: [],
-                includePaths: [],
-                collectWindowErrors: true,
-                maxMessageLength: 0,
-
-                // By default, truncates URL values to 250 chars
-                maxUrlLength: 250,
-                stackTraceLimit: 50,
-                autoBreadcrumbs: true,
-                instrument: true,
-                sampleRate: 1
-              };
-              this._ignoreOnError = 0;
-              this._isRavenInstalled = false;
-              this._originalErrorStackTraceLimit = Error.stackTraceLimit;
-              // capture references to window.console *and* all its methods first
-              // before the console plugin has a chance to monkey patch
-              this._originalConsole = _window.console || {};
-              this._originalConsoleMethods = {};
-              this._plugins = [];
-              this._startTime = now();
-              this._wrappedBuiltIns = [];
-              this._breadcrumbs = [];
-              this._lastCapturedEvent = null;
-              this._keypressTimeout;
-              this._location = _window.location;
-              this._lastHref = this._location && this._location.href;
-              this._resetBackoff();
-
-              // eslint-disable-next-line guard-for-in
-              for (var method in this._originalConsole) {
-                this._originalConsoleMethods[method] = this._originalConsole[method];
-              }
-            }
-
-            /*
+  };
+};
+
+module.exports = {
+  wrapMethod: wrapMethod
+};
+
+},{"5":5}],3:[function(_dereq_,module,exports){
+(function (global){
+/*global XDomainRequest:false */
+
+var TraceKit = _dereq_(6);
+var stringify = _dereq_(7);
+var md5 = _dereq_(8);
+var RavenConfigError = _dereq_(1);
+
+var utils = _dereq_(5);
+var isError = utils.isError;
+var isObject = utils.isObject;
+var isPlainObject = utils.isPlainObject;
+var isErrorEvent = utils.isErrorEvent;
+var isUndefined = utils.isUndefined;
+var isFunction = utils.isFunction;
+var isString = utils.isString;
+var isArray = utils.isArray;
+var isEmptyObject = utils.isEmptyObject;
+var each = utils.each;
+var objectMerge = utils.objectMerge;
+var truncate = utils.truncate;
+var objectFrozen = utils.objectFrozen;
+var hasKey = utils.hasKey;
+var joinRegExp = utils.joinRegExp;
+var urlencode = utils.urlencode;
+var uuid4 = utils.uuid4;
+var htmlTreeAsString = utils.htmlTreeAsString;
+var isSameException = utils.isSameException;
+var isSameStacktrace = utils.isSameStacktrace;
+var parseUrl = utils.parseUrl;
+var fill = utils.fill;
+var supportsFetch = utils.supportsFetch;
+var supportsReferrerPolicy = utils.supportsReferrerPolicy;
+var serializeKeysForMessage = utils.serializeKeysForMessage;
+var serializeException = utils.serializeException;
+var sanitize = utils.sanitize;
+
+var wrapConsoleMethod = _dereq_(2).wrapMethod;
+
+var dsnKeys = 'source protocol user pass host port path'.split(' '),
+  dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/;
+
+function now() {
+  return +new Date();
+}
+
+// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
+var _window =
+  typeof window !== 'undefined'
+    ? window
+    : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+var _document = _window.document;
+var _navigator = _window.navigator;
+
+function keepOriginalCallback(original, callback) {
+  return isFunction(callback)
+    ? function(data) {
+        return callback(data, original);
+      }
+    : callback;
+}
+
+// First, check for JSON support
+// If there is no JSON, we no-op the core features of Raven
+// since JSON is required to encode the payload
+function Raven() {
+  this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify);
+  // Raven can run in contexts where there's no document (react-native)
+  this._hasDocument = !isUndefined(_document);
+  this._hasNavigator = !isUndefined(_navigator);
+  this._lastCapturedException = null;
+  this._lastData = null;
+  this._lastEventId = null;
+  this._globalServer = null;
+  this._globalKey = null;
+  this._globalProject = null;
+  this._globalContext = {};
+  this._globalOptions = {
+    // SENTRY_RELEASE can be injected by https://github.com/getsentry/sentry-webpack-plugin
+    release: _window.SENTRY_RELEASE && _window.SENTRY_RELEASE.id,
+    logger: 'javascript',
+    ignoreErrors: [],
+    ignoreUrls: [],
+    whitelistUrls: [],
+    includePaths: [],
+    headers: null,
+    collectWindowErrors: true,
+    captureUnhandledRejections: true,
+    maxMessageLength: 0,
+    // By default, truncates URL values to 250 chars
+    maxUrlLength: 250,
+    stackTraceLimit: 50,
+    autoBreadcrumbs: true,
+    instrument: true,
+    sampleRate: 1,
+    sanitizeKeys: []
+  };
+  this._fetchDefaults = {
+    method: 'POST',
+    keepalive: true,
+    // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
+    // https://caniuse.com/#feat=referrer-policy
+    // It doesn't. And it throw exception instead of ignoring this parameter...
+    // REF: https://github.com/getsentry/raven-js/issues/1233
+    referrerPolicy: supportsReferrerPolicy() ? 'origin' : ''
+  };
+  this._ignoreOnError = 0;
+  this._isRavenInstalled = false;
+  this._originalErrorStackTraceLimit = Error.stackTraceLimit;
+  // capture references to window.console *and* all its methods first
+  // before the console plugin has a chance to monkey patch
+  this._originalConsole = _window.console || {};
+  this._originalConsoleMethods = {};
+  this._plugins = [];
+  this._startTime = now();
+  this._wrappedBuiltIns = [];
+  this._breadcrumbs = [];
+  this._lastCapturedEvent = null;
+  this._keypressTimeout;
+  this._location = _window.location;
+  this._lastHref = this._location && this._location.href;
+  this._resetBackoff();
+
+  // eslint-disable-next-line guard-for-in
+  for (var method in this._originalConsole) {
+    this._originalConsoleMethods[method] = this._originalConsole[method];
+  }
+}
+
+/*
  * The core Raven singleton
  *
  * @this {Raven}
  */
 
-            Raven.prototype = {
-              // Hardcode version string so that raven source can be loaded directly via
-              // webpack (using a build step causes webpack #1617). Grunt verifies that
-              // this value matches package.json during build.
-              //   See: https://github.com/getsentry/raven-js/issues/465
-              VERSION: '3.18.1',
-
-              debug: false,
-
-              TraceKit: TraceKit, // alias to TraceKit
-
-              /*
+Raven.prototype = {
+  // Hardcode version string so that raven source can be loaded directly via
+  // webpack (using a build step causes webpack #1617). Grunt verifies that
+  // this value matches package.json during build.
+  //   See: https://github.com/getsentry/raven-js/issues/465
+  VERSION: '3.24.1',
+
+  debug: false,
+
+  TraceKit: TraceKit, // alias to TraceKit
+
+  /*
      * Configure Raven with a DSN and extra options
      *
      * @param {string} dsn The public Sentry DSN
      * @param {object} options Set of global options [optional]
      * @return {Raven}
      */
-              config: function(dsn, options) {
-                var self = this;
-
-                if (self._globalServer) {
-                  this._logDebug('error', 'Error: Raven has already been configured');
-                  return self;
-                }
-                if (!dsn) return self;
-
-                var globalOptions = self._globalOptions;
-
-                // merge in options
-                if (options) {
-                  each(options, function(key, value) {
-                    // tags and extra are special and need to be put into context
-                    if (key === 'tags' || key === 'extra' || key === 'user') {
-                      self._globalContext[key] = value;
-                    } else {
-                      globalOptions[key] = value;
-                    }
-                  });
-                }
-
-                self.setDSN(dsn);
-
-                // "Script error." is hard coded into browsers for errors that it can't read.
-                // this is the result of a script being pulled in from an external domain and CORS.
-                globalOptions.ignoreErrors.push(/^Script error\.?$/);
-                globalOptions.ignoreErrors.push(
-                  /^Javascript error: Script error\.? on line 0$/
-                );
-
-                // join regexp rules into one big rule
-                globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors);
-                globalOptions.ignoreUrls = globalOptions.ignoreUrls.length
-                  ? joinRegExp(globalOptions.ignoreUrls)
-                  : false;
-                globalOptions.whitelistUrls = globalOptions.whitelistUrls.length
-                  ? joinRegExp(globalOptions.whitelistUrls)
-                  : false;
-                globalOptions.includePaths = joinRegExp(globalOptions.includePaths);
-                globalOptions.maxBreadcrumbs = Math.max(
-                  0,
-                  Math.min(globalOptions.maxBreadcrumbs || 100, 100)
-                ); // default and hard limit is 100
-
-                var autoBreadcrumbDefaults = {
-                  xhr: true,
-                  console: true,
-                  dom: true,
-                  location: true
-                };
-
-                var autoBreadcrumbs = globalOptions.autoBreadcrumbs;
-                if ({}.toString.call(autoBreadcrumbs) === '[object Object]') {
-                  autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs);
-                } else if (autoBreadcrumbs !== false) {
-                  autoBreadcrumbs = autoBreadcrumbDefaults;
-                }
-                globalOptions.autoBreadcrumbs = autoBreadcrumbs;
-
-                var instrumentDefaults = {
-                  tryCatch: true
-                };
-
-                var instrument = globalOptions.instrument;
-                if ({}.toString.call(instrument) === '[object Object]') {
-                  instrument = objectMerge(instrumentDefaults, instrument);
-                } else if (instrument !== false) {
-                  instrument = instrumentDefaults;
-                }
-                globalOptions.instrument = instrument;
-
-                TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors;
-
-                // return for chaining
-                return self;
-              },
-
-              /*
+  config: function(dsn, options) {
+    var self = this;
+
+    if (self._globalServer) {
+      this._logDebug('error', 'Error: Raven has already been configured');
+      return self;
+    }
+    if (!dsn) return self;
+
+    var globalOptions = self._globalOptions;
+
+    // merge in options
+    if (options) {
+      each(options, function(key, value) {
+        // tags and extra are special and need to be put into context
+        if (key === 'tags' || key === 'extra' || key === 'user') {
+          self._globalContext[key] = value;
+        } else {
+          globalOptions[key] = value;
+        }
+      });
+    }
+
+    self.setDSN(dsn);
+
+    // "Script error." is hard coded into browsers for errors that it can't read.
+    // this is the result of a script being pulled in from an external domain and CORS.
+    globalOptions.ignoreErrors.push(/^Script error\.?$/);
+    globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/);
+
+    // join regexp rules into one big rule
+    globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors);
+    globalOptions.ignoreUrls = globalOptions.ignoreUrls.length
+      ? joinRegExp(globalOptions.ignoreUrls)
+      : false;
+    globalOptions.whitelistUrls = globalOptions.whitelistUrls.length
+      ? joinRegExp(globalOptions.whitelistUrls)
+      : false;
+    globalOptions.includePaths = joinRegExp(globalOptions.includePaths);
+    globalOptions.maxBreadcrumbs = Math.max(
+      0,
+      Math.min(globalOptions.maxBreadcrumbs || 100, 100)
+    ); // default and hard limit is 100
+
+    var autoBreadcrumbDefaults = {
+      xhr: true,
+      console: true,
+      dom: true,
+      location: true,
+      sentry: true
+    };
+
+    var autoBreadcrumbs = globalOptions.autoBreadcrumbs;
+    if ({}.toString.call(autoBreadcrumbs) === '[object Object]') {
+      autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs);
+    } else if (autoBreadcrumbs !== false) {
+      autoBreadcrumbs = autoBreadcrumbDefaults;
+    }
+    globalOptions.autoBreadcrumbs = autoBreadcrumbs;
+
+    var instrumentDefaults = {
+      tryCatch: true
+    };
+
+    var instrument = globalOptions.instrument;
+    if ({}.toString.call(instrument) === '[object Object]') {
+      instrument = objectMerge(instrumentDefaults, instrument);
+    } else if (instrument !== false) {
+      instrument = instrumentDefaults;
+    }
+    globalOptions.instrument = instrument;
+
+    TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors;
+
+    // return for chaining
+    return self;
+  },
+
+  /*
      * Installs a global window.onerror error handler
      * to capture and report uncaught exceptions.
      * At this point, install() is required to be called due
      * to the way TraceKit is set up.
      *
      * @return {Raven}
      */
-              install: function() {
-                var self = this;
-                if (self.isSetup() && !self._isRavenInstalled) {
-                  TraceKit.report.subscribe(function() {
-                    self._handleOnErrorStackInfo.apply(self, arguments);
-                  });
-                  if (
-                    self._globalOptions.instrument &&
-                    self._globalOptions.instrument.tryCatch
-                  ) {
-                    self._instrumentTryCatch();
-                  }
-
-                  if (self._globalOptions.autoBreadcrumbs) self._instrumentBreadcrumbs();
-
-                  // Install all of the plugins
-                  self._drainPlugins();
-
-                  self._isRavenInstalled = true;
-                }
-
-                Error.stackTraceLimit = self._globalOptions.stackTraceLimit;
-                return this;
-              },
-
-              /*
+  install: function() {
+    var self = this;
+    if (self.isSetup() && !self._isRavenInstalled) {
+      TraceKit.report.subscribe(function() {
+        self._handleOnErrorStackInfo.apply(self, arguments);
+      });
+
+      if (self._globalOptions.captureUnhandledRejections) {
+        self._attachPromiseRejectionHandler();
+      }
+
+      self._patchFunctionToString();
+
+      if (self._globalOptions.instrument && self._globalOptions.instrument.tryCatch) {
+        self._instrumentTryCatch();
+      }
+
+      if (self._globalOptions.autoBreadcrumbs) self._instrumentBreadcrumbs();
+
+      // Install all of the plugins
+      self._drainPlugins();
+
+      self._isRavenInstalled = true;
+    }
+
+    Error.stackTraceLimit = self._globalOptions.stackTraceLimit;
+    return this;
+  },
+
+  /*
      * Set the DSN (can be called multiple time unlike config)
      *
      * @param {string} dsn The public Sentry DSN
      */
-              setDSN: function(dsn) {
-                var self = this,
-                  uri = self._parseDSN(dsn),
-                  lastSlash = uri.path.lastIndexOf('/'),
-                  path = uri.path.substr(1, lastSlash);
-
-                self._dsn = dsn;
-                self._globalKey = uri.user;
-                self._globalSecret = uri.pass && uri.pass.substr(1);
-                self._globalProject = uri.path.substr(lastSlash + 1);
-
-                self._globalServer = self._getGlobalServer(uri);
-
-                self._globalEndpoint =
-                  self._globalServer +
-                  '/' +
-                  path +
-                  'api/' +
-                  self._globalProject +
-                  '/store/';
-
-                // Reset backoff state since we may be pointing at a
-                // new project/server
-                this._resetBackoff();
-              },
-
-              /*
+  setDSN: function(dsn) {
+    var self = this,
+      uri = self._parseDSN(dsn),
+      lastSlash = uri.path.lastIndexOf('/'),
+      path = uri.path.substr(1, lastSlash);
+
+    self._dsn = dsn;
+    self._globalKey = uri.user;
+    self._globalSecret = uri.pass && uri.pass.substr(1);
+    self._globalProject = uri.path.substr(lastSlash + 1);
+
+    self._globalServer = self._getGlobalServer(uri);
+
+    self._globalEndpoint =
+      self._globalServer + '/' + path + 'api/' + self._globalProject + '/store/';
+
+    // Reset backoff state since we may be pointing at a
+    // new project/server
+    this._resetBackoff();
+  },
+
+  /*
      * Wrap code within a context so Raven can capture errors
      * reliably across domains that is executed immediately.
      *
      * @param {object} options A specific set of options for this context [optional]
      * @param {function} func The callback to be immediately executed within the context
      * @param {array} args An array of arguments to be called with the callback [optional]
      */
-              context: function(options, func, args) {
-                if (isFunction(options)) {
-                  args = func || [];
-                  func = options;
-                  options = undefined;
-                }
-
-                return this.wrap(options, func).apply(this, args);
-              },
-
-              /*
+  context: function(options, func, args) {
+    if (isFunction(options)) {
+      args = func || [];
+      func = options;
+      options = undefined;
+    }
+
+    return this.wrap(options, func).apply(this, args);
+  },
+
+  /*
      * Wrap code within a context and returns back a new function to be executed
      *
      * @param {object} options A specific set of options for this context [optional]
      * @param {function} func The function to be wrapped in a new context
      * @param {function} func A function to call before the try/catch wrapper [optional, private]
      * @return {function} The newly wrapped functions with a context
      */
-              wrap: function(options, func, _before) {
-                var self = this;
-                // 1 argument has been passed, and it's not a function
-                // so just return it
-                if (isUndefined(func) && !isFunction(options)) {
-                  return options;
-                }
-
-                // options is optional
-                if (isFunction(options)) {
-                  func = options;
-                  options = undefined;
-                }
-
-                // At this point, we've passed along 2 arguments, and the second one
-                // is not a function either, so we'll just return the second argument.
-                if (!isFunction(func)) {
-                  return func;
-                }
-
-                // We don't wanna wrap it twice!
-                try {
-                  if (func.__raven__) {
-                    return func;
-                  }
-
-                  // If this has already been wrapped in the past, return that
-                  if (func.__raven_wrapper__) {
-                    return func.__raven_wrapper__;
-                  }
-                } catch (e) {
-                  // Just accessing custom props in some Selenium environments
-                  // can cause a "Permission denied" exception (see raven-js#495).
-                  // Bail on wrapping and return the function as-is (defers to window.onerror).
-                  return func;
-                }
-
-                function wrapped() {
-                  var args = [],
-                    i = arguments.length,
-                    deep = !options || (options && options.deep !== false);
-
-                  if (_before && isFunction(_before)) {
-                    _before.apply(this, arguments);
-                  }
-
-                  // Recursively wrap all of a function's arguments that are
-                  // functions themselves.
-                  while (i--)
-                    args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i];
-
-                  try {
-                    // Attempt to invoke user-land function
-                    // NOTE: If you are a Sentry user, and you are seeing this stack frame, it
-                    //       means Raven caught an error invoking your application code. This is
-                    //       expected behavior and NOT indicative of a bug with Raven.js.
-                    return func.apply(this, args);
-                  } catch (e) {
-                    self._ignoreNextOnError();
-                    self.captureException(e, options);
-                    throw e;
-                  }
-                }
-
-                // copy over properties of the old function
-                for (var property in func) {
-                  if (hasKey(func, property)) {
-                    wrapped[property] = func[property];
-                  }
-                }
-                wrapped.prototype = func.prototype;
-
-                func.__raven_wrapper__ = wrapped;
-                // Signal that this function has been wrapped already
-                // for both debugging and to prevent it to being wrapped twice
-                wrapped.__raven__ = true;
-                wrapped.__inner__ = func;
-
-                return wrapped;
-              },
-
-              /*
-     * Uninstalls the global error handler.
-     *
-     * @return {Raven}
-     */
-              uninstall: function() {
-                TraceKit.report.uninstall();
-
-                this._restoreBuiltIns();
-
-                Error.stackTraceLimit = this._originalErrorStackTraceLimit;
-                this._isRavenInstalled = false;
-
-                return this;
-              },
-
-              /*
-     * Manually capture an exception and send it over to Sentry
-     *
-     * @param {error} ex An exception to be logged
-     * @param {object} options A specific set of options for this error [optional]
-     * @return {Raven}
-     */
-              captureException: function(ex, options) {
-                // If not an Error is passed through, recall as a message instead
-                if (!isError(ex)) {
-                  return this.captureMessage(
-                    ex,
-                    objectMerge(
-                      {
-                        trimHeadFrames: 1,
-                        stacktrace: true // if we fall back to captureMessage, default to attempting a new trace
-                      },
-                      options
-                    )
-                  );
-                }
-
-                // Store the raw exception object for potential debugging and introspection
-                this._lastCapturedException = ex;
-
-                // TraceKit.report will re-raise any exception passed to it,
-                // which means you have to wrap it in try/catch. Instead, we
-                // can wrap it here and only re-raise if TraceKit.report
-                // raises an exception different from the one we asked to
-                // report on.
-                try {
-                  var stack = TraceKit.computeStackTrace(ex);
-                  this._handleStackInfo(stack, options);
-                } catch (ex1) {
-                  if (ex !== ex1) {
-                    throw ex1;
-                  }
-                }
-
-                return this;
-              },
-
-              /*
+  wrap: function(options, func, _before) {
+    var self = this;
+    // 1 argument has been passed, and it's not a function
+    // so just return it
+    if (isUndefined(func) && !isFunction(options)) {
+      return options;
+    }
+
+    // options is optional
+    if (isFunction(options)) {
+      func = options;
+      options = undefined;
+    }
+
+    // At this point, we've passed along 2 arguments, and the second one
+    // is not a function either, so we'll just return the second argument.
+    if (!isFunction(func)) {
+      return func;
+    }
+
+    // We don't wanna wrap it twice!
+    try {
+      if (func.__raven__) {
+        return func;
+      }
+
+      // If this has already been wrapped in the past, return that
+      if (func.__raven_wrapper__) {
+        return func.__raven_wrapper__;
+      }
+    } catch (e) {
+      // Just accessing custom props in some Selenium environments
+      // can cause a "Permission denied" exception (see raven-js#495).
+      // Bail on wrapping and return the function as-is (defers to window.onerror).
+      return func;
+    }
+
+    function wrapped() {
+      var args = [],
+        i = arguments.length,
+        deep = !options || (options && options.deep !== false);
+
+      if (_before && isFunction(_before)) {
+        _before.apply(this, arguments);
+      }
+
+      // Recursively wrap all of a function's arguments that are
+      // functions themselves.
+      while (i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i];
+
+      try {
+        // Attempt to invoke user-land function
+        // NOTE: If you are a Sentry user, and you are seeing this stack frame, it
+        //       means Raven caught an error invoking your application code. This is
+        //       expected behavior and NOT indicative of a bug with Raven.js.
+        return func.apply(this, args);
+      } catch (e) {
+        self._ignoreNextOnError();
+        self.captureException(e, options);
+        throw e;
+      }
+    }
+
+    // copy over properties of the old function
+    for (var property in func) {
+      if (hasKey(func, property)) {
+        wrapped[property] = func[property];
+      }
+    }
+    wrapped.prototype = func.prototype;
+
+    func.__raven_wrapper__ = wrapped;
+    // Signal that this function has been wrapped/filled already
+    // for both debugging and to prevent it to being wrapped/filled twice
+    wrapped.__raven__ = true;
+    wrapped.__orig__ = func;
+
+    return wrapped;
+  },
+
+  /**
+   * Uninstalls the global error handler.
+   *
+   * @return {Raven}
+   */
+  uninstall: function() {
+    TraceKit.report.uninstall();
+
+    this._detachPromiseRejectionHandler();
+    this._unpatchFunctionToString();
+    this._restoreBuiltIns();
+    this._restoreConsole();
+
+    Error.stackTraceLimit = this._originalErrorStackTraceLimit;
+    this._isRavenInstalled = false;
+
+    return this;
+  },
+
+  /**
+   * Callback used for `unhandledrejection` event
+   *
+   * @param {PromiseRejectionEvent} event An object containing
+   *   promise: the Promise that was rejected
+   *   reason: the value with which the Promise was rejected
+   * @return void
+   */
+  _promiseRejectionHandler: function(event) {
+    this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
+    this.captureException(event.reason, {
+      extra: {
+        unhandledPromiseRejection: true
+      }
+    });
+  },
+
+  /**
+   * Installs the global promise rejection handler.
+   *
+   * @return {raven}
+   */
+  _attachPromiseRejectionHandler: function() {
+    this._promiseRejectionHandler = this._promiseRejectionHandler.bind(this);
+    _window.addEventListener &&
+      _window.addEventListener('unhandledrejection', this._promiseRejectionHandler);
+    return this;
+  },
+
+  /**
+   * Uninstalls the global promise rejection handler.
+   *
+   * @return {raven}
+   */
+  _detachPromiseRejectionHandler: function() {
+    _window.removeEventListener &&
+      _window.removeEventListener('unhandledrejection', this._promiseRejectionHandler);
+    return this;
+  },
+
+  /**
+   * Manually capture an exception and send it over to Sentry
+   *
+   * @param {error} ex An exception to be logged
+   * @param {object} options A specific set of options for this error [optional]
+   * @return {Raven}
+   */
+  captureException: function(ex, options) {
+    options = objectMerge({trimHeadFrames: 0}, options ? options : {});
+
+    if (isErrorEvent(ex) && ex.error) {
+      // If it is an ErrorEvent with `error` property, extract it to get actual Error
+      ex = ex.error;
+    } else if (isError(ex)) {
+      // we have a real Error object
+      ex = ex;
+    } else if (isPlainObject(ex)) {
+      // If it is plain Object, serialize it manually and extract options
+      // This will allow us to group events based on top-level keys
+      // which is much better than creating new group when any key/value change
+      options = this._getCaptureExceptionOptionsFromPlainObject(options, ex);
+      ex = new Error(options.message);
+    } else {
+      // If none of previous checks were valid, then it means that
+      // it's not a plain Object
+      // it's not a valid ErrorEvent (one with an error property)
+      // it's not an Error
+      // So bail out and capture it as a simple message:
+      return this.captureMessage(
+        ex,
+        objectMerge(options, {
+          stacktrace: true, // if we fall back to captureMessage, default to attempting a new trace
+          trimHeadFrames: options.trimHeadFrames + 1
+        })
+      );
+    }
+
+    // Store the raw exception object for potential debugging and introspection
+    this._lastCapturedException = ex;
+
+    // TraceKit.report will re-raise any exception passed to it,
+    // which means you have to wrap it in try/catch. Instead, we
+    // can wrap it here and only re-raise if TraceKit.report
+    // raises an exception different from the one we asked to
+    // report on.
+    try {
+      var stack = TraceKit.computeStackTrace(ex);
+      this._handleStackInfo(stack, options);
+    } catch (ex1) {
+      if (ex !== ex1) {
+        throw ex1;
+      }
+    }
+
+    return this;
+  },
+
+  _getCaptureExceptionOptionsFromPlainObject: function(currentOptions, ex) {
+    var exKeys = Object.keys(ex).sort();
+    var options = objectMerge(currentOptions, {
+      message:
+        'Non-Error exception captured with keys: ' + serializeKeysForMessage(exKeys),
+      fingerprint: [md5(exKeys)],
+      extra: currentOptions.extra || {}
+    });
+    options.extra.__serialized__ = serializeException(ex);
+
+    return options;
+  },
+
+  /*
      * Manually send a message to Sentry
      *
      * @param {string} msg A plain message to be captured in Sentry
      * @param {object} options A specific set of options for this message [optional]
      * @return {Raven}
      */
-              captureMessage: function(msg, options) {
-                // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an
-                // early call; we'll error on the side of logging anything called before configuration since it's
-                // probably something you should see:
-                if (
-                  !!this._globalOptions.ignoreErrors.test &&
-                  this._globalOptions.ignoreErrors.test(msg)
-                ) {
-                  return;
-                }
-
-                options = options || {};
-
-                var data = objectMerge(
-                  {
-                    message: msg + '' // Make sure it's actually a string
-                  },
-                  options
-                );
-
-                if (this._globalOptions.stacktrace || (options && options.stacktrace)) {
-                  var ex;
-                  // Generate a "synthetic" stack trace from this point.
-                  // NOTE: If you are a Sentry user, and you are seeing this stack frame, it is NOT indicative
-                  //       of a bug with Raven.js. Sentry generates synthetic traces either by configuration,
-                  //       or if it catches a thrown object without a "stack" property.
-                  try {
-                    throw new Error(msg);
-                  } catch (ex1) {
-                    ex = ex1;
-                  }
-
-                  // null exception name so `Error` isn't prefixed to msg
-                  ex.name = null;
-
-                  options = objectMerge(
-                    {
-                      // fingerprint on msg, not stack trace (legacy behavior, could be
-                      // revisited)
-                      fingerprint: msg,
-                      // since we know this is a synthetic trace, the top N-most frames
-                      // MUST be from Raven.js, so mark them as in_app later by setting
-                      // trimHeadFrames
-                      trimHeadFrames: (options.trimHeadFrames || 0) + 1
-                    },
-                    options
-                  );
-
-                  var stack = TraceKit.computeStackTrace(ex);
-                  var frames = this._prepareFrames(stack, options);
-                  data.stacktrace = {
-                    // Sentry expects frames oldest to newest
-                    frames: frames.reverse()
-                  };
-                }
-
-                // Fire away!
-                this._send(data);
-
-                return this;
-              },
-
-              captureBreadcrumb: function(obj) {
-                var crumb = objectMerge(
-                  {
-                    timestamp: now() / 1000
-                  },
-                  obj
-                );
-
-                if (isFunction(this._globalOptions.breadcrumbCallback)) {
-                  var result = this._globalOptions.breadcrumbCallback(crumb);
-
-                  if (isObject(result) && !isEmptyObject(result)) {
-                    crumb = result;
-                  } else if (result === false) {
-                    return this;
-                  }
-                }
-
-                this._breadcrumbs.push(crumb);
-                if (this._breadcrumbs.length > this._globalOptions.maxBreadcrumbs) {
-                  this._breadcrumbs.shift();
-                }
-                return this;
-              },
-
-              addPlugin: function(plugin /*arg1, arg2, ... argN*/) {
-                var pluginArgs = [].slice.call(arguments, 1);
-
-                this._plugins.push([plugin, pluginArgs]);
-                if (this._isRavenInstalled) {
-                  this._drainPlugins();
-                }
-
-                return this;
-              },
-
-              /*
+  captureMessage: function(msg, options) {
+    // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an
+    // early call; we'll error on the side of logging anything called before configuration since it's
+    // probably something you should see:
+    if (
+      !!this._globalOptions.ignoreErrors.test &&
+      this._globalOptions.ignoreErrors.test(msg)
+    ) {
+      return;
+    }
+
+    options = options || {};
+    msg = msg + ''; // Make sure it's actually a string
+
+    var data = objectMerge(
+      {
+        message: msg
+      },
+      options
+    );
+
+    var ex;
+    // Generate a "synthetic" stack trace from this point.
+    // NOTE: If you are a Sentry user, and you are seeing this stack frame, it is NOT indicative
+    //       of a bug with Raven.js. Sentry generates synthetic traces either by configuration,
+    //       or if it catches a thrown object without a "stack" property.
+    try {
+      throw new Error(msg);
+    } catch (ex1) {
+      ex = ex1;
+    }
+
+    // null exception name so `Error` isn't prefixed to msg
+    ex.name = null;
+    var stack = TraceKit.computeStackTrace(ex);
+
+    // stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
+    var initialCall = isArray(stack.stack) && stack.stack[1];
+    var fileurl = (initialCall && initialCall.url) || '';
+
+    if (
+      !!this._globalOptions.ignoreUrls.test &&
+      this._globalOptions.ignoreUrls.test(fileurl)
+    ) {
+      return;
+    }
+
+    if (
+      !!this._globalOptions.whitelistUrls.test &&
+      !this._globalOptions.whitelistUrls.test(fileurl)
+    ) {
+      return;
+    }
+
+    if (this._globalOptions.stacktrace || (options && options.stacktrace)) {
+      // fingerprint on msg, not stack trace (legacy behavior, could be revisited)
+      data.fingerprint = data.fingerprint == null ? msg : data.fingerprint;
+
+      options = objectMerge(
+        {
+          trimHeadFrames: 0
+        },
+        options
+      );
+      // Since we know this is a synthetic trace, the top frame (this function call)
+      // MUST be from Raven.js, so mark it for trimming
+      // We add to the trim counter so that callers can choose to trim extra frames, such
+      // as utility functions.
+      options.trimHeadFrames += 1;
+
+      var frames = this._prepareFrames(stack, options);
+      data.stacktrace = {
+        // Sentry expects frames oldest to newest
+        frames: frames.reverse()
+      };
+    }
+
+    // Make sure that fingerprint is always wrapped in an array
+    if (data.fingerprint) {
+      data.fingerprint = isArray(data.fingerprint)
+        ? data.fingerprint
+        : [data.fingerprint];
+    }
+
+    // Fire away!
+    this._send(data);
+
+    return this;
+  },
+
+  captureBreadcrumb: function(obj) {
+    var crumb = objectMerge(
+      {
+        timestamp: now() / 1000
+      },
+      obj
+    );
+
+    if (isFunction(this._globalOptions.breadcrumbCallback)) {
+      var result = this._globalOptions.breadcrumbCallback(crumb);
+
+      if (isObject(result) && !isEmptyObject(result)) {
+        crumb = result;
+      } else if (result === false) {
+        return this;
+      }
+    }
+
+    this._breadcrumbs.push(crumb);
+    if (this._breadcrumbs.length > this._globalOptions.maxBreadcrumbs) {
+      this._breadcrumbs.shift();
+    }
+    return this;
+  },
+
+  addPlugin: function(plugin /*arg1, arg2, ... argN*/) {
+    var pluginArgs = [].slice.call(arguments, 1);
+
+    this._plugins.push([plugin, pluginArgs]);
+    if (this._isRavenInstalled) {
+      this._drainPlugins();
+    }
+
+    return this;
+  },
+
+  /*
      * Set/clear a user to be sent along with the payload.
      *
      * @param {object} user An object representing user data [optional]
      * @return {Raven}
      */
-              setUserContext: function(user) {
-                // Intentionally do not merge here since that's an unexpected behavior.
-                this._globalContext.user = user;
-
-                return this;
-              },
-
-              /*
+  setUserContext: function(user) {
+    // Intentionally do not merge here since that's an unexpected behavior.
+    this._globalContext.user = user;
+
+    return this;
+  },
+
+  /*
      * Merge extra attributes to be sent along with the payload.
      *
      * @param {object} extra An object representing extra data [optional]
      * @return {Raven}
      */
-              setExtraContext: function(extra) {
-                this._mergeContext('extra', extra);
-
-                return this;
-              },
-
-              /*
+  setExtraContext: function(extra) {
+    this._mergeContext('extra', extra);
+
+    return this;
+  },
+
+  /*
      * Merge tags to be sent along with the payload.
      *
      * @param {object} tags An object representing tags [optional]
      * @return {Raven}
      */
-              setTagsContext: function(tags) {
-                this._mergeContext('tags', tags);
-
-                return this;
-              },
-
-              /*
+  setTagsContext: function(tags) {
+    this._mergeContext('tags', tags);
+
+    return this;
+  },
+
+  /*
      * Clear all of the context.
      *
      * @return {Raven}
      */
-              clearContext: function() {
-                this._globalContext = {};
-
-                return this;
-              },
-
-              /*
+  clearContext: function() {
+    this._globalContext = {};
+
+    return this;
+  },
+
+  /*
      * Get a copy of the current context. This cannot be mutated.
      *
      * @return {object} copy of context
      */
-              getContext: function() {
-                // lol javascript
-                return JSON.parse(stringify(this._globalContext));
-              },
-
-              /*
+  getContext: function() {
+    // lol javascript
+    return JSON.parse(stringify(this._globalContext));
+  },
+
+  /*
      * Set environment of application
      *
      * @param {string} environment Typically something like 'production'.
      * @return {Raven}
      */
-              setEnvironment: function(environment) {
-                this._globalOptions.environment = environment;
-
-                return this;
-              },
-
-              /*
+  setEnvironment: function(environment) {
+    this._globalOptions.environment = environment;
+
+    return this;
+  },
+
+  /*
      * Set release version of application
      *
      * @param {string} release Typically something like a git SHA to identify version
      * @return {Raven}
      */
-              setRelease: function(release) {
-                this._globalOptions.release = release;
-
-                return this;
-              },
-
-              /*
+  setRelease: function(release) {
+    this._globalOptions.release = release;
+
+    return this;
+  },
+
+  /*
      * Set the dataCallback option
      *
      * @param {function} callback The callback to run which allows the
      *                            data blob to be mutated before sending
      * @return {Raven}
      */
-              setDataCallback: function(callback) {
-                var original = this._globalOptions.dataCallback;
-                this._globalOptions.dataCallback = keepOriginalCallback(
-                  original,
-                  callback
-                );
-                return this;
-              },
-
-              /*
+  setDataCallback: function(callback) {
+    var original = this._globalOptions.dataCallback;
+    this._globalOptions.dataCallback = keepOriginalCallback(original, callback);
+    return this;
+  },
+
+  /*
      * Set the breadcrumbCallback option
      *
      * @param {function} callback The callback to run which allows filtering
      *                            or mutating breadcrumbs
      * @return {Raven}
      */
-              setBreadcrumbCallback: function(callback) {
-                var original = this._globalOptions.breadcrumbCallback;
-                this._globalOptions.breadcrumbCallback = keepOriginalCallback(
-                  original,
-                  callback
-                );
-                return this;
-              },
-
-              /*
+  setBreadcrumbCallback: function(callback) {
+    var original = this._globalOptions.breadcrumbCallback;
+    this._globalOptions.breadcrumbCallback = keepOriginalCallback(original, callback);
+    return this;
+  },
+
+  /*
      * Set the shouldSendCallback option
      *
      * @param {function} callback The callback to run which allows
      *                            introspecting the blob before sending
      * @return {Raven}
      */
-              setShouldSendCallback: function(callback) {
-                var original = this._globalOptions.shouldSendCallback;
-                this._globalOptions.shouldSendCallback = keepOriginalCallback(
-                  original,
-                  callback
-                );
-                return this;
-              },
-
-              /**
-     * Override the default HTTP transport mechanism that transmits data
-     * to the Sentry server.
-     *
-     * @param {function} transport Function invoked instead of the default
-     *                             `makeRequest` handler.
-     *
-     * @return {Raven}
-     */
-              setTransport: function(transport) {
-                this._globalOptions.transport = transport;
-
-                return this;
-              },
-
-              /*
+  setShouldSendCallback: function(callback) {
+    var original = this._globalOptions.shouldSendCallback;
+    this._globalOptions.shouldSendCallback = keepOriginalCallback(original, callback);
+    return this;
+  },
+
+  /**
+   * Override the default HTTP transport mechanism that transmits data
+   * to the Sentry server.
+   *
+   * @param {function} transport Function invoked instead of the default
+   *                             `makeRequest` handler.
+   *
+   * @return {Raven}
+   */
+  setTransport: function(transport) {
+    this._globalOptions.transport = transport;
+
+    return this;
+  },
+
+  /*
      * Get the latest raw exception that was captured by Raven.
      *
      * @return {error}
      */
-              lastException: function() {
-                return this._lastCapturedException;
-              },
-
-              /*
+  lastException: function() {
+    return this._lastCapturedException;
+  },
+
+  /*
      * Get the last event id
      *
      * @return {string}
      */
-              lastEventId: function() {
-                return this._lastEventId;
-              },
-
-              /*
+  lastEventId: function() {
+    return this._lastEventId;
+  },
+
+  /*
      * Determine if Raven is setup and ready to go.
      *
      * @return {boolean}
      */
-              isSetup: function() {
-                if (!this._hasJSON) return false; // needs JSON support
-                if (!this._globalServer) {
-                  if (!this.ravenNotConfiguredError) {
-                    this.ravenNotConfiguredError = true;
-                    this._logDebug('error', 'Error: Raven has not been configured.');
-                  }
-                  return false;
-                }
-                return true;
-              },
-
-              afterLoad: function() {
-                // TODO: remove window dependence?
-
-                // Attempt to initialize Raven on load
-                var RavenConfig = _window.RavenConfig;
-                if (RavenConfig) {
-                  this.config(RavenConfig.dsn, RavenConfig.config).install();
-                }
-              },
-
-              showReportDialog: function(options) {
-                if (
-                  !_document // doesn't work without a document (React native)
-                )
-                  return;
-
-                options = options || {};
-
-                var lastEventId = options.eventId || this.lastEventId();
-                if (!lastEventId) {
-                  throw new RavenConfigError('Missing eventId');
-                }
-
-                var dsn = options.dsn || this._dsn;
-                if (!dsn) {
-                  throw new RavenConfigError('Missing DSN');
-                }
-
-                var encode = encodeURIComponent;
-                var qs = '';
-                qs += '?eventId=' + encode(lastEventId);
-                qs += '&dsn=' + encode(dsn);
-
-                var user = options.user || this._globalContext.user;
-                if (user) {
-                  if (user.name) qs += '&name=' + encode(user.name);
-                  if (user.email) qs += '&email=' + encode(user.email);
+  isSetup: function() {
+    if (!this._hasJSON) return false; // needs JSON support
+    if (!this._globalServer) {
+      if (!this.ravenNotConfiguredError) {
+        this.ravenNotConfiguredError = true;
+        this._logDebug('error', 'Error: Raven has not been configured.');
+      }
+      return false;
+    }
+    return true;
+  },
+
+  afterLoad: function() {
+    // TODO: remove window dependence?
+
+    // Attempt to initialize Raven on load
+    var RavenConfig = _window.RavenConfig;
+    if (RavenConfig) {
+      this.config(RavenConfig.dsn, RavenConfig.config).install();
+    }
+  },
+
+  showReportDialog: function(options) {
+    if (
+      !_document // doesn't work without a document (React native)
+    )
+      return;
+
+    options = options || {};
+
+    var lastEventId = options.eventId || this.lastEventId();
+    if (!lastEventId) {
+      throw new RavenConfigError('Missing eventId');
+    }
+
+    var dsn = options.dsn || this._dsn;
+    if (!dsn) {
+      throw new RavenConfigError('Missing DSN');
+    }
+
+    var encode = encodeURIComponent;
+    var qs = '';
+    qs += '?eventId=' + encode(lastEventId);
+    qs += '&dsn=' + encode(dsn);
+
+    var user = options.user || this._globalContext.user;
+    if (user) {
+      if (user.name) qs += '&name=' + encode(user.name);
+      if (user.email) qs += '&email=' + encode(user.email);
+    }
+
+    var globalServer = this._getGlobalServer(this._parseDSN(dsn));
+
+    var script = _document.createElement('script');
+    script.async = true;
+    script.src = globalServer + '/api/embed/error-page/' + qs;
+    (_document.head || _document.body).appendChild(script);
+  },
+
+  /**** Private functions ****/
+  _ignoreNextOnError: function() {
+    var self = this;
+    this._ignoreOnError += 1;
+    setTimeout(function() {
+      // onerror should trigger before setTimeout
+      self._ignoreOnError -= 1;
+    });
+  },
+
+  _triggerEvent: function(eventType, options) {
+    // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it
+    var evt, key;
+
+    if (!this._hasDocument) return;
+
+    options = options || {};
+
+    eventType = 'raven' + eventType.substr(0, 1).toUpperCase() + eventType.substr(1);
+
+    if (_document.createEvent) {
+      evt = _document.createEvent('HTMLEvents');
+      evt.initEvent(eventType, true, true);
+    } else {
+      evt = _document.createEventObject();
+      evt.eventType = eventType;
+    }
+
+    for (key in options)
+      if (hasKey(options, key)) {
+        evt[key] = options[key];
+      }
+
+    if (_document.createEvent) {
+      // IE9 if standards
+      _document.dispatchEvent(evt);
+    } else {
+      // IE8 regardless of Quirks or Standards
+      // IE9 if quirks
+      try {
+        _document.fireEvent('on' + evt.eventType.toLowerCase(), evt);
+      } catch (e) {
+        // Do nothing
+      }
+    }
+  },
+
+  /**
+   * Wraps addEventListener to capture UI breadcrumbs
+   * @param evtName the event name (e.g. "click")
+   * @returns {Function}
+   * @private
+   */
+  _breadcrumbEventHandler: function(evtName) {
+    var self = this;
+    return function(evt) {
+      // reset keypress timeout; e.g. triggering a 'click' after
+      // a 'keypress' will reset the keypress debounce so that a new
+      // set of keypresses can be recorded
+      self._keypressTimeout = null;
+
+      // It's possible this handler might trigger multiple times for the same
+      // event (e.g. event propagation through node ancestors). Ignore if we've
+      // already captured the event.
+      if (self._lastCapturedEvent === evt) return;
+
+      self._lastCapturedEvent = evt;
+
+      // try/catch both:
+      // - accessing evt.target (see getsentry/raven-js#838, #768)
+      // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly
+      //   can throw an exception in some circumstances.
+      var target;
+      try {
+        target = htmlTreeAsString(evt.target);
+      } catch (e) {
+        target = '<unknown>';
+      }
+
+      self.captureBreadcrumb({
+        category: 'ui.' + evtName, // e.g. ui.click, ui.input
+        message: target
+      });
+    };
+  },
+
+  /**
+   * Wraps addEventListener to capture keypress UI events
+   * @returns {Function}
+   * @private
+   */
+  _keypressEventHandler: function() {
+    var self = this,
+      debounceDuration = 1000; // milliseconds
+
+    // TODO: if somehow user switches keypress target before
+    //       debounce timeout is triggered, we will only capture
+    //       a single breadcrumb from the FIRST target (acceptable?)
+    return function(evt) {
+      var target;
+      try {
+        target = evt.target;
+      } catch (e) {
+        // just accessing event properties can throw an exception in some rare circumstances
+        // see: https://github.com/getsentry/raven-js/issues/838
+        return;
+      }
+      var tagName = target && target.tagName;
+
+      // only consider keypress events on actual input elements
+      // this will disregard keypresses targeting body (e.g. tabbing
+      // through elements, hotkeys, etc)
+      if (
+        !tagName ||
+        (tagName !== 'INPUT' && tagName !== 'TEXTAREA' && !target.isContentEditable)
+      )
+        return;
+
+      // record first keypress in a series, but ignore subsequent
+      // keypresses until debounce clears
+      var timeout = self._keypressTimeout;
+      if (!timeout) {
+        self._breadcrumbEventHandler('input')(evt);
+      }
+      clearTimeout(timeout);
+      self._keypressTimeout = setTimeout(function() {
+        self._keypressTimeout = null;
+      }, debounceDuration);
+    };
+  },
+
+  /**
+   * Captures a breadcrumb of type "navigation", normalizing input URLs
+   * @param to the originating URL
+   * @param from the target URL
+   * @private
+   */
+  _captureUrlChange: function(from, to) {
+    var parsedLoc = parseUrl(this._location.href);
+    var parsedTo = parseUrl(to);
+    var parsedFrom = parseUrl(from);
+
+    // because onpopstate only tells you the "new" (to) value of location.href, and
+    // not the previous (from) value, we need to track the value of the current URL
+    // state ourselves
+    this._lastHref = to;
+
+    // Use only the path component of the URL if the URL matches the current
+    // document (almost all the time when using pushState)
+    if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host)
+      to = parsedTo.relative;
+    if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host)
+      from = parsedFrom.relative;
+
+    this.captureBreadcrumb({
+      category: 'navigation',
+      data: {
+        to: to,
+        from: from
+      }
+    });
+  },
+
+  _patchFunctionToString: function() {
+    var self = this;
+    self._originalFunctionToString = Function.prototype.toString;
+    // eslint-disable-next-line no-extend-native
+    Function.prototype.toString = function() {
+      if (typeof this === 'function' && this.__raven__) {
+        return self._originalFunctionToString.apply(this.__orig__, arguments);
+      }
+      return self._originalFunctionToString.apply(this, arguments);
+    };
+  },
+
+  _unpatchFunctionToString: function() {
+    if (this._originalFunctionToString) {
+      // eslint-disable-next-line no-extend-native
+      Function.prototype.toString = this._originalFunctionToString;
+    }
+  },
+
+  /**
+   * Wrap timer functions and event targets to catch errors and provide
+   * better metadata.
+   */
+  _instrumentTryCatch: function() {
+    var self = this;
+
+    var wrappedBuiltIns = self._wrappedBuiltIns;
+
+    function wrapTimeFn(orig) {
+      return function(fn, t) {
+        // preserve arity
+        // Make a copy of the arguments to prevent deoptimization
+        // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
+        var args = new Array(arguments.length);
+        for (var i = 0; i < args.length; ++i) {
+          args[i] = arguments[i];
+        }
+        var originalCallback = args[0];
+        if (isFunction(originalCallback)) {
+          args[0] = self.wrap(originalCallback);
+        }
+
+        // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
+        // also supports only two arguments and doesn't care what this is, so we
+        // can just call the original function directly.
+        if (orig.apply) {
+          return orig.apply(this, args);
+        } else {
+          return orig(args[0], args[1]);
+        }
+      };
+    }
+
+    var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
+
+    function wrapEventTarget(global) {
+      var proto = _window[global] && _window[global].prototype;
+      if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) {
+        fill(
+          proto,
+          'addEventListener',
+          function(orig) {
+            return function(evtName, fn, capture, secure) {
+              // preserve arity
+              try {
+                if (fn && fn.handleEvent) {
+                  fn.handleEvent = self.wrap(fn.handleEvent);
                 }
-
-                var globalServer = this._getGlobalServer(this._parseDSN(dsn));
-
-                var script = _document.createElement('script');
-                script.async = true;
-                script.src = globalServer + '/api/embed/error-page/' + qs;
-                (_document.head || _document.body).appendChild(script);
-              },
-
-              /**** Private functions ****/
-              _ignoreNextOnError: function() {
-                var self = this;
-                this._ignoreOnError += 1;
-                setTimeout(function() {
-                  // onerror should trigger before setTimeout
-                  self._ignoreOnError -= 1;
-                });
-              },
-
-              _triggerEvent: function(eventType, options) {
-                // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it
-                var evt, key;
-
-                if (!this._hasDocument) return;
-
-                options = options || {};
-
-                eventType =
-                  'raven' + eventType.substr(0, 1).toUpperCase() + eventType.substr(1);
-
-                if (_document.createEvent) {
-                  evt = _document.createEvent('HTMLEvents');
-                  evt.initEvent(eventType, true, true);
-                } else {
-                  evt = _document.createEventObject();
-                  evt.eventType = eventType;
-                }
-
-                for (key in options)
-                  if (hasKey(options, key)) {
-                    evt[key] = options[key];
-                  }
-
-                if (_document.createEvent) {
-                  // IE9 if standards
-                  _document.dispatchEvent(evt);
-                } else {
-                  // IE8 regardless of Quirks or Standards
-                  // IE9 if quirks
+              } catch (err) {
+                // can sometimes get 'Permission denied to access property "handle Event'
+              }
+
+              // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs`
+              // so that we don't have more than one wrapper function
+              var before, clickHandler, keypressHandler;
+
+              if (
+                autoBreadcrumbs &&
+                autoBreadcrumbs.dom &&
+                (global === 'EventTarget' || global === 'Node')
+              ) {
+                // NOTE: generating multiple handlers per addEventListener invocation, should
+                //       revisit and verify we can just use one (almost certainly)
+                clickHandler = self._breadcrumbEventHandler('click');
+                keypressHandler = self._keypressEventHandler();
+                before = function(evt) {
+                  // need to intercept every DOM event in `before` argument, in case that
+                  // same wrapped method is re-used for different events (e.g. mousemove THEN click)
+                  // see #724
+                  if (!evt) return;
+
+                  var eventType;
                   try {
-                    _document.fireEvent('on' + evt.eventType.toLowerCase(), evt);
-                  } catch (e) {
-                    // Do nothing
-                  }
-                }
-              },
-
-              /**
-     * Wraps addEventListener to capture UI breadcrumbs
-     * @param evtName the event name (e.g. "click")
-     * @returns {Function}
-     * @private
-     */
-              _breadcrumbEventHandler: function(evtName) {
-                var self = this;
-                return function(evt) {
-                  // reset keypress timeout; e.g. triggering a 'click' after
-                  // a 'keypress' will reset the keypress debounce so that a new
-                  // set of keypresses can be recorded
-                  self._keypressTimeout = null;
-
-                  // It's possible this handler might trigger multiple times for the same
-                  // event (e.g. event propagation through node ancestors). Ignore if we've
-                  // already captured the event.
-                  if (self._lastCapturedEvent === evt) return;
-
-                  self._lastCapturedEvent = evt;
-
-                  // try/catch both:
-                  // - accessing evt.target (see getsentry/raven-js#838, #768)
-                  // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly
-                  //   can throw an exception in some circumstances.
-                  var target;
-                  try {
-                    target = htmlTreeAsString(evt.target);
-                  } catch (e) {
-                    target = '<unknown>';
-                  }
-
-                  self.captureBreadcrumb({
-                    category: 'ui.' + evtName, // e.g. ui.click, ui.input
-                    message: target
-                  });
-                };
-              },
-
-              /**
-     * Wraps addEventListener to capture keypress UI events
-     * @returns {Function}
-     * @private
-     */
-              _keypressEventHandler: function() {
-                var self = this,
-                  debounceDuration = 1000; // milliseconds
-
-                // TODO: if somehow user switches keypress target before
-                //       debounce timeout is triggered, we will only capture
-                //       a single breadcrumb from the FIRST target (acceptable?)
-                return function(evt) {
-                  var target;
-                  try {
-                    target = evt.target;
+                    eventType = evt.type;
                   } catch (e) {
                     // just accessing event properties can throw an exception in some rare circumstances
                     // see: https://github.com/getsentry/raven-js/issues/838
                     return;
                   }
-                  var tagName = target && target.tagName;
-
-                  // only consider keypress events on actual input elements
-                  // this will disregard keypresses targeting body (e.g. tabbing
-                  // through elements, hotkeys, etc)
-                  if (
-                    !tagName ||
-                    (tagName !== 'INPUT' &&
-                      tagName !== 'TEXTAREA' &&
-                      !target.isContentEditable)
-                  )
-                    return;
-
-                  // record first keypress in a series, but ignore subsequent
-                  // keypresses until debounce clears
-                  var timeout = self._keypressTimeout;
-                  if (!timeout) {
-                    self._breadcrumbEventHandler('input')(evt);
-                  }
-                  clearTimeout(timeout);
-                  self._keypressTimeout = setTimeout(function() {
-                    self._keypressTimeout = null;
-                  }, debounceDuration);
+                  if (eventType === 'click') return clickHandler(evt);
+                  else if (eventType === 'keypress') return keypressHandler(evt);
                 };
-              },
-
-              /**
-     * Captures a breadcrumb of type "navigation", normalizing input URLs
-     * @param to the originating URL
-     * @param from the target URL
-     * @private
-     */
-              _captureUrlChange: function(from, to) {
-                var parsedLoc = parseUrl(this._location.href);
-                var parsedTo = parseUrl(to);
-                var parsedFrom = parseUrl(from);
-
-                // because onpopstate only tells you the "new" (to) value of location.href, and
-                // not the previous (from) value, we need to track the value of the current URL
-                // state ourselves
-                this._lastHref = to;
-
-                // Use only the path component of the URL if the URL matches the current
-                // document (almost all the time when using pushState)
-                if (
-                  parsedLoc.protocol === parsedTo.protocol &&
-                  parsedLoc.host === parsedTo.host
-                )
-                  to = parsedTo.relative;
-                if (
-                  parsedLoc.protocol === parsedFrom.protocol &&
-                  parsedLoc.host === parsedFrom.host
-                )
-                  from = parsedFrom.relative;
-
-                this.captureBreadcrumb({
-                  category: 'navigation',
-                  data: {
-                    to: to,
-                    from: from
-                  }
-                });
-              },
-
-              /**
-     * Wrap timer functions and event targets to catch errors and provide
-     * better metadata.
-     */
-              _instrumentTryCatch: function() {
-                var self = this;
-
-                var wrappedBuiltIns = self._wrappedBuiltIns;
-
-                function wrapTimeFn(orig) {
-                  return function(fn, t) {
-                    // preserve arity
-                    // Make a copy of the arguments to prevent deoptimization
-                    // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
-                    var args = new Array(arguments.length);
-                    for (var i = 0; i < args.length; ++i) {
-                      args[i] = arguments[i];
-                    }
-                    var originalCallback = args[0];
-                    if (isFunction(originalCallback)) {
-                      args[0] = self.wrap(originalCallback);
-                    }
-
-                    // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
-                    // also supports only two arguments and doesn't care what this is, so we
-                    // can just call the original function directly.
-                    if (orig.apply) {
-                      return orig.apply(this, args);
-                    } else {
-                      return orig(args[0], args[1]);
-                    }
-                  };
-                }
-
-                var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
-
-                function wrapEventTarget(global) {
-                  var proto = _window[global] && _window[global].prototype;
-                  if (
-                    proto &&
-                    proto.hasOwnProperty &&
-                    proto.hasOwnProperty('addEventListener')
-                  ) {
-                    fill(
-                      proto,
-                      'addEventListener',
-                      function(orig) {
-                        return function(evtName, fn, capture, secure) {
-                          // preserve arity
-                          try {
-                            if (fn && fn.handleEvent) {
-                              fn.handleEvent = self.wrap(fn.handleEvent);
-                            }
-                          } catch (err) {
-                            // can sometimes get 'Permission denied to access property "handle Event'
-                          }
-
-                          // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs`
-                          // so that we don't have more than one wrapper function
-                          var before, clickHandler, keypressHandler;
-
-                          if (
-                            autoBreadcrumbs &&
-                            autoBreadcrumbs.dom &&
-                            (global === 'EventTarget' || global === 'Node')
-                          ) {
-                            // NOTE: generating multiple handlers per addEventListener invocation, should
-                            //       revisit and verify we can just use one (almost certainly)
-                            clickHandler = self._breadcrumbEventHandler('click');
-                            keypressHandler = self._keypressEventHandler();
-                            before = function(evt) {
-                              // need to intercept every DOM event in `before` argument, in case that
-                              // same wrapped method is re-used for different events (e.g. mousemove THEN click)
-                              // see #724
-                              if (!evt) return;
-
-                              var eventType;
-                              try {
-                                eventType = evt.type;
-                              } catch (e) {
-                                // just accessing event properties can throw an exception in some rare circumstances
-                                // see: https://github.com/getsentry/raven-js/issues/838
-                                return;
-                              }
-                              if (eventType === 'click') return clickHandler(evt);
-                              else if (eventType === 'keypress')
-                                return keypressHandler(evt);
-                            };
-                          }
-                          return orig.call(
-                            this,
-                            evtName,
-                            self.wrap(fn, undefined, before),
-                            capture,
-                            secure
-                          );
-                        };
-                      },
-                      wrappedBuiltIns
-                    );
-                    fill(
-                      proto,
-                      'removeEventListener',
-                      function(orig) {
-                        return function(evt, fn, capture, secure) {
-                          try {
-                            fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);
-                          } catch (e) {
-                            // ignore, accessing __raven_wrapper__ will throw in some Selenium environments
-                          }
-                          return orig.call(this, evt, fn, capture, secure);
-                        };
-                      },
-                      wrappedBuiltIns
-                    );
-                  }
-                }
-
-                fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns);
-                fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns);
-                if (_window.requestAnimationFrame) {
-                  fill(
-                    _window,
-                    'requestAnimationFrame',
-                    function(orig) {
-                      return function(cb) {
-                        return orig(self.wrap(cb));
-                      };
-                    },
-                    wrappedBuiltIns
-                  );
-                }
-
-                // event targets borrowed from bugsnag-js:
-                // https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666
-                var eventTargets = [
-                  'EventTarget',
-                  'Window',
-                  'Node',
-                  'ApplicationCache',
-                  'AudioTrackList',
-                  'ChannelMergerNode',
-                  'CryptoOperation',
-                  'EventSource',
-                  'FileReader',
-                  'HTMLUnknownElement',
-                  'IDBDatabase',
-                  'IDBRequest',
-                  'IDBTransaction',
-                  'KeyOperation',
-                  'MediaController',
-                  'MessagePort',
-                  'ModalWindow',
-                  'Notification',
-                  'SVGElementInstance',
-                  'Screen',
-                  'TextTrack',
-                  'TextTrackCue',
-                  'TextTrackList',
-                  'WebSocket',
-                  'WebSocketWorker',
-                  'Worker',
-                  'XMLHttpRequest',
-                  'XMLHttpRequestEventTarget',
-                  'XMLHttpRequestUpload'
-                ];
-                for (var i = 0; i < eventTargets.length; i++) {
-                  wrapEventTarget(eventTargets[i]);
-                }
-              },
-
-              /**
-     * Instrument browser built-ins w/ breadcrumb capturing
-     *  - XMLHttpRequests
-     *  - DOM interactions (click/typing)
-     *  - window.location changes
-     *  - console
-     *
-     * Can be disabled or individually configured via the `autoBreadcrumbs` config option
-     */
-              _instrumentBreadcrumbs: function() {
-                var self = this;
-                var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
-
-                var wrappedBuiltIns = self._wrappedBuiltIns;
-
-                function wrapProp(prop, xhr) {
-                  if (prop in xhr && isFunction(xhr[prop])) {
-                    fill(xhr, prop, function(orig) {
-                      return self.wrap(orig);
-                    }); // intentionally don't track filled methods on XHR instances
-                  }
+              }
+              return orig.call(
+                this,
+                evtName,
+                self.wrap(fn, undefined, before),
+                capture,
+                secure
+              );
+            };
+          },
+          wrappedBuiltIns
+        );
+        fill(
+          proto,
+          'removeEventListener',
+          function(orig) {
+            return function(evt, fn, capture, secure) {
+              try {
+                fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);
+              } catch (e) {
+                // ignore, accessing __raven_wrapper__ will throw in some Selenium environments
+              }
+              return orig.call(this, evt, fn, capture, secure);
+            };
+          },
+          wrappedBuiltIns
+        );
+      }
+    }
+
+    fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns);
+    fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns);
+    if (_window.requestAnimationFrame) {
+      fill(
+        _window,
+        'requestAnimationFrame',
+        function(orig) {
+          return function(cb) {
+            return orig(self.wrap(cb));
+          };
+        },
+        wrappedBuiltIns
+      );
+    }
+
+    // event targets borrowed from bugsnag-js:
+    // https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666
+    var eventTargets = [
+      'EventTarget',
+      'Window',
+      'Node',
+      'ApplicationCache',
+      'AudioTrackList',
+      'ChannelMergerNode',
+      'CryptoOperation',
+      'EventSource',
+      'FileReader',
+      'HTMLUnknownElement',
+      'IDBDatabase',
+      'IDBRequest',
+      'IDBTransaction',
+      'KeyOperation',
+      'MediaController',
+      'MessagePort',
+      'ModalWindow',
+      'Notification',
+      'SVGElementInstance',
+      'Screen',
+      'TextTrack',
+      'TextTrackCue',
+      'TextTrackList',
+      'WebSocket',
+      'WebSocketWorker',
+      'Worker',
+      'XMLHttpRequest',
+      'XMLHttpRequestEventTarget',
+      'XMLHttpRequestUpload'
+    ];
+    for (var i = 0; i < eventTargets.length; i++) {
+      wrapEventTarget(eventTargets[i]);
+    }
+  },
+
+  /**
+   * Instrument browser built-ins w/ breadcrumb capturing
+   *  - XMLHttpRequests
+   *  - DOM interactions (click/typing)
+   *  - window.location changes
+   *  - console
+   *
+   * Can be disabled or individually configured via the `autoBreadcrumbs` config option
+   */
+  _instrumentBreadcrumbs: function() {
+    var self = this;
+    var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
+
+    var wrappedBuiltIns = self._wrappedBuiltIns;
+
+    function wrapProp(prop, xhr) {
+      if (prop in xhr && isFunction(xhr[prop])) {
+        fill(xhr, prop, function(orig) {
+          return self.wrap(orig);
+        }); // intentionally don't track filled methods on XHR instances
+      }
+    }
+
+    if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in _window) {
+      var xhrproto = _window.XMLHttpRequest && _window.XMLHttpRequest.prototype;
+      fill(
+        xhrproto,
+        'open',
+        function(origOpen) {
+          return function(method, url) {
+            // preserve arity
+
+            // if Sentry key appears in URL, don't capture
+            if (isString(url) && url.indexOf(self._globalKey) === -1) {
+              this.__raven_xhr = {
+                method: method,
+                url: url,
+                status_code: null
+              };
+            }
+
+            return origOpen.apply(this, arguments);
+          };
+        },
+        wrappedBuiltIns
+      );
+
+      fill(
+        xhrproto,
+        'send',
+        function(origSend) {
+          return function() {
+            // preserve arity
+            var xhr = this;
+
+            function onreadystatechangeHandler() {
+              if (xhr.__raven_xhr && xhr.readyState === 4) {
+                try {
+                  // touching statusCode in some platforms throws
+                  // an exception
+                  xhr.__raven_xhr.status_code = xhr.status;
+                } catch (e) {
+                  /* do nothing */
                 }
 
-                if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in _window) {
-                  var xhrproto = XMLHttpRequest.prototype;
-                  fill(
-                    xhrproto,
-                    'open',
-                    function(origOpen) {
-                      return function(method, url) {
-                        // preserve arity
-
-                        // if Sentry key appears in URL, don't capture
-                        if (isString(url) && url.indexOf(self._globalKey) === -1) {
-                          this.__raven_xhr = {
-                            method: method,
-                            url: url,
-                            status_code: null
-                          };
-                        }
-
-                        return origOpen.apply(this, arguments);
-                      };
-                    },
-                    wrappedBuiltIns
-                  );
-
-                  fill(
-                    xhrproto,
-                    'send',
-                    function(origSend) {
-                      return function(data) {
-                        // preserve arity
-                        var xhr = this;
-
-                        function onreadystatechangeHandler() {
-                          if (xhr.__raven_xhr && xhr.readyState === 4) {
-                            try {
-                              // touching statusCode in some platforms throws
-                              // an exception
-                              xhr.__raven_xhr.status_code = xhr.status;
-                            } catch (e) {
-                              /* do nothing */
-                            }
-
-                            self.captureBreadcrumb({
-                              type: 'http',
-                              category: 'xhr',
-                              data: xhr.__raven_xhr
-                            });
-                          }
-                        }
-
-                        var props = ['onload', 'onerror', 'onprogress'];
-                        for (var j = 0; j < props.length; j++) {
-                          wrapProp(props[j], xhr);
-                        }
-
-                        if (
-                          'onreadystatechange' in xhr &&
-                          isFunction(xhr.onreadystatechange)
-                        ) {
-                          fill(
-                            xhr,
-                            'onreadystatechange',
-                            function(orig) {
-                              return self.wrap(
-                                orig,
-                                undefined,
-                                onreadystatechangeHandler
-                              );
-                            } /* intentionally don't track this instrumentation */
-                          );
-                        } else {
-                          // if onreadystatechange wasn't actually set by the page on this xhr, we
-                          // are free to set our own and capture the breadcrumb
-                          xhr.onreadystatechange = onreadystatechangeHandler;
-                        }
-
-                        return origSend.apply(this, arguments);
-                      };
-                    },
-                    wrappedBuiltIns
-                  );
-                }
-
-                if (autoBreadcrumbs.xhr && 'fetch' in _window) {
-                  fill(
-                    _window,
-                    'fetch',
-                    function(origFetch) {
-                      return function(fn, t) {
-                        // preserve arity
-                        // Make a copy of the arguments to prevent deoptimization
-                        // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
-                        var args = new Array(arguments.length);
-                        for (var i = 0; i < args.length; ++i) {
-                          args[i] = arguments[i];
-                        }
-
-                        var fetchInput = args[0];
-                        var method = 'GET';
-                        var url;
-
-                        if (typeof fetchInput === 'string') {
-                          url = fetchInput;
-                        } else {
-                          url = fetchInput.url;
-                          if (fetchInput.method) {
-                            method = fetchInput.method;
-                          }
-                        }
-
-                        if (args[1] && args[1].method) {
-                          method = args[1].method;
-                        }
-
-                        var fetchData = {
-                          method: method,
-                          url: url,
-                          status_code: null
-                        };
-
-                        self.captureBreadcrumb({
-                          type: 'http',
-                          category: 'fetch',
-                          data: fetchData
-                        });
-
-                        return origFetch.apply(this, args).then(function(response) {
-                          fetchData.status_code = response.status;
-
-                          return response;
-                        });
-                      };
-                    },
-                    wrappedBuiltIns
-                  );
-                }
-
-                // Capture breadcrumbs from any click that is unhandled / bubbled up all the way
-                // to the document. Do this before we instrument addEventListener.
-                if (autoBreadcrumbs.dom && this._hasDocument) {
-                  if (_document.addEventListener) {
-                    _document.addEventListener(
-                      'click',
-                      self._breadcrumbEventHandler('click'),
-                      false
-                    );
-                    _document.addEventListener(
-                      'keypress',
-                      self._keypressEventHandler(),
-                      false
-                    );
-                  } else {
-                    // IE8 Compatibility
-                    _document.attachEvent(
-                      'onclick',
-                      self._breadcrumbEventHandler('click')
-                    );
-                    _document.attachEvent('onkeypress', self._keypressEventHandler());
-                  }
-                }
-
-                // record navigation (URL) changes
-                // NOTE: in Chrome App environment, touching history.pushState, *even inside
-                //       a try/catch block*, will cause Chrome to output an error to console.error
-                // borrowed from: https://github.com/angular/angular.js/pull/13945/files
-                var chrome = _window.chrome;
-                var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime;
-                var hasPushState =
-                  !isChromePackagedApp && _window.history && history.pushState;
-                if (autoBreadcrumbs.location && hasPushState) {
-                  // TODO: remove onpopstate handler on uninstall()
-                  var oldOnPopState = _window.onpopstate;
-                  _window.onpopstate = function() {
-                    var currentHref = self._location.href;
-                    self._captureUrlChange(self._lastHref, currentHref);
-
-                    if (oldOnPopState) {
-                      return oldOnPopState.apply(this, arguments);
-                    }
-                  };
-
-                  fill(
-                    history,
-                    'pushState',
-                    function(origPushState) {
-                      // note history.pushState.length is 0; intentionally not declaring
-                      // params to preserve 0 arity
-                      return function(/* state, title, url */) {
-                        var url = arguments.length > 2 ? arguments[2] : undefined;
-
-                        // url argument is optional
-                        if (url) {
-                          // coerce to string (this is what pushState does)
-                          self._captureUrlChange(self._lastHref, url + '');
-                        }
-
-                        return origPushState.apply(this, arguments);
-                      };
-                    },
-                    wrappedBuiltIns
-                  );
-                }
-
-                if (autoBreadcrumbs.console && 'console' in _window && console.log) {
-                  // console
-                  var consoleMethodCallback = function(msg, data) {
-                    self.captureBreadcrumb({
-                      message: msg,
-                      level: data.level,
-                      category: 'console'
-                    });
-                  };
-
-                  each(['debug', 'info', 'warn', 'error', 'log'], function(_, level) {
-                    wrapConsoleMethod(console, level, consoleMethodCallback);
-                  });
-                }
-              },
-
-              _restoreBuiltIns: function() {
-                // restore any wrapped builtins
-                var builtin;
-                while (this._wrappedBuiltIns.length) {
-                  builtin = this._wrappedBuiltIns.shift();
-
-                  var obj = builtin[0],
-                    name = builtin[1],
-                    orig = builtin[2];
-
-                  obj[name] = orig;
-                }
-              },
-
-              _drainPlugins: function() {
-                var self = this;
-
-                // FIX ME TODO
-                each(this._plugins, function(_, plugin) {
-                  var installer = plugin[0];
-                  var args = plugin[1];
-                  installer.apply(self, [self].concat(args));
-                });
-              },
-
-              _parseDSN: function(str) {
-                var m = dsnPattern.exec(str),
-                  dsn = {},
-                  i = 7;
-
-                try {
-                  while (i--) dsn[dsnKeys[i]] = m[i] || '';
-                } catch (e) {
-                  throw new RavenConfigError('Invalid DSN: ' + str);
-                }
-
-                if (dsn.pass && !this._globalOptions.allowSecretKey) {
-                  throw new RavenConfigError(
-                    'Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key'
-                  );
-                }
-
-                return dsn;
-              },
-
-              _getGlobalServer: function(uri) {
-                // assemble the endpoint from the uri pieces
-                var globalServer = '//' + uri.host + (uri.port ? ':' + uri.port : '');
-
-                if (uri.protocol) {
-                  globalServer = uri.protocol + ':' + globalServer;
-                }
-                return globalServer;
-              },
-
-              _handleOnErrorStackInfo: function() {
-                // if we are intentionally ignoring errors via onerror, bail out
-                if (!this._ignoreOnError) {
-                  this._handleStackInfo.apply(this, arguments);
-                }
-              },
-
-              _handleStackInfo: function(stackInfo, options) {
-                var frames = this._prepareFrames(stackInfo, options);
-
-                this._triggerEvent('handle', {
-                  stackInfo: stackInfo,
-                  options: options
+                self.captureBreadcrumb({
+                  type: 'http',
+                  category: 'xhr',
+                  data: xhr.__raven_xhr
                 });
-
-                this._processException(
-                  stackInfo.name,
-                  stackInfo.message,
-                  stackInfo.url,
-                  stackInfo.lineno,
-                  frames,
-                  options
-                );
-              },
-
-              _prepareFrames: function(stackInfo, options) {
-                var self = this;
-                var frames = [];
-                if (stackInfo.stack && stackInfo.stack.length) {
-                  each(stackInfo.stack, function(i, stack) {
-                    var frame = self._normalizeFrame(stack, stackInfo.url);
-                    if (frame) {
-                      frames.push(frame);
-                    }
-                  });
-
-                  // e.g. frames captured via captureMessage throw
-                  if (options && options.trimHeadFrames) {
-                    for (
-                      var j = 0;
-                      j < options.trimHeadFrames && j < frames.length;
-                      j++
-                    ) {
-                      frames[j].in_app = false;
-                    }
-                  }
-                }
-                frames = frames.slice(0, this._globalOptions.stackTraceLimit);
-                return frames;
-              },
-
-              _normalizeFrame: function(frame, stackInfoUrl) {
-                // normalize the frames data
-                var normalized = {
-                  filename: frame.url,
-                  lineno: frame.line,
-                  colno: frame.column,
-                  function: frame.func || '?'
-                };
-
-                // Case when we don't have any information about the error
-                // E.g. throwing a string or raw object, instead of an `Error` in Firefox
-                // Generating synthetic error doesn't add any value here
-                //
-                // We should probably somehow let a user know that they should fix their code
-                if (!frame.url) {
-                  normalized.filename = stackInfoUrl; // fallback to whole stacks url from onerror handler
-                }
-
-                normalized.in_app = !// determine if an exception came from outside of our app
-                // first we check the global includePaths list.
-                (
-                  (!!this._globalOptions.includePaths.test &&
-                    !this._globalOptions.includePaths.test(normalized.filename)) ||
-                  // Now we check for fun, if the function name is Raven or TraceKit
-                  /(Raven|TraceKit)\./.test(normalized['function']) ||
-                  // finally, we do a last ditch effort and check for raven.min.js
-                  /raven\.(min\.)?js$/.test(normalized.filename)
-                );
-
-                return normalized;
-              },
-
-              _processException: function(
-                type,
-                message,
-                fileurl,
-                lineno,
-                frames,
-                options
-              ) {
-                var testString = (type || '') + ': ' + (message || '');
-
-                if (
-                  !!this._globalOptions.ignoreErrors.test &&
-                  this._globalOptions.ignoreErrors.test(testString)
-                )
-                  return;
-
-                var stacktrace;
-
-                if (frames && frames.length) {
-                  fileurl = frames[0].filename || fileurl;
-                  // Sentry expects frames oldest to newest
-                  // and JS sends them as newest to oldest
-                  frames.reverse();
-                  stacktrace = {frames: frames};
-                } else if (fileurl) {
-                  stacktrace = {
-                    frames: [
-                      {
-                        filename: fileurl,
-                        lineno: lineno,
-                        in_app: true
-                      }
-                    ]
-                  };
-                }
-
-                if (
-                  !!this._globalOptions.ignoreUrls.test &&
-                  this._globalOptions.ignoreUrls.test(fileurl)
-                )
-                  return;
-                if (
-                  !!this._globalOptions.whitelistUrls.test &&
-                  !this._globalOptions.whitelistUrls.test(fileurl)
-                )
-                  return;
-
-                var data = objectMerge(
-                  {
-                    // sentry.interfaces.Exception
-                    exception: {
-                      values: [
-                        {
-                          type: type,
-                          value: message,
-                          stacktrace: stacktrace
-                        }
-                      ]
-                    },
-                    culprit: fileurl
-                  },
-                  options
-                );
-
-                // Fire away!
-                this._send(data);
-              },
-
-              _trimPacket: function(data) {
-                // For now, we only want to truncate the two different messages
-                // but this could/should be expanded to just trim everything
-                var max = this._globalOptions.maxMessageLength;
-                if (data.message) {
-                  data.message = truncate(data.message, max);
-                }
-                if (data.exception) {
-                  var exception = data.exception.values[0];
-                  exception.value = truncate(exception.value, max);
-                }
-
-                var request = data.request;
-                if (request) {
-                  if (request.url) {
-                    request.url = truncate(request.url, this._globalOptions.maxUrlLength);
-                  }
-                  if (request.Referer) {
-                    request.Referer = truncate(
-                      request.Referer,
-                      this._globalOptions.maxUrlLength
-                    );
-                  }
-                }
-
-                if (data.breadcrumbs && data.breadcrumbs.values)
-                  this._trimBreadcrumbs(data.breadcrumbs);
-
-                return data;
-              },
-
-              /**
-     * Truncate breadcrumb values (right now just URLs)
-     */
-              _trimBreadcrumbs: function(breadcrumbs) {
-                // known breadcrumb properties with urls
-                // TODO: also consider arbitrary prop values that start with (https?)?://
-                var urlProps = ['to', 'from', 'url'],
-                  urlProp,
-                  crumb,
-                  data;
-
-                for (var i = 0; i < breadcrumbs.values.length; ++i) {
-                  crumb = breadcrumbs.values[i];
-                  if (
-                    !crumb.hasOwnProperty('data') ||
-                    !isObject(crumb.data) ||
-                    objectFrozen(crumb.data)
-                  )
-                    continue;
-
-                  data = objectMerge({}, crumb.data);
-                  for (var j = 0; j < urlProps.length; ++j) {
-                    urlProp = urlProps[j];
-                    if (data.hasOwnProperty(urlProp) && data[urlProp]) {
-                      data[urlProp] = truncate(
-                        data[urlProp],
-                        this._globalOptions.maxUrlLength
-                      );
-                    }
-                  }
-                  breadcrumbs.values[i].data = data;
-                }
-              },
-
-              _getHttpData: function() {
-                if (!this._hasNavigator && !this._hasDocument) return;
-                var httpData = {};
-
-                if (this._hasNavigator && _navigator.userAgent) {
-                  httpData.headers = {
-                    'User-Agent': navigator.userAgent
-                  };
-                }
-
-                if (this._hasDocument) {
-                  if (_document.location && _document.location.href) {
-                    httpData.url = _document.location.href;
-                  }
-                  if (_document.referrer) {
-                    if (!httpData.headers) httpData.headers = {};
-                    httpData.headers.Referer = _document.referrer;
-                  }
-                }
-
-                return httpData;
-              },
-
-              _resetBackoff: function() {
-                this._backoffDuration = 0;
-                this._backoffStart = null;
-              },
-
-              _shouldBackoff: function() {
-                return (
-                  this._backoffDuration &&
-                  now() - this._backoffStart < this._backoffDuration
-                );
-              },
-
-              /**
-     * Returns true if the in-process data payload matches the signature
-     * of the previously-sent data
-     *
-     * NOTE: This has to be done at this level because TraceKit can generate
-     *       data from window.onerror WITHOUT an exception object (IE8, IE9,
-     *       other old browsers). This can take the form of an "exception"
-     *       data object with a single frame (derived from the onerror args).
-     */
-              _isRepeatData: function(current) {
-                var last = this._lastData;
-
-                if (
-                  !last ||
-                  current.message !== last.message || // defined for captureMessage
-                  current.culprit !== last.culprit // defined for captureException/onerror
-                )
-                  return false;
-
-                // Stacktrace interface (i.e. from captureMessage)
-                if (current.stacktrace || last.stacktrace) {
-                  return isSameStacktrace(current.stacktrace, last.stacktrace);
-                } else if (current.exception || last.exception) {
-                  // Exception interface (i.e. from captureException/onerror)
-                  return isSameException(current.exception, last.exception);
-                }
-
-                return true;
-              },
-
-              _setBackoffState: function(request) {
-                // If we are already in a backoff state, don't change anything
-                if (this._shouldBackoff()) {
-                  return;
-                }
-
-                var status = request.status;
-
-                // 400 - project_id doesn't exist or some other fatal
-                // 401 - invalid/revoked dsn
-                // 429 - too many requests
-                if (!(status === 400 || status === 401 || status === 429)) return;
-
-                var retry;
-                try {
-                  // If Retry-After is not in Access-Control-Expose-Headers, most
-                  // browsers will throw an exception trying to access it
-                  retry = request.getResponseHeader('Retry-After');
-                  retry = parseInt(retry, 10) * 1000; // Retry-After is returned in seconds
-                } catch (e) {
-                  /* eslint no-empty:0 */
-                }
-
-                this._backoffDuration = retry
-                  ? // If Sentry server returned a Retry-After value, use it
-                    retry
-                  : // Otherwise, double the last backoff duration (starts at 1 sec)
-                    this._backoffDuration * 2 || 1000;
-
-                this._backoffStart = now();
-              },
-
-              _send: function(data) {
-                var globalOptions = this._globalOptions;
-
-                var baseData = {
-                    project: this._globalProject,
-                    logger: globalOptions.logger,
-                    platform: 'javascript'
-                  },
-                  httpData = this._getHttpData();
-
-                if (httpData) {
-                  baseData.request = httpData;
-                }
-
-                // HACK: delete `trimHeadFrames` to prevent from appearing in outbound payload
-                if (data.trimHeadFrames) delete data.trimHeadFrames;
-
-                data = objectMerge(baseData, data);
-
-                // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge
-                data.tags = objectMerge(
-                  objectMerge({}, this._globalContext.tags),
-                  data.tags
-                );
-                data.extra = objectMerge(
-                  objectMerge({}, this._globalContext.extra),
-                  data.extra
-                );
-
-                // Send along our own collected metadata with extra
-                data.extra['session:duration'] = now() - this._startTime;
-
-                if (this._breadcrumbs && this._breadcrumbs.length > 0) {
-                  // intentionally make shallow copy so that additions
-                  // to breadcrumbs aren't accidentally sent in this request
-                  data.breadcrumbs = {
-                    values: [].slice.call(this._breadcrumbs, 0)
-                  };
-                }
-
-                // If there are no tags/extra, strip the key from the payload alltogther.
-                if (isEmptyObject(data.tags)) delete data.tags;
-
-                if (this._globalContext.user) {
-                  // sentry.interfaces.User
-                  data.user = this._globalContext.user;
-                }
-
-                // Include the environment if it's defined in globalOptions
-                if (globalOptions.environment)
-                  data.environment = globalOptions.environment;
-
-                // Include the release if it's defined in globalOptions
-                if (globalOptions.release) data.release = globalOptions.release;
-
-                // Include server_name if it's defined in globalOptions
-                if (globalOptions.serverName) data.server_name = globalOptions.serverName;
-
-                if (isFunction(globalOptions.dataCallback)) {
-                  data = globalOptions.dataCallback(data) || data;
-                }
-
-                // Why??????????
-                if (!data || isEmptyObject(data)) {
-                  return;
-                }
-
-                // Check if the request should be filtered or not
-                if (
-                  isFunction(globalOptions.shouldSendCallback) &&
-                  !globalOptions.shouldSendCallback(data)
-                ) {
-                  return;
-                }
-
-                // Backoff state: Sentry server previously responded w/ an error (e.g. 429 - too many requests),
-                // so drop requests until "cool-off" period has elapsed.
-                if (this._shouldBackoff()) {
-                  this._logDebug('warn', 'Raven dropped error due to backoff: ', data);
-                  return;
-                }
-
-                if (typeof globalOptions.sampleRate === 'number') {
-                  if (Math.random() < globalOptions.sampleRate) {
-                    this._sendProcessedPayload(data);
-                  }
-                } else {
-                  this._sendProcessedPayload(data);
-                }
-              },
-
-              _getUuid: function() {
-                return uuid4();
-              },
-
-              _sendProcessedPayload: function(data, callback) {
-                var self = this;
-                var globalOptions = this._globalOptions;
-
-                if (!this.isSetup()) return;
-
-                // Try and clean up the packet before sending by truncating long values
-                data = this._trimPacket(data);
-
-                // ideally duplicate error testing should occur *before* dataCallback/shouldSendCallback,
-                // but this would require copying an un-truncated copy of the data packet, which can be
-                // arbitrarily deep (extra_data) -- could be worthwhile? will revisit
-                if (!this._globalOptions.allowDuplicates && this._isRepeatData(data)) {
-                  this._logDebug('warn', 'Raven dropped repeat event: ', data);
-                  return;
-                }
-
-                // Send along an event_id if not explicitly passed.
-                // This event_id can be used to reference the error within Sentry itself.
-                // Set lastEventId after we know the error should actually be sent
-                this._lastEventId = data.event_id || (data.event_id = this._getUuid());
-
-                // Store outbound payload after trim
-                this._lastData = data;
-
-                this._logDebug('debug', 'Raven about to send:', data);
-
-                var auth = {
-                  sentry_version: '7',
-                  sentry_client: 'raven-js/' + this.VERSION,
-                  sentry_key: this._globalKey
-                };
-
-                if (this._globalSecret) {
-                  auth.sentry_secret = this._globalSecret;
-                }
-
-                var exception = data.exception && data.exception.values[0];
-                this.captureBreadcrumb({
-                  category: 'sentry',
-                  message: exception
-                    ? (exception.type ? exception.type + ': ' : '') + exception.value
-                    : data.message,
-                  event_id: data.event_id,
-                  level: data.level || 'error' // presume error unless specified
-                });
-
-                var url = this._globalEndpoint;
-                (globalOptions.transport || this._makeRequest).call(this, {
-                  url: url,
-                  auth: auth,
-                  data: data,
-                  options: globalOptions,
-                  onSuccess: function success() {
-                    self._resetBackoff();
-
-                    self._triggerEvent('success', {
-                      data: data,
-                      src: url
-                    });
-                    callback && callback();
-                  },
-                  onError: function failure(error) {
-                    self._logDebug('error', 'Raven transport failed to send: ', error);
-
-                    if (error.request) {
-                      self._setBackoffState(error.request);
-                    }
-
-                    self._triggerEvent('failure', {
-                      data: data,
-                      src: url
-                    });
-                    error =
-                      error ||
-                      new Error('Raven send failed (no additional details provided)');
-                    callback && callback(error);
-                  }
-                });
-              },
-
-              _makeRequest: function(opts) {
-                var request = _window.XMLHttpRequest && new _window.XMLHttpRequest();
-                if (!request) return;
-
-                // if browser doesn't support CORS (e.g. IE7), we are out of luck
-                var hasCORS =
-                  'withCredentials' in request || typeof XDomainRequest !== 'undefined';
-
-                if (!hasCORS) return;
-
-                var url = opts.url;
-
-                if ('withCredentials' in request) {
-                  request.onreadystatechange = function() {
-                    if (request.readyState !== 4) {
-                      return;
-                    } else if (request.status === 200) {
-                      opts.onSuccess && opts.onSuccess();
-                    } else if (opts.onError) {
-                      var err = new Error('Sentry error code: ' + request.status);
-                      err.request = request;
-                      opts.onError(err);
-                    }
-                  };
-                } else {
-                  request = new XDomainRequest();
-                  // xdomainrequest cannot go http -> https (or vice versa),
-                  // so always use protocol relative
-                  url = url.replace(/^https?:/, '');
-
-                  // onreadystatechange not supported by XDomainRequest
-                  if (opts.onSuccess) {
-                    request.onload = opts.onSuccess;
-                  }
-                  if (opts.onError) {
-                    request.onerror = function() {
-                      var err = new Error('Sentry error code: XDomainRequest');
-                      err.request = request;
-                      opts.onError(err);
-                    };
-                  }
-                }
-
-                // NOTE: auth is intentionally sent as part of query string (NOT as custom
-                //       HTTP header) so as to avoid preflight CORS requests
-                request.open('POST', url + '?' + urlencode(opts.auth));
-                request.send(stringify(opts.data));
-              },
-
-              _logDebug: function(level) {
-                if (this._originalConsoleMethods[level] && this.debug) {
-                  // In IE<10 console methods do not have their own 'apply' method
-                  Function.prototype.apply.call(
-                    this._originalConsoleMethods[level],
-                    this._originalConsole,
-                    [].slice.call(arguments, 1)
-                  );
-                }
-              },
-
-              _mergeContext: function(key, context) {
-                if (isUndefined(context)) {
-                  delete this._globalContext[key];
-                } else {
-                  this._globalContext[key] = objectMerge(
-                    this._globalContext[key] || {},
-                    context
-                  );
-                }
-              }
-            };
-
-            /*------------------------------------------------
- * utils
- *
- * conditionally exported for test via Raven.utils
- =================================================
- */
-            var objectPrototype = Object.prototype;
-
-            function isUndefined(what) {
-              return what === void 0;
-            }
-
-            function isFunction(what) {
-              return typeof what === 'function';
-            }
-
-            function isString(what) {
-              return objectPrototype.toString.call(what) === '[object String]';
-            }
-
-            function isEmptyObject(what) {
-              for (var _ in what) return false; // eslint-disable-line guard-for-in, no-unused-vars
-              return true;
-            }
-
-            function each(obj, callback) {
-              var i, j;
-
-              if (isUndefined(obj.length)) {
-                for (i in obj) {
-                  if (hasKey(obj, i)) {
-                    callback.call(null, i, obj[i]);
-                  }
-                }
-              } else {
-                j = obj.length;
-                if (j) {
-                  for (i = 0; i < j; i++) {
-                    callback.call(null, i, obj[i]);
-                  }
-                }
               }
             }
 
-            function objectMerge(obj1, obj2) {
-              if (!obj2) {
-                return obj1;
+            var props = ['onload', 'onerror', 'onprogress'];
+            for (var j = 0; j < props.length; j++) {
+              wrapProp(props[j], xhr);
+            }
+
+            if ('onreadystatechange' in xhr && isFunction(xhr.onreadystatechange)) {
+              fill(
+                xhr,
+                'onreadystatechange',
+                function(orig) {
+                  return self.wrap(orig, undefined, onreadystatechangeHandler);
+                } /* intentionally don't track this instrumentation */
+              );
+            } else {
+              // if onreadystatechange wasn't actually set by the page on this xhr, we
+              // are free to set our own and capture the breadcrumb
+              xhr.onreadystatechange = onreadystatechangeHandler;
+            }
+
+            return origSend.apply(this, arguments);
+          };
+        },
+        wrappedBuiltIns
+      );
+    }
+
+    if (autoBreadcrumbs.xhr && supportsFetch()) {
+      fill(
+        _window,
+        'fetch',
+        function(origFetch) {
+          return function() {
+            // preserve arity
+            // Make a copy of the arguments to prevent deoptimization
+            // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
+            var args = new Array(arguments.length);
+            for (var i = 0; i < args.length; ++i) {
+              args[i] = arguments[i];
+            }
+
+            var fetchInput = args[0];
+            var method = 'GET';
+            var url;
+
+            if (typeof fetchInput === 'string') {
+              url = fetchInput;
+            } else if ('Request' in _window && fetchInput instanceof _window.Request) {
+              url = fetchInput.url;
+              if (fetchInput.method) {
+                method = fetchInput.method;
               }
-              each(obj2, function(key, value) {
-                obj1[key] = value;
+            } else {
+              url = '' + fetchInput;
+            }
+
+            // if Sentry key appears in URL, don't capture, as it's our own request
+            if (url.indexOf(self._globalKey) !== -1) {
+              return origFetch.apply(this, args);
+            }
+
+            if (args[1] && args[1].method) {
+              method = args[1].method;
+            }
+
+            var fetchData = {
+              method: method,
+              url: url,
+              status_code: null
+            };
+
+            return origFetch.apply(this, args).then(function(response) {
+              fetchData.status_code = response.status;
+
+              self.captureBreadcrumb({
+                type: 'http',
+                category: 'fetch',
+                data: fetchData
               });
-              return obj1;
+
+              return response;
+            });
+          };
+        },
+        wrappedBuiltIns
+      );
+    }
+
+    // Capture breadcrumbs from any click that is unhandled / bubbled up all the way
+    // to the document. Do this before we instrument addEventListener.
+    if (autoBreadcrumbs.dom && this._hasDocument) {
+      if (_document.addEventListener) {
+        _document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
+        _document.addEventListener('keypress', self._keypressEventHandler(), false);
+      } else if(_document.attachEvent){
+        // IE8 Compatibility
+        _document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
+        _document.attachEvent('onkeypress', self._keypressEventHandler());
+      }
+    }
+
+    // record navigation (URL) changes
+    // NOTE: in Chrome App environment, touching history.pushState, *even inside
+    //       a try/catch block*, will cause Chrome to output an error to console.error
+    // borrowed from: https://github.com/angular/angular.js/pull/13945/files
+    var chrome = _window.chrome;
+    var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime;
+    var hasPushAndReplaceState =
+      !isChromePackagedApp &&
+      _window.history &&
+      history.pushState &&
+      history.replaceState;
+    if (autoBreadcrumbs.location && hasPushAndReplaceState) {
+      // TODO: remove onpopstate handler on uninstall()
+      var oldOnPopState = _window.onpopstate;
+      _window.onpopstate = function() {
+        var currentHref = self._location.href;
+        self._captureUrlChange(self._lastHref, currentHref);
+
+        if (oldOnPopState) {
+          return oldOnPopState.apply(this, arguments);
+        }
+      };
+
+      var historyReplacementFunction = function(origHistFunction) {
+        // note history.pushState.length is 0; intentionally not declaring
+        // params to preserve 0 arity
+        return function(/* state, title, url */) {
+          var url = arguments.length > 2 ? arguments[2] : undefined;
+
+          // url argument is optional
+          if (url) {
+            // coerce to string (this is what pushState does)
+            self._captureUrlChange(self._lastHref, url + '');
+          }
+
+          return origHistFunction.apply(this, arguments);
+        };
+      };
+
+      fill(history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
+      fill(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
+    }
+
+    if (autoBreadcrumbs.console && 'console' in _window && console.log) {
+      // console
+      var consoleMethodCallback = function(msg, data) {
+        self.captureBreadcrumb({
+          message: msg,
+          level: data.level,
+          category: 'console'
+        });
+      };
+
+      each(['debug', 'info', 'warn', 'error', 'log'], function(_, level) {
+        wrapConsoleMethod(console, level, consoleMethodCallback);
+      });
+    }
+  },
+
+  _restoreBuiltIns: function() {
+    // restore any wrapped builtins
+    var builtin;
+    while (this._wrappedBuiltIns.length) {
+      builtin = this._wrappedBuiltIns.shift();
+
+      var obj = builtin[0],
+        name = builtin[1],
+        orig = builtin[2];
+
+      obj[name] = orig;
+    }
+  },
+
+  _restoreConsole: function() {
+    // eslint-disable-next-line guard-for-in
+    for (var method in this._originalConsoleMethods) {
+      this._originalConsole[method] = this._originalConsoleMethods[method];
+    }
+  },
+
+  _drainPlugins: function() {
+    var self = this;
+
+    // FIX ME TODO
+    each(this._plugins, function(_, plugin) {
+      var installer = plugin[0];
+      var args = plugin[1];
+      installer.apply(self, [self].concat(args));
+    });
+  },
+
+  _parseDSN: function(str) {
+    var m = dsnPattern.exec(str),
+      dsn = {},
+      i = 7;
+
+    try {
+      while (i--) dsn[dsnKeys[i]] = m[i] || '';
+    } catch (e) {
+      throw new RavenConfigError('Invalid DSN: ' + str);
+    }
+
+    if (dsn.pass && !this._globalOptions.allowSecretKey) {
+      throw new RavenConfigError(
+        'Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key'
+      );
+    }
+
+    return dsn;
+  },
+
+  _getGlobalServer: function(uri) {
+    // assemble the endpoint from the uri pieces
+    var globalServer = '//' + uri.host + (uri.port ? ':' + uri.port : '');
+
+    if (uri.protocol) {
+      globalServer = uri.protocol + ':' + globalServer;
+    }
+    return globalServer;
+  },
+
+  _handleOnErrorStackInfo: function() {
+    // if we are intentionally ignoring errors via onerror, bail out
+    if (!this._ignoreOnError) {
+      this._handleStackInfo.apply(this, arguments);
+    }
+  },
+
+  _handleStackInfo: function(stackInfo, options) {
+    var frames = this._prepareFrames(stackInfo, options);
+
+    this._triggerEvent('handle', {
+      stackInfo: stackInfo,
+      options: options
+    });
+
+    this._processException(
+      stackInfo.name,
+      stackInfo.message,
+      stackInfo.url,
+      stackInfo.lineno,
+      frames,
+      options
+    );
+  },
+
+  _prepareFrames: function(stackInfo, options) {
+    var self = this;
+    var frames = [];
+    if (stackInfo.stack && stackInfo.stack.length) {
+      each(stackInfo.stack, function(i, stack) {
+        var frame = self._normalizeFrame(stack, stackInfo.url);
+        if (frame) {
+          frames.push(frame);
+        }
+      });
+
+      // e.g. frames captured via captureMessage throw
+      if (options && options.trimHeadFrames) {
+        for (var j = 0; j < options.trimHeadFrames && j < frames.length; j++) {
+          frames[j].in_app = false;
+        }
+      }
+    }
+    frames = frames.slice(0, this._globalOptions.stackTraceLimit);
+    return frames;
+  },
+
+  _normalizeFrame: function(frame, stackInfoUrl) {
+    // normalize the frames data
+    var normalized = {
+      filename: frame.url,
+      lineno: frame.line,
+      colno: frame.column,
+      function: frame.func || '?'
+    };
+
+    // Case when we don't have any information about the error
+    // E.g. throwing a string or raw object, instead of an `Error` in Firefox
+    // Generating synthetic error doesn't add any value here
+    //
+    // We should probably somehow let a user know that they should fix their code
+    if (!frame.url) {
+      normalized.filename = stackInfoUrl; // fallback to whole stacks url from onerror handler
+    }
+
+    normalized.in_app = !// determine if an exception came from outside of our app
+    // first we check the global includePaths list.
+    (
+      (!!this._globalOptions.includePaths.test &&
+        !this._globalOptions.includePaths.test(normalized.filename)) ||
+      // Now we check for fun, if the function name is Raven or TraceKit
+      /(Raven|TraceKit)\./.test(normalized['function']) ||
+      // finally, we do a last ditch effort and check for raven.min.js
+      /raven\.(min\.)?js$/.test(normalized.filename)
+    );
+
+    return normalized;
+  },
+
+  _processException: function(type, message, fileurl, lineno, frames, options) {
+    var prefixedMessage = (type ? type + ': ' : '') + (message || '');
+    if (
+      !!this._globalOptions.ignoreErrors.test &&
+      (this._globalOptions.ignoreErrors.test(message) ||
+        this._globalOptions.ignoreErrors.test(prefixedMessage))
+    ) {
+      return;
+    }
+
+    var stacktrace;
+
+    if (frames && frames.length) {
+      fileurl = frames[0].filename || fileurl;
+      // Sentry expects frames oldest to newest
+      // and JS sends them as newest to oldest
+      frames.reverse();
+      stacktrace = {frames: frames};
+    } else if (fileurl) {
+      stacktrace = {
+        frames: [
+          {
+            filename: fileurl,
+            lineno: lineno,
+            in_app: true
+          }
+        ]
+      };
+    }
+
+    if (
+      !!this._globalOptions.ignoreUrls.test &&
+      this._globalOptions.ignoreUrls.test(fileurl)
+    ) {
+      return;
+    }
+
+    if (
+      !!this._globalOptions.whitelistUrls.test &&
+      !this._globalOptions.whitelistUrls.test(fileurl)
+    ) {
+      return;
+    }
+
+    var data = objectMerge(
+      {
+        // sentry.interfaces.Exception
+        exception: {
+          values: [
+            {
+              type: type,
+              value: message,
+              stacktrace: stacktrace
             }
-
-            /**
+          ]
+        },
+        culprit: fileurl
+      },
+      options
+    );
+
+    // Fire away!
+    this._send(data);
+  },
+
+  _trimPacket: function(data) {
+    // For now, we only want to truncate the two different messages
+    // but this could/should be expanded to just trim everything
+    var max = this._globalOptions.maxMessageLength;
+    if (data.message) {
+      data.message = truncate(data.message, max);
+    }
+    if (data.exception) {
+      var exception = data.exception.values[0];
+      exception.value = truncate(exception.value, max);
+    }
+
+    var request = data.request;
+    if (request) {
+      if (request.url) {
+        request.url = truncate(request.url, this._globalOptions.maxUrlLength);
+      }
+      if (request.Referer) {
+        request.Referer = truncate(request.Referer, this._globalOptions.maxUrlLength);
+      }
+    }
+
+    if (data.breadcrumbs && data.breadcrumbs.values)
+      this._trimBreadcrumbs(data.breadcrumbs);
+
+    return data;
+  },
+
+  /**
+   * Truncate breadcrumb values (right now just URLs)
+   */
+  _trimBreadcrumbs: function(breadcrumbs) {
+    // known breadcrumb properties with urls
+    // TODO: also consider arbitrary prop values that start with (https?)?://
+    var urlProps = ['to', 'from', 'url'],
+      urlProp,
+      crumb,
+      data;
+
+    for (var i = 0; i < breadcrumbs.values.length; ++i) {
+      crumb = breadcrumbs.values[i];
+      if (
+        !crumb.hasOwnProperty('data') ||
+        !isObject(crumb.data) ||
+        objectFrozen(crumb.data)
+      )
+        continue;
+
+      data = objectMerge({}, crumb.data);
+      for (var j = 0; j < urlProps.length; ++j) {
+        urlProp = urlProps[j];
+        if (data.hasOwnProperty(urlProp) && data[urlProp]) {
+          data[urlProp] = truncate(data[urlProp], this._globalOptions.maxUrlLength);
+        }
+      }
+      breadcrumbs.values[i].data = data;
+    }
+  },
+
+  _getHttpData: function() {
+    if (!this._hasNavigator && !this._hasDocument) return;
+    var httpData = {};
+
+    if (this._hasNavigator && _navigator.userAgent) {
+      httpData.headers = {
+        'User-Agent': navigator.userAgent
+      };
+    }
+
+    // Check in `window` instead of `document`, as we may be in ServiceWorker environment
+    if (_window.location && _window.location.href) {
+      httpData.url = _window.location.href;
+    }
+
+    if (this._hasDocument && _document.referrer) {
+      if (!httpData.headers) httpData.headers = {};
+      httpData.headers.Referer = _document.referrer;
+    }
+
+    return httpData;
+  },
+
+  _resetBackoff: function() {
+    this._backoffDuration = 0;
+    this._backoffStart = null;
+  },
+
+  _shouldBackoff: function() {
+    return this._backoffDuration && now() - this._backoffStart < this._backoffDuration;
+  },
+
+  /**
+   * Returns true if the in-process data payload matches the signature
+   * of the previously-sent data
+   *
+   * NOTE: This has to be done at this level because TraceKit can generate
+   *       data from window.onerror WITHOUT an exception object (IE8, IE9,
+   *       other old browsers). This can take the form of an "exception"
+   *       data object with a single frame (derived from the onerror args).
+   */
+  _isRepeatData: function(current) {
+    var last = this._lastData;
+
+    if (
+      !last ||
+      current.message !== last.message || // defined for captureMessage
+      current.culprit !== last.culprit // defined for captureException/onerror
+    )
+      return false;
+
+    // Stacktrace interface (i.e. from captureMessage)
+    if (current.stacktrace || last.stacktrace) {
+      return isSameStacktrace(current.stacktrace, last.stacktrace);
+    } else if (current.exception || last.exception) {
+      // Exception interface (i.e. from captureException/onerror)
+      return isSameException(current.exception, last.exception);
+    }
+
+    return true;
+  },
+
+  _setBackoffState: function(request) {
+    // If we are already in a backoff state, don't change anything
+    if (this._shouldBackoff()) {
+      return;
+    }
+
+    var status = request.status;
+
+    // 400 - project_id doesn't exist or some other fatal
+    // 401 - invalid/revoked dsn
+    // 429 - too many requests
+    if (!(status === 400 || status === 401 || status === 429)) return;
+
+    var retry;
+    try {
+      // If Retry-After is not in Access-Control-Expose-Headers, most
+      // browsers will throw an exception trying to access it
+      if (supportsFetch()) {
+        retry = request.headers.get('Retry-After');
+      } else {
+        retry = request.getResponseHeader('Retry-After');
+      }
+
+      // Retry-After is returned in seconds
+      retry = parseInt(retry, 10) * 1000;
+    } catch (e) {
+      /* eslint no-empty:0 */
+    }
+
+    this._backoffDuration = retry
+      ? // If Sentry server returned a Retry-After value, use it
+        retry
+      : // Otherwise, double the last backoff duration (starts at 1 sec)
+        this._backoffDuration * 2 || 1000;
+
+    this._backoffStart = now();
+  },
+
+  _send: function(data) {
+    var globalOptions = this._globalOptions;
+
+    var baseData = {
+        project: this._globalProject,
+        logger: globalOptions.logger,
+        platform: 'javascript'
+      },
+      httpData = this._getHttpData();
+
+    if (httpData) {
+      baseData.request = httpData;
+    }
+
+    // HACK: delete `trimHeadFrames` to prevent from appearing in outbound payload
+    if (data.trimHeadFrames) delete data.trimHeadFrames;
+
+    data = objectMerge(baseData, data);
+
+    // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge
+    data.tags = objectMerge(objectMerge({}, this._globalContext.tags), data.tags);
+    data.extra = objectMerge(objectMerge({}, this._globalContext.extra), data.extra);
+
+    // Send along our own collected metadata with extra
+    data.extra['session:duration'] = now() - this._startTime;
+
+    if (this._breadcrumbs && this._breadcrumbs.length > 0) {
+      // intentionally make shallow copy so that additions
+      // to breadcrumbs aren't accidentally sent in this request
+      data.breadcrumbs = {
+        values: [].slice.call(this._breadcrumbs, 0)
+      };
+    }
+
+    if (this._globalContext.user) {
+      // sentry.interfaces.User
+      data.user = this._globalContext.user;
+    }
+
+    // Include the environment if it's defined in globalOptions
+    if (globalOptions.environment) data.environment = globalOptions.environment;
+
+    // Include the release if it's defined in globalOptions
+    if (globalOptions.release) data.release = globalOptions.release;
+
+    // Include server_name if it's defined in globalOptions
+    if (globalOptions.serverName) data.server_name = globalOptions.serverName;
+
+    data = this._sanitizeData(data);
+
+    // Cleanup empty properties before sending them to the server
+    Object.keys(data).forEach(function(key) {
+      if (data[key] == null || data[key] === '' || isEmptyObject(data[key])) {
+        delete data[key];
+      }
+    });
+
+    if (isFunction(globalOptions.dataCallback)) {
+      data = globalOptions.dataCallback(data) || data;
+    }
+
+    // Why??????????
+    if (!data || isEmptyObject(data)) {
+      return;
+    }
+
+    // Check if the request should be filtered or not
+    if (
+      isFunction(globalOptions.shouldSendCallback) &&
+      !globalOptions.shouldSendCallback(data)
+    ) {
+      return;
+    }
+
+    // Backoff state: Sentry server previously responded w/ an error (e.g. 429 - too many requests),
+    // so drop requests until "cool-off" period has elapsed.
+    if (this._shouldBackoff()) {
+      this._logDebug('warn', 'Raven dropped error due to backoff: ', data);
+      return;
+    }
+
+    if (typeof globalOptions.sampleRate === 'number') {
+      if (Math.random() < globalOptions.sampleRate) {
+        this._sendProcessedPayload(data);
+      }
+    } else {
+      this._sendProcessedPayload(data);
+    }
+  },
+
+  _sanitizeData: function(data) {
+    return sanitize(data, this._globalOptions.sanitizeKeys);
+  },
+
+  _getUuid: function() {
+    return uuid4();
+  },
+
+  _sendProcessedPayload: function(data, callback) {
+    var self = this;
+    var globalOptions = this._globalOptions;
+
+    if (!this.isSetup()) return;
+
+    // Try and clean up the packet before sending by truncating long values
+    data = this._trimPacket(data);
+
+    // ideally duplicate error testing should occur *before* dataCallback/shouldSendCallback,
+    // but this would require copying an un-truncated copy of the data packet, which can be
+    // arbitrarily deep (extra_data) -- could be worthwhile? will revisit
+    if (!this._globalOptions.allowDuplicates && this._isRepeatData(data)) {
+      this._logDebug('warn', 'Raven dropped repeat event: ', data);
+      return;
+    }
+
+    // Send along an event_id if not explicitly passed.
+    // This event_id can be used to reference the error within Sentry itself.
+    // Set lastEventId after we know the error should actually be sent
+    this._lastEventId = data.event_id || (data.event_id = this._getUuid());
+
+    // Store outbound payload after trim
+    this._lastData = data;
+
+    this._logDebug('debug', 'Raven about to send:', data);
+
+    var auth = {
+      sentry_version: '7',
+      sentry_client: 'raven-js/' + this.VERSION,
+      sentry_key: this._globalKey
+    };
+
+    if (this._globalSecret) {
+      auth.sentry_secret = this._globalSecret;
+    }
+
+    var exception = data.exception && data.exception.values[0];
+
+    // only capture 'sentry' breadcrumb is autoBreadcrumbs is truthy
+    if (
+      this._globalOptions.autoBreadcrumbs &&
+      this._globalOptions.autoBreadcrumbs.sentry
+    ) {
+      this.captureBreadcrumb({
+        category: 'sentry',
+        message: exception
+          ? (exception.type ? exception.type + ': ' : '') + exception.value
+          : data.message,
+        event_id: data.event_id,
+        level: data.level || 'error' // presume error unless specified
+      });
+    }
+
+    var url = this._globalEndpoint;
+    (globalOptions.transport || this._makeRequest).call(this, {
+      url: url,
+      auth: auth,
+      data: data,
+      options: globalOptions,
+      onSuccess: function success() {
+        self._resetBackoff();
+
+        self._triggerEvent('success', {
+          data: data,
+          src: url
+        });
+        callback && callback();
+      },
+      onError: function failure(error) {
+        self._logDebug('error', 'Raven transport failed to send: ', error);
+
+        if (error.request) {
+          self._setBackoffState(error.request);
+        }
+
+        self._triggerEvent('failure', {
+          data: data,
+          src: url
+        });
+        error = error || new Error('Raven send failed (no additional details provided)');
+        callback && callback(error);
+      }
+    });
+  },
+
+  _makeRequest: function(opts) {
+    // Auth is intentionally sent as part of query string (NOT as custom HTTP header) to avoid preflight CORS requests
+    var url = opts.url + '?' + urlencode(opts.auth);
+
+    var evaluatedHeaders = null;
+    var evaluatedFetchParameters = {};
+
+    if (opts.options.headers) {
+      evaluatedHeaders = this._evaluateHash(opts.options.headers);
+    }
+
+    if (opts.options.fetchParameters) {
+      evaluatedFetchParameters = this._evaluateHash(opts.options.fetchParameters);
+    }
+
+    if (supportsFetch()) {
+      evaluatedFetchParameters.body = stringify(opts.data);
+
+      var defaultFetchOptions = objectMerge({}, this._fetchDefaults);
+      var fetchOptions = objectMerge(defaultFetchOptions, evaluatedFetchParameters);
+
+      if (evaluatedHeaders) {
+        fetchOptions.headers = evaluatedHeaders;
+      }
+
+      return _window
+        .fetch(url, fetchOptions)
+        .then(function(response) {
+          if (response.ok) {
+            opts.onSuccess && opts.onSuccess();
+          } else {
+            var error = new Error('Sentry error code: ' + response.status);
+            // It's called request only to keep compatibility with XHR interface
+            // and not add more redundant checks in setBackoffState method
+            error.request = response;
+            opts.onError && opts.onError(error);
+          }
+        })
+        ['catch'](function() {
+          opts.onError &&
+            opts.onError(new Error('Sentry error code: network unavailable'));
+        });
+    }
+
+    var request = _window.XMLHttpRequest && new _window.XMLHttpRequest();
+    if (!request) return;
+
+    // if browser doesn't support CORS (e.g. IE7), we are out of luck
+    var hasCORS = 'withCredentials' in request || typeof XDomainRequest !== 'undefined';
+
+    if (!hasCORS) return;
+
+    if ('withCredentials' in request) {
+      request.onreadystatechange = function() {
+        if (request.readyState !== 4) {
+          return;
+        } else if (request.status === 200) {
+          opts.onSuccess && opts.onSuccess();
+        } else if (opts.onError) {
+          var err = new Error('Sentry error code: ' + request.status);
+          err.request = request;
+          opts.onError(err);
+        }
+      };
+    } else {
+      request = new XDomainRequest();
+      // xdomainrequest cannot go http -> https (or vice versa),
+      // so always use protocol relative
+      url = url.replace(/^https?:/, '');
+
+      // onreadystatechange not supported by XDomainRequest
+      if (opts.onSuccess) {
+        request.onload = opts.onSuccess;
+      }
+      if (opts.onError) {
+        request.onerror = function() {
+          var err = new Error('Sentry error code: XDomainRequest');
+          err.request = request;
+          opts.onError(err);
+        };
+      }
+    }
+
+    request.open('POST', url);
+
+    if (evaluatedHeaders) {
+      each(evaluatedHeaders, function(key, value) {
+        request.setRequestHeader(key, value);
+      });
+    }
+
+    request.send(stringify(opts.data));
+  },
+
+  _evaluateHash: function(hash) {
+    var evaluated = {};
+
+    for (var key in hash) {
+      if (hash.hasOwnProperty(key)) {
+        var value = hash[key];
+        evaluated[key] = typeof value === 'function' ? value() : value;
+      }
+    }
+
+    return evaluated;
+  },
+
+  _logDebug: function(level) {
+    if (this._originalConsoleMethods[level] && this.debug) {
+      // In IE<10 console methods do not have their own 'apply' method
+      Function.prototype.apply.call(
+        this._originalConsoleMethods[level],
+        this._originalConsole,
+        [].slice.call(arguments, 1)
+      );
+    }
+  },
+
+  _mergeContext: function(key, context) {
+    if (isUndefined(context)) {
+      delete this._globalContext[key];
+    } else {
+      this._globalContext[key] = objectMerge(this._globalContext[key] || {}, context);
+    }
+  }
+};
+
+// Deprecations
+Raven.prototype.setUser = Raven.prototype.setUserContext;
+Raven.prototype.setReleaseContext = Raven.prototype.setRelease;
+
+module.exports = Raven;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"1":1,"2":2,"5":5,"6":6,"7":7,"8":8}],4:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * Enforces a single instance of the Raven client, and the
+ * main entry point for Raven. If you are a consumer of the
+ * Raven library, you SHOULD load this file (vs raven.js).
+ **/
+
+var RavenConstructor = _dereq_(3);
+
+// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
+var _window =
+  typeof window !== 'undefined'
+    ? window
+    : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+var _Raven = _window.Raven;
+
+var Raven = new RavenConstructor();
+
+/*
+ * Allow multiple versions of Raven to be installed.
+ * Strip Raven from the global context and returns the instance.
+ *
+ * @return {Raven}
+ */
+Raven.noConflict = function() {
+  _window.Raven = _Raven;
+  return Raven;
+};
+
+Raven.afterLoad();
+
+module.exports = Raven;
+
+/**
+ * DISCLAIMER:
+ *
+ * Expose `Client` constructor for cases where user want to track multiple "sub-applications" in one larger app.
+ * It's not meant to be used by a wide audience, so pleaaase make sure that you know what you're doing before using it.
+ * Accidentally calling `install` multiple times, may result in an unexpected behavior that's very hard to debug.
+ *
+ * It's called `Client' to be in-line with Raven Node implementation.
+ *
+ * HOWTO:
+ *
+ * import Raven from 'raven-js';
+ *
+ * const someAppReporter = new Raven.Client();
+ * const someOtherAppReporter = new Raven.Client();
+ *
+ * someAppReporter.config('__DSN__', {
+ *   ...config goes here
+ * });
+ *
+ * someOtherAppReporter.config('__OTHER_DSN__', {
+ *   ...config goes here
+ * });
+ *
+ * someAppReporter.captureMessage(...);
+ * someAppReporter.captureException(...);
+ * someAppReporter.captureBreadcrumb(...);
+ *
+ * someOtherAppReporter.captureMessage(...);
+ * someOtherAppReporter.captureException(...);
+ * someOtherAppReporter.captureBreadcrumb(...);
+ *
+ * It should "just work".
+ */
+module.exports.Client = RavenConstructor;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"3":3}],5:[function(_dereq_,module,exports){
+(function (global){
+var stringify = _dereq_(7);
+
+var _window =
+  typeof window !== 'undefined'
+    ? window
+    : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+
+function isObject(what) {
+  return typeof what === 'object' && what !== null;
+}
+
+// Yanked from https://git.io/vS8DV re-used under CC0
+// with some tiny modifications
+function isError(value) {
+  switch ({}.toString.call(value)) {
+    case '[object Error]':
+      return true;
+    case '[object Exception]':
+      return true;
+    case '[object DOMException]':
+      return true;
+    default:
+      return value instanceof Error;
+  }
+}
+
+function isErrorEvent(value) {
+  return supportsErrorEvent() && {}.toString.call(value) === '[object ErrorEvent]';
+}
+
+function isUndefined(what) {
+  return what === void 0;
+}
+
+function isFunction(what) {
+  return typeof what === 'function';
+}
+
+function isPlainObject(what) {
+  return Object.prototype.toString.call(what) === '[object Object]';
+}
+
+function isString(what) {
+  return Object.prototype.toString.call(what) === '[object String]';
+}
+
+function isArray(what) {
+  return Object.prototype.toString.call(what) === '[object Array]';
+}
+
+function isEmptyObject(what) {
+  if (!isPlainObject(what)) return false;
+
+  for (var _ in what) {
+    if (what.hasOwnProperty(_)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+function supportsErrorEvent() {
+  try {
+    new ErrorEvent(''); // eslint-disable-line no-new
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+function supportsFetch() {
+  if (!('fetch' in _window)) return false;
+
+  try {
+    new Headers(); // eslint-disable-line no-new
+    new Request(''); // eslint-disable-line no-new
+    new Response(); // eslint-disable-line no-new
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
+// https://caniuse.com/#feat=referrer-policy
+// It doesn't. And it throw exception instead of ignoring this parameter...
+// REF: https://github.com/getsentry/raven-js/issues/1233
+function supportsReferrerPolicy() {
+  if (!supportsFetch()) return false;
+
+  try {
+    // eslint-disable-next-line no-new
+    new Request('pickleRick', {
+      referrerPolicy: 'origin'
+    });
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+function supportsPromiseRejectionEvent() {
+  return typeof PromiseRejectionEvent === 'function';
+}
+
+function wrappedCallback(callback) {
+  function dataCallback(data, original) {
+    var normalizedData = callback(data) || data;
+    if (original) {
+      return original(normalizedData) || normalizedData;
+    }
+    return normalizedData;
+  }
+
+  return dataCallback;
+}
+
+function each(obj, callback) {
+  var i, j;
+
+  if (isUndefined(obj.length)) {
+    for (i in obj) {
+      if (hasKey(obj, i)) {
+        callback.call(null, i, obj[i]);
+      }
+    }
+  } else {
+    j = obj.length;
+    if (j) {
+      for (i = 0; i < j; i++) {
+        callback.call(null, i, obj[i]);
+      }
+    }
+  }
+}
+
+function objectMerge(obj1, obj2) {
+  if (!obj2) {
+    return obj1;
+  }
+  each(obj2, function(key, value) {
+    obj1[key] = value;
+  });
+  return obj1;
+}
+
+/**
  * This function is only used for react-native.
  * react-native freezes object that have already been sent over the
  * js bridge. We need this function in order to check if the object is frozen.
  * So it's ok that objectFrozen returns false if Object.isFrozen is not
  * supported because it's not relevant for other "platforms". See related issue:
  * https://github.com/getsentry/react-native-sentry/issues/57
  */
-            function objectFrozen(obj) {
-              if (!Object.isFrozen) {
-                return false;
-              }
-              return Object.isFrozen(obj);
-            }
-
-            function truncate(str, max) {
-              return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
-            }
-
-            /**
+function objectFrozen(obj) {
+  if (!Object.isFrozen) {
+    return false;
+  }
+  return Object.isFrozen(obj);
+}
+
+function truncate(str, max) {
+  return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
+}
+
+/**
  * hasKey, a better form of hasOwnProperty
  * Example: hasKey(MainHostObject, property) === true/false
  *
  * @param {Object} host object to check property
  * @param {string} key to check
  */
-            function hasKey(object, key) {
-              return objectPrototype.hasOwnProperty.call(object, key);
-            }
-
-            function joinRegExp(patterns) {
-              // Combine an array of regular expressions and strings into one large regexp
-              // Be mad.
-              var sources = [],
-                i = 0,
-                len = patterns.length,
-                pattern;
-
-              for (; i < len; i++) {
-                pattern = patterns[i];
-                if (isString(pattern)) {
-                  // If it's a string, we need to escape it
-                  // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
-                  sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'));
-                } else if (pattern && pattern.source) {
-                  // If it's a regexp already, we want to extract the source
-                  sources.push(pattern.source);
-                }
-                // Intentionally skip other cases
-              }
-              return new RegExp(sources.join('|'), 'i');
-            }
-
-            function urlencode(o) {
-              var pairs = [];
-              each(o, function(key, value) {
-                pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
-              });
-              return pairs.join('&');
-            }
-
-            // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B
-            // intentionally using regex and not <a/> href parsing trick because React Native and other
-            // environments where DOM might not be available
-            function parseUrl(url) {
-              var match = url.match(
-                /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/
-              );
-              if (!match) return {};
-
-              // coerce to undefined values to empty string so we don't get 'undefined'
-              var query = match[6] || '';
-              var fragment = match[8] || '';
-              return {
-                protocol: match[2],
-                host: match[4],
-                path: match[5],
-                relative: match[5] + query + fragment // everything minus origin
-              };
-            }
-            function uuid4() {
-              var crypto = _window.crypto || _window.msCrypto;
-
-              if (!isUndefined(crypto) && crypto.getRandomValues) {
-                // Use window.crypto API if available
-                // eslint-disable-next-line no-undef
-                var arr = new Uint16Array(8);
-                crypto.getRandomValues(arr);
-
-                // set 4 in byte 7
-                arr[3] = (arr[3] & 0xfff) | 0x4000;
-                // set 2 most significant bits of byte 9 to '10'
-                arr[4] = (arr[4] & 0x3fff) | 0x8000;
-
-                var pad = function(num) {
-                  var v = num.toString(16);
-                  while (v.length < 4) {
-                    v = '0' + v;
-                  }
-                  return v;
-                };
-
-                return (
-                  pad(arr[0]) +
-                  pad(arr[1]) +
-                  pad(arr[2]) +
-                  pad(arr[3]) +
-                  pad(arr[4]) +
-                  pad(arr[5]) +
-                  pad(arr[6]) +
-                  pad(arr[7])
-                );
-              } else {
-                // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
-                return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
-                  var r = (Math.random() * 16) | 0,
-                    v = c === 'x' ? r : (r & 0x3) | 0x8;
-                  return v.toString(16);
-                });
-              }
-            }
-
-            /**
+function hasKey(object, key) {
+  return Object.prototype.hasOwnProperty.call(object, key);
+}
+
+function joinRegExp(patterns) {
+  // Combine an array of regular expressions and strings into one large regexp
+  // Be mad.
+  var sources = [],
+    i = 0,
+    len = patterns.length,
+    pattern;
+
+  for (; i < len; i++) {
+    pattern = patterns[i];
+    if (isString(pattern)) {
+      // If it's a string, we need to escape it
+      // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+      sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'));
+    } else if (pattern && pattern.source) {
+      // If it's a regexp already, we want to extract the source
+      sources.push(pattern.source);
+    }
+    // Intentionally skip other cases
+  }
+  return new RegExp(sources.join('|'), 'i');
+}
+
+function urlencode(o) {
+  var pairs = [];
+  each(o, function(key, value) {
+    pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
+  });
+  return pairs.join('&');
+}
+
+// borrowed from https://tools.ietf.org/html/rfc3986#appendix-B
+// intentionally using regex and not <a/> href parsing trick because React Native and other
+// environments where DOM might not be available
+function parseUrl(url) {
+  if (typeof url !== 'string') return {};
+  var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/);
+
+  // coerce to undefined values to empty string so we don't get 'undefined'
+  var query = match[6] || '';
+  var fragment = match[8] || '';
+  return {
+    protocol: match[2],
+    host: match[4],
+    path: match[5],
+    relative: match[5] + query + fragment // everything minus origin
+  };
+}
+function uuid4() {
+  var crypto = _window.crypto || _window.msCrypto;
+
+  if (!isUndefined(crypto) && crypto.getRandomValues) {
+    // Use window.crypto API if available
+    // eslint-disable-next-line no-undef
+    var arr = new Uint16Array(8);
+    crypto.getRandomValues(arr);
+
+    // set 4 in byte 7
+    arr[3] = (arr[3] & 0xfff) | 0x4000;
+    // set 2 most significant bits of byte 9 to '10'
+    arr[4] = (arr[4] & 0x3fff) | 0x8000;
+
+    var pad = function(num) {
+      var v = num.toString(16);
+      while (v.length < 4) {
+        v = '0' + v;
+      }
+      return v;
+    };
+
+    return (
+      pad(arr[0]) +
+      pad(arr[1]) +
+      pad(arr[2]) +
+      pad(arr[3]) +
+      pad(arr[4]) +
+      pad(arr[5]) +
+      pad(arr[6]) +
+      pad(arr[7])
+    );
+  } else {
+    // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
+    return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+      var r = (Math.random() * 16) | 0,
+        v = c === 'x' ? r : (r & 0x3) | 0x8;
+      return v.toString(16);
+    });
+  }
+}
+
+/**
  * Given a child DOM element, returns a query-selector statement describing that
  * and its ancestors
  * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
  * @param elem
  * @returns {string}
  */
-            function htmlTreeAsString(elem) {
-              /* eslint no-extra-parens:0*/
-              var MAX_TRAVERSE_HEIGHT = 5,
-                MAX_OUTPUT_LEN = 80,
-                out = [],
-                height = 0,
-                len = 0,
-                separator = ' > ',
-                sepLength = separator.length,
-                nextStr;
-
-              while (elem && height++ < MAX_TRAVERSE_HEIGHT) {
-                nextStr = htmlElementAsString(elem);
-                // bail out if
-                // - nextStr is the 'html' element
-                // - the length of the string that would be created exceeds MAX_OUTPUT_LEN
-                //   (ignore this limit if we are on the first iteration)
-                if (
-                  nextStr === 'html' ||
-                  (height > 1 &&
-                    len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)
-                ) {
-                  break;
-                }
-
-                out.push(nextStr);
-
-                len += nextStr.length;
-                elem = elem.parentNode;
-              }
-
-              return out.reverse().join(separator);
-            }
-
-            /**
+function htmlTreeAsString(elem) {
+  /* eslint no-extra-parens:0*/
+  var MAX_TRAVERSE_HEIGHT = 5,
+    MAX_OUTPUT_LEN = 80,
+    out = [],
+    height = 0,
+    len = 0,
+    separator = ' > ',
+    sepLength = separator.length,
+    nextStr;
+
+  while (elem && height++ < MAX_TRAVERSE_HEIGHT) {
+    nextStr = htmlElementAsString(elem);
+    // bail out if
+    // - nextStr is the 'html' element
+    // - the length of the string that would be created exceeds MAX_OUTPUT_LEN
+    //   (ignore this limit if we are on the first iteration)
+    if (
+      nextStr === 'html' ||
+      (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)
+    ) {
+      break;
+    }
+
+    out.push(nextStr);
+
+    len += nextStr.length;
+    elem = elem.parentNode;
+  }
+
+  return out.reverse().join(separator);
+}
+
+/**
  * Returns a simple, query-selector representation of a DOM element
  * e.g. [HTMLElement] => input#foo.btn[name=baz]
  * @param HTMLElement
  * @returns {string}
  */
-            function htmlElementAsString(elem) {
-              var out = [],
-                className,
-                classes,
-                key,
-                attr,
-                i;
-
-              if (!elem || !elem.tagName) {
-                return '';
-              }
-
-              out.push(elem.tagName.toLowerCase());
-              if (elem.id) {
-                out.push('#' + elem.id);
-              }
-
-              className = elem.className;
-              if (className && isString(className)) {
-                classes = className.split(/\s+/);
-                for (i = 0; i < classes.length; i++) {
-                  out.push('.' + classes[i]);
-                }
-              }
-              var attrWhitelist = ['type', 'name', 'title', 'alt'];
-              for (i = 0; i < attrWhitelist.length; i++) {
-                key = attrWhitelist[i];
-                attr = elem.getAttribute(key);
-                if (attr) {
-                  out.push('[' + key + '="' + attr + '"]');
-                }
-              }
-              return out.join('');
-            }
-
-            /**
+function htmlElementAsString(elem) {
+  var out = [],
+    className,
+    classes,
+    key,
+    attr,
+    i;
+
+  if (!elem || !elem.tagName) {
+    return '';
+  }
+
+  out.push(elem.tagName.toLowerCase());
+  if (elem.id) {
+    out.push('#' + elem.id);
+  }
+
+  className = elem.className;
+  if (className && isString(className)) {
+    classes = className.split(/\s+/);
+    for (i = 0; i < classes.length; i++) {
+      out.push('.' + classes[i]);
+    }
+  }
+  var attrWhitelist = ['type', 'name', 'title', 'alt'];
+  for (i = 0; i < attrWhitelist.length; i++) {
+    key = attrWhitelist[i];
+    attr = elem.getAttribute(key);
+    if (attr) {
+      out.push('[' + key + '="' + attr + '"]');
+    }
+  }
+  return out.join('');
+}
+
+/**
  * Returns true if either a OR b is truthy, but not both
  */
-            function isOnlyOneTruthy(a, b) {
-              return !!(!!a ^ !!b);
-            }
-
-            /**
+function isOnlyOneTruthy(a, b) {
+  return !!(!!a ^ !!b);
+}
+
+/**
+ * Returns true if both parameters are undefined
+ */
+function isBothUndefined(a, b) {
+  return isUndefined(a) && isUndefined(b);
+}
+
+/**
  * Returns true if the two input exception interfaces have the same content
  */
-            function isSameException(ex1, ex2) {
-              if (isOnlyOneTruthy(ex1, ex2)) return false;
-
-              ex1 = ex1.values[0];
-              ex2 = ex2.values[0];
-
-              if (ex1.type !== ex2.type || ex1.value !== ex2.value) return false;
-
-              return isSameStacktrace(ex1.stacktrace, ex2.stacktrace);
-            }
-
-            /**
+function isSameException(ex1, ex2) {
+  if (isOnlyOneTruthy(ex1, ex2)) return false;
+
+  ex1 = ex1.values[0];
+  ex2 = ex2.values[0];
+
+  if (ex1.type !== ex2.type || ex1.value !== ex2.value) return false;
+
+  // in case both stacktraces are undefined, we can't decide so default to false
+  if (isBothUndefined(ex1.stacktrace, ex2.stacktrace)) return false;
+
+  return isSameStacktrace(ex1.stacktrace, ex2.stacktrace);
+}
+
+/**
  * Returns true if the two input stack trace interfaces have the same content
  */
-            function isSameStacktrace(stack1, stack2) {
-              if (isOnlyOneTruthy(stack1, stack2)) return false;
-
-              var frames1 = stack1.frames;
-              var frames2 = stack2.frames;
-
-              // Exit early if frame count differs
-              if (frames1.length !== frames2.length) return false;
-
-              // Iterate through every frame; bail out if anything differs
-              var a, b;
-              for (var i = 0; i < frames1.length; i++) {
-                a = frames1[i];
-                b = frames2[i];
-                if (
-                  a.filename !== b.filename ||
-                  a.lineno !== b.lineno ||
-                  a.colno !== b.colno ||
-                  a['function'] !== b['function']
-                )
-                  return false;
-              }
-              return true;
-            }
-
-            /**
+function isSameStacktrace(stack1, stack2) {
+  if (isOnlyOneTruthy(stack1, stack2)) return false;
+
+  var frames1 = stack1.frames;
+  var frames2 = stack2.frames;
+
+  // Exit early if frame count differs
+  if (frames1.length !== frames2.length) return false;
+
+  // Iterate through every frame; bail out if anything differs
+  var a, b;
+  for (var i = 0; i < frames1.length; i++) {
+    a = frames1[i];
+    b = frames2[i];
+    if (
+      a.filename !== b.filename ||
+      a.lineno !== b.lineno ||
+      a.colno !== b.colno ||
+      a['function'] !== b['function']
+    )
+      return false;
+  }
+  return true;
+}
+
+/**
  * Polyfill a method
  * @param obj object e.g. `document`
  * @param name method name present on object e.g. `addEventListener`
  * @param replacement replacement function
  * @param track {optional} record instrumentation to an array
  */
-            function fill(obj, name, replacement, track) {
-              var orig = obj[name];
-              obj[name] = replacement(orig);
-              if (track) {
-                track.push([obj, name, orig]);
-              }
-            }
-
-            if (typeof __DEV__ !== 'undefined' && __DEV__) {
-              Raven.utils = {
-                isUndefined: isUndefined,
-                isFunction: isFunction,
-                isString: isString,
-                isObject: isObject,
-                isEmptyObject: isEmptyObject,
-                isError: isError,
-                each: each,
-                objectMerge: objectMerge,
-                truncate: truncate,
-                hasKey: hasKey,
-                joinRegExp: joinRegExp,
-                urlencode: urlencode,
-                uuid4: uuid4,
-                htmlTreeAsString: htmlTreeAsString,
-                htmlElementAsString: htmlElementAsString,
-                parseUrl: parseUrl,
-                fill: fill
-              };
-            }
-
-            // Deprecations
-            Raven.prototype.setUser = Raven.prototype.setUserContext;
-            Raven.prototype.setReleaseContext = Raven.prototype.setRelease;
-
-            module.exports = Raven;
-          }.call(
-            this,
-            typeof global !== 'undefined'
-              ? global
-              : typeof self !== 'undefined'
-                ? self
-                : typeof window !== 'undefined' ? window : {}
-          ));
-        },
-        {'1': 1, '2': 2, '5': 5, '6': 6, '7': 7}
-      ],
-      4: [
-        function(_dereq_, module, exports) {
-          (function(global) {
-            /**
- * Enforces a single instance of the Raven client, and the
- * main entry point for Raven. If you are a consumer of the
- * Raven library, you SHOULD load this file (vs raven.js).
- **/
-
-            var RavenConstructor = _dereq_(3);
-
-            // This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
-            var _window =
-              typeof window !== 'undefined'
-                ? window
-                : typeof global !== 'undefined'
-                  ? global
-                  : typeof self !== 'undefined' ? self : {};
-            var _Raven = _window.Raven;
-
-            var Raven = new RavenConstructor();
-
-            /*
- * Allow multiple versions of Raven to be installed.
- * Strip Raven from the global context and returns the instance.
- *
- * @return {Raven}
+function fill(obj, name, replacement, track) {
+  if (obj == null) return;
+  var orig = obj[name];
+  obj[name] = replacement(orig);
+  obj[name].__raven__ = true;
+  obj[name].__orig__ = orig;
+  if (track) {
+    track.push([obj, name, orig]);
+  }
+}
+
+/**
+ * Join values in array
+ * @param input array of values to be joined together
+ * @param delimiter string to be placed in-between values
+ * @returns {string}
  */
-            Raven.noConflict = function() {
-              _window.Raven = _Raven;
-              return Raven;
-            };
-
-            Raven.afterLoad();
-
-            module.exports = Raven;
-          }.call(
-            this,
-            typeof global !== 'undefined'
-              ? global
-              : typeof self !== 'undefined'
-                ? self
-                : typeof window !== 'undefined' ? window : {}
-          ));
-        },
-        {'3': 3}
-      ],
-      5: [
-        function(_dereq_, module, exports) {
-          function isObject(what) {
-            return typeof what === 'object' && what !== null;
-          }
-
-          // Yanked from https://git.io/vS8DV re-used under CC0
-          // with some tiny modifications
-          function isError(value) {
-            switch ({}.toString.call(value)) {
-              case '[object Error]':
-                return true;
-              case '[object Exception]':
-                return true;
-              case '[object DOMException]':
-                return true;
-              default:
-                return value instanceof Error;
-            }
-          }
-
-          function wrappedCallback(callback) {
-            function dataCallback(data, original) {
-              var normalizedData = callback(data) || data;
-              if (original) {
-                return original(normalizedData) || normalizedData;
-              }
-              return normalizedData;
-            }
-
-            return dataCallback;
-          }
-
-          module.exports = {
-            isObject: isObject,
-            isError: isError,
-            wrappedCallback: wrappedCallback
-          };
-        },
-        {}
-      ],
-      6: [
-        function(_dereq_, module, exports) {
-          (function(global) {
-            var utils = _dereq_(5);
-
-            /*
+function safeJoin(input, delimiter) {
+  if (!isArray(input)) return '';
+
+  var output = [];
+
+  for (var i = 0; i < input.length; i++) {
+    try {
+      output.push(String(input[i]));
+    } catch (e) {
+      output.push('[value cannot be serialized]');
+    }
+  }
+
+  return output.join(delimiter);
+}
+
+// Default Node.js REPL depth
+var MAX_SERIALIZE_EXCEPTION_DEPTH = 3;
+// 50kB, as 100kB is max payload size, so half sounds reasonable
+var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024;
+var MAX_SERIALIZE_KEYS_LENGTH = 40;
+
+function utf8Length(value) {
+  return ~-encodeURI(value).split(/%..|./).length;
+}
+
+function jsonSize(value) {
+  return utf8Length(JSON.stringify(value));
+}
+
+function serializeValue(value) {
+  var maxLength = 40;
+
+  if (typeof value === 'string') {
+    return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
+  } else if (
+    typeof value === 'number' ||
+    typeof value === 'boolean' ||
+    typeof value === 'undefined'
+  ) {
+    return value;
+  }
+
+  var type = Object.prototype.toString.call(value);
+
+  // Node.js REPL notation
+  if (type === '[object Object]') return '[Object]';
+  if (type === '[object Array]') return '[Array]';
+  if (type === '[object Function]')
+    return value.name ? '[Function: ' + value.name + ']' : '[Function]';
+
+  return value;
+}
+
+function serializeObject(value, depth) {
+  if (depth === 0) return serializeValue(value);
+
+  if (isPlainObject(value)) {
+    return Object.keys(value).reduce(function(acc, key) {
+      acc[key] = serializeObject(value[key], depth - 1);
+      return acc;
+    }, {});
+  } else if (Array.isArray(value)) {
+    return value.map(function(val) {
+      return serializeObject(val, depth - 1);
+    });
+  }
+
+  return serializeValue(value);
+}
+
+function serializeException(ex, depth, maxSize) {
+  if (!isPlainObject(ex)) return ex;
+
+  depth = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_DEPTH : depth;
+  maxSize = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_SIZE : maxSize;
+
+  var serialized = serializeObject(ex, depth);
+
+  if (jsonSize(stringify(serialized)) > maxSize) {
+    return serializeException(ex, depth - 1);
+  }
+
+  return serialized;
+}
+
+function serializeKeysForMessage(keys, maxLength) {
+  if (typeof keys === 'number' || typeof keys === 'string') return keys.toString();
+  if (!Array.isArray(keys)) return '';
+
+  keys = keys.filter(function(key) {
+    return typeof key === 'string';
+  });
+  if (keys.length === 0) return '[object has no keys]';
+
+  maxLength = typeof maxLength !== 'number' ? MAX_SERIALIZE_KEYS_LENGTH : maxLength;
+  if (keys[0].length >= maxLength) return keys[0];
+
+  for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) {
+    var serialized = keys.slice(0, usedKeys).join(', ');
+    if (serialized.length > maxLength) continue;
+    if (usedKeys === keys.length) return serialized;
+    return serialized + '\u2026';
+  }
+
+  return '';
+}
+
+function sanitize(input, sanitizeKeys) {
+  if (!isArray(sanitizeKeys) || (isArray(sanitizeKeys) && sanitizeKeys.length === 0))
+    return input;
+
+  var sanitizeRegExp = joinRegExp(sanitizeKeys);
+  var sanitizeMask = '********';
+  var safeInput;
+
+  try {
+    safeInput = JSON.parse(stringify(input));
+  } catch (o_O) {
+    return input;
+  }
+
+  function sanitizeWorker(workerInput) {
+    if (isArray(workerInput)) {
+      return workerInput.map(function(val) {
+        return sanitizeWorker(val);
+      });
+    }
+
+    if (isPlainObject(workerInput)) {
+      return Object.keys(workerInput).reduce(function(acc, k) {
+        if (sanitizeRegExp.test(k)) {
+          acc[k] = sanitizeMask;
+        } else {
+          acc[k] = sanitizeWorker(workerInput[k]);
+        }
+        return acc;
+      }, {});
+    }
+
+    return workerInput;
+  }
+
+  return sanitizeWorker(safeInput);
+}
+
+module.exports = {
+  isObject: isObject,
+  isError: isError,
+  isErrorEvent: isErrorEvent,
+  isUndefined: isUndefined,
+  isFunction: isFunction,
+  isPlainObject: isPlainObject,
+  isString: isString,
+  isArray: isArray,
+  isEmptyObject: isEmptyObject,
+  supportsErrorEvent: supportsErrorEvent,
+  supportsFetch: supportsFetch,
+  supportsReferrerPolicy: supportsReferrerPolicy,
+  supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
+  wrappedCallback: wrappedCallback,
+  each: each,
+  objectMerge: objectMerge,
+  truncate: truncate,
+  objectFrozen: objectFrozen,
+  hasKey: hasKey,
+  joinRegExp: joinRegExp,
+  urlencode: urlencode,
+  uuid4: uuid4,
+  htmlTreeAsString: htmlTreeAsString,
+  htmlElementAsString: htmlElementAsString,
+  isSameException: isSameException,
+  isSameStacktrace: isSameStacktrace,
+  parseUrl: parseUrl,
+  fill: fill,
+  safeJoin: safeJoin,
+  serializeException: serializeException,
+  serializeKeysForMessage: serializeKeysForMessage,
+  sanitize: sanitize
+};
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"7":7}],6:[function(_dereq_,module,exports){
+(function (global){
+var utils = _dereq_(5);
+
+/*
  TraceKit - Cross brower stack traces
 
  This was originally forked from github.com/occ/TraceKit, but has since been
  largely re-written and is now maintained as part of raven-js.  Tests for
  this are in test/vendor.
 
  MIT license
 */
 
-            var TraceKit = {
-              collectWindowErrors: true,
-              debug: false
-            };
-
-            // This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
-            var _window =
-              typeof window !== 'undefined'
-                ? window
-                : typeof global !== 'undefined'
-                  ? global
-                  : typeof self !== 'undefined' ? self : {};
-
-            // global reference to slice
-            var _slice = [].slice;
-            var UNKNOWN_FUNCTION = '?';
-
-            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types
-            var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/;
-
-            function getLocationHref() {
-              if (typeof document === 'undefined' || document.location == null) return '';
-
-              return document.location.href;
-            }
-
-            /**
+var TraceKit = {
+  collectWindowErrors: true,
+  debug: false
+};
+
+// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
+var _window =
+  typeof window !== 'undefined'
+    ? window
+    : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+
+// global reference to slice
+var _slice = [].slice;
+var UNKNOWN_FUNCTION = '?';
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types
+var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/;
+
+function getLocationHref() {
+  if (typeof document === 'undefined' || document.location == null) return '';
+
+  return document.location.href;
+}
+
+/**
  * TraceKit.report: cross-browser processing of unhandled exceptions
  *
  * Syntax:
  *   TraceKit.report.subscribe(function(stackInfo) { ... })
  *   TraceKit.report.unsubscribe(function(stackInfo) { ... })
  *   TraceKit.report(exception)
  *   try { ...code... } catch(ex) { TraceKit.report(ex); }
  *
@@ -2610,217 +2965,218 @@
  * subscribed handlers. Please note that TraceKit.report will rethrow the
  * exception. This is REQUIRED in order to get a useful stack trace in IE.
  * If the exception does not reach the top of the browser, you will only
  * get a stack trace from the point where TraceKit.report was called.
  *
  * Handlers receive a stackInfo object as described in the
  * TraceKit.computeStackTrace docs.
  */
-            TraceKit.report = (function reportModuleWrapper() {
-              var handlers = [],
-                lastArgs = null,
-                lastException = null,
-                lastExceptionStack = null;
-
-              /**
-     * Add a crash handler.
-     * @param {Function} handler
-     */
-              function subscribe(handler) {
-                installGlobalHandler();
-                handlers.push(handler);
-              }
-
-              /**
-     * Remove a crash handler.
-     * @param {Function} handler
-     */
-              function unsubscribe(handler) {
-                for (var i = handlers.length - 1; i >= 0; --i) {
-                  if (handlers[i] === handler) {
-                    handlers.splice(i, 1);
-                  }
-                }
-              }
-
-              /**
-     * Remove all crash handlers.
-     */
-              function unsubscribeAll() {
-                uninstallGlobalHandler();
-                handlers = [];
-              }
-
-              /**
-     * Dispatch stack information to all handlers.
-     * @param {Object.<string, *>} stack
-     */
-              function notifyHandlers(stack, isWindowError) {
-                var exception = null;
-                if (isWindowError && !TraceKit.collectWindowErrors) {
-                  return;
-                }
-                for (var i in handlers) {
-                  if (handlers.hasOwnProperty(i)) {
-                    try {
-                      handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2)));
-                    } catch (inner) {
-                      exception = inner;
-                    }
-                  }
-                }
-
-                if (exception) {
-                  throw exception;
-                }
-              }
-
-              var _oldOnerrorHandler, _onErrorHandlerInstalled;
-
-              /**
-     * Ensures all global unhandled exceptions are recorded.
-     * Supported by Gecko and IE.
-     * @param {string} message Error message.
-     * @param {string} url URL of script that generated the exception.
-     * @param {(number|string)} lineNo The line number at which the error
-     * occurred.
-     * @param {?(number|string)} colNo The column number at which the error
-     * occurred.
-     * @param {?Error} ex The actual Error object.
-     */
-              function traceKitWindowOnError(message, url, lineNo, colNo, ex) {
-                var stack = null;
-
-                if (lastExceptionStack) {
-                  TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(
-                    lastExceptionStack,
-                    url,
-                    lineNo,
-                    message
-                  );
-                  processLastException();
-                } else if (ex && utils.isError(ex)) {
-                  // non-string `ex` arg; attempt to extract stack trace
-
-                  // New chrome and blink send along a real error object
-                  // Let's just report that like a normal error.
-                  // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
-                  stack = TraceKit.computeStackTrace(ex);
-                  notifyHandlers(stack, true);
-                } else {
-                  var location = {
-                    url: url,
-                    line: lineNo,
-                    column: colNo
-                  };
-
-                  var name = undefined;
-                  var msg = message; // must be new var or will modify original `arguments`
-                  var groups;
-                  if ({}.toString.call(message) === '[object String]') {
-                    var groups = message.match(ERROR_TYPES_RE);
-                    if (groups) {
-                      name = groups[1];
-                      msg = groups[2];
-                    }
-                  }
-
-                  location.func = UNKNOWN_FUNCTION;
-
-                  stack = {
-                    name: name,
-                    message: msg,
-                    url: getLocationHref(),
-                    stack: [location]
-                  };
-                  notifyHandlers(stack, true);
-                }
-
-                if (_oldOnerrorHandler) {
-                  return _oldOnerrorHandler.apply(this, arguments);
-                }
-
-                return false;
-              }
-
-              function installGlobalHandler() {
-                if (_onErrorHandlerInstalled) {
-                  return;
-                }
-                _oldOnerrorHandler = _window.onerror;
-                _window.onerror = traceKitWindowOnError;
-                _onErrorHandlerInstalled = true;
-              }
-
-              function uninstallGlobalHandler() {
-                if (!_onErrorHandlerInstalled) {
-                  return;
-                }
-                _window.onerror = _oldOnerrorHandler;
-                _onErrorHandlerInstalled = false;
-                _oldOnerrorHandler = undefined;
-              }
-
-              function processLastException() {
-                var _lastExceptionStack = lastExceptionStack,
-                  _lastArgs = lastArgs;
-                lastArgs = null;
-                lastExceptionStack = null;
-                lastException = null;
-                notifyHandlers.apply(
-                  null,
-                  [_lastExceptionStack, false].concat(_lastArgs)
-                );
-              }
-
-              /**
-     * Reports an unhandled Error to TraceKit.
-     * @param {Error} ex
-     * @param {?boolean} rethrow If false, do not re-throw the exception.
-     * Only used for window.onerror to not cause an infinite loop of
-     * rethrowing.
-     */
-              function report(ex, rethrow) {
-                var args = _slice.call(arguments, 1);
-                if (lastExceptionStack) {
-                  if (lastException === ex) {
-                    return; // already caught by an inner catch block, ignore
-                  } else {
-                    processLastException();
-                  }
-                }
-
-                var stack = TraceKit.computeStackTrace(ex);
-                lastExceptionStack = stack;
-                lastException = ex;
-                lastArgs = args;
-
-                // If the stack trace is incomplete, wait for 2 seconds for
-                // slow slow IE to see if onerror occurs or not before reporting
-                // this exception; otherwise, we will end up with an incomplete
-                // stack trace
-                setTimeout(function() {
-                  if (lastException === ex) {
-                    processLastException();
-                  }
-                }, stack.incomplete ? 2000 : 0);
-
-                if (rethrow !== false) {
-                  throw ex; // re-throw to propagate to the top level (and cause window.onerror)
-                }
-              }
-
-              report.subscribe = subscribe;
-              report.unsubscribe = unsubscribe;
-              report.uninstall = unsubscribeAll;
-              return report;
-            })();
-
-            /**
+TraceKit.report = (function reportModuleWrapper() {
+  var handlers = [],
+    lastArgs = null,
+    lastException = null,
+    lastExceptionStack = null;
+
+  /**
+   * Add a crash handler.
+   * @param {Function} handler
+   */
+  function subscribe(handler) {
+    installGlobalHandler();
+    handlers.push(handler);
+  }
+
+  /**
+   * Remove a crash handler.
+   * @param {Function} handler
+   */
+  function unsubscribe(handler) {
+    for (var i = handlers.length - 1; i >= 0; --i) {
+      if (handlers[i] === handler) {
+        handlers.splice(i, 1);
+      }
+    }
+  }
+
+  /**
+   * Remove all crash handlers.
+   */
+  function unsubscribeAll() {
+    uninstallGlobalHandler();
+    handlers = [];
+  }
+
+  /**
+   * Dispatch stack information to all handlers.
+   * @param {Object.<string, *>} stack
+   */
+  function notifyHandlers(stack, isWindowError) {
+    var exception = null;
+    if (isWindowError && !TraceKit.collectWindowErrors) {
+      return;
+    }
+    for (var i in handlers) {
+      if (handlers.hasOwnProperty(i)) {
+        try {
+          handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2)));
+        } catch (inner) {
+          exception = inner;
+        }
+      }
+    }
+
+    if (exception) {
+      throw exception;
+    }
+  }
+
+  var _oldOnerrorHandler, _onErrorHandlerInstalled;
+
+  /**
+   * Ensures all global unhandled exceptions are recorded.
+   * Supported by Gecko and IE.
+   * @param {string} msg Error message.
+   * @param {string} url URL of script that generated the exception.
+   * @param {(number|string)} lineNo The line number at which the error
+   * occurred.
+   * @param {?(number|string)} colNo The column number at which the error
+   * occurred.
+   * @param {?Error} ex The actual Error object.
+   */
+  function traceKitWindowOnError(msg, url, lineNo, colNo, ex) {
+    var stack = null;
+    // If 'ex' is ErrorEvent, get real Error from inside
+    var exception = utils.isErrorEvent(ex) ? ex.error : ex;
+    // If 'msg' is ErrorEvent, get real message from inside
+    var message = utils.isErrorEvent(msg) ? msg.message : msg;
+
+    if (lastExceptionStack) {
+      TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(
+        lastExceptionStack,
+        url,
+        lineNo,
+        message
+      );
+      processLastException();
+    } else if (exception && utils.isError(exception)) {
+      // non-string `exception` arg; attempt to extract stack trace
+
+      // New chrome and blink send along a real error object
+      // Let's just report that like a normal error.
+      // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
+      stack = TraceKit.computeStackTrace(exception);
+      notifyHandlers(stack, true);
+    } else {
+      var location = {
+        url: url,
+        line: lineNo,
+        column: colNo
+      };
+
+      var name = undefined;
+      var groups;
+
+      if ({}.toString.call(message) === '[object String]') {
+        var groups = message.match(ERROR_TYPES_RE);
+        if (groups) {
+          name = groups[1];
+          message = groups[2];
+        }
+      }
+
+      location.func = UNKNOWN_FUNCTION;
+
+      stack = {
+        name: name,
+        message: message,
+        url: getLocationHref(),
+        stack: [location]
+      };
+      notifyHandlers(stack, true);
+    }
+
+    if (_oldOnerrorHandler) {
+      return _oldOnerrorHandler.apply(this, arguments);
+    }
+
+    return false;
+  }
+
+  function installGlobalHandler() {
+    if (_onErrorHandlerInstalled) {
+      return;
+    }
+    _oldOnerrorHandler = _window.onerror;
+    _window.onerror = traceKitWindowOnError;
+    _onErrorHandlerInstalled = true;
+  }
+
+  function uninstallGlobalHandler() {
+    if (!_onErrorHandlerInstalled) {
+      return;
+    }
+    _window.onerror = _oldOnerrorHandler;
+    _onErrorHandlerInstalled = false;
+    _oldOnerrorHandler = undefined;
+  }
+
+  function processLastException() {
+    var _lastExceptionStack = lastExceptionStack,
+      _lastArgs = lastArgs;
+    lastArgs = null;
+    lastExceptionStack = null;
+    lastException = null;
+    notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs));
+  }
+
+  /**
+   * Reports an unhandled Error to TraceKit.
+   * @param {Error} ex
+   * @param {?boolean} rethrow If false, do not re-throw the exception.
+   * Only used for window.onerror to not cause an infinite loop of
+   * rethrowing.
+   */
+  function report(ex, rethrow) {
+    var args = _slice.call(arguments, 1);
+    if (lastExceptionStack) {
+      if (lastException === ex) {
+        return; // already caught by an inner catch block, ignore
+      } else {
+        processLastException();
+      }
+    }
+
+    var stack = TraceKit.computeStackTrace(ex);
+    lastExceptionStack = stack;
+    lastException = ex;
+    lastArgs = args;
+
+    // If the stack trace is incomplete, wait for 2 seconds for
+    // slow slow IE to see if onerror occurs or not before reporting
+    // this exception; otherwise, we will end up with an incomplete
+    // stack trace
+    setTimeout(function() {
+      if (lastException === ex) {
+        processLastException();
+      }
+    }, stack.incomplete ? 2000 : 0);
+
+    if (rethrow !== false) {
+      throw ex; // re-throw to propagate to the top level (and cause window.onerror)
+    }
+  }
+
+  report.subscribe = subscribe;
+  report.unsubscribe = unsubscribe;
+  report.uninstall = unsubscribeAll;
+  return report;
+})();
+
+/**
  * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript
  *
  * Syntax:
  *   s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below)
  * Returns:
  *   s.name              - exception name
  *   s.message           - exception message
  *   s.stack[i].url      - JavaScript or HTML file URL
@@ -2861,414 +3217,658 @@
  *
  * This is okay for tracing (because you are likely to be calling
  * computeStackTrace from the function you want to be the topmost element
  * of the stack trace anyway), but not okay for logging unhandled
  * exceptions (because your catch block will likely be far away from the
  * inner function that actually caused the exception).
  *
  */
-            TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
-              // Contents of Exception in various browsers.
-              //
-              // SAFARI:
-              // ex.message = Can't find variable: qq
-              // ex.line = 59
-              // ex.sourceId = 580238192
-              // ex.sourceURL = http://...
-              // ex.expressionBeginOffset = 96
-              // ex.expressionCaretOffset = 98
-              // ex.expressionEndOffset = 98
-              // ex.name = ReferenceError
-              //
-              // FIREFOX:
-              // ex.message = qq is not defined
-              // ex.fileName = http://...
-              // ex.lineNumber = 59
-              // ex.columnNumber = 69
-              // ex.stack = ...stack trace... (see the example below)
-              // ex.name = ReferenceError
-              //
-              // CHROME:
-              // ex.message = qq is not defined
-              // ex.name = ReferenceError
-              // ex.type = not_defined
-              // ex.arguments = ['aa']
-              // ex.stack = ...stack trace...
-              //
-              // INTERNET EXPLORER:
-              // ex.message = ...
-              // ex.name = ReferenceError
-              //
-              // OPERA:
-              // ex.message = ...message... (see the example below)
-              // ex.name = ReferenceError
-              // ex.opera#sourceloc = 11  (pretty much useless, duplicates the info in ex.message)
-              // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace'
-
-              /**
-     * Computes stack trace information from the stack property.
-     * Chrome and Gecko use this property.
-     * @param {Error} ex
-     * @return {?Object.<string, *>} Stack trace information.
-     */
-              function computeStackTraceFromStackProp(ex) {
-                if (typeof ex.stack === 'undefined' || !ex.stack) return;
-
-                var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,
-                  gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i,
-                  winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,
-                  // Used to additionally parse URL/line/column from eval frames
-                  geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i,
-                  chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/,
-                  lines = ex.stack.split('\n'),
-                  stack = [],
-                  submatch,
-                  parts,
-                  element,
-                  reference = /^(.*) is undefined$/.exec(ex.message);
-
-                for (var i = 0, j = lines.length; i < j; ++i) {
-                  if ((parts = chrome.exec(lines[i]))) {
-                    var isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
-                    var isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
-                    if (isEval && (submatch = chromeEval.exec(parts[2]))) {
-                      // throw out eval line/column and use top-most line/column number
-                      parts[2] = submatch[1]; // url
-                      parts[3] = submatch[2]; // line
-                      parts[4] = submatch[3]; // column
-                    }
-                    element = {
-                      url: !isNative ? parts[2] : null,
-                      func: parts[1] || UNKNOWN_FUNCTION,
-                      args: isNative ? [parts[2]] : [],
-                      line: parts[3] ? +parts[3] : null,
-                      column: parts[4] ? +parts[4] : null
-                    };
-                  } else if ((parts = winjs.exec(lines[i]))) {
-                    element = {
-                      url: parts[2],
-                      func: parts[1] || UNKNOWN_FUNCTION,
-                      args: [],
-                      line: +parts[3],
-                      column: parts[4] ? +parts[4] : null
-                    };
-                  } else if ((parts = gecko.exec(lines[i]))) {
-                    var isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
-                    if (isEval && (submatch = geckoEval.exec(parts[3]))) {
-                      // throw out eval line/column and use top-most line number
-                      parts[3] = submatch[1];
-                      parts[4] = submatch[2];
-                      parts[5] = null; // no column when eval
-                    } else if (
-                      i === 0 &&
-                      !parts[5] &&
-                      typeof ex.columnNumber !== 'undefined'
-                    ) {
-                      // FireFox uses this awesome columnNumber property for its top frame
-                      // Also note, Firefox's column number is 0-based and everything else expects 1-based,
-                      // so adding 1
-                      // NOTE: this hack doesn't work if top-most frame is eval
-                      stack[0].column = ex.columnNumber + 1;
-                    }
-                    element = {
-                      url: parts[3],
-                      func: parts[1] || UNKNOWN_FUNCTION,
-                      args: parts[2] ? parts[2].split(',') : [],
-                      line: parts[4] ? +parts[4] : null,
-                      column: parts[5] ? +parts[5] : null
-                    };
-                  } else {
-                    continue;
-                  }
-
-                  if (!element.func && element.line) {
-                    element.func = UNKNOWN_FUNCTION;
-                  }
-
-                  stack.push(element);
-                }
-
-                if (!stack.length) {
-                  return null;
-                }
-
-                return {
-                  name: ex.name,
-                  message: ex.message,
-                  url: getLocationHref(),
-                  stack: stack
-                };
-              }
-
-              /**
-     * Adds information about the first frame to incomplete stack traces.
-     * Safari and IE require this to get complete data on the first frame.
-     * @param {Object.<string, *>} stackInfo Stack trace information from
-     * one of the compute* methods.
-     * @param {string} url The URL of the script that caused an error.
-     * @param {(number|string)} lineNo The line number of the script that
-     * caused an error.
-     * @param {string=} message The error generated by the browser, which
-     * hopefully contains the name of the object that caused the error.
-     * @return {boolean} Whether or not the stack information was
-     * augmented.
-     */
-              function augmentStackTraceWithInitialElement(
-                stackInfo,
-                url,
-                lineNo,
-                message
-              ) {
-                var initial = {
-                  url: url,
-                  line: lineNo
-                };
-
-                if (initial.url && initial.line) {
-                  stackInfo.incomplete = false;
-
-                  if (!initial.func) {
-                    initial.func = UNKNOWN_FUNCTION;
-                  }
-
-                  if (stackInfo.stack.length > 0) {
-                    if (stackInfo.stack[0].url === initial.url) {
-                      if (stackInfo.stack[0].line === initial.line) {
-                        return false; // already in stack trace
-                      } else if (
-                        !stackInfo.stack[0].line &&
-                        stackInfo.stack[0].func === initial.func
-                      ) {
-                        stackInfo.stack[0].line = initial.line;
-                        return false;
-                      }
-                    }
-                  }
-
-                  stackInfo.stack.unshift(initial);
-                  stackInfo.partial = true;
-                  return true;
-                } else {
-                  stackInfo.incomplete = true;
-                }
-
-                return false;
-              }
-
-              /**
-     * Computes stack trace information by walking the arguments.caller
-     * chain at the time the exception occurred. This will cause earlier
-     * frames to be missed but is the only way to get any stack trace in
-     * Safari and IE. The top frame is restored by
-     * {@link augmentStackTraceWithInitialElement}.
-     * @param {Error} ex
-     * @return {?Object.<string, *>} Stack trace information.
-     */
-              function computeStackTraceByWalkingCallerChain(ex, depth) {
-                var functionName = /function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i,
-                  stack = [],
-                  funcs = {},
-                  recursion = false,
-                  parts,
-                  item,
-                  source;
-
-                for (
-                  var curr = computeStackTraceByWalkingCallerChain.caller;
-                  curr && !recursion;
-                  curr = curr.caller
-                ) {
-                  if (curr === computeStackTrace || curr === TraceKit.report) {
-                    // console.log('skipping internal function');
-                    continue;
-                  }
-
-                  item = {
-                    url: null,
-                    func: UNKNOWN_FUNCTION,
-                    line: null,
-                    column: null
-                  };
-
-                  if (curr.name) {
-                    item.func = curr.name;
-                  } else if ((parts = functionName.exec(curr.toString()))) {
-                    item.func = parts[1];
-                  }
-
-                  if (typeof item.func === 'undefined') {
-                    try {
-                      item.func = parts.input.substring(0, parts.input.indexOf('{'));
-                    } catch (e) {}
-                  }
-
-                  if (funcs['' + curr]) {
-                    recursion = true;
-                  } else {
-                    funcs['' + curr] = true;
-                  }
-
-                  stack.push(item);
-                }
-
-                if (depth) {
-                  // console.log('depth is ' + depth);
-                  // console.log('stack is ' + stack.length);
-                  stack.splice(0, depth);
-                }
-
-                var result = {
-                  name: ex.name,
-                  message: ex.message,
-                  url: getLocationHref(),
-                  stack: stack
-                };
-                augmentStackTraceWithInitialElement(
-                  result,
-                  ex.sourceURL || ex.fileName,
-                  ex.line || ex.lineNumber,
-                  ex.message || ex.description
-                );
-                return result;
-              }
-
-              /**
-     * Computes a stack trace for an exception.
-     * @param {Error} ex
-     * @param {(string|number)=} depth
-     */
-              function computeStackTrace(ex, depth) {
-                var stack = null;
-                depth = depth == null ? 0 : +depth;
-
-                try {
-                  stack = computeStackTraceFromStackProp(ex);
-                  if (stack) {
-                    return stack;
-                  }
-                } catch (e) {
-                  if (TraceKit.debug) {
-                    throw e;
-                  }
-                }
-
-                try {
-                  stack = computeStackTraceByWalkingCallerChain(ex, depth + 1);
-                  if (stack) {
-                    return stack;
-                  }
-                } catch (e) {
-                  if (TraceKit.debug) {
-                    throw e;
-                  }
-                }
-                return {
-                  name: ex.name,
-                  message: ex.message,
-                  url: getLocationHref()
-                };
-              }
-
-              computeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement;
-              computeStackTrace.computeStackTraceFromStackProp = computeStackTraceFromStackProp;
-
-              return computeStackTrace;
-            })();
-
-            module.exports = TraceKit;
-          }.call(
-            this,
-            typeof global !== 'undefined'
-              ? global
-              : typeof self !== 'undefined'
-                ? self
-                : typeof window !== 'undefined' ? window : {}
-          ));
-        },
-        {'5': 5}
-      ],
-      7: [
-        function(_dereq_, module, exports) {
-          /*
+TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
+  // Contents of Exception in various browsers.
+  //
+  // SAFARI:
+  // ex.message = Can't find variable: qq
+  // ex.line = 59
+  // ex.sourceId = 580238192
+  // ex.sourceURL = http://...
+  // ex.expressionBeginOffset = 96
+  // ex.expressionCaretOffset = 98
+  // ex.expressionEndOffset = 98
+  // ex.name = ReferenceError
+  //
+  // FIREFOX:
+  // ex.message = qq is not defined
+  // ex.fileName = http://...
+  // ex.lineNumber = 59
+  // ex.columnNumber = 69
+  // ex.stack = ...stack trace... (see the example below)
+  // ex.name = ReferenceError
+  //
+  // CHROME:
+  // ex.message = qq is not defined
+  // ex.name = ReferenceError
+  // ex.type = not_defined
+  // ex.arguments = ['aa']
+  // ex.stack = ...stack trace...
+  //
+  // INTERNET EXPLORER:
+  // ex.message = ...
+  // ex.name = ReferenceError
+  //
+  // OPERA:
+  // ex.message = ...message... (see the example below)
+  // ex.name = ReferenceError
+  // ex.opera#sourceloc = 11  (pretty much useless, duplicates the info in ex.message)
+  // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace'
+
+  /**
+   * Computes stack trace information from the stack property.
+   * Chrome and Gecko use this property.
+   * @param {Error} ex
+   * @return {?Object.<string, *>} Stack trace information.
+   */
+  function computeStackTraceFromStackProp(ex) {
+    if (typeof ex.stack === 'undefined' || !ex.stack) return;
+
+    var chrome = /^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
+    var winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx(?:-web)|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
+    // NOTE: blob urls are now supposed to always have an origin, therefore it's format
+    // which is `blob:http://url/path/with-some-uuid`, is matched by `blob.*?:\/` as well
+    var gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|moz-extension).*?:\/.*?|\[native code\]|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i;
+    // Used to additionally parse URL/line/column from eval frames
+    var geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
+    var chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/;
+    var lines = ex.stack.split('\n');
+    var stack = [];
+    var submatch;
+    var parts;
+    var element;
+    var reference = /^(.*) is undefined$/.exec(ex.message);
+
+    for (var i = 0, j = lines.length; i < j; ++i) {
+      if ((parts = chrome.exec(lines[i]))) {
+        var isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
+        var isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
+        if (isEval && (submatch = chromeEval.exec(parts[2]))) {
+          // throw out eval line/column and use top-most line/column number
+          parts[2] = submatch[1]; // url
+          parts[3] = submatch[2]; // line
+          parts[4] = submatch[3]; // column
+        }
+        element = {
+          url: !isNative ? parts[2] : null,
+          func: parts[1] || UNKNOWN_FUNCTION,
+          args: isNative ? [parts[2]] : [],
+          line: parts[3] ? +parts[3] : null,
+          column: parts[4] ? +parts[4] : null
+        };
+      } else if ((parts = winjs.exec(lines[i]))) {
+        element = {
+          url: parts[2],
+          func: parts[1] || UNKNOWN_FUNCTION,
+          args: [],
+          line: +parts[3],
+          column: parts[4] ? +parts[4] : null
+        };
+      } else if ((parts = gecko.exec(lines[i]))) {
+        var isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
+        if (isEval && (submatch = geckoEval.exec(parts[3]))) {
+          // throw out eval line/column and use top-most line number
+          parts[3] = submatch[1];
+          parts[4] = submatch[2];
+          parts[5] = null; // no column when eval
+        } else if (i === 0 && !parts[5] && typeof ex.columnNumber !== 'undefined') {
+          // FireFox uses this awesome columnNumber property for its top frame
+          // Also note, Firefox's column number is 0-based and everything else expects 1-based,
+          // so adding 1
+          // NOTE: this hack doesn't work if top-most frame is eval
+          stack[0].column = ex.columnNumber + 1;
+        }
+        element = {
+          url: parts[3],
+          func: parts[1] || UNKNOWN_FUNCTION,
+          args: parts[2] ? parts[2].split(',') : [],
+          line: parts[4] ? +parts[4] : null,
+          column: parts[5] ? +parts[5] : null
+        };
+      } else {
+        continue;
+      }
+
+      if (!element.func && element.line) {
+        element.func = UNKNOWN_FUNCTION;
+      }
+
+      stack.push(element);
+    }
+
+    if (!stack.length) {
+      return null;
+    }
+
+    return {
+      name: ex.name,
+      message: ex.message,
+      url: getLocationHref(),
+      stack: stack
+    };
+  }
+
+  /**
+   * Adds information about the first frame to incomplete stack traces.
+   * Safari and IE require this to get complete data on the first frame.
+   * @param {Object.<string, *>} stackInfo Stack trace information from
+   * one of the compute* methods.
+   * @param {string} url The URL of the script that caused an error.
+   * @param {(number|string)} lineNo The line number of the script that
+   * caused an error.
+   * @param {string=} message The error generated by the browser, which
+   * hopefully contains the name of the object that caused the error.
+   * @return {boolean} Whether or not the stack information was
+   * augmented.
+   */
+  function augmentStackTraceWithInitialElement(stackInfo, url, lineNo, message) {
+    var initial = {
+      url: url,
+      line: lineNo
+    };
+
+    if (initial.url && initial.line) {
+      stackInfo.incomplete = false;
+
+      if (!initial.func) {
+        initial.func = UNKNOWN_FUNCTION;
+      }
+
+      if (stackInfo.stack.length > 0) {
+        if (stackInfo.stack[0].url === initial.url) {
+          if (stackInfo.stack[0].line === initial.line) {
+            return false; // already in stack trace
+          } else if (
+            !stackInfo.stack[0].line &&
+            stackInfo.stack[0].func === initial.func
+          ) {
+            stackInfo.stack[0].line = initial.line;
+            return false;
+          }
+        }
+      }
+
+      stackInfo.stack.unshift(initial);
+      stackInfo.partial = true;
+      return true;
+    } else {
+      stackInfo.incomplete = true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Computes stack trace information by walking the arguments.caller
+   * chain at the time the exception occurred. This will cause earlier
+   * frames to be missed but is the only way to get any stack trace in
+   * Safari and IE. The top frame is restored by
+   * {@link augmentStackTraceWithInitialElement}.
+   * @param {Error} ex
+   * @return {?Object.<string, *>} Stack trace information.
+   */
+  function computeStackTraceByWalkingCallerChain(ex, depth) {
+    var functionName = /function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i,
+      stack = [],
+      funcs = {},
+      recursion = false,
+      parts,
+      item,
+      source;
+
+    for (
+      var curr = computeStackTraceByWalkingCallerChain.caller;
+      curr && !recursion;
+      curr = curr.caller
+    ) {
+      if (curr === computeStackTrace || curr === TraceKit.report) {
+        // console.log('skipping internal function');
+        continue;
+      }
+
+      item = {
+        url: null,
+        func: UNKNOWN_FUNCTION,
+        line: null,
+        column: null
+      };
+
+      if (curr.name) {
+        item.func = curr.name;
+      } else if ((parts = functionName.exec(curr.toString()))) {
+        item.func = parts[1];
+      }
+
+      if (typeof item.func === 'undefined') {
+        try {
+          item.func = parts.input.substring(0, parts.input.indexOf('{'));
+        } catch (e) {}
+      }
+
+      if (funcs['' + curr]) {
+        recursion = true;
+      } else {
+        funcs['' + curr] = true;
+      }
+
+      stack.push(item);
+    }
+
+    if (depth) {
+      // console.log('depth is ' + depth);
+      // console.log('stack is ' + stack.length);
+      stack.splice(0, depth);
+    }
+
+    var result = {
+      name: ex.name,
+      message: ex.message,
+      url: getLocationHref(),
+      stack: stack
+    };
+    augmentStackTraceWithInitialElement(
+      result,
+      ex.sourceURL || ex.fileName,
+      ex.line || ex.lineNumber,
+      ex.message || ex.description
+    );
+    return result;
+  }
+
+  /**
+   * Computes a stack trace for an exception.
+   * @param {Error} ex
+   * @param {(string|number)=} depth
+   */
+  function computeStackTrace(ex, depth) {
+    var stack = null;
+    depth = depth == null ? 0 : +depth;
+
+    try {
+      stack = computeStackTraceFromStackProp(ex);
+      if (stack) {
+        return stack;
+      }
+    } catch (e) {
+      if (TraceKit.debug) {
+        throw e;
+      }
+    }
+
+    try {
+      stack = computeStackTraceByWalkingCallerChain(ex, depth + 1);
+      if (stack) {
+        return stack;
+      }
+    } catch (e) {
+      if (TraceKit.debug) {
+        throw e;
+      }
+    }
+    return {
+      name: ex.name,
+      message: ex.message,
+      url: getLocationHref()
+    };
+  }
+
+  computeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement;
+  computeStackTrace.computeStackTraceFromStackProp = computeStackTraceFromStackProp;
+
+  return computeStackTrace;
+})();
+
+module.exports = TraceKit;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"5":5}],7:[function(_dereq_,module,exports){
+/*
  json-stringify-safe
  Like JSON.stringify, but doesn't throw on circular references.
 
  Originally forked from https://github.com/isaacs/json-stringify-safe
  version 5.0.1 on 3/8/2017 and modified to handle Errors serialization
  and IE8 compatibility. Tests for this are in test/vendor.
 
  ISC license: https://github.com/isaacs/json-stringify-safe/blob/master/LICENSE
 */
 
-          exports = module.exports = stringify;
-          exports.getSerialize = serializer;
-
-          function indexOf(haystack, needle) {
-            for (var i = 0; i < haystack.length; ++i) {
-              if (haystack[i] === needle) return i;
-            }
-            return -1;
-          }
-
-          function stringify(obj, replacer, spaces, cycleReplacer) {
-            return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces);
-          }
-
-          // https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106
-          function stringifyError(value) {
-            var err = {
-              // These properties are implemented as magical getters and don't show up in for in
-              stack: value.stack,
-              message: value.message,
-              name: value.name
-            };
-
-            for (var i in value) {
-              if (Object.prototype.hasOwnProperty.call(value, i)) {
-                err[i] = value[i];
-              }
-            }
-
-            return err;
-          }
-
-          function serializer(replacer, cycleReplacer) {
-            var stack = [];
-            var keys = [];
-
-            if (cycleReplacer == null) {
-              cycleReplacer = function(key, value) {
-                if (stack[0] === value) {
-                  return '[Circular ~]';
-                }
-                return (
-                  '[Circular ~.' + keys.slice(0, indexOf(stack, value)).join('.') + ']'
-                );
-              };
-            }
-
-            return function(key, value) {
-              if (stack.length > 0) {
-                var thisPos = indexOf(stack, this);
-                ~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
-                ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
-
-                if (~indexOf(stack, value)) {
-                  value = cycleReplacer.call(this, key, value);
-                }
-              } else {
-                stack.push(value);
-              }
-
-              return replacer == null
-                ? value instanceof Error ? stringifyError(value) : value
-                : replacer.call(this, key, value);
-            };
-          }
-        },
-        {}
-      ]
-    },
-    {},
-    [4]
-  )(4);
-});
+exports = module.exports = stringify;
+exports.getSerialize = serializer;
+
+function indexOf(haystack, needle) {
+  for (var i = 0; i < haystack.length; ++i) {
+    if (haystack[i] === needle) return i;
+  }
+  return -1;
+}
+
+function stringify(obj, replacer, spaces, cycleReplacer) {
+  return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces);
+}
+
+// https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106
+function stringifyError(value) {
+  var err = {
+    // These properties are implemented as magical getters and don't show up in for in
+    stack: value.stack,
+    message: value.message,
+    name: value.name
+  };
+
+  for (var i in value) {
+    if (Object.prototype.hasOwnProperty.call(value, i)) {
+      err[i] = value[i];
+    }
+  }
+
+  return err;
+}
+
+function serializer(replacer, cycleReplacer) {
+  var stack = [];
+  var keys = [];
+
+  if (cycleReplacer == null) {
+    cycleReplacer = function(key, value) {
+      if (stack[0] === value) {
+        return '[Circular ~]';
+      }
+      return '[Circular ~.' + keys.slice(0, indexOf(stack, value)).join('.') + ']';
+    };
+  }
+
+  return function(key, value) {
+    if (stack.length > 0) {
+      var thisPos = indexOf(stack, this);
+      ~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
+      ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
+
+      if (~indexOf(stack, value)) {
+        value = cycleReplacer.call(this, key, value);
+      }
+    } else {
+      stack.push(value);
+    }
+
+    return replacer == null
+      ? value instanceof Error ? stringifyError(value) : value
+      : replacer.call(this, key, value);
+  };
+}
+
+},{}],8:[function(_dereq_,module,exports){
+/*
+ * JavaScript MD5
+ * https://github.com/blueimp/JavaScript-MD5
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ *
+ * Based on
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*
+* Add integers, wrapping at 2^32. This uses 16-bit operations internally
+* to work around bugs in some JS interpreters.
+*/
+function safeAdd(x, y) {
+  var lsw = (x & 0xffff) + (y & 0xffff);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xffff);
+}
+
+/*
+* Bitwise rotate a 32-bit number to the left.
+*/
+function bitRotateLeft(num, cnt) {
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+/*
+* These functions implement the four basic operations the algorithm uses.
+*/
+function md5cmn(q, a, b, x, s, t) {
+  return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
+}
+function md5ff(a, b, c, d, x, s, t) {
+  return md5cmn((b & c) | (~b & d), a, b, x, s, t);
+}
+function md5gg(a, b, c, d, x, s, t) {
+  return md5cmn((b & d) | (c & ~d), a, b, x, s, t);
+}
+function md5hh(a, b, c, d, x, s, t) {
+  return md5cmn(b ^ c ^ d, a, b, x, s, t);
+}
+function md5ii(a, b, c, d, x, s, t) {
+  return md5cmn(c ^ (b | ~d), a, b, x, s, t);
+}
+
+/*
+* Calculate the MD5 of an array of little-endian words, and a bit length.
+*/
+function binlMD5(x, len) {
+  /* append padding */
+  x[len >> 5] |= 0x80 << (len % 32);
+  x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+  var i;
+  var olda;
+  var oldb;
+  var oldc;
+  var oldd;
+  var a = 1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d = 271733878;
+
+  for (i = 0; i < x.length; i += 16) {
+    olda = a;
+    oldb = b;
+    oldc = c;
+    oldd = d;
+
+    a = md5ff(a, b, c, d, x[i], 7, -680876936);
+    d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
+    c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
+    b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
+    a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
+    d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
+    c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
+    b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
+    a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
+    d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
+    c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
+    b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
+    a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
+    d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
+    c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
+    b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
+
+    a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
+    d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
+    c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
+    b = md5gg(b, c, d, a, x[i], 20, -373897302);
+    a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
+    d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
+    c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
+    b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
+    a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
+    d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
+    c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
+    b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
+    a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
+    d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
+    c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
+    b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
+
+    a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
+    d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
+    c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
+    b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
+    a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
+    d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
+    c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
+    b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
+    a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
+    d = md5hh(d, a, b, c, x[i], 11, -358537222);
+    c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
+    b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
+    a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
+    d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
+    c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
+    b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
+
+    a = md5ii(a, b, c, d, x[i], 6, -198630844);
+    d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
+    c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
+    b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
+    a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
+    d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
+    c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
+    b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
+    a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
+    d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
+    c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
+    b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
+    a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
+    d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
+    c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
+    b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
+
+    a = safeAdd(a, olda);
+    b = safeAdd(b, oldb);
+    c = safeAdd(c, oldc);
+    d = safeAdd(d, oldd);
+  }
+  return [a, b, c, d];
+}
+
+/*
+* Convert an array of little-endian words to a string
+*/
+function binl2rstr(input) {
+  var i;
+  var output = '';
+  var length32 = input.length * 32;
+  for (i = 0; i < length32; i += 8) {
+    output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff);
+  }
+  return output;
+}
+
+/*
+* Convert a raw string to an array of little-endian words
+* Characters >255 have their high-byte silently ignored.
+*/
+function rstr2binl(input) {
+  var i;
+  var output = [];
+  output[(input.length >> 2) - 1] = undefined;
+  for (i = 0; i < output.length; i += 1) {
+    output[i] = 0;
+  }
+  var length8 = input.length * 8;
+  for (i = 0; i < length8; i += 8) {
+    output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32);
+  }
+  return output;
+}
+
+/*
+* Calculate the MD5 of a raw string
+*/
+function rstrMD5(s) {
+  return binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
+}
+
+/*
+* Calculate the HMAC-MD5, of a key and some data (raw strings)
+*/
+function rstrHMACMD5(key, data) {
+  var i;
+  var bkey = rstr2binl(key);
+  var ipad = [];
+  var opad = [];
+  var hash;
+  ipad[15] = opad[15] = undefined;
+  if (bkey.length > 16) {
+    bkey = binlMD5(bkey, key.length * 8);
+  }
+  for (i = 0; i < 16; i += 1) {
+    ipad[i] = bkey[i] ^ 0x36363636;
+    opad[i] = bkey[i] ^ 0x5c5c5c5c;
+  }
+  hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
+  return binl2rstr(binlMD5(opad.concat(hash), 512 + 128));
+}
+
+/*
+* Convert a raw string to a hex string
+*/
+function rstr2hex(input) {
+  var hexTab = '0123456789abcdef';
+  var output = '';
+  var x;
+  var i;
+  for (i = 0; i < input.length; i += 1) {
+    x = input.charCodeAt(i);
+    output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f);
+  }
+  return output;
+}
+
+/*
+* Encode a string as utf-8
+*/
+function str2rstrUTF8(input) {
+  return unescape(encodeURIComponent(input));
+}
+
+/*
+* Take string arguments and return either raw or hex encoded strings
+*/
+function rawMD5(s) {
+  return rstrMD5(str2rstrUTF8(s));
+}
+function hexMD5(s) {
+  return rstr2hex(rawMD5(s));
+}
+function rawHMACMD5(k, d) {
+  return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d));
+}
+function hexHMACMD5(k, d) {
+  return rstr2hex(rawHMACMD5(k, d));
+}
+
+function md5(string, key, raw) {
+  if (!key) {
+    if (!raw) {
+      return hexMD5(string);
+    }
+    return rawMD5(string);
+  }
+  if (!raw) {
+    return hexHMACMD5(key, string);
+  }
+  return rawHMACMD5(key, string);
+}
+
+module.exports = md5;
+
+},{}]},{},[4])(4)
+});
\ No newline at end of file
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -1,40 +1,37 @@
 this.shot = (function () {let exports={}; // Note: in this library we can't use any "system" dependencies because this can be used from multiple
 // environments
 
+const isNode = typeof process !== "undefined" && Object.prototype.toString.call(process) === "[object process]";
+const URL = (isNode && require("url").URL) || window.URL;
+
 /** Throws an error if the condition isn't true.  Any extra arguments after the condition
     are used as console.error() arguments. */
 function assert(condition, ...args) {
   if (condition) {
     return;
   }
   console.error("Failed assertion", ...args);
   throw new Error(`Failed assertion: ${args.join(" ")}`);
 }
 
 /** True if `url` is a valid URL */
 function isUrl(url) {
-  // FIXME: this is rather naive, obviously
-  if ((/^about:.{1,8000}$/i).test(url)) {
-    return true;
-  }
-  if ((/^file:\/.{0,8000}$/i).test(url)) {
-    return true;
-  }
-  if ((/^data:.*$/i).test(url)) {
+  try {
+    const parsed = new URL(url);
+
+    if (parsed.protocol === "view-source:") {
+      return isUrl(url.substr("view-source:".length));
+    }
+
     return true;
-  }
-  if ((/^chrome:.{0,8000}/i).test(url)) {
-    return true;
+  } catch (e) {
+    return false;
   }
-  if ((/^view-source:/i).test(url)) {
-    return isUrl(url.substr("view-source:".length));
-  }
-  return (/^https?:\/\/[a-z0-9._-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
 }
 
 function isValidClipImageUrl(url) {
     return isUrl(url) && !(url.indexOf(")") > -1);
 }
 
 function assertUrl(url) {
   if (!url) {
@@ -43,17 +40,17 @@ function assertUrl(url) {
   if (!isUrl(url)) {
     const exc = new Error("Not a URL");
     exc.scheme = url.split(":")[0];
     throw exc;
   }
 }
 
 function isSecureWebUri(url) {
-  return (/^https?:\/\/[a-z0-9._-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+  return isUrl(url) && url.toLowerCase().startsWith("https");
 }
 
 function assertOrigin(url) {
   assertUrl(url);
   if (url.search(/^https?:/i) !== -1) {
     const match = (/^https?:\/\/[^/:]{1,4000}\/?$/i).exec(url);
     if (!match) {
       throw new Error("Bad origin, might include path");
@@ -110,49 +107,16 @@ function jsonify(obj, required, optional
   for (const attr of optional) {
     if (obj[attr]) {
       result[attr] = obj[attr];
     }
   }
   return result;
 }
 
-/** Resolve url relative to base */
-function resolveUrl(base, url) {
-  // FIXME: totally ad hoc and probably incorrect, but we can't
-  // use any libraries in this file
-  if (url.search(/^https?:/) !== -1) {
-    // Absolute url
-    return url;
-  }
-  if (url.indexOf("//") === 0) {
-    // Protocol-relative URL
-    return (/^https?:/i).exec(base)[0] + url;
-  }
-  if (url.indexOf("/") === 0) {
-    // Domain-relative URL
-    return (/^https?:\/\/[a-z0-9._-]{1,4000}/i).exec(base)[0] + url;
-  }
-  // Otherwise, a full relative URL
-  while (url.indexOf("./") === 0) {
-    url = url.substr(2);
-  }
-  if (!base) {
-    // It's not an absolute URL, and we don't have a base URL, so we have
-    // to throw away the URL
-    return null;
-  }
-  let match = (/.*\//).exec(base)[0];
-  if (match.search(/^https?:\/$/i) === 0) {
-    // Domain without path
-    match = match + "/";
-  }
-  return match + url;
-}
-
 /** True if the two objects look alike.  Null, undefined, and absent properties
     are all treated as equivalent.  Traverses objects and arrays */
 function deepEqual(a, b) {
   if ((a === null || a === undefined) && (b === null || b === undefined)) {
     return true;
   }
   if (typeof a !== "object" || typeof b !== "object") {
     return a === b;
@@ -270,70 +234,70 @@ class AbstractShot {
     const ALL_ATTRS = ["clips"].concat(this.REGULAR_ATTRS);
     assert(checkObject(json, [], ALL_ATTRS), "Bad attr to new Shot():", Object.keys(json));
     for (const attr in json) {
       if (attr === "clips") {
         continue;
       }
       if (typeof json[attr] === "object" && typeof this[attr] === "object" && this[attr] !== null) {
         let val = this[attr];
-        if (val.asJson) {
-          val = val.asJson();
+        if (val.toJSON) {
+          val = val.toJSON();
         }
         if (!deepEqual(json[attr], val)) {
           this[attr] = json[attr];
         }
       } else if (json[attr] !== this[attr] &&
           (json[attr] || this[attr])) {
         this[attr] = json[attr];
       }
     }
     if (json.clips) {
       for (const clipId in json.clips) {
         if (!json.clips[clipId]) {
           this.delClip(clipId);
         } else if (!this.getClip(clipId)) {
           this.setClip(clipId, json.clips[clipId]);
-        } else if (!deepEqual(this.getClip(clipId).asJson(), json.clips[clipId])) {
+        } else if (!deepEqual(this.getClip(clipId).toJSON(), json.clips[clipId])) {
           this.setClip(clipId, json.clips[clipId]);
         }
       }
     }
 
   }
 
   /** Returns a JSON version of this shot */
-  asJson() {
+  toJSON() {
     const result = {};
     for (const attr of this.REGULAR_ATTRS) {
       let val = this[attr];
-      if (val && val.asJson) {
-        val = val.asJson();
+      if (val && val.toJSON) {
+        val = val.toJSON();
       }
       result[attr] = val;
     }
     result.clips = {};
     for (const attr in this._clips) {
-      result.clips[attr] = this._clips[attr].asJson();
+      result.clips[attr] = this._clips[attr].toJSON();
     }
     return result;
   }
 
   /** A more minimal JSON representation for creating indexes of shots */
   asRecallJson() {
     const result = {clips: {}};
     for (const attr of this.RECALL_ATTRS) {
       let val = this[attr];
-      if (val && val.asJson) {
-        val = val.asJson();
+      if (val && val.toJSON) {
+        val = val.toJSON();
       }
       result[attr] = val;
     }
     for (const name of this.clipNames()) {
-      result.clips[name] = this.getClip(name).asJson();
+      result.clips[name] = this.getClip(name).toJSON();
     }
     return result;
   }
 
   get backend() {
     return this._backend;
   }
 
@@ -369,17 +333,18 @@ class AbstractShot {
   }
 
   get filename() {
     let filenameTitle = this.title;
     const date = new Date(this.createdDate);
     // eslint-disable-next-line no-control-regex
     filenameTitle = filenameTitle.replace(/[:\\<>/!@&?"*.|\x00-\x1F]/g, " ");
     filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
-    let clipFilename = `Screenshot-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${filenameTitle}`;
+    const filenameDate = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000).toISOString().substring(0, 10);
+    let clipFilename = `Screenshot_${filenameDate} ${filenameTitle}`;
     const clipFilenameBytesSize = clipFilename.length * 2; // JS STrings are UTF-16
     if (clipFilenameBytesSize > 251) { // 255 bytes (Usual filesystems max) - 4 for the ".png" file extension string
       const excedingchars = (clipFilenameBytesSize - 246) / 2; // 251 - 5 for ellipsis "[...]"
       clipFilename = clipFilename.substring(0, clipFilename.length - excedingchars);
       clipFilename = clipFilename + "[...]";
     }
     const clip = this.getClip(this.clipNames()[0]);
     let extension = ".png";
@@ -492,24 +457,18 @@ class AbstractShot {
     assert(val === null || typeof val === "number", "Bad createdDate:", val);
     this._createdDate = val;
   }
 
   get favicon() {
     return this._favicon;
   }
   set favicon(val) {
-    // We allow but ignore bad favicon URLs, as they seem somewhat common
+    // We set the favicon with tabs.Tab.faviConUrl, which is a full URL.
     val = val || null;
-    if (!isUrl(val)) {
-      val = null;
-    }
-    if (val) {
-      val = resolveUrl(this.url, val);
-    }
     this._favicon = val;
   }
 
   clipNames() {
     const names = Object.getOwnPropertyNames(this._clips);
     names.sort(function(a, b) {
       return a.sortOrder < b.sortOrder ? 1 : 0;
     });
@@ -653,17 +612,17 @@ class _Image {
            "Bad Image dimensions:", json.dimensions);
     this.dimensions = json.dimensions;
     assert(typeof json.title === "string" || !json.title, "Bad Image title:", json.title);
     this.title = json.title;
     assert(typeof json.alt === "string" || !json.alt, "Bad Image alt:", json.alt);
     this.alt = json.alt;
   }
 
-  asJson() {
+  toJSON() {
     return jsonify(this, ["url"], ["dimensions"]);
   }
 }
 
 AbstractShot.prototype.Image = _Image;
 
 /** Represents a clip, either a text or image clip */
 class _Clip {
@@ -684,17 +643,17 @@ class _Clip {
     }
     this.image = json.image;
   }
 
   toString() {
     return `[Shot Clip id=${this.id} sortOrder=${this.sortOrder} image ${this.image.dimensions.x}x${this.image.dimensions.y}]`;
   }
 
-  asJson() {
+  toJSON() {
     return jsonify(this, ["createdDate"], ["sortOrder", "image"]);
   }
 
   get id() {
     return this._id;
   }
 
   get createdDate() {
--- a/browser/extensions/screenshots/webextension/clipboard.js
+++ b/browser/extensions/screenshots/webextension/clipboard.js
@@ -9,16 +9,17 @@ this.clipboard = (function() {
     return new Promise((resolve, reject) => {
       const element = document.createElement("iframe");
       element.src = browser.extension.getURL("blank.html");
       // We can't actually hide the iframe while copying, but we can make
       // it close to invisible:
       element.style.opacity = "0";
       element.style.width = "1px";
       element.style.height = "1px";
+      element.style.display = "block";
       element.addEventListener("load", catcher.watchFunction(() => {
         try {
           const doc = element.contentDocument;
           assertIsBlankDocument(doc);
           const el = doc.createElement("textarea");
           doc.body.appendChild(el);
           el.value = text;
           if (!text) {
--- a/browser/extensions/screenshots/webextension/icons/icon-v2.svg
+++ b/browser/extensions/screenshots/webextension/icons/icon-v2.svg
@@ -1,1 +1,1 @@
-<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity"><path d="M8 2a4 4 0 0 0-4 4h4V2zm12 0h-4v4h4V2zm8 0v4h4a4 4 0 0 0-4-4zM14 2h-4v4h4V2zm12 0h-4v4h4V2zm2 10h4V8h-4v4zm0 12a4 4 0 0 0 4-4h-4v4zm0-6h4v-4h-4v4zm-.882-4.334a4 4 0 0 0-5.57-.984l-7.67 5.662-3.936-2.76c.031-.193.05-.388.058-.584a4.976 4.976 0 0 0-2-3.978V8H4v2.1a5 5 0 1 0 3.916 8.948l2.484 1.738-2.8 1.964a4.988 4.988 0 1 0 2.3 3.266l17.218-12.35zM5 17.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm10.8-4.858l6.034 4.6a4 4 0 0 0 5.57-.984L19.28 22.2l-3.48 2.442z"/></svg>
\ No newline at end of file
+<svg viewBox="0 0 32 32" width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity"><path d="M8 2a4 4 0 0 0-4 4h4V2zm12 0h-4v4h4V2zm8 0v4h4a4 4 0 0 0-4-4zM14 2h-4v4h4V2zm12 0h-4v4h4V2zm2 10h4V8h-4v4zm0 12a4 4 0 0 0 4-4h-4v4zm0-6h4v-4h-4v4zm-.882-4.334a4 4 0 0 0-5.57-.984l-7.67 5.662-3.936-2.76c.031-.193.05-.388.058-.584a4.976 4.976 0 0 0-2-3.978V8H4v2.1a5 5 0 1 0 3.916 8.948l2.484 1.738-2.8 1.964a4.988 4.988 0 1 0 2.3 3.266l17.218-12.35zM5 17.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm10.8-4.858l6.034 4.6a4 4 0 0 0 5.57-.984L19.28 22.2l-3.48 2.442z"/></svg>
index 541f5e01c200c595b9f122468d295c8bd5f6ca25..22bab1da8d2386f0f88e817649f1eea5db54ea40
GIT binary patch
literal 27596
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
index f29292be45ce4cb28c03a08e5cd2309faf78932a..10e86053f25768091018c6d394a0d8430c6a2688
GIT binary patch
literal 41136
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