├── .gitignore ├── .npmignore ├── start.js ├── lib ├── stackman.js ├── middleware │ └── connect.js ├── logger.js ├── instrumentation │ ├── express-utils.js │ ├── modules │ │ ├── express-graphql.js │ │ ├── https.js │ │ ├── ws.js │ │ ├── koa-router.js │ │ ├── knex.js │ │ ├── http.js │ │ ├── ioredis.js │ │ ├── generic-pool.js │ │ ├── bluebird.js │ │ ├── redis.js │ │ ├── express.js │ │ ├── mongodb-core.js │ │ ├── hapi.js │ │ ├── mysql.js │ │ ├── pg.js │ │ └── graphql.js │ ├── es6-wrapped-promise.js │ ├── shimmer.js │ ├── queue.js │ ├── trace.js │ ├── index.js │ ├── http-shared.js │ └── transaction.js ├── filters.js ├── config.js └── request.js ├── test ├── start │ ├── file │ │ ├── opbeat.js │ │ └── test.js │ └── env │ │ └── test.js ├── sourcemaps │ ├── fixtures │ │ ├── src │ │ │ └── error.js │ │ └── lib │ │ │ ├── error-broken.js.map │ │ │ ├── error.js.map │ │ │ ├── error-src-missing.js.map │ │ │ ├── error.js │ │ │ ├── error-broken.js │ │ │ ├── error-map-missing.js │ │ │ ├── error-src-embedded.js │ │ │ ├── error-src-missing.js │ │ │ ├── error-src-embedded.js.map │ │ │ ├── error-inline.js │ │ │ └── error-inline-broken.js │ └── index.js ├── posttest.sh ├── instrumentation │ ├── _instrumentation.js │ ├── modules │ │ ├── koa-router │ │ │ ├── _generators.js │ │ │ ├── _non-generators.js │ │ │ └── index.js │ │ ├── http │ │ │ ├── _echo_server_util.js │ │ │ ├── _echo_server.js │ │ │ ├── outgoing.js │ │ │ ├── _assert.js │ │ │ ├── github-179.js │ │ │ ├── basic.js │ │ │ ├── blacklisting.js │ │ │ ├── timeout-disabled.js │ │ │ └── sse.js │ │ ├── mysql │ │ │ ├── _utils.js │ │ │ └── pool-release-1.js │ │ ├── pg │ │ │ ├── _utils.js │ │ │ └── knex.js │ │ ├── bluebird │ │ │ ├── cancel.js │ │ │ └── _coroutine.js │ │ ├── generic-pool.js │ │ ├── ws.js │ │ ├── mongodb-core.js │ │ ├── graphql.js │ │ ├── redis.js │ │ └── ioredis.js │ ├── _async-await.js │ ├── async-await.js │ ├── _agent.js │ ├── native-promises.js │ ├── queue.js │ ├── trace.js │ └── transaction.js ├── pretest.sh ├── _helpers.js ├── test.sh └── request.js ├── index.js ├── README.md ├── LICENSE ├── .tav.yml ├── package.json └── .travis.yml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | CHANGELOG.md 3 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./').start() 4 | -------------------------------------------------------------------------------- /lib/stackman.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('stackman')() 4 | -------------------------------------------------------------------------------- /test/start/file/opbeat.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | appId: 'from-file' 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Agent = require('./lib/agent') 4 | 5 | module.exports = new Agent() 6 | -------------------------------------------------------------------------------- /test/start/env/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var agent = require('../../..') 5 | 6 | assert.equal(agent.appId, 'from-env') 7 | -------------------------------------------------------------------------------- /test/start/file/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var agent = require('../../..') 5 | 6 | assert.equal(agent.appId, 'from-file') 7 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/src/error.js: -------------------------------------------------------------------------------- 1 | // Just a little prefixing line 2 | const generateError = (msg = 'foo') => new Error(msg) 3 | 4 | module.exports = generateError 5 | -------------------------------------------------------------------------------- /test/posttest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$TRAVIS" != "true" ]; then 4 | pg_ctl -D /usr/local/var/postgres stop 5 | kill `cat /tmp/mongod.pid` 6 | redis-cli shutdown 7 | mysql.server stop 8 | fi 9 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-broken.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/error.js"],"names":[],"mappings":";;AAA;ACA,IAM,gBAgB,SAhB,aAAgB;AAAA,MAAC,GAAD,uEAAO,KAAP;AAAA,SAAiB,IAAI,KAAJ,CAAU,GAAV,CAAjB;AAAA,CAAtB;;AAEA,OAAO,OAAP,GAAiB,aAAjB","file":"error.js"} 2 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/error.js"],"names":[],"mappings":";;AAAA;AACA,IAAM,gBAAgB,SAAhB,aAAgB;AAAA,MAAC,GAAD,uEAAO,KAAP;AAAA,SAAiB,IAAI,KAAJ,CAAU,GAAV,CAAjB;AAAA,CAAtB;;AAEA,OAAO,OAAP,GAAiB,aAAjB","file":"error.js"} 2 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-src-missing.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/not/found.js"],"names":[],"mappings":";;AAAA;AACA,IAAM,gBAAgB,SAAhB,aAAgB;AAAA,MAAC,GAAD,uEAAO,KAAP;AAAA,SAAiB,IAAI,KAAJ,CAAU,GAAV,CAAjB;AAAA,CAAtB;;AAEA,OAAO,OAAP,GAAiB,aAAjB","file":"error.js"} 2 | -------------------------------------------------------------------------------- /test/instrumentation/_instrumentation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var mockAgent = require('./_agent') 4 | 5 | module.exports = function mockInstrumentation (cb) { 6 | var agent = mockAgent() 7 | agent._instrumentation.addEndedTransaction = cb 8 | return agent._instrumentation 9 | } 10 | -------------------------------------------------------------------------------- /lib/middleware/connect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function connectMiddleware () { 4 | var agent = this 5 | return function (err, req, res, next) { 6 | agent.captureError(err, { request: req }, function opbeatMiddleware () { 7 | next(err) 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/pretest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$TRAVIS" != "true" ]; then 4 | pg_ctl -D /usr/local/var/postgres start 5 | mongod --fork --config /usr/local/etc/mongod.conf --pidfilepath /tmp/mongod.pid >/tmp/mongod.log 2>&1 6 | redis-server /usr/local/etc/redis.conf --daemonize yes 7 | mysql.server start 8 | fi 9 | -------------------------------------------------------------------------------- /test/instrumentation/modules/koa-router/_generators.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (router) { 4 | router.get('/hello', function * (next) { 5 | this.body = 'hello world' 6 | }) 7 | router.get('/hello/:name', function * (next) { 8 | this.body = 'hello ' + this.params.name 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /test/instrumentation/modules/koa-router/_non-generators.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (router) { 4 | router.get('/hello', function (ctx, next) { 5 | ctx.body = 'hello world' 6 | }) 7 | router.get('/hello/:name', function (ctx, next) { 8 | ctx.body = 'hello ' + ctx.params.name 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Logger = require('console-log-level') 4 | 5 | exports.init = function (opts) { 6 | var logger = Logger(opts) 7 | Object.keys(logger).forEach(function (method) { 8 | exports[method] = function () { 9 | return logger[method].apply(logger, arguments) 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=error.js.map -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-broken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=error-broken.js.map 12 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-map-missing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=invalid.js.map 12 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-src-embedded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=error-src-embedded.js.map 12 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-src-missing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=error-src-missing.js.map 12 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-src-embedded.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/error.js"],"names":[],"mappings":";;AAAA;AACA,IAAM,gBAAgB,SAAhB,aAAgB;AAAA,MAAC,GAAD,uEAAO,KAAP;AAAA,SAAiB,IAAI,KAAJ,CAAU,GAAV,CAAjB;AAAA,CAAtB;;AAEA,OAAO,OAAP,GAAiB,aAAjB","file":"error.js","sourcesContent":["// Just a little prefixing line\nconst generateError = (msg = 'foo') => new Error(msg)\n\nmodule.exports = generateError\n"]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Opbeat for Node.js 2 | 3 | Opbeat has joined forces with [Elastic](https://www.elastic.co) and this agent is now deprecated. 4 | We recommend that you check out [Elastic APM](https://www.elastic.co/solutions/apm) and the [Elastic APM Node.js agent](https://github.com/elastic/apm-agent-nodejs) instead. 5 | 6 | ## License 7 | 8 | BSD-2-Clause 9 | 10 |
Made with ♥️ and ☕️ by Opbeat and our community. 11 | -------------------------------------------------------------------------------- /test/instrumentation/_async-await.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.promise = promise 4 | exports.nonPromise = nonPromise 5 | 6 | async function promise (delay) { 7 | var res = await promise2(delay) 8 | return res.toUpperCase() 9 | } 10 | 11 | async function promise2 (delay) { 12 | return new Promise(function (resolve) { 13 | setTimeout(function () { 14 | resolve('success') 15 | }, delay) 16 | }) 17 | } 18 | 19 | async function nonPromise () { 20 | var res = await 'success' 21 | return res.toUpperCase() 22 | } 23 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/_echo_server_util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var exec = require('child_process').exec 5 | 6 | exports.echoServer = echoServer 7 | 8 | function echoServer (type, cb) { 9 | if (typeof type === 'function') return echoServer('http', type) 10 | var cp = exec('node ' + path.join(__dirname, '/_echo_server.js ' + type)) 11 | cp.stderr.pipe(process.stderr) 12 | cp.stdout.once('data', function (chunk) { 13 | var port = chunk.trim().split('\n')[0] 14 | cb(cp, port) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /test/instrumentation/modules/mysql/_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var mysql = require('mysql') 4 | 5 | exports.reset = reset 6 | 7 | function reset (cb) { 8 | var client = mysql.createConnection({user: 'root', database: 'mysql'}) 9 | 10 | client.connect(function (err) { 11 | if (err) throw err 12 | client.query('DROP DATABASE IF EXISTS test_opbeat', function (err) { 13 | if (err) throw err 14 | client.query('CREATE DATABASE test_opbeat', function (err) { 15 | if (err) throw err 16 | client.end(cb) 17 | }) 18 | }) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /test/_helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var _oldConsoleInfo = console.info 4 | var _oldConsoleWarn = console.warn 5 | var _oldConsoleError = console.error 6 | 7 | exports.mockLogger = function () { 8 | console.info = function () { console.info._called = true } 9 | console.warn = function () { console.warn._called = true } 10 | console.error = function () { console.error._called = true } 11 | console.info._called = false 12 | console.warn._called = false 13 | console.error._called = false 14 | } 15 | 16 | exports.restoreLogger = function () { 17 | console.info = _oldConsoleInfo 18 | console.warn = _oldConsoleWarn 19 | console.error = _oldConsoleError 20 | } 21 | -------------------------------------------------------------------------------- /lib/instrumentation/express-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.getPathFromRequest = function (req) { 4 | var path 5 | 6 | // Get proper route name from Express 4.x 7 | if (req._opbeat_static) { 8 | path = 'static file' 9 | } else if (req.route) { 10 | path = req.route.path || (req.route.regexp && req.route.regexp.source) || '' 11 | if (req._opbeat_mountstack) path = req._opbeat_mountstack.join('') + (path === '/' ? '' : path) 12 | } else if (req._opbeat_mountstack && req._opbeat_mountstack.length > 0) { 13 | // in the case of custom middleware that terminates the request 14 | // so it doesn't reach the regular router (like express-graphql), 15 | // the req.route will not be set, but we'll see something on the 16 | // mountstack and simply use that 17 | path = req._opbeat_mountstack.join('') 18 | } 19 | 20 | return path 21 | } 22 | -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-inline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9lcnJvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBO0FBQ0EsSUFBTSxnQkFBZ0IsU0FBaEIsYUFBZ0I7QUFBQSxNQUFDLEdBQUQsdUVBQU8sS0FBUDtBQUFBLFNBQWlCLElBQUksS0FBSixDQUFVLEdBQVYsQ0FBakI7QUFBQSxDQUF0Qjs7QUFFQSxPQUFPLE9BQVAsR0FBaUIsYUFBakIiLCJmaWxlIjoiZXJyb3ItaW5saW5lLmpzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gSnVzdCBhIGxpdHRsZSBwcmVmaXhpbmcgbGluZVxuY29uc3QgZ2VuZXJhdGVFcnJvciA9IChtc2cgPSAnZm9vJykgPT4gbmV3IEVycm9yKG1zZylcblxubW9kdWxlLmV4cG9ydHMgPSBnZW5lcmF0ZUVycm9yXG4iXX0= -------------------------------------------------------------------------------- /test/sourcemaps/fixtures/lib/error-inline-broken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Just a little prefixing line 4 | var generateError = function generateError() { 5 | var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'foo'; 6 | return new Error(msg); 7 | }; 8 | 9 | module.exports = generateError; 10 | 11 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9lcnJvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBO0FBQ0EsSUFBTSxnQkFBZ0IsU0FBaEIsYUFBZ0I7QUFBQSxNQUFDLEdBQUQsdUVBQU8sS0FBUDtBQUFBLFNBQWlCLElBQUksS0FBSixDQUFVLEdBQVYsQ0FBakI7QUFBQSxDQUF0Qjs7QUFFQSxPQUFPLE9BQVAsR0FBaUIsYUFBakIiLCJmaWxlIjoiZXJyb3ItaW5saW5lLmpzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gSnVzdCBhIGxpdHRsZSBwcmVmaXhpbmcgbGluZVxuY29uc3QgZ2VuZXJhdGVFcnJvciA9IChtc2cgPSAnZm9vJykgPT4gbmV3IEVycm9yKG1zZylcblxubW9kdWxlLmV4cG9ydHMgPSBnZW5lcmF0ZUVycm9yXG4iXX0= 12 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/express-graphql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | 6 | module.exports = function (graphqlHTTP, agent, version) { 7 | if (!semver.satisfies(version, '^0.6.1') || typeof graphqlHTTP !== 'function') { 8 | debug('express-graphql version %s not supported - aborting...', version) 9 | return graphqlHTTP 10 | } 11 | 12 | Object.keys(graphqlHTTP).forEach(function (key) { 13 | wrappedGraphqlHTTP[key] = graphqlHTTP[key] 14 | }) 15 | 16 | return wrappedGraphqlHTTP 17 | 18 | function wrappedGraphqlHTTP () { 19 | var orig = graphqlHTTP.apply(this, arguments) 20 | 21 | if (typeof orig !== 'function') return orig 22 | 23 | // Express is very particular with the number of arguments! 24 | return function (req, res) { 25 | var trans = agent._instrumentation.currentTransaction 26 | if (trans) trans._graphqlRoute = true 27 | return orig.apply(this, arguments) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/_echo_server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var http = require('http') 4 | var https = require('https') 5 | var zlib = require('zlib') 6 | var pem = require('https-pem') 7 | 8 | process.title = 'echo-server' 9 | 10 | var server = process.argv[2] === 'https' 11 | ? https.createServer(pem) 12 | : http.createServer() 13 | 14 | server.on('request', function (req, res) { 15 | var acceptEncoding = req.headers['accept-encoding'] || '' 16 | 17 | if (/\bdeflate\b/.test(acceptEncoding)) { 18 | res.writeHead(200, {'Content-Encoding': 'deflate'}) 19 | req.pipe(zlib.createDeflate()).pipe(res) 20 | } else if (/\bgzip\b/.test(acceptEncoding)) { 21 | res.writeHead(200, {'Content-Encoding': 'gzip'}) 22 | req.pipe(zlib.createGzip()).pipe(res) 23 | } else { 24 | req.pipe(res) 25 | } 26 | }) 27 | 28 | server.listen(function () { 29 | console.log(server.address().port) 30 | }) 31 | 32 | // auto-shutdown after 1 minute (tests that last longer are probably broken) 33 | setTimeout(function () { 34 | server.close() 35 | }, 60 * 1000) 36 | -------------------------------------------------------------------------------- /test/instrumentation/modules/mysql/pool-release-1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var test = require('tape') 11 | var mysql = require('mysql') 12 | var utils = require('./_utils') 13 | 14 | test('release connection prior to transaction', function (t) { 15 | createPool(function (pool) { 16 | pool.getConnection(function (err, conn) { 17 | t.error(err) 18 | conn.release() // important to release connection before starting the transaction 19 | 20 | agent.startTransaction('foo') 21 | t.ok(agent._instrumentation.currentTransaction) 22 | 23 | pool.getConnection(function (err, conn) { 24 | t.error(err) 25 | t.ok(agent._instrumentation.currentTransaction) 26 | pool.end() 27 | t.end() 28 | }) 29 | }) 30 | }) 31 | }) 32 | 33 | function createPool (cb) { 34 | setup(function () { 35 | var pool = mysql.createPool({ 36 | user: 'root', 37 | database: 'test_opbeat' 38 | }) 39 | 40 | cb(pool) 41 | }) 42 | } 43 | 44 | function setup (cb) { 45 | utils.reset(cb) 46 | } 47 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/https.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | var shared = require('../http-shared') 7 | 8 | module.exports = function (https, agent, version) { 9 | debug('shimming https.Server.prototype.emit function') 10 | shimmer.wrap(https && https.Server && https.Server.prototype, 'emit', shared.instrumentRequest(agent, 'https')) 11 | 12 | // From Node.js v0.11.12 until v9.0.0, https requests just uses the 13 | // http.request function. So to avoid creating a trace twice for the same 14 | // request, we'll only instrument the https.request function if the Node 15 | // version is less than 0.11.12 or >=9.0.0 16 | // 17 | // The was introduced in: 18 | // https://github.com/nodejs/node/commit/d6bbb19f1d1d6397d862d09304bc63c476f675c1 19 | // 20 | // And removed again in: 21 | // https://github.com/nodejs/node/commit/5118f3146643dc55e7e7bd3082d1de4d0e7d5426 22 | if (semver.satisfies(version, '<0.11.12 || >=9.0.0')) { 23 | debug('shimming https.request function') 24 | shimmer.wrap(https, 'request', shared.traceOutgoingRequest(agent, 'https')) 25 | } 26 | 27 | return https 28 | } 29 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/ws.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (ws, agent, version) { 8 | if (!semver.satisfies(version, '^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0')) { 9 | debug('ws version %s not supported - aborting...', version) 10 | return ws 11 | } 12 | 13 | debug('shimming ws.prototype.send function') 14 | shimmer.wrap(ws.prototype, 'send', wrapSend) 15 | 16 | return ws 17 | 18 | function wrapSend (orig) { 19 | return function wrappedSend () { 20 | var trace = agent.buildTrace() 21 | var id = trace && trace.transaction.id 22 | 23 | debug('intercepted call to ws.prototype.send %o', { id: id }) 24 | 25 | if (!trace) return orig.apply(this, arguments) 26 | 27 | var args = [].slice.call(arguments) 28 | var cb = args[args.length - 1] 29 | if (typeof cb === 'function') { 30 | args[args.length - 1] = done 31 | } else { 32 | cb = null 33 | args.push(done) 34 | } 35 | 36 | trace.start('Send WebSocket Message', 'websocket.send') 37 | 38 | return orig.apply(this, args) 39 | 40 | function done () { 41 | trace.end() 42 | if (cb) cb.apply(this, arguments) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/instrumentation/modules/pg/_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Client = require('pg').Client 4 | 5 | exports.reset = reset 6 | exports.loadData = loadData 7 | 8 | function reset (cb) { 9 | var client = new Client({database: 'postgres'}) 10 | 11 | client.connect(function (err) { 12 | if (err) throw err 13 | client.query('DROP DATABASE IF EXISTS test_opbeat', function (err) { 14 | if (err) throw err 15 | client.query('CREATE DATABASE test_opbeat', function (err) { 16 | if (err) throw err 17 | client.once('end', cb) 18 | client.end() 19 | }) 20 | }) 21 | }) 22 | } 23 | 24 | function loadData (cb) { 25 | var client = new Client({database: 'test_opbeat'}) 26 | 27 | client.connect(function (err) { 28 | if (err) throw err 29 | client.query('CREATE TABLE test (id serial NOT NULL, c1 varchar, c2 varchar)', function (err) { 30 | if (err) throw err 31 | 32 | var sql = 'INSERT INTO test (c1, c2) ' + 33 | 'VALUES (\'foo1\', \'bar1\'), ' + 34 | '(\'foo2\', \'bar2\'), ' + 35 | '(\'foo3\', \'bar3\'), ' + 36 | '(\'foo4\', \'bar4\'), ' + 37 | '(\'foo5\', \'bar5\')' 38 | 39 | client.query(sql, function (err) { 40 | if (err) throw err 41 | client.once('end', cb) 42 | client.end() 43 | }) 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/outgoing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../_agent')() 4 | 5 | var test = require('tape') 6 | var echoServer = require('./_echo_server_util').echoServer 7 | 8 | var transports = [['http', require('http')], ['https', require('https')]] 9 | 10 | transports.forEach(function (tuple) { 11 | var name = tuple[0] 12 | var transport = tuple[1] 13 | 14 | test(name + '.request', function (t) { 15 | echoServer(name, function (cp, port) { 16 | resetAgent(function (endpoint, headers, data, cb) { 17 | t.equal(data.transactions.length, 1) 18 | t.equal(data.traces.groups.length, 2) 19 | t.equal(data.traces.groups[0].signature, 'GET localhost:' + port + '/') 20 | t.equal(data.traces.groups[1].signature, 'transaction') 21 | t.end() 22 | cp.kill() 23 | }) 24 | 25 | agent.startTransaction() 26 | var req = transport.request({port: port, rejectUnauthorized: false}, function (res) { 27 | res.on('end', function () { 28 | agent.endTransaction() 29 | agent._instrumentation._queue._flush() 30 | }) 31 | res.resume() 32 | }) 33 | req.end() 34 | }) 35 | }) 36 | }) 37 | 38 | function resetAgent (cb) { 39 | agent.timeout.active = false 40 | agent._instrumentation._queue._clear() 41 | agent._instrumentation.currentTransaction = null 42 | agent._httpClient = { request: cb } 43 | } 44 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/_assert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = assert 4 | 5 | // { 6 | // traces: { 7 | // groups: [ { extra: { _frames: [Object] }, kind: 'transaction', parents: [], signature: 'transaction', timestamp: '2016-06-14T22:34:00.000Z', transaction: 'GET unknown route' } ], 8 | // raw: [ [ 5.404068, [ 0, 0, 5.404068 ] ] ] 9 | // }, 10 | // transactions: [ { durations: [ 5.404068 ], kind: 'request', result: 200, timestamp: '2016-06-14T22:34:00.000Z', transaction: 'GET unknown route' } ] 11 | // } 12 | function assert (t, data) { 13 | t.equal(data.transactions[0].kind, 'request') 14 | t.equal(data.transactions[0].result, 200) 15 | t.equal(data.transactions[0].transaction, 'GET unknown route') 16 | 17 | t.equal(data.traces.groups.length, 1) 18 | t.equal(data.traces.raw.length, 1) 19 | t.equal(data.transactions.length, 1) 20 | t.equal(data.traces.groups[0].kind, 'transaction') 21 | t.deepEqual(data.traces.groups[0].parents, []) 22 | t.equal(data.traces.groups[0].signature, 'transaction') 23 | t.equal(data.traces.groups[0].transaction, 'GET unknown route') 24 | 25 | t.equal(data.traces.raw[0].length, 3) 26 | t.equal(data.traces.raw[0][1].length, 3) 27 | t.equal(data.traces.raw[0][1][0], 0) 28 | t.equal(data.traces.raw[0][1][1], 0) 29 | t.equal(data.traces.raw[0][1][2], data.traces.raw[0][0]) 30 | t.equal(data.traces.raw[0][2].http.method, 'GET') 31 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 32 | } 33 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/koa-router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (Router, agent, version) { 8 | if (!semver.satisfies(version, '>=5.2.0 <8')) { 9 | debug('koa-router version %s not supported - aborting...', version) 10 | return Router 11 | } 12 | 13 | debug('shimming koa-router prototype.match function') 14 | shimmer.wrap(Router.prototype, 'match', function (orig) { 15 | return function (_, method) { 16 | var matched = orig.apply(this, arguments) 17 | 18 | if (typeof method !== 'string') { 19 | debug('unexpected method type in koa-router prototype.match: %s', typeof method) 20 | return matched 21 | } 22 | 23 | if (matched && matched.pathAndMethod && matched.pathAndMethod.length) { 24 | var match = matched.pathAndMethod[matched.pathAndMethod.length - 1] 25 | var path = match && match.path 26 | if (typeof path === 'string') { 27 | var name = method + ' ' + path 28 | agent._instrumentation.setDefaultTransactionName(name) 29 | } else { 30 | debug('unexpected path type in koa-router prototype.match: %s', typeof path) 31 | } 32 | } else { 33 | debug('unexpected match result in koa-router prototype.match: %s', typeof matched) 34 | } 35 | 36 | return matched 37 | } 38 | }) 39 | 40 | return Router 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2012, Matt Robenolt 4 | Copyright (c) 2013-2014, Thomas Watson Steen and Opbeat 5 | Copyright (c) 2015-2018, Opbeat 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /lib/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var redact = require('redact-secrets')('[REDACTED]') 4 | 5 | var REDACTED = '[REDACTED]' 6 | 7 | module.exports = Filters 8 | 9 | function Filters () { 10 | if (!(this instanceof Filters)) return new Filters() 11 | this._filters = [] 12 | } 13 | 14 | Filters.prototype.config = function (opts) { 15 | if (opts.filterHttpHeaders) this.add(httpHeaders) 16 | } 17 | 18 | Filters.prototype.add = function (fn) { 19 | this._filters.push(fn) 20 | } 21 | 22 | Filters.prototype.process = function (payload) { 23 | var context = {} 24 | if (payload.http) context.http = payload.http 25 | if (payload.user) context.user = payload.user 26 | if (payload.extra) context.extra = payload.extra 27 | 28 | // abort if a filter function doesn't return an object 29 | this._filters.some(function (filter) { 30 | context = filter(context) 31 | return !context 32 | }) 33 | 34 | if (!context) return 35 | if (context.http !== payload.http) payload.http = context.http 36 | if (context.user !== payload.user) payload.user = context.user 37 | if (context.extra !== payload.extra) payload.extra = context.extra 38 | 39 | return payload 40 | } 41 | 42 | function httpHeaders (context) { 43 | if (context.http) { 44 | if (context.http.headers && context.http.headers.authorization) { 45 | context.http.headers.authorization = REDACTED 46 | } 47 | if (context.http.cookies) { 48 | context.http.cookies = redact.map(context.http.cookies) 49 | } 50 | } 51 | 52 | return context 53 | } 54 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/github-179.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var semver = require('semver') 11 | if (semver.lt(process.version, '4.0.0')) process.exit() 12 | 13 | var test = require('tape') 14 | 15 | var zlib = require('zlib') 16 | var http = require('http') 17 | var PassThrough = require('stream').PassThrough 18 | var mimicResponse = require('mimic-response') 19 | var echoServer = require('./_echo_server_util').echoServer 20 | 21 | test('https://github.com/opbeat/opbeat-node/issues/179', function (t) { 22 | echoServer(function (cp, port) { 23 | var opts = { 24 | port: port, 25 | headers: {'Accept-Encoding': 'gzip'} 26 | } 27 | 28 | agent.startTransaction() 29 | 30 | var req = http.request(opts, function (res) { 31 | process.nextTick(function () { 32 | var unzip = zlib.createUnzip() 33 | var stream = new PassThrough() 34 | 35 | // This would previously copy res.emit to the stream object which 36 | // shouldn't happen since res.emit is supposed to be on the res.prototype 37 | // chain (but was directly on the res object because it was wrapped). 38 | mimicResponse(res, stream) 39 | 40 | res.pipe(unzip).pipe(stream).pipe(new PassThrough()) 41 | 42 | stream.on('end', function () { 43 | cp.kill() 44 | t.end() 45 | }) 46 | }) 47 | }) 48 | 49 | req.end() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/knex.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('opbeat') 4 | var shimmer = require('../shimmer') 5 | 6 | module.exports = function (Knex, agent, version) { 7 | if (Knex.Client && Knex.Client.prototype) { 8 | var QUERY_FNS = ['queryBuilder', 'raw'] 9 | debug('shimming Knex.Client.prototype.runner') 10 | shimmer.wrap(Knex.Client.prototype, 'runner', wrapRunner) 11 | debug('shimming Knex.Client.prototype functions:', QUERY_FNS) 12 | shimmer.massWrap(Knex.Client.prototype, QUERY_FNS, wrapQueryStartPoint) 13 | } else { 14 | debug('could not shim Knex') 15 | } 16 | 17 | return Knex 18 | } 19 | 20 | function wrapQueryStartPoint (original) { 21 | return function wrappedQueryStartPoint () { 22 | var builder = original.apply(this, arguments) 23 | 24 | debug('capturing custom stack trace for knex') 25 | var obj = {} 26 | Error.captureStackTrace(obj) 27 | builder._opbeatStackObj = obj 28 | 29 | return builder 30 | } 31 | } 32 | 33 | function wrapRunner (original) { 34 | return function wrappedRunner () { 35 | var runner = original.apply(this, arguments) 36 | 37 | debug('shimming knex runner.query') 38 | shimmer.wrap(runner, 'query', wrapQuery) 39 | 40 | return runner 41 | } 42 | } 43 | 44 | function wrapQuery (original) { 45 | return function wrappedQuery () { 46 | debug('intercepted call to knex runner.query') 47 | if (this.connection) { 48 | this.connection._opbeatStackObj = this.builder ? this.builder._opbeatStackObj : null 49 | } 50 | return original.apply(this, arguments) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/instrumentation/es6-wrapped-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * This file is extracted from the 'async-listener' project copyright by 5 | * Forrest L Norvell. It have been modified slightly to be used in the current 6 | * context and where possible changes have been contributed back to the 7 | * original project. 8 | * 9 | * https://github.com/othiym23/async-listener 10 | * 11 | * Original file: 12 | * 13 | * https://github.com/othiym23/async-listener/blob/master/es6-wrapped-promise.js 14 | * 15 | * License: 16 | * 17 | * BSD-2-Clause, http://opensource.org/licenses/BSD-2-Clause 18 | */ 19 | 20 | module.exports = (Promise, ensureObWrapper) => { 21 | // Updates to this class should also be applied to the the ES3 version 22 | // in async-hooks.js 23 | return class WrappedPromise extends Promise { 24 | constructor (executor) { 25 | var context, args 26 | super(wrappedExecutor) 27 | var promise = this 28 | 29 | try { 30 | executor.apply(context, args) 31 | } catch (err) { 32 | args[1](err) 33 | } 34 | 35 | return promise 36 | function wrappedExecutor (resolve, reject) { 37 | context = this 38 | args = [wrappedResolve, wrappedReject] 39 | 40 | // These wrappers create a function that can be passed a function and an argument to 41 | // call as a continuation from the resolve or reject. 42 | function wrappedResolve (val) { 43 | ensureObWrapper(promise, false) 44 | return resolve(val) 45 | } 46 | 47 | function wrappedReject (val) { 48 | ensureObWrapper(promise, false) 49 | return reject(val) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/instrumentation/async-await.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | var ins = agent._instrumentation 10 | 11 | var test = require('tape') 12 | var semver = require('semver') 13 | 14 | // async/await isn't supported in old versions of Node.js 15 | if (semver.lt(process.version, '7.0.0')) process.exit() 16 | 17 | var _async = require('./_async-await') 18 | 19 | test('await promise', function (t) { 20 | t.plan(4) 21 | var t1 = ins.startTransaction() 22 | _async.promise(100).then(function (result) { 23 | t.equal(result, 'SUCCESS') 24 | t.equal(ins.currentTransaction && ins.currentTransaction.id, t1.id) 25 | }) 26 | var t2 = ins.startTransaction() 27 | _async.promise(50).then(function (result) { 28 | t.equal(result, 'SUCCESS') 29 | t.equal(ins.currentTransaction && ins.currentTransaction.id, t2.id) 30 | }) 31 | }) 32 | 33 | test('await non-promise', function (t) { 34 | t.plan(7) 35 | var n = 0 36 | var t1 = ins.startTransaction() 37 | _async.nonPromise().then(function (result) { 38 | t.equal(++n, 2) // this should be the first then-callback to execute 39 | t.equal(result, 'SUCCESS') 40 | t.equal(ins.currentTransaction && ins.currentTransaction.id, t1.id) 41 | }) 42 | var t2 = ins.startTransaction() 43 | _async.nonPromise().then(function (result) { 44 | t.equal(++n, 3) // this should be the second then-callback to execute 45 | t.equal(result, 'SUCCESS') 46 | t.equal(ins.currentTransaction && ins.currentTransaction.id, t2.id) 47 | }) 48 | t.equal(++n, 1) // this line should execute before any of the then-callbacks 49 | }) 50 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/http.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('opbeat') 4 | var shimmer = require('../shimmer') 5 | var shared = require('../http-shared') 6 | 7 | module.exports = function (http, agent) { 8 | debug('shimming http.Server.prototype.emit function') 9 | shimmer.wrap(http && http.Server && http.Server.prototype, 'emit', shared.instrumentRequest(agent, 'http')) 10 | 11 | debug('shimming http.request function') 12 | shimmer.wrap(http, 'request', shared.traceOutgoingRequest(agent, 'http')) 13 | 14 | debug('shimming http.ServerResponse.prototype.writeHead function') 15 | shimmer.wrap(http && http.ServerResponse && http.ServerResponse.prototype, 'writeHead', wrapWriteHead) 16 | 17 | return http 18 | 19 | function wrapWriteHead (original) { 20 | return function wrappedWriteHead () { 21 | var headers = arguments.length === 1 22 | ? this._headers // might be because of implicit headers 23 | : arguments[arguments.length - 1] 24 | 25 | var result = original.apply(this, arguments) 26 | 27 | var trans = agent._instrumentation.currentTransaction 28 | 29 | if (trans) { 30 | trans.result = this.statusCode 31 | 32 | // End transacton early in case of SSE 33 | if (headers && typeof headers === 'object' && !Array.isArray(headers)) { 34 | Object.keys(headers).some(function (key) { 35 | if (key.toLowerCase() !== 'content-type') return false 36 | if (String(headers[key]).toLowerCase().indexOf('text/event-stream') !== 0) return false 37 | debug('detected SSE response - ending transaction %o', { id: trans.id }) 38 | agent.endTransaction() 39 | return true 40 | }) 41 | } 42 | } 43 | 44 | return result 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s extglob # allow for complex regex-like globs 4 | 5 | files () { 6 | [ -f "$1" ] && echo "$@" 7 | } 8 | 9 | NODE_VERSION="$(node --version)" 10 | 11 | if [[ "${NODE_VERSION:0:6}" != "v0.10." && "${NODE_VERSION:0:6}" != "v0.12." ]]; then 12 | echo "running: test/start/env/test.js" 13 | cd test/start/env 14 | OPBEAT_APP_ID=from-env node -r ../../../start test.js || exit $?; 15 | cd ../../.. 16 | 17 | echo "running: test/start/file/test.js" 18 | cd test/start/file 19 | node -r ../../../start test.js || exit $?; 20 | cd ../../.. 21 | fi 22 | 23 | for file in $(files test/!(_*).js); do 24 | echo "running: node $file" 25 | node "$file" || exit $?; 26 | done 27 | 28 | for file in $(files test/sourcemaps/!(_*).js); do 29 | echo "running: node $file" 30 | node "$file" || exit $?; 31 | done 32 | 33 | for file in $(files test/instrumentation/!(_*).js); do 34 | echo "running: node $file" 35 | node "$file" || exit $?; 36 | done 37 | 38 | for file in $(files test/instrumentation/modules/!(_*).js); do 39 | echo "running: node $file" 40 | node "$file" || exit $?; 41 | done 42 | 43 | for file in $(files test/instrumentation/modules/http/!(_*).js); do 44 | echo "running: node $file" 45 | node "$file" || exit $?; 46 | done 47 | 48 | for file in $(files test/instrumentation/modules/pg/!(_*).js); do 49 | echo "running: node $file" 50 | node "$file" || exit $?; 51 | done 52 | 53 | for file in $(files test/instrumentation/modules/mysql/!(_*).js); do 54 | echo "running: node $file" 55 | node "$file" || exit $?; 56 | done 57 | 58 | for file in $(files test/instrumentation/modules/bluebird/!(_*).js); do 59 | echo "running: node $file" 60 | node "$file" || exit $?; 61 | done 62 | 63 | for file in $(files test/instrumentation/modules/koa-router/!(_*).js); do 64 | echo "running: node $file" 65 | node "$file" || exit $?; 66 | done 67 | -------------------------------------------------------------------------------- /test/instrumentation/_agent.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Instrumentation = require('../../lib/instrumentation') 4 | var Filters = require('../../lib/filters') 5 | var logger = require('../../lib/logger') 6 | 7 | logger.init({ level: 'fatal' }) 8 | 9 | var noop = function () {} 10 | var sharedInstrumentation 11 | 12 | module.exports = function mockAgent (cb) { 13 | var agent = { 14 | active: true, 15 | instrument: true, 16 | captureTraceStackTraces: true, 17 | timeout: { 18 | active: false, 19 | errorThreshold: 250 20 | }, 21 | _httpClient: { 22 | request: cb || noop 23 | }, 24 | _ignoreUrlStr: [], 25 | _ignoreUrlRegExp: [], 26 | _ignoreUserAgentStr: [], 27 | _ignoreUserAgentRegExp: [], 28 | _platform: {}, 29 | _filters: new Filters() 30 | } 31 | 32 | // We do not want to start the instrumentation multiple times during testing. 33 | // This would result in core functions being patched multiple times 34 | if (!sharedInstrumentation) { 35 | sharedInstrumentation = new Instrumentation(agent) 36 | agent._instrumentation = sharedInstrumentation 37 | agent.startTransaction = sharedInstrumentation.startTransaction.bind(sharedInstrumentation) 38 | agent.endTransaction = sharedInstrumentation.endTransaction.bind(sharedInstrumentation) 39 | agent.setTransactionName = sharedInstrumentation.setTransactionName.bind(sharedInstrumentation) 40 | agent.buildTrace = sharedInstrumentation.buildTrace.bind(sharedInstrumentation) 41 | agent._instrumentation.start() 42 | } else { 43 | sharedInstrumentation._agent = agent 44 | agent._instrumentation = sharedInstrumentation 45 | agent._instrumentation.currentTransaction = null 46 | agent._instrumentation._queue._clear() 47 | agent.startTransaction = sharedInstrumentation.startTransaction.bind(sharedInstrumentation) 48 | agent.endTransaction = sharedInstrumentation.endTransaction.bind(sharedInstrumentation) 49 | agent.setTransactionName = sharedInstrumentation.setTransactionName.bind(sharedInstrumentation) 50 | agent.buildTrace = sharedInstrumentation.buildTrace.bind(sharedInstrumentation) 51 | } 52 | 53 | return agent 54 | } 55 | -------------------------------------------------------------------------------- /test/instrumentation/modules/bluebird/cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | var ins = agent._instrumentation 10 | 11 | var semver = require('semver') 12 | var test = require('tape') 13 | var Promise = require('bluebird') 14 | 15 | var BLUEBIRD_VERSION = require('bluebird/package').version 16 | 17 | if (semver.satisfies(BLUEBIRD_VERSION, '>=3')) { 18 | Promise.config({cancellation: true}) 19 | 20 | var CANCEL_NAMES = ['cancel', 'break'] 21 | CANCEL_NAMES.forEach(function (fnName) { 22 | test('Promise.prototype.' + fnName, function (t) { 23 | t.plan(8) 24 | twice(function () { 25 | var trans = ins.startTransaction() 26 | var cancelled = false 27 | var p = new Promise(function (resolve, reject, onCancel) { 28 | setTimeout(function () { 29 | resolve('foo') 30 | }, 100) 31 | 32 | t.equal(ins.currentTransaction.id, trans.id, 'before calling onCancel') 33 | 34 | onCancel(function () { 35 | t.ok(cancelled, 'should be cancelled') 36 | t.equal(ins.currentTransaction.id, trans.id, 'onCancel callback') 37 | }) 38 | }).then(function () { 39 | t.fail('should not resolve') 40 | }).catch(function () { 41 | t.fail('should not reject') 42 | }) 43 | 44 | setTimeout(function () { 45 | cancelled = true 46 | t.equal(ins.currentTransaction.id, trans.id, 'before p.cancel') 47 | p[fnName]() 48 | }, 25) 49 | }) 50 | }) 51 | }) 52 | } else { 53 | test('Promise.prototype.cancel', function (t) { 54 | t.plan(4) 55 | twice(function () { 56 | var trans = ins.startTransaction() 57 | var p = new Promise(function () {}).cancellable() 58 | var err = new Error() 59 | p.cancel(err) 60 | p.then(t.fail, function (e) { 61 | t.equal(e, err) 62 | t.equal(ins.currentTransaction.id, trans.id) 63 | }) 64 | }) 65 | }) 66 | } 67 | 68 | function twice (fn) { 69 | setImmediate(fn) 70 | setImmediate(fn) 71 | } 72 | -------------------------------------------------------------------------------- /test/instrumentation/native-promises.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | var ins = agent._instrumentation 10 | 11 | var semver = require('semver') 12 | 13 | // Native promises wasn't available until Node.js 0.12 14 | if (!semver.satisfies(process.version, '>=0.12')) process.exit() 15 | 16 | var test = require('tape') 17 | 18 | require('./_shared-promise-tests')(test, Promise, ins) 19 | 20 | // non-standard v8 21 | if (semver.lt(process.version, '7.0.0')) { 22 | test('Promise.prototype.chain - short', function (t) { 23 | t.plan(4) 24 | twice(function () { 25 | var trans = ins.startTransaction() 26 | new Promise(function (resolve, reject) { 27 | resolve('foo') 28 | }).chain(function (data) { 29 | t.equal(data, 'foo') 30 | t.equal(ins.currentTransaction.id, trans.id) 31 | }, function () { 32 | t.fail('should not reject') 33 | }) 34 | }) 35 | }) 36 | 37 | // non-standard v8 38 | test('Promise.prototype.chain - long', function (t) { 39 | t.plan(8) 40 | twice(function () { 41 | var trans = ins.startTransaction() 42 | new Promise(function (resolve, reject) { 43 | resolve('foo') 44 | }).chain(function (data) { 45 | t.equal(data, 'foo') 46 | t.equal(ins.currentTransaction.id, trans.id) 47 | return 'bar' 48 | }, function () { 49 | t.fail('should not reject') 50 | }).chain(function (data) { 51 | t.equal(data, 'bar') 52 | t.equal(ins.currentTransaction.id, trans.id) 53 | }, function () { 54 | t.fail('should not reject') 55 | }) 56 | }) 57 | }) 58 | 59 | // non-standard v8 60 | test('Promise.accept', function (t) { 61 | t.plan(4) 62 | twice(function () { 63 | var trans = ins.startTransaction() 64 | Promise.accept('foo') 65 | .then(function (data) { 66 | t.equal(data, 'foo') 67 | t.equal(ins.currentTransaction.id, trans.id) 68 | }) 69 | .catch(function () { 70 | t.fail('should not reject') 71 | }) 72 | }) 73 | }) 74 | } 75 | 76 | function twice (fn) { 77 | setImmediate(fn) 78 | setImmediate(fn) 79 | } 80 | -------------------------------------------------------------------------------- /.tav.yml: -------------------------------------------------------------------------------- 1 | generic-pool: 2 | versions: ^2.0.0 || ^3.1.0 3 | commands: node test/instrumentation/modules/generic-pool.js 4 | mysql: 5 | versions: ^2.0.0 6 | commands: 7 | - node test/instrumentation/modules/mysql/mysql.js 8 | - node test/instrumentation/modules/mysql/pool-release-1.js 9 | redis: 10 | versions: ^2.0.0 11 | commands: node test/instrumentation/modules/redis.js 12 | ioredis: 13 | versions: '>=2 <3.1.3 || >3.1.3 <4' # v3.1.3 is broken in older versions of Node because of https://github.com/luin/ioredis/commit/d5867f7c7f03a770a8c0ca5680fdcbfcaf8488e7 14 | commands: node test/instrumentation/modules/ioredis.js 15 | pg: 16 | versions: '>=4 <8' 17 | peerDependencies: 18 | - bluebird@^3.0.0 19 | - knex@^0.13.0 20 | commands: 21 | - node test/instrumentation/modules/pg/pg.js 22 | - node test/instrumentation/modules/pg/knex.js 23 | bluebird: 24 | versions: '>=2 <4' 25 | commands: 26 | - node test/instrumentation/modules/bluebird/bluebird.js 27 | - node test/instrumentation/modules/bluebird/cancel.js 28 | knex: 29 | versions: ^0.14.0 || ^0.13.0 || ^0.12.5 || <0.12.4 >0.11.6 || <0.11.6 >0.9.0 30 | commands: node test/instrumentation/modules/pg/knex.js 31 | ws: 32 | versions: '>=1 <5' 33 | commands: node test/instrumentation/modules/ws.js 34 | graphql: 35 | node: '>=1.0.0' 36 | preinstall: rm -fr node_modules/express-graphql 37 | versions: '>=0.7.0 <0.11.0 || >=0.11.1 <0.13.0' 38 | commands: node test/instrumentation/modules/graphql.js 39 | express-graphql-1: 40 | name: express-graphql 41 | node: '>=1.0.0' 42 | peerDependencies: graphql@^0.8.2 43 | versions: '0.6.1' 44 | commands: node test/instrumentation/modules/express-graphql.js 45 | express-graphql-2: 46 | name: express-graphql 47 | node: '>=1.0.0' 48 | peerDependencies: graphql@^0.9.0 49 | versions: '>=0.6.2 <0.6.6' 50 | commands: node test/instrumentation/modules/express-graphql.js 51 | express-graphql-3: 52 | name: express-graphql 53 | node: '>=1.0.0' 54 | peerDependencies: graphql@^0.10.0 55 | versions: '^0.6.6' 56 | commands: node test/instrumentation/modules/express-graphql.js 57 | express-graphql-4: 58 | name: express-graphql 59 | node: '>=1.0.0' 60 | peerDependencies: graphql@^0.11.0 61 | versions: '^0.6.8' 62 | commands: node test/instrumentation/modules/express-graphql.js 63 | koa-router: 64 | node: '>=6.0.0' 65 | peerDependencies: koa@2 66 | versions: '>=5.2.0 <8' 67 | commands: node test/instrumentation/modules/koa-router/index.js 68 | hapi: 69 | versions: '>=9.0.1 <17.0.0' 70 | commands: node test/instrumentation/modules/hapi.js 71 | -------------------------------------------------------------------------------- /lib/instrumentation/shimmer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * This file is extracted from the 'shimmer' project copyright by Forrest L 5 | * Norvell. It have been modified slightly to be used in the current context. 6 | * 7 | * https://github.com/othiym23/shimmer 8 | * 9 | * Original file: 10 | * 11 | * https://github.com/othiym23/shimmer/blob/master/index.js 12 | * 13 | * License: 14 | * 15 | * BSD-2-Clause, http://opensource.org/licenses/BSD-2-Clause 16 | */ 17 | 18 | var debug = require('debug')('opbeat') 19 | 20 | exports.wrap = wrap 21 | exports.massWrap = massWrap 22 | exports.unwrap = unwrap 23 | 24 | function isFunction (funktion) { 25 | return funktion && {}.toString.call(funktion) === '[object Function]' 26 | } 27 | 28 | function wrap (nodule, name, wrapper) { 29 | if (!nodule || !nodule[name]) { 30 | debug('no original function %s to wrap', name) 31 | return 32 | } 33 | 34 | if (!wrapper) { 35 | debug('no wrapper function') 36 | debug((new Error()).stack) 37 | return 38 | } 39 | 40 | if (!isFunction(nodule[name]) || !isFunction(wrapper)) { 41 | debug('original object and wrapper must be functions') 42 | return 43 | } 44 | 45 | if (nodule[name].__obWrapped) { 46 | debug('function %s already wrapped', name) 47 | return 48 | } 49 | 50 | var original = nodule[name] 51 | var wrapped = wrapper(original, name) 52 | 53 | wrapped.__obWrapped = true 54 | wrapped.__obUnwrap = function __obUnwrap () { 55 | if (nodule[name] === wrapped) { 56 | nodule[name] = original 57 | wrapped.__obWrapped = false 58 | } 59 | } 60 | 61 | nodule[name] = wrapped 62 | 63 | return wrapped 64 | } 65 | 66 | function massWrap (nodules, names, wrapper) { 67 | if (!nodules) { 68 | debug('must provide one or more modules to patch') 69 | debug((new Error()).stack) 70 | return 71 | } else if (!Array.isArray(nodules)) { 72 | nodules = [nodules] 73 | } 74 | 75 | if (!(names && Array.isArray(names))) { 76 | debug('must provide one or more functions to wrap on modules') 77 | return 78 | } 79 | 80 | nodules.forEach(function (nodule) { 81 | names.forEach(function (name) { 82 | wrap(nodule, name, wrapper) 83 | }) 84 | }) 85 | } 86 | 87 | function unwrap (nodule, name) { 88 | if (!nodule || !nodule[name]) { 89 | debug('no function to unwrap.') 90 | debug((new Error()).stack) 91 | return 92 | } 93 | 94 | if (!nodule[name].__obUnwrap) { 95 | debug('no original to unwrap to -- has %s already been unwrapped?', name) 96 | } else { 97 | return nodule[name].__obUnwrap() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/instrumentation/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('opbeat') 4 | var protocol = require('./protocol') 5 | 6 | var MAX_FLUSH_DELAY_ON_BOOT = 5000 7 | var boot = true 8 | 9 | module.exports = Queue 10 | 11 | function Queue (opts, onFlush) { 12 | if (typeof opts === 'function') return new Queue(null, opts) 13 | if (!opts) opts = {} 14 | this._onFlush = onFlush 15 | this._samples = [] 16 | this._sampled = {} 17 | this._durations = {} 18 | this._timeout = null 19 | this._flushInterval = (opts.flushInterval || 60) * 1000 20 | 21 | // The purpose of the boot flush time is to be lower than the normal flush 22 | // time in order to get a result quickly when the app first boots. But if a 23 | // custom flush interval is provided and it's lower than the boot flush time, 24 | // it doesn't make much sense anymore. In that case, just pretend we have 25 | // already used the boot flush time. 26 | if (this._flushInterval < MAX_FLUSH_DELAY_ON_BOOT) boot = false 27 | } 28 | 29 | Queue.prototype.add = function (transaction) { 30 | var k1 = protocol.transactionGroupingKey(transaction) 31 | var k2 = sampleKey(transaction) 32 | 33 | if (k1 in this._durations) { 34 | this._durations[k1].push(transaction.duration()) 35 | } else { 36 | this._durations[k1] = [transaction.duration()] 37 | } 38 | 39 | if (!(k2 in this._sampled)) { 40 | this._sampled[k2] = true 41 | this._samples.push(transaction) 42 | } 43 | 44 | if (!this._timeout) this._queueFlush() 45 | } 46 | 47 | Queue.prototype._queueFlush = function () { 48 | var self = this 49 | var ms = boot ? MAX_FLUSH_DELAY_ON_BOOT : this._flushInterval 50 | 51 | // Randomize flush time to avoid servers started at the same time to 52 | // all connect to the APM server simultaneously 53 | ms = fuzzy(ms, 0.05) // +/- 5% 54 | 55 | debug('setting timer to flush queue: %dms', ms) 56 | this._timeout = setTimeout(function () { 57 | self._flush() 58 | }, ms) 59 | this._timeout.unref() 60 | boot = false 61 | } 62 | 63 | Queue.prototype._flush = function () { 64 | debug('flushing transaction queue') 65 | protocol.encode(this._samples, this._durations, this._onFlush) 66 | this._clear() 67 | } 68 | 69 | Queue.prototype._clear = function () { 70 | clearTimeout(this._timeout) 71 | this._samples = [] 72 | this._sampled = {} 73 | this._durations = {} 74 | this._timeout = null 75 | } 76 | 77 | function sampleKey (trans) { 78 | var durationBucket = Math.floor(trans.duration() / 15) 79 | return durationBucket + '|' + trans.type + '|' + trans.name 80 | } 81 | 82 | // TODO: Check if there's an existing algorithm for this we can use instead 83 | function fuzzy (n, pct) { 84 | var variance = n * pct * 2 85 | return Math.floor(n + (Math.random() * variance - variance / 2)) 86 | } 87 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/ioredis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (ioredis, agent, version) { 8 | if (!semver.satisfies(version, '^2.0.0 || ^3.0.0')) { 9 | debug('ioredis version %s not supported - aborting...', version) 10 | return ioredis 11 | } 12 | 13 | debug('shimming ioredis.Command.prototype.initPromise') 14 | shimmer.wrap(ioredis.Command && ioredis.Command.prototype, 'initPromise', wrapInitPromise) 15 | 16 | debug('shimming ioredis.prototype.sendCommand') 17 | shimmer.wrap(ioredis.prototype, 'sendCommand', wrapSendCommand) 18 | 19 | return ioredis 20 | 21 | // wrap initPromise to allow us to get notified when the callback to a 22 | // command is called. If we don't do this we will still get notified because 23 | // we register a callback with command.promise.finally the 24 | // wrappedSendCommand, but the finally call will not get fired until the tick 25 | // after the command.callback have fired, so if the transaction is ended in 26 | // the same tick as the call to command.callback, we'll lose the last trace 27 | // as it hasn't yet ended. 28 | function wrapInitPromise (original) { 29 | return function wrappedInitPromise () { 30 | var command = this 31 | var cb = this.callback 32 | 33 | if (typeof cb === 'function') { 34 | this.callback = agent._instrumentation.bindFunction(function wrappedCallback () { 35 | var trace = command.__obTrace 36 | if (trace && !trace.ended) trace.end() 37 | return cb.apply(this, arguments) 38 | }) 39 | } 40 | 41 | return original.apply(this, arguments) 42 | } 43 | } 44 | 45 | function wrapSendCommand (original) { 46 | return function wrappedSendCommand (command) { 47 | var trace = agent.buildTrace() 48 | var id = trace && trace.transaction.id 49 | 50 | debug('intercepted call to ioredis.prototype.sendCommand %o', { id: id, command: command && command.name }) 51 | 52 | if (trace && command) { 53 | // store trace on command to it can be accessed by callback in initPromise 54 | command.__obTrace = trace 55 | 56 | if (typeof command.resolve === 'function') { 57 | command.resolve = agent._instrumentation.bindFunction(command.resolve) 58 | } 59 | if (typeof command.reject === 'function') { 60 | command.reject = agent._instrumentation.bindFunction(command.reject) 61 | } 62 | if (command.promise && typeof command.promise.finally === 'function') { 63 | command.promise.finally(function () { 64 | if (!trace.ended) trace.end() 65 | }) 66 | } 67 | 68 | trace.start(String(command.name).toUpperCase(), 'cache.redis') 69 | } 70 | 71 | return original.apply(this, arguments) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/generic-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (generic, agent, version) { 8 | if (semver.satisfies(version, '^2.0.0')) { 9 | debug('shimming generic-pool.Pool') 10 | shimmer.wrap(generic, 'Pool', function (orig) { 11 | return function wrappedPool () { 12 | var trans = agent._instrumentation.currentTransaction 13 | var id = trans && trans.id 14 | debug('intercepted call to generic-pool.Pool %o', { id: id }) 15 | 16 | var pool 17 | if (this instanceof generic.Pool) { 18 | var args = [].slice.call(arguments) 19 | args.unshift(null) 20 | pool = new (Function.prototype.bind.apply(orig, args))() 21 | } else { 22 | pool = orig.apply(this, arguments) 23 | } 24 | 25 | shimmer.wrap(pool, 'acquire', function (orig) { 26 | return function wrappedAcquire () { 27 | var trans = agent._instrumentation.currentTransaction 28 | var id = trans && trans.id 29 | debug('intercepted call to pool.acquire %o', { id: id }) 30 | 31 | var cb = arguments[0] 32 | if (typeof cb === 'function') { 33 | arguments[0] = agent._instrumentation.bindFunction(cb) 34 | } 35 | 36 | return orig.apply(this, arguments) 37 | } 38 | }) 39 | 40 | return pool 41 | } 42 | }) 43 | } else if (semver.satisfies(version, '^3.1.0') && generic.PriorityQueue) { 44 | // A work-around as an alternative patching the returned promise from the 45 | // acquire function, we instead patch its resolve and reject functions. 46 | // 47 | // We can do that because they are exposed to the PriorityQueue when 48 | // enqueuing a ResourceRequest: 49 | // 50 | // https://github.com/coopernurse/node-pool/blob/58c275c5146977192165f679e86950396be1b9f1/lib/Pool.js#L404 51 | debug('shimming generic-pool.PriorityQueue.prototype.enqueue') 52 | shimmer.wrap(generic.PriorityQueue.prototype, 'enqueue', function (orig) { 53 | return function wrappedEnqueue () { 54 | var trans = agent._instrumentation.currentTransaction 55 | var id = trans && trans.id 56 | debug('intercepted call to generic-pool.PriorityQueue.prototype.enqueue %o', { id: id }) 57 | 58 | var obj = arguments[0] 59 | // Expect obj to of type Deferred 60 | if (obj._resolve && obj._reject) { 61 | obj._resolve = agent._instrumentation.bindFunction(obj._resolve) 62 | obj._reject = agent._instrumentation.bindFunction(obj._reject) 63 | } 64 | 65 | return orig.apply(this, arguments) 66 | } 67 | }) 68 | } else { 69 | debug('generic-pool version %s not supported - aborting...', version) 70 | } 71 | 72 | return generic 73 | } 74 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/bluebird.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var wrap = require('../shimmer').wrap 6 | var massWrap = require('../shimmer').massWrap 7 | 8 | var BLUEBIRD_FNS = ['_then', '_addCallbacks'] 9 | 10 | module.exports = function (bluebird, agent, version) { 11 | var ins = agent._instrumentation 12 | 13 | if (!semver.satisfies(version, '>=2 <4')) { 14 | debug('bluebird version %s not supported - aborting...', version) 15 | return bluebird 16 | } 17 | 18 | debug('shimming bluebird.prototype functions:', BLUEBIRD_FNS) 19 | massWrap(bluebird.prototype, BLUEBIRD_FNS, wrapThen) 20 | 21 | // Calling bluebird.config might overwrite the 22 | // bluebird.prototype._attachCancellationCallback function with a new 23 | // function. We need to hook into this new function 24 | debug('shimming bluebird.config') 25 | wrap(bluebird, 'config', function wrapConfig (original) { 26 | return function wrappedConfig () { 27 | var result = original.apply(this, arguments) 28 | 29 | debug('shimming bluebird.prototype._attachCancellationCallback') 30 | wrap(bluebird.prototype, '_attachCancellationCallback', function wrapAttachCancellationCallback (original) { 31 | return function wrappedAttachCancellationCallback (onCancel) { 32 | if (arguments.length !== 1) return original.apply(this, arguments) 33 | return original.call(this, ins.bindFunction(onCancel)) 34 | } 35 | }) 36 | 37 | return result 38 | } 39 | }) 40 | 41 | // WARNING: even if you remove these two shims, the tests might still pass 42 | // for bluebird@2. The tests are flaky and will only fail sometimes and in 43 | // some cases only if run together with the other tests. 44 | // 45 | // To test, run in a while-loop: 46 | // 47 | // while :; do node test/instrumentation/modules/bluebird/bluebird.js || exit $?; done 48 | if (semver.satisfies(version, '<3')) { 49 | debug('shimming bluebird.each') 50 | wrap(bluebird, 'each', function wrapEach (original) { 51 | return function wrappedEach (promises, fn) { 52 | if (arguments.length !== 2) return original.apply(this, arguments) 53 | return original.call(this, promises, ins.bindFunction(fn)) 54 | } 55 | }) 56 | 57 | debug('shimming bluebird.prototype.each') 58 | wrap(bluebird.prototype, 'each', function wrapEach (original) { 59 | return function wrappedEach (fn) { 60 | if (arguments.length !== 1) return original.apply(this, arguments) 61 | return original.call(this, ins.bindFunction(fn)) 62 | } 63 | }) 64 | } 65 | 66 | return bluebird 67 | 68 | function wrapThen (original) { 69 | return function wrappedThen () { 70 | var args = Array.prototype.slice.call(arguments) 71 | if (typeof args[0] === 'function') args[0] = ins.bindFunction(args[0]) 72 | if (typeof args[1] === 'function') args[1] = ins.bindFunction(args[1]) 73 | return original.apply(this, args) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/instrumentation/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var mockAgent = require('./_agent') 5 | var Transaction = require('../../lib/instrumentation/transaction') 6 | var Queue = require('../../lib/instrumentation/queue') 7 | 8 | test('queue flush isolation', function (t) { 9 | var agent = mockAgent() 10 | var flush = 0 11 | var t0 = new Transaction(agent, 'foo0', 'bar0') 12 | var t1 = new Transaction(agent, 'foo1', 'bar1') 13 | t0.result = 'baz0' 14 | t1.result = 'baz1' 15 | 16 | var queue = new Queue(function (data) { 17 | t.equal(data.transactions.length, 1, 'should have 1 transaction') 18 | t.equal(data.traces.groups.length, 1, 'should have 1 group') 19 | t.equal(data.traces.raw.length, 1, 'should have 1 raw') 20 | 21 | switch (++flush) { 22 | case 1: 23 | t.equal(data.transactions[0].transaction, 'foo0') 24 | t.equal(data.transactions[0].kind, 'bar0') 25 | t.equal(data.transactions[0].result, 'baz0') 26 | break 27 | case 2: 28 | t.equal(data.transactions[0].transaction, 'foo1') 29 | t.equal(data.transactions[0].kind, 'bar1') 30 | t.equal(data.transactions[0].result, 'baz1') 31 | t.end() 32 | break 33 | } 34 | }) 35 | 36 | t0.end() 37 | t1.end() 38 | 39 | queue.add(t0) 40 | queue._flush() 41 | queue.add(t1) 42 | queue._flush() 43 | }) 44 | 45 | test('queue sampling', function (t) { 46 | var agent = mockAgent() 47 | var t0 = new Transaction(agent, 'same-name', 'same-type') 48 | var t1 = new Transaction(agent, 'same-name', 'same-type') 49 | var t2 = new Transaction(agent, 'other-name', 'other-type') 50 | t0.result = 'same-result' 51 | t1.result = 'same-result' 52 | t2.result = 'other-result' 53 | 54 | var queue = new Queue(function (data) { 55 | t.equal(data.transactions.length, 2, 'should have 2 transaction') 56 | t.equal(data.transactions[0].transaction, 'same-name') 57 | t.equal(data.transactions[0].kind, 'same-type') 58 | t.equal(data.transactions[0].result, 'same-result') 59 | t.equal(data.transactions[0].durations.length, 2) 60 | t.equal(data.transactions[0].durations[0], t0.duration()) 61 | t.equal(data.transactions[0].durations[1], t1.duration()) 62 | t.equal(data.transactions[1].transaction, 'other-name') 63 | t.equal(data.transactions[1].kind, 'other-type') 64 | t.equal(data.transactions[1].result, 'other-result') 65 | t.equal(data.transactions[1].durations.length, 1) 66 | t.equal(data.transactions[1].durations[0], t2.duration()) 67 | 68 | t.equal(data.traces.groups.length, 2, 'should have 2 groups') 69 | t.equal(data.traces.groups[0].transaction, 'same-name') 70 | t.equal(data.traces.groups[1].transaction, 'other-name') 71 | 72 | t.equal(data.traces.raw.length, 2, 'should have 2 raws') 73 | t.equal(data.traces.raw[0].length, 3) 74 | t.equal(data.traces.raw[0][0], t0.duration()) 75 | t.equal(data.traces.raw[1].length, 3) 76 | t.equal(data.traces.raw[1][0], t2.duration()) 77 | 78 | t.end() 79 | }) 80 | 81 | t0.end() 82 | t1.end() 83 | t2.end() 84 | 85 | queue.add(t0) 86 | queue.add(t1) 87 | queue.add(t2) 88 | queue._flush() 89 | }) 90 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (redis, agent, version) { 8 | if (!semver.satisfies(version, '^2.0.0')) { 9 | debug('redis version %s not supported - aborting...', version) 10 | return redis 11 | } 12 | 13 | var proto = redis.RedisClient && redis.RedisClient.prototype 14 | if (semver.satisfies(version, '>2.5.3')) { 15 | debug('shimming redis.RedisClient.prototype.internal_send_command') 16 | shimmer.wrap(proto, 'internal_send_command', wrapInternalSendCommand) 17 | } else { 18 | debug('shimming redis.RedisClient.prototype.send_command') 19 | shimmer.wrap(proto, 'send_command', wrapSendCommand) 20 | } 21 | 22 | return redis 23 | 24 | function wrapInternalSendCommand (original) { 25 | return function wrappedInternalSendCommand (commandObj) { 26 | var trace = agent.buildTrace() 27 | var id = trace && trace.transaction.id 28 | var command = commandObj && commandObj.command 29 | 30 | debug('intercepted call to RedisClient.prototype.internal_send_command %o', { id: id, command: command }) 31 | 32 | if (trace && commandObj) { 33 | var cb = commandObj.callback 34 | commandObj.callback = agent._instrumentation.bindFunction(function wrappedCallback () { 35 | trace.end() 36 | if (cb) { 37 | return cb.apply(this, arguments) 38 | } 39 | }) 40 | trace.start(String(command).toUpperCase(), 'cache.redis') 41 | } 42 | 43 | return original.apply(this, arguments) 44 | } 45 | } 46 | 47 | function wrapSendCommand (original) { 48 | return function wrappedSendCommand (command) { 49 | var trace = agent.buildTrace() 50 | var id = trace && trace.transaction.id 51 | var args = Array.prototype.slice.call(arguments) 52 | 53 | debug('intercepted call to RedisClient.prototype.internal_send_command %o', { id: id, command: command }) 54 | 55 | if (trace && args.length > 0) { 56 | var index = args.length - 1 57 | var cb = args[index] 58 | if (typeof cb === 'function') { 59 | args[index] = agent._instrumentation.bindFunction(function wrappedCallback () { 60 | trace.end() 61 | return cb.apply(this, arguments) 62 | }) 63 | } else if (Array.isArray(cb) && typeof cb[cb.length - 1] === 'function') { 64 | var cb2 = cb[cb.length - 1] 65 | cb[cb.length - 1] = agent._instrumentation.bindFunction(function wrappedCallback () { 66 | trace.end() 67 | return cb2.apply(this, arguments) 68 | }) 69 | } else { 70 | var obCb = agent._instrumentation.bindFunction(function wrappedCallback () { 71 | trace.end() 72 | }) 73 | if (typeof args[index] === 'undefined') { 74 | args[index] = obCb 75 | } else { 76 | args.push(obCb) 77 | } 78 | } 79 | trace.start(String(command).toUpperCase(), 'cache.redis') 80 | } 81 | 82 | return original.apply(this, args) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../_agent')() 4 | 5 | var assert = require('./_assert') 6 | var test = require('tape') 7 | var http = require('http') 8 | 9 | test('http.createServer', function (t) { 10 | t.test('direct callback', function (t) { 11 | resetAgent(function (endpoint, headers, data, cb) { 12 | assert(t, data) 13 | server.close() 14 | t.end() 15 | }) 16 | 17 | var server = http.createServer(onRequest) 18 | sendRequest(server) 19 | }) 20 | 21 | t.test('server.addListener()', function (t) { 22 | resetAgent(function (endpoint, headers, data, cb) { 23 | assert(t, data) 24 | server.close() 25 | t.end() 26 | }) 27 | 28 | var server = http.createServer() 29 | server.addListener('request', onRequest) 30 | sendRequest(server) 31 | }) 32 | 33 | t.test('server.on()', function (t) { 34 | resetAgent(function (endpoint, headers, data, cb) { 35 | assert(t, data) 36 | server.close() 37 | t.end() 38 | }) 39 | 40 | var server = http.createServer() 41 | server.on('request', onRequest) 42 | sendRequest(server) 43 | }) 44 | }) 45 | 46 | test('new http.Server', function (t) { 47 | t.test('direct callback', function (t) { 48 | resetAgent(function (endpoint, headers, data, cb) { 49 | assert(t, data) 50 | server.close() 51 | t.end() 52 | }) 53 | 54 | var server = new http.Server(onRequest) 55 | sendRequest(server) 56 | }) 57 | 58 | t.test('server.addListener()', function (t) { 59 | resetAgent(function (endpoint, headers, data, cb) { 60 | assert(t, data) 61 | server.close() 62 | t.end() 63 | }) 64 | 65 | var server = new http.Server() 66 | server.addListener('request', onRequest) 67 | sendRequest(server) 68 | }) 69 | 70 | t.test('server.on()', function (t) { 71 | resetAgent(function (endpoint, headers, data, cb) { 72 | assert(t, data) 73 | server.close() 74 | t.end() 75 | }) 76 | 77 | var server = new http.Server() 78 | server.on('request', onRequest) 79 | sendRequest(server) 80 | }) 81 | }) 82 | 83 | function sendRequest (server, timeout) { 84 | server.listen(function () { 85 | var port = server.address().port 86 | var req = http.get('http://localhost:' + port, function (res) { 87 | if (timeout) throw new Error('should not get to here') 88 | res.on('end', function () { 89 | agent._instrumentation._queue._flush() 90 | }) 91 | res.resume() 92 | }) 93 | 94 | if (timeout) { 95 | req.on('error', function (err) { 96 | if (err.code !== 'ECONNRESET') throw err 97 | agent._instrumentation._queue._flush() 98 | }) 99 | 100 | process.nextTick(function () { 101 | req.abort() 102 | }) 103 | } 104 | }) 105 | } 106 | 107 | function onRequest (req, res) { 108 | res.end() 109 | } 110 | 111 | function resetAgent (cb) { 112 | agent.timeout.active = false 113 | agent._instrumentation._queue._clear() 114 | agent._instrumentation.currentTransaction = null 115 | agent._httpClient = { request: cb } 116 | } 117 | -------------------------------------------------------------------------------- /test/instrumentation/modules/generic-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | var ins = global.ins = agent._instrumentation 10 | 11 | var semver = require('semver') 12 | var pkg = require('generic-pool/package') 13 | 14 | if (semver.lt(process.version, '4.0.0') && semver.gte(pkg.version, '3.0.0')) { 15 | console.log('Unsupported version of Node.js') 16 | process.exit() 17 | } 18 | 19 | var test = require('tape') 20 | var genericPool = require('generic-pool') 21 | 22 | if (genericPool.createPool) { 23 | test('v3.x', function (t) { 24 | var active = 0 25 | 26 | var pool = genericPool.createPool({ 27 | create: function () { 28 | var p = new Promise(function (resolve, reject) { 29 | process.nextTick(function () { 30 | resolve({id: ++active}) 31 | }) 32 | }) 33 | p.foo = 42 34 | return p 35 | }, 36 | destroy: function (resource) { 37 | return new Promise(function (resolve, reject) { 38 | process.nextTick(function () { 39 | resolve() 40 | if (--active <= 0) t.end() 41 | }) 42 | }) 43 | } 44 | }) 45 | 46 | var t1 = ins.startTransaction() 47 | 48 | pool.acquire().then(function (resource) { 49 | t.equal(resource.id, 1) 50 | t.equal(ins.currentTransaction.id, t1.id) 51 | pool.release(resource) 52 | }).catch(function (err) { 53 | t.error(err) 54 | }) 55 | 56 | t.equal(ins.currentTransaction.id, t1.id) 57 | var t2 = ins.startTransaction() 58 | 59 | pool.acquire().then(function (resource) { 60 | t.equal(resource.id, 1) 61 | t.equal(ins.currentTransaction.id, t2.id) 62 | pool.release(resource) 63 | }).catch(function (err) { 64 | t.error(err) 65 | }) 66 | 67 | t.equal(ins.currentTransaction.id, t2.id) 68 | 69 | pool.drain().then(function () { 70 | pool.clear() 71 | }).catch(function (err) { 72 | t.error(err) 73 | }) 74 | }) 75 | } else { 76 | test('v2.x', function (t) { 77 | var active = 0 78 | 79 | var pool = new genericPool.Pool({ 80 | create: function (cb) { 81 | process.nextTick(function () { 82 | cb(null, {id: ++active}) 83 | }) 84 | }, 85 | destroy: function (resource) { 86 | if (--active <= 0) t.end() 87 | } 88 | }) 89 | 90 | var t1 = ins.startTransaction() 91 | 92 | pool.acquire(function (err, resource) { 93 | t.error(err) 94 | t.equal(resource.id, 1) 95 | t.equal(ins.currentTransaction.id, t1.id) 96 | pool.release(resource) 97 | }) 98 | 99 | t.equal(ins.currentTransaction.id, t1.id) 100 | var t2 = ins.startTransaction() 101 | 102 | pool.acquire(function (err, resource) { 103 | t.error(err) 104 | t.equal(resource.id, 1) 105 | t.equal(ins.currentTransaction.id, t2.id) 106 | pool.release(resource) 107 | }) 108 | 109 | t.equal(ins.currentTransaction.id, t2.id) 110 | 111 | pool.drain(function () { 112 | pool.destroyAllNow() 113 | }) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opbeat", 3 | "version": "4.17.0", 4 | "description": "The official Opbeat agent for Node.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "lint-fix": "standard --fix", 9 | "pretest": "test/pretest.sh", 10 | "test": "standard && npm run test-deps && test/test.sh", 11 | "posttest": "test/posttest.sh", 12 | "test-cli": "node test/scripts/cli.js", 13 | "test-deps": "dependency-check . && dependency-check . --unused --no-dev --entry lib/instrumentation/modules/*" 14 | }, 15 | "directories": { 16 | "test": "test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/opbeat/opbeat-node.git" 21 | }, 22 | "keywords": [ 23 | "opbeat", 24 | "log", 25 | "logging", 26 | "bug", 27 | "bugs", 28 | "error", 29 | "errors", 30 | "exception", 31 | "exceptions", 32 | "catch", 33 | "monitor", 34 | "monitoring", 35 | "alert", 36 | "alerts", 37 | "performance", 38 | "apm", 39 | "ops", 40 | "devops", 41 | "deployment", 42 | "deploying", 43 | "deploy", 44 | "release", 45 | "stacktrace" 46 | ], 47 | "author": "Thomas Watson Steen (https://twitter.com/wa7son)", 48 | "license": "BSD-2-Clause", 49 | "bugs": { 50 | "url": "https://github.com/opbeat/opbeat-node/issues" 51 | }, 52 | "homepage": "https://github.com/opbeat/opbeat-node", 53 | "dependencies": { 54 | "after-all-results": "^2.0.0", 55 | "console-log-level": "^1.4.0", 56 | "cookie": "^0.3.1", 57 | "core-util-is": "^1.0.2", 58 | "debug": "^3.0.0", 59 | "end-of-stream": "^1.1.0", 60 | "fast-safe-stringify": "^1.1.3", 61 | "hashlru": "^2.0.0", 62 | "is-native": "^1.0.1", 63 | "normalize-bool": "^1.0.0", 64 | "object-assign": "^4.1.1", 65 | "opbeat-http-client": "^1.2.2", 66 | "opbeat-release-tracker": "^1.1.1", 67 | "redact-secrets": "^1.0.0", 68 | "require-in-the-middle": "^2.1.2", 69 | "semver": "^5.3.0", 70 | "sql-summary": "^1.0.0", 71 | "stackman": "^2.0.1", 72 | "unicode-byte-truncate": "^1.0.0", 73 | "uuid": "^3.0.1" 74 | }, 75 | "devDependencies": { 76 | "bluebird": "^3.4.6", 77 | "connect": "^3.6.3", 78 | "dependency-check": "^2.9.1", 79 | "express": "^4.14.0", 80 | "express-graphql": "^0.6.11", 81 | "generic-pool": "^3.1.5", 82 | "graphql": "^0.11.2", 83 | "hapi": "^16.5.2", 84 | "https-pem": "^2.0.0", 85 | "inquirer": "^0.12.0", 86 | "ioredis": "^3.0.0", 87 | "knex": "^0.14.2", 88 | "koa": "^2.2.0", 89 | "koa-router": "^7.1.1", 90 | "mimic-response": "1.0.0", 91 | "mkdirp": "^0.5.0", 92 | "mongodb-core": "^2.1.2", 93 | "mysql": "^2.14.1", 94 | "nock": "^9.0.14", 95 | "once": "^1.4.0", 96 | "pg": "^7.1.0", 97 | "redis": "^2.6.3", 98 | "restify": "^4.3.0", 99 | "standard": "^10.0.2", 100 | "tape": "^4.8.0", 101 | "test-all-versions": "^3.1.1", 102 | "untildify": "^3.0.2", 103 | "ws": "^3.0.0" 104 | }, 105 | "greenkeeper": { 106 | "ignore": [ 107 | "inquirer", 108 | "restify" 109 | ] 110 | }, 111 | "standard": { 112 | "ignore": [ 113 | "/test/sourcemaps/fixtures/lib" 114 | ] 115 | }, 116 | "coordinates": [ 117 | 55.777577, 118 | 12.589851 119 | ] 120 | } 121 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/express.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (express, agent, version) { 8 | agent._platform.framework = { name: 'express', version: version } 9 | 10 | if (!semver.satisfies(version, '^4.0.0')) { 11 | debug('express version %s not supported - aborting...', version) 12 | return express 13 | } 14 | 15 | debug('shimming express.Router.use function') 16 | 17 | // The `use` function is called when Express app or Router sub-app is 18 | // initialized. This is the only place where we can get a hold of the 19 | // original path given when mounting a sub-app. 20 | shimmer.wrap(express.Router, 'use', function (orig) { 21 | return function (fn) { 22 | if (typeof fn === 'string' && Array.isArray(this.stack)) { 23 | var offset = this.stack.length 24 | var result = orig.apply(this, arguments) 25 | var layer 26 | 27 | for (; offset < this.stack.length; offset++) { 28 | layer = this.stack[offset] 29 | 30 | if (layer && (fn !== '/' || (layer.regexp && !layer.regexp.fast_slash))) { 31 | debug('shimming layer.handle_request function (layer: %s)', layer.name) 32 | 33 | shimmer.wrap(layer, 'handle_request', function (orig) { 34 | return function (req, res, next) { 35 | if (req.route) { 36 | // We use the signal of the route being set on the request 37 | // object as indicating that the correct route have been 38 | // found. When this happens we should no longer push and pop 39 | // mount-paths on the stack 40 | req._opbeat_mountstack_locked = true 41 | } else if (!req._opbeat_mountstack_locked && typeof next === 'function') { 42 | if (!req._opbeat_mountstack) req._opbeat_mountstack = [fn] 43 | else req._opbeat_mountstack.push(fn) 44 | 45 | arguments[2] = function () { 46 | req._opbeat_mountstack.pop() 47 | return next.apply(this, arguments) 48 | } 49 | } 50 | 51 | return orig.apply(this, arguments) 52 | } 53 | }) 54 | } else { 55 | debug('skip shimming layer.handle_request (layer: %s, path: %s)', (layer && layer.name) || typeof layer, fn) 56 | } 57 | } 58 | 59 | return result 60 | } else { 61 | return orig.apply(this, arguments) 62 | } 63 | } 64 | }) 65 | 66 | debug('shimming express.static function') 67 | 68 | shimmer.wrap(express, 'static', function wrapStatic (orig) { 69 | // By the time of this writing, Express adds a `mime` property to the 70 | // `static` function that needs to be copied to the wrapped function. 71 | // Instead of only copying the `mime` function, let's loop over all 72 | // properties in case new properties are added in later versions of 73 | // Express. 74 | Object.keys(orig).forEach(function (prop) { 75 | debug('copying property %s from express.static', prop) 76 | wrappedStatic[prop] = orig[prop] 77 | }) 78 | 79 | return wrappedStatic 80 | 81 | function wrappedStatic () { 82 | var origServeStatic = orig.apply(this, arguments) 83 | return function serveStatic (req, res, next) { 84 | req._opbeat_static = true 85 | 86 | return origServeStatic(req, res, nextHook) 87 | 88 | function nextHook (err) { 89 | if (!err) req._opbeat_static = false 90 | return next.apply(this, arguments) 91 | } 92 | } 93 | } 94 | }) 95 | 96 | return express 97 | } 98 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var os = require('os') 4 | var fs = require('fs') 5 | var path = require('path') 6 | var objectAssign = require('object-assign') 7 | var bool = require('normalize-bool') 8 | 9 | module.exports = config 10 | 11 | var confPath = path.resolve(process.env.OPBEAT_CONFIG_FILE || 'opbeat.js') 12 | if (fs.existsSync(confPath)) { 13 | try { 14 | var confFile = require(confPath) 15 | } catch (err) { 16 | console.error('Opbeat initialization error: Can\'t read config file %s', confPath) 17 | console.error(err.stack) 18 | } 19 | } 20 | 21 | var DEFAULTS = { 22 | active: true, 23 | logLevel: 'info', 24 | hostname: os.hostname(), 25 | stackTraceLimit: Infinity, 26 | captureExceptions: true, 27 | exceptionLogLevel: 'fatal', 28 | filterHttpHeaders: true, 29 | captureTraceStackTraces: true, 30 | logBody: false, 31 | timeout: true, 32 | timeoutErrorThreshold: 25000, 33 | instrument: true, 34 | ff_captureFrame: false, 35 | _apiHost: 'intake.opbeat.com' 36 | } 37 | 38 | var ENV_TABLE = { 39 | appId: 'OPBEAT_APP_ID', 40 | organizationId: 'OPBEAT_ORGANIZATION_ID', 41 | secretToken: 'OPBEAT_SECRET_TOKEN', 42 | active: 'OPBEAT_ACTIVE', 43 | logLevel: 'OPBEAT_LOG_LEVEL', 44 | hostname: 'OPBEAT_HOSTNAME', 45 | stackTraceLimit: 'OPBEAT_STACK_TRACE_LIMIT', 46 | captureExceptions: 'OPBEAT_CAPTURE_EXCEPTIONS', 47 | exceptionLogLevel: 'OPBEAT_EXCEPTION_LOG_LEVEL', 48 | filterHttpHeaders: 'OPBEAT_FILTER_HTTP_HEADERS', 49 | captureTraceStackTraces: 'OPBEAT_CAPTURE_TRACE_STACK_TRACES', 50 | logBody: 'OPBEAT_LOG_BODY', 51 | timeout: 'OPBEAT_TIMEOUT', 52 | timeoutErrorThreshold: 'OPBEAT_TIMEOUT_ERROR_THRESHOLD', 53 | instrument: 'OPBEAT_INSTRUMENT', 54 | flushInterval: 'OPBEAT_FLUSH_INTERVAL', 55 | ff_captureFrame: 'OPBEAT_FF_CAPTURE_FRAME', 56 | _apiHost: 'OPBEAT_API_HOST', // for testing only - don't use! 57 | _apiPort: 'OPBEAT_API_PORT', // for testing only - don't use! 58 | _apiSecure: 'OPBEAT_API_SECURE' // for testing only - don't use! 59 | } 60 | 61 | var BOOL_OPTS = [ 62 | 'active', 63 | 'captureExceptions', 64 | 'filterHttpHeaders', 65 | 'captureTraceStackTraces', 66 | 'logBody', 67 | 'timeout', 68 | 'instrument', 69 | 'ff_captureFrame' 70 | ] 71 | 72 | function config (opts) { 73 | opts = objectAssign( 74 | {}, 75 | DEFAULTS, // default options 76 | readEnv(), // options read from environment variables 77 | confFile, // options read from opbeat.js config file 78 | opts // options passed in to agent.start() 79 | ) 80 | 81 | normalizeIgnoreOptions(opts) 82 | normalizeBools(opts) 83 | 84 | return opts 85 | } 86 | 87 | function readEnv () { 88 | var opts = {} 89 | 90 | Object.keys(ENV_TABLE).forEach(function (key) { 91 | var env = ENV_TABLE[key] 92 | if (env in process.env) opts[key] = process.env[env] 93 | }) 94 | 95 | return opts 96 | } 97 | 98 | function normalizeIgnoreOptions (opts) { 99 | opts.ignoreUrlStr = [] 100 | opts.ignoreUrlRegExp = [] 101 | opts.ignoreUserAgentStr = [] 102 | opts.ignoreUserAgentRegExp = [] 103 | 104 | if (opts.ignoreUrls) { 105 | opts.ignoreUrls.forEach(function (ptn) { 106 | if (typeof ptn === 'string') opts.ignoreUrlStr.push(ptn) 107 | else opts.ignoreUrlRegExp.push(ptn) 108 | }) 109 | delete opts.ignoreUrls 110 | } 111 | 112 | if (opts.ignoreUserAgents) { 113 | opts.ignoreUserAgents.forEach(function (ptn) { 114 | if (typeof ptn === 'string') opts.ignoreUserAgentStr.push(ptn) 115 | else opts.ignoreUserAgentRegExp.push(ptn) 116 | }) 117 | delete opts.ignoreUserAgents 118 | } 119 | } 120 | 121 | function normalizeBools (opts) { 122 | BOOL_OPTS.forEach(function (key) { 123 | if (key in opts) opts[key] = bool(opts[key]) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /test/instrumentation/modules/koa-router/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var semver = require('semver') 11 | var version = require('koa-router/package').version 12 | var koaVersion = require('koa/package').version 13 | 14 | if (semver.gte(koaVersion, '2.0.0') && semver.lt(process.version, '6.0.0')) process.exit() 15 | 16 | var test = require('tape') 17 | var http = require('http') 18 | var Koa = require('koa') 19 | var Router = require('koa-router') 20 | 21 | test('route naming', function (t) { 22 | t.plan(19) 23 | 24 | resetAgent(function (endpoint, headers, data, cb) { 25 | assert(t, data) 26 | server.close() 27 | }) 28 | 29 | var server = startServer(function (port) { 30 | http.get('http://localhost:' + port + '/hello', function (res) { 31 | t.equal(res.statusCode, 200) 32 | res.on('data', function (chunk) { 33 | t.equal(chunk.toString(), 'hello world') 34 | }) 35 | res.on('end', function () { 36 | agent._instrumentation._queue._flush() 37 | }) 38 | }) 39 | }) 40 | }) 41 | 42 | test('route naming with params', function (t) { 43 | t.plan(19) 44 | 45 | resetAgent(function (endpoint, headers, data, cb) { 46 | assert(t, data, {name: 'GET /hello/:name'}) 47 | server.close() 48 | }) 49 | 50 | var server = startServer(function (port) { 51 | http.get('http://localhost:' + port + '/hello/thomas', function (res) { 52 | t.equal(res.statusCode, 200) 53 | res.on('data', function (chunk) { 54 | t.equal(chunk.toString(), 'hello thomas') 55 | }) 56 | res.on('end', function () { 57 | agent._instrumentation._queue._flush() 58 | }) 59 | }) 60 | }) 61 | }) 62 | 63 | function startServer (cb) { 64 | var server = buildServer() 65 | server.listen(function () { 66 | cb(server.address().port) 67 | }) 68 | return server 69 | } 70 | 71 | function buildServer () { 72 | var app = new Koa() 73 | var router = new Router() 74 | 75 | if (semver.lt(version, '6.0.0')) { 76 | require('./_generators')(router) 77 | } else if (semver.gte(version, '6.0.0')) { 78 | require('./_non-generators')(router) 79 | } 80 | 81 | app 82 | .use(router.routes()) 83 | .use(router.allowedMethods()) 84 | 85 | return http.createServer(app.callback()) 86 | } 87 | 88 | function assert (t, data, results) { 89 | if (!results) results = {} 90 | results.status = results.status || 200 91 | results.name = results.name || 'GET /hello' 92 | 93 | t.equal(data.transactions.length, 1) 94 | t.equal(data.transactions[0].kind, 'request') 95 | t.equal(data.transactions[0].result, results.status) 96 | t.equal(data.transactions[0].transaction, results.name) 97 | 98 | t.equal(data.traces.groups.length, 1) 99 | t.equal(data.traces.groups[0].kind, 'transaction') 100 | t.deepEqual(data.traces.groups[0].parents, []) 101 | t.equal(data.traces.groups[0].signature, 'transaction') 102 | t.equal(data.traces.groups[0].transaction, results.name) 103 | 104 | t.equal(data.traces.raw.length, 1) 105 | t.equal(data.traces.raw[0].length, 3) 106 | t.equal(data.traces.raw[0][1].length, 3) 107 | t.equal(data.traces.raw[0][1][0], 0) 108 | t.equal(data.traces.raw[0][1][1], 0) 109 | t.equal(data.traces.raw[0][1][2], data.traces.raw[0][0]) 110 | t.equal(data.traces.raw[0][2].http.method, 'GET') 111 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 112 | } 113 | 114 | function resetAgent (cb) { 115 | agent._instrumentation.currentTransaction = null 116 | agent._instrumentation._queue._clear() 117 | agent._httpClient = { request: cb || function () {} } 118 | agent.captureError = function (err) { throw err } 119 | } 120 | -------------------------------------------------------------------------------- /lib/instrumentation/trace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('opbeat') 4 | 5 | module.exports = Trace 6 | 7 | function Trace (transaction) { 8 | this.transaction = transaction 9 | this.started = false 10 | this.truncated = false 11 | this.ended = false 12 | this.extra = {} 13 | this.name = null 14 | this.type = null 15 | this._start = 0 16 | this._hrtime = null 17 | this._diff = null 18 | this._stackObj = null 19 | this._agent = transaction._agent 20 | this._parent = transaction._rootTrace 21 | 22 | debug('init trace %o', {id: this.transaction.id}) 23 | } 24 | 25 | Trace.prototype.start = function (name, type) { 26 | if (this.started) { 27 | debug('tried to call trace.start() on already started trace %o', {id: this.transaction.id, name: this.name, type: this.type}) 28 | return 29 | } 30 | 31 | this.started = true 32 | this.name = name || this.name || 'unnamed' 33 | this.type = type || this.type || 'custom' 34 | 35 | if (!this._stackObj) this._recordStackTrace() 36 | 37 | this._start = Date.now() 38 | this._hrtime = process.hrtime() 39 | 40 | debug('start trace %o', {id: this.transaction.id, name: name, type: type}) 41 | } 42 | 43 | Trace.prototype.customStackTrace = function (stackObj) { 44 | debug('applying custom stack trace to trace %o', {id: this.transaction.id}) 45 | this._recordStackTrace(stackObj) 46 | } 47 | 48 | Trace.prototype.truncate = function () { 49 | if (!this.started) { 50 | debug('tried to truncate non-started trace - ignoring %o', {id: this.transaction.id, name: this.name, type: this.type}) 51 | return 52 | } else if (this.ended) { 53 | debug('tried to truncate already ended trace - ignoring %o', {id: this.transaction.id, name: this.name, type: this.type}) 54 | return 55 | } 56 | this.truncated = true 57 | this.end() 58 | } 59 | 60 | Trace.prototype.end = function () { 61 | if (!this.started) { 62 | debug('tried to call trace.end() on un-started trace %o', {id: this.transaction.id, name: this.name, type: this.type}) 63 | return 64 | } else if (this.ended) { 65 | debug('tried to call trace.end() on already ended trace %o', {id: this.transaction.id, name: this.name, type: this.type}) 66 | return 67 | } 68 | 69 | this._diff = process.hrtime(this._hrtime) 70 | this._agent._instrumentation._recoverTransaction(this.transaction) 71 | 72 | this.ended = true 73 | debug('ended trace %o', {id: this.transaction.id, name: this.name, type: this.type, truncated: this.truncated}) 74 | this.transaction._recordEndedTrace(this) 75 | } 76 | 77 | Trace.prototype.duration = function () { 78 | if (!this.ended) { 79 | debug('tried to call trace.duration() on un-ended trace %o', {id: this.transaction.id, name: this.name, type: this.type}) 80 | return null 81 | } 82 | 83 | var ns = this._diff[0] * 1e9 + this._diff[1] 84 | return ns / 1e6 85 | } 86 | 87 | Trace.prototype.startTime = function () { 88 | if (!this.started) { 89 | debug('tried to call trace.startTime() for un-started trace %o', {id: this.transaction.id, name: this.name, type: this.type}) 90 | return null 91 | } 92 | 93 | if (!this._parent) return 0 94 | var start = this._parent._hrtime 95 | var ns = (this._hrtime[0] - start[0]) * 1e9 + (this._hrtime[1] - start[1]) 96 | return ns / 1e6 97 | } 98 | 99 | Trace.prototype.ancestors = function () { 100 | if (!this.ended || !this.transaction.ended) { 101 | debug('tried to call trace.ancestors() for un-ended trace/transaction %o', {id: this.transaction.id, name: this.name, type: this.type}) 102 | return null 103 | } 104 | 105 | if (!this._parent) return [] 106 | return this._parent.ancestors().concat(this._parent.name) 107 | } 108 | 109 | Trace.prototype._recordStackTrace = function (obj) { 110 | if (!obj) { 111 | obj = {} 112 | Error.captureStackTrace(obj, this) 113 | } 114 | this._stackObj = { err: obj } 115 | } 116 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/mongodb-core.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | var SERVER_FNS = ['insert', 'update', 'remove', 'auth'] 8 | var CURSOR_FNS_FIRST = ['_find', '_getmore'] 9 | 10 | module.exports = function (mongodb, agent, version) { 11 | if (!semver.satisfies(version, '>=1.2.7 <4.0.0')) { 12 | debug('mongodb-core version %s not supported - aborting...', version) 13 | return mongodb 14 | } 15 | 16 | if (mongodb.Server) { 17 | debug('shimming mongodb-core.Server.prototype.command') 18 | shimmer.wrap(mongodb.Server.prototype, 'command', wrapCommand) 19 | debug('shimming mongodb-core.Server.prototype functions:', SERVER_FNS) 20 | shimmer.massWrap(mongodb.Server.prototype, SERVER_FNS, wrapQuery) 21 | } 22 | 23 | if (mongodb.Cursor) { 24 | debug('shimming mongodb-core.Cursor.prototype functions:', CURSOR_FNS_FIRST) 25 | shimmer.massWrap(mongodb.Cursor.prototype, CURSOR_FNS_FIRST, wrapCursor) 26 | } 27 | 28 | return mongodb 29 | 30 | function wrapCommand (orig) { 31 | return function wrappedFunction (ns, cmd) { 32 | var trace = agent.buildTrace() 33 | var id = trace && trace.transaction.id 34 | 35 | debug('intercepted call to mongodb-core.Server.prototype.command %o', { id: id, ns: ns }) 36 | 37 | if (trace && arguments.length > 0) { 38 | var index = arguments.length - 1 39 | var cb = arguments[index] 40 | if (typeof cb === 'function') { 41 | var type 42 | if (cmd.findAndModify) type = 'findAndModify' 43 | else if (cmd.createIndexes) type = 'createIndexes' 44 | else if (cmd.ismaster) type = 'ismaster' 45 | else if (cmd.count) type = 'count' 46 | else type = 'command' 47 | 48 | arguments[index] = wrappedCallback 49 | trace.start(ns + '.' + type, 'db.mongodb.query') 50 | } 51 | } 52 | 53 | return orig.apply(this, arguments) 54 | 55 | function wrappedCallback () { 56 | debug('intercepted mongodb-core.Server.prototype.command callback %o', { id: id }) 57 | trace.end() 58 | return cb.apply(this, arguments) 59 | } 60 | } 61 | } 62 | 63 | function wrapQuery (orig, name) { 64 | return function wrappedFunction (ns) { 65 | var trace = agent.buildTrace() 66 | var id = trace && trace.transaction.id 67 | 68 | debug('intercepted call to mongodb-core.Server.prototype.%s %o', name, { id: id, ns: ns }) 69 | 70 | if (trace && arguments.length > 0) { 71 | var index = arguments.length - 1 72 | var cb = arguments[index] 73 | if (typeof cb === 'function') { 74 | arguments[index] = wrappedCallback 75 | trace.start(ns + '.' + name, 'db.mongodb.query') 76 | } 77 | } 78 | 79 | return orig.apply(this, arguments) 80 | 81 | function wrappedCallback () { 82 | debug('intercepted mongodb-core.Server.prototype.%s callback %o', name, { id: id }) 83 | trace.end() 84 | return cb.apply(this, arguments) 85 | } 86 | } 87 | } 88 | 89 | function wrapCursor (orig, name) { 90 | return function wrappedFunction () { 91 | var trace = agent.buildTrace() 92 | var id = trace && trace.transaction.id 93 | 94 | debug('intercepted call to mongodb-core.Cursor.prototype.%s %o', name, { id: id }) 95 | 96 | if (trace && arguments.length > 0) { 97 | var cb = arguments[0] 98 | if (typeof cb === 'function') { 99 | arguments[0] = wrappedCallback 100 | trace.start(this.ns + '.' + (this.cmd.find ? 'find' : name), 'db.mongodb.query') 101 | } 102 | } 103 | 104 | return orig.apply(this, arguments) 105 | 106 | function wrappedCallback () { 107 | debug('intercepted mongodb-core.Cursor.prototype.%s callback %o', name, { id: id }) 108 | trace.end() 109 | return cb.apply(this, arguments) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/instrumentation/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var hook = require('require-in-the-middle') 6 | var Transaction = require('./transaction') 7 | var Queue = require('./queue') 8 | var debug = require('debug')('opbeat') 9 | var request = require('../request') 10 | 11 | var MODULES = ['http', 'https', 'generic-pool', 'mongodb-core', 'pg', 'mysql', 'express', 'hapi', 'redis', 'ioredis', 'bluebird', 'knex', 'koa-router', 'ws', 'graphql', 'express-graphql'] 12 | 13 | module.exports = Instrumentation 14 | 15 | function Instrumentation (agent) { 16 | this._agent = agent 17 | this._queue = null 18 | this._started = false 19 | this.currentTransaction = null 20 | } 21 | 22 | Instrumentation.prototype.start = function () { 23 | if (!this._agent.instrument) return 24 | 25 | var self = this 26 | this._started = true 27 | 28 | this._queue = new Queue({flushInterval: this._agent._flushInterval}, function (result) { 29 | if (self._agent.active) request.transactions(self._agent, result) 30 | }) 31 | 32 | require('./patch-async')(this) 33 | 34 | debug('shimming Module._load function') 35 | hook(MODULES, function (exports, name, basedir) { 36 | var pkg, version 37 | 38 | if (basedir) { 39 | pkg = path.join(basedir, 'package.json') 40 | try { 41 | version = JSON.parse(fs.readFileSync(pkg)).version 42 | } catch (e) { 43 | debug('could not shim %s module: %s', name, e.message) 44 | return exports 45 | } 46 | } else { 47 | version = process.versions.node 48 | } 49 | 50 | debug('shimming %s@%s module', name, version) 51 | return require('./modules/' + name)(exports, self._agent, version) 52 | }) 53 | } 54 | 55 | Instrumentation.prototype.addEndedTransaction = function (transaction) { 56 | if (this._started) { 57 | debug('adding transaction to queue %o', {id: transaction.id}) 58 | this._queue.add(transaction) 59 | } else { 60 | debug('ignoring transaction %o', {id: transaction.id}) 61 | } 62 | } 63 | 64 | Instrumentation.prototype.startTransaction = function (name, type) { 65 | return new Transaction(this._agent, name, type) 66 | } 67 | 68 | Instrumentation.prototype.endTransaction = function () { 69 | if (!this.currentTransaction) return debug('cannot end transaction - no active transaction found') 70 | this.currentTransaction.end() 71 | } 72 | 73 | Instrumentation.prototype.setDefaultTransactionName = function (name) { 74 | var trans = this.currentTransaction 75 | if (!trans) return debug('no active transaction found - cannot set default transaction name') 76 | trans.setDefaultName(name) 77 | } 78 | 79 | Instrumentation.prototype.setTransactionName = function (name) { 80 | var trans = this.currentTransaction 81 | if (!trans) return debug('no active transaction found - cannot set transaction name') 82 | trans.name = name 83 | } 84 | 85 | Instrumentation.prototype.buildTrace = function () { 86 | if (!this.currentTransaction) { 87 | debug('no active transaction found - cannot build new trace') 88 | return null 89 | } 90 | 91 | return this.currentTransaction.buildTrace() 92 | } 93 | 94 | Instrumentation.prototype.bindFunction = function (original) { 95 | if (typeof original !== 'function' || original.name === 'opbeatCallbackWrapper') return original 96 | 97 | var ins = this 98 | var trans = this.currentTransaction 99 | 100 | return opbeatCallbackWrapper 101 | 102 | function opbeatCallbackWrapper () { 103 | var prev = ins.currentTransaction 104 | ins.currentTransaction = trans 105 | var result = original.apply(this, arguments) 106 | ins.currentTransaction = prev 107 | return result 108 | } 109 | } 110 | 111 | Instrumentation.prototype._recoverTransaction = function (trans) { 112 | if (this.currentTransaction === trans) return 113 | 114 | debug('recovering from wrong currentTransaction %o', { 115 | wrong: this.currentTransaction ? this.currentTransaction.id : undefined, 116 | correct: trans.id 117 | }) 118 | 119 | this.currentTransaction = trans 120 | } 121 | -------------------------------------------------------------------------------- /test/instrumentation/modules/bluebird/_coroutine.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var BLUEBIRD_VERSION = require('bluebird/package').version 5 | 6 | module.exports = function (test, Promise, ins) { 7 | var bluebird = Promise 8 | 9 | // bluebird@3.1.2 have a bug in the Promise.coroutine code prior to iojs 3 10 | if (!(semver.lt(process.version, '3.0.0') && 11 | semver.eq(BLUEBIRD_VERSION, '3.1.2'))) { 12 | test('Promise.coroutine', function (t) { 13 | t.plan(10) 14 | twice(function () { 15 | var trans = ins.startTransaction() 16 | var start = Date.now() 17 | var pongTime 18 | 19 | function PingPong () {} 20 | 21 | PingPong.prototype.ping = Promise.coroutine(function * (val) { 22 | if (val === 2) { 23 | // timing is hard, let's give it a -5ms slack 24 | var pingTime = Date.now() 25 | t.ok(pongTime + 45 <= pingTime, 'after pong, min 100ms should have passed (took ' + (pingTime - pongTime) + 'ms)') 26 | t.ok(pongTime + 100 > pingTime, 'after pong, max 125ms should have passed (took ' + (pingTime - pongTime) + 'ms)') 27 | t.equal(ins.currentTransaction.id, trans.id) 28 | return 29 | } 30 | yield Promise.delay(50) 31 | this.pong(val + 1) 32 | }) 33 | 34 | PingPong.prototype.pong = Promise.coroutine(function * (val) { 35 | // timing is hard, let's give it a -5ms slack 36 | pongTime = Date.now() 37 | t.ok(start + 45 <= pongTime, 'after ping, min 50ms should have passed (took ' + (pongTime - start) + 'ms)') 38 | t.ok(start + 100 > pongTime, 'after ping, max 75ms should have passed (took ' + (pongTime - start) + 'ms)') 39 | yield Promise.delay(50) 40 | this.ping(val + 1) 41 | }) 42 | 43 | var a = new PingPong() 44 | a.ping(0) 45 | }) 46 | }) 47 | } 48 | 49 | if (semver.satisfies(BLUEBIRD_VERSION, '>3.4.0')) { 50 | test('Promise.coroutine.addYieldHandler', function (t) { 51 | t.plan(10) 52 | 53 | var Promise = bluebird.getNewLibraryCopy() 54 | 55 | Promise.coroutine.addYieldHandler(function (value) { 56 | return Promise.delay(value) 57 | }) 58 | 59 | twice(function () { 60 | var trans = ins.startTransaction() 61 | var start = Date.now() 62 | var pongTime 63 | 64 | function PingPong () {} 65 | 66 | PingPong.prototype.ping = Promise.coroutine(function * (val) { 67 | if (val === 2) { 68 | // timing is hard, let's give it a -5ms slack 69 | var pingTime = Date.now() 70 | t.ok(pongTime + 45 <= pingTime, 'after pong, min 100ms should have passed (took ' + (pingTime - pongTime) + 'ms)') 71 | t.ok(pongTime + 100 > pingTime, 'after pong, max 125ms should have passed (took ' + (pingTime - pongTime) + 'ms)') 72 | t.equal(ins.currentTransaction.id, trans.id) 73 | return 74 | } 75 | yield 50 76 | this.pong(val + 1) 77 | }) 78 | 79 | PingPong.prototype.pong = Promise.coroutine(function * (val) { 80 | // timing is hard, let's give it a -5ms slack 81 | pongTime = Date.now() 82 | t.ok(start + 45 <= pongTime, 'after ping, min 50ms should have passed (took ' + (pongTime - start) + 'ms)') 83 | t.ok(start + 100 > pongTime, 'after ping, max 75ms should have passed (took ' + (pongTime - start) + 'ms)') 84 | yield 50 85 | this.ping(val + 1) 86 | }) 87 | 88 | var a = new PingPong() 89 | a.ping(0) 90 | }) 91 | }) 92 | } 93 | 94 | // Promise.spawn throws a deprecation error in <=2.8.2 95 | if (semver.gt(BLUEBIRD_VERSION, '2.8.2')) { 96 | test('Promise.spawn', function (t) { 97 | t.plan(4) 98 | twice(function () { 99 | var trans = ins.startTransaction() 100 | 101 | Promise.spawn(function * () { 102 | return yield Promise.resolve('foo') 103 | }).then(function (value) { 104 | t.equal(value, 'foo') 105 | t.equal(ins.currentTransaction.id, trans.id) 106 | }) 107 | }) 108 | }) 109 | } 110 | } 111 | 112 | function twice (fn) { 113 | setImmediate(fn) 114 | setImmediate(fn) 115 | } 116 | -------------------------------------------------------------------------------- /test/instrumentation/modules/ws.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | // ws >2 doesn't support Node.js v0.x 11 | var semver = require('semver') 12 | var pkg = require('ws/package') 13 | if (semver.lt(process.version, '1.0.0') && semver.gte(pkg.version, '2.0.0')) { 14 | process.exit() 15 | } 16 | 17 | var PORT = 12342 18 | 19 | var test = require('tape') 20 | var WebSocket = require('ws') 21 | 22 | test('ws.send', function (t) { 23 | resetAgent(done(t)) 24 | 25 | var wss = new WebSocket.Server({port: PORT}) 26 | 27 | wss.on('connection', function (ws) { 28 | ws.on('message', function (message) { 29 | t.equal(message, 'ping') 30 | ws.send('pong') 31 | }) 32 | }) 33 | 34 | var ws = new WebSocket('ws://localhost:' + PORT) 35 | 36 | ws.on('open', function () { 37 | agent.startTransaction('foo', 'websocket') 38 | ws.send('ping', function () { 39 | agent.endTransaction() 40 | }) 41 | }) 42 | 43 | ws.on('message', function (message) { 44 | t.equal(message, 'pong') 45 | wss.close(function () { 46 | agent._instrumentation._queue._flush() 47 | }) 48 | }) 49 | }) 50 | 51 | // { transactions: 52 | // [ { transaction: 'foo', 53 | // result: undefined, 54 | // kind: 'websocket', 55 | // timestamp: '2017-01-13T13:44:00.000Z', 56 | // durations: [ 8.973545 ] } ], 57 | // traces: 58 | // { groups: 59 | // [ { transaction: 'foo', 60 | // signature: 'Send WebSocket Message', 61 | // kind: 'websocket.send', 62 | // transaction_kind: 'websocket', 63 | // timestamp: '2017-01-13T13:44:00.000Z', 64 | // parents: [ 'transaction' ], 65 | // extra: { _frames: [Object] } }, 66 | // { transaction: 'foo', 67 | // signature: 'transaction', 68 | // kind: 'transaction', 69 | // transaction_kind: 'websocket', 70 | // timestamp: '2017-01-13T13:44:00.000Z', 71 | // parents: [], 72 | // extra: { _frames: [Object] } } ], 73 | // raw: 74 | // [ [ 8.973545, 75 | // [ 0, 1.216082, 7.105701 ], 76 | // [ 1, 0, 8.973545 ], 77 | // { extra: [Object] } ] ] } } 78 | function done (t) { 79 | return function (endpoint, headers, data, cb) { 80 | t.equal(data.transactions.length, 1) 81 | t.equal(data.transactions[0].transaction, 'foo') 82 | t.equal(data.transactions[0].kind, 'websocket') 83 | 84 | t.equal(data.traces.groups.length, 2) 85 | 86 | t.equal(data.traces.groups[0].kind, 'websocket.send') 87 | t.equal(data.traces.groups[0].transaction_kind, 'websocket') 88 | t.deepEqual(data.traces.groups[0].parents, ['transaction']) 89 | t.equal(data.traces.groups[0].signature, 'Send WebSocket Message') 90 | t.equal(data.traces.groups[0].transaction, 'foo') 91 | 92 | t.equal(data.traces.groups[1].kind, 'transaction') 93 | t.equal(data.traces.groups[1].transaction_kind, 'websocket') 94 | t.deepEqual(data.traces.groups[1].parents, []) 95 | t.equal(data.traces.groups[1].signature, 'transaction') 96 | t.equal(data.traces.groups[1].transaction, 'foo') 97 | 98 | var totalTraces = data.traces.raw[0].length - 2 99 | var totalTime = data.traces.raw[0][0] 100 | 101 | t.equal(data.traces.raw.length, 1) 102 | t.equal(totalTraces, 2) 103 | 104 | for (var i = 1; i < totalTraces + 1; i++) { 105 | t.equal(data.traces.raw[0][i].length, 3) 106 | t.ok(data.traces.raw[0][i][0] >= 0, 'group index should be >= 0') 107 | t.ok(data.traces.raw[0][i][0] < data.traces.groups.length, 'group index should be within allowed range') 108 | t.ok(data.traces.raw[0][i][1] >= 0) 109 | t.ok(data.traces.raw[0][i][2] <= totalTime) 110 | } 111 | 112 | t.equal(data.traces.raw[0][totalTraces][1], 0, 'root trace should start at 0') 113 | t.equal(data.traces.raw[0][totalTraces][2], data.traces.raw[0][0], 'root trace should last to total time') 114 | 115 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 116 | 117 | t.end() 118 | } 119 | } 120 | 121 | function resetAgent (cb) { 122 | agent._instrumentation._queue._clear() 123 | agent._instrumentation.currentTransaction = null 124 | agent._httpClient = { request: cb || function () {} } 125 | agent.captureError = function (err) { throw err } 126 | } 127 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/blacklisting.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../_agent')() 4 | 5 | var test = require('tape') 6 | var http = require('http') 7 | 8 | test('ignore url string - no match', function (t) { 9 | resetAgent({ 10 | _ignoreUrlStr: ['/exact'] 11 | }, function (endpoint, headers, data, cb) { 12 | assertNoMatch(t, data) 13 | t.end() 14 | }) 15 | request('/not/exact') 16 | }) 17 | 18 | test('ignore url string - match', function (t) { 19 | resetAgent({ 20 | _ignoreUrlStr: ['/exact'] 21 | }, function (endpoint, headers, data, cb) { 22 | assertMatch(t, data) 23 | t.end() 24 | }) 25 | request('/exact') 26 | }) 27 | 28 | test('ignore url regex - no match', function (t) { 29 | resetAgent({ 30 | _ignoreUrlRegExp: [/regex/] 31 | }, function (endpoint, headers, data, cb) { 32 | assertNoMatch(t, data) 33 | t.end() 34 | }) 35 | request('/no-match') 36 | }) 37 | 38 | test('ignore url regex - match', function (t) { 39 | resetAgent({ 40 | _ignoreUrlRegExp: [/regex/] 41 | }, function (endpoint, headers, data, cb) { 42 | assertMatch(t, data) 43 | t.end() 44 | }) 45 | request('/foo/regex/bar') 46 | }) 47 | 48 | test('ignore User-Agent string - no match', function (t) { 49 | resetAgent({ 50 | _ignoreUserAgentStr: ['exact'] 51 | }, function (endpoint, headers, data, cb) { 52 | assertNoMatch(t, data) 53 | t.end() 54 | }) 55 | request('/', { 'User-Agent': 'not-exact' }) 56 | }) 57 | 58 | test('ignore User-Agent string - match', function (t) { 59 | resetAgent({ 60 | _ignoreUserAgentStr: ['exact'] 61 | }, function (endpoint, headers, data, cb) { 62 | assertMatch(t, data) 63 | t.end() 64 | }) 65 | request('/', { 'User-Agent': 'exact-start' }) 66 | }) 67 | 68 | test('ignore User-Agent regex - no match', function (t) { 69 | resetAgent({ 70 | _ignoreUserAgentRegExp: [/regex/] 71 | }, function (endpoint, headers, data, cb) { 72 | assertNoMatch(t, data) 73 | t.end() 74 | }) 75 | request('/', { 'User-Agent': 'no-match' }) 76 | }) 77 | 78 | test('ignore User-Agent regex - match', function (t) { 79 | resetAgent({ 80 | _ignoreUserAgentRegExp: [/regex/] 81 | }, function (endpoint, headers, data, cb) { 82 | assertMatch(t, data) 83 | t.end() 84 | }) 85 | request('/', { 'User-Agent': 'foo-regex-bar' }) 86 | }) 87 | 88 | function assertNoMatch (t, data) { 89 | // data.traces.groups: 90 | t.equal(data.traces.groups.length, 1) 91 | 92 | t.equal(data.traces.groups[0].transaction, 'GET unknown route') 93 | t.equal(data.traces.groups[0].signature, 'transaction') 94 | t.equal(data.traces.groups[0].kind, 'transaction') 95 | t.deepEqual(data.traces.groups[0].parents, []) 96 | 97 | // data.transactions: 98 | t.equal(data.transactions.length, 1) 99 | t.equal(data.transactions[0].transaction, 'GET unknown route') 100 | t.equal(data.transactions[0].durations.length, 1) 101 | t.ok(data.transactions[0].durations[0] > 0) 102 | 103 | // data.traces.raw: 104 | // 105 | // [ 106 | // [ 107 | // 15.240414, // total transaction time 108 | // [ 0, 0, 15.240414 ] // root trace 109 | // ] 110 | // ] 111 | t.equal(data.traces.raw.length, 1) 112 | t.equal(data.traces.raw[0].length, 3) 113 | t.equal(data.traces.raw[0][0], data.transactions[0].durations[0]) 114 | t.equal(data.traces.raw[0][1].length, 3) 115 | 116 | t.equal(data.traces.raw[0][1][0], 0) 117 | t.equal(data.traces.raw[0][1][1], 0) 118 | t.equal(data.traces.raw[0][1][2], data.traces.raw[0][0]) 119 | 120 | t.equal(data.traces.raw[0][2].http.method, 'GET') 121 | } 122 | 123 | function assertMatch (t, data) { 124 | t.deepEqual(data, { transactions: [], traces: { groups: [], raw: [] } }) 125 | } 126 | 127 | function request (path, headers) { 128 | var server = http.createServer(function (req, res) { 129 | res.end() 130 | }) 131 | 132 | server.listen(function () { 133 | var opts = { 134 | port: server.address().port, 135 | path: path, 136 | headers: headers 137 | } 138 | http.request(opts, function (res) { 139 | res.on('end', function () { 140 | agent._instrumentation._queue._flush() 141 | server.close() 142 | }) 143 | res.resume() 144 | }).end() 145 | }) 146 | } 147 | 148 | function resetAgent (opts, cb) { 149 | agent._httpClient = { request: cb } 150 | agent._ignoreUrlStr = opts._ignoreUrlStr || [] 151 | agent._ignoreUrlRegExp = opts._ignoreUrlRegExp || [] 152 | agent._ignoreUserAgentStr = opts._ignoreUserAgentStr || [] 153 | agent._ignoreUserAgentRegExp = opts._ignoreUserAgentRegExp || [] 154 | agent._instrumentation._queue._clear() 155 | agent._instrumentation.currentTransaction = null 156 | } 157 | -------------------------------------------------------------------------------- /lib/instrumentation/http-shared.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var url = require('url') 4 | var semver = require('semver') 5 | var eos = require('end-of-stream') 6 | var debug = require('debug')('opbeat') 7 | 8 | var SUPPORT_PREFINISH = semver.satisfies(process.version, '>=0.12') 9 | 10 | exports.instrumentRequest = function (agent, moduleName) { 11 | return function (orig) { 12 | return function (event, req, res) { 13 | if (event === 'request') { 14 | debug('intercepted request event call to %s.Server.prototype.emit', moduleName) 15 | 16 | if (isRequestBlacklisted(agent, req)) { 17 | debug('ignoring blacklisted request to %s', req.url) 18 | // don't leak previous transaction 19 | agent._instrumentation.currentTransaction = null 20 | } else { 21 | var trans = agent.startTransaction() 22 | trans.type = 'request' 23 | trans.req = req 24 | 25 | eos(res, function (err) { 26 | if (!err) return trans.end() 27 | 28 | if (agent.timeout.active && !trans.ended) { 29 | var duration = Date.now() - trans._start 30 | if (duration > agent.timeout.errorThreshold) { 31 | agent.captureError('Socket closed with active HTTP request (>' + (agent.timeout.errorThreshold / 1000) + ' sec)', { 32 | request: req, 33 | extra: { abortTime: duration } 34 | }) 35 | } 36 | } 37 | 38 | // Handle case where res.end is called after an error occurred on the 39 | // stream (e.g. if the underlying socket was prematurely closed) 40 | if (SUPPORT_PREFINISH) { 41 | res.on('prefinish', function () { 42 | trans.end() 43 | }) 44 | } else { 45 | res.on('finish', function () { 46 | trans.end() 47 | }) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | return orig.apply(this, arguments) 54 | } 55 | } 56 | } 57 | 58 | function isRequestBlacklisted (agent, req) { 59 | var i 60 | 61 | for (i = 0; i < agent._ignoreUrlStr.length; i++) { 62 | if (agent._ignoreUrlStr[i] === req.url) return true 63 | } 64 | for (i = 0; i < agent._ignoreUrlRegExp.length; i++) { 65 | if (agent._ignoreUrlRegExp[i].test(req.url)) return true 66 | } 67 | 68 | var ua = req.headers['user-agent'] 69 | if (!ua) return false 70 | 71 | for (i = 0; i < agent._ignoreUserAgentStr.length; i++) { 72 | if (ua.indexOf(agent._ignoreUserAgentStr[i]) === 0) return true 73 | } 74 | for (i = 0; i < agent._ignoreUserAgentRegExp.length; i++) { 75 | if (agent._ignoreUserAgentRegExp[i].test(ua)) return true 76 | } 77 | 78 | return false 79 | } 80 | 81 | exports.traceOutgoingRequest = function (agent, moduleName) { 82 | var traceType = 'ext.' + moduleName + '.http' 83 | 84 | return function (orig) { 85 | return function () { 86 | var trace = agent.buildTrace() 87 | var id = trace && trace.transaction.id 88 | 89 | debug('intercepted call to %s.request %o', moduleName, {id: id}) 90 | 91 | var req = orig.apply(this, arguments) 92 | if (!trace) return req 93 | if (req._headers.host === agent._apiHost) { 94 | debug('ignore %s request to intake API %o', moduleName, {id: id}) 95 | return req 96 | } else { 97 | var protocol = req.agent && req.agent.protocol 98 | debug('request details: %o', {protocol: protocol, host: req._headers.host, id: id}) 99 | } 100 | 101 | var name = req.method + ' ' + req._headers.host + url.parse(req.path).pathname 102 | trace.start(name, traceType) 103 | req.on('response', onresponse) 104 | 105 | return req 106 | 107 | function onresponse (res) { 108 | debug('intercepted http.ClientRequest response event %o', {id: id}) 109 | 110 | // Inspired by: 111 | // https://github.com/nodejs/node/blob/9623ce572a02632b7596452e079bba066db3a429/lib/events.js#L258-L274 112 | if (res.prependListener) { 113 | // Added in Node.js 6.0.0 114 | res.prependListener('end', onEnd) 115 | } else { 116 | var existing = res._events && res._events.end 117 | if (!existing) { 118 | res.on('end', onEnd) 119 | } else { 120 | if (typeof existing === 'function') { 121 | res._events.end = [onEnd, existing] 122 | } else { 123 | existing.unshift(onEnd) 124 | } 125 | } 126 | } 127 | 128 | function onEnd () { 129 | debug('intercepted http.IncomingMessage end event %o', {id: id}) 130 | trace.end() 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/hapi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var debug = require('debug')('opbeat') 5 | var shimmer = require('../shimmer') 6 | 7 | module.exports = function (hapi, agent, version) { 8 | agent._platform.framework = { name: 'hapi', version: version } 9 | 10 | if (!semver.satisfies(version, '>=9.0.0')) { 11 | debug('hapi version %s not supported - aborting...', version) 12 | return hapi 13 | } 14 | 15 | debug('shimming hapi.Server.prototype.initialize') 16 | 17 | shimmer.wrap(hapi.Server.prototype, 'initialize', function (orig) { 18 | return function () { 19 | // Hooks that are always allowed 20 | if (typeof this.on === 'function') { 21 | this.on('request-error', function (req, err) { 22 | agent.captureError(err, {request: req.raw && req.raw.req}) 23 | }) 24 | 25 | this.on('log', function (event, tags) { 26 | if (!event || !tags.error) return 27 | 28 | var payload = {extra: event} 29 | 30 | var err = event.data 31 | if (!(err instanceof Error) && typeof err !== 'string') { 32 | err = 'hapi server emitted a log event tagged error' 33 | } 34 | 35 | agent.captureError(err, payload) 36 | }) 37 | 38 | this.on('request', function (req, event, tags) { 39 | if (!event || !tags.error) return 40 | 41 | var payload = { 42 | extra: event, 43 | request: req.raw && req.raw.req 44 | } 45 | 46 | var err = event.data 47 | if (!(err instanceof Error) && typeof err !== 'string') { 48 | err = 'hapi server emitted a request event tagged error' 49 | } 50 | 51 | agent.captureError(err, payload) 52 | }) 53 | } else { 54 | debug('unable to enable hapi error tracking') 55 | } 56 | 57 | // When the hapi server has no connections we don't make connection 58 | // lifecycle hooks 59 | if (this.connections.length === 0) { 60 | debug('unable to enable hapi instrumentation on connectionless server') 61 | return orig.apply(this, arguments) 62 | } 63 | 64 | // Hooks that are only allowed when the hapi server has connections 65 | if (typeof this.ext === 'function') { 66 | this.ext('onPreAuth', function (request, reply) { 67 | debug('received hapi onPreAuth event') 68 | 69 | // Record the fact that the preAuth extension have been called. This 70 | // info is useful later to know if this is a CORS preflight request 71 | // that is automatically handled by hapi (as those will not trigger 72 | // the onPreAuth extention) 73 | request.__opbeat_onPreAuth = true 74 | 75 | if (request.route) { 76 | // fingerprint was introduced in hapi 11 and is a little more 77 | // stable in case the param names change 78 | // - path example: /foo/{bar*2} 79 | // - fingerprint example: /foo/?/? 80 | var fingerprint = request.route.fingerprint || request.route.path 81 | 82 | if (fingerprint) { 83 | var name = (request.raw && request.raw.req && request.raw.req.method) || 84 | (request.route.method && request.route.method.toUpperCase()) 85 | 86 | if (typeof name === 'string') { 87 | name = name + ' ' + fingerprint 88 | } else { 89 | name = fingerprint 90 | } 91 | 92 | agent._instrumentation.setDefaultTransactionName(name) 93 | } 94 | } 95 | 96 | return reply.continue() 97 | }) 98 | 99 | this.ext('onPreResponse', function (request, reply) { 100 | debug('received hapi onPreResponse event') 101 | 102 | // Detection of CORS preflight requests: 103 | // There is no easy way in hapi to get the matched route for a 104 | // CORS preflight request that matches any of the autogenerated 105 | // routes created by hapi when `cors: true`. The best solution is to 106 | // detect the request "fingerprint" using the magic if-sentence below 107 | // and group all those requests into on type of transaction 108 | if (!request.__opbeat_onPreAuth && 109 | request.route && request.route.path === '/{p*}' && 110 | request.raw && request.raw.req && request.raw.req.method === 'OPTIONS' && 111 | request.raw.req.headers['access-control-request-method']) { 112 | agent._instrumentation.setDefaultTransactionName('CORS preflight') 113 | } 114 | 115 | return reply.continue() 116 | }) 117 | } else { 118 | debug('unable to enable automatic hapi transaction naming') 119 | } 120 | 121 | return orig.apply(this, arguments) 122 | } 123 | }) 124 | 125 | return hapi 126 | } 127 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var os = require('os') 5 | var path = require('path') 6 | var util = require('util') 7 | var debug = require('debug')('opbeat') 8 | var trunc = require('unicode-byte-truncate') 9 | 10 | var GENERIC_PLATFORM_HEADER = util.format('lang=node/%s platform=%s os=%s', 11 | process.versions.node, 12 | process.release ? process.release.name : 'node', 13 | process.platform) 14 | 15 | var request = function (agent, endpoint, payload, cb) { 16 | if (!agent.active) { 17 | // failsafe - we should never reach this point 18 | debug('request attempted on an inactive agent - ignoring!') 19 | return 20 | } 21 | 22 | var headers = { 23 | 'X-Opbeat-Platform': GENERIC_PLATFORM_HEADER + (agent._platform.framework 24 | ? util.format(' framework=%s/%s', agent._platform.framework.name, agent._platform.framework.version) 25 | : '') 26 | } 27 | 28 | if (process.env.OPBEAT_DEBUG_PAYLOAD) capturePayload(endpoint, payload) 29 | 30 | agent._httpClient.request(endpoint, headers, payload, function (err, res, body) { 31 | if (err) return cb(err) 32 | if (res.statusCode < 200 || res.statusCode > 299) { 33 | var msg = util.format('Opbeat error (%d): %s', res.statusCode, body) 34 | cb(new Error(msg)) 35 | return 36 | } 37 | cb(null, res.headers.location) 38 | }) 39 | } 40 | 41 | exports.error = function (agent, payload, cb) { 42 | truncErrorPayload(payload) 43 | 44 | request(agent, 'errors', payload, function (err, url) { 45 | var uuid = payload.extra && payload.extra.uuid 46 | if (err) { 47 | if (cb) cb(err) 48 | agent.emit('error', err, uuid) 49 | return 50 | } 51 | if (cb) cb(null, url) 52 | agent.emit('logged', url, uuid) 53 | }) 54 | } 55 | 56 | exports.transactions = function (agent, payload, cb) { 57 | filterTransactionPayload(payload, agent._filters) 58 | truncTransactionPayload(payload) 59 | 60 | debug('sending transactions to intake api') 61 | request(agent, 'transactions', payload, function (err) { 62 | if (err) { 63 | if (cb) cb(err) 64 | agent.emit('error', err) 65 | return 66 | } 67 | debug('logged transactions successfully') 68 | }) 69 | } 70 | 71 | function filterTransactionPayload (payload, filters) { 72 | payload.traces.raw.forEach(function (raw) { 73 | var context = raw[raw.length - 1] 74 | context = filters.process(context) 75 | if (!context) raw.pop() 76 | else raw[raw.length - 1] = context 77 | }) 78 | } 79 | 80 | function truncErrorPayload (payload) { 81 | if (payload.stacktrace && payload.stacktrace.frames) { 82 | payload.stacktrace.frames = truncFrames(payload.stacktrace.frames) 83 | } 84 | 85 | if (payload.exception && payload.exception.value) { 86 | payload.exception.value = trunc(String(payload.exception.value), 2048) 87 | } 88 | 89 | if (payload.culprit) { 90 | payload.culprit = trunc(String(payload.culprit), 100) 91 | } 92 | 93 | if (payload.message) { 94 | payload.message = trunc(String(payload.message), 200) 95 | } 96 | } 97 | 98 | function truncTransactionPayload (payload) { 99 | if (payload.transactions) { 100 | payload.transactions.forEach(function (trans) { 101 | trans.transaction = trunc(String(trans.transaction), 512) 102 | }) 103 | } 104 | 105 | if (payload.traces && payload.traces.groups) { 106 | payload.traces.groups.forEach(function (group) { 107 | if (group.transaction) group.transaction = trunc(String(group.transaction), 512) 108 | if (group.signature) group.signature = trunc(String(group.signature), 512) 109 | if (group.extra && group.extra._frames) group.extra._frames = truncFrames(group.extra._frames) 110 | }) 111 | } 112 | } 113 | 114 | function truncFrames (frames) { 115 | // max 300 stack frames 116 | if (frames.length > 300) { 117 | // frames are reversed to match API requirements 118 | frames = frames.slice(-300) 119 | } 120 | 121 | // each line in stack trace must not exeed 1000 chars 122 | frames.forEach(function (frame, i) { 123 | if (frame.pre_context) frame.pre_context = truncEach(frame.pre_context, 1000) 124 | if (frame.context_line) frame.context_line = trunc(String(frame.context_line), 1000) 125 | if (frame.post_context) frame.post_context = truncEach(frame.post_context, 1000) 126 | }) 127 | 128 | return frames 129 | } 130 | 131 | function truncEach (arr, len) { 132 | return arr.map(function (str) { 133 | return trunc(String(str), len) 134 | }) 135 | } 136 | 137 | // Used only for debugging data sent to the intake API 138 | function capturePayload (endpoint, payload) { 139 | var dumpfile = path.join(os.tmpdir(), 'opbeat-' + endpoint + '-' + Date.now() + '.json') 140 | fs.writeFile(dumpfile, JSON.stringify(payload), function (err) { 141 | if (err) console.log('could not capture intake payload: %s', err.message) 142 | else console.log('intake payload captured: %s', dumpfile) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/timeout-disabled.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../_agent')() 4 | 5 | var test = require('tape') 6 | var http = require('http') 7 | 8 | agent.timeout.active = false 9 | 10 | test('client-side timeout - call end', function (t) { 11 | resetAgent() 12 | var clientReq 13 | 14 | t.equal(agent._instrumentation._queue._samples.length, 0, 'should not have any samples to begin with') 15 | 16 | var server = http.createServer(function (req, res) { 17 | res.on('close', function () { 18 | setTimeout(function () { 19 | t.equal(agent._instrumentation._queue._samples.length, 1, 'should add transactions to queue') 20 | server.close() 21 | t.end() 22 | }, 100) 23 | }) 24 | 25 | clientReq.abort() 26 | setTimeout(function () { 27 | res.write('Hello') // server emits clientError if written in same tick as abort 28 | setTimeout(function () { 29 | res.end(' World') 30 | }, 10) 31 | }, 10) 32 | }) 33 | 34 | server.listen(function () { 35 | var port = server.address().port 36 | clientReq = http.get('http://localhost:' + port, function (res) { 37 | t.fail('should not call http.get callback') 38 | }) 39 | clientReq.on('error', function (err) { 40 | if (err.code !== 'ECONNRESET') throw err 41 | }) 42 | }) 43 | }) 44 | 45 | test('client-side timeout - don\'t call end', function (t) { 46 | resetAgent() 47 | var clientReq 48 | 49 | t.equal(agent._instrumentation._queue._samples.length, 0, 'should not have any samples to begin with') 50 | 51 | var server = http.createServer(function (req, res) { 52 | res.on('close', function () { 53 | setTimeout(function () { 54 | t.equal(agent._instrumentation._queue._samples.length, 0, 'should not add transactions to queue') 55 | server.close() 56 | t.end() 57 | }, 100) 58 | }) 59 | 60 | clientReq.abort() 61 | setTimeout(function () { 62 | res.write('Hello') // server emits clientError if written in same tick as abort 63 | }, 10) 64 | }) 65 | 66 | server.listen(function () { 67 | var port = server.address().port 68 | clientReq = http.get('http://localhost:' + port, function (res) { 69 | t.fail('should not call http.get callback') 70 | }) 71 | clientReq.on('error', function (err) { 72 | if (err.code !== 'ECONNRESET') throw err 73 | }) 74 | }) 75 | }) 76 | 77 | test('server-side timeout - call end', function (t) { 78 | resetAgent() 79 | var timedout = false 80 | var closeEvent = false 81 | 82 | t.equal(agent._instrumentation._queue._samples.length, 0, 'should not have any samples to begin with') 83 | 84 | var server = http.createServer(function (req, res) { 85 | res.on('close', function () { 86 | closeEvent = true 87 | }) 88 | 89 | setTimeout(function () { 90 | t.ok(timedout, 'should have closed socket') 91 | t.ok(closeEvent, 'res should emit close event') 92 | res.end('Hello World') 93 | 94 | setTimeout(function () { 95 | t.equal(agent._instrumentation._queue._samples.length, 1, 'should not add transactions to queue') 96 | server.close() 97 | t.end() 98 | }, 50) 99 | }, 200) 100 | }) 101 | 102 | server.setTimeout(100) 103 | 104 | server.listen(function () { 105 | var port = server.address().port 106 | var clientReq = http.get('http://localhost:' + port, function (res) { 107 | t.fail('should not call http.get callback') 108 | }) 109 | clientReq.on('error', function (err) { 110 | if (err.code !== 'ECONNRESET') throw err 111 | timedout = true 112 | }) 113 | }) 114 | }) 115 | 116 | test('server-side timeout - don\'t call end', function (t) { 117 | resetAgent() 118 | var timedout = false 119 | var closeEvent = false 120 | 121 | t.equal(agent._instrumentation._queue._samples.length, 0, 'should not have any samples to begin with') 122 | 123 | var server = http.createServer(function (req, res) { 124 | res.on('close', function () { 125 | closeEvent = true 126 | }) 127 | 128 | setTimeout(function () { 129 | t.ok(timedout, 'should have closed socket') 130 | t.ok(closeEvent, 'res should emit close event') 131 | t.equal(agent._instrumentation._queue._samples.length, 0, 'should not add transactions to queue') 132 | server.close() 133 | t.end() 134 | }, 200) 135 | }) 136 | 137 | server.setTimeout(100) 138 | 139 | server.listen(function () { 140 | var port = server.address().port 141 | var clientReq = http.get('http://localhost:' + port, function (res) { 142 | t.fail('should not call http.get callback') 143 | }) 144 | clientReq.on('error', function (err) { 145 | if (err.code !== 'ECONNRESET') throw err 146 | timedout = true 147 | }) 148 | }) 149 | }) 150 | 151 | function resetAgent () { 152 | agent._instrumentation._queue._clear() 153 | agent._instrumentation.currentTransaction = null 154 | } 155 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/mysql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var sqlSummary = require('sql-summary') 5 | var debug = require('debug')('opbeat') 6 | var shimmer = require('../shimmer') 7 | 8 | module.exports = function (mysql, agent, version) { 9 | if (!semver.satisfies(version, '^2.0.0')) { 10 | debug('mysql version %s not supported - aborting...', version) 11 | return mysql 12 | } 13 | 14 | debug('shimming mysql.createConnection') 15 | shimmer.wrap(mysql, 'createConnection', wrapCreateConnection) 16 | 17 | debug('shimming mysql.createPool') 18 | shimmer.wrap(mysql, 'createPool', wrapCreatePool) 19 | 20 | debug('shimming mysql.createPoolCluster') 21 | shimmer.wrap(mysql, 'createPoolCluster', wrapCreatePoolCluster) 22 | 23 | return mysql 24 | 25 | function wrapCreateConnection (original) { 26 | return function wrappedCreateConnection () { 27 | var connection = original.apply(this, arguments) 28 | 29 | wrapQueryable(connection, 'connection', agent) 30 | 31 | return connection 32 | } 33 | } 34 | 35 | function wrapCreatePool (original) { 36 | return function wrappedCreatePool () { 37 | var pool = original.apply(this, arguments) 38 | 39 | debug('shimming mysql pool.getConnection') 40 | shimmer.wrap(pool, 'getConnection', wrapGetConnection) 41 | 42 | return pool 43 | } 44 | } 45 | 46 | function wrapCreatePoolCluster (original) { 47 | return function wrappedCreatePoolCluster () { 48 | var cluster = original.apply(this, arguments) 49 | 50 | debug('shimming mysql cluster.of') 51 | shimmer.wrap(cluster, 'of', function wrapOf (original) { 52 | return function wrappedOf () { 53 | var ofCluster = original.apply(this, arguments) 54 | 55 | debug('shimming mysql cluster of.getConnection') 56 | shimmer.wrap(ofCluster, 'getConnection', wrapGetConnection) 57 | 58 | return ofCluster 59 | } 60 | }) 61 | 62 | return cluster 63 | } 64 | } 65 | 66 | function wrapGetConnection (original) { 67 | return function wrappedGetConnection () { 68 | var cb = arguments[0] 69 | 70 | if (typeof cb === 'function') { 71 | arguments[0] = agent._instrumentation.bindFunction(function wrapedCallback (err, connection) { // eslint-disable-line handle-callback-err 72 | if (connection) wrapQueryable(connection, 'getConnection() > connection', agent) 73 | return cb.apply(this, arguments) 74 | }) 75 | } 76 | 77 | return original.apply(this, arguments) 78 | } 79 | } 80 | } 81 | 82 | function wrapQueryable (obj, objType, agent) { 83 | debug('shimming mysql %s.query', objType) 84 | shimmer.wrap(obj, 'query', wrapQuery) 85 | 86 | function wrapQuery (original) { 87 | return function wrappedQuery (sql, values, cb) { 88 | var trace = agent.buildTrace() 89 | var id = trace && trace.transaction.id 90 | var hasCallback = false 91 | var sqlStr 92 | 93 | debug('intercepted call to mysql %s.query %o', objType, { id: id }) 94 | 95 | if (trace) { 96 | trace.type = 'db.mysql.query' 97 | 98 | if (this._opbeatStackObj) { 99 | trace.customStackTrace(this._opbeatStackObj) 100 | this._opbeatStackObj = null 101 | } 102 | 103 | switch (typeof sql) { 104 | case 'string': 105 | sqlStr = sql 106 | break 107 | case 'object': 108 | if (typeof sql._callback === 'function') { 109 | sql._callback = wrapCallback(sql._callback) 110 | } 111 | sqlStr = sql.sql 112 | break 113 | case 'function': 114 | arguments[0] = wrapCallback(sql) 115 | break 116 | } 117 | 118 | if (sqlStr) { 119 | debug('extracted sql from mysql query %o', { id: id, sql: sqlStr }) 120 | trace.extra.sql = sqlStr 121 | trace.name = sqlSummary(sqlStr) 122 | } 123 | 124 | if (typeof values === 'function') { 125 | arguments[1] = wrapCallback(values) 126 | } else if (typeof cb === 'function') { 127 | arguments[2] = wrapCallback(cb) 128 | } 129 | } 130 | 131 | var result = original.apply(this, arguments) 132 | 133 | if (trace && result && !hasCallback) { 134 | shimmer.wrap(result, 'emit', function (original) { 135 | return function (event) { 136 | trace.start() 137 | switch (event) { 138 | case 'error': 139 | case 'end': 140 | trace.end() 141 | } 142 | return original.apply(this, arguments) 143 | } 144 | }) 145 | } 146 | 147 | return result 148 | 149 | function wrapCallback (cb) { 150 | hasCallback = true 151 | trace.start() 152 | return function wrappedCallback () { 153 | trace.end() 154 | return cb.apply(this, arguments) 155 | } 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/pg.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var sqlSummary = require('sql-summary') 5 | var debug = require('debug')('opbeat') 6 | var shimmer = require('../shimmer') 7 | 8 | module.exports = function (pg, agent, version) { 9 | if (!semver.satisfies(version, '>=4.0.0 <8.0.0')) { 10 | debug('pg version %s not supported - aborting...', version) 11 | return pg 12 | } 13 | 14 | patchClient(pg.Client, 'pg.Client', agent) 15 | 16 | // Trying to access the pg.native getter will trigger and log the warning 17 | // "Cannot find module 'pg-native'" to STDERR if the module isn't installed. 18 | // Overwriting the getter we can lazily patch the native client only if the 19 | // user is acually requesting it. 20 | var getter = pg.__lookupGetter__('native') 21 | if (getter) { 22 | delete pg.native 23 | // To be as true to the original pg module as possible, we use 24 | // __defineGetter__ instead of Object.defineProperty. 25 | pg.__defineGetter__('native', function () { 26 | var native = getter() 27 | if (native && native.Client) { 28 | patchClient(native.Client, 'pg.native.Client', agent) 29 | } 30 | return native 31 | }) 32 | } 33 | 34 | return pg 35 | } 36 | 37 | function patchClient (Client, klass, agent) { 38 | debug('shimming %s.prototype.query', klass) 39 | shimmer.wrap(Client.prototype, 'query', wrapQuery) 40 | shimmer.wrap(Client.prototype, '_pulseQueryQueue', wrapPulseQueryQueue) 41 | 42 | function wrapQuery (orig, name) { 43 | return function wrappedFunction (sql) { 44 | var trace = agent.buildTrace() 45 | var id = trace && trace.transaction.id 46 | 47 | if (sql && typeof sql.text === 'string') sql = sql.text 48 | 49 | debug('intercepted call to %s.prototype.%s %o', klass, name, { id: id, sql: sql }) 50 | 51 | if (trace) { 52 | var args = arguments 53 | var index = args.length - 1 54 | var cb = args[index] 55 | 56 | if (this._opbeatStackObj) { 57 | trace.customStackTrace(this._opbeatStackObj) 58 | this._opbeatStackObj = null 59 | } 60 | 61 | if (Array.isArray(cb)) { 62 | index = cb.length - 1 63 | cb = cb[index] 64 | } 65 | 66 | if (typeof sql === 'string') { 67 | trace.extra.sql = sql 68 | trace.start(sqlSummary(sql), 'db.postgresql.query') 69 | } else { 70 | debug('unable to parse sql form pg module (type: %s)', typeof sql) 71 | trace.start('SQL', 'db.postgresql.query') 72 | } 73 | 74 | if (typeof cb === 'function') { 75 | args[index] = end 76 | return orig.apply(this, arguments) 77 | } else { 78 | cb = null 79 | var query = orig.apply(this, arguments) 80 | 81 | // The order of these if-statements matter! 82 | // 83 | // `query.then` is broken in pg <7 >=6.3.0, and since 6.x supports 84 | // `query.on`, we'll try that first to ensure we don't fall through 85 | // and use `query.then` by accident. 86 | // 87 | // In 7+, we must use `query.then`, and since `query.on` have been 88 | // removed in 7.0.0, then it should work out. 89 | // 90 | // See this comment for details: 91 | // https://github.com/brianc/node-postgres/commit/b5b49eb895727e01290e90d08292c0d61ab86322#commitcomment-23267714 92 | if (typeof query.on === 'function') { 93 | query.on('end', end) 94 | query.on('error', end) 95 | } else if (typeof query.then === 'function') { 96 | query.then(end) 97 | } else { 98 | debug('ERROR: unknown pg query type: %s %o', typeof query, { id: id }) 99 | } 100 | 101 | return query 102 | } 103 | } else { 104 | return orig.apply(this, arguments) 105 | } 106 | 107 | function end () { 108 | debug('intercepted end of %s.prototype.%s %o', klass, name, { id: id }) 109 | trace.end() 110 | if (cb) return cb.apply(this, arguments) 111 | } 112 | } 113 | } 114 | 115 | // The client maintains an internal callback queue for all the queries. In 116 | // 7.0.0, the queries are true promises (as opposed to faking the Promise API 117 | // in ^6.3.0). To properly get the right context when the Promise API is 118 | // used, we need to patch all callbacks in the callback queue. 119 | // 120 | // _pulseQueryQueue is usually called when something have been added to the 121 | // client.queryQueue array. This gives us a chance to bind to the newly 122 | // queued objects callback. 123 | function wrapPulseQueryQueue (orig) { 124 | return function wrappedFunction () { 125 | if (this.queryQueue) { 126 | var query = this.queryQueue[this.queryQueue.length - 1] 127 | if (query && typeof query.callback === 'function' && query.callback.name !== 'opbeatCallbackWrapper') { 128 | query.callback = agent._instrumentation.bindFunction(query.callback) 129 | } 130 | } else { 131 | debug('ERROR: Internal structure of pg Client object have changed!') 132 | } 133 | return orig.apply(this, arguments) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/instrumentation/modules/mongodb-core.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var test = require('tape') 11 | var Server = require('mongodb-core').Server 12 | 13 | // { transactions: 14 | // [ { transaction: 'foo', 15 | // result: undefined, 16 | // kind: 'bar', 17 | // timestamp: '2016-07-15T13:39:00.000Z', 18 | // durations: [ 3.287613 ] } ], 19 | // traces: 20 | // { groups: 21 | // [ { transaction: 'foo', 22 | // signature: 'system.$cmd.ismaster', 23 | // kind: 'db.mongodb.query', 24 | // timestamp: '2016-07-15T13:39:00.000Z', 25 | // parents: [ 'transaction' ], 26 | // extra: { _frames: [Object] } }, 27 | // { transaction: 'foo', 28 | // signature: 'transaction', 29 | // kind: 'transaction', 30 | // timestamp: '2016-07-15T13:39:00.000Z', 31 | // parents: [], 32 | // extra: { _frames: [Object] } } ], 33 | // raw: [ [ 3.287613, [ 0, 0.60478, 1.661918 ], [ 1, 0, 3.287613 ] ] ] } } 34 | test('trace simple command', function (t) { 35 | resetAgent(function (endpoint, headers, data, cb) { 36 | var groups = [ 37 | 'system.$cmd.ismaster', 38 | // 'opbeat.$cmd.command', // only appears in mongodb-core 1.x 39 | 'opbeat.test.insert', 40 | 'opbeat.test.update', 41 | 'opbeat.test.remove', 42 | 'opbeat.test.find' 43 | ] 44 | 45 | t.equal(data.transactions.length, 1) 46 | t.equal(data.transactions[0].transaction, 'foo') 47 | t.equal(data.transactions[0].kind, 'bar') 48 | t.equal(data.transactions[0].result, 200) 49 | 50 | t.equal(data.traces.groups.length, groups.length + 1) 51 | 52 | groups.forEach(function (signature, i) { 53 | t.equal(data.traces.groups[i].kind, 'db.mongodb.query') 54 | t.deepEqual(data.traces.groups[i].parents, ['transaction']) 55 | t.equal(data.traces.groups[i].signature, signature) 56 | t.equal(data.traces.groups[i].transaction, 'foo') 57 | }) 58 | 59 | t.equal(data.traces.groups[groups.length].kind, 'transaction') 60 | t.deepEqual(data.traces.groups[groups.length].parents, []) 61 | t.equal(data.traces.groups[groups.length].signature, 'transaction') 62 | t.equal(data.traces.groups[groups.length].transaction, 'foo') 63 | 64 | var totalTraces = data.traces.raw[0].length - 2 65 | var totalTime = data.traces.raw[0][0] 66 | 67 | t.equal(data.traces.raw.length, 1) 68 | t.equal(totalTraces, groups.length + 2) // +1 for an extra ismaster command, +1 for the root trace 69 | 70 | for (var i = 1; i < totalTraces + 1; i++) { 71 | t.equal(data.traces.raw[0][i].length, 3) 72 | t.ok(data.traces.raw[0][i][0] >= 0, 'group index should be >= 0') 73 | t.ok(data.traces.raw[0][i][0] < data.traces.groups.length, 'group index should be within allowed range') 74 | t.ok(data.traces.raw[0][i][1] >= 0) 75 | t.ok(data.traces.raw[0][i][2] <= totalTime) 76 | } 77 | 78 | t.equal(data.traces.raw[0][totalTraces][1], 0, 'root trace should start at 0') 79 | t.equal(data.traces.raw[0][totalTraces][2], data.traces.raw[0][0], 'root trace should last to total time') 80 | 81 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 82 | 83 | t.end() 84 | }) 85 | 86 | var server = new Server({}) 87 | 88 | agent.startTransaction('foo', 'bar') 89 | 90 | // test example lifted from https://github.com/christkv/mongodb-core/blob/2.0/README.md#connecting-to-mongodb 91 | server.on('connect', function (_server) { 92 | _server.command('system.$cmd', {ismaster: true}, function (err, results) { 93 | t.error(err) 94 | t.equal(results.result.ismaster, true) 95 | 96 | _server.insert('opbeat.test', [{a: 1}, {a: 2}], {writeConcern: {w: 1}, ordered: true}, function (err, results) { 97 | t.error(err) 98 | t.equal(results.result.n, 2) 99 | 100 | _server.update('opbeat.test', [{q: {a: 1}, u: {'$set': {b: 1}}}], {writeConcern: {w: 1}, ordered: true}, function (err, results) { 101 | t.error(err) 102 | t.equal(results.result.n, 1) 103 | 104 | _server.remove('opbeat.test', [{q: {a: 1}, limit: 1}], {writeConcern: {w: 1}, ordered: true}, function (err, results) { 105 | t.error(err) 106 | t.equal(results.result.n, 1) 107 | 108 | var cursor = _server.cursor('opbeat.test', {find: 'opbeat.test', query: {a: 2}}) 109 | 110 | cursor.next(function (err, doc) { 111 | t.error(err) 112 | t.equal(doc.a, 2) 113 | 114 | _server.command('system.$cmd', {ismaster: true}, function (err, result) { 115 | t.error(err) 116 | agent.endTransaction() 117 | _server.destroy() 118 | agent._instrumentation._queue._flush() 119 | }) 120 | }) 121 | }) 122 | }) 123 | }) 124 | }) 125 | }) 126 | 127 | server.connect() 128 | }) 129 | 130 | function resetAgent (cb) { 131 | agent._instrumentation._queue._clear() 132 | agent._instrumentation.currentTransaction = null 133 | agent._httpClient = { request: cb || function () {} } 134 | agent.captureError = function (err) { throw err } 135 | } 136 | -------------------------------------------------------------------------------- /test/instrumentation/modules/http/sse.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../_agent')() 4 | 5 | var test = require('tape') 6 | var http = require('http') 7 | 8 | test('normal response', function (t) { 9 | resetAgent(function (endpoint, headers, data, cb) { 10 | assertNonSSEResponse(t, data) 11 | t.end() 12 | }) 13 | 14 | var server = http.createServer(function (req, res) { 15 | var trace = agent.buildTrace() 16 | if (trace) trace.start('foo', 'bar') 17 | setTimeout(function () { 18 | if (trace) trace.end() 19 | res.end() 20 | }, 10) 21 | }) 22 | 23 | request(server) 24 | }) 25 | 26 | test('SSE response with explicit headers', function (t) { 27 | resetAgent(function (endpoint, headers, data, cb) { 28 | assertSSEResponse(t, data) 29 | t.end() 30 | }) 31 | 32 | var server = http.createServer(function (req, res) { 33 | res.writeHead(200, {'Content-Type': 'text/event-stream'}) 34 | var trace = agent.buildTrace() 35 | if (trace) trace.start('foo', 'bar') 36 | setTimeout(function () { 37 | if (trace) trace.end() 38 | res.end() 39 | }, 10) 40 | }) 41 | 42 | request(server) 43 | }) 44 | 45 | test('SSE response with implicit headers', function (t) { 46 | resetAgent(function (endpoint, headers, data, cb) { 47 | assertSSEResponse(t, data) 48 | t.end() 49 | }) 50 | 51 | var server = http.createServer(function (req, res) { 52 | res.setHeader('Content-type', 'text/event-stream; foo') 53 | res.write('data: hello world\n\n') 54 | var trace = agent.buildTrace() 55 | if (trace) trace.start('foo', 'bar') 56 | setTimeout(function () { 57 | if (trace) trace.end() 58 | res.end() 59 | }, 10) 60 | }) 61 | 62 | request(server) 63 | }) 64 | 65 | function assertNonSSEResponse (t, data) { 66 | // data.traces.groups: 67 | t.equal(data.traces.groups.length, 2) 68 | 69 | t.equal(data.traces.groups[0].transaction, 'GET unknown route') 70 | t.equal(data.traces.groups[0].signature, 'foo') 71 | t.equal(data.traces.groups[0].kind, 'bar') 72 | t.deepEqual(data.traces.groups[0].parents, ['transaction']) 73 | 74 | t.equal(data.traces.groups[1].transaction, 'GET unknown route') 75 | t.equal(data.traces.groups[1].signature, 'transaction') 76 | t.equal(data.traces.groups[1].kind, 'transaction') 77 | t.deepEqual(data.traces.groups[1].parents, []) 78 | 79 | // data.transactions: 80 | t.equal(data.transactions.length, 1) 81 | t.equal(data.transactions[0].transaction, 'GET unknown route') 82 | t.equal(data.transactions[0].durations.length, 1) 83 | t.ok(data.transactions[0].durations[0] > 0) 84 | 85 | // data.traces.raw: 86 | // 87 | // [ 88 | // [ 89 | // 15.240414, // total transaction time 90 | // [ 0, 0.428756, 11.062134 ], // foo trace 91 | // [ 1, 0, 15.240414 ] // root trace 92 | // ] 93 | // ] 94 | t.equal(data.traces.raw.length, 1) 95 | t.equal(data.traces.raw[0].length, 4) 96 | t.equal(data.traces.raw[0][0], data.transactions[0].durations[0]) 97 | t.equal(data.traces.raw[0][1].length, 3) 98 | t.equal(data.traces.raw[0][2].length, 3) 99 | 100 | t.equal(data.traces.raw[0][1][0], 0) 101 | t.ok(data.traces.raw[0][1][1] > 0) 102 | t.ok(data.traces.raw[0][1][2] > 0) 103 | t.ok(data.traces.raw[0][1][1] < data.traces.raw[0][0]) 104 | t.ok(data.traces.raw[0][1][2] < data.traces.raw[0][0]) 105 | 106 | t.equal(data.traces.raw[0][2][0], 1) 107 | t.equal(data.traces.raw[0][2][1], 0) 108 | t.equal(data.traces.raw[0][2][2], data.traces.raw[0][0]) 109 | 110 | t.equal(data.traces.raw[0][3].http.method, 'GET') 111 | } 112 | 113 | function assertSSEResponse (t, data) { 114 | // data.traces.groups: 115 | t.equal(data.traces.groups.length, 1) 116 | 117 | t.equal(data.traces.groups[0].transaction, 'GET unknown route') 118 | t.equal(data.traces.groups[0].signature, 'transaction') 119 | t.equal(data.traces.groups[0].kind, 'transaction') 120 | t.deepEqual(data.traces.groups[0].parents, []) 121 | 122 | // data.transactions: 123 | t.equal(data.transactions.length, 1) 124 | t.equal(data.transactions[0].transaction, 'GET unknown route') 125 | t.equal(data.transactions[0].durations.length, 1) 126 | t.ok(data.transactions[0].durations[0] > 0) 127 | 128 | // data.traces.raw: 129 | // 130 | // [ 131 | // [ 132 | // 15.240414, // total transaction time 133 | // [ 0, 0, 15.240414 ] // root trace 134 | // ] 135 | // ] 136 | t.equal(data.traces.raw.length, 1) 137 | t.equal(data.traces.raw[0].length, 3) 138 | t.equal(data.traces.raw[0][0], data.transactions[0].durations[0]) 139 | t.equal(data.traces.raw[0][1].length, 3) 140 | 141 | t.equal(data.traces.raw[0][1][0], 0) 142 | t.equal(data.traces.raw[0][1][1], 0) 143 | t.equal(data.traces.raw[0][1][2], data.traces.raw[0][0]) 144 | 145 | t.equal(data.traces.raw[0][2].http.method, 'GET') 146 | } 147 | 148 | function request (server) { 149 | server.listen(function () { 150 | var port = server.address().port 151 | http.request({ port: port }, function (res) { 152 | res.on('end', function () { 153 | agent._instrumentation._queue._flush() 154 | server.close() 155 | }) 156 | res.resume() 157 | }).end() 158 | }) 159 | } 160 | 161 | function resetAgent (cb) { 162 | agent._httpClient = { request: cb } 163 | agent._instrumentation._queue._clear() 164 | agent._instrumentation.currentTransaction = null 165 | } 166 | -------------------------------------------------------------------------------- /test/instrumentation/modules/graphql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var semver = require('semver') 11 | if (semver.lt(process.version, '1.0.0')) process.exit() 12 | 13 | var test = require('tape') 14 | var graphql = require('graphql') 15 | var pkg = require('graphql/package.json') 16 | 17 | test('graphql.graphql', function (t) { 18 | resetAgent(done(t)) 19 | 20 | var schema = graphql.buildSchema('type Query { hello: String }') 21 | var root = {hello: function () { 22 | return 'Hello world!' 23 | }} 24 | var query = '{ hello }' 25 | 26 | agent.startTransaction('foo') 27 | 28 | graphql.graphql(schema, query, root).then(function (response) { 29 | agent.endTransaction() 30 | t.deepEqual(response, {data: {hello: 'Hello world!'}}) 31 | agent._instrumentation._queue._flush() 32 | }) 33 | }) 34 | 35 | test('graphql.execute', function (t) { 36 | resetAgent(done(t)) 37 | 38 | var schema = graphql.buildSchema('type Query { hello: String }') 39 | var root = {hello: function () { 40 | return Promise.resolve('Hello world!') 41 | }} 42 | var query = '{ hello }' 43 | var source = new graphql.Source(query) 44 | var documentAST = graphql.parse(source) 45 | 46 | agent.startTransaction('foo') 47 | 48 | graphql.execute(schema, documentAST, root).then(function (response) { 49 | agent.endTransaction() 50 | t.deepEqual(response, {data: {hello: 'Hello world!'}}) 51 | agent._instrumentation._queue._flush() 52 | }) 53 | }) 54 | 55 | if (semver.satisfies(pkg.version, '>=0.12')) { 56 | test('graphql.execute sync', function (t) { 57 | resetAgent(done(t)) 58 | 59 | var schema = graphql.buildSchema('type Query { hello: String }') 60 | var root = {hello: function () { 61 | return 'Hello world!' 62 | }} 63 | var query = '{ hello }' 64 | var source = new graphql.Source(query) 65 | var documentAST = graphql.parse(source) 66 | 67 | agent.startTransaction('foo') 68 | 69 | var response = graphql.execute(schema, documentAST, root) 70 | 71 | agent.endTransaction() 72 | t.deepEqual(response, {data: {hello: 'Hello world!'}}) 73 | agent._instrumentation._queue._flush() 74 | }) 75 | } 76 | 77 | // { transactions: 78 | // [ { transaction: 'foo', 79 | // result: undefined, 80 | // kind: 'custom', 81 | // timestamp: '2017-01-30T16:15:00.000Z', 82 | // durations: [ 6.560766 ] } ], 83 | // traces: 84 | // { groups: 85 | // [ { transaction: 'foo', 86 | // signature: 'GraphQL: hello', 87 | // kind: 'db.graphql.execute', 88 | // transaction_kind: 'custom', 89 | // timestamp: '2017-01-30T16:15:00.000Z', 90 | // parents: [ 'transaction' ], 91 | // extra: { _frames: [Object] } }, 92 | // { transaction: 'foo', 93 | // signature: 'transaction', 94 | // kind: 'transaction', 95 | // transaction_kind: 'custom', 96 | // timestamp: '2017-01-30T16:15:00.000Z', 97 | // parents: [], 98 | // extra: { _frames: [Object] } } ], 99 | // raw: 100 | // [ [ 6.560766, 101 | // [ 0, 1.392375, 3.968823 ], 102 | // [ 1, 0, 6.560766 ], 103 | // { extra: [Object], user: {} } ] ] } } 104 | function done (t) { 105 | return function (endpoint, headers, data, cb) { 106 | t.equal(data.transactions.length, 1) 107 | t.equal(data.transactions[0].transaction, 'foo') 108 | t.equal(data.transactions[0].kind, 'custom') 109 | 110 | t.equal(data.traces.groups.length, 2) 111 | 112 | t.equal(data.traces.groups[0].kind, 'db.graphql.execute') 113 | t.equal(data.traces.groups[0].transaction_kind, 'custom') 114 | t.deepEqual(data.traces.groups[0].parents, ['transaction']) 115 | t.equal(data.traces.groups[0].signature, 'GraphQL: hello') 116 | t.equal(data.traces.groups[0].transaction, 'foo') 117 | 118 | t.equal(data.traces.groups[1].kind, 'transaction') 119 | t.equal(data.traces.groups[1].transaction_kind, 'custom') 120 | t.deepEqual(data.traces.groups[1].parents, []) 121 | t.equal(data.traces.groups[1].signature, 'transaction') 122 | t.equal(data.traces.groups[1].transaction, 'foo') 123 | 124 | var totalTraces = data.traces.raw[0].length - 2 125 | var totalTime = data.traces.raw[0][0] 126 | 127 | t.equal(data.traces.raw.length, 1) 128 | t.equal(totalTraces, 2) 129 | 130 | for (var i = 1; i < totalTraces + 1; i++) { 131 | t.equal(data.traces.raw[0][i].length, 3) 132 | t.ok(data.traces.raw[0][i][0] >= 0, 'group index should be >= 0') 133 | t.ok(data.traces.raw[0][i][0] < data.traces.groups.length, 'group index should be within allowed range') 134 | t.ok(data.traces.raw[0][i][1] >= 0) 135 | t.ok(data.traces.raw[0][i][2] <= totalTime) 136 | } 137 | 138 | t.equal(data.traces.raw[0][totalTraces][1], 0, 'root trace should start at 0') 139 | t.equal(data.traces.raw[0][totalTraces][2], data.traces.raw[0][0], 'root trace should last to total time') 140 | 141 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 142 | 143 | t.end() 144 | } 145 | } 146 | 147 | function resetAgent (cb) { 148 | agent._instrumentation._queue._clear() 149 | agent._instrumentation.currentTransaction = null 150 | agent._httpClient = { request: cb || function () {} } 151 | agent.captureError = function (err) { throw err } 152 | } 153 | -------------------------------------------------------------------------------- /test/sourcemaps/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var test = require('tape') 5 | 6 | var agent = require('../../').start({ 7 | organizationId: 'test', 8 | appId: 'test', 9 | secretToken: 'test', 10 | captureExceptions: false, 11 | logLevel: 'fatal' 12 | }) 13 | 14 | test('source map inlined', function (t) { 15 | onError(t, assertSourceFound) 16 | agent.captureError(require('./fixtures/lib/error-inline')()) 17 | }) 18 | 19 | test('source map linked', function (t) { 20 | t.test('source mapped source code embedded', function (t) { 21 | onError(t, assertSourceFound) 22 | agent.captureError(require('./fixtures/lib/error-src-embedded')()) 23 | }) 24 | 25 | t.test('source mapped source code on disk', function (t) { 26 | onError(t, assertSourceFound) 27 | agent.captureError(require('./fixtures/lib/error')()) 28 | }) 29 | 30 | t.test('source mapped source code not found', function (t) { 31 | onError(t, assertSourceNotFound) 32 | agent.captureError(require('./fixtures/lib/error-src-missing')()) 33 | }) 34 | }) 35 | 36 | test('fails', function (t) { 37 | t.test('inlined source map broken', function (t) { 38 | onError(t, function (t, data) { 39 | t.equal(data.message, 'Error: foo') 40 | t.deepEqual(data.exception, {type: 'Error', value: 'foo'}) 41 | t.equal(data.culprit, 'generateError (test/sourcemaps/fixtures/lib/error-inline-broken.js)') 42 | 43 | var frame = data.stacktrace.frames.reverse()[0] 44 | t.equal(frame.filename, 'test/sourcemaps/fixtures/lib/error-inline-broken.js') 45 | t.equal(frame.lineno, 6) 46 | t.equal(frame.function, 'generateError') 47 | t.equal(frame.in_app, __dirname.indexOf('node_modules') === -1) 48 | t.equal(frame.abs_path, path.join(__dirname, 'fixtures', 'lib', 'error-inline-broken.js')) 49 | t.equal(frame.context_line, ' return new Error(msg);') 50 | }) 51 | agent.captureError(require('./fixtures/lib/error-inline-broken')()) 52 | }) 53 | 54 | t.test('linked source map not found', function (t) { 55 | onError(t, function (t, data) { 56 | t.equal(data.message, 'Error: foo') 57 | t.deepEqual(data.exception, {type: 'Error', value: 'foo'}) 58 | t.equal(data.culprit, 'generateError (test/sourcemaps/fixtures/lib/error-map-missing.js)') 59 | 60 | var frame = data.stacktrace.frames.reverse()[0] 61 | t.equal(frame.filename, 'test/sourcemaps/fixtures/lib/error-map-missing.js') 62 | t.equal(frame.lineno, 6) 63 | t.equal(frame.function, 'generateError') 64 | t.equal(frame.in_app, __dirname.indexOf('node_modules') === -1) 65 | t.equal(frame.abs_path, path.join(__dirname, 'fixtures', 'lib', 'error-map-missing.js')) 66 | t.equal(frame.context_line, ' return new Error(msg);') 67 | }) 68 | agent.captureError(require('./fixtures/lib/error-map-missing')()) 69 | }) 70 | 71 | t.test('linked source map broken', function (t) { 72 | onError(t, function (t, data) { 73 | t.equal(data.message, 'Error: foo') 74 | t.deepEqual(data.exception, {type: 'Error', value: 'foo'}) 75 | t.equal(data.culprit, 'generateError (test/sourcemaps/fixtures/lib/error-broken.js)') 76 | 77 | var frame = data.stacktrace.frames.reverse()[0] 78 | t.equal(frame.filename, 'test/sourcemaps/fixtures/lib/error-broken.js') 79 | t.equal(frame.lineno, 6) 80 | t.equal(frame.function, 'generateError') 81 | t.equal(frame.in_app, __dirname.indexOf('node_modules') === -1) 82 | t.equal(frame.abs_path, path.join(__dirname, 'fixtures', 'lib', 'error-broken.js')) 83 | t.equal(frame.context_line, ' return new Error(msg);') 84 | }) 85 | agent.captureError(require('./fixtures/lib/error-broken')()) 86 | }) 87 | }) 88 | 89 | function onError (t, assert) { 90 | agent._httpClient = {request: function (endpoint, headers, data, cb) { 91 | assert(t, data) 92 | t.end() 93 | }} 94 | } 95 | 96 | function assertSourceFound (t, data) { 97 | t.equal(data.message, 'Error: foo') 98 | t.deepEqual(data.exception, {type: 'Error', value: 'foo'}) 99 | t.equal(data.culprit, 'generateError (test/sourcemaps/fixtures/src/error.js)') 100 | 101 | var frame = data.stacktrace.frames.reverse()[0] 102 | t.equal(frame.filename, 'test/sourcemaps/fixtures/src/error.js') 103 | t.equal(frame.lineno, 2) 104 | t.equal(frame.function, 'generateError') 105 | t.equal(frame.in_app, __dirname.indexOf('node_modules') === -1) 106 | t.equal(frame.abs_path, path.join(__dirname, 'fixtures', 'src', 'error.js')) 107 | t.deepEqual(frame.pre_context, ['// Just a little prefixing line']) 108 | t.equal(frame.context_line, 'const generateError = (msg = \'foo\') => new Error(msg)') 109 | t.deepEqual(frame.post_context, ['', 'module.exports = generateError', '']) 110 | } 111 | 112 | function assertSourceNotFound (t, data) { 113 | t.equal(data.message, 'Error: foo') 114 | t.deepEqual(data.exception, {type: 'Error', value: 'foo'}) 115 | t.equal(data.culprit, 'generateError (test/sourcemaps/fixtures/src/not/found.js)') 116 | 117 | var frame = data.stacktrace.frames.reverse()[0] 118 | t.equal(frame.filename, 'test/sourcemaps/fixtures/src/not/found.js') 119 | t.equal(frame.lineno, 2) 120 | t.equal(frame.function, 'generateError') 121 | t.equal(frame.in_app, __dirname.indexOf('node_modules') === -1) 122 | t.equal(frame.abs_path, path.join(__dirname, 'fixtures', 'src', 'not', 'found.js')) 123 | t.equal(frame.pre_context, undefined) 124 | t.equal(frame.context_line, undefined) 125 | t.equal(frame.post_context, undefined) 126 | } 127 | -------------------------------------------------------------------------------- /test/instrumentation/trace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var mockAgent = require('./_agent') 5 | var mockInstrumentation = require('./_instrumentation') 6 | var Transaction = require('../../lib/instrumentation/transaction') 7 | var Trace = require('../../lib/instrumentation/trace') 8 | 9 | var agent = mockAgent() 10 | 11 | test('properties', function (t) { 12 | var trans = new Transaction(agent) 13 | var trace = new Trace(trans) 14 | trace.start('sig', 'type') 15 | t.equal(trace.transaction, trans) 16 | t.equal(trace.name, 'sig') 17 | t.equal(trace.type, 'type') 18 | t.equal(trace.ended, false) 19 | t.end() 20 | }) 21 | 22 | test('#end()', function (t) { 23 | var trans = new Transaction(agent) 24 | var trace = new Trace(trans) 25 | trace.start('sig', 'type') 26 | t.equal(trace.ended, false) 27 | t.equal(trans.traces.indexOf(trace), -1) 28 | trace.end() 29 | t.equal(trace.ended, true) 30 | t.equal(trans.traces.indexOf(trace), 0) 31 | t.end() 32 | }) 33 | 34 | test('#duration()', function (t) { 35 | var trans = new Transaction(agent) 36 | var trace = new Trace(trans) 37 | trace.start() 38 | setTimeout(function () { 39 | trace.end() 40 | t.ok(trace.duration() > 49, trace.duration() + ' should be larger than 49') 41 | t.end() 42 | }, 50) 43 | }) 44 | 45 | test('#duration() - return null if not ended', function (t) { 46 | var trans = new Transaction(agent) 47 | var trace = new Trace(trans) 48 | trace.start() 49 | t.equal(trace.duration(), null) 50 | t.end() 51 | }) 52 | 53 | test('#startTime() - return null if trace isn\'t started', function (t) { 54 | var trans = new Transaction(agent) 55 | var trace = new Trace(trans) 56 | t.equal(trace.startTime(), null) 57 | t.end() 58 | }) 59 | 60 | test('#startTime() - not return null if trace is started', function (t) { 61 | var trans = new Transaction(agent) 62 | var trace = new Trace(trans) 63 | trace.start() 64 | t.ok(trace.startTime() > 0) 65 | t.ok(trace.startTime() < 100) 66 | t.end() 67 | }) 68 | 69 | test('#startTime() - root trace', function (t) { 70 | var trans = new Transaction(mockInstrumentation(function () { 71 | t.equal(trans.traces[0].startTime(), 0) 72 | t.end() 73 | })._agent) 74 | trans.end() 75 | }) 76 | 77 | test('#startTime() - sub trace', function (t) { 78 | var trans = new Transaction(mockInstrumentation(function () { 79 | t.ok(trace.startTime() > 49, trace.startTime() + ' should be larger than 49') 80 | t.end() 81 | })._agent) 82 | var trace 83 | setTimeout(function () { 84 | trace = new Trace(trans) 85 | trace.start() 86 | trace.end() 87 | trans.end() 88 | }, 50) 89 | }) 90 | 91 | test('#ancestors() - root trace', function (t) { 92 | var trans = new Transaction(mockInstrumentation(function () { 93 | t.deepEqual(trans.traces[0].ancestors(), []) 94 | t.end() 95 | })._agent) 96 | trans.end() 97 | }) 98 | 99 | test('#ancestors() - sub trace, start/end on same tick', function (t) { 100 | var trans = new Transaction(mockInstrumentation(function () { 101 | t.deepEqual(trace.ancestors(), ['transaction']) 102 | t.end() 103 | })._agent) 104 | var trace = new Trace(trans) 105 | trace.start() 106 | trace.end() 107 | trans.end() 108 | }) 109 | 110 | test('#ancestors() - sub trace, end on next tick', function (t) { 111 | var trans = new Transaction(mockInstrumentation(function () { 112 | t.deepEqual(trace.ancestors(), ['transaction']) 113 | t.end() 114 | })._agent) 115 | var trace = new Trace(trans) 116 | trace.start() 117 | process.nextTick(function () { 118 | trace.end() 119 | trans.end() 120 | }) 121 | }) 122 | 123 | test('#ancestors() - sub sub trace', function (t) { 124 | var trans = new Transaction(mockInstrumentation(function () { 125 | t.deepEqual(t2.ancestors(), ['transaction']) 126 | t.end() 127 | })._agent) 128 | var t1 = new Trace(trans) 129 | t1.start('sig1') 130 | var t2 131 | process.nextTick(function () { 132 | t2 = new Trace(trans) 133 | t2.start('sig2') 134 | t2.end() 135 | t1.end() 136 | trans.end() 137 | }) 138 | }) 139 | 140 | test('#ancestors() - parallel sub traces, start/end on same tick', function (t) { 141 | var trans = new Transaction(mockInstrumentation(function () { 142 | t.deepEqual(t1.ancestors(), ['transaction']) 143 | t.deepEqual(t2.ancestors(), ['transaction']) 144 | t.end() 145 | })._agent) 146 | var t1 = new Trace(trans) 147 | t1.start('sig1a') 148 | var t2 = new Trace(trans) 149 | t2.start('sig1b') 150 | t2.end() 151 | t1.end() 152 | trans.end() 153 | }) 154 | 155 | test('#ancestors() - parallel sub traces, end on same tick', function (t) { 156 | var trans = new Transaction(mockInstrumentation(function () { 157 | t.deepEqual(t1.ancestors(), ['transaction']) 158 | t.deepEqual(t2.ancestors(), ['transaction']) 159 | t.end() 160 | })._agent) 161 | var t1 = new Trace(trans) 162 | t1.start('sig1a') 163 | var t2 = new Trace(trans) 164 | t2.start('sig1b') 165 | process.nextTick(function () { 166 | t2.end() 167 | t1.end() 168 | trans.end() 169 | }) 170 | }) 171 | 172 | test('#ancestors() - parallel sub traces, end on different ticks', function (t) { 173 | var trans = new Transaction(mockInstrumentation(function () { 174 | t.deepEqual(t1.ancestors(), ['transaction']) 175 | t.deepEqual(t2.ancestors(), ['transaction']) 176 | t.end() 177 | })._agent) 178 | var t1 = new Trace(trans) 179 | t1.start('sig1a') 180 | var t2 = new Trace(trans) 181 | t2.start('sig1b') 182 | process.nextTick(function () { 183 | t2.end() 184 | }) 185 | setTimeout(function () { 186 | t1.end() 187 | trans.end() 188 | }, 25) 189 | }) 190 | -------------------------------------------------------------------------------- /test/instrumentation/modules/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var test = require('tape') 11 | var redis = require('redis') 12 | 13 | // { transactions: 14 | // [ { transaction: 'foo', 15 | // result: 200, 16 | // kind: 'bar', 17 | // timestamp: '2016-07-28T17:57:00.000Z', 18 | // durations: [ 20.077148 ] } ], 19 | // traces: 20 | // { groups: 21 | // [ { transaction: 'foo', 22 | // signature: 'FLUSHALL', 23 | // kind: 'cache.redis', 24 | // timestamp: '2016-07-28T17:57:00.000Z', 25 | // parents: [ 'transaction' ], 26 | // extra: { _frames: [Object] } }, 27 | // { transaction: 'foo', 28 | // signature: 'SET', 29 | // kind: 'cache.redis', 30 | // timestamp: '2016-07-28T17:57:00.000Z', 31 | // parents: [ 'transaction' ], 32 | // extra: { _frames: [Object] } }, 33 | // { transaction: 'foo', 34 | // signature: 'HSET', 35 | // kind: 'cache.redis', 36 | // timestamp: '2016-07-28T17:57:00.000Z', 37 | // parents: [ 'transaction' ], 38 | // extra: { _frames: [Object] } }, 39 | // { transaction: 'foo', 40 | // signature: 'HKEYS', 41 | // kind: 'cache.redis', 42 | // timestamp: '2016-07-28T17:57:00.000Z', 43 | // parents: [ 'transaction' ], 44 | // extra: { _frames: [Object] } }, 45 | // { transaction: 'foo', 46 | // signature: 'transaction', 47 | // kind: 'transaction', 48 | // timestamp: '2016-07-28T17:57:00.000Z', 49 | // parents: [], 50 | // extra: { _frames: [Object] } } ], 51 | // raw: 52 | // [ [ 20.077148, 53 | // [ 0, 0.591359, 11.905252 ], 54 | // [ 1, 13.884884, 3.74471 ], 55 | // [ 2, 14.508829, 3.65903 ], 56 | // [ 2, 15.342503, 3.294208 ], 57 | // [ 3, 16.915787, 2.167231 ], 58 | // [ 4, 0, 20.077148 ] ] ] } } 59 | test(function (t) { 60 | resetAgent(function (endpoint, headers, data, cb) { 61 | var groups = [ 62 | 'FLUSHALL', 63 | 'SET', 64 | 'HSET', 65 | 'HKEYS' 66 | ] 67 | 68 | t.equal(data.transactions.length, 1) 69 | t.equal(data.transactions[0].transaction, 'foo') 70 | t.equal(data.transactions[0].kind, 'bar') 71 | t.equal(data.transactions[0].result, 200) 72 | 73 | t.equal(data.traces.groups.length, groups.length + 1) 74 | 75 | groups.forEach(function (signature, i) { 76 | t.equal(data.traces.groups[i].kind, 'cache.redis') 77 | t.deepEqual(data.traces.groups[i].parents, ['transaction']) 78 | t.equal(data.traces.groups[i].signature, signature) 79 | t.equal(data.traces.groups[i].transaction, 'foo') 80 | }) 81 | 82 | t.equal(data.traces.groups[groups.length].kind, 'transaction') 83 | t.deepEqual(data.traces.groups[groups.length].parents, []) 84 | t.equal(data.traces.groups[groups.length].signature, 'transaction') 85 | t.equal(data.traces.groups[groups.length].transaction, 'foo') 86 | 87 | var totalTraces = data.traces.raw[0].length - 2 88 | var totalTime = data.traces.raw[0][0] 89 | 90 | t.equal(data.traces.raw.length, 1) 91 | t.equal(totalTraces, groups.length + 3) // +1 for an extra hset command, +1 for the root trace, +1 for the callback-less SET 92 | 93 | for (var i = 1; i < totalTraces + 1; i++) { 94 | t.equal(data.traces.raw[0][i].length, 3) 95 | t.ok(data.traces.raw[0][i][0] >= 0, 'group index should be >= 0') 96 | t.ok(data.traces.raw[0][i][0] < data.traces.groups.length, 'group index should be within allowed range') 97 | t.ok(data.traces.raw[0][i][1] >= 0) 98 | t.ok(data.traces.raw[0][i][2] <= totalTime) 99 | } 100 | 101 | t.equal(data.traces.raw[0][totalTraces][1], 0, 'root trace should start at 0') 102 | t.equal(data.traces.raw[0][totalTraces][2], data.traces.raw[0][0], 'root trace should last to total time') 103 | 104 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 105 | 106 | t.end() 107 | }) 108 | 109 | var client = redis.createClient() 110 | 111 | agent.startTransaction('foo', 'bar') 112 | 113 | client.flushall(function (err, reply) { 114 | t.error(err) 115 | t.equal(reply, 'OK') 116 | var done = 0 117 | 118 | client.set('string key', 'string val', function (err, reply) { 119 | t.error(err) 120 | t.equal(reply, 'OK') 121 | done++ 122 | }) 123 | 124 | // callback is optional 125 | client.set('string key', 'string val') 126 | 127 | client.hset('hash key', 'hashtest 1', 'some value', function (err, reply) { 128 | t.error(err) 129 | t.equal(reply, 1) 130 | done++ 131 | }) 132 | client.hset(['hash key', 'hashtest 2', 'some other value'], function (err, reply) { 133 | t.error(err) 134 | t.equal(reply, 1) 135 | done++ 136 | }) 137 | 138 | client.hkeys('hash key', function (err, replies) { 139 | t.error(err) 140 | t.equal(replies.length, 2) 141 | replies.forEach(function (reply, i) { 142 | t.equal(reply, 'hashtest ' + (i + 1)) 143 | }) 144 | t.equal(done, 3) 145 | 146 | agent.endTransaction() 147 | client.quit() 148 | agent._instrumentation._queue._flush() 149 | }) 150 | }) 151 | }) 152 | 153 | function resetAgent (cb) { 154 | agent._instrumentation._queue._clear() 155 | agent._instrumentation.currentTransaction = null 156 | agent._httpClient = { request: cb || function () {} } 157 | agent.captureError = function (err) { throw err } 158 | } 159 | -------------------------------------------------------------------------------- /lib/instrumentation/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var uuid = require('uuid') 4 | var objectAssign = require('object-assign') 5 | var express = require('./express-utils') 6 | var debug = require('debug')('opbeat') 7 | var Trace = require('./trace') 8 | 9 | module.exports = Transaction 10 | 11 | function Transaction (agent, name, type) { 12 | Object.defineProperty(this, 'name', { 13 | configurable: true, 14 | enumerable: true, 15 | get: function () { 16 | // Fall back to a somewhat useful name in case no _defaultName is set. 17 | // This might happen if res.writeHead wasn't called. 18 | return this._customName || 19 | this._defaultName || 20 | (this.req ? this.req.method + ' unknown route (unnamed)' : 'unnamed') 21 | }, 22 | set: function (name) { 23 | if (this.ended) { 24 | debug('tried to set transaction.name on already ended transaction %o', {id: this.id}) 25 | return 26 | } 27 | debug('setting transaction name %o', {id: this.id, name: name}) 28 | this._customName = name 29 | } 30 | }) 31 | 32 | Object.defineProperty(this, 'result', { 33 | configurable: true, 34 | enumerable: true, 35 | get: function () { 36 | return this._result 37 | }, 38 | set: function (result) { 39 | if (this.ended) { 40 | debug('tried to set transaction.result on already ended transaction %o', {id: this.id}) 41 | return 42 | } 43 | debug('setting transaction result %o', {id: this.id, result: result}) 44 | this._result = result 45 | } 46 | }) 47 | 48 | this.id = uuid.v4() 49 | this._defaultName = name || '' 50 | this._customName = '' 51 | this._context = null 52 | this.type = type || 'custom' 53 | this.result = 200 54 | this.traces = [] 55 | this._builtTraces = [] 56 | this.ended = false 57 | this._abortTime = 0 58 | this._agent = agent 59 | this._agent._instrumentation.currentTransaction = this 60 | 61 | debug('start transaction %o', {id: this.id, name: name, type: type}) 62 | 63 | // A transaction should always have a root trace spanning the entire 64 | // transaction. 65 | this._rootTrace = new Trace(this) 66 | this._rootTrace.start('transaction', 'transaction') 67 | this._start = this._rootTrace._start 68 | } 69 | 70 | Transaction.prototype.setUserContext = function (context) { 71 | if (!context) return 72 | if (!this._context) this._context = {} 73 | this._context.user = objectAssign(this._context.user || {}, context) 74 | } 75 | 76 | Transaction.prototype.setExtraContext = function (context) { 77 | if (!context) return 78 | if (!this._context) this._context = {} 79 | this._context.extra = objectAssign(this._context.extra || {}, context) 80 | } 81 | 82 | Transaction.prototype.buildTrace = function () { 83 | if (this.ended) { 84 | debug('transaction already ended - cannot build new trace %o', {id: this.id}) 85 | return null 86 | } 87 | 88 | var trace = new Trace(this) 89 | this._builtTraces.push(trace) 90 | return trace 91 | } 92 | 93 | Transaction.prototype.duration = function () { 94 | return this._rootTrace.duration() 95 | } 96 | 97 | Transaction.prototype.setDefaultName = function (name) { 98 | debug('setting default transaction name: %s %o', name, {id: this.id}) 99 | this._defaultName = name 100 | } 101 | 102 | Transaction.prototype.setDefaultNameFromRequest = function () { 103 | var req = this.req 104 | var path = express.getPathFromRequest(req) 105 | 106 | if (!path) { 107 | debug('could not extract route name from request %o', { 108 | url: req.url, 109 | type: typeof path, 110 | null: path === null, // because typeof null === 'object' 111 | route: !!req.route, 112 | regex: req.route ? !!req.route.regexp : false, 113 | mountstack: req._opbeat_mountstack ? req._opbeat_mountstack.length : false, 114 | id: this.id 115 | }) 116 | path = 'unknown route' 117 | } 118 | 119 | this.setDefaultName(req.method + ' ' + path) 120 | } 121 | 122 | Transaction.prototype.end = function () { 123 | if (this.ended) { 124 | debug('tried to call transaction.end() on already ended transaction %o', {id: this.id}) 125 | return 126 | } 127 | 128 | if (!this._defaultName && this.req) this.setDefaultNameFromRequest() 129 | 130 | this._builtTraces.forEach(function (trace) { 131 | if (trace.ended || !trace.started) return 132 | trace.truncate() 133 | }) 134 | 135 | this._rootTrace.end() 136 | this.ended = true 137 | 138 | var trans = this._agent._instrumentation.currentTransaction 139 | 140 | // These two edge-cases should normally not happen, but if the hooks into 141 | // Node.js doesn't work as intended it might. In that case we want to 142 | // gracefully handle it. That involves ignoring all traces under the given 143 | // transaction as they will most likely be incomplete. We still want to send 144 | // the transaction without any traces as it's still valuable data. 145 | if (!trans) { 146 | debug('WARNING: no currentTransaction found %o', {current: trans, traces: this.traces.length, id: this.id}) 147 | this.traces = [] 148 | } else if (trans !== this) { 149 | debug('WARNING: transaction is out of sync %o', {traces: this.traces.length, id: this.id, other: trans.id}) 150 | this.traces = [] 151 | } 152 | 153 | this._agent._instrumentation.addEndedTransaction(this) 154 | debug('ended transaction %o', {id: this.id, type: this.type, result: this.result, name: this.name}) 155 | } 156 | 157 | Transaction.prototype._recordEndedTrace = function (trace) { 158 | if (this.ended) { 159 | debug('Can\'t record ended trace after parent transaction have ended - ignoring %o', {id: this.id, trace: trace.name}) 160 | return 161 | } 162 | 163 | this.traces.push(trace) 164 | } 165 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # We don't need sudo as such, but setting sudo: required allows us to run on 2 | # the faster machines which means that the builds complete much faster (in 3 | # about half the time). For comparison, here are two builds on the two 4 | # different machines: 5 | # - With `sudo: false`: https://travis-ci.org/opbeat/opbeat-node/builds/252301998 6 | # - With `sudo: required`: https://travis-ci.org/opbeat/opbeat-node/builds/252302576 7 | sudo: required 8 | 9 | language: node_js 10 | 11 | services: 12 | - mysql 13 | - mongodb 14 | - redis-server 15 | - postgresql 16 | 17 | node_js: 18 | - '9' 19 | - '8' 20 | - '7.9.0' # currently 7.10.0 fails. For details see: https://github.com/nodejs/node/pull/12861 21 | - '6' 22 | - '5' 23 | - '4' 24 | - '0.12' 25 | - '0.10' 26 | 27 | jobs: 28 | fast_finish: true 29 | 30 | include: 31 | 32 | # Node.js 9 33 | - stage: dependencies 34 | node_js: '9' 35 | env: TAV=generic-pool,mysql,redis,koa-router 36 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 37 | - 38 | node_js: '9' 39 | env: TAV=ioredis,pg 40 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 41 | - 42 | node_js: '9' 43 | env: TAV=bluebird 44 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 45 | - 46 | node_js: '9' 47 | env: TAV=knex,ws,graphql,express-graphql,hapi 48 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 49 | 50 | # Node.js 8 51 | - 52 | node_js: '8' 53 | env: TAV=generic-pool,mysql,redis,koa-router 54 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 55 | - 56 | node_js: '8' 57 | env: TAV=ioredis,pg 58 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 59 | - 60 | node_js: '8' 61 | env: TAV=bluebird 62 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 63 | - 64 | node_js: '8' 65 | env: TAV=knex,ws,graphql,express-graphql,hapi 66 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 67 | 68 | # Node.js 7 69 | - 70 | node_js: '7' 71 | env: TAV=generic-pool,mysql,redis,koa-router 72 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 73 | - 74 | node_js: '7' 75 | env: TAV=ioredis,pg 76 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 77 | - 78 | node_js: '7' 79 | env: TAV=bluebird 80 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 81 | - 82 | node_js: '7' 83 | env: TAV=knex,ws,graphql,express-graphql,hapi 84 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 85 | 86 | # Node.js 6 87 | - 88 | node_js: '6' 89 | env: TAV=generic-pool,mysql,redis,koa-router 90 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 91 | - 92 | node_js: '6' 93 | env: TAV=ioredis,pg 94 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 95 | - 96 | node_js: '6' 97 | env: TAV=bluebird 98 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 99 | - 100 | node_js: '6' 101 | env: TAV=knex,ws,graphql,express-graphql,hapi 102 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 103 | 104 | # Node.js 5 105 | - 106 | node_js: '5' 107 | env: TAV=generic-pool,mysql,redis,koa-router 108 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 109 | - 110 | node_js: '5' 111 | env: TAV=ioredis,pg 112 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 113 | - 114 | node_js: '5' 115 | env: TAV=bluebird 116 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 117 | - 118 | node_js: '5' 119 | env: TAV=knex,ws,graphql,express-graphql,hapi 120 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 121 | 122 | # Node.js 4 123 | - 124 | node_js: '4' 125 | env: TAV=generic-pool,mysql,redis,koa-router 126 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 127 | - 128 | node_js: '4' 129 | env: TAV=ioredis,pg 130 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 131 | - 132 | node_js: '4' 133 | env: TAV=bluebird 134 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 135 | - 136 | node_js: '4' 137 | env: TAV=knex,ws,graphql,express-graphql,hapi 138 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 139 | 140 | # Node.js 0.12 141 | - 142 | node_js: '0.12' 143 | env: TAV=generic-pool,mysql,redis,koa-router 144 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 145 | - 146 | node_js: '0.12' 147 | env: TAV=ioredis,pg 148 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 149 | - 150 | node_js: '0.12' 151 | env: TAV=bluebird 152 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 153 | - 154 | node_js: '0.12' 155 | env: TAV=knex,ws,graphql,express-graphql,hapi 156 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 157 | 158 | # Node.js 0.10 159 | - 160 | node_js: '0.10' 161 | env: TAV=generic-pool,mysql,redis,koa-router 162 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 163 | - 164 | node_js: '0.10' 165 | env: TAV=ioredis,pg 166 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 167 | - 168 | node_js: '0.10' 169 | env: TAV=bluebird 170 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 171 | - 172 | node_js: '0.10' 173 | env: TAV=knex,ws,graphql,express-graphql,hapi 174 | script: 'if ! [[ $TRAVIS_BRANCH == greenkeeper/* ]]; then tav --quiet; fi' 175 | 176 | notifications: 177 | email: 178 | - watson@elastic.co 179 | slack: 180 | secure: Jq9ST6TYsZZtPgUdn60rZCfcclNF1cXaCqemt9ZKvqlDie9kbyJjU9t0K+EFdlQXgzM5sGAC+okRO9c29zMDuWvsuY6wb5K2p9j1cxfOn1FTc4xcxh/fKelu1Q7nGaMOIPvQuoI/TQBo4pwACyjli+ohz7DMVMRcans6GR+P0S8= 181 | on_success: change 182 | on_failure: change 183 | on_pull_requests: false 184 | -------------------------------------------------------------------------------- /test/instrumentation/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var mockInstrumentation = require('./_instrumentation') 5 | var Transaction = require('../../lib/instrumentation/transaction') 6 | var Trace = require('../../lib/instrumentation/trace') 7 | 8 | test('init', function (t) { 9 | var ins = mockInstrumentation(function (added) { 10 | t.ok(false) 11 | }) 12 | var trans = new Transaction(ins._agent, 'name', 'type') 13 | t.equal(trans.name, 'name') 14 | t.equal(trans.type, 'type') 15 | t.equal(trans.result, 200) 16 | t.equal(trans.ended, false) 17 | t.deepEqual(trans.traces, []) 18 | t.end() 19 | }) 20 | 21 | test('#setUserContext', function (t) { 22 | var ins = mockInstrumentation(function (added) { 23 | t.equal(added.ended, true) 24 | t.equal(added, trans) 25 | t.equal(trans.traces.length, 1) 26 | t.deepEqual(trans.traces, [trans._rootTrace]) 27 | t.end() 28 | }) 29 | var trans = new Transaction(ins._agent) 30 | t.equal(trans._context, null) 31 | trans.setUserContext() 32 | t.equal(trans._context, null) 33 | trans.setUserContext({foo: 1}) 34 | t.deepEqual(trans._context, {user: {foo: 1}}) 35 | trans.setUserContext({bar: {baz: 2}}) 36 | t.deepEqual(trans._context, {user: {foo: 1, bar: {baz: 2}}}) 37 | trans.setUserContext({foo: 3}) 38 | t.deepEqual(trans._context, {user: {foo: 3, bar: {baz: 2}}}) 39 | trans.setUserContext({bar: {shallow: true}}) 40 | t.deepEqual(trans._context, {user: {foo: 3, bar: {shallow: true}}}) 41 | t.end() 42 | }) 43 | 44 | test('#setExtraContext', function (t) { 45 | var ins = mockInstrumentation(function (added) { 46 | t.equal(added.ended, true) 47 | t.equal(added, trans) 48 | t.equal(trans.traces.length, 1) 49 | t.deepEqual(trans.traces, [trans._rootTrace]) 50 | t.end() 51 | }) 52 | var trans = new Transaction(ins._agent) 53 | t.equal(trans._context, null) 54 | trans.setExtraContext() 55 | t.equal(trans._context, null) 56 | trans.setExtraContext({foo: 1}) 57 | t.deepEqual(trans._context, {extra: {foo: 1}}) 58 | trans.setExtraContext({bar: {baz: 2}}) 59 | t.deepEqual(trans._context, {extra: {foo: 1, bar: {baz: 2}}}) 60 | trans.setExtraContext({foo: 3}) 61 | t.deepEqual(trans._context, {extra: {foo: 3, bar: {baz: 2}}}) 62 | trans.setExtraContext({bar: {shallow: true}}) 63 | t.deepEqual(trans._context, {extra: {foo: 3, bar: {shallow: true}}}) 64 | t.end() 65 | }) 66 | 67 | test('#setUserContext + #setExtraContext', function (t) { 68 | var ins = mockInstrumentation(function (added) { 69 | t.equal(added.ended, true) 70 | t.equal(added, trans) 71 | t.equal(trans.traces.length, 1) 72 | t.deepEqual(trans.traces, [trans._rootTrace]) 73 | t.end() 74 | }) 75 | var trans = new Transaction(ins._agent) 76 | trans.setUserContext({foo: 1}) 77 | trans.setExtraContext({bar: 1}) 78 | t.deepEqual(trans._context, {user: {foo: 1}, extra: {bar: 1}}) 79 | t.end() 80 | }) 81 | 82 | test('#end() - no traces', function (t) { 83 | var ins = mockInstrumentation(function (added) { 84 | t.equal(added.ended, true) 85 | t.equal(added, trans) 86 | t.equal(trans.traces.length, 1) 87 | t.deepEqual(trans.traces, [trans._rootTrace]) 88 | t.end() 89 | }) 90 | var trans = new Transaction(ins._agent) 91 | trans.end() 92 | }) 93 | 94 | test('#end() - with traces', function (t) { 95 | var ins = mockInstrumentation(function (added) { 96 | t.equal(added.ended, true) 97 | t.equal(added, trans) 98 | t.equal(trans.traces.length, 2) 99 | t.deepEqual(trans.traces, [trace, trans._rootTrace]) 100 | t.end() 101 | }) 102 | var trans = new Transaction(ins._agent) 103 | var trace = new Trace(trans) 104 | trace.start() 105 | trace.end() 106 | trans.end() 107 | }) 108 | 109 | test('#duration()', function (t) { 110 | var ins = mockInstrumentation(function (added) { 111 | t.ok(added.duration() > 40) 112 | t.ok(added.duration() < 60) 113 | t.end() 114 | }) 115 | var trans = new Transaction(ins._agent) 116 | setTimeout(function () { 117 | trans.end() 118 | }, 50) 119 | }) 120 | 121 | test('#duration() - un-ended transaction', function (t) { 122 | var ins = mockInstrumentation(function (added) { 123 | t.ok(false) 124 | }) 125 | var trans = new Transaction(ins._agent) 126 | t.equal(trans.duration(), null) 127 | t.end() 128 | }) 129 | 130 | test('#setDefaultName() - with initial value', function (t) { 131 | var ins = mockInstrumentation(function (added) { 132 | t.ok(false) 133 | }) 134 | var trans = new Transaction(ins._agent, 'default-1') 135 | t.equal(trans.name, 'default-1') 136 | trans.setDefaultName('default-2') 137 | t.equal(trans.name, 'default-2') 138 | t.end() 139 | }) 140 | 141 | test('#setDefaultName() - no initial value', function (t) { 142 | var ins = mockInstrumentation(function (added) { 143 | t.ok(false) 144 | }) 145 | var trans = new Transaction(ins._agent) 146 | t.equal(trans.name, 'unnamed') 147 | trans.setDefaultName('default') 148 | t.equal(trans.name, 'default') 149 | t.end() 150 | }) 151 | 152 | test('name - custom first, then default', function (t) { 153 | var ins = mockInstrumentation(function (added) { 154 | t.ok(false) 155 | }) 156 | var trans = new Transaction(ins._agent) 157 | trans.name = 'custom' 158 | trans.setDefaultName('default') 159 | t.equal(trans.name, 'custom') 160 | t.end() 161 | }) 162 | 163 | test('name - default first, then custom', function (t) { 164 | var ins = mockInstrumentation(function (added) { 165 | t.ok(false) 166 | }) 167 | var trans = new Transaction(ins._agent) 168 | trans.setDefaultName('default') 169 | trans.name = 'custom' 170 | t.equal(trans.name, 'custom') 171 | t.end() 172 | }) 173 | 174 | test('parallel transactions', function (t) { 175 | var calls = 0 176 | var ins = mockInstrumentation(function (added) { 177 | t.equal(added._rootTrace.name, 'transaction') 178 | t.equal(added.traces.length, 1, added.name + ' should have 1 trace') 179 | t.equal(added.traces[0], added._rootTrace) 180 | 181 | calls++ 182 | if (calls === 1) { 183 | t.equal(added.name, 'second') 184 | } else if (calls === 2) { 185 | t.equal(added.name, 'first') 186 | t.end() 187 | } 188 | }) 189 | ins.currentTransaction = null 190 | 191 | setImmediate(function () { 192 | var t1 = new Transaction(ins._agent, 'first') 193 | setTimeout(function () { 194 | t1.end() 195 | }, 100) 196 | }) 197 | 198 | setTimeout(function () { 199 | var t2 = new Transaction(ins._agent, 'second') 200 | setTimeout(function () { 201 | t2.end() 202 | }, 25) 203 | }, 25) 204 | }) 205 | -------------------------------------------------------------------------------- /lib/instrumentation/modules/graphql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var semver = require('semver') 4 | var express = require('../express-utils') 5 | var debug = require('debug')('opbeat') 6 | 7 | module.exports = function (graphql, agent, version) { 8 | if (!semver.satisfies(version, '>=0.7.0 <1.0.0') || 9 | !graphql.Kind || 10 | typeof graphql.Source !== 'function' || 11 | typeof graphql.parse !== 'function' || 12 | typeof graphql.validate !== 'function' || 13 | typeof graphql.execute !== 'function') { 14 | debug('graphql version %s not supported - aborting...', version) 15 | return graphql 16 | } 17 | 18 | var wrapped = {} 19 | 20 | Object.defineProperty(wrapped, '__esModule', { 21 | value: true 22 | }) 23 | 24 | Object.keys(graphql).forEach(function (key) { 25 | var getter = graphql.__lookupGetter__(key) 26 | var setter = graphql.__lookupSetter__(key) 27 | var opts = {enumerable: true} 28 | 29 | if (getter) { 30 | switch (key) { 31 | case 'graphql': 32 | opts.get = function get () { 33 | return wrapGraphql(getter()) 34 | } 35 | break 36 | case 'execute': 37 | opts.get = function get () { 38 | return wrapExecute(getter()) 39 | } 40 | break 41 | default: 42 | opts.get = getter 43 | } 44 | } 45 | 46 | if (setter) { 47 | opts.set = setter 48 | } 49 | 50 | Object.defineProperty(wrapped, key, opts) 51 | }) 52 | 53 | return wrapped 54 | 55 | function wrapGraphql (orig) { 56 | return function wrappedGraphql (schema, requestString, rootValue, contextValue, variableValues, operationName) { 57 | var trans = agent._instrumentation.currentTransaction 58 | var trace = agent.buildTrace() 59 | var id = trace && trace.transaction.id 60 | var traceName = 'GraphQL: Unkown Query' 61 | debug('intercepted call to graphql.graphql %o', {id: id}) 62 | 63 | // As of now, the only reason why there might be a transaction but no 64 | // trace is if the transaction have ended. But just to be sure this 65 | // doesn't break in the future we add the extra `!trace` guard as well 66 | if (!trans || trans.ended || !trace) { 67 | debug('no active transaction found - skipping graphql tracing') 68 | return orig.apply(this, arguments) 69 | } 70 | 71 | var source = new graphql.Source(requestString || '', 'GraphQL request') 72 | if (source) { 73 | var documentAST = graphql.parse(source) 74 | if (documentAST) { 75 | var validationErrors = graphql.validate(schema, documentAST) 76 | if (validationErrors && validationErrors.length === 0) { 77 | var queries = extractDetails(documentAST, operationName).queries 78 | if (queries.length > 0) traceName = 'GraphQL: ' + queries.join(', ') 79 | } 80 | } else { 81 | debug('graphql.parse(source) failed - skipping graphql query extraction') 82 | } 83 | } else { 84 | debug('graphql.Source(query) failed - skipping graphql query extraction') 85 | } 86 | 87 | trace.start(traceName, 'db.graphql.execute') 88 | var p = orig.apply(this, arguments) 89 | p.then(function () { 90 | trace.end() 91 | }) 92 | return p 93 | } 94 | } 95 | 96 | function wrapExecute (orig) { 97 | return function wrappedExecute (schema, document, rootValue, contextValue, variableValues, operationName) { 98 | var trans = agent._instrumentation.currentTransaction 99 | var trace = agent.buildTrace() 100 | var id = trace && trace.transaction.id 101 | var traceName = 'GraphQL: Unkown Query' 102 | debug('intercepted call to graphql.execute %o', {id: id}) 103 | 104 | // As of now, the only reason why there might be a transaction but no 105 | // trace is if the transaction have ended. But just to be sure this 106 | // doesn't break in the future we add the extra `!trace` guard as well 107 | if (!trans || trans.ended || !trace) { 108 | debug('no active transaction found - skipping graphql tracing') 109 | return orig.apply(this, arguments) 110 | } 111 | 112 | var details = extractDetails(document, operationName) 113 | var queries = details.queries 114 | operationName = operationName || (details.operation && details.operation.name && details.operation.name.value) 115 | if (queries.length > 0) traceName = 'GraphQL: ' + (operationName ? operationName + ' ' : '') + queries.join(', ') 116 | 117 | if (trans._graphqlRoute) { 118 | var name = queries.length > 0 ? queries.join(', ') : 'Unknown GraphQL query' 119 | if (trans.req) var path = express.getPathFromRequest(trans.req) 120 | var defaultName = name 121 | defaultName = path ? defaultName + ' (' + path + ')' : defaultName 122 | defaultName = operationName ? operationName + ' ' + defaultName : defaultName 123 | trans.setDefaultName(defaultName) 124 | } 125 | 126 | trace.start(traceName, 'db.graphql.execute') 127 | var p = orig.apply(this, arguments) 128 | if (typeof p.then === 'function') { 129 | p.then(function () { 130 | trace.end() 131 | }) 132 | } else { 133 | trace.end() 134 | } 135 | return p 136 | } 137 | } 138 | 139 | function extractDetails (document, operationName) { 140 | var queries = [] 141 | var operation 142 | 143 | if (document && Array.isArray(document.definitions)) { 144 | document.definitions.some(function (definition) { 145 | if (!definition || definition.kind !== graphql.Kind.OPERATION_DEFINITION) return 146 | if (!operationName && operation) return 147 | if (!operationName || (definition.name && definition.name.value === operationName)) { 148 | operation = definition 149 | return true 150 | } 151 | }) 152 | 153 | var selections = operation && operation.selectionSet && operation.selectionSet.selections 154 | if (selections && Array.isArray(selections)) { 155 | selections.forEach(function (selection) { 156 | var kind = selection.name && selection.name.kind 157 | if (kind === graphql.Kind.NAME) { 158 | var queryName = selection.name.value 159 | if (queryName) queries.push(queryName) 160 | } 161 | }) 162 | 163 | queries = queries.sort(function (a, b) { 164 | if (a > b) return 1 165 | else if (a < b) return -1 166 | return 0 167 | }) 168 | } 169 | } else { 170 | debug('unexpected document format - skipping graphql query extraction') 171 | } 172 | 173 | return { queries: queries, operation: operation } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/instrumentation/modules/ioredis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var agent = require('../../..').start({ 4 | appId: 'test', 5 | organizationId: 'test', 6 | secretToken: 'test', 7 | captureExceptions: false 8 | }) 9 | 10 | var test = require('tape') 11 | var Redis = require('ioredis') 12 | 13 | test('not nested', function (t) { 14 | resetAgent(done(t)) 15 | 16 | var redis = new Redis() 17 | 18 | agent.startTransaction('foo', 'bar') 19 | 20 | var calls = 0 21 | 22 | redis.flushall(function (err, reply) { 23 | t.error(err) 24 | t.equal(reply, 'OK') 25 | calls++ 26 | }) 27 | 28 | redis.set('foo', 'bar') 29 | redis.get('foo', function (err, result) { 30 | t.error(err) 31 | t.equal(result, 'bar') 32 | calls++ 33 | }) 34 | 35 | redis.get('foo').then(function (result) { 36 | t.equal(result, 'bar') 37 | calls++ 38 | }) 39 | 40 | redis.sadd('set', 1, 3, 5, 7) 41 | redis.sadd('set', [1, 3, 5, 7]) 42 | 43 | redis.set('key', 100, 'EX', 10) 44 | 45 | redis.keys('*', function testing123 (err, replies) { 46 | t.error(err) 47 | t.deepEqual(replies.sort(), ['foo', 'key', 'set']) 48 | t.equal(calls, 3) 49 | 50 | agent.endTransaction() 51 | redis.quit() 52 | agent._instrumentation._queue._flush() 53 | }) 54 | }) 55 | 56 | test('nested', function (t) { 57 | resetAgent(done(t)) 58 | 59 | var redis = new Redis() 60 | 61 | agent.startTransaction('foo', 'bar') 62 | 63 | redis.flushall(function (err, reply) { 64 | t.error(err) 65 | t.equal(reply, 'OK') 66 | var calls = 0 67 | 68 | redis.set('foo', 'bar') 69 | redis.get('foo', function (err, result) { 70 | t.error(err) 71 | t.equal(result, 'bar') 72 | calls++ 73 | }) 74 | 75 | redis.get('foo').then(function (result) { 76 | t.equal(result, 'bar') 77 | calls++ 78 | }) 79 | 80 | redis.sadd('set', 1, 3, 5, 7) 81 | redis.sadd('set', [1, 3, 5, 7]) 82 | 83 | redis.set('key', 100, 'EX', 10) 84 | 85 | redis.keys('*', function testing123 (err, replies) { 86 | t.error(err) 87 | t.deepEqual(replies.sort(), ['foo', 'key', 'set']) 88 | t.equal(calls, 2) 89 | 90 | agent.endTransaction() 91 | redis.quit() 92 | agent._instrumentation._queue._flush() 93 | }) 94 | }) 95 | }) 96 | 97 | // { transactions: 98 | // [ { transaction: 'foo', 99 | // result: 200, 100 | // kind: 'bar', 101 | // timestamp: '2016-07-29T09:58:00.000Z', 102 | // durations: [ 31.891944 ] } ], 103 | // traces: 104 | // { groups: 105 | // [ { transaction: 'foo', 106 | // signature: 'FLUSHALL', 107 | // kind: 'cache.redis', 108 | // timestamp: '2016-07-29T09:58:00.000Z', 109 | // parents: [ 'transaction' ], 110 | // extra: { _frames: [Object] } }, 111 | // { transaction: 'foo', 112 | // signature: 'SET', 113 | // kind: 'cache.redis', 114 | // timestamp: '2016-07-29T09:58:00.000Z', 115 | // parents: [ 'transaction' ], 116 | // extra: { _frames: [Object] } }, 117 | // { transaction: 'foo', 118 | // signature: 'GET', 119 | // kind: 'cache.redis', 120 | // timestamp: '2016-07-29T09:58:00.000Z', 121 | // parents: [ 'transaction' ], 122 | // extra: { _frames: [Object] } }, 123 | // { transaction: 'foo', 124 | // signature: 'SADD', 125 | // kind: 'cache.redis', 126 | // timestamp: '2016-07-29T09:58:00.000Z', 127 | // parents: [ 'transaction' ], 128 | // extra: { _frames: [Object] } }, 129 | // { transaction: 'foo', 130 | // signature: 'KEYS', 131 | // kind: 'cache.redis', 132 | // timestamp: '2016-07-29T09:58:00.000Z', 133 | // parents: [ 'transaction' ], 134 | // extra: { _frames: [Object] } }, 135 | // { transaction: 'foo', 136 | // signature: 'transaction', 137 | // kind: 'transaction', 138 | // timestamp: '2016-07-29T09:58:00.000Z', 139 | // parents: [], 140 | // extra: { _frames: [Object] } } ], 141 | // raw: 142 | // [ [ 31.891944, 143 | // [ 0, 1.905168, 16.86911 ], 144 | // [ 1, 21.505805, 6.67724 ], 145 | // [ 2, 22.866657, 5.70388 ], 146 | // [ 2, 23.724921, 5.359812 ], 147 | // [ 3, 24.354847, 5.082269 ], 148 | // [ 3, 24.979798, 4.675697 ], 149 | // [ 1, 25.492804, 4.582025 ], 150 | // [ 4, 26.094776, 4.187654 ], 151 | // [ 5, 0, 31.891944 ] ] ] } } 152 | function done (t) { 153 | return function (endpoint, headers, data, cb) { 154 | var groups = [ 155 | 'FLUSHALL', 156 | 'SET', 157 | 'GET', 158 | 'SADD', 159 | 'KEYS' 160 | ] 161 | 162 | t.equal(data.transactions.length, 1) 163 | t.equal(data.transactions[0].transaction, 'foo') 164 | t.equal(data.transactions[0].kind, 'bar') 165 | t.equal(data.transactions[0].result, 200) 166 | 167 | t.equal(data.traces.groups.length, groups.length + 1) 168 | 169 | groups.forEach(function (signature, i) { 170 | t.equal(data.traces.groups[i].kind, 'cache.redis') 171 | t.deepEqual(data.traces.groups[i].parents, ['transaction']) 172 | t.equal(data.traces.groups[i].signature, signature) 173 | t.equal(data.traces.groups[i].transaction, 'foo') 174 | }) 175 | 176 | t.equal(data.traces.groups[groups.length].kind, 'transaction') 177 | t.deepEqual(data.traces.groups[groups.length].parents, []) 178 | t.equal(data.traces.groups[groups.length].signature, 'transaction') 179 | t.equal(data.traces.groups[groups.length].transaction, 'foo') 180 | 181 | var totalTraces = data.traces.raw[0].length - 2 182 | var totalTime = data.traces.raw[0][0] 183 | 184 | t.equal(data.traces.raw.length, 1) 185 | t.equal(totalTraces, groups.length + 4) // +3 for double set, get and sadd commands, +1 for the root trace 186 | 187 | for (var i = 1; i < totalTraces + 1; i++) { 188 | t.equal(data.traces.raw[0][i].length, 3) 189 | t.ok(data.traces.raw[0][i][0] >= 0, 'group index should be >= 0') 190 | t.ok(data.traces.raw[0][i][0] < data.traces.groups.length, 'group index should be within allowed range') 191 | t.ok(data.traces.raw[0][i][1] >= 0) 192 | t.ok(data.traces.raw[0][i][2] <= totalTime) 193 | } 194 | 195 | t.equal(data.traces.raw[0][totalTraces][1], 0, 'root trace should start at 0') 196 | t.equal(data.traces.raw[0][totalTraces][2], data.traces.raw[0][0], 'root trace should last to total time') 197 | 198 | t.deepEqual(data.transactions[0].durations, [data.traces.raw[0][0]]) 199 | 200 | t.end() 201 | } 202 | } 203 | 204 | function resetAgent (cb) { 205 | agent._instrumentation._queue._clear() 206 | agent._instrumentation.currentTransaction = null 207 | agent._httpClient = { request: cb || function () {} } 208 | agent.captureError = function (err) { throw err } 209 | } 210 | -------------------------------------------------------------------------------- /test/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var zlib = require('zlib') 4 | var test = require('tape') 5 | var nock = require('nock') 6 | var helpers = require('./_helpers') 7 | var Agent = require('../lib/agent') 8 | var request = require('../lib/request') 9 | 10 | var opts = { 11 | organizationId: 'some-org-id', 12 | appId: 'some-app-id', 13 | secretToken: 'secret', 14 | captureExceptions: false 15 | } 16 | 17 | var data = { extra: { uuid: 'foo' } } 18 | var body = JSON.stringify(data) 19 | 20 | test('#error()', function (t) { 21 | t.test('non-string exception.value', function (t) { 22 | global.__opbeat_initialized = null 23 | var agent = new Agent() 24 | agent.start(opts) 25 | agent._httpClient.request = function () { 26 | t.end() 27 | } 28 | request.error(agent, { exception: { value: 1 } }) 29 | }) 30 | 31 | t.test('non-string culprit', function (t) { 32 | global.__opbeat_initialized = null 33 | var agent = new Agent() 34 | agent.start(opts) 35 | agent._httpClient.request = function () { 36 | t.end() 37 | } 38 | request.error(agent, { culprit: 1 }) 39 | }) 40 | 41 | t.test('non-string message', function (t) { 42 | global.__opbeat_initialized = null 43 | var agent = new Agent() 44 | agent.start(opts) 45 | agent._httpClient.request = function () { 46 | t.end() 47 | } 48 | request.error(agent, { message: 1 }) 49 | }) 50 | 51 | t.test('without callback and successful request', function (t) { 52 | zlib.deflate(body, function (err, buffer) { 53 | t.error(err) 54 | global.__opbeat_initialized = null 55 | var agent = new Agent() 56 | agent.start(opts) 57 | var scope = nock('https://intake.opbeat.com') 58 | .filteringRequestBody(function (body) { 59 | t.equal(body, buffer.toString('hex')) 60 | return 'ok' 61 | }) 62 | .defaultReplyHeaders({'Location': 'foo'}) 63 | .post('/api/v1/organizations/some-org-id/apps/some-app-id/errors/', 'ok') 64 | .reply(200) 65 | agent.on('logged', function (url) { 66 | scope.done() 67 | t.equal(url, 'foo') 68 | t.end() 69 | }) 70 | request.error(agent, data) 71 | }) 72 | }) 73 | 74 | t.test('with callback and successful request', function (t) { 75 | zlib.deflate(body, function (err, buffer) { 76 | t.error(err) 77 | global.__opbeat_initialized = null 78 | var agent = new Agent() 79 | agent.start(opts) 80 | var scope = nock('https://intake.opbeat.com') 81 | .filteringRequestBody(function (body) { 82 | t.equal(body, buffer.toString('hex')) 83 | return 'ok' 84 | }) 85 | .defaultReplyHeaders({'Location': 'foo'}) 86 | .post('/api/v1/organizations/some-org-id/apps/some-app-id/errors/', 'ok') 87 | .reply(200) 88 | request.error(agent, data, function (err, url) { 89 | scope.done() 90 | t.error(err) 91 | t.equal(url, 'foo') 92 | t.end() 93 | }) 94 | }) 95 | }) 96 | 97 | t.test('without callback and bad request', function (t) { 98 | global.__opbeat_initialized = null 99 | var agent = new Agent() 100 | agent.start(opts) 101 | var scope = nock('https://intake.opbeat.com') 102 | .filteringRequestBody(function () { return '*' }) 103 | .post('/api/v1/organizations/some-org-id/apps/some-app-id/errors/', '*') 104 | .reply(500) 105 | agent.on('error', function (err) { 106 | helpers.restoreLogger() 107 | scope.done() 108 | t.equal(err.message, 'Opbeat error (500): ') 109 | t.end() 110 | }) 111 | helpers.mockLogger() 112 | request.error(agent, data) 113 | }) 114 | 115 | t.test('with callback and bad request', function (t) { 116 | var called = false 117 | global.__opbeat_initialized = null 118 | var agent = new Agent() 119 | agent.start(opts) 120 | var scope = nock('https://intake.opbeat.com') 121 | .filteringRequestBody(function () { return '*' }) 122 | .post('/api/v1/organizations/some-org-id/apps/some-app-id/errors/', '*') 123 | .reply(500) 124 | agent.on('error', function (err) { 125 | helpers.restoreLogger() 126 | scope.done() 127 | t.ok(called) 128 | t.equal(err.message, 'Opbeat error (500): ') 129 | t.end() 130 | }) 131 | helpers.mockLogger() 132 | request.error(agent, data, function (err) { 133 | called = true 134 | t.equal(err.message, 'Opbeat error (500): ') 135 | }) 136 | }) 137 | }) 138 | 139 | test('#transactions()', function (t) { 140 | t.test('non-string transactions.$.transaction', function (t) { 141 | global.__opbeat_initialized = null 142 | var agent = new Agent() 143 | agent.start(opts) 144 | agent._httpClient.request = function () { 145 | t.end() 146 | } 147 | request.transactions(agent, { 148 | transactions: [{ transaction: 1 }], 149 | traces: { raw: [], groups: [] } 150 | }) 151 | }) 152 | 153 | t.test('non-string traces.groups.$.transaction', function (t) { 154 | global.__opbeat_initialized = null 155 | var agent = new Agent() 156 | agent.start(opts) 157 | agent._httpClient.request = function () { 158 | t.end() 159 | } 160 | request.transactions(agent, { 161 | transactions: [{ transaction: 1 }], 162 | traces: { raw: [], groups: [{ transaction: 1 }] } 163 | }) 164 | }) 165 | 166 | t.test('non-string traces.groups.$.extra._frames.$.context_line', function (t) { 167 | global.__opbeat_initialized = null 168 | var agent = new Agent() 169 | agent.start(opts) 170 | agent._httpClient.request = function () { 171 | t.end() 172 | } 173 | request.transactions(agent, { 174 | transactions: [{ transaction: 'foo' }], 175 | traces: { 176 | raw: [], 177 | groups: [{transaction: 'foo', extra: {_frames: [{context_line: 1}]}}] 178 | } 179 | }) 180 | }) 181 | 182 | t.test('non-string traces.groups.$.extra._frames.$.pre_context.$', function (t) { 183 | global.__opbeat_initialized = null 184 | var agent = new Agent() 185 | agent.start(opts) 186 | agent._httpClient.request = function () { 187 | t.end() 188 | } 189 | request.transactions(agent, { 190 | transactions: [{ transaction: 'foo' }], 191 | traces: { 192 | raw: [], 193 | groups: [{transaction: 'foo', extra: {_frames: [{pre_context: [1]}]}}] 194 | } 195 | }) 196 | }) 197 | 198 | t.test('non-string traces.groups.$.extra._frames.$.pre_context.$', function (t) { 199 | global.__opbeat_initialized = null 200 | var agent = new Agent() 201 | agent.start(opts) 202 | agent._httpClient.request = function () { 203 | t.end() 204 | } 205 | request.transactions(agent, { 206 | transactions: [{ transaction: 'foo' }], 207 | traces: { 208 | raw: [], 209 | groups: [{transaction: 'foo', extra: {_frames: [{post_context: [1]}]}}] 210 | } 211 | }) 212 | }) 213 | }) 214 | -------------------------------------------------------------------------------- /test/instrumentation/modules/pg/knex.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.OPBEAT_TEST = true 4 | 5 | var agent = require('../../../..').start({ 6 | appId: 'test', 7 | organizationId: 'test', 8 | secretToken: 'test', 9 | captureExceptions: false 10 | }) 11 | 12 | var semver = require('semver') 13 | var pgVersion = require('pg/package').version 14 | var knexVersion = require('knex/package').version 15 | 16 | // pg@7+ doesn't support Node.js pre 4.5.0 17 | if (semver.lt(process.version, '4.5.0') && semver.gte(pgVersion, '7.0.0')) process.exit() 18 | 19 | var utils = require('./_utils') 20 | 21 | var test = require('tape') 22 | var Knex = require('knex') 23 | 24 | var transNo = 0 25 | var knex 26 | 27 | var selectTests = [ 28 | 'knex.select().from(\'test\')', 29 | 'knex.select(\'c1\', \'c2\').from(\'test\')', 30 | 'knex.column(\'c1\', \'c2\').select().from(\'test\')', 31 | 'knex(\'test\').select()' 32 | ] 33 | 34 | if (semver.gte(knexVersion, '0.11.0')) { 35 | selectTests.push('knex.select().from(\'test\').timeout(10000)') 36 | } 37 | 38 | var insertTests = [ 39 | 'knex(\'test\').insert({c1: \'test1\', c2: \'test2\'})' 40 | ] 41 | 42 | selectTests.forEach(function (source) { 43 | test(source, function (t) { 44 | resetAgent(function (endpoint, headers, data, cb) { 45 | assertBasicQuery(t, data) 46 | t.end() 47 | }) 48 | createClient(function userLandCode () { 49 | agent.startTransaction('foo' + ++transNo) 50 | 51 | var query = eval(source) // eslint-disable-line no-eval 52 | 53 | query.then(function (rows) { 54 | t.equal(rows.length, 5) 55 | rows.forEach(function (row, i) { 56 | t.equal(row.c1, 'foo' + (i + 1)) 57 | t.equal(row.c2, 'bar' + (i + 1)) 58 | }) 59 | agent.endTransaction() 60 | agent._instrumentation._queue._flush() 61 | }).catch(function (err) { 62 | t.error(err) 63 | }) 64 | }) 65 | }) 66 | }) 67 | 68 | insertTests.forEach(function (source) { 69 | test(source, function (t) { 70 | resetAgent(function (endpoint, headers, data, cb) { 71 | assertBasicQuery(t, data) 72 | t.end() 73 | }) 74 | createClient(function userLandCode () { 75 | agent.startTransaction('foo' + ++transNo) 76 | 77 | var query = eval(source) // eslint-disable-line no-eval 78 | 79 | query.then(function (result) { 80 | t.equal(result.command, 'INSERT') 81 | t.equal(result.rowCount, 1) 82 | agent.endTransaction() 83 | agent._instrumentation._queue._flush() 84 | }).catch(function (err) { 85 | t.error(err) 86 | }) 87 | }) 88 | }) 89 | }) 90 | 91 | test('knex.raw', function (t) { 92 | resetAgent(function (endpoint, headers, data, cb) { 93 | assertBasicQuery(t, data) 94 | t.end() 95 | }) 96 | createClient(function userLandCode () { 97 | agent.startTransaction('foo' + ++transNo) 98 | 99 | var query = knex.raw('SELECT * FROM "test"') 100 | 101 | query.then(function (result) { 102 | var rows = result.rows 103 | t.equal(rows.length, 5) 104 | rows.forEach(function (row, i) { 105 | t.equal(row.c1, 'foo' + (i + 1)) 106 | t.equal(row.c2, 'bar' + (i + 1)) 107 | }) 108 | agent.endTransaction() 109 | agent._instrumentation._queue._flush() 110 | }).catch(function (err) { 111 | t.error(err) 112 | }) 113 | }) 114 | }) 115 | 116 | function assertBasicQuery (t, data) { 117 | // remove the 'select versions();' query that knex injects - just makes 118 | // testing too hard 119 | var selectVersion = false 120 | data.traces.groups = data.traces.groups.filter(function (group) { 121 | if (group.extra.sql === 'select version();') { 122 | selectVersion = true 123 | return false 124 | } 125 | return true 126 | }) 127 | 128 | // data.traces.groups: 129 | t.equal(data.traces.groups.length, 2) 130 | 131 | t.equal(data.traces.groups[0].kind, 'db.postgresql.query') 132 | t.deepEqual(data.traces.groups[0].parents, ['transaction']) 133 | t.equal(data.traces.groups[0].transaction, 'foo' + transNo) 134 | t.ok(data.traces.groups[0].extra._frames.some(function (frame) { 135 | return frame.function === 'userLandCode' 136 | }), 'include user-land code frame') 137 | 138 | t.equal(data.traces.groups[1].kind, 'transaction') 139 | t.deepEqual(data.traces.groups[1].parents, []) 140 | t.equal(data.traces.groups[1].signature, 'transaction') 141 | t.equal(data.traces.groups[1].transaction, 'foo' + transNo) 142 | 143 | // data.transactions: 144 | t.equal(data.transactions.length, 1) 145 | t.equal(data.transactions[0].transaction, 'foo' + transNo) 146 | t.equal(data.transactions[0].durations.length, 1) 147 | t.ok(data.transactions[0].durations[0] > 0) 148 | 149 | // data.traces.raw: 150 | // 151 | // [ 152 | // [ 153 | // 59.695363, // total transaction time 154 | // [ 0, 31.647005, 18.31168 ], // sql trace (version) 155 | // [ 1, 48.408276, 4.157207 ], // sql trace (select) 156 | // [ 2, 0, 59.695363 ], // root trace 157 | // { extra: [Object] } // extra 158 | // ] 159 | // ] 160 | t.equal(data.traces.raw.length, 1) 161 | t.equal(data.traces.raw[0].length, selectVersion ? 5 : 4) 162 | t.equal(data.traces.raw[0][0], data.transactions[0].durations[0]) 163 | t.equal(data.traces.raw[0][1].length, 3) 164 | t.equal(data.traces.raw[0][2].length, 3) 165 | if (selectVersion) t.equal(data.traces.raw[0][3].length, 3) 166 | 167 | for (var rawNo = 1; rawNo <= (selectVersion ? 2 : 1); rawNo++) { 168 | t.equal(data.traces.raw[0][rawNo][0], rawNo - 1) 169 | t.ok(data.traces.raw[0][rawNo][1] > 0) 170 | t.ok(data.traces.raw[0][rawNo][2] > 0) 171 | t.ok(data.traces.raw[0][rawNo][1] < data.traces.raw[0][0]) 172 | t.ok(data.traces.raw[0][rawNo][2] < data.traces.raw[0][0]) 173 | } 174 | 175 | t.equal(data.traces.raw[0][rawNo][0], rawNo - 1) 176 | t.equal(data.traces.raw[0][rawNo][1], 0) 177 | t.equal(data.traces.raw[0][rawNo][2], data.traces.raw[0][0]) 178 | 179 | t.ok('extra' in data.traces.raw[0][rawNo + 1]) 180 | } 181 | 182 | function createClient (cb) { 183 | setup(function () { 184 | knex = Knex({ 185 | client: 'pg', 186 | connection: 'postgres://localhost/test_opbeat' 187 | }) 188 | cb() 189 | }) 190 | } 191 | 192 | function setup (cb) { 193 | // just in case it didn't happen at the end of the previous test 194 | teardown(function () { 195 | utils.reset(function () { 196 | utils.loadData(cb) 197 | }) 198 | }) 199 | } 200 | 201 | function teardown (cb) { 202 | if (knex) { 203 | knex.destroy(function (err) { 204 | if (err) throw err 205 | knex = undefined 206 | cb() 207 | }) 208 | } else { 209 | process.nextTick(cb) 210 | } 211 | } 212 | 213 | function resetAgent (cb) { 214 | agent._httpClient = { request: function () { 215 | var self = this 216 | var args = [].slice.call(arguments) 217 | teardown(function () { 218 | cb.apply(self, args) 219 | }) 220 | } } 221 | agent._instrumentation._queue._clear() 222 | agent._instrumentation.currentTransaction = null 223 | } 224 | --------------------------------------------------------------------------------