"use strict"; var assert = require('assert'); var util = require('util'); var vows = require('vows'); var sinon = require('sinon'); var t = require('../src/server/transforms'); process.env.NODE_ENV = process.env.NODE_ENV || 'test-node-env'; var rollbar = require('../src/server/rollbar'); var _ = require('../src/utility'); function CustomError(message, nested) { rollbar.Error.call(this, message, nested); } util.inherits(CustomError, rollbar.Error); async function wait(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); } async function throwInScriptFile(rollbar, filepath, callback) { setTimeout(function () { var error = require(filepath); error(); }, 10); await wait(500); callback(rollbar); } var nodeVersion = function () { var version = process.versions.node.split('.'); return [ parseInt(version[0]), parseInt(version[1]), parseInt(version[2]), ]; }(); var isMinNodeVersion = function(major, minor) { return ( nodeVersion[0] > major || (nodeVersion[0] === major && nodeVersion[1] >= minor) ); } vows.describe('transforms') .addBatch({ 'baseData': { 'options': { 'defaults': { topic: function() { return rollbar.defaultOptions; }, 'item': { 'empty': { topic: function(options) { var item = {}; t.baseData(item, options, this.callback); }, 'should have a timestamp': function(err, item) { assert.ifError(err); assert.notEqual(item.data, undefined); assert.notEqual(item.data.timestamp, undefined); }, 'should have an error level': function(err, item) { assert.ifError(err); assert.notEqual(item.data, undefined); assert.equal(item.data.level, 'error'); }, 'should have some defaults': function(err, item) { assert.ifError(err); var data = item.data; assert.equal(data.environment, process.env.NODE_ENV); assert.equal(data.framework, 'node-js'); assert.equal(data.language, 'javascript'); assert.ok(data.server); assert.ok(data.server.host); assert.ok(data.server.pid); } }, 'with values': { topic: function(options) { var item = { level: 'critical', framework: 'star-wars', uuid: '12345', environment: 'production', custom: { one: 'a1', stuff: 'b2', language: 'english' } }; t.baseData(item, options, this.callback); }, 'should have a critical level': function(err, item) { assert.ifError(err); assert.equal(item.data.level, 'critical'); }, 'should have the defaults overriden by the item': function(err, item) { assert.ifError(err); assert.equal(item.data.environment, 'production'); assert.equal(item.data.framework, 'star-wars'); assert.equal(item.data.language, 'javascript'); assert.equal(item.data.uuid, '12345'); }, 'should have data from custom': function(err, item) { assert.equal(item.data.one, 'a1'); assert.equal(item.data.stuff, 'b2'); assert.notEqual(item.data.language, 'english'); } } } }, 'with values': { topic: function() { return _.merge(rollbar.defaultOptions, { payload: { environment: 'payload-prod', }, framework: 'opt-node', host: 'opt-host', branch: 'opt-master' }); }, 'item': { 'empty': { topic: function(options) { var item = {}; t.baseData(item, options, this.callback); }, 'should have a timestamp': function(err, item) { assert.ifError(err); assert.notEqual(item.data, undefined); assert.notEqual(item.data.timestamp, undefined); }, 'should have an error level': function(err, item) { assert.ifError(err); assert.notEqual(item.data, undefined); assert.equal(item.data.level, 'error'); }, 'should have data from options and defaults': function(err, item) { assert.ifError(err); var data = item.data; assert.equal(data.environment, 'payload-prod'); assert.equal(data.framework, 'opt-node'); assert.equal(data.language, 'javascript'); assert.ok(data.server); assert.equal(data.server.host, 'opt-host'); assert.equal(data.server.branch, 'opt-master'); assert.ok(data.server.pid); } }, 'with values': { topic: function(options) { var item = { level: 'critical', environment: 'production', framework: 'star-wars', uuid: '12345', custom: { one: 'a1', stuff: 'b2', language: 'english' } }; t.baseData(item, options, this.callback); }, 'should have a critical level': function(err, item) { assert.ifError(err); assert.equal(item.data.level, 'critical'); }, 'should have the defaults overriden by the item': function(err, item) { assert.ifError(err); assert.equal(item.data.environment, 'production'); assert.equal(item.data.framework, 'star-wars'); assert.equal(item.data.language, 'javascript'); assert.equal(item.data.uuid, '12345'); }, 'should have data from custom': function(err, item) { assert.equal(item.data.one, 'a1'); assert.equal(item.data.stuff, 'b2'); assert.notEqual(item.data.language, 'english'); } } } } } } }) .addBatch({ 'addBody': { 'options': { 'anything': { topic: function() { return {whatever: 'stuff'}; }, 'item': { 'with stackInfo': { topic: function(options) { var item = {stackInfo: [{message: 'hey'}]}; t.addBody(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should set the trace_chain': function(err, item) { assert.ok(item.data.body.trace_chain); }, 'should not set a message': function(err, item) { assert.ok(!item.data.body.message); } }, 'with no stackInfo': { topic: function(options) { var item = {message: 'hello'}; t.addBody(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should not set the trace_chain': function(err, item) { assert.ok(!item.data.body.trace_chain); }, 'should set a message': function(err, item) { assert.ok(item.data.body.message); } } } } } } }) .addBatch({ 'addMessageData': { 'options': { 'anything': { topic: function() { return {random: 'stuff'}; }, 'item': { 'no message': { topic: function(options) { var item = {err: 'stuff', not: 'a message'}; t.addMessageData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should add an empty body': function(err, item) { assert.ok(item.data.body); } }, 'with a message': { topic: function(options) { var item = {message: 'this is awesome'}; t.addMessageData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should add a body with the message': function(err, item) { assert.equal(item.data.body.message.body, 'this is awesome'); } } } } } } }) .addBatch({ 'nodeSourceMaps': { 'with original source present': { topic: function() { var Rollbar = new rollbar({ accessToken: 'abc123', captureUncaught: true, nodeSourceMaps: true }); var queue = Rollbar.client.notifier.queue; Rollbar.addItemStub = sinon.stub(queue, 'addItem'); throwInScriptFile(Rollbar, '../examples/node-typescript/dist/index', this.callback); }, 'should map the stack with context': function(r) { var addItem = r.addItemStub; assert.isTrue(addItem.called); if (addItem.called) { var frame = addItem.getCall(0).args[0].body.trace_chain[0].frames.pop(); assert.ok(frame.filename.includes('src/index.ts')); assert.equal(frame.lineno, 10); assert.equal(frame.colno, 22); assert.equal(frame.code, " var error = new CustomError('foo');"); assert.equal(frame.context.pre[0], ' }'); assert.equal(frame.context.pre[1], ' }'); assert.equal(frame.context.pre[2], ' // TypeScript code snippet will include ``'); assert.equal(frame.context.post[0], ' throw error;'); assert.equal(frame.context.post[1], '}'); var sourceMappingURLs = addItem.getCall(0).args[0].notifier.diagnostic .node_source_maps.source_mapping_urls; var urls = Object.keys(sourceMappingURLs); assert.ok(urls[0].includes('index.js')); assert.ok(sourceMappingURLs[urls[0]].includes('index.js.map')); assert.ok(urls[1].includes('server.transforms.test.js')); assert.ok(sourceMappingURLs[urls[1]].includes('not found')); // Node until v12 will have 'timers.js' here. // Node 12 - 14 will have 'internal/timers.js' here. // Starting in v16, this is 'node:internal/timers'. // This assert works for all and is specific enough for this test case. assert.ok(urls[2].includes('timers')); assert.ok(sourceMappingURLs[urls[2]].includes('not found')); } addItem.reset(); } } } }) .addBatch({ 'nodeSourceMaps': { 'using sourcesContent': { topic: function() { var Rollbar = new rollbar({ accessToken: 'abc123', captureUncaught: true, nodeSourceMaps: true }); var queue = Rollbar.client.notifier.queue; Rollbar.addItemStub = sinon.stub(queue, 'addItem'); throwInScriptFile(Rollbar, '../examples/node-dist/index', this.callback); }, 'should map the stack with context': function(r) { var addItem = r.addItemStub; assert.isTrue(addItem.called); if (addItem.called) { var frame = addItem.getCall(0).args[0].body.trace_chain[0].frames.pop(); assert.ok(frame.filename.includes('src/index.ts')); assert.equal(frame.lineno, 10); assert.equal(frame.colno, 22); assert.equal(frame.code, " var error = new CustomError('foo');"); assert.equal(frame.context.pre[0], ' }'); assert.equal(frame.context.pre[1], ' }'); assert.equal(frame.context.pre[2], ' // TypeScript code snippet will include ``'); assert.equal(frame.context.post[0], ' throw error;'); assert.equal(frame.context.post[1], '}'); var sourceMappingURLs = addItem.getCall(0).args[0].notifier.diagnostic .node_source_maps.source_mapping_urls; var urls = Object.keys(sourceMappingURLs); assert.ok(urls.length === 1); assert.ok(urls[0].includes('index.js')); assert.ok(sourceMappingURLs[urls[0]].includes('index.js.map')); } addItem.reset(); } } } }) .addBatch({ 'handleItemWithError': { 'options': { 'anything': { topic: function() { return { some: 'stuff', captureIp: true, }; }, 'item': { 'no error': { topic: function(options) { var item = { data: {body: {yo: 'hey'}}, message: 'hey' }; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should not change the item': function(err, item) { assert.equal(item.data.body.yo, 'hey'); } }, 'with a simple error': { topic: function (options) { var item = { data: {body: {}}, err: new Error('wookie') }; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should add some data to the trace_chain': function(err, item) { assert.ok(item.stackInfo); } }, 'with a normal error': { topic: function (options) { var test = function() { var x = thisVariableIsNotDefined; }; var err; try { test(); } catch (e) { err = e; } var item = { data: {body: {}}, err: err }; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should add some data to the trace_chain': function(err, item) { assert.ok(item.stackInfo); } }, 'with a nested error': { topic: function (options) { var test = function() { var x = thisVariableIsNotDefined; }; var err; try { test(); } catch (e) { err = new CustomError('nested-message', e); } var item = { data: {body: {}}, err: err }; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have the right data in the trace_chain': function(err, item) { var trace_chain = item.stackInfo; assert.lengthOf(trace_chain, 2); assert.equal(trace_chain[0].exception.class, 'CustomError'); assert.equal(trace_chain[0].exception.message, 'nested-message'); assert.equal(trace_chain[1].exception.class, 'ReferenceError'); } }, 'with a null nested error': { topic: function (options) { var err = new CustomError('With null nested error'); // Set nested to null for the test err.nested = null; var item = { data: {body: {}}, err: err }; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have the right data in the trace_chain': function(err, item) { var trace_chain = item.stackInfo; assert.lengthOf(trace_chain, 1); assert.equal(trace_chain[0].exception.class, 'CustomError'); } }, 'with error context': { topic: function (options) { var test = function() { var x = thisVariableIsNotDefined; }; var err; try { test(); } catch (e) { err = new CustomError('nested-message', e); e.rollbarContext = { err1: 'nested context' }; err.rollbarContext = { err2: 'error context' }; } var item = { data: {body: {}}, err: err }; options.addErrorContext = true; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should add the error context': function(err, item) { var trace_chain = item.stackInfo; assert.lengthOf(trace_chain, 2); assert.equal(item.data.custom.err1, 'nested context'); assert.equal(item.data.custom.err2, 'error context'); } }, 'with an error cause': { topic: function (options) { var test = function() { var x = thisVariableIsNotDefined; }; var err; try { test(); } catch (e) { err = new Error('cause message', { cause: e }); e.rollbarContext = { err1: 'cause context' }; err.rollbarContext = { err2: 'error context' }; } var item = { data: {body: {}}, err: err }; t.handleItemWithError(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have the right data in the trace_chain': function(err, item) { // Error cause was introduced in Node 16.9. if (!isMinNodeVersion(16, 9)) return; var trace_chain = item.stackInfo; assert.lengthOf(trace_chain, 2); assert.equal(trace_chain[0].exception.class, 'Error'); assert.equal(trace_chain[0].exception.message, 'cause message'); assert.equal(trace_chain[1].exception.class, 'ReferenceError'); assert.equal(item.data.custom.err1, 'cause context'); assert.equal(item.data.custom.err2, 'error context'); } }, } } } } }) .addBatch({ 'addRequestData': { 'options': { 'without custom addRequestData method': { 'without scrub fields': { topic: function() { return { nothing: 'here', captureEmail: true, captureUsername: true, captureIp: true }; }, 'item': { 'without a request': { topic: function(options) { var item = { data: {body: {message: 'hey'}} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should not change the item': function(err, item) { assert.equal(item.request, undefined); assert.equal(item.data.request, undefined); } }, 'with an empty request object': { topic: function(options) { var item = { request: {}, data: {body: {message: 'hey'}} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should not change request object': function(err, item) { assert.equal(item.request.headers, undefined); } }, 'with a request': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.1', method: 'GET', body: { token: 'abc123', something: 'else' }, route: { path: '/api/:bork' }, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: {other: 'thing'} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should set a person based on request user': function(err, item) { assert.equal(item.data.person.id, 42); assert.equal(item.data.person.email, 'fake@example.com'); }, 'should set some fields based on request data': function(err, item) { var r = item.data.request; assert.equal(r.url, 'https://example.com/some/endpoint'); assert.equal(r.user_ip, '192.192.192.1'); assert.ok(r.GET); assert.equal(item.data.context, '/api/:bork'); }, }, 'with a request for a nested router with a baseURL': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', baseUrl: '/nested', ip: '192.192.192.1', method: 'GET', body: { token: 'abc123', something: 'else' }, route: { path: '/api/:bork' }, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: {other: 'thing'} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should set some fields based on request data': function(err, item) { var r = item.data.request; assert.equal(r.url, 'https://example.com/nested/some/endpoint'); assert.equal(item.data.context, '/nested/api/:bork'); }, }, 'with a request like from hapi': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: '', query: {}, pathname: '/some/endpoint', path: '/some/endpoint', href: '/some/endpoint' }, ip: '192.192.192.1', method: 'POST', payload: { token: 'abc123', something: 'else' }, route: { path: '/api/:bork' }, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: {other: 'thing'} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should set a person based on request user': function(err, item) { assert.equal(item.data.person.id, 42); assert.equal(item.data.person.email, 'fake@example.com'); }, 'should set some fields based on request data': function(err, item) { var r = item.data.request; assert.equal(r.url, 'https://example.com/some/endpoint'); assert.equal(r.user_ip, '192.192.192.1'); assert.ok(!r.GET); assert.ok(r.POST); assert.equal(item.data.context, '/api/:bork'); }, }, 'with a request with an array body': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.1', method: 'POST', body: [{ token: 'abc123', something: 'else' }, 'otherStuff'], user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: {other: 'thing'} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should set a person based on request user': function(err, item) { assert.equal(item.data.person.id, 42); assert.equal(item.data.person.email, 'fake@example.com'); }, 'should set some fields based on request data': function(err, item) { var r = item.data.request; assert.equal(r.url, 'https://example.com/some/endpoint'); assert.equal(r.user_ip, '192.192.192.1'); assert.ok(r.POST); assert.equal(r.POST['0'].something, 'else'); assert.equal(r.POST['1'], 'otherStuff'); }, } } }, 'with scrub fields': { topic: function() { return { scrubHeaders: [ 'x-auth-token' ], scrubFields: [ 'passwd', 'access_token', 'request.cookie' ] }; }, 'item': { 'with a request': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.192', method: 'GET', body: { token: 'abc123', something: 'else' }, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: {other: 'thing'} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, } } } }, 'with custom addRequestData': { 'with scrub fields': { topic: function() { var customFn = function(i, r) { assert.equal(i.stuff, undefined); assert.equal(i.other, 'thing'); i.myRequest = {body: r.body.token}; }; return { captureIp: true, addRequestData: customFn, scrubFields: [ 'passwd', 'access_token', 'token', 'request.cookie' ] }; }, 'item': { 'with a request': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.192', method: 'GET', body: { token: 'abc123', something: 'else' }, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: {other: 'thing'} }; t.addRequestData(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should do what the function does': function(err, item) { assert.equal(item.data.request, undefined); assert.equal(item.data.myRequest.body, 'abc123'); } } } } } } } }) .addBatch({ 'scrubPayload': { 'options': { 'without scrub fields': { topic: function() { return rollbar.defaultOptions; }, 'item': { topic: function(options) { var item = { data: { body: { message: 'hey', password: '123', secret: {stuff: 'here'} } } }; t.scrubPayload(item, options, this.callback); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should not scrub okay keys': function(err, item) { assert.equal(item.data.body.message, 'hey'); }, 'should scrub key/value based on defaults': function(err, item) { assert.matches(item.data.body.password, /\*+/); assert.matches(item.data.body.secret, /\*+/); } } }, 'with scrub fields': { topic: function() { return { captureIp: true, scrubHeaders: [ 'x-auth-token' ], scrubFields: [ 'passwd', 'access_token', 'request.cookie', 'sauce' ], scrubRequestBody: true }; }, 'item': { 'with a request': { topic: function(options) { var item = { request: { headers: { host: 'example.com', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.192', method: 'GET', body: { token: 'abc123', something: 'else' }, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: { other: 'thing', sauce: 'secrets', someParams: 'foo=okay&passwd=iamhere' } }; t.addRequestData(item, options, function(e, i) { if (e) { this.callback(e); return; } t.scrubPayload(i, options, this.callback) }.bind(this)); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should scrub based on the options': function(err, item) { var r = item.data.request; assert.equal(r.GET.token, 'abc123'); assert.match(r.headers['x-auth-token'], /\*+/); assert.equal(r.headers['host'], 'example.com'); assert.match(item.data.sauce, /\*+/); assert.equal(item.data.other, 'thing'); assert.match(item.data.someParams, /foo=okay&passwd=\*+/); } }, 'with a json request body': { topic: function(options) { var requestBody = JSON.stringify({ token: 'abc123', something: 'else', passwd: '123456' }); var item = { request: { headers: { host: 'example.com', 'content-type': 'application/json', 'x-auth-token': '12345' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.192', method: 'GET', body: requestBody, user: { id: 42, email: 'fake@example.com' } }, stuff: 'hey', data: { other: 'thing', sauce: 'secrets', someParams: 'foo=okay&passwd=iamhere' } }; t.addRequestData(item, options, function(e, i) { if (e) { this.callback(e); return; } t.scrubPayload(i, options, this.callback) }.bind(this)); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should scrub based on the options': function(err, item) { var r = item.data.request; assert.match(r.headers['x-auth-token'], /\*+/); assert.equal(r.headers['host'], 'example.com'); assert.match(item.data.sauce, /\*+/); assert.equal(item.data.other, 'thing'); assert.match(item.data.someParams, /foo=okay&passwd=\*+/); var requestBody = JSON.parse(item.data.request.body); assert.match(requestBody.passwd, /\*+/); } }, 'with a bad json request body': { topic: function(options) { var requestBody = 'not valid json'; var item = { request: { headers: { 'content-type': 'application/json' }, protocol: 'https', url: '/some/endpoint', ip: '192.192.192.192', method: 'GET', body: requestBody } }; t.addRequestData(item, options, function(e, i) { if (e) { this.callback(e); return; } t.scrubPayload(i, options, this.callback) }.bind(this)); }, 'should not error': function(err, item) { assert.ifError(err); }, 'should have a request object inside data': function(err, item) { assert.ok(item.data.request); }, 'should delete the body and add a diagnostic error': function(err, item) { var requestBody = JSON.parse(item.data.request.body); assert.equal(requestBody, null); assert.match(item.data.request.error, /request.body parse failed/); } } } } } } }) .export(module, {error: false});