diff --git a/packages/datadog-instrumentations/src/fastify.js b/packages/datadog-instrumentations/src/fastify.js index a6d954a9460..c750bcf8b19 100644 --- a/packages/datadog-instrumentations/src/fastify.js +++ b/packages/datadog-instrumentations/src/fastify.js @@ -2,9 +2,13 @@ const shimmer = require('../../datadog-shimmer') const { addHook, channel, AsyncResource } = require('./helpers/instrument') +const { entryTag } = require('../../dd-trace/src/code_origin') const errorChannel = channel('apm:fastify:middleware:error') const handleChannel = channel('apm:fastify:request:handle') +const codeOriginEnabledChannel = channel('datadog:code-origin-enabled') + +const kCodeOriginForSpansTagsSym = Symbol('kCodeOriginForSpansTagsSym') const parsingResources = new WeakMap() @@ -27,6 +31,11 @@ function wrapFastify (fastify, hasParsingEvents) { app.addHook('preHandler', preValidation) } + // Hack to check if the Code Origin for Spans feature is enabled + if (codeOriginEnabledChannel.hasSubscribers) { + app.addHook('onRoute', onRoute) + } + app.addHook = wrapAddHook(app.addHook) return app @@ -86,8 +95,9 @@ function onRequest (request, reply, done) { const req = getReq(request) const res = getRes(reply) + const tags = getCodeOriginForSpansTags(request) - handleChannel.publish({ req, res }) + handleChannel.publish({ req, res, tags }) return done() } @@ -142,6 +152,10 @@ function getRes (reply) { return reply && (reply.raw || reply.res || reply) } +function getCodeOriginForSpansTags (request) { + return request?.routeOptions?.config?.[kCodeOriginForSpansTagsSym] +} + function publishError (error, req) { if (error) { errorChannel.publish({ error, req }) @@ -150,6 +164,11 @@ function publishError (error, req) { return error } +function onRoute (routeOptions) { + if (!routeOptions.config) routeOptions.config = {} + routeOptions.config[kCodeOriginForSpansTagsSym] = entryTag(onRoute) +} + addHook({ name: 'fastify', versions: ['>=3'] }, fastify => { const wrapped = shimmer.wrapFunction(fastify, fastify => wrapFastify(fastify, true)) diff --git a/packages/datadog-instrumentations/src/mocha/common.js b/packages/datadog-instrumentations/src/mocha/common.js index 1d31290ce6c..c25ab2fdb21 100644 --- a/packages/datadog-instrumentations/src/mocha/common.js +++ b/packages/datadog-instrumentations/src/mocha/common.js @@ -1,6 +1,6 @@ const { addHook, channel } = require('../helpers/instrument') const shimmer = require('../../../datadog-shimmer') -const { getCallSites } = require('../../../dd-trace/src/plugins/util/test') +const { getCallSites } = require('../../../dd-trace/src/plugins/util/stacktrace') const { testToStartLine } = require('./utils') const parameterizedTestCh = channel('ci:mocha:test:parameterize') diff --git a/packages/datadog-plugin-fastify/src/index.js b/packages/datadog-plugin-fastify/src/index.js index 6b4768279f8..0c3018f2642 100644 --- a/packages/datadog-plugin-fastify/src/index.js +++ b/packages/datadog-plugin-fastify/src/index.js @@ -10,9 +10,14 @@ class FastifyPlugin extends RouterPlugin { constructor (...args) { super(...args) - this.addSub('apm:fastify:request:handle', ({ req }) => { + this.addSub('apm:fastify:request:handle', ({ req, tags }) => { this.setFramework(req, 'fastify', this.config) + this.setSpanTags(req, tags) }) + + if (this._tracerConfig.codeOriginForSpansEnabled) { + this.addSub('datadog:code-origin-enabled') + } } } diff --git a/packages/datadog-plugin-fastify/test/index.spec.js b/packages/datadog-plugin-fastify/test/index.spec.js index 6b20e58a728..bfa01a5d717 100644 --- a/packages/datadog-plugin-fastify/test/index.spec.js +++ b/packages/datadog-plugin-fastify/test/index.spec.js @@ -16,6 +16,8 @@ describe('Plugin', () => { describe('fastify', () => { withVersions('fastify', 'fastify', (version, _, specificVersion) => { + if (NODE_MAJOR <= 18 && semver.satisfies(specificVersion, '>=5')) return + beforeEach(() => { tracer = require('../../dd-trace') }) @@ -26,8 +28,6 @@ describe('Plugin', () => { withExports('fastify', version, ['default', 'fastify'], '>=3', getExport => { describe('without configuration', () => { - if (NODE_MAJOR <= 18 && semver.satisfies(specificVersion, '>=5')) return - before(() => { return agent.load(['fastify', 'find-my-way', 'http'], [{}, {}, { client: false }]) }) @@ -542,7 +542,193 @@ describe('Plugin', () => { }) } }) + + describe('with tracer config codeOriginForSpansEnabled', () => { + if (semver.satisfies(specificVersion, '<4')) return // TODO: Why doesn't it work on older versions? + + before(() => { + return agent.load( + ['fastify', 'find-my-way', 'http'], + [{}, {}, { client: false }], + { codeOriginForSpansEnabled: true } + ) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + fastify = getExport() + app = fastify() + + if (semver.intersects(version, '>=3')) { + return app.register(require('../../../versions/middie').get()) + } + }) + + it('should add code_origin tag on entry spans when feature is enabled', done => { + let routeRegisterLine + + // Wrap in a named function to have at least one frame with a function name + function wrapperFunction () { + routeRegisterLine = getNextLineNumber() + app.get('/user', function userHandler (request, reply) { + reply.send() + }) + } + + const callWrapperLine = getNextLineNumber() + wrapperFunction() + + app.listen(() => { + const port = app.server.address().port + + agent + .use(traces => { + const spans = traces[0] + const tags = spans[0].meta + + expect(tags).to.have.property('_dd.code_origin.type', 'entry') + + expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename) + expect(tags).to.have.property('_dd.code_origin.frames.0.line', routeRegisterLine) + expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'wrapperFunction') + expect(tags).to.not.have.property('_dd.code_origin.frames.0.type') + + expect(tags).to.have.property('_dd.code_origin.frames.1.file', __filename) + expect(tags).to.have.property('_dd.code_origin.frames.1.line', callWrapperLine) + expect(tags).to.not.have.property('_dd.code_origin.frames.1.method') + expect(tags).to.have.property('_dd.code_origin.frames.1.type', 'Context') + + expect(tags).to.not.have.property('_dd.code_origin.frames.2.file') + }) + .then(done) + .catch(done) + + axios + .get(`http://localhost:${port}/user`) + .catch(done) + }) + }) + + it('should point to where actual route handler is configured, not the prefix', done => { + let routeRegisterLine + + app.register(function v1Handler (app, opts, done) { + routeRegisterLine = getNextLineNumber() + app.get('/user', function userHandler (request, reply) { + reply.send() + }) + done() + }, { prefix: '/v1' }) + + app.listen(() => { + const port = app.server.address().port + + agent + .use(traces => { + const spans = traces[0] + const tags = spans[0].meta + + expect(tags).to.have.property('_dd.code_origin.type', 'entry') + + expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename) + expect(tags).to.have.property('_dd.code_origin.frames.0.line', routeRegisterLine) + expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'v1Handler') + expect(tags).to.not.have.property('_dd.code_origin.frames.0.type') + + expect(tags).to.not.have.property('_dd.code_origin.frames.1.file') + }) + .then(done) + .catch(done) + + axios + .get(`http://localhost:${port}/v1/user`) + .catch(done) + }) + }) + + it('should point to route handler even if passed through a middleware', function testCase (done) { + app.use(function middleware (req, res, next) { + next() + }) + + const routeRegisterLine = getNextLineNumber() + app.get('/user', function userHandler (request, reply) { + reply.send() + }) + + app.listen({ host, port: 0 }, () => { + const port = app.server.address().port + + agent + .use(traces => { + const spans = traces[0] + const tags = spans[0].meta + + expect(tags).to.have.property('_dd.code_origin.type', 'entry') + + expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename) + expect(tags).to.have.property('_dd.code_origin.frames.0.line', routeRegisterLine) + expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'testCase') + expect(tags).to.have.property('_dd.code_origin.frames.0.type', 'Context') + + expect(tags).to.not.have.property('_dd.code_origin.frames.1.file') + }) + .then(done) + .catch(done) + + axios + .get(`http://localhost:${port}/user`) + .catch(done) + }) + }) + + // TODO: In Fastify, the route is resolved before the middleware is called, so we actually can get the line + // number of where the route handler is defined. However, this might not be the right choice and it might be + // better to point to the middleware. + it.skip('should point to middleware if middleware responds early', function testCase (done) { + const middlewareRegisterLine = getNextLineNumber() + app.use(function middleware (req, res, next) { + res.end() + }) + + app.get('/user', function userHandler (request, reply) { + reply.send() + }) + + app.listen({ host, port: 0 }, () => { + const port = app.server.address().port + + agent + .use(traces => { + const spans = traces[0] + const tags = spans[0].meta + + expect(tags).to.have.property('_dd.code_origin.type', 'entry') + + expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename) + expect(tags).to.have.property('_dd.code_origin.frames.0.line', middlewareRegisterLine) + expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'testCase') + expect(tags).to.have.property('_dd.code_origin.frames.0.type', 'Context') + + expect(tags).to.not.have.property('_dd.code_origin.frames.1.file') + }) + .then(done) + .catch(done) + + axios + .get(`http://localhost:${port}/user`) + .catch(done) + }) + }) + }) }) }) }) }) + +function getNextLineNumber () { + return String(Number(new Error().stack.split('\n')[2].match(/:(\d+):/)[1]) + 1) +} diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 688d5dc2781..605683fad74 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -15,6 +15,10 @@ class WebPlugin extends Plugin { setFramework (req, name, config) { web.setFramework(req, name, config) } + + setSpanTags (req, tags) { + web.setSpanTags(req, tags) + } } module.exports = WebPlugin diff --git a/packages/dd-trace/src/code_origin.js b/packages/dd-trace/src/code_origin.js new file mode 100644 index 00000000000..cb829247163 --- /dev/null +++ b/packages/dd-trace/src/code_origin.js @@ -0,0 +1,37 @@ +'use strict' + +const { getUserLandFrames } = require('./plugins/util/stacktrace') + +const limit = Number(process.env._DD_CODE_ORIGIN_MAX_USER_FRAMES) || 8 + +module.exports = { + entryTag, + exitTag +} + +function entryTag (topOfStackFunc) { + return tag('entry', topOfStackFunc) +} + +function exitTag (topOfStackFunc) { + return tag('exit', topOfStackFunc) +} + +function tag (type, topOfStackFunc) { + const frames = getUserLandFrames(topOfStackFunc, limit) + const tags = { + '_dd.code_origin.type': type + } + for (let i = 0; i < frames.length; i++) { + const frame = frames[i] + tags[`_dd.code_origin.frames.${i}.file`] = frame.file + tags[`_dd.code_origin.frames.${i}.line`] = String(frame.line) + if (frame.method) { + tags[`_dd.code_origin.frames.${i}.method`] = frame.method + } + if (frame.type) { + tags[`_dd.code_origin.frames.${i}.type`] = frame.type + } + } + return tags +} diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index dc5bb524d1a..070f33dd010 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -464,6 +464,7 @@ class Config { this._setValue(defaults, 'appsec.wafTimeout', 5e3) // µs this._setValue(defaults, 'clientIpEnabled', false) this._setValue(defaults, 'clientIpHeader', null) + this._setValue(defaults, 'codeOriginForSpansEnabled', false) this._setValue(defaults, 'dbmPropagationMode', 'disabled') this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1') this._setValue(defaults, 'dogstatsd.port', '8125') @@ -571,6 +572,7 @@ class Config { DD_APPSEC_RASP_ENABLED, DD_APPSEC_TRACE_RATE_LIMIT, DD_APPSEC_WAF_TIMEOUT, + DD_CODE_ORIGIN_FOR_SPANS_ENABLED, DD_DATA_STREAMS_ENABLED, DD_DBM_PROPAGATION_MODE, DD_DOGSTATSD_HOSTNAME, @@ -701,6 +703,7 @@ class Config { this._envUnprocessed['appsec.wafTimeout'] = DD_APPSEC_WAF_TIMEOUT this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED) this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER) + this._setBoolean(env, 'codeOriginForSpansEnabled', DD_CODE_ORIGIN_FOR_SPANS_ENABLED) this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE) this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME) this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT) @@ -867,6 +870,7 @@ class Config { this._optsUnprocessed['appsec.wafTimeout'] = options.appsec.wafTimeout this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled) this._setString(opts, 'clientIpHeader', options.clientIpHeader) + this._setBoolean(opts, 'codeOriginForSpansEnabled', options.codeOriginForSpansEnabled) this._setString(opts, 'dbmPropagationMode', options.dbmPropagationMode) if (options.dogstatsd) { this._setString(opts, 'dogstatsd.hostname', options.dogstatsd.hostname) diff --git a/packages/dd-trace/src/plugins/util/stacktrace.js b/packages/dd-trace/src/plugins/util/stacktrace.js new file mode 100644 index 00000000000..9a11e72eee4 --- /dev/null +++ b/packages/dd-trace/src/plugins/util/stacktrace.js @@ -0,0 +1,94 @@ +'use strict' + +const { relative, sep, isAbsolute } = require('path') + +const cwd = process.cwd() + +module.exports = { + getCallSites, + getUserLandFrames +} + +// From https://github.com/felixge/node-stack-trace/blob/ba06dcdb50d465cd440d84a563836e293b360427/index.js#L1 +function getCallSites (constructorOpt) { + const oldLimit = Error.stackTraceLimit + Error.stackTraceLimit = Infinity + + const dummy = {} + + const v8Handler = Error.prepareStackTrace + Error.prepareStackTrace = function (_, v8StackTrace) { + return v8StackTrace + } + Error.captureStackTrace(dummy, constructorOpt) + + const v8StackTrace = dummy.stack + Error.prepareStackTrace = v8Handler + Error.stackTraceLimit = oldLimit + + return v8StackTrace +} + +/** + * Get stack trace of user-land frames. + * + * @param {Function} constructorOpt - Function to pass along to Error.captureStackTrace + * @param {number} [limit=Infinity] - The maximum number of frames to return + * @returns {{ file: string, line: number, method: (string|undefined), type: (string|undefined) }[]} - A + */ +function getUserLandFrames (constructorOpt, limit = Infinity) { + const callsites = getCallSites(constructorOpt) + const frames = [] + + for (const callsite of callsites) { + if (callsite.isNative()) { + continue + } + + const filename = callsite.getFileName() + + // If the callsite is native, there will be no associated filename. However, there might be other instances where + // this can happen, so to be sure, we add this additional check + if (filename === null) { + continue + } + + // ESM module paths start with the "file://" protocol (because ESM supports https imports) + // TODO: Node.js also supports `data:` and `node:` imports, should we do something specific for `data:`? + const containsFileProtocol = filename.startsWith('file:') + + // TODO: I'm not sure how stable this check is. Alternatively, we could consider reversing it if we can get + // a comprehensive list of all non-file-based values, eg: + // + // filename === '' || filename.startsWith('node:') + if (containsFileProtocol === false && isAbsolute(filename) === false) { + continue + } + + // TODO: Technically, the algorithm below could be simplified to not use the relative path, but be simply: + // + // if (filename.includes(sep + 'node_modules' + sep)) continue + // + // However, the tests in `packages/dd-trace/test/plugins/util/stacktrace.spec.js` will fail on my machine + // because I have the source code in a parent folder called `node_modules`. So the code below thinks that + // it's not in user-land + const relativePath = relative(cwd, containsFileProtocol ? filename.substring(7) : filename) + if (relativePath.startsWith('node_modules' + sep) || relativePath.includes(sep + 'node_modules' + sep)) { + continue + } + + const method = callsite.getFunctionName() + const type = callsite.getTypeName() + frames.push({ + file: filename, + line: callsite.getLineNumber(), + // TODO: Add support for column + method: method ?? undefined, // force to undefined if null so JSON.stringify will omit it + type: type ?? undefined // force to undefined if null so JSON.stringify will omit it + }) + + if (frames.length === limit) break + } + + return frames +} diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 3cf1421ad15..e7e60823987 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -168,7 +168,6 @@ module.exports = { mergeCoverage, fromCoverageMapToCoverage, getTestLineStart, - getCallSites, removeInvalidMetadata, parseAnnotations, EFD_STRING, @@ -557,26 +556,6 @@ function getTestLineStart (err, testSuitePath) { } } -// From https://github.com/felixge/node-stack-trace/blob/ba06dcdb50d465cd440d84a563836e293b360427/index.js#L1 -function getCallSites () { - const oldLimit = Error.stackTraceLimit - Error.stackTraceLimit = Infinity - - const dummy = {} - - const v8Handler = Error.prepareStackTrace - Error.prepareStackTrace = function (_, v8StackTrace) { - return v8StackTrace - } - Error.captureStackTrace(dummy) - - const v8StackTrace = dummy.stack - Error.prepareStackTrace = v8Handler - Error.stackTraceLimit = oldLimit - - return v8StackTrace -} - /** * Gets an object of test tags from an Playwright annotations array. * @param {Object[]} annotations - Annotations from a Playwright test. diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index c9cdf1990aa..1e9c32589d2 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -68,6 +68,15 @@ const web = { web.setConfig(req, config) }, + setSpanTags (req, tags) { + const context = this.patch(req) + const span = context.span + + if (!span) return + + span.addTags(tags) + }, + setConfig (req, config) { const context = contexts.get(req) const span = context.span diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index ca4d8b142d3..2d0307f2ca6 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -219,6 +219,7 @@ describe('Config', () => { expect(config).to.have.property('reportHostname', false) expect(config).to.have.property('scope', undefined) expect(config).to.have.property('logLevel', 'debug') + expect(config).to.have.property('codeOriginForSpansEnabled', false) expect(config).to.have.property('dynamicInstrumentationEnabled', false) expect(config).to.have.property('traceId128BitGenerationEnabled', true) expect(config).to.have.property('traceId128BitLoggingEnabled', false) @@ -291,6 +292,7 @@ describe('Config', () => { { name: 'appsec.wafTimeout', value: 5e3, origin: 'default' }, { name: 'clientIpEnabled', value: false, origin: 'default' }, { name: 'clientIpHeader', value: null, origin: 'default' }, + { name: 'codeOriginForSpansEnabled', value: false, origin: 'default' }, { name: 'dbmPropagationMode', value: 'disabled', origin: 'default' }, { name: 'dogstatsd.hostname', value: '127.0.0.1', origin: 'calculated' }, { name: 'dogstatsd.port', value: '8125', origin: 'default' }, @@ -410,6 +412,7 @@ describe('Config', () => { }) it('should initialize from environment variables', () => { + process.env.DD_CODE_ORIGIN_FOR_SPANS_ENABLED = 'true' process.env.DD_TRACE_AGENT_HOSTNAME = 'agent' process.env.DD_TRACE_AGENT_PORT = '6218' process.env.DD_DOGSTATSD_HOSTNAME = 'dsd-agent' @@ -509,6 +512,7 @@ describe('Config', () => { expect(config).to.have.property('clientIpHeader', 'x-true-client-ip') expect(config).to.have.property('runtimeMetrics', true) expect(config).to.have.property('reportHostname', true) + expect(config).to.have.property('codeOriginForSpansEnabled', true) expect(config).to.have.property('dynamicInstrumentationEnabled', true) expect(config).to.have.property('env', 'test') expect(config).to.have.property('sampleRate', 0.5) @@ -604,6 +608,7 @@ describe('Config', () => { { name: 'appsec.wafTimeout', value: '42', origin: 'env_var' }, { name: 'clientIpEnabled', value: true, origin: 'env_var' }, { name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'env_var' }, + { name: 'codeOriginForSpansEnabled', value: true, origin: 'env_var' }, { name: 'dogstatsd.hostname', value: 'dsd-agent', origin: 'env_var' }, { name: 'dogstatsd.port', value: '5218', origin: 'env_var' }, { name: 'dynamicInstrumentationEnabled', value: true, origin: 'env_var' }, @@ -733,6 +738,7 @@ describe('Config', () => { env: 'test', clientIpEnabled: true, clientIpHeader: 'x-true-client-ip', + codeOriginForSpansEnabled: false, sampleRate: 0.5, rateLimit: 1000, samplingRules, @@ -820,6 +826,7 @@ describe('Config', () => { expect(config).to.have.property('reportHostname', true) expect(config).to.have.property('plugins', false) expect(config).to.have.property('logLevel', logLevel) + expect(config).to.have.property('codeOriginForSpansEnabled', false) expect(config).to.have.property('traceId128BitGenerationEnabled', true) expect(config).to.have.property('traceId128BitLoggingEnabled', true) expect(config).to.have.property('spanRemoveIntegrationFromService', true) @@ -874,6 +881,7 @@ describe('Config', () => { { name: 'appsec.standalone.enabled', value: true, origin: 'code' }, { name: 'clientIpEnabled', value: true, origin: 'code' }, { name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'code' }, + { name: 'codeOriginForSpansEnabled', value: false, origin: 'code' }, { name: 'dogstatsd.hostname', value: 'agent-dsd', origin: 'code' }, { name: 'dogstatsd.port', value: '5218', origin: 'code' }, { name: 'dynamicInstrumentationEnabled', value: true, origin: 'code' }, @@ -1161,6 +1169,7 @@ describe('Config', () => { remoteConfig: { pollInterval: 42 }, + codeOriginForSpansEnabled: false, traceId128BitGenerationEnabled: false, traceId128BitLoggingEnabled: false }) @@ -1177,6 +1186,7 @@ describe('Config', () => { expect(config).to.have.property('flushMinSpans', 500) expect(config).to.have.property('service', 'test') expect(config).to.have.property('version', '1.0.0') + expect(config).to.have.property('codeOriginForSpansEnabled', false) expect(config).to.have.property('dynamicInstrumentationEnabled', false) expect(config).to.have.property('env', 'development') expect(config).to.have.property('clientIpEnabled', true) diff --git a/packages/dd-trace/test/opentracing/tracer.spec.js b/packages/dd-trace/test/opentracing/tracer.spec.js index 1a6ae261f0b..8116c3e0b62 100644 --- a/packages/dd-trace/test/opentracing/tracer.spec.js +++ b/packages/dd-trace/test/opentracing/tracer.spec.js @@ -127,6 +127,7 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, + codeOriginForSpansEnabled: undefined, traceId128BitGenerationEnabled: undefined, integrationName: undefined, links: undefined @@ -186,6 +187,7 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: os.hostname(), + codeOriginForSpansEnabled: undefined, traceId128BitGenerationEnabled: undefined, integrationName: undefined, links: undefined @@ -258,6 +260,7 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, + codeOriginForSpansEnabled: undefined, traceId128BitGenerationEnabled: true, integrationName: undefined, links: undefined @@ -280,6 +283,7 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, + codeOriginForSpansEnabled: undefined, traceId128BitGenerationEnabled: undefined, integrationName: undefined, links: [{ context }] diff --git a/packages/dd-trace/test/plugins/util/stacktrace.spec.js b/packages/dd-trace/test/plugins/util/stacktrace.spec.js new file mode 100644 index 00000000000..db8a20eaf26 --- /dev/null +++ b/packages/dd-trace/test/plugins/util/stacktrace.spec.js @@ -0,0 +1,59 @@ +'use strict' + +const { isAbsolute } = require('path') + +require('../../setup/tap') + +const { + getCallSites, + getUserLandFrames +} = require('../../../src/plugins/util/stacktrace') + +describe('stacktrace utils', () => { + it('should get callsites array from getCallsites', () => { + const callsites = getCallSites() + expect(callsites).to.be.an('array') + expect(callsites.length).to.be.gt(0) + callsites.forEach((callsite) => { + expect(callsite).to.be.an.instanceof(Object) + expect(callsite.constructor.name).to.equal('CallSite') + expect(callsite.getFileName).to.be.an.instanceof(Function) + }) + }) + + describe('getUserLandFrames', () => { + it('should return array of frame objects', function helloWorld () { + (function someFunction () { + const frames = getUserLandFrames(someFunction) + + expect(frames).to.be.an('array') + expect(frames.length).to.be.gt(1) + frames.forEach((frame) => { + expect(frame).to.be.an.instanceof(Object) + expect(frame).to.have.all.keys('file', 'line', 'method', 'type') + expect(frame.file).to.be.a('string') + expect(frame.line).to.be.gt(0) + expect(typeof frame.method).to.be.oneOf(['string', 'undefined']) + expect(typeof frame.type).to.be.oneOf(['string', 'undefined']) + expect(isAbsolute(frame.file)).to.be.true + }) + + const frame = frames[0] + expect(frame.file).to.equal(__filename) + expect(frame.method).to.equal('helloWorld') + expect(frame.type).to.equal('Test') + })() + }) + + it('should respect limit', function helloWorld () { + (function someFunction () { + const frames = getUserLandFrames(someFunction, 1) + expect(frames.length).to.equal(1) + const frame = frames[0] + expect(frame.file).to.equal(__filename) + expect(frame.method).to.equal('helloWorld') + expect(frame.type).to.equal('Test') + })() + }) + }) +}) diff --git a/packages/dd-trace/test/plugins/util/web.spec.js b/packages/dd-trace/test/plugins/util/web.spec.js index 821d2d8d537..397838f4574 100644 --- a/packages/dd-trace/test/plugins/util/web.spec.js +++ b/packages/dd-trace/test/plugins/util/web.spec.js @@ -612,6 +612,22 @@ describe('plugins/util/web', () => { }) }) + describe('setSpanTags', () => { + it('should add expected tags', () => { + web.instrument(tracer, config, req, res, 'test.request', span => { + web.setSpanTags(req, { foo: 'bar' }) + + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + foo: 'bar' + }) + }) + }) + }) + describe('enterRoute', () => { beforeEach(() => { config = web.normalizeConfig(config)