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. + + ![Error Reporting overview](https://raw.githubusercontent.com/googleapis/nodejs-error-reporting/master/doc/images/errors-overview.png) + + # 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`." +Google Cloud Platform logo + +# [Error Reporting: Node.js Client](https://github.com/googleapis/nodejs-error-reporting) + +[![release level](https://img.shields.io/badge/release%20level-stable-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) +[![npm version](https://img.shields.io/npm/v/@google-cloud/error-reporting.svg)](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. + +![Error Reporting overview](https://raw.githubusercontent.com/googleapis/nodejs-error-reporting/master/doc/images/errors-overview.png) + +# 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 +}