crash: Add private API for reporting JavaScript errors.

We add a new private API for Chrome OS component extensions to report
JavaScript errors to Crash. The API mirrors the window.onerror API.

Design doc:
https://docs.google.com/document/d/1XqN_wO1_UfVRTfhDf6yzHCyBwq_TcSw8ILDOOY_h3w4/edit?usp=sharing

Bug: 986178
Change-Id: I8e11553fd7d1c07c712c7a87a68f1c0ccace54f1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1732329
Commit-Queue: Darren Shen <[email protected]>
Reviewed-by: Giovanni Ortuño Urquidi <[email protected]>
Reviewed-by: Devlin <[email protected]>
Reviewed-by: Robert Sesek <[email protected]>
Reviewed-by: Bruce Dawson <[email protected]>
Reviewed-by: Chris Palmer <[email protected]>
Cr-Commit-Position: refs/heads/master@{#719493}
diff --git a/extensions/browser/BUILD.gn b/extensions/browser/BUILD.gn
index 263437d..ed37429 100644
--- a/extensions/browser/BUILD.gn
+++ b/extensions/browser/BUILD.gn
@@ -504,6 +504,7 @@
     sources += [
       "api/audio/audio_apitest_chromeos.cc",
       "api/cec_private/cec_private_apitest.cc",
+      "api/crash_report_private/crash_report_private_apitest.cc",
       "api/media_perception_private/media_perception_private_apitest.cc",
       "api/system_power_source/system_power_source_apitest.cc",
       "api/virtual_keyboard/virtual_keyboard_apitest.cc",
@@ -521,6 +522,7 @@
       "//chromeos/dbus/upstart",
       "//chromeos/login/login_state",
       "//chromeos/network",
+      "//components/crash/content/app:app",
     ]
   }
 }
