From c085df1eaee486cc0b26eba2782d8b0c2ed040ca Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 14 Oct 2024 07:00:14 +0200 Subject: [PATCH] Add support for Fastify entry spans for Code Origin for Spans (#4449) This commit does two things: - It lays the groundwork for an upcoming feature called "Code Origin for Spans". - To showcase this feature, it adds limited support for just Fastify entry-spans. To enable, set `DD_CODE_ORIGIN_FOR_SPANS_ENABLED=true`. --- CODEOWNERS | 2 + packages/datadog-code-origin/index.js | 38 +++ .../datadog-instrumentations/src/fastify.js | 13 +- .../src/mocha/common.js | 2 +- .../datadog-plugin-fastify/src/code_origin.js | 31 +++ packages/datadog-plugin-fastify/src/index.js | 22 +- .../datadog-plugin-fastify/src/tracing.js | 19 ++ .../test/code_origin.spec.js | 216 ++++++++++++++++++ .../test/{index.spec.js => tracing.spec.js} | 4 +- packages/dd-trace/src/config.js | 4 + .../dd-trace/src/plugins/util/stacktrace.js | 94 ++++++++ packages/dd-trace/src/plugins/util/test.js | 21 -- packages/dd-trace/test/config.spec.js | 14 ++ .../test/plugins/util/stacktrace.spec.js | 68 ++++++ 14 files changed, 511 insertions(+), 37 deletions(-) create mode 100644 packages/datadog-code-origin/index.js create mode 100644 packages/datadog-plugin-fastify/src/code_origin.js create mode 100644 packages/datadog-plugin-fastify/src/tracing.js create mode 100644 packages/datadog-plugin-fastify/test/code_origin.spec.js rename packages/datadog-plugin-fastify/test/{index.spec.js => tracing.spec.js} (99%) create mode 100644 packages/dd-trace/src/plugins/util/stacktrace.js create mode 100644 packages/dd-trace/test/plugins/util/stacktrace.spec.js diff --git a/CODEOWNERS b/CODEOWNERS index 8f7d53e03b0..da66c3557b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,6 +4,8 @@ /packages/dd-trace/test/appsec/ @DataDog/asm-js /integration-tests/debugger/ @DataDog/dd-trace-js @DataDog/debugger +/packages/datadog-code-origin/ @DataDog/dd-trace-js @DataDog/debugger +/packages/datadog-plugin-*/**/code_origin.* @DataDog/dd-trace-js @DataDog/debugger /packages/dd-trace/src/debugger/ @DataDog/dd-trace-js @DataDog/debugger /packages/dd-trace/test/debugger/ @DataDog/dd-trace-js @DataDog/debugger diff --git a/packages/datadog-code-origin/index.js b/packages/datadog-code-origin/index.js new file mode 100644 index 00000000000..530dd3cc8ae --- /dev/null +++ b/packages/datadog-code-origin/index.js @@ -0,0 +1,38 @@ +'use strict' + +const { getUserLandFrames } = require('../dd-trace/src/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) + tags[`_dd.code_origin.frames.${i}.column`] = String(frame.column) + 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/datadog-instrumentations/src/fastify.js b/packages/datadog-instrumentations/src/fastify.js index a6d954a9460..726e8284f92 100644 --- a/packages/datadog-instrumentations/src/fastify.js +++ b/packages/datadog-instrumentations/src/fastify.js @@ -5,6 +5,7 @@ const { addHook, channel, AsyncResource } = require('./helpers/instrument') const errorChannel = channel('apm:fastify:middleware:error') const handleChannel = channel('apm:fastify:request:handle') +const routeAddedChannel = channel('apm:fastify:route:added') const parsingResources = new WeakMap() @@ -16,6 +17,7 @@ function wrapFastify (fastify, hasParsingEvents) { if (!app || typeof app.addHook !== 'function') return app + app.addHook('onRoute', onRoute) app.addHook('onRequest', onRequest) app.addHook('preHandler', preHandler) @@ -86,8 +88,9 @@ function onRequest (request, reply, done) { const req = getReq(request) const res = getRes(reply) + const routeConfig = getRouteConfig(request) - handleChannel.publish({ req, res }) + handleChannel.publish({ req, res, routeConfig }) return done() } @@ -142,6 +145,10 @@ function getRes (reply) { return reply && (reply.raw || reply.res || reply) } +function getRouteConfig (request) { + return request?.routeOptions?.config +} + function publishError (error, req) { if (error) { errorChannel.publish({ error, req }) @@ -150,6 +157,10 @@ function publishError (error, req) { return error } +function onRoute (routeOptions) { + routeAddedChannel.publish({ routeOptions, 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/code_origin.js b/packages/datadog-plugin-fastify/src/code_origin.js new file mode 100644 index 00000000000..3e6f58d5624 --- /dev/null +++ b/packages/datadog-plugin-fastify/src/code_origin.js @@ -0,0 +1,31 @@ +'use strict' + +const { entryTag } = require('../../datadog-code-origin') +const Plugin = require('../../dd-trace/src/plugins/plugin') +const web = require('../../dd-trace/src/plugins/util/web') + +const kCodeOriginForSpansTagsSym = Symbol('datadog.codeOriginForSpansTags') + +class FastifyCodeOriginForSpansPlugin extends Plugin { + static get id () { + return 'fastify' + } + + constructor (...args) { + super(...args) + + this.addSub('apm:fastify:request:handle', ({ req, routeConfig }) => { + const tags = routeConfig?.[kCodeOriginForSpansTagsSym] + if (!tags) return + const context = web.getContext(req) + context.span?.addTags(tags) + }) + + this.addSub('apm:fastify:route:added', ({ routeOptions, onRoute }) => { + if (!routeOptions.config) routeOptions.config = {} + routeOptions.config[kCodeOriginForSpansTagsSym] = entryTag(onRoute) + }) + } +} + +module.exports = FastifyCodeOriginForSpansPlugin diff --git a/packages/datadog-plugin-fastify/src/index.js b/packages/datadog-plugin-fastify/src/index.js index 6b4768279f8..18371458346 100644 --- a/packages/datadog-plugin-fastify/src/index.js +++ b/packages/datadog-plugin-fastify/src/index.js @@ -1,18 +1,16 @@ 'use strict' -const RouterPlugin = require('../../datadog-plugin-router/src') +const FastifyTracingPlugin = require('./tracing') +const FastifyCodeOriginForSpansPlugin = require('./code_origin') +const CompositePlugin = require('../../dd-trace/src/plugins/composite') -class FastifyPlugin extends RouterPlugin { - static get id () { - return 'fastify' - } - - constructor (...args) { - super(...args) - - this.addSub('apm:fastify:request:handle', ({ req }) => { - this.setFramework(req, 'fastify', this.config) - }) +class FastifyPlugin extends CompositePlugin { + static get id () { return 'fastify' } + static get plugins () { + return { + tracing: FastifyTracingPlugin, + codeOriginForSpans: FastifyCodeOriginForSpansPlugin + } } } diff --git a/packages/datadog-plugin-fastify/src/tracing.js b/packages/datadog-plugin-fastify/src/tracing.js new file mode 100644 index 00000000000..90b2e5e8451 --- /dev/null +++ b/packages/datadog-plugin-fastify/src/tracing.js @@ -0,0 +1,19 @@ +'use strict' + +const RouterPlugin = require('../../datadog-plugin-router/src') + +class FastifyTracingPlugin extends RouterPlugin { + static get id () { + return 'fastify' + } + + constructor (...args) { + super(...args) + + this.addSub('apm:fastify:request:handle', ({ req }) => { + this.setFramework(req, 'fastify', this.config) + }) + } +} + +module.exports = FastifyTracingPlugin diff --git a/packages/datadog-plugin-fastify/test/code_origin.spec.js b/packages/datadog-plugin-fastify/test/code_origin.spec.js new file mode 100644 index 00000000000..711c2ffff6c --- /dev/null +++ b/packages/datadog-plugin-fastify/test/code_origin.spec.js @@ -0,0 +1,216 @@ +'use strict' + +const axios = require('axios') +const semver = require('semver') +const agent = require('../../dd-trace/test/plugins/agent') +const { NODE_MAJOR } = require('../../../version') + +const host = 'localhost' + +describe('Plugin', () => { + let fastify + let app + + describe('fastify', () => { + withVersions('fastify', 'fastify', (version, _, specificVersion) => { + if (NODE_MAJOR <= 18 && semver.satisfies(specificVersion, '>=5')) return + + afterEach(() => { + app.close() + }) + + withExports('fastify', version, ['default', 'fastify'], '>=3', getExport => { + describe('with tracer config codeOriginForSpans.enabled: true', () => { + 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 }], + { codeOriginForSpans: { enabled: 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.column').to.match(/^\d+$/) + 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.have.property('_dd.code_origin.frames.1.column').to.match(/^\d+$/) + 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.column').to.match(/^\d+$/) + 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.column').to.match(/^\d+$/) + 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.column').to.match(/^\d+$/) + 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-fastify/test/index.spec.js b/packages/datadog-plugin-fastify/test/tracing.spec.js similarity index 99% rename from packages/datadog-plugin-fastify/test/index.spec.js rename to packages/datadog-plugin-fastify/test/tracing.spec.js index 6b20e58a728..c8924c98dfd 100644 --- a/packages/datadog-plugin-fastify/test/index.spec.js +++ b/packages/datadog-plugin-fastify/test/tracing.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 }]) }) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index c6cd23945ba..e827d1b6d0f 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, 'codeOriginForSpans.enabled', false) this._setValue(defaults, 'dbmPropagationMode', 'disabled') this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1') this._setValue(defaults, 'dogstatsd.port', '8125') @@ -573,6 +574,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, @@ -704,6 +706,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, 'codeOriginForSpans.enabled', 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) @@ -871,6 +874,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, 'codeOriginForSpans.enabled', options.codeOriginForSpans?.enabled) 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..f67ba52c7c2 --- /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(), + column: callsite.getColumnNumber(), + 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/test/config.spec.js b/packages/dd-trace/test/config.spec.js index ec34d7e71dd..6558485b529 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.nested.property('codeOriginForSpans.enabled', 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: 'codeOriginForSpans.enabled', 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' }, @@ -411,6 +413,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' @@ -511,6 +514,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.nested.property('codeOriginForSpans.enabled', true) expect(config).to.have.property('dynamicInstrumentationEnabled', true) expect(config).to.have.property('env', 'test') expect(config).to.have.property('sampleRate', 0.5) @@ -607,6 +611,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: 'codeOriginForSpans.enabled', 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' }, @@ -737,6 +742,9 @@ describe('Config', () => { env: 'test', clientIpEnabled: true, clientIpHeader: 'x-true-client-ip', + codeOriginForSpans: { + enabled: false + }, sampleRate: 0.5, rateLimit: 1000, samplingRules, @@ -825,6 +833,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.nested.property('codeOriginForSpans.enabled', false) expect(config).to.have.property('traceId128BitGenerationEnabled', true) expect(config).to.have.property('traceId128BitLoggingEnabled', true) expect(config).to.have.property('spanRemoveIntegrationFromService', true) @@ -880,6 +889,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: 'codeOriginForSpans.enabled', 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' }, @@ -1170,6 +1180,9 @@ describe('Config', () => { remoteConfig: { pollInterval: 42 }, + codeOriginForSpans: { + enabled: false + }, traceId128BitGenerationEnabled: false, traceId128BitLoggingEnabled: false }) @@ -1186,6 +1199,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.nested.property('codeOriginForSpans.enabled', 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/plugins/util/stacktrace.spec.js b/packages/dd-trace/test/plugins/util/stacktrace.spec.js new file mode 100644 index 00000000000..3fefc2b29ef --- /dev/null +++ b/packages/dd-trace/test/plugins/util/stacktrace.spec.js @@ -0,0 +1,68 @@ +'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', 'column', 'method', 'type') + expect(frame.file).to.be.a('string') + expect(frame.line).to.be.gt(0) + expect(frame.column).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.line).to.equal(lineNumber) + expect(frame.method).to.equal('helloWorld') + expect(frame.type).to.equal('Test') + } + + const lineNumber = getNextLineNumber() + someFunction() + }) + + 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') + })() + }) + }) +}) + +function getNextLineNumber () { + return Number(new Error().stack.split('\n')[2].match(/:(\d+):/)[1]) + 1 +}