dd-trace 5.91.0 → 5.93.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.91.0",
3
+ "version": "5.93.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -138,12 +138,12 @@
138
138
  "import-in-the-middle": "^3.0.0"
139
139
  },
140
140
  "optionalDependencies": {
141
- "@datadog/libdatadog": "0.8.1",
141
+ "@datadog/libdatadog": "0.9.2",
142
142
  "@datadog/native-appsec": "11.0.1",
143
143
  "@datadog/native-iast-taint-tracking": "4.1.0",
144
144
  "@datadog/native-metrics": "3.1.1",
145
145
  "@datadog/openfeature-node-server": "^1.1.0",
146
- "@datadog/pprof": "5.13.5",
146
+ "@datadog/pprof": "5.14.0",
147
147
  "@datadog/wasm-js-rewriter": "5.0.1",
148
148
  "@opentelemetry/api": ">=1.0.0 <1.10.0",
149
149
  "@opentelemetry/api-logs": "<1.0.0",
@@ -12,7 +12,7 @@ const {
12
12
  const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')
13
13
 
14
14
  // ignored exec method because it calls to execFile directly
15
- const execAsyncMethods = ['execFile', 'spawn']
15
+ const execAsyncMethods = ['execFile', 'spawn', 'fork']
16
16
 
17
17
  const names = ['child_process', 'node:child_process']
18
18
 
@@ -97,8 +97,10 @@ function wrapChildProcessSyncMethod (returnError, shell = false) {
97
97
  return childProcessMethod.apply(this, arguments)
98
98
  }
99
99
 
100
- const childProcessInfo = normalizeArgs(arguments, shell)
100
+ const callArgs = [...arguments]
101
+ const childProcessInfo = normalizeArgs(callArgs, shell)
101
102
  const context = createContextFromChildProcessInfo(childProcessInfo)
103
+ context.callArgs = callArgs
102
104
 
103
105
  return childProcessChannel.start.runStores(context, () => {
104
106
  try {
@@ -108,7 +110,7 @@ function wrapChildProcessSyncMethod (returnError, shell = false) {
108
110
  return returnError(error, context)
109
111
  }
110
112
 
111
- const result = childProcessMethod.apply(this, arguments)
113
+ const result = childProcessMethod.apply(this, context.callArgs)
112
114
  context.result = result
113
115
 
114
116
  return result
@@ -131,9 +133,11 @@ function wrapChildProcessCustomPromisifyMethod (customPromisifyMethod, shell) {
131
133
  return customPromisifyMethod.apply(this, arguments)
132
134
  }
133
135
 
134
- const childProcessInfo = normalizeArgs(arguments, shell)
136
+ const callArgs = [...arguments]
137
+ const childProcessInfo = normalizeArgs(callArgs, shell)
135
138
 
136
139
  const context = createContextFromChildProcessInfo(childProcessInfo)
140
+ context.callArgs = callArgs
137
141
 
138
142
  const { start, end, asyncStart, asyncEnd, error } = childProcessChannel
139
143
  start.publish(context)
@@ -143,7 +147,7 @@ function wrapChildProcessCustomPromisifyMethod (customPromisifyMethod, shell) {
143
147
  result = Promise.reject(context.abortController.signal.reason || new Error('Aborted'))
144
148
  } else {
145
149
  try {
146
- result = customPromisifyMethod.apply(this, arguments)
150
+ result = customPromisifyMethod.apply(this, context.callArgs)
147
151
  } catch (error) {
148
152
  context.error = error
149
153
  error.publish(context)
@@ -181,9 +185,11 @@ function wrapChildProcessAsyncMethod (ChildProcess, shell = false) {
181
185
  return childProcessMethod.apply(this, arguments)
182
186
  }
183
187
 
184
- const childProcessInfo = normalizeArgs(arguments, shell)
188
+ const callArgs = [...arguments]
189
+ const childProcessInfo = normalizeArgs(callArgs, shell)
185
190
 
186
191
  const context = createContextFromChildProcessInfo(childProcessInfo)
192
+ context.callArgs = callArgs
187
193
  return childProcessChannel.start.runStores(context, () => {
188
194
  let childProcess
189
195
  if (context.abortController.signal.aborted) {
@@ -194,7 +200,7 @@ function wrapChildProcessAsyncMethod (ChildProcess, shell = false) {
194
200
  const error = context.abortController.signal.reason || new Error('Aborted')
195
201
  childProcess.emit('error', error)
196
202
 
197
- const cb = arguments[arguments.length - 1]
203
+ const cb = context.callArgs[context.callArgs.length - 1]
198
204
  if (typeof cb === 'function') {
199
205
  cb(error)
200
206
  }
@@ -202,7 +208,7 @@ function wrapChildProcessAsyncMethod (ChildProcess, shell = false) {
202
208
  childProcess.emit('close')
203
209
  })
204
210
  } else {
205
- childProcess = childProcessMethod.apply(this, arguments)
211
+ childProcess = childProcessMethod.apply(this, context.callArgs)
206
212
  }
207
213
 
208
214
  if (childProcess) {
@@ -277,7 +277,7 @@ function assertField (rootCtx, info, args) {
277
277
  let field = fields[pathString]
278
278
 
279
279
  if (!field) {
280
- const fieldCtx = { info, rootCtx, args }
280
+ const fieldCtx = { info, rootCtx, args, path, pathString }
281
281
  startResolveCh.publish(fieldCtx)
282
282
  field = fields[pathString] = {
283
283
  error: null,
@@ -154,6 +154,11 @@ class BaseAwsSdkPlugin extends ClientPlugin {
154
154
  })
155
155
 
156
156
  this.finish(ctx)
157
+
158
+ if (IS_SERVERLESS) {
159
+ const peerStore = storage('peerServerless').getStore()
160
+ if (peerStore) delete peerStore.peerHostname
161
+ }
157
162
  })
158
163
 
159
164
  this.addBind(`apm:aws:response:start:${this.serviceIdentifier}`, ctx => {
@@ -58,8 +58,8 @@ function addVariableTags (config, span, variableValues) {
58
58
 
59
59
  if (variableValues && config.variables) {
60
60
  const variables = config.variables(variableValues)
61
- for (const param in variables) {
62
- tags[`graphql.variables.${param}`] = variables[param]
61
+ for (const [param, value] of Object.entries(variables)) {
62
+ tags[`graphql.variables.${param}`] = value
63
63
  }
64
64
  }
65
65
 
@@ -10,13 +10,13 @@ class GraphQLResolvePlugin extends TracingPlugin {
10
10
  static operation = 'resolve'
11
11
 
12
12
  start (fieldCtx) {
13
- const { info, rootCtx, args } = fieldCtx
13
+ const { info, rootCtx, args, path: pathAsArray, pathString } = fieldCtx
14
14
 
15
- const path = getPath(info, this.config)
15
+ const path = getPath(this.config, pathAsArray)
16
16
 
17
17
  // we need to get the parent span to the field if it exists for correct span parenting
18
18
  // of nested fields
19
- const parentField = getParentField(rootCtx, pathToArray(info && info.path))
19
+ const parentField = getParentField(rootCtx, pathString)
20
20
  const childOf = parentField?.ctx?.currentStore?.span
21
21
 
22
22
  fieldCtx.parent = parentField
@@ -76,9 +76,9 @@ class GraphQLResolvePlugin extends TracingPlugin {
76
76
  super(...args)
77
77
 
78
78
  this.addTraceSub('updateField', (ctx) => {
79
- const { field, info, error } = ctx
79
+ const { field, error, path: pathAsArray } = ctx
80
80
 
81
- const path = getPath(info, this.config)
81
+ const path = getPath(this.config, pathAsArray)
82
82
 
83
83
  if (!shouldInstrument(this.config, path)) return
84
84
 
@@ -118,28 +118,14 @@ function shouldInstrument (config, path) {
118
118
  return config.depth < 0 || config.depth >= depth
119
119
  }
120
120
 
121
- function getPath (info, config) {
122
- const responsePathAsArray = config.collapse
123
- ? withCollapse(pathToArray)
124
- : pathToArray
125
- return responsePathAsArray(info && info.path)
121
+ function getPath (config, pathAsArray) {
122
+ return config.collapse
123
+ ? withCollapse(pathAsArray)
124
+ : pathAsArray
126
125
  }
127
126
 
128
- function pathToArray (path) {
129
- const flattened = []
130
- let curr = path
131
- while (curr) {
132
- flattened.push(curr.key)
133
- curr = curr.prev
134
- }
135
- return flattened.reverse()
136
- }
137
-
138
- function withCollapse (responsePathAsArray) {
139
- return function () {
140
- return responsePathAsArray.apply(this, arguments)
141
- .map(segment => typeof segment === 'number' ? '*' : segment)
142
- }
127
+ function withCollapse (pathAsArray) {
128
+ return pathAsArray.map(segment => typeof segment === 'number' ? '*' : segment)
143
129
  }
144
130
 
145
131
  function getResolverInfo (info, args) {
@@ -173,19 +159,20 @@ function getResolverInfo (info, args) {
173
159
  return resolverInfo
174
160
  }
175
161
 
176
- function getParentField (parentCtx, path) {
177
- for (let i = path.length - 1; i > 0; i--) {
178
- const field = getField(parentCtx, path.slice(0, i))
179
- if (field) {
180
- return field
181
- }
162
+ function getParentField (parentCtx, pathToString) {
163
+ let current = pathToString
164
+
165
+ while (current) {
166
+ const lastJoin = current.lastIndexOf('.')
167
+ if (lastJoin === -1) break
168
+
169
+ current = current.slice(0, lastJoin)
170
+ const field = parentCtx.fields[current]
171
+
172
+ if (field) return field
182
173
  }
183
174
 
184
175
  return null
185
176
  }
186
177
 
187
- function getField (parentCtx, path) {
188
- return parentCtx.fields[path.join('.')]
189
- }
190
-
191
178
  module.exports = GraphQLResolvePlugin
@@ -211,7 +211,7 @@ function getHeaders (config) {
211
211
  if (typeof header === 'string') {
212
212
  const separatorIndex = header.indexOf(':')
213
213
  result.push(separatorIndex === -1
214
- ? [header, undefined]
214
+ ? [header.toLowerCase(), undefined]
215
215
  : [
216
216
  header.slice(0, separatorIndex).toLowerCase(),
217
217
  header.slice(separatorIndex + 1),
@@ -2,6 +2,7 @@
2
2
 
3
3
  const pkg = require('../pkg')
4
4
  const { isFalse, isTrue } = require('../util')
5
+ const { DD_MAJOR } = require('../../../../version')
5
6
  const { getEnvironmentVariable: getEnv } = require('./helper')
6
7
 
7
8
  const {
@@ -117,6 +118,7 @@ const defaultsWithoutSupportedConfigurationEntry = {
117
118
  // TODO: These entries should be removed. They are off by default
118
119
  // because they rely on other configs.
119
120
  const defaultsWithConditionalRuntimeBehavior = {
121
+ startupLogs: DD_MAJOR >= 6,
120
122
  isGitUploadEnabled: false,
121
123
  isImpactedTestsEnabled: false,
122
124
  isIntelligentTestRunnerEnabled: false,
@@ -49,6 +49,8 @@ const VALID_PROPAGATION_BEHAVIOR_EXTRACT = new Set(['continue', 'restart', 'igno
49
49
  const VALID_LOG_LEVELS = new Set(['debug', 'info', 'warn', 'error'])
50
50
  const DEFAULT_OTLP_PORT = 4318
51
51
  const RUNTIME_ID = uuid()
52
+ // eslint-disable-next-line eslint-rules/eslint-process-env -- internal propagation, not user config
53
+ const ROOT_SESSION_ID = process.env.DD_ROOT_JS_SESSION_ID || RUNTIME_ID
52
54
  const NAMING_VERSIONS = new Set(['v0', 'v1'])
53
55
  const DEFAULT_NAMING_VERSION = 'v0'
54
56
 
@@ -145,6 +147,8 @@ class Config {
145
147
  'runtime-id': RUNTIME_ID,
146
148
  })
147
149
 
150
+ this.rootSessionId = ROOT_SESSION_ID
151
+
148
152
  if (this.isCiVisibility) {
149
153
  tagger.add(this.tags, {
150
154
  [ORIGIN_KEY]: 'ciapp-test',
@@ -1125,6 +1129,8 @@ class Config {
1125
1129
  setBoolean(calc, 'reportHostname', true)
1126
1130
  // Clear sampling rules - server-side sampling handles this
1127
1131
  calc['sampler.rules'] = []
1132
+ // Agentless intake only accepts 64-bit trace IDs; disable 128-bit generation
1133
+ setBoolean(calc, 'traceId128BitGenerationEnabled', false)
1128
1134
  }
1129
1135
 
1130
1136
  if (this.#isCiVisibility()) {
@@ -744,9 +744,9 @@
744
744
  ],
745
745
  "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
746
746
  {
747
- "implementation": "A",
747
+ "implementation": "B",
748
748
  "type": "boolean",
749
- "default": "false",
749
+ "default": "true",
750
750
  "configurationNames": [
751
751
  "propagateProcessTags.enabled"
752
752
  ]
@@ -1,9 +1,15 @@
1
1
  'use strict'
2
2
 
3
+ const { existsSync } = require('node:fs')
3
4
  const { isMainThread } = require('worker_threads')
4
5
  const log = require('../log')
5
6
 
6
- if (isMainThread) {
7
+ // libdatadog v29 crashtracker segfaults during init on ARM64 musl (Alpine).
8
+ // The segfault bypasses JS try/catch so we must avoid loading it entirely.
9
+ // See: https://github.com/DataDog/libdatadog-nodejs/issues/114
10
+ const isArm64Musl = process.arch === 'arm64' && existsSync('/etc/alpine-release')
11
+
12
+ if (isMainThread && !isArm64Musl) {
7
13
  try {
8
14
  module.exports = require('./crashtracker')
9
15
  } catch (e) {
@@ -16,6 +16,9 @@ const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
16
16
  function formatSpan (span, isFirstSpan) {
17
17
  span = normalizeSpan(truncateSpan(span, false))
18
18
 
19
+ // Remove _dd.p.tid (the upper 64 bits of a 128-bit trace ID) since trace_id is truncated to lower 64 bits
20
+ delete span.meta['_dd.p.tid']
21
+
19
22
  if (span.span_events) {
20
23
  span.meta.events = JSON.stringify(span.span_events)
21
24
  delete span.span_events
@@ -45,7 +48,7 @@ function formatSpan (span, isFirstSpan) {
45
48
  */
46
49
  function spanToJSON (span) {
47
50
  const result = {
48
- trace_id: span.trace_id.toString(16).toLowerCase(),
51
+ trace_id: span.trace_id.toString(16).toLowerCase().slice(-16),
49
52
  span_id: span.span_id.toString(16).toLowerCase(),
50
53
  parent_id: span.parent_id.toString(16).toLowerCase(),
51
54
  name: span.name,
@@ -35,6 +35,7 @@ if (inodePath) {
35
35
  const entityId = containerId ? `ci-${containerId}` : inode && `in-${inode}`
36
36
 
37
37
  module.exports = {
38
+ containerId,
38
39
  entityId,
39
40
 
40
41
  inject (carrier) {
@@ -14,9 +14,9 @@ const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
14
14
  const docker = require('./docker')
15
15
  const { httpAgent, httpsAgent } = require('./agents')
16
16
 
17
- const maxActiveRequests = 8
17
+ const maxActiveBufferSize = 1024 * 1024 * 64
18
18
 
19
- let activeRequests = 0
19
+ let activeBufferSize = 0
20
20
 
21
21
  function parseUrl (urlObjOrString) {
22
22
  if (urlObjOrString !== null && typeof urlObjOrString === 'object') return urlToHttpOptions(urlObjOrString)
@@ -50,7 +50,22 @@ function request (data, options, callback) {
50
50
  }
51
51
  }
52
52
 
53
- const isReadable = data instanceof Readable
53
+ if (data instanceof Readable) {
54
+ const chunks = []
55
+
56
+ data
57
+ .on('data', (data) => {
58
+ chunks.push(data)
59
+ })
60
+ .on('end', () => {
61
+ request(Buffer.concat(chunks), options, callback)
62
+ })
63
+ .on('error', (err) => {
64
+ callback(err)
65
+ })
66
+
67
+ return
68
+ }
54
69
 
55
70
  // The timeout should be kept low to avoid excessive queueing.
56
71
  const timeout = options.timeout || 2000
@@ -58,12 +73,10 @@ function request (data, options, callback) {
58
73
  const client = isSecure ? https : http
59
74
  let dataArray = data
60
75
 
61
- if (!isReadable) {
62
- if (!Array.isArray(data)) {
63
- dataArray = [data]
64
- }
65
- options.headers['Content-Length'] = byteLength(dataArray)
76
+ if (!Array.isArray(data)) {
77
+ dataArray = [data]
66
78
  }
79
+ options.headers['Content-Length'] = byteLength(dataArray)
67
80
 
68
81
  docker.inject(options.headers)
69
82
 
@@ -126,14 +139,14 @@ function request (data, options, callback) {
126
139
  return callback(null)
127
140
  }
128
141
 
129
- activeRequests++
142
+ activeBufferSize += options.headers['Content-Length'] ?? 0
130
143
 
131
144
  storage('legacy').run({ noop: true }, () => {
132
145
  let finished = false
133
146
  const finalize = () => {
134
147
  if (finished) return
135
148
  finished = true
136
- activeRequests--
149
+ activeBufferSize -= options.headers['Content-Length'] ?? 0
137
150
  }
138
151
 
139
152
  const req = client.request(options, (res) => onResponse(res, finalize))
@@ -158,12 +171,8 @@ function request (data, options, callback) {
158
171
  }
159
172
  })
160
173
 
161
- if (isReadable) {
162
- data.pipe(req) // TODO: Validate whether this is actually retriable.
163
- } else {
164
- for (const buffer of dataArray) req.write(buffer)
165
- req.end()
166
- }
174
+ for (const buffer of dataArray) req.write(buffer)
175
+ req.end()
167
176
  })
168
177
  }
169
178
 
@@ -183,7 +192,7 @@ function byteLength (data) {
183
192
 
184
193
  Object.defineProperty(request, 'writable', {
185
194
  get () {
186
- return activeRequests < maxActiveRequests
195
+ return activeBufferSize < maxActiveBufferSize
187
196
  },
188
197
  })
189
198
 
@@ -37,13 +37,16 @@ const {
37
37
  const DEFAULT_KEY = 'service:,env:'
38
38
 
39
39
  /**
40
- * Formats a sampling rate as a string with up to 6 significant digits and no trailing zeros.
40
+ * Formats a sampling rate as a string with up to 6 decimal digits and no trailing zeros.
41
41
  *
42
42
  * @param {number} rate
43
- * @returns {string}
44
43
  */
45
44
  function formatKnuthRate (rate) {
46
- return Number(rate.toPrecision(6)).toString()
45
+ const string = Number(rate).toFixed(6)
46
+ for (let i = string.length - 1; i > 0; i--) {
47
+ if (string[i] === '0') continue
48
+ return string.slice(0, i + (string[i] === '.' ? 0 : 1))
49
+ }
47
50
  }
48
51
 
49
52
  const defaultSampler = new Sampler(AUTO_KEEP)
@@ -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) => {
@@ -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
  /**
@@ -91,12 +91,17 @@ let agentTelemetry = true
91
91
  * @returns {Record<string, string>}
92
92
  */
93
93
  function getHeaders (config, application, reqType) {
94
+ const sessionId = config.tags['runtime-id']
94
95
  const headers = {
95
96
  'content-type': 'application/json',
96
97
  'dd-telemetry-api-version': 'v2',
97
98
  'dd-telemetry-request-type': reqType,
98
99
  'dd-client-library-language': application.language_name,
99
100
  'dd-client-library-version': application.tracer_version,
101
+ 'dd-session-id': sessionId,
102
+ }
103
+ if (config.rootSessionId && config.rootSessionId !== sessionId) {
104
+ headers['dd-root-session-id'] = config.rootSessionId
100
105
  }
101
106
  const debug = config.telemetry && config.telemetry.debug
102
107
  if (debug) {
@@ -0,0 +1,78 @@
1
+ 'use strict'
2
+
3
+ const dc = require('dc-polyfill')
4
+
5
+ const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')
6
+
7
+ let subscribed = false
8
+ let rootSessionId
9
+ let runtimeId
10
+
11
+ function injectSessionEnv (existingEnv) {
12
+ // eslint-disable-next-line eslint-rules/eslint-process-env -- not in supported-configurations.json
13
+ const base = existingEnv == null ? process.env : existingEnv
14
+ return {
15
+ ...base,
16
+ DD_ROOT_JS_SESSION_ID: rootSessionId,
17
+ DD_PARENT_JS_SESSION_ID: runtimeId,
18
+ }
19
+ }
20
+
21
+ function findOptionsIndex (args, shell) {
22
+ if (Array.isArray(args[1])) {
23
+ return { index: 2, exists: args[2] != null && typeof args[2] === 'object' }
24
+ }
25
+ if (args[1] != null && typeof args[1] === 'object') {
26
+ return { index: 1, exists: true }
27
+ }
28
+ if (!shell && args[2] != null && typeof args[2] === 'object') {
29
+ return { index: 2, exists: true }
30
+ }
31
+ return { index: shell ? 1 : 2, exists: false }
32
+ }
33
+
34
+ function onChildProcessStart (context) {
35
+ if (!context.callArgs) return
36
+
37
+ const args = context.callArgs
38
+ const { index, exists } = findOptionsIndex(args, context.shell)
39
+
40
+ if (exists) {
41
+ args[index] = { ...args[index], env: injectSessionEnv(args[index].env) }
42
+ return
43
+ }
44
+
45
+ const opts = { env: injectSessionEnv(null) }
46
+
47
+ if (!context.shell && !Array.isArray(args[1])) {
48
+ args.splice(1, 0, [])
49
+ }
50
+
51
+ if (typeof args[index] === 'function') {
52
+ args.splice(index, 0, opts)
53
+ } else {
54
+ args[index] = opts
55
+ }
56
+ }
57
+
58
+ const handler = { start: onChildProcessStart }
59
+
60
+ function start (config) {
61
+ if (!config.telemetry?.enabled || subscribed) return
62
+ subscribed = true
63
+
64
+ rootSessionId = config.rootSessionId
65
+ runtimeId = config.tags['runtime-id']
66
+
67
+ childProcessChannel.subscribe(handler)
68
+ }
69
+
70
+ function stop () {
71
+ if (!subscribed) return
72
+ childProcessChannel.unsubscribe(handler)
73
+ subscribed = false
74
+ rootSessionId = undefined
75
+ runtimeId = undefined
76
+ }
77
+
78
+ module.exports = { start, stop, _onChildProcessStart: onChildProcessStart }
@@ -12,6 +12,7 @@ const endpoints = require('./endpoints')
12
12
  const { sendData } = require('./send-data')
13
13
  const { manager: metricsManager } = require('./metrics')
14
14
  const telemetryLogger = require('./logs')
15
+ const sessionPropagation = require('./session-propagation')
15
16
 
16
17
  /**
17
18
  * @typedef {Record<string, unknown>} TelemetryPayloadObject
@@ -370,6 +371,7 @@ function start (aConfig, thePluginManager) {
370
371
  dependencies.start(config, application, host, getRetryData, updateRetryData)
371
372
  telemetryLogger.start(config)
372
373
  endpoints.start(config, application, host, getRetryData, updateRetryData)
374
+ sessionPropagation.start(config)
373
375
 
374
376
  sendData(config, application, host, 'app-started', appStarted(config))
375
377
 
@@ -397,6 +399,7 @@ function stop () {
397
399
  telemetryStopChannel.publish(getTelemetryData())
398
400
 
399
401
  endpoints.stop()
402
+ sessionPropagation.stop()
400
403
  config = undefined
401
404
  }
402
405
 
@@ -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)