diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 9831883b51c..ee874f82319 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,4 +1,5 @@
{
+ "handwritten/nodejs-error-reporting": "3.0.5",
"packages/gapic-node-processing": "0.1.6",
"packages/google-ads-admanager": "0.5.0",
"packages/google-ads-datamanager": "0.1.0",
@@ -145,11 +146,11 @@
"packages/google-cloud-saasplatform-saasservicemgmt": "0.1.1",
"packages/google-cloud-scheduler": "5.3.1",
"packages/google-cloud-secretmanager": "6.1.1",
+ "packages/google-cloud-securesourcemanager": "0.8.1",
"packages/google-cloud-security-privateca": "7.0.1",
"packages/google-cloud-security-publicca": "2.2.1",
"packages/google-cloud-securitycenter": "9.2.1",
"packages/google-cloud-securitycentermanagement": "0.7.1",
- "packages/google-cloud-securesourcemanager": "0.8.1",
"packages/google-cloud-servicedirectory": "6.1.1",
"packages/google-cloud-servicehealth": "0.7.1",
"packages/google-cloud-shell": "4.1.1",
@@ -217,4 +218,4 @@
"packages/google-streetview-publish": "0.4.1",
"packages/grafeas": "6.1.1",
"packages/typeless-sample-bot": "3.1.1"
-}
\ No newline at end of file
+}
diff --git a/handwritten/nodejs-error-reporting/.OwlBot.yaml b/handwritten/nodejs-error-reporting/.OwlBot.yaml
new file mode 100644
index 00000000000..10389756341
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.OwlBot.yaml
@@ -0,0 +1,17 @@
+# Copyright 2021 Google LLC
+#
+# 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.
+
+
+begin-after-commit-hash: 674a41e0de2869f44f45eb7b1a605852a5394bba
+
diff --git a/handwritten/nodejs-error-reporting/.compodocrc b/handwritten/nodejs-error-reporting/.compodocrc
new file mode 100644
index 00000000000..cd8b42152a6
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.compodocrc
@@ -0,0 +1,10 @@
+---
+tsconfig: ./tsconfig.json
+output: ./docs
+theme: material
+hideGenerator: true
+disablePrivate: true
+disableProtected: true
+disableInternal: true
+disableCoverage: true
+disableGraph: true
diff --git a/handwritten/nodejs-error-reporting/.eslintignore b/handwritten/nodejs-error-reporting/.eslintignore
new file mode 100644
index 00000000000..c4a0963e9bd
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.eslintignore
@@ -0,0 +1,8 @@
+**/node_modules
+**/coverage
+test/fixtures
+build/
+docs/
+protos/
+samples/generated/
+system-test/**/fixtures
diff --git a/handwritten/nodejs-error-reporting/.eslintrc.json b/handwritten/nodejs-error-reporting/.eslintrc.json
new file mode 100644
index 00000000000..78215349546
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "./node_modules/gts"
+}
diff --git a/handwritten/nodejs-error-reporting/.gitattributes b/handwritten/nodejs-error-reporting/.gitattributes
new file mode 100644
index 00000000000..33739cb74e4
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.gitattributes
@@ -0,0 +1,4 @@
+*.ts text eol=lf
+*.js text eol=lf
+protos/* linguist-generated
+**/api-extractor.json linguist-language=JSON-with-Comments
diff --git a/handwritten/nodejs-error-reporting/.gitignore b/handwritten/nodejs-error-reporting/.gitignore
new file mode 100644
index 00000000000..6f009f34ec3
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.gitignore
@@ -0,0 +1,15 @@
+**/*.log
+**/node_modules
+.coverage
+.DS_Store
+.nyc_output
+docs/
+google-cloud-error-reporting-*.tgz
+out/
+system-test/secrets.js
+system-test/*key.json
+build
+.vscode
+package-lock.json
+key.json
+__pycache__
diff --git a/handwritten/nodejs-error-reporting/.jsdoc.js b/handwritten/nodejs-error-reporting/.jsdoc.js
new file mode 100644
index 00000000000..2ec930f0992
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.jsdoc.js
@@ -0,0 +1,51 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// https://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';
+
+module.exports = {
+ opts: {
+ readme: './README.md',
+ package: './package.json',
+ template: './node_modules/jsdoc-fresh',
+ recurse: true,
+ verbose: true,
+ destination: './docs/'
+ },
+ plugins: [
+ 'plugins/markdown',
+ 'jsdoc-region-tag'
+ ],
+ source: {
+ excludePattern: '(^|\\/|\\\\)[._]',
+ include: [
+ 'src'
+ ],
+ includePattern: '\\.js$'
+ },
+ templates: {
+ copyright: 'Copyright 2019 Google, LLC.',
+ includeDate: false,
+ sourceFiles: false,
+ systemName: '@google-cloud/error-reporting',
+ theme: 'lumen',
+ default: {
+ "outputSourceFiles": false
+ }
+ },
+ markdown: {
+ idInHeadings: true
+ }
+};
diff --git a/handwritten/nodejs-error-reporting/.mocharc.js b/handwritten/nodejs-error-reporting/.mocharc.js
new file mode 100644
index 00000000000..2431859019f
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.mocharc.js
@@ -0,0 +1,29 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+const config = {
+ "enable-source-maps": true,
+ "throw-deprecation": true,
+ "timeout": 10000,
+ "recursive": true
+}
+if (process.env.MOCHA_THROW_DEPRECATION === 'false') {
+ delete config['throw-deprecation'];
+}
+if (process.env.MOCHA_REPORTER) {
+ config.reporter = process.env.MOCHA_REPORTER;
+}
+if (process.env.MOCHA_REPORTER_OUTPUT) {
+ config['reporter-option'] = `output=${process.env.MOCHA_REPORTER_OUTPUT}`;
+}
+module.exports = config
diff --git a/handwritten/nodejs-error-reporting/.nycrc b/handwritten/nodejs-error-reporting/.nycrc
new file mode 100644
index 00000000000..b18d5472b62
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.nycrc
@@ -0,0 +1,24 @@
+{
+ "report-dir": "./.coverage",
+ "reporter": ["text", "lcov"],
+ "exclude": [
+ "**/*-test",
+ "**/.coverage",
+ "**/apis",
+ "**/benchmark",
+ "**/conformance",
+ "**/docs",
+ "**/samples",
+ "**/scripts",
+ "**/protos",
+ "**/test",
+ "**/*.d.ts",
+ ".jsdoc.js",
+ "**/.jsdoc.js",
+ "karma.conf.js",
+ "webpack-tests.config.js",
+ "webpack.config.js"
+ ],
+ "exclude-after-remap": false,
+ "all": true
+}
diff --git a/handwritten/nodejs-error-reporting/.prettierignore b/handwritten/nodejs-error-reporting/.prettierignore
new file mode 100644
index 00000000000..9340ad9b86d
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.prettierignore
@@ -0,0 +1,6 @@
+**/node_modules
+**/coverage
+test/fixtures
+build/
+docs/
+protos/
diff --git a/handwritten/nodejs-error-reporting/.prettierrc.js b/handwritten/nodejs-error-reporting/.prettierrc.js
new file mode 100644
index 00000000000..d2eddc2ed89
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.prettierrc.js
@@ -0,0 +1,17 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// https://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.
+
+module.exports = {
+ ...require('gts/.prettierrc.json')
+}
diff --git a/handwritten/nodejs-error-reporting/.readme-partials.yaml b/handwritten/nodejs-error-reporting/.readme-partials.yaml
new file mode 100644
index 00000000000..b158b6364e9
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.readme-partials.yaml
@@ -0,0 +1,35 @@
+introduction: |-
+ > Node.js idiomatic client for [Error Reporting][product-docs].
+
+ [Error Reporting](https://cloud.google.com/error-reporting/docs/) aggregates and displays errors produced in your running cloud services.
+
+body: |-
+ This module provides custom Error Reporting support for Node.js applications.
+ [Error Reporting](https://cloud.google.com/error-reporting/) is a feature of
+ Google Cloud Platform that allows in-depth monitoring and viewing of errors reported by
+ applications running in almost any environment.
+
+ However, note that [@google-cloud/logging-winston](https://github.com/googleapis/nodejs-logging-winston) and [@google-cloud/logging-bunyan](https://github.com/googleapis/nodejs-logging-bunyan) automatically integrate with the Error Reporting service for Error objects logged at severity `error` or higher, for applications running on Google Cloud Platform.
+
+ Thus, if you are already using Winston or Bunyan in your application, and don't need custom error reporting capabilities, you do not need to use the `@google-cloud/error-reporting` library directly to report errors to the Error Reporting Console.
+
+ 
+
+ # When Errors Are Reported
+
+ The `reportMode` configuration option is used to specify when errors are reported to the Error Reporting Console. It can have one of three values:
+ * `'production'` (default): Only report errors if the NODE_ENV environment variable is set to "production".
+ * `'always'`: Always report errors regardless of the value of NODE_ENV.
+ * `'never'`: Never report errors regardless of the value of NODE_ENV.
+
+ The `reportMode` configuration option replaces the deprecated `ignoreEnvironmentCheck` configuration option. If both the `reportMode` and `ignoreEnvironmentCheck` options are specified, the `reportMode` configuration option takes precedence.
+
+ The `ignoreEnvironmentCheck` option should not be used. However, if it is used, and the `reportMode` option is not specified, it can have the values:
+ * `false` (default): Only report errors if the NODE_ENV environment variable is set to "production".
+ * `true`: Always report errors regardless of the value of NODE_ENV.
+
+ ## Setup, Configuration, and Examples
+
+ See the documentation for setup instructions, configuration options, and examples: https://cloud.google.com/error-reporting/docs/setup/nodejs
+
+ Additional code samples can also be found here: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/main/error-reporting
diff --git a/handwritten/nodejs-error-reporting/.repo-metadata.json b/handwritten/nodejs-error-reporting/.repo-metadata.json
new file mode 100644
index 00000000000..cfb9605591f
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/.repo-metadata.json
@@ -0,0 +1,15 @@
+{
+ "name": "error-reporting",
+ "name_pretty": "Error Reporting",
+ "product_documentation": "https://cloud.google.com/error-reporting",
+ "client_documentation": "https://cloud.google.com/nodejs/docs/reference/error-reporting/latest",
+ "issue_tracker": "https://issuetracker.google.com/savedsearches/559780",
+ "release_level": "stable",
+ "language": "nodejs",
+ "repo": "googleapis/google-cloud-node",
+ "distribution_name": "@google-cloud/error-reporting",
+ "api_id": "clouderrorreporting.googleapis.com",
+ "codeowner_team": "@googleapis/yoshi-nodejs",
+ "api_shortname": "error-reporting",
+ "library_type": "REST"
+}
diff --git a/handwritten/nodejs-error-reporting/CHANGELOG.md b/handwritten/nodejs-error-reporting/CHANGELOG.md
new file mode 100644
index 00000000000..950a97e6157
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/CHANGELOG.md
@@ -0,0 +1,350 @@
+# Changelog
+
+[npm history][1]
+
+[1]: https://www.npmjs.com/package/@google-cloud/error-reporting?activeTab=versions
+
+## [3.0.5](https://github.com/googleapis/nodejs-error-reporting/compare/v3.0.4...v3.0.5) (2022-12-02)
+
+
+### Bug Fixes
+
+* Add a partner team as approvers for PRs ([#686](https://github.com/googleapis/nodejs-error-reporting/issues/686)) ([18d2fed](https://github.com/googleapis/nodejs-error-reporting/commit/18d2fed84b4adc06274603a3fbae130313f9aa46))
+
+## [3.0.4](https://github.com/googleapis/nodejs-error-reporting/compare/v3.0.3...v3.0.4) (2022-10-13)
+
+
+### Bug Fixes
+
+* Do not try and authenticate when error reporting is disabled ([#676](https://github.com/googleapis/nodejs-error-reporting/issues/676)) ([c9cef5a](https://github.com/googleapis/nodejs-error-reporting/commit/c9cef5afa32f18175a41a7994a5813b451214ce8))
+
+## [3.0.3](https://github.com/googleapis/nodejs-error-reporting/compare/v3.0.2...v3.0.3) (2022-08-28)
+
+
+### Bug Fixes
+
+* remove pip install statements ([#1546](https://github.com/googleapis/nodejs-error-reporting/issues/1546)) ([#653](https://github.com/googleapis/nodejs-error-reporting/issues/653)) ([2793504](https://github.com/googleapis/nodejs-error-reporting/commit/2793504f9ef2d8f209bd4db64dd1c3170c660291))
+
+## [3.0.2](https://github.com/googleapis/nodejs-error-reporting/compare/v3.0.1...v3.0.2) (2022-08-24)
+
+
+### Bug Fixes
+
+* add hashes to requirements.txt ([#1544](https://github.com/googleapis/nodejs-error-reporting/issues/1544)) ([#654](https://github.com/googleapis/nodejs-error-reporting/issues/654)) ([c0af3a2](https://github.com/googleapis/nodejs-error-reporting/commit/c0af3a2f0e009b3889901da518a78c520d8bee45))
+
+## [3.0.1](https://github.com/googleapis/nodejs-error-reporting/compare/v3.0.0...v3.0.1) (2022-06-09)
+
+
+### Bug Fixes
+
+* **deps:** update dependency @google-cloud/common to v4 ([#648](https://github.com/googleapis/nodejs-error-reporting/issues/648)) ([8572ecb](https://github.com/googleapis/nodejs-error-reporting/commit/8572ecb884009002475d745af746c8ee7b805bcb))
+
+## [3.0.0](https://github.com/googleapis/nodejs-error-reporting/compare/v2.0.5...v3.0.0) (2022-05-20)
+
+
+### ⚠ BREAKING CHANGES
+
+* promote to stable (#646)
+* update library to use Node 12 (#644)
+
+### Features
+
+* promote to stable ([#646](https://github.com/googleapis/nodejs-error-reporting/issues/646)) ([da4c8cd](https://github.com/googleapis/nodejs-error-reporting/commit/da4c8cd411c568fc7c37dce71ed1090ea2279562))
+
+
+### Build System
+
+* update library to use Node 12 ([#644](https://github.com/googleapis/nodejs-error-reporting/issues/644)) ([a583341](https://github.com/googleapis/nodejs-error-reporting/commit/a583341c5b92848eb9db193aeca8af64c6d6c3ff))
+
+### [2.0.5](https://github.com/googleapis/nodejs-error-reporting/compare/v2.0.4...v2.0.5) (2022-04-21)
+
+
+### Bug Fixes
+
+* Reenable staleness bot ([#632](https://github.com/googleapis/nodejs-error-reporting/issues/632)) ([b56836e](https://github.com/googleapis/nodejs-error-reporting/commit/b56836e8b16b79ae7969096e2166efce3125fc89))
+
+### [2.0.4](https://www.github.com/googleapis/nodejs-error-reporting/compare/v2.0.3...v2.0.4) (2021-10-04)
+
+
+### Bug Fixes
+
+* export ErrorMessage as part of the package ([#604](https://www.github.com/googleapis/nodejs-error-reporting/issues/604)) ([eeb0218](https://www.github.com/googleapis/nodejs-error-reporting/commit/eeb02180691260960438b1fbb42425843aa2d677)), closes [#584](https://www.github.com/googleapis/nodejs-error-reporting/issues/584)
+
+### [2.0.3](https://www.github.com/googleapis/nodejs-error-reporting/compare/v2.0.2...v2.0.3) (2021-08-26)
+
+
+### Bug Fixes
+
+* **build:** migrate to using main branch ([#593](https://www.github.com/googleapis/nodejs-error-reporting/issues/593)) ([5e37253](https://www.github.com/googleapis/nodejs-error-reporting/commit/5e37253b82f148719afd534aa48d7643810d33b5))
+
+### [2.0.2](https://www.github.com/googleapis/nodejs-error-reporting/compare/v2.0.1...v2.0.2) (2021-05-12)
+
+
+### Miscellaneous Chores
+
+* release 2.0.2 ([#570](https://www.github.com/googleapis/nodejs-error-reporting/issues/570)) ([7733375](https://www.github.com/googleapis/nodejs-error-reporting/commit/7733375a7686e51b4b6bf96aa0f8999a4d012b36))
+
+### [2.0.1](https://www.github.com/googleapis/nodejs-error-reporting/compare/v2.0.0...v2.0.1) (2021-01-14)
+
+
+### Bug Fixes
+
+* convert AdditionalMessage param into string type CustomMessage ([#535](https://www.github.com/googleapis/nodejs-error-reporting/issues/535)) ([ba7d8b0](https://www.github.com/googleapis/nodejs-error-reporting/commit/ba7d8b01b6351354c88a675bfe55910e7a2c0eff))
+
+## [2.0.0](https://www.github.com/googleapis/nodejs-error-reporting/compare/v1.1.3...v2.0.0) (2020-05-27)
+
+
+### ⚠ BREAKING CHANGES
+
+* update to latest version of gts and typescript (#467)
+* require node 10 in engines field (#465)
+
+### Features
+
+* require node 10 in engines field ([#465](https://www.github.com/googleapis/nodejs-error-reporting/issues/465)) ([f7d1164](https://www.github.com/googleapis/nodejs-error-reporting/commit/f7d11649329cd33aa992f156c0379f457758cd45))
+
+
+### Bug Fixes
+
+* apache license URL ([#468](https://www.github.com/googleapis/nodejs-error-reporting/issues/468)) ([#463](https://www.github.com/googleapis/nodejs-error-reporting/issues/463)) ([191326c](https://www.github.com/googleapis/nodejs-error-reporting/commit/191326c7e29197eb3f09d68d2a89d72e0229f3b4))
+* **deps:** update dependency @google-cloud/common to v3 ([#459](https://www.github.com/googleapis/nodejs-error-reporting/issues/459)) ([0ce41a8](https://www.github.com/googleapis/nodejs-error-reporting/commit/0ce41a854ed4bc2971b535b86efda5593dc2d232))
+* **typescript:** cast Object.assign value to ServiceOptions ([#483](https://www.github.com/googleapis/nodejs-error-reporting/issues/483)) ([cfee918](https://www.github.com/googleapis/nodejs-error-reporting/commit/cfee9187fdb553bca5e290c6b17e42a57ce5a291))
+
+
+### Build System
+
+* update to latest version of gts and typescript ([#467](https://www.github.com/googleapis/nodejs-error-reporting/issues/467)) ([454f76b](https://www.github.com/googleapis/nodejs-error-reporting/commit/454f76b33abc5775af64289e41b83e460f6bc519))
+
+### [1.1.3](https://www.github.com/googleapis/nodejs-error-reporting/compare/v1.1.2...v1.1.3) (2019-12-05)
+
+
+### Bug Fixes
+
+* **deps:** pin TypeScript below 3.7.0 ([e54ef75](https://www.github.com/googleapis/nodejs-error-reporting/commit/e54ef75df1aced12b862bf0688ddb02a28b87c81))
+* **docs:** snippets are now replaced in jsdoc comments ([#411](https://www.github.com/googleapis/nodejs-error-reporting/issues/411)) ([edd884f](https://www.github.com/googleapis/nodejs-error-reporting/commit/edd884fdb81fb6d4434f8487f8040fd9f463869e))
+
+### [1.1.2](https://www.github.com/googleapis/nodejs-error-reporting/compare/v1.1.1...v1.1.2) (2019-09-26)
+
+
+### Bug Fixes
+
+* **docs:** remove anchor from reference doc link ([#388](https://www.github.com/googleapis/nodejs-error-reporting/issues/388)) ([c7d5d22](https://www.github.com/googleapis/nodejs-error-reporting/commit/c7d5d22))
+
+### [1.1.1](https://www.github.com/googleapis/nodejs-error-reporting/compare/v1.1.0...v1.1.1) (2019-06-26)
+
+
+### Bug Fixes
+
+* **docs:** link to reference docs section on googleapis.dev ([#381](https://www.github.com/googleapis/nodejs-error-reporting/issues/381)) ([d54aa8f](https://www.github.com/googleapis/nodejs-error-reporting/commit/d54aa8f))
+
+## [1.1.0](https://www.github.com/googleapis/nodejs-error-reporting/compare/v1.0.0...v1.1.0) (2019-06-20)
+
+
+### Bug Fixes
+
+* bump min required versions and fix package scripts ([#376](https://www.github.com/googleapis/nodejs-error-reporting/issues/376)) ([eb3ae66](https://www.github.com/googleapis/nodejs-error-reporting/commit/eb3ae66))
+
+
+### Features
+
+* automatically use service and revision name in Knative environments ([#375](https://www.github.com/googleapis/nodejs-error-reporting/issues/375)) ([453bd6e](https://www.github.com/googleapis/nodejs-error-reporting/commit/453bd6e)), closes [#370](https://www.github.com/googleapis/nodejs-error-reporting/issues/370)
+
+## [1.0.0](https://www.github.com/googleapis/nodejs-error-reporting/compare/v0.6.3...v1.0.0) (2019-05-17)
+
+
+### Bug Fixes
+
+* **deps:** update dependency @google-cloud/common to v1 ([#357](https://www.github.com/googleapis/nodejs-error-reporting/issues/357)) ([1e928e8](https://www.github.com/googleapis/nodejs-error-reporting/commit/1e928e8))
+
+
+### Build System
+
+* upgrade engines field to >=8.10.0 ([#349](https://www.github.com/googleapis/nodejs-error-reporting/issues/349)) ([1ab75f2](https://www.github.com/googleapis/nodejs-error-reporting/commit/1ab75f2))
+
+
+### BREAKING CHANGES
+
+* upgrade engines field to >=8.10.0 (#349)
+
+## v0.6.3
+
+04-11-2019 11:37 PDT
+
+### Bug Fixes
+
+- fix: update Koa2 plugin to await next ([#339](https://github.com/googleapis/nodejs-error-reporting/pull/339))
+
+## v0.6.2
+
+04-09-2019 11:14 PDT
+
+### Bug Fixes
+
+- fix: in Koa2 interface await `next` as a function ([#336](https://github.com/googleapis/nodejs-error-reporting/pull/336))
+
+### CI/CD
+
+- build: use per-repo publish token ([#327](https://github.com/googleapis/nodejs-error-reporting/pull/327))
+- chore: publish to npm using wombat ([#328](https://github.com/googleapis/nodejs-error-reporting/pull/328))
+
+### Dependencies
+
+- fix(deps): update dependency @google-cloud/common to ^0.32.0 ([#334](https://github.com/googleapis/nodejs-error-reporting/pull/334))
+- chore(deps): update dependency typescript to ~3.4.0
+
+### Internal / Testing Changes
+
+- chore: test Restify 18 in the install tests ([#324](https://github.com/googleapis/nodejs-error-reporting/pull/324))
+
+## v0.6.1
+
+03-13-2019 16:21 PDT
+
+### Bug Fixes
+- fix: properly handle hapi v16+ req.url ([#311](https://github.com/googleapis/nodejs-error-reporting/pull/311))
+
+### Dependencies
+- fix(deps): update dependency @google-cloud/common to ^0.31.0 ([#304](https://github.com/googleapis/nodejs-error-reporting/pull/304))
+
+### Documentation
+- docs: document how to get async stack traces ([#314](https://github.com/googleapis/nodejs-error-reporting/pull/314))
+- docs: update links in contrib guide ([#316](https://github.com/googleapis/nodejs-error-reporting/pull/316))
+- docs: add custom documentation to the README ([#313](https://github.com/googleapis/nodejs-error-reporting/pull/313))
+- docs: update contributing path in README ([#307](https://github.com/googleapis/nodejs-error-reporting/pull/307))
+- docs: move CONTRIBUTING.md to root ([#306](https://github.com/googleapis/nodejs-error-reporting/pull/306))
+- docs: add lint/fix example to contributing guide ([#303](https://github.com/googleapis/nodejs-error-reporting/pull/303))
+
+### Internal / Testing Changes
+- build: Add docuploader credentials to node publish jobs ([#322](https://github.com/googleapis/nodejs-error-reporting/pull/322))
+- build: use node10 to run samples-test, system-test etc ([#321](https://github.com/googleapis/nodejs-error-reporting/pull/321))
+- build: update release configuration
+- chore(deps): update dependency restify to v8 ([#318](https://github.com/googleapis/nodejs-error-reporting/pull/318))
+- chore(deps): update dependency mocha to v6
+- build: use linkinator for docs test ([#315](https://github.com/googleapis/nodejs-error-reporting/pull/315))
+- fix(deps): update dependency yargs to v13 ([#312](https://github.com/googleapis/nodejs-error-reporting/pull/312))
+- refactor: cleanup types for reportManualError ([#310](https://github.com/googleapis/nodejs-error-reporting/pull/310))
+- build: create docs test npm scripts ([#309](https://github.com/googleapis/nodejs-error-reporting/pull/309))
+- build: test using @grpc/grpc-js in CI ([#308](https://github.com/googleapis/nodejs-error-reporting/pull/308))
+- build: check for 404s when generating docs ([#301](https://github.com/googleapis/nodejs-error-reporting/pull/301))
+- chore(deps): update dependency eslint-config-prettier to v4 ([#300](https://github.com/googleapis/nodejs-error-reporting/pull/300))
+- chore(deps): update dependency @types/hapi to v18 ([#297](https://github.com/googleapis/nodejs-error-reporting/pull/297))
+
+## v0.6.0
+
+01-22-2019 09:59 PST
+
+### New Features
+- feat: add a new `reportMode` configuration option ([#295](https://github.com/googleapis/nodejs-error-reporting/pull/295))
+
+## v0.5.2
+
+12-20-2018 11:49 PST
+
+### Implementation Changes
+
+- fix: improve error item message for plain objects ([#286](https://github.com/googleapis/nodejs-error-reporting/pull/286))
+
+### Dependencies
+
+- chore(deps): update dependency typescript to ~3.2.0 ([#266](https://github.com/googleapis/nodejs-error-reporting/pull/266))
+- fix(deps): update dependency @google-cloud/common to ^0.27.0 ([#264](https://github.com/googleapis/nodejs-error-reporting/pull/264))
+- chore(deps): update dependency gts to ^0.9.0 ([#255](https://github.com/googleapis/nodejs-error-reporting/pull/255))
+- chore(deps): update dependency @google-cloud/nodejs-repo-tools to v3 ([#250](https://github.com/googleapis/nodejs-error-reporting/pull/250))
+- chore(deps): update dependency @types/is to v0.0.21 ([#247](https://github.com/googleapis/nodejs-error-reporting/pull/247))
+- fix(deps): update dependency @google-cloud/common to ^0.26.0 ([#230](https://github.com/googleapis/nodejs-error-reporting/pull/230))
+- chore(deps): update dependency eslint-plugin-node to v8 ([#235](https://github.com/googleapis/nodejs-error-reporting/pull/235))
+- chore(deps): update dependency typescript to ~3.1.0 ([#218](https://github.com/googleapis/nodejs-error-reporting/pull/218))
+- chore(deps): update dependency sinon to v7 ([#223](https://github.com/googleapis/nodejs-error-reporting/pull/223))
+- chore(deps): update dependency eslint-plugin-prettier to v3 ([#219](https://github.com/googleapis/nodejs-error-reporting/pull/219))
+- chore(deps): update dependency @types/glob to v7 ([#212](https://github.com/googleapis/nodejs-error-reporting/pull/212))
+- fix(deps): update dependency @google-cloud/common to ^0.25.0 ([#210](https://github.com/googleapis/nodejs-error-reporting/pull/210))
+- chore(deps): update dependency nock to v10 ([#208](https://github.com/googleapis/nodejs-error-reporting/pull/208))
+- fix(deps): update dependency @google-cloud/common to ^0.24.0 ([#206](https://github.com/googleapis/nodejs-error-reporting/pull/206))
+- fix(deps): update dependency @google-cloud/common to ^0.23.0 ([#198](https://github.com/googleapis/nodejs-error-reporting/pull/198))
+- chore(deps): update dependency nyc to v13 ([#200](https://github.com/googleapis/nodejs-error-reporting/pull/200))
+- chore(deps): update dependency eslint-config-prettier to v3 ([#195](https://github.com/googleapis/nodejs-error-reporting/pull/195))
+- chore(deps): update dependency pify to v4 ([#194](https://github.com/googleapis/nodejs-error-reporting/pull/194))
+- fix(deps): update dependency @google-cloud/common to ^0.21.0 ([#192](https://github.com/googleapis/nodejs-error-reporting/pull/192))
+- chore(deps): lock file maintenance ([#191](https://github.com/googleapis/nodejs-error-reporting/pull/191))
+- chore(deps): lock file maintenance ([#181](https://github.com/googleapis/nodejs-error-reporting/pull/181))
+- chore(deps): update dependency typescript to v3 ([#180](https://github.com/googleapis/nodejs-error-reporting/pull/180))
+- chore(deps): lock file maintenance ([#175](https://github.com/googleapis/nodejs-error-reporting/pull/175))
+- chore(deps): lock file maintenance ([#174](https://github.com/googleapis/nodejs-error-reporting/pull/174))
+- chore(deps): lock file maintenance ([#173](https://github.com/googleapis/nodejs-error-reporting/pull/173))
+- chore(deps): lock file maintenance ([#172](https://github.com/googleapis/nodejs-error-reporting/pull/172))
+- chore(deps): update dependency eslint-plugin-node to v7 ([#170](https://github.com/googleapis/nodejs-error-reporting/pull/170))
+- chore(deps): lock file maintenance ([#169](https://github.com/googleapis/nodejs-error-reporting/pull/169))
+- chore(deps): update dependency gts to ^0.8.0 ([#167](https://github.com/googleapis/nodejs-error-reporting/pull/167))
+- chore(deps): lock file maintenance ([#165](https://github.com/googleapis/nodejs-error-reporting/pull/165))
+- chore(deps): lock file maintenance ([#164](https://github.com/googleapis/nodejs-error-reporting/pull/164))
+- chore(deps): lock file maintenance ([#163](https://github.com/googleapis/nodejs-error-reporting/pull/163))
+- chore(deps): lock file maintenance ([#160](https://github.com/googleapis/nodejs-error-reporting/pull/160))
+
+
+### Documentation
+
+- docs: update readme badges ([#269](https://github.com/googleapis/nodejs-error-reporting/pull/269))
+
+### Internal / Testing Changes
+
+- chore: increase the system test delay ([#291](https://github.com/googleapis/nodejs-error-reporting/pull/291))
+- chore: fix test config to include skipped tests ([#287](https://github.com/googleapis/nodejs-error-reporting/pull/287))
+- chore: change sort order for retrieving err items ([#289](https://github.com/googleapis/nodejs-error-reporting/pull/289))
+- chore(build): inject yoshi automation key ([#285](https://github.com/googleapis/nodejs-error-reporting/pull/285))
+- chore: update nyc and eslint configs ([#284](https://github.com/googleapis/nodejs-error-reporting/pull/284))
+- chore: fix publish.sh permission +x ([#282](https://github.com/googleapis/nodejs-error-reporting/pull/282))
+- fix(build): fix Kokoro release script ([#281](https://github.com/googleapis/nodejs-error-reporting/pull/281))
+- build: add Kokoro configs for autorelease ([#280](https://github.com/googleapis/nodejs-error-reporting/pull/280))
+- chore: address system test flakiness ([#275](https://github.com/googleapis/nodejs-error-reporting/pull/275))
+- chore: always nyc report before calling codecov ([#277](https://github.com/googleapis/nodejs-error-reporting/pull/277))
+- chore: nyc ignore build/test by default ([#276](https://github.com/googleapis/nodejs-error-reporting/pull/276))
+- chore: clean up usage of prettier and eslint ([#274](https://github.com/googleapis/nodejs-error-reporting/pull/274))
+- chore: update system tests key ([#272](https://github.com/googleapis/nodejs-error-reporting/pull/272))
+- chore: update license file ([#271](https://github.com/googleapis/nodejs-error-reporting/pull/271))
+- fix(build): fix system key decryption ([#267](https://github.com/googleapis/nodejs-error-reporting/pull/267))
+- chore: update key for system tests ([#265](https://github.com/googleapis/nodejs-error-reporting/pull/265))
+- refactor(samples): convert sample tests from ava to mocha ([#257](https://github.com/googleapis/nodejs-error-reporting/pull/257))
+- Update region tags in samples ([#259](https://github.com/googleapis/nodejs-error-reporting/pull/259))
+- chore: add a synth.metadata
+- fix: sys tests use async/await to allow a fix ([#253](https://github.com/googleapis/nodejs-error-reporting/pull/253))
+- chore: update eslintignore config ([#254](https://github.com/googleapis/nodejs-error-reporting/pull/254))
+- refactor: remove unused deps and simplify ([#248](https://github.com/googleapis/nodejs-error-reporting/pull/248))
+- chore: drop contributors from multiple places ([#249](https://github.com/googleapis/nodejs-error-reporting/pull/249))
+- chore: use latest npm on Windows ([#246](https://github.com/googleapis/nodejs-error-reporting/pull/246))
+- chore: increase system test timeout ([#245](https://github.com/googleapis/nodejs-error-reporting/pull/245))
+- chore: update CircleCI config ([#244](https://github.com/googleapis/nodejs-error-reporting/pull/244))
+- chore: include build in eslintignore ([#241](https://github.com/googleapis/nodejs-error-reporting/pull/241))
+- chore: update issue templates ([#234](https://github.com/googleapis/nodejs-error-reporting/pull/234))
+- chore: remove old issue template ([#232](https://github.com/googleapis/nodejs-error-reporting/pull/232))
+- build: run tests on node11 ([#231](https://github.com/googleapis/nodejs-error-reporting/pull/231))
+- chores(build): do not collect sponge.xml from windows builds ([#229](https://github.com/googleapis/nodejs-error-reporting/pull/229))
+- chore: update nock path in system tests ([#216](https://github.com/googleapis/nodejs-error-reporting/pull/216))
+- chores(build): run codecov on continuous builds ([#228](https://github.com/googleapis/nodejs-error-reporting/pull/228))
+- chore: update new issue template ([#227](https://github.com/googleapis/nodejs-error-reporting/pull/227))
+- build: fix codecov uploading on Kokoro ([#224](https://github.com/googleapis/nodejs-error-reporting/pull/224))
+- Update kokoro config ([#220](https://github.com/googleapis/nodejs-error-reporting/pull/220))
+- Don't publish sourcemaps ([#217](https://github.com/googleapis/nodejs-error-reporting/pull/217))
+- test: remove appveyor config ([#215](https://github.com/googleapis/nodejs-error-reporting/pull/215))
+- Enable prefer-const in the eslint config ([#211](https://github.com/googleapis/nodejs-error-reporting/pull/211))
+- Enable no-var in eslint ([#209](https://github.com/googleapis/nodejs-error-reporting/pull/209))
+- Update CI config ([#207](https://github.com/googleapis/nodejs-error-reporting/pull/207))
+- Add a synth file and update CI ([#204](https://github.com/googleapis/nodejs-error-reporting/pull/204))
+- Retry npm install in CI ([#203](https://github.com/googleapis/nodejs-error-reporting/pull/203))
+- feat: use small HTTP dependency ([#201](https://github.com/googleapis/nodejs-error-reporting/pull/201))
+- chore: assert.deepEqual => assert.deepStrictEqual ([#179](https://github.com/googleapis/nodejs-error-reporting/pull/179))
+- test: fix a node 10 test failure ([#199](https://github.com/googleapis/nodejs-error-reporting/pull/199))
+- chore: ignore package-lock.json ([#193](https://github.com/googleapis/nodejs-error-reporting/pull/193))
+- feat: add Koa2 support ([#117](https://github.com/googleapis/nodejs-error-reporting/pull/117))
+- chore: fix sys test failure caused by a type error ([#188](https://github.com/googleapis/nodejs-error-reporting/pull/188))
+- chore: update renovate config ([#189](https://github.com/googleapis/nodejs-error-reporting/pull/189))
+- chore: do not target `es5` ([#187](https://github.com/googleapis/nodejs-error-reporting/pull/187))
+- chore: fix `lodash.has` usage ([#185](https://github.com/googleapis/nodejs-error-reporting/pull/185))
+- chore: delete an unused file ([#184](https://github.com/googleapis/nodejs-error-reporting/pull/184))
+- fix: fix installation tests ([#183](https://github.com/googleapis/nodejs-error-reporting/pull/183))
+- chore: move mocha options to mocha.opts ([#177](https://github.com/googleapis/nodejs-error-reporting/pull/177))
+- chore: require node 8 for samples ([#178](https://github.com/googleapis/nodejs-error-reporting/pull/178))
+- chore: switch to console-log-level for logging ([#176](https://github.com/googleapis/nodejs-error-reporting/pull/176))
+- test: use strictEqual in tests ([#171](https://github.com/googleapis/nodejs-error-reporting/pull/171))
+- chore: use post-install-check ([#166](https://github.com/googleapis/nodejs-error-reporting/pull/166))
+- test: fix system tests ([#162](https://github.com/googleapis/nodejs-error-reporting/pull/162))
+- fix: drop support for nodejs 9.x ([#161](https://github.com/googleapis/nodejs-error-reporting/pull/161))
diff --git a/handwritten/nodejs-error-reporting/CODE_OF_CONDUCT.md b/handwritten/nodejs-error-reporting/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000000..2add2547a81
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/CODE_OF_CONDUCT.md
@@ -0,0 +1,94 @@
+
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to *googleapis-stewards@google.com*, the
+Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out to the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
\ No newline at end of file
diff --git a/handwritten/nodejs-error-reporting/CONTRIBUTING.md b/handwritten/nodejs-error-reporting/CONTRIBUTING.md
new file mode 100644
index 00000000000..2227810444e
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/CONTRIBUTING.md
@@ -0,0 +1,75 @@
+# How to become a contributor and submit your own code
+
+**Table of contents**
+
+* [Contributor License Agreements](#contributor-license-agreements)
+* [Contributing a patch](#contributing-a-patch)
+* [Running the tests](#running-the-tests)
+* [Releasing the library](#releasing-the-library)
+
+## Contributor License Agreements
+
+We'd love to accept your sample apps and patches! Before we can take them, we
+have to jump a couple of legal hurdles.
+
+Please fill out either the individual or corporate Contributor License Agreement
+(CLA).
+
+ * If you are an individual writing original source code and you're sure you
+ own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual).
+ * If you work for a company that wants to allow you to contribute your work,
+ then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate).
+
+Follow either of the two links above to access the appropriate CLA and
+instructions for how to sign and return it. Once we receive it, we'll be able to
+accept your pull requests.
+
+## Contributing A Patch
+
+1. Submit an issue describing your proposed change to the repo in question.
+1. The repo owner will respond to your issue promptly.
+1. If your proposed change is accepted, and you haven't already done so, sign a
+ Contributor License Agreement (see details above).
+1. Fork the desired repo, develop and test your code changes.
+1. Ensure that your code adheres to the existing style in the code to which
+ you are contributing.
+1. Ensure that your code has an appropriate set of tests which all pass.
+1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling.
+1. Submit a pull request.
+
+### Before you begin
+
+1. [Select or create a Cloud Platform project][projects].
+1. [Enable the Error Reporting API][enable_api].
+1. [Set up authentication with a service account][auth] so you can access the
+ API from your local workstation.
+
+
+## Running the tests
+
+1. [Prepare your environment for Node.js setup][setup].
+
+1. Install dependencies:
+
+ npm install
+
+1. Run the tests:
+
+ # Run unit tests.
+ npm test
+
+ # Run sample integration tests.
+ npm run samples-test
+
+ # Run all system tests.
+ npm run system-test
+
+1. Lint (and maybe fix) any changes:
+
+ npm run fix
+
+[setup]: https://cloud.google.com/nodejs/docs/setup
+[projects]: https://console.cloud.google.com/project
+[billing]: https://support.google.com/cloud/answer/6293499#enable-billing
+[enable_api]: https://console.cloud.google.com/flows/enableapi?apiid=clouderrorreporting.googleapis.com
+[auth]: https://cloud.google.com/docs/authentication/getting-started
\ No newline at end of file
diff --git a/handwritten/nodejs-error-reporting/LICENSE b/handwritten/nodejs-error-reporting/LICENSE
new file mode 100644
index 00000000000..d6456956733
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/handwritten/nodejs-error-reporting/README.md b/handwritten/nodejs-error-reporting/README.md
new file mode 100644
index 00000000000..161cc6a2b6a
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/README.md
@@ -0,0 +1,155 @@
+[//]: # "This README.md file is auto-generated, all changes to this file will be lost."
+[//]: # "To regenerate it, use `python -m synthtool`."
+
+
+# [Error Reporting: Node.js Client](https://github.com/googleapis/nodejs-error-reporting)
+
+[](https://cloud.google.com/terms/launch-stages)
+[](https://www.npmjs.org/package/@google-cloud/error-reporting)
+
+
+
+
+> Node.js idiomatic client for [Error Reporting][product-docs].
+
+[Error Reporting](https://cloud.google.com/error-reporting/docs/) aggregates and displays errors produced in your running cloud services.
+
+
+A comprehensive list of changes in each version may be found in
+[the CHANGELOG](https://github.com/googleapis/nodejs-error-reporting/blob/main/CHANGELOG.md).
+
+* [Error Reporting Node.js Client API Reference][client-docs]
+* [Error Reporting Documentation][product-docs]
+* [github.com/googleapis/nodejs-error-reporting](https://github.com/googleapis/nodejs-error-reporting)
+
+Read more about the client libraries for Cloud APIs, including the older
+Google APIs Client Libraries, in [Client Libraries Explained][explained].
+
+[explained]: https://cloud.google.com/apis/docs/client-libraries-explained
+
+**Table of contents:**
+
+
+* [Quickstart](#quickstart)
+ * [Before you begin](#before-you-begin)
+ * [Installing the client library](#installing-the-client-library)
+
+
+* [Versioning](#versioning)
+* [Contributing](#contributing)
+* [License](#license)
+
+## Quickstart
+
+### Before you begin
+
+1. [Select or create a Cloud Platform project][projects].
+1. [Enable the Error Reporting API][enable_api].
+1. [Set up authentication with a service account][auth] so you can access the
+ API from your local workstation.
+
+### Installing the client library
+
+```bash
+npm install @google-cloud/error-reporting
+```
+
+This module provides custom Error Reporting support for Node.js applications.
+[Error Reporting](https://cloud.google.com/error-reporting/) is a feature of
+Google Cloud Platform that allows in-depth monitoring and viewing of errors reported by
+applications running in almost any environment.
+
+However, note that [@google-cloud/logging-winston](https://github.com/googleapis/nodejs-logging-winston) and [@google-cloud/logging-bunyan](https://github.com/googleapis/nodejs-logging-bunyan) automatically integrate with the Error Reporting service for Error objects logged at severity `error` or higher, for applications running on Google Cloud Platform.
+
+Thus, if you are already using Winston or Bunyan in your application, and don't need custom error reporting capabilities, you do not need to use the `@google-cloud/error-reporting` library directly to report errors to the Error Reporting Console.
+
+
+
+# When Errors Are Reported
+
+The `reportMode` configuration option is used to specify when errors are reported to the Error Reporting Console. It can have one of three values:
+* `'production'` (default): Only report errors if the NODE_ENV environment variable is set to "production".
+* `'always'`: Always report errors regardless of the value of NODE_ENV.
+* `'never'`: Never report errors regardless of the value of NODE_ENV.
+
+The `reportMode` configuration option replaces the deprecated `ignoreEnvironmentCheck` configuration option. If both the `reportMode` and `ignoreEnvironmentCheck` options are specified, the `reportMode` configuration option takes precedence.
+
+The `ignoreEnvironmentCheck` option should not be used. However, if it is used, and the `reportMode` option is not specified, it can have the values:
+* `false` (default): Only report errors if the NODE_ENV environment variable is set to "production".
+* `true`: Always report errors regardless of the value of NODE_ENV.
+
+## Setup, Configuration, and Examples
+
+See the documentation for setup instructions, configuration options, and examples: https://cloud.google.com/error-reporting/docs/setup/nodejs
+
+Additional code samples can also be found here: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/main/error-reporting
+
+
+
+The [Error Reporting Node.js Client API Reference][client-docs] documentation
+also contains samples.
+
+## Supported Node.js Versions
+
+Our client libraries follow the [Node.js release schedule](https://github.com/nodejs/release#release-schedule).
+Libraries are compatible with all current _active_ and _maintenance_ versions of
+Node.js.
+If you are using an end-of-life version of Node.js, we recommend that you update
+as soon as possible to an actively supported LTS version.
+
+Google's client libraries support legacy versions of Node.js runtimes on a
+best-efforts basis with the following warnings:
+
+* Legacy versions are not tested in continuous integration.
+* Some security patches and features cannot be backported.
+* Dependencies cannot be kept up-to-date.
+
+Client libraries targeting some end-of-life versions of Node.js are available, and
+can be installed through npm [dist-tags](https://docs.npmjs.com/cli/dist-tag).
+The dist-tags follow the naming convention `legacy-(version)`.
+For example, `npm install @google-cloud/error-reporting@legacy-8` installs client libraries
+for versions compatible with Node.js 8.
+
+## Versioning
+
+This library follows [Semantic Versioning](http://semver.org/).
+
+
+
+This library is considered to be **stable**. The code surface will not change in backwards-incompatible ways
+unless absolutely necessary (e.g. because of critical security issues) or with
+an extensive deprecation period. Issues and requests against **stable** libraries
+are addressed with the highest priority.
+
+
+
+
+
+
+More Information: [Google Cloud Platform Launch Stages][launch_stages]
+
+[launch_stages]: https://cloud.google.com/terms/launch-stages
+
+## Contributing
+
+Contributions welcome! See the [Contributing Guide](https://github.com/googleapis/nodejs-error-reporting/blob/main/CONTRIBUTING.md).
+
+Please note that this `README.md`, the `samples/README.md`,
+and a variety of configuration files in this repository (including `.nycrc` and `tsconfig.json`)
+are generated from a central template. To edit one of these files, make an edit
+to its templates in
+[directory](https://github.com/googleapis/synthtool).
+
+## License
+
+Apache Version 2.0
+
+See [LICENSE](https://github.com/googleapis/nodejs-error-reporting/blob/main/LICENSE)
+
+[client-docs]: https://cloud.google.com/nodejs/docs/reference/error-reporting/latest
+[product-docs]: https://cloud.google.com/error-reporting
+[shell_img]: https://gstatic.com/cloudssh/images/open-btn.png
+[projects]: https://console.cloud.google.com/project
+[billing]: https://support.google.com/cloud/answer/6293499#enable-billing
+[enable_api]: https://console.cloud.google.com/flows/enableapi?apiid=clouderrorreporting.googleapis.com
+[auth]: https://cloud.google.com/docs/authentication/getting-started
diff --git a/handwritten/nodejs-error-reporting/doc/images/errors-overview.png b/handwritten/nodejs-error-reporting/doc/images/errors-overview.png
new file mode 100644
index 00000000000..6c4eb54689c
Binary files /dev/null and b/handwritten/nodejs-error-reporting/doc/images/errors-overview.png differ
diff --git a/handwritten/nodejs-error-reporting/linkinator.config.json b/handwritten/nodejs-error-reporting/linkinator.config.json
new file mode 100644
index 00000000000..29a223b6db6
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/linkinator.config.json
@@ -0,0 +1,10 @@
+{
+ "recurse": true,
+ "skip": [
+ "https://codecov.io/gh/googleapis/",
+ "www.googleapis.com",
+ "img.shields.io"
+ ],
+ "silent": true,
+ "concurrency": 10
+}
diff --git a/handwritten/nodejs-error-reporting/owlbot.py b/handwritten/nodejs-error-reporting/owlbot.py
new file mode 100644
index 00000000000..4a8b19e9c39
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/owlbot.py
@@ -0,0 +1,47 @@
+# Copyright 2018 Google LLC.
+#
+# 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.
+
+import synthtool as s
+import synthtool.gcp as gcp
+import synthtool.languages.node_mono_repo as node
+import logging
+import os
+
+logging.basicConfig(level=logging.DEBUG)
+
+common_templates = gcp.CommonTemplates()
+templates = common_templates.node_library()
+s.copy(templates, excludes=[".github/auto-label.yaml", ".github/CODEOWNERS", ".github/sync-repo-settings.yaml"])
+node.fix()
+
+
+# --------------------------------------------------------------------------
+# Modify test configs
+# --------------------------------------------------------------------------
+
+# add shared environment variables to test configs
+s.move(
+ ".kokoro/common_env_vars.cfg",
+ ".kokoro/common.cfg",
+ merge=lambda src, dst, _, : f"{dst}\n{src}",
+)
+for path, subdirs, files in os.walk(f".kokoro/continuous"):
+ for name in files:
+ if name == "common.cfg":
+ file_path = os.path.join(path, name)
+ s.move(
+ ".kokoro/common_env_vars.cfg",
+ file_path,
+ merge=lambda src, dst, _, : f"{dst}\n{src}",
+ )
diff --git a/handwritten/nodejs-error-reporting/package.json b/handwritten/nodejs-error-reporting/package.json
new file mode 100644
index 00000000000..e05e9350458
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@google-cloud/error-reporting",
+ "description": "Error Reporting Client Library for Node.js",
+ "version": "3.0.5",
+ "license": "Apache-2.0",
+ "author": "Google Inc.",
+ "engines": {
+ "node": ">=18"
+ },
+ "repository": {
+ "type": "git",
+ "directory": "handwritten/nodejs-error-reporting",
+ "url": "https://github.com/googleapis/google-cloud-node.git"
+ },
+ "main": "./build/src/index.js",
+ "types": "./build/src/index.d.ts",
+ "files": [
+ "build/src",
+ "!build/src/**/*.map"
+ ],
+ "scripts": {
+ "docs": "compodoc src/",
+ "lint": "gts check",
+ "presystem-test": "npm run compile",
+ "system-test": "MOCHA_THROW_DEPRECATION=false c8 mocha build/system-test",
+ "test": "c8 mocha --recursive build/test/unit",
+ "clean": "gts clean",
+ "compile": "tsc -p .",
+ "fix": "gts fix",
+ "prepare": "npm run compile",
+ "pretest": "npm run compile",
+ "license-check": "jsgl --local .",
+ "docs-test": "linkinator docs",
+ "predocs-test": "npm run docs",
+ "precompile": "gts clean"
+ },
+ "dependencies": {
+ "@google-cloud/common": "^6.0.0",
+ "console-log-level": "^1.4.1"
+ },
+ "devDependencies": {
+ "@compodoc/compodoc": "1.1.19",
+ "@hapi/hapi": "^21.4.4",
+ "@types/boom": "^7.3.5",
+ "@types/console-log-level": "^1.4.0",
+ "@types/express": "^4.17.21",
+ "@types/json-stable-stringify": "^1.2.0",
+ "@types/koa": "^3.0.1",
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^24.10.1",
+ "@types/once": "^1.4.5",
+ "@types/proxyquire": "^1.3.31",
+ "@types/restify": "^8.0.0",
+ "@types/uuid": "^8.3.0",
+ "boom": "^7.2.0",
+ "c8": "^10.1.3",
+ "codecov": "^3.6.2",
+ "express": "^4.17.1",
+ "gts": "^6.0.2",
+ "joi": "^17.0.0",
+ "js-green-licenses": "^4.0.0",
+ "json-stable-stringify": "^1.3.0",
+ "koa": "^3.1.1",
+ "linkinator": "^6.1.2",
+ "mocha": "^11.7.5",
+ "nock": "^14.0.10",
+ "pack-n-play": "^2.0.0",
+ "proxyquire": "^2.1.3",
+ "restify": "^11.0.0",
+ "typescript": "^5.9.3",
+ "uuid": "^8.3.2"
+ },
+ "overrides": {
+ "undici": "5.28.4"
+ },
+ "homepage": "https://github.com/googleapis/google-cloud-node/tree/main/handwritten/nodejs-error-reporting"
+}
diff --git a/handwritten/nodejs-error-reporting/src/build-stack-trace.ts b/handwritten/nodejs-error-reporting/src/build-stack-trace.ts
new file mode 100644
index 00000000000..3b39c0b00ae
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/build-stack-trace.ts
@@ -0,0 +1,44 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+const SRC_ROOT = __dirname;
+
+/**
+ * Constructs a string representation of the stack trace at the point where
+ * this function was invoked. Note that the stack trace will not include any
+ * references to frames specific to this error-reporting library itself.
+ * @param {String?} message - The message that should appear as the first line
+ * of the stack trace. This value defaults to the empty string.
+ * @returns {String} - A string representation of the stack trace at the point
+ * where this method was invoked.
+ */
+export function buildStackTrace(message?: string | null) {
+ const target = {};
+ // Build a stack trace without the frames associated with `buildStackTrace`.
+ // The stack is located at `target.stack`.
+ Error.captureStackTrace(target, buildStackTrace);
+ const prefix = message ? message + '\n' : '';
+ return (
+ prefix +
+ (target as {stack: string}).stack
+ .split('\n')
+ .slice(1)
+ .filter((line: string) => {
+ // Filter out all frames that are specific to the error-reporting
+ // library
+ return !line || line.indexOf(SRC_ROOT) === -1;
+ })
+ .join('\n')
+ );
+}
diff --git a/handwritten/nodejs-error-reporting/src/classes/error-message.ts b/handwritten/nodejs-error-reporting/src/classes/error-message.ts
new file mode 100644
index 00000000000..52a31562598
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/classes/error-message.ts
@@ -0,0 +1,304 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {ServiceContext} from '../configuration';
+
+import {RequestInformationContainer} from './request-information-container';
+
+export interface Context {
+ httpRequest: {
+ method: string;
+ url: string;
+ userAgent: string;
+ referrer: string;
+ responseStatusCode: number;
+ remoteIp: string;
+ };
+ user: string;
+ reportLocation: {filePath: string; lineNumber: number; functionName: string};
+}
+
+export class ErrorMessage {
+ eventTime: string;
+ serviceContext: ServiceContext;
+ message: string;
+ context: Context;
+
+ /**
+ * The constructor for ErrorMessage takes no arguments and is solely meant to
+ * to instantiate properties on the instance. Each property should be
+ * externally set using the corresponding set function with the exception of
+ * eventTime which can be set externally but does not need to be since it is
+ * inited to an ISO-8601 compliant time string.
+ * @type {Object}
+ * @class ErrorMessage
+ * @classdesc ErrorMessage is a class which is meant to store and control-for
+ * Google Cloud Error API submittable values. Meant to be JSON string-ifiable
+ * representation of the final values which will be submitted to the Error
+ * API this class enforces type-checking on every setter function and will
+ * write default type-friendly values to instance properties if given values
+ * which are type-incompatible to expectations. These type-friendly default
+ * substitutions will occur silently and no errors will be thrown on
+ * attempted invalid input under the premise that during misassignment some
+ * error information sent to the Error API is better than no error information
+ * due to the Error library failing under invalid input.
+ * @property {String} eventTime - an ISO-8601 compliant string representing when
+ * the error was created
+ * @property {Object} serviceContext - The service information for the error
+ * @property {String} serviceContext.service - The service that the error was
+ * was produced on
+ * @property {String|Undefined} serviceContext.version - The service version
+ * that the error was produced on
+ * @property {String} message - The error message
+ * @property {Object} context - the request, user and report context
+ * @property {Object} context.httpRequest - the request context
+ * @property {String} context.httpRequest.method - the request method (e.g. GET)
+ * @property {String} context.httpRequest.url - the request url or path
+ * @property {String} context.httpRequest.userAgent - the requesting user-agent
+ * @property {String} context.httpRequest.referrer - the request referrer
+ * @property {Number} context.httpRequest.responseStatusCode - the request
+ * status-code
+ * @property {String} context.httpRequest.remoteIp - the requesting remote ip
+ * @property {String} context.user - the vm instances user
+ * @property {Object} context.reportLocation - the report context
+ * @property {String} context.reportLocation.filePath - the file path of the
+ * report site
+ * @property {Number} context.reportLocation.lineNumber - the line number of the
+ * report site
+ * @property {String} context.reportLocation.functionName - the function name of
+ * the report site
+ */
+ constructor() {
+ this.eventTime = new Date().toISOString();
+ this.serviceContext = {service: 'node', version: undefined};
+ this.message = '';
+ this.context = {
+ httpRequest: {
+ method: '',
+ url: '',
+ userAgent: '',
+ referrer: '',
+ responseStatusCode: 0,
+ remoteIp: '',
+ },
+ user: '',
+ reportLocation: {filePath: '', lineNumber: 0, functionName: ''},
+ };
+ }
+
+ /**
+ * Sets the eventTime property on the instance to an ISO-8601 compliant string
+ * representing the current time at invocation.
+ * @function setEventTimeToNow
+ * @chainable
+ * @returns {this} - returns the instance for chaining
+ */
+ setEventTimeToNow() {
+ this.eventTime = new Date().toISOString();
+
+ return this;
+ }
+
+ /**
+ * Sets the serviceContext property on the instance and its two constituent
+ * properties: service and version.
+ * @function setServiceContext
+ * @chainable
+ * @param {String} service - the service the error was reported on
+ * @param {String|Undefined} version - the version the service was on when the
+ * error was reported
+ * @returns {this} - returns the instance for chaining
+ */
+ setServiceContext(service?: string, version?: string) {
+ this.serviceContext.service = (
+ typeof service === 'string' ? service : 'node'
+ )!;
+ this.serviceContext.version =
+ typeof version === 'string' ? version : undefined;
+
+ return this;
+ }
+
+ /**
+ * Sets the message property on the instance.
+ * @chainable
+ * @param {String} message - the error message
+ * @returns {this} - returns the instance for chaining
+ */
+ setMessage(message?: string) {
+ this.message = (typeof message === 'string' ? message : '')!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.httpRequest.method property on the instance.
+ * @chainable
+ * @param {String} method - the HTTP method on the request which caused the
+ * errors instantiation
+ * @returns {this} - returns the instance for chaining
+ */
+ setHttpMethod(method?: string) {
+ this.context.httpRequest.method = (
+ typeof method === 'string' ? method : ''
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.httpRequest.url property on the instance.
+ * @chainable
+ * @param {String} url - the requests target url
+ * @returns {this} - returns the instance for chaining
+ */
+ setUrl(url?: string) {
+ this.context.httpRequest.url = (typeof url === 'string' ? url : '')!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.httpRequest.userAgent property on the instance.
+ * @chainable
+ * @param {String} userAgent - the requests user-agent
+ * @returns {this} - returns the instance for chaining
+ */
+ setUserAgent(userAgent?: string) {
+ this.context.httpRequest.userAgent = (
+ typeof userAgent === 'string' ? userAgent : ''
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.httpRequest.referrer property on the instance.
+ * @chainable
+ * @param {String} referrer - the requests referrer
+ * @returns {this} - returns the instance for chaining
+ */
+ setReferrer(referrer?: string) {
+ this.context.httpRequest.referrer = (
+ typeof referrer === 'string' ? referrer : ''
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.httpRequest.responseStatusCode property on the instance.
+ * @chainable
+ * @param {Number} responseStatusCode - the response status code
+ * @returns {this} - returns the instance for chaining
+ */
+ setResponseStatusCode(responseStatusCode?: number) {
+ this.context.httpRequest.responseStatusCode = (
+ typeof responseStatusCode === 'number' ? responseStatusCode : 0
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.httpRequest.remoteIp property on the instance
+ * @chainable
+ * @param {String} remoteIp - the requesters remote IP
+ * @returns {this} - returns the instance for chaining
+ */
+ setRemoteIp(remoteIp?: string) {
+ this.context.httpRequest.remoteIp = (
+ typeof remoteIp === 'string' ? remoteIp : ''
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.user property on the instance
+ * @chainable
+ * @param {String} user - the vm instances user
+ * @returns {this} - returns the instance for chaining
+ */
+ setUser(user?: string) {
+ this.context.user = (typeof user === 'string' ? user : '')!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.reportLocation.filePath property on the instance
+ * @chainable
+ * @param {String} filePath - the vm instances filePath
+ * @returns {this} - returns the instance for chaining
+ */
+ setFilePath(filePath?: string) {
+ this.context.reportLocation.filePath = (
+ typeof filePath === 'string' ? filePath : ''
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.reportLocation.lineNumber property on the instance
+ * @chainable
+ * @param {Number} lineNumber - the line number of the report context
+ * @returns {this} - returns the instance for chaining
+ */
+ setLineNumber(lineNumber?: number) {
+ this.context.reportLocation.lineNumber = (
+ typeof lineNumber === 'number' ? lineNumber : 0
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Sets the context.reportLocation.functionName property on the instance
+ * @chainable
+ * @param {String} functionName - the function name of the report context
+ * @returns {this} - returns the instance for chaining
+ */
+ setFunctionName(functionName?: string) {
+ this.context.reportLocation.functionName = (
+ typeof functionName === 'string' ? functionName : ''
+ )!;
+
+ return this;
+ }
+
+ /**
+ * Consumes the standard object created by the requestInformationExtractors
+ * and assigns the properties of the object onto the instance.
+ * @chainable
+ * @param {Object} requestInformation - the standardized object created by the
+ * information extractors
+ * @returns {this} - returns the instance for chaining
+ */
+ consumeRequestInformation(requestInformation: RequestInformationContainer) {
+ if (requestInformation?.toString() !== '[object Object]') {
+ return this;
+ }
+
+ this.setHttpMethod(requestInformation.method)
+ .setUrl(requestInformation.url)
+ .setUserAgent(requestInformation.userAgent)
+ .setReferrer(requestInformation.referrer)
+ .setResponseStatusCode(requestInformation.statusCode)
+ .setRemoteIp(requestInformation.remoteAddress);
+
+ return this;
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/src/classes/request-information-container.ts b/handwritten/nodejs-error-reporting/src/classes/request-information-container.ts
new file mode 100644
index 00000000000..5b6391986b2
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/classes/request-information-container.ts
@@ -0,0 +1,123 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+export class RequestInformationContainer {
+ url: string;
+ method: string;
+ referrer: string;
+ userAgent: string;
+ remoteAddress: string;
+ statusCode: number;
+
+ /**
+ * The constructor for RequestInformationContainer does not take any arugments
+ * and is solely meant to allocate several properties on the instance. The
+ * constructor will init properties which closely relate to the ErrorMessage
+ * context.httpRequest object properties. The properties on the instance
+ * should be set through there corresponding setters as these will enforce
+ * type validation around input.
+ * @class RequestInformationContainer
+ * @classdesc RequestInformationContainer is a class which is meant to
+ * standardize and contain values corresponding to request information around
+ * an error-inducing request. This class is meant to be a temporary container
+ * for request information and essentially a standardized interface consumed
+ * by the ErrorMessage class itself.
+ * @property {String} url - The route/url that the request addressed
+ * @property {String} method - The method that the request used
+ * @property {String} referrer - The referrer of the request
+ * @property {String} userAgent - The user-agent of the requester
+ * @property {String} remoteAddress - The IP address of the requester
+ * @property {Number} statusCode - The response status code
+ */
+ constructor() {
+ this.url = '';
+ this.method = '';
+ this.referrer = '';
+ this.userAgent = '';
+ this.remoteAddress = '';
+ this.statusCode = 0;
+ }
+
+ /**
+ * Sets the url property on the instance.
+ * @chainable
+ * @param {String} url - the url of the request
+ * @returns {this} - returns the instance for chaining
+ */
+ setUrl(url: string) {
+ this.url = typeof url === 'string' ? url : '';
+
+ return this;
+ }
+
+ /**
+ * Sets the method property on the instance.
+ * @chainable
+ * @param {String} method - the method of the request
+ * @returns {this} - returns the instance for chaining
+ */
+ setMethod(method: string) {
+ this.method = typeof method === 'string' ? method : '';
+
+ return this;
+ }
+
+ /**
+ * Sets the referrer property on the instance.
+ * @chainable
+ * @param {String} referrer - the referrer of the request
+ * @returns {this} - returns the instance for chaining
+ */
+ setReferrer(referrer?: string) {
+ this.referrer = (typeof referrer === 'string' ? referrer : '')!;
+
+ return this;
+ }
+
+ /**
+ * Sets the userAgent property on the instance.
+ * @chainable
+ * @param {String} userAgent - the user agent committing the request
+ * @returns {this} - returns the instance for chaining
+ */
+ setUserAgent(userAgent?: string) {
+ this.userAgent = (typeof userAgent === 'string' ? userAgent : '')!;
+
+ return this;
+ }
+
+ /**
+ * Sets the remoteAddress property on the instance.
+ * @chainable
+ * @param {String} remoteIp - the remote IP of the requester
+ * @returns {this} - returns the instance for chaining
+ */
+ setRemoteAddress(remoteIp?: string) {
+ this.remoteAddress = (typeof remoteIp === 'string' ? remoteIp : '')!;
+
+ return this;
+ }
+
+ /**
+ * Sets the statusCode property on the instance.
+ * @chainable
+ * @param {Number} statusCode - the status code of the response to the request
+ * @returns {this} - returns the instance for chaining
+ */
+ setStatusCode(statusCode: number) {
+ this.statusCode = typeof statusCode === 'number' ? statusCode : 0;
+
+ return this;
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/src/configuration.ts b/handwritten/nodejs-error-reporting/src/configuration.ts
new file mode 100644
index 00000000000..b0feeb6df46
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/configuration.ts
@@ -0,0 +1,500 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+const env = process.env;
+
+// The Logger interface defined below matches the interface
+// used by console-log-level. If the console-log-level
+// module is imported here to get the Logger interface,
+// TypeScript users of the error reporting library would
+// need to install @types/console-log-level to compile their
+// code. As a result, the interface is explicitly specified instead.
+export type LogLevel =
+ | 'error'
+ | 'trace'
+ | 'debug'
+ | 'info'
+ | 'warn'
+ | 'fatal'
+ | undefined;
+export interface Logger {
+ error(...args: Array<{}>): void;
+ trace(...args: Array<{}>): void;
+ debug(...args: Array<{}>): void;
+ info(...args: Array<{}>): void;
+ warn(...args: Array<{}>): void;
+ fatal(...args: Array<{}>): void;
+}
+
+export type ReportMode = 'production' | 'always' | 'never';
+
+export interface ConfigurationOptions {
+ projectId?: string;
+ keyFilename?: string;
+ logLevel?: string | number;
+ key?: string;
+ serviceContext?: {service?: string; version?: string};
+ ignoreEnvironmentCheck?: boolean;
+ reportMode?: ReportMode;
+ credentials?: {};
+ reportUnhandledRejections?: boolean;
+}
+
+export interface ServiceContext {
+ service: string;
+ version?: string;
+}
+
+/**
+ * The Configuration constructor function initializes several internal
+ * properties on the Configuration instance and accepts a runtime-given
+ * configuration object which may be used by the Configuration instance
+ * depending on the initialization transaction that occurs with the meta-data
+ * service.
+ * @class Configuration
+ * @classdesc The Configuration class represents the runtime configuration of
+ * the error handling library. This Configuration class accepts the
+ * configuration options potentially given through the application interface
+ * but it also preferences values received from the metadata service over
+ * values given through the application interface. Becuase the Configuration
+ * class must handle async network I/O it exposes some methods as async
+ * functions which may cache their interactions results to speed access to
+ * properties.
+ * @param {ConfigurationOptions} givenConfig - The config given by the
+ * hosting application at runtime. Configuration values will only be observed
+ * if they are given as a plain JS object; all other values will be ignored.
+ * @param {Object} logger - The logger instance created when the library API has
+ * been initialized.
+ */
+export class Configuration {
+ _logger: Logger;
+ _reportMode: ReportMode;
+ _projectId: string | null;
+ _key: string | null;
+ keyFilename: string | null;
+ credentials: {} | null;
+ _serviceContext: ServiceContext;
+ _reportUnhandledRejections: boolean;
+ _givenConfiguration: ConfigurationOptions;
+
+ constructor(givenConfig: ConfigurationOptions | undefined, logger: Logger) {
+ /**
+ * The _logger property caches the logger instance created at the top-level
+ * for configuration logging purposes.
+ */
+ this._logger = logger;
+ this._reportMode = 'production';
+ /**
+ * The _projectId property is meant to contain the string project id that
+ * the hosting application is running under. The project id is a unique
+ * string identifier for the project. If the Configuration instance is not
+ * able to retrieve a project id from the metadata service or the
+ * runtime-given configuration then the property will remain null. If given
+ * both a project id through the metadata service and the runtime
+ * configuration then the instance will assign the value given by the
+ * metadata service over the runtime configuration. If the instance is
+ * unable to retrieve a valid project id or number from runtime
+ * configuration and the metadata service then this will trigger the `error`
+ * event in which listening components must operate in 'offline' mode.
+ * {@link https://cloud.google.com/compute/docs/storing-retrieving-metadata}
+ * @memberof Configuration
+ * @private
+ * @type {String|Null}
+ * @defaultvalue null
+ */
+ this._projectId = null;
+ /**
+ * The _key property is meant to contain the optional Google Cloud API key
+ * that may be used in place of default application credentials to
+ * authenticate with the Error API. This property will remain
+ * null if a key is not given in the runtime configuration or an invalid
+ * type is given as the runtime configuration.
+ * {@link https://support.google.com/cloud/answer/6158862?hl=en}
+ * @memberof Configuration
+ * @private
+ * @type {String|Null}
+ * @defaultvalue null
+ */
+ this._key = null;
+ /**
+ * The keyFilename property is meant to contain a path to a file containing
+ * user or service account credentials, which will be used in place of
+ * application default credentials. This property will remain null if no
+ * value for keyFilename is given in the runtime configuration.
+ * @memberof Configuration
+ * @private
+ * @type {String|Null}
+ * @defaultvalue null
+ */
+ this.keyFilename = null;
+ /**
+ * The credentials property is meant to contain an object representation of
+ * user or service account credentials, which will be used in place of
+ * application default credentials. This property will remain null if no
+ * value for credentials is given in the runtime configuration.
+ * @memberof Configuration
+ * @private
+ * @type {Credentials|Null}
+ * @defaultvalue null
+ */
+ this.credentials = null;
+ /**
+ * The _serviceContext property is meant to contain the optional service
+ * context information which may be given in the runtime configuration. If
+ * not given in the runtime configuration then the property value will
+ * remain null.
+ * @memberof Configuration
+ * @private
+ * @type {Object}
+ */
+ this._serviceContext = {service: 'nodejs', version: ''};
+ /**
+ * The _reportUnhandledRejections property is meant to specify whether or
+ * not unhandled rejections should be reported to the error-reporting
+ * console.
+ * @memberof Configuration
+ * @private
+ * @type {Boolean}
+ */
+ this._reportUnhandledRejections = false;
+ /**
+ * The _givenConfiguration property holds a ConfigurationOptions object
+ * which, if valid, will be merged against by the values taken from the
+ * meta-data service. If the _givenConfiguration property is not valid then
+ * only metadata values will be used in the Configuration instance.
+ * @memberof Configuration
+ * @private
+ * @type {Object|Null}
+ * @defaultvalue null
+ */
+ this._givenConfiguration =
+ givenConfig?.toString() === '[object Object]' ? givenConfig! : {};
+ this._checkLocalServiceContext();
+ this._gatherLocalConfiguration();
+ }
+ /**
+ * The _checkLocalServiceContext function is responsible for attempting to
+ * source the _serviceContext objects values from runtime configuration and
+ * the environment. First the function will check the env for known service
+ * context names, if these are not set then it will defer to the
+ * _givenConfiguration property if it is set on the instance. The function
+ * will check env variables `GAE_MODULE_NAME` and `GAE_MODULE_VERSION` for
+ * `_serviceContext.service` and
+ * `_serviceContext.version` respectively. If these are not set the
+ * `_serviceContext` properties will be left at default unless the given
+ * runtime configuration supplies any values as substitutes.
+ * @memberof Configuration
+ * @private
+ * @function _checkLocalServiceContext
+ * @returns {Undefined} - does not return anything
+ */
+ _checkLocalServiceContext() {
+ // Update June 18, 2019: When running on Cloud Run, Cloud Run
+ // on GKE, or any Knative install, the
+ // K_SERVICE env var should be used as
+ // the service and the K_REVISION env var
+ // should be used as the version. See the
+ // Knative runtime contract for more info
+ // (https://github.com/knative/serving/blob/master/docs/runtime-contract.md#process)
+ //
+ // Note: The GAE_MODULE_NAME environment variable is set on GAE.
+ // If the code is, in particular, running on GCF, then the
+ // FUNCTION_NAME environment variable is set.
+ //
+ // To determine the service name to use:
+ // If the user specified a service name it should be used, otherwise
+ // if the FUNCTION_NAME environment variable is set (indicating that the
+ // code is running on GCF) then the FUNCTION_NAME value should be used as
+ // the service name. If neither of these conditions are true, the
+ // value of the GAE_MODULE_NAME environment variable should be used as the
+ // service name.
+ //
+ // To determine the service version to use:
+ // If the user species a version, then that version will be used.
+ // Otherwise, the value of the environment variable GAE_MODULE_VERSION
+ // will be used if and only if the FUNCTION_NAME environment variable is
+ // not set.
+ let service;
+ let version;
+
+ if (env.K_SERVICE) {
+ service = env.K_SERVICE;
+ version = env.K_REVISION;
+ } else if (env.FUNCTION_NAME) {
+ service = env.FUNCTION_NAME;
+ } else if (env.GAE_SERVICE) {
+ service = env.GAE_SERVICE;
+ version = env.GAE_VERSION;
+ } else if (env.GAE_MODULE_NAME) {
+ service = env.GAE_MODULE_NAME;
+ version = env.GAE_MODULE_VERSION;
+ }
+
+ this._serviceContext.service = (
+ typeof service === 'string' ? service : 'node'
+ )!;
+ this._serviceContext.version =
+ typeof version === 'string' ? version : undefined;
+
+ if (
+ this._givenConfiguration.serviceContext?.toString() === '[object Object]'
+ ) {
+ if (
+ typeof this._givenConfiguration.serviceContext!.service === 'string'
+ ) {
+ this._serviceContext.service =
+ this._givenConfiguration.serviceContext!.service!;
+ } else if (
+ this._givenConfiguration.serviceContext?.service !== undefined
+ ) {
+ throw new Error('config.serviceContext.service must be a string');
+ }
+
+ if (
+ typeof this._givenConfiguration.serviceContext!.version === 'string'
+ ) {
+ this._serviceContext.version =
+ this._givenConfiguration.serviceContext!.version;
+ } else if (
+ this._givenConfiguration.serviceContext?.version !== undefined
+ ) {
+ throw new Error('config.serviceContext.version must be a string');
+ }
+ }
+ }
+ _determineReportMode() {
+ if (this._givenConfiguration.reportMode) {
+ this._reportMode =
+ this._givenConfiguration.reportMode.toLowerCase() as ReportMode;
+ }
+ }
+ /**
+ * The _gatherLocalConfiguration function is responsible for determining
+ * directly determing whether the properties `reportUncaughtExceptions` and
+ * `key`, which can be optionally supplied in the runtime configuration,
+ * should be merged into the instance. This function also calls several
+ * specialized environmental variable checkers which not only check for the
+ * optional runtime configuration supplied values but also the processes
+ * environmental values.
+ * @memberof Configuration
+ * @private
+ * @function _gatherLocalConfiguration
+ * @returns {Undefined} - does not return anything
+ */
+ _gatherLocalConfiguration() {
+ let isReportModeValid = true;
+ if (this._givenConfiguration?.reportMode !== undefined) {
+ const reportMode = this._givenConfiguration.reportMode;
+ isReportModeValid =
+ typeof reportMode === 'string' &&
+ (reportMode === 'production' ||
+ reportMode === 'always' ||
+ reportMode === 'never');
+ }
+
+ if (!isReportModeValid) {
+ throw new Error(
+ 'config.reportMode must a string that is one ' +
+ 'of "production", "always", or "never".',
+ );
+ }
+
+ const hasEnvCheck =
+ this._givenConfiguration?.ignoreEnvironmentCheck !== undefined;
+ const hasReportMode = this._givenConfiguration?.reportMode !== undefined;
+ if (hasEnvCheck) {
+ this._logger.warn(
+ 'The "ignoreEnvironmentCheck" config option is deprecated. ' +
+ 'Use the "reportMode" config option instead.',
+ );
+ }
+ if (hasEnvCheck && hasReportMode) {
+ this._logger.warn(
+ [
+ 'Both the "ignoreEnvironmentCheck" and "reportMode" configuration options',
+ 'have been specified. The "reportMode" option will take precedence.',
+ ].join(' '),
+ );
+ this._determineReportMode();
+ } else if (hasEnvCheck) {
+ if (this._givenConfiguration.ignoreEnvironmentCheck === true) {
+ this._reportMode = 'always';
+ } else if (
+ this._givenConfiguration?.ignoreEnvironmentCheck !== undefined &&
+ typeof this._givenConfiguration.ignoreEnvironmentCheck !== 'boolean'
+ ) {
+ throw new Error('config.ignoreEnvironmentCheck must be a boolean');
+ } else {
+ this._reportMode = 'production';
+ }
+ } else if (hasReportMode) {
+ this._determineReportMode();
+ }
+
+ if (this.isReportingEnabled() && !this.getShouldReportErrorsToAPI()) {
+ this._logger.warn(
+ [
+ 'The error reporting client is configured to report errors',
+ 'if and only if the NODE_ENV environment variable is set to "production".',
+ 'Errors will not be reported. To have errors always reported, regardless of the',
+ 'value of NODE_ENV, set the reportMode configuration option to "always".',
+ ].join(' '),
+ );
+ }
+
+ if (typeof this._givenConfiguration.key === 'string') {
+ this._key = this._givenConfiguration.key!;
+ } else if (this._givenConfiguration?.key !== undefined) {
+ throw new Error('config.key must be a string');
+ }
+ if (typeof this._givenConfiguration.keyFilename === 'string') {
+ this.keyFilename = this._givenConfiguration.keyFilename!;
+ } else if (this._givenConfiguration?.keyFilename !== undefined) {
+ throw new Error('config.keyFilename must be a string');
+ }
+ if (
+ this._givenConfiguration.credentials?.toString() === '[object Object]'
+ ) {
+ this.credentials = this._givenConfiguration.credentials!;
+ } else if (this._givenConfiguration?.credentials !== undefined) {
+ throw new Error('config.credentials must be a valid credentials object');
+ }
+ if (
+ typeof this._givenConfiguration.reportUnhandledRejections === 'boolean'
+ ) {
+ this._reportUnhandledRejections =
+ this._givenConfiguration.reportUnhandledRejections!;
+ } else if (
+ this._givenConfiguration?.reportUnhandledRejections !== undefined
+ ) {
+ throw new Error('config.reportUnhandledRejections must be a boolean');
+ }
+ }
+ /**
+ * The _checkLocalProjectId function is responsible for determing whether the
+ * _projectId property was set by the metadata service and whether or not the
+ * _projectId property should/can be set with a environmental or runtime
+ * configuration variable. If, upon execution of the _checkLocalProjectId
+ * function, the _projectId property has already been set to a string then it
+ * is assumed that this property has been set with the metadata services
+ * response. The metadata value for the project id always take precedence over
+ * any other locally configured project id value. Given that the metadata
+ * service did not set the project id this function will defer next to the
+ * value set in the environment named `GCLOUD_PROJECT` if it is set and of
+ * type string. If this environmental variable is not set the function will
+ * defer to the _givenConfiguration property if it is of type object and has a
+ * string property named projectId. If none of these conditions are met then
+ * the _projectId property will be left at its default value.
+ * @memberof Configuration
+ * @private
+ * @function _checkLocalProjectId
+ * @param {Function} cb - The original user callback to invoke with the project
+ * id or error encountered during id capture
+ * @returns {Undefined} - does not return anything
+ */
+ _checkLocalProjectId() {
+ if (typeof this._projectId === 'string') {
+ // already has been set by the metadata service
+ return this._projectId;
+ }
+ if (this._givenConfiguration?.projectId !== undefined) {
+ if (typeof this._givenConfiguration.projectId === 'string') {
+ this._projectId = this._givenConfiguration.projectId!;
+ } else if (typeof this._givenConfiguration.projectId === 'number') {
+ this._projectId = String(this._givenConfiguration!.projectId);
+ }
+ }
+ return this._projectId;
+ }
+ /**
+ * Returns whether this configuration specifies that errors should be
+ * reported to the error reporting API. That is, "reportMode" is
+ * either set to "always" or it is set to "production" and the value
+ * of the NODE_ENV environment variable is "production".
+ * @memberof Configuration
+ * @public
+ * @function getShouldReportErrorsToAPI
+ * @returns {Boolean} - whether errors should be reported to the API
+ */
+ getShouldReportErrorsToAPI() {
+ return (
+ this._reportMode === 'always' ||
+ (this._reportMode === 'production' &&
+ (process.env.NODE_ENV || '').toLowerCase() === 'production')
+ );
+ }
+ isReportingEnabled() {
+ return this._reportMode !== 'never';
+ }
+ /**
+ * Returns the _projectId property on the instance.
+ * @memberof Configuration
+ * @public
+ * @function getProjectId
+ * @returns {String|Null} - returns the _projectId property
+ */
+ getProjectId() {
+ return this._checkLocalProjectId();
+ }
+ /**
+ * Returns the _key property on the instance.
+ * @memberof Configuration
+ * @public
+ * @function getKey
+ * @returns {String|Null} - returns the _key property
+ */
+ getKey() {
+ return this._key;
+ }
+ /**
+ * Returns the keyFilename property on the instance.
+ * @memberof Configuration
+ * @public
+ * @function getKeyFilename
+ * @returns {String|Null} - returns the keyFilename property
+ */
+ getKeyFilename() {
+ return this.keyFilename;
+ }
+ /**
+ * Returns the credentials property on the instance.
+ * @memberof Configuration
+ * @public
+ * @function getCredentials
+ * @returns {Credentials|Null} - returns the credentials property
+ */
+ getCredentials() {
+ return this.credentials;
+ }
+ /**
+ * Returns the _serviceContext property on the instance.
+ * @memberof Configuration
+ * @public
+ * @function getKey
+ * @returns {Object|Null} - returns the _serviceContext property
+ */
+ getServiceContext() {
+ return this._serviceContext;
+ }
+ /**
+ * Returns the _reportUnhandledRejections property on the instance.
+ * @memberof Configuration
+ * @public
+ * @function getReportUnhandledRejections
+ * @returns {Boolean} - returns the _reportUnhandledRejections property
+ */
+ getReportUnhandledRejections() {
+ return this._reportUnhandledRejections;
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/src/google-apis/auth-client.ts b/handwritten/nodejs-error-reporting/src/google-apis/auth-client.ts
new file mode 100644
index 00000000000..c5ef312b607
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/google-apis/auth-client.ts
@@ -0,0 +1,243 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const pkg = require('../../../package.json');
+import {Configuration, Logger} from '../configuration';
+import {ErrorMessage} from '../classes/error-message';
+import * as http from 'http';
+import {Service, ServiceOptions} from '@google-cloud/common';
+
+/* @const {Array} list of scopes needed to work with the errors api. */
+const SCOPES = ['https://www.googleapis.com/auth/cloud-platform'];
+
+/* @const {String} Base Error Reporting API */
+const API = 'https://clouderrorreporting.googleapis.com/v1beta1';
+
+const API_ENDPOINT = 'clouderrorreporting.googleapis.com';
+
+/**
+ * The RequestHandler constructor initializes several properties on the
+ * RequestHandler instance and create a new request factory for requesting
+ * against the Error Reporting API.
+ * @param {Configuration} config - The configuration instance
+ * @param {Object} logger - the logger instance
+ * @class RequestHandler
+ * @classdesc The RequestHandler class provides a centralized way of managing a
+ * pool of ongoing requests and routing there callback execution to the right
+ * handlers. The RequestHandler relies on the diag-common request factory
+ * and therefore only manages the routing of execution to the proper callback
+ * and does not do any queueing/batching. The RequestHandler instance has
+ * several properties: the projectId property is used to create a correct url
+ * for interacting with the API and key property can be optionally provided a
+ * value which can be used in place of default application authentication. The
+ * shouldReportErrors property will dictate whether or not the handler instance
+ * will attempt to send payloads to the API. If it is false the handler will
+ * immediately call back to the completion callback with a constant error value.
+ * @property {Function} _request - a npm.im/request style request function that
+ * provides the transport layer for requesting against the Error Reporting API.
+ * It includes retry and authorization logic.
+ * @property {String} _projectId - the project id used to uniquely identify and
+ * address the correct project in the Error Reporting API
+ * @property {Object} _logger - the instance-cached logger instance
+ */
+export class RequestHandler extends Service {
+ private _config: Configuration;
+ private _logger: Logger;
+ // TODO: Make this more precise
+
+ /**
+ * Returns a query-string request object if a string key is given, otherwise
+ * will return null.
+ * @param {String|Null} [key] - the API key used to authenticate against the
+ * service in place of application default credentials.
+ * @returns {Object|Null} api key query string object for use with request or
+ * null in case no api key is given
+ * @static
+ */
+ static manufactureQueryString(key: string | null) {
+ if (typeof key === 'string') {
+ return {key};
+ }
+ return null;
+ }
+
+ /**
+ * No-operation stub function for user callback substitution
+ * @param {Error|Null} err - the error
+ * @param {Object|Null} response - the response object
+ * @param {Any} body - the response body
+ * @returns {Null}
+ * @static
+ */
+ static noOp() {
+ return null;
+ }
+ /**
+ * @constructor
+ * @param {Configuration} config - an instance of the Configuration class
+ * @param {Logger} logger - an instance of logger
+ */
+ constructor(config: Configuration, logger: Logger) {
+ const pid = config.getProjectId();
+ // If an API key is provided, do not try to authenticate.
+ const tryAuthenticate = !config.getKey();
+ const serviceOptions: ServiceOptions = Object.assign(config, {
+ projectId: pid !== null ? pid : undefined,
+ customEndpoint: !tryAuthenticate,
+ }) as ServiceOptions;
+ super(
+ {
+ packageJson: pkg,
+ baseUrl: API,
+ apiEndpoint: API_ENDPOINT,
+ scopes: SCOPES,
+ projectIdRequired: true,
+ },
+ serviceOptions,
+ );
+ this._config = config;
+ this._logger = logger;
+
+ if (!this._config.getShouldReportErrorsToAPI()) {
+ this._logger.info(
+ 'Not configured to send errors to the API; skipping Google Cloud API Authentication.',
+ );
+ } else if (tryAuthenticate) {
+ this.authClient.getAccessToken().then(
+ () => {},
+ err => {
+ this._logger.error(
+ [
+ 'Unable to find credential information on instance. This library',
+ 'will be unable to communicate with the Google Cloud API to save',
+ 'errors. Message: ' + err.message,
+ ].join(' '),
+ );
+ },
+ );
+ } else {
+ this.request(
+ {
+ uri: 'events:report',
+ qs: RequestHandler.manufactureQueryString(this._config.getKey()),
+ method: 'POST',
+ json: {},
+ },
+ (err, body, response) => {
+ if (
+ err &&
+ err.message !== 'Message cannot be empty.' &&
+ response &&
+ response.statusCode === 400
+ ) {
+ this._logger.error(
+ [
+ 'Encountered an error while attempting to validate the provided',
+ 'API key',
+ ].join(' '),
+ err,
+ );
+ }
+ },
+ );
+ this._logger.info('API key provided; skipping OAuth2 token request.');
+ }
+ }
+ /**
+ * Creates a request options object given the value of the error message and
+ * will callback to the user supplied callback if given one. If a callback is
+ * not given then the request will execute and silently dissipate.
+ * @function sendError
+ * @param {ErrorMessage} payload - the ErrorMessage instance to JSON.stringify
+ * for submission to the service
+ * @param {RequestHandler~requestCallback} [userCb] - function called when the
+ * request has succeeded or failed.
+ * @returns {Undefined} - does not return anything
+ * @instance
+ */
+ sendError(
+ errorMessage: ErrorMessage,
+ userCb?: (
+ err: Error | null,
+ response: http.ServerResponse | null,
+ body: {},
+ ) => void,
+ ) {
+ const cb: Function = (
+ typeof userCb === 'function' ? userCb : RequestHandler.noOp
+ )!;
+ if (!this._config.isReportingEnabled()) {
+ cb(null, null, {});
+ return;
+ }
+ if (this._config.getShouldReportErrorsToAPI()) {
+ this.request(
+ {
+ uri: 'events:report',
+ qs: RequestHandler.manufactureQueryString(this._config.getKey()),
+ method: 'POST',
+ json: errorMessage,
+ },
+ (err, body, response) => {
+ if (err) {
+ this._logger.error(
+ [
+ 'Encountered an error while attempting to transmit an error to',
+ 'the Error Reporting API.',
+ ].join(' '),
+ err,
+ );
+ }
+ cb(err, response, body);
+ },
+ );
+ } else {
+ cb(
+ new Error(
+ [
+ 'The error reporting client is configured to report errors',
+ 'if and only if the NODE_ENV environment variable is set to "production".',
+ 'Errors will not be reported. To have errors always reported, regardless of the',
+ 'value of NODE_ENV, set the reportMode configuration option to "always".',
+ ].join(' '),
+ ),
+ null,
+ null,
+ );
+ }
+ }
+}
+
+/**
+ * The requestCallback callback function is called on completion of an API
+ * request whether that completion is success or failure. The request can
+ * either fail by reaching the max number of retries or encountering an
+ * unrecoverable response from the API. The first parameter to any invocation
+ * of the requestCallback function type will be the applicable error if one
+ * was generated during the request-response transaction. If an error was not
+ * generated during the transaction then the first parameter will be of type
+ * Null. The second parameter is the entire response from the transaction,
+ * this is an object that as well as containing the body of the response from
+ * the transaction will also include transaction information. The third
+ * parameter is the body of the response, this can be an object, a string or
+ * any type given by the response object.
+ * @callback RequestHandler~requestCallback cb - The function that will be
+ * invoked once the transaction has completed
+ * @param {Error|Null} err - The error, if applicable, generated during the
+ * transaction
+ * @param {Object|Undefined|Null} response - The response, if applicable,
+ * received during the transaction
+ * @param {Any} body - The response body if applicable
+ */
diff --git a/handwritten/nodejs-error-reporting/src/index.ts b/handwritten/nodejs-error-reporting/src/index.ts
new file mode 100644
index 00000000000..08799013560
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/index.ts
@@ -0,0 +1,231 @@
+/*!
+ * Copyright 2026 Google LLC
+ *
+ * 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.
+ */
+
+/*!
+ * @module error-reporting
+ */
+
+import {ErrorMessage} from './classes/error-message';
+import {Configuration, ConfigurationOptions, Logger} from './configuration';
+import {RequestHandler as AuthClient} from './google-apis/auth-client';
+// Begin error reporting interfaces
+import * as expressInterface from './interfaces/express';
+import * as hapiInterface from './interfaces/hapi';
+import * as koaInterface from './interfaces/koa';
+import * as koa2Interface from './interfaces/koa2';
+import * as manualInterface from './interfaces/manual';
+import * as messageBuilderInterface from './interfaces/message-builder';
+import * as restifyInterface from './interfaces/restify';
+import {createLogger} from './logger';
+import * as manualRequestExtractor from './request-extractors/manual';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type RestifyRequestHandler = (req: any, res: any, next: Function) => any;
+
+export {ErrorMessage};
+
+/**
+ * @typedef ConfigurationOptions
+ * @type {Object}
+ * @property {String} [projectId] - the projectId of the project deployed
+ * @property {String} [keyFilename] - path to a key file to use for an API key
+ * @property {String|Number} logLevel - a integer between and including 0-5 or a
+ * decimal representation of an integer including and between 0-5 in String
+ * form
+ * @property {String} [key] - API key to use for communication with the service
+ * @property {uncaughtHandlingEnum}
+ * [onUncaughtException=uncaughtHandlingEnum.ignore] - one of the uncaught
+ * handling options
+ * @property {Object} [serviceContext] - the service context of the application
+ * @property {String} [serviceContext.service] - the service the application is
+ * running on
+ * @property {String} [serviceContext.version] - the version the hosting
+ * application is currently labelled as
+ * @property {Boolean} [ignoreEnvironmentCheck] - flag indicating whether or not
+ * to communicate errors to the Google Cloud service even if NODE_ENV is not set
+ * to production
+ * @property {String} [reportMode] - flag indicating whether or not
+ * to communicate errors to the Google Cloud service. Possible values are:
+ * -> 'production' (default)
+ * -> Only report errors if NODE_ENV is set to "production".
+ * -> 'always'
+ * -> Always report errors regardless of the value of NODE_ENV.
+ * -> 'never'
+ * -> Never report errors regardless of the value of NODE_ENV.
+ */
+
+/**
+ * @typedef Errors
+ * @type {Object}
+ * @property {Function} report - The manual interface to report Errors to the
+ * Error Reporting Service
+ * @property {ErrorMessage} event - Returns a new ErrorMessage class instance
+ * to use to create custom messages
+ * @property {Function} express - The express plugin for Error Reporting
+ * @property {Object} hapi - The hapi plugin for Error Reporting
+ * @property {Function} koa - The koa plugin for Error Reporting
+ * @property {Function} restify - The restify plugin for Error Reporting
+ */
+
+/**
+ * This module provides Error Reporting support for Node.js
+ * applications.
+ * [Error Reporting](https://cloud.google.com/error-reporting/) is
+ * a feature of Google Cloud Platform that allows in-depth monitoring and
+ * viewing of errors reported by applications running in almost any environment.
+ *
+ * This is the entry point for initializing the error reporting middleware. This
+ * function will invoke configuration gathering and attempt to create a API
+ * client which will send errors to the Error Reporting Service.
+ *
+ * @alias module:error-reporting
+ * @constructor
+ *
+ * @resource [What is Error Reporting]{@link
+ * https://cloud.google.com/error-reporting/}
+ *
+ * @param {ConfigurationOptions} initConfiguration - The desired project/error
+ * reporting configuration.
+ */
+export class ErrorReporting {
+ private _logger!: Logger;
+ private _config!: Configuration;
+ private _client!: AuthClient;
+ // the `err` argument can be anything, including `null` and `undefined`
+ report!: (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ err: any,
+ request?: manualRequestExtractor.Request,
+ customMessage?: string,
+ callback?: manualInterface.Callback | {} | string,
+ ) => ErrorMessage;
+ event!: () => ErrorMessage;
+ hapi!: {
+ register: (server: {}, options: {}, next?: Function) => void;
+ name: string;
+ version?: string;
+ };
+ express!: (err: {}, req: {}, res: {}, next: Function) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ restify!: (server: any) => RestifyRequestHandler | RestifyRequestHandler[];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ koa!: (context: any, next: {}) => IterableIterator<{}>;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ koa2!: (context: any, next: Function) => Promise;
+
+ constructor(initConfiguration?: ConfigurationOptions) {
+ if (!(this instanceof ErrorReporting)) {
+ return new ErrorReporting(initConfiguration);
+ }
+
+ this._logger = createLogger(initConfiguration);
+ this._config = new Configuration(initConfiguration, this._logger);
+ this._client = new AuthClient(this._config, this._logger);
+
+ if (this._config.getReportUnhandledRejections()) {
+ process.on('unhandledRejection', reason => {
+ this._logger.warn(
+ 'UnhandledPromiseRejectionWarning: ' +
+ 'Unhandled promise rejection: ' +
+ reason +
+ '. This rejection has been reported to the ' +
+ 'Google Cloud Platform error-reporting console.',
+ );
+ this.report(reason);
+ });
+ }
+
+ // Build the application interfaces for use by the hosting application
+ /**
+ * @example
+ * // Use to report errors manually like so
+ * errors.report(new Error('xyz'), function () {
+ * console.log('done!');
+ * });
+ */
+ this.report = manualInterface.handlerSetup(
+ this._client,
+ this._config,
+ this._logger,
+ );
+
+ /**
+ * @example
+ * // Use to create and report errors manually with a high-degree
+ * // of manual control
+ * const err = errors.event()
+ * .setMessage('My error message')
+ * .setUser('root@nexus');
+ * errors.report(err, function () {
+ * console.log('done!');
+ * });
+ */
+ this.event = messageBuilderInterface.handlerSetup(this._config);
+
+ /**
+ * @example
+ * const hapi = require('hapi');
+ * const server = new hapi.Server();
+ * server.connection({ port: 3000 });
+ * server.start();
+ * // AFTER ALL OTHER ROUTE HANDLERS
+ * server.register({register: errors.hapi});
+ */
+ this.hapi = hapiInterface.makeHapiPlugin(this._client, this._config);
+
+ /**
+ * @example
+ * const express = require('express');
+ * const app = express();
+ * // AFTER ALL OTHER ROUTE HANDLERS
+ * app.use(errors.express);
+ * app.listen(3000);
+ */
+ this.express = expressInterface.makeExpressHandler(
+ this._client,
+ this._config,
+ );
+
+ /**
+ * @example
+ * const restify = require('restify');
+ * const server = restify.createServer();
+ * // BEFORE ALL OTHER ROUTE HANDLERS
+ * server.use(errors.restify(server));
+ */
+ this.restify = restifyInterface.handlerSetup(this._client, this._config);
+
+ /**
+ * @example
+ * // for Koa@1
+ * const koa = require('koa');
+ * const app = koa();
+ * // BEFORE ALL OTHER ROUTE HANDLERS HANDLERS
+ * app.use(errors.koa);
+ */
+ this.koa = koaInterface.koaErrorHandler(this._client, this._config);
+
+ /**
+ * @example
+ * // for Koa@2
+ * const koa = require('koa');
+ * const app = koa();
+ * // BEFORE ALL OTHER ROUTE HANDLERS HANDLERS
+ * app.use(errors.koa2);
+ */
+ this.koa2 = koa2Interface.koa2ErrorHandler(this._client, this._config);
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/express.ts b/handwritten/nodejs-error-reporting/src/interfaces/express.ts
new file mode 100644
index 00000000000..cacc1e5468f
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/express.ts
@@ -0,0 +1,82 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as express from 'express';
+
+import {ErrorMessage} from '../classes/error-message';
+import {Configuration} from '../configuration';
+import {RequestHandler} from '../google-apis/auth-client';
+import {populateErrorMessage} from '../populate-error-message';
+import {expressRequestInformationExtractor} from '../request-extractors/express';
+
+/**
+ * Returns a function that can be used as an express error handling middleware.
+ * @function makeExpressHandler
+ * @param {AuthClient} client - an inited Auth Client instance
+ * @param {NormalizedConfigurationVariables} config - the environmental
+ * configuration
+ * @returns {expressErrorHandler} - a function that can be used as an express
+ * error handling middleware.
+ */
+export function makeExpressHandler(
+ client: RequestHandler,
+ config: Configuration,
+) {
+ /**
+ * The Express Error Handler function is an interface for the error handler
+ * stack into the Express architecture.
+ * @function expressErrorHandler
+ * @param {Any} err - a error of some type propagated by the express plugin
+ * stack
+ * @param {Object} req - an Express request object
+ * @param {Object} res - an Express response object
+ * @param {Function} next - an Express continuation callback
+ * @returns {ErrorMessage} - Returns the ErrorMessage instance
+ */
+ function expressErrorHandler(err: {}, req: {}, res: {}, next: Function) {
+ let ctxService = '';
+ let ctxVersion: string | undefined = '';
+
+ if (config?.toString() === '[object Object]') {
+ ctxService = config.getServiceContext().service;
+ ctxVersion = config.getServiceContext().version;
+ }
+
+ const em = new ErrorMessage()
+ .consumeRequestInformation(
+ expressRequestInformationExtractor(
+ req as express.Request,
+ res as express.Response,
+ ),
+ )
+ .setServiceContext(ctxService, ctxVersion);
+
+ populateErrorMessage(err, em);
+
+ if (
+ client?.toString() === '[object Object]' &&
+ typeof client.sendError === 'function'
+ ) {
+ client.sendError(em);
+ }
+
+ if (typeof next === 'function') {
+ next(err);
+ }
+
+ return em;
+ }
+
+ return expressErrorHandler;
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/hapi.ts b/handwritten/nodejs-error-reporting/src/interfaces/hapi.ts
new file mode 100644
index 00000000000..33402dcaa8b
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/hapi.ts
@@ -0,0 +1,153 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as boom from 'boom';
+
+import {ErrorMessage} from '../classes/error-message';
+import {populateErrorMessage} from '../populate-error-message';
+import {hapiRequestInformationExtractor} from '../request-extractors/hapi';
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const packageJson = require('../../../package.json');
+
+import {RequestHandler} from '../google-apis/auth-client';
+import {Configuration} from '../configuration';
+import * as hapi from '@hapi/hapi';
+
+/**
+ * The Hapi error handler function serves simply to create an error message
+ * and begin that error message on the path of correct population.
+ * @function hapiErrorHandler
+ * @param {Object} req - The Hapi request object
+ * @param {Any} err - The error input
+ * @param {Object} config - the env configuration
+ * @returns {ErrorMessage} - a partially or fully populated instance of
+ * ErrorMessage
+ */
+function hapiErrorHandler(err: {}, req?: hapi.Request, config?: Configuration) {
+ let service = '';
+ let version: string | undefined = '';
+
+ if (config?.toString() === '[object Object]') {
+ service = config!.getServiceContext().service;
+ version = config!.getServiceContext().version;
+ }
+
+ const em = new ErrorMessage()
+ .consumeRequestInformation(hapiRequestInformationExtractor(req))
+ .setServiceContext(service, version);
+
+ populateErrorMessage(err, em);
+
+ return em;
+}
+
+/**
+ * Creates a Hapi plugin object which can be used to handle errors in Hapi.
+ * @param {AuthClient} client - an inited auth client instance
+ * @param {NormalizedConfigurationVariables} config - the environmental
+ * configuration
+ * @returns {Object} - the actual Hapi plugin
+ */
+export function makeHapiPlugin(client: RequestHandler, config: Configuration) {
+ /**
+ * The register function serves to attach the hapiErrorHandler to specific
+ * points in the hapi request-response lifecycle. Namely: it attaches to the
+ * 'request-error' event in Hapi which is emitted when a plugin or receiver
+ * throws an error while executing and the 'onPreResponse' event to intercept
+ * error code carrying requests before they are sent back to the client so
+ * that the errors can be logged to the Error Reporting API.
+ * @function hapiRegisterFunction
+ * @param {Hapi.Server} server - A Hapi server instance
+ * @param {Object} options - The server configuration options object
+ * @param {Function} next - The Hapi callback to move execution to the next
+ * plugin
+ * @returns {Undefined} - returns the execution of the next callback
+ */
+ function hapiRegisterFunction(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ server: any,
+ options: {},
+ next?: Function,
+ ) {
+ if (server) {
+ if (server.events && server.events.on) {
+ // Hapi 17 is being used
+ server.events.on('log', (event: {error?: {}; channel: string}) => {
+ if (event.error && event.channel === 'app') {
+ client.sendError(hapiErrorHandler(event.error));
+ }
+ });
+
+ server.events.on(
+ 'request',
+ (request: hapi.Request, event: {error?: {}; channel: string}) => {
+ if (event.error && event.channel === 'error') {
+ client.sendError(hapiErrorHandler(event.error, request));
+ }
+ },
+ );
+ } else {
+ if (typeof server.on === 'function') {
+ server.on('request-error', (req: hapi.Request, err: {}) => {
+ client.sendError(hapiErrorHandler(err, req, config));
+ });
+ }
+
+ if (typeof server.ext === 'function') {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ server.ext('onPreResponse', (request: hapi.Request, reply: any) => {
+ if (
+ request?.toString() === '[object Object]' &&
+ request.response &&
+ (request.response as unknown as boom).isBoom
+ ) {
+ // Cast to {} is necessary, as@types/hapi@16 incorrectly types
+ // response as 'Response | null' instead of 'Response | Boom |
+ // null'.
+ const boom = request.response as {} as Error;
+ const em = hapiErrorHandler(
+ new Error(boom.message),
+ request,
+ config,
+ );
+ client.sendError(em);
+ }
+
+ if (reply && typeof reply.continue === 'function') {
+ reply.continue();
+ }
+ });
+ }
+ }
+ }
+
+ if (typeof next === 'function') {
+ return next!();
+ }
+ }
+
+ const hapiPlugin = {
+ register: hapiRegisterFunction,
+ name: packageJson.name,
+ version: packageJson.version,
+ };
+
+ (hapiPlugin.register as {} as {attributes: {}}).attributes = {
+ name: packageJson.name,
+ version: packageJson.version,
+ };
+
+ return hapiPlugin;
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/koa.ts b/handwritten/nodejs-error-reporting/src/interfaces/koa.ts
new file mode 100644
index 00000000000..5d71fddb6ca
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/koa.ts
@@ -0,0 +1,63 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {Request, Response} from 'koa';
+
+import {ErrorMessage} from '../classes/error-message';
+import {Configuration} from '../configuration';
+import {RequestHandler} from '../google-apis/auth-client';
+import {populateErrorMessage} from '../populate-error-message';
+import {koaRequestInformationExtractor} from '../request-extractors/koa';
+
+/**
+ * The koaErrorHandler should be placed at the beginning of the koa middleware
+ * stack and should catch the yield of the output of the request handling chain.
+ * The Koa error handler returns the actual error handler which will be used in
+ * the request chain handling and this function corresponds to the format given
+ * in: https://github.com/koajs/koa/wiki/Error-Handling.
+ * @function koaErrorHandler
+ * @param {AuthClient} - The API client instance to report errors to Google Cloud
+ * @param {NormalizedConfigurationVariables} - The application configuration
+ * @returns {Function} - The function used to catch errors yielded by downstream
+ * request handlers.
+ */
+export function koaErrorHandler(client: RequestHandler, config: Configuration) {
+ /**
+ * The actual error handler for the Koa plugin attempts to yield the results
+ * of downstream request handlers and will attempt to catch errors emitted by
+ * these handlers.
+ * @param {Function} next - the result of the request handlers to yield
+ * @returns {Undefined} does not return anything
+ */
+ return function* (
+ this: {request: Request; response: Response},
+ next: Function,
+ ) {
+ const svc = config.getServiceContext();
+
+ try {
+ yield next();
+ } catch (err) {
+ const em = new ErrorMessage()
+ .consumeRequestInformation(
+ koaRequestInformationExtractor(this.request, this.response),
+ )
+ .setServiceContext(svc.service, svc.version);
+
+ populateErrorMessage(err, em);
+
+ client.sendError(em);
+ }
+ };
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/koa2.ts b/handwritten/nodejs-error-reporting/src/interfaces/koa2.ts
new file mode 100644
index 00000000000..3c6baed77ff
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/koa2.ts
@@ -0,0 +1,71 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {Request, Response} from 'koa';
+
+import {ErrorMessage} from '../classes/error-message';
+import {Configuration} from '../configuration';
+import {RequestHandler} from '../google-apis/auth-client';
+import {populateErrorMessage} from '../populate-error-message';
+import {koaRequestInformationExtractor} from '../request-extractors/koa';
+
+interface KoaContext {
+ request: Request;
+ response: Response;
+}
+type KoaNext = Function;
+
+/**
+ * The koaErrorHandler should be placed at the beginning of the koa middleware
+ * stack and should catch the awaited output of the request handling chain. The
+ * Koa error handler returns the actual error handler which will be used in the
+ * request chain handling and this function corresponds to the format given in:
+ * https://github.com/koajs/koa/wiki/Error-Handling.
+ * @function koaErrorHandler
+ * @param {AuthClient} - The API client instance to report errors to Google Cloud
+ * @param {NormalizedConfigurationVariables} - The application configuration
+ * @returns {Function} - The function used to catch errors yielded by downstream
+ * request handlers.
+ */
+export function koa2ErrorHandler(
+ client: RequestHandler,
+ config: Configuration,
+) {
+ /**
+ * The actual error handler for the Koa plugin attempts to await the results
+ * of downstream request handlers and will attempt to catch errors emitted by
+ * these handlers.
+ * @param {Object} ctx - the result of the request handlers to await
+ * @param {Function} next - the result of the request handlers to await
+ * @returns {Undefined} does not return anything
+ */
+
+ return async (ctx: KoaContext, next: KoaNext) => {
+ const svc = config.getServiceContext();
+
+ try {
+ await next();
+ } catch (err) {
+ const em = new ErrorMessage()
+ .consumeRequestInformation(
+ koaRequestInformationExtractor(ctx.request, ctx.response),
+ )
+ .setServiceContext(svc.service, svc.version);
+
+ populateErrorMessage(err, em);
+
+ client.sendError(em);
+ }
+ };
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/manual.ts b/handwritten/nodejs-error-reporting/src/interfaces/manual.ts
new file mode 100644
index 00000000000..26a70e924aa
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/manual.ts
@@ -0,0 +1,173 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as http from 'http';
+
+import {ErrorMessage} from '../classes/error-message';
+import {Configuration, Logger} from '../configuration';
+import {RequestHandler} from '../google-apis/auth-client';
+import {populateErrorMessage} from '../populate-error-message';
+import {manualRequestInformationExtractor} from '../request-extractors/manual';
+import {Request} from '../request-extractors/manual';
+
+export type Callback = (
+ err: Error | null,
+ response: http.ServerResponse | null,
+ body: {},
+) => void;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type AnyError = any;
+
+/**
+ * The handler setup function serves to produce a bound instance of the
+ * reportManualError function with no bound context, a bound first arugment
+ * which is intended to be an initialized instance of the API client and a bound
+ * second argument which is the environmental configuration.
+ * @function handlerSetup
+ * @param {AuthClient} client - an initialized API client
+ * @param {NormalizedConfigurationVariables} config - the environmental
+ * configuration
+ * @param {Object} logger - The logger instance created when the library API has
+ * been initialized.
+ * @returns {reportManualError} - a bound version of the reportManualError
+ * function
+ */
+export function handlerSetup(
+ client: RequestHandler,
+ config: Configuration,
+ logger: Logger,
+) {
+ /**
+ * The interface for manually reporting errors to the Google Error API in
+ * application code.
+ * @param {Any|ErrorMessage} err - error information of any type or content.
+ * This can be of any type but by giving an instance of ErrorMessage as the
+ * error argument one can manually provide values to all fields of the
+ * potential payload.
+ * @param {Object} [request] - an object containing request information. This
+ * is expected to be an object similar to the Node/Express request object.
+ * @param {String} [customMessage] - an optional error message string that
+ * overrides the default message & stack trace. Message format must comply
+ * with message field requirements defined in the documentation:
+ * https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#reportederrorevent
+ * @param {Function} [callback] - a callback to be invoked once the message
+ * has been successfully submitted to the error reporting API or has failed
+ * after four attempts with the success or error response.
+ * @returns {ErrorMessage} - returns the error message created through with
+ * the parameters given.
+ */
+ function reportManualError(err: AnyError): ErrorMessage;
+ function reportManualError(err: AnyError, request: Request): ErrorMessage;
+ function reportManualError(
+ err: AnyError,
+ customMessage: string,
+ ): ErrorMessage;
+ function reportManualError(err: AnyError, callback: Callback): ErrorMessage;
+ function reportManualError(
+ err: AnyError,
+ request: Request,
+ callback: Callback,
+ ): ErrorMessage;
+ function reportManualError(
+ err: AnyError,
+ request: Request,
+ customMessage: string,
+ ): ErrorMessage;
+ function reportManualError(
+ err: AnyError,
+ customMessage: string,
+ callback: Callback,
+ ): ErrorMessage;
+ function reportManualError(
+ err: AnyError,
+ request: Request,
+ customMessage: string,
+ callback: Callback,
+ ): ErrorMessage;
+ function reportManualError(
+ err: AnyError,
+ request?: Request | Callback | string,
+ customMessage?: Callback | string,
+ callback?: Callback | {} | string,
+ ): ErrorMessage {
+ let em;
+ if (typeof request === 'string') {
+ // no request given
+ callback = customMessage;
+ customMessage = request as string;
+ request = undefined;
+ } else if (typeof request === 'function') {
+ // neither request nor customMessage given
+ callback = request;
+ request = undefined;
+ customMessage = undefined;
+ }
+
+ if (typeof customMessage === 'function') {
+ callback = customMessage;
+ customMessage = undefined;
+ }
+
+ if (err instanceof ErrorMessage) {
+ // The API expects the error to contain a stack trace. Thus we
+ // append the stack trace of the point where the error was
+ // constructed. See the `message-builder.js` file for more details.
+ const stackErr = err as ErrorMessage & {
+ _autoGeneratedStackTrace?: string;
+ };
+ if (stackErr._autoGeneratedStackTrace) {
+ err.setMessage(err.message + '\n' + stackErr._autoGeneratedStackTrace);
+ // Delete the property so that if the ErrorMessage is reported a
+ // second time, the stack trace is not appended a second time. Also,
+ // the API will not accept the ErrorMessage if it has additional
+ // properties.
+ delete stackErr._autoGeneratedStackTrace;
+ } else {
+ logger.warn(
+ 'Encountered a manually constructed error with message "' +
+ err.message +
+ '" but without a construction site ' +
+ 'stack trace. This error might not be visible in the ' +
+ 'error reporting console.',
+ );
+ }
+ em = err;
+ } else {
+ em = new ErrorMessage();
+ em.setServiceContext(
+ config.getServiceContext().service,
+ config.getServiceContext().version,
+ );
+ populateErrorMessage(err, em);
+ }
+
+ if (request?.toString() === '[object Object]') {
+ // TODO: Address this explicit cast
+ em.consumeRequestInformation(
+ manualRequestInformationExtractor(request as Request),
+ );
+ }
+
+ if (typeof customMessage === 'string') {
+ em.setMessage(customMessage as string);
+ }
+
+ // TODO: Address this type cast
+ client.sendError(em, callback as Callback);
+ return em;
+ }
+
+ return reportManualError;
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/message-builder.ts b/handwritten/nodejs-error-reporting/src/interfaces/message-builder.ts
new file mode 100644
index 00000000000..71ab0f31831
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/message-builder.ts
@@ -0,0 +1,61 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {buildStackTrace} from '../build-stack-trace';
+import {ErrorMessage} from '../classes/error-message';
+import {Configuration} from '../configuration';
+
+/**
+ * The handler setup function serves to produce a bound instance of the
+ * of a factory for ErrorMessage class instances with configuration-supplied
+ * service contexts automatically set.
+ * @function handlerSetup
+ * @param {NormalizedConfigurationVariables} config - the environmental
+ * configuration
+ * @returns {ErrorMessage} - a new ErrorMessage instance
+ */
+export function handlerSetup(config: Configuration) {
+ /**
+ * The interface for creating new instances of the ErrorMessage class which
+ * can be used to send custom payloads to the Error reporting service.
+ * @returns {ErrorMessage} - returns a new instance of the ErrorMessage class
+ */
+ function newMessage() {
+ // The API expects a reported error to contain a stack trace.
+ // However, users do not need to provide a stack trace for ErrorMessage
+ // objects built using the message builder. Instead, here we store
+ // the stack trace with the parts that reference the error-reporting's
+ // internals removed. Then when the error is reported, the stored
+ // stack trace will be appended to the user's message for the error.
+ //
+ // Note: The stack trace at the point where the user constructed the
+ // error is used instead of the stack trace where the error is
+ // reported to be consistent with the behavior of reporting a
+ // an error when reporting an actual Node.js Error object.
+ const cleanedStack = buildStackTrace('');
+
+ const em = new ErrorMessage().setServiceContext(
+ config.getServiceContext().service,
+ config.getServiceContext().version,
+ );
+ (
+ em as {} as {
+ _autoGeneratedStackTrace: string;
+ }
+ )._autoGeneratedStackTrace = cleanedStack;
+ return em;
+ }
+
+ return newMessage;
+}
diff --git a/handwritten/nodejs-error-reporting/src/interfaces/restify.ts b/handwritten/nodejs-error-reporting/src/interfaces/restify.ts
new file mode 100644
index 00000000000..68361974072
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/interfaces/restify.ts
@@ -0,0 +1,189 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as express from 'express';
+import * as restify from 'restify';
+
+import {ErrorMessage} from '../classes/error-message';
+import {Configuration} from '../configuration';
+import {RequestHandler} from '../google-apis/auth-client';
+import {populateErrorMessage} from '../populate-error-message';
+import * as expressRequestInformationExtractor from '../request-extractors/express';
+
+/**
+ * The restifyErrorHandler is responsible for taking the captured error, setting
+ * the serviceContext property on the corresponding ErrorMessage instance,
+ * routing the captured error to the right handler so that it can be correctly
+ * marshaled into the ErrorMessage instance and then attempting to send it to
+ * the Google Cloud API via the given API client instance.
+ * @function restifyErrorHandler
+ * @param {AuthClient} client - the API client
+ * @param {NormalizedConfigurationVariables} config - the application
+ * configuration
+ * @param {Any} err - the error being handled
+ * @param {ErrorMessage} - the error message instance container
+ * @returns {Undefined} - does not return anything
+ */
+function restifyErrorHandler(
+ client: RequestHandler,
+ config: Configuration,
+ err: {},
+ em: ErrorMessage,
+) {
+ const svc = config.getServiceContext();
+ em.setServiceContext(svc.service, svc.version);
+
+ populateErrorMessage(err, em);
+
+ client.sendError(em);
+}
+
+/**
+ * The restifyRequestFinishHandler will be called once the response has emitted
+ * the `finish` event and is now in its finalized state. This function will
+ * attempt to determine whether or not the body of response is an instance of
+ * the Error class or its status codes indicate that the response ended in an
+ * error state. If either of the preceding are true then the restifyErrorHandler
+ * will be called with the error to be routed to the Google Cloud service.
+ * @function restifyRequestFinishHandler
+ * @param {AuthClient} client - the API client
+ * @param {NormalizedConfigurationVariables} config - the application
+ * configuration
+ * @param {Object} req - the restify request
+ * @param {Object} res - the restify response
+ * @returns {Undefined} - does not return anything
+ */
+function restifyRequestFinishHandler(
+ client: RequestHandler,
+ config: Configuration,
+ req: restify.Request,
+ res: restify.Response,
+) {
+ let em;
+
+ // TODO: Address the fact that `_body` does not exist in `res`
+ if (
+ (res as {} as {_body: {}})._body instanceof Error ||
+ (res.statusCode > 309 && res.statusCode < 512)
+ ) {
+ em = new ErrorMessage().consumeRequestInformation(
+ // TODO: Address the type conflict with `req` and `res` and the types
+ // expected for `expressRequestInformationExtractor`
+ expressRequestInformationExtractor.expressRequestInformationExtractor(
+ req as {} as express.Request,
+ res as {} as express.Response,
+ ),
+ );
+
+ restifyErrorHandler(client, config, (res as {} as {_body: {}})._body, em);
+ }
+}
+
+/**
+ * The restifyRequestHandler attaches the restifyRequestFinishHandler to each
+ * responses 'finish' event wherein the callback function will determine
+ * whether or not the response is an error response or not. The finish event is
+ * used since the restify response object will not have any error information
+ * contained within it until the downstream request handlers have had the
+ * opportunity to deal with the request and create a contextually significant
+ * response.
+ * @function restifyRequestHandler
+ * @param {AuthClient} client - the API client
+ * @param {NormalizedConfigurationVariables} config - the application
+ * configuration
+ * @param {Object} req - the current request
+ * @param {Object} res - the current response
+ * @param {Function} next - the callback function to pass the request onto the
+ * downstream request handlers
+ * @returns {Any} - the result of the next function
+ */
+function restifyRequestHandler(
+ client: RequestHandler,
+ config: Configuration,
+ req: restify.Request,
+ res: restify.Response,
+ next: Function,
+) {
+ // TODO: Address the fact that a cast is needed to use `listener`
+ let listener = {};
+
+ if (
+ res?.toString() === '[object Object]' &&
+ typeof res.on === 'function' &&
+ typeof res.removeListener === 'function'
+ ) {
+ listener = () => {
+ restifyRequestFinishHandler(client, config, req, res);
+ res.removeListener(
+ 'finish',
+ listener as {} as (...args: Array<{}>) => void,
+ );
+ };
+
+ res.on('finish', listener as {} as (...args: Array<{}>) => void);
+ }
+
+ return next();
+}
+
+/**
+ * The serverErrorHandler is the actual function used by the restify error
+ * handling stack and should be used as a bound instance with its first two
+ * arguments (client & config) bound to it. The serverErrorHandler function must
+ * be given the restify server instance as a parameter so that it can listen
+ * to the `uncaughtException` event in the restify request handling stack. This
+ * event is emitted when an uncaught error is thrown inside a restify request
+ * handler. This init function will return the actual request handler function
+ * which will attach to outgoing responses, determine if they are instances of
+ * errors and then attempt to send this information to the Google Cloud API.
+ * @function serverErrorHandler
+ * @param {AuthClient} client - the API client
+ * @param {NormalizedConfigurationVariables} config - the application
+ * configuration
+ * @param {Object} server - the restify server instance
+ * @returns {Function} - the actual request error handler
+ */
+function serverErrorHandler(
+ client: RequestHandler,
+ config: Configuration,
+ server: restify.Server,
+) {
+ server.on('uncaughtException', (req, res, reqConfig, err) => {
+ const em = new ErrorMessage().consumeRequestInformation(
+ expressRequestInformationExtractor.expressRequestInformationExtractor(
+ req,
+ res,
+ ),
+ );
+
+ restifyErrorHandler(client, config, err, em);
+ });
+
+ return restifyRequestHandler.bind(null, client, config);
+}
+
+/**
+ * The handler setup function serves to provide a simple interface to init the
+ * the restify server error handler by binding the needed client and config
+ * variables to the error-handling chain.
+ * @function handlerSetup
+ * @param {AuthClient} client - the API client
+ * @param {NormalizedConfigurationVariables} config - the application
+ * configuration
+ * @returns {Function} - returns the serverErrorHandler function for use in the
+ * restify middleware stack
+ */
+export function handlerSetup(client: RequestHandler, config: Configuration) {
+ return serverErrorHandler.bind(null, client, config);
+}
diff --git a/handwritten/nodejs-error-reporting/src/logger.ts b/handwritten/nodejs-error-reporting/src/logger.ts
new file mode 100644
index 00000000000..fab7c0d228d
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/logger.ts
@@ -0,0 +1,88 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import consoleLogLevel = require('console-log-level');
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const packageJson = require('../../package.json');
+
+import {ConfigurationOptions, Logger} from './configuration';
+
+const LEVELNAMES: consoleLogLevel.LogLevelNames[] = [
+ 'fatal',
+ 'error',
+ 'warn',
+ 'info',
+ 'debug',
+ 'trace',
+];
+const DEFAULT_LEVEL = 2; // warn.
+
+function logLevelToName(level: number): consoleLogLevel.LogLevelNames {
+ if (typeof level === 'string') {
+ level = Number(level);
+ }
+ if (typeof level !== 'number') {
+ level = DEFAULT_LEVEL;
+ }
+ if (level < 0) level = 0;
+ if (level > 4) level = 4;
+ return LEVELNAMES[level];
+}
+
+/**
+ * Creates an instance of the a Logger class. This
+ * instance will be configured to log at the level given by the environment or
+ * the runtime configuration property `logLevel`. If neither of these inputs are
+ * given or valid then the logger will default to logging at log level `WARN`.
+ * Order of precedence for logging level is:
+ * 1) Environmental variable `GCLOUD_ERRORS_LOGLEVEL`
+ * 2) Runtime configuration property `logLevel`
+ * 3) Default log level of `WARN` (2)
+ * @function createLogger
+ * @param {ConfigurationOptions} initConfiguration - the desired project/error
+ * reporting configuration. Will look for the `logLevel` property which, if
+ * supplied, must be a number or stringified decimal representation of a
+ * number between and including 1 through 5
+ * @returns {Object} - returns an instance of the logger created with the given/
+ * default options
+ */
+export function createLogger(config?: ConfigurationOptions): Logger {
+ // Default to log level: warn (2)
+ let level = DEFAULT_LEVEL;
+ if (process.env.GCLOUD_ERRORS_LOGLEVEL) {
+ // Cast env string as integer
+ level = ~~process.env.GCLOUD_ERRORS_LOGLEVEL! || DEFAULT_LEVEL;
+ } else if (
+ config?.toString() === '[object Object]' &&
+ config?.logLevel !== undefined
+ ) {
+ if (typeof config!.logLevel === 'string') {
+ // Cast string as integer
+ level = ~~config!.logLevel! || DEFAULT_LEVEL;
+ } else if (typeof config!.logLevel === 'number') {
+ level = Number(config!.logLevel!) || DEFAULT_LEVEL;
+ } else {
+ throw new Error(
+ 'config.logLevel must be a number or decimal ' +
+ 'representation of a number in string form',
+ );
+ }
+ }
+ return consoleLogLevel({
+ stderr: true,
+ prefix: (level: string) => `${level.toUpperCase()}:${packageJson.name}:`,
+ level: logLevelToName(level),
+ });
+}
diff --git a/handwritten/nodejs-error-reporting/src/populate-error-message.ts b/handwritten/nodejs-error-reporting/src/populate-error-message.ts
new file mode 100644
index 00000000000..7d1384c52a6
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/populate-error-message.ts
@@ -0,0 +1,144 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as util from 'util';
+
+import {buildStackTrace} from './build-stack-trace';
+import {ErrorMessage} from './classes/error-message';
+
+export interface PopulatedObject {
+ message?: string;
+ user?: string;
+ filePath?: string;
+ lineNumber?: number;
+ functionName?: string;
+ serviceContext?: {service?: string; version?: string};
+}
+
+/**
+ * The Error handler router is responsible for taking an object of some type and
+ * and Error message container, analyzing the type of the object and marshalling
+ * the object's information into the error message container.
+ * @function populateErrorMessage
+ * @param {Any} ob - the object information to extract from
+ * @param {ErrorMessage} em - an instance of ErrorMessage to marshal object
+ * information into
+ * @returns {Undefined} - does not return a value
+ */
+// the `ob` argument can be anything, including `null` and `undefined`
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function populateErrorMessage(ob: any, em: ErrorMessage) {
+ if (ob === null || ob === undefined) {
+ em.setMessage(buildStackTrace('' + ob));
+ } else if ((ob as {stack: {}}).stack) {
+ populateFromError(ob as Error, em);
+ } else if (typeof ob === 'object' && ob?.toString() === '[object Object]') {
+ populateFromObject(ob, em);
+ } else {
+ em.setMessage(buildStackTrace(ob.toString()));
+ }
+
+ return em;
+}
+
+/**
+ * Extracts error information from an instance of the Error class and marshals
+ * that information into the provided instance of error message. This function
+ * will check before accessing any part of the error for propety presence but
+ * will not check the types of these property values that is instead work that
+ * is allocated to the error message instance itself.
+ * @function populateFromError
+ * @param {Error} err - the error instance
+ * @param {ErrorMessage} errorMessage - the error message instance to have the
+ * error information marshaled into
+ * @returns {Undefined} - does not return anything
+ */
+function populateFromError(
+ err: Error & PopulatedObject,
+ errorMessage: ErrorMessage,
+) {
+ errorMessage.setMessage(err.stack!);
+
+ if (err?.user !== undefined) {
+ errorMessage.setUser(err.user!);
+ }
+
+ if (
+ err?.serviceContext !== undefined &&
+ err.serviceContext?.toString() === '[object Object]'
+ ) {
+ errorMessage.setServiceContext(
+ err.serviceContext!.service!,
+ err.serviceContext!.version,
+ );
+ }
+}
+
+/**
+ * Attempts to extract error information given an object as the input for the
+ * error. This function will check presence of each property before attempting
+ * to access the given property on the object but will not check for type
+ * compliance as that is allocated to the instance of the error message itself.
+ * @function populateFromObject
+ * @param {Object} ob - the Object given as the content of the error
+ * @param {String} [ob.message] - the error message
+ * @param {String} [ob.user] - the user the error occurred for
+ * @param {String} [ob.filePath] - the file path and file where the error
+ * occurred at
+ * @param {Number} [ob.lineNumber] - the line number where the error occurred
+ * at
+ * @param {String} [ob.functionName] - the function where the error occurred at
+ * @param {Object} [ob.serviceContext] - the service context object of the
+ * error
+ * @param {String} [ob.serviceContext.service] - the service the error occurred
+ * on
+ * @param {String} [ob.serviceContext.version] - the version of the application
+ * that the error occurred on
+ * @param {ErrorMessage} errorMessage - the error message instance to marshal
+ * error information into
+ * @returns {Undefined} - does not return anything
+ */
+function populateFromObject(ob: PopulatedObject, errorMessage: ErrorMessage) {
+ if (ob?.message !== undefined) {
+ errorMessage.setMessage(ob.message!);
+ } else {
+ errorMessage.setMessage(buildStackTrace(util.inspect(ob)));
+ }
+
+ if (ob?.user !== undefined) {
+ errorMessage.setUser(ob.user!);
+ }
+
+ if (ob?.filePath !== undefined) {
+ errorMessage.setFilePath(ob.filePath!);
+ }
+
+ if (ob?.lineNumber !== undefined) {
+ errorMessage.setLineNumber(ob.lineNumber!);
+ }
+
+ if (ob?.functionName !== undefined) {
+ errorMessage.setFunctionName(ob.functionName!);
+ }
+
+ if (
+ ob?.serviceContext !== undefined &&
+ ob.serviceContext?.toString() === '[object Object]'
+ ) {
+ errorMessage.setServiceContext(
+ ob.serviceContext!.service!,
+ ob.serviceContext!.version,
+ );
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/src/request-extractors/express.ts b/handwritten/nodejs-error-reporting/src/request-extractors/express.ts
new file mode 100644
index 00000000000..79e06c72ba4
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/request-extractors/express.ts
@@ -0,0 +1,72 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as express from 'express';
+import {RequestInformationContainer} from '../classes/request-information-container';
+
+/**
+ * This function checks for the presence of an `x-forwarded-for` header on the
+ * request to check for remote address forwards, if that is header is not
+ * present in the request then the function will attempt to extract the remote
+ * address from the express request object.
+ * @function extractRemoteAddressFromRequest
+ * @param {Object} req - the express request object
+ * @returns {String} - the remote address or, if one cannot be found, an empty
+ * string
+ */
+function extractRemoteAddressFromRequest(req: express.Request) {
+ if (typeof req.header('x-forwarded-for') !== 'undefined') {
+ return req.header('x-forwarded-for');
+ } else if (req.connection?.toString() === '[object Object]') {
+ return req.connection.remoteAddress;
+ }
+
+ return '';
+}
+
+/**
+ * The expressRequestInformationExtractor is a function which is made to extract
+ * request information from a express request object. This function will do a
+ * basic check for type and method presence but will not check for the presence
+ * of properties on the request object.
+ * @function expressRequestInformationExtractor
+ * @param {Object} req - the express request object
+ * @param {Object} res - the express response object
+ * @returns {RequestInformationContainer} - an object containing the request
+ * information in a standardized format
+ */
+export function expressRequestInformationExtractor(
+ req: express.Request,
+ res: express.Response,
+) {
+ const returnObject = new RequestInformationContainer();
+
+ if (
+ req?.toString() !== '[object Object]' ||
+ typeof req.header !== 'function' ||
+ res?.toString() !== '[object Object]'
+ ) {
+ return returnObject;
+ }
+
+ returnObject
+ .setMethod(req.method)
+ .setUrl(req.url)
+ .setUserAgent(req.header('user-agent'))
+ .setReferrer(req.header('referrer'))
+ .setStatusCode(res.statusCode)
+ .setRemoteAddress(extractRemoteAddressFromRequest(req));
+
+ return returnObject;
+}
diff --git a/handwritten/nodejs-error-reporting/src/request-extractors/hapi.ts b/handwritten/nodejs-error-reporting/src/request-extractors/hapi.ts
new file mode 100644
index 00000000000..9b16eca0d36
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/request-extractors/hapi.ts
@@ -0,0 +1,102 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as boom from 'boom';
+
+import {RequestInformationContainer} from '../classes/request-information-container';
+import * as hapi from '@hapi/hapi';
+
+/**
+ * This function is used to check for a pending status code on the response
+ * or a set status code on the response.output object property. If neither of
+ * these properties can be found then -1 will be returned as the output.
+ * @function attemptToExtractStatusCode
+ * @param {Object} req - the request information object to extract from
+ * @returns {Number} - Either an HTTP status code or -1 in absence of an
+ * extractable status code.
+ */
+function attemptToExtractStatusCode(req: hapi.Request) {
+ // TODO: Handle the cases where `req.response` and `req.response.output` are
+ // `null` in this function
+ if (typeof req.response === 'object') {
+ if ('statusCode' in req.response) {
+ return (req.response as hapi.ResponseObject).statusCode;
+ } else if (
+ (req.response as unknown as boom).output?.toString() === '[object Object]'
+ ) {
+ return (req.response as unknown as boom).output.statusCode;
+ }
+ }
+ return 0;
+}
+
+/**
+ * This function is used to check for the x-forwarded-for header first to
+ * identify source IP connnections. If this header is not present, then the
+ * function will attempt to extract the remoteAddress from the request.info
+ * object property. If neither of these properties can be found then an empty
+ * string will be returned.
+ * @function extractRemoteAddressFromRequest
+ * @param {Object} req - the request information object to extract from
+ * @returns {String} - Either an empty string if the IP cannot be extracted or
+ * a string that represents the remote IP address
+ */
+function extractRemoteAddressFromRequest(req: hapi.Request) {
+ if ('x-forwarded-for' in req.headers) {
+ return req.headers['x-forwarded-for'];
+ } else if (req.info?.toString() === '[object Object]') {
+ return req.info.remoteAddress;
+ }
+
+ return '';
+}
+
+/**
+ * This function is used to extract request information from a hapi request
+ * object. This function will not check for the presence of properties on the
+ * request object.
+ * @function hapiRequestInformationExtractor
+ * @param {Object} req - the hapi request object to extract from
+ * @returns {RequestInformationContainer} - an object containing the request
+ * information in a standardized format
+ */
+export function hapiRequestInformationExtractor(req?: hapi.Request) {
+ const returnObject = new RequestInformationContainer();
+
+ if (
+ req?.toString() !== '[object Object]' ||
+ req!.headers?.toString() !== '[object Object]' ||
+ typeof req === 'function' ||
+ Array.isArray(req)
+ ) {
+ return returnObject;
+ }
+
+ let urlString: string;
+ if (typeof req!.url === 'string') {
+ urlString = req!.url as {} as string;
+ } else {
+ urlString = req!.url.pathname;
+ }
+
+ returnObject
+ .setMethod(req!.method)
+ .setUrl(urlString)
+ .setUserAgent(req!.headers['user-agent'])
+ .setReferrer(req!.headers.referrer)
+ .setStatusCode(attemptToExtractStatusCode(req!))
+ .setRemoteAddress(extractRemoteAddressFromRequest(req!));
+
+ return returnObject;
+}
diff --git a/handwritten/nodejs-error-reporting/src/request-extractors/koa.ts b/handwritten/nodejs-error-reporting/src/request-extractors/koa.ts
new file mode 100644
index 00000000000..feed0e58dfa
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/request-extractors/koa.ts
@@ -0,0 +1,56 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as koa from 'koa';
+
+import {RequestInformationContainer} from '../classes/request-information-container';
+
+/**
+ * The koaRequestInformationExtractor attempts to extract information from a Koa
+ * request/reponse set and marshal it into a RequestInformationContainer
+ * instance.
+ * @function koaRequestInformationExtractor
+ * @param {Object} req - the Koa request object
+ * @param {Object} res - the Koa response object
+ * @returns {RequestInformationContainer} - returns a request information
+ * container instance that may be in its initial state
+ */
+export function koaRequestInformationExtractor(
+ req: koa.Request,
+ res: koa.Response,
+) {
+ const returnObject = new RequestInformationContainer();
+
+ if (
+ req?.toString() !== '[object Object]' ||
+ res?.toString() !== '[object Object]' ||
+ typeof req === 'function' ||
+ typeof res === 'function' ||
+ Array.isArray(req) ||
+ Array.isArray(res) ||
+ req.headers?.toString() !== '[object Object]'
+ ) {
+ return returnObject;
+ }
+
+ returnObject
+ .setMethod(req.method)
+ .setUrl(req.url)
+ .setUserAgent(req.headers['user-agent'])
+ .setReferrer(req.headers.referrer as string)
+ .setStatusCode(res.status)
+ .setRemoteAddress(req.ip);
+
+ return returnObject;
+}
diff --git a/handwritten/nodejs-error-reporting/src/request-extractors/manual.ts b/handwritten/nodejs-error-reporting/src/request-extractors/manual.ts
new file mode 100644
index 00000000000..ca35e4741db
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/src/request-extractors/manual.ts
@@ -0,0 +1,80 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {RequestInformationContainer} from '../classes/request-information-container';
+
+export interface Request {
+ method?: string;
+ url?: string;
+ userAgent?: string;
+ referrer?: string;
+ statusCode?: number;
+ remoteAddress?: string;
+}
+
+/**
+ * The manualRequestInformationExtractor is meant to take a standard object
+ * and extract request information based on the inclusion of several properties.
+ * This function will check the presence of properties before attempting to
+ * access them on the object but it will not attempt to check for these
+ * properties types as this is allocated to the RequestInformationContainer.
+ * @function manualRequestInformationExtractor
+ * @param {Object} req - the request information object to extract from
+ * @param {String} [req.method] - the request method (ex GET, PUT, POST, DELETE)
+ * @param {String} [req.url] - the request url
+ * @param {String} [req.userAgent] - the requesters user-agent
+ * @param {String} [req.referrer] - the requesters referrer
+ * @param {Number} [req.statusCode] - the status code given in response to the
+ * request
+ * @param {String} [req.remoteAddress] - the remote address of the requester
+ * @returns {RequestInformationContainer} - an object containing the request
+ * information in a standardized format
+ */
+export function manualRequestInformationExtractor(req: Request) {
+ const returnObject = new RequestInformationContainer();
+
+ if (
+ req?.toString() !== '[object Object]' ||
+ Array.isArray(req) ||
+ typeof req === 'function'
+ ) {
+ return returnObject;
+ }
+
+ if (req?.method !== undefined) {
+ returnObject.setMethod(req.method!);
+ }
+
+ if (req?.url !== undefined) {
+ returnObject.setUrl(req.url!);
+ }
+
+ if (req?.userAgent !== undefined) {
+ returnObject.setUserAgent(req.userAgent);
+ }
+
+ if (req?.referrer !== undefined) {
+ returnObject.setReferrer(req.referrer);
+ }
+
+ if (req?.statusCode !== undefined) {
+ returnObject.setStatusCode(req.statusCode!);
+ }
+
+ if (req?.remoteAddress !== undefined) {
+ returnObject.setRemoteAddress(req.remoteAddress);
+ }
+
+ return returnObject;
+}
diff --git a/handwritten/nodejs-error-reporting/system-test/error-reporting.ts b/handwritten/nodejs-error-reporting/system-test/error-reporting.ts
new file mode 100644
index 00000000000..be53d9b9529
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/system-test/error-reporting.ts
@@ -0,0 +1,811 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, before, beforeEach, after, afterEach} from 'mocha';
+import * as nock from 'nock';
+
+import {ErrorReporting} from '../src';
+import {ErrorMessage} from '../src/classes/error-message';
+import {RequestHandler} from '../src/google-apis/auth-client';
+import {createLogger} from '../src/logger';
+import {FakeConfiguration as Configuration} from '../test/fixtures/configuration';
+import {ReportMode} from 'src/configuration';
+import {deepStrictEqual} from '../test/util';
+import {
+ ErrorGroupStats,
+ ErrorsApiTransport,
+} from '../utils/errors-api-transport';
+
+import * as uuid from 'uuid';
+import * as util from 'util';
+import * as path from 'path';
+
+const ERR_TOKEN = '_@google_STACKDRIVER_INTEGRATION_TEST_ERROR__';
+const TIMEOUT = 20 * 60 * 1000;
+
+const envKeys = [
+ 'GOOGLE_APPLICATION_CREDENTIALS',
+ 'GCLOUD_PROJECT',
+ 'NODE_ENV',
+];
+
+class InstancedEnv {
+ injectedEnv: {[key: string]: string | undefined};
+ _originalEnv: Partial>;
+ apiKey!: string;
+ projectId!: string;
+
+ constructor(injectedEnv: {[key: string]: string | undefined}) {
+ Object.assign(this, injectedEnv);
+ this.injectedEnv = injectedEnv;
+ this._originalEnv = this._captureProcessProperties();
+ }
+
+ _captureProcessProperties() {
+ const envVars = {...process.env};
+ Object.entries(envVars).forEach(
+ ([key, value]) =>
+ envKeys.includes(key) &&
+ typeof value !== 'string' &&
+ delete envVars[key],
+ );
+ return envVars;
+ }
+
+ sterilizeProcess() {
+ envKeys.forEach(key => delete process.env[key]);
+ return this;
+ }
+
+ setProjectId() {
+ Object.assign(process.env, {
+ GCLOUD_PROJECT: this.injectedEnv.projectId,
+ });
+ return this;
+ }
+
+ setProjectNumber() {
+ Object.assign(process.env, {
+ GCLOUD_PROJECT: this.injectedEnv.projectNumber,
+ });
+ return this;
+ }
+
+ setKeyFilename() {
+ Object.assign(process.env, {
+ GOOGLE_APPLICATION_CREDENTIALS: this.injectedEnv.keyFilename,
+ });
+ return this;
+ }
+
+ setProduction() {
+ Object.assign(process.env, {
+ NODE_ENV: 'production',
+ });
+ return this;
+ }
+
+ restoreProcessToOriginalState() {
+ Object.assign(process.env, this._originalEnv);
+ return this;
+ }
+
+ injected() {
+ return Object.assign({}, this.injectedEnv);
+ }
+}
+
+const env = new InstancedEnv({
+ projectId: process.env.GCLOUD_TESTS_PROJECT_ID,
+ keyFilename: process.env.GCLOUD_TESTS_KEY,
+ apiKey: process.env.GCLOUD_TESTS_API_KEY,
+ projectNumber: process.env.GCLOUD_TESTS_PROJECT_NUMBER,
+});
+
+function shouldRun() {
+ let shouldRun = true;
+ if (typeof env.injected().projectId !== 'string') {
+ console.log('The project id (projectId) was not set in the env');
+ shouldRun = false;
+ }
+
+ if (typeof env.injected().apiKey !== 'string') {
+ console.log('The api key (apiKey) was not set as an env variable');
+ shouldRun = false;
+ }
+
+ if (typeof env.injected().projectNumber !== 'string') {
+ console.log('The project number (projectNumber) was not set in the env');
+ shouldRun = false;
+ }
+
+ if (typeof env.injected().keyFilename !== 'string') {
+ console.log('The key filename (keyFilename) was not set in the env');
+ shouldRun = false;
+ }
+
+ return shouldRun;
+}
+
+function delay(ms: number) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+if (!shouldRun()) {
+ throw new Error('Skipping error-reporting system tests');
+}
+
+describe('Request/Response lifecycle mocking', () => {
+ const sampleError = new Error(ERR_TOKEN);
+ const errorMessage = new ErrorMessage().setMessage(sampleError.message);
+ let fakeService: {reply: Function; query: Function};
+ let client: RequestHandler;
+ let logger;
+ before(() => {
+ env.sterilizeProcess();
+ });
+
+ beforeEach(() => {
+ env.setProjectId().setKeyFilename().setProduction();
+ fakeService = nock(
+ 'https://clouderrorreporting.googleapis.com/v1beta1/projects/' +
+ process.env.GCLOUD_PROJECT,
+ )
+ .persist()
+ .post('/events:report?');
+ logger = createLogger({logLevel: 5});
+ client = new RequestHandler(
+ new Configuration({reportMode: 'always'}, logger),
+ logger,
+ );
+ });
+
+ afterEach(() => {
+ env.sterilizeProcess();
+ nock.cleanAll();
+ });
+
+ after(() => {
+ env.restoreProcessToOriginalState();
+ });
+
+ it('Should fail when receiving non-retryable errors', function (this, done) {
+ this.timeout(5000);
+ client.sendError({} as ErrorMessage, (err, response) => {
+ assert(err instanceof Error);
+ assert.strictEqual(
+ err!.message.toLowerCase(),
+ 'message cannot be empty.',
+ );
+ assert(response?.toString() === '[object Object]');
+ assert.strictEqual(response!.statusCode, 400);
+ done();
+ });
+ });
+
+ it('Should retry when receiving retryable errors', function (this, done) {
+ this.timeout(25000);
+ let tries = 0;
+ const intendedTries = 4;
+ fakeService.reply(429, () => {
+ tries += 1;
+ console.log('Mock Server Received Request:', tries + '/' + intendedTries);
+ return {error: 'Please try again later'};
+ });
+ client.sendError(errorMessage, () => {
+ assert.strictEqual(tries, intendedTries);
+ done();
+ });
+ });
+
+ it(
+ 'Should provide the key as a query string on outgoing requests when ' +
+ 'using an API key',
+ done => {
+ env.sterilizeProcess().setProjectId().setProduction();
+ const key = env.apiKey;
+ const logger = createLogger({logLevel: 5});
+ const client = new RequestHandler(
+ new Configuration({key, reportMode: 'always'}, logger),
+ logger,
+ );
+ const fakeService = nock(
+ 'https://clouderrorreporting.googleapis.com/v1beta1/projects/' +
+ process.env.GCLOUD_PROJECT,
+ )
+ .persist()
+ .post('/events:report');
+ fakeService.query({key}).reply(200, (uri: string) => {
+ assert(uri.indexOf('key=' + key) > -1);
+ return {};
+ });
+ client.sendError(errorMessage, () => {
+ done();
+ });
+ },
+ );
+
+ it('Should still execute the request with a callback-less invocation', done => {
+ fakeService.reply(200, () => {
+ done();
+ });
+ client.sendError(errorMessage);
+ });
+});
+
+describe('Client creation', () => {
+ const sampleError = new Error(ERR_TOKEN);
+ const errorMessage = new ErrorMessage().setMessage(sampleError.stack!);
+ after(() => {
+ env.sterilizeProcess();
+ });
+
+ it(
+ 'Should not throw on initialization when using only project id as a ' +
+ 'runtime argument',
+ function (this, done) {
+ env.sterilizeProcess().setKeyFilename();
+ const logger = createLogger({logLevel: 5});
+ const cfg = new Configuration(
+ {
+ projectId: env.injected().projectId,
+ reportMode: 'always',
+ },
+ logger,
+ );
+ this.timeout(10000);
+ assert.doesNotThrow(() => {
+ new RequestHandler(cfg, logger).sendError(
+ errorMessage,
+ (err, response, body) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(response!.statusCode, 200);
+ assert(
+ body?.toString() === '[object Object]' &&
+ Object.keys(body).length === 0,
+ );
+ done();
+ },
+ );
+ });
+ },
+ );
+
+ it(
+ 'Should not throw on initialization when using only project id as an ' +
+ 'env variable',
+ function (this, done) {
+ env.sterilizeProcess().setProjectId().setKeyFilename();
+ const logger = createLogger({logLevel: 5});
+ const cfg = new Configuration({reportMode: 'always'}, logger);
+ this.timeout(10000);
+ assert.doesNotThrow(() => {
+ new RequestHandler(cfg, logger).sendError(
+ errorMessage,
+ (err, response, body) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(response!.statusCode, 200);
+ assert(
+ body?.toString() === '[object Object]' &&
+ Object.keys(body).length === 0,
+ );
+ done();
+ },
+ );
+ });
+ },
+ );
+
+ it(
+ 'Should not throw on initialization when using only project number as ' +
+ 'a runtime argument',
+ function (this, done) {
+ env.sterilizeProcess().setKeyFilename();
+ const logger = createLogger({logLevel: 5});
+ const cfg = new Configuration(
+ {
+ projectId: '' + Number(env.injected().projectNumber),
+ reportMode: 'always',
+ },
+ logger,
+ );
+ this.timeout(10000);
+ assert.doesNotThrow(() => {
+ new RequestHandler(cfg, logger).sendError(
+ errorMessage,
+ (err, response, body) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(response!.statusCode, 200);
+ assert(
+ body?.toString() === '[object Object]' &&
+ Object.keys(body).length === 0,
+ );
+ done();
+ },
+ );
+ });
+ },
+ );
+
+ it(
+ 'Should not throw on initialization when using only project number as ' +
+ 'an env variable',
+ function (this, done) {
+ env.sterilizeProcess().setKeyFilename().setProjectNumber();
+ const logger = createLogger({logLevel: 5});
+ const cfg = new Configuration({reportMode: 'always'}, logger);
+ this.timeout(10000);
+ assert.doesNotThrow(() => {
+ new RequestHandler(cfg, logger).sendError(
+ errorMessage,
+ (err, response, body) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(response!.statusCode, 200);
+ assert(
+ body?.toString() === '[object Object]' &&
+ Object.keys(body).length === 0,
+ );
+ done();
+ },
+ );
+ });
+ },
+ );
+});
+
+describe('Expected Behavior', () => {
+ const ERROR_STRING = [
+ 'The error reporting client is configured to report errors',
+ 'if and only if the NODE_ENV environment variable is set to "production".',
+ 'Errors will not be reported. To have errors always reported, regardless of the',
+ 'value of NODE_ENV, set the reportMode configuration option to "always".',
+ ].join(' ');
+
+ const er = new Error(ERR_TOKEN);
+ const em = new ErrorMessage().setMessage(er.stack!);
+
+ after(() => {
+ env.sterilizeProcess();
+ });
+
+ it('Should not call auth and should callback with an error in a configuration that cannot report errors', done => {
+ env.sterilizeProcess().setKeyFilename().setProjectId();
+ const scope = nock('https://www.googleapis.com:443')
+ .post('/oauth2/v4/token')
+ .reply(400);
+ process.env.NODE_ENV = 'null';
+ const logger = createLogger({logLevel: 5, reportMode: 'production'});
+ const client = new RequestHandler(
+ new Configuration(undefined, logger),
+ logger,
+ );
+
+ client.sendError({} as ErrorMessage, (err, response) => {
+ assert.strictEqual(scope.isDone(), false);
+ nock.cleanAll();
+ assert(err instanceof Error);
+ assert.strictEqual(err!.message, ERROR_STRING);
+ assert.strictEqual(response, null);
+ done();
+ });
+ });
+
+ it('Should succeed in its request given a valid project id', done => {
+ env.sterilizeProcess().setKeyFilename();
+ const logger = createLogger({logLevel: 5});
+ const cfg = new Configuration(
+ {
+ projectId: env.injected().projectId,
+ reportMode: 'always',
+ },
+ logger,
+ );
+ const client = new RequestHandler(cfg, logger);
+
+ client.sendError(em, (err, response, body) => {
+ assert.strictEqual(err, null);
+ assert(body?.toString() === '[object Object]');
+ assert(Object.keys(body).length === 0);
+ assert.strictEqual(response!.statusCode, 200);
+ done();
+ });
+ });
+
+ it('Should succeed in its request given a valid project number', done => {
+ env.sterilizeProcess().setKeyFilename();
+ const logger = createLogger({logLevel: 5});
+ const cfg = new Configuration(
+ {
+ projectId: '' + Number(env.injected().projectNumber),
+ reportMode: 'always',
+ },
+ logger,
+ );
+ const client = new RequestHandler(cfg, logger);
+ client.sendError(em, (err, response, body) => {
+ assert.strictEqual(err, null);
+ assert(body?.toString() === '[object Object]');
+ assert(Object.keys(body).length === 0);
+ assert.strictEqual(response!.statusCode, 200);
+ done();
+ });
+ });
+});
+
+describe('error-reporting', () => {
+ const SRC_ROOT = path.join(__dirname, '..', 'src');
+ const UUID = uuid.v4();
+ const BASE_NAME = 'error-reporting-system-test';
+ function buildName(suffix: string) {
+ return [UUID, BASE_NAME, suffix].join('_');
+ }
+
+ const SERVICE = buildName('service-name');
+ const VERSION = buildName('service-version');
+ const PAGE_SIZE = 1000;
+
+ let errors: ErrorReporting;
+ let transport: ErrorsApiTransport;
+ let oldLogger: (text: string) => void;
+ let logOutput = '';
+ before(async () => {
+ // This test assumes that the error reporting library will be adding listeners
+ // to the 'unhandledRejection' event. Thus we need to make sure other default
+ // listeners do not interfere. If this check fails, then update the reinitialize
+ // method below to more carefully reinitialize the error-reporting library.
+ assert.strictEqual(process.listenerCount('unhandledRejection'), 1);
+ oldLogger = console.error;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ console.error = function (this, ...args: any[]) {
+ const text = util.format(null, args);
+ oldLogger(text);
+ logOutput += text;
+ };
+ reinitialize();
+ });
+
+ function reinitialize(extraConfig?: {}) {
+ for (const listener of process.listeners('unhandledRejection')) {
+ // Do not interfere with existing Mocha listener
+ if (!listener.toString().includes('isMochaError')) {
+ process.removeListener('unhandledRejection', listener);
+ }
+ }
+
+ const initConfiguration = Object.assign(
+ {
+ reportMode: 'always' as ReportMode,
+ serviceContext: {
+ service: SERVICE,
+ version: VERSION,
+ },
+ projectId: env.projectId,
+ keyFilename: process.env.GCLOUD_TESTS_KEY,
+ },
+ extraConfig || {},
+ );
+ errors = new ErrorReporting(initConfiguration);
+ const logger = createLogger(initConfiguration);
+ const configuration = new Configuration(initConfiguration, logger);
+ transport = new ErrorsApiTransport(configuration, logger);
+ }
+
+ afterEach(() => {
+ logOutput = '';
+ });
+
+ async function verifyAllGroups(
+ messageTest: (message: string) => void,
+ maxCount: number,
+ timeout: number,
+ ) {
+ const start = Date.now();
+ let groups: ErrorGroupStats[] = [];
+ const shouldContinue = () =>
+ groups.length < maxCount && Date.now() - start <= timeout;
+ while (shouldContinue()) {
+ let prevPageToken: string | undefined;
+ let allGroups: ErrorGroupStats[] | undefined;
+ while (shouldContinue() && (!allGroups || allGroups.length > 0)) {
+ const response = await transport.getAllGroups(
+ SERVICE,
+ VERSION,
+ PAGE_SIZE,
+ prevPageToken,
+ );
+ prevPageToken = response.nextPageToken;
+ allGroups = response.errorGroupStats || [];
+ assert.ok(
+ allGroups,
+ 'Failed to get groups from the Error Reporting API',
+ );
+
+ const filteredGroups = allGroups!.filter(errItem => {
+ return (
+ errItem &&
+ errItem.representative &&
+ errItem.representative.serviceContext &&
+ errItem.representative.serviceContext.service === SERVICE &&
+ errItem.representative.serviceContext.version === VERSION &&
+ messageTest(errItem.representative.message)
+ );
+ });
+ groups = groups.concat(filteredGroups);
+ await delay(15000);
+ }
+ }
+
+ return groups;
+ }
+
+ async function verifyServerResponse(
+ messageTest: (message: string) => void,
+ maxCount: number,
+ timeout: number,
+ ) {
+ const matchedErrors = await verifyAllGroups(messageTest, maxCount, timeout);
+ assert.strictEqual(
+ matchedErrors.length,
+ maxCount,
+ `Expected to find ${maxCount} error items but found ${
+ matchedErrors.length
+ }: ${JSON.stringify(matchedErrors, null, 2)}`,
+ );
+ const errItem = matchedErrors[0];
+ assert.ok(
+ errItem,
+ 'Retrieved an error item from the Error Reporting API but it is falsy.',
+ );
+ const rep = errItem.representative;
+ assert.ok(rep, 'Expected the error item to have representative');
+ // Ensure the stack trace in the message does not contain any frames
+ // specific to the error-reporting library.
+ assert.strictEqual(
+ rep.message.indexOf(SRC_ROOT),
+ -1,
+ `Expected the error item's representative's message to start with ${SRC_ROOT} but found '${rep.message}'`,
+ );
+ // Ensure the stack trace in the mssage contains the frame corresponding
+ // to the 'expectedTopOfStack' function because that is the name of
+ // function used in this file that is the topmost function in the call
+ // stack that is not internal to the error-reporting library.
+ // This ensures that only the frames specific to the
+ // error-reporting library are removed from the stack trace.
+ const expectedTopOfStack = 'expectedTopOfStack';
+ assert.notStrictEqual(
+ rep.message.indexOf(expectedTopOfStack),
+ -1,
+ `Expected the error item's representative's message to not contain ${expectedTopOfStack} but found '${rep.message}'`,
+ );
+ const context = rep.serviceContext;
+ assert.ok(
+ context,
+ "Expected the error item's representative to have a context",
+ );
+ assert.strictEqual(context.service, SERVICE);
+ assert.strictEqual(context.version, VERSION);
+ }
+
+ // the `errOb` argument can be anything, including `null` and `undefined`
+ async function verifyReporting(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ errOb: any,
+ messageTest: (message: string) => void,
+ maxCount: number,
+ timeout: number,
+ ) {
+ function expectedTopOfStack() {
+ return new Promise((resolve, reject) => {
+ errors.report(
+ errOb,
+ undefined,
+ undefined,
+ async (err, response, body) => {
+ try {
+ assert.ifError(err);
+ assert(response?.toString() === '[object Object]');
+ deepStrictEqual(body, {});
+ await verifyServerResponse(messageTest, maxCount, timeout);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ },
+ );
+ });
+ }
+ await expectedTopOfStack();
+ }
+
+ // For each test below, after an error is reported, the test waits
+ // TIMEOUT ms before verifying the error has been reported to ensure
+ // the system had enough time to receive the error report and process it.
+ // As such, each test is set to fail due to a timeout only if sufficiently
+ // more than TIMEOUT ms has elapsed to avoid test fragility.
+
+ it.skip('Should correctly publish an error that is an Error object', async function verifyErrors() {
+ this.timeout(TIMEOUT);
+ const errorId = buildName('with-error-constructor');
+ function expectedTopOfStack() {
+ return new Error(errorId);
+ }
+ const errOb = expectedTopOfStack();
+ await verifyReporting(
+ errOb,
+ message => {
+ return message.startsWith('Error: ' + errorId + '\n');
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it('Should correctly publish an error that is a string', async function (this) {
+ this.timeout(TIMEOUT);
+ const errorId = buildName('with-string');
+ await verifyReporting(
+ errorId,
+ message => {
+ return message.startsWith(errorId + '\n');
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it.skip('Should correctly publish an error that is undefined', async function (this) {
+ this.timeout(TIMEOUT);
+ await verifyReporting(
+ undefined,
+ message => {
+ return message.startsWith('undefined\n');
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it.skip('Should correctly publish an error that is null', async function (this: any) {
+ this.timeout(TIMEOUT);
+ await verifyReporting(
+ null,
+ message => {
+ return message.startsWith('null\n');
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it('Should correctly publish an error that is a plain object', async function (this) {
+ this.timeout(TIMEOUT);
+ await verifyReporting(
+ {someKey: 'someValue'},
+ message => {
+ return message.startsWith("{ someKey: 'someValue' }\n");
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it('Should correctly publish an error that is a number', async function (this) {
+ this.timeout(TIMEOUT);
+ const num = new Date().getTime();
+ await verifyReporting(
+ num,
+ message => {
+ return message.startsWith('' + num + '\n');
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it.skip('Should correctly publish an error that is of an unknown type', async function (this) {
+ this.timeout(TIMEOUT);
+ const bool = true;
+ await verifyReporting(
+ bool,
+ message => {
+ return message.startsWith('true\n');
+ },
+ 1,
+ TIMEOUT,
+ );
+ });
+
+ it('Should correctly publish errors using an error builder', async function (this) {
+ this.timeout(TIMEOUT);
+ const errorId = buildName('with-error-builder');
+ // Use an IIFE with the name `definitionSiteFunction` to use later to
+ // ensure the stack trace of the point where the error message was
+ // constructed is used. Use an IIFE with the name `expectedTopOfStack` so
+ // that the test can verify that the stack trace used does not contain
+ // any frames specific to the error-reporting library.
+ function definitionSiteFunction() {
+ function expectedTopOfStack() {
+ return errors.event().setMessage(errorId);
+ }
+ return expectedTopOfStack();
+ }
+ const errOb = definitionSiteFunction();
+ async function callingSiteFunction() {
+ await verifyReporting(
+ errOb,
+ message => {
+ // Verify that the stack trace of the constructed error
+ // uses the stack trace at the point where the error was constructed
+ // and not the stack trace at the point where the `report` method
+ // was called.
+ return (
+ message.startsWith(errorId) &&
+ message.indexOf('callingSiteFunction') === -1 &&
+ message.indexOf('definitionSiteFunction') !== -1
+ );
+ },
+ 1,
+ TIMEOUT,
+ );
+ }
+ await callingSiteFunction();
+ });
+
+ it('Should report unhandledRejections if enabled', async function (this) {
+ this.timeout(TIMEOUT);
+ reinitialize({reportUnhandledRejections: true});
+ const rejectValue = buildName('report-promise-rejection');
+ function expectedTopOfStack() {
+ // An Error is used for the rejection value so that it's stack
+ // contains the stack trace at the point the rejection occured and is
+ // rejected within a function named `expectedTopOfStack` so that the
+ // test can verify that the collected stack is correct.
+ void Promise.reject(new Error(rejectValue));
+ }
+ expectedTopOfStack();
+ const rejectText = 'Error: ' + rejectValue;
+ const expected =
+ 'UnhandledPromiseRejectionWarning: Unhandled ' +
+ 'promise rejection: ' +
+ rejectText +
+ '. This rejection has been reported to the ' +
+ 'Google Cloud Platform error-reporting console.';
+ await delay(10000);
+ assert.notStrictEqual(logOutput.indexOf(expected), -1);
+ });
+
+ it('Should not report unhandledRejections if disabled', async function (this) {
+ this.timeout(TIMEOUT);
+ reinitialize({reportUnhandledRejections: false});
+ const rejectValue = buildName('do-not-report-promise-rejection');
+ function expectedTopOfStack() {
+ // An Error is used for the rejection value so that it's stack
+ // contains the stack trace at the point the rejection occured and is
+ // rejected within a function named `expectedTopOfStack` so that the
+ // test can verify that the collected stack is correct.
+ void Promise.reject(new Error(rejectValue));
+ }
+ expectedTopOfStack();
+ const rejectText = 'Error: ' + rejectValue;
+ const expected =
+ 'UnhandledPromiseRejectionWarning: Unhandled ' +
+ 'promise rejection: ' +
+ rejectText +
+ '. This rejection has been reported to the ' +
+ 'Google Cloud Platform error-reporting console.';
+ await delay(10000);
+ assert.strictEqual(logOutput.indexOf(expected), -1);
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/system-test/test-install.ts b/handwritten/nodejs-error-reporting/system-test/test-install.ts
new file mode 100644
index 00000000000..bd7c543df27
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/system-test/test-install.ts
@@ -0,0 +1,383 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {packNTest} from 'pack-n-play';
+import {describe, it} from 'mocha';
+
+describe('pack-n-play', () => {
+ const TS_CODE_SAMPLES = [
+ {
+ ts: `import * as errorReporting from '@google-cloud/error-reporting';
+new errorReporting.ErrorReporting();`,
+ description: 'imports the module using * syntax',
+ },
+ {
+ ts: `import {ErrorReporting} from '@google-cloud/error-reporting';
+new ErrorReporting();`,
+ description: 'imports the module with {} syntax',
+ },
+ {
+ ts: `import {ErrorReporting} from '@google-cloud/error-reporting';
+new ErrorReporting({
+ serviceContext: {
+ service: 'some service'
+ }
+});`,
+ description:
+ 'imports the module and starts with a partial `serviceContext`',
+ },
+ {
+ ts: `import {ErrorReporting} from '@google-cloud/error-reporting';
+new ErrorReporting({
+ projectId: 'some-project',
+ serviceContext: {
+ service: 'Some service',
+ version: 'Some version'
+ }
+});`,
+ description:
+ 'imports the module and starts with a complete `serviceContext`',
+ },
+ {
+ ts: `import * as express from 'express';
+
+import {ErrorReporting} from '@google-cloud/error-reporting';
+const errors = new ErrorReporting();
+
+const app = express();
+
+app.get('/error', (req, res, next) => {
+ res.send('Something broke!');
+ next!(new Error('Custom error message'));
+});
+
+app.get('/exception', () => {
+ JSON.parse('{"malformedJson": true');
+});
+
+app.use(errors.express);
+`,
+ description: 'uses express',
+ dependencies: ['express@4.x.x'],
+ devDependencies: ['@types/express@4.x.x'],
+ },
+ {
+ ts: `import * as hapi from 'hapi';
+
+import {ErrorReporting} from '@google-cloud/error-reporting';
+const errors = new ErrorReporting();
+
+const server = new hapi.Server();
+server.connection({ port: 3000 });
+
+server.route({
+ method: 'GET',
+ path: '/error',
+ handler: (request, reply) => {
+ reply('Something broke!');
+ throw new Error('Custom error message');
+ }
+});
+
+server.register(errors.hapi);
+`,
+ description: 'uses hapi16',
+ dependencies: ['hapi@16.x.x'],
+ devDependencies: ['@types/hapi@16.x.x'],
+ },
+ {
+ ts: `import * as hapi from 'hapi';
+
+import {ErrorReporting} from '@google-cloud/error-reporting';
+const errors = new ErrorReporting();
+
+async function start() {
+ const server = new hapi.Server({
+ host: '0.0.0.0',
+ port: 3000
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/error',
+ handler: async (request, h) => {
+ throw new Error(\`You requested an error at ${new Date()}\`);
+ }
+ });
+
+ await server.register(errors.hapi);
+}
+
+start().catch(console.error);
+`,
+ description: 'uses hapi17',
+ dependencies: ['hapi@17.x.x'],
+ devDependencies: ['@types/hapi@17.x.x'],
+ },
+ {
+ ts: `import * as Koa from 'koa';
+
+import {ErrorReporting} from '@google-cloud/error-reporting';
+const errors = new ErrorReporting();
+
+const app = new Koa();
+
+app.use(errors.koa);
+
+app.use(function *(this: any): IterableIterator {
+ //This will set status and message
+ this.throw('Error Message', 500);
+});
+
+// response
+app.use(function *(this: any): IterableIterator {
+ this.body = 'Hello World';
+});
+`,
+ description: 'uses koa1',
+ dependencies: ['koa@3.x.x'],
+ devDependencies: ['@types/koa@3.x.x'],
+ },
+ {
+ ts: `import * as Koa from 'koa';
+
+import {ErrorReporting} from '@google-cloud/error-reporting';
+const errors = new ErrorReporting();
+
+const app = new Koa();
+
+app.use(errors.koa2);
+
+app.use(async (ctx: Koa.Context, next: {}) => {
+ //This will set status and message
+ ctx.throw('Error Message', 500);
+});
+
+// response
+app.use(async (ctx: Koa.Context, next: {}): Promise => {
+ ctx.body = 'Hello World';
+});
+`,
+ description: 'uses koa2',
+ dependencies: ['koa@2.x.x'],
+ devDependencies: ['@types/koa@2.x.x'],
+ },
+ {
+ ts: `import * as restify from 'restify';
+
+import {ErrorReporting} from '@google-cloud/error-reporting';
+const errors = new ErrorReporting();
+
+function respond(req: {}, res: {}, next: Function) {
+ next(new Error('this is a restify error'));
+}
+
+const server = restify.createServer();
+
+server.use(errors.restify(server));
+server.get('/hello/:name', respond);
+server.head('/hello/:name', respond);
+`,
+ description: 'uses restify',
+ dependencies: ['restify@11.x.x'],
+ devDependencies: ['@types/restify@^8.5.0'],
+ },
+ ];
+
+ const JS_CODE_SAMPLES = [
+ {
+ js: `const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+new ErrorReporting();`,
+ description: 'requires the module using Node 4+ syntax',
+ },
+ {
+ js: `const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+new ErrorReporting({
+ serviceContext: {
+ service: 'some service'
+ }
+});`,
+ description:
+ 'requires the module and starts with a partial `serviceContext`',
+ },
+ {
+ js: `const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+new ErrorReporting({
+ projectId: 'some-project',
+ serviceContext: {
+ service: 'Some service',
+ version: 'Some version'
+ }
+});`,
+ description:
+ 'requires the module and starts with a complete `serviceContext`',
+ },
+ {
+ js: `const express = require('express');
+
+const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+const errors = new ErrorReporting();
+
+const app = express();
+
+app.get('/error', (req, res, next) => {
+ res.send('Something broke!');
+ next(new Error('Custom error message'));
+});
+
+app.get('/exception', () => {
+ JSON.parse('{"malformedJson": true');
+});
+
+// Note that express error handling middleware should be attached after all
+// the other routes and use() calls. See [express docs][express-error-docs].
+app.use(errors.express);
+`,
+ description: 'uses express with require',
+ dependencies: ['express@4.x.x'],
+ },
+ {
+ js: `const hapi = require('hapi');
+
+const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+const errors = new ErrorReporting();
+
+const server = new hapi.Server();
+server.connection({ port: 3000 });
+
+server.route({
+ method: 'GET',
+ path: '/error',
+ handler: (request, reply) => {
+ reply('Something broke!');
+ throw new Error('Custom error message');
+ }
+});
+
+server.register(errors.hapi);
+`,
+ description: 'uses hapi16 with require',
+ dependencies: ['hapi@16.x.x'],
+ },
+ {
+ js: `const hapi = require('hapi');
+
+const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+const errors = new ErrorReporting();
+
+async function start() {
+ const server = new hapi.Server({
+ host: '0.0.0.0',
+ port: 3000
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/error',
+ handler: async (request, h) => {
+ throw new Error(\`You requested an error at ${new Date()}\`);
+ }
+ });
+
+ await server.register(errors.hapi);
+}
+
+start().catch(console.error);
+`,
+ description: 'uses hapi17 with require',
+ dependencies: ['hapi@17.x.x'],
+ },
+ {
+ js: `const Koa = require('koa');
+
+const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+const errors = new ErrorReporting();
+
+const app = new Koa();
+
+app.use(errors.koa);
+
+app.use(function *(next) {
+ //This will set status and message
+ this.throw('Error Message', 500);
+});
+
+// response
+app.use(function *(){
+ this.body = 'Hello World';
+});
+`,
+ description: 'uses koa1 with require',
+ dependencies: ['koa@1.x.x'],
+ },
+ {
+ js: `const Koa = require('koa');
+
+const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+const errors = new ErrorReporting();
+
+const app = new Koa();
+
+app.use(errors.koa2);
+
+app.use(async (ctx, next) => {
+ //This will set status and message
+ ctx.throw('Error Message', 500);
+});
+
+// response
+app.use(async (ctx, next) => {
+ ctx.body = 'Hello World';
+});
+`,
+ description: 'uses koa2 with require',
+ dependencies: ['koa@2.x.x'],
+ },
+ {
+ js: `const restify = require('restify');
+
+const ErrorReporting = require('@google-cloud/error-reporting').ErrorReporting;
+const errors = new ErrorReporting();
+
+function respond(req, res, next) {
+ next(new Error('this is a restify error'));
+}
+
+const server = restify.createServer();
+
+server.use(errors.restify(server));
+server.get('/hello/:name', respond);
+server.head('/hello/:name', respond);
+`,
+ description: 'uses restify with require',
+ dependencies: ['restify@11.x.x'],
+ },
+ ];
+
+ TS_CODE_SAMPLES.forEach(sample => {
+ it(sample.description, async () => {
+ await packNTest({
+ sample,
+ });
+ }).timeout(2 * 60 * 1000);
+ });
+
+ JS_CODE_SAMPLES.forEach(sample => {
+ it(sample.description, async () => {
+ await packNTest({
+ sample,
+ });
+ }).timeout(2 * 60 * 1000);
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/.eslintrc.yml b/handwritten/nodejs-error-reporting/test/.eslintrc.yml
new file mode 100644
index 00000000000..cd088a97818
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/.eslintrc.yml
@@ -0,0 +1,3 @@
+---
+rules:
+ node/no-unpublished-require: off
diff --git a/handwritten/nodejs-error-reporting/test/fixtures/configuration.ts b/handwritten/nodejs-error-reporting/test/fixtures/configuration.ts
new file mode 100644
index 00000000000..4e73d516541
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/fixtures/configuration.ts
@@ -0,0 +1,25 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {
+ Configuration,
+ ConfigurationOptions,
+ Logger,
+} from '../../src/configuration';
+
+export class FakeConfiguration extends Configuration {
+ constructor(config: ConfigurationOptions | undefined, logger?: Logger) {
+ super(config, logger || (({warn() {}} as {}) as Logger));
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/test/fixtures/gcloud-credentials.json b/handwritten/nodejs-error-reporting/test/fixtures/gcloud-credentials.json
new file mode 100644
index 00000000000..3499fcc9c3d
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/fixtures/gcloud-credentials.json
@@ -0,0 +1,6 @@
+{
+ "client_id": "x",
+ "client_secret": "y",
+ "refresh_token": "z",
+ "type": "authorized_user"
+}
diff --git a/handwritten/nodejs-error-reporting/test/test-servers/express_scaffold_server.ts b/handwritten/nodejs-error-reporting/test/test-servers/express_scaffold_server.ts
new file mode 100644
index 00000000000..f2eb5e35162
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/test-servers/express_scaffold_server.ts
@@ -0,0 +1,111 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+const WARNING_HEADER = '\n!! -WARNING-';
+const EXCLAMATION_LN = '\n!!';
+import * as express from 'express';
+const app = express();
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const errorHandler = require('../../src/index.js')({
+ onUncaughtException: 'report',
+ key: process.env.STUBBED_API_KEY,
+ projectId: process.env.STUBBED_PROJECT_NUM,
+});
+
+// eslint-disable-next-line no-console
+const log = console.log;
+
+app.use(express.json());
+
+app.post('/testErrorHandling', (req, res, next) => {
+ if (req.body?.test !== true) {
+ return next!(new Error('Error on Express Regular Error POST Route'));
+ } else {
+ res.send('Success');
+ res.end();
+ }
+});
+
+app.get('/customError', (req, res, next) => {
+ errorHandler.report(
+ 'Error on Express Custom Error GET Route',
+ (err: Error | null) => {
+ if (err) {
+ log(WARNING_HEADER);
+ log('Error in sending custom get error to api');
+ log(err);
+ log(EXCLAMATION_LN);
+ } else {
+ log(EXCLAMATION_LN);
+ log('Successfully sent custom get error to api');
+ log(EXCLAMATION_LN);
+ }
+ },
+ );
+
+ res.send('Success');
+ res.end();
+
+ next!();
+});
+
+app.get('/getError', (req, res, next) => {
+ return next!(new Error('Error on Express Regular Error GET Route'));
+});
+
+app.use(errorHandler.express);
+
+function throwUncaughtError() {
+ log('Throwing an uncaught error..');
+ throw new Error('This is an uncaught error');
+}
+
+function reportManualError() {
+ log('Reporting a manual error..');
+ errorHandler.report(
+ new Error('This is a manually reported error'),
+ null,
+ null,
+ (err: Error | null) => {
+ if (err) {
+ log(WARNING_HEADER);
+ log('Got an error in sending error information to the API');
+ log(err);
+ log(EXCLAMATION_LN);
+ } else {
+ log(EXCLAMATION_LN);
+ log('Successfully sent error information to the API');
+ log(EXCLAMATION_LN);
+ }
+
+ if (process.env.THROW_ON_STARTUP) {
+ throwUncaughtError();
+ }
+ },
+ );
+}
+log('reporting a manual error first');
+errorHandler.report(new Error('This is a test'), (err: Error | null) => {
+ log('reported first manual error');
+ if (err) {
+ log('Error was unable to be reported', err);
+ } else {
+ log('Error reported!');
+ }
+});
+
+app.listen(3000, () => {
+ log('Scaffold Server has been started on port 3000');
+ reportManualError();
+});
diff --git a/handwritten/nodejs-error-reporting/test/test-servers/hapi_scaffold_server.ts b/handwritten/nodejs-error-reporting/test/test-servers/hapi_scaffold_server.ts
new file mode 100644
index 00000000000..afa1481d1fb
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/test-servers/hapi_scaffold_server.ts
@@ -0,0 +1,50 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as Hapi from '@hapi/hapi';
+import {ErrorReporting} from '../../src/index';
+const errorHandler = new ErrorReporting();
+
+const server = new Hapi.Server({port: 3000});
+
+const log = console.log;
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const error = console.error;
+
+server.route({
+ method: 'GET',
+ path: '/get',
+ handler() {
+ log('Got a GET');
+ throw new Error('an error');
+ },
+});
+
+server.route({
+ method: 'POST',
+ path: '/post',
+ handler(request) {
+ log('Got a POST', request.payload);
+ throw new Error('An error on the hapi post route');
+ },
+});
+
+const startServer = async () => {
+ await server.register({plugin: errorHandler.hapi});
+ log('Plugin registered.');
+ await server.start();
+ log('Server running at', server.info!.uri);
+};
+
+void startServer();
diff --git a/handwritten/nodejs-error-reporting/test/test-servers/koa2_scaffold_server.ts b/handwritten/nodejs-error-reporting/test/test-servers/koa2_scaffold_server.ts
new file mode 100644
index 00000000000..7b2bd545230
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/test-servers/koa2_scaffold_server.ts
@@ -0,0 +1,57 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+// jscs doesn't understand koa..
+// jscs:disable
+
+import {ErrorReporting} from '../../src';
+const errorHandler = new ErrorReporting({
+ // TODO: Address the fact that this configuration
+ // option is now invalid.
+ onUncaughtException: 'report',
+} as {});
+import * as Koa from 'koa';
+const app = new Koa();
+
+app.use(errorHandler.koa);
+
+app.use(async (ctx: {throw: Function}, next: {}) => {
+ // This will set status and message
+ ctx.throw('Error Message', 500);
+ await next;
+});
+
+app.use(async (ctx: {set: Function}, next: {}) => {
+ const start = Date.now();
+ await next;
+ const ms = Date.now() - start;
+ ctx.set('X-Response-Time', ms + 'ms');
+});
+
+// logger
+
+app.use(async (ctx: {method: {}; url: {}}, next: {}) => {
+ const start = Date.now();
+ await next;
+ const ms = Date.now() - start;
+ // eslint-disable-next-line no-console
+ console.log('%s %s - %s', ctx.method, ctx.url, ms);
+});
+
+// response
+app.use(async (ctx: {body: string}, next: {}) => {
+ ctx.body = 'Hello World';
+ await next;
+});
+
+app.listen(3000);
diff --git a/handwritten/nodejs-error-reporting/test/test-servers/koa_scaffold_server.ts b/handwritten/nodejs-error-reporting/test/test-servers/koa_scaffold_server.ts
new file mode 100644
index 00000000000..8aa59009004
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/test-servers/koa_scaffold_server.ts
@@ -0,0 +1,55 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {ErrorReporting} from '../../src';
+const errorHandler = new ErrorReporting({
+ // TODO: Address the fact that this configuration
+ // option is now invalid.
+ onUncaughtException: 'report',
+} as {});
+import * as Koa from 'koa';
+const app = new Koa();
+
+app.use(errorHandler.koa);
+
+app.use(function* (this: {throw: Function}, next: {}) {
+ // This will set status and message
+ this.throw('Error Message', 500);
+ yield next;
+});
+
+app.use(function* (this: {set: Function}, next: {}) {
+ const start = Date.now();
+ yield next;
+ const ms = Date.now() - start;
+ this.set('X-Response-Time', ms + 'ms');
+});
+
+// logger
+
+app.use(function* (this: {method: {}; url: {}}, next: {}) {
+ const start = Date.now();
+ yield next;
+ const ms = Date.now() - start;
+ // eslint-disable-next-line no-console
+ console.log('%s %s - %s', this.method, this.url, ms);
+});
+
+// response
+app.use(function* (this: {body: string}, next: {}) {
+ this.body = 'Hello World';
+ yield next;
+});
+
+app.listen(3000);
diff --git a/handwritten/nodejs-error-reporting/test/test-servers/manual_scaffold_server.ts b/handwritten/nodejs-error-reporting/test/test-servers/manual_scaffold_server.ts
new file mode 100644
index 00000000000..695337fb79c
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/test-servers/manual_scaffold_server.ts
@@ -0,0 +1,26 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {ErrorReporting} from '../../src/index';
+const errors = new ErrorReporting();
+(errors.report as Function)('Sample test string', (err: Error | null) => {
+ // eslint-disable-next-line no-console
+ console.log(
+ 'Callback from report:\n',
+ '\tError: ',
+ err,
+ '\n',
+ '\tResponse Body:',
+ );
+});
diff --git a/handwritten/nodejs-error-reporting/test/test-servers/restify_scaffold_server.ts b/handwritten/nodejs-error-reporting/test/test-servers/restify_scaffold_server.ts
new file mode 100644
index 00000000000..8fc501bf063
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/test-servers/restify_scaffold_server.ts
@@ -0,0 +1,32 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+function respond(req: {}, res: {}, next: Function) {
+ next(new Error('this is a restify error'));
+}
+
+import * as restify from 'restify';
+import {ErrorReporting} from '../../src/index';
+const errorHandler = new ErrorReporting();
+
+const server = restify.createServer();
+
+server.use(errorHandler.restify(server));
+server.get('/hello/:name', respond);
+server.head('/hello/:name', respond);
+
+server.listen(8080, () => {
+ // eslint-disable-next-line no-console
+ console.log('%s listening at %s', server.name, server.url);
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/build-stack-trace.ts b/handwritten/nodejs-error-reporting/test/unit/build-stack-trace.ts
new file mode 100644
index 00000000000..36ff2281e81
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/build-stack-trace.ts
@@ -0,0 +1,58 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it} from 'mocha';
+import * as path from 'path';
+import {buildStackTrace} from '../../src/build-stack-trace';
+
+const SRC_ROOT = path.join(__dirname, '..', '..', 'src');
+
+describe('build-stack-trace', () => {
+ it('Should not have a message attached if none is given', () => {
+ assert(buildStackTrace().includes(' at'));
+ assert(!buildStackTrace(undefined).startsWith('undefined'));
+ assert(!buildStackTrace(null).startsWith('null'));
+ });
+
+ it('Should attach a message if given', () => {
+ assert(buildStackTrace('Some Message').startsWith('Some Message\n'));
+ });
+
+ it('Should not contain error-reporting specific frames', () => {
+ (function functionA() {
+ (function functionB() {
+ (function functionC() {
+ const stackTrace = buildStackTrace();
+ assert(stackTrace);
+ assert.strictEqual(stackTrace.indexOf(SRC_ROOT), -1);
+ })();
+ })();
+ })();
+ });
+
+ it('Should return the stack trace', () => {
+ (function functionA() {
+ (function functionB() {
+ (function functionC() {
+ const stackTrace = buildStackTrace();
+ assert(stackTrace);
+ assert.notStrictEqual(stackTrace.indexOf('functionA'), -1);
+ assert.notStrictEqual(stackTrace.indexOf('functionB'), -1);
+ assert.notStrictEqual(stackTrace.indexOf('functionC'), -1);
+ })();
+ })();
+ })();
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/classes/error-message.ts b/handwritten/nodejs-error-reporting/test/unit/classes/error-message.ts
new file mode 100644
index 00000000000..b437d1b9a1e
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/classes/error-message.ts
@@ -0,0 +1,686 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, beforeEach} from 'mocha';
+import {ErrorMessage} from '../../../src/classes/error-message';
+import {RequestInformationContainer} from '../../../src/classes/request-information-container';
+import {deepStrictEqual} from '../../util';
+
+describe('Instantiating a new ErrorMessage', () => {
+ let em: ErrorMessage;
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+
+ it('Should have a default service context', () => {
+ deepStrictEqual(em.serviceContext, {service: 'node', version: undefined});
+ });
+ it('Should have a default message', () => {
+ assert.strictEqual(em.message, '');
+ });
+ it('Should have a default http context', () => {
+ deepStrictEqual(em.context.httpRequest, {
+ method: '',
+ url: '',
+ userAgent: '',
+ referrer: '',
+ responseStatusCode: 0,
+ remoteIp: '',
+ });
+ });
+ it('Should have a default reportLocation', () => {
+ deepStrictEqual(em.context.reportLocation, {
+ filePath: '',
+ lineNumber: 0,
+ functionName: '',
+ });
+ });
+});
+
+describe('Calling against setEventTimeToNow', () => {
+ let em: ErrorMessage;
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set the eventTime property', () => {
+ em.setEventTimeToNow();
+ assert(typeof em.eventTime === 'string');
+ });
+});
+
+describe('Fuzzing against setServiceContext', () => {
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const DEFAULT_TEST_VALUE = 'DEFAULT';
+ const DEFAULT_VERSION_VALUE = undefined;
+ const DEFAULT_SERVICE_VALUE = 'node';
+ let em: ErrorMessage;
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+
+ it('Should set the value for service context', () => {
+ em.setServiceContext(AFFIRMATIVE_TEST_VALUE, AFFIRMATIVE_TEST_VALUE);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: AFFIRMATIVE_TEST_VALUE,
+ version: AFFIRMATIVE_TEST_VALUE,
+ },
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should set the default values', () => {
+ em.setServiceContext(DEFAULT_TEST_VALUE, DEFAULT_TEST_VALUE);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: DEFAULT_TEST_VALUE,
+ version: DEFAULT_TEST_VALUE,
+ },
+ [
+ 'In resetting to default valid values the instance should reflect the',
+ 'value update',
+ ].join(' '),
+ );
+ });
+ it('Should still set version with affirmative value', () => {
+ em.setServiceContext(null!, AFFIRMATIVE_TEST_VALUE);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: DEFAULT_SERVICE_VALUE,
+ version: AFFIRMATIVE_TEST_VALUE,
+ },
+ [
+ 'Providing only a valid value to the second argument of',
+ 'setServiceContext should set the service property as an empty string',
+ 'but set the version property to the affirmative value.',
+ ].join(' '),
+ );
+ });
+ it('Should still set service with affirmative value', () => {
+ em.setServiceContext(AFFIRMATIVE_TEST_VALUE, null!);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: AFFIRMATIVE_TEST_VALUE,
+ version: DEFAULT_VERSION_VALUE,
+ },
+ [
+ 'Providing only a valid value to the first argument of',
+ 'setServiceContext should set the version property as an empty string',
+ 'but set the service property to the affirmative value.',
+ ].join(' '),
+ );
+ });
+ it('Should set default values on both', () => {
+ em.setServiceContext(null!, null!);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: DEFAULT_SERVICE_VALUE,
+ version: DEFAULT_VERSION_VALUE,
+ },
+ [
+ 'Providing null as the value to both arguments should set both',
+ 'properties as empty strings.',
+ ].join(' '),
+ );
+ });
+ it('Should set default values on both', () => {
+ em.setServiceContext(2 as {} as string, 1.3 as {} as string);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: DEFAULT_SERVICE_VALUE,
+ version: DEFAULT_VERSION_VALUE,
+ },
+ [
+ 'Providing numbers as the value to both arguments should set both',
+ 'properties as empty strings.',
+ ].join(' '),
+ );
+ });
+ it('Should set as default', () => {
+ em.setServiceContext({test: 'true'} as {} as string, [] as {} as string);
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: DEFAULT_SERVICE_VALUE,
+ version: DEFAULT_VERSION_VALUE,
+ },
+ [
+ 'Providing arrays or objects as the value to both arguments',
+ 'should set both properties as empty strings.',
+ ].join(' '),
+ );
+ });
+ it('Should set as default', () => {
+ em.setServiceContext();
+ deepStrictEqual(
+ em.serviceContext,
+ {
+ service: DEFAULT_SERVICE_VALUE,
+ version: DEFAULT_VERSION_VALUE,
+ },
+ 'Providing no arguments should set both properties as empty strings',
+ );
+ });
+});
+
+describe('Fuzzing against setMessage', () => {
+ let em: ErrorMessage;
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+
+ it('Should set the message', () => {
+ em.setMessage(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.message === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setMessage();
+ assert(
+ em.message === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setMessage the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setHttpMethod', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set the method', () => {
+ em.setHttpMethod(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.httpRequest.method === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setHttpMethod();
+ assert(
+ em.context.httpRequest.method === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setHttpMethod the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setUrl', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set url', () => {
+ em.setUrl(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.httpRequest.url === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setUrl();
+ assert(
+ em.context.httpRequest.url === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setUrl the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setUserAgent', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set userAgent', () => {
+ em.setUserAgent(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.httpRequest.userAgent === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setUserAgent();
+ assert(
+ em.context.httpRequest.userAgent === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setUserAgent the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setReferrer', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set referrer', () => {
+ em.setReferrer(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.httpRequest.referrer === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setReferrer();
+ assert(
+ em.context.httpRequest.referrer === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setReferrer the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setResponseStatusCode', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 200;
+ const NEGATIVE_TEST_VALUE = 0;
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set responseStatusCode', () => {
+ em.setResponseStatusCode(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.httpRequest.responseStatusCode === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setResponseStatusCode();
+ assert(
+ em.context.httpRequest.responseStatusCode === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setResponseStatusCode the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setRemoteIp', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set remoteIp', () => {
+ em.setRemoteIp(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.httpRequest.remoteIp === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setRemoteIp();
+ assert(
+ em.context.httpRequest.remoteIp === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setRemoteIp the property',
+ 'message should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setUser', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set user', () => {
+ em.setUser(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.user === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setUser();
+ assert(
+ em.context.user === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setUser the property',
+ 'user should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setFilePath', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set filePath', () => {
+ em.setFilePath(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.reportLocation.filePath === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setFilePath();
+ assert(
+ em.context.reportLocation.filePath === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setFilePath the property',
+ 'filePath should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setLineNumber', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 27;
+ const NEGATIVE_TEST_VALUE = 0;
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set lineNumber', () => {
+ em.setLineNumber(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.reportLocation.lineNumber === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setLineNumber();
+ assert(
+ em.context.reportLocation.lineNumber === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setLineNumber the property',
+ 'lineNumber should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against setFunctionName', () => {
+ let em: ErrorMessage;
+ const AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE';
+ const NEGATIVE_TEST_VALUE = '';
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+ it('Should set functionName', () => {
+ em.setFunctionName(AFFIRMATIVE_TEST_VALUE);
+ assert(
+ em.context.reportLocation.functionName === AFFIRMATIVE_TEST_VALUE,
+ [
+ 'In the affirmative case the value should be settable to a valid string',
+ 'and by setting this value this should mutate the instance',
+ ].join(' '),
+ );
+ });
+ it('Should default', () => {
+ em.setFunctionName();
+ assert(
+ em.context.reportLocation.functionName === NEGATIVE_TEST_VALUE,
+ [
+ 'By providing no argument (undefined) to setFunctionName the property',
+ 'functionName should be set to an empty string on the instance',
+ ].join(' '),
+ );
+ });
+});
+
+describe('Fuzzing against consumeRequestInformation', () => {
+ const em = new ErrorMessage();
+ const A_VALID_STRING = 'A_VALID_STRING';
+ const A_VALID_NUMBER = 201;
+ const NEGATIVE_STRING_CASE = '';
+ const NEGATIVE_NUMBER_CASE = 0;
+
+ const AFFIRMATIVE_TEST_VALUE = {
+ method: A_VALID_STRING,
+ url: A_VALID_STRING,
+ userAgent: A_VALID_STRING,
+ referrer: A_VALID_STRING,
+ statusCode: A_VALID_NUMBER,
+ remoteAddress: A_VALID_STRING,
+ };
+ const NEGATIVE_TEST_VALUE = {
+ method: null,
+ url: A_VALID_NUMBER,
+ userAgent: {},
+ referrer: [],
+ statusCode: A_VALID_STRING,
+ remoteAddress: undefined,
+ };
+ it('Should consume the stubbed request object', () => {
+ em.consumeRequestInformation(
+ AFFIRMATIVE_TEST_VALUE as RequestInformationContainer,
+ );
+ assert(
+ em.context.httpRequest.method === A_VALID_STRING,
+ [
+ 'The error messages method, given a valid string, should be',
+ 'set to that value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.url === A_VALID_STRING,
+ [
+ 'The error messages url, given a valid string, should be',
+ 'set to that value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.userAgent === A_VALID_STRING,
+ [
+ 'The error messages userAgent, given a valid string, should be',
+ 'set to that value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.referrer === A_VALID_STRING,
+ [
+ 'The error messages referrer, given a valid string, should be',
+ 'set to that value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.responseStatusCode === A_VALID_NUMBER,
+ [
+ 'The error messages responseStatusCode, given a valid number, should be',
+ 'set to that value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.remoteIp === A_VALID_STRING,
+ [
+ 'The error messages remoteAddress, given a valid string, should be',
+ 'set to that value',
+ ].join(' '),
+ );
+ });
+ it('Should default when consuming a malformed request object', () => {
+ em.consumeRequestInformation(null!);
+ assert(
+ em.context.httpRequest.method === A_VALID_STRING,
+ [
+ 'The error messages method, given an invalid type a the top-level',
+ 'should remain untouched',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.url === A_VALID_STRING,
+ [
+ 'The error messages url, given an invalid type a the top-level',
+ 'should remain untouched',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.userAgent === A_VALID_STRING,
+ [
+ 'The error messages userAgent, given an invalid type a the top-level',
+ 'should remain untouched',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.referrer === A_VALID_STRING,
+ [
+ 'The error messages referrer, given an invalid type a the top-level',
+ 'should remain untouched',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.responseStatusCode === A_VALID_NUMBER,
+ [
+ 'The error messages responseStatusCode, given an invalid type a the top-level',
+ 'should remain untouched',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.remoteIp === A_VALID_STRING,
+ [
+ 'The error messages remoteAddress, given an invalid type a the top-level',
+ 'should remain untouched',
+ ].join(' '),
+ );
+ });
+ it('Should default when consuming mistyped response object properties', () => {
+ em.consumeRequestInformation(
+ NEGATIVE_TEST_VALUE as {} as RequestInformationContainer,
+ );
+ assert(
+ em.context.httpRequest.method === NEGATIVE_STRING_CASE,
+ [
+ 'The error messages method, given an invalid input should default to',
+ 'the negative value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.url === NEGATIVE_STRING_CASE,
+ [
+ 'The error messages url, given an invalid input should default to',
+ 'the negative value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.userAgent === NEGATIVE_STRING_CASE,
+ [
+ 'The error messages userAgent, ggiven an invalid input should default to',
+ 'the negative value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.referrer === NEGATIVE_STRING_CASE,
+ [
+ 'The error messages referrer, given an invalid input should default to',
+ 'the negative value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.responseStatusCode === NEGATIVE_NUMBER_CASE,
+ [
+ 'The error messages responseStatusCode, given an invalid input should default to',
+ 'the negative value',
+ ].join(' '),
+ );
+ assert(
+ em.context.httpRequest.remoteIp === NEGATIVE_STRING_CASE,
+ [
+ 'The error messages remoteAddress, given an invalid input should default to',
+ 'the negative value',
+ ].join(' '),
+ );
+ });
+ it('Should return the instance on calling consumeRequestInformation', () => {
+ assert(
+ em.consumeRequestInformation(
+ AFFIRMATIVE_TEST_VALUE as RequestInformationContainer,
+ ) instanceof ErrorMessage,
+ [
+ 'Calling consumeRequestInformation with valid input should return',
+ 'the ErrorMessage instance',
+ ].join(' '),
+ );
+ assert(
+ em.consumeRequestInformation(undefined!) instanceof ErrorMessage,
+ [
+ 'Calling consumeRequestInformation with invalid input should return',
+ 'the ErrorMessage instance',
+ ].join(' '),
+ );
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/classes/request-information-container.ts b/handwritten/nodejs-error-reporting/test/unit/classes/request-information-container.ts
new file mode 100644
index 00000000000..429d841a651
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/classes/request-information-container.ts
@@ -0,0 +1,93 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, beforeEach} from 'mocha';
+import {RequestInformationContainer} from '../../../src/classes/request-information-container';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {deepStrictEqual} from '../../util';
+
+describe('RequestInformationContainer', () => {
+ const f = new Fuzzer();
+ let cbFn, ric: RequestInformationContainer;
+ beforeEach(() => {
+ ric = new RequestInformationContainer();
+ });
+ describe('Fuzzing against RequestInformationContainer', () => {
+ it('Should return the property as an empty string', () => {
+ cbFn = () => {
+ deepStrictEqual(ric.url, '');
+ };
+ f.fuzzFunctionForTypes(ric.setUrl, ['string'], cbFn, ric);
+ });
+ it('Should return the method property as an empty string', () => {
+ cbFn = () => {
+ deepStrictEqual(ric.method, '');
+ };
+ f.fuzzFunctionForTypes(ric.setMethod, ['string'], cbFn, ric);
+ });
+ it('Should return the referrer property as an empty string', () => {
+ cbFn = () => {
+ deepStrictEqual(ric.referrer, '');
+ };
+ f.fuzzFunctionForTypes(ric.setReferrer, ['string'], cbFn, ric);
+ });
+ it('Should return the userAgent property as an empty string', () => {
+ cbFn = () => {
+ deepStrictEqual(ric.userAgent, '');
+ };
+ f.fuzzFunctionForTypes(ric.setUserAgent, ['string'], cbFn, ric);
+ });
+ it('Should return the property as an empty string', () => {
+ cbFn = () => {
+ deepStrictEqual(ric.remoteAddress, '');
+ };
+ f.fuzzFunctionForTypes(ric.setRemoteAddress, ['string'], cbFn, ric);
+ });
+ it('Should return the default value for statusCode', () => {
+ cbFn = () => {
+ assert.strictEqual(ric.statusCode, 0);
+ };
+ f.fuzzFunctionForTypes(ric.setStatusCode, ['number'], cbFn, ric);
+ });
+ });
+ describe('Fuzzing against for positive cases', () => {
+ const VALID_STRING_INPUT = 'valid';
+ const VALID_NUMBER_INPUT = 500;
+ it('Should assign the value to the url property', () => {
+ ric.setUrl(VALID_STRING_INPUT);
+ assert.strictEqual(ric.url, VALID_STRING_INPUT);
+ });
+ it('Should assign the value to the method property', () => {
+ ric.setMethod(VALID_STRING_INPUT);
+ assert.strictEqual(ric.method, VALID_STRING_INPUT);
+ });
+ it('Should assign the value to the referrer property', () => {
+ ric.setReferrer(VALID_STRING_INPUT);
+ assert.strictEqual(ric.referrer, VALID_STRING_INPUT);
+ });
+ it('Should assign the value to the userAgent property', () => {
+ ric.setUserAgent(VALID_STRING_INPUT);
+ assert.strictEqual(ric.userAgent, VALID_STRING_INPUT);
+ });
+ it('Should assign the value to remoteAddress property', () => {
+ ric.setRemoteAddress(VALID_STRING_INPUT);
+ assert.strictEqual(ric.remoteAddress, VALID_STRING_INPUT);
+ });
+ it('Should assign the value to statusCode property', () => {
+ ric.setStatusCode(VALID_NUMBER_INPUT);
+ assert.strictEqual(ric.statusCode, VALID_NUMBER_INPUT);
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/configuration.ts b/handwritten/nodejs-error-reporting/test/unit/configuration.ts
new file mode 100644
index 00000000000..2d9d6e4a713
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/configuration.ts
@@ -0,0 +1,515 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, before, after, afterEach, beforeEach} from 'mocha';
+import {FakeConfiguration as Configuration} from '../fixtures/configuration';
+import {ConfigurationOptions, Logger} from '../../src/configuration';
+import {Fuzzer} from '../../utils/fuzzer';
+import {deepStrictEqual} from '../util';
+const level = process.env.GCLOUD_ERRORS_LOGLEVEL;
+import {createLogger} from '../../src/logger';
+const logger = createLogger({
+ logLevel: typeof level === 'number' ? level : 4,
+});
+import * as nock from 'nock';
+
+const METADATA_URL =
+ 'http://metadata.google.internal/computeMetadata/v1/project';
+
+const configEnv = {
+ NODE_ENV: process.env.NODE_ENV,
+ GCLOUD_PROJECT: process.env.GCLOUD_PROJECT,
+ GAE_MODULE_NAME: process.env.GAE_MODULE_NAME,
+ GAE_MODULE_VERSION: process.env.GAE_MODULE_VERSION,
+};
+function sterilizeConfigEnv() {
+ delete process.env.NODE_ENV;
+ delete process.env.GCLOUD_PROJECT;
+ delete process.env.GAE_MODULE_NAME;
+ delete process.env.GAE_MODULE_VERSION;
+}
+function restoreConfigEnv() {
+ process.env.NODE_ENV = configEnv.NODE_ENV;
+ process.env.GCLOUD_PROJECT = configEnv.GCLOUD_PROJECT;
+ process.env.GAE_MODULE_NAME = configEnv.GAE_MODULE_NAME;
+ process.env.GAE_MODULE_VERSION = configEnv.GAE_MODULE_VERSION;
+}
+function createDeadMetadataService() {
+ return nock(METADATA_URL).get('/project-id').times(1).reply(500);
+}
+
+describe('Configuration class', () => {
+ before(() => {
+ sterilizeConfigEnv();
+ });
+ after(() => {
+ restoreConfigEnv();
+ });
+ describe('Initialization', () => {
+ const f = new Fuzzer();
+ describe('fuzzing the constructor', () => {
+ it('Should return default values', () => {
+ let c;
+ f.fuzzFunctionForTypes(
+ (givenConfigFuzz: ConfigurationOptions) => {
+ c = new Configuration(givenConfigFuzz, logger);
+ deepStrictEqual(c._givenConfiguration, {});
+ },
+ ['object'],
+ );
+ });
+ });
+ describe('valid config and default values', () => {
+ let c: Configuration;
+ const validConfig = {reportMode: 'always'} as {reportMode: 'always'};
+ before(() => {
+ process.env.NODE_ENV = 'development';
+ });
+ after(() => {
+ sterilizeConfigEnv();
+ });
+ it('Should not throw with a valid configuration', () => {
+ assert.doesNotThrow(() => {
+ c = new Configuration(validConfig, logger);
+ });
+ });
+ it('Should have a property reflecting the config argument', () => {
+ deepStrictEqual(c._givenConfiguration, validConfig);
+ });
+ it('Should not have a project id', () => {
+ assert.strictEqual(c._projectId, null);
+ });
+ it('Should not have a key', () => {
+ assert.strictEqual(c.getKey(), null);
+ });
+ it('Should have a default service context', () => {
+ deepStrictEqual(c.getServiceContext(), {
+ service: 'node',
+ version: undefined,
+ });
+ });
+ it('Should specify to not report unhandledRejections', () => {
+ assert.strictEqual(c.getReportUnhandledRejections(), false);
+ });
+ });
+ describe('reportMode', () => {
+ let nodeEnv: string | undefined;
+ beforeEach(() => {
+ nodeEnv = process.env.NODE_ENV;
+ });
+
+ afterEach(() => {
+ if (nodeEnv === undefined) {
+ delete process.env.NODE_ENV;
+ } else {
+ process.env.NODE_ENV = nodeEnv;
+ }
+ });
+
+ it('Should print a deprecation warning if "ignoreEvnironmentCheck" is used', () => {
+ let warnText = '';
+ const logger = {
+ warn: text => {
+ warnText += text + '\n';
+ },
+ } as Logger;
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration({ignoreEnvironmentCheck: true}, logger);
+ assert.strictEqual(
+ warnText,
+ 'The "ignoreEnvironmentCheck" config option is deprecated. ' +
+ 'Use the "reportMode" config option instead.\n',
+ );
+ });
+
+ it('Should print a warning if both "ignoreEnvironmentCheck" and "reportMode" are specified', () => {
+ let warnText = '';
+ const logger = {
+ warn: text => {
+ warnText += text + '\n';
+ },
+ } as Logger;
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {ignoreEnvironmentCheck: true, reportMode: 'never'},
+ logger,
+ );
+ assert.strictEqual(
+ warnText,
+ 'The "ignoreEnvironmentCheck" config option is deprecated. ' +
+ 'Use the "reportMode" config option instead.\nBoth the "ignoreEnvironmentCheck" ' +
+ 'and "reportMode" configuration options have been specified. The "reportMode" ' +
+ 'option will take precedence.\n',
+ );
+ });
+
+ it('Should set "reportMode" to "always" if "ignoreEnvironmentCheck" is true', () => {
+ const conf = new Configuration({ignoreEnvironmentCheck: true}, logger);
+ assert.strictEqual(conf._reportMode, 'always');
+ });
+
+ it('Should set "reportMode" to "production" if "ignoreEnvironmentCheck" is false', () => {
+ const conf = new Configuration({ignoreEnvironmentCheck: false}, logger);
+ assert.strictEqual(conf._reportMode, 'production');
+ });
+
+ it('Should prefer "reportMode" config if "ignoreEnvironmentCheck" is also set', () => {
+ const conf = new Configuration(
+ {ignoreEnvironmentCheck: true, reportMode: 'never'},
+ logger,
+ );
+ assert.strictEqual(conf._reportMode, 'never');
+ });
+
+ it('Should be set to "production" by default', () => {
+ const conf = new Configuration({}, logger);
+ assert.strictEqual(conf._reportMode, 'production');
+ });
+
+ it('Should state reporting is enabled with mode "production"', () => {
+ const conf = new Configuration({reportMode: 'production'}, logger);
+ assert.strictEqual(conf.isReportingEnabled(), true);
+ });
+
+ it('Should state reporting is enabled with mode "always"', () => {
+ const conf = new Configuration({reportMode: 'always'}, logger);
+ assert.strictEqual(conf.isReportingEnabled(), true);
+ });
+
+ it('Should state reporting is not enabled with mode "never"', () => {
+ const conf = new Configuration({reportMode: 'never'}, logger);
+ assert.strictEqual(conf.isReportingEnabled(), false);
+ });
+
+ it('Should state reporting should proceed with mode "production" and env "production"', () => {
+ process.env.NODE_ENV = 'production';
+ const conf = new Configuration({reportMode: 'production'}, logger);
+ assert.strictEqual(conf.getShouldReportErrorsToAPI(), true);
+ });
+
+ it('Should state reporting should not proceed with mode "production" and env not "production"', () => {
+ process.env.NODE_ENV = 'dev';
+ const conf = new Configuration({reportMode: 'production'}, logger);
+ assert.strictEqual(conf.getShouldReportErrorsToAPI(), false);
+ });
+
+ it('Should state reporting should proceed with mode "always" and env "production"', () => {
+ process.env.NODE_ENV = 'production';
+ const conf = new Configuration({reportMode: 'always'}, logger);
+ assert.strictEqual(conf.getShouldReportErrorsToAPI(), true);
+ });
+
+ it('Should state reporting should proceed with mode "always" and env not "production"', () => {
+ process.env.NODE_ENV = 'dev';
+ const conf = new Configuration({reportMode: 'always'}, logger);
+ assert.strictEqual(conf.getShouldReportErrorsToAPI(), true);
+ });
+
+ it('Should state reporting should not proceed with mode "never" and env "production"', () => {
+ process.env.NODE_ENV = 'production';
+ const conf = new Configuration({reportMode: 'never'}, logger);
+ assert.strictEqual(conf.getShouldReportErrorsToAPI(), false);
+ });
+
+ it('Should state reporting should not proceed with mode "never" and env not "production"', () => {
+ process.env.NODE_ENV = 'dev';
+ const conf = new Configuration({reportMode: 'never'}, logger);
+ assert.strictEqual(conf.getShouldReportErrorsToAPI(), false);
+ });
+ });
+ describe('with ignoreEnvironmentCheck', () => {
+ const conf = Object.assign(
+ {},
+ {projectId: 'some-id'},
+ {ignoreEnvironmentCheck: true},
+ );
+ const c = new Configuration(conf, logger);
+ it('Should reportErrorsToAPI', () => {
+ assert.strictEqual(c.getShouldReportErrorsToAPI(), true);
+ });
+ });
+ describe('without ignoreEnvironmentCheck', () => {
+ describe('report behaviour with production env', () => {
+ let c: Configuration;
+ before(() => {
+ sterilizeConfigEnv();
+ process.env.NODE_ENV = 'production';
+ c = new Configuration(undefined, logger);
+ });
+ after(() => {
+ sterilizeConfigEnv();
+ });
+ it('Should reportErrorsToAPI', () => {
+ assert.strictEqual(c.getShouldReportErrorsToAPI(), true);
+ });
+ });
+ });
+ describe('exception behaviour', () => {
+ it('Should throw if invalid type for reportMode', () => {
+ assert.throws(() => {
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {reportMode: new Date()} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ });
+ it('Should throw if invalid value for reportMode', () => {
+ assert.throws(() => {
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {reportMode: 'invalid-mode'} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ });
+ it('Should throw if invalid type for key', () => {
+ assert.throws(() => {
+ // we are intentionally providing an invalid configuration
+ // thus an explicit cast is needed
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration({key: null} as {} as ConfigurationOptions, logger);
+ });
+ });
+ it('Should throw if invalid for ignoreEnvironmentCheck', () => {
+ assert.throws(() => {
+ // we are intentionally providing an invalid configuration
+ // thus an explicit cast is needed
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {ignoreEnvironmentCheck: null} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ });
+ it('Should throw if invalid for serviceContext.service', () => {
+ assert.throws(() => {
+ // we are intentionally providing an invalid configuration
+ // thus an explicit cast is needed
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {serviceContext: {service: false}} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ });
+ it('Should throw if invalid for serviceContext.version', () => {
+ assert.throws(() => {
+ // we are intentionally providing an invalid configuration
+ // thus an explicit cast is needed
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {serviceContext: {version: true}} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ });
+ it('Should throw if invalid for reportUnhandledRejections', () => {
+ assert.throws(() => {
+ // we are intentionally providing an invalid configuration
+ // thus an explicit cast is needed
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration(
+ {
+ reportUnhandledRejections: 'INVALID',
+ } as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ });
+ it('Should not throw given an empty object for serviceContext', () => {
+ assert.doesNotThrow(() => {
+ // tslint:disable-next-line:no-unused-expression
+ new Configuration({serviceContext: {}}, logger);
+ });
+ });
+ });
+ describe('Configuration resource aquisition', () => {
+ before(() => {
+ sterilizeConfigEnv();
+ });
+ describe('project id from configuration instance', () => {
+ const pi = 'test';
+ let c: Configuration;
+ before(() => {
+ c = new Configuration({projectId: pi}, logger);
+ });
+ after(() => {
+ nock.cleanAll();
+ });
+ it('Should return the project id', () => {
+ assert.strictEqual(c.getProjectId(), pi);
+ });
+ });
+ describe('project number from configuration instance', () => {
+ const pn = 1234;
+ let c: Configuration;
+ before(() => {
+ sterilizeConfigEnv();
+ c = new Configuration(
+ {projectId: pn} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ after(() => {
+ nock.cleanAll();
+ sterilizeConfigEnv();
+ });
+ it('Should return the project number', () => {
+ assert.strictEqual(c.getProjectId(), pn.toString());
+ });
+ });
+ });
+ describe('Exception behaviour', () => {
+ describe('While lacking a project id', () => {
+ let c: Configuration;
+ before(() => {
+ sterilizeConfigEnv();
+ createDeadMetadataService();
+ c = new Configuration(undefined, logger);
+ });
+ after(() => {
+ nock.cleanAll();
+ sterilizeConfigEnv();
+ });
+ it('Should return null', () => {
+ assert.strictEqual(c.getProjectId(), null);
+ });
+ });
+ describe('Invalid type for projectId in runtime config', () => {
+ let c: Configuration;
+ before(() => {
+ sterilizeConfigEnv();
+ createDeadMetadataService();
+ // we are intentionally providing an invalid configuration
+ // thus an explicit cast is needed
+ c = new Configuration(
+ {projectId: null} as {} as ConfigurationOptions,
+ logger,
+ );
+ });
+ after(() => {
+ nock.cleanAll();
+ sterilizeConfigEnv();
+ });
+ it('Should return null', () => {
+ assert.strictEqual(c.getProjectId(), null);
+ });
+ });
+ });
+ describe('Resource aquisition', () => {
+ after(() => {
+ /*
+ * !! IMPORTANT !!
+ * THE restoreConfigEnv FUNCTION SHOULD BE CALLED LAST AS THIS TEST FILE
+ * EXITS AND SHOULD THEREFORE BE THE LAST THING TO EXECUTE FROM THIS
+ * FILE.
+ * !! IMPORTANT !!
+ */
+ restoreConfigEnv();
+ });
+ describe('via env', () => {
+ before(() => {
+ sterilizeConfigEnv();
+ });
+ afterEach(() => {
+ sterilizeConfigEnv();
+ });
+ describe('no longer tests env itself', () => {
+ let c: Configuration;
+ const projectId = 'test-xyz';
+ before(() => {
+ process.env.GCLOUD_PROJECT = projectId;
+ c = new Configuration(undefined, logger);
+ });
+ it('Should assign', () => {
+ assert.strictEqual(c.getProjectId(), null);
+ });
+ });
+ describe('serviceContext', () => {
+ let c: Configuration;
+ const projectId = 'test-abc';
+ const serviceContext = {
+ service: 'test',
+ version: '1.x',
+ };
+ before(() => {
+ process.env.GCLOUD_PROJECT = projectId;
+ process.env.GAE_MODULE_NAME = serviceContext.service;
+ process.env.GAE_MODULE_VERSION = serviceContext.version;
+ c = new Configuration(undefined, logger);
+ });
+ it('Should assign', () => {
+ deepStrictEqual(c.getServiceContext(), serviceContext);
+ });
+ });
+ });
+ describe('via runtime configuration', () => {
+ before(() => {
+ sterilizeConfigEnv();
+ });
+ describe('serviceContext', () => {
+ let c: Configuration;
+ const projectId = 'xyz123';
+ const serviceContext = {
+ service: 'evaluation',
+ version: '2.x',
+ };
+ before(() => {
+ c = new Configuration({
+ projectId,
+ serviceContext,
+ });
+ });
+ it('Should assign', () => {
+ deepStrictEqual(c.getServiceContext(), serviceContext);
+ });
+ });
+ describe('api key', () => {
+ let c: Configuration;
+ const projectId = '987abc';
+ const key = '1337-api-key';
+ before(() => {
+ c = new Configuration(
+ {
+ key,
+ projectId,
+ },
+ logger,
+ );
+ });
+ it('Should assign', () => {
+ assert.strictEqual(c.getKey(), key);
+ });
+ });
+ describe('reportUnhandledRejections', () => {
+ let c: Configuration;
+ const reportRejections = false;
+ before(() => {
+ c = new Configuration({
+ reportUnhandledRejections: reportRejections,
+ });
+ });
+ it('Should assign', () => {
+ assert.strictEqual(
+ c.getReportUnhandledRejections(),
+ reportRejections,
+ );
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/google-apis/auth-client.ts b/handwritten/nodejs-error-reporting/test/unit/google-apis/auth-client.ts
new file mode 100644
index 00000000000..9737f40899e
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/google-apis/auth-client.ts
@@ -0,0 +1,233 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as proxyquire from 'proxyquire';
+import {describe, beforeEach, afterEach, it} from 'mocha';
+
+import {
+ Configuration,
+ ConfigurationOptions,
+ Logger,
+} from '../../../src/configuration';
+import {deepStrictEqual} from '../../util';
+
+function verifyReportedMessage(
+ config1: ConfigurationOptions,
+ errToReturn: Error | null | undefined,
+ expectedLogs: {error?: string; info?: string; warn?: string},
+ done: () => void,
+) {
+ class ServiceStub {
+ authClient: {};
+ request: {};
+ constructor() {
+ this.authClient = {
+ async getAccessToken() {
+ if (errToReturn) {
+ throw errToReturn;
+ }
+ return 'some-token';
+ },
+ };
+ this.request = () => {};
+ }
+ }
+
+ const RequestHandler = proxyquire('../../../src/google-apis/auth-client', {
+ '@google-cloud/common': {
+ Service: ServiceStub,
+ },
+ }).RequestHandler;
+
+ const logs: {error?: string; info?: string; warn?: string} = {};
+ const logger = {
+ error(text: string) {
+ if (!logs.error) {
+ logs.error = '';
+ }
+ logs.error += text;
+ },
+ info(text: string) {
+ if (!logs.info) {
+ logs.info = '';
+ }
+ logs.info += text;
+ },
+ warn(text: string) {
+ if (!logs.warn) {
+ logs.warn = '';
+ }
+ logs.warn += text;
+ },
+ } as {} as Logger;
+ const config2 = new Configuration(config1, logger);
+ // tslint:disable-next-line:no-unused-expression
+ new RequestHandler(config2, logger);
+ setImmediate(() => {
+ deepStrictEqual(logs, expectedLogs);
+ done();
+ });
+}
+describe('RequestHandler', () => {
+ let nodeEnv: string | undefined;
+ beforeEach(() => {
+ nodeEnv = process.env.NODE_ENV;
+ });
+
+ afterEach(() => {
+ if (nodeEnv === undefined) {
+ delete process.env.NODE_ENV;
+ } else {
+ process.env.NODE_ENV = nodeEnv;
+ }
+ });
+
+ it('should not request OAuth2 token if key is provided', function (done) {
+ this.timeout(8000);
+ const config: ConfigurationOptions = {
+ reportMode: 'always',
+ key: 'key',
+ };
+ const message = 'Made OAuth2 Token Request';
+ verifyReportedMessage(
+ config,
+ new Error(message),
+ {
+ info: 'API key provided; skipping OAuth2 token request.',
+ },
+ done,
+ );
+ });
+
+ it('should not request OAuth2 token if error reporting is disabled', done => {
+ verifyReportedMessage(
+ {reportMode: 'never'},
+ null, // no access token error
+ {
+ info: 'Not configured to send errors to the API; skipping Google Cloud API Authentication.',
+ },
+ done,
+ );
+ });
+
+ it('should not issue a warning if disabled and can communicate with the API', done => {
+ process.env.NODE_ENV = 'production';
+ verifyReportedMessage(
+ {reportMode: 'never'},
+ null, // no access token error
+ {
+ info: 'Not configured to send errors to the API; skipping Google Cloud API Authentication.',
+ },
+ done,
+ );
+ });
+
+ it('should not issue a warning if disabled and cannot communicate with the API', done => {
+ process.env.NODE_ENV = 'dev';
+ verifyReportedMessage(
+ {reportMode: 'never'},
+ null, // no access token error
+ {
+ info: 'Not configured to send errors to the API; skipping Google Cloud API Authentication.',
+ },
+ done,
+ );
+ });
+
+ it('should not issue a warning if enabled and can communicate with the API', done => {
+ process.env.NODE_ENV = 'production';
+ verifyReportedMessage(
+ {reportMode: 'production'},
+ null, // no access token error
+ {}, // no expected logs
+ done,
+ );
+ });
+
+ it('should not issue a warning with a default config and can communicate with the API', done => {
+ process.env.NODE_ENV = 'production';
+ verifyReportedMessage(
+ {},
+ null, // no access token error
+ {}, // no expected logs
+ done,
+ );
+ });
+
+ it('should not issue a warning if it can communicate with the API', (done: () => void) => {
+ const config = {ignoreEnvironmentCheck: true};
+ const warn =
+ 'The "ignoreEnvironmentCheck" config option is deprecated. ' +
+ 'Use the "reportMode" config option instead.';
+ verifyReportedMessage(config, null, {warn}, () => {
+ verifyReportedMessage(config, undefined, {warn}, done);
+ });
+ });
+
+ it('should issue a warning if enabled and cannot communicate with the API', done => {
+ process.env.NODE_ENV = 'dev';
+ verifyReportedMessage(
+ {reportMode: 'production'},
+ null, // no access token error
+ {
+ info: 'Not configured to send errors to the API; skipping Google Cloud API Authentication.',
+ warn:
+ 'The error reporting client is configured to report ' +
+ 'errors if and only if the NODE_ENV environment variable is set to ' +
+ '"production". Errors will not be reported. To have errors always ' +
+ 'reported, regardless of the value of NODE_ENV, set the reportMode ' +
+ 'configuration option to "always".',
+ },
+ done,
+ );
+ });
+
+ it('should issue a warning with a default config and cannot communicate with the API', done => {
+ process.env.NODE_ENV = 'dev';
+ verifyReportedMessage(
+ {},
+ null, // no access token error
+ {
+ info: 'Not configured to send errors to the API; skipping Google Cloud API Authentication.',
+ warn:
+ 'The error reporting client is configured to report ' +
+ 'errors if and only if the NODE_ENV environment variable is set to ' +
+ '"production". Errors will not be reported. To have errors always ' +
+ 'reported, regardless of the value of NODE_ENV, set the reportMode ' +
+ 'configuration option to "always".',
+ },
+ done,
+ );
+ });
+
+ it('should issue a warning if it cannot communicate with the API', (done: () => void) => {
+ const config = {ignoreEnvironmentCheck: true};
+ const message = 'Test Error';
+ verifyReportedMessage(
+ config,
+ new Error(message),
+ {
+ error:
+ 'Unable to find credential information on instance. This ' +
+ 'library will be unable to communicate with the Google Cloud API to ' +
+ 'save errors. Message: ' +
+ message,
+ warn:
+ 'The "ignoreEnvironmentCheck" config option is deprecated. ' +
+ 'Use the "reportMode" config option instead.',
+ },
+ done,
+ );
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/interfaces/express.ts b/handwritten/nodejs-error-reporting/test/unit/interfaces/express.ts
new file mode 100644
index 00000000000..3dbac98239d
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/interfaces/express.ts
@@ -0,0 +1,104 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it} from 'mocha';
+
+import {ErrorMessage} from '../../../src/classes/error-message';
+import {RequestHandler} from '../../../src/google-apis/auth-client';
+import {makeExpressHandler as expressInterface} from '../../../src/interfaces/express';
+import {createLogger} from '../../../src/logger';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {FakeConfiguration as Configuration} from '../../fixtures/configuration';
+import {deepStrictEqual} from '../../util';
+
+describe('expressInterface', () => {
+ describe('Exception handling', () => {
+ describe('Given invalid input', () => {
+ it('Should not throw errors', () => {
+ const f = new Fuzzer();
+ assert.doesNotThrow(() => {
+ f.fuzzFunctionForTypes(expressInterface, ['object', 'object']);
+ return;
+ });
+ });
+ });
+ });
+ describe('Intended behaviour', () => {
+ const stubbedConfig = new Configuration(
+ {
+ serviceContext: {
+ service: 'a_test_service',
+ version: 'a_version',
+ },
+ },
+ createLogger({logLevel: 4}),
+ );
+ (
+ stubbedConfig as {} as {
+ lacksCredentials: Function;
+ }
+ ).lacksCredentials = () => {
+ return false;
+ };
+ const client = {
+ sendError() {
+ return;
+ },
+ };
+ const testError = new Error('This is a test');
+ const validBoundHandler = expressInterface(
+ client as {} as RequestHandler,
+ stubbedConfig,
+ );
+ it('Should return the error message', () => {
+ const res = validBoundHandler(testError, null!, null!, null!);
+ deepStrictEqual(
+ res,
+ Object.assign(
+ new ErrorMessage()
+ .setMessage(testError.stack!)
+ .setServiceContext(
+ stubbedConfig._serviceContext.service,
+ stubbedConfig._serviceContext.version,
+ ),
+ {eventTime: res.eventTime},
+ ),
+ );
+ });
+ describe('Calling back to express builtins', () => {
+ it('Should callback to next', done => {
+ const nextCb = () => {
+ done();
+ };
+ validBoundHandler(testError, null!, null!, nextCb);
+ });
+ it('Should callback to sendError', done => {
+ const sendError = () => {
+ done();
+ };
+ const client = {
+ sendError,
+ };
+ const handler = expressInterface(
+ client as {} as RequestHandler,
+ stubbedConfig,
+ );
+ handler(testError, null!, null!, () => {
+ return;
+ });
+ });
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/interfaces/hapi.ts b/handwritten/nodejs-error-reporting/test/unit/interfaces/hapi.ts
new file mode 100644
index 00000000000..8ac740f81fd
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/interfaces/hapi.ts
@@ -0,0 +1,302 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, beforeEach, before, afterEach} from 'mocha';
+import {makeHapiPlugin as hapiInterface} from '../../../src/interfaces/hapi';
+import {ErrorMessage} from '../../../src/classes/error-message';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {EventEmitter} from 'events';
+import * as config from '../../../src/configuration';
+import {RequestHandler} from '../../../src/google-apis/auth-client';
+import {FakeConfiguration as Configuration} from '../../fixtures/configuration';
+import * as http from 'http';
+import * as hapi from '@hapi/hapi';
+import * as boom from 'boom';
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const packageJson = require('../../../../package.json');
+
+interface HapiPlugin {
+ register: ((server: {}, options: {}, next: Function) => void) & {
+ attributes?: {name: string; version: string};
+ };
+}
+
+describe('Hapi interface', () => {
+ describe('Fuzzing the setup handler', () => {
+ it('Should not throw when fuzzed with invalid types', () => {
+ const f = new Fuzzer();
+ assert.doesNotThrow(() => {
+ f.fuzzFunctionForTypes(hapiInterface, ['object', 'object']);
+ return;
+ });
+ });
+ });
+ describe('Providing valid input to the setup handler', () => {
+ const givenConfig = {
+ getVersion() {
+ return '1';
+ },
+ };
+ let plugin: HapiPlugin;
+ beforeEach(() => {
+ plugin = hapiInterface(null!, givenConfig as {} as config.Configuration);
+ });
+ it('should have plain object as plugin', () => {
+ assert(plugin?.toString() === '[object Object]');
+ });
+ it('plugin should have a register function property', () => {
+ assert(typeof plugin?.register === 'function');
+ });
+ it("the plugin's register property should have an attributes property", () => {
+ assert(typeof plugin.register!.attributes === 'object');
+ });
+ it("the plugin's attribute property should have a name property", () => {
+ assert.strictEqual(
+ plugin.register!.attributes!.name,
+ '@google-cloud/error-reporting',
+ );
+ });
+ it("the plugin's attribute property should have a version property", () => {
+ assert(plugin.register!.attributes!.version !== undefined);
+ });
+ });
+ describe('hapiRegisterFunction behaviour', () => {
+ let fakeServer: EventEmitter;
+ beforeEach(() => {
+ fakeServer = new EventEmitter();
+ });
+ it('Should call fn when the request-error event is emitted', () => {
+ const fakeClient = {
+ sendError(errMsg: ErrorMessage) {
+ assert(
+ errMsg instanceof ErrorMessage,
+ 'should be an instance of Error message',
+ );
+ },
+ } as {} as RequestHandler;
+ const plugin = hapiInterface(fakeClient, {
+ lacksCredentials() {
+ return false;
+ },
+ getVersion() {
+ return '1';
+ },
+ getServiceContext() {
+ return {service: 'node'};
+ },
+ } as {} as config.Configuration);
+ plugin.register(fakeServer, null!, null!);
+ fakeServer.emit('request-error');
+ });
+ });
+ describe('Behaviour around the request/response lifecycle', () => {
+ const EVENT = 'onPreResponse';
+ const fakeClient = {sendError() {}} as {} as RequestHandler;
+ let fakeServer: EventEmitter & {ext?: Function},
+ config: Configuration & {lacksCredentials?: () => boolean},
+ plugin: HapiPlugin;
+ before(() => {
+ config = new Configuration({
+ projectId: 'xyz',
+ serviceContext: {
+ service: 'x',
+ version: '1.x',
+ },
+ });
+ config.lacksCredentials = () => {
+ return false;
+ };
+ plugin = hapiInterface(fakeClient, config);
+ });
+ beforeEach(() => {
+ fakeServer = new EventEmitter();
+ fakeServer.ext = fakeServer.on;
+ });
+ afterEach(() => {
+ fakeServer.removeAllListeners();
+ });
+ it('Should call continue when a boom is emitted if reply is an object', done => {
+ plugin.register(fakeServer, null!, () => {});
+ fakeServer.emit(
+ EVENT,
+ {response: new boom('message', {statusCode: 427})},
+ {
+ continue() {
+ // The continue function should be called
+ done();
+ },
+ },
+ );
+ });
+ it('Should call continue when a boom is emitted if reply is a function', done => {
+ // Manually testing has shown that in actual usage the `reply` object
+ // provided to listeners of the `onPreResponse` event can be a
+ // function that has a `continue` property that is a function. If
+ // `reply.continue()` is not invoked in this situation, the Hapi app
+ // will become unresponsive.
+ plugin.register(fakeServer, null!, () => {});
+ const reply: Function & {continue?: Function} = () => {};
+ reply.continue = () => {
+ // The continue function should be called
+ done();
+ };
+ fakeServer.emit(
+ EVENT,
+ {response: new boom('message', {statusCode: 427})},
+ reply,
+ );
+ });
+ it('Should call sendError when a boom is received', done => {
+ const fakeClient = {
+ sendError(err: ErrorMessage) {
+ assert(err instanceof ErrorMessage);
+ done();
+ },
+ } as {} as RequestHandler;
+ const plugin = hapiInterface(fakeClient, config);
+ plugin.register(fakeServer, null!, () => {});
+ fakeServer.emit('onPreResponse', {
+ response: new boom('message', {statusCode: 427}),
+ });
+ });
+ it('Should call next when completing a request', done => {
+ plugin.register(fakeServer, null!, () => {
+ // The next function should be called
+ done();
+ });
+ fakeServer.emit(
+ EVENT,
+ {response: new boom('message', {statusCode: 427})},
+ {continue() {}},
+ );
+ });
+ });
+ describe('Hapi17', () => {
+ const errorsSent: ErrorMessage[] = [];
+ // the only method in the client that should be used is `sendError`
+ const fakeClient = {
+ sendError: (
+ errorMessage: ErrorMessage,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ userCb?: (
+ err: Error | null,
+ response: http.ServerResponse | null,
+ body: {},
+ ) => void,
+ ) => {
+ errorsSent.push(errorMessage);
+ },
+ } as {} as RequestHandler;
+
+ // the configuration should be not be needed to send errors correctly
+ const plugin = hapiInterface(fakeClient, {} as Configuration);
+
+ afterEach(() => {
+ errorsSent.length = 0;
+ });
+
+ it('Plugin should have name and version properties', () => {
+ assert.strictEqual(plugin.name, packageJson.name);
+ assert.strictEqual(plugin.version, packageJson.version);
+ });
+
+ it("Should record 'log' events correctly", () => {
+ const fakeServer = {events: new EventEmitter()};
+
+ // emulate how the hapi server would register itself
+ plugin.register(fakeServer, {});
+
+ // emulate the hapi server emitting a log event
+ const testError = new Error('Error emitted through a log event');
+
+ // this event should not be recorded
+ fakeServer.events.emit('log', {error: testError, channel: 'internal'});
+
+ // this event should be recorded
+ fakeServer.events.emit('log', {error: testError, channel: 'app'});
+
+ assert.strictEqual(errorsSent.length, 1);
+ const errorMessage = errorsSent[0];
+
+ // note: the error's stack contains the error message
+ assert.strictEqual(errorMessage.message, testError.stack);
+ });
+
+ it("Should record 'request' events correctly", () => {
+ const fakeServer = {events: new EventEmitter()};
+
+ // emulate how the hapi server would register itself
+ plugin.register(fakeServer, {});
+
+ // emulate the hapi server emitting a request event
+ // a cast to hapi.Request is being done since only the listed
+ // properties are the properties that are being tested. In
+ // addition other properties of hapi.Request should be needed
+ // to properly send the error.
+ const fakeRequest = {
+ method: 'custom-method',
+ url: 'custom-url',
+ headers: {
+ 'user-agent': 'custom-user-agent',
+ referrer: 'custom-referrer',
+ 'x-forwarded-for': 'some-remote-address',
+ },
+ response: {statusCode: 42},
+ } as {} as hapi.Request;
+
+ const testError = new Error('Error emitted through a request event');
+
+ // this event should not be recorded
+ fakeServer.events.emit('request', fakeRequest, {
+ error: testError,
+ channel: 'internal',
+ });
+
+ // this event should be recorded
+ fakeServer.events.emit('request', fakeRequest, {
+ error: testError,
+ channel: 'error',
+ });
+
+ assert.strictEqual(errorsSent.length, 1);
+ const errorMessage = errorsSent[0];
+
+ // note: the error's stack contains the error message
+ assert.strictEqual(errorMessage.message, testError.stack);
+ assert.strictEqual(
+ errorMessage.context.httpRequest.method,
+ 'custom-method',
+ );
+ assert.strictEqual(errorMessage.context.httpRequest.url, 'custom-url');
+ assert.strictEqual(
+ errorMessage.context.httpRequest.userAgent,
+ 'custom-user-agent',
+ );
+ assert.strictEqual(
+ errorMessage.context.httpRequest.referrer,
+ 'custom-referrer',
+ );
+ assert.strictEqual(
+ errorMessage.context.httpRequest.remoteIp,
+ 'some-remote-address',
+ );
+ assert.strictEqual(
+ errorMessage.context.httpRequest.responseStatusCode,
+ 42,
+ );
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/interfaces/manual.ts b/handwritten/nodejs-error-reporting/test/unit/interfaces/manual.ts
new file mode 100644
index 00000000000..1549b29a516
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/interfaces/manual.ts
@@ -0,0 +1,242 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it} from 'mocha';
+
+import {Logger} from '../../../src/configuration';
+import * as manual from '../../../src/interfaces/manual';
+import {FakeConfiguration as Configuration} from '../../fixtures/configuration';
+
+const config = new Configuration({});
+(config as {} as {lacksCredentials: Function}).lacksCredentials = () => {
+ return false;
+};
+import {ErrorMessage} from '../../../src/classes/error-message';
+import {RequestHandler} from '../../../src/google-apis/auth-client';
+import {RequestInformationContainer} from '../../../src/classes/request-information-container';
+import {Request} from '../../../src/request-extractors/manual';
+
+describe('Manual handler', () => {
+ // nock.disableNetConnect();
+ // Mocked client
+ const client: RequestHandler = {
+ sendError(e: ErrorMessage, cb: () => void) {
+ // immediately callback
+ if (cb) {
+ setImmediate(cb);
+ }
+ },
+ } as {} as RequestHandler;
+ const report = manual.handlerSetup(client, config, {
+ warn(message: string) {
+ // The use of `report` in this class should issue the following
+ // warning becasue the `report` class is used directly and, as such,
+ // cannot by itself have information where a ErrorMessage was
+ // constructed. It only knows that an error has been reported. Thus,
+ // the ErrorMessage objects given to the `report` method in the tests
+ // do not have construction site information to verify that if that
+ // information is not available, the user is issued a warning.
+ assert.strictEqual(
+ message,
+ 'Encountered a manually constructed error ' +
+ 'with message "builder test" but without a construction site stack ' +
+ 'trace. This error might not be visible in the error reporting ' +
+ 'console.',
+ );
+ },
+ } as {} as Logger);
+ describe('Report invocation behaviour', () => {
+ it('Should allow argument-less invocation', () => {
+ const r = report(null!);
+ assert(r instanceof ErrorMessage, 'should be an inst of ErrorMessage');
+ });
+ it('Should allow single string', () => {
+ const r = report('doohickey');
+ assert(r instanceof ErrorMessage, 'should be an inst of ErrorMessage');
+ assert(r.message.match(/doohickey/), 'string error should propagate');
+ });
+ it('Should allow single inst of Error', () => {
+ const r = report(new Error('hokeypokey'));
+ assert(r.message.match(/hokeypokey/));
+ });
+ it('Should allow a function as a malformed error input', function (this, done) {
+ this.timeout(2000);
+ const r = report(() => {
+ assert(false, 'callback should not be called');
+ });
+ assert(r instanceof ErrorMessage, 'should be an inst of ErrorMessage');
+ setTimeout(() => {
+ done();
+ }, 1000);
+ });
+ it('Should callback to the supplied function', done => {
+ const r = report('malarkey', () => {
+ done();
+ });
+ assert(r.message.match(/malarkey/), 'string error should propagate');
+ });
+ it('replace the error string with the additional message', done => {
+ const r = report('monkey', 'wrench', () => {
+ done();
+ });
+ assert.strictEqual(
+ r.message,
+ 'wrench',
+ 'additional message should replace',
+ );
+ });
+ it('Should allow a full array of optional arguments', done => {
+ const r = report('donkey', {method: 'FETCH'}, 'cart', () => {
+ done();
+ });
+ assert.strictEqual(r.message, 'cart', 'additional message replace');
+ assert.strictEqual(r.context.httpRequest.method, 'FETCH');
+ });
+ it('Should allow all optional arguments except the callback', () => {
+ const r = report('whiskey', {method: 'SIP'}, 'sour');
+ assert.strictEqual(r.message, 'sour', 'additional message replace');
+ assert.strictEqual(r.context.httpRequest.method, 'SIP');
+ });
+ it('Should allow a lack of additional message', done => {
+ const r = report('ticky', {method: 'TACKEY'}, () => {
+ done();
+ });
+ assert(
+ r.message.match(/ticky/) && !r.message.match(/TACKEY/),
+ 'original message should be preserved',
+ );
+ assert.strictEqual(r.context.httpRequest.method, 'TACKEY');
+ });
+ it('Should ignore arguments', done => {
+ const r = report(
+ 'hockey',
+ (() => {
+ done();
+ }) as unknown as string,
+ 'field' as unknown as manual.Callback,
+ );
+ assert(
+ r.message.match('hockey') && !r.message.match('field'),
+ 'string after callback should be ignored',
+ );
+ });
+ it('Should ignore arguments', done => {
+ const r = report(
+ 'passkey',
+ (() => {
+ done();
+ }) as unknown as string,
+ {method: 'HONK'} as unknown as manual.Callback,
+ );
+ assert.notStrictEqual(r.context.httpRequest.method, 'HONK');
+ });
+ it('Should allow null arguments as placeholders', done => {
+ const r = report('pokey', null!, null!, () => {
+ done();
+ });
+ assert(r.message.match(/pokey/), 'string error should propagate');
+ });
+ it('Should allow explicit undefined', done => {
+ const r = report(
+ 'Turkey',
+ undefined as unknown as Request,
+ undefined as unknown as string,
+ () => {
+ done();
+ },
+ );
+ assert(r.message.match(/Turkey/), 'string error should propagate');
+ });
+ it('Should allow request to be supplied as undefined', done => {
+ const r = report(
+ 'turnkey',
+ undefined as unknown as Request,
+ 'solution',
+ () => {
+ done();
+ },
+ );
+ assert.strictEqual(r.message, 'solution', 'error should propagate');
+ });
+ it('Should allow additional message', done => {
+ const r = report(
+ 'Mickey',
+ {method: 'SNIFF'},
+ undefined as unknown as string,
+ () => {
+ done();
+ },
+ );
+ assert(
+ r.message.match(/Mickey/) && !r.message.match(/SNIFF/),
+ 'string error should propagate',
+ );
+ assert.strictEqual(r.context.httpRequest.method, 'SNIFF');
+ });
+ });
+
+ describe('Custom Payload Builder', () => {
+ it('Should accept builder inst as only argument', () => {
+ const msg = 'builder test';
+ const r = report(new ErrorMessage().setMessage(msg));
+ assert(
+ r.message.startsWith(msg),
+ 'string message should propagate from error message inst',
+ );
+ });
+ it('Should accept builder and request as arguments', () => {
+ const msg = 'builder test';
+ const oldReq = {method: 'GET'};
+ const newReq = {method: 'POST'};
+ const r = report(
+ new ErrorMessage()
+ .setMessage(msg)
+ .consumeRequestInformation(oldReq as RequestInformationContainer),
+ newReq,
+ );
+ assert(
+ r.message.startsWith(msg),
+ 'string message should propagate from error message inst',
+ );
+ assert.strictEqual(
+ r.context.httpRequest.method,
+ newReq.method,
+ [
+ 'request argument supplied at report invocation should propagte and',
+ 'if supplied, should overwrite any prexisting data in the field.',
+ ].join('\n'),
+ );
+ });
+ it('Should accept message and additional message params as', () => {
+ const oldMsg = 'builder test';
+ const newMsg = 'analysis';
+ const r = report(new ErrorMessage().setMessage(oldMsg), newMsg);
+ assert.strictEqual(
+ r.message,
+ newMsg,
+ [
+ 'message supplied at report invocation should propagte and, if',
+ 'supplied, should overwrite any prexisting data in the message field.',
+ ].join('\n'),
+ );
+ });
+ it('Should accept message and callback function', done => {
+ const oldMsg = 'builder test';
+ report(new ErrorMessage().setMessage(oldMsg), () => {
+ done();
+ });
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/interfaces/restify.ts b/handwritten/nodejs-error-reporting/test/unit/interfaces/restify.ts
new file mode 100644
index 00000000000..15d5d5fab50
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/interfaces/restify.ts
@@ -0,0 +1,156 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it} from 'mocha';
+import {EventEmitter} from 'events';
+import * as restify from 'restify';
+
+import {Configuration} from '../../../src/configuration';
+import {RequestHandler} from '../../../src/google-apis/auth-client';
+import {handlerSetup as restifyInterface} from '../../../src/interfaces/restify';
+
+// node v0.12 compatibility
+if (!EventEmitter.prototype.listenerCount) {
+ EventEmitter.prototype.listenerCount = function (this, eventName) {
+ return this.listeners(eventName as string).length;
+ };
+}
+
+describe('restifyInterface', () => {
+ const UNCAUGHT_EVENT = 'uncaughtException';
+ const FINISH = 'finish';
+ const noOp = () => {
+ return;
+ };
+ describe('Attachment to the uncaughtException event', () => {
+ it('Should attach one listener after instantiation', () => {
+ const ee = new EventEmitter();
+ assert.strictEqual(
+ ee.listenerCount(UNCAUGHT_EVENT),
+ 0,
+ 'Listeners on event should be zero',
+ );
+ // return the bound function which the user will actually interface with
+ const errorHandlerInstance = restifyInterface(null!, null!);
+ // execute the handler the user will use with the stubbed server instance
+ errorHandlerInstance(ee as restify.Server);
+ assert.strictEqual(
+ ee.listenerCount(UNCAUGHT_EVENT),
+ 1,
+ 'Listeners on event should now be one',
+ );
+ });
+ });
+ describe('Request handler lifecycle events', () => {
+ const ee = new EventEmitter();
+ const errorHandlerInstance = restifyInterface(null!, null!);
+ const requestHandlerInstance = errorHandlerInstance(ee as restify.Server);
+ describe('default path on invalid input', () => {
+ it('Should not throw', () => {
+ assert.doesNotThrow(() => {
+ requestHandlerInstance(null!, null!, noOp);
+ });
+ });
+ });
+ describe('default path without req/res error', () => {
+ ee.removeAllListeners();
+ const req = new EventEmitter();
+ const res = new EventEmitter();
+ (res as {} as {statusCode: number}).statusCode = 200;
+ it('Should have 0 listeners on the finish event', () => {
+ assert.strictEqual(res.listenerCount(FINISH), 0);
+ });
+ it('Should not throw while handling the req/res objects', () => {
+ assert.doesNotThrow(() => {
+ requestHandlerInstance(
+ req as restify.Request,
+ res as restify.Response,
+ noOp,
+ );
+ });
+ });
+ it('Should have 1 listener', () => {
+ assert.strictEqual(res.listenerCount(FINISH), 1);
+ });
+ it('Should not throw when emitting the finish event', () => {
+ assert.doesNotThrow(() => {
+ res.emit(FINISH);
+ });
+ });
+ });
+ describe('default path with req/res error', () => {
+ ee.removeAllListeners();
+ const client = {
+ sendError() {
+ assert(true, 'sendError should be called');
+ },
+ };
+ const config = {
+ getServiceContext() {
+ assert(true, 'getServiceContext should be called');
+ return {
+ service: 'stub-service',
+ version: 'stub-version',
+ };
+ },
+ lacksCredentials() {
+ return false;
+ },
+ getVersion() {
+ return '1';
+ },
+ } as {} as Configuration;
+ const errorHandlerInstance = restifyInterface(
+ client as {} as RequestHandler,
+ config,
+ );
+ const requestHandlerInstance = errorHandlerInstance(ee as restify.Server);
+ const req = new EventEmitter();
+ const res = new EventEmitter();
+ (res as {} as {statusCode: number}).statusCode = 500;
+ it('Should have 0 Listeners on the finish event', () => {
+ assert.strictEqual(res.listenerCount(FINISH), 0);
+ });
+ it('Should not throw on instantiation', () => {
+ assert.doesNotThrow(() => {
+ requestHandlerInstance(
+ req as restify.Request,
+ res as restify.Response,
+ noOp,
+ );
+ });
+ });
+ it('Should have 1 listener on the finish event', () => {
+ assert.strictEqual(res.listenerCount(FINISH), 1);
+ });
+ it('Should not throw on emission of the finish event', () => {
+ assert.doesNotThrow(() => {
+ res.emit(FINISH);
+ });
+ });
+ describe('Exercise the uncaughtException event path', () => {
+ it('Should call the sendError function property', done => {
+ client.sendError = () => {
+ assert(true, 'sendError should be called');
+ done();
+ };
+ assert.doesNotThrow(() => {
+ ee.emit(UNCAUGHT_EVENT);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/logger.ts b/handwritten/nodejs-error-reporting/test/unit/logger.ts
new file mode 100644
index 00000000000..282b0648d48
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/logger.ts
@@ -0,0 +1,107 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, before, after, beforeEach, afterEach} from 'mocha';
+import {createLogger} from '../../src/logger';
+
+describe('logger', () => {
+ describe('Initialization', () => {
+ let oldEnv: string | undefined;
+ before(() => {
+ oldEnv = process.env.GCLOUD_ERRORS_LOGLEVEL;
+ delete process.env.GCLOUD_ERRORS_LOGLEVEL;
+ });
+ after(() => {
+ process.env.GCLOUD_ERRORS_LOGLEVEL = oldEnv;
+ });
+ describe('Exception handling', () => {
+ it('Should not throw given undefined', () => {
+ assert.doesNotThrow(
+ createLogger,
+ createLogger() as {} as (err: Error) => boolean,
+ );
+ });
+ it('Should not throw given an empty object', () => {
+ assert.doesNotThrow(
+ createLogger.bind(null, {}),
+ createLogger() as {} as (err: Error) => boolean,
+ );
+ });
+ it('Should not throw given logLevel as a number', () => {
+ assert.doesNotThrow(
+ createLogger.bind(null, {logLevel: 3}),
+ createLogger({logLevel: 3}) as {} as (err: Error) => boolean,
+ );
+ });
+ it('Should not throw given logLevel as a string', () => {
+ assert.doesNotThrow(
+ createLogger.bind(null, {logLevel: '3'}),
+ createLogger({logLevel: 3}) as {} as (err: Error) => boolean,
+ );
+ });
+ it('Should not throw given an env variable to use', () => {
+ process.env.GCLOUD_ERRORS_LOGLEVEL = '4';
+ assert.doesNotThrow(
+ createLogger,
+ createLogger({
+ logLevel: 4,
+ }) as {} as (err: Error) => boolean,
+ );
+ delete process.env.GCLOUD_ERRORS_LOGLEVEL;
+ });
+ it('Should thow given logLevel as null', () => {
+ assert.throws(createLogger.bind(null, {logLevel: null!}), undefined);
+ });
+ });
+ describe('Default log level', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let oldLog: (text: any, args: string[]) => void;
+ let text: string | undefined;
+ beforeEach(() => {
+ // eslint-disable-next-line no-console
+ oldLog = console.error;
+ text = '';
+ // eslint-disable-next-line no-console
+ console.error = function (this, ...args: string[]) {
+ oldLog(this, args);
+ for (let i = 0; i < args.length; i++) {
+ text += args[i];
+ }
+ };
+ });
+ afterEach(() => {
+ text = undefined;
+ // eslint-disable-next-line no-console
+ console.error = oldLog;
+ });
+ it('Should print WARN logs by default', () => {
+ const logger = createLogger();
+ logger.warn('test warning message');
+ assert.strictEqual(
+ text,
+ 'WARN:@google-cloud/error-reporting: test warning message',
+ );
+ });
+ it('Should print ERROR logs by default', () => {
+ const logger = createLogger();
+ logger.error('test error message');
+ assert.strictEqual(
+ text,
+ 'ERROR:@google-cloud/error-reporting: test error message',
+ );
+ });
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/populate-error-message.ts b/handwritten/nodejs-error-reporting/test/unit/populate-error-message.ts
new file mode 100644
index 00000000000..4408d41ff27
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/populate-error-message.ts
@@ -0,0 +1,311 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, it, beforeEach} from 'mocha';
+
+import {ErrorMessage} from '../../src/classes/error-message';
+import {populateErrorMessage} from '../../src/populate-error-message';
+import {deepStrictEqual} from '../util';
+
+const TEST_USER_INVALID = 12;
+const TEST_MESSAGE = 'This is a test';
+const TEST_SERVICE_DEFAULT = {
+ service: 'node',
+ version: undefined,
+};
+const TEST_STACK_DEFAULT = {
+ filePath: '',
+ lineNumber: 0,
+ functionName: '',
+};
+
+/*
+ * The type of each property is {} to allow the tests to set values
+ * of various types to test the outcome.
+ */
+interface AnnotatedError {
+ user?: {};
+ serviceContext?: {};
+ stack?: {};
+ filePath?: {};
+ lineNumber?: {};
+ functionName?: {};
+}
+
+describe('populate-error-message', () => {
+ let em: ErrorMessage;
+ const adversarialObjectInput = {
+ stack: {},
+ };
+ const adversarialObjectInputTwo = {
+ stack: [],
+ };
+ beforeEach(() => {
+ em = new ErrorMessage();
+ });
+
+ it('Should not throw given undefined', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, undefined, em));
+ });
+
+ it('Should not throw given null', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, null, em));
+ });
+
+ it('Should not throw given a string', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, 'string_test', em));
+ });
+
+ it('Should not throw given a number', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, 1.2, em));
+ });
+
+ it('Should not throw given an array', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, [], em));
+ });
+
+ it('Should not throw given an object', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, {}, em));
+ });
+
+ it('Should not throw given an instance of Error', () => {
+ assert.doesNotThrow(populateErrorMessage.bind(null, new Error(), em));
+ });
+
+ it('Should not throw given an object of invalid form', () => {
+ assert.doesNotThrow(
+ populateErrorMessage.bind(null, adversarialObjectInput, em),
+ );
+ assert.doesNotThrow(
+ populateErrorMessage.bind(null, adversarialObjectInputTwo, em),
+ );
+ });
+
+ it('Message Field: Should set the message as the stack given an Error', () => {
+ const err = new Error(TEST_MESSAGE);
+ populateErrorMessage(err, em);
+ deepStrictEqual(
+ em.message,
+ err.stack,
+ 'Given a valid message the ' +
+ 'error message should absorb the error stack as the message',
+ );
+ });
+
+ it('Message Field: Should set the field given valid input given an object', () => {
+ let err = {};
+ const MESSAGE = 'test';
+ err = {message: MESSAGE};
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.message, MESSAGE);
+ });
+
+ it(
+ 'Message Field: Should default the field given lack-of input given ' +
+ 'an object',
+ () => {
+ const err = {error: 'some error message'};
+ populateErrorMessage(err, em);
+ assert(em.message.startsWith("{ error: 'some error message' }"));
+ },
+ );
+
+ it('User Field: Should set the field given valid input given an Error', () => {
+ const err: AnnotatedError = new Error();
+ const TEST_USER_VALID = 'TEST_USER';
+ err.user = TEST_USER_VALID;
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.user, TEST_USER_VALID);
+ });
+
+ it('User Field: Should default the field given invalid input given an Error', () => {
+ const err: AnnotatedError = new Error();
+ err.user = TEST_USER_INVALID;
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.user, '');
+ });
+
+ it('User Field: Should set the field given valid input given an object', () => {
+ const err: AnnotatedError = {};
+ const USER = 'test';
+ err.user = USER;
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.user, USER);
+ });
+
+ it(
+ 'User Field: Should default the field given lack-of input given an ' +
+ 'object',
+ () => {
+ const err = {};
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.user, '');
+ },
+ );
+
+ it(
+ 'ServiceContext Field: Should set the field given valid input given ' +
+ 'an Error',
+ () => {
+ const err: AnnotatedError = new Error();
+ const TEST_SERVICE_VALID = {service: 'test', version: 'test'};
+ err.serviceContext = TEST_SERVICE_VALID;
+ populateErrorMessage(err, em);
+ deepStrictEqual(err.serviceContext, TEST_SERVICE_VALID);
+ },
+ );
+
+ it(
+ 'ServiceContext Field: Should default the field given invalid input ' +
+ 'given an Error',
+ () => {
+ const err: AnnotatedError = new Error();
+ const TEST_SERVICE_INVALID = 12;
+ err.serviceContext = TEST_SERVICE_INVALID;
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.serviceContext, TEST_SERVICE_DEFAULT);
+ },
+ );
+
+ it(
+ 'ServiceContext Field: Should default the field if not given input ' +
+ 'given an Error',
+ () => {
+ const err = new Error();
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.serviceContext, TEST_SERVICE_DEFAULT);
+ },
+ );
+
+ it(
+ 'ServiceContext Field: Should set the field given valid input given an ' +
+ 'object',
+ () => {
+ const err: AnnotatedError = {};
+ const TEST_SERVICE_VALID = {service: 'test', version: 'test'};
+ err.serviceContext = TEST_SERVICE_VALID;
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.serviceContext, TEST_SERVICE_VALID);
+ },
+ );
+
+ it(
+ 'ServiceContext Field: Should default the field given invalid input ' +
+ 'given an object',
+ () => {
+ const err: AnnotatedError = {};
+ const TEST_SERVICE_INVALID = 12;
+ err.serviceContext = TEST_SERVICE_INVALID;
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.serviceContext, TEST_SERVICE_DEFAULT);
+ },
+ );
+
+ it(
+ 'ServiceContext Field: Should default the field given lack-of input ' +
+ 'given an object',
+ () => {
+ const err = {};
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.serviceContext, TEST_SERVICE_DEFAULT);
+ },
+ );
+
+ it(
+ 'Report location Field: Should default the field if given invalid input ' +
+ 'given an Error',
+ () => {
+ const TEST_STACK_INVALID_CONTENTS = {
+ filePath: null,
+ lineNumber: '2',
+ functionName: {},
+ };
+ const err: AnnotatedError = new Error();
+ err.stack = TEST_STACK_INVALID_CONTENTS;
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.context.reportLocation, TEST_STACK_DEFAULT);
+ },
+ );
+
+ it(
+ 'Report location Field: Should default field if not given a valid type ' +
+ 'given an Error',
+ () => {
+ const err: AnnotatedError = new Error();
+ const TEST_STACK_INVALID_TYPE = [] as {};
+ err.stack = TEST_STACK_INVALID_TYPE;
+ populateErrorMessage(err, em);
+ deepStrictEqual(em.context.reportLocation, TEST_STACK_DEFAULT);
+ },
+ );
+
+ it('FilePath Field: Should set the field given valid input given an object', () => {
+ const err: AnnotatedError = {};
+ const PATH = 'test';
+ err.filePath = PATH;
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.reportLocation.filePath, PATH);
+ });
+
+ it(
+ 'FilePath Field: Should default the field given lack-of input given ' +
+ 'an object',
+ () => {
+ const err = {};
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.reportLocation.filePath, '');
+ },
+ );
+
+ it('LineNumber Field: Should set the field given valid input given an object', () => {
+ const err: AnnotatedError = {};
+ const LINE_NUMBER = 10;
+ err.lineNumber = LINE_NUMBER;
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.reportLocation.lineNumber, LINE_NUMBER);
+ });
+
+ it(
+ 'LineNumber Field: Should default the field given lack-of input given ' +
+ 'an object',
+ () => {
+ const err = {};
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.reportLocation.lineNumber, 0);
+ },
+ );
+
+ it(
+ 'FunctionName Field: Should set the field given valid input given ' +
+ 'an object',
+ () => {
+ const err: AnnotatedError = {};
+ const FUNCTION_NAME = 'test';
+ err.functionName = FUNCTION_NAME;
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.reportLocation.functionName, FUNCTION_NAME);
+ },
+ );
+
+ it(
+ 'FunctionName Field: Should default the field given lack-of input given ' +
+ 'an object',
+ () => {
+ const err = {};
+ populateErrorMessage(err, em);
+ assert.strictEqual(em.context.reportLocation.functionName, '');
+ },
+ );
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/request-extractors/express.ts b/handwritten/nodejs-error-reporting/test/unit/request-extractors/express.ts
new file mode 100644
index 00000000000..d119826384a
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/request-extractors/express.ts
@@ -0,0 +1,159 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {Response} from 'express';
+import {expressRequestInformationExtractor} from '../../../src/request-extractors/express';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {deepStrictEqual} from '../../util';
+import {describe, it, beforeEach} from 'mocha';
+
+describe('Behaviour under varying input', () => {
+ let f: Fuzzer;
+ const DEFAULT_RETURN_VALUE = {
+ method: '',
+ url: '',
+ userAgent: '',
+ referrer: '',
+ statusCode: 0,
+ remoteAddress: '',
+ };
+ beforeEach(() => {
+ f = new Fuzzer();
+ });
+ it('Should return a default value given invalid input', () => {
+ const cbFn = (value: {}) => {
+ deepStrictEqual(value, DEFAULT_RETURN_VALUE);
+ };
+ f.fuzzFunctionForTypes(
+ expressRequestInformationExtractor,
+ ['object', 'object'],
+ cbFn,
+ );
+ });
+ it('Should return valid request object given valid input', () => {
+ const FULL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD',
+ url: 'www.TEST-URL.com',
+ 'user-agent': 'Something like Mozilla',
+ referrer: 'www.ANOTHER-TEST.com',
+ 'x-forwarded-for': '0.0.0.1',
+ connection: {
+ remoteAddress: '0.0.0.0',
+ },
+ };
+ const FULL_RES_DERIVATION_VALUE = {
+ statusCode: 200,
+ };
+ const FULL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD',
+ url: 'www.TEST-URL.com',
+ userAgent: 'Something like Mozilla',
+ referrer: 'www.ANOTHER-TEST.com',
+ remoteAddress: '0.0.0.1',
+ statusCode: 200,
+ };
+
+ const PARTIAL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ 'user-agent': 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ connection: {
+ remoteAddress: '0.0.2.1',
+ },
+ };
+ const PARTIAL_RES_DERIVATION_VALUE = {
+ statusCode: 201,
+ };
+ const PARTIAL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ userAgent: 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ remoteAddress: '0.0.2.1',
+ statusCode: 201,
+ };
+
+ const ANOTHER_PARTIAL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ 'user-agent': 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ };
+ const ANOTHER_PARTIAL_RES_DERIVATION_VALUE = {
+ statusCode: 201,
+ };
+ const ANOTHER_PARTIAL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ userAgent: 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ remoteAddress: '',
+ statusCode: 201,
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const headerFactory = (toDeriveFrom: any) => {
+ const lrn = Object.assign({}, toDeriveFrom);
+ lrn.header = (toRet: string) => {
+ if (Object.prototype.hasOwnProperty.call(lrn, toRet)) {
+ return lrn[toRet];
+ }
+ return undefined;
+ };
+ return lrn;
+ };
+ let tmpOutput = expressRequestInformationExtractor(
+ headerFactory(FULL_REQ_DERIVATION_VALUE),
+ FULL_RES_DERIVATION_VALUE as Response,
+ );
+ deepStrictEqual(
+ tmpOutput,
+ FULL_REQ_EXPECTED_VALUE,
+ [
+ 'Given a valid object input for the request parameter and an',
+ "'x-forwarded-for' parameter the request extractor should return",
+ "the expected full req output and the 'x-forwarded-for' value",
+ "as the value for the 'remoteAddress' property.",
+ ].join(' '),
+ );
+ tmpOutput = expressRequestInformationExtractor(
+ headerFactory(PARTIAL_REQ_DERIVATION_VALUE),
+ PARTIAL_RES_DERIVATION_VALUE as Response,
+ );
+ deepStrictEqual(
+ tmpOutput,
+ PARTIAL_REQ_EXPECTED_VALUE,
+ [
+ 'Given a valid object input for the request parameter but sans an',
+ "'x-forwarded-for' parameter the request extractor should return",
+ 'the expected parital req output and the remoteAddress value',
+ "as the value for the 'remoteAddress' property.",
+ ].join(' '),
+ );
+ tmpOutput = expressRequestInformationExtractor(
+ headerFactory(ANOTHER_PARTIAL_REQ_DERIVATION_VALUE),
+ ANOTHER_PARTIAL_RES_DERIVATION_VALUE as Response,
+ );
+ deepStrictEqual(
+ tmpOutput,
+ ANOTHER_PARTIAL_REQ_EXPECTED_VALUE,
+ [
+ 'Given a valid object input for the request parameter but sans an',
+ "'x-forwarded-for' parameter or a remoteAddress parameter",
+ 'the request extractor should return an empty string',
+ "as the value for the 'remoteAddress' property.",
+ ].join(' '),
+ );
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/request-extractors/hapi.ts b/handwritten/nodejs-error-reporting/test/unit/request-extractors/hapi.ts
new file mode 100644
index 00000000000..05490cfdc19
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/request-extractors/hapi.ts
@@ -0,0 +1,142 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as hapi from '@hapi/hapi';
+import {URL} from 'url';
+import {describe, it} from 'mocha';
+
+import {hapiRequestInformationExtractor} from '../../../src/request-extractors/hapi';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {deepStrictEqual} from '../../util';
+
+describe('hapiRequestInformationExtractor behaviour', () => {
+ describe('behaviour given invalid input', () => {
+ it('Should produce the default value', () => {
+ const DEFAULT_RETURN_VALUE = {
+ method: '',
+ url: '',
+ userAgent: '',
+ referrer: '',
+ statusCode: 0,
+ remoteAddress: '',
+ };
+ const f = new Fuzzer();
+ const cbFn = (value: {}) => {
+ deepStrictEqual(value, DEFAULT_RETURN_VALUE);
+ };
+ f.fuzzFunctionForTypes(hapiRequestInformationExtractor, ['object'], cbFn);
+ });
+ });
+ describe('behaviour given valid input', () => {
+ const FULL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD',
+ url: 'www.TEST-URL.com',
+ info: {
+ remoteAddress: '0.0.0.0',
+ },
+ headers: {
+ 'x-forwarded-for': '0.0.0.1',
+ 'user-agent': 'Something like Mozilla',
+ referrer: 'www.ANOTHER-TEST.com',
+ },
+ response: {
+ statusCode: 200,
+ },
+ };
+ const FULL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD',
+ url: 'www.TEST-URL.com',
+ userAgent: 'Something like Mozilla',
+ referrer: 'www.ANOTHER-TEST.com',
+ remoteAddress: '0.0.0.1',
+ statusCode: 200,
+ };
+ const PARTIAL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ info: {
+ remoteAddress: '0.0.2.1',
+ },
+ headers: {
+ 'user-agent': 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ },
+ response: {
+ output: {
+ statusCode: 201,
+ },
+ },
+ };
+ const PARTIAL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ userAgent: 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ remoteAddress: '0.0.2.1',
+ statusCode: 201,
+ };
+ const ANOTHER_PARTIAL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ headers: {
+ 'user-agent': 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ },
+ };
+ const ANOTHER_PARTIAL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD_#2',
+ url: 'www.SUPER-TEST.com',
+ userAgent: 'Something like Gecko',
+ referrer: 'www.SUPER-ANOTHER-TEST.com',
+ remoteAddress: '',
+ statusCode: 0,
+ };
+ it('Should produce the full request input', () => {
+ deepStrictEqual(
+ hapiRequestInformationExtractor(
+ FULL_REQ_DERIVATION_VALUE as {} as hapi.Request,
+ ),
+ FULL_REQ_EXPECTED_VALUE,
+ );
+ });
+ it('Should produce the partial request input', () => {
+ deepStrictEqual(
+ hapiRequestInformationExtractor(
+ PARTIAL_REQ_DERIVATION_VALUE as {} as hapi.Request,
+ ),
+ PARTIAL_REQ_EXPECTED_VALUE,
+ );
+ });
+ it('Should produce the second partial request input', () => {
+ deepStrictEqual(
+ hapiRequestInformationExtractor(
+ ANOTHER_PARTIAL_REQ_DERIVATION_VALUE as {} as hapi.Request,
+ ),
+ ANOTHER_PARTIAL_REQ_EXPECTED_VALUE,
+ );
+ });
+ it('Should deal with hapi v16+ URL objects', () => {
+ const PATH = '/foo/bar';
+ const REQUEST = {
+ ...FULL_REQ_DERIVATION_VALUE,
+ url: new URL(`https://www.SUPER-TEST.com${PATH}`),
+ };
+ const EXPECTED = {...FULL_REQ_EXPECTED_VALUE, url: PATH};
+ deepStrictEqual(
+ hapiRequestInformationExtractor(REQUEST as {} as hapi.Request),
+ EXPECTED,
+ );
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/request-extractors/koa.ts b/handwritten/nodejs-error-reporting/test/unit/request-extractors/koa.ts
new file mode 100644
index 00000000000..589c42757f0
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/request-extractors/koa.ts
@@ -0,0 +1,75 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {Request, Response} from 'koa';
+import {describe, it} from 'mocha';
+
+import {koaRequestInformationExtractor} from '../../../src/request-extractors/koa';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {deepStrictEqual} from '../../util';
+
+describe('koaRequestInformationExtractor', () => {
+ describe('Behaviour under invalid input', () => {
+ it('Should produce a default value', () => {
+ const DEFAULT_RETURN_VALUE = {
+ method: '',
+ url: '',
+ userAgent: '',
+ referrer: '',
+ statusCode: 0,
+ remoteAddress: '',
+ };
+ const f = new Fuzzer();
+ const cbFn = (value: {}) => {
+ deepStrictEqual(value, DEFAULT_RETURN_VALUE);
+ };
+ f.fuzzFunctionForTypes(
+ koaRequestInformationExtractor,
+ ['object', 'object'],
+ cbFn,
+ );
+ });
+ });
+ describe('Behaviour under valid input', () => {
+ it('Should produce the expected value', () => {
+ const FULL_REQ_DERIVATION_VALUE = {
+ method: 'STUB_METHOD',
+ url: 'www.TEST-URL.com',
+ headers: {
+ 'user-agent': 'Something like Mozilla',
+ referrer: 'www.ANOTHER-TEST.com',
+ },
+ ip: '0.0.0.0',
+ };
+ const FULL_RES_DERIVATION_VALUE = {
+ status: 200,
+ };
+ const FULL_REQ_EXPECTED_VALUE = {
+ method: 'STUB_METHOD',
+ url: 'www.TEST-URL.com',
+ userAgent: 'Something like Mozilla',
+ referrer: 'www.ANOTHER-TEST.com',
+ remoteAddress: '0.0.0.0',
+ statusCode: 200,
+ };
+ deepStrictEqual(
+ koaRequestInformationExtractor(
+ FULL_REQ_DERIVATION_VALUE as unknown as Request,
+ FULL_RES_DERIVATION_VALUE as Response,
+ ),
+ FULL_REQ_EXPECTED_VALUE,
+ );
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/request-extractors/manual.ts b/handwritten/nodejs-error-reporting/test/unit/request-extractors/manual.ts
new file mode 100644
index 00000000000..f4fd83a00d6
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/request-extractors/manual.ts
@@ -0,0 +1,122 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {manualRequestInformationExtractor} from '../../../src/request-extractors/manual';
+import {Fuzzer} from '../../../utils/fuzzer';
+import {deepStrictEqual} from '../../util';
+import {describe, it} from 'mocha';
+
+describe('manualRequestInformationExtractor', () => {
+ describe('Behaviour given invalid input', () => {
+ it('Should return default values', () => {
+ const DEFAULT_RETURN_VALUE = {
+ method: '',
+ url: '',
+ userAgent: '',
+ referrer: '',
+ statusCode: 0,
+ remoteAddress: '',
+ };
+ const f = new Fuzzer();
+ const cbFn = (value: {}) => {
+ deepStrictEqual(value, DEFAULT_RETURN_VALUE);
+ };
+ f.fuzzFunctionForTypes(
+ manualRequestInformationExtractor,
+ ['object'],
+ cbFn,
+ );
+ });
+ });
+ describe('Behaviour given valid input', () => {
+ const FULL_VALID_INPUT = {
+ method: 'GET',
+ url: 'http://0.0.0.0/myTestRoute',
+ userAgent: 'Something like Gecko',
+ referrer: 'www.example.com',
+ statusCode: 500,
+ remoteAddress: '0.0.0.1',
+ };
+ it('Should return expected output', () => {
+ deepStrictEqual(
+ manualRequestInformationExtractor(FULL_VALID_INPUT),
+ FULL_VALID_INPUT,
+ [
+ 'Given a full valid input object these values should be reflected by',
+ 'the output of the request extraction',
+ ].join(' '),
+ );
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const {method, ...sansMethod} = FULL_VALID_INPUT;
+ deepStrictEqual(
+ manualRequestInformationExtractor(sansMethod),
+ Object.assign({}, FULL_VALID_INPUT, {method: ''}),
+ [
+ 'Given a full valid input object sans the method property values',
+ 'should be reflected by the output of the request extraction',
+ ].join(' '),
+ );
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const {url, ...sansUrl} = FULL_VALID_INPUT;
+ deepStrictEqual(
+ manualRequestInformationExtractor(sansUrl),
+ Object.assign({}, FULL_VALID_INPUT, {url: ''}),
+ [
+ 'Given a valid input sans the url property these values should be',
+ 'reflected by the output of the request extraction',
+ ].join(''),
+ );
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const {userAgent, ...sansUserAgent} = FULL_VALID_INPUT;
+ deepStrictEqual(
+ manualRequestInformationExtractor(sansUserAgent),
+ Object.assign({}, FULL_VALID_INPUT, {userAgent: ''}),
+ [
+ 'Given a full valid input sans the userAgent property these values',
+ 'should be reflected by the output of the request extraction',
+ ].join(''),
+ );
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const {referrer, ...sansReferrer} = FULL_VALID_INPUT;
+ deepStrictEqual(
+ manualRequestInformationExtractor(sansReferrer),
+ Object.assign({}, FULL_VALID_INPUT, {referrer: ''}),
+ [
+ 'Given a full valid input sans the referrer property these values',
+ 'should be reflected by the output of the request extraction',
+ ].join(''),
+ );
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const {statusCode, ...sansStatusCode} = FULL_VALID_INPUT;
+ deepStrictEqual(
+ manualRequestInformationExtractor(sansStatusCode),
+ Object.assign({}, FULL_VALID_INPUT, {statusCode: 0}),
+ [
+ 'Given a full valid input sans the statusCode property these values',
+ 'should be reflected by the output of the request extraction',
+ ].join(''),
+ );
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const {remoteAddress, ...sansRemoteAddress} = FULL_VALID_INPUT;
+ deepStrictEqual(
+ manualRequestInformationExtractor(sansRemoteAddress),
+ Object.assign({}, FULL_VALID_INPUT, {remoteAddress: ''}),
+ [
+ 'Given a valid input sans the remoteAddress property these values',
+ 'should be reflected by the output of the request extraction',
+ ].join(''),
+ );
+ });
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/unit/service-configuration.ts b/handwritten/nodejs-error-reporting/test/unit/service-configuration.ts
new file mode 100644
index 00000000000..d9f96118ac3
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/unit/service-configuration.ts
@@ -0,0 +1,369 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import {describe, beforeEach, after, it} from 'mocha';
+import {FakeConfiguration as Configuration} from '../fixtures/configuration';
+import {deepStrictEqual} from '../util';
+const level = process.env.GCLOUD_ERRORS_LOGLEVEL;
+import {createLogger} from '../../src/logger';
+
+const logger = createLogger({
+ logLevel: typeof level === 'number' ? level : 4,
+});
+const serviceConfigEnv = {
+ GAE_SERVICE: process.env.GAE_SERVICE,
+ GAE_VERSION: process.env.GAE_VERSION,
+ GAE_MODULE_VERSION: process.env.GAE_MODULE_VERSION,
+ FUNCTION_NAME: process.env.FUNCTION_NAME,
+ GAE_MODULE_NAME: process.env.GAE_MODULE_NAME,
+};
+function sterilizeServiceConfigEnv() {
+ Object.keys(serviceConfigEnv).forEach(key => {
+ delete process.env[key];
+ });
+}
+function setEnv(envData: {
+ gaeServiceName: string | null;
+ gaeServiceVersion: string;
+ gaeModuleName: string | null;
+ gaeModuleVersion: string;
+ functionName: string | null;
+ kService: string | null;
+ kRevision: string | null;
+}) {
+ Object.assign(
+ process.env,
+ envData.gaeServiceName && {GAE_SERVICE: envData.gaeServiceName},
+ envData.gaeServiceVersion && {GAE_VERSION: envData.gaeServiceVersion},
+ envData.gaeModuleName && {GAE_MODULE_NAME: envData.gaeModuleName},
+ envData.gaeModuleVersion && {GAE_MODULE_VERSION: envData.gaeModuleVersion},
+ envData.functionName && {FUNCTION_NAME: envData.functionName},
+ envData.kService && {K_SERVICE: envData.kService},
+ envData.kRevision && {K_REVISION: envData.kRevision},
+ );
+}
+function restoreServiceConfigEnv() {
+ Object.assign(process.env, serviceConfigEnv);
+}
+
+describe('Testing service configuration', () => {
+ beforeEach(() => {
+ sterilizeServiceConfigEnv();
+ });
+ after(() => {
+ restoreServiceConfigEnv();
+ });
+ it(
+ 'A Configuration uses the function name as the service name on GCF ' +
+ 'if the service name is not given in the given config',
+ () => {
+ setEnv({
+ gaeServiceName: 'someModuleName',
+ gaeServiceVersion: '1.0',
+ gaeModuleName: 'InvalidName',
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: 'someFunction',
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration({}, logger);
+ deepStrictEqual(c.getServiceContext().service, 'someFunction');
+ // FUNCTION_NAME is set and the user didn't specify a version, and so
+ // the version should not be defined
+ deepStrictEqual(c.getServiceContext().version, undefined);
+ },
+ );
+ it(
+ 'A Configuration uses the function name as the service name on GCF ' +
+ 'if the service name is not given in the given config ' +
+ 'even if the GAE_SERVICE was not set',
+ () => {
+ setEnv({
+ gaeServiceName: null,
+ gaeServiceVersion: '1.0',
+ gaeModuleName: null,
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: 'someFunction',
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration({}, logger);
+ deepStrictEqual(c.getServiceContext().service, 'someFunction');
+ // The user didn't specify a version and FUNCTION_NAME is defined, and
+ // so the version should not be defined
+ deepStrictEqual(c.getServiceContext().version, undefined);
+ },
+ );
+ it(
+ 'A Configuration uses the GAE_SERVICE env value as the service name ' +
+ 'if the FUNCTION_NAME env variable is not set and the given config ' +
+ 'does not specify the service name',
+ () => {
+ setEnv({
+ gaeServiceName: 'someModuleName',
+ gaeServiceVersion: '1.0',
+ gaeModuleName: 'InvalidName',
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: null,
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration({}, logger);
+ deepStrictEqual(c.getServiceContext().service, 'someModuleName');
+ // The user didn't specify a version, and FUNCTION_NAME is not defined,
+ // and so use the GAE_MODULE_VERSION
+ deepStrictEqual(c.getServiceContext().version, '1.0');
+ },
+ );
+ it(
+ 'A Configuration uses the service name in the given config if it ' +
+ 'was specified and both the GAE_SERVICE and FUNCTION_NAME ' +
+ 'env vars are set',
+ () => {
+ setEnv({
+ gaeServiceName: 'someModuleName',
+ gaeServiceVersion: '1.0',
+ gaeModuleName: 'InvalidName',
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: 'someFunction',
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration(
+ {
+ serviceContext: {
+ service: 'customService',
+ },
+ },
+ logger,
+ );
+ deepStrictEqual(c.getServiceContext().service, 'customService');
+ // The user didn't specify a version, but FUNCTION_NAME is defined, and
+ // so the version should not be defined
+ deepStrictEqual(c.getServiceContext().version, undefined);
+ },
+ );
+ it(
+ 'A Configuration uses the service name and version in the given config' +
+ 'they were both specified and both the GAE_SERVICE and FUNCTION_NAME ' +
+ 'env vars are set',
+ () => {
+ setEnv({
+ gaeServiceName: 'someModuleName',
+ gaeServiceVersion: '1.0',
+ gaeModuleName: 'InvalidName',
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: 'someFunction',
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration(
+ {
+ serviceContext: {
+ service: 'customService',
+ version: '2.0',
+ },
+ },
+ logger,
+ );
+ deepStrictEqual(c.getServiceContext().service, 'customService');
+ // The user specified version should be used
+ deepStrictEqual(c.getServiceContext().version, '2.0');
+ },
+ );
+ it(
+ 'A Configuration uses the service name in the given config if it ' +
+ 'was specified and only the GAE_SERVICE env const is set',
+ () => {
+ setEnv({
+ gaeServiceName: 'someModuleName',
+ gaeServiceVersion: '1.0',
+ gaeModuleName: 'InvalidName',
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: null,
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration(
+ {
+ serviceContext: {
+ service: 'customService',
+ },
+ },
+ logger,
+ );
+ deepStrictEqual(c.getServiceContext().service, 'customService');
+ // The user didn't specify a version and FUNCTION_NAME is not defined
+ // and so the GAE_MODULE_VERSION should be used
+ deepStrictEqual(c.getServiceContext().version, '1.0');
+ },
+ );
+ it(
+ 'A Configuration uses the service name and version in the given config ' +
+ 'they were both specified and only the GAE_SERVICE env const is set',
+ () => {
+ setEnv({
+ gaeServiceName: 'someModuleName',
+ gaeServiceVersion: '1.0',
+ gaeModuleName: 'InvalidName',
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: null,
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration(
+ {
+ serviceContext: {
+ service: 'customService',
+ version: '2.0',
+ },
+ },
+ logger,
+ );
+ deepStrictEqual(c.getServiceContext().service, 'customService');
+ // The user specified version should be used
+ deepStrictEqual(c.getServiceContext().version, '2.0');
+ },
+ );
+ it(
+ 'A Configuration uses the service name in the given config if it ' +
+ 'was specified and only the FUNCTION_NAME env const is set',
+ () => {
+ setEnv({
+ gaeServiceName: null,
+ gaeServiceVersion: '1.0',
+ gaeModuleName: null,
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: 'someFunction',
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration(
+ {
+ serviceContext: {
+ service: 'customService',
+ },
+ },
+ logger,
+ );
+ deepStrictEqual(c.getServiceContext().service, 'customService');
+ // The user didn't specify a version and thus because FUNCTION_NAME is
+ // defined the version should not be defined
+ deepStrictEqual(c.getServiceContext().version, undefined);
+ },
+ );
+ it(
+ 'A Configuration uses the service name and version in the given config ' +
+ 'if they were both specified and only the FUNCTION_NAME env const is set',
+ () => {
+ setEnv({
+ gaeServiceName: null,
+ gaeServiceVersion: '1.0',
+ gaeModuleName: null,
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: 'someFunction',
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration(
+ {
+ serviceContext: {
+ service: 'customService',
+ version: '2.0',
+ },
+ },
+ logger,
+ );
+ assert.strictEqual(c.getServiceContext().service, 'customService');
+ // The user specified version should be used
+ assert.strictEqual(c.getServiceContext().version, '2.0');
+ },
+ );
+ it(
+ 'A Configuration uses the service name "node" and no version if ' +
+ 'GAE_SERVICE is not set, FUNCTION_NAME is not set, and the user has ' +
+ 'not specified a service name or version',
+ () => {
+ const c = new Configuration({}, logger);
+ assert.strictEqual(c.getServiceContext().service, 'node');
+ assert.strictEqual(c.getServiceContext().version, undefined);
+ },
+ );
+ it(
+ 'A Configuration uses the service name "node" and no version if ' +
+ 'GAE_SERVICE is not set, FUNCTION_NAME is not set, and the user has ' +
+ 'not specified a service name or version even if GAE_VERSION has ' +
+ 'been set',
+ () => {
+ setEnv({
+ gaeServiceName: null,
+ gaeServiceVersion: 'InvalidVersion',
+ gaeModuleName: null,
+ gaeModuleVersion: 'InvalidVersion',
+ functionName: null,
+ kService: null,
+ kRevision: null,
+ });
+ const c = new Configuration({}, logger);
+ assert.strictEqual(c.getServiceContext().service, 'node');
+ assert.strictEqual(c.getServiceContext().version, undefined);
+ },
+ );
+ it(
+ 'A Configuration uses the service name "node" and the user specified ' +
+ 'version if GAE_SERVICE is not set, FUNCTION_NAME is not set, and the ' +
+ 'user has not specified a service name but has specified a version',
+ () => {
+ const c = new Configuration(
+ {
+ serviceContext: {
+ version: '2.0',
+ },
+ },
+ logger,
+ );
+ deepStrictEqual(c.getServiceContext().service, 'node');
+ deepStrictEqual(c.getServiceContext().version, '2.0');
+ },
+ );
+ it('A Configuration uses the K_SERVICE and K_REVISION env variables if set', () => {
+ setEnv({
+ gaeServiceName: null,
+ gaeServiceVersion: 'x',
+ gaeModuleName: null,
+ gaeModuleVersion: 'y',
+ functionName: null,
+ kService: 'custom-service',
+ kRevision: 'custom-revision',
+ });
+ const c = new Configuration({}, logger);
+ assert.strictEqual(c.getServiceContext().service, 'custom-service');
+ assert.strictEqual(c.getServiceContext().version, 'custom-revision');
+ });
+ it('A Configuration gives priority to K_SERVICE and K_REVISION env variables', () => {
+ setEnv({
+ gaeServiceName: 'gae-service-name',
+ gaeServiceVersion: 'gae-service-version',
+ gaeModuleName: 'gae-module-name',
+ gaeModuleVersion: 'gae-module-version',
+ functionName: 'function-name',
+ kService: 'k-service',
+ kRevision: 'k-revision',
+ });
+ const c = new Configuration({}, logger);
+ assert.strictEqual(c.getServiceContext().service, 'k-service');
+ assert.strictEqual(c.getServiceContext().version, 'k-revision');
+ });
+});
diff --git a/handwritten/nodejs-error-reporting/test/util.ts b/handwritten/nodejs-error-reporting/test/util.ts
new file mode 100644
index 00000000000..cd62fbe328c
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/test/util.ts
@@ -0,0 +1,26 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import * as assert from 'assert';
+import * as stringify from 'json-stable-stringify';
+
+export type Anything = {} | undefined | null;
+
+export function deepStrictEqual(
+ actual: Anything,
+ expected: Anything,
+ message?: string,
+) {
+ assert.deepStrictEqual(stringify(actual), stringify(expected), message);
+}
diff --git a/handwritten/nodejs-error-reporting/tsconfig.json b/handwritten/nodejs-error-reporting/tsconfig.json
new file mode 100644
index 00000000000..a4dd53115a9
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "./node_modules/gts/tsconfig-google.json",
+ "compilerOptions": {
+ "lib": ["es2018", "dom"],
+ "rootDir": ".",
+ "outDir": "build",
+ "baseUrl": ".",
+ "paths": {
+ "@hapi/podium": [
+ "node_modules/@types/hapi__podium"
+ ]
+ }
+ },
+ "include": [
+ "src/*.ts",
+ "src/**/*.ts",
+ "test/*.ts",
+ "test/**/*.ts",
+ "system-test/*.ts",
+ "system-test/**/*.ts",
+ "utils/*.ts"
+ ]
+}
diff --git a/handwritten/nodejs-error-reporting/utils/errors-api-transport.ts b/handwritten/nodejs-error-reporting/utils/errors-api-transport.ts
new file mode 100644
index 00000000000..afb5d0a58be
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/utils/errors-api-transport.ts
@@ -0,0 +1,81 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+import {Configuration, Logger} from '../src/configuration';
+import {RequestHandler as AuthClient} from '../src/google-apis/auth-client';
+
+export interface ServiceContext {
+ service: string;
+ version: string;
+ resourceType: string;
+}
+
+export interface ErrorEvent {
+ eventTime: string;
+ serviceContext: ServiceContext;
+ message: string;
+ // other fields not used in the tests have been omitted
+}
+
+export interface ErrorGroupStats {
+ representative: ErrorEvent;
+ count: string;
+ // other fields not used in the tests have been omitted
+}
+
+export interface GroupStatesResponse {
+ errorGroupStats?: ErrorGroupStats[];
+ nextPageToken?: string;
+ timeRangeBegin: string;
+}
+
+/* @const {String} Base Error Reporting API */
+const API = 'https://clouderrorreporting.googleapis.com/v1beta1/projects';
+
+const ONE_HOUR_API = 'timeRange.period=PERIOD_1_HOUR';
+
+export class ErrorsApiTransport extends AuthClient {
+ constructor(config: Configuration, logger: Logger) {
+ super(config, logger);
+ }
+
+ async getAllGroups(
+ service: string,
+ version: string,
+ pageSize: number,
+ pageToken?: string,
+ ): Promise {
+ const id = await this.getProjectId();
+ const options = {
+ uri: [
+ API,
+ id,
+ 'groupStats?' +
+ ONE_HOUR_API +
+ `&serviceFilter.service=${service}&serviceFilter.version=${version}&pageSize=${pageSize}&order=LAST_SEEN_DESC` +
+ (pageToken ? `&pageToken=${pageToken}` : ''),
+ ].join('/'),
+ method: 'GET',
+ };
+ return new Promise((resolve, reject) => {
+ this.request(options, (err, body) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+ resolve(body);
+ });
+ });
+ }
+}
diff --git a/handwritten/nodejs-error-reporting/utils/fuzzer.ts b/handwritten/nodejs-error-reporting/utils/fuzzer.ts
new file mode 100644
index 00000000000..415ce11b6fe
--- /dev/null
+++ b/handwritten/nodejs-error-reporting/utils/fuzzer.ts
@@ -0,0 +1,316 @@
+// Copyright 2026 Google LLC
+//
+// 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.
+
+function _random(a: number, b: number) {
+ const lower = Math.ceil(Math.min(a, b));
+ const upper = Math.floor(Math.max(a, b));
+ return Math.floor(lower + Math.random() * (upper - lower + 1));
+}
+
+export class Fuzzer {
+ generate = {
+ types() {
+ return [
+ 'object',
+ 'array',
+ 'string',
+ 'number',
+ 'null',
+ 'undefined',
+ 'function',
+ 'boolean',
+ ];
+ },
+
+ string(len?: number) {
+ const lenChecked = (typeof len === 'number' ? len : 10)!;
+ const chars: string[] = [];
+
+ for (let i = 0; i < lenChecked; i++) {
+ chars.push(String.fromCharCode(_random(32, 126)));
+ }
+
+ return chars.join('');
+ },
+
+ boolean() {
+ return !!_random(0, 1);
+ },
+
+ alphaNumericString(len?: number) {
+ const lenChecked = (typeof len === 'number' ? len : 10)!;
+ const chars: string[] = [];
+ let thisRange: number[] = [];
+ const ranges = [
+ [48, 57],
+ [65, 90],
+ [97, 122],
+ ];
+
+ for (let i = 0; i < lenChecked; i++) {
+ thisRange = ranges[_random(0, 2)];
+ chars.push(String.fromCharCode(_random(thisRange[0], thisRange[1])));
+ }
+
+ return chars.join('');
+ },
+
+ function(this: {[key: string]: () => void; types: () => string[]}) {
+ const availableTypes = this.types().filter(i => i !== 'function');
+ const typeToGen = this.types()[_random(0, availableTypes.length - 1)];
+ const fnToCall = this[typeToGen];
+
+ return () => {
+ return fnToCall();
+ };
+ },
+
+ number(lower?: number, upper?: number) {
+ const lowerChecked = (typeof lower === 'number' ? lower : 0)!;
+ const upperChecked = (typeof upper === 'number' ? upper : 100)!;
+
+ return _random(lowerChecked, upperChecked);
+ },
+
+ null() {
+ return null;
+ },
+
+ undefined() {
+ return undefined;
+ },
+
+ array(
+ len?: number,
+ ofOneType?: string,
+ currentDepth?: number,
+ allowedDepth?: number,
+ ) {
+ const lenChecked = (typeof len === 'number' ? len : _random(1, 10))!;
+ let availableTypes = (
+ typeof ofOneType === 'string' && this.types().indexOf(ofOneType!) > -1
+ ? [ofOneType]
+ : this.types()
+ )!;
+ let currentDepthChecked = (
+ typeof currentDepth === 'number' ? currentDepth : 0
+ )!;
+ const allowedDepthChecked = (
+ typeof allowedDepth === 'number' ? allowedDepth : 3
+ )!;
+ const arr: Array<{}> = [];
+ let currentTypeBeingGenerated: string | undefined = '';
+ currentDepthChecked += 1;
+
+ // Deny the ability to nest more objects
+ if (currentDepthChecked >= allowedDepthChecked) {
+ availableTypes = this.types().filter(
+ i => i !== 'object' && i !== 'array',
+ );
+ }
+
+ for (let i = 0; i < lenChecked; i++) {
+ currentTypeBeingGenerated =
+ availableTypes[_random(0, availableTypes.length - 1)];
+
+ if (currentTypeBeingGenerated === 'object') {
+ arr.push(
+ this[currentTypeBeingGenerated](
+ null!,
+ currentDepthChecked,
+ allowedDepthChecked,
+ ),
+ );
+ } else if (currentTypeBeingGenerated === 'array') {
+ arr.push(
+ this[currentTypeBeingGenerated](
+ null!,
+ ofOneType,
+ currentDepthChecked,
+ allowedDepthChecked,
+ ),
+ );
+ } else {
+ arr.push(
+ (this as {[key: string]: Function})[currentTypeBeingGenerated!](),
+ );
+ }
+ }
+
+ return arr;
+ },
+
+ object(
+ numProperties?: number,
+ currentDepth?: number,
+ allowedDepth?: number,
+ ) {
+ const numPropertiesChecked = (
+ typeof numProperties === 'number' ? numProperties : _random(1, 10)
+ )!;
+ let currentDepthChecked = (
+ typeof currentDepth === 'number' ? currentDepth : 0
+ )!;
+ const allowedDepthChecked = (
+ typeof allowedDepth === 'number' ? allowedDepth : 3
+ )!;
+ const obj: {[key: string]: {}} = {};
+ currentDepthChecked += 1;
+
+ let availableTypes = this.types();
+
+ // Deny the ability to nest more objects
+ if (currentDepth! >= allowedDepth!) {
+ availableTypes = availableTypes.filter(
+ i => i !== 'object' && i !== 'array',
+ );
+ }
+
+ let currentTypeBeingGenerated: string | number = 0;
+ let currentKey = '';
+
+ for (let i = 0; i < numPropertiesChecked; i++) {
+ currentTypeBeingGenerated =
+ availableTypes[_random(0, availableTypes.length - 1)];
+ currentKey = this.alphaNumericString(_random(1, 10));
+
+ if (currentTypeBeingGenerated === 'object') {
+ obj[currentKey] = this[currentTypeBeingGenerated](
+ null!,
+ currentDepthChecked,
+ allowedDepthChecked,
+ );
+ } else if (currentTypeBeingGenerated === 'array') {
+ obj[currentKey] = this[currentTypeBeingGenerated](
+ null!,
+ null!,
+ currentDepthChecked,
+ allowedDepthChecked,
+ );
+ } else {
+ obj[currentKey] = (this as {[key: string]: Function})[
+ currentTypeBeingGenerated
+ ]();
+ }
+ }
+
+ return obj;
+ },
+ };
+
+ _maxBy(arr: Array>) {
+ const max = Math.max(...arr.map(o => o.length));
+ return arr.find(item => item.length === max);
+ }
+
+ _backFillUnevenTypesArrays(argsTypesArray: Array>) {
+ const largestLength = this._maxBy(argsTypesArray)!.length;
+
+ for (let i = 0; i < argsTypesArray.length; i++) {
+ if (argsTypesArray[i].length !== largestLength) {
+ while (argsTypesArray[i].length < largestLength) {
+ argsTypesArray[i].push(
+ argsTypesArray[i][_random(0, argsTypesArray[i].length - 1)],
+ );
+ }
+ }
+ }
+
+ return argsTypesArray;
+ }
+
+ _normalizeTypesArrayLengths(argsTypesArray: Array>) {
+ let allAreTheSameLength = true;
+ const lastLength = argsTypesArray[0].length;
+
+ for (let i = 1; i < argsTypesArray.length; i++) {
+ if (argsTypesArray[i].length !== lastLength) {
+ allAreTheSameLength = false;
+ break;
+ }
+ }
+
+ if (allAreTheSameLength) {
+ return argsTypesArray;
+ }
+
+ return this._backFillUnevenTypesArrays(argsTypesArray);
+ }
+
+ _generateTypesToFuzzWith(expectsArgTypes: Array) {
+ let argsTypesArray: Array> = [];
+ let tmpArray = this.generate.types();
+
+ for (let i = 0; i < expectsArgTypes.length; i++) {
+ if (!Array.isArray(expectsArgTypes[i])) {
+ argsTypesArray.push(
+ this.generate.types().filter(item => item !== expectsArgTypes[i]),
+ );
+ } else {
+ for (let j = 0; j < expectsArgTypes[i].length; j++) {
+ tmpArray = tmpArray.filter(arg => arg !== expectsArgTypes[i][j]);
+ }
+
+ argsTypesArray.push(([] as Array<{}>).concat(tmpArray));
+ tmpArray = this.generate.types();
+ }
+ }
+
+ argsTypesArray = this._normalizeTypesArrayLengths(argsTypesArray);
+ return argsTypesArray;
+ }
+
+ _generateValuesForFuzzTyping(typesToFuzzOnEach: string[][], index: number) {
+ const args: Array<{}> = [];
+ let typeToGen = '';
+ const gen = this.generate as {[key: string]: Function};
+
+ for (let i = 0; i < typesToFuzzOnEach.length; i++) {
+ typeToGen = typesToFuzzOnEach[i][index];
+
+ args.push(gen[typeToGen]());
+ }
+
+ return args;
+ }
+
+ fuzzFunctionForTypes(
+ fnToFuzz: Function,
+ expectsArgTypes?: string[],
+ cb?: Function,
+ withContext?: {},
+ ) {
+ const expectsArgTypesChecked = (
+ Array.isArray(expectsArgTypes) ? expectsArgTypes : []
+ )!;
+ const typesToFuzzOnEach = this._generateTypesToFuzzWith(
+ expectsArgTypesChecked,
+ ) as string[][];
+
+ let returnValue = undefined;
+
+ for (let i = 0; i < typesToFuzzOnEach[0].length; i++) {
+ returnValue = fnToFuzz.apply(
+ withContext,
+ this._generateValuesForFuzzTyping(typesToFuzzOnEach, i),
+ );
+
+ if (typeof cb === 'function') {
+ cb!(returnValue);
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/package.json b/package.json
index 8a65d2a1077..f745bc7114d 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,12 @@
"devDependencies": {
"semistandard": "^17.0.0"
},
+ "pnpm": {
+ "overrides": {
+ "cheerio": "1.0.0",
+ "tablesort": "5.2.1"
+ }
+ },
"engines": {
"node": ">=18"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 86d481864dc..338c05e1ff2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ cheerio: 1.0.0
+ tablesort: 5.2.1
+
importers:
.:
diff --git a/release-please-config.json b/release-please-config.json
index 9c211610789..605c313b4af 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -1,6 +1,8 @@
{
+ "bump-minor-pre-major": true,
"initial-version": "0.1.0",
"packages": {
+ "handwritten/nodejs-error-reporting": {},
"packages/gapic-node-processing": {},
"packages/google-ads-admanager": {},
"packages/google-ads-datamanager": {},
@@ -149,11 +151,11 @@
"packages/google-cloud-saasplatform-saasservicemgmt": {},
"packages/google-cloud-scheduler": {},
"packages/google-cloud-secretmanager": {},
+ "packages/google-cloud-securesourcemanager": {},
"packages/google-cloud-security-privateca": {},
"packages/google-cloud-security-publicca": {},
"packages/google-cloud-securitycenter": {},
"packages/google-cloud-securitycentermanagement": {},
- "packages/google-cloud-securesourcemanager": {},
"packages/google-cloud-servicedirectory": {},
"packages/google-cloud-servicehealth": {},
"packages/google-cloud-shell": {},
@@ -227,6 +229,5 @@
"type": "sentence-case"
}
],
- "bump-minor-pre-major": true,
"release-type": "node"
-}
\ No newline at end of file
+}