├── .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 |
--------------------------------------------------------------------------------