dd-trace 5.92.0 → 5.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +15 -11
  2. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +23 -0
  3. package/packages/datadog-instrumentations/src/jest.js +118 -32
  4. package/packages/datadog-instrumentations/src/mocha/main.js +6 -0
  5. package/packages/datadog-instrumentations/src/mocha/utils.js +89 -5
  6. package/packages/datadog-instrumentations/src/playwright.js +10 -0
  7. package/packages/datadog-instrumentations/src/vitest.js +119 -0
  8. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
  9. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -0
  10. package/packages/datadog-plugin-jest/src/index.js +6 -0
  11. package/packages/datadog-plugin-mocha/src/index.js +11 -0
  12. package/packages/datadog-plugin-playwright/src/index.js +9 -0
  13. package/packages/datadog-plugin-vitest/src/index.js +9 -0
  14. package/packages/datadog-webpack/index.js +187 -0
  15. package/packages/datadog-webpack/src/loader.js +27 -0
  16. package/packages/datadog-webpack/src/log.js +32 -0
  17. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +103 -32
  18. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +10 -21
  19. package/packages/dd-trace/src/config/supported-configurations.json +2 -2
  20. package/packages/dd-trace/src/crashtracking/index.js +7 -1
  21. package/packages/dd-trace/src/exporters/common/docker.js +1 -0
  22. package/packages/dd-trace/src/exporters/common/request.js +26 -17
  23. package/packages/dd-trace/src/opentracing/span.js +5 -0
  24. package/packages/dd-trace/src/plugin_manager.js +10 -7
  25. package/packages/dd-trace/src/plugins/util/test.js +76 -0
  26. package/packages/dd-trace/src/priority_sampler.js +6 -3
  27. package/packages/dd-trace/src/profiling/profiler.js +78 -47
  28. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -28
  29. package/packages/dd-trace/src/proxy.js +4 -3
  30. package/packages/dd-trace/src/tracer_metadata.js +10 -1
  31. package/webpack.js +3 -0
@@ -1,8 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { EventEmitter } = require('events')
4
- const { promisify } = require('util')
5
- const zlib = require('zlib')
6
4
  const dc = require('dc-polyfill')
7
5
  const crashtracker = require('../crashtracking')
8
6
  const log = require('../log')
@@ -34,6 +32,14 @@ function findWebSpan (startedSpans, spanId) {
34
32
  return false
35
33
  }
36
34
 