diff --git a/extensions/browser/api/BUILD.gn b/extensions/browser/api/BUILD.gn
index d394098..3329155 100644
--- a/extensions/browser/api/BUILD.gn
+++ b/extensions/browser/api/BUILD.gn
@@ -131,6 +131,7 @@
     public_deps += [
       "//extensions/browser/api/cec_private",
       "//extensions/browser/api/clipboard",
+      "//extensions/browser/api/crash_report_private",
       "//extensions/browser/api/diagnostics",
       "//extensions/browser/api/networking_config",
       "//extensions/browser/api/system_power_source",
diff --git a/extensions/browser/api/crash_report_private/BUILD.gn b/extensions/browser/api/crash_report_private/BUILD.gn
new file mode 100644
index 0000000..da1fc86
--- /dev/null
+++ b/extensions/browser/api/crash_report_private/BUILD.gn
@@ -0,0 +1,24 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//extensions/buildflags/buildflags.gni")
+
+assert(enable_extensions,
+       "Cannot depend on extensions because enable_extensions=false.")
+
+source_set("crash_report_private") {
+  sources = [
+    "crash_report_private_api.cc",
+    "crash_report_private_api.h",
+  ]
+
+  deps = [
+    "//components/crash/content/app",
+    "//content/public/browser",
+    "//extensions/common/api",
+    "//net",
+    "//services/network:network_service",
+    "//services/network/public/cpp",
+  ]
+}
diff --git a/extensions/browser/api/crash_report_private/DEPS b/extensions/browser/api/crash_report_private/DEPS
new file mode 100644
index 0000000..392684af8
--- /dev/null
+++ b/extensions/browser/api/crash_report_private/DEPS
@@ -0,0 +1,9 @@
+include_rules = [
+  "+components/crash/content/app/client_upload_info.h",
+]
+
+specific_include_rules = {
+  "crash_report_private_apitest.cc": [
+    "+components/crash/content/app/crash_reporter_client.h",
+  ],
+}
diff --git a/extensions/browser/api/crash_report_private/OWNERS b/extensions/browser/api/crash_report_private/OWNERS
new file mode 100644
index 0000000..c7441c16
--- /dev/null
+++ b/extensions/browser/api/crash_report_private/OWNERS
@@ -0,0 +1,4 @@
[email protected]
[email protected]
+
+# COMPONENT: Platform>Apps>SystemWebApps
diff --git a/extensions/browser/api/crash_report_private/crash_report_private_api.cc b/extensions/browser/api/crash_report_private/crash_report_private_api.cc
new file mode 100644
index 0000000..9d58eb6
--- /dev/null
+++ b/extensions/browser/api/crash_report_private/crash_report_private_api.cc
@@ -0,0 +1,238 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "extensions/browser/api/crash_report_private/crash_report_private_api.h"
+
+#include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
+#include "base/system/sys_info.h"
+#include "base/task/post_task.h"
+#include "base/time/default_clock.h"
+#include "components/crash/content/app/client_upload_info.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition.h"
+#include "extensions/common/api/crash_report_private.h"
+#include "net/base/escape.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/simple_url_loader.h"
+
+namespace extensions {
+namespace api {
+
+namespace {
+
+// Used for throttling the API calls.
+base::Time g_last_called_time;
+
+base::Clock* g_clock = base::DefaultClock::GetInstance();
+
+#if defined(GOOGLE_CHROME_BUILD)
+constexpr char kCrashEndpointUrl[] = "https://clients2.google.com/cr/report";
+#else
+constexpr char kCrashEndpointUrl[] = "";
+#endif
+
+std::string& GetCrashEndpoint() {
+  static base::NoDestructor<std::string> crash_endpoint(kCrashEndpointUrl);
+  return *crash_endpoint;
+}
+
+constexpr int kCrashEndpointResponseMaxSizeInBytes = 1024;
+
+void OnRequestComplete(std::unique_ptr<network::SimpleURLLoader> url_loader,
+                       base::OnceCallback<void()> callback,
+                       std::unique_ptr<std::string> response_body) {
+  if (response_body) {
+    DVLOG(1) << "Uploaded crash report. ID: " << *response_body;
+  } else {
+    LOG(ERROR) << "Failed to upload crash report";
+  }
+  std::move(callback).Run();
+}
+
+// Sometimes, the stack trace will contain an error message as the first line,
+// which confuses the Crash server. This function deletes it if it is present.
+std::string RemoveErrorMessageFromStackTrace(const std::string& stack_trace,
+                                             const std::string& error_message) {
+  // Return the original stack trace if the error message is not present.
+  const auto error_message_index = stack_trace.find(error_message);
+  if (error_message_index == std::string::npos)
+    return stack_trace;
+
+  // If the stack trace only contains one line, then delete the whole trace.
+  const auto first_line_end_index = stack_trace.find('\n');
+  if (first_line_end_index == std::string::npos)
+    return std::string();
+
+  // Otherwise, delete the first line.
+  return stack_trace.substr(first_line_end_index + 1);
+}
+
+using ParameterMap = std::map<std::string, std::string>;
+
+std::string BuildPostRequestQueryString(const ParameterMap& params) {
+  std::vector<std::string> query_parts;
+  for (const auto& kv : params) {
+    query_parts.push_back(base::StrCat(
+        {kv.first, "=",
+         net::EscapeQueryParamValue(kv.second, /* use_plus */ false)}));
+  }
+  return base::JoinString(query_parts, "&");
+}
+
+struct PlatformInfo {
+  std::string product_name;
+  std::string version;
+  std::string channel;
+  std::string platform;
+  std::string os_version;
+};
+
+PlatformInfo GetPlatformInfo() {
+  PlatformInfo info;
+  crash_reporter::GetClientProductNameAndVersion(&info.product_name,
+                                                 &info.version, &info.channel);
+
+  int32_t os_major_version = 0;
+  int32_t os_minor_version = 0;
+  int32_t os_bugfix_version = 0;
+  base::SysInfo::OperatingSystemVersionNumbers(
+      &os_major_version, &os_minor_version, &os_bugfix_version);
+
+  info.os_version = base::StringPrintf("%d.%d.%d", os_major_version,
+                                       os_minor_version, os_bugfix_version);
+  return info;
+}
+
+void SendReport(network::mojom::URLLoaderFactory* loader_factory,
+                const GURL& url,
+                const std::string& body,
+                base::OnceCallback<void()> callback) {
+  auto resource_request = std::make_unique<network::ResourceRequest>();
+  resource_request->method = "POST";
+  resource_request->url = url;
+
+  const auto traffic_annotation =
+      net::DefineNetworkTrafficAnnotation("javascript_report_error", R"(
+      semantics {
+        sender: "JavaScript error reporter"
+        description:
+          "Chrome can send JavaScript errors that occur within built-in "
+          "component extensions. If enabled, the error message, along "
+          "with information about Chrome and the operating system."
+        trigger:
+            "A JavaScript error occurs in a Chrome component extension"
+        data:
+            "The JavaScript error message, the version and channel of Chrome, "
+            "the URL of the extension, the line and column number where the "
+            "error occurred, and a stack trace of the error."
+        destination: GOOGLE_OWNED_SERVICE
+      }
+  )");
+
+  DVLOG(1) << "Sending crash report: " << resource_request->url;
+
+  auto url_loader = network::SimpleURLLoader::Create(
+      std::move(resource_request), traffic_annotation);
+
+  if (!body.empty()) {
+    url_loader->AttachStringForUpload(body, "text/plain");
+  }
+
+  network::SimpleURLLoader* loader = url_loader.get();
+  loader->DownloadToString(
+      loader_factory,
+      base::BindOnce(&OnRequestComplete, std::move(url_loader),
+                     std::move(callback)),
+      kCrashEndpointResponseMaxSizeInBytes);
+}
+
+void ReportJavaScriptError(network::mojom::URLLoaderFactory* loader_factory,
+                           const crash_report_private::ErrorInfo& error,
+                           base::OnceCallback<void()> callback) {
+  const auto platform = GetPlatformInfo();
+
+  const GURL source(error.url);
+  const auto product = error.product ? *error.product : platform.product_name;
+  const auto version = error.version ? *error.version : platform.version;
+
+  ParameterMap params;
+  params["prod"] = net::EscapeQueryParamValue(product, /* use_plus */ false);
+  params["ver"] = net::EscapeQueryParamValue(version, /* use_plus */ false);
+  params["type"] = "JavascriptError";
+  // TODO(https://crbug.com/986178): Include |error.message| once we scrub PII.
+  params["browser"] = "Chrome";
+  params["browser_version"] = platform.version;
+  params["channel"] = platform.channel;
+  params["os"] = "ChromeOS";
+  params["os_version"] = platform.os_version;
+  params["full_url"] = source.spec();
+  params["url"] = source.path();
+  params["src"] = source.spec();
+  if (error.line_number)
+    params["line"] = *error.line_number;
+  if (error.column_number)
+    params["column"] = *error.column_number;
+
+  // The network request must be made on the UI thread.
+  const GURL url(base::StrCat(
+      {GetCrashEndpoint(), "?", BuildPostRequestQueryString(params)}));
+  const std::string body =
+      error.stack_trace
+          ? RemoveErrorMessageFromStackTrace(*error.stack_trace, error.message)
+          : "";
+
+  SendReport(loader_factory, url, body, std::move(callback));
+}
+
+}  // namespace
+
+CrashReportPrivateReportErrorFunction::CrashReportPrivateReportErrorFunction() =
+    default;
+
+CrashReportPrivateReportErrorFunction::
+    ~CrashReportPrivateReportErrorFunction() = default;
+
+ExtensionFunction::ResponseAction CrashReportPrivateReportErrorFunction::Run() {
+  // Do not report errors if the user did not give consent for crash reporting.
+  if (!crash_reporter::GetClientCollectStatsConsent())
+    return RespondNow(NoArguments());
+
+  // Ensure we don't send too many crash reports. Limit to one report per hour.
+  if (!g_last_called_time.is_null() &&
+      g_clock->Now() - g_last_called_time < base::TimeDelta::FromHours(1)) {
+    return RespondNow(Error("Too many calls to this API"));
+  }
+
+  // TODO(https://crbug.com/986166): Use crash_reporter for Chrome OS.
+  const auto params = crash_report_private::ReportError::Params::Create(*args_);
+  EXTENSION_FUNCTION_VALIDATE(params.get());
+
+  ReportJavaScriptError(
+      content::BrowserContext::GetDefaultStoragePartition(browser_context())
+          ->GetURLLoaderFactoryForBrowserProcess()
+          .get(),
+      params->info,
+      base::BindOnce(&CrashReportPrivateReportErrorFunction::OnReportComplete,
+                     this));
+
+  g_last_called_time = base::Time::Now();
+
+  return RespondLater();
+}
+
+void CrashReportPrivateReportErrorFunction::OnReportComplete() {
+  Respond(NoArguments());
+}
+
+void SetClockForTesting(base::Clock* clock) {
+  g_clock = clock;
+}
+
+void SetCrashEndpointForTesting(const std::string& endpoint) {
+  GetCrashEndpoint() = endpoint;
+}
+
+}  // namespace api
+}  // namespace extensions
diff --git a/extensions/browser/api/crash_report_private/crash_report_private_api.h b/extensions/browser/api/crash_report_private/crash_report_private_api.h
new file mode 100644
index 0000000..f91819f4
--- /dev/null
+++ b/extensions/browser/api/crash_report_private/crash_report_private_api.h
@@ -0,0 +1,43 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef EXTENSIONS_BROWSER_API_CRASH_REPORT_PRIVATE_CRASH_REPORT_PRIVATE_API_H_
+#define EXTENSIONS_BROWSER_API_CRASH_REPORT_PRIVATE_CRASH_REPORT_PRIVATE_API_H_
+
+#include <string>
+
+#include "extensions/browser/extension_function.h"
+#include "extensions/browser/extension_function_histogram_value.h"
+
+namespace base {
+class Clock;
+}
+
+namespace extensions {
+namespace api {
+
+class CrashReportPrivateReportErrorFunction : public ExtensionFunction {
+ public:
+  CrashReportPrivateReportErrorFunction();
+  DECLARE_EXTENSION_FUNCTION("crashReportPrivate.reportError",
+                             CRASHREPORTPRIVATE_REPORTERROR)
+
+ protected:
+  ~CrashReportPrivateReportErrorFunction() override;
+  ResponseAction Run() override;
+
+ private:
+  void OnReportComplete();
+
+  DISALLOW_COPY_AND_ASSIGN(CrashReportPrivateReportErrorFunction);
+};
+
+void SetClockForTesting(base::Clock* clock);
+
+void SetCrashEndpointForTesting(const std::string& endpoint);
+
+}  // namespace api
+}  // namespace extensions
+
+#endif  // EXTENSIONS_BROWSER_API_CRASH_REPORT_PRIVATE_CRASH_REPORT_PRIVATE_API_H_
diff --git a/extensions/browser/api/crash_report_private/crash_report_private_apitest.cc b/extensions/browser/api/crash_report_private/crash_report_private_apitest.cc
new file mode 100644
index 0000000..72352b4e
--- /dev/null
+++ b/extensions/browser/api/crash_report_private/crash_report_private_apitest.cc
@@ -0,0 +1,233 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/system/sys_info.h"
+#include "base/test/simple_test_clock.h"
+#include "components/crash/content/app/crash_reporter_client.h"
+#include "content/public/test/browser_task_environment.h"
+#include "extensions/browser/api/crash_report_private/crash_report_private_api.h"
+#include "extensions/browser/browsertest_util.h"
+#include "extensions/common/switches.h"
+#include "extensions/shell/test/shell_apitest.h"
+#include "extensions/test/extension_test_message_listener.h"
+#include "extensions/test/test_extension_dir.h"
+#include "net/http/http_status_code.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+
+namespace extensions {
+
+using browsertest_util::ExecuteScriptInBackgroundPage;
+
+namespace {
+
+constexpr const char* kTestExtensionId = "jjeoclcdfjddkdjokiejckgcildcflpp";
+constexpr const char* kTestCrashEndpoint = "/crash";
+
+class MockCrashReporterClient : public crash_reporter::CrashReporterClient {
+  bool GetCollectStatsConsent() override { return true; }
+  void GetProductNameAndVersion(std::string* product_name,
+                                std::string* version,
+                                std::string* channel) override {
+    *product_name = "Chrome (Chrome OS)";
+    *version = "1.2.3.4";
+    *channel = "Stable";
+  }
+};
+
+std::string GetOsVersion() {
+  int32_t os_major_version = 0;
+  int32_t os_minor_version = 0;
+  int32_t os_bugfix_version = 0;
+  base::SysInfo::OperatingSystemVersionNumbers(
+      &os_major_version, &os_minor_version, &os_bugfix_version);
+  return base::StringPrintf("%d.%d.%d", os_major_version, os_minor_version,
+                            os_bugfix_version);
+}
+
+}  // namespace
+
+class CrashReportPrivateApiTest : public ShellApiTest {
+ public:
+  CrashReportPrivateApiTest() = default;
+  ~CrashReportPrivateApiTest() override = default;
+
+  void SetUpOnMainThread() override {
+    ShellApiTest::SetUpOnMainThread();
+
+    constexpr char kKey[] =
+        "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+uU63MD6T82Ldq5wjrDFn5mGmPnnnj"
+        "WZBWxYXfpG4kVf0s+p24VkXwTXsxeI12bRm8/ft9sOq0XiLfgQEh5JrVUZqvFlaZYoS+g"
+        "iZfUqzKFGMLa4uiSMDnvv+byxrqAepKz5G8XX/q5Wm5cvpdjwgiu9z9iM768xJy+Ca/G5"
+        "qQwIDAQAB";
+    constexpr char kManifestTemplate[] =
+        R"({
+      "key": "%s",
+      "name": "chrome.crashReportPrivate basic extension tests",
+      "version": "1.0",
+      "manifest_version": 2,
+      "background": { "scripts": ["test.js"] },
+      "permissions": ["crashReportPrivate"]
+    })";
+
+    TestExtensionDir test_dir;
+    test_dir.WriteManifest(base::StringPrintf(kManifestTemplate, kKey));
+    test_dir.WriteFile(FILE_PATH_LITERAL("test.js"),
+                       R"(chrome.test.sendMessage('ready');)");
+
+    ExtensionTestMessageListener listener("ready", false);
+    extension_ = LoadExtension(test_dir.UnpackedPath());
+    EXPECT_TRUE(listener.WaitUntilSatisfied());
+
+    embedded_test_server()->RegisterRequestHandler(base::Bind(
+        &CrashReportPrivateApiTest::HandleRequest, base::Unretained(this)));
+    ASSERT_TRUE(embedded_test_server()->Start());
+
+    api::SetCrashEndpointForTesting(
+        embedded_test_server()->GetURL(kTestCrashEndpoint).spec());
+    crash_reporter::SetCrashReporterClient(&client_);
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    command_line->AppendSwitchASCII(
+        extensions::switches::kWhitelistedExtensionID, kTestExtensionId);
+    ShellApiTest::SetUpCommandLine(command_line);
+  }
+
+ protected:
+  struct Report {
+    std::string query;
+    std::string content;
+  };
+
+  const Extension* extension_;
+  Report last_report_;
+
+ private:
+  std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
+      const net::test_server::HttpRequest& request) {
+    GURL absolute_url = embedded_test_server()->GetURL(request.relative_url);
+    if (absolute_url.path() != kTestCrashEndpoint) {
+      return nullptr;
+    }
+
+    last_report_ = {absolute_url.query(), request.content};
+    auto http_response =
+        std::make_unique<net::test_server::BasicHttpResponse>();
+    http_response->set_code(net::HTTP_OK);
+    http_response->set_content("123");
+    http_response->set_content_type("text/plain");
+    return http_response;
+  }
+
+  MockCrashReporterClient client_;
+  DISALLOW_COPY_AND_ASSIGN(CrashReportPrivateApiTest);
+};
+
+IN_PROC_BROWSER_TEST_F(CrashReportPrivateApiTest, Basic) {
+  constexpr char kTestScript[] = R"(
+    chrome.crashReportPrivate.reportError({
+        message: "hi",
+        url: "http://www.test.com",
+      },
+      () => window.domAutomationController.send(""));
+  )";
+  ExecuteScriptInBackgroundPage(browser_context(), extension_->id(),
+                                kTestScript);
+
+  EXPECT_EQ(last_report_.query,
+            "browser=Chrome&browser_version=1.2.3.4&channel=Stable&"
+            "full_url=https%3A%2F%2Fblue-sea-697d.quartiers047.workers.dev%3A443%2Fhttp%2Fwww.test.com%2F&os=ChromeOS&os_version=" +
+                GetOsVersion() +
+                "&prod=Chrome%2520(Chrome%2520OS)&src=https%3A%2F%2Fblue-sea-697d.quartiers047.workers.dev%3A443%2Fhttp%2Fwww.test."
+                "com%2F&type=JavascriptError&url=%2F&ver=1.2.3.4");
+  EXPECT_EQ(last_report_.content, "");
+}
+
+IN_PROC_BROWSER_TEST_F(CrashReportPrivateApiTest, ExtraParamsAndStackTrace) {
+  constexpr char kTestScript[] = R"(
+    chrome.crashReportPrivate.reportError({
+        message: "hi",
+        url: "http://www.test.com/foo",
+        product: "TestApp",
+        version: "1.0.0.0",
+        lineNumber: 123,
+        columnNumber: 456,
+        stackTrace: "   at <anonymous>:1:1",
+      },
+      () => window.domAutomationController.send(""));
+  )";
+  ExecuteScriptInBackgroundPage(browser_context(), extension_->id(),
+                                kTestScript);
+
+  EXPECT_EQ(last_report_.query,
+            "browser=Chrome&browser_version=1.2.3.4&channel=Stable&column=%C8&"
+            "full_url=https%3A%2F%2Fblue-sea-697d.quartiers047.workers.dev%3A443%2Fhttp%2Fwww.test.com%2Ffoo&line=%7B&os=ChromeOS&"
+            "os_version=" +
+                GetOsVersion() +
+                "&prod=TestApp&src=https%3A%2F%2Fblue-sea-697d.quartiers047.workers.dev%3A443%2Fhttp%2Fwww.test.com%2Ffoo&type="
+                "JavascriptError&url=%2Ffoo&ver=1.0.0.0");
+  EXPECT_EQ(last_report_.content, "   at <anonymous>:1:1");
+}
+
+IN_PROC_BROWSER_TEST_F(CrashReportPrivateApiTest, StackTraceWithErrorMessage) {
+  constexpr char kTestScript[] = R"(
+    chrome.crashReportPrivate.reportError({
+        message: "hi",
+        url: "http://www.test.com/foo",
+        product: 'TestApp',
+        version: '1.0.0.0',
+        lineNumber: 123,
+        columnNumber: 456,
+        stackTrace: 'hi'
+      },
+      () => window.domAutomationController.send(""));
+  )";
+  ExecuteScriptInBackgroundPage(browser_context(), extension_->id(),
+                                kTestScript);
+
+  EXPECT_EQ(last_report_.query,
+            "browser=Chrome&browser_version=1.2.3.4&channel=Stable&column=%C8&"
+            "full_url=https%3A%2F%2Fblue-sea-697d.quartiers047.workers.dev%3A443%2Fhttp%2Fwww.test.com%2Ffoo&line=%7B&os=ChromeOS&"
+            "os_version=" +
+                GetOsVersion() +
+                "&prod=TestApp&src=https%3A%2F%2Fblue-sea-697d.quartiers047.workers.dev%3A443%2Fhttp%2Fwww.test.com%2Ffoo&type="
+                "JavascriptError&url=%2Ffoo&ver=1.0.0.0");
+  EXPECT_EQ(last_report_.content, "");
+}
+
+IN_PROC_BROWSER_TEST_F(CrashReportPrivateApiTest, Throttling) {
+  constexpr char kTestScript[] = R"(
+    chrome.crashReportPrivate.reportError({
+        message: "hi",
+        url: "http://www.test.com",
+      },
+      () => {
+        window.domAutomationController.send(chrome.runtime.lastError ?
+            chrome.runtime.lastError.message : "")
+      });
+  )";
+
+  base::SimpleTestClock test_clock;
+  test_clock.SetNow(base::Time::Now());
+  api::SetClockForTesting(&test_clock);
+
+  // Use an exact time for the first API call.
+  EXPECT_EQ("", ExecuteScriptInBackgroundPage(browser_context(),
+                                              extension_->id(), kTestScript));
+
+  // API is limited to one call per hr. So pretend the second call is just
+  // before 1 hr.
+  test_clock.Advance(base::TimeDelta::FromMinutes(59));
+  EXPECT_EQ("Too many calls to this API",
+            ExecuteScriptInBackgroundPage(browser_context(), extension_->id(),
+                                          kTestScript));
+
+  // Call again after 1 hr.
+  test_clock.Advance(base::TimeDelta::FromMinutes(2));
+  EXPECT_EQ("", ExecuteScriptInBackgroundPage(browser_context(),
+                                              extension_->id(), kTestScript));
+}
+
+}  // namespace extensions
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index 33318591..677c81c4 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1479,6 +1479,7 @@
   TERMINALPRIVATE_SETSETTINGS = 1416,
   WEBSTOREPRIVATE_REQUESTEXTENSION = 1417,
   AUTOTESTPRIVATE_INSTALLPLUGINVM = 1418,
