dd-trace 5.30.0 → 5.31.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.30.0",
3
+ "version": "5.31.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@datadog/libdatadog": "^0.3.0",
85
- "@datadog/native-appsec": "8.3.0",
85
+ "@datadog/native-appsec": "8.4.0",
86
86
  "@datadog/native-iast-rewriter": "2.6.1",
87
87
  "@datadog/native-iast-taint-tracking": "3.2.0",
88
88
  "@datadog/native-metrics": "^3.1.0",
@@ -145,7 +145,7 @@
145
145
  "jszip": "^3.5.0",
146
146
  "knex": "^2.4.2",
147
147
  "mkdirp": "^3.0.1",
148
- "mocha": "^9",
148
+ "mocha": "^10",
149
149
  "msgpack-lite": "^0.1.26",
150
150
  "multer": "^1.4.5-lts.1",
151
151
  "nock": "^11.3.3",
@@ -21,8 +21,16 @@ class DatadogStorage {
21
21
  this._storage.exit(callback, ...args)
22
22
  }
23
23
 
24
- getStore () {
25
- const handle = this._storage.getStore()
24
+ // TODO: Refactor the Scope class to use a span-only store and remove this.
25
+ getHandle () {
26
+ return this._storage.getStore()
27
+ }
28
+
29
+ getStore (handle) {
30
+ if (!handle) {
31
+ handle = this._storage.getStore()
32
+ }
33
+
26
34
  return stores.get(handle)
27
35
  }
28
36
 
@@ -50,6 +58,7 @@ const storage = function (namespace) {
50
58
  storage.disable = legacyStorage.disable.bind(legacyStorage)
51
59
  storage.enterWith = legacyStorage.enterWith.bind(legacyStorage)
52
60
  storage.exit = legacyStorage.exit.bind(legacyStorage)
61
+ storage.getHandle = legacyStorage.getHandle.bind(legacyStorage)
53
62
  storage.getStore = legacyStorage.getStore.bind(legacyStorage)
54
63
  storage.run = legacyStorage.run.bind(legacyStorage)
55
64
 
@@ -40,7 +40,7 @@ function wrapProcess (process) {
40
40
  addHook({
41
41
  name: 'aerospike',
42
42
  file: 'lib/commands/command.js',
43
- versions: ['4', '5']
43
+ versions: ['4', '5', '6']
44
44
  },
45
45
  commandFactory => {
46
46
  return shimmer.wrapFunction(commandFactory, f => wrapCreateCommand(f))
@@ -9,6 +9,7 @@ const log = require('../../log')
9
9
  const { getExtraServices } = require('../../service-naming/extra-services')
10
10
  const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states')
11
11
  const Scheduler = require('./scheduler')
12
+ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
12
13
 
13
14
  const clientId = uuid()
14
15
 
@@ -33,6 +34,14 @@ class RemoteConfigManager extends EventEmitter {
33
34
  port: config.port
34
35
  }))
35
36
 
37
+ const tags = config.repositoryUrl
38
+ ? {
39
+ ...config.tags,
40
+ [GIT_REPOSITORY_URL]: config.repositoryUrl,
41
+ [GIT_COMMIT_SHA]: config.commitSHA
42
+ }
43
+ : config.tags
44
+
36
45
  this._handlers = new Map()
37
46
  const appliedConfigs = this.appliedConfigs = new Map()
38
47
 
@@ -67,7 +76,8 @@ class RemoteConfigManager extends EventEmitter {
67
76
  service: config.service,
68
77
  env: config.env,
69
78
  app_version: config.version,
70
- extra_services: []
79
+ extra_services: [],
80
+ tags: Object.entries(tags).map((pair) => pair.join(':'))
71
81
  },
72
82
  capabilities: DEFAULT_CAPABILITY // updated by `updateCapabilities()`
73
83
  },
@@ -9,7 +9,8 @@ const config = module.exports = {
9
9
  service: parentConfig.service,
10
10
  commitSHA: parentConfig.commitSHA,
11
11
  repositoryUrl: parentConfig.repositoryUrl,
12
- parentThreadId
12
+ parentThreadId,
13
+ maxTotalPayloadSize: 5 * 1024 * 1024 // 5MB
13
14
  }
14
15
 
15
16
  updateUrl(parentConfig)
@@ -129,9 +129,8 @@ session.on('Debugger.paused', async ({ params }) => {
129
129
  }
130
130
 
131
131
  // TODO: Process template (DEBUG-2628)
132
- send(probe.template, logger, dd, snapshot, (err) => {
133
- if (err) log.error('Debugger error', err)
134
- else ackEmitting(probe)
132
+ send(probe.template, logger, dd, snapshot, () => {
133
+ ackEmitting(probe)
135
134
  })
136
135
  }
137
136
  })
@@ -141,6 +140,8 @@ function highestOrUndefined (num, max) {
141
140
  }
142
141
 
143
142
  async function getDD (callFrameId) {
143
+ // TODO: Consider if an `objectGroup` should be used, so it can be explicitly released using
144
+ // `Runtime.releaseObjectGroup`
144
145
  const { result } = await session.post('Debugger.evaluateOnCallFrame', {
145
146
  callFrameId,
146
147
  expression,
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ class JSONBuffer {
4
+ constructor ({ size, timeout, onFlush }) {
5
+ this._maxSize = size
6
+ this._timeout = timeout
7
+ this._onFlush = onFlush
8
+ this._reset()
9
+ }
10
+
11
+ _reset () {
12
+ clearTimeout(this._timer)
13
+ this._timer = null
14
+ this._partialJson = null
15
+ }
16
+
17
+ _flush () {
18
+ const json = `${this._partialJson}]`
19
+ this._reset()
20
+ this._onFlush(json)
21
+ }
22
+
23
+ write (str, size = Buffer.byteLength(str)) {
24
+ if (this._timer === null) {
25
+ this._partialJson = `[${str}`
26
+ this._timer = setTimeout(() => this._flush(), this._timeout)
27
+ } else if (Buffer.byteLength(this._partialJson) + size + 2 > this._maxSize) {
28
+ this._flush()
29
+ this.write(str, size)
30
+ } else {
31
+ this._partialJson += `,${str}`
32
+ }
33
+ }
34
+ }
35
+
36
+ module.exports = JSONBuffer
@@ -4,32 +4,35 @@ const { hostname: getHostname } = require('os')
4
4
  const { stringify } = require('querystring')
5
5
 
6
6
  const config = require('./config')
7
+ const JSONBuffer = require('./json-buffer')
7
8
  const request = require('../../exporters/common/request')
8
9
  const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('../../plugins/util/tags')
10
+ const log = require('../../log')
11
+ const { version } = require('../../../../../package.json')
9
12
 
10
13
  module.exports = send
11
14
 
12
- const MAX_PAYLOAD_SIZE = 1024 * 1024 // 1MB
15
+ const MAX_LOG_PAYLOAD_SIZE = 1024 * 1024 // 1MB
13
16
 
14
17
  const ddsource = 'dd_debugger'
15
18
  const hostname = getHostname()
16
19
  const service = config.service
17
20
 
18
21
  const ddtags = [
22
+ ['env', process.env.DD_ENV],
23
+ ['version', process.env.DD_VERSION],
24
+ ['debugger_version', version],
25
+ ['host_name', hostname],
19
26
  [GIT_COMMIT_SHA, config.commitSHA],
20
27
  [GIT_REPOSITORY_URL, config.repositoryUrl]
21
28
  ].map((pair) => pair.join(':')).join(',')
22
29
 
23
30
  const path = `/debugger/v1/input?${stringify({ ddtags })}`
24
31
 
25
- function send (message, logger, dd, snapshot, cb) {
26
- const opts = {
27
- method: 'POST',
28
- url: config.url,
29
- path,
30
- headers: { 'Content-Type': 'application/json; charset=utf-8' }
31
- }
32
+ let callbacks = []
33
+ const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
32
34
 
35
+ function send (message, logger, dd, snapshot, cb) {
33
36
  const payload = {
34
37
  ddsource,
35
38
  hostname,
@@ -41,8 +44,9 @@ function send (message, logger, dd, snapshot, cb) {
41
44
  }
42
45
 
43
46
  let json = JSON.stringify(payload)
47
+ let size = Buffer.byteLength(json)
44
48
 
45
- if (Buffer.byteLength(json) > MAX_PAYLOAD_SIZE) {
49
+ if (size > MAX_LOG_PAYLOAD_SIZE) {
46
50
  // TODO: This is a very crude way to handle large payloads. Proper pruning will be implemented later (DEBUG-2624)
47
51
  const line = Object.values(payload['debugger.snapshot'].captures.lines)[0]
48
52
  line.locals = {
@@ -50,7 +54,26 @@ function send (message, logger, dd, snapshot, cb) {
50
54
  size: Object.keys(line.locals).length
51
55
  }
52
56
  json = JSON.stringify(payload)
57
+ size = Buffer.byteLength(json)
58
+ }
59
+
60
+ jsonBuffer.write(json, size)
61
+ callbacks.push(cb)
62
+ }
63
+
64
+ function onFlush (payload) {
65
+ const opts = {
66
+ method: 'POST',
67
+ url: config.url,
68
+ path,
69
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
53
70
  }
54
71
 
55
- request(json, opts, cb)
72
+ const _callbacks = callbacks
73
+ callbacks = []
74
+
75
+ request(payload, opts, (err) => {
76
+ if (err) log.error('Could not send debugger payload', err)
77
+ else _callbacks.forEach(cb => cb())
78
+ })
56
79
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const LRUCache = require('lru-cache')
4
4
  const config = require('./config')
5
+ const JSONBuffer = require('./json-buffer')
5
6
  const request = require('../../exporters/common/request')
6
7
  const FormData = require('../../exporters/common/form-data')
7
8
  const log = require('../../log')
@@ -25,6 +26,8 @@ const cache = new LRUCache({
25
26
  ttlAutopurge: true
26
27
  })
27
28
 
29
+ const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
30
+
28
31
  const STATUSES = {
29
32
  RECEIVED: 'RECEIVED',
30
33
  INSTALLED: 'INSTALLED',
@@ -71,11 +74,15 @@ function ackError (err, { id: probeId, version }) {
71
74
  }
72
75
 
73
76
  function send (payload) {
77
+ jsonBuffer.write(JSON.stringify(payload))
78
+ }
79
+
80
+ function onFlush (payload) {
74
81
  const form = new FormData()
75
82
 
76
83
  form.append(
77
84
  'event',
78
- JSON.stringify(payload),
85
+ payload,
79
86
  { filename: 'event.json', contentType: 'application/json; charset=utf-8' }
80
87
  )
81
88
 
@@ -87,7 +94,7 @@ function send (payload) {
87
94
  }
88
95
 
89
96
  request(form, options, (err) => {
90
- if (err) log.error('[debugger:devtools_client] Error sending debugger payload', err)
97
+ if (err) log.error('[debugger:devtools_client] Error sending probe payload', err)
91
98
  })
92
99
  }
93
100
 
@@ -1,12 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { SPAN_KIND, OUTPUT_VALUE } = require('./constants/tags')
3
+ const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags')
4
4
 
5
5
  const {
6
6
  getFunctionArguments,
7
7
  validateKind
8
8
  } = require('./util')
9
- const { isTrue } = require('../util')
9
+ const { isTrue, isError } = require('../util')
10
10
 
11
11
  const { storage } = require('./storage')
12
12
 
@@ -134,29 +134,63 @@ class LLMObs extends NoopLLMObs {
134
134
 
135
135
  function wrapped () {
136
136
  const span = llmobs._tracer.scope().active()
137
-
138
- const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => {
139
- if (!['llm', 'embedding'].includes(kind)) {
140
- llmobs.annotate(span, { inputData: getFunctionArguments(fn, arguments) })
137
+ const fnArgs = arguments
138
+
139
+ const lastArgId = fnArgs.length - 1
140
+ const cb = fnArgs[lastArgId]
141
+ const hasCallback = typeof cb === 'function'
142
+
143
+ if (hasCallback) {
144
+ const scopeBoundCb = llmobs._bind(cb)
145
+ fnArgs[lastArgId] = function () {
146
+ // it is standard practice to follow the callback signature (err, result)
147
+ // however, we try to parse the arguments to determine if the first argument is an error
148
+ // if it is not, and is not undefined, we will use that for the output value
149
+ const maybeError = arguments[0]
150
+ const maybeResult = arguments[1]
151
+
152
+ llmobs._autoAnnotate(
153
+ span,
154
+ kind,
155
+ getFunctionArguments(fn, fnArgs),
156
+ isError(maybeError) || maybeError == null ? maybeResult : maybeError
157
+ )
158
+
159
+ return scopeBoundCb.apply(this, arguments)
141
160
  }
161
+ }
142
162
 
143
- return fn.apply(this, arguments)
144
- })
163
+ try {
164
+ const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => fn.apply(this, fnArgs))
165
+
166
+ if (result && typeof result.then === 'function') {
167
+ return result.then(
168
+ value => {
169
+ if (!hasCallback) {
170
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), value)
171
+ }
172
+ return value
173
+ },
174
+ err => {
175
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
176
+ throw err
177
+ }
178
+ )
179
+ }
145
180
 
146
- if (result && typeof result.then === 'function') {
147
- return result.then(value => {
148
- if (value && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
149
- llmobs.annotate(span, { outputData: value })
150
- }
151
- return value
152
- })
153
- }
181
+ // it is possible to return a value and have a callback
182
+ // however, since the span finishes when the callback is called, it is possible that
183
+ // the callback is called before the function returns (although unlikely)
184
+ // we do not want to throw for "annotating a finished span" in this case
185
+ if (!hasCallback) {
186
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), result)
187
+ }
154
188
 
155
- if (result && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
156
- llmobs.annotate(span, { outputData: result })
189
+ return result
190
+ } catch (e) {
191
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
192
+ throw e
157
193
  }
158
-
159
- return result
160
194
  }
161
195
 
162
196
  return this._tracer.wrap(name, spanOptions, wrapped)
@@ -333,20 +367,34 @@ class LLMObs extends NoopLLMObs {
333
367
  flushCh.publish()
334
368
  }
335
369
 
370
+ _autoAnnotate (span, kind, input, output) {
371
+ const annotations = {}
372
+ if (input && !['llm', 'embedding'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[INPUT_VALUE]) {
373
+ annotations.inputData = input
374
+ }
375
+
376
+ if (output && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
377
+ annotations.outputData = output
378
+ }
379
+
380
+ this.annotate(span, annotations)
381
+ }
382
+
336
383
  _active () {
337
384
  const store = storage.getStore()
338
385
  return store?.span
339
386
  }
340
387
 
341
- _activate (span, { kind, options } = {}, fn) {
388
+ _activate (span, options, fn) {
342
389
  const parent = this._active()
343
390
  if (this.enabled) storage.enterWith({ span })
344
391
 
345
- this._tagger.registerLLMObsSpan(span, {
346
- ...options,
347
- parent,
348
- kind
349
- })
392
+ if (options) {
393
+ this._tagger.registerLLMObsSpan(span, {
394
+ ...options,
395
+ parent
396
+ })
397
+ }
350
398
 
351
399
  try {
352
400
  return fn()
@@ -355,6 +403,22 @@ class LLMObs extends NoopLLMObs {
355
403
  }
356
404
  }
357
405
 
406
+ // bind function to active LLMObs span
407
+ _bind (fn) {
408
+ if (typeof fn !== 'function') return fn
409
+
410
+ const llmobs = this
411
+ const activeSpan = llmobs._active()
412
+
413
+ const bound = function () {
414
+ return llmobs._activate(activeSpan, null, () => {
415
+ return fn.apply(this, arguments)
416
+ })
417
+ }
418
+
419
+ return bound
420
+ }
421
+
358
422
  _extractOptions (options) {
359
423
  const {
360
424
  modelName,
@@ -63,15 +63,14 @@ const log = {
63
63
 
64
64
  Error.captureStackTrace(logRecord, this.trace)
65
65
 
66
- const fn = logRecord.stack.split('\n')[1].replace(/^\s+at ([^\s]+) .+/, '$1')
67
- const params = args.map(a => {
68
- return a && a.hasOwnProperty('toString') && typeof a.toString === 'function'
69
- ? a.toString()
70
- : inspect(a, { depth: 3, breakLength: Infinity, compact: true })
71
- }).join(', ')
72
- const formatted = logRecord.stack.replace('Error: ', `Trace: ${fn}(${params})`)
73
-
74
- traceChannel.publish(Log.parse(formatted))
66
+ const stack = logRecord.stack.split('\n')
67
+ const fn = stack[1].replace(/^\s+at ([^\s]+) .+/, '$1')
68
+ const options = { depth: 2, breakLength: Infinity, compact: true, maxArrayLength: Infinity }
69
+ const params = args.map(a => inspect(a, options)).join(', ')
70
+
71
+ stack[0] = `Trace: ${fn}(${params})`
72
+
73
+ traceChannel.publish(Log.parse(stack.join('\n')))
75
74
  }
76
75
  return this
77
76
  },
@@ -10,7 +10,7 @@ const noopAppsec = new NoopAppsecSdk()
10
10
  const noopDogStatsDClient = new NoopDogStatsDClient()
11
11
  const noopLLMObs = new NoopLLMObsSDK(noop)
12
12
 
13
- class Tracer {
13
+ class NoopProxy {
14
14
  constructor () {
15
15
  this._tracer = noop
16
16
  this.appsec = noopAppsec
@@ -91,4 +91,4 @@ class Tracer {
91
91
  }
92
92
  }
93
93
 
94
- module.exports = Tracer
94
+ module.exports = NoopProxy
@@ -6,7 +6,7 @@ const { storage } = require('../../../datadog-core') // TODO: noop storage?
6
6
 
7
7
  class NoopSpan {
8
8
  constructor (tracer, parent) {
9
- this._store = storage.getStore()
9
+ this._store = storage.getHandle()
10
10
  this._noopTracer = tracer
11
11
  this._noopContext = this._createContext(parent)
12
12
  }
@@ -14,6 +14,7 @@ const { storage } = require('../../../datadog-core')
14
14
  const telemetryMetrics = require('../telemetry/metrics')
15
15
  const { channel } = require('dc-polyfill')
16
16
  const spanleak = require('../spanleak')
17
+ const util = require('util')
17
18
 
18
19
  const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
19
20
 
@@ -64,7 +65,7 @@ class DatadogSpan {
64
65
  this._debug = debug
65
66
  this._processor = processor
66
67
  this._prioritySampler = prioritySampler
67
- this._store = storage.getStore()
68
+ this._store = storage.getHandle()
68
69
  this._duration = undefined
69
70
 
70
71
  this._events = []
@@ -105,6 +106,15 @@ class DatadogSpan {
105
106
  }
106
107
  }
107
108
 
109
+ [util.inspect.custom] () {
110
+ return {
111
+ ...this,
112
+ _parentTracer: `[${this._parentTracer.constructor.name}]`,
113
+ _prioritySampler: `[${this._prioritySampler.constructor.name}]`,
114
+ _processor: `[${this._processor.constructor.name}]`
115
+ }
116
+ }
117
+
108
118
  toString () {
109
119
  const spanContext = this.context()
110
120
  const resourceName = spanContext._tags['resource.name'] || ''
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const util = require('util')
3
4
  const { AUTO_KEEP } = require('../../../../ext/priority')
4
5
 
5
6
  // the lowercase, hex encoded upper 64 bits of a 128-bit trace id, if present
@@ -31,6 +32,17 @@ class DatadogSpanContext {
31
32
  this._otelSpanContext = undefined
32
33
  }
33
34
 
35
+ [util.inspect.custom] () {
36
+ return {
37
+ ...this,
38
+ _trace: {
39
+ ...this._trace,
40
+ started: '[Array]',
41
+ finished: '[Array]'
42
+ }
43
+ }
44
+ }
45
+
34
46
  toTraceId (get128bitId = false) {
35
47
  if (get128bitId) {
36
48
  return this._traceId.toBuffer().length <= 8 && this._trace.tags[TRACE_ID_128]
@@ -120,13 +120,15 @@ class PrioritySampler {
120
120
  if (!span || !this.validate(samplingPriority)) return
121
121
 
122
122
  const context = this._getContext(span)
123
+ const root = context._trace.started[0]
124
+
125
+ if (!root) return // noop span
123
126
 
124
127
  context._sampling.priority = samplingPriority
125
128
  context._sampling.mechanism = mechanism
126
129
 
127
- const root = context._trace.started[0]
128
-
129
130
  log.trace(span, samplingPriority, mechanism)
131
+
130
132
  this._addDecisionMaker(root)
131
133
  }
132
134
 
@@ -17,7 +17,7 @@ class Scope {
17
17
  if (typeof callback !== 'function') return callback
18
18
 
19
19
  const oldStore = storage.getStore()
20
- const newStore = span ? span._store : oldStore
20
+ const newStore = span ? storage.getStore(span._store) : oldStore
21
21
 
22
22
  storage.enterWith({ ...newStore, span })
23
23
 
@@ -307,6 +307,8 @@ function updateConfig (changes, config) {
307
307
  if (!config.telemetry.enabled) return
308
308
  if (changes.length === 0) return
309
309
 
310
+ logger.trace(changes)
311
+
310
312
  const application = createAppObject(config)
311
313
  const host = createHostObject()
312
314