From 4fc204cce3c7c823e5c38f4cca8ae5de87cb67bf Mon Sep 17 00:00:00 2001 From: Cristian Cavalli Date: Thu, 6 Apr 2017 10:20:36 -0700 Subject: [PATCH] Stage errors for repo integration (#2017) --- packages/error-reporting/.clang-format.yaml | 2 + packages/error-reporting/.gitignore | 9 + packages/error-reporting/.jshintignore | 4 + packages/error-reporting/.jshintrc | 27 + packages/error-reporting/README.md | 230 ++++++ packages/error-reporting/config.tar.enc | Bin 0 -> 9232 bytes packages/error-reporting/package.json | 53 ++ .../src/classes/custom-stack-trace.js | 117 +++ .../src/classes/error-message.js | 291 ++++++++ .../classes/request-information-container.js | 130 ++++ packages/error-reporting/src/configuration.js | 450 ++++++++++++ .../src/error-extractors/error.js | 49 ++ .../src/error-extractors/object.js | 80 ++ .../src/error-handlers/error.js | 46 ++ .../src/error-handlers/number.js | 45 ++ .../src/error-handlers/object.js | 45 ++ .../src/error-handlers/string.js | 47 ++ .../src/error-handlers/unknown.js | 37 + packages/error-reporting/src/error-router.js | 63 ++ .../src/google-apis/auth-client.js | 164 +++++ packages/error-reporting/src/index.js | 148 ++++ .../error-reporting/src/interfaces/express.js | 81 +++ .../error-reporting/src/interfaces/hapi.js | 127 ++++ .../error-reporting/src/interfaces/koa.js | 68 ++ .../error-reporting/src/interfaces/manual.js | 103 +++ .../src/interfaces/message-builder.js | 44 ++ .../error-reporting/src/interfaces/restify.js | 159 ++++ .../src/interfaces/uncaught.js | 67 ++ packages/error-reporting/src/logger.js | 64 ++ .../src/request-extractors/express.js | 77 ++ .../src/request-extractors/hapi.js | 103 +++ .../src/request-extractors/koa.js | 55 ++ .../src/request-extractors/manual.js | 85 +++ .../system-test/testAuthClient.js | 349 +++++++++ .../test/fixtures/configuration.js | 27 + .../test/fixtures/gcloud-credentials.json | 6 + .../test/fixtures/uncaughtExitBehaviour.js | 93 +++ .../test-servers/express_scaffold_server.js | 134 ++++ .../test/test-servers/hapi_scaffold_server.js | 55 ++ .../test/test-servers/koa_scaffold_server.js | 57 ++ .../test-servers/manual_scaffold_server.js | 25 + .../test-servers/restify_scaffold_server.js | 33 + .../test/unit/testConfiguration.js | 314 ++++++++ .../test/unit/testCustomStackTrace.js | 77 ++ .../test/unit/testErrorMessage.js | 682 ++++++++++++++++++ .../test/unit/testExpressInterface.js | 90 +++ .../testExpressRequestInformationExtractor.js | 157 ++++ .../test/unit/testExtractFromErrorClass.js | 107 +++ .../test/unit/testExtractFromObject.js | 108 +++ .../test/unit/testHandleErrorClassError.js | 56 ++ .../test/unit/testHandleNumberAsError.js | 44 ++ .../test/unit/testHandleObjectAsError.js | 44 ++ .../test/unit/testHandleStringAsError.js | 44 ++ .../test/unit/testHandleUnknownAsError.js | 47 ++ .../test/unit/testHapiInterface.js | 147 ++++ .../testHapiRequestInformationExtractor.js | 127 ++++ .../testKoaRequestInformationExtractor.js | 71 ++ .../error-reporting/test/unit/testLogger.js | 61 ++ .../test/unit/testManualHandler.js | 186 +++++ .../testManualRequestInformationExtractor.js | 114 +++ .../unit/testRequestInformationContainer.js | 96 +++ .../test/unit/testRestifyInterface.js | 136 ++++ .../test/unit/testServiceConfiguration.js | 233 ++++++ .../error-reporting/test/unit/testUncaught.js | 95 +++ packages/error-reporting/utils/fuzzer.js | 315 ++++++++ scripts/docs/config.js | 4 + test/docs.js | 38 +- 67 files changed, 7241 insertions(+), 1 deletion(-) create mode 100644 packages/error-reporting/.clang-format.yaml create mode 100644 packages/error-reporting/.gitignore create mode 100644 packages/error-reporting/.jshintignore create mode 100644 packages/error-reporting/.jshintrc create mode 100644 packages/error-reporting/README.md create mode 100644 packages/error-reporting/config.tar.enc create mode 100644 packages/error-reporting/package.json create mode 100644 packages/error-reporting/src/classes/custom-stack-trace.js create mode 100644 packages/error-reporting/src/classes/error-message.js create mode 100644 packages/error-reporting/src/classes/request-information-container.js create mode 100644 packages/error-reporting/src/configuration.js create mode 100644 packages/error-reporting/src/error-extractors/error.js create mode 100644 packages/error-reporting/src/error-extractors/object.js create mode 100644 packages/error-reporting/src/error-handlers/error.js create mode 100644 packages/error-reporting/src/error-handlers/number.js create mode 100644 packages/error-reporting/src/error-handlers/object.js create mode 100644 packages/error-reporting/src/error-handlers/string.js create mode 100644 packages/error-reporting/src/error-handlers/unknown.js create mode 100644 packages/error-reporting/src/error-router.js create mode 100644 packages/error-reporting/src/google-apis/auth-client.js create mode 100644 packages/error-reporting/src/index.js create mode 100644 packages/error-reporting/src/interfaces/express.js create mode 100644 packages/error-reporting/src/interfaces/hapi.js create mode 100644 packages/error-reporting/src/interfaces/koa.js create mode 100644 packages/error-reporting/src/interfaces/manual.js create mode 100644 packages/error-reporting/src/interfaces/message-builder.js create mode 100644 packages/error-reporting/src/interfaces/restify.js create mode 100644 packages/error-reporting/src/interfaces/uncaught.js create mode 100644 packages/error-reporting/src/logger.js create mode 100644 packages/error-reporting/src/request-extractors/express.js create mode 100644 packages/error-reporting/src/request-extractors/hapi.js create mode 100644 packages/error-reporting/src/request-extractors/koa.js create mode 100644 packages/error-reporting/src/request-extractors/manual.js create mode 100644 packages/error-reporting/system-test/testAuthClient.js create mode 100644 packages/error-reporting/test/fixtures/configuration.js create mode 100644 packages/error-reporting/test/fixtures/gcloud-credentials.json create mode 100644 packages/error-reporting/test/fixtures/uncaughtExitBehaviour.js create mode 100644 packages/error-reporting/test/test-servers/express_scaffold_server.js create mode 100644 packages/error-reporting/test/test-servers/hapi_scaffold_server.js create mode 100644 packages/error-reporting/test/test-servers/koa_scaffold_server.js create mode 100644 packages/error-reporting/test/test-servers/manual_scaffold_server.js create mode 100644 packages/error-reporting/test/test-servers/restify_scaffold_server.js create mode 100644 packages/error-reporting/test/unit/testConfiguration.js create mode 100644 packages/error-reporting/test/unit/testCustomStackTrace.js create mode 100644 packages/error-reporting/test/unit/testErrorMessage.js create mode 100644 packages/error-reporting/test/unit/testExpressInterface.js create mode 100644 packages/error-reporting/test/unit/testExpressRequestInformationExtractor.js create mode 100644 packages/error-reporting/test/unit/testExtractFromErrorClass.js create mode 100644 packages/error-reporting/test/unit/testExtractFromObject.js create mode 100644 packages/error-reporting/test/unit/testHandleErrorClassError.js create mode 100644 packages/error-reporting/test/unit/testHandleNumberAsError.js create mode 100644 packages/error-reporting/test/unit/testHandleObjectAsError.js create mode 100644 packages/error-reporting/test/unit/testHandleStringAsError.js create mode 100644 packages/error-reporting/test/unit/testHandleUnknownAsError.js create mode 100644 packages/error-reporting/test/unit/testHapiInterface.js create mode 100644 packages/error-reporting/test/unit/testHapiRequestInformationExtractor.js create mode 100644 packages/error-reporting/test/unit/testKoaRequestInformationExtractor.js create mode 100644 packages/error-reporting/test/unit/testLogger.js create mode 100644 packages/error-reporting/test/unit/testManualHandler.js create mode 100644 packages/error-reporting/test/unit/testManualRequestInformationExtractor.js create mode 100644 packages/error-reporting/test/unit/testRequestInformationContainer.js create mode 100644 packages/error-reporting/test/unit/testRestifyInterface.js create mode 100644 packages/error-reporting/test/unit/testServiceConfiguration.js create mode 100644 packages/error-reporting/test/unit/testUncaught.js create mode 100644 packages/error-reporting/utils/fuzzer.js diff --git a/packages/error-reporting/.clang-format.yaml b/packages/error-reporting/.clang-format.yaml new file mode 100644 index 00000000000..e1a95210184 --- /dev/null +++ b/packages/error-reporting/.clang-format.yaml @@ -0,0 +1,2 @@ +BasedOnStyle: Google +Language: Javascript diff --git a/packages/error-reporting/.gitignore b/packages/error-reporting/.gitignore new file mode 100644 index 00000000000..43e232a9716 --- /dev/null +++ b/packages/error-reporting/.gitignore @@ -0,0 +1,9 @@ +node_modules +coverage +npm-debug.log +.DS_Store +.eslintrc.js +docs +tests/configuration +.nyc_output +*.patch diff --git a/packages/error-reporting/.jshintignore b/packages/error-reporting/.jshintignore new file mode 100644 index 00000000000..db20f1d7121 --- /dev/null +++ b/packages/error-reporting/.jshintignore @@ -0,0 +1,4 @@ +node_modules +test/e2e/node_modules +test/fixtures +coverage diff --git a/packages/error-reporting/.jshintrc b/packages/error-reporting/.jshintrc new file mode 100644 index 00000000000..53d73b7e997 --- /dev/null +++ b/packages/error-reporting/.jshintrc @@ -0,0 +1,27 @@ +{ + "bitwise": true, + "curly": true, + "eqeqeq": true, + "esnext": true, + "freeze": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "maxlen": 100, + "newcap": true, + "node": true, + "noarg": true, + "quotmark": "single", + "strict": true, + "trailing": true, + "undef": true, + "unused": "vars", + "globals": { + "describe": false, + "it": false, + "before": false, + "beforeEach": false, + "after": false, + "afterEach": false + } +} diff --git a/packages/error-reporting/README.md b/packages/error-reporting/README.md new file mode 100644 index 00000000000..54f56ab2b7c --- /dev/null +++ b/packages/error-reporting/README.md @@ -0,0 +1,230 @@ +# Node.js module for Stackdriver Error Reporting + +[![NPM Version][npm-image]][npm-url] +[![Known Vulnerabilities][snyk-image]][snyk-url] + +> **This is not an official Google product.** This module is experimental and may not be ready for use. +> This module uses APIs that may be undocumented and are subject to change without notice. + +This module provides Stackdriver Error Reporting support for Node.js applications. +[Stackdriver 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. Here's an introductory video: + +[![Learn about Error Reporting in Stackdriver](https://img.youtube.com/vi/cVpWVD75Hs8/0.jpg)](https://www.youtube.com/watch?v=cVpWVD75Hs8) + +## Prerequisites + +1. Your application needs to use Node.js version 4.x or greater. +1. You need a [Google Cloud project](https://console.cloud.google.com). Your application can run anywhere, but errors are reported to a particular project. +1. [Enable the Stackdriver Error Reporting API](https://console.cloud.google.com/apis/api/clouderrorreporting.googleapis.com/overview) for your project. +1. The module will only send errors when the `NODE_ENV` environment variable is +set to `production` or the `ignoreEnvironmentCheck` property given in the +runtime configuration object is set to `true`. + +## Quick Start + +1. **Install the module:** + + In your project, on the command line: + + ``` + # Install through npm while saving to the local 'package.json' + npm install --save @google-cloud/error-reporting + ``` +1. **Instrument your application:** + +```js +// Require the library and initialize the error handler +var errors = require('@google-cloud/error-reporting')({ + serviceContext: {service: 'my-service'} // not needed on Google Cloud +}); + +// Report an error to the Stackdriver Error Reporting API +errors.report(new Error('Something broke!')); +``` + +1. **View reported errors:** + + Open Stackdriver Error Reporting at https://console.cloud.google.com/errors to view the reported errors. + +## Running on Google Cloud Platform + +### Google App Engine Flexible environment + +If you are using [Google App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/), you do not have to do any additional configuration. + +### Google Compute Engine + +Your VM instances need to be created with the `https://www.googleapis.com/auth/cloud-platform` scope if created via the [gcloud](https://cloud.google.com/sdk) CLI or the Google Cloud Platform API, or by enabling at least one of the Stackdriver APIs if created through the browser-based console. + +If you already have VMs that were created without API access and do not wish to recreate it, you can follow the instructions for using a service account under [running elsewhere](#running-elsewhere). + +### Google Container Engine + +Container Engine nodes need to also be created with the `https://www.googleapis.com/auth/cloud-platform` scope, which is configurable during cluster creation. Alternatively, you can follow the instructions for using a service account under [running elsewhere](#running-elsewhere). It's recommended that you store the service account credentials as [Kubernetes Secret](http://kubernetes.io/v1.1/docs/user-guide/secrets.html). + +## Running Elsewhere + +If your application is running outside of Google Cloud Platform, such as locally, on-premise, or on another cloud provider, you can still use Stackdriver Errors. + +1. You will need to specify your project ID when starting the errors agent. + + GCLOUD_PROJECT=particular-future-12345 node myapp.js + +1. You need to provide service account credentials to your application. + * The recommended way is via [Application Default Credentials][app-default-credentials]. + 1. [Create a new JSON service account key][service-account]. + 1. Copy the key somewhere your application can access it. Be sure not to expose the key publicly. + 1. Set the environment variable `GOOGLE_APPLICATION_CREDENTIALS` to the full path to the key. The trace agent will automatically look for this environment variable. + * If you are running your application on a development machine or test environment where you are using the [`gcloud` command line tools][gcloud-sdk], and are logged using `gcloud beta auth application-default login`, you already have sufficient credentials, and a service account key is not required. + * Alternatively, you may set the `keyFilename` or `credentials` configuration field to the full path or contents to the key file, respectively. Setting either of these fields will override either setting `GOOGLE_APPLICATION_CREDENTIALS` or logging in using `gcloud`. For example: + +```js + // Require and start the agent with configuration options + var errors = require('@google-cloud/error-reporting')({ + // The path to your key file: + keyFilename: '/path/to/keyfile.json', + + // Or the contents of the key file: + credentials: require('./path/to/keyfile.json') + }); +``` + +When running on Google Cloud Platform, we handle these for you automatically. + +## Configuration + +The following code snippet lists all available configuration options. All configuration options are optional. + +```js +var errors = require('@google-cloud/error-reporting')({ + projectId: 'my-project-id', + keyFilename: '/path/to/keyfile.json', + credentials: require('./path/to/keyfile.json'), + // if true library will attempt to report errors to the service regardless + // of the value of NODE_ENV + // defaults to false + ignoreEnvironmentCheck: false, + // determines if the library will attempt to report uncaught exceptions + // defaults to true + reportUncaughtExceptions: true, + // determines the logging level internal to the library; levels range 0-5 + // defaults to 2 (warnings) + logLevel: 2, + serviceContext: { + service: 'my-service', + version: 'my-service-version' + } +}); +``` + +## Examples + +### Reporting Manually + +```js +var errors = require('@google-cloud/error-reporting')(); +// Use the error message builder to custom set all message fields +var errorEvt = errors.event() + .setMessage('My error message') + .setUser('root@nexus'); +errors.report(errorEvt, () => console.log('done!')); +// Or just use a regular error +errors.report(new Error('My error message'), () => console.log('done!')); +// One can even just use a string +errors.report('My error message'); +``` + +### Using Express + +```js +var express = require('express'); +var app = express(); +// Will create an errors instance based off env variables +var errors = require('@google-cloud/error-reporting')(); + +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); + +app.listen(3000); +``` + +### Using Hapi + +```js +var hapi = require('hapi'); +var errors = require('@google-cloud/error-reporting')(); + +var server = new hapi.Server(); +server.connection({ port: 3000 }); +server.start(); + +server.route({ + method: 'GET', + path: '/error', + handler: (request, reply) => { + reply('Something broke!'); + throw new Error('Custom error message'); + } +}); + +server.register({ register: errors.hapi }); +``` + +### Using Koa + +```js +var errors = require('@google-cloud/error-reporting')(); +var koa = require('koa'); +var app = 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'; +}); + +app.listen(3000); +``` + +### Using Restify + +```js +function respond(req, res, next) { + next(new Error('this is a restify error')); +} + +var restify = require('restify'); +var errors = require('@google-cloud/error-reporting')(); + +var server = restify.createServer(); + +server.use(errors.restify(server)); +server.get('/hello/:name', respond); +server.head('/hello/:name', respond); + +server.listen(8080); +``` + +[gcloud-sdk]: https://cloud.google.com/sdk/gcloud/ +[app-default-credentials]: https://developers.google.com/identity/protocols/application-default-credentials +[service-account]: https://console.developers.google.com/apis/credentials/serviceaccountkey +[npm-image]: https://badge.fury.io/js/%40google-cloud%2Ferror-reporting.svg +[npm-url]: https://npmjs.org/package/@google-cloud/error-reporting +[snyk-image]: https://snyk.io/test/npm/@google-cloud/error-reporting/badge.svg +[snyk-url]: https://snyk.io/test/npm/@google-cloud/error-reporting diff --git a/packages/error-reporting/config.tar.enc b/packages/error-reporting/config.tar.enc new file mode 100644 index 0000000000000000000000000000000000000000..ed17326de570eff6820ab5ed1b0d8009476e7fc4 GIT binary patch literal 9232 zcmV+rB=6hr0IYH{j!loT!9hObHJy7}mrQpR&908QnM$XKuhfFOpx#x9%n~Ik80EXj zHwG2S41-?H?=R;G)LD-c&Zv{DRQ-(SJynpf4e3R)d; z#uda4I5U8;PjwGFNmD{Zdtv&Wa+S#L7%Wml9+35zs*qz~5g8?O-Cq+U`=dTuXW z&ZPvf$2J;fPmh~Lw+HZ+sc2$VT27ZcMwF@WL@uf6(Up%J^_f%|P@K`_8upHpJ6ZR`PU;+LDPG>nDBGTObaWG5A z)ed9Afh7S8V`8m&|E7Bt2Ui<5QW|Gm6tt`C%#j1WF8kxyV7^9?$3b3s=f4D5^^^kgS+TUTqD*xOGiP6;@iqTQ??>#5^1CIgQvsEaKX;Qd!^`TtOWst3;rK|2eP_3fasAf|vm*@+O zT(HcaYWMm|xiOpa!a9W>ZG9sm@RjbQTTDV@Bj=PFthN9`03F_`KYV}g zPtpEL{Qz}jG&RT;3lzqRV0H>J{O!;_=bt-Xx#r--VwFJ0qopwR4F9o+OJR)Cw`20s z3g2RXc}?PG6k_evg{)V&YJO^o#Sn&62h5-dq`*tf0Dr_1K@08S0r?@R(x$4kPR^r0 z|B7W|(yW)_7e!qxpcSL!^Kw#(#8pSg$O11tX&MTG$OKo^#MmdMNpyc}Uu-zfKo{W^q{2Q_ zxA0a)?$H)Z{#Z&Up(4_Fu*dVBiO#gWv`cKq9>EHjSFJ67pl~thMcr>ZsOO_Z78Tzu zt90%Lt$$ z0coT%5c%o@e>ax7pj0`vP6?dd_nw*I?~hLp1F@X3c&l;4I?C9{6}>PWIgwAA=kzij zH}H$n!k7$mwL^d5Le`h6nYBfsfP&5dqwvxt?3_KUY2D(ckul*?p>u*|)+?{A+u zsh09PL@cRi>w7b~_dr~mh`T0?ZxJ`g7ZquHG@Bgff&ZOyie0*A9zok*@tFkxph(9B zyX&BqT;Z#+pKO^|NJ_YW zuJm^JU6KPQp5y zWj8t9B}0`tC_0>*_^GOu4(n_x#G&?L5h3%yYB24-dM-=Pc2{nPCl;&MR~V#4T}tt5+kI(%bBO(SZ9x`BeZ_ZqVj!7ZD*yGLo|pVk=p6q8|vn`G~cn!Hi zDOIRqDiF4H5Vc9Q#j-H4HZ8S0x)k2;S{ zW}?CARjg|ik<*>JB)i8j69#2%7SXUad?V`NSF4`X)8V{h{*E-%3Bqr){#Y3*Am}+N z=XazjGyWeML8V#b$W|YcJn9Q^A)0&}hipGzhaNYm<69zO-vuliTqbvPP!JMV(DIm!RhLlkD6`K zE1cr@5%MvjK|A@*$Br8qd^?A6WS**K&#yH(uo?$rOW@p=t|;`k&%~d!ZJv=hFHNd_ zwIo*({!Ip7&nJB>Q00;Lm?4|kZqk;qADKHe_Dvjq_NBo4H3eK_adjCmhf zD2>Vhm~8n3#lEcaS)JcXHdjaO1cw%?XzQo-%^@mo+9}hLQd{8|`2;`f0;S?L%y79) z@M2MknOmyTM*7#6T;_Gc{qx3Nh;QsaCRXQ!z91FY`X09>i}CmDhb7{JXQh%;E^ILJLbkMxI7PPz4SDh~K2Z8C!(e~*S#0=ocrkhy#U(E{V z%pX1|^SShgbIaIa(7tv>=A4?9ovmT%CFz7a6W=;`ef_KZ1k70myVP5+6a0O@^27^p zmLIzR0=06)f!Db5yf;8U8=~0Hm$CW&)XKitG|(Kg?d7Lr)nar|gecy=m|1fjO%sa% z`~{ZVo0-wpMyQN!pE+t3fhM4sK*h>3%0k+5XN-&bNFiCO)6On)QpCf{7;P5x+;bUk z&CsSYK6Bd4L~4B{Fd*%xrLR;=biiOcs>3*a4>KEDQj*T2hML29I-Xe&Cxt0%7TM#FKmWmUU zzEhJD@@{hdsZ!elV)Tc{aC>}bi zDk7Gxy8>y&)Lzv+e1cwJ%ayICj^EFy10)||2GprW>{O0UoRjY~l@jpEMShGE!@&)* z4>FKvGSQ)h-FzLXkLAr3oiGmYXi+%T zE;^YDXK6ihn@}6C@D@3{<5Jhnn2tBLbwBvmwSm2;m*lXX{ny3c3B#oQ%6a8f50k`8ATf<0I1H=FttC-0;m?AiOh)R(GJl zWnclQ;b%f6)dhOy%bBPba%f50I9S|X;!FtN7Sgaq_vBIQnp3C#Vn7hlkK4hUjYu-- zk!$OR-0gGv17!t*gjCb3%{@KjmXuC|Kf&!N@M;N>AIDh)ssk18T-3B;SUnhaTQ4S% z*6LjiJp3u_(!9FIh{)g;%msf0YaM5W4iYE3N8nwPw(3Dy>L{+$F$HP>K8s^{I#mMo z?77PesT~Nl z{!7n<&xQuj`MPPUvnlv5q<)po2$ZBdL9O6xuQdYu`HiEp=BZy(MJA%~Mx@v_FsF`|yjtba%ocex>|ivUvgxA2J6bf9N9(L! z8uv_krTcr|T{N=w`LqgJDMu4fNQ1AXRgprAPPvN0(t4*r5D=Lv0naa*bm-ewsV{#f ziZGPf>^5v+Go8^$k+|o?luX>7%-S01c9#P-shD+DSP$F4OW^4r8w*^~#7dqKL2l*H z@dY>Uzt)X~e%F>@$bi?QrudF#%%S$H`gg^2`jxju9~KKp)1^x9MTD?bzE$8e zAjd&BWlz5&N8$;b0A*T)fBOf7$>b80dP7-F!+ z?Vc&_U?0Pl$msmxYp#9cSwoHNugN>T{=f=qa0}bk>1*h(Gc#WQKr?4nGKjATp8vA( zo_^4&`b1iA9MLQ0L9#Qg5EwUNIU~W|X~vWE9U}nb&7n76(2hV>M8udR4cAj$g8XLP zRxQ%mJMX@-nbBZuLhpIrXmK&pXsq}#g_9pf^~cH zT{i$fuvrv_ZbGP2(q%s!8hgr&c3)NNMSUA^9DQH(GT z;)@^3Z51QSu6fUFThmSkeMvd)G$M=F|h{76@N9XE=oK5GU)4w7e}j`v_>-(7iVZix?;ro~0X0F7V=tcVMnu)35b*T2MhK&j9%&yj zPOpG=9jsj{H*JyN z>tGC^k#pW(jxfZZBh+_1R2fKBh_`5(dsDjanrH_1{*&wpTavV_Z)2@_=5X|iSX%|$ z-;CQ#AQ^_4lAPEodp-DyWzhK;g1ON|LlJ%k$;KW#^HgwX9krtmEv|A}X~{zuwE>&3 z7Q?FlfqIYEuE&C_lJ9Jv!>46$A1VOeke_9@8fPNGT9wEW~Pg^#Yq6Vp;E z+j;32a&ZSUE$dkzM}&RWHON|IhA?ZSOANEqpeZxy-g`7Sg}dFw<^iCwxT;k@sJ3on zDt1Y~0@G0g2g>sr9(fA&Ob>IKGlz!v1A3qX|84Q6AAz1iE&%#Ep#mB7w^9z}`66=@ z(oE=rEUcckIcsV1HW2_@+dyJ&QtKhhq#Mq0QMe+D?j5->G5WM6r84B?I0%>k&~luR zKx!O3B&$bH(M6ETpYDqh;rUt+<-2o>{fzU2UKZ`*V*r`h|Vr=%0=gvvuDGDrjD~r=%t@(s#LsMn5{5Ci9VOX)M2oalQ2NsLd zA!)r=o#Xrp&RX{)s6vIlqHZdAa-26h>p+l0J3k&%KCWxNuB#7Q-t7wX_F5>G92IuHvNQ9P5Jn^m?$5n$i)*xA9=CCE}+g(i3l) zrqmAC*KdRbXvGJYL^M#b32_MM)u|(UO}ggM1JFgl)uwg;A?o){>H=u8qB9A9S#|B7 z4OeVW()6^WLQ{pHYtTzcOU!hPL?}N@uF+WsBO`roO@&>nMWX(p#4!*d+CYzkiq+mK;HdWMscR=_#H#Kj8~6ks zF{3F;Ax&!6{E#=yBN$x;0&QtAhwkGSm+NWz%sI$?`X=tu`2k}gafXBomm^}a(h(Ek zFrIeRZKPTkn=-{NqhZP;#<2K7Baa`xymlaRdX`K&OT3(a`hKdo$SJ8J3%xlqK~jRG z%+x6%K;jC`&1l~KFIp85Aohe8GuAHbVS?jlOK{tW2<0`q{&`yM`*LOmVH6Q%U zBd@q7DVcr{o}ihy9V91a$q(P_D4_Qc=M#ul$;bDk+=rI097eHA52xZBA#gI`LpRlf zxh@RnWT@H#wrjPecq@u`pxRZqUE6W|kKW)CZ%X)-kwoE^iG{3tnG$OZ7z#n;!)F$& zUWUuM3$OPBmqR+UW~pBNMB!$J%24um-0xSb^AuUUg~wwUMQ@Wwnb7u71YC$w`mc%R z3qT}Gb2Ft8HpLXfzD|?j89hCfrrT100|jDqQS_XQJ!&Ra|ce zT@XHVA}ZjJ9_;n23L>>?+&VCRA&iv1tcZBXCbaPN<}8LiJku;3eHUa+X>Zm_-|flc znV#i!)SLY?HZi=*gH19hMaY@jdlA4E!+}aHyvZScOZ+-w8TAh|LRs`O~B9{4f>pJSX2AF46w(LFg+uJ9~pa_~Cyg~&MTqy`${=ZbEIYD~A#8~T6 zRT@f5YfTz>yW*0?aSOoVY3dUP@GPBbSqsib{p;m}y)3ub9Vw&CbUWz70awi=B@UPo z3J69F6WLg2rQb0%_x1%xGhW^{0lRI!GcUc(lQxB?OWc$yfo!1Zeri`4En5 zp-o6BK|e}Q#+il4{xnK+XxC&{z%deL$Iu=M@b|Z+WC*a2jpEfYkGz3f;p{k{QmcGQ zzxaW03X1v3_oC`D96K=0v6npg=2_wCCDg=^UjN34GM7g~(ISReKNKL4&cMX~j=}Ti z8Pca2Q=fXCju|*&OMOa1T~;ob3HSP=J2}nMLP$a~chE#Nsq6qkg5NjY5e*qT5H?6O zIpj{Rn6pe53G#K3YoGKa?D3z8=(w{piieeFR!&5p+Kh8C;%WhoqvSdO@v%wE3~ebT zPNWc#;cy8Ktg~mpos*$vhw4mWAK@ZPATeI?v;5~Ya4lDZw)SjLZA!Q?rWb)@*P38L zVm4%AgDn_vMWl#rwnzB>o{fXE$`pHhM*81}lCI>6STSoBy{h)jZhVowyF8&5lO|!| z65xjN+caF_==`c~^7GVPt3S%#JdJ?^tL>Q9D1kjeHK=4{=tozHjJyKP{}SkT(wCO? z3_lE1Bi6^H(37f?`*x96<)=S}ZvhDL&j2>*(o!W^5V(f?2I(tA+-U4{uOeaBNHBnP z>lLv>%4(lp8?QH{RE#|-jw@!EC$cN}&Jr<^$F26P{$neq<@Tn8RbALGh*{w3Y4tTW z%6`6>ZOc}RqV2L}k^LAf9dyW8LT87C!8ye9U@xX}Qm{Nr0!YGljx;!5^x~`PuD9w- z(&yNH9Ckd;^QX!OA~?8ZE40%<%4QvA?N)L-VkrWYO^V>q3Sm7Vz6Pbd$0l#p56 z>%6V|Qw#sLoJy+2&iT9I2k8)^0Up+2#xAhBpHjB{uAs&qhW)Xy*hyz^q>dF^w}VWX zwCOns5g0JV4sB9xD$k0q$nYqBbloFQPxaAqbxUrpK)rD6-|FcT4S{n9 zoi&XWymj8wFsdp0>U8eDxNS+xH*blOy(9X14h5IBHTO{0lYy_%^;4HKQq_bE{rzHC z`UMh$CP%ZkKV~Hx=YS#g#O*jjX00($dMwzS`6k{yTTX*y%0@CxDS((#Vp6dsh-Ekc zf(HvaR0#&s=f{vIKjuNg?}jJ$!_>cE{hPPwb}aj24PJ=u!`Nj_U#c)$*c4Et+BA;< zl9L;?Lt0J}9!1>LqG$@-fAH}az|>X2EAdLR%lP7VC=s6<75RTr0Fsi8eVdZ-rw5QE zd;iu{SOs(O(q2dG$+AIce$PiQielywwB)~ItaPKJe37v7ed-~ZZuGy6_>B*X5AUkJ zy?#mze6K;M3S@C|*G!;p;dXkcdQwk1(Fi+hukYu-&tuvkBF_M~nBX9df5`Z+~&VIV48&DPLaMEDfTB4qh+3kqvM~KaS6RA14Ka!4~G21+9T58J7y}5v`OK zE1uu6X%WY<9Ze&C(nS-5OIl(}zO*lIpK_;zt&|&hXRyyJ1|dsDuFBX7IGC5d<^m$p zlV_k2*YtBJ0idX56$Pkp_LoOlJ_C!w+xjOf{V-W!yv%Lflfr@j$BmTFidpw&>+qU1MhmboLWfnS!q;6ZX+7#?ukhP&6bOpd<1E-uBmU;Kx|(cz_%)*RYu z2}qO~97QB3;_&wQwvWpji&fg1-glr@5eLdhLh>P*9rwi!pRI9#nrFgEM$NM1hYe%7 zMe0pm8Oy>EF@gilmAJvy+)1s%^C+n2<-_uV;o^)ZYgR1icF1H_)+61b*O;g$bQgv< zZSiRc*4$xPf3}-q)fDj%juUzGH3L$gR7?kbDVWLrq2Krf*>uxyuflfC!)oYJ-c~0= zwB6)XXzn%0C{Ve}96w!;U2?jQwBwU@F!@ybq*Ecj+@+abn+eW%7a>zkg@b|Y`S-(f zg&g1mkD{(l8B*;DYFJ=+li8a6asr1U*b(XX&C9 zO#}EvB6%o2Y43lqFWh}VNF61*hIt*2D$b_9$H4K^-hI{+c|OFL+u`v7ixqu&r{Fyr z71#?;VEZ7^`SL<+k3R1y`Cn^r@L7_xy^WxR?udu?|7Y|-O^v}MuBKknHt1mX1H%yF=R}l6(}MV45H@78`W4mUUA;Nx6|<% zl1GFY#X@Sr$NQ}1)!okBAH-XiDt@PaI&fal^^d_{Ie8^$b{4Yo5b_(|9on;{6l8D} zOEkY%L0U_7w|lxnFV%A%J0PdK2HPe`RqW=5=Q1m1{i0Upv&gVasF*fZBz;3S~5E#^E`Z**eR1c&hv5yL10p63^nXQcy+kOG*6Mmf^gus-#fl z7K~BlM#<3<=W-VI0Eo_!qJSiICC7`vdV;pnv#$QESuPM#%a*3^C0lrj(Nm!seU&}V zhcCPAt`@v9wc&iMrbSlMS9%e8hQJ;t~zS@lA1`mgTI zZeN0CddHTcNFOT|pMd4vLww+uyu|uvkB?9FLeHcgQLv^#ZsOd^W%V?zDAF z#;q&=4.0" + }, + "files": [ + "src", + "utils", + "index.js" + ] +} diff --git a/packages/error-reporting/src/classes/custom-stack-trace.js b/packages/error-reporting/src/classes/custom-stack-trace.js new file mode 100644 index 00000000000..7e9d3ea8eba --- /dev/null +++ b/packages/error-reporting/src/classes/custom-stack-trace.js @@ -0,0 +1,117 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isString = is.string; +var isNumber = is.number; +var isFunction = is.fn; + +/** + * A function which is used as a default substitute to handle cases where an + * Error's call site array was not properly generated or its length was not + * greater than 0. This function aims to produce a basic notification that + * CallSite extraction failed. + * @function stubbedNoOp + * @returns {String} - A JSON object serving as a notification with one + * property: error whose value is a string describing that a stack trace could + * not be captured. + */ +function stubbedNoOp() { + + return JSON.stringify({error: 'Unable to capture stack trace information'}); +} + +/** + * The constuctor for CustomStackTrace takes no arugments and is solely meant + * to instantiate properties on the instance. Each property should be externally + * set using the corresponding set function. + * @class CustomStackTrace + * @classdesc CustomStackTrace is a class which is meant to store and control- + * for top-frame values on a stack trace dump. This class can be stringified + * for representation in JSON. + * @property {String} filePath - The file path of CallSite object + * @property {Number} lineNumber - The line number of the CallSite object + * @property {String} functionName - The function name of the CallSite object + * @property {Function} stringifyStucturedCallList - A function which is meant + * to produce the JSON representation of the full stack trace. + */ +function CustomStackTrace() { + + this.filePath = ''; + this.lineNumber = 0; + this.functionName = ''; + this.stringifyStucturedCallList = stubbedNoOp; +} + +/** + * Sets the filePath property on the instance + * @function setFilePath + * @chainable + * @param {String} filePath - the file path of the CallSite object + * @returns {this} - returns the instance for chaining + */ +CustomStackTrace.prototype.setFilePath = function(filePath) { + + this.filePath = isString(filePath) ? filePath : ''; + + return this; +}; + +/** + * Sets the lineNumber property on the instance + * @function setLineNumber + * @chainable + * @param {Number} lineNumber - the line number of the CallSite object + * @returns {this} - returns the instance for chaining + */ +CustomStackTrace.prototype.setLineNumber = function(lineNumber) { + + this.lineNumber = isNumber(lineNumber) ? lineNumber : 0; + + return this; +}; + +/** + * Sets the functionName property on the instance + * @function setFunctionName + * @chainable + * @param {String} functionName - the function name of the CallSite object + * @returns {this} - returns the instance for chaining + */ +CustomStackTrace.prototype.setFunctionName = function(functionName) { + + this.functionName = isString(functionName) ? functionName : ''; + + return this; +}; + +/** + * Sets the stringifyStucturedCallList on the instance + * @function setStringifyStructuredCallList + * @chainable + * @param {Function} op - the function to produce the JSON representation of + * the full stack trace + * @returns {this} - returns the instance for chaining + */ +CustomStackTrace.prototype.setStringifyStructuredCallList = function(op) { + + this.stringifyStucturedCallList = isFunction(op) ? op : stubbedNoOp; + + return this; +}; + +module.exports = CustomStackTrace; diff --git a/packages/error-reporting/src/classes/error-message.js b/packages/error-reporting/src/classes/error-message.js new file mode 100644 index 00000000000..9683c87f08d --- /dev/null +++ b/packages/error-reporting/src/classes/error-message.js @@ -0,0 +1,291 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isString = is.string; +var isNumber = is.number; +var isObject = is.object; + +/** + * 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 + * Stackdriver 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 + */ +function ErrorMessage() { + + 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 + */ +ErrorMessage.prototype.setEventTimeToNow = function() { + + 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 + */ +ErrorMessage.prototype.setServiceContext = function(service, version) { + + this.serviceContext.service = isString(service) ? service : 'node'; + this.serviceContext.version = isString(version) ? 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 + */ +ErrorMessage.prototype.setMessage = function(message) { + + this.message = isString(message) ? 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 + */ +ErrorMessage.prototype.setHttpMethod = function(method) { + + this.context.httpRequest.method = isString(method) ? 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 + */ +ErrorMessage.prototype.setUrl = function(url) { + + this.context.httpRequest.url = isString(url) ? 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 + */ +ErrorMessage.prototype.setUserAgent = function(userAgent) { + + this.context.httpRequest.userAgent = isString(userAgent) ? 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 + */ +ErrorMessage.prototype.setReferrer = function(referrer) { + + this.context.httpRequest.referrer = isString(referrer) ? 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 + */ +ErrorMessage.prototype.setResponseStatusCode = function(responseStatusCode) { + + this.context.httpRequest.responseStatusCode = + isNumber(responseStatusCode) ? 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 + */ +ErrorMessage.prototype.setRemoteIp = function(remoteIp) { + + this.context.httpRequest.remoteIp = isString(remoteIp) ? 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 + */ +ErrorMessage.prototype.setUser = function(user) { + + this.context.user = isString(user) ? 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 + */ +ErrorMessage.prototype.setFilePath = function(filePath) { + + this.context.reportLocation.filePath = isString(filePath) ? 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 + */ +ErrorMessage.prototype.setLineNumber = function(lineNumber) { + + this.context.reportLocation.lineNumber = + isNumber(lineNumber) ? 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 + */ +ErrorMessage.prototype.setFunctionName = function(functionName) { + + this.context.reportLocation.functionName = + isString(functionName) ? 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 + */ +ErrorMessage.prototype.consumeRequestInformation = function( + requestInformation) { + + if (!isObject(requestInformation)) { + + return this; + } + + this.setHttpMethod(requestInformation.method) + .setUrl(requestInformation.url) + .setUserAgent(requestInformation.userAgent) + .setReferrer(requestInformation.referrer) + .setResponseStatusCode(requestInformation.statusCode) + .setRemoteIp(requestInformation.remoteAddress); + + return this; +}; + +module.exports = ErrorMessage; diff --git a/packages/error-reporting/src/classes/request-information-container.js b/packages/error-reporting/src/classes/request-information-container.js new file mode 100644 index 00000000000..70c76b846d9 --- /dev/null +++ b/packages/error-reporting/src/classes/request-information-container.js @@ -0,0 +1,130 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isString = is.string; +var isNumber = is.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 + */ +function RequestInformationContainer() { + + 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 + */ +RequestInformationContainer.prototype.setUrl = function(url) { + + this.url = isString(url) ? 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 + */ +RequestInformationContainer.prototype.setMethod = function(method) { + + this.method = isString(method) ? 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 + */ +RequestInformationContainer.prototype.setReferrer = function(referrer) { + + this.referrer = isString(referrer) ? 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 + */ +RequestInformationContainer.prototype.setUserAgent = function(userAgent) { + + this.userAgent = isString(userAgent) ? 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 + */ +RequestInformationContainer.prototype.setRemoteAddress = function(remoteIp) { + + this.remoteAddress = isString(remoteIp) ? 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 + */ +RequestInformationContainer.prototype.setStatusCode = function(statusCode) { + + this.statusCode = isNumber(statusCode) ? statusCode : 0; + + return this; +}; + +module.exports = RequestInformationContainer; diff --git a/packages/error-reporting/src/configuration.js b/packages/error-reporting/src/configuration.js new file mode 100644 index 00000000000..36add709104 --- /dev/null +++ b/packages/error-reporting/src/configuration.js @@ -0,0 +1,450 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var env = process.env; +var has = require('lodash.has'); +var is = require('is'); +var isObject = is.object; +var isBoolean = is.boolean; +var isString = is.string; +var isNumber = is.number; +var isEmpty = is.empty; +var isNull = is.null; +var version = require('../package.json').version; + +/** + * 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 Stackdriver 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. + */ +var Configuration = function(givenConfig, logger) { + /** + * The _logger property caches the logger instance created at the top-level + * for configuration logging purposes. + * @memberof Configuration + * @private + * @type {Object} + * @defaultvalue Object + */ + this._logger = logger; + /** + * The _reportUncaughtExceptions property is meant to contain the optional + * runtime configuration property reportUncaughtExceptions. This property will + * default to true if not given false through the runtime configuration + * meaning that the default behavior is to catch uncaught exceptions, report + * them to the Stackdriver Errors API and then exit. If given false uncaught + * exceptions will not be listened for and not be caught or reported. + * @memberof Configuration + * @private + * @type {Boolean} + * @defaultvalue true + */ + this._reportUncaughtExceptions = true; + /** + * The _shouldReportErrorsToAPI property is meant to denote whether or not + * the Stackdriver error reporting library will actually try to report Errors + * to the Stackdriver Error API. The value of this property is derived from + * the `NODE_ENV` environmental variable or the value of ignoreEnvironmentChec + * property if present in the runtime configuration. If either the `NODE_ENV` + * variable is set to 'production' or the ignoreEnvironmentCheck propery on + * the runtime configuration is set to true then the error reporting library + * attempt to send errors to the Error API. Otherwise the value will remain + * false and errors will not be reported to the API. + * @memberof Configuration + * @private + * @type {Boolean} + * @defaultvalue false + */ + this._shouldReportErrorsToAPI = false; + /** + * 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 Stackdriver API key that + * may be used in place of default application credentials to authenticate + * with the Stackdriver 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 _version of the Error reporting library that is currently being run. + * This information will be logged in errors communicated to the Stackdriver + * Error API. + * @memberof Configuration + * @private + * @type {String} + */ + this._version = version; + /** + * Boolean flag indicating whether or not the configuration instance was able + * to find a set of usable credentials to attempt authorization against the + * Stackdriver API. + * @memberof Configuration + * @private + * @type {Boolean} + */ + this._lacksValidCredentials = true; + /** + * 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 = isObject(givenConfig) ? 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 + */ +Configuration.prototype._checkLocalServiceContext = function() { + // 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. + var service; + var version; + + 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 = isString(service) ? service : 'node'; + this._serviceContext.version = isString(version) ? version : undefined; + + if (isObject(this._givenConfiguration.serviceContext)) { + if (isString(this._givenConfiguration.serviceContext.service)) { + this._serviceContext.service = + this._givenConfiguration.serviceContext.service; + } else if (has(this._givenConfiguration.serviceContext, 'service')) { + throw new Error('config.serviceContext.service must be a string'); + } + + if (isString(this._givenConfiguration.serviceContext.version)) { + this._serviceContext.version = + this._givenConfiguration.serviceContext.version; + } else if (has(this._givenConfiguration.serviceContext, 'version')) { + throw new Error('config.serviceContext.version must be a string'); + } + } +}; +/** + * 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 + */ +Configuration.prototype._gatherLocalConfiguration = function() { + if (this._givenConfiguration.ignoreEnvironmentCheck === true) { + this._shouldReportErrorsToAPI = true; + } else if (has(this._givenConfiguration, 'ignoreEnvironmentCheck') && + !isBoolean(this._givenConfiguration.ignoreEnvironmentCheck)) { + throw new Error('config.ignoreEnvironmentCheck must be a boolean'); + } else { + this._shouldReportErrorsToAPI = env.NODE_ENV === 'production'; + } + if (!this._shouldReportErrorsToAPI) { + this._logger.warn([ + 'Stackdriver error reporting client has not been configured to send', + 'errors, please check the NODE_ENV environment variable and make sure it', + 'is set to "production" or the ignoreEnvironmentCheck property is set to', + 'true in the runtime configuration object' + ].join(' ')); + } + if (isBoolean(this._givenConfiguration.reportUncaughtExceptions)) { + this._reportUncaughtExceptions = this._givenConfiguration + .reportUncaughtExceptions; + } else if (has(this._givenConfiguration, 'reportUncaughtExceptions')) { + throw new Error('config.reportUncaughtExceptions must be a boolean'); + } + if (isString(this._givenConfiguration.key)) { + this._key = this._givenConfiguration.key; + } else if (has(this._givenConfiguration, 'key')) { + throw new Error('config.key must be a string'); + } + if (isString(this._givenConfiguration.keyFilename)) { + this._keyFilename = this._givenConfiguration.keyFilename; + } else if (has(this._givenConfiguration, 'keyFilename')) { + throw new Error('config.keyFilename must be a string'); + } + if (isObject(this._givenConfiguration.credentials)) { + this._credentials = this._givenConfiguration.credentials; + } else if (has(this._givenConfiguration, 'credentials')) { + throw new Error('config.credentials must be a valid credentials object'); + } + this._checkAuthConfiguration(); +}; +/** + * The _checkAuthConfiguration function is responsible for determining whether + * or not a configuration instance, after having gathered configuration from + * environment and runtime, has credentials information enough to attempt + * authorization with the Stackdriver Errors API. + * @memberof Configuration + * @private + * @function _checkAuthConfiguration + * @returns {Undefined} - does not return anything + */ +Configuration.prototype._checkAuthConfiguration = function() { + if (!isNull(this._key) || !isNull(this._keyFilename) || + !isNull(this._credentials) || + !isEmpty(process.env.GOOGLE_APPLICATION_CREDENTIALS)) { + this._lacksValidCredentials = false; + } else { + this._logger.warn([ + 'Unable to find credential information on instance. This library will', + 'be unable to communicate with the Stackdriver API to save errors.' + ].join(' ')); + } +}; +/** + * 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 + */ +Configuration.prototype._checkLocalProjectId = function(cb) { + if (isString(this._projectId)) { + // already has been set by the metadata service + return this._projectId; + } + if (has(this._givenConfiguration, 'projectId')) { + if (isString(this._givenConfiguration.projectId)) { + this._projectId = this._givenConfiguration.projectId; + } else if (isNumber(this._givenConfiguration.projectId)) { + this._projectId = this._givenConfiguration.projectId.toString(); + } + } + return this._projectId; +}; +/** + * Returns the _reportUncaughtExceptions property on the instance. + * @memberof Configuration + * @public + * @function getReportUncaughtExceptions + * @returns {Boolean} - returns the _reportUncaughtExceptions property + */ +Configuration.prototype.getReportUncaughtExceptions = function() { + return this._reportUncaughtExceptions; +}; +/** + * Returns the _shouldReportErrorsToAPI property on the instance. + * @memberof Configuration + * @public + * @function getShouldReportErrorsToAPI + * @returns {Boolean} - returns the _shouldReportErrorsToAPI property + */ +Configuration.prototype.getShouldReportErrorsToAPI = function() { + return this._shouldReportErrorsToAPI; +}; +/** + * Returns the _projectId property on the instance. + * @memberof Configuration + * @public + * @function getProjectId + * @returns {String|Null} - returns the _projectId property + */ +Configuration.prototype.getProjectId = function(cb) { + return this._checkLocalProjectId(); +}; +/** + * Returns the _key property on the instance. + * @memberof Configuration + * @public + * @function getKey + * @returns {String|Null} - returns the _key property + */ +Configuration.prototype.getKey = function() { + return this._key; +}; +/** + * Returns the _keyFilename property on the instance. + * @memberof Configuration + * @public + * @function getKeyFilename + * @returns {String|Null} - returns the _keyFilename property + */ +Configuration.prototype.getKeyFilename = function() { + return this._keyFilename; +}; +/** + * Returns the _credentials property on the instance. + * @memberof Configuration + * @public + * @function getCredentials + * @returns {Credentials|Null} - returns the _credentials property + */ +Configuration.prototype.getCredentials = function() { + return this._credentials; +}; +/** + * Returns the _serviceContext property on the instance. + * @memberof Configuration + * @public + * @function getKey + * @returns {Object|Null} - returns the _serviceContext property + */ +Configuration.prototype.getServiceContext = function() { + return this._serviceContext; +}; +/** + * Returns the _version property on the instance. + * @memberof Configuration + * @public + * @function getVersion + * @returns {String} - returns the _version property + */ +Configuration.prototype.getVersion = function() { + return this._version; +}; +/** + * Returns the _lacksValidCredentials property on the instance. + * @memberof Configuration + * @public + * @function getVersion + * @returns {Boolean} - returns the _lacksValidCredentials property + */ +Configuration.prototype.lacksCredentials = function() { + return this._lacksValidCredentials; +}; +module.exports = Configuration; diff --git a/packages/error-reporting/src/error-extractors/error.js b/packages/error-reporting/src/error-extractors/error.js new file mode 100644 index 00000000000..e2abe83ec5d --- /dev/null +++ b/packages/error-reporting/src/error-extractors/error.js @@ -0,0 +1,49 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var has = require('lodash.has'); +var is = require('is'); +var isObject = is.object; + +/** + * 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. + * @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 extractFromErrorClass(err, errorMessage) { + + errorMessage.setMessage(err.stack); + + if (has(err, 'user')) { + + errorMessage.setUser(err.user); + } + + if (has(err, 'serviceContext') && isObject(err.serviceContext)) { + + errorMessage.setServiceContext(err.serviceContext.service, + err.serviceContext.version); + } +} + +module.exports = extractFromErrorClass; diff --git a/packages/error-reporting/src/error-extractors/object.js b/packages/error-reporting/src/error-extractors/object.js new file mode 100644 index 00000000000..f1b155177e4 --- /dev/null +++ b/packages/error-reporting/src/error-extractors/object.js @@ -0,0 +1,80 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var has = require('lodash.has'); +var is = require('is'); +var isObject = is.object; + +/** + * 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 extractFromObject + * @param {Object} err - the Object given as the content of the error + * @param {String} [err.message] - the error message + * @param {String} [err.user] - the user the error occurred for + * @param {String} [err.filePath] - the file path and file where the error + * occurred at + * @param {Number} [err.lineNumber] - the line number where the error occurred + * at + * @param {String} [err.functionName] - the function where the error occurred at + * @param {Object} [err.serviceContext] - the service context object of the + * error + * @param {String} [err.serviceContext.service] - the service the error occurred + * on + * @param {String} [err.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 extractFromObject(err, errorMessage) { + + if (has(err, 'message')) { + + errorMessage.setMessage(err.message); + } + + if (has(err, 'user')) { + + errorMessage.setUser(err.user); + } + + if (has(err, 'filePath')) { + + errorMessage.setFilePath(err.filePath); + } + + if (has(err, 'lineNumber')) { + + errorMessage.setLineNumber(err.lineNumber); + } + + if (has(err, 'functionName')) { + + errorMessage.setFunctionName(err.functionName); + } + + if (has(err, 'serviceContext') && isObject(err.serviceContext)) { + + errorMessage.setServiceContext(err.serviceContext.service, + err.serviceContext.version); + } +} + +module.exports = extractFromObject; diff --git a/packages/error-reporting/src/error-handlers/error.js b/packages/error-reporting/src/error-handlers/error.js new file mode 100644 index 00000000000..a3ca5a938e6 --- /dev/null +++ b/packages/error-reporting/src/error-handlers/error.js @@ -0,0 +1,46 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var handleUnknownAsError = require('./unknown.js'); +var extractFromErrorClass = require('../error-extractors/error.js'); + +/** + * Handles routing and validation for parsing an errorMessage that was + * flagged as an instance of the Error class. This function does not + * discriminate against regular objects, checking only to see if the err + * parameter is itself a basic object and has the function property + * hasOwnProperty. Given that the input passes this basic test the input + * will undergo extraction by the extractFromErrorClass function, otherwise + * it will be treated and processed as an unknown. + * @function handleErrorClassError + * @param {Error} err - the error instance to extract information from + * @param {ErrorMessage} errorMessage - the error message to marshal error + * information into. + * @returns {Undefined} - does not return anything + */ +function handleErrorClassError(err, errorMessage) { + + if (err instanceof Error) { + + extractFromErrorClass(err, errorMessage); + } else { + + handleUnknownAsError(err, errorMessage); + } +} + +module.exports = handleErrorClassError; diff --git a/packages/error-reporting/src/error-handlers/number.js b/packages/error-reporting/src/error-handlers/number.js new file mode 100644 index 00000000000..ebec109028e --- /dev/null +++ b/packages/error-reporting/src/error-handlers/number.js @@ -0,0 +1,45 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isNumber = is.number; +var isFunction = is.function; + +/** + * Handles routing and validation for parsing an error which has been indicated + * to be of type Number. This handler will manufacture a new Error to create + * a stack-trace for submission to the Error API and will attempt to caste the + * given number to a string for submission to the Error API. + * @function handleNumberAsError + * @param {Number} err - the number submitted as content for the error message + * @param {ErrorMessage} errorMessage - the error messag instance to marshall + * error information into. + * @returns {Undefined} - does not return anything + */ +function handleNumberAsError(err, errorMessage) { + var fauxError = new Error(); + var errChecked = fauxError.stack; + + if (isNumber(err) && isFunction(err.toString)) { + + errChecked = err.toString(); + } + + errorMessage.setMessage(errChecked); +} + +module.exports = handleNumberAsError; diff --git a/packages/error-reporting/src/error-handlers/object.js b/packages/error-reporting/src/error-handlers/object.js new file mode 100644 index 00000000000..2b3c11a13c2 --- /dev/null +++ b/packages/error-reporting/src/error-handlers/object.js @@ -0,0 +1,45 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isObject = is.object; +var extractFromObject = require('../error-extractors/object.js'); +var handleUnknownAsError = require('./unknown.js'); + +/** + * Handles routing and validation for parsing an error that has been indicated + * to be of type object. If the value submitted for err passes a basic check + * for being of type Object than the input will extracted as such, otherwise the + * input will be treated as unknown. + * @function handleObjectAsError + * @param {Object} err - the error information object to extract from + * @param {ErrorMessage} errorMessage - the error message instance to marshal + * error information into + * @returns {Undefined} - does not return anything + */ +function handleObjectAsError(err, errorMessage) { + + if (isObject(err)) { + + extractFromObject(err, errorMessage); + } else { + + handleUnknownAsError(err, errorMessage); + } +} + +module.exports = handleObjectAsError; diff --git a/packages/error-reporting/src/error-handlers/string.js b/packages/error-reporting/src/error-handlers/string.js new file mode 100644 index 00000000000..8e2e0fb816f --- /dev/null +++ b/packages/error-reporting/src/error-handlers/string.js @@ -0,0 +1,47 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var isString = require('is').string; + +/** + * Handles validation of an error which has been indicated to be of type String. + * This function will create a new instance of the Error class to produce a + * stack trace for submission to the API and check to confirm that the given + * value is of type string. + * @function handleStringAsError + * @param {String} err - the String indicated as the content of the error + * @param {ErrorMessage} errorMessage - the error message instance to marshal + * error information into. + * @returns {Undefined} - does not return anything + */ +function handleStringAsError(err, errorMessage) { + var fauxError = new Error(); + var fullStack = fauxError.stack.split('\n'); + var cleanedStack = fullStack.slice(0, 1).concat(fullStack.slice(4)); + var errChecked = ''; + + if (isString(err)) { + // Replace the generic error message with the user-provided string + cleanedStack[0] = err; + } + + errChecked = cleanedStack.join('\n'); + + errorMessage.setMessage(errChecked); +} + +module.exports = handleStringAsError; diff --git a/packages/error-reporting/src/error-handlers/unknown.js b/packages/error-reporting/src/error-handlers/unknown.js new file mode 100644 index 00000000000..6243f184bc5 --- /dev/null +++ b/packages/error-reporting/src/error-handlers/unknown.js @@ -0,0 +1,37 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * Handles unknown/unsupported input as the content of the error message. Since + * the problem-space is not defined for this path the library only attempts to + * manufacture a stack trace for submission to the API and discards the input + * that was given as the error content. + * @function handleUnknownAsError + * @param {Any} err - the unknown/unsupported input indicated as the content of + * the error. + * @param {ErrorMessage} errorMessage - the error message instance to marshal + * error information into. + * @returns {Undefined} - does not return anything + */ +function handleUnknownAsError(err, errorMessage) { + var fauxError = new Error(); + + errorMessage.setMessage(fauxError.stack); +} + +module.exports = handleUnknownAsError; diff --git a/packages/error-reporting/src/error-router.js b/packages/error-reporting/src/error-router.js new file mode 100644 index 00000000000..d8dbcf5ab8e --- /dev/null +++ b/packages/error-reporting/src/error-router.js @@ -0,0 +1,63 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var handleErrorClassError = require('./error-handlers/error.js'); +var handleObjectAsError = require('./error-handlers/object.js'); +var handleStringAsError = require('./error-handlers/string.js'); +var handleNumberAsError = require('./error-handlers/number.js'); +var handleUnknownAsError = require('./error-handlers/unknown.js'); + +/** + * The Error handler router is responsible for taking an error of some type and + * and Error message container, analyzing the type of the error and routing it + * to the proper handler so that the error information can be marshaled into the + * the error message container. + * @function errorHandlerRouter + * @param {Any} err - the error information to extract from + * @param {ErrorMessage} em - an instance of ErrorMessage to marshal error + * information into + * @returns {Undefined} - does not return a value + */ +function errorHandlerRouter(err, em) { + + if (err instanceof Error) { + + handleErrorClassError(err, em); + + return; + } + + switch (typeof err) { + case 'object': { + handleObjectAsError(err, em); + break; + } + case 'string': { + handleStringAsError(err, em); + break; + } + case 'number': { + handleNumberAsError(err, em); + break; + } + default: { + handleUnknownAsError(err, em); + } + } +} + +module.exports = errorHandlerRouter; diff --git a/packages/error-reporting/src/google-apis/auth-client.js b/packages/error-reporting/src/google-apis/auth-client.js new file mode 100644 index 00000000000..d5c31fa0b9c --- /dev/null +++ b/packages/error-reporting/src/google-apis/auth-client.js @@ -0,0 +1,164 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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. + */ +/*jshint unused:false*/ + +'use strict'; +const common = require('@google-cloud/common'); +const pkg = require('../../package.json'); +var logger = require('../logger.js'); +var is = require('is'); +var isFunction = is.fn; +var isString = is.string; + +/* @const {Array} list of scopes needed to work with the errors api. */ +var SCOPES = ['https://www.googleapis.com/auth/cloud-platform']; + +/* @const {String} Base Error Reporting API */ +var API = 'https://clouderrorreporting.googleapis.com/v1beta1/projects'; + +/** + * 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 + */ +class RequestHandler extends common.Service { + /** + * 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) { + if (isString(key)) { + return {key: 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(err, response, body) { + return null; + } + /** + * @constructor + * @param {Configuration} config - an instance of the Configuration class + * @param {Logger} logger - an instance of logger + */ + constructor(config, logger) { + var pid = config.getProjectId(); + super({ + packageJson: pkg, + baseUrl: 'https://clouderrorreporting.googleapis.com/v1beta1/', + scopes: SCOPES, + projectId: pid !== null ? pid : undefined, + projectIdRequired: true + }, config); + this._config = config; + this._logger = logger; + } + /** + * 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, userCb) { + var self = this; + var cb = isFunction(userCb) ? userCb : RequestHandler.noOp; + 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 Stackdriver Error Reporting API.' + ].join(' '), err); + } + cb(err, response, body); + }); + } else { + cb(new Error([ + 'Stackdriver error reporting client has not been configured to send', + 'errors, please check the NODE_ENV environment variable and make sure', + 'it is set to "production" or set the ignoreEnvironmentCheck property', + 'to true in the runtime configuration object' + ].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 + */ + +module.exports = RequestHandler; diff --git a/packages/error-reporting/src/index.js b/packages/error-reporting/src/index.js new file mode 100644 index 00000000000..8a134e8bd29 --- /dev/null +++ b/packages/error-reporting/src/index.js @@ -0,0 +1,148 @@ +/*! + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +var Configuration = require('./configuration.js'); +var AuthClient = require('./google-apis/auth-client.js'); +// Begin error reporting interfaces + +var koa = require('./interfaces/koa.js'); +var hapi = require('./interfaces/hapi.js'); +var manual = require('./interfaces/manual.js'); +var express = require('./interfaces/express.js'); +var restify = require('./interfaces/restify'); +var messageBuilder = require('./interfaces/message-builder.js'); +var uncaughtException = require('./interfaces/uncaught.js'); +var createLogger = require('./logger.js'); + +/** + * @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 Stackdriver service even if NODE_ENV is not set + * to production + */ + +/** + * @typedef ApplicationErrorReportingInterface + * @type Object + * @property {Object} hapi - The hapi plugin for Stackdriver Error Reporting + * @property {Function} report - The manual interface to report Errors to the + * Stackdriver Error Reporting Service + * @property {Function} express - The express plugin for Stackdriver Error + * Reporting + * @property {Function} message - Returns a new ErrorMessage class instance + */ + +// TODO: Update this documentation +/** + * 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. Invocation of + * this function will also return an interface which can be used manually via + * the `report` function property, with hapi via the `hapi` object property or + * with express via the `express` function property. + * @function Errors + * @param {ConfigurationOptions} initConfiguration - the desired project/error + * reporting configuration + * @constructor + * @alias module:error-reporting + */ +function Errors(initConfiguration) { + if (!(this instanceof Errors)) { + return new Errors(initConfiguration); + } + + var logger = createLogger(initConfiguration); + var config = new Configuration(initConfiguration, logger); + var client = new AuthClient(config, logger); + + // Setup the uncaught exception handler + uncaughtException(client, config); + + // 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 = manual(client, config); + /** + * @example + * // Use to create and report errors manually with a high-degree + * // of manual control + * var err = errors.event() + * .setMessage('My error message') + * .setUser('root@nexus'); + * errors.report(err, function () { + * console.log('done!'); + * }); + */ + this.event = messageBuilder(config); + /** + * @example + * var hapi = require('hapi'); + * var server = new hapi.Server(); + * server.connection({ port: 3000 }); + * server.start(); + * // AFTER ALL OTHER ROUTE HANDLERS + * server.register({register: errors.hapi}); + */ + this.hapi = hapi(client, config); + /** + * @example + * var express = require('express'); + * var app = express(); + * // AFTER ALL OTHER ROUTE HANDLERS + * app.use(errors.express); + * app.listen(3000); + */ + this.express = express(client, config); + /** + * @example + * var restify = require('restify'); + * var server = restify.createServer(); + * // BEFORE ALL OTHER ROUTE HANDLERS + * server.use(errors.restify(server)); + */ + this.restify = restify(client, config); + /** + * @example + * var koa = require('koa'); + * var app = koa(); + * // BEFORE ALL OTHER ROUTE HANDLERS HANDLERS + * app.use(errors.koa); + */ + this.koa = koa(client, config); +} + +module.exports = Errors; diff --git a/packages/error-reporting/src/interfaces/express.js b/packages/error-reporting/src/interfaces/express.js new file mode 100644 index 00000000000..07a0614a603 --- /dev/null +++ b/packages/error-reporting/src/interfaces/express.js @@ -0,0 +1,81 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isObject = is.object; +var isFunction = is.fn; +var ErrorMessage = require('../classes/error-message.js'); +var expressRequestInformationExtractor = + require('../request-extractors/express.js'); +var errorHandlerRouter = require('../error-router.js'); + +/** + * 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. + */ +function makeExpressHandler(client, config) { + /** + * 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) { + var ctxService = ''; + var ctxVersion = ''; + + if (config.lacksCredentials()) { + next(err); + } + + if (isObject(config)) { + ctxService = config.getServiceContext().service; + ctxVersion = config.getServiceContext().version; + } + + var em = new ErrorMessage() + .consumeRequestInformation( + expressRequestInformationExtractor(req, res)) + .setServiceContext(ctxService, ctxVersion); + + errorHandlerRouter(err, em); + + if (isObject(client) && isFunction(client.sendError)) { + client.sendError(em); + } + + if (isFunction(next)) { + next(err); + } + + return em; + } + + return expressErrorHandler; +} + +module.exports = makeExpressHandler; diff --git a/packages/error-reporting/src/interfaces/hapi.js b/packages/error-reporting/src/interfaces/hapi.js new file mode 100644 index 00000000000..10f542c8863 --- /dev/null +++ b/packages/error-reporting/src/interfaces/hapi.js @@ -0,0 +1,127 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isObject = is.object; +var isFunction = is.fn; +var ErrorMessage = require('../classes/error-message.js'); +var hapiRequestInformationExtractor = require('../request-extractors/hapi.js'); +var errorHandlerRouter = require('../error-router.js'); + +/** + * 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(req, err, config) { + var service = ''; + var version = ''; + + if (isObject(config)) { + service = config.getServiceContext().service; + version = config.getServiceContext().version; + } + + var em = new ErrorMessage() + .consumeRequestInformation(hapiRequestInformationExtractor(req)) + .setServiceContext(service, version); + + errorHandlerRouter(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 + */ +function makeHapiPlugin(client, config) { + + /** + * 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(server, options, next) { + if (isObject(server)) { + if (isFunction(server.on)) { + server.on('request-error', function(req, err) { + var em = hapiErrorHandler(req, err, config); + + if (!config.lacksCredentials()) { + client.sendError(em); + } + }); + } + + if (isFunction(server.ext)) { + server.ext('onPreResponse', function(request, reply) { + var em = null; + + if (isObject(request) && isObject(request.response) && + request.response.isBoom) { + em = hapiErrorHandler(request, new Error(request.response.message), + config); + + if (!config.lacksCredentials()) { + client.sendError(em); + } + } + + if (isObject(reply) && isFunction(reply.continue)) { + reply.continue(); + } + }); + } + } + + if (isFunction(next)) { + return next(); + } + } + + var hapiPlugin = {register: hapiRegisterFunction}; + var version = (isObject(config) && config.getVersion()) ? + config.getVersion(): '0.0.0'; + + hapiPlugin.register.attributes = { + name: '@google/cloud-errors', + version: version + }; + + return hapiPlugin; +} + +module.exports = makeHapiPlugin; diff --git a/packages/error-reporting/src/interfaces/koa.js b/packages/error-reporting/src/interfaces/koa.js new file mode 100644 index 00000000000..e3f4751d068 --- /dev/null +++ b/packages/error-reporting/src/interfaces/koa.js @@ -0,0 +1,68 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var ErrorMessage = require('../classes/error-message.js'); +var koaRequestInformationExtractor = require('../request-extractors/koa.js'); +var errorHandlerRouter = require('../error-router.js'); + +/** + * 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 Stackdriver + * @param {NormalizedConfigurationVariables} - The application configuration + * @returns {Function} - The function used to catch errors yielded by downstream + * request handlers. + */ +function koaErrorHandler(client, config) { + + /** + * 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 *(next) { + var em; + var svc = config.getServiceContext(); + + try { + + yield next; + } catch (err) { + if (config.lacksCredentials()) { + return; + } + + em = new ErrorMessage() + .consumeRequestInformation( + koaRequestInformationExtractor(this.request, this.response)) + .setServiceContext(svc.service, + svc.version); + + errorHandlerRouter(err, em); + + client.sendError(em); + } + }; +} + +module.exports = koaErrorHandler; diff --git a/packages/error-reporting/src/interfaces/manual.js b/packages/error-reporting/src/interfaces/manual.js new file mode 100644 index 00000000000..f2c7230158f --- /dev/null +++ b/packages/error-reporting/src/interfaces/manual.js @@ -0,0 +1,103 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isString = is.string; +var isObject = is.object; +var isFunction = is.fn; +var ErrorMessage = require('../classes/error-message.js'); +var manualRequestInformationExtractor = + require('../request-extractors/manual.js'); +var errorHandlerRouter = require('../error-router.js'); + +/** + * 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 + * @returns {reportManualError} - a bound version of the reportManualError + * function + */ +function handlerSetup(client, config) { + /** + * 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 arugment 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} [additionalMessage] - a string containing error message + * information to override the builtin message given by an Error/Exception + * @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, request, additionalMessage, callback) { + var em; + if (config.lacksCredentials()) { + return; + } + if (isString(request)) { + // no request given + callback = additionalMessage; + additionalMessage = request; + request = undefined; + } else if (isFunction(request)) { + // neither request nor additionalMessage given + callback = request; + request = undefined; + additionalMessage = undefined; + } + + if (isFunction(additionalMessage)) { + callback = additionalMessage; + additionalMessage = undefined; + } + + if (err instanceof ErrorMessage) { + em = err; + } else { + em = new ErrorMessage(); + em.setServiceContext(config.getServiceContext().service, + config.getServiceContext().version); + errorHandlerRouter(err, em); + } + + if (isObject(request)) { + em.consumeRequestInformation(manualRequestInformationExtractor(request)); + } + + if (isString(additionalMessage)) { + em.setMessage(additionalMessage); + } + + client.sendError(em, callback); + return em; + } + + return reportManualError; +} + +module.exports = handlerSetup; diff --git a/packages/error-reporting/src/interfaces/message-builder.js b/packages/error-reporting/src/interfaces/message-builder.js new file mode 100644 index 00000000000..c22f3c11cc0 --- /dev/null +++ b/packages/error-reporting/src/interfaces/message-builder.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var ErrorMessage = require('../classes/error-message.js'); + +/** + * 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 + */ +function handlerSetup(config) { + /** + * 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() { + return new ErrorMessage().setServiceContext( + config.getServiceContext().service, + config.getServiceContext().version); + } + + return newMessage; +} + +module.exports = handlerSetup; diff --git a/packages/error-reporting/src/interfaces/restify.js b/packages/error-reporting/src/interfaces/restify.js new file mode 100644 index 00000000000..df03996aae4 --- /dev/null +++ b/packages/error-reporting/src/interfaces/restify.js @@ -0,0 +1,159 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isObject = is.object; +var isFunction = is.function; +var ErrorMessage = require('../classes/error-message.js'); +var expressRequestInformationExtractor = + require('../request-extractors/express.js'); +var errorHandlerRouter = require('../error-router.js'); + +/** + * 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 Stackdriver 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, config, err, em) { + var svc = config.getServiceContext(); + em.setServiceContext(svc.service, svc.version); + + errorHandlerRouter(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 Stackdriver 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, config, req, res) { + var em; + + if (res._body instanceof Error || + res.statusCode > 309 && res.statusCode < 512) { + em = new ErrorMessage().consumeRequestInformation( + expressRequestInformationExtractor(req, res)); + + restifyErrorHandler(client, config, res._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, config, req, res, next) { + var listener = {}; + + if (isObject(res) && isFunction(res.on) && isFunction(res.removeListener)) { + + listener = function() { + + restifyRequestFinishHandler(client, config, req, res); + res.removeListener('finish', listener); + }; + + res.on('finish', listener); + } + + 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 Stackdriver 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, config, server) { + + server.on('uncaughtException', function(req, res, reqConfig, err) { + if (config.lacksCredentials()) { + return; + } + var em = new ErrorMessage().consumeRequestInformation( + 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 + */ +function handlerSetup(client, config) { + + return serverErrorHandler.bind(null, client, config); +} + +module.exports = handlerSetup; diff --git a/packages/error-reporting/src/interfaces/uncaught.js b/packages/error-reporting/src/interfaces/uncaught.js new file mode 100644 index 00000000000..e426e5d25e0 --- /dev/null +++ b/packages/error-reporting/src/interfaces/uncaught.js @@ -0,0 +1,67 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var errorHandlerRouter = require('../error-router.js'); +var ErrorMessage = require('../classes/error-message.js'); + +/** + * Exits the process with exit code `1` which indicates that an unhandled error + * occurred. !! Invocation of this function will terminate the process !! + * @function handleProcessExit + * @return {Undefined} - does not return a value + */ +function handleProcessExit() { process.exit(1); } + +/** + * If the configuraiton allows, install an uncaught exception handler that will + * report the uncaught error to the API and then terminate the process. + * @function handlerSetup + * @param {AuthClient} client - the API client for communication with the + * Stackdriver Error API + * @param {Configuration} config - the init configuration + * @returns {Null|process} - Returns null if the config demands ignoring + * uncaught + * exceptions, otherwise return the process instance + */ +function handlerSetup(client, config) { + /** + * The actual exception handler creates a new instance of `ErrorMessage`, + * extracts infomation from the propagated `Error` and marshals it into the + * `ErrorMessage` instance, attempts to send this `ErrorMessage` instance to + * the Stackdriver Error Reporting API. Subsequently the process is + * terminated. + * @function uncaughtExceptionHandler + * @listens module:process~event:uncaughtException + * @param {Error} err - The error that has been uncaught to this point + * @returns {Undefined} - does not return a value + */ + function uncaughtExceptionHandler(err) { + var em = new ErrorMessage(); + errorHandlerRouter(err, em); + client.sendError(em, handleProcessExit); + setTimeout(handleProcessExit, 2000); + } + + if (!config.getReportUncaughtExceptions() || config.lacksCredentials()) { + // Do not attach a listener to the process + return null; + } + + return process.on('uncaughtException', uncaughtExceptionHandler); +} + +module.exports = handlerSetup; diff --git a/packages/error-reporting/src/logger.js b/packages/error-reporting/src/logger.js new file mode 100644 index 00000000000..9024814ee44 --- /dev/null +++ b/packages/error-reporting/src/logger.js @@ -0,0 +1,64 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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. + */ +/*jshint bitwise: false*/ + +'use strict'; +var has = require('lodash.has'); +var is = require('is'); +var isObject = is.object; +var isString = is.string; +var isNumber = is.number; +var logger = require('@google-cloud/common').logger; +/** + * Creates an instance of the Google Cloud Diagnostics 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 + */ +function createLogger(initConfiguration) { + // Default to log level: warn (2) + var level = logger.WARN; + if (has(process.env, 'GCLOUD_ERRORS_LOGLEVEL')) { + // Cast env string as integer + level = logger.LEVELS[~~process.env.GCLOUD_ERRORS_LOGLEVEL] || + logger.LEVELS.warn; + } else if (isObject(initConfiguration) && + has(initConfiguration, 'logLevel')) { + if (isString(initConfiguration.logLevel)) { + // Cast string as integer + level = logger.LEVELS[~~initConfiguration.logLevel] || logger.LEVELS.warn; + } else if (isNumber(initConfiguration.logLevel)) { + level = logger.LEVELS[initConfiguration.logLevel] || logger.LEVELS.warn; + } else { + throw new Error('config.logLevel must be a number or decimal ' + + 'representation of a number in string form'); + } + } + return logger({level: level, tag: '@google/cloud-errors'}); +} + +module.exports = createLogger; diff --git a/packages/error-reporting/src/request-extractors/express.js b/packages/error-reporting/src/request-extractors/express.js new file mode 100644 index 00000000000..197e73978b7 --- /dev/null +++ b/packages/error-reporting/src/request-extractors/express.js @@ -0,0 +1,77 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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:disable requireEarlyReturn +'use strict'; +var is = require('is'); +var isFunction = is.fn; +var isObject = is.object; +var RequestInformationContainer = + require('../classes/request-information-container.js'); + +/** + * 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) { + + if (typeof req.header('x-forwarded-for') !== 'undefined') { + + return req.header('x-forwarded-for'); + } else if (isObject(req.connection)) { + + 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 + */ +function expressRequestInformationExtractor(req, res) { + + var returnObject = new RequestInformationContainer(); + + if (!isObject(req) || !isFunction(req.header) || !isObject(res)) { + + 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; +} + +module.exports = expressRequestInformationExtractor; diff --git a/packages/error-reporting/src/request-extractors/hapi.js b/packages/error-reporting/src/request-extractors/hapi.js new file mode 100644 index 00000000000..cc44bf1764d --- /dev/null +++ b/packages/error-reporting/src/request-extractors/hapi.js @@ -0,0 +1,103 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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:disable requireEarlyReturn +'use strict'; +var has = require('lodash.has'); +var is = require('is'); +var isObject = is.object; +var isFunction = is.function; +var isArray = is.array; +var RequestInformationContainer = + require('../classes/request-information-container.js'); + +/** + * 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) { + + if (has(req, 'response') && isObject(req.response) && + has(req.response, 'statusCode')) { + + return req.response.statusCode; + } else if (has(req, 'response') && isObject(req.response) && + isObject(req.response.output)) { + + return req.response.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) { + + if (has(req.headers, 'x-forwarded-for')) { + + return req.headers['x-forwarded-for']; + } else if (isObject(req.info)) { + + 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 + */ +function hapiRequestInformationExtractor(req) { + + var returnObject = new RequestInformationContainer(); + + if (!isObject(req) || !isObject(req.headers) || isFunction(req) || + isArray(req)) { + + return returnObject; + } + + returnObject.setMethod(req.method) + .setUrl(req.url) + .setUserAgent(req.headers['user-agent']) + .setReferrer(req.headers.referrer) + .setStatusCode(attemptToExtractStatusCode(req)) + .setRemoteAddress(extractRemoteAddressFromRequest(req)); + + return returnObject; +} + +module.exports = hapiRequestInformationExtractor; diff --git a/packages/error-reporting/src/request-extractors/koa.js b/packages/error-reporting/src/request-extractors/koa.js new file mode 100644 index 00000000000..5dceb5aeec9 --- /dev/null +++ b/packages/error-reporting/src/request-extractors/koa.js @@ -0,0 +1,55 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var is = require('is'); +var isObject = is.object; +var isFunction = is.function; +var isArray = is.array; +var RequestInformationContainer = + require('../classes/request-information-container.js'); + +/** + * 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 + */ +function koaRequestInformationExtractor(req, res) { + + var returnObject = new RequestInformationContainer(); + + if (!isObject(req) || !isObject(res) || isFunction(req) || isFunction(res) || + isArray(req) || isArray(res) || !isObject(req.headers)) { + + return returnObject; + } + + returnObject.setMethod(req.method) + .setUrl(req.url) + .setUserAgent(req.headers['user-agent']) + .setReferrer(req.headers.referrer) + .setStatusCode(res.status) + .setRemoteAddress(req.ip); + + return returnObject; +} + +module.exports = koaRequestInformationExtractor; diff --git a/packages/error-reporting/src/request-extractors/manual.js b/packages/error-reporting/src/request-extractors/manual.js new file mode 100644 index 00000000000..706e8a0c88a --- /dev/null +++ b/packages/error-reporting/src/request-extractors/manual.js @@ -0,0 +1,85 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; +var has = require('lodash.has'); +var is = require('is'); +var isObject = is.object; +var isArray = is.array; +var isFunction = is.fn; +var RequestInformationContainer = + require('../classes/request-information-container.js'); + +/** + * 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 + */ +function manualRequestInformationExtractor(req) { + + var returnObject = new RequestInformationContainer(); + + if (!isObject(req) || isArray(req) || isFunction(req)) { + + return returnObject; + } + + if (has(req, 'method')) { + + returnObject.setMethod(req.method); + } + + if (has(req, 'url')) { + + returnObject.setUrl(req.url); + } + + if (has(req, 'userAgent')) { + + returnObject.setUserAgent(req.userAgent); + } + + if (has(req, 'referrer')) { + + returnObject.setReferrer(req.referrer); + } + + if (has(req, 'statusCode')) { + + returnObject.setStatusCode(req.statusCode); + } + + if (has(req, 'remoteAddress')) { + + returnObject.setRemoteAddress(req.remoteAddress); + } + + return returnObject; +} + +module.exports = manualRequestInformationExtractor; diff --git a/packages/error-reporting/system-test/testAuthClient.js b/packages/error-reporting/system-test/testAuthClient.js new file mode 100644 index 00000000000..c0b02ab1ba4 --- /dev/null +++ b/packages/error-reporting/system-test/testAuthClient.js @@ -0,0 +1,349 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var assert = require('assert'); +var nock = require('nock'); +var RequestHandler = require('../src/google-apis/auth-client.js'); +var ErrorMessage = require('../src/classes/error-message.js'); +var Configuration = require('../test/fixtures/configuration.js'); +var createLogger = require('../src/logger.js'); +var is = require('is'); +var isObject = is.object; +var isString = is.string; +var isEmpty = is.empty; +var forEach = require('lodash.foreach'); +var assign = require('lodash.assign'); +const ERR_TOKEN = '_@google_STACKDRIVER_INTEGRATION_TEST_ERROR__'; + + +describe('Behvaiour acceptance testing', function() { + before(function() { + // Before starting the suite make sure we have the proper resources + if (!isString(process.env.GCLOUD_PROJECT)) { + throw new Error( + 'The gcloud project id (GCLOUD_PROJECT) was not set in the env'); + } else if (!isString(process.env.STUBBED_API_KEY)) { + throw new Error( + 'The api key (STUBBED_API_KEY) was not set as an env variable'); + } else if (!isString(process.env.STUBBED_PROJECT_NUM)) { + throw new Error( + 'The project number (STUBBED_PROJECT_NUM) was not set in the env'); + } else if (process.env.NODE_ENV !== 'production') { + throw new Error( + 'The NODE_ENV is not set to production as an env variable. Please ' + + 'set NODE_ENV to production'); + } + // In case we are running after unit mocks which were not destroyed properly + nock.cleanAll(); + }); + describe('Request/Response lifecycle mocking', function() { + var sampleError = new Error(ERR_TOKEN); + var errorMessage = new ErrorMessage().setMessage(sampleError); + var fakeService, client, logger; + beforeEach(function() { + 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({ignoreEnvironmentCheck: true}, logger), logger); + }); + afterEach(function() { + nock.cleanAll(); + }); + describe('Receiving non-retryable errors', function() { + it('Should fail', function(done) { + this.timeout(5000); + client.sendError({}, function(err, response, body) { + assert(err instanceof Error); + assert.strictEqual(err.message.toLowerCase(), + 'message cannot be empty.'); + assert(isObject(response)); + assert.strictEqual(response.statusCode, 400); + done(); + }); + }); + }); + describe('Receiving retryable errors', function() { + it('Should retry', function(done) { + this.timeout(25000); + var tries = 0; + var intendedTries = 4; + fakeService.reply(429, function() { + tries += 1; + console.log('Mock Server Received Request:', tries + '/' + + intendedTries); + return {error: 'Please try again later'}; + }); + client.sendError(errorMessage, function(err, response, body) { + assert.strictEqual(tries, intendedTries); + done(); + }); + }); + }); + describe('Using an API key', function() { + it('Should provide the key as a query string on outgoing requests', + function(done) { + var key = process.env.STUBBED_API_KEY; + var client = new RequestHandler(new Configuration( + {key: key, ignoreEnvironmentCheck: true}, + createLogger({logLevel: 5}))); + fakeService.query({key: key}).reply(200, function(uri) { + assert(uri.indexOf('key=' + key) > -1); + return {}; + }); + client.sendError(errorMessage, function() { + done(); + }); + } + ); + }); + describe('Callback-less invocation', function() { + it('Should still execute the request', function(done) { + fakeService.reply(200, function() { + done(); + }); + client.sendError(errorMessage); + }); + }); + }); + describe('System-live integration testing', function() { + var sampleError = new Error(ERR_TOKEN); + var errorMessage = new ErrorMessage().setMessage(sampleError.stack); + var oldEnv = { + GCLOUD_PROJECT: process.env.GCLOUD_PROJECT, + STUBBED_PROJECT_NUM: process.env.STUBBED_PROJECT_NUM, + NODE_ENV: process.env.NODE_ENV + }; + function sterilizeEnv() { + forEach(oldEnv, function(val, key) { + delete process.env[key]; + }); + } + function restoreEnv() { + assign(process.env, oldEnv); + } + describe('Client creation', function() { + describe('Using only project id', function() { + describe('As a runtime argument', function() { + var cfg, logger; + before(function() { + sterilizeEnv(); + logger = createLogger({logLevel: 5}); + cfg = new Configuration({projectId: oldEnv.GCLOUD_PROJECT, + ignoreEnvironmentCheck: true}, logger); + }); + after(restoreEnv); + it('Should not throw on initialization', function(done) { + this.timeout(10000); + assert.doesNotThrow(function() { + (new RequestHandler(cfg, logger)).sendError(errorMessage, + function(err, response, body) { + assert.strictEqual(err, null); + assert.strictEqual(response.statusCode, 200); + assert(isObject(body) && isEmpty(body)); + done(); + } + ); + }); + }); + }); + describe('As an env variable', function() { + var cfg, logger; + before(function() { + sterilizeEnv(); + process.env.GCLOUD_PROJECT = oldEnv.GCLOUD_PROJECT; + logger = createLogger({logLevel: 5}); + cfg = new Configuration({ignoreEnvironmentCheck: true}, logger); + }); + after(restoreEnv); + it('Should not throw on initialization', function(done) { + this.timeout(10000); + assert.doesNotThrow(function() { + (new RequestHandler(cfg, logger)).sendError(errorMessage, + function(err, response, body) { + assert.strictEqual(err, null); + assert.strictEqual(response.statusCode, 200); + assert(isObject(body) && isEmpty(body)); + done(); + } + ); + }); + }); + }); + }); + describe('Using only project number', function() { + describe('As a runtime argument', function() { + var cfg, logger; + before(function() { + sterilizeEnv(); + logger = createLogger({logLevel: 5}); + cfg = new Configuration({ + projectId: parseInt(oldEnv.STUBBED_PROJECT_NUM), + ignoreEnvironmentCheck: true + }, logger); + }); + after(restoreEnv); + it('Should not throw on initialization', function(done) { + this.timeout(10000); + assert.doesNotThrow(function() { + (new RequestHandler(cfg, logger)).sendError(errorMessage, + function(err, response, body) { + assert.strictEqual(err, null); + assert.strictEqual(response.statusCode, 200); + assert(isObject(body) && isEmpty(body)); + done(); + } + ); + }); + }); + }); + describe('As an env variable', function() { + var cfg, logger; + before(function() { + sterilizeEnv(); + process.env.GCLOUD_PROJECT = oldEnv.STUBBED_PROJECT_NUM; + logger = createLogger({logLevel: 5}); + cfg = new Configuration({ignoreEnvironmentCheck: true}, logger); + }); + after(restoreEnv); + it('Should not throw on initialization', function(done) { + this.timeout(10000); + assert.doesNotThrow(function() { + (new RequestHandler(cfg, logger)).sendError(errorMessage, + function(err, response, body) { + assert.strictEqual(err, null); + assert.strictEqual(response.statusCode, 200); + assert(isObject(body) && isEmpty(body)); + done(); + } + ); + }); + }); + }); + }); + }); + describe('Error behvaiour', function() { + describe('With a configuration to not report errors', function() { + var ERROR_STRING = [ + 'Stackdriver error reporting client has not been configured to send', + 'errors, please check the NODE_ENV environment variable and make', + 'sure it is set to "production" or set the ignoreEnvironmentCheck', + 'property to true in the runtime configuration object' + ].join(' '); + var logger, client; + before(function() { + delete process.env.NODE_ENV; + logger = createLogger({logLevel: 5}); + client = new RequestHandler(new Configuration(undefined, logger), + logger); + }); + after(function() { + process.env.NODE_ENV = oldEnv.NODE_ENV; + }); + it('Should callback with an error', function(done) { + client.sendError({}, function(err, response, body) { + assert(err instanceof Error); + assert.strictEqual(err.message, ERROR_STRING); + assert.strictEqual(response, null); + done(); + }); + }); + }); + // describe('An invalid env configuration', function() { + // var ERROR_STRING = [ + // 'Unable to find the project Id for communication with the', + // 'Stackdriver Error Reporting service. This app will be unable to', + // 'send errors to the reporting service unless a valid project Id', + // 'is supplied via runtime configuration or the GCLOUD_PROJECT', + // 'environmental variable.' + // ].join(' '); + // var logger, client; + // before(function() { + // delete process.env.GCLOUD_PROJECT; + // logger = createLogger({logLevel: 5}); + // client = new RequestHandler(new Configuration( + // {ignoreEnvironmentCheck: true}, logger), logger); + // }); + // after(function() { + // process.env.GCLOUD_PROJECT = oldEnv.GCLOUD_PROJECT; + // }); + // it('Should callback with an error', function(done) { + // client.sendError(errorMessage, function(err, response, body) { + // assert(err instanceof Error); + // assert.strictEqual(err.message, ERROR_STRING); + // assert.strictEqual(response, null); + // done(); + // }); + // }); + // }); + }); + describe('Success behaviour', function() { + var er = new Error(ERR_TOKEN); + var em = new ErrorMessage().setMessage(er.stack); + describe('Given a valid project id', function() { + var logger, client, cfg; + before(function() { + sterilizeEnv(); + logger = createLogger({logLevel: 5}); + cfg = new Configuration({ + projectId: oldEnv.GCLOUD_PROJECT, + ignoreEnvironmentCheck: true + }, logger); + client = new RequestHandler(cfg, logger); + }); + after(restoreEnv); + it('Should succeed in its request', function(done) { + client.sendError(em, function(err, response, body) { + assert.strictEqual(err, null); + assert(isObject(body)); + assert(isEmpty(body)); + assert.strictEqual(response.statusCode, 200); + done(); + }); + }); + }); + describe('Given a valid project number', function() { + var logger, client, cfg; + before(function() { + forEach(oldEnv, function(val, key) { + delete process.env[key]; + }); + logger = createLogger({logLevel: 5}); + cfg = new Configuration({ + projectId: parseInt(oldEnv.STUBBED_PROJECT_NUM), + ignoreEnvironmentCheck: true + }, logger); + client = new RequestHandler(cfg, logger); + }); + after(function() { + assign(process.env, oldEnv); + }); + it('Should succeed in its request', function(done) { + client.sendError(em, function(err, response, body) { + assert.strictEqual(err, null); + assert(isObject(body)); + assert(isEmpty(body)); + assert.strictEqual(response.statusCode, 200); + done(); + }); + }); + }); + }); + }); +}); diff --git a/packages/error-reporting/test/fixtures/configuration.js b/packages/error-reporting/test/fixtures/configuration.js new file mode 100644 index 00000000000..2e0834e6e61 --- /dev/null +++ b/packages/error-reporting/test/fixtures/configuration.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var Configuration = require('../../src/configuration.js'); + +var FakeConfiguration = function(config) { + return Configuration.call(this, config, { warn: function() {} }); +}; + +FakeConfiguration.prototype = Object.create(Configuration.prototype); + +module.exports = FakeConfiguration; diff --git a/packages/error-reporting/test/fixtures/gcloud-credentials.json b/packages/error-reporting/test/fixtures/gcloud-credentials.json new file mode 100644 index 00000000000..3499fcc9c3d --- /dev/null +++ b/packages/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/packages/error-reporting/test/fixtures/uncaughtExitBehaviour.js b/packages/error-reporting/test/fixtures/uncaughtExitBehaviour.js new file mode 100644 index 00000000000..7f4b8728ad0 --- /dev/null +++ b/packages/error-reporting/test/fixtures/uncaughtExitBehaviour.js @@ -0,0 +1,93 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var uncaughtSetup = require('../../src/interfaces/uncaught.js'); +var nock = require('nock'); +var createLogger = require('../../src/logger.js'); +var isString = require('is').string; +var Configuration = require('../fixtures/configuration.js'); +var RequestHandler = require('../../src/google-apis/auth-client.js'); +var originalHandlers = process.listeners('uncaughtException'); +var UNCAUGHT = 'uncaughtException'; +var client; + +function reattachOriginalListeners() { + for (var i = 0; i < originalHandlers.length; i++) { + process.on(UNCAUGHT, originalHandlers[i]); + } +} +var env = { + NODE_ENV: process.env.NODE_ENV +}; +function setEnv() { + process.env.NODE_ENV = 'production'; +} +function restoreEnv() { + process.env.NODE_ENV = env.NODE_ENV; +} + +describe('Uncaught Exception exit behaviour', function() { + before(function() { + process.removeAllListeners(UNCAUGHT); + if (!isString(process.env.GCLOUD_PROJECT)) { + // The gcloud project id (GCLOUD_PROJECT) was not set as an env variable + this.skip(); + process.exit(1); + } else if (!isString(process.env.GOOGLE_APPLICATION_CREDENTIALS)) { + // The app credentials (GOOGLE_APPLICATION_CREDENTIALS) + // was not set as an env variable + this.skip(); + process.exit(1); + } + setEnv(); + }); + after(function() { + nock.cleanAll(); + nock.enableNetConnect(); + reattachOriginalListeners(); + restoreEnv(); + }); + it('Should attempt to report the uncaught exception', function(done) { + var id = 'xyz'; + nock( + 'http://metadata.google.internal/computeMetadata/v1/project' + ).get('/project-id').times(1).reply(200, id); + nock('https://accounts.google.com:443/o/oauth2') + .post('/token').query(function() {return true;}).reply(200, { + refresh_token: 'hello', + access_token: 'goodbye', + expiry_date: new Date(9999, 1, 1) + }); + this.timeout(2000); + nock( + 'https://clouderrorreporting.googleapis.com/v1beta1/projects/' + id + ).post('/events:report').once().reply(200, function() { + done(); + return {success: true}; + }); + var cfg = new Configuration( + {reportUncaughtExceptions: true, projectId: 'xyz'}); + cfg.lacksCredentials = function() { + return false; + }; + client = new RequestHandler(cfg, createLogger({logLevel: 4})); + uncaughtSetup(client, cfg); + setTimeout(function() { + throw new Error('This error was supposed to be thrown'); + }, 10); + }); +}); diff --git a/packages/error-reporting/test/test-servers/express_scaffold_server.js b/packages/error-reporting/test/test-servers/express_scaffold_server.js new file mode 100644 index 00000000000..f9a6efeca79 --- /dev/null +++ b/packages/error-reporting/test/test-servers/express_scaffold_server.js @@ -0,0 +1,134 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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:disable requireEarlyReturn + +'use strict'; + +var WARNING_HEADER = '\n!! -WARNING-'; +var EXCLAMATION_LN = '\n!!'; +var has = require('lodash.has'); +var express = require('express'); +var app = express(); +var errorHandler = require('../../src/index.js')({ + onUncaughtException: 'report', + key: process.env.STUBBED_API_KEY, + projectId: process.env.STUBBED_PROJECT_NUM +}); +var bodyParser = require('body-parser'); + +app.use(bodyParser.json()); + +app.post('/testErrorHandling', function(req, res, next) { + + + if (has(req.body, 'test') && req.body.test !== true) { + + return next(new Error('Error on Express Regular Error POST Route')); + } else { + + res.send('Success'); + res.end(); + } +} +); + +app.get( + '/customError', function(req, res, next) { + + errorHandler.report( + 'Error on Express Custom Error GET Route', function(err, res) { + + if (err) { + + console.log(WARNING_HEADER); + console.log('Error in sending custom get error to api'); + console.log(err); + console.log(EXCLAMATION_LN); + } else { + + console.log(EXCLAMATION_LN); + console.log('Successfully sent custom get error to api'); + console.log(EXCLAMATION_LN); + } + } + ); + + res.send('Success'); + res.end(); + + next(); + } +); + +app.get( + '/getError', function(req, res, next) { + + return next(new Error('Error on Express Regular Error GET Route')); + } +); + +app.use(errorHandler.express); + +function throwUncaughtError() { + console.log('Throwing an uncaught error..'); + throw new Error('This is an uncaught error'); +} + +function reportManualError() { + console.log('Reporting a manual error..'); + errorHandler.report( + new Error('This is a manually reported error'), null, null, + function(err, res) { + + if (err) { + + console.log(WARNING_HEADER); + console.log('Got an error in sending error information to the API'); + console.log(err); + console.log(EXCLAMATION_LN); + } else { + + console.log(EXCLAMATION_LN); + console.log('Successfully sent error information to the API'); + console.log(EXCLAMATION_LN); + } + + if (process.env.THROW_ON_STARTUP) { + throwUncaughtError(); + } + } + ); +} +console.log('reporting a manual error first'); +errorHandler.report( + new Error('This is a test'), + (err, res) => { + console.log('reported first manual error'); + if (err) { + console.log('Error was unable to be reported', err); + } else { + console.log('Error reported!'); + } + } +); + +app.listen( + 3000, + function() { + console.log('Scaffold Server has been started on port 3000'); + reportManualError(); + } +); diff --git a/packages/error-reporting/test/test-servers/hapi_scaffold_server.js b/packages/error-reporting/test/test-servers/hapi_scaffold_server.js new file mode 100644 index 00000000000..e16e50d8220 --- /dev/null +++ b/packages/error-reporting/test/test-servers/hapi_scaffold_server.js @@ -0,0 +1,55 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +var hapi = require('hapi'); +var errorHandler = require('../../src/index.js')(); + +var server = new hapi.Server(); +server.connection({ port: 3000 }); + +server.start( + (err) => { + if (err) { + throw err; + } + console.log( + 'Server running at', server.info.uri); + } +); + +server.route({ + method: 'GET', path: '/get', handler: function(request, reply) { + console.log('Got a GET'); + throw new Error('an error'); + } +}); + +server.route({ + method: 'POST', path: '/post', handler: function(request, reply) { + console.log('Got a POST', request.payload); + throw new Error('An error on the hapi post route'); + } +}); + + +server.register( + { register: errorHandler.hapi }, (err) => { + if (err) { + console.error('There was an error in registering the plugin', err); + } + } +); diff --git a/packages/error-reporting/test/test-servers/koa_scaffold_server.js b/packages/error-reporting/test/test-servers/koa_scaffold_server.js new file mode 100644 index 00000000000..6e9bf3aceb8 --- /dev/null +++ b/packages/error-reporting/test/test-servers/koa_scaffold_server.js @@ -0,0 +1,57 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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 +'use strict'; + +var errorHandler = require('../../src/index.js')({ + onUncaughtException: 'report' +}); +var koa = require('koa'); +var app = koa(); + +app.use(errorHandler.koa); + +app.use(function *(next) { + //This will set status and message + this.throw('Error Message', 500); + yield next; +}); + + +app.use(function *(next){ + var start = new Date(); + yield next; + var ms = new Date() - start; + this.set('X-Response-Time', ms + 'ms'); +}); + +// logger + +app.use(function *(next){ + var start = new Date(); + yield next; + var ms = new Date() - start; + console.log('%s %s - %s', this.method, this.url, ms); +}); + +// response +app.use(function *(next){ + this.body = 'Hello World'; + yield next; +}); + +app.listen(3000); diff --git a/packages/error-reporting/test/test-servers/manual_scaffold_server.js b/packages/error-reporting/test/test-servers/manual_scaffold_server.js new file mode 100644 index 00000000000..1831bc18d22 --- /dev/null +++ b/packages/error-reporting/test/test-servers/manual_scaffold_server.js @@ -0,0 +1,25 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +const errors = require('../../src/index.js')(); +errors.report('Sample test string', (err, response, body) => { + console.log( + 'Callback from report:\n', + '\tError: ', err, '\n', + '\tResponse Body:' + ); +}); \ No newline at end of file diff --git a/packages/error-reporting/test/test-servers/restify_scaffold_server.js b/packages/error-reporting/test/test-servers/restify_scaffold_server.js new file mode 100644 index 00000000000..8b8d4655a73 --- /dev/null +++ b/packages/error-reporting/test/test-servers/restify_scaffold_server.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +function respond(req, res, next) { + next(new Error('this is a restify error')); +} + +var restify = require('restify'); +var errorHandler = require('../../src/index.js')(); + +var server = restify.createServer(); + +server.use(errorHandler.restify(server)); +server.get('/hello/:name', respond); +server.head('/hello/:name', respond); + +server.listen(8080, function() { + console.log('%s listening at %s', server.name, server.url); +}); diff --git a/packages/error-reporting/test/unit/testConfiguration.js b/packages/error-reporting/test/unit/testConfiguration.js new file mode 100644 index 00000000000..13529cfca04 --- /dev/null +++ b/packages/error-reporting/test/unit/testConfiguration.js @@ -0,0 +1,314 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var assert = require('assert'); +var isNumber = require('is').number; +var merge = require('lodash.merge'); +var Configuration = require('../fixtures/configuration.js'); +var version = require('../../package.json').version; +var Fuzzer = require('../../utils/fuzzer.js'); +var level = process.env.GCLOUD_ERRORS_LOGLEVEL; +var logger = require('../../src/logger.js')({ + logLevel: isNumber(level) ? level : 4 +}); +var nock = require('nock'); + +var METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/project'; + +process.removeAllListeners('uncaughtException'); +var env = { + 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 sterilizeEnv() { + delete process.env.NODE_ENV; + delete process.env.GCLOUD_PROJECT; + delete process.env.GAE_MODULE_NAME; + delete process.env.GAE_MODULE_VERSION; +} +function restoreEnv() { + process.env.NODE_ENV = env.NODE_ENV; + process.env.GCLOUD_PROJECT = env.GCLOUD_PROJECT; + process.env.GAE_MODULE_NAME = env.GAE_MODULE_NAME; + process.env.GAE_MODULE_VERSION = env.GAE_MODULE_VERSION; +} +function createDeadMetadataService() { + return nock(METADATA_URL).get('/project-id').times(1).reply(500); +} + +describe('Configuration class', function() { + before(function() {sterilizeEnv();}); + after(function() {restoreEnv();}); + describe( + 'Initialization', + function() { + var f = new Fuzzer(); + var stubConfig = {test: true}; + describe('fuzzing the constructor', function() { + it('Should return default values', function() { + var c; + f.fuzzFunctionForTypes( + function(givenConfigFuzz) { + c = new Configuration(givenConfigFuzz, logger); + assert.deepEqual(c._givenConfiguration, {}); + }, + ['object'] + ); + }); + }); + describe('valid config and default values', function() { + var c; + before(function() {process.env.NODE_ENV = 'development';}); + after(function() {sterilizeEnv();}); + it('Should not throw with a valid configuration', function() { + assert.doesNotThrow(function() { + c = new Configuration(stubConfig, logger); + }); + }); + it('Should have a property reflecting the config argument', function() { + assert.deepEqual(c._givenConfiguration, stubConfig); + }); + it('Should reportUncaughtExceptions', function() { + assert.strictEqual(c.getReportUncaughtExceptions(), true); + }); + it('Should not reportUncaughtExceptions', function() { + assert.strictEqual(c.getShouldReportErrorsToAPI(), false); + }); + it('Should not have a project id', function() { + assert.strictEqual(c._projectId, null); + }); + it('Should not have a key', function() { + assert.strictEqual(c.getKey(), null); + }); + it('Should have a default service context', function() { + assert.deepEqual(c.getServiceContext(), + {service: 'node', version: undefined}); + }); + it('Should have a version corresponding to package.json', function() { + assert.strictEqual(c.getVersion(), version); + }); + }); + describe('with ignoreEnvironmentCheck', function() { + var conf = merge({}, stubConfig, {ignoreEnvironmentCheck: true}); + var c = new Configuration(conf, logger); + it('Should reportErrorsToAPI', function() { + assert.strictEqual(c.getShouldReportErrorsToAPI(), true); + }); + }); + describe('without ignoreEnvironmentCheck', function() { + describe('report behaviour with production env', function() { + var c; + before(function() { + sterilizeEnv(); + process.env.NODE_ENV = 'production'; + c = new Configuration(undefined, logger); + }); + after(function() {sterilizeEnv();}); + it('Should reportErrorsToAPI', function() { + assert.strictEqual(c.getShouldReportErrorsToAPI(), true); + }); + }); + describe('exception behaviour', function() { + it('Should throw', function() { + assert.throws(function() { + new Configuration({reportUncaughtExceptions: 1}, logger); + }); + }); + it('Should throw if invalid type for key', function() { + assert.throws(function() { + new Configuration({key: null}, logger); + }); + }); + it('Should throw if invalid for ignoreEnvironmentCheck', function() { + assert.throws(function() { + new Configuration({ignoreEnvironmentCheck: null}, logger); + }); + }); + it('Should throw if invalid for serviceContext.service', function() { + assert.throws(function() { + new Configuration({serviceContext: {service: false}}, logger); + }); + }); + it('Should throw if invalid for serviceContext.version', function() { + assert.throws(function() { + new Configuration({serviceContext: {version: true}}, logger); + }); + }); + it('Should not throw given an empty object for serviceContext', + function() { + assert.doesNotThrow(function() { + new Configuration({serviceContext: {}}, logger); + }); + } + ); + }); + }); + } + ); + describe('Configuration resource aquisition', function() { + before(function() {sterilizeEnv();}); + describe('project id from configuration instance', function() { + var pi = 'test'; + var c; + before(function() { + c = new Configuration({projectId: pi}, logger); + }); + after(function() {nock.cleanAll();}); + it('Should return the project id', function() { + assert.strictEqual(c.getProjectId(), pi); + }); + }); + describe('project number from configuration instance', function() { + var pn = 1234; + var serve, c; + before(function() { + sterilizeEnv(); + c = new Configuration({projectId: pn}, logger); + }); + after(function() {nock.cleanAll(); sterilizeEnv();}); + it('Should return the project number', function() { + assert.strictEqual(c.getProjectId(), pn.toString()); + }); + }); + }); + describe('Exception behaviour', function() { + describe('While lacking a project id', function() { + var serve, c; + before(function() { + sterilizeEnv(); + serve = createDeadMetadataService(); + c = new Configuration(undefined, logger); + }); + after(function() { + nock.cleanAll(); + sterilizeEnv(); + }); + it('Should return null', function() { + assert.strictEqual(c.getProjectId(), null); + }); + }); + describe('Invalid type for projectId in runtime config', function() { + var serve, c; + before(function() { + sterilizeEnv(); + serve = createDeadMetadataService(); + c = new Configuration({projectId: null}, logger); + }); + after(function() { + nock.cleanAll(); + sterilizeEnv(); + }); + it('Should return null', function() { + assert.strictEqual(c.getProjectId(), null); + }); + }); + }); + describe('Resource aquisition', function() { + after(function() { + /* + * !! IMPORTANT !! + * THE restoreEnv FUNCTION SHOULD BE CALLED LAST AS THIS TEST FILE EXITS + * AND SHOULD THEREFORE BE THE LAST THING TO EXECUTE FROM THIS FILE. + * !! IMPORTANT !! + */ + restoreEnv(); + }); + describe('via env', function() { + before(function() {sterilizeEnv();}); + afterEach(function() {sterilizeEnv();}); + describe('no longer tests env itself', function() { + var c; + var projectId = 'test-xyz'; + before(function() { + process.env.GCLOUD_PROJECT = projectId; + c = new Configuration(undefined, logger); + }); + it('Should assign', function() { + assert.strictEqual(c.getProjectId(), null); + }); + }); + describe('serviceContext', function() { + var c; + var projectId = 'test-abc'; + var serviceContext = { + service: 'test', + version: '1.x' + }; + before(function() { + 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', function() { + assert.deepEqual(c.getServiceContext(), serviceContext); + }); + }); + }); + describe('via runtime configuration', function() { + before(function() {sterilizeEnv();}); + describe('serviceContext', function() { + var c; + var projectId = 'xyz123'; + var serviceContext = { + service: 'evaluation', + version: '2.x' + }; + before(function() { + c = new Configuration({ + projectId: projectId, + serviceContext: serviceContext + }); + }); + it('Should assign', function() { + assert.deepEqual(c.getServiceContext(), serviceContext); + }); + }); + describe('api key', function() { + var c; + var projectId = '987abc'; + var key = '1337-api-key'; + before(function() { + c = new Configuration({ + key: key, + projectId: projectId + }, logger); + }); + it('Should assign', function() { + assert.strictEqual(c.getKey(), key); + }); + }); + describe('reportUncaughtExceptions', function() { + var c; + var projectId = '123-xyz'; + var reportUncaughtExceptions = false; + before(function() { + c = new Configuration({ + projectId: projectId, + reportUncaughtExceptions: reportUncaughtExceptions + }); + }); + it('Should assign', function() { + assert.strictEqual(c.getReportUncaughtExceptions(), + reportUncaughtExceptions); + }); + }); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testCustomStackTrace.js b/packages/error-reporting/test/unit/testCustomStackTrace.js new file mode 100644 index 00000000000..fe1f9ad5e64 --- /dev/null +++ b/packages/error-reporting/test/unit/testCustomStackTrace.js @@ -0,0 +1,77 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +var assert = require('assert'); +var CustomStackTrace = require('../../src/classes/custom-stack-trace.js'); + +describe('Fuzzing the CustomStackTrace class', function() { + var testFunction = function testFunction() { + return ''; + }; + var cs; + beforeEach(function() { cs = new CustomStackTrace(); }); + it('Should accept value for file path', function() { + cs.setFilePath('test'); + assert( + cs.filePath === 'test', + 'Setting a valid string on the CustomStackTrace.filePath instance ' + + 'should result in assignment' + ); + }); + it('Should reject invalid type for file path', function() { + cs.setFilePath(null); + assert( + cs.filePath === '', + 'Setting an invalid type on the CustomStackTrace.filePath instance ' + + 'should result in default value of an empty string' + ); + }); + it('Should accept value for line number', function() { + cs.setLineNumber(10); + assert( + cs.lineNumber === 10, + 'Setting a valid number on the CustomStackTrace.lineNumber instance ' + + 'should result in assignment' + ); + }); + it('Should reject invalid type for line number', function() { + cs.setLineNumber('10'); + assert( + cs.lineNumber === 0, + 'Setting an invalid type on the CustomStackTrace.lineNumber instance ' + + 'should result in default value of number 0' + ); + }); + it('Should accept value for call list', function() { + cs.setStringifyStructuredCallList(testFunction); + assert.strictEqual( + cs.stringifyStucturedCallList, + testFunction, + 'Setting a valid function on the CustomStackTrace. ' + + 'setStringifyStructuredCallList should result in assignment' + ); + }); + it('Should reject incalid value for call list', function() { + cs.setStringifyStructuredCallList(null); + assert( + ((typeof cs.setStringifyStructuredCallList) === 'function'), + 'Setting an invalid setStringifyStructuredCallList on the ' + + 'CustomStackTrace. setStringifyStructuredCallList should result in a ' + + 'default value of a function' + ); + }); +}); diff --git a/packages/error-reporting/test/unit/testErrorMessage.js b/packages/error-reporting/test/unit/testErrorMessage.js new file mode 100644 index 00000000000..043d91b1f7b --- /dev/null +++ b/packages/error-reporting/test/unit/testErrorMessage.js @@ -0,0 +1,682 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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:disable +// jshint ignore: start + +var assert = require('assert'); +var ErrorMessage = require('../../src/classes/error-message.js'); + +describe('Instantiating a new ErrorMessage', function() { + var em; + beforeEach(function() {em = new ErrorMessage();}); + + it('Should have a default service context', function() { + assert.deepEqual( + em.serviceContext, + { service: 'node', version: undefined } + ); + }); + it('Should have a default message', function() { + assert.strictEqual(em.message, ''); + }); + it('Should have a default http context', function() { + assert.deepEqual( + em.context.httpRequest, + { + method: '', + url: '', + userAgent: '', + referrer: '', + responseStatusCode: 0, + remoteIp: '' + } + ); + }); + it('Should have a default reportLocation', function() { + assert.deepEqual( + em.context.reportLocation, + { + filePath: '', + lineNumber: 0, + functionName: '' + } + ); + }) +}); + +describe('Calling against setEventTimeToNow', function() { + var em; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set the eventTime property', function() { + em.setEventTimeToNow(); + assert((typeof em.eventTime) === 'string'); + }); +}); + +describe('Fuzzing against setServiceContext', function() { + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var DEFAULT_TEST_VALUE = 'DEFAULT'; + var DEFAULT_VERSION_VALUE = undefined; + var DEFAULT_SERVICE_VALUE = 'node'; + var em; + beforeEach(function() {em = new ErrorMessage()}); + + it('Should set the value for service context', function() { + em.setServiceContext(AFFIRMATIVE_TEST_VALUE, AFFIRMATIVE_TEST_VALUE); + assert.deepEqual( + 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', function() { + em.setServiceContext(DEFAULT_TEST_VALUE, DEFAULT_TEST_VALUE); + assert.deepEqual( + 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', function() { + em.setServiceContext(null, AFFIRMATIVE_TEST_VALUE); + assert.deepEqual( + 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', function() { + em.setServiceContext(AFFIRMATIVE_TEST_VALUE, null); + assert.deepEqual( + 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', function() { + em.setServiceContext(null, null); + assert.deepEqual( + 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', function() { + em.setServiceContext(2, 1.3); + assert.deepEqual( + 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', function() { + em.setServiceContext({ test: 'true' }, []); + assert.deepEqual( + 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', function() { + em.setServiceContext(); + assert.deepEqual( + em.serviceContext + , { + service: DEFAULT_SERVICE_VALUE + , version: DEFAULT_VERSION_VALUE + } + , 'Providing no arguments should set both properties as empty strings' + ); + }) +}); + +describe( + 'Fuzzing against setMessage', + function() { + var em; + beforeEach(function() {em = new ErrorMessage()}); + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + + it('Should set the message', function() { + 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', function() { + 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', + function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set the method', function() { + 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', function() { + 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', + function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set url', function() { + 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', function() { + 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', + function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set userAgent', function() { + 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', function() { + 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', function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set referrer', function() { + 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', function() { + 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', function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 200; + var NEGATIVE_TEST_VALUE = 0; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set responseStatusCode', function() { + 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', function() { + 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', function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set remoteIp', function() { + 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', function() { + 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', + function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set user', function() { + 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', function() { + 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', function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set filePath', function() { + 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', function() { + 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', function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 27; + var NEGATIVE_TEST_VALUE = 0; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set lineNumber', function() { + 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', function() { + 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', function() { + var em; + var AFFIRMATIVE_TEST_VALUE = 'VALID_INPUT_AND_TYPE'; + var NEGATIVE_TEST_VALUE = ''; + beforeEach(function() {em = new ErrorMessage()}); + it('Should set functionName', function() { + 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', function() { + 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', function() { + var em = new ErrorMessage(); + var A_VALID_STRING = 'A_VALID_STRING'; + var A_VALID_NUMBER = 201; + var NEGATIVE_STRING_CASE = ''; + var NEGATIVE_NUMBER_CASE = 0; + + var 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 + }; + var NEGATIVE_TEST_VALUE = { + method: null + , url: A_VALID_NUMBER + , userAgent: {} + , referrer: [] + , statusCode: A_VALID_STRING + , remoteAddress: undefined + }; + it('Should consume the stubbed request object', function() { + em.consumeRequestInformation(AFFIRMATIVE_TEST_VALUE); + 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', function() { + 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', + function() { + em.consumeRequestInformation(NEGATIVE_TEST_VALUE); + 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', + function() { + assert( + em.consumeRequestInformation(AFFIRMATIVE_TEST_VALUE) instanceof ErrorMessage + , [ + 'Calling consumeRequestInformation with valid input should return' + , 'the ErrorMessage instance' + ].join(' ') + ); + assert( + em.consumeRequestInformation() instanceof ErrorMessage + , [ + 'Calling consumeRequestInformation with invalid input should return' + , 'the ErrorMessage instance' + ].join(' ') + ); + } + ); +}); diff --git a/packages/error-reporting/test/unit/testExpressInterface.js b/packages/error-reporting/test/unit/testExpressInterface.js new file mode 100644 index 00000000000..33d2350f95c --- /dev/null +++ b/packages/error-reporting/test/unit/testExpressInterface.js @@ -0,0 +1,90 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var merge = require('lodash.merge'); +var expressInterface = require('../../src/interfaces/express.js'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var Fuzzer = require('../../utils/fuzzer.js'); +var Configuration = require('../fixtures/configuration.js'); +var createLogger = require('../../src/logger.js'); + +describe('expressInterface', function() { + describe('Exception handling', function() { + describe('Given invalid input', function() { + it('Should not throw errors', function() { + var f = new Fuzzer(); + assert.doesNotThrow( + function() { + f.fuzzFunctionForTypes( + expressInterface, ['object', 'object'] + ); + return; + } + ); + }); + }); + }); + describe('Intended behaviour', function() { + var stubbedConfig = new Configuration({ + serviceContext: { + service: 'a_test_service', version: 'a_version' + } + }, createLogger({logLevel: 4})); + stubbedConfig.lacksCredentials = function() { + return false; + }; + var client = { + sendError: function() { + return; + } + }; + var testError = new Error('This is a test'); + var validBoundHandler = expressInterface(client, stubbedConfig); + it('Should return the error message', function() { + var res = validBoundHandler(testError, null, null, null); + assert.deepEqual( + res, + merge(new ErrorMessage().setMessage(testError.stack) + .setServiceContext( + stubbedConfig._serviceContext.service, + stubbedConfig._serviceContext.version), + {eventTime: res.eventTime} + ) + ); + }); + describe('Calling back to express builtins', function() { + it('Should callback to next', function(done) { + var nextCb = function() { + done(); + }; + validBoundHandler(testError, null, null, nextCb); + }); + it('Should callback to sendError', function(done) { + var sendError = function() { + done(); + }; + var client = { + sendError: sendError + }; + var handler = expressInterface(client, stubbedConfig); + handler(testError, null, null, function() {return;}); + }); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testExpressRequestInformationExtractor.js b/packages/error-reporting/test/unit/testExpressRequestInformationExtractor.js new file mode 100644 index 00000000000..ad1b4594af6 --- /dev/null +++ b/packages/error-reporting/test/unit/testExpressRequestInformationExtractor.js @@ -0,0 +1,157 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var expressRequestInformationExtractor = + require('../../src/request-extractors/express.js'); +var Fuzzer = require('../../utils/fuzzer.js'); +var extend = require('extend'); + +describe('Behaviour under varying input', + function() { + var f; + var DEFAULT_RETURN_VALUE = { + method: '', + url: '', + userAgent: '', + referrer: '', + statusCode: 0, + remoteAddress: '' + }; + beforeEach(function() {f = new Fuzzer();}); + it('Should return a default value given invalid input', function() { + var cbFn = function(value) { + assert.deepEqual(value, DEFAULT_RETURN_VALUE); + }; + f.fuzzFunctionForTypes(expressRequestInformationExtractor, + ['object', 'object'], cbFn); + }); + it('Should return valid request object given valid input', function() { + var 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' + } + }; + var FULL_RES_DERIVATION_VALUE = { + statusCode: 200 + }; + var 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 + }; + + var 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' + } + }; + var PARTIAL_RES_DERIVATION_VALUE = { + statusCode: 201 + }; + var 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 + }; + + var 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' + }; + var ANOTHER_PARTIAL_RES_DERIVATION_VALUE = { + statusCode: 201 + }; + var 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 + }; + var headerFactory = function(toDeriveFrom) { + var lrn = extend({}, toDeriveFrom); + lrn.header = function(toRet) { + if (lrn.hasOwnProperty(toRet)) { + return lrn[toRet]; + } + return undefined; + }; + return lrn; + }; + var tmpOutput = expressRequestInformationExtractor( + headerFactory(FULL_REQ_DERIVATION_VALUE), + FULL_RES_DERIVATION_VALUE + ); + assert.deepEqual(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 + ); + assert.deepEqual( + 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 + ); + assert.deepEqual( + 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/packages/error-reporting/test/unit/testExtractFromErrorClass.js b/packages/error-reporting/test/unit/testExtractFromErrorClass.js new file mode 100644 index 00000000000..76c2a532975 --- /dev/null +++ b/packages/error-reporting/test/unit/testExtractFromErrorClass.js @@ -0,0 +1,107 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var extractFromErrorClass = require('../../src/error-extractors/error.js'); +var ErrorMessage = require('../../src/classes/error-message.js'); + + +describe('Writing and reading ErrorMessage properties', function() { + describe('Message field', function() { + it('Should set the message as the stack of the given error', function() { + var TEST_MESSAGE = 'This is a test'; + var em = new ErrorMessage(); + var err = new Error(TEST_MESSAGE); + extractFromErrorClass(err, em); + assert.deepEqual(em.message, err.stack, 'Given a valid message the ' + + 'error message should absorb the error stack as the message' + ); + }); + }); + describe('User field', function() { + var em, err; + var TEST_USER_INVALID = 12; + beforeEach(function() { + em = new ErrorMessage(); + err = new Error(); + }); + it('Should set the user field if given valid input', function() { + var TEST_USER_VALID = 'TEST_USER'; + err.user = TEST_USER_VALID; + extractFromErrorClass(err, em); + assert.strictEqual(em.context.user, TEST_USER_VALID); + }); + it('Should default the user field if given invalid input', function() { + err.user = TEST_USER_INVALID; + extractFromErrorClass(err, em); + assert.strictEqual(em.context.user, ''); + }); + }); + describe('Service field', function() { + var em, err; + var TEST_SERVICE_DEFAULT = {service: 'node', version: undefined}; + beforeEach(function() { + em = new ErrorMessage(); + err = new Error(); + }); + it('Should set the field if given valid input', function() { + var TEST_SERVICE_VALID = {service: 'test', version: 'test'}; + err.serviceContext = TEST_SERVICE_VALID; + extractFromErrorClass(err, em); + assert.deepEqual(err.serviceContext, TEST_SERVICE_VALID); + }); + it('Should default the field if given invalid input', function() { + var TEST_SERVICE_INVALID = 12; + err.serviceContext = TEST_SERVICE_INVALID; + extractFromErrorClass(err, em); + assert.deepEqual(em.serviceContext, TEST_SERVICE_DEFAULT); + }); + it('Should default the field if not given input', function() { + extractFromErrorClass(err, em); + assert.deepEqual(em.serviceContext, TEST_SERVICE_DEFAULT); + }); + }); + describe('Report location field', function() { + var em, err; + var TEST_STACK_DEFAULT = { + filePath: '', + lineNumber: 0, + functionName: '' + }; + beforeEach(function() { + em = new ErrorMessage(); + err = new Error(); + }); + it('Should default the field if given invalid input', function() { + var TEST_STACK_INVALID_CONTENTS = { + filePath: null, + lineNumber: '2', + functionName: {} + }; + err.stack = TEST_STACK_INVALID_CONTENTS; + extractFromErrorClass(err, em); + assert.deepEqual(em.context.reportLocation, TEST_STACK_DEFAULT); + }); + it('Should default field if not given a valid type', function() { + var TEST_STACK_INVALID_TYPE = []; + err.stack = TEST_STACK_INVALID_TYPE; + extractFromErrorClass(err, em); + assert.deepEqual(em.context.reportLocation, TEST_STACK_DEFAULT); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testExtractFromObject.js b/packages/error-reporting/test/unit/testExtractFromObject.js new file mode 100644 index 00000000000..58c75ae6a3e --- /dev/null +++ b/packages/error-reporting/test/unit/testExtractFromObject.js @@ -0,0 +1,108 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var extractFromObject = require('../../src/error-extractors/object.js'); +var ErrorMessage = require('../../src/classes/error-message.js'); + +describe('Object value extraction as error message', function() { + var em, err; + beforeEach(function() { + em = new ErrorMessage(); + err = {}; + }); + describe('Message field', function() { + it('Should write to the field given valid input', function() { + var MESSAGE = 'test'; + err = {message: MESSAGE}; + extractFromObject(err, em); + assert.strictEqual(em.message, MESSAGE); + }); + it('Should default the field given lack-of input', function() { + extractFromObject(err, em); + assert.strictEqual(em.message, ''); + }); + }); + describe('User field', function() { + it('Should write to the field given valid input', function() { + var USER = 'test'; + err.user = USER; + extractFromObject(err, em); + assert.strictEqual(em.context.user, USER); + }); + it('Should default the field given lack-of input', function() { + extractFromObject(err, em); + assert.strictEqual(em.context.user, ''); + }); + }); + describe('filePath field', function() { + it('Should write to the field given valid input', function() { + var PATH = 'test'; + err.filePath = PATH; + extractFromObject(err, em); + assert.strictEqual(em.context.reportLocation.filePath, PATH); + }); + it('Should default the field given lack-of input', function() { + extractFromObject(err, em); + assert.strictEqual(em.context.reportLocation.filePath, ''); + }); + }); + describe('lineNumber field', function() { + it('Should write to the field given valid input', function() { + var LINE_NUMBER = 10; + err.lineNumber = LINE_NUMBER; + extractFromObject(err, em); + assert.strictEqual(em.context.reportLocation.lineNumber, LINE_NUMBER); + }); + it('Should default the field given lack-of input', function() { + extractFromObject(err, em); + assert.strictEqual(em.context.reportLocation.lineNumber, 0); + }); + }); + describe('functionName field', function() { + it('Should write to the field given valid input', function() { + var FUNCTION_NAME = 'test'; + err.functionName = FUNCTION_NAME; + extractFromObject(err, em); + assert.strictEqual(em.context.reportLocation.functionName, FUNCTION_NAME); + }); + it('Should default the field given lack-of input', function() { + extractFromObject(err, em); + assert.strictEqual(em.context.reportLocation.functionName, ''); + }); + }); + describe('serviceContext field', function() { + var TEST_SERVICE_DEFAULT = {service: 'node', version: undefined}; + it('Should write to the field given valid input', function() { + var TEST_SERVICE_VALID = {service: 'test', version: 'test'}; + err.serviceContext = TEST_SERVICE_VALID; + extractFromObject(err, em); + assert.deepEqual(em.serviceContext, TEST_SERVICE_VALID); + }); + it('Should default the field given invalid input', function() { + var TEST_SERVICE_INVALID = 12; + err.serviceContext = TEST_SERVICE_INVALID; + extractFromObject(err, em); + assert.deepEqual(em.serviceContext, TEST_SERVICE_DEFAULT); + }); + it('Should default the field given lack-of input', function() { + extractFromObject(err, em); + assert.deepEqual(em.serviceContext, TEST_SERVICE_DEFAULT); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testHandleErrorClassError.js b/packages/error-reporting/test/unit/testHandleErrorClassError.js new file mode 100644 index 00000000000..ba1ce9f5575 --- /dev/null +++ b/packages/error-reporting/test/unit/testHandleErrorClassError.js @@ -0,0 +1,56 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var handleErrorClassError = require('../../src/error-handlers/error.js'); + +describe('Behaviour under various type inputs', function() { + var em; + var adversarialObjectInput = { + stack: {} + }; + var adversarialObjectInputTwo = { + stack: [] + }; + beforeEach(function() {em = new ErrorMessage();}); + it('Should not throw given undefined', function() { + assert.doesNotThrow(handleErrorClassError.bind(null, undefined, em)); + }); + it('Should not throw given null', function() { + assert.doesNotThrow(handleErrorClassError.bind(null, null, em)); + }); + it('Should not throw given a string', function() { + assert.doesNotThrow(handleErrorClassError.bind(null, 'string_test', em)); + }); + it('Should not throw given a number', function() { + assert.doesNotThrow(handleErrorClassError.bind(null, 1.2, em)); + }); + it('Should not throw given an array', function() { + assert.doesNotThrow(handleErrorClassError.bind(null, [], em)); + }); + it('Should not throw given an object of invalid form', function() { + assert.doesNotThrow( + handleErrorClassError.bind(null, adversarialObjectInput, em)); + assert.doesNotThrow( + handleErrorClassError.bind(null, adversarialObjectInputTwo, em)); + }); + it('Should not throw given valid input', function() { + assert.doesNotThrow(handleErrorClassError.bind(null, new Error(), em)); + }); +}); diff --git a/packages/error-reporting/test/unit/testHandleNumberAsError.js b/packages/error-reporting/test/unit/testHandleNumberAsError.js new file mode 100644 index 00000000000..89a3198a441 --- /dev/null +++ b/packages/error-reporting/test/unit/testHandleNumberAsError.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var handleNumberAsError = require('../../src/error-handlers/number.js'); + +describe('handleNumberAsError behaviour under varying input', function() { + var em; + beforeEach(function() {em = new ErrorMessage();}); + it('Should not throw given undefined', function() { + assert.doesNotThrow(handleNumberAsError.bind(null, undefined, em)); + }); + it('Should not throw given null', function() { + assert.doesNotThrow(handleNumberAsError.bind(null, null, em)); + }); + it('Should not throw given a string', function() { + assert.doesNotThrow(handleNumberAsError.bind(null, 'test', em)); + }); + it('Should not throw given an instance of Error', function() { + assert.doesNotThrow(handleNumberAsError.bind(null, new Error(), em)); + }); + it('Should not throw given an object', function() { + assert.doesNotThrow(handleNumberAsError.bind(null, {}, em)); + }); + it('Should not throw given valid input', function() { + assert.doesNotThrow(handleNumberAsError.bind(null, 1.3, em)); + }); +}); diff --git a/packages/error-reporting/test/unit/testHandleObjectAsError.js b/packages/error-reporting/test/unit/testHandleObjectAsError.js new file mode 100644 index 00000000000..58f84c86f0c --- /dev/null +++ b/packages/error-reporting/test/unit/testHandleObjectAsError.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var handleObjectAsError = require('../../src/error-handlers/object.js'); + +describe('handleObjectAsError behaviour under varying inputs', function() { + var em; + beforeEach(function() {em = new ErrorMessage();}); + it('Should not throw given undefined', function() { + assert.doesNotThrow(handleObjectAsError.bind(null, undefined, em)); + }); + it('Should not throw given null', function() { + assert.doesNotThrow(handleObjectAsError.bind(null, null, em)); + }); + it('Should not throw given a string', function() { + assert.doesNotThrow(handleObjectAsError.bind(null, 'msg', em)); + }); + it('Should not throw given an instance of Error', function() { + assert.doesNotThrow(handleObjectAsError.bind(null, new Error(), em)); + }); + it('Should not throw given a number', function() { + assert.doesNotThrow(handleObjectAsError.bind(null, 1.3, em)); + }); + it('Should not throw given valid input', function() { + assert.doesNotThrow(handleObjectAsError.bind(null, {}, em)); + }); +}); diff --git a/packages/error-reporting/test/unit/testHandleStringAsError.js b/packages/error-reporting/test/unit/testHandleStringAsError.js new file mode 100644 index 00000000000..7218ef8513c --- /dev/null +++ b/packages/error-reporting/test/unit/testHandleStringAsError.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var handleStringAsError = require('../../src/error-handlers/string.js'); + +describe('handleStringAsError behaviour under varying inputs', function() { + var em; + beforeEach(function() {em = new ErrorMessage();}); + it('Should not throw given undefined', function() { + assert.doesNotThrow(handleStringAsError.bind(null, undefined, em)); + }); + it('Should not throw given null', function() { + assert.doesNotThrow(handleStringAsError.bind(null, null, em)); + }); + it('Should not throw given an object', function() { + assert.doesNotThrow(handleStringAsError.bind(null, {}, em)); + }); + it('Should not throw given an array', function() { + assert.doesNotThrow(handleStringAsError.bind(null, [], em)); + }); + it('Should not throw given an instance of Error', function() { + assert.doesNotThrow(handleStringAsError.bind(null, 1.3, em)); + }); + it('Should not throw given valid input', function() { + assert.doesNotThrow(handleStringAsError.bind(null, 'test', em)); + }); +}); diff --git a/packages/error-reporting/test/unit/testHandleUnknownAsError.js b/packages/error-reporting/test/unit/testHandleUnknownAsError.js new file mode 100644 index 00000000000..2865b64b538 --- /dev/null +++ b/packages/error-reporting/test/unit/testHandleUnknownAsError.js @@ -0,0 +1,47 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var handleUnknownAsError = require('../../src/error-handlers/unknown.js'); + +describe('handleUnknownAsError behvaiour under varying input', function() { + var em; + beforeEach(function() {em = new ErrorMessage();}); + it('Should not throw given undefined', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, undefined, em)); + }); + it('Should not throw given null', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, null, em)); + }); + it('Should not throw given an object', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, {}, em)); + }); + it('Should not throw given an array', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, [], em)); + }); + it('Should not throw given an instance of Error', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, new Error(), em)); + }); + it('Should not throw given a number', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, 1.3, em)); + }); + it('Should not throw given a string', function() { + assert.doesNotThrow(handleUnknownAsError.bind(null, 'msg', em)); + }); +}); diff --git a/packages/error-reporting/test/unit/testHapiInterface.js b/packages/error-reporting/test/unit/testHapiInterface.js new file mode 100644 index 00000000000..fe5d0213a40 --- /dev/null +++ b/packages/error-reporting/test/unit/testHapiInterface.js @@ -0,0 +1,147 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var has = require('lodash.has'); +var is = require('is'); +var isFunction = is.fn; +var isObject = is.object; +var assert = require('assert'); +var hapiInterface = require('../../src/interfaces/hapi.js'); +var ErrorMessage = require('../../src/classes/error-message.js'); +var Fuzzer = require('../../utils/fuzzer.js'); +var EventEmitter = require('events').EventEmitter; +var Configuration = require('../fixtures/configuration.js'); + +describe('Hapi interface', function() { + describe('Fuzzing the setup handler', function() { + it('Should not throw when fuzzed with invalid types', function() { + var f = new Fuzzer(); + assert.doesNotThrow(function() { + f.fuzzFunctionForTypes(hapiInterface, ['object', 'object']); + return; + }); + }); + }); + describe('Providing valid input to the setup handler', function() { + var givenConfig = {getVersion: function() {return '1';}}; + var plugin; + beforeEach(function() {plugin = hapiInterface(null, givenConfig);}); + it('should have plain object as plugin', function() { + assert(isObject(plugin)); + }); + it('plugin should have a register function property', function() { + assert(has(plugin, 'register') && isFunction(plugin.register)); + }); + it('the plugin\'s register property should have an attributes property', + function() { + assert(has(plugin.register, 'attributes') && + isObject(plugin.register.attributes)); + } + ); + it('the plugin\'s attribute property should have a name property', + function() { + assert(has(plugin.register.attributes, 'name')); + assert.strictEqual(plugin.register.attributes.name, + '@google/cloud-errors'); + } + ); + it('the plugin\'s attribute property should have a version property', + function() { + assert(has(plugin.register.attributes, 'version')); + } + ); + }); + describe('hapiRegisterFunction behaviour', function() { + var fakeServer; + beforeEach(function() {fakeServer = new EventEmitter();}); + it('Should call fn when the request-error event is emitted', function() { + var fakeClient = { + sendError: function(errMsg) { + assert(errMsg instanceof ErrorMessage, + 'should be an instance of Error message'); + } + }; + var plugin = hapiInterface(fakeClient, { + lacksCredentials: function() { + return false; + }, + getVersion: function() { + return '1'; + }, + getServiceContext: function() { + return {service: 'node'}; + } + }); + plugin.register(fakeServer, null, null, null); + fakeServer.emit('request-error'); + }); + }); + describe('Behaviour around the request/response lifecycle', function() { + var EVENT = 'onPreResponse'; + var fakeClient = {sendError: function() {}}; + var fakeServer, config, plugin; + before(function() { + config = new Configuration({ + projectId: 'xyz', + serviceContext: { + service: 'x', + version: '1.x' + } + }); + config.lacksCredentials = function() {return false;}; + plugin = hapiInterface(fakeClient, config); + }); + beforeEach(function() { + fakeServer = new EventEmitter(); + fakeServer.ext = fakeServer.on; + }); + afterEach(function() { + fakeServer.removeAllListeners(); + }); + it('Should call continue when a boom is emitted', function(done) { + plugin.register(fakeServer, null, function() {}); + fakeServer.emit(EVENT, {response: {isBoom: true}}, + { + continue: function() { + // The continue function should be called + done(); + } + } + ); + }); + it('Should call sendError when a boom is received', function(done) { + var fakeClient = { + sendError: function(err) { + assert(err instanceof ErrorMessage); + done(); + } + }; + var plugin = hapiInterface(fakeClient, config); + plugin.register(fakeServer, null, function() {}); + fakeServer.emit('onPreResponse', {response: {isBoom: true}}); + }); + it('Should call next when completing a request', function(done) { + plugin.register(fakeServer, null, function(err) { + // The next function should be called + done(); + }); + fakeServer.emit(EVENT, {response: {isBoom: true}}, + {continue: function() {}}); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testHapiRequestInformationExtractor.js b/packages/error-reporting/test/unit/testHapiRequestInformationExtractor.js new file mode 100644 index 00000000000..c580910987c --- /dev/null +++ b/packages/error-reporting/test/unit/testHapiRequestInformationExtractor.js @@ -0,0 +1,127 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var hapiRequestInformationExtractor = + require('../../src/request-extractors/hapi.js'); +var Fuzzer = require('../../utils/fuzzer.js'); + +describe('hapiRequestInformationExtractor behaviour', function() { + describe('behaviour given invalid input', function() { + it('Should produce the default value', function() { + var DEFAULT_RETURN_VALUE = { + method: '', + url: '', + userAgent: '', + referrer: '', + statusCode: 0, + remoteAddress: '' + }; + var f = new Fuzzer(); + var cbFn = function(value) { + assert.deepEqual(value, DEFAULT_RETURN_VALUE); + }; + f.fuzzFunctionForTypes( + hapiRequestInformationExtractor, + ['object'], + cbFn + ); + }); + }); + describe('behaviour given valid input', function() { + var 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 + } + }; + var 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 + }; + var 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 + } + } + }; + var 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 + }; + var 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' + } + }; + var 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', function() { + assert.deepEqual( + hapiRequestInformationExtractor(FULL_REQ_DERIVATION_VALUE), + FULL_REQ_EXPECTED_VALUE); + }); + it('Should produce the partial request input', function() { + assert.deepEqual( + hapiRequestInformationExtractor(PARTIAL_REQ_DERIVATION_VALUE), + PARTIAL_REQ_EXPECTED_VALUE); + }); + it('Should produce the second partial request input', function() { + assert.deepEqual( + hapiRequestInformationExtractor(ANOTHER_PARTIAL_REQ_DERIVATION_VALUE), + ANOTHER_PARTIAL_REQ_EXPECTED_VALUE + ); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testKoaRequestInformationExtractor.js b/packages/error-reporting/test/unit/testKoaRequestInformationExtractor.js new file mode 100644 index 00000000000..45dddf7fa22 --- /dev/null +++ b/packages/error-reporting/test/unit/testKoaRequestInformationExtractor.js @@ -0,0 +1,71 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var koaRequestInformationExtractor = + require('../../src/request-extractors/koa.js'); +var Fuzzer = require('../../utils/fuzzer.js'); + +describe('koaRequestInformationExtractor', function() { + describe('Behaviour under invalid input', function() { + it('Should produce a default value', function() { + var DEFAULT_RETURN_VALUE = { + method: '', + url: '', + userAgent: '', + referrer: '', + statusCode: 0, + remoteAddress: '' + }; + var f = new Fuzzer(); + var cbFn = function(value) { + assert.deepEqual(value, DEFAULT_RETURN_VALUE); + }; + f.fuzzFunctionForTypes(koaRequestInformationExtractor, + ['object', 'object'], cbFn); + }); + }); + describe('Behaviour under valid input', function() { + it('Should produce the expected value', function() { + var 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' + }; + var FULL_RES_DERIVATION_VALUE = { + status: 200 + }; + var 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 + }; + assert.deepEqual( + koaRequestInformationExtractor(FULL_REQ_DERIVATION_VALUE, + FULL_RES_DERIVATION_VALUE), + FULL_REQ_EXPECTED_VALUE); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testLogger.js b/packages/error-reporting/test/unit/testLogger.js new file mode 100644 index 00000000000..cd7aa29e57f --- /dev/null +++ b/packages/error-reporting/test/unit/testLogger.js @@ -0,0 +1,61 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var assert = require('assert'); +var createLogger = require('../../src/logger.js'); + +describe('logger', function() { + describe('Initialization', function() { + var oldEnv; + before(function() { + oldEnv = process.env.GCLOUD_ERRORS_LOGLEVEL; + delete process.env.GCLOUD_ERRORS_LOGLEVEL; + }); + after(function() {process.env.GCLOUD_ERRORS_LOGLEVEL = oldEnv;}); + describe('Exception handling', function() { + it('Should not throw given undefined', function() { + assert.doesNotThrow(createLogger, createLogger()); + }); + it('Should not throw given an empty object', function() { + assert.doesNotThrow(createLogger.bind(null, {}), createLogger()); + }); + it('Should not throw given logLevel as a number', function() { + assert.doesNotThrow(createLogger.bind(null, {logLevel: 3}), + createLogger({logLevel: 3})); + }); + it('Should not throw given logLevel as a string', function() { + assert.doesNotThrow(createLogger.bind(null, {logLevel: '3'}), + createLogger({logLevel: 3})); + }); + it('Should not throw given an env variable to use', function() { + process.env.GCLOUD_ERRORS_LOGLEVEL = 4; + assert.doesNotThrow(createLogger, createLogger({logLevel: 4})); + delete process.env.GCLOUD_ERRORS_LOGLEVEL; + }); + it('Should thow given logLevel as null', function() { + assert.throws(createLogger.bind(null, {logLevel: null}), + undefined); + }); + }); + describe('Default log level', function() { + it('Should be able to WARN by default', function() { + var logger = createLogger(); + logger.warn('test warning message'); + }); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testManualHandler.js b/packages/error-reporting/test/unit/testManualHandler.js new file mode 100644 index 00000000000..bb39e4ed048 --- /dev/null +++ b/packages/error-reporting/test/unit/testManualHandler.js @@ -0,0 +1,186 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var manual = require('../../src/interfaces/manual.js'); +var Configuration = require('../fixtures/configuration.js'); +var config = new Configuration({}); +config.lacksCredentials = function() { + return false; +}; +var ErrorMessage = require('../../src/classes/error-message.js'); +// var nock = require('nock'); + +describe('Manual handler', function() { + // nock.disableNetConnect(); + // Mocked client + var client = { + sendError: function(e, cb) { + // immediately callback + if (cb) { + setImmediate(cb); + } + } + }; + var report = manual(client, config); + describe('Report invocation behaviour', function() { + it('Should allow argument-less invocation', function() { + var r = report(); + assert(r instanceof ErrorMessage, 'should be an inst of ErrorMessage'); + }); + it('Should allow single string', function() { + var 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', function() { + var r = report(new Error('hokeypokey')); + assert(r.message.match(/hokeypokey/)); + }); + it('Should allow a function as a malformed error input', function(done) { + this.timeout(2000); + var r = report(function(err, res) { + assert(false, 'callback should not be called'); + done(); + }); + assert(r instanceof ErrorMessage, 'should be an inst of ErrorMessage'); + setTimeout(function() { + done(); + }, 1000); + }); + it('Should callback to the supplied function', function(done) { + var r = report('malarkey', function(err, res) { + done(); + }); + assert(r.message.match(/malarkey/), 'string error should propagate'); + }); + it('replace the error string with the additional message', function(done) { + var r = report('monkey', 'wrench', function(err, res) { + done(); + }); + assert.strictEqual(r.message, 'wrench', + 'additional message should replace'); + }); + it('Should allow a full array of optional arguments', function(done) { + var r = report('donkey', { method: 'FETCH' }, 'cart', function(err, res) { + 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', function() { + var 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', function(done) { + var r = report('ticky', { method: 'TACKEY' }, function(err, res) { + 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', function(done) { + var r = report('hockey', function(err, res) { + done(); + }, 'field'); + assert(r.message.match('hockey') && !r.message.match('field'), + 'string after callback should be ignored'); + }); + it('Should ignore arguments', function(done) { + var r = report('passkey', function(err, res) { + done(); + }, { method: 'HONK'}); + assert.notEqual(r.context.httpRequest.method, 'HONK'); + }); + it('Should allow null arguments as placeholders', function(done) { + var r = report('pokey', null, null, function(err, res) { + done(); + }); + assert(r.message.match(/pokey/), 'string error should propagate'); + }); + it('Should allow explicit undefined', function(done) { + var r = report('Turkey', undefined, undefined, function(err, res) { + done(); + }); + assert(r.message.match(/Turkey/), 'string error should propagate'); + }); + it('Should allow request to be supplied as undefined', function(done) { + var r = report('turnkey', undefined, 'solution', function(err, res) { + done(); + }); + assert.strictEqual(r.message, 'solution', 'error should propagate'); + }); + it('Should allow additional message', function(done) { + var r = + report('Mickey', { method: 'SNIFF'}, undefined, function(err, res) { + 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', function() { + it('Should accept builder inst as only argument', function() { + var msg = 'test'; + var r = report(new ErrorMessage().setMessage(msg)); + assert.strictEqual(r.message, msg, + 'string message should propagate from error message inst'); + }); + it('Should accept builder and request as arguments', function() { + var msg = 'test'; + var oldReq = {method: 'GET'}; + var newReq = {method: 'POST'}; + var r = report( + new ErrorMessage().setMessage(msg).consumeRequestInformation(oldReq), + newReq + ); + assert.strictEqual(r.message, 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', function() { + var oldMsg = 'test'; + var newMsg = 'analysis'; + var 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', function(done) { + var oldMsg = 'test'; + report( + new ErrorMessage().setMessage(oldMsg), + function() { done(); } + ); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testManualRequestInformationExtractor.js b/packages/error-reporting/test/unit/testManualRequestInformationExtractor.js new file mode 100644 index 00000000000..43fc1661474 --- /dev/null +++ b/packages/error-reporting/test/unit/testManualRequestInformationExtractor.js @@ -0,0 +1,114 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var omit = require('lodash.omit'); +var extend = require('extend'); +var manualRequestInformationExtractor = + require('../../src/request-extractors/manual.js'); +var Fuzzer = require('../../utils/fuzzer.js'); + +describe('manualRequestInformationExtractor', function() { + describe('Behaviour given invalid input', function() { + it('Should return default values', function() { + var DEFAULT_RETURN_VALUE = { + method: '', + url: '', + userAgent: '', + referrer: '', + statusCode: 0, + remoteAddress: '' + }; + var f = new Fuzzer(); + var cbFn = function(value) { + assert.deepEqual(value, DEFAULT_RETURN_VALUE); + }; + f.fuzzFunctionForTypes(manualRequestInformationExtractor, ['object'], + cbFn); + }); + }); + describe('Behaviour given valid input', function() { + var 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', function() { + assert.deepEqual( + 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(' ') + ); + assert.deepEqual( + manualRequestInformationExtractor(omit(FULL_VALID_INPUT, 'method')), + extend({}, 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(' ') + ); + assert.deepEqual( + manualRequestInformationExtractor(omit(FULL_VALID_INPUT, 'url')), + extend({}, FULL_VALID_INPUT, {url: ''}), + [ + 'Given a valid input sans the url property these values should be', + 'reflected by the output of the request extraction' + ] + ); + assert.deepEqual( + manualRequestInformationExtractor(omit(FULL_VALID_INPUT, 'userAgent')), + extend({}, 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' + ] + ); + assert.deepEqual( + manualRequestInformationExtractor(omit(FULL_VALID_INPUT, 'referrer')), + extend({}, 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' + ] + ); + assert.deepEqual( + manualRequestInformationExtractor(omit(FULL_VALID_INPUT, 'statusCode')), + extend({}, 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' + ] + ); + assert.deepEqual( + manualRequestInformationExtractor(omit(FULL_VALID_INPUT, + 'remoteAddress')), + extend({}, FULL_VALID_INPUT, {remoteAddress: ''}), + [ + 'Given a valid input sans the remoteAddress property these values', + 'should be reflected by the output of the request extraction' + ] + ); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testRequestInformationContainer.js b/packages/error-reporting/test/unit/testRequestInformationContainer.js new file mode 100644 index 00000000000..baf2fcf691f --- /dev/null +++ b/packages/error-reporting/test/unit/testRequestInformationContainer.js @@ -0,0 +1,96 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var RequestInformationContainer = + require('../../src/classes/request-information-container.js'); +var Fuzzer = require('../../utils/fuzzer.js'); + +describe('RequestInformationContainer', function() { + var f = new Fuzzer(); + var cbFn, ric; + beforeEach(function() {ric = new RequestInformationContainer();}); + describe('Fuzzing against RequestInformationContainer', function() { + it('Should return the property as an empty string', + function() { + cbFn = function() { + assert.deepEqual(ric.url, ''); + }; + f.fuzzFunctionForTypes(ric.setUrl, ['string'], cbFn, ric); + } + ); + it('Should return the method property as an empty string', function() { + cbFn = function(returnValue) { + assert.deepEqual(ric.method, ''); + }; + f.fuzzFunctionForTypes(ric.setMethod, ['string'], cbFn, ric); + }); + it('Should return the referrer property as an empty string', function() { + cbFn = function(returnValue) { + assert.deepEqual(ric.referrer, ''); + }; + f.fuzzFunctionForTypes(ric.setReferrer, ['string'], cbFn, ric); + }); + it('Should return the userAgent property as an empty string', function() { + cbFn = function(returnValue) { + assert.deepEqual(ric.userAgent, ''); + }; + f.fuzzFunctionForTypes(ric.setUserAgent, ['string'], cbFn, ric); + }); + it('Should return the property as an empty string', function() { + cbFn = function(returnValue) { + assert.deepEqual(ric.remoteAddress, ''); + }; + f.fuzzFunctionForTypes(ric.setRemoteAddress, ['string'], cbFn, ric); + }); + it('Should return the default value for statusCode', function() { + cbFn = function(returnValue) { + assert.strictEqual(ric.statusCode, 0); + }; + f.fuzzFunctionForTypes(ric.setStatusCode, ['number'], cbFn, ric); + }); + }); + describe('Fuzzing against for positive cases', function() { + var VALID_STRING_INPUT = 'valid'; + var VALID_NUMBER_INPUT = 500; + it('Should assign the value to the url property', function() { + ric.setUrl(VALID_STRING_INPUT); + assert.deepEqual(ric.url, VALID_STRING_INPUT); + }); + it('Should assign the value to the method property', function() { + ric.setMethod(VALID_STRING_INPUT); + assert.deepEqual(ric.method, VALID_STRING_INPUT); + }); + it('Should assign the value to the referrer property', function() { + ric.setReferrer(VALID_STRING_INPUT); + assert.deepEqual(ric.referrer, VALID_STRING_INPUT); + }); + it('Should assign the value to the userAgent property', function() { + ric.setUserAgent(VALID_STRING_INPUT); + assert.deepEqual(ric.userAgent, VALID_STRING_INPUT); + }); + it('Should assign the value to remoteAddress property', function() { + ric.setRemoteAddress(VALID_STRING_INPUT); + assert.deepEqual(ric.remoteAddress, VALID_STRING_INPUT); + }); + it('Should assign the value to statusCode property', function() { + ric.setStatusCode(VALID_NUMBER_INPUT); + assert.deepEqual(ric.statusCode, VALID_NUMBER_INPUT); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testRestifyInterface.js b/packages/error-reporting/test/unit/testRestifyInterface.js new file mode 100644 index 00000000000..781204f0bc9 --- /dev/null +++ b/packages/error-reporting/test/unit/testRestifyInterface.js @@ -0,0 +1,136 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var assert = require('assert'); +var restifyInterface = require('../../src/interfaces/restify.js'); + +// node v0.12 compatibility +if (!EventEmitter.prototype.listenerCount) { + EventEmitter.prototype.listenerCount = function(eventName) { + return EventEmitter.listenerCount(this, eventName); + }; +} + +describe('restifyInterface', function() { + var UNCAUGHT_EVENT = 'uncaughtException'; + var FINISH = 'finish'; + var noOp = function() {return;}; + describe('Attachment to the uncaughtException event', function() { + it('Should attach one listener after instantiation', function() { + var 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 + var errorHandlerInstance = restifyInterface(null, null); + // execute the handler the user will use with the stubbed server instance + errorHandlerInstance(ee); + assert.strictEqual(ee.listenerCount(UNCAUGHT_EVENT), 1, + 'Listeners on event should now be one'); + }); + }); + describe('Request handler lifecycle events', function() { + var ee = new EventEmitter(); + var errorHandlerInstance = restifyInterface(null, null); + var requestHandlerInstance = errorHandlerInstance(ee); + describe('default path on invalid input', function() { + it('Should not throw', function() { + assert.doesNotThrow(function() { + requestHandlerInstance(null, null, noOp); + }); + }); + }); + describe('default path without req/res error', function() { + ee.removeAllListeners(); + var req = new EventEmitter(); + var res = new EventEmitter(); + res.statusCode = 200; + it('Should have 0 listeners on the finish event', function() { + assert.strictEqual(res.listenerCount(FINISH), 0); + }); + it('Should not throw while handling the req/res objects', function() { + assert.doesNotThrow(function() { + requestHandlerInstance(req, res, noOp); + }); + }); + it('Should have 1 listener', function() { + assert.strictEqual(res.listenerCount(FINISH), 1); + }); + it('Should not throw when emitting the finish event', function() { + assert.doesNotThrow(function() { + res.emit(FINISH); + }); + }); + }); + describe('default path with req/res error', function(done) { + ee.removeAllListeners(); + var client = { + sendError: function() { + assert(true, 'sendError should be called'); + } + }; + var config = { + getServiceContext: function() { + assert(true, 'getServiceContext should be called'); + return { + service: 'stub-service', + version: 'stub-version' + }; + }, + lacksCredentials: function() { + return false; + }, + getVersion: function() { + return '1'; + } + }; + var errorHandlerInstance = restifyInterface(client, config); + var requestHandlerInstance = errorHandlerInstance(ee); + var req = new EventEmitter(); + var res = new EventEmitter(); + res.statusCode = 500; + it('Should have 0 Listeners on the finish event', function() { + assert.strictEqual(res.listenerCount(FINISH), 0); + }); + it('Should not throw on instantiation', function() { + assert.doesNotThrow(function() { + requestHandlerInstance(req, res, noOp); + }); + }); + it('Should have 1 listener on the finish event', function() { + assert.strictEqual(res.listenerCount(FINISH), 1); + }); + it('Should not throw on emission of the finish event', function() { + assert.doesNotThrow(function() { + res.emit(FINISH); + }); + }); + describe('Exercise the uncaughtException event path', function() { + it('Should call the sendError function property', function(done) { + client.sendError = function() { + assert(true, 'sendError should be called'); + done(); + }; + assert.doesNotThrow(function() { + ee.emit(UNCAUGHT_EVENT); + }); + }); + }); + }); + }); +}); diff --git a/packages/error-reporting/test/unit/testServiceConfiguration.js b/packages/error-reporting/test/unit/testServiceConfiguration.js new file mode 100644 index 00000000000..f1dc4e634f9 --- /dev/null +++ b/packages/error-reporting/test/unit/testServiceConfiguration.js @@ -0,0 +1,233 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var assert = require('assert'); +var is = require('is'); +var isString = is.string; +var isNumber = is.number; +var forEach = require('lodash.foreach'); +var assign = require('lodash.assign'); +var omitBy = require('lodash.omitby'); +var Configuration = require('../fixtures/configuration.js'); +var level = process.env.GCLOUD_ERRORS_LOGLEVEL; +var logger = require('../../src/logger.js')({ + logLevel: isNumber(level) ? level : 4 +}); +var env = { + 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 sterilizeEnv() { + forEach(env, function(val, key) { + delete process.env[key]; + }); +} +function setEnv(serviceName, serviceVersion, moduleName, mv, fn) { + assign(process.env, omitBy({ + GAE_SERVICE: serviceName, + GAE_VERSION: serviceVersion, + GAE_MODULE_NAME: moduleName, + GAE_MODULE_VERSION: mv, + FUNCTION_NAME: fn + }, function(val) {return !isString(val);})); +} +function restoreEnv() { + assign(process.env, env); +} + +describe('Testing service configuration', function() { + beforeEach(function() {sterilizeEnv();}); + after(function() {restoreEnv();}); + it( + 'A Configuration uses the function name as the service name on GCF ' + + 'if the service name is not given in the given config', + function() { + setEnv('someModuleName', '1.0', 'InvalidName', 'InvalidVersion', + 'someFunction'); + var c = new Configuration({}, logger); + assert.deepEqual(c.getServiceContext().service, 'someFunction'); + // FUNCTION_NAME is set and the user didn't specify a version, and so + // the version should not be defined + assert.deepEqual(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', + function() { + setEnv(null, '1.0', null, 'InvalidVersion', 'someFunction'); + var c = new Configuration({}, logger); + assert.deepEqual(c.getServiceContext().service, 'someFunction'); + // The user didn't specify a version and FUNCTION_NAME is defined, and + // so the version should not be defined + assert.deepEqual(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', + function() { + setEnv('someModuleName', '1.0', 'InvalidName', 'InvalidVersion', null); + var c = new Configuration({}, logger); + assert.deepEqual(c.getServiceContext().service, 'someModuleName'); + // The user didn't specify a version, and FUNCTION_NAME is not defined, + // and so use the GAE_MODULE_VERSION + assert.deepEqual(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', + function() { + setEnv('someModuleName', '1.0', 'InvalidName', 'InvalidVersion', + 'someFunction'); + var c = new Configuration({ + serviceContext: { + service: 'customService' + } + }, logger); + assert.deepEqual(c.getServiceContext().service, 'customService'); + // The user didn't specify a version, but FUNCTION_NAME is defined, and + // so the version should not be defined + assert.deepEqual(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', + function() { + setEnv('someModuleName', '1.0', 'InvalidName', 'InvalidVersion', + 'someFunction'); + var c = new Configuration({ + serviceContext: { + service: 'customService', + version: '2.0' + } + }, logger); + assert.deepEqual(c.getServiceContext().service, 'customService'); + // The user specified version should be used + assert.deepEqual(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 var is set', + function() { + setEnv('someModuleName', '1.0', 'InvalidName', 'InvalidVersion', null); + var c = new Configuration({ + serviceContext: { + service: 'customService' + } + }, logger); + assert.deepEqual(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 + assert.deepEqual(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 var is set', + function() { + setEnv('someModuleName', '1.0', 'InvalidName', 'InvalidVersion', null); + var c = new Configuration({ + serviceContext: { + service: 'customService', + version: '2.0' + } + }, logger); + assert.deepEqual(c.getServiceContext().service, 'customService'); + // The user specified version should be used + assert.deepEqual(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 var is set', + function() { + setEnv(null, '1.0', null, 'InvalidVersion', 'someFunction'); + var c = new Configuration({ + serviceContext: { + service: 'customService' + } + }, logger); + assert.deepEqual(c.getServiceContext().service, 'customService'); + // The user didn't specify a version and thus because FUNCTION_NAME is + // defined the version should not be defined + assert.deepEqual(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 var is set', + function() { + setEnv(null, '1.0', null, 'InvalidVersion', 'someFunction'); + var 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', + function() { + var 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', + function() { + setEnv(null, 'InvalidVersion', null, 'InvalidVersion', null); + var 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', + function() { + var c = new Configuration({ + serviceContext: { + version: '2.0' + } + }, logger); + assert.deepEqual(c.getServiceContext().service, 'node'); + assert.deepEqual(c.getServiceContext().version, '2.0'); + } + ); +}); diff --git a/packages/error-reporting/test/unit/testUncaught.js b/packages/error-reporting/test/unit/testUncaught.js new file mode 100644 index 00000000000..8d9fb503a7e --- /dev/null +++ b/packages/error-reporting/test/unit/testUncaught.js @@ -0,0 +1,95 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var isString = require('is').string; +var uncaughtSetup = require('../../src/interfaces/uncaught.js'); +var Configuration = require('../fixtures/configuration.js'); +var createLogger = require('../../src/logger.js'); +var originalHandlers = process.listeners('uncaughtException'); +var spawn = require('child_process').spawn; + +function reattachOriginalListeners() { + for (var i = 0; i < originalHandlers.length; i++) { + process.on('uncaughtException', originalHandlers[i]); + } +} + +// Returns a Configuration object with given value for reportUncaughtExceptions, +// and dummy logger +function getConfig(reportUncaughtExceptions) { + var c = new Configuration({ + reportUncaughtExceptions: reportUncaughtExceptions + }, createLogger({logLevel: 4})); + c.lacksCredentials = function() { + return false; + }; + return c; +} + +describe('Uncaught exception handler behvaiour', function() { + var UNCAUGHT = 'uncaughtException'; + describe('Instantiation', function() { + beforeEach(function() {process.removeAllListeners(UNCAUGHT);}); + it('Should throw without a configuration', function() { + assert.throws(uncaughtSetup); + }); + it('Should not throw given a valid configuration', function() { + assert.doesNotThrow(uncaughtSetup.bind(null, {}, getConfig(false))); + assert.doesNotThrow(uncaughtSetup.bind(null, {}, getConfig(true))); + }); + it('Should return the process object after instantiation', function() { + assert.strictEqual(process, uncaughtSetup({}, getConfig(true))); + }); + it('Should not attach a listener to the uncaughtException event if ' + + 'reportUncaughtExceptions is false', function() { + uncaughtSetup({}, getConfig(false)); + assert.strictEqual(process.listeners(UNCAUGHT).length, 0); + }); + it('Should attach a listener to the uncaughtException event if ' + + 'reportUncaughtExceptions is true', function() { + uncaughtSetup({}, getConfig(true)); + assert.strictEqual(process.listeners(UNCAUGHT).length, 1); + }); + }); + describe('Uncaught exception handling shutdown behaviour', function() { + before(function() { + if (!isString(process.env.GOOGLE_APPLICATION_CREDENTIALS) || + !isString(process.env.GCLOUD_PROJECT)) { + return this.skip(); + } + }); + after(function() { + reattachOriginalListeners(); + }); + it('Should terminate before 2500ms', function(done) { + var TERMINATE_MSG = 'Should terminate before 2500ms'; + this.timeout(3500); + var isolate = spawn('./node_modules/mocha/bin/mocha', + ['../../test/fixtures/uncaughtExitBehaviour.js'], {env: process.env}); + isolate.on('close', function() { + done(); + }); + isolate.on('error', function() { + console.log('Test isolate encountered error:', '\n', arguments); + assert(false, TERMINATE_MSG); + done(); + }); + }); + }); +}); diff --git a/packages/error-reporting/utils/fuzzer.js b/packages/error-reporting/utils/fuzzer.js new file mode 100644 index 00000000000..956a688aae3 --- /dev/null +++ b/packages/error-reporting/utils/fuzzer.js @@ -0,0 +1,315 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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:disable +// jshint ignore: start + +'use strict'; +var indexOf = require('lodash.indexof'); +var without = require('lodash.without'); +var maxBy = require('lodash.maxby'); +var random = require('lodash.random'); +var is = require('is'); +var isNumber = is.number; +var isObject = is.object; +var isString = is.string; +var isArray = is.array; +var isNull = is.null; +var isFunction = is.function; + +function Fuzzer() { } + +Fuzzer.prototype.generate = {}; + +Fuzzer.prototype.generate.types = function() { + return [ + "object", + "array", + "string", + "number", + "null", + "undefined", + "function", + "boolean" + ]; +} + +Fuzzer.prototype.generate.string = function(len) { + var lenChecked = isNumber(len) ? len : 10; + var chars = []; + + for (var i = 0; i < lenChecked; i++) { + + chars.push(String.fromCharCode(random(32, 126))); + } + + return chars.join(""); +}; + +Fuzzer.prototype.generate.boolean = function() { + + return !!random(0, 1); +} + +Fuzzer.prototype.generate.alphaNumericString = function(len) { + var lenChecked = isNumber(len) ? len : 10; + var chars = []; + var thisRange = []; + var ranges = [[48, 57], [65, 90], [97, 122]]; + + for (var i = 0; i < lenChecked; i++) { + + thisRange = ranges[random(0, 2)]; + chars.push( + String.fromCharCode( + random( + thisRange[0] + , thisRange[1] + ) + ) + ); + } + + return chars.join(""); +} + +Fuzzer.prototype.generate.function = function() { + + var availableTypes = without(this.types(), "function"); + var typeToGen = this.types()[random(0, availableTypes.length-1)]; + var fnToCall = this[typeToGen]; + + return function() { + + return fnToCall(); + }; +} + +Fuzzer.prototype.generate.number = function(lower, upper) { + + var lowerChecked = isNumber(lower) ? lower : 0; + var upperChecked = isNumber(upper) ? upper : 100; + + return random(lowerChecked, upperChecked); +} + +Fuzzer.prototype.generate.null = function() { + + return null; +} + +Fuzzer.prototype.generate.undefined = function() { + + return undefined; +} + +Fuzzer.prototype.generate.array = function(len, ofOneType, currentDepth, allowedDepth) { + + var lenChecked = isNumber(len) ? len : random(1, 10); + var availableTypes = (isString(ofOneType) && (indexOf(this.types(), ofOneType) > -1)) ? [ofOneType] : this.types(); + var currentDepthChecked = isNumber(currentDepth) ? currentDepth : 0; + var allowedDepthChecked = isNumber(allowedDepth) ? allowedDepth : 3; + var arr = []; + var currentTypeBeingGenerated = ""; + currentDepthChecked += 1; + + // Deny the ability to nest more objects + if (currentDepthChecked >= allowedDepthChecked) { + + availableTypes = without(this.types(), "object", "array"); + } + + for (var 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[currentTypeBeingGenerated]()); + } + } + + return arr; +} + +Fuzzer.prototype.generate.object = function(numProperties, currentDepth, allowedDepth) { + + var numPropertiesChecked = isNumber(numProperties) ? numProperties : random(1, 10); + var currentDepthChecked = isNumber(currentDepth) ? currentDepth : 0; + var allowedDepthChecked = isNumber(allowedDepth) ? allowedDepth : 3; + var obj = {}; + currentDepthChecked += 1; + + var availableTypes = this.types() + + // Deny the ability to nest more objects + if (currentDepth >= allowedDepth) { + availableTypes = without(availableTypes, "object", "array"); + } + + var currentTypeBeingGenerated = 0; + var currentKey = ""; + + for (var 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[currentTypeBeingGenerated](); + } + } + + return obj; +} + +Fuzzer.prototype._backFillUnevenTypesArrays = function(argsTypesArray) { + + var largestIndex = 0; + var largestLength = (maxBy( + argsTypesArray + , function(o) { return o.length } + )).length; + + for (var 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; +} + +Fuzzer.prototype._normalizeTypesArrayLengths = function(argsTypesArray) { + + var allAreTheSameLength = true; + var lastLength = argsTypesArray[0].length; + + for (var i = 1; i < argsTypesArray.length; i++) { + + if (argsTypesArray[i].length !== lastLength) { + + allAreTheSameLength = false; + break; + } + } + + if (allAreTheSameLength) { + + return argsTypesArray; + } + + return this._backFillUnevenTypesArrays(argsTypesArray); +} + +Fuzzer.prototype._generateTypesToFuzzWith = function(expectsArgTypes) { + var argsTypesArray = []; + var tmpArray = this.generate.types(); + + for (var i = 0; i < expectsArgTypes.length; i++) { + + if (!isArray(expectsArgTypes[i])) { + argsTypesArray.push( + without( + this.generate.types() + , expectsArgTypes[i] + ) + ); + } else { + + for (var j = 0; j < expectsArgTypes[i].length; j++) { + + tmpArray = without( + tmpArray + , expectsArgTypes[i][j] + ); + } + + argsTypesArray.push([].concat(tmpArray)); + tmpArray = this.generate.types(); + } + } + + argsTypesArray = this._normalizeTypesArrayLengths(argsTypesArray); + return argsTypesArray; +} + +Fuzzer.prototype._generateValuesForFuzzTyping = function(typesToFuzzOnEach, index) { + var args = []; + var typeToGen = ""; + + for (var i = 0; i < typesToFuzzOnEach.length; i++) { + typeToGen = typesToFuzzOnEach[i][index]; + + args.push(this.generate[typeToGen]()); + } + + return args; +} + +Fuzzer.prototype.fuzzFunctionForTypes = function(fnToFuzz, expectsArgTypes, cb, withContext) { + var expectsArgTypesChecked = isArray(expectsArgTypes) ? expectsArgTypes : []; + var typesToFuzzOnEach = this._generateTypesToFuzzWith(expectsArgTypesChecked); + var withContextChecked = (withContext !== undefined) ? withContext : null; + + var returnValue = undefined; + + for (var i = 0; i < typesToFuzzOnEach[0].length; i++) { + + returnValue = fnToFuzz.apply( + withContext + , this._generateValuesForFuzzTyping(typesToFuzzOnEach, i) + ); + + if (isFunction(cb)) { + + cb(returnValue); + } + } + + return true; +} + +module.exports = Fuzzer; diff --git a/scripts/docs/config.js b/scripts/docs/config.js index 66045a27041..8857ef3ede1 100644 --- a/scripts/docs/config.js +++ b/scripts/docs/config.js @@ -46,6 +46,10 @@ module.exports = { title: 'Google Cloud', instanceName: 'gcloud' }, + 'error-reporting': { + title: 'Error Reporting', + instanceName: 'errors' + }, bigquery: { title: 'BigQuery' }, diff --git a/test/docs.js b/test/docs.js index 315a07b4546..a267202362d 100644 --- a/test/docs.js +++ b/test/docs.js @@ -69,15 +69,48 @@ var FakeConsole = Object.keys(console) return console; }, {}); -// For {module:datastore} docs. +// For {module:datastore && module:error-reporting} docs. var FakeExpress = function() { return { get: function(route, callback) { callback({ query: {} }, {}); + }, + use: function() {}, + listen: function() {} + }; +}; + +// For {module:error-reporting} docs. +var FakeHapi = function() { + return { + Server: function() { + return { + connection: function() {}, + start: function() {}, + register: function() {} + }; + } + }; +}; + +// For {module:error-reporting} docs. +var FakeRestify = function() { + return { + createServer: function() { + return { + use: function() {}, + on: function() {} + }; } }; }; +var FakeKoa = function() { + return { + use: function() {} + }; +}; + // For {module:vision} docs. var FakeLevel = function() { return { @@ -290,6 +323,9 @@ function createSnippet(mod, instantiation, method) { 'keyFilename: \'\'' ) .replace('require(\'express\')', FakeExpress.toString()) + .replace('require(\'hapi\')', '(' + FakeHapi.toString() + '())') + .replace('require(\'restify\')', '(' + FakeRestify.toString() + '())') + .replace('require(\'koa\')', FakeKoa.toString()) .replace('require(\'level\')', FakeLevel.toString()) .replace('require(\'bluebird\')', FakeBluebird.toString()) .replace('require(\'bunyan\')', '(' + fakeBunyan.toString() + '())')