├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md └── release.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.md ├── History.md ├── LICENSE ├── Makefile ├── README.md ├── bin └── raven ├── docs ├── Makefile ├── coffeescript.rst ├── conf.py ├── config.rst ├── index.rst ├── integrations │ ├── connect.rst │ ├── express.rst │ ├── index.rst │ ├── koa.rst │ ├── loopback.rst │ └── sails.rst ├── make.bat ├── sentry-doc-config.json ├── sourcemaps.rst ├── typescript.rst └── usage.rst ├── index.js ├── lib ├── client.js ├── instrumentation │ ├── console.js │ ├── http.js │ ├── instrumentor.js │ └── pg.js ├── parsers.js ├── transports.js └── utils.js ├── package-lock.json ├── package.json ├── test ├── exit │ ├── capture.js │ ├── capture_callback.js │ ├── capture_with_second_domain_error.js │ ├── capture_with_second_error.js │ ├── domain_capture.js │ ├── domain_capture_callback.js │ ├── domain_capture_with_second_error.js │ ├── domain_throw_on_send.js │ ├── throw_on_fatal.js │ └── throw_on_send.js ├── fixtures │ ├── file with spaces in path.js │ └── long-line.js ├── instrumentation │ ├── README.md │ ├── http.test.js │ ├── run-node-http-test.js │ └── run.sh ├── manual │ ├── README.md │ ├── context-memory.js │ ├── express-patient.js │ ├── largeModule.js │ ├── manager.js │ └── poke-patient.sh ├── mocha.opts ├── raven.client.js ├── raven.parsers.js ├── raven.transports.js ├── raven.utils.js ├── run.coffee └── vendor │ └── json-stringify-safe.js └── vendor ├── json-stringify-safe.js └── node-lsmod.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test/instrumentation/node-* 3 | test/manual/largeModule.js 4 | test/fixtures/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "block-scoped-var": 2, 9 | "camelcase": [1, "properties": "never"], 10 | "comma-dangle": 0, 11 | "consistent-return": 2, 12 | "consistent-this": [2, "self"], 13 | "default-case": 2, 14 | "dot-notation": 2, 15 | "eqeqeq": [2, "allow-null"], 16 | "guard-for-in": 2, 17 | "new-cap": 2, 18 | "no-alert": 2, 19 | "no-caller": 2, 20 | "no-catch-shadow": 1, 21 | "no-console": 1, 22 | "no-debugger": 2, 23 | "no-else-return": 2, 24 | "no-empty": [2, {"allowEmptyCatch": true}], 25 | "no-eval": 2, 26 | "no-extra-bind": 2, 27 | "no-extend-native": 2, 28 | "no-fallthrough": 2, 29 | "no-floating-decimal": 2, 30 | "no-implied-eval": 2, 31 | "no-inner-declarations": 2, 32 | "no-iterator": 2, 33 | "no-lonely-if": 2, 34 | "no-loop-func": 2, 35 | "no-multi-str": 2, 36 | "no-new": 2, 37 | "no-param-reassign": 1, 38 | "no-proto": 2, 39 | "no-redeclare": 2, 40 | "no-return-assign": 2, 41 | "no-script-url": 2, 42 | "no-self-compare": 2, 43 | "no-sequences": 2, 44 | "no-shadow": 2, 45 | "no-shadow-restricted-names": 2, 46 | "no-undef": 2, 47 | "no-undefined": 2, 48 | "no-underscore-dangle": 0, 49 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 50 | "no-use-before-define": [2, "nofunc"], 51 | "no-useless-call": 2, 52 | "no-useless-escape": 2, 53 | "no-warning-comments": 0, 54 | "no-with": 2, 55 | "spaced-comment": [2, "always"], 56 | "strict": [2, "global"], 57 | "valid-jsdoc": 0, 58 | "valid-typeof": 2, 59 | "yoda": [2, "never"] 60 | }, 61 | "extends": [ 62 | "prettier" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Do you want to request a *feature* or report a *bug*?** 2 | 3 | (If this is a *usage question*, please **do not post it here**—post it on [forum.sentry.io](https://forum.sentry.io) instead. If this is not a “feature” or a “bug”, or the phrase “How do I...?” applies, then it's probably a usage question.) 4 | 5 | **Has someone had this problem before?** 6 | 7 | Before opening an issue, please search the issue tracker to make sure your issue hasn't already been reported. 8 | 9 | **What is the current behavior?** 10 | 11 | If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal code sample. 12 | 13 | **What is the expected behavior?** 14 | 15 | Which versions of raven-node are affected by this issue? How are you setting up raven-node in your application? Did this work previously, or in any other configuration? Are you using [hosted Sentry](https://sentry.io) or on-premises? If on-premises, which version (e.g. 8.7.0)? 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | store: zeus 2 | targets: 3 | - name: github 4 | changelog: History.md 5 | - npm 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | docs/_build 15 | docs/doctrees 16 | 17 | node_modules 18 | npm-debug.log 19 | 20 | working/ 21 | coverage/ 22 | test/instrumentation/node-* 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_sentryext"] 2 | path = docs/_sentryext 3 | url = https://github.com/getsentry/sentry-doc-support 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | test/ 3 | working/ 4 | coverage/ 5 | 6 | .git/ 7 | .github/ 8 | .gitignore 9 | .gitmodules 10 | 11 | .eslintignore 12 | .eslintrc 13 | 14 | .npmignore 15 | .travis.yml 16 | Makefile 17 | AUTHORS 18 | CONTRIBUTING.md 19 | 20 | .idea/ 21 | 22 | lib-cov 23 | *.seed 24 | *.log 25 | *.csv 26 | *.dat 27 | *.out 28 | *.pid 29 | *.gz 30 | 31 | pids 32 | logs 33 | results 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | - "7" 8 | - "8" 9 | - "9" 10 | - "10" 11 | 12 | cache: 13 | directories: 14 | - node_modules 15 | 16 | branches: 17 | only: 18 | - master 19 | 20 | script: npm run test-full 21 | 22 | matrix: 23 | include: 24 | - script: npm pack 25 | node_js: "8" 26 | after_success: 27 | - npm install -g @zeus-ci/cli 28 | - zeus upload -t "application/tar+npm" *.tgz 29 | 30 | notifications: 31 | webhooks: 32 | urls: 33 | - https://zeus.ci/hooks/b152d48c-d694-11e7-99e7-0a580a28020f/public/provider/travis/webhook 34 | on_success: always 35 | on_failure: always 36 | on_start: always 37 | on_cancel: always 38 | on_error: always 39 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | https://github.com/mattrobenolt/raven-node/graphs/contributors 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to release raven-node: 2 | 3 | * [ ] Run the manual memory tests in `test/manual` to make sure we didn't introduce a memory leak 4 | * [ ] Consider whether any changes warrant additions to these tests 5 | * [ ] Stop and think "What version number should this be according to SemVer?" 6 | * [ ] Add an entry to the [History](https://github.com/getsentry/raven-node/blob/master/History.md) file. 7 | * [ ] Bump version number in `package.json` 8 | * [ ] Commit changes `git commit -am ""` 9 | * [ ] Create a tag `git tag -a -m ""` 10 | * [ ] Push to GitHub (`git push origin master --follow-tags`). 11 | * [ ] Once CI builds pass, `sentry-probot` will publish a release on npm and GitHub. 12 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.6.2 - 2018-05-17 4 | 5 | - ref: Emit transaction instead of culprit (#458) 6 | 7 | ## 2.6.1 - 2018-05-10 8 | 9 | - fix: correctly detect errors from vm (#457) 10 | - ref: use console.warn for alerts and store them in Set (#455) 11 | - ci: Add node 9 and 10 to travis builds (#456) 12 | 13 | ## 2.6.0 - 2018-04-24 14 | 15 | - fix: Use shallow-copy instead of deep-copy when working with context to prevent too large memory usage and slowing down request resonses [See #452] 16 | 17 | ## 2.5.0 - 2018-04-09 18 | 19 | - feat: log non-standard ports in breadcrumbs (#440) 20 | - feat: add flag for unhandled promise rejections (#446) 21 | - fix: Remove a redundant try-catch block (#445) 22 | - fix: Do not override context when capturing non-error exceptions (#444) 23 | - fix: Update stack-trace to handle spaces in paths (#437) 24 | - docs: Remove private DSNs from the docs (#447) 25 | - docs: Update Usage docs to include Domains in Promise support (#438) 26 | 27 | ## 2.4.2 - 2018-02-27 28 | 29 | - fix: Dont throw exception when called captureException without config (#431) 30 | - fix: Preserve context in for rejected promises (#428) 31 | - ref: Log promise rejection reason alongside eventid (#434) 32 | - ref: Use named function for middlewares (#429) 33 | 34 | ## 2.4.1 - 2018-02-09 35 | 36 | - fix: Handle scoped packages structure in node-lsmod (#426) 37 | - fix: Report fatal errors as indeed "fatal" (#425) 38 | - ref: Use a named function for the middleware over an anonymous (#424) 39 | 40 | ## 2.4.0 - 2018-01-24 41 | 42 | - feat: Sensible non-Error exception serializer (#416) 43 | - fix: workaround for express.js 'host' deprecation message (#413) 44 | 45 | ## 2.3.0 - 12/11/2017 46 | 47 | - fix: attach remaining non-enumerables to req [See #387] 48 | - feat: Allow to configure stacktrace for captureMessage calls [See #388] 49 | - fix: access 'process' through global variable [See #399] 50 | - ref: Enable http instrumentation by default [See #401] 51 | - ref: Warn user when using capture/wrap without installing Raven [See #402] 52 | - ci: Integrate Zeus and release with the bot [See #403] 53 | - ref: Delete empty properties before sending event to the server [See #407] 54 | - feat: Support Boom library statusCode [See #408] 55 | 56 | ## 2.2.1 - 10/02/2017 57 | 58 | - Remove unintentional memwatch-next dependency 59 | 60 | ## 2.2.0 - 10/02/2017 61 | - Fixed all Node v8 tests to ensure everything is working correctly [See #366] 62 | - Raven now uses Prettier to format it's code [See #364] 63 | - Prevent Raven from queueing too many requests in case server is down [See #132] 64 | - Enable keep-alive on socket connection and limit number of sockets [See #284] 65 | - Pull Error's name from constructor, not Error itself to always get correct error type [See #372] 66 | - Updated Errors serialization to store all additional properties and allow for attaching other object instances directly to it [See #376] 67 | - Preserve some non-enumerable properties from request [See #379] 68 | - Fall back to `NODE_ENV` for Sentry Environment [See #384] 69 | 70 | ## 2.1.2 - 8/16/2017 71 | - Remove errant large file that accidentally got published in 2.1.1. [See #361] 72 | 73 | ## 2.1.1 - 7/27/2017 74 | - Fix issue where HTTP request was duplicated as `req` (and dropped by Sentry server). [See #340] 75 | 76 | ## 2.1.0 - 6/20/2017 77 | - Truncate long lines in surrounding source to avoid sending large amounts of minified code [See #329] 78 | - Refactor automatic breadcrumb instrumentation of modules to accommodate compilation tools [See #322] 79 | - Testing for Node 8 [See #328] 80 | 81 | ## 2.0.2 - 5/24/2017 82 | - Fix issue with sending empty request details when no request is present [See #324] 83 | 84 | ## 2.0.1 - 5/16/2017 85 | - Fix memory explosion when parsing stack for large files [See #320] 86 | 87 | ## 2.0.0 - 5/10/2017 88 | - Properly surface errors to preserve process exit conditions [See #308, #257] 89 | - Node processes with raven will now exit in exactly the same situations as if raven were not present 90 | - Previously, there were failure scenarios where raven would erroneously cause the process to continue to run when it should have shut down 91 | - **Be aware when upgrading:** it is possible that with raven-node 2.0, your node process will shut down from exceptions where it previously did not 92 | - This also includes changes to more reliably finish capturing the exception that induces process shutdown 93 | - Don't include `domain` property as extra property on `Error` objects [See #309] 94 | - Parse `req` object from context or kwargs [See #310] 95 | - For Express middleware users in particular, raven will now automatically report request details and user context in situations where it previously did not 96 | - Tidied up `.npmignore` to exclude unnecessary files in npm package [See #311] 97 | - Install size reduced from 484kB to 84kB, which should save npm ~100GB/month in bandwidth 98 | - Removed various deprecated methods [See #313] 99 | - Removed postgres autoBreadcrumbs to avoid webpack dependency issue [See #315, #254] 100 | - postgres (and more) autoBreadcrumbs will return in version 2.1 101 | 102 | ## 1.1.6, 1.2.1 - 4/7/2017 103 | - Fix memory leak in `consoleAlert` (and thus, if not disabled, in `captureException`) [See #300] 104 | 105 | ## 1.2.0 - 3/16/2017 106 | - Add sampleRate config option [See #292] 107 | 108 | ## 1.1.5 - 3/16/2017 109 | - Fix memory leak in http autoBreadcrumb instrumentation [See #296] 110 | 111 | ## 1.1.4 - 3/6/2017 112 | - Use `util.format` to get message string in `console` instrumentation [See #289] 113 | 114 | ## 1.1.3 - 2/27/2017 115 | - Add `parseUser` option to control user parsing behavior [See #274] 116 | - Make http instrumentation use `req.emit` instead of response event handler [See #276] 117 | - Add alert about raven-node vs raven-js when it seems like a browser env [See #277] 118 | 119 | ## 1.1.2 - 2/8/2017 120 | - Send kwargs to `shouldSendCallback` [See #251] 121 | - Capture breadcrumbs from global context [See #267] 122 | - Make stack parsing native-frame-check work on Windows paths [See #268] 123 | - Bind req/res to context domain in express requestHandler [See #269] 124 | - Fix postgres/pg name mismatch [See #270] 125 | 126 | ## 1.1.1 and 1.0.1 - 12/13/2016 127 | - Fix middleware backwards compatibility [See #246] 128 | 129 | ## 1.1.0 - 12/12/2016 130 | - Added support for (automatic) breadcrumbs [See #240] 131 | - `Raven.captureBreadcrumb` manual method 132 | - `autoBreadcrumbs` config field to automatically capture breadcrumbs for: 133 | - console logs 134 | - http requests 135 | - postgres queries 136 | - Deprecate `captureQuery` [See #239] 137 | 138 | ## 1.0.0 - 12/12/2016 139 | - `Raven.config(...)` instead of `new raven.Client(...)` 140 | - `Raven.install()` instead of `client.patchGlobal()` 141 | - The callback to `Raven.captureException` now fires after transmission [See #217] 142 | - Added `captureUnhandledRejections` option for Promise rejections 143 | - Introduced contexts and associated `set/merge/getContext` methods [See #207] 144 | - Added `shouldSendCallback` config option and `set*Callback` methods [See #220] 145 | - Added `intercept()` method [See #225] 146 | - Backwards compatibility was mostly maintained, but lots of stuff was deprecated 147 | - We'll print console messages if you're doing anything the old way 148 | - We'll also print console messages in certain situations where behavior might be surprising, like if no DSN is configured 149 | - You can disable these alerts with `Raven.disableConsoleAlerts();` 150 | 151 | ## 0.12.3 - 11/21/2016 152 | * Replace `node-uuid` dependency with `uuid` [See #236] 153 | 154 | ## 0.12.2 - 11/17/2016 155 | * Add column number to stack frames [See #235] 156 | * Check that `require.main.filename` is defined [See #233] 157 | 158 | ## 0.12.1 - 8/4/2016 159 | * Fix bug where `environment` option was not actually being transmitted to Sentry [See #185] 160 | 161 | ## 0.12.0 - 8/1/2016 162 | * Add `environment` config option and `setRelease` method [See #179] 163 | * No longer passes `process.env` values [See #181] 164 | * Connect/Express middleware now attempts to attach `req.user` as User interface [See #177] 165 | * Use json-stringify-safe to prevent circular refs [See #182] 166 | 167 | ## 0.11.0 - 5/5/2016 168 | * `captureError` renamed to `captureException` to match raven-js (alias exists for backwards compat) 169 | * `parsers.parseError` now coerces Error type to string. [See #155] 170 | 171 | ## 0.10.0 - 1/24/2016 172 | * Now supports global context for extra data, tags, user [See #141] 173 | * Added `setUserContext`, `setExtraContext`, `setTagsContext` 174 | 175 | ## 0.9.0 - 11/23/2015 176 | * Always coerce req.body to string. [See 2061d4efbf269c5e2096f2b7b55f5af2249c4aa7] 177 | * Allow passing options to HTTP transports. [See #123] 178 | * Fixed tests for node 4.0/5.0 179 | * Don't send a body for GET/HEAD requests unless one has been passed. [See 0476a6e9818135b8b258b0be0724c369fe30e3c7] 180 | 181 | ## 0.8.1 - 06/15/2015 182 | * Fixed a missing `domain` import in the Express/Connect middleware [See #120] 183 | 184 | ## 0.8.0 - 06/15/2015 185 | * Drop support for node 0.6 186 | * Remove `SENTRY_SITE` environment variable usage 187 | * Fixed `express deprecated req.host: Use req.hostname instead` warning [See #101] 188 | * Allow passing custom `ca` options for an https request [See #110 #108] 189 | * Use newer API endpoint [See #105] 190 | * Added support for Sentry's new Releases feature 191 | * Update Express/Connect middleware to support domains [See #116] 192 | 193 | ## 0.7.3 - 03/05/2015 194 | * When calling `captureError` without an Error, generate a fake `Error` object to pull stacktrace from. [See #87] 195 | * Prevent `patchGlobal` from causing recursion [See #84] 196 | * Fixed issues arond capturing tags. 197 | * Removed deprecated `site` parameter. 198 | * Allow explicitly declaring the `culprit` [See #107] 199 | * Publicly export out the various parsers [See #111] 200 | * Support for iojs and node 0.12 201 | 202 | ## 0.7.2 - 09/09/2014 203 | * Added `dataCallback` option to Client configuration. See: https://github.com/getsentry/raven-node#pre-processing-data 204 | 205 | ## 0.7.1 - 08/24/2014 206 | * Fixed package.json to not install junk from `optionalDependencies`. TIL `optionalDependencies` are still installed. [See #89] 207 | 208 | ## 0.7.0 - 06/24/2014 209 | * Moved from mattrobenolt/raven-node into getsentry/raven-node 210 | * Bumped to sentry protocol version 5 211 | * Capture all properties off of an Error object and send them along as as `extra` [See #72] 212 | * Better feature detection support for capturing request parameters. Adds support for use in Koa. [See #78 #79] 213 | 214 | ## 0.6.3 - 04/02/2014 215 | * Fix another issue that was breaking when running Raven from the REPL [See #66] 216 | * Add additional meta data on the error callbacks [See #67 #73] 217 | 218 | ## 0.6.2 - 02/14/2014 219 | * Allow overriding the logger name for an individual event 220 | * Update lsmod to not break when running Raven from the REPL 221 | * Added a `raven` bin so you can run `raven test [DSN]` 222 | 223 | ## 0.6.1 - 01/23/2014 224 | * Use lsmod for getting the list of installed modules [See #55] 225 | * Parse cookies on the http request always [See #56] 226 | * Use `stack-trace` to assist in capturing stacks. This should fix compat with the New Relic plugin [See #57] 227 | 228 | ## 0.6.0 - 11/9/2013 229 | * Updated sentry protocol to version 4 (Requires Sentry 6.0+ now) 230 | * Module names now include the full path 231 | * Attach client IP address to env object 232 | 233 | ## 0.5.6 - 11/8/2013 234 | * Include module and function name in stacktrace culprit 235 | 236 | ## 0.5.5 - 11/8/2013 237 | * Only record exceptions for 500 status codes from Connect middleware 238 | 239 | ## 0.5.4 - 10/13/2013 240 | * Fix DSN parser when using Sentry at a non-root URL, thanks @rcoup [See #44] 241 | 242 | ## 0.5.3 - 10/4/2013 243 | * Bump raw-stacktrace version 244 | 245 | ## 0.5.2 - 9/10/2013 246 | * Fix compatibilities with CoffeeScript [Fixes #47] [Fixes #50] 247 | * Doesnt chose on circular references 248 | 249 | ## 0.5.1 - 5/1/2013 250 | * Add support for third party transports, thanks @crankycoder 251 | 252 | ## 0.5.0 - 4/8/2013 253 | * Remove NODE_ENV entirely, fixes many issues since people have different opinions on wtf this means 254 | * Several fixes in collecting a better stack trace, thanks @azylman 255 | * Pass exception through to the patchGlobal callback, thanks @ktmud [See #28] 256 | * Official 0.10 support! 257 | * Other misc things. https://github.com/mattrobenolt/raven-node/compare/v0.4.7...v0.5.0 258 | 259 | ## 0.4.7 - 1/13/2013 260 | * Actually disable when NODE_ENV does not equal 'production' [Fixes #25] 261 | 262 | ## 0.4.6 - 1/13/2013 263 | * Added `platform=node` to payload for Sentry 5.1 264 | 265 | ## 0.4.5 - 12/05/2012 266 | * Resolve `node_modules` path properly [Fixes #23] 267 | 268 | ## 0.4.4 - 11/10/2012 269 | * Prevent 'error' event from bubbling up due to no listeners [See #22] 270 | * Augment 'error' event emitter with an actual Error object [See #22] 271 | 272 | ## 0.4.3 - 10/02/2012 273 | * Allow a callback to be given to `patchGlobal()` [Fixes #19] 274 | * Removed old `patch_global()` alias 275 | 276 | ## 0.4.2 - 9/29/2012 277 | * Added test coverage to `patchGlobal()` 278 | * Quit using my own deprecated `get_ident()` method inside `patchGlobal` 279 | * Send string errors as a normal message to prevent Sentry server from crying [Fixes #18] 280 | 281 | ## 0.4.1 - 9/3/2012 282 | * patchGlobal() was actually broken. :( Thanks @ligthyear [Fixes #17] 283 | 284 | ## 0.4.0 - 8/14/2012 285 | * Silence and disable Raven/Sentry when using a non-existent or falsey DSN value 286 | 287 | ## 0.3.0 - 6/23/2012 288 | * Separate transports out into their own module for portability 289 | * Added UDP transport [Fixes #10] 290 | * Ignore sub-transports, such as gevent+https, raven sees it as just https 291 | 292 | ## 0.2.4 - 6/16/2012 293 | * Added parsing DSNs with non-standard port. [Fixes #4] 294 | * Added BSD license 295 | 296 | ## 0.2.3 - 3/30/2012 297 | * Prevent any potentially odd stack traces from causing Raven to crash. [Fixes #2] 298 | 299 | ## 0.2.2 - 3/22/2012 300 | * raven.Client now emits `logged` and `error` events. 301 | 302 | ## 0.2.1 - 3/22/2012 303 | * Fixed connect/express middleware, thanks Almad! 304 | 305 | ## 0.2.0 - 3/18/2012 306 | * Renamed all methods to follow `client.capture*()` pattern. (Sorry if you were already using it!) 307 | * All `npm` installed modules are shoved into Sentry for debugging 308 | * Toggle actual sending based on `NODE_ENV` variable. Check README for information. 309 | * Fixes for more types of stack traces. 310 | * Added `client.captureQuery()` 311 | * Support for `SENTRY_DSN`, `SENTRY_NAME`, and `SENTRY_SITE` enviornment variables 312 | * More test coverage 313 | 314 | ## 0.1.0 - 3/17/2012 315 | * Initial release 316 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Sentry (https://sentry.io) and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | develop: update-submodules 2 | npm install . 3 | 4 | update-submodules: 5 | git submodule init 6 | git submodule update 7 | 8 | docs: 9 | cd docs; $(MAKE) html 10 | 11 | docs-live: 12 | while true; do \ 13 | sleep 2; \ 14 | $(MAKE) docs; \ 15 | done 16 | 17 | clean: 18 | rm -rf docs/html 19 | 20 | .PHONY: develop update-submodules docs docs-live clean 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # We are in the process of unifying all our JavaScript based SDKs. Because of that, `raven-node` has been moved into the main [Sentry JS SDK](https://github.com/getsentry/raven-js/) mono-repo and this repo has been archived. 2 | 3 | Please report any issues and requests there, as this repo is read-only. 4 | 5 | --- 6 | 7 |

8 | 9 | 10 | 11 |
12 |

Raven-node - Sentry SDK for Node.js

13 |

14 | 15 | [![Build Status](https://travis-ci.org/getsentry/raven-node.svg?branch=master)](https://travis-ci.org/getsentry/raven-node) 16 | 17 | `raven-node` supports only LTS versions of Node, therefore currently required release is `>= v4.0.0`. 18 | The last known working version for `v0.10` and `v0.12` is `raven-node@2.1.2`. 19 | Please use this version if you need to support those releases of Node. 20 | 21 | To see up-to-date Node.js LTS Release Schedule, go to https://github.com/nodejs/LTS. 22 | 23 | ## Resources 24 | 25 | * [Documentation](https://docs.getsentry.com/hosted/clients/node/) 26 | * [Bug Tracker](https://github.com/getsentry/raven-node/issues) 27 | * [IRC](irc://chat.freenode.net/sentry) (chat.freenode.net, #sentry) 28 | -------------------------------------------------------------------------------- /bin/raven: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var os = require('os'); 4 | var raven = require('../'); 5 | 6 | function usage() { 7 | var path = require('path'); 8 | console.log('usage:', path.basename(process.argv[1]), 'test [SENTRY_DSN]'); 9 | } 10 | 11 | if (process.argv[2] !== 'test') { 12 | usage(); 13 | process.exit(1); 14 | } 15 | 16 | var dsn = process.argv.slice(3).join(' ') || process.env.SENTRY_DSN; 17 | if (!dsn) { 18 | usage(); 19 | console.log(); 20 | console.log('Error: No configuration detected!'); 21 | console.log('You must either pass a DSN to the command or set the SENTRY_DSN environment variable.'); 22 | process.exit(1); 23 | } 24 | 25 | console.log('Using DSN configuration:'); 26 | console.log(' ', dsn); 27 | console.log(); 28 | 29 | var client = new raven.Client(dsn); 30 | 31 | console.log('Sending a test message...'); 32 | 33 | client.on('logged', function(result) { 34 | console.log('success!'); 35 | console.log('Event ID was', client.getIdent(result)); 36 | }); 37 | client.on('error', function(err) { 38 | console.log('error!'); 39 | throw err; 40 | }); 41 | 42 | try { 43 | test 44 | } catch (ex) { 45 | client.captureException(ex, { 46 | message: 'This is a test message generated using ``raven test``', 47 | level: 'info', 48 | logger: 'sentry.test', 49 | transaction: 'bin:raven at main', 50 | request: { 51 | method: 'GET', 52 | url: 'http://example.com' 53 | }, 54 | extra: { 55 | user: process.getuid && process.getuid(), 56 | loadavg: os.loadavg() 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/raven-node.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/raven-node.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/raven-node" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/raven-node" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/coffeescript.rst: -------------------------------------------------------------------------------- 1 | CoffeeScript 2 | ============ 3 | 4 | In order to use raven-node with coffee-script or another library which overwrites 5 | Error.prepareStackTrace you might run into the exception "Traceback does not 6 | support Error.prepareStackTrace being defined already." 7 | 8 | In order to not have raven-node (and the underlying raw-stacktrace library) require 9 | Traceback you can pass your own stackFunction in the options. For example: 10 | 11 | .. code-block:: coffeescript 12 | 13 | client = new raven.Client('___PUBLIC_DSN___', { 14 | stackFunction: {{ Your stack function }} 15 | }); 16 | 17 | 18 | So for example: 19 | 20 | .. code-block:: coffeescript 21 | 22 | client = new raven.Client('___PUBLIC_DSN___', { 23 | stackFunction: Error.prepareStackTrace 24 | }); 25 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # raven-node documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jan 21 21:04:27 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os, datetime 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'raven-node' 44 | copyright = u'%s, Sentry' % datetime.date.today().year 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | 51 | import json 52 | # The full version, including alpha/beta/rc tags. 53 | release = json.load(open('../package.json'))['version'] 54 | # The short X.Y version. 55 | version = release.rsplit('.', 1)[0] 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'default' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'raven-nodedoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | latex_elements = { 175 | # The paper size ('letterpaper' or 'a4paper'). 176 | #'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'raven-node.tex', u'raven-node Documentation', 189 | u'Matt Robenolt', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'raven-node', u'raven-node Documentation', 219 | [u'Sentry'], 1) 220 | ] 221 | 222 | # If true, show URL addresses after external links. 223 | #man_show_urls = False 224 | 225 | 226 | # -- Options for Texinfo output ------------------------------------------------ 227 | 228 | # Grouping the document tree into Texinfo files. List of tuples 229 | # (source start file, target name, title, author, 230 | # dir menu entry, description, category) 231 | texinfo_documents = [ 232 | ('index', 'raven-node', u'raven-node Documentation', 233 | u'Sentry', 'raven-node', 'One line description of project.', 234 | 'Miscellaneous'), 235 | ] 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #texinfo_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #texinfo_domain_indices = True 242 | 243 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 244 | #texinfo_show_urls = 'footnote' 245 | 246 | if os.environ.get('SENTRY_FEDERATED_DOCS') != '1': 247 | sys.path.insert(0, os.path.abspath('_sentryext')) 248 | import sentryext 249 | sentryext.activate() 250 | -------------------------------------------------------------------------------- /docs/config.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | To get started, you need to configure Raven to use your Sentry DSN: 5 | 6 | .. sourcecode:: javascript 7 | 8 | var Raven = require('raven'); 9 | Raven.config('___PUBLIC_DSN___').install(); 10 | 11 | At this point, Raven is ready to capture any uncaught exceptions. 12 | 13 | Note that the ``install`` method can optionally take a callback function that is invoked if a fatal, non-recoverable error occurs. 14 | You can use this callback to perform any cleanup that should occur before the Node process exits. 15 | 16 | .. sourcecode:: javascript 17 | 18 | Raven.config('___PUBLIC_DSN___').install(function (err, initialErr, eventId) { 19 | console.error(err); 20 | process.exit(1); 21 | }); 22 | 23 | Optional settings 24 | ----------------- 25 | 26 | ``Raven.config()`` can optionally be passed an additional argument for extra configuration: 27 | 28 | .. sourcecode:: javascript 29 | 30 | Raven.config('___PUBLIC_DSN___', { 31 | release: '1.3.0' 32 | }).install(); 33 | 34 | Those configuration options are documented below: 35 | 36 | .. describe:: logger 37 | 38 | The name of the logger used by Sentry. 39 | 40 | .. code-block:: javascript 41 | 42 | { 43 | logger: 'default' 44 | } 45 | 46 | .. describe:: name 47 | 48 | Set the server name for the client to use. Default: ``require('os').hostname()`` 49 | Optionally, use ``SENTRY_NAME`` environment variable. 50 | 51 | .. code-block:: javascript 52 | 53 | { 54 | name: 'primary' 55 | } 56 | 57 | .. describe:: release 58 | 59 | Track the version of your application in Sentry. 60 | Optionally, use ``SENTRY_RELEASE`` environment variable. 61 | 62 | .. code-block:: javascript 63 | 64 | { 65 | release: '721e41770371db95eee98ca2707686226b993eda' 66 | } 67 | 68 | This is usually a Git SHA hash, which can be obtained using various npm packages, e.g. 69 | 70 | .. code-block:: javascript 71 | 72 | var git = require('git-rev-sync'); 73 | 74 | { 75 | // this will return 40 characters long hash 76 | // eg. '75bf4eea9aa1a7fd6505d0d0aa43105feafa92ef' 77 | release: git.long() 78 | } 79 | 80 | .. describe:: environment 81 | 82 | Track the environment name inside Sentry. 83 | Optionally, use ``SENTRY_ENVIRONMENT`` environment variable. 84 | 85 | .. code-block:: javascript 86 | 87 | { 88 | environment: 'staging' 89 | } 90 | 91 | .. describe:: tags 92 | 93 | Additional tags to assign to each event. 94 | 95 | .. code-block:: javascript 96 | 97 | { 98 | tags: {git_commit: 'c0deb10c4'} 99 | } 100 | 101 | .. describe:: extra 102 | 103 | Arbitrary data to associate with the event. 104 | 105 | .. code-block:: javascript 106 | 107 | { 108 | extra: {planet: {name: 'Earth'}} 109 | } 110 | 111 | .. describe:: parseUser 112 | 113 | Controls how Raven tries to parse user context when parsing a request object. 114 | 115 | An array of strings will serve as a whitelist for fields to grab from ``req.user``. 116 | ``true`` will collect all keys from ``req.user``. ``false`` will collect nothing. 117 | 118 | Defaults to ``['id', 'username', 'email']``. 119 | 120 | Alternatively, a function can be provided for fully custom parsing: 121 | 122 | .. code-block:: javascript 123 | 124 | { 125 | parseUser: function (req) { 126 | // custom user parsing logic 127 | return { 128 | username: req.specialUserField.username, 129 | id: req.specialUserField.getId() 130 | }; 131 | } 132 | } 133 | 134 | .. describe:: sampleRate 135 | 136 | A sampling rate to apply to events. A value of 0.0 will send no events, 137 | and a value of 1.0 will send all events (default). 138 | 139 | .. code-block:: javascript 140 | 141 | { 142 | sampleRate: 0.5 // send 50% of events, drop the other half 143 | } 144 | 145 | .. describe:: sendTimeout 146 | 147 | The time to wait to connect to the server or receive a response when capturing an exception, in seconds. 148 | 149 | If it takes longer, the transport considers it a failed request and will pass back a timeout error. 150 | 151 | Defaults to 1 second. Make it longer if you run into timeout problems when sending exceptions to Sentry. 152 | 153 | .. code-block:: javascript 154 | 155 | { 156 | sendTimeout: 5 // wait 5 seconds before considering the capture to have failed 157 | } 158 | 159 | .. describe:: dataCallback 160 | 161 | A function that allows mutation of the data payload right before being 162 | sent to Sentry. 163 | 164 | .. code-block:: javascript 165 | 166 | { 167 | dataCallback: function(data) { 168 | // add a user context 169 | data.user = { 170 | id: 1337, 171 | name: 'janedoe', 172 | email: 'janedoe@example.com' 173 | }; 174 | return data; 175 | } 176 | } 177 | 178 | .. describe:: shouldSendCallback 179 | 180 | A callback function that allows you to apply your own filters to determine if the event should be sent to Sentry. 181 | 182 | .. code-block:: javascript 183 | 184 | { 185 | shouldSendCallback: function (data) { 186 | // randomly omit half of events 187 | return Math.random() > 0.5; 188 | } 189 | } 190 | 191 | .. describe:: autoBreadcrumbs 192 | 193 | Enables/disables automatic collection of breadcrumbs. Possible values are: 194 | 195 | * `false` - all automatic breadcrumb collection disabled (default) 196 | * `true` - all automatic breadcrumb collection enabled 197 | * A dictionary of individual breadcrumb types that can be enabled/disabled: 198 | 199 | .. code-block:: javascript 200 | 201 | autoBreadcrumbs: { 202 | 'console': false, // console logging 203 | 'http': true, // http and https requests 204 | } 205 | 206 | .. describe:: maxBreadcrumbs 207 | 208 | Raven captures up to 30 breadcrumb entries by default. You can increase this to 209 | be as high as 100, or reduce it if you find 30 is too noisy, by setting `maxBreadcrumbs`. 210 | 211 | Note that in very high-concurrency situations where you might have a large number of 212 | long-lived contexts each with a large number of associated breadcrumbs, there is potential 213 | for significant memory usage. 10,000 contexts with 10kB of breadcrumb data each will use 214 | around 120mB of memory. Most applications will be nowhere close to either of these numbers, 215 | but if yours might be, you can use the `maxBreadcrumbs` parameter to limit the amount of 216 | breadcrumb data each context will keep around. 217 | 218 | .. describe:: transport 219 | 220 | Override the default HTTP data transport handler. 221 | 222 | .. code-block:: javascript 223 | 224 | { 225 | transport: function (options) { 226 | // send data 227 | } 228 | } 229 | 230 | Please see the raven-node source code to see `how transports are implemented 231 | `__. 232 | 233 | .. describe:: maxReqQueueCount 234 | 235 | Controls how many requests can be maximally queued before bailing out and emitting an error. Defaults to `100`. 236 | 237 | .. describe:: stacktrace 238 | 239 | Attach stack trace to `captureMessage` calls by generatic "synthetic" error object and extracting all frames. 240 | 241 | Environment Variables 242 | --------------------- 243 | 244 | .. describe:: SENTRY_DSN 245 | 246 | Optionally declare the DSN to use for the client through the environment. Initializing the client in your app won't require setting the DSN. 247 | 248 | .. describe:: SENTRY_NAME 249 | 250 | Optionally set the server name for the client to use. 251 | 252 | .. describe:: SENTRY_RELEASE 253 | 254 | Optionally set the application release version for the client to use, this is usually a Git SHA hash. 255 | 256 | .. describe:: SENTRY_ENVIRONMENT 257 | 258 | Optionally set the environment name, e.g. "staging", "production". Sentry will default to the value of `NODE_ENV`, if present. 259 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. sentry:edition:: self 2 | 3 | raven-node 4 | ========== 5 | 6 | .. sentry:edition:: hosted, on-premise 7 | 8 | .. class:: platform-node 9 | 10 | Node.js 11 | ======= 12 | 13 | raven-node is the official Node.js client for Sentry. 14 | 15 | **Note**: If you're using JavaScript in the browser, you'll need 16 | `raven-js `_. 17 | 18 | Installation 19 | ------------ 20 | 21 | Raven is distributed via ``npm``: 22 | 23 | .. code-block:: bash 24 | 25 | $ npm install raven --save 26 | 27 | Configuring the Client 28 | ---------------------- 29 | 30 | Next you need to initialize the Raven client and configure it to use your `Sentry DSN 31 | `_: 32 | 33 | .. code-block:: javascript 34 | 35 | var Raven = require('raven'); 36 | Raven.config('___PUBLIC_DSN___').install(); 37 | 38 | At this point, Raven is set up to capture and report any uncaught exceptions. 39 | 40 | You can optionally pass an object of configuration options as the 2nd argument to `Raven.config`. For 41 | more information, see :doc:`config`. 42 | 43 | Reporting Errors 44 | ---------------- 45 | 46 | Raven's ``install`` method sets up a global handler to automatically capture any uncaught exceptions. You can also report errors manually with ``try...catch`` and 47 | a call to ``captureException``: 48 | 49 | .. code-block:: javascript 50 | 51 | try { 52 | doSomething(a[0]); 53 | } catch (e) { 54 | Raven.captureException(e); 55 | } 56 | 57 | You can also use ``wrap`` and ``context`` to have Raven wrap a function and automatically capture any exceptions it throws: 58 | 59 | .. code-block:: javascript 60 | 61 | Raven.context(function () { 62 | doSomething(a[0]); 63 | }); 64 | 65 | For more information on reporting errors, see :doc:`usage`. 66 | 67 | Adding Context 68 | -------------- 69 | 70 | Code run via ``wrap`` or ``context`` has an associated set of context data, and Raven provides methods for managing that data. 71 | 72 | You'll most commonly use this to associate the current user with an exception: 73 | 74 | .. code-block:: javascript 75 | 76 | Raven.context(function () { 77 | Raven.setContext({ 78 | user: { 79 | email: 'matt@example.com', 80 | id: '123' 81 | } 82 | }); 83 | // errors thrown here will be associated with matt 84 | }); 85 | // errors thrown here will not be associated with matt 86 | 87 | This can also be used to set ``tags`` and ``extra`` keys for associated tags and extra data. 88 | 89 | You can update the context data with ``mergeContext`` or retrieve it with ``getContext``. When an exception is captured by a wrapper, the current context state will be passed as options to ``captureException``. 90 | 91 | See :ref:`raven-node-additional-context` for more. 92 | 93 | Breadcrumbs 94 | ----------- 95 | 96 | Breadcrumbs are records of server and application lifecycle events that can be helpful in understanding the state of the application leading up to a crash. 97 | 98 | We can capture breadcrumbs and associate them with a context, and then send them along with any errors captured from that context: 99 | 100 | .. code-block:: javascript 101 | 102 | Raven.context(function () { 103 | Raven.captureBreadcrumb({ 104 | message: 'Received payment confirmation', 105 | category: 'payment', 106 | data: { 107 | amount: 312, 108 | } 109 | }); 110 | // errors thrown here will have breadcrumb attached 111 | }); 112 | 113 | Raven can be configured to automatically capture breadcrubs for certain events including: 114 | 115 | * http/https requests 116 | * console log statements 117 | * postgres queries 118 | 119 | For more information, see :ref:`raven-recording-breadcrumbs`. 120 | 121 | Dealing with Minified Source Code 122 | --------------------------------- 123 | 124 | Raven and Sentry support `Source Maps 125 | `_. If 126 | you provide source maps in addition to your minified files that data 127 | becomes available in Sentry. For more information see 128 | :ref:`raven-node-sourcemaps`. 129 | 130 | Middleware and Integrations 131 | --------------------------- 132 | 133 | If you're using Node.js with a web server framework/library like Connect, Express, or Koa, it is recommended 134 | to configure one of Raven's server middleware integrations. See :doc:`integrations/index`. 135 | 136 | Deep Dive 137 | --------- 138 | 139 | For more detailed information about how to get most out of Raven there 140 | is additional documentation available that covers all the rest: 141 | 142 | .. toctree:: 143 | :maxdepth: 2 144 | :titlesonly: 145 | 146 | config 147 | usage 148 | integrations/index 149 | sourcemaps 150 | typescript 151 | coffeescript 152 | 153 | Resources: 154 | 155 | * `Bug Tracker `_ 156 | * `Github Project `_ 157 | -------------------------------------------------------------------------------- /docs/integrations/connect.rst: -------------------------------------------------------------------------------- 1 | Connect 2 | ======= 3 | 4 | .. code-block:: javascript 5 | 6 | var connect = require('connect'); 7 | var Raven = require('raven'); 8 | 9 | // Must configure Raven before doing anything else with it 10 | Raven.config('___PUBLIC_DSN___').install(); 11 | 12 | function mainHandler(req, res) { 13 | throw new Error('Broke!'); 14 | } 15 | 16 | function onError(err, req, res, next) { 17 | // The error id is attached to `res.sentry` to be returned 18 | // and optionally displayed to the user for support. 19 | res.statusCode = 500; 20 | res.end(res.sentry + '\n'); 21 | } 22 | 23 | connect( 24 | // The request handler be the first item 25 | Raven.requestHandler(), 26 | 27 | connect.bodyParser(), 28 | connect.cookieParser(), 29 | mainHandler, 30 | 31 | // The error handler must be before any other error middleware 32 | Raven.errorHandler(), 33 | 34 | // Optional fallthrough error handler 35 | onError, 36 | ).listen(3000); 37 | -------------------------------------------------------------------------------- /docs/integrations/express.rst: -------------------------------------------------------------------------------- 1 | Express 2 | ======= 3 | 4 | .. code-block:: javascript 5 | 6 | var app = require('express')(); 7 | var Raven = require('raven'); 8 | 9 | // Must configure Raven before doing anything else with it 10 | Raven.config('__DSN__').install(); 11 | 12 | // The request handler must be the first middleware on the app 13 | app.use(Raven.requestHandler()); 14 | 15 | app.get('/', function mainHandler(req, res) { 16 | throw new Error('Broke!'); 17 | }); 18 | 19 | // The error handler must be before any other error middleware 20 | app.use(Raven.errorHandler()); 21 | 22 | // Optional fallthrough error handler 23 | app.use(function onError(err, req, res, next) { 24 | // The error id is attached to `res.sentry` to be returned 25 | // and optionally displayed to the user for support. 26 | res.statusCode = 500; 27 | res.end(res.sentry + '\n'); 28 | }); 29 | 30 | app.listen(3000); 31 | -------------------------------------------------------------------------------- /docs/integrations/index.rst: -------------------------------------------------------------------------------- 1 | Integrations 2 | ============ 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | connect 8 | express 9 | koa 10 | loopback 11 | sails 12 | -------------------------------------------------------------------------------- /docs/integrations/koa.rst: -------------------------------------------------------------------------------- 1 | Koa 2 | === 3 | 4 | .. code-block:: javascript 5 | 6 | var koa = require('koa'); 7 | var Raven = require('raven'); 8 | 9 | var app = koa(); 10 | Raven.config('___PUBLIC_DSN___').install(); 11 | 12 | app.on('error', function (err) { 13 | Raven.captureException(err, function (err, eventId) { 14 | console.log('Reported error ' + eventId); 15 | }); 16 | }); 17 | 18 | app.listen(3000); 19 | -------------------------------------------------------------------------------- /docs/integrations/loopback.rst: -------------------------------------------------------------------------------- 1 | Loopback 2 | ======== 3 | 4 | If you're using Loopback 2.x LTS, make sure 5 | you've migrated to `strong-error-handler 6 | `_, otherwise no 7 | errors will get to ``raven-node``. 8 | 9 | Configure ``raven-node`` as early as possible: 10 | 11 | .. code-block:: javascript 12 | 13 | // server/server.js 14 | 15 | const Raven = require('raven'); 16 | Raven.config('__DSN__').install(); 17 | 18 | Add ``Raven.errorHandler`` as a Loopback middleware: 19 | 20 | .. code-block:: json 21 | 22 | // server/middleware.json 23 | 24 | "final:after": { 25 | "raven#errorHandler": {}, 26 | "strong-error-handler": { 27 | "debug": false, 28 | "log": false 29 | } 30 | } 31 | 32 | You're all set! 33 | -------------------------------------------------------------------------------- /docs/integrations/sails.rst: -------------------------------------------------------------------------------- 1 | Sails 2 | ===== 3 | 4 | .. code-block:: javascript 5 | 6 | // config/http.js 7 | 8 | var Raven = require('raven'); 9 | Raven.config('__DSN__').install(); 10 | 11 | module.exports.http = { 12 | middleware: { 13 | // Raven's handlers has to be added as a keys to http.middleware config object 14 | requestHandler: Raven.requestHandler(), 15 | errorHandler: Raven.errorHandler(), 16 | 17 | // And they have to be added in a correct order to middlewares list 18 | order: [ 19 | // The request handler must be the very first one 20 | 'requestHandler', 21 | // ...more middlewares 22 | 'router', 23 | // The error handler must be after router, but before any other error middleware 24 | 'errorHandler', 25 | /// ...remaining middlewares 26 | ] 27 | } 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\raven-node.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\raven-node.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/sentry-doc-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "support_level": "production", 3 | "platforms": { 4 | "node": { 5 | "name": "Node.js", 6 | "type": "language", 7 | "doc_link": "", 8 | "wizard": [ 9 | "index#installation", 10 | "index#configuring-the-client", 11 | "index#reporting-errors" 12 | ] 13 | }, 14 | "node.express": { 15 | "name": "Express", 16 | "type": "framework", 17 | "doc_link": "integrations/express/", 18 | "wizard": [ 19 | "index#installation", 20 | "integrations/express#express" 21 | ] 22 | }, 23 | "node.koa": { 24 | "name": "Koa", 25 | "type": "framework", 26 | "doc_link": "integrations/koa/", 27 | "wizard": [ 28 | "index#installation", 29 | "integrations/koa#koa" 30 | ] 31 | }, 32 | "node.connect": { 33 | "name": "Connect", 34 | "type": "framework", 35 | "doc_link": "integrations/connect/", 36 | "wizard": [ 37 | "index#installation", 38 | "integrations/connect#connect" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/sourcemaps.rst: -------------------------------------------------------------------------------- 1 | .. _raven-node-sourcemaps: 2 | 3 | Source Maps 4 | =========== 5 | 6 | Sentry supports un-minifying JavaScript via `Source Maps 7 | `_. This lets you 8 | view source code context obtained from stack traces in their original untransformed form, which is particularly useful for debugging minified code (e.g. UglifyJS), or transpiled code from a higher-level 9 | language (e.g. TypeScript, ES6). 10 | 11 | Generating a Source Map 12 | ----------------------- 13 | 14 | Most modern JavaScript transpilers support source maps. Below are instructions for some common tools. 15 | 16 | Webpack 17 | ~~~~~~~ 18 | 19 | Webpack is a powerful build tool that resolves and bundles your JavaScript modules into files fit for running in the 20 | browser. It also supports many different "loaders" which can convert higher-level languages like TypeScript and 21 | ES6/ES2015 into browser-compatible JavaScript. 22 | 23 | Webpack can be configured to output source maps by editing ``webpack.config.js``. 24 | 25 | :: 26 | 27 | module.exports = { 28 | // ... other config above ... 29 | target: 'node', 30 | devtool: 'source-map', 31 | entry: { 32 | "app": 'src/app.js' 33 | }, 34 | output: { 35 | path: path.join(__dirname, 'dist'), 36 | filename: '[name].js' 37 | } 38 | }; 39 | 40 | 41 | Making Source Maps Available to Sentry 42 | -------------------------------------- 43 | 44 | Source maps for Node.js projects should be uploaded directly to Sentry. 45 | 46 | Uploading Source Maps to Sentry 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | Sentry provides an abstraction called **Releases** which you can attach source artifacts to. 50 | The release API is intended to allow you to store source files (and sourcemaps) within Sentry. 51 | 52 | It can be easily done with a help of the ``sentry-webpack-plugin``, which internally uses our Sentry CLI. 53 | 54 | * Start by creating a new authentication token under **[Account] > API**. 55 | * Ensure you you have ``project:write`` selected under scopes. 56 | * Install ``@sentry/webpack-plugin`` using ``npm`` 57 | * Create ``.sentryclirc`` file with necessary config (see Sentry Webpack Plugin docs below) 58 | * Update your ``webpack.config.json`` 59 | 60 | :: 61 | 62 | const SentryPlugin = require('@sentry/webpack-plugin'); 63 | 64 | module.exports = { 65 | // ... other config above ... 66 | plugins: [ 67 | new SentryPlugin({ 68 | release: process.env.RELEASE, 69 | include: './dist', 70 | ignore: ['node_modules', 'webpack.config.js'], 71 | }) 72 | ] 73 | }; 74 | 75 | 76 | You can take a look at `Sentry Webpack Plugin documentation `_ 77 | for more information on how to configure the plugin. 78 | 79 | Additionally, you'll need to configure the client to send the ``release``: 80 | 81 | .. code-block:: javascript 82 | 83 | Raven.config('your-dsn', { 84 | release: process.env.RELEASE 85 | }); 86 | 87 | Note: You dont *have* to use `RELEASE` environment variables. You can provide them in any way you want. 88 | 89 | Additional information can be found in the `Releases API documentation 90 | `_. 91 | 92 | 93 | Updating Raven configuration to support Source Maps 94 | --------------------------------------------------- 95 | 96 | In order for Sentry to understand how to resolve errors sources, we need to modify the data we send. 97 | Because Source Maps support is still in experimental phase, this task is not integrated into the core library itself. 98 | To do that however, we can normalize all urls using ``dataCallback`` method: 99 | 100 | .. code-block:: javascript 101 | 102 | var path = require('path'); 103 | 104 | Raven.config('your-dsn', { 105 | // the rest of configuration 106 | 107 | dataCallback: function (data) { 108 | var stacktrace = data.exception && data.exception[0].stacktrace; 109 | 110 | if (stacktrace && stacktrace.frames) { 111 | stacktrace.frames.forEach(function(frame) { 112 | if (frame.filename.startsWith('/')) { 113 | frame.filename = 'app:///' + path.basename(frame.filename); 114 | } 115 | }); 116 | } 117 | 118 | return data; 119 | } 120 | ).install(); 121 | 122 | There's one very important thing to note here. This config assumes, that you'll bundle your application into a single file. 123 | That's why we are using `path.basename` to get the filename. 124 | 125 | If you are not doing this, eg. you are using TypeScript and upload all your compiled files separately to the server, 126 | then we need to be a little smarter about this. 127 | Please refer to `TypeScript usage docs `_ to see a more complex and detailed example. 128 | -------------------------------------------------------------------------------- /docs/typescript.rst: -------------------------------------------------------------------------------- 1 | .. _raven-node-typescript: 2 | 3 | Source Maps 4 | =========== 5 | 6 | Please read `Source Maps docs `_ first to learn how to configure Raven SDK, upload artifacts to our servers or use Webpack (if you're willing to use `ts-loader` for your TypeScript compilation). 7 | 8 | Using Raven and Source Maps with TypeScript unfortunatelly requires slightly more configuration. 9 | 10 | There are two main reasons for this: 11 | 12 | 1) TypeScript compiles and outputs all files separately 13 | 2) SourceRoot is by default set to, well, source directory, which would require uploading artifacts from 2 separate directories and modification of source maps themselves 14 | 15 | We can still make it work with two additional steps, so let's do this. 16 | 17 | The first one is configuring TypeScript compiler in a way, in which we'll override `sourceRoot` and merge original sources with corresponding maps. 18 | The former is not required, but it'll help Sentry display correct filepaths, eg. `/lib/utils/helper.ts` instead of a full one like `/Users/Sentry/Projects/TSExample/lib/utils/helper.ts`. 19 | You can skip this option if you're fine with such a long names. 20 | 21 | Assuming you already have a `tsconfig.json` file similar to this: 22 | 23 | :: 24 | 25 | { 26 | "compilerOptions": { 27 | "target": "es6", 28 | "module": "commonjs", 29 | "allowJs": true, 30 | "moduleResolution": "node", 31 | "outDir": "dist" 32 | }, 33 | "include": [ 34 | "./src/**/*" 35 | ] 36 | } 37 | 38 | create a new one called `tsconfig.production.json` and paste the snippet below: 39 | 40 | :: 41 | 42 | { 43 | "extends": "./tsconfig", 44 | "compilerOptions": { 45 | "sourceMap": true, 46 | "inlineSources": true, 47 | "sourceRoot": "/" 48 | } 49 | } 50 | 51 | From now on, when you want to run the production build, that'll be uploaded you specify this very config, eg. `tsc -p tsconfig.production.json`. 52 | This will create necessary source maps and attach original sources to them instead of making us to upload them and modify source paths in our maps by hand. 53 | 54 | The second step is changing events frames, so that Sentry can link stacktraces with correct source files. 55 | 56 | This can be done using `dataCallback`, in a very similar manner as we do with a single entrypoint described in Source Maps docs, with one, very important difference. 57 | Instead of using `basename`, we have to somehow detect and pass the root directory of our project. 58 | 59 | Unfortunately, Node is very fragile in that manner and doesn't have a very reliable way to do this. 60 | The easiest and the most reliable way we found, is to store the `__dirname` or `process.cwd()` in the global variable and using it in other places of your app. 61 | This *has to be done* as the first thing in your code and from the entrypoint, otherwise the path will be incorrect. 62 | 63 | If you want, you can set this value by hand to something like `/var/www/html/some-app` if you can get this from some external source or you know it won't ever change. 64 | 65 | This can also be achieved by creating a separate file called `root.js` or similar that'll be placed in the same place as your entrypoint and exporting obtained value 66 | instead of exporting it globally. 67 | 68 | .. code-block:: javascript 69 | 70 | // index.js 71 | 72 | // This allows TypeScript to detect our global value 73 | declare global { 74 | namespace NodeJS { 75 | interface Global { 76 | __rootdir__: string; 77 | } 78 | } 79 | } 80 | 81 | global.__rootdir__ = __dirname || process.cwd(); 82 | 83 | .. code-block:: javascript 84 | 85 | import * as path from 'path'; 86 | const root = global.__rootdir__; 87 | 88 | Raven.config('your-dsn', { 89 | // the rest of configuration 90 | 91 | dataCallback: function (data) { 92 | var stacktrace = data.exception && data.exception[0].stacktrace; 93 | 94 | if (stacktrace && stacktrace.frames) { 95 | stacktrace.frames.forEach(function(frame) { 96 | if (frame.filename.startsWith('/')) { 97 | frame.filename = "app:///" + path.relative(root, frame.filename); 98 | } 99 | }); 100 | } 101 | 102 | return data; 103 | } 104 | ).install(); 105 | 106 | This config should be enough to make everything work and use TypeScript with Node and still being able to digest all original sources by Sentry. 107 | 108 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Capturing Errors 5 | ---------------- 6 | 7 | You can use ``captureException`` to manually report errors: 8 | 9 | .. code-block:: javascript 10 | 11 | try { 12 | throw new Error(); 13 | } catch (e) { 14 | // You can get eventId either as the synchronous return value, or via the callback 15 | var eventId = Raven.captureException(e, function (sendErr, eventId) { 16 | // This callback fires once the report has been sent to Sentry 17 | if (sendErr) { 18 | console.error('Failed to send captured exception to Sentry'); 19 | } else { 20 | console.log('Captured exception and send to Sentry successfully'); 21 | } 22 | }); 23 | } 24 | 25 | The recommended usage pattern, though, is to run your entire program inside a Raven context: 26 | 27 | .. code-block:: javascript 28 | 29 | var Raven = require('raven'); 30 | 31 | Raven.config('___PUBLIC_DSN___').install(); 32 | Raven.context(function () { 33 | // all your stuff goes here 34 | }); 35 | 36 | Raven will automatically catch and report any unhandled errors originating inside this function 37 | (or anything it calls, etc), so you don't have to manually `captureException` everywhere. This 38 | also gives your code access to context methods. See below for more on contexts. 39 | 40 | .. _raven-node-additional-context: 41 | 42 | context/wrap 43 | ------------ 44 | 45 | ``Raven.context`` allows you to wrap any function to be immediately 46 | executed. Behind the scenes, this uses `domains `__ to wrap, catch, and record any exceptions originating from the function. 47 | 48 | .. code-block:: javascript 49 | 50 | Raven.context(function () { 51 | doSomething(a[0]) 52 | }); 53 | 54 | ``Raven.wrap`` wraps a function in a similar way to ``Raven.context``, but 55 | instead of invoking the function, it returns another function. This is 56 | especially useful when passing around a callback. 57 | 58 | .. code-block:: javascript 59 | 60 | var doIt = function () { 61 | // doing cool stuff 62 | } 63 | 64 | setTimeout(Raven.wrap(doIt), 1000) 65 | 66 | We refer to code wrapped via ``Raven.context`` or ``Raven.wrap`` as being inside a context. Code inside a context 67 | has access to the ``setContext``, ``mergeContext``, and ``getContext`` methods for associating data with that context. 68 | 69 | .. code-block:: javascript 70 | 71 | Raven.setContext({ 72 | user: { 73 | username: 'lewis' 74 | } 75 | }); 76 | 77 | Raven.mergeContext({ 78 | tags: { 79 | component: 'api' 80 | } 81 | }); 82 | 83 | console.log(Raven.getContext()); 84 | // { user: ..., tags: ... } 85 | 86 | A context most commonly corresponds to a request; if you're using our Express middleware, each request is automatically 87 | wrapped in its own context, so you can use Raven's context methods from inside any of your middleware or handlers. 88 | A context might also correspond to, say, a connection lifecycle or a job being handled in a worker process. 89 | 90 | Notable keys that you might set include ``user``, ``tags``, and ``extra``. 91 | These types of extra context data are detailed more under :ref:`raven-node-additional-data`. 92 | 93 | Since ``domains`` are not supported in native ``Promise`` until Node.js v8, version ``>=8.0.0`` is required if you want to have an access to the context in ``Promise`` rejections. 94 | When older version of Node.js is used, it'll just be skipped and globally set context will be used instead. 95 | Context for regular error handlers and ``context/wrap`` calls is working in every version, including v0.x. 96 | 97 | Tracking Users 98 | -------------- 99 | 100 | While a user is logged in, you can tell Sentry to associate errors with 101 | user data. This is really just a particular use of the context methods described above: 102 | 103 | .. code-block:: javascript 104 | 105 | Raven.setContext({ 106 | user: { 107 | email: 'matt@example.com', 108 | id: '123' 109 | } 110 | }); 111 | 112 | This data is then included with any errors or messages, allowing you to see which users are affected by problems. 113 | 114 | Capturing Messages 115 | ------------------ 116 | 117 | .. code-block:: javascript 118 | 119 | client.captureMessage('Broken!', function (err, eventId) { 120 | // The message has now been sent to Sentry 121 | }); 122 | 123 | 124 | .. _raven-node-additional-data: 125 | 126 | Additional Data 127 | --------------- 128 | 129 | All optional attributes are passed as part of the options to ``captureException`` and ``captureMessage``. 130 | 131 | .. describe:: user 132 | 133 | User context for this event. Must be a mapping. Children can be any native JSON type. 134 | 135 | .. code-block:: javascript 136 | 137 | { 138 | user: { name: 'matt' } 139 | } 140 | 141 | If you're inside a context and your context data includes a ``user`` key, that data will be merged into this. 142 | 143 | .. describe:: request 144 | 145 | Alias: ``req``. The ``request`` object associated with this event, from a Node http server, Express, Koa, or similar. 146 | Will be parsed for request details and user context from ``request.user`` if present. It will only pull out the data 147 | that's handled by the server: ``headers``, ``method``, ``host``, ``protocol``, ``url``, ``query``, ``cookies``, ``body``, ``ip`` and ``user``. 148 | 149 | .. code-block:: javascript 150 | 151 | app.use(function (req, res, next) { 152 | if (someError) { 153 | Raven.captureException(someError, { req: req }); 154 | } 155 | }); 156 | 157 | Note that the ``Raven.requestHandler()`` Express middleware adds the ``req`` object to the context for you automatically, so you won't need to provide it manually. 158 | 159 | .. describe:: tags 160 | 161 | Tags to index with this event. Must be a mapping of strings. 162 | 163 | .. code-block:: javascript 164 | 165 | { 166 | tags: { key: 'value' } 167 | } 168 | 169 | If you're inside a context and your context data includes a `tags` key, that data will be merged into this. 170 | You can also set tags data globally to be merged with all events by passing a ``tags`` option to ``config``. 171 | 172 | .. describe:: extra 173 | 174 | Additional context for this event. Must be a mapping. Children can be any native JSON type. 175 | 176 | .. code-block:: javascript 177 | 178 | { 179 | extra: { key: 'value' } 180 | } 181 | 182 | If you're inside a context and your context data includes an `extra` key, that data will be merged into this. 183 | You can also set extra data globally to be merged with all events by passing an ``extra`` option to ``config``. 184 | 185 | 186 | .. describe:: fingerprint 187 | 188 | The fingerprint for grouping this event. Learn more how `Sentry groups errors `__. 189 | 190 | .. code-block:: javascript 191 | 192 | { 193 | // dont group events from the same NODE_ENV together 194 | fingerprint: ['{{ default }}', process.env.NODE_ENV] 195 | } 196 | 197 | .. describe:: level 198 | 199 | The level of the event. Defaults to ``error``. 200 | 201 | .. code-block:: javascript 202 | 203 | { 204 | level: 'warning' 205 | } 206 | 207 | Sentry is aware of the following levels: 208 | 209 | * debug (the least serious) 210 | * info 211 | * warning 212 | * error 213 | * fatal (the most serious) 214 | 215 | .. _raven-recording-breadcrumbs: 216 | 217 | Recording Breadcrumbs 218 | --------------------- 219 | 220 | Breadcrumbs are records of server and application lifecycle events that can be helpful in understanding the state of the application leading up to a crash. 221 | 222 | We can capture breadcrumbs and associate them with a context, and then send them along with any errors captured from that context: 223 | 224 | .. code-block:: javascript 225 | 226 | Raven.context(function () { 227 | Raven.captureBreadcrumb({ 228 | message: 'Received payment confirmation', 229 | category: 'payment', 230 | data: { 231 | amount: 312, 232 | } 233 | }); 234 | // errors thrown here will have breadcrumb attached 235 | }); 236 | 237 | To learn more about what types of data can be collected via breadcrumbs, see the `breadcrumbs client API specification 238 | `_. 239 | 240 | Raven can be configured to automatically capture breadcrubs for certain events including: 241 | 242 | * http/https requests 243 | * console log statements 244 | * postgres queries 245 | 246 | Automatic breadcrumb collection is disabled by default. You can enable it with a config option: 247 | 248 | .. code-block:: javascript 249 | 250 | Raven.config('___PUBLIC_DSN___', { 251 | autoBreadcrumbs: true 252 | }); 253 | 254 | Or just enable specific types of automatic breadcrumbs: 255 | 256 | .. code-block:: javascript 257 | 258 | Raven.config('___PUBLIC_DSN___', { 259 | autoBreadcrumbs: { 260 | http: true 261 | } 262 | }); 263 | 264 | For more on configuring breadcrumbs, see :doc:`config`. 265 | 266 | Event IDs 267 | --------- 268 | 269 | To make referencing an event easy (both by the developer and customer), you can 270 | get an event ID from any captured message or exception. It's provided both as the 271 | synchronous return value of the capture method and as an argument to the callback: 272 | 273 | .. code-block:: javascript 274 | 275 | var eventId = Raven.captureException(e, function (sendErr, eventId2) { 276 | // eventId === eventId2 277 | }); 278 | 279 | Promises 280 | -------- 281 | 282 | By default, Raven does not capture unhandled promise rejections. You can have it do so automatically: 283 | 284 | .. code-block:: javascript 285 | 286 | Raven.config('___PUBLIC_DSN___', { 287 | captureUnhandledRejections: true 288 | }).install(); 289 | 290 | Global Fatal Error Handler 291 | -------------------------- 292 | 293 | The ``install`` method sets up a global listener for uncaught exceptions, and ``context`` and ``wrap`` can catch exceptions as well. 294 | These are situations where Raven catches what would otherwise be a fatal process-ending exception. A process should generally not 295 | continue to run after such events occur, (see `Node docs `_), 296 | so Raven has a concept of a "fatal error handler". When Raven catches an otherwise-fatal exception, it will capture the exception 297 | (send it to Sentry) and then call the fatal error handler. 298 | 299 | By default, the fatal error handler prints the error and then exits the process. If you want to do your own clean-up, 300 | pre-exit logging, or other shutdown procedures, you can provide your own fatal error handler as an argument to ``install()``. 301 | 302 | The fatal error handler callback will be the last thing called before the process should shut down. 303 | It can do anything necessary, including asynchronous operations, to make a best effort to clean up and shut down the process, but it should 304 | not throw, and it absolutely must not allow the process to keep running indefinitely. This means it should probably make an explicit ``process.exit()`` call. 305 | 306 | After catching a fatal exception, Raven will make a best-effort attempt to send it to Sentry before it calls the fatal exception handler. 307 | If sending fails, a ``sendErr`` error object will be passed, and otherwise the ``eventId`` will be provided. In either case, the error object 308 | resulting in the shutdown is passed as the first parameter. 309 | 310 | .. code-block:: javascript 311 | 312 | Raven.install(function (err, sendErr, eventId) { 313 | if (!sendErr) { 314 | console.log('Successfully sent fatal error with eventId ' + eventId + ' to Sentry:'); 315 | console.error(err.stack); 316 | } 317 | console.log('This is thy sheath; there rust, and let me die.'); 318 | process.exit(1); 319 | }); 320 | 321 | Events 322 | ------ 323 | 324 | If you want to know if an event was logged or errored out, Raven instances emit two events, `logged` and `error`: 325 | 326 | .. code-block:: javascript 327 | 328 | Raven.on('logged', function () { 329 | console.log('Yay, it worked!'); 330 | }); 331 | 332 | Raven.on('error', function (e) { 333 | // The event contains information about the failure: 334 | // e.reason -- raw response body 335 | // e.statusCode -- response status code 336 | // e.response -- raw http response object 337 | 338 | console.log('uh oh, couldnt record the event'); 339 | }); 340 | 341 | Raven.captureMessage('Boom'); 342 | 343 | Configuring the HTTP Transport 344 | ------------------------------ 345 | 346 | .. code-block:: javascript 347 | 348 | Raven.config('___PUBLIC_DSN___', { 349 | transport: new raven.transports.HTTPSTransport({rejectUnauthorized: false}) 350 | }); 351 | 352 | Disable Raven 353 | ------------- 354 | 355 | Passing any falsey value as the DSN will disable sending events upstream: 356 | 357 | .. code-block:: javascript 358 | 359 | Raven.config(process.env.NODE_ENV === 'production' && '___PUBLIC_DSN___'); 360 | 361 | Disable Console Alerts 362 | ---------------------- 363 | Raven will print console alerts in situations where you're using a deprecated API 364 | or where behavior might be surprising, like if there's no DSN configured. 365 | 366 | These alerts are hopefully helpful during initial setup or in upgrading Raven versions, 367 | but once you have everything set up and going, we recommend disabling them: 368 | 369 | .. code-block:: javascript 370 | 371 | Raven.disableConsoleAlerts(); 372 | 373 | Multiple Instances 374 | ------------------ 375 | Normally there is just one instance of Raven: 376 | 377 | .. code-block:: javascript 378 | 379 | var Raven = require('raven'); 380 | // Raven is already a Raven instance, and we do everything based on that instance 381 | 382 | This should be sufficient for almost all users, but for various reasons some users might like to have multiple instances. 383 | Additional instances can be created like this: 384 | 385 | .. code-block:: javascript 386 | 387 | var Raven2 = new Raven.Client(); 388 | 389 | 390 | Dealing with Minified Source Code 391 | --------------------------------- 392 | 393 | Raven and Sentry support `Source Maps 394 | `_. 395 | 396 | We have provided some instructions to creating Source Maps over at :ref:`raven-node-sourcemaps`. 397 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/client'); 4 | module.exports.utils = require('./lib/utils'); 5 | 6 | module.exports.transports = require('./lib/transports'); 7 | module.exports.parsers = require('./lib/parsers'); 8 | 9 | // To infinity and beyond 10 | Error.stackTraceLimit = Infinity; 11 | -------------------------------------------------------------------------------- /lib/instrumentation/console.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var utils = require('../utils'); 5 | 6 | module.exports = function(Raven, console, originals) { 7 | var wrapConsoleMethod = function(level) { 8 | if (!(level in console)) { 9 | return; 10 | } 11 | 12 | utils.fill( 13 | console, 14 | level, 15 | function(originalConsoleLevel) { 16 | var sentryLevel = level === 'warn' ? 'warning' : level; 17 | 18 | return function() { 19 | var args = [].slice.call(arguments); 20 | 21 | Raven.captureBreadcrumb({ 22 | message: util.format.apply(null, args), 23 | level: sentryLevel, 24 | category: 'console' 25 | }); 26 | 27 | originalConsoleLevel.apply(console, args); 28 | }; 29 | }, 30 | originals 31 | ); 32 | }; 33 | 34 | ['debug', 'info', 'warn', 'error', 'log'].forEach(wrapConsoleMethod); 35 | 36 | return console; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/instrumentation/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var utils = require('../utils'); 4 | 5 | module.exports = function(Raven, http, originals) { 6 | var OrigClientRequest = http.ClientRequest; 7 | var ClientRequest = function(options, cb) { 8 | // Note: this won't capture a breadcrumb if a response never comes 9 | // It would be useful to know if that was the case, though, so 10 | // todo: revisit to see if we can capture sth indicating response never came 11 | // possibility: capture one breadcrumb for "req sent" and one for "res recvd" 12 | // seems excessive but solves the problem and *is* strictly more information 13 | // could be useful for weird response sequencing bug scenarios 14 | OrigClientRequest.call(this, options, cb); 15 | 16 | // We could just always reconstruct this from this.agent, this._headers, this.path, etc 17 | // but certain other http-instrumenting libraries (like nock, which we use for tests) fail to 18 | // maintain the guarantee that after calling OrigClientRequest, those fields will be populated 19 | if (typeof options === 'string') { 20 | this.__ravenBreadcrumbUrl = options; 21 | } else { 22 | var protocol = options.protocol || ''; 23 | var hostname = options.hostname || options.host || ''; 24 | // Don't log standard :80 (http) and :443 (https) ports to reduce the noise 25 | var port = 26 | !options.port || options.port === 80 || options.port === 443 27 | ? '' 28 | : ':' + options.port; 29 | var path = options.path || '/'; 30 | 31 | this.__ravenBreadcrumbUrl = protocol + '//' + hostname + port + path; 32 | } 33 | }; 34 | util.inherits(ClientRequest, OrigClientRequest); 35 | 36 | utils.fill(ClientRequest.prototype, 'emit', function(origEmit) { 37 | return function(evt, maybeResp) { 38 | if (evt === 'response' && this.__ravenBreadcrumbUrl) { 39 | if (!Raven.dsn || this.__ravenBreadcrumbUrl.indexOf(Raven.dsn.host) === -1) { 40 | Raven.captureBreadcrumb({ 41 | type: 'http', 42 | category: 'http', 43 | data: { 44 | method: this.method, 45 | url: this.__ravenBreadcrumbUrl, 46 | status_code: maybeResp.statusCode 47 | } 48 | }); 49 | } 50 | } 51 | return origEmit.apply(this, arguments); 52 | }; 53 | }); 54 | 55 | utils.fill( 56 | http, 57 | 'ClientRequest', 58 | function() { 59 | return ClientRequest; 60 | }, 61 | originals 62 | ); 63 | 64 | // http.request orig refs module-internal ClientRequest, not exported one, so 65 | // it still points at orig ClientRequest after our monkeypatch; these reimpls 66 | // just get that reference updated to use our new ClientRequest 67 | utils.fill( 68 | http, 69 | 'request', 70 | function() { 71 | return function(options, cb) { 72 | return new http.ClientRequest(options, cb); 73 | }; 74 | }, 75 | originals 76 | ); 77 | 78 | utils.fill( 79 | http, 80 | 'get', 81 | function() { 82 | return function(options, cb) { 83 | var req = http.request(options, cb); 84 | req.end(); 85 | return req; 86 | }; 87 | }, 88 | originals 89 | ); 90 | 91 | return http; 92 | }; 93 | -------------------------------------------------------------------------------- /lib/instrumentation/instrumentor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../utils'); 4 | 5 | var defaultOnConfig = { 6 | console: true, 7 | http: true 8 | }; 9 | 10 | var defaultConfig = { 11 | console: false, 12 | http: false, 13 | pg: false 14 | }; 15 | 16 | function instrument(Raven, config) { 17 | if (config === false) { 18 | return; 19 | } else if (config === true) { 20 | config = defaultOnConfig; 21 | } else { 22 | config = utils.extend({}, defaultConfig, config); 23 | } 24 | 25 | Raven.instrumentedOriginals = []; 26 | Raven.instrumentedModules = []; 27 | 28 | var Module = require('module'); 29 | utils.fill( 30 | Module, 31 | '_load', 32 | function(origLoad) { 33 | return function(moduleId, parent, isMain) { 34 | var origModule = origLoad.apply(this, arguments); 35 | if (config[moduleId] && Raven.instrumentedModules.indexOf(moduleId) === -1) { 36 | Raven.instrumentedModules.push(moduleId); 37 | return require('./' + moduleId)(Raven, origModule, Raven.instrumentedOriginals); 38 | } 39 | return origModule; 40 | }; 41 | }, 42 | Raven.instrumentedOriginals 43 | ); 44 | 45 | // special case: since console is built-in and app-level code won't require() it, do that here 46 | if (config.console) { 47 | require('console'); 48 | } 49 | 50 | // observation: when the https module does its own require('http'), it *does not* hit our hooked require to instrument http on the fly 51 | // but if we've previously instrumented http, https *does* get our already-instrumented version 52 | // this is because raven's transports are required before this instrumentation takes place, which loads https (and http) 53 | // so module cache will have uninstrumented http; proactively loading it here ensures instrumented version is in module cache 54 | // alternatively we could refactor to load our transports later, but this is easier and doesn't have much drawback 55 | if (config.http) { 56 | require('http'); 57 | } 58 | } 59 | 60 | function deinstrument(Raven) { 61 | if (!Raven.instrumentedOriginals) return; 62 | var original; 63 | // eslint-disable-next-line no-cond-assign 64 | while ((original = Raven.instrumentedOriginals.shift())) { 65 | var obj = original[0]; 66 | var name = original[1]; 67 | var orig = original[2]; 68 | obj[name] = orig; 69 | } 70 | } 71 | 72 | module.exports = { 73 | instrument: instrument, 74 | deinstrument: deinstrument 75 | }; 76 | -------------------------------------------------------------------------------- /lib/instrumentation/pg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(Raven, pg, originals) { 3 | // Using fill helper here is hard because of `this` binding 4 | var origQuery = pg.Connection.prototype.query; 5 | pg.Connection.prototype.query = function(text) { 6 | Raven.captureBreadcrumb({ 7 | category: 'postgres', 8 | message: text 9 | }); 10 | origQuery.call(this, text); 11 | }; 12 | // todo thread this through 13 | // originals.push([pg.Connection.prototype, 'query', origQuery]); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/parsers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cookie = require('cookie'); 4 | var urlParser = require('url'); 5 | var stringify = require('../vendor/json-stringify-safe'); 6 | 7 | var utils = require('./utils'); 8 | 9 | module.exports.parseText = function parseText(message, kwargs) { 10 | kwargs = kwargs || {}; 11 | kwargs.message = message; 12 | 13 | return kwargs; 14 | }; 15 | 16 | module.exports.parseError = function parseError(err, kwargs, cb) { 17 | utils.parseStack(err, function(frames) { 18 | var name = 19 | ({}.hasOwnProperty.call(err, 'name') ? err.name : err.constructor.name) + ''; 20 | if (typeof kwargs.message === 'undefined') { 21 | kwargs.message = name + ': ' + (err.message || ''); 22 | } 23 | kwargs.exception = [ 24 | { 25 | type: name, 26 | value: err.message, 27 | stacktrace: { 28 | frames: frames 29 | } 30 | } 31 | ]; 32 | 33 | // Save additional error properties to `extra` under the error type (e.g. `extra.AttributeError`) 34 | var extraErrorProps; 35 | for (var key in err) { 36 | if (err.hasOwnProperty(key)) { 37 | if (key !== 'name' && key !== 'message' && key !== 'stack' && key !== 'domain') { 38 | extraErrorProps = extraErrorProps || {}; 39 | extraErrorProps[key] = err[key]; 40 | } 41 | } 42 | } 43 | if (extraErrorProps) { 44 | kwargs.extra = kwargs.extra || {}; 45 | kwargs.extra[name] = extraErrorProps; 46 | } 47 | 48 | if (!kwargs.transaction && !kwargs.culprit) { 49 | for (var n = frames.length - 1; n >= 0; n--) { 50 | if (frames[n].in_app) { 51 | kwargs.transaction = utils.getTransaction(frames[n]); 52 | break; 53 | } 54 | } 55 | } 56 | 57 | cb(kwargs); 58 | }); 59 | }; 60 | 61 | module.exports.parseRequest = function parseRequest(req, parseUser) { 62 | var kwargs = {}; 63 | 64 | // headers: 65 | // node, express: req.headers 66 | // koa: req.header 67 | var headers = req.headers || req.header || {}; 68 | 69 | // method: 70 | // node, express, koa: req.method 71 | var method = req.method; 72 | 73 | // host: 74 | // express: req.hostname in > 4 and req.host in < 4 75 | // koa: req.host 76 | // node: req.headers.host 77 | var host = req.hostname || req.host || headers.host || ''; 78 | 79 | // protocol: 80 | // node: 81 | // express, koa: req.protocol 82 | var protocol = 83 | req.protocol === 'https' || req.secure || (req.socket || {}).encrypted 84 | ? 'https' 85 | : 'http'; 86 | 87 | // url (including path and query string): 88 | // node, express: req.originalUrl 89 | // koa: req.url 90 | var originalUrl = req.originalUrl || req.url; 91 | 92 | // absolute url 93 | var absoluteUrl = protocol + '://' + host + originalUrl; 94 | 95 | // query string: 96 | // node: req.url (raw) 97 | // express, koa: req.query 98 | var query = req.query || urlParser.parse(originalUrl || '', true).query; 99 | 100 | // cookies: 101 | // node, express, koa: req.headers.cookie 102 | var cookies = cookie.parse(headers.cookie || ''); 103 | 104 | // body data: 105 | // node, express, koa: req.body 106 | var data = req.body; 107 | if (['GET', 'HEAD'].indexOf(method) === -1) { 108 | if (typeof data === 'undefined') { 109 | data = ''; 110 | } 111 | } 112 | 113 | if (data && typeof data !== 'string' && {}.toString.call(data) !== '[object String]') { 114 | // Make sure the request body is a string 115 | data = stringify(data); 116 | } 117 | 118 | // http interface 119 | var http = { 120 | method: method, 121 | query_string: query, 122 | headers: headers, 123 | cookies: cookies, 124 | data: data, 125 | url: absoluteUrl 126 | }; 127 | 128 | // expose http interface 129 | kwargs.request = http; 130 | 131 | // user: typically found on req.user in express/passport patterns 132 | // five cases for parseUser value: 133 | // absent: grab only id, username, email from req.user 134 | // false: capture nothing 135 | // true: capture all keys from req.user 136 | // array: provided whitelisted keys to grab from req.user 137 | // function :: req -> user: custom parsing function 138 | if (parseUser == null) parseUser = ['id', 'username', 'email']; 139 | if (parseUser) { 140 | var user = {}; 141 | if (typeof parseUser === 'function') { 142 | user = parseUser(req); 143 | } else if (req.user) { 144 | if (parseUser === true) { 145 | for (var key in req.user) { 146 | if ({}.hasOwnProperty.call(req.user, key)) { 147 | user[key] = req.user[key]; 148 | } 149 | } 150 | } else { 151 | parseUser.forEach(function(fieldName) { 152 | if ({}.hasOwnProperty.call(req.user, fieldName)) { 153 | user[fieldName] = req.user[fieldName]; 154 | } 155 | }); 156 | } 157 | } 158 | 159 | // client ip: 160 | // node: req.connection.remoteAddress 161 | // express, koa: req.ip 162 | var ip = req.ip || (req.connection && req.connection.remoteAddress); 163 | if (ip) { 164 | user.ip_address = ip; 165 | } 166 | 167 | kwargs.user = user; 168 | } 169 | 170 | return kwargs; 171 | }; 172 | -------------------------------------------------------------------------------- /lib/transports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | var timeoutReq = require('timed-out'); 6 | 7 | var http = require('http'); 8 | var https = require('https'); 9 | 10 | var agentOptions = {keepAlive: true, maxSockets: 100}; 11 | var httpAgent = new http.Agent(agentOptions); 12 | var httpsAgent = new https.Agent(agentOptions); 13 | 14 | function Transport() {} 15 | util.inherits(Transport, events.EventEmitter); 16 | 17 | function HTTPTransport(options) { 18 | this.defaultPort = 80; 19 | this.transport = http; 20 | this.options = options || {}; 21 | this.agent = httpAgent; 22 | } 23 | util.inherits(HTTPTransport, Transport); 24 | HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { 25 | var options = { 26 | hostname: client.dsn.host, 27 | path: client.dsn.path + 'api/' + client.dsn.project_id + '/store/', 28 | headers: headers, 29 | method: 'POST', 30 | port: client.dsn.port || this.defaultPort, 31 | ca: client.ca, 32 | agent: this.agent 33 | }; 34 | for (var key in this.options) { 35 | if (this.options.hasOwnProperty(key)) { 36 | options[key] = this.options[key]; 37 | } 38 | } 39 | 40 | // prevent off heap memory explosion 41 | var _name = this.agent.getName({host: client.dsn.host, port: client.dsn.port}); 42 | var _requests = this.agent.requests[_name]; 43 | if (_requests && Object.keys(_requests).length > client.maxReqQueueCount) { 44 | // other feedback strategy 45 | client.emit('error', new Error('client req queue is full..')); 46 | return; 47 | } 48 | 49 | var req = this.transport.request(options, function(res) { 50 | res.setEncoding('utf8'); 51 | if (res.statusCode >= 200 && res.statusCode < 300) { 52 | client.emit('logged', eventId); 53 | cb && cb(null, eventId); 54 | } else { 55 | var reason = res.headers['x-sentry-error']; 56 | var e = new Error('HTTP Error (' + res.statusCode + '): ' + reason); 57 | e.response = res; 58 | e.statusCode = res.statusCode; 59 | e.reason = reason; 60 | e.sendMessage = message; 61 | e.requestHeaders = headers; 62 | e.eventId = eventId; 63 | client.emit('error', e); 64 | cb && cb(e); 65 | } 66 | 67 | // force the socket to drain 68 | var noop = function() {}; 69 | res.on('data', noop); 70 | res.on('end', noop); 71 | }); 72 | 73 | timeoutReq(req, client.sendTimeout * 1000); 74 | 75 | var cbFired = false; 76 | req.on('error', function(e) { 77 | client.emit('error', e); 78 | if (!cbFired) { 79 | cb && cb(e); 80 | cbFired = true; 81 | } 82 | }); 83 | req.end(message); 84 | }; 85 | 86 | function HTTPSTransport(options) { 87 | this.defaultPort = 443; 88 | this.transport = https; 89 | this.options = options || {}; 90 | this.agent = httpsAgent; 91 | } 92 | util.inherits(HTTPSTransport, HTTPTransport); 93 | 94 | module.exports.http = new HTTPTransport(); 95 | module.exports.https = new HTTPSTransport(); 96 | module.exports.Transport = Transport; 97 | module.exports.HTTPTransport = HTTPTransport; 98 | module.exports.HTTPSTransport = HTTPSTransport; 99 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var url = require('url'); 5 | var transports = require('./transports'); 6 | var path = require('path'); 7 | var lsmod = require('../vendor/node-lsmod'); 8 | var stacktrace = require('stack-trace'); 9 | var stringify = require('../vendor/json-stringify-safe'); 10 | 11 | var ravenVersion = require('../package.json').version; 12 | 13 | var protocolMap = { 14 | http: 80, 15 | https: 443 16 | }; 17 | 18 | var consoleAlerts = new Set(); 19 | 20 | // Default Node.js REPL depth 21 | var MAX_SERIALIZE_EXCEPTION_DEPTH = 3; 22 | // 50kB, as 100kB is max payload size, so half sounds reasonable 23 | var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024; 24 | var MAX_SERIALIZE_KEYS_LENGTH = 40; 25 | 26 | function utf8Length(value) { 27 | return ~-encodeURI(value).split(/%..|./).length; 28 | } 29 | 30 | function jsonSize(value) { 31 | return utf8Length(JSON.stringify(value)); 32 | } 33 | 34 | function isError(what) { 35 | return ( 36 | Object.prototype.toString.call(what) === '[object Error]' || what instanceof Error 37 | ); 38 | } 39 | 40 | module.exports.isError = isError; 41 | 42 | function isPlainObject(what) { 43 | return Object.prototype.toString.call(what) === '[object Object]'; 44 | } 45 | 46 | module.exports.isPlainObject = isPlainObject; 47 | 48 | function serializeValue(value) { 49 | var maxLength = 40; 50 | 51 | if (typeof value === 'string') { 52 | return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026'; 53 | } else if ( 54 | typeof value === 'number' || 55 | typeof value === 'boolean' || 56 | typeof value === 'undefined' 57 | ) { 58 | return value; 59 | } 60 | 61 | var type = Object.prototype.toString.call(value); 62 | 63 | // Node.js REPL notation 64 | if (type === '[object Object]') return '[Object]'; 65 | if (type === '[object Array]') return '[Array]'; 66 | if (type === '[object Function]') 67 | return value.name ? '[Function: ' + value.name + ']' : '[Function]'; 68 | 69 | return value; 70 | } 71 | 72 | function serializeObject(value, depth) { 73 | if (depth === 0) return serializeValue(value); 74 | 75 | if (isPlainObject(value)) { 76 | return Object.keys(value).reduce(function(acc, key) { 77 | acc[key] = serializeObject(value[key], depth - 1); 78 | return acc; 79 | }, {}); 80 | } else if (Array.isArray(value)) { 81 | return value.map(function(val) { 82 | return serializeObject(val, depth - 1); 83 | }); 84 | } 85 | 86 | return serializeValue(value); 87 | } 88 | 89 | function serializeException(ex, depth, maxSize) { 90 | if (!isPlainObject(ex)) return ex; 91 | 92 | depth = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_DEPTH : depth; 93 | maxSize = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_SIZE : maxSize; 94 | 95 | var serialized = serializeObject(ex, depth); 96 | 97 | if (jsonSize(stringify(serialized)) > maxSize) { 98 | return serializeException(ex, depth - 1); 99 | } 100 | 101 | return serialized; 102 | } 103 | 104 | module.exports.serializeException = serializeException; 105 | 106 | function serializeKeysForMessage(keys, maxLength) { 107 | if (typeof keys === 'number' || typeof keys === 'string') return keys.toString(); 108 | if (!Array.isArray(keys)) return ''; 109 | 110 | keys = keys.filter(function(key) { 111 | return typeof key === 'string'; 112 | }); 113 | if (keys.length === 0) return '[object has no keys]'; 114 | 115 | maxLength = typeof maxLength !== 'number' ? MAX_SERIALIZE_KEYS_LENGTH : maxLength; 116 | if (keys[0].length >= maxLength) return keys[0]; 117 | 118 | for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) { 119 | var serialized = keys.slice(0, usedKeys).join(', '); 120 | if (serialized.length > maxLength) continue; 121 | if (usedKeys === keys.length) return serialized; 122 | return serialized + '\u2026'; 123 | } 124 | 125 | return ''; 126 | } 127 | 128 | module.exports.serializeKeysForMessage = serializeKeysForMessage; 129 | 130 | module.exports.disableConsoleAlerts = function disableConsoleAlerts() { 131 | consoleAlerts = false; 132 | }; 133 | 134 | module.exports.enableConsoleAlerts = function enableConsoleAlerts() { 135 | consoleAlerts = new Set(); 136 | }; 137 | 138 | module.exports.consoleAlert = function consoleAlert(msg) { 139 | if (consoleAlerts) { 140 | console.warn('raven@' + ravenVersion + ' alert: ' + msg); 141 | } 142 | }; 143 | 144 | module.exports.consoleAlertOnce = function consoleAlertOnce(msg) { 145 | if (consoleAlerts && !consoleAlerts.has(msg)) { 146 | consoleAlerts.add(msg); 147 | console.warn('raven@' + ravenVersion + ' alert: ' + msg); 148 | } 149 | }; 150 | 151 | module.exports.extend = 152 | Object.assign || 153 | function(target) { 154 | for (var i = 1; i < arguments.length; i++) { 155 | var source = arguments[i]; 156 | for (var key in source) { 157 | if (Object.prototype.hasOwnProperty.call(source, key)) { 158 | target[key] = source[key]; 159 | } 160 | } 161 | } 162 | return target; 163 | }; 164 | 165 | module.exports.getAuthHeader = function getAuthHeader(timestamp, apiKey, apiSecret) { 166 | var header = ['Sentry sentry_version=5']; 167 | header.push('sentry_timestamp=' + timestamp); 168 | header.push('sentry_client=raven-node/' + ravenVersion); 169 | header.push('sentry_key=' + apiKey); 170 | if (apiSecret) header.push('sentry_secret=' + apiSecret); 171 | return header.join(', '); 172 | }; 173 | 174 | module.exports.parseDSN = function parseDSN(dsn) { 175 | if (!dsn) { 176 | // Let a falsey value return false explicitly 177 | return false; 178 | } 179 | try { 180 | var parsed = url.parse(dsn), 181 | response = { 182 | protocol: parsed.protocol.slice(0, -1), 183 | public_key: parsed.auth.split(':')[0], 184 | host: parsed.host.split(':')[0] 185 | }; 186 | 187 | if (parsed.auth.split(':')[1]) { 188 | response.private_key = parsed.auth.split(':')[1]; 189 | } 190 | 191 | if (~response.protocol.indexOf('+')) { 192 | response.protocol = response.protocol.split('+')[1]; 193 | } 194 | 195 | if (!transports.hasOwnProperty(response.protocol)) { 196 | throw new Error('Invalid transport'); 197 | } 198 | 199 | var index = parsed.pathname.lastIndexOf('/'); 200 | response.path = parsed.pathname.substr(0, index + 1); 201 | response.project_id = parsed.pathname.substr(index + 1); 202 | response.port = ~~parsed.port || protocolMap[response.protocol] || 443; 203 | return response; 204 | } catch (e) { 205 | throw new Error('Invalid Sentry DSN: ' + dsn); 206 | } 207 | }; 208 | 209 | module.exports.getTransaction = function getTransaction(frame) { 210 | if (frame.module || frame.function) { 211 | return (frame.module || '?') + ' at ' + (frame.function || '?'); 212 | } 213 | return ''; 214 | }; 215 | 216 | var moduleCache; 217 | module.exports.getModules = function getModules() { 218 | if (!moduleCache) { 219 | moduleCache = lsmod(); 220 | } 221 | return moduleCache; 222 | }; 223 | 224 | module.exports.fill = function(obj, name, replacement, track) { 225 | var orig = obj[name]; 226 | obj[name] = replacement(orig); 227 | if (track) { 228 | track.push([obj, name, orig]); 229 | } 230 | }; 231 | 232 | var LINES_OF_CONTEXT = 7; 233 | 234 | function getFunction(line) { 235 | try { 236 | return ( 237 | line.getFunctionName() || 238 | line.getTypeName() + '.' + (line.getMethodName() || '') 239 | ); 240 | } catch (e) { 241 | // This seems to happen sometimes when using 'use strict', 242 | // stemming from `getTypeName`. 243 | // [TypeError: Cannot read property 'constructor' of undefined] 244 | return ''; 245 | } 246 | } 247 | 248 | var mainModule = 249 | ((require.main && require.main.filename && path.dirname(require.main.filename)) || 250 | global.process.cwd()) + '/'; 251 | 252 | function getModule(filename, base) { 253 | if (!base) base = mainModule; 254 | 255 | // It's specifically a module 256 | var file = path.basename(filename, '.js'); 257 | filename = path.dirname(filename); 258 | var n = filename.lastIndexOf('/node_modules/'); 259 | if (n > -1) { 260 | // /node_modules/ is 14 chars 261 | return filename.substr(n + 14).replace(/\//g, '.') + ':' + file; 262 | } 263 | // Let's see if it's a part of the main module 264 | // To be a part of main module, it has to share the same base 265 | n = (filename + '/').lastIndexOf(base, 0); 266 | if (n === 0) { 267 | var module = filename.substr(base.length).replace(/\//g, '.'); 268 | if (module) module += ':'; 269 | module += file; 270 | return module; 271 | } 272 | return file; 273 | } 274 | 275 | function readSourceFiles(filenames, cb) { 276 | // we're relying on filenames being de-duped already 277 | if (filenames.length === 0) return setTimeout(cb, 0, {}); 278 | 279 | var sourceFiles = {}; 280 | var numFilesToRead = filenames.length; 281 | return filenames.forEach(function(filename) { 282 | fs.readFile(filename, function(readErr, file) { 283 | if (!readErr) sourceFiles[filename] = file.toString().split('\n'); 284 | if (--numFilesToRead === 0) cb(sourceFiles); 285 | }); 286 | }); 287 | } 288 | 289 | // This is basically just `trim_line` from https://github.com/getsentry/sentry/blob/master/src/sentry/lang/javascript/processor.py#L67 290 | function snipLine(line, colno) { 291 | var ll = line.length; 292 | if (ll <= 150) return line; 293 | if (colno > ll) colno = ll; 294 | 295 | var start = Math.max(colno - 60, 0); 296 | if (start < 5) start = 0; 297 | 298 | var end = Math.min(start + 140, ll); 299 | if (end > ll - 5) end = ll; 300 | if (end === ll) start = Math.max(end - 140, 0); 301 | 302 | line = line.slice(start, end); 303 | if (start > 0) line = '{snip} ' + line; 304 | if (end < ll) line += ' {snip}'; 305 | 306 | return line; 307 | } 308 | 309 | function snipLine0(line) { 310 | return snipLine(line, 0); 311 | } 312 | 313 | function parseStack(err, cb) { 314 | if (!err) return cb([]); 315 | 316 | var stack = stacktrace.parse(err); 317 | if (!stack || !Array.isArray(stack) || !stack.length || !stack[0].getFileName) { 318 | // the stack is not the useful thing we were expecting :/ 319 | return cb([]); 320 | } 321 | 322 | // Sentry expects the stack trace to be oldest -> newest, v8 provides newest -> oldest 323 | stack.reverse(); 324 | 325 | var frames = []; 326 | var filesToRead = {}; 327 | stack.forEach(function(line) { 328 | var frame = { 329 | filename: line.getFileName() || '', 330 | lineno: line.getLineNumber(), 331 | colno: line.getColumnNumber(), 332 | function: getFunction(line) 333 | }; 334 | 335 | var isInternal = 336 | line.isNative() || 337 | (frame.filename[0] !== '/' && 338 | frame.filename[0] !== '.' && 339 | frame.filename.indexOf(':\\') !== 1); 340 | 341 | // in_app is all that's not an internal Node function or a module within node_modules 342 | // note that isNative appears to return true even for node core libraries 343 | // see https://github.com/getsentry/raven-node/issues/176 344 | frame.in_app = !isInternal && frame.filename.indexOf('node_modules/') === -1; 345 | 346 | // Extract a module name based on the filename 347 | if (frame.filename) { 348 | frame.module = getModule(frame.filename); 349 | if (!isInternal) filesToRead[frame.filename] = true; 350 | } 351 | 352 | frames.push(frame); 353 | }); 354 | 355 | return readSourceFiles(Object.keys(filesToRead), function(sourceFiles) { 356 | frames.forEach(function(frame) { 357 | if (frame.filename && sourceFiles[frame.filename]) { 358 | var lines = sourceFiles[frame.filename]; 359 | try { 360 | frame.pre_context = lines 361 | .slice(Math.max(0, frame.lineno - (LINES_OF_CONTEXT + 1)), frame.lineno - 1) 362 | .map(snipLine0); 363 | frame.context_line = snipLine(lines[frame.lineno - 1], frame.colno); 364 | frame.post_context = lines 365 | .slice(frame.lineno, frame.lineno + LINES_OF_CONTEXT) 366 | .map(snipLine0); 367 | } catch (e) { 368 | // anomaly, being defensive in case 369 | // unlikely to ever happen in practice but can definitely happen in theory 370 | } 371 | } 372 | }); 373 | 374 | cb(frames); 375 | }); 376 | } 377 | 378 | // expose basically for testing because I don't know what I'm doing 379 | module.exports.parseStack = parseStack; 380 | module.exports.getModule = getModule; 381 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raven", 3 | "description": "A standalone (Node.js) client for Sentry", 4 | "keywords": [ 5 | "debugging", 6 | "errors", 7 | "exceptions", 8 | "logging", 9 | "raven", 10 | "sentry" 11 | ], 12 | "version": "2.6.2", 13 | "repository": "git://github.com/getsentry/raven-node.git", 14 | "license": "BSD-2-Clause", 15 | "homepage": "https://github.com/getsentry/raven-node", 16 | "author": "Matt Robenolt ", 17 | "main": "index.js", 18 | "bin": { 19 | "raven": "./bin/raven" 20 | }, 21 | "scripts": { 22 | "lint": "eslint . --quiet", 23 | "precommit": "lint-staged", 24 | "pretest": "npm install && npm run lint", 25 | "test": "NODE_ENV=test istanbul cover _mocha -- --reporter dot && NODE_ENV=test coffee ./test/run.coffee", 26 | "test-mocha": "NODE_ENV=test mocha", 27 | "test-full": "npm run test && cd test/instrumentation && ./run.sh" 28 | }, 29 | "engines": { 30 | "node": ">= 4.0.0" 31 | }, 32 | "dependencies": { 33 | "cookie": "0.3.1", 34 | "md5": "^2.2.1", 35 | "stack-trace": "0.0.10", 36 | "timed-out": "4.0.1", 37 | "uuid": "3.0.0" 38 | }, 39 | "devDependencies": { 40 | "coffee-script": "~1.10.0", 41 | "connect": "*", 42 | "eslint": "^4.5.0", 43 | "eslint-config-prettier": "^2.3.0", 44 | "express": "*", 45 | "glob": "~3.1.13", 46 | "husky": "^0.14.3", 47 | "istanbul": "^0.4.3", 48 | "lint-staged": "^4.0.4", 49 | "mocha": "~3.1.2", 50 | "nock": "~9.0.0", 51 | "prettier": "^1.6.1", 52 | "should": "11.2.0", 53 | "sinon": "^3.3.0" 54 | }, 55 | "prettier": { 56 | "singleQuote": true, 57 | "bracketSpacing": false, 58 | "printWidth": 90 59 | }, 60 | "lint-staged": { 61 | "*.js": [ 62 | "prettier --write", 63 | "git add" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/exit/capture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var dsn = 'https://public:private@app.getsentry.com/269'; 4 | 5 | var nock = require('nock'); 6 | var scope = nock('https://app.getsentry.com') 7 | .filteringRequestBody(/.*/, '*') 8 | .post('/api/269/store/', '*') 9 | .reply(200, 'OK'); 10 | 11 | Raven.config(dsn).install(); 12 | 13 | process.on('exit', function() { 14 | scope.done(); 15 | console.log('exit test assertions complete'); 16 | }); 17 | 18 | setImmediate(function() { 19 | throw new Error('derp'); 20 | }); 21 | -------------------------------------------------------------------------------- /test/exit/capture_callback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | var nock = require('nock'); 7 | var scope = nock('https://app.getsentry.com') 8 | .filteringRequestBody(/.*/, '*') 9 | .post('/api/269/store/', '*') 10 | .reply(200, 'OK'); 11 | 12 | Raven.config(dsn).install(function(err, sendErr, eventId) { 13 | scope.done(); 14 | assert.equal(sendErr, null); 15 | assert.ok(err instanceof Error); 16 | assert.equal(err.message, 'derp'); 17 | console.log('exit test assertions complete'); 18 | process.exit(20); 19 | }); 20 | 21 | setImmediate(function() { 22 | throw new Error('derp'); 23 | }); 24 | -------------------------------------------------------------------------------- /test/exit/capture_with_second_domain_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | var nock = require('nock'); 7 | var scope = nock('https://app.getsentry.com') 8 | .filteringRequestBody(/.*/, '*') 9 | .post('/api/269/store/', '*') 10 | .reply(200, 'OK'); 11 | 12 | Raven.config(dsn).install(); 13 | 14 | var uncaughts = 0; 15 | process.on('uncaughtException', function() { 16 | uncaughts++; 17 | }); 18 | 19 | process.on('exit', function() { 20 | scope.done(); 21 | assert.equal(uncaughts, 2); 22 | console.log('exit test assertions complete'); 23 | }); 24 | 25 | setImmediate(function() { 26 | throw new Error('derp'); 27 | }); 28 | 29 | Raven.context(function() { 30 | process.domain.on('error', function() { 31 | uncaughts++; 32 | }); 33 | setImmediate(function() { 34 | throw new Error('herp'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/exit/capture_with_second_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | var nock = require('nock'); 7 | var scope = nock('https://app.getsentry.com') 8 | .filteringRequestBody(/.*/, '*') 9 | .post('/api/269/store/', '*') 10 | .reply(200, 'OK'); 11 | 12 | Raven.config(dsn).install(); 13 | 14 | var uncaughts = 0; 15 | process.on('uncaughtException', function() { 16 | uncaughts++; 17 | }); 18 | 19 | process.on('exit', function() { 20 | scope.done(); 21 | assert.equal(uncaughts, 2); 22 | console.log('exit test assertions complete'); 23 | }); 24 | 25 | setImmediate(function() { 26 | setImmediate(function() { 27 | throw new Error('herp'); 28 | }); 29 | throw new Error('derp'); 30 | }); 31 | -------------------------------------------------------------------------------- /test/exit/domain_capture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var dsn = 'https://public:private@app.getsentry.com/269'; 4 | 5 | var nock = require('nock'); 6 | var scope = nock('https://app.getsentry.com') 7 | .filteringRequestBody(/.*/, '*') 8 | .post('/api/269/store/', '*') 9 | .reply(200, 'OK'); 10 | 11 | Raven.config(dsn).install(); 12 | 13 | process.on('exit', function() { 14 | scope.done(); 15 | console.log('exit test assertions complete'); 16 | }); 17 | 18 | Raven.context(function() { 19 | setImmediate(function() { 20 | throw new Error('derp'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/exit/domain_capture_callback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | var nock = require('nock'); 7 | var scope = nock('https://app.getsentry.com') 8 | .filteringRequestBody(/.*/, '*') 9 | .post('/api/269/store/', '*') 10 | .reply(200, 'OK'); 11 | 12 | Raven.config(dsn).install(function(err, sendErr, eventId) { 13 | scope.done(); 14 | assert.equal(sendErr, null); 15 | assert.ok(err instanceof Error); 16 | assert.equal(err.message, 'derp'); 17 | console.log('exit test assertions complete'); 18 | process.exit(20); 19 | }); 20 | 21 | Raven.context(function() { 22 | setImmediate(function() { 23 | throw new Error('derp'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/exit/domain_capture_with_second_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | var nock = require('nock'); 7 | var scope = nock('https://app.getsentry.com') 8 | .filteringRequestBody(/.*/, '*') 9 | .post('/api/269/store/', '*') 10 | .reply(200, 'OK'); 11 | 12 | Raven.config(dsn).install(); 13 | 14 | var uncaughts = 0; 15 | 16 | process.on('exit', function() { 17 | scope.done(); 18 | assert.equal(uncaughts, 2); 19 | console.log('exit test assertions complete'); 20 | }); 21 | 22 | Raven.context(function() { 23 | process.domain.on('error', function() { 24 | uncaughts++; 25 | }); 26 | setImmediate(function() { 27 | setImmediate(function() { 28 | throw new Error('herp'); 29 | }); 30 | throw new Error('derp'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/exit/domain_throw_on_send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | Raven.config(dsn).install(function(err, sendErr) { 7 | assert.ok(err instanceof Error); 8 | assert.ok(sendErr instanceof Error); 9 | assert.equal(err.message, 'derp'); 10 | assert.equal(sendErr.message, 'foo'); 11 | console.log('exit test assertions complete'); 12 | process.exit(20); 13 | }); 14 | 15 | Raven.transport.send = function() { 16 | throw new Error('foo'); 17 | }; 18 | 19 | Raven.context(function() { 20 | setImmediate(function() { 21 | throw new Error('derp'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/exit/throw_on_fatal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var dsn = 'https://public:private@app.getsentry.com/269'; 4 | 5 | var nock = require('nock'); 6 | var scope = nock('https://app.getsentry.com') 7 | .filteringRequestBody(/.*/, '*') 8 | .post('/api/269/store/', '*') 9 | .reply(200, 'OK'); 10 | 11 | Raven.disableConsoleAlerts(); 12 | Raven.config(dsn).install(function() { 13 | setImmediate(function() { 14 | throw new Error('fatal derp'); 15 | }); 16 | }); 17 | 18 | process.on('exit', function() { 19 | scope.done(); 20 | console.log('exit test assertions complete'); 21 | }); 22 | 23 | setImmediate(function() { 24 | throw new Error('derp'); 25 | }); 26 | -------------------------------------------------------------------------------- /test/exit/throw_on_send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | var assert = require('assert'); 4 | var dsn = 'https://public:private@app.getsentry.com/269'; 5 | 6 | Raven.config(dsn).install(function(err, sendErr) { 7 | assert.ok(err instanceof Error); 8 | assert.ok(sendErr instanceof Error); 9 | assert.equal(err.message, 'derp'); 10 | assert.equal(sendErr.message, 'foo'); 11 | console.log('exit test assertions complete'); 12 | process.exit(20); 13 | }); 14 | 15 | Raven.transport.send = function() { 16 | throw new Error('foo'); 17 | }; 18 | 19 | process.emit('uncaughtException', new Error('derp')); 20 | -------------------------------------------------------------------------------- /test/fixtures/file with spaces in path.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | throw new Error('boom'); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/long-line.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var w = "this is a really long line to make sure that we will truncate it and not send the entire massive line to the server because we don't want to do that, we want to send just the beginning not the whole thing"; 3 | var x = "this is a really long line but not as long as the last one"; throw new Error('boom'); var y = "we will continue here to make sure we grab the middle of this line and don't just grab the beginning or end"; 4 | var z = "this is a really long line to make sure that we will truncate it and not send the entire massive line to the server because we don't want to do that, we want to send just the beginning not the whole thing"; 5 | }; 6 | -------------------------------------------------------------------------------- /test/instrumentation/README.md: -------------------------------------------------------------------------------- 1 | # Instrumentation tests 2 | 3 | These are tests where we run our breadcrumb instrumentation on a module, then run the instrumented module's original test suite to make sure we didn't break anything. 4 | 5 | We have the following tests: 6 | - node core http (`node-http.test.js`) 7 | 8 | We may add in the future: 9 | - postgres, mysql, etc 10 | - other database drivers 11 | - any other node core modules we choose to instrument 12 | 13 | ### Usage: 14 | We don't run these by default as part of `npm test` since they require a heavy download and take ~a minute to run, but we run them in CI. You can run them locally with: 15 | ``` 16 | npm run test-full 17 | ``` 18 | Or: 19 | ```bash 20 | cd test/instrumentation 21 | ./run.sh 22 | ``` 23 | This will check what version of node you have, grab the source of that version, then run against that http test suite. 24 | -------------------------------------------------------------------------------- /test/instrumentation/http.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var child_process = require('child_process'); 5 | 6 | var nodeRoot = process.argv[2]; 7 | var testRoot = path.join(nodeRoot, 'test/parallel'); 8 | var testFiles = fs.readdirSync(testRoot).filter(function (filename) { 9 | return filename.indexOf('test-http') === 0; 10 | }); 11 | 12 | var defaultFlags = [ 13 | '--allow-natives-syntax', 14 | '--expose-gc', 15 | '--expose-internals' 16 | ]; 17 | 18 | if (process.version >= 'v8') defaultFlags.push('--expose-http2'); 19 | 20 | var failedTests = []; 21 | var numSuccesses = 0; 22 | testFiles.forEach(function (filename) { 23 | var testModulePath = path.join(testRoot, filename); 24 | var singleTestFlags = defaultFlags.concat([ 25 | 'run-node-http-test.js', 26 | testModulePath 27 | ]); 28 | 29 | // this is the only test, that actually asserts the lack of http2 flag 30 | // therefore we have to remove it from the process we are about to run 31 | if (filename === 'test-http2-noflag.js') { 32 | singleTestFlags = singleTestFlags.filter(function (flag) { 33 | return flag !== '--expose-http2'; 34 | }); 35 | } 36 | 37 | try { 38 | child_process.execFileSync('node', singleTestFlags, { stdio: 'ignore' }); 39 | console.log('✓ ' + filename); 40 | numSuccesses++; 41 | } catch (e) { 42 | // non-zero exit code -> test failure 43 | failedTests.push(filename); 44 | console.log('X ' + filename + ' - error!'); 45 | } 46 | }); 47 | 48 | console.log('Finished, failures: ' + failedTests.length + ', successes: ' + numSuccesses); 49 | 50 | // pipeline-flood is a harness issue with how it expects argvs 51 | // header response splitting is something about becoming more strict on allowed chars in headers 52 | // not worried about either one; they also fail without our instrumentation 53 | var knownFailures = [ 54 | 'test-http-pipeline-flood.js', 55 | 'test-http-header-response-splitting.js' 56 | ]; 57 | 58 | var didPass = failedTests.every(function (filename) { 59 | return knownFailures.indexOf(filename) !== -1; 60 | }); 61 | if (!didPass) { 62 | console.log('Some unexpected failures, failing...'); 63 | process.exit(1); 64 | } else { 65 | console.log('All failures were known/expected, passing...'); 66 | process.exit(0); 67 | } 68 | -------------------------------------------------------------------------------- /test/instrumentation/run-node-http-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | // We never actually report any errors, just getting Raven to play ball 4 | var sentryDsn = 'https://fake:dsn@app.getsentry.com/12345'; 5 | 6 | Raven.disableConsoleAlerts(); 7 | Raven.config(sentryDsn, { 8 | autoBreadcrumbs: { 9 | console: false, 10 | http: true, 11 | } 12 | }).install(); 13 | process.removeAllListeners('uncaughtException'); 14 | 15 | var testModulePath = process.argv[process.argv.length - 1]; 16 | require(testModulePath); 17 | -------------------------------------------------------------------------------- /test/instrumentation/run.sh: -------------------------------------------------------------------------------- 1 | # /bin/bash 2 | version=`node -v` 3 | versionMinusV=`echo $version | cut -c 2-` 4 | nodeRoot="node-$versionMinusV" 5 | 6 | if [ ! -d $nodeRoot ]; then 7 | url="https://codeload.github.com/nodejs/node/tar.gz/$version" 8 | curl $url -o "$version.tar.gz" 9 | tar -xf "$version.tar.gz" 10 | rm "$version.tar.gz" 11 | fi 12 | exec node http.test.js `pwd`/$nodeRoot 13 | -------------------------------------------------------------------------------- /test/manual/README.md: -------------------------------------------------------------------------------- 1 | # Manual Tests 2 | 3 | ## How this works 4 | `express-patient.js` is an express app with a collection of endpoints that exercise various functionalities of Raven-node, including exception capturing, contexts, autobreadcrumbs, and the express middleware. 5 | 6 | It uses [memwatch-next](https://www.npmjs.com/package/memwatch-next) to record memory usage after each GC. `manager.js` does some child process stuff to have a fresh patient process for each test scenario, while poke-patient.sh uses apache bench to send a bunch of traffic so we can see what happens. 7 | 8 | ## Routes and what we test 9 | The raven express middleware is used on all endpoints, so each request constitutes its own Raven context. 10 | - `/hello`: just send a basic response without doing anything 11 | - `/context/basic`: `setContext` call 12 | - `/breadcrumbs/capture`: manual `captureBreadcrumb` call 13 | - `/breadcrumbs/auto/console`: console log with console autoBreadcrumbs enabled 14 | - `/breadcrumbs/auto/http`: send an http request with http autoBreadcrumbs enabled 15 | - uses nock to mock the response, not actual request 16 | - If the request has querystring param `doError=true`, we pass an error via Express's error handling mechanism with `next(new Error(responseText))` which will then be captured by the Raven express middleware error handler. 17 | - We test all 5 above cases with and without `doError=true` 18 | 19 | We also have a `/gc` endpoint for forcing a garbage collection; this is used at the end of each test scenario to see final memory usage. 20 | 21 | Note: there's a `/capture` endpoint which does a basic `captureException` call 1000 times. That's our current problem child requiring some more investigation on its memory usage. 22 | 23 | ## How to run it 24 | ```bash 25 | npm install memwatch-next 26 | node manager.js 27 | # in another tab send some traffic at it: 28 | curl localhost:3000/capture 29 | ``` 30 | 31 | ## Why this can't be more automated 32 | Some objects can have long lifecycles or not be cleaned up by GC when you think they would be, and so it isn't straightforward to make the assertion "memory usage should have returned to baseline by now". Also, when the numbers look bad, it's pretty obvious to a trained eye that they're bad, but it can be hard to quantify an exact threshold of pass or fail. 33 | 34 | ## Interpreting results 35 | Starting the manager and then running `ab -c 5 -n 5000 /context/basic && sleep 1 && curl localhost:3000/gc` will get us this output: 36 |
37 | ``` 38 | :[/Users/lewis/dev/raven-node/test/manual]#memleak-tests?$ node manager.js 39 | starting child 40 | patient is waiting to be poked on port 3000 41 | gc #1: min 0, max 0, est base 11639328, curr base 11639328 42 | gc #2: min 0, max 0, est base 11582672, curr base 11582672 43 | hit /context/basic for first time 44 | gc #3: min 16864536, max 16864536, est base 16864536, curr base 16864536 45 | gc #4: min 14830680, max 16864536, est base 14830680, curr base 14830680 46 | gc #5: min 14830680, max 16864536, est base 16013904, curr base 16013904 47 | hit /gc for first time 48 | gc #6: min 12115288, max 16864536, est base 12115288, curr base 12115288 49 | gc #7: min 11673824, max 16864536, est base 11673824, curr base 11673824 50 | ``` 51 |
52 | This test stores some basic data in the request's Raven context, with the hope being for that context data to go out of scope and be garbage collected after the request is over. We can see that we start at a base of ~11.6MB, go up to ~16.8MB during the test, and then return to ~11.6MB. Everything checks out, no memory leak issue here. 53 | 54 | Back when we had a memory leak in `captureException`, if we started the manager and ran: 55 | ```shell 56 | ab -c 5 -n 5000 localhost:3000/context/basic?doError=true && sleep 5 && curl localhost:3000/gc 57 | sleep 5 58 | curl localhost:3000/gc 59 | sleep 10 60 | curl localhost:3000/gc 61 | sleep 15 62 | curl localhost:3000/gc 63 | ``` 64 | we'd get this output: 65 |
66 | ``` 67 | [/Users/lewis/dev/raven-node/test/manual]#memleak-tests?$ node manager.js 68 | starting child 69 | patient is waiting to be poked on port 3000 70 | gc #1: min 0, max 0, est base 11657056, curr base 11657056 71 | gc #2: min 0, max 0, est base 11599392, curr base 11599392 72 | hit /context/basic?doError=true for first time 73 | gc #3: min 20607752, max 20607752, est base 20607752, curr base 20607752 74 | gc #4: min 20607752, max 20969872, est base 20969872, curr base 20969872 75 | gc #5: min 19217632, max 20969872, est base 19217632, curr base 19217632 76 | gc #6: min 19217632, max 21025056, est base 21025056, curr base 21025056 77 | gc #7: min 19217632, max 21096656, est base 21096656, curr base 21096656 78 | gc #8: min 19085432, max 21096656, est base 19085432, curr base 19085432 79 | gc #9: min 19085432, max 22666768, est base 22666768, curr base 22666768 80 | gc #10: min 19085432, max 22666768, est base 22487320, curr base 20872288 81 | gc #11: min 19085432, max 22708656, est base 22509453, curr base 22708656 82 | gc #12: min 19085432, max 22708656, est base 22470302, curr base 22117952 83 | gc #13: min 19085432, max 22708656, est base 22440838, curr base 22175664 84 | gc #14: min 19085432, max 22829952, est base 22479749, curr base 22829952 85 | gc #15: min 19085432, max 25273504, est base 22759124, curr base 25273504 86 | gc #16: min 19085432, max 25273504, est base 22707814, curr base 22246024 87 | gc #17: min 19085432, max 33286216, est base 23765654, curr base 33286216 88 | gc #18: min 19085432, max 33286216, est base 23863713, curr base 24746248 89 | gc #19: min 19085432, max 33286216, est base 23685980, curr base 22086392 90 | gc #20: min 19085432, max 33286216, est base 23705022, curr base 23876400 91 | gc #21: min 19085432, max 33286216, est base 23769947, curr base 24354272 92 | gc #22: min 19085432, max 33286216, est base 23987724, curr base 25947720 93 | gc #23: min 19085432, max 33286216, est base 24636946, curr base 30479952 94 | gc #24: min 19085432, max 33286216, est base 24668561, curr base 24953096 95 | gc #25: min 19085432, max 33286216, est base 24750980, curr base 25492760 96 | gc #26: min 19085432, max 33286216, est base 24956242, curr base 26803600 97 | gc #27: min 19085432, max 33286216, est base 25127122, curr base 26665048 98 | gc #28: min 19085432, max 33286216, est base 25357309, curr base 27428992 99 | gc #29: min 19085432, max 33286216, est base 25519102, curr base 26975240 100 | gc #30: min 19085432, max 33286216, est base 25830428, curr base 28632368 101 | gc #31: min 19085432, max 33286216, est base 26113116, curr base 28657312 102 | gc #32: min 19085432, max 33286216, est base 26474999, curr base 29731952 103 | gc #33: min 19085432, max 41429616, est base 27970460, curr base 41429616 104 | gc #34: min 19085432, max 41429616, est base 29262386, curr base 40889728 105 | gc #35: min 19085432, max 41429616, est base 29402336, curr base 30661888 106 | gc #36: min 19085432, max 41429616, est base 29602979, curr base 31408768 107 | gc #37: min 19085432, max 42724544, est base 30915135, curr base 42724544 108 | gc #38: min 19085432, max 42724544, est base 31095390, curr base 32717688 109 | gc #39: min 19085432, max 42724544, est base 31907458, curr base 39216072 110 | gc #40: min 19085432, max 42724544, est base 32093021, curr base 33763088 111 | gc #41: min 19085432, max 42724544, est base 32281586, curr base 33978672 112 | gc #42: min 19085432, max 42724544, est base 32543090, curr base 34896632 113 | gc #43: min 19085432, max 42724544, est base 32743548, curr base 34547672 114 | gc #44: min 19085432, max 42724544, est base 33191109, curr base 37219160 115 | gc #45: min 19085432, max 42724544, est base 33659862, curr base 37878640 116 | gc #46: min 19085432, max 42724544, est base 34162262, curr base 38683864 117 | gc #47: min 19085432, max 42724544, est base 34624103, curr base 38780680 118 | gc #48: min 19085432, max 42724544, est base 35125267, curr base 39635752 119 | gc #49: min 19085432, max 42724544, est base 35547207, curr base 39344672 120 | gc #50: min 19085432, max 42724544, est base 35827942, curr base 38354560 121 | gc #51: min 19085432, max 42724544, est base 36185625, curr base 39404776 122 | gc #52: min 19085432, max 52995432, est base 37866605, curr base 52995432 123 | gc #53: min 19085432, max 52995432, est base 39230884, curr base 51509400 124 | gc #54: min 19085432, max 52995432, est base 39651220, curr base 43434248 125 | gc #55: min 19085432, max 52995432, est base 40010377, curr base 43242792 126 | gc #56: min 19085432, max 52995432, est base 40443827, curr base 44344880 127 | gc #57: min 19085432, max 52995432, est base 40979365, curr base 45799208 128 | gc #58: min 19085432, max 52995432, est base 41337723, curr base 44562952 129 | gc #59: min 19085432, max 57831608, est base 42987111, curr base 57831608 130 | hit /gc for first time 131 | gc #60: min 19085432, max 57831608, est base 42763791, curr base 40753920 132 | gc #61: min 19085432, max 57831608, est base 42427528, curr base 39401168 133 | gc #62: min 19085432, max 57831608, est base 42125779, curr base 39410040 134 | gc #63: min 19085432, max 57831608, est base 41850385, curr base 39371848 135 | gc #64: min 19085432, max 57831608, est base 41606578, curr base 39412320 136 | gc #65: min 19085432, max 57831608, est base 41386124, curr base 39402040 137 | ``` 138 |
139 | This test, after storing some basic data in the request's Raven context, generates an error which Raven's express error handling middleware will capture. We can see that we started at a base of ~11.6MB, climbed steadily throughout the test to ~40-50MB toward the end, returned to ~39.4MB after the test ends, and were then still at ~39.4MB after 30 seconds and more GCing. This was worrysome, being 30MB over our baseline after 1000 captures. Something was up with capturing exceptions and we uncovered and fixed a memory leak as a result. Now the test returns to a baseline of ~13MB; the slight increase over 11.6MB is due to some warmup costs, but the marginal cost of additional capturing is zero (i.e. we return to that ~13MB baseline whether we do 1000 captures or 5000). 140 | -------------------------------------------------------------------------------- /test/manual/context-memory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Raven = require('../../'); 3 | Raven.config('https://public:private@app.getsentry.com/12345').install(); 4 | 5 | // We create a bunch of contexts, capture some breadcrumb data in all of them, 6 | // then watch memory usage. It'll go up to ~40 megs then after 10 or 20 seconds 7 | // gc will drop it back to ~5. 8 | 9 | console.log(process.memoryUsage()); 10 | for (var i = 0; i < 10000; i++) { 11 | Raven.context(function () { 12 | Raven.captureBreadcrumb({ message: Array(1000).join('.') }); 13 | setTimeout(function () { 14 | Raven.captureBreadcrumb({ message: Array(1000).join('a') }); 15 | }, 2000); 16 | }); 17 | } 18 | 19 | console.log(process.memoryUsage()); 20 | setInterval(function () { 21 | console.log(process.memoryUsage()); 22 | }, 1000); 23 | -------------------------------------------------------------------------------- /test/manual/express-patient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // make console log not print so when we hammer the console endpoint we don't flood the terminal 4 | console.log = function () {}; 5 | 6 | var Raven = require('../../'); 7 | 8 | var sentryDsn = 'https://public:private@app.getsentry.com/269'; 9 | Raven.config(sentryDsn, { 10 | autoBreadcrumbs: true 11 | }).install(); 12 | 13 | var util = require('util'); 14 | var http = require('http'); 15 | var nock = require('nock'); 16 | 17 | // have to call this for each request :/ ref https://github.com/node-nock/nock#read-this---about-interceptors 18 | function nockRequest() { 19 | nock('https://app.getsentry.com') 20 | .filteringRequestBody(/.*/, '*') 21 | .post('/api/269/store/', '*') 22 | .reply(200, 'OK'); 23 | } 24 | 25 | var memwatch = require('memwatch-next'); 26 | memwatch.on('stats', function (stats) { 27 | process._rawDebug(util.format('gc #%d: min %d, max %d, est base %d, curr base %d', 28 | stats.num_full_gc, stats.min, stats.max, stats.estimated_base, stats.current_base 29 | )); 30 | }); 31 | 32 | var express = require('express'); 33 | var app = express(); 34 | 35 | var hitBefore = {}; 36 | 37 | app.use(Raven.requestHandler()); 38 | 39 | app.use(function (req, res, next) { 40 | if (!hitBefore[req.url]) { 41 | hitBefore[req.url] = true; 42 | process._rawDebug('hit ' + req.url + ' for first time'); 43 | } 44 | next(); 45 | }); 46 | 47 | app.get('/context/basic', function (req, res, next) { 48 | Raven.setContext({ 49 | extra: { 50 | example: 'hey look we set some example context data yay', 51 | } 52 | }); 53 | res.textToSend = 'hello there! we set some stuff to the context'; 54 | next(); 55 | }); 56 | 57 | app.get('/breadcrumbs/capture', function (req, res, next) { 58 | Raven.captureBreadcrumb({ 59 | message: 'Captured example breadcrumb', 60 | category: 'log', 61 | data: { 62 | example: 'hey look we captured this example breadcrumb yay' 63 | } 64 | }); 65 | res.textToSend = 'hello there! we captured an example breadcrumb'; 66 | next(); 67 | }); 68 | 69 | app.get('/breadcrumbs/auto/console', function (req, res, next) { 70 | console.log('hello there! i am printing to the console!'); 71 | res.textToSend = 'hello there! we printed to the console'; 72 | next(); 73 | }); 74 | 75 | app.get('/breadcrumbs/auto/http', function (req, res, next) { 76 | var scope = nock('http://www.example.com') 77 | .get('/hello') 78 | .reply(200, 'hello world'); 79 | 80 | http.get('http://www.example.com/hello', function (nockRes) { 81 | scope.done(); 82 | res.textToSend = 'hello there! we got hello world from example.com'; 83 | next(); 84 | }).on('error', next); 85 | }); 86 | 87 | app.get('/hello', function (req, res, next) { 88 | res.textToSend = 'hello!'; 89 | next(); 90 | }); 91 | 92 | app.get('/gc', function (req, res, next) { 93 | memwatch.gc(); 94 | res.textToSend = 'collected garbage'; 95 | next(); 96 | }); 97 | 98 | app.get('/shutdown', function (req, res, next) { 99 | setTimeout(function () { 100 | server.close(function () { // eslint-disable-line no-use-before-define 101 | process.exit(); 102 | }); 103 | }, 100); 104 | return res.send('shutting down'); 105 | }); 106 | 107 | app.get('/capture', function (req, res, next) { 108 | for (var i = 0; i < 1000; ++i) { 109 | nockRequest(); 110 | Raven.captureException(new Error('oh no an exception to capture')); 111 | } 112 | memwatch.gc(); 113 | res.textToSend = 'capturing an exception!'; 114 | next(); 115 | }); 116 | 117 | app.get('/capture_large_source', function (req, res, next) { 118 | nockRequest(); 119 | 120 | // largeModule.run recurses 1000 times, largeModule is a 5MB file 121 | // if we read the largeModule source once for each frame, we'll use a ton of memory 122 | var largeModule = require('./largeModule'); 123 | 124 | try { 125 | largeModule.run(); 126 | } catch (e) { 127 | Raven.captureException(e); 128 | } 129 | 130 | memwatch.gc(); 131 | res.textToSend = 'capturing an exception!'; 132 | next(); 133 | }); 134 | 135 | app.use(function (req, res, next) { 136 | if (req.query.doError) { 137 | nockRequest(); 138 | return next(new Error(res.textToSend)); 139 | } 140 | return res.send(res.textToSend); 141 | }); 142 | 143 | app.use(Raven.errorHandler()); 144 | 145 | app.use(function (err, req, res, next) { 146 | return res.status(500).send('oh no there was an error: ' + err.message); 147 | }); 148 | 149 | var server = app.listen(3000, function () { 150 | process._rawDebug('patient is waiting to be poked on port 3000'); 151 | memwatch.gc(); 152 | }); 153 | -------------------------------------------------------------------------------- /test/manual/manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var child_process = require('child_process'); 5 | 6 | var child; 7 | 8 | var serverPath = path.join(__dirname, 'express-patient.js'); 9 | 10 | function startChild() { 11 | console.log('starting child'); 12 | child = child_process.spawn('node', [serverPath]); 13 | child.stdout.pipe(process.stdout); 14 | child.stderr.pipe(process.stderr); 15 | child.on('exit', function () { 16 | console.log('child exited'); 17 | startChild(); 18 | }); 19 | } 20 | 21 | startChild(); 22 | -------------------------------------------------------------------------------- /test/manual/poke-patient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gc() { 4 | sleep 2 5 | curl localhost:3000/gc 6 | } 7 | 8 | gc_restart() { 9 | gc 10 | sleep 2 11 | curl localhost:3000/shutdown 12 | sleep 2 13 | } 14 | 15 | curl localhost:3000/capture 16 | gc 17 | gc 18 | curl localhost:3000/capture 19 | gc 20 | gc_restart 21 | 22 | curl localhost:3000/capture_large_source 23 | gc 24 | gc 25 | gc_restart 26 | 27 | ab -c 5 -n 5000 localhost:3000/hello 28 | gc_restart 29 | 30 | ab -c 5 -n 5000 localhost:3000/context/basic 31 | gc_restart 32 | 33 | ab -c 5 -n 5000 localhost:3000/breadcrumbs/capture 34 | gc_restart 35 | 36 | ab -c 5 -n 5000 localhost:3000/breadcrumbs/auto/console 37 | gc_restart 38 | 39 | ab -c 5 -n 5000 localhost:3000/breadcrumbs/auto/http 40 | gc_restart 41 | 42 | 43 | ab -c 5 -n 2000 localhost:3000/hello?doError=true 44 | gc_restart 45 | 46 | ab -c 5 -n 2000 localhost:3000/context/basic?doError=true 47 | gc_restart 48 | 49 | ab -c 5 -n 2000 localhost:3000/breadcrumbs/capture?doError=true 50 | gc_restart 51 | 52 | ab -c 5 -n 2000 localhost:3000/breadcrumbs/auto/console?doError=true 53 | gc_restart 54 | 55 | ab -c 5 -n 2000 localhost:3000/breadcrumbs/auto/http?doError=true 56 | gc_restart 57 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --slow 20 -------------------------------------------------------------------------------- /test/raven.parsers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var assert = require('assert'); 5 | var raven = require('../'); 6 | 7 | raven.parsers = require('../lib/parsers'); 8 | 9 | describe('raven.parsers', function() { 10 | describe('#parseText()', function() { 11 | it('should parse some text without kwargs', function() { 12 | var parsed = raven.parsers.parseText('Howdy'); 13 | parsed.message.should.equal('Howdy'); 14 | }); 15 | 16 | it('should parse some text with kwargs', function() { 17 | var parsed = raven.parsers.parseText('Howdy', { 18 | foo: 'bar' 19 | }); 20 | parsed.message.should.equal('Howdy'); 21 | parsed.foo.should.equal('bar'); 22 | }); 23 | }); 24 | 25 | describe('#parseRequest()', function() { 26 | it('should parse a request object', function() { 27 | var mockReq = { 28 | method: 'GET', 29 | url: '/some/path?key=value', 30 | headers: { 31 | host: 'mattrobenolt.com' 32 | }, 33 | body: '', 34 | cookies: {}, 35 | socket: { 36 | encrypted: true 37 | }, 38 | connection: { 39 | remoteAddress: '127.0.0.1' 40 | } 41 | }; 42 | var parsed = raven.parsers.parseRequest(mockReq); 43 | parsed.should.have.property('request'); 44 | parsed.request.url.should.equal('https://mattrobenolt.com/some/path?key=value'); 45 | parsed.user.ip_address.should.equal('127.0.0.1'); 46 | }); 47 | 48 | describe('`headers` detection', function() { 49 | it('should detect headers via `req.headers`', function() { 50 | var mockReq = { 51 | method: 'GET', 52 | hostname: 'mattrobenolt.com', 53 | url: '/some/path?key=value', 54 | headers: { 55 | foo: 'bar' 56 | } 57 | }; 58 | 59 | var parsed = raven.parsers.parseRequest(mockReq); 60 | 61 | parsed.request.headers.should.eql({ 62 | foo: 'bar' 63 | }); 64 | }); 65 | 66 | it('should detect headers via `req.header`', function() { 67 | var mockReq = { 68 | method: 'GET', 69 | hostname: 'mattrobenolt.com', 70 | url: '/some/path?key=value', 71 | header: { 72 | foo: 'bar' 73 | } 74 | }; 75 | 76 | var parsed = raven.parsers.parseRequest(mockReq); 77 | 78 | parsed.request.headers.should.eql({ 79 | foo: 'bar' 80 | }); 81 | }); 82 | }); 83 | 84 | describe('`method` detection', function() { 85 | it('should detect method via `req.method`', function() { 86 | var mockReq = { 87 | method: 'GET', 88 | hostname: 'mattrobenolt.com', 89 | url: '/some/path?key=value' 90 | }; 91 | 92 | var parsed = raven.parsers.parseRequest(mockReq); 93 | 94 | parsed.request.method.should.equal('GET'); 95 | }); 96 | }); 97 | 98 | describe('`host` detection', function() { 99 | it('should detect host via `req.hostname`', function() { 100 | var mockReq = { 101 | method: 'GET', 102 | hostname: 'mattrobenolt.com', 103 | url: '/some/path?key=value' 104 | }; 105 | 106 | var parsed = raven.parsers.parseRequest(mockReq); 107 | 108 | parsed.request.url.should.equal('http://mattrobenolt.com/some/path?key=value'); 109 | }); 110 | 111 | it('should detect host via deprecated `req.host`', function() { 112 | var mockReq = { 113 | method: 'GET', 114 | host: 'mattrobenolt.com', 115 | url: '/some/path?key=value' 116 | }; 117 | 118 | var parsed = raven.parsers.parseRequest(mockReq); 119 | 120 | parsed.request.url.should.equal('http://mattrobenolt.com/some/path?key=value'); 121 | }); 122 | 123 | it('should detect host via `req.header.host`', function() { 124 | var mockReq = { 125 | method: 'GET', 126 | header: { 127 | host: 'mattrobenolt.com' 128 | }, 129 | url: '/some/path?key=value' 130 | }; 131 | 132 | var parsed = raven.parsers.parseRequest(mockReq); 133 | 134 | parsed.request.url.should.equal('http://mattrobenolt.com/some/path?key=value'); 135 | }); 136 | 137 | it('should detect host via `req.headers.host`', function() { 138 | var mockReq = { 139 | method: 'GET', 140 | headers: { 141 | host: 'mattrobenolt.com' 142 | }, 143 | url: '/some/path?key=value' 144 | }; 145 | 146 | var parsed = raven.parsers.parseRequest(mockReq); 147 | 148 | parsed.request.url.should.equal('http://mattrobenolt.com/some/path?key=value'); 149 | }); 150 | 151 | it('should fallback to if host is not available', function() { 152 | var mockReq = { 153 | method: 'GET', 154 | url: '/some/path?key=value' 155 | }; 156 | 157 | var parsed = raven.parsers.parseRequest(mockReq); 158 | 159 | parsed.request.url.should.equal('http:///some/path?key=value'); 160 | }); 161 | }); 162 | 163 | describe('`protocol` detection', function() { 164 | it('should detect protocol via `req.protocol`', function() { 165 | var mockReq = { 166 | method: 'GET', 167 | url: '/some/path?key=value', 168 | headers: { 169 | host: 'mattrobenolt.com' 170 | }, 171 | protocol: 'https', 172 | socket: { 173 | encrypted: false 174 | } 175 | }; 176 | 177 | var parsed = raven.parsers.parseRequest(mockReq); 178 | 179 | parsed.request.url.should.equal('https://mattrobenolt.com/some/path?key=value'); 180 | }); 181 | 182 | it('should detect protocol via `req.secure`', function() { 183 | var mockReq = { 184 | method: 'GET', 185 | url: '/some/path?key=value', 186 | headers: { 187 | host: 'mattrobenolt.com' 188 | }, 189 | secure: true, 190 | socket: { 191 | encrypted: false 192 | } 193 | }; 194 | 195 | var parsed = raven.parsers.parseRequest(mockReq); 196 | 197 | parsed.request.url.should.equal('https://mattrobenolt.com/some/path?key=value'); 198 | }); 199 | 200 | it('should detect protocol via `req.socket.encrypted`', function() { 201 | var mockReq = { 202 | method: 'GET', 203 | url: '/some/path?key=value', 204 | headers: { 205 | host: 'mattrobenolt.com' 206 | }, 207 | socket: { 208 | encrypted: true 209 | } 210 | }; 211 | 212 | var parsed = raven.parsers.parseRequest(mockReq); 213 | 214 | parsed.request.url.should.equal('https://mattrobenolt.com/some/path?key=value'); 215 | }); 216 | }); 217 | 218 | describe('`cookie` detection', function() { 219 | it('should parse `req.headers.cookie`', function() { 220 | var mockReq = { 221 | method: 'GET', 222 | url: '/some/path?key=value', 223 | headers: { 224 | host: 'mattrobenolt.com', 225 | cookie: 'foo=bar' 226 | } 227 | }; 228 | 229 | var parsed = raven.parsers.parseRequest(mockReq); 230 | parsed.request.cookies.should.eql({ 231 | foo: 'bar' 232 | }); 233 | }); 234 | 235 | it('should parse `req.header.cookie`', function() { 236 | var mockReq = { 237 | method: 'GET', 238 | url: '/some/path?key=value', 239 | header: { 240 | host: 'mattrobenolt.com', 241 | cookie: 'foo=bar' 242 | } 243 | }; 244 | 245 | var parsed = raven.parsers.parseRequest(mockReq); 246 | parsed.request.cookies.should.eql({ 247 | foo: 'bar' 248 | }); 249 | }); 250 | }); 251 | 252 | describe('`query` detection', function() { 253 | it('should detect query via `req.query`', function() { 254 | var mockReq = { 255 | method: 'GET', 256 | hostname: 'mattrobenolt.com', 257 | url: '/some/path?key=value', 258 | query: { 259 | some: 'key' 260 | } 261 | }; 262 | 263 | var parsed = raven.parsers.parseRequest(mockReq); 264 | 265 | parsed.request.query_string.should.eql({ 266 | some: 'key' 267 | }); 268 | }); 269 | 270 | it('should detect query via `req.url`', function() { 271 | var mockReq = { 272 | method: 'GET', 273 | hostname: 'mattrobenolt.com', 274 | url: '/some/path?foo=bar' 275 | }; 276 | 277 | // using assert instead of should here because query_string has no prototype 278 | // https://github.com/nodejs/node/pull/6289 279 | var parsed = raven.parsers.parseRequest(mockReq); 280 | var expected = {foo: 'bar'}; 281 | assert.deepEqual(parsed.request.query_string, expected); 282 | }); 283 | }); 284 | 285 | describe('`ip` detection', function() { 286 | it('should detect ip via `req.ip`', function() { 287 | var mockReq = { 288 | method: 'GET', 289 | url: '/some/path?key=value', 290 | headers: { 291 | hostname: 'mattrobenolt.com' 292 | }, 293 | ip: '127.0.0.1' 294 | }; 295 | 296 | var parsed = raven.parsers.parseRequest(mockReq); 297 | 298 | parsed.user.ip_address.should.equal('127.0.0.1'); 299 | }); 300 | 301 | it('should detect ip via `req.connection.remoteAddress`', function() { 302 | var mockReq = { 303 | method: 'GET', 304 | url: '/some/path?key=value', 305 | headers: { 306 | hostname: 'mattrobenolt.com' 307 | }, 308 | connection: { 309 | remoteAddress: '127.0.0.1' 310 | } 311 | }; 312 | 313 | var parsed = raven.parsers.parseRequest(mockReq); 314 | 315 | parsed.user.ip_address.should.equal('127.0.0.1'); 316 | }); 317 | }); 318 | 319 | describe('`url` detection', function() { 320 | it('should detect url via `req.originalUrl`', function() { 321 | var mockReq = { 322 | method: 'GET', 323 | protocol: 'https', 324 | hostname: 'mattrobenolt.com', 325 | originalUrl: '/some/path?key=value' 326 | }; 327 | 328 | var parsed = raven.parsers.parseRequest(mockReq); 329 | 330 | parsed.request.url.should.equal('https://mattrobenolt.com/some/path?key=value'); 331 | }); 332 | 333 | it('should detect url via `req.url`', function() { 334 | var mockReq = { 335 | method: 'GET', 336 | protocol: 'https', 337 | hostname: 'mattrobenolt.com', 338 | url: '/some/path?key=value' 339 | }; 340 | 341 | var parsed = raven.parsers.parseRequest(mockReq); 342 | 343 | parsed.request.url.should.equal('https://mattrobenolt.com/some/path?key=value'); 344 | }); 345 | }); 346 | 347 | describe('`body` detection', function() { 348 | it('should detect body via `req.body`', function() { 349 | var mockReq = { 350 | method: 'GET', 351 | hostname: 'mattrobenolt.com', 352 | url: '/some/path?key=value', 353 | body: 'foo=bar' 354 | }; 355 | 356 | var parsed = raven.parsers.parseRequest(mockReq); 357 | 358 | parsed.request.data.should.equal('foo=bar'); 359 | }); 360 | 361 | it('should fallback to if body is not available', function() { 362 | var mockReq = { 363 | method: 'POST', 364 | hostname: 'mattrobenolt.com', 365 | url: '/some/path?key=value' 366 | }; 367 | 368 | var parsed = raven.parsers.parseRequest(mockReq); 369 | 370 | parsed.request.data.should.equal(''); 371 | }); 372 | 373 | it('should not fallback to if GET', function() { 374 | var mockReq = { 375 | method: 'GET', 376 | hostname: 'mattrobenolt.com', 377 | url: '/some/path?key=value' 378 | }; 379 | 380 | var parsed = raven.parsers.parseRequest(mockReq); 381 | 382 | (typeof parsed.request.data === 'undefined').should.be.ok; 383 | }); 384 | 385 | it('should make sure that body is a string', function() { 386 | var mockReq = { 387 | method: 'POST', 388 | hostname: 'mattrobenolt.com', 389 | url: '/some/path?key=value', 390 | body: { 391 | foo: true 392 | } 393 | }; 394 | 395 | var parsed = raven.parsers.parseRequest(mockReq); 396 | parsed.request.data.should.equal('{"foo":true}'); 397 | }); 398 | }); 399 | 400 | describe('`user` detection', function() { 401 | it('should assign req.user to kwargs', function() { 402 | var mockReq = { 403 | method: 'POST', 404 | hostname: 'example.org', 405 | url: '/some/path?key=value', 406 | user: { 407 | username: 'janedoe', 408 | email: 'hello@janedoe.com' 409 | } 410 | }; 411 | 412 | var parsed = raven.parsers.parseRequest(mockReq); 413 | parsed.should.have.property('user', { 414 | username: 'janedoe', 415 | email: 'hello@janedoe.com' 416 | }); 417 | }); 418 | 419 | it('should add ip address to user if available', function() { 420 | var mockReq = { 421 | method: 'POST', 422 | hostname: 'example.org', 423 | url: '/some/path?key=value', 424 | ip: '127.0.0.1', 425 | user: { 426 | username: 'janedoe', 427 | email: 'hello@janedoe.com' 428 | } 429 | }; 430 | 431 | var parsed = raven.parsers.parseRequest(mockReq); 432 | parsed.should.have.property('user', { 433 | username: 'janedoe', 434 | email: 'hello@janedoe.com', 435 | ip_address: '127.0.0.1' 436 | }); 437 | }); 438 | 439 | describe('parseUser option', function() { 440 | var mockReq = { 441 | method: 'POST', 442 | hostname: 'example.org', 443 | url: '/some/path?key=value', 444 | ip: '127.0.0.1', 445 | user: { 446 | username: 'janedoe', 447 | email: 'hello@janedoe.com', 448 | random: 'random' 449 | } 450 | }; 451 | 452 | it('should limit to default whitelisted fields', function() { 453 | var parsed = raven.parsers.parseRequest(mockReq); 454 | parsed.should.have.property('user', { 455 | username: 'janedoe', 456 | email: 'hello@janedoe.com', 457 | ip_address: '127.0.0.1' 458 | }); 459 | }); 460 | 461 | it('should limit to provided whitelisted fields array', function() { 462 | var parsed = raven.parsers.parseRequest(mockReq, ['username', 'random']); 463 | parsed.should.have.property('user', { 464 | username: 'janedoe', 465 | random: 'random', 466 | ip_address: '127.0.0.1' 467 | }); 468 | }); 469 | 470 | it('should parse all fields when passed true', function() { 471 | var parsed = raven.parsers.parseRequest(mockReq, true); 472 | parsed.should.have.property('user', { 473 | username: 'janedoe', 474 | email: 'hello@janedoe.com', 475 | random: 'random', 476 | ip_address: '127.0.0.1' 477 | }); 478 | }); 479 | 480 | it('should parse nothing when passed false', function() { 481 | var parsed = raven.parsers.parseRequest(mockReq, false); 482 | parsed.should.not.have.property('user'); 483 | }); 484 | 485 | it('should take a custom parsing function', function() { 486 | var parsed = raven.parsers.parseRequest(mockReq, function(req) { 487 | return {user: req.user.username}; 488 | }); 489 | parsed.should.have.property('user', { 490 | user: 'janedoe', 491 | ip_address: '127.0.0.1' 492 | }); 493 | }); 494 | }); 495 | }); 496 | }); 497 | 498 | describe('#parseError()', function() { 499 | it('should parse plain Error object', function(done) { 500 | raven.parsers.parseError(new Error(), {}, function(parsed) { 501 | parsed.message.should.equal('Error: '); 502 | parsed.should.have.property('exception'); 503 | parsed.exception[0].type.should.equal('Error'); 504 | parsed.exception[0].value.should.equal(''); 505 | parsed.exception[0].stacktrace.should.have.property('frames'); 506 | done(); 507 | }); 508 | }); 509 | 510 | it('should parse Error with message', function(done) { 511 | raven.parsers.parseError(new Error('Crap'), {}, function(parsed) { 512 | parsed.message.should.equal('Error: Crap'); 513 | parsed.should.have.property('exception'); 514 | parsed.exception[0].type.should.equal('Error'); 515 | parsed.exception[0].value.should.equal('Crap'); 516 | parsed.exception[0].stacktrace.should.have.property('frames'); 517 | done(); 518 | }); 519 | }); 520 | 521 | it('should parse TypeError with message', function(done) { 522 | raven.parsers.parseError(new TypeError('Crap'), {}, function(parsed) { 523 | parsed.message.should.equal('TypeError: Crap'); 524 | parsed.should.have.property('exception'); 525 | parsed.exception[0].type.should.equal('TypeError'); 526 | parsed.exception[0].value.should.equal('Crap'); 527 | parsed.exception[0].stacktrace.should.have.property('frames'); 528 | done(); 529 | }); 530 | }); 531 | 532 | it('should parse thrown Error', function(done) { 533 | try { 534 | throw new Error('Derp'); 535 | } catch (e) { 536 | raven.parsers.parseError(e, {}, function(parsed) { 537 | parsed.message.should.equal('Error: Derp'); 538 | parsed.should.have.property('exception'); 539 | parsed.exception[0].type.should.equal('Error'); 540 | parsed.exception[0].value.should.equal('Derp'); 541 | parsed.exception[0].stacktrace.should.have.property('frames'); 542 | done(); 543 | }); 544 | } 545 | }); 546 | 547 | it('should allow specifying a custom `culprit`', function(done) { 548 | try { 549 | throw new Error('Foobar'); 550 | } catch (e) { 551 | raven.parsers.parseError( 552 | e, 553 | { 554 | culprit: 'foobar' 555 | }, 556 | function(parsed) { 557 | parsed.culprit.should.equal('foobar'); 558 | done(); 559 | } 560 | ); 561 | } 562 | }); 563 | 564 | it('should allow specifying a custom `transaction`', function(done) { 565 | try { 566 | throw new Error('Foobar'); 567 | } catch (e) { 568 | raven.parsers.parseError( 569 | e, 570 | { 571 | transaction: 'foobar' 572 | }, 573 | function(parsed) { 574 | parsed.transaction.should.equal('foobar'); 575 | done(); 576 | } 577 | ); 578 | } 579 | }); 580 | 581 | it('should have a string stack after parsing', function(done) { 582 | try { 583 | throw new Error('Derp'); 584 | } catch (e) { 585 | raven.parsers.parseError(e, {}, function(parsed) { 586 | e.stack.should.be.a.String; 587 | done(); 588 | }); 589 | } 590 | }); 591 | 592 | it('should parse caught real error', function(done) { 593 | /* eslint new-cap:0 */ 594 | try { 595 | var o = {}; 596 | o['...'].Derp(); 597 | } catch (e) { 598 | raven.parsers.parseError(e, {}, function(parsed) { 599 | parsed.message.should.containEql('TypeError'); 600 | parsed.message.should.containEql('Derp'); 601 | parsed.should.have.property('exception'); 602 | parsed.exception[0].type.should.equal('TypeError'); 603 | parsed.exception[0].value.should.containEql('Derp'); 604 | parsed.exception[0].stacktrace.should.have.property('frames'); 605 | done(); 606 | }); 607 | } 608 | }); 609 | 610 | it('should parse an error with additional information', function(done) { 611 | try { 612 | var err = new Error('boom'); 613 | err.prop = 'value'; 614 | throw err; 615 | } catch (e) { 616 | raven.parsers.parseError(e, {}, function(parsed) { 617 | parsed.should.have.property('exception'); 618 | parsed.exception[0].stacktrace.should.have.property('frames'); 619 | parsed.should.have.property('extra'); 620 | parsed.extra.should.have.property('Error'); 621 | parsed.extra.Error.should.have.property('prop'); 622 | parsed.extra.Error.prop.should.equal('value'); 623 | done(); 624 | }); 625 | } 626 | }); 627 | 628 | it('should read name from an Error constructor', function(done) { 629 | var err = new Error(); 630 | raven.parsers.parseError(err, {}, function(parsed) { 631 | parsed.message.should.equal('Error: '); 632 | parsed.exception[0].type.should.equal('Error'); 633 | done(); 634 | }); 635 | }); 636 | 637 | it('should read name from an Error constructor for custom errors', function(done) { 638 | // https://gist.github.com/justmoon/15511f92e5216fa2624b 639 | function CustomError(message) { 640 | Error.captureStackTrace(this, this.constructor); 641 | this.name = this.constructor.name; 642 | this.message = message; 643 | } 644 | 645 | util.inherits(CustomError, Error); 646 | 647 | var err = new CustomError('boom'); 648 | raven.parsers.parseError(err, {}, function(parsed) { 649 | parsed.message.should.equal('CustomError: boom'); 650 | parsed.exception[0].type.should.equal('CustomError'); 651 | done(); 652 | }); 653 | }); 654 | 655 | it('should allow for overriding name for built-in errors', function(done) { 656 | var err = new Error(); 657 | err.name = 'foo'; 658 | raven.parsers.parseError(err, {}, function(parsed) { 659 | parsed.message.should.equal('foo: '); 660 | parsed.exception[0].type.should.equal('foo'); 661 | done(); 662 | }); 663 | }); 664 | 665 | it('should allow for overriding name for custom errors', function(done) { 666 | // https://gist.github.com/justmoon/15511f92e5216fa2624b 667 | function CustomError(message) { 668 | Error.captureStackTrace(this, this.constructor); 669 | this.name = 'foo'; 670 | this.message = message; 671 | } 672 | 673 | util.inherits(CustomError, Error); 674 | 675 | var err = new CustomError('boom'); 676 | raven.parsers.parseError(err, {}, function(parsed) { 677 | parsed.message.should.equal('foo: boom'); 678 | parsed.exception[0].type.should.equal('foo'); 679 | done(); 680 | }); 681 | }); 682 | 683 | it('should parse Error with non-string type', function(done) { 684 | var err = new Error(); 685 | err.name = {}; 686 | raven.parsers.parseError(err, {}, function(parsed) { 687 | parsed.message.should.equal('[object Object]: '); 688 | parsed.exception[0].type.should.equal('[object Object]'); 689 | parsed.exception[0].value.should.equal(''); 690 | done(); 691 | }); 692 | }); 693 | }); 694 | }); 695 | -------------------------------------------------------------------------------- /test/raven.transports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var transports = require('../lib/transports'); 4 | 5 | describe('transports', function() { 6 | it('should have an http/s agent with correct config attached by default', function() { 7 | var http = transports.http; 8 | http.agent.should.exist; 9 | http.agent.keepAlive.should.equal(true); 10 | http.agent.maxSockets.should.equal(100); 11 | 12 | var https = transports.https; 13 | https.agent.should.exist; 14 | https.agent.keepAlive.should.equal(true); 15 | https.agent.maxSockets.should.equal(100); 16 | }); 17 | 18 | it('should emit error when requests queued over the limit', function(done) { 19 | var http = transports.http; 20 | var _cachedAgent = http.options.agent; 21 | 22 | var requests = {}; 23 | for (var i = 0; i < 10; i++) { 24 | requests[i] = 'req'; 25 | } 26 | 27 | http.agent = Object.assign({}, _cachedAgent, { 28 | getName: function() { 29 | return 'foo:123'; 30 | }, 31 | requests: { 32 | 'foo:123': requests 33 | } 34 | }); 35 | 36 | http.send({ 37 | dsn: { 38 | host: 'foo', 39 | port: 123 40 | }, 41 | maxReqQueueCount: 5, 42 | emit: function(event, body) { 43 | event.should.equal('error'); 44 | body.message.should.equal('client req queue is full..'); 45 | http.options.agent = _cachedAgent; 46 | done(); 47 | } 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/raven.utils.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:0, no-undefined:0 */ 2 | 'use strict'; 3 | 4 | var versionRegexp = /^v(\d+)\.(\d+)\.(\d+)$/i; 5 | var majorVersion = parseInt(versionRegexp.exec(process.version)[1], 10); 6 | 7 | var raven = require('../'); 8 | 9 | var _oldConsoleWarn = console.warn; 10 | 11 | function mockConsoleWarn() { 12 | console.warn = function() { 13 | console.warn._called = true; 14 | ++console.warn._callCount; 15 | }; 16 | console.warn._called = false; 17 | console.warn._callCount = 0; 18 | } 19 | 20 | function restoreConsoleWarn() { 21 | console.warn = _oldConsoleWarn; 22 | } 23 | 24 | describe('raven.utils', function() { 25 | describe('#parseDSN()', function() { 26 | it('should parse hosted Sentry DSN without path', function() { 27 | var dsn = raven.utils.parseDSN( 28 | 'https://8769c40cf49c4cc58b51fa45d8e2d166:296768aa91084e17b5ac02d3ad5bc7e7@app.getsentry.com/269' 29 | ); 30 | var expected = { 31 | protocol: 'https', 32 | public_key: '8769c40cf49c4cc58b51fa45d8e2d166', 33 | private_key: '296768aa91084e17b5ac02d3ad5bc7e7', 34 | host: 'app.getsentry.com', 35 | path: '/', 36 | project_id: '269', 37 | port: 443 38 | }; 39 | dsn.should.eql(expected); 40 | }); 41 | 42 | it('should parse http not on hosted Sentry with path', function() { 43 | var dsn = raven.utils.parseDSN( 44 | 'http://8769c40cf49c4cc58b51fa45d8e2d166:296768aa91084e17b5ac02d3ad5bc7e7@mysentry.com/some/other/path/269' 45 | ); 46 | var expected = { 47 | protocol: 'http', 48 | public_key: '8769c40cf49c4cc58b51fa45d8e2d166', 49 | private_key: '296768aa91084e17b5ac02d3ad5bc7e7', 50 | host: 'mysentry.com', 51 | path: '/some/other/path/', 52 | project_id: '269', 53 | port: 80 54 | }; 55 | dsn.should.eql(expected); 56 | }); 57 | 58 | it('should parse DSN with non-standard port', function() { 59 | var dsn = raven.utils.parseDSN( 60 | 'https://8769c40cf49c4cc58b51fa45d8e2d166:296768aa91084e17b5ac02d3ad5bc7e7@mysentry.com:8443/some/other/path/269' 61 | ); 62 | var expected = { 63 | protocol: 'https', 64 | public_key: '8769c40cf49c4cc58b51fa45d8e2d166', 65 | private_key: '296768aa91084e17b5ac02d3ad5bc7e7', 66 | host: 'mysentry.com', 67 | path: '/some/other/path/', 68 | project_id: '269', 69 | port: 8443 70 | }; 71 | dsn.should.eql(expected); 72 | }); 73 | 74 | it('should return false for a falsey dns', function() { 75 | raven.utils.parseDSN(false).should.eql(false); 76 | raven.utils.parseDSN('').should.eql(false); 77 | }); 78 | 79 | it('show throw an Error on invalid transport protocol', function() { 80 | (function() { 81 | raven.utils.parseDSN( 82 | 'noop://8769c40cf49c4cc58b51fa45d8e2d166:296768aa91084e17b5ac02d3ad5bc7e7@mysentry.com:1234/some/other/path/269' 83 | ); 84 | }.should.throw()); 85 | }); 86 | 87 | it('should ignore a sub-transport protocol', function() { 88 | var dsn = raven.utils.parseDSN( 89 | 'gevent+https://8769c40cf49c4cc58b51fa45d8e2d166:296768aa91084e17b5ac02d3ad5bc7e7@mysentry.com:8443/some/other/path/269' 90 | ); 91 | var expected = { 92 | protocol: 'https', 93 | public_key: '8769c40cf49c4cc58b51fa45d8e2d166', 94 | private_key: '296768aa91084e17b5ac02d3ad5bc7e7', 95 | host: 'mysentry.com', 96 | path: '/some/other/path/', 97 | project_id: '269', 98 | port: 8443 99 | }; 100 | dsn.should.eql(expected); 101 | }); 102 | 103 | it('should parse DSN without private key', function() { 104 | var dsn = raven.utils.parseDSN( 105 | 'https://8769c40cf49c4cc58b51fa45d8e2d166@mysentry.com:8443/some/other/path/269' 106 | ); 107 | var expected = { 108 | protocol: 'https', 109 | public_key: '8769c40cf49c4cc58b51fa45d8e2d166', 110 | host: 'mysentry.com', 111 | path: '/some/other/path/', 112 | project_id: '269', 113 | port: 8443 114 | }; 115 | dsn.should.eql(expected); 116 | }); 117 | }); 118 | 119 | describe('#parseAuthHeader()', function() { 120 | it('should parse all parameters', function() { 121 | var timestamp = 12345, 122 | apiKey = 'abc', 123 | apiSecret = 'xyz'; 124 | var expected = 125 | 'Sentry sentry_version=5, sentry_timestamp=12345, sentry_client=raven-node/' + 126 | raven.version + 127 | ', sentry_key=abc, sentry_secret=xyz'; 128 | raven.utils.getAuthHeader(timestamp, apiKey, apiSecret).should.equal(expected); 129 | }); 130 | 131 | it('should skip sentry_secret if apiSecret not provided', function() { 132 | var timestamp = 12345, 133 | apiKey = 'abc'; 134 | var expected = 135 | 'Sentry sentry_version=5, sentry_timestamp=12345, sentry_client=raven-node/' + 136 | raven.version + 137 | ', sentry_key=abc'; 138 | raven.utils.getAuthHeader(timestamp, apiKey).should.equal(expected); 139 | }); 140 | }); 141 | 142 | describe('#parseStack()', function() { 143 | // needs new tests with a mock callsite object 144 | it('shouldnt barf on an invalid stack', function() { 145 | var parseStack = raven.utils.parseStack; 146 | var callback = function(frames) { 147 | frames.length.should.equal(0); 148 | }; 149 | parseStack('lol', callback); 150 | parseStack(void 0, callback); 151 | parseStack([], callback); 152 | parseStack( 153 | [ 154 | { 155 | lol: 1 156 | } 157 | ], 158 | callback 159 | ); 160 | }); 161 | 162 | it('should extract context from last stack line', function(done) { 163 | var parseStack = raven.utils.parseStack; 164 | var callback = function(frames) { 165 | var frame = frames.pop(); 166 | 167 | /* verify that the frame has the properties: pre_context, context_line 168 | and post_context, which are valid for in-app stack lines only.*/ 169 | frame.pre_context.should.be.an.instanceOf(Array); 170 | frame.context_line.should.be.type('string'); 171 | frame.context_line.trim().should.endWith('undeclared_function();'); 172 | frame.post_context.should.be.an.instanceOf(Array); 173 | 174 | frame.in_app.should.be.true; 175 | done(); 176 | }; 177 | try { 178 | // eslint-disable-next-line no-undef 179 | undeclared_function(); 180 | } catch (e) { 181 | parseStack(e, callback); 182 | } 183 | }); 184 | 185 | it('should trim long source line in surrounding source context', function(done) { 186 | var parseStack = raven.utils.parseStack; 187 | var callback = function(frames) { 188 | var frame = frames.pop(); 189 | frame.in_app.should.be.true; 190 | 191 | var lineBefore = frame.pre_context[frame.pre_context.length - 1].trim(); 192 | lineBefore.should.not.startWith('{snip}'); 193 | lineBefore.should.endWith('{snip}'); 194 | 195 | var lineOf = frame.context_line.trim(); 196 | lineOf.should.startWith('{snip}'); 197 | lineOf.should.endWith('{snip}'); 198 | lineOf.length.should.equal(154); // 140 limit + 7 for `{snip} ` and ` {snip}` 199 | lineOf.should.containEql("throw new Error('boom');"); 200 | 201 | var lineAfter = frame.post_context[0].trim(); 202 | lineAfter.should.not.startWith('{snip}'); 203 | lineAfter.should.endWith('{snip}'); 204 | done(); 205 | }; 206 | try { 207 | require('./fixtures/long-line')(); 208 | } catch (e) { 209 | parseStack(e, callback); 210 | } 211 | }); 212 | 213 | it('should treat windows files as being in app: in_app should be true', function(done) { 214 | var parseStack = raven.utils.parseStack; 215 | var callback = function(frames) { 216 | var frame = frames.pop(); 217 | frame.filename.should.be.type('string'); 218 | frame.filename.should.startWith('C:\\'); 219 | frame.in_app.should.be.true; 220 | done(); 221 | }; 222 | var err = new Error('some error message'); 223 | // get first line of err stack (line after err message) 224 | var firstFileLine = err.stack.split('\n')[1]; 225 | // replace first occurrence of "/" with C:\ to mock windows style 226 | var winFirstFileLine = firstFileLine.replace(/[/]/, 'C:\\'); 227 | // replace all remaining "/" with "\" 228 | winFirstFileLine = winFirstFileLine.replace(/[/]/g, '\\'); 229 | // create a "win-style" stack replacing the first err.stack line with our above win-style line 230 | err.stack = err.stack.replace(firstFileLine, winFirstFileLine); 231 | parseStack(err, callback); 232 | }); 233 | 234 | it('should mark node core library frames as not being in app', function(done) { 235 | var qsStringify = require('querystring').stringify; 236 | var parseStack = raven.utils.parseStack; 237 | 238 | var callback = function(frames) { 239 | var frame1 = frames.pop(); 240 | frame1.in_app.should.be.false; 241 | frame1.filename.should.equal('querystring.js'); 242 | 243 | var frame2 = frames.pop(); 244 | frame2.in_app.should.be.false; 245 | frame2.filename.should.equal('querystring.js'); 246 | 247 | done(); 248 | }; 249 | 250 | try { 251 | // Incomplete surrogate pair will cause qs.encode (used by qs.stringify) to throw 252 | qsStringify({a: '\uDCA9'}); 253 | } catch (e) { 254 | parseStack(e, callback); 255 | } 256 | }); 257 | 258 | it('should not read the same source file multiple times when getting source context lines', function(done) { 259 | var fs = require('fs'); 260 | var origReadFile = fs.readFile; 261 | var filesRead = []; 262 | 263 | fs.readFile = function(file) { 264 | filesRead.push(file); 265 | origReadFile.apply(this, arguments); 266 | }; 267 | 268 | function parseCallback(frames) { 269 | // first two frames will both be from this file, but we should have only read this file once 270 | var frame1 = frames.pop(); 271 | var frame2 = frames.pop(); 272 | frame1.context_line.trim().should.endWith("throw new Error('error');"); 273 | frame2.context_line.trim().should.endWith('nestedThrow();'); 274 | frame1.filename.should.equal(frame2.filename); 275 | 276 | var uniqueFilesRead = filesRead.filter(function(filename, idx, arr) { 277 | return arr.indexOf(filename) === idx; 278 | }); 279 | filesRead.length.should.equal(uniqueFilesRead.length); 280 | 281 | fs.readFile = origReadFile; 282 | done(); 283 | } 284 | 285 | function nestedThrow() { 286 | throw new Error('error'); 287 | } 288 | 289 | try { 290 | nestedThrow(); 291 | } catch (e) { 292 | raven.utils.parseStack(e, parseCallback); 293 | } 294 | }); 295 | 296 | it('should handle spaces in paths', function(done) { 297 | var parseStack = raven.utils.parseStack; 298 | var callback = function(frames) { 299 | var frame = frames.pop(); 300 | frame.module.should.endWith('file with spaces in path'); 301 | frame.filename.should.endWith('file with spaces in path.js'); 302 | done(); 303 | }; 304 | try { 305 | require('./fixtures/file with spaces in path')(); 306 | } catch (e) { 307 | parseStack(e, callback); 308 | } 309 | }); 310 | }); 311 | 312 | describe('#getTransaction()', function() { 313 | it('should handle empty', function() { 314 | raven.utils.getTransaction({}).should.eql(''); 315 | }); 316 | 317 | it('should handle missing module', function() { 318 | raven.utils 319 | .getTransaction({ 320 | function: 'foo' 321 | }) 322 | .should.eql('? at foo'); 323 | }); 324 | 325 | it('should handle missing function', function() { 326 | raven.utils 327 | .getTransaction({ 328 | module: 'foo' 329 | }) 330 | .should.eql('foo at ?'); 331 | }); 332 | 333 | it('should work', function() { 334 | raven.utils 335 | .getTransaction({ 336 | module: 'foo', 337 | function: 'bar' 338 | }) 339 | .should.eql('foo at bar'); 340 | }); 341 | }); 342 | 343 | describe('#getModule()', function() { 344 | it('should identify a node_module', function() { 345 | var filename = '/home/x/node_modules/foo/bar/baz.js'; 346 | raven.utils.getModule(filename).should.eql('foo.bar:baz'); 347 | }); 348 | 349 | it('should identify a main module', function() { 350 | var filename = '/home/x/foo/bar/baz.js'; 351 | raven.utils.getModule(filename, '/home/x/').should.eql('foo.bar:baz'); 352 | }); 353 | 354 | it('should fallback to just filename', function() { 355 | var filename = '/home/lol.js'; 356 | raven.utils.getModule(filename).should.eql('lol'); 357 | }); 358 | }); 359 | 360 | describe('#serializeException()', function() { 361 | it('return [Object] when reached depth=0', function() { 362 | var actual = raven.utils.serializeException( 363 | { 364 | a: 42, 365 | b: 'asd', 366 | c: true 367 | }, 368 | 0 369 | ); 370 | var expected = '[Object]'; 371 | 372 | actual.should.eql(expected); 373 | }); 374 | 375 | it('should serialize one level deep with depth=1', function() { 376 | var actual = raven.utils.serializeException( 377 | { 378 | a: 42, 379 | b: 'asd', 380 | c: true, 381 | d: undefined, 382 | e: 383 | 'very long string that is definitely over 120 characters, which is default for now but can be changed anytime because why not?', 384 | f: {foo: 42}, 385 | g: [1, 'a', true], 386 | h: function() {} 387 | }, 388 | 1 389 | ); 390 | var expected = { 391 | a: 42, 392 | b: 'asd', 393 | c: true, 394 | d: undefined, 395 | e: 'very long string that is definitely ove\u2026', 396 | f: '[Object]', 397 | g: '[Array]', 398 | // Node < 6 is not capable of pulling function name from unnamed object methods 399 | h: majorVersion < 6 ? '[Function]' : '[Function: h]' 400 | }; 401 | 402 | actual.should.eql(expected); 403 | }); 404 | 405 | it('should serialize arbitrary number of depths', function() { 406 | var actual = raven.utils.serializeException( 407 | { 408 | a: 42, 409 | b: 'asd', 410 | c: true, 411 | d: undefined, 412 | e: 413 | 'very long string that is definitely over 40 characters, which is default for now but can be changed', 414 | f: { 415 | foo: 42, 416 | bar: { 417 | foo: 42, 418 | bar: { 419 | bar: { 420 | bar: { 421 | bar: 42 422 | } 423 | } 424 | }, 425 | baz: ['hello'] 426 | }, 427 | baz: [1, 'a', true] 428 | }, 429 | g: [1, 'a', true], 430 | h: function bar() {} 431 | }, 432 | 5 433 | ); 434 | var expected = { 435 | a: 42, 436 | b: 'asd', 437 | c: true, 438 | d: undefined, 439 | e: 'very long string that is definitely ove\u2026', 440 | f: { 441 | foo: 42, 442 | bar: { 443 | foo: 42, 444 | bar: { 445 | bar: { 446 | bar: '[Object]' 447 | } 448 | }, 449 | baz: ['hello'] 450 | }, 451 | baz: [1, 'a', true] 452 | }, 453 | g: [1, 'a', true], 454 | h: '[Function: bar]' 455 | }; 456 | 457 | actual.should.eql(expected); 458 | }); 459 | 460 | it('should reduce depth if payload size was exceeded', function() { 461 | var actual = raven.utils.serializeException( 462 | { 463 | a: { 464 | a: '50kB worth of payload pickle rick', 465 | b: '50kB worth of payload pickle rick' 466 | }, 467 | b: '50kB worth of payload pickle rick' 468 | }, 469 | 2, 470 | 100 471 | ); 472 | var expected = { 473 | a: '[Object]', 474 | b: '50kB worth of payload pickle rick' 475 | }; 476 | 477 | actual.should.eql(expected); 478 | }); 479 | 480 | it('should reduce depth only one level at the time', function() { 481 | var actual = raven.utils.serializeException( 482 | { 483 | a: { 484 | a: { 485 | a: { 486 | a: [ 487 | '50kB worth of payload pickle rick', 488 | '50kB worth of payload pickle rick', 489 | '50kB worth of payload pickle rick' 490 | ] 491 | } 492 | }, 493 | b: '50kB worth of payload pickle rick' 494 | }, 495 | b: '50kB worth of payload pickle rick' 496 | }, 497 | 4, 498 | 200 499 | ); 500 | var expected = { 501 | a: { 502 | a: { 503 | a: { 504 | a: '[Array]' 505 | } 506 | }, 507 | b: '50kB worth of payload pickle rick' 508 | }, 509 | b: '50kB worth of payload pickle rick' 510 | }; 511 | 512 | actual.should.eql(expected); 513 | }); 514 | 515 | it('should fallback to [Object] if cannot reduce payload size enough', function() { 516 | var actual = raven.utils.serializeException( 517 | { 518 | a: '50kB worth of payload pickle rick', 519 | b: '50kB worth of payload pickle rick', 520 | c: '50kB worth of payload pickle rick', 521 | d: '50kB worth of payload pickle rick' 522 | }, 523 | 1, 524 | 100 525 | ); 526 | var expected = '[Object]'; 527 | 528 | actual.should.eql(expected); 529 | }); 530 | }); 531 | 532 | describe('#serializeKeysForMessage()', function() { 533 | it('should fit as many keys as possible in default limit of 40', function() { 534 | var actual = raven.utils.serializeKeysForMessage([ 535 | 'pickle', 536 | 'rick', 537 | 'morty', 538 | 'snuffles', 539 | 'server', 540 | 'request' 541 | ]); 542 | var expected = 'pickle, rick, morty, snuffles, server\u2026'; 543 | actual.should.eql(expected); 544 | }); 545 | 546 | it('shouldnt append ellipsis if have enough space', function() { 547 | var actual = raven.utils.serializeKeysForMessage(['pickle', 'rick', 'morty']); 548 | var expected = 'pickle, rick, morty'; 549 | actual.should.eql(expected); 550 | }); 551 | 552 | it('should default to no-keys message if empty array provided', function() { 553 | var actual = raven.utils.serializeKeysForMessage([]); 554 | var expected = '[object has no keys]'; 555 | actual.should.eql(expected); 556 | }); 557 | 558 | it('should leave first key as is, if its too long for the limit', function() { 559 | var actual = raven.utils.serializeKeysForMessage([ 560 | 'imSuchALongKeyThatIDontEvenFitInTheLimitOf40Characters', 561 | 'pickle' 562 | ]); 563 | var expected = 'imSuchALongKeyThatIDontEvenFitInTheLimitOf40Characters'; 564 | actual.should.eql(expected); 565 | }); 566 | 567 | it('should with with provided maxLength', function() { 568 | var actual = raven.utils.serializeKeysForMessage(['foo', 'bar', 'baz'], 10); 569 | var expected = 'foo, bar\u2026'; 570 | actual.should.eql(expected); 571 | }); 572 | 573 | it('handles incorrect input', function() { 574 | raven.utils.serializeKeysForMessage({}).should.eql(''); 575 | raven.utils.serializeKeysForMessage(false).should.eql(''); 576 | raven.utils.serializeKeysForMessage(undefined).should.eql(''); 577 | raven.utils.serializeKeysForMessage(42).should.eql('42'); 578 | raven.utils.serializeKeysForMessage('foo').should.eql('foo'); 579 | }); 580 | }); 581 | 582 | describe('isError', function() { 583 | it('should work as advertised', function() { 584 | function RavenError(message) { 585 | this.name = 'RavenError'; 586 | this.message = message; 587 | } 588 | RavenError.prototype = new Error(); 589 | RavenError.prototype.constructor = RavenError; 590 | 591 | raven.utils.isError(new Error()).should.be.true; 592 | raven.utils.isError(new RavenError()).should.be.true; 593 | raven.utils.isError({}).should.be.false; 594 | raven.utils.isError({ 595 | message: 'A fake error', 596 | stack: 'no stack here' 597 | }).should.be.false; 598 | raven.utils.isError('').should.be.false; 599 | raven.utils.isError(true).should.be.false; 600 | }); 601 | 602 | it('should work with errors from different contexts, eg. vm module', function(done) { 603 | var vm = require('vm'); 604 | var script = new vm.Script("throw new Error('this is the error')"); 605 | 606 | try { 607 | script.runInNewContext(); 608 | } catch (e) { 609 | raven.utils.isError(e).should.be.true; 610 | done(); 611 | } 612 | }); 613 | }); 614 | 615 | describe('#consoleAlert()', function() { 616 | it('should call console.warn if enabled', function() { 617 | mockConsoleWarn(); 618 | raven.utils.consoleAlert('foo'); 619 | raven.utils.consoleAlert('foo'); 620 | console.warn._called.should.eql(true); 621 | console.warn._callCount.should.eql(2); 622 | restoreConsoleWarn(); 623 | }); 624 | 625 | it('should be disabled after calling disableConsoleAlerts', function() { 626 | mockConsoleWarn(); 627 | raven.utils.disableConsoleAlerts(); 628 | raven.utils.consoleAlert('foo'); 629 | console.warn._called.should.eql(false); 630 | console.warn._callCount.should.eql(0); 631 | raven.utils.enableConsoleAlerts(); 632 | restoreConsoleWarn(); 633 | }); 634 | 635 | it('should be disabled after calling disableConsoleAlerts, even after previous successful calls', function() { 636 | mockConsoleWarn(); 637 | raven.utils.consoleAlert('foo'); 638 | console.warn._called.should.eql(true); 639 | console.warn._callCount.should.eql(1); 640 | raven.utils.disableConsoleAlerts(); 641 | raven.utils.consoleAlert('foo'); 642 | console.warn._callCount.should.eql(1); 643 | raven.utils.enableConsoleAlerts(); 644 | restoreConsoleWarn(); 645 | }); 646 | }); 647 | 648 | describe('#consoleAlertOnce()', function() { 649 | it('should call console.warn if enabled, but only once with the same message', function() { 650 | mockConsoleWarn(); 651 | raven.utils.consoleAlertOnce('foo'); 652 | console.warn._called.should.eql(true); 653 | console.warn._callCount.should.eql(1); 654 | raven.utils.consoleAlertOnce('foo'); 655 | console.warn._callCount.should.eql(1); 656 | restoreConsoleWarn(); 657 | }); 658 | 659 | it('should be disable after calling disableConsoleAlerts', function() { 660 | mockConsoleWarn(); 661 | raven.utils.disableConsoleAlerts(); 662 | raven.utils.consoleAlertOnce('foo'); 663 | console.warn._called.should.eql(false); 664 | console.warn._callCount.should.eql(0); 665 | raven.utils.enableConsoleAlerts(); 666 | restoreConsoleWarn(); 667 | }); 668 | }); 669 | }); 670 | -------------------------------------------------------------------------------- /test/run.coffee: -------------------------------------------------------------------------------- 1 | Mocha = require('mocha') 2 | should = require('should') 3 | 4 | fs = require('fs') 5 | path = require('path') 6 | 7 | mocha = new Mocha 8 | 9 | ['test', 'test/vendor'].forEach (dir) -> 10 | fs.readdirSync(dir).filter (file) -> 11 | file.substr(-3) == '.js'; 12 | .forEach (file) -> 13 | mocha.addFile path.join(dir, file) 14 | 15 | mocha.run (failures) -> 16 | process.on 'exit', () -> 17 | process.exit(failures); 18 | -------------------------------------------------------------------------------- /test/vendor/json-stringify-safe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var assert = require('assert'); 5 | var stringify = require('../../vendor/json-stringify-safe'); 6 | 7 | function jsonify(obj) { 8 | return JSON.stringify(obj, null, 2); 9 | } 10 | 11 | describe('Stringify', function() { 12 | it('must stringify circular objects', function() { 13 | var obj = {name: 'Alice'}; 14 | obj.self = obj; 15 | var json = stringify(obj, null, 2); 16 | assert.deepEqual(json, jsonify({name: 'Alice', self: '[Circular ~]'})); 17 | }); 18 | 19 | it('must stringify circular objects with intermediaries', function() { 20 | var obj = {name: 'Alice'}; 21 | obj.identity = {self: obj}; 22 | var json = stringify(obj, null, 2); 23 | assert.deepEqual(json, jsonify({name: 'Alice', identity: {self: '[Circular ~]'}})); 24 | }); 25 | 26 | it('must stringify circular objects deeper', function() { 27 | var obj = {name: 'Alice', child: {name: 'Bob'}}; 28 | obj.child.self = obj.child; 29 | 30 | assert.deepEqual( 31 | stringify(obj, null, 2), 32 | jsonify({ 33 | name: 'Alice', 34 | child: {name: 'Bob', self: '[Circular ~.child]'} 35 | }) 36 | ); 37 | }); 38 | 39 | it('must stringify circular objects deeper with intermediaries', function() { 40 | var obj = {name: 'Alice', child: {name: 'Bob'}}; 41 | obj.child.identity = {self: obj.child}; 42 | 43 | assert.deepEqual( 44 | stringify(obj, null, 2), 45 | jsonify({ 46 | name: 'Alice', 47 | child: {name: 'Bob', identity: {self: '[Circular ~.child]'}} 48 | }) 49 | ); 50 | }); 51 | 52 | it('must stringify circular objects in an array', function() { 53 | var obj = {name: 'Alice'}; 54 | obj.self = [obj, obj]; 55 | 56 | assert.deepEqual( 57 | stringify(obj, null, 2), 58 | jsonify({ 59 | name: 'Alice', 60 | self: ['[Circular ~]', '[Circular ~]'] 61 | }) 62 | ); 63 | }); 64 | 65 | it('must stringify circular objects deeper in an array', function() { 66 | var obj = {name: 'Alice', children: [{name: 'Bob'}, {name: 'Eve'}]}; 67 | obj.children[0].self = obj.children[0]; 68 | obj.children[1].self = obj.children[1]; 69 | 70 | assert.deepEqual( 71 | stringify(obj, null, 2), 72 | jsonify({ 73 | name: 'Alice', 74 | children: [ 75 | {name: 'Bob', self: '[Circular ~.children.0]'}, 76 | {name: 'Eve', self: '[Circular ~.children.1]'} 77 | ] 78 | }) 79 | ); 80 | }); 81 | 82 | it('must stringify circular arrays', function() { 83 | var obj = []; 84 | obj.push(obj); 85 | obj.push(obj); 86 | var json = stringify(obj, null, 2); 87 | assert.deepEqual(json, jsonify(['[Circular ~]', '[Circular ~]'])); 88 | }); 89 | 90 | it('must stringify circular arrays with intermediaries', function() { 91 | var obj = []; 92 | obj.push({name: 'Alice', self: obj}); 93 | obj.push({name: 'Bob', self: obj}); 94 | 95 | assert.deepEqual( 96 | stringify(obj, null, 2), 97 | jsonify([ 98 | {name: 'Alice', self: '[Circular ~]'}, 99 | {name: 'Bob', self: '[Circular ~]'} 100 | ]) 101 | ); 102 | }); 103 | 104 | it('must stringify repeated objects in objects', function() { 105 | var obj = {}; 106 | var alice = {name: 'Alice'}; 107 | obj.alice1 = alice; 108 | obj.alice2 = alice; 109 | 110 | assert.deepEqual( 111 | stringify(obj, null, 2), 112 | jsonify({ 113 | alice1: {name: 'Alice'}, 114 | alice2: {name: 'Alice'} 115 | }) 116 | ); 117 | }); 118 | 119 | it('must stringify repeated objects in arrays', function() { 120 | var alice = {name: 'Alice'}; 121 | var obj = [alice, alice]; 122 | var json = stringify(obj, null, 2); 123 | assert.deepEqual(json, jsonify([{name: 'Alice'}, {name: 'Alice'}])); 124 | }); 125 | 126 | it('must call given decycler and use its output', function() { 127 | var obj = {}; 128 | obj.a = obj; 129 | obj.b = obj; 130 | 131 | var decycle = Sinon.spy(function() { 132 | return decycle.callCount; 133 | }); 134 | var json = stringify(obj, null, 2, decycle); 135 | assert.deepEqual(json, jsonify({a: 1, b: 2}, null, 2)); 136 | 137 | assert.strictEqual(decycle.callCount, 2); 138 | assert.strictEqual(decycle.thisValues[0], obj); 139 | assert.strictEqual(decycle.args[0][0], 'a'); 140 | assert.strictEqual(decycle.args[0][1], obj); 141 | assert.strictEqual(decycle.thisValues[1], obj); 142 | assert.strictEqual(decycle.args[1][0], 'b'); 143 | assert.strictEqual(decycle.args[1][1], obj); 144 | }); 145 | 146 | it('must call replacer and use its output', function() { 147 | var obj = {name: 'Alice', child: {name: 'Bob'}}; 148 | 149 | var replacer = Sinon.spy(bangString); 150 | var json = stringify(obj, replacer, 2); 151 | assert.deepEqual(json, jsonify({name: 'Alice!', child: {name: 'Bob!'}})); 152 | 153 | assert.strictEqual(replacer.callCount, 4); 154 | assert.strictEqual(replacer.args[0][0], ''); 155 | assert.strictEqual(replacer.args[0][1], obj); 156 | assert.strictEqual(replacer.thisValues[1], obj); 157 | assert.strictEqual(replacer.args[1][0], 'name'); 158 | assert.strictEqual(replacer.args[1][1], 'Alice'); 159 | assert.strictEqual(replacer.thisValues[2], obj); 160 | assert.strictEqual(replacer.args[2][0], 'child'); 161 | assert.strictEqual(replacer.args[2][1], obj.child); 162 | assert.strictEqual(replacer.thisValues[3], obj.child); 163 | assert.strictEqual(replacer.args[3][0], 'name'); 164 | assert.strictEqual(replacer.args[3][1], 'Bob'); 165 | }); 166 | 167 | it('must call replacer after describing circular references', function() { 168 | var obj = {name: 'Alice'}; 169 | obj.self = obj; 170 | 171 | var replacer = Sinon.spy(bangString); 172 | var json = stringify(obj, replacer, 2); 173 | assert.deepEqual(json, jsonify({name: 'Alice!', self: '[Circular ~]!'})); 174 | 175 | assert.strictEqual(replacer.callCount, 3); 176 | assert.strictEqual(replacer.args[0][0], ''); 177 | assert.strictEqual(replacer.args[0][1], obj); 178 | assert.strictEqual(replacer.thisValues[1], obj); 179 | assert.strictEqual(replacer.args[1][0], 'name'); 180 | assert.strictEqual(replacer.args[1][1], 'Alice'); 181 | assert.strictEqual(replacer.thisValues[2], obj); 182 | assert.strictEqual(replacer.args[2][0], 'self'); 183 | assert.strictEqual(replacer.args[2][1], '[Circular ~]'); 184 | }); 185 | 186 | it('must call given decycler and use its output for nested objects', function() { 187 | var obj = {}; 188 | obj.a = obj; 189 | obj.b = {self: obj}; 190 | 191 | var decycle = Sinon.spy(function() { 192 | return decycle.callCount; 193 | }); 194 | var json = stringify(obj, null, 2, decycle); 195 | assert.deepEqual(json, jsonify({a: 1, b: {self: 2}})); 196 | 197 | assert.strictEqual(decycle.callCount, 2); 198 | assert.strictEqual(decycle.args[0][0], 'a'); 199 | assert.strictEqual(decycle.args[0][1], obj); 200 | assert.strictEqual(decycle.args[1][0], 'self'); 201 | assert.strictEqual(decycle.args[1][1], obj); 202 | }); 203 | 204 | it("must use decycler's output when it returned null", function() { 205 | var obj = {a: 'b'}; 206 | obj.self = obj; 207 | obj.selves = [obj, obj]; 208 | 209 | function decycle() { 210 | return null; 211 | } 212 | assert.deepEqual( 213 | stringify(obj, null, 2, decycle), 214 | jsonify({ 215 | a: 'b', 216 | self: null, 217 | selves: [null, null] 218 | }) 219 | ); 220 | }); 221 | 222 | it("must use decycler's output when it returned undefined", function() { 223 | var obj = {a: 'b'}; 224 | obj.self = obj; 225 | obj.selves = [obj, obj]; 226 | 227 | function decycle() {} 228 | assert.deepEqual( 229 | stringify(obj, null, 2, decycle), 230 | jsonify({ 231 | a: 'b', 232 | selves: [null, null] 233 | }) 234 | ); 235 | }); 236 | 237 | it('must throw given a decycler that returns a cycle', function() { 238 | var obj = {}; 239 | obj.self = obj; 240 | var err; 241 | function identity(key, value) { 242 | return value; 243 | } 244 | try { 245 | stringify(obj, null, 2, identity); 246 | } catch (ex) { 247 | err = ex; 248 | } 249 | assert.ok(err instanceof TypeError); 250 | }); 251 | 252 | it('must stringify error objects, including extra properties', function() { 253 | var obj = new Error('Wubba Lubba Dub Dub'); 254 | obj.reason = new TypeError("I'm pickle Riiick!"); 255 | 256 | // Testing whole stack is reduntant, we just need to know that it has been stringified 257 | // And that it includes our first frame 258 | var stringified = stringify(obj, null, 2); 259 | assert.equal(obj.stack.indexOf('Error: Wubba Lubba Dub Dub') === 0, true); 260 | assert.equal(obj.reason.stack.indexOf("TypeError: I'm pickle Riiick!") === 0, true); 261 | 262 | // Skip stack, as it has been tested above 263 | delete obj.stack; 264 | delete obj.reason.stack; 265 | stringified = stringify(obj, null, 2); 266 | 267 | assert.deepEqual( 268 | stringified, 269 | jsonify({ 270 | message: 'Wubba Lubba Dub Dub', 271 | name: 'Error', 272 | reason: { 273 | message: "I'm pickle Riiick!", 274 | name: 'TypeError' 275 | } 276 | }) 277 | ); 278 | }); 279 | 280 | it('must stringify error objects with circular references', function() { 281 | var obj = new Error('Wubba Lubba Dub Dub'); 282 | obj.reason = obj; 283 | 284 | // Skip stack, as it has been tested above 285 | delete obj.stack; 286 | var stringified = stringify(obj, null, 2); 287 | 288 | assert.deepEqual( 289 | stringified, 290 | jsonify({ 291 | message: 'Wubba Lubba Dub Dub', 292 | name: 'Error', 293 | reason: '[Circular ~]' 294 | }) 295 | ); 296 | }); 297 | 298 | describe('.getSerialize', function() { 299 | it('must stringify circular objects', function() { 300 | var obj = {a: 'b'}; 301 | obj.circularRef = obj; 302 | obj.list = [obj, obj]; 303 | 304 | var json = JSON.stringify(obj, stringify.getSerialize(), 2); 305 | assert.deepEqual( 306 | json, 307 | jsonify({ 308 | a: 'b', 309 | circularRef: '[Circular ~]', 310 | list: ['[Circular ~]', '[Circular ~]'] 311 | }) 312 | ); 313 | }); 314 | 315 | // This is the behavior as of Mar 3, 2015. 316 | // The serializer function keeps state inside the returned function and 317 | // so far I'm not sure how to not do that. JSON.stringify's replacer is not 318 | // called _after_ serialization. 319 | xit('must return a function that could be called twice', function() { 320 | var obj = {name: 'Alice'}; 321 | obj.self = obj; 322 | 323 | var json; 324 | var serializer = stringify.getSerialize(); 325 | 326 | json = JSON.stringify(obj, serializer, 2); 327 | assert.deepEqual(json, jsonify({name: 'Alice', self: '[Circular ~]'})); 328 | 329 | json = JSON.stringify(obj, serializer, 2); 330 | assert.deepEqual(json, jsonify({name: 'Alice', self: '[Circular ~]'})); 331 | }); 332 | }); 333 | }); 334 | 335 | function bangString(key, value) { 336 | return typeof value === 'string' ? value + '!' : value; 337 | } 338 | -------------------------------------------------------------------------------- /vendor/json-stringify-safe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | json-stringify-safe 5 | Like JSON.stringify, but doesn't throw on circular references. 6 | 7 | Originally forked from https://github.com/isaacs/json-stringify-safe 8 | version 5.0.1 on 2017-09-21 and modified to handle Errors serialization. 9 | Tests for this are in test/vendor. 10 | 11 | ISC license: https://github.com/isaacs/json-stringify-safe/blob/master/LICENSE 12 | */ 13 | 14 | exports = module.exports = stringify; 15 | exports.getSerialize = serializer; 16 | 17 | function stringify(obj, replacer, spaces, cycleReplacer) { 18 | return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces); 19 | } 20 | 21 | // https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106 22 | function stringifyError(value) { 23 | var err = { 24 | // These properties are implemented as magical getters and don't show up in for in 25 | stack: value.stack, 26 | message: value.message, 27 | name: value.name 28 | }; 29 | 30 | for (var i in value) { 31 | if (Object.prototype.hasOwnProperty.call(value, i)) { 32 | err[i] = value[i]; 33 | } 34 | } 35 | 36 | return err; 37 | } 38 | 39 | function serializer(replacer, cycleReplacer) { 40 | var stack = []; 41 | var keys = []; 42 | 43 | if (cycleReplacer == null) { 44 | cycleReplacer = function(key, value) { 45 | if (stack[0] === value) { 46 | return '[Circular ~]'; 47 | } 48 | return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'; 49 | }; 50 | } 51 | 52 | return function(key, value) { 53 | if (stack.length > 0) { 54 | var thisPos = stack.indexOf(this); 55 | ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); 56 | ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); 57 | 58 | if (~stack.indexOf(value)) { 59 | value = cycleReplacer.call(this, key, value); 60 | } 61 | } else { 62 | stack.push(value); 63 | } 64 | 65 | return replacer == null 66 | ? value instanceof Error ? stringifyError(value) : value 67 | : replacer.call(this, key, value); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /vendor/node-lsmod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Original repository: https://github.com/defunctzombie/node-lsmod/ 4 | // 5 | // [2018-02-09] @kamilogorek - Handle scoped packages structure 6 | 7 | // builtin 8 | var fs = require('fs'); 9 | var path = require('path'); 10 | 11 | // node 0.6 support 12 | fs.existsSync = fs.existsSync || path.existsSync; 13 | 14 | // mainPaths are the paths where our mainprog will be able to load from 15 | // we store these to avoid grabbing the modules that were loaded as a result 16 | // of a dependency module loading its dependencies, we only care about deps our 17 | // mainprog loads 18 | var mainPaths = (require.main && require.main.paths) || []; 19 | 20 | module.exports = function() { 21 | var paths = Object.keys(require.cache || []); 22 | 23 | // module information 24 | var infos = {}; 25 | 26 | // paths we have already inspected to avoid traversing again 27 | var seen = {}; 28 | 29 | paths.forEach(function(p) { 30 | /* eslint-disable consistent-return */ 31 | 32 | var dir = p; 33 | 34 | (function updir() { 35 | var orig = dir; 36 | dir = path.dirname(orig); 37 | 38 | if (/@[^/]+$/.test(dir)) { 39 | dir = path.dirname(dir); 40 | } 41 | 42 | if (!dir || orig === dir || seen[orig]) { 43 | return; 44 | } else if (mainPaths.indexOf(dir) < 0) { 45 | return updir(); 46 | } 47 | 48 | var pkgfile = path.join(orig, 'package.json'); 49 | var exists = fs.existsSync(pkgfile); 50 | 51 | seen[orig] = true; 52 | 53 | // travel up the tree if no package.json here 54 | if (!exists) { 55 | return updir(); 56 | } 57 | 58 | try { 59 | var info = JSON.parse(fs.readFileSync(pkgfile, 'utf8')); 60 | infos[info.name] = info.version; 61 | } catch (e) {} 62 | })(); 63 | 64 | /* eslint-enable consistent-return */ 65 | }); 66 | 67 | return infos; 68 | }; 69 | --------------------------------------------------------------------------------