35
+ const MISSING_SOURCE_MAPS_TOKEN = 'dd:has-missing-map-files'
36
+
37
+ function profileHasMissingSourceMaps (profile) {
38
+ const strings = profile?.stringTable?.strings
39
+ if (!strings) return false
40
+ return profile.comment?.some(idx => strings[idx] === MISSING_SOURCE_MAPS_TOKEN) ?? false
41
+ }
42
+
37
43
  function processInfo (infos, info, type) {
38
44
  if (Object.keys(info).length > 0) {
39
45
  infos[type] = info
@@ -42,6 +48,7 @@ function processInfo (infos, info, type) {
42
48
 
43
49
  class Profiler extends EventEmitter {
44
50
  #compressionFn
51
+ #compressionFnInitialized = false
45
52
  #compressionOptions
46
53
  #config
47
54
  #enabled = false
@@ -50,7 +57,6 @@ class Profiler extends EventEmitter {
50
57
  #logger
51
58
  #profileSeq = 0
52
59
  #spanFinishListener
53
- #sourceMapCount = 0
54
60
  #timer
55
61
 
56
62
  constructor () {
@@ -116,11 +122,13 @@ class Profiler extends EventEmitter {
116
122
  reportHostname,
117
123
  }
118
124
 
119
- return this._start(options).catch((err) => {
125
+ try {
126
+ return this._start(options)
127
+ } catch (err) {
120
128
  logError(logger, 'Error starting profiler. For troubleshooting tips, see ' +
121
129
  '<https://dtdg.co/nodejs-profiler-troubleshooting>', err)
122
130
  return false
123
- })
131
+ }
124
132
  }
125
133
 
126
134
  get enabled () {
@@ -131,7 +139,50 @@ class Profiler extends EventEmitter {
131
139
  logError(this.#logger, err)
132
140
  }
133
141
 
134
- async _start (options) {
142
+ #getCompressionFn () {
143
+ if (!this.#compressionFnInitialized) {
144
+ this.#compressionFnInitialized = true
145
+ try {
146
+ const { promisify } = require('util')
147
+ const zlib = require('zlib')
148
+ const { method, level: clevel } = this.#config.uploadCompression
149
+ switch (method) {
150
+ case 'gzip':
151
+ this.#compressionFn = promisify(zlib.gzip)
152
+ if (clevel !== undefined) {
153
+ this.#compressionOptions = {
154
+ level: clevel,
155
+ }
156
+ }
157
+ break
158
+ case 'zstd':
159
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
160
+ if (typeof zlib.zstdCompress === 'function') {
161
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
162
+ this.#compressionFn = promisify(zlib.zstdCompress)
163
+ if (clevel !== undefined) {
164
+ this.#compressionOptions = {
165
+ params: {
166
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
167
+ [zlib.constants.ZSTD_c_compressionLevel]: clevel,
168
+ },
169
+ }
170
+ }
171
+ } else {
172
+ const zstdCompress = require('@datadog/libdatadog').load('datadog-js-zstd').zstd_compress
173
+ const level = clevel ?? 0 // 0 is zstd default compression level
174
+ this.#compressionFn = (buffer) => Promise.resolve(Buffer.from(zstdCompress(buffer, level)))
175
+ }
176
+ break
177
+ }
178
+ } catch (err) {
179
+ this.#logError(err)
180
+ }
181
+ }
182
+ return this.#compressionFn
183
+ }
184
+
185
+ _start (options) {
135
186
  if (this.enabled) return true
136
187
 
137
188
  const config = this.#config = new Config(options)
@@ -148,45 +199,21 @@ class Profiler extends EventEmitter {
148
199
  setLogger(config.logger)
149
200
 
150
201
  if (config.sourceMap) {
151
- mapper = await SourceMapper.create([process.cwd()], config.debugSourceMaps)
152
- this.#sourceMapCount = mapper.infoMap.size
153
- if (config.debugSourceMaps) {
154
- this.#logger.debug(() => {
155
- return this.#sourceMapCount === 0
156
- ? 'Found no source maps'
157
- : `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
158
- })
159
- }
160
- }
161
-
162
- const clevel = config.uploadCompression.level
163
- switch (config.uploadCompression.method) {
164
- case 'gzip':
165
- this.#compressionFn = promisify(zlib.gzip)
166
- if (clevel !== undefined) {
167
- this.#compressionOptions = {
168
- level: clevel,
169
- }
170
- }
171
- break
172
- case 'zstd':
173
- if (typeof zlib.zstdCompress === 'function') { // eslint-disable-line n/no-unsupported-features/node-builtins
174
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
175
- this.#compressionFn = promisify(zlib.zstdCompress)
176
- if (clevel !== undefined) {
177
- this.#compressionOptions = {
178
- params: {
179
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
180
- [zlib.constants.ZSTD_c_compressionLevel]: clevel,
181
- },
182
- }
202
+ mapper = new SourceMapper(config.debugSourceMaps)
203
+ mapper.loadDirectory(process.cwd())
204
+ .then(() => {
205
+ if (config.debugSourceMaps) {
206
+ const count = mapper.infoMap.size
207
+ this.#logger.debug(() => {
208
+ return count === 0
209
+ ? 'Found no source maps'
210
+ : `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
211
+ })
183
212
  }
184
- } else {
185
- const zstdCompress = require('@datadog/libdatadog').load('datadog-js-zstd').zstd_compress
186
- const level = clevel ?? 0 // 0 is zstd default compression level
187
- this.#compressionFn = (buffer) => Promise.resolve(Buffer.from(zstdCompress(buffer, level)))
188
- }
189
- break
213
+ })
214
+ .catch((err) => {
215
+ this.#logError(err)
216
+ })
190
217
  }
191
218
  } catch (err) {
192
219
  this.#logError(err)
@@ -295,7 +322,7 @@ class Profiler extends EventEmitter {
295
322
  return {
296
323
  serverless: this.serverless,
297
324
  settings: this.#config.systemInfoReport,
298
- sourceMapCount: this.#sourceMapCount,
325
+ hasMissingSourceMaps: false,
299
326
  }
300
327
  }
301
328
 
@@ -332,15 +359,19 @@ class Profiler extends EventEmitter {
332
359
 
333
360
  const encodedProfiles = {}
334
361
  const infos = this.#createInitialInfos()
362
+ const compressionFn = this.#getCompressionFn()
335
363
 
336
364
  // encode and export asynchronously
337
365
  await Promise.all(profiles.map(async ({ profiler, profile, info }) => {
338
366
  try {
339
367
  const encoded = await profiler.encode(profile)
340
- const compressed = encoded instanceof Buffer && this.#compressionFn !== undefined
341
- ? await this.#compressionFn(encoded, this.#compressionOptions)
368
+ const compressed = encoded instanceof Buffer && compressionFn !== undefined
369
+ ? await compressionFn(encoded, this.#compressionOptions)
342
370
  : encoded
343
371
  encodedProfiles[profiler.type] = compressed
372
+ if (profileHasMissingSourceMaps(profile)) {
373
+ infos.hasMissingSourceMaps = true
374
+ }
344
375
  processInfo(infos, info, profiler.type)
345
376
  this.#logger.debug(() => {
346
377
  const profileJson = JSON.stringify(profile, (_, value) => {
@@ -20,6 +20,7 @@ const TRACE_ENDPOINT_LABEL = 'trace endpoint'
20
20
  let beforeCh
21
21
  const enterCh = dc.channel('dd-trace:storage:enter')
22
22
  const spanFinishCh = dc.channel('dd-trace:span:finish')
23
+ const tagsUpdateCh = dc.channel('dd-trace:span:tags:update')
23
24
  const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
24
25
 
25
26
  const ProfilingContext = Symbol('NativeWallProfiler.ProfilingContext')
@@ -58,33 +59,30 @@ function ensureChannelsActivated (asyncContextFrameEnabled) {
58
59
  if (channelsActivated) return
59
60
 
60
61
  const shimmer = require('../../../../datadog-shimmer')
61
- const asyncHooks = require('async_hooks')
62
62
 
63
- // When using AsyncContextFrame to store sample context, we do not need to use
64
- // async_hooks.createHook to create a "before" callback anymore.
65
- if (!asyncContextFrameEnabled) {
66
- const { createHook } = asyncHooks
67
- beforeCh = dc.channel('dd-trace:storage:before')
68
- createHook({ before: () => beforeCh.publish() }).enable()
69
- }
70
-
71
- const { AsyncLocalStorage } = asyncHooks
72
-
73
- // We need to instrument AsyncLocalStorage.enterWith() both with and without AsyncContextFrame.
63
+ // We need to instrument enterWith() on the legacy storage that's the storage
64
+ // carrying span data and the only one the profiler cares about.
65
+ const legacyStorage = storage('legacy')
74
66
  let inRun = false
75
- shimmer.wrap(AsyncLocalStorage.prototype, 'enterWith', function (original) {
76
- return function (...args) {
77
- const retVal = original.apply(this, args)
67
+ shimmer.wrap(legacyStorage, 'enterWith', function (original) {
68
+ return function (store) {
69
+ const retVal = original.call(this, store)
78
70
  if (!inRun) enterCh.publish()
79
71
  return retVal
80
72
  }
81
73
  })
82
74
 
83
- // We only need to instrument AsyncLocalStorage.run() when not using AsyncContextFrame.
84
- // AsyncContextFrame-based implementation of AsyncLocalStorage.run() delegates
85
- // to AsyncLocalStorage.enterWith() so it doesn't need to be separately instrumented.
75
+ // When not using AsyncContextFrame, we need additional instrumentation.
86
76
  if (!asyncContextFrameEnabled) {
87
- shimmer.wrap(AsyncLocalStorage.prototype, 'run', function (original) {
77
+ // We need async_hooks.createHook to create a "before" callback.
78
+ const { createHook } = require('async_hooks')
79
+ beforeCh = dc.channel('dd-trace:storage:before')
80
+ createHook({ before: () => beforeCh.publish() }).enable()
81
+
82
+ // In ACF-based implementation run() delegates to enterWith() so it doesn't
83
+ // need to be separately instrumented. in non-ACF implementation run()
84
+ // doesn't delegate to enterWith(), so separate instrumentation is necessary.
85
+ shimmer.wrap(legacyStorage, 'run', function (original) {
88
86
  return function (store, callback, ...args) {
89
87
  const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
90
88
  inRun = false
@@ -125,6 +123,7 @@ class NativeWallProfiler {
125
123
  // Bind these to this so they can be used as callbacks
126
124
  #boundEnter = this.#enter.bind(this)
127
125
  #boundSpanFinished = this.#spanFinished.bind(this)
126
+ #boundSpanTagsUpdated = this.#spanTagsUpdated.bind(this)
128
127
  #boundGenerateLabels = this._generateLabels.bind(this)
129
128
 
130
129
  get type () { return 'wall' }
@@ -204,6 +203,9 @@ class NativeWallProfiler {
204
203
  }
205
204
  enterCh.subscribe(this.#boundEnter)
206
205
  spanFinishCh.subscribe(this.#boundSpanFinished)
206
+ if (this.#endpointCollectionEnabled) {
207
+ tagsUpdateCh.subscribe(this.#boundSpanTagsUpdated)
208
+ }
207
209
  }
208
210
  }
209
211
 
@@ -290,15 +292,7 @@ class NativeWallProfiler {
290
292
  }
291
293
 
292
294
  profilingContext = { spanId, rootSpanId, webTags }
293
- // Don't cache if endpoint collection is enabled and webTags is undefined but
294
- // the span's type hasn't been set yet. TracingPlugin.startSpan() calls
295
- // enterWith() before the plugin sets span.type='web' via addRequestTags(),
296
- // so the first enterCh event fires before the type is known. Without this
297
- // guard we'd cache webTags=undefined and then serve that stale value on the
298
- // subsequent activation (when span.type='web' is already set).
299
- if (!this.#endpointCollectionEnabled || webTags !== undefined || context._tags['span.type']) {
300
- span[ProfilingContext] = profilingContext
301
- }
295
+ span[ProfilingContext] = profilingContext
302
296
  }
303
297
  return profilingContext
304
298
  }
@@ -317,6 +311,16 @@ class NativeWallProfiler {
317
311
  }
318
312
  }
319
313
 
314
+ #spanTagsUpdated (span) {
315
+ if (!this.#started) return
316
+ const profilingContext = span[ProfilingContext]
317
+ if (profilingContext === undefined || profilingContext.webTags !== undefined) return
318
+ const tags = span.context()._tags
319
+ if (isWebServerSpan(tags)) {
320
+ profilingContext.webTags = tags
321
+ }
322
+ }
323
+
320
324
  #reportV8bug (maybeBug) {
321
325
  const tag = `v8_profiler_bug_workaround_enabled:${this.#v8ProfilerBugWorkaroundEnabled}`
322
326
  const metric = `v8_cpu_profiler${maybeBug ? '_maybe' : ''}_stuck_event_loop`
@@ -355,6 +359,9 @@ class NativeWallProfiler {
355
359
  }
356
360
  enterCh.unsubscribe(this.#boundEnter)
357
361
  spanFinishCh.unsubscribe(this.#boundSpanFinished)
362
+ if (this.#endpointCollectionEnabled) {
363
+ tagsUpdateCh.unsubscribe(this.#boundSpanTagsUpdated)
364
+ }
358
365
  this._profilerState = undefined
359
366
  }
360
367
  this.#started = false
@@ -180,7 +180,7 @@ class Tracer extends NoopProxy {
180
180
  if (config.profiling.enabled === 'true') {
181
181
  this._profilerStarted = this._startProfiler(config)
182
182
  } else {
183
- this._profilerStarted = Promise.resolve(false)
183
+ this._profilerStarted = false
184
184
  if (config.profiling.enabled === 'auto') {
185
185
  const { SSIHeuristics } = require('./profiling/ssi-heuristics')
186
186
  const ssiHeuristics = new SSIHeuristics(config)
@@ -252,6 +252,7 @@ class Tracer extends NoopProxy {
252
252
  'Error starting profiler. For troubleshooting tips, see <https://dtdg.co/nodejs-profiler-troubleshooting>',
253
253
  e
254
254
  )
255
+ return false
255
256
  }
256
257
  }
257
258
 
@@ -329,11 +330,11 @@ class Tracer extends NoopProxy {
329
330
  * @override
330
331
  */
331
332
  profilerStarted () {
332
- if (!this._profilerStarted) {
333
+ if (this._profilerStarted === undefined) {
333
334
  // injection hardening: this is only ever invoked from tests.
334
335
  throw new Error('profilerStarted() must be called after init()')
335
336
  }
336
- return this._profilerStarted
337
+ return Promise.resolve(this._profilerStarted)
337
338
  }
338
339
 
339
340
  /**
@@ -11,13 +11,22 @@ function storeConfig (config) {
11
11
  return
12
12
  }
13
13
 
14
+ const { containerId } = require('./exporters/common/docker')
15
+ const processTags = require('./process-tags')
16
+
17
+ const processTagsSerialized = config.propagateProcessTags?.enabled
18
+ ? (processTags.serialized || null)
19
+ : null
20
+
14
21
  const metadata = new processDiscovery.TracerMetadata(
15
22
  config.tags['runtime-id'],
16
23
  tracerVersion,
17
24
  config.hostname,
18
25
  config.service || null,
19
26
  config.env || null,
20
- config.version || null
27
+ config.version || null,
28
+ processTagsSerialized,
29
+ containerId || null
21
30
  )
22
31
 
23
32
  return processDiscovery.storeMetadata(metadata)
package/webpack.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = require('./packages/datadog-webpack/index.js')