+  CRASHREPORTPRIVATE_REPORTERROR = 1419,
   // Last entry: Add new entries above, then run:
   // python tools/metrics/histograms/update_extension_histograms.py
   ENUM_BOUNDARY
diff --git a/extensions/common/api/_api_features.json b/extensions/common/api/_api_features.json
index a517a5f..d9c3b34 100644
--- a/extensions/common/api/_api_features.json
+++ b/extensions/common/api/_api_features.json
@@ -142,6 +142,10 @@
   "clipboard.setImageData": {
     "dependencies": ["permission:clipboardWrite"]
   },
+  "crashReportPrivate": {
+    "dependencies": ["permission:crashReportPrivate"],
+    "contexts": ["blessed_extension"]
+  },
   "declarativeNetRequest": {
     "dependencies": ["permission:declarativeNetRequest"],
     "contexts": ["blessed_extension"]
diff --git a/extensions/common/api/_permission_features.json b/extensions/common/api/_permission_features.json
index 04a6c062..48f0789 100644
--- a/extensions/common/api/_permission_features.json
+++ b/extensions/common/api/_permission_features.json
@@ -199,6 +199,13 @@
     "extension_types": ["platform_app"],
     "platforms": ["chromeos"]
   },
+  "crashReportPrivate": {
+    "channel": "dev",
+    "extension_types": ["extension"],
+    "whitelist": [
+      "06BE211D5F014BAB34BC22D9DDA09C63A81D828E"   // http://crbug.com/946241
+    ]
+  },
   "declarativeNetRequest": {
     "channel": "beta",
     "extension_types": ["extension"],
diff --git a/extensions/common/api/crash_report_private.idl b/extensions/common/api/crash_report_private.idl
new file mode 100644
index 0000000..9833e9b
--- /dev/null
+++ b/extensions/common/api/crash_report_private.idl
@@ -0,0 +1,48 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Private API for Chrome component extensions to report errors.
+[platforms=("chromeos")]
+namespace crashReportPrivate {
+  // A dictionary containing additional context about the error.
+  dictionary ErrorInfo {
+    // The error message.
+    DOMString message;
+
+    // URL where the error occurred.
+    // Must be the full URL, containing the protocol (e.g.
+    // http://www.example.com).
+    DOMString url;
+
+    // Name of the product where the error occurred.
+    // Defaults to the product variant of Chrome that is hosting the extension.
+    // (e.g. "Chrome" or "Chrome_ChromeOS").
+    DOMString? product;
+
+    // Version of the product where the error occurred.
+    // Defaults to the version of Chrome that is hosting the extension (e.g.
+    // "73.0.3683.75").
+    DOMString? version;
+
+    // Line number where the error occurred.
+    long? lineNumber;
+
+    // Column number where the error occurred.
+    long? columnNumber;
+
+    // String containing the stack trace for the error.
+    // Defaults to the empty string.
+    DOMString? stackTrace;
+  };
+
+  // Callback for |reportError|.
+  callback ReportCallback = void ();
+
+  interface Functions {
+    // Report and upload an error to Crash.
+    // |info|: Information about the error.
+    // |callback|: Called when the error has been uploaded.
+    static void reportError(ErrorInfo info, ReportCallback callback);
+  };
+};
diff --git a/extensions/common/api/schema.gni b/extensions/common/api/schema.gni
index ac2a944..ac8db0f 100644
--- a/extensions/common/api/schema.gni
+++ b/extensions/common/api/schema.gni
@@ -63,6 +63,7 @@
 
 if (is_chromeos) {
   extensions_api_schema_files_ += [
+    "crash_report_private.idl",
     "diagnostics.idl",
     "lock_screen_data.idl",
     "media_perception_private.idl",
diff --git a/extensions/common/permissions/api_permission.h b/extensions/common/permissions/api_permission.h
index 911a31a..0b2c6ef8 100644
--- a/extensions/common/permissions/api_permission.h
+++ b/extensions/common/permissions/api_permission.h
@@ -266,6 +266,7 @@
     kLoginState = 222,
     kPrintingMetrics = 223,
     kPrinting = 224,
+    kCrashReportPrivate = 225,
     // Last entry: Add new entries above and ensure to update the
     // "ExtensionPermission3" enum in tools/metrics/histograms/enums.xml
     // (by running update_extension_permission.py).
diff --git a/extensions/common/permissions/extensions_api_permissions.cc b/extensions/common/permissions/extensions_api_permissions.cc
index 0a0ebfe..eeb5ca50 100644
--- a/extensions/common/permissions/extensions_api_permissions.cc
+++ b/extensions/common/permissions/extensions_api_permissions.cc
@@ -44,6 +44,7 @@
      APIPermissionInfo::kFlagSupportsContentCapabilities},
     {APIPermission::kClipboardWrite, "clipboardWrite",
      APIPermissionInfo::kFlagSupportsContentCapabilities},
+    {APIPermission::kCrashReportPrivate, "crashReportPrivate"},
     {APIPermission::kDeclarativeWebRequest, "declarativeWebRequest"},
     {APIPermission::kDiagnostics, "diagnostics",
      APIPermissionInfo::kFlagCannotBeOptional},
diff --git a/extensions/shell/test/shell_apitest.cc b/extensions/shell/test/shell_apitest.cc
index acbe697..e3d94a9 100644
--- a/extensions/shell/test/shell_apitest.cc
+++ b/extensions/shell/test/shell_apitest.cc
@@ -4,7 +4,6 @@
 
 #include "extensions/shell/test/shell_apitest.h"
 
-#include "base/files/file_path.h"
 #include "base/path_service.h"
 #include "base/threading/thread_restrictions.h"
 #include "content/public/browser/notification_service.h"
@@ -31,6 +30,11 @@
   return extension_system_->LoadExtension(extension_path);
 }
 
+const Extension* ShellApiTest::LoadExtension(
+    const base::FilePath& extension_path) {
+  return extension_system_->LoadExtension(extension_path);
+}
+
 const Extension* ShellApiTest::LoadApp(const std::string& app_dir) {
   base::ScopedAllowBlockingForTesting allow_blocking;
   base::FilePath test_data_dir;
diff --git a/extensions/shell/test/shell_apitest.h b/extensions/shell/test/shell_apitest.h
index 11ed054..8b0a00e 100644
--- a/extensions/shell/test/shell_apitest.h
+++ b/extensions/shell/test/shell_apitest.h
@@ -7,6 +7,7 @@
 
 #include <string>
 
+#include "base/files/file_path.h"
 #include "base/macros.h"
 #include "extensions/shell/test/shell_test.h"
 
@@ -27,6 +28,11 @@
   // |extension_dir| should be a subpath under extensions/test/data.
   const Extension* LoadExtension(const std::string& extension_dir);
 
+  // Loads an unpacked extension. Returns an instance of the extension that was
+  // just loaded.
+  // |extension_path| should be an absolute path to the extension.
+  const Extension* LoadExtension(const base::FilePath& extension_path);
+
   // Loads and launches an unpacked platform app. Returns an instance of the
   // extension that was just loaded.
   // |app_dir| should be a subpath under extensions/test/data.