diff --git a/extensions/chromium/pdfHandler.html b/extensions/chromium/pdfHandler.html
index e412ca1ae..728a038c3 100644
--- a/extensions/chromium/pdfHandler.html
+++ b/extensions/chromium/pdfHandler.html
@@ -22,3 +22,4 @@ limitations under the License.
 <script src="pdfHandler-vcros.js"></script>
 <script src="pageAction/background.js"></script>
 <script src="suppress-update.js"></script>
+<script src="telemetry.js"></script>
diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json
index 921ef28df..c4ea94889 100644
--- a/extensions/chromium/preferences_schema.json
+++ b/extensions/chromium/preferences_schema.json
@@ -88,6 +88,12 @@
         4
       ],
       "default": 0
+    },
+    "disableTelemetry": {
+      "title": "Disable telemetry",
+      "type": "boolean",
+      "description": "Whether to prevent the extension from reporting the extension and browser version to the extension developers.",
+      "default": false
     }
   }
 }
diff --git a/extensions/chromium/telemetry.js b/extensions/chromium/telemetry.js
new file mode 100644
index 000000000..04061681d
--- /dev/null
+++ b/extensions/chromium/telemetry.js
@@ -0,0 +1,162 @@
+/*
+Copyright 2016 Mozilla Foundation
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+/* globals chrome, crypto, Headers, Request */
+
+(function() {
+  'use strict';
+  // This module sends the browser and extension version to a server, to
+  // determine whether it is safe to drop support for old Chrome versions in
+  // future extension updates.
+  //
+  // The source code for the server is available at:
+  // https://github.com/Rob--W/pdfjs-telemetry
+  var LOG_URL = 'https://pdfjs.robwu.nl/logpdfjs';
+
+  // The minimum time to wait before sending a ping, so that we don't send too
+  // many requests even if the user restarts their browser very often.
+  // We want one ping a day, so a minimum delay of 12 hours should be OK.
+  var MINIMUM_TIME_BETWEEN_PING = 12 * 36E5;
+
+  if (chrome.extension.inIncognitoContext) {
+    // The extension uses incognito split mode, so there are two background
+    // pages. Only send telemetry when not in incognito mode.
+    return;
+  }
+
+  if (chrome.runtime.id !== 'oemmndcbldboiebfnladdacbdfmadadm') {
+    // Only send telemetry for the official PDF.js extension.
+    console.warn('Disabled telemetry because this is not an official build.');
+    return;
+  }
+
+  maybeSendPing();
+  setInterval(maybeSendPing, 36E5);
+
+  function maybeSendPing() {
+    getLoggingPref(function(didOptOut) {
+      if (didOptOut) {
+        // Respect the user's decision to not send statistics.
+        return;
+      }
+      if (!navigator.onLine) {
+        // No network available; Wait until the next scheduled ping opportunity.
+        // Even if onLine is true, the server may still be unreachable. But
+        // because it is impossible to tell whether a request failed due to the
+        // inability to connect, or a deliberate connection termination by the
+        // server, we don't validate the response and assume that the request
+        // succeeded. This ensures that the server cannot ask the client to
+        // send more pings.
+        return;
+      }
+      var lastTime = parseInt(localStorage.telemetryLastTime) || 0;
+      var wasUpdated = didUpdateSinceLastCheck();
+      if (!wasUpdated && Date.now() - lastTime < MINIMUM_TIME_BETWEEN_PING) {
+        return;
+      }
+      localStorage.telemetryLastTime = Date.now();
+
+      var deduplication_id = getDeduplicationId(wasUpdated);
+      var extension_version = chrome.runtime.getManifest().version;
+      if (window.Request && 'mode' in Request.prototype) {
+        // fetch is supported in extensions since Chrome 42 (though the above
+        // feature-detection method detects selects Chrome 43+).
+        // Unlike XMLHttpRequest, fetch omits credentials such as cookies in the
+        // requests, which guarantees that the server cannot track the client
+        // via HTTP cookies.
+        fetch(LOG_URL, {
+          method: 'POST',
+          headers: new Headers({
+            'Deduplication-Id': deduplication_id,
+            'Extension-Version': extension_version,
+          }),
+          // Set mode=cors so that the above custom headers are included in the
+          // request.
+          mode: 'cors',
+        });
+        return;
+      }
+      var x = new XMLHttpRequest();
+      x.open('POST', LOG_URL);
+      x.setRequestHeader('Deduplication-Id', deduplication_id);
+      x.setRequestHeader('Extension-Version', extension_version);
+      x.send();
+    });
+  }
+
+  /**
+   * Generate a 40-bit hexadecimal string (=10 letters, 1.1E12 possibilities).
+   * This is used by the server to discard duplicate entries of the same browser
+   * version when the log data is aggregated.
+   */
+  function getDeduplicationId(wasUpdated) {
+    var id = localStorage.telemetryDeduplicationId;
+    // The ID is only used to deduplicate reports for the same browser version,
+    // so it is OK to change the ID if the browser is updated. By changing the
+    // ID, the server cannot track users for a long period even if it wants to.
+    if (!id || !/^[0-9a-f]{10}$/.test(id) || wasUpdated) {
+      id = '';
+      var buf = new Uint8Array(5);
+      crypto.getRandomValues(buf);
+      for (var i = 0; i < buf.length; ++i) {
+        var c = buf[i];
+        id += (c >>> 4).toString(16) + (c & 0xF).toString(16);
+      }
+      localStorage.telemetryDeduplicationId = id;
+    }
+    return id;
+  }
+
+  /**
+   * Returns whether the browser has received a major update since the last call
+   * to this function.
+   */
+  function didUpdateSinceLastCheck() {
+    var chromeVersion = /Chrome\/(\d+)\./.exec(navigator.userAgent);
+    chromeVersion = chromeVersion && chromeVersion[1];
+    if (!chromeVersion || localStorage.telemetryLastVersion === chromeVersion) {
+      return false;
+    }
+    localStorage.telemetryLastVersion = chromeVersion;
+    return true;
+  }
+
+  /**
+   * Get the value of the telemetry preference. The callback is invoked with a
+   * boolean if a preference is found, and with the undefined value otherwise.
+   */
+  function getLoggingPref(callback) {
+    // Try to look up the preference in the storage, in the following order:
+    var areas = ['sync', 'local', 'managed'];
+
+    next();
+    function next(result) {
+      var storageAreaName = areas.shift();
+      if (typeof result === 'boolean' || !storageAreaName) {
+        callback(result);
+        return;
+      }
+
+      if (!chrome.storage[storageAreaName]) {
+        next();
+        return;
+      }
+
+      chrome.storage[storageAreaName].get('disableTelemetry', function(items) {
+        next(items && items.disableTelemetry);
+      });
+    }
+  }
+})();
diff --git a/gulpfile.js b/gulpfile.js
index 0ac9dc1dc..b13865343 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -79,6 +79,13 @@ function checkChromePreferencesFile(chromePrefsPath, webPrefsPath) {
   var webPrefs = JSON.parse(fs.readFileSync(webPrefsPath).toString());
   var webPrefsKeys = Object.keys(webPrefs);
   webPrefsKeys.sort();
+  var telemetryIndex = chromePrefsKeys.indexOf('disableTelemetry');
+  if (telemetryIndex >= 0) {
+    chromePrefsKeys.splice(telemetryIndex, 1);
+  } else {
+    console.log('Warning: disableTelemetry key not found in chrome prefs!');
+    return false;
+  }
   if (webPrefsKeys.length !== chromePrefsKeys.length) {
     return false;
   }
diff --git a/test/chromium/test-telemetry.js b/test/chromium/test-telemetry.js
new file mode 100755
index 000000000..a290c0866
--- /dev/null
+++ b/test/chromium/test-telemetry.js
@@ -0,0 +1,411 @@
+#!/usr/bin/env node
+/* Copyright 2016 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var fs = require('fs');
+var vm = require('vm');
+
+var SRC_DIR = __dirname + '/../../';
+var telemetryJsPath = 'extensions/chromium/telemetry.js';
+var telemetryJsSource = fs.readFileSync(SRC_DIR + telemetryJsPath);
+var telemetryScript = new vm.Script(telemetryJsSource, {
+  filename: telemetryJsPath,
+  displayErrors: true,
+});
+var LOG_URL = /LOG_URL = '([^']+)'/.exec(telemetryJsSource)[1];
+
+// Create a minimal extension global that mocks the extension environment that
+// is used by telemetry.js
+function createExtensionGlobal() {
+  var window = {};
+
+  // Whenever a "request" was made, the extra headers are appended to this list.
+  var test_requests = window.test_requests = [];
+
+  // Extension API mocks.
+  window.window = window;
+  window.chrome = {};
+  window.chrome.extension = {};
+  window.chrome.extension.inIncognitoContext = false;
+  window.chrome.runtime = {};
+  window.chrome.runtime.id = 'oemmndcbldboiebfnladdacbdfmadadm';
+  window.chrome.runtime.getManifest = function() {
+    return {version: '1.0.0'};
+  };
+
+  function createStorageAPI() {
+    var storageArea = {};
+    storageArea.get = function(key, callback) {
+      assert.equal(key, 'disableTelemetry');
+      // chrome.storage.*. is async, but we make it synchronous to ease testing.
+      callback(storageArea.mock_data);
+    };
+    return storageArea;
+  }
+  window.chrome.storage = {};
+  window.chrome.storage.managed = createStorageAPI();
+  window.chrome.storage.local = createStorageAPI();
+  window.chrome.storage.sync = createStorageAPI();
+
+  // Other DOM.
+  window.navigator = {};
+  window.navigator.onLine = true;
+  window.navigator.userAgent =
+    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' +
+    'Chrome/50.0.2661.94 Safari/537.36';
+  window.localStorage = {};
+
+  var getRandomValues_state = 0;
+  window.crypto = {};
+  window.crypto.getRandomValues = function(buf) {
+    var state = getRandomValues_state++;
+    for (var i = 0; i < buf.length; ++i) {
+      // Totally random byte ;)
+      buf[i] = 0x42 + state;
+    }
+    return buf;
+  };
+
+  // Network-related mocks.
+  window.Request = {};
+  window.Request.prototype = {
+    get mode() { throw new TypeError('Illegal invocation'); },
+  };
+  window.fetch = function(url, options) {
+    assert.equal(url, LOG_URL);
+    assert.equal(options.method, 'POST');
+    assert.equal(options.mode, 'cors');
+    assert.ok(!options.body);
+    test_requests.push(options.headers);
+  };
+  window.Headers = function(headers) {
+    headers = JSON.parse(JSON.stringify(headers)); // Clone.
+    Object.keys(headers).forEach(function(k) {
+      headers[k] = String(headers[k]);
+    });
+    return headers;
+  };
+  window.XMLHttpRequest = function() {
+    var invoked = {
+      open: false,
+      send: false,
+    };
+    var headers = {};
+    return {
+      open: function(method, url) {
+        assert.equal(invoked.open, false);
+        invoked.open = true;
+        assert.equal(method, 'POST');
+        assert.equal(url, LOG_URL);
+      },
+      setRequestHeader: function(k, v) {
+        assert.equal(invoked.open, true);
+        headers[k] = String(v);
+      },
+      send: function(body) {
+        assert.equal(invoked.open, true);
+        assert.equal(invoked.send, false);
+        invoked.send = true;
+        assert.ok(!body);
+        test_requests.push(headers);
+      },
+    };
+  };
+
+  // Time-related logic.
+  var timers = [];
+  window.setInterval = function(callback, ms) {
+    assert.equal(typeof callback, 'function');
+    timers.push(callback);
+  };
+  window.Date = {
+    test_now_value: Date.now(),
+    now: function() {
+      return window.Date.test_now_value;
+    },
+  };
+  window.test_fireTimers = function() {
+    assert.ok(timers.length);
+    timers.forEach(function(timer) {
+      timer();
+    });
+  };
+
+  return window;
+}
+
+// Simulate an update of the browser.
+function updateBrowser(window) {
+  window.navigator.userAgent =
+    window.navigator.userAgent.replace(/Chrome\/(\d+)/, function(_, v) {
+      return 'Chrome/' + (parseInt(v) + 1);
+    });
+}
+
+var tests = [
+  function test_first_run() {
+    // Default settings, run extension for the first time.
+    var window = createExtensionGlobal();
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_first_run_incognito() {
+    // The extension should not send any requests when in incognito mode.
+    var window = createExtensionGlobal();
+    window.chrome.extension.inIncognitoContext = true;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, []);
+  },
+
+  function test_storage_managed_unavailable() {
+    var window = createExtensionGlobal();
+    delete window.chrome.storage.managed;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_managed_pref() {
+    var window = createExtensionGlobal();
+    window.chrome.storage.managed.mock_data = {
+      disableTelemetry: true,
+    };
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, []);
+  },
+
+  function test_local_pref() {
+    var window = createExtensionGlobal();
+    window.chrome.storage.local.mock_data = {
+      disableTelemetry: true,
+    };
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, []);
+  },
+
+  function test_managed_pref_is_overridden() {
+    var window = createExtensionGlobal();
+    window.chrome.storage.managed.mock_data = {
+      disableTelemetry: true,
+    };
+    window.chrome.storage.sync.mock_data = {
+      disableTelemetry: false,
+    };
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_run_extension_again() {
+    var window = createExtensionGlobal();
+    telemetryScript.runInNewContext(window);
+    telemetryScript.runInNewContext(window);
+    // Only one request should be sent because of rate-limiting.
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+
+    // Simulate that quite some hours passed, but it's still rate-limited.
+    window.Date.test_now_value += 11 * 36E5;
+    telemetryScript.runInNewContext(window);
+    // Only one request should be sent because of rate-limiting.
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+
+    // Another hour passes and the request should not be rate-limited any more.
+    window.Date.test_now_value += 1 * 36E5;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }, {
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_running_for_a_while() {
+    var window = createExtensionGlobal();
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+
+    // Simulate that the timer fired 11 hours since the last ping. The request
+    // should still be rate-limited.
+    window.Date.test_now_value += 11 * 36E5;
+    window.test_fireTimers();
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+
+    // Another hour passes and the request should not be rate-limited any more.
+    window.Date.test_now_value += 1 * 36E5;
+    window.test_fireTimers();
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }, {
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_browser_update() {
+    var window = createExtensionGlobal();
+    telemetryScript.runInNewContext(window);
+    updateBrowser(window);
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }, {
+      // Generate a new ID for better privacy.
+      'Deduplication-Id': '4343434343',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_browser_update_between_pref_toggle() {
+    var window = createExtensionGlobal();
+    telemetryScript.runInNewContext(window);
+    window.chrome.storage.local.mock_data = {
+      disableTelemetry: true,
+    };
+    updateBrowser(window);
+    telemetryScript.runInNewContext(window);
+    window.chrome.storage.local.mock_data = {
+      disableTelemetry: false,
+    };
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }, {
+      // Generate a new ID for better privacy, even if the update happened
+      // while telemetry was disabled.
+      'Deduplication-Id': '4343434343',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_extension_update() {
+    var window = createExtensionGlobal();
+    telemetryScript.runInNewContext(window);
+    window.chrome.runtime.getManifest = function() {
+     return {version: '1.0.1'};
+    };
+    window.Date.test_now_value += 12 * 36E5;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }, {
+      // The ID did not change because the browser version did not change.
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.1',
+    }]);
+  },
+
+  function test_unofficial_build() {
+    var window = createExtensionGlobal();
+    var didWarn = false;
+    window.console = {};
+    window.console.warn = function() { didWarn = true; };
+    window.chrome.runtime.id = 'abcdefghijklmnopabcdefghijklmnop';
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, []);
+    assert.ok(didWarn);
+  },
+
+  function test_fetch_is_supported() {
+    var window = createExtensionGlobal();
+    // XMLHttpRequest should not be called when fetch is available. So removing
+    // the XMLHttpRequest API should not change behavior.
+    delete window.XMLHttpRequest;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_fetch_not_supported() {
+    var window = createExtensionGlobal();
+    delete window.fetch;
+    delete window.Request;
+    delete window.Headers;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_fetch_mode_not_supported() {
+    var window = createExtensionGlobal();
+    delete window.Request.prototype.mode;
+    window.fetch = function() { throw new Error('Unexpected call to fetch!'); };
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+
+  function test_network_offline() {
+    var window = createExtensionGlobal();
+    // Simulate that the network is down for sure.
+    window.navigator.onLine = false;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, []);
+
+    // Simulate that the network might be up.
+    window.navigator.onLine = true;
+    telemetryScript.runInNewContext(window);
+    assert.deepEqual(window.test_requests, [{
+      'Deduplication-Id': '4242424242',
+      'Extension-Version': '1.0.0',
+    }]);
+  },
+];
+var test_i = 0;
+
+(function next() {
+  var test = tests[test_i++];
+  if (!test) {
+    console.log('All tests completed.');
+    return;
+  }
+  console.log('Running test ' + test_i + '/' + tests.length + ': ' + test.name);
+  test();
+  process.nextTick(next);
+})();