dd-trace 4.19.0 → 4.21.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 (62) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/index.d.ts +24 -0
  3. package/package.json +6 -5
  4. package/packages/datadog-instrumentations/src/aerospike.js +47 -0
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/http/client.js +22 -0
  7. package/packages/datadog-instrumentations/src/jest.js +11 -5
  8. package/packages/datadog-instrumentations/src/kafkajs.js +27 -0
  9. package/packages/datadog-instrumentations/src/next.js +3 -1
  10. package/packages/datadog-instrumentations/src/restify.js +1 -1
  11. package/packages/datadog-plugin-aerospike/src/index.js +113 -0
  12. package/packages/datadog-plugin-http/src/client.js +19 -2
  13. package/packages/datadog-plugin-kafkajs/src/consumer.js +51 -0
  14. package/packages/datadog-plugin-kafkajs/src/producer.js +55 -0
  15. package/packages/datadog-plugin-next/src/index.js +7 -7
  16. package/packages/dd-trace/src/appsec/addresses.js +2 -1
  17. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  18. package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +105 -0
  19. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/constants.js +7 -0
  20. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +12 -19
  21. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/header-sensitive-analyzer.js +20 -0
  22. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +6 -10
  23. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +18 -25
  24. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +79 -85
  25. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +27 -36
  26. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +14 -11
  27. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  28. package/packages/dd-trace/src/appsec/index.js +14 -2
  29. package/packages/dd-trace/src/appsec/recommended.json +1395 -2
  30. package/packages/dd-trace/src/appsec/reporter.js +19 -0
  31. package/packages/dd-trace/src/appsec/rule_manager.js +9 -6
  32. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +6 -3
  33. package/packages/dd-trace/src/appsec/waf/waf_manager.js +0 -1
  34. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +17 -1
  35. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +75 -56
  36. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +15 -9
  37. package/packages/dd-trace/src/config.js +37 -3
  38. package/packages/dd-trace/src/datastreams/processor.js +107 -12
  39. package/packages/dd-trace/src/id.js +12 -0
  40. package/packages/dd-trace/src/noop/proxy.js +4 -0
  41. package/packages/dd-trace/src/opentracing/propagation/text_map.js +14 -5
  42. package/packages/dd-trace/src/opentracing/span.js +2 -0
  43. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -1
  44. package/packages/dd-trace/src/plugins/index.js +1 -0
  45. package/packages/dd-trace/src/plugins/util/git.js +2 -2
  46. package/packages/dd-trace/src/plugins/util/test.js +3 -2
  47. package/packages/dd-trace/src/profiler.js +5 -3
  48. package/packages/dd-trace/src/profiling/config.js +17 -10
  49. package/packages/dd-trace/src/profiling/profiler.js +10 -4
  50. package/packages/dd-trace/src/profiling/profilers/events.js +171 -73
  51. package/packages/dd-trace/src/profiling/profilers/wall.js +93 -67
  52. package/packages/dd-trace/src/proxy.js +21 -1
  53. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +5 -0
  54. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +4 -0
  55. package/packages/dd-trace/src/spanleak.js +98 -0
  56. package/packages/dd-trace/src/startup-log.js +7 -1
  57. package/packages/dd-trace/src/telemetry/dependencies.js +55 -9
  58. package/packages/dd-trace/src/telemetry/index.js +135 -43
  59. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  60. package/packages/dd-trace/src/telemetry/send-data.js +47 -5
  61. package/packages/dd-trace/src/tracer.js +4 -0
  62. package/scripts/install_plugin_modules.js +11 -3
@@ -15,7 +15,7 @@ const spanFinishCh = dc.channel('dd-trace:span:finish')
15
15
  const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
16
16
  const threadName = `${threadNamePrefix} Event Loop`
17
17
 
18
- const CachedWebTags = Symbol('NativeWallProfiler.CachedWebTags')
18
+ const MemoizedWebTags = Symbol('NativeWallProfiler.MemoizedWebTags')
19
19
 
20
20
  let kSampleCount
21
21
 
@@ -28,28 +28,6 @@ function getStartedSpans (context) {
28
28
  return context._trace.started
29
29
  }
30
30
 
31
- function generateLabels ({ context: { spanId, rootSpanId, webTags, endpoint }, timestamp }) {
32
- const labels = {
33
- [THREAD_NAME]: threadName,
34
- // Incoming timestamps are in microseconds, we emit nanos.
35
- [END_TIMESTAMP]: timestamp * 1000n
36
- }
37
- if (spanId) {
38
- labels['span id'] = spanId
39
- }
40
- if (rootSpanId) {
41
- labels['local root span id'] = rootSpanId
42
- }
43
- if (webTags && Object.keys(webTags).length !== 0) {
44
- labels['trace endpoint'] = endpointNameFromTags(webTags)
45
- } else if (endpoint) {
46
- // fallback to endpoint computed when sample was taken
47
- labels['trace endpoint'] = endpoint
48
- }
49
-
50
- return labels
51
- }
52
-
53
31
  function isWebServerSpan (tags) {
54
32
  return tags[SPAN_TYPE] === WEB
55
33
  }
@@ -61,6 +39,38 @@ function endpointNameFromTags (tags) {
61
39
  ].filter(v => v).join(' ')
62
40
  }
63
41
 
42
+ function getWebTags (startedSpans, i, span) {
43
+ // Are web tags for this span already memoized?
44
+ const memoizedWebTags = span[MemoizedWebTags]
45
+ if (memoizedWebTags !== undefined) {
46
+ return memoizedWebTags
47
+ }
48
+ // No, we'll have to memoize a new value
49
+ function memoize (tags) {
50
+ span[MemoizedWebTags] = tags
51
+ return tags
52
+ }
53
+ // Is this span itself a web span?
54
+ const context = span.context()
55
+ const tags = context._tags
56
+ if (isWebServerSpan(tags)) {
57
+ return memoize(tags)
58
+ }
59
+ // It isn't. Get parent's web tags (memoize them too recursively.)
60
+ // There might be several webspans, for example with next.js, http plugin creates the first span
61
+ // and then next.js plugin creates a child span, and this child span has the correct endpoint
62
+ // information. That's why we always use the tags of the closest ancestor web span.
63
+ const parentId = context._parentId
64
+ while (--i >= 0) {
65
+ const ispan = startedSpans[i]
66
+ if (ispan.context()._spanId === parentId) {
67
+ return memoize(getWebTags(startedSpans, i, ispan))
68
+ }
69
+ }
70
+ // Local root span with no web span
71
+ return memoize(null)
72
+ }
73
+
64
74
  class NativeWallProfiler {
65
75
  constructor (options = {}) {
66
76
  this.type = 'wall'
@@ -68,14 +78,30 @@ class NativeWallProfiler {
68
78
  this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
69
79
  this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
70
80
  this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
71
- this._withContexts = this._codeHotspotsEnabled || this._endpointCollectionEnabled
81
+ this._timelineEnabled = !!options.timelineEnabled
82
+ // We need to capture span data into the sample context for either code hotspots
83
+ // or endpoint collection.
84
+ this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
85
+ // We need to run the pprof wall profiler with sample contexts if we're either
86
+ // capturing span data or timeline is enabled (so we need sample timestamps, and for now
87
+ // timestamps require the sample contexts feature in the pprof wall profiler.)
88
+ this._withContexts = this._captureSpanData || this._timelineEnabled
72
89
  this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
73
90
  this._mapper = undefined
74
91
  this._pprof = undefined
75
92
 
76
- // Bind to this so the same value can be used to unsubscribe later
77
- this._enter = this._enter.bind(this)
78
- this._spanFinished = this._spanFinished.bind(this)
93
+ // Bind these to this so they can be used as callbacks
94
+ if (this._withContexts) {
95
+ if (this._captureSpanData) {
96
+ this._enter = this._enter.bind(this)
97
+ this._spanFinished = this._spanFinished.bind(this)
98
+ }
99
+ this._generateLabels = this._generateLabels.bind(this)
100
+ } else {
101
+ // Explicitly assigning, to express the intent that this is meant to be
102
+ // undefined when passed to pprof.time.stop() when not using sample contexts.
103
+ this._generateLabels = undefined
104
+ }
79
105
  this._logger = options.logger
80
106
  this._started = false
81
107
  }
@@ -113,17 +139,20 @@ class NativeWallProfiler {
113
139
  })
114
140
 
115
141
  if (this._withContexts) {
116
- this._profilerState = this._pprof.time.getState()
117
142
  this._currentContext = {}
118
143
  this._pprof.time.setContext(this._currentContext)
119
- this._lastSpan = undefined
120
- this._lastStartedSpans = undefined
121
- this._lastWebTags = undefined
122
- this._lastSampleCount = 0
123
144
 
124
- beforeCh.subscribe(this._enter)
125
- enterCh.subscribe(this._enter)
126
- spanFinishCh.subscribe(this._spanFinished)
145
+ if (this._captureSpanData) {
146
+ this._profilerState = this._pprof.time.getState()
147
+ this._lastSpan = undefined
148
+ this._lastStartedSpans = undefined
149
+ this._lastWebTags = undefined
150
+ this._lastSampleCount = 0
151
+
152
+ beforeCh.subscribe(this._enter)
153
+ enterCh.subscribe(this._enter)
154
+ spanFinishCh.subscribe(this._spanFinished)
155
+ }
127
156
  }
128
157
 
129
158
  this._started = true
@@ -149,33 +178,7 @@ class NativeWallProfiler {
149
178
  const startedSpans = getStartedSpans(context)
150
179
  this._lastStartedSpans = startedSpans
151
180
  if (this._endpointCollectionEnabled) {
152
- const cachedWebTags = span[CachedWebTags]
153
- if (cachedWebTags === undefined) {
154
- let found = false
155
- // Find the first webspan starting from the end:
156
- // There might be several webspans, for example with next.js, http plugin creates a first span
157
- // and then next.js plugin creates a child span, and this child span has the correct endpoint information.
158
- let nextSpanId = context._spanId
159
- for (let i = startedSpans.length - 1; i >= 0; i--) {
160
- const nextContext = startedSpans[i].context()
161
- if (nextContext._spanId === nextSpanId) {
162
- const tags = nextContext._tags
163
- if (isWebServerSpan(tags)) {
164
- this._lastWebTags = tags
165
- span[CachedWebTags] = tags
166
- found = true
167
- break
168
- }
169
- nextSpanId = nextContext._parentId
170
- }
171
- }
172
- if (!found) {
173
- this._lastWebTags = undefined
174
- span[CachedWebTags] = null // cache negative lookup result
175
- }
176
- } else {
177
- this._lastWebTags = cachedWebTags
178
- }
181
+ this._lastWebTags = getWebTags(startedSpans, startedSpans.length, span)
179
182
  }
180
183
  } else {
181
184
  this._lastStartedSpans = undefined
@@ -204,8 +207,8 @@ class NativeWallProfiler {
204
207
  }
205
208
 
206
209
  _spanFinished (span) {
207
- if (span[CachedWebTags]) {
208
- span[CachedWebTags] = undefined
210
+ if (span[MemoizedWebTags]) {
211
+ span[MemoizedWebTags] = undefined
209
212
  }
210
213
  }
211
214
 
@@ -221,12 +224,12 @@ class NativeWallProfiler {
221
224
 
222
225
  _stop (restart) {
223
226
  if (!this._started) return
224
- if (this._withContexts) {
227
+ if (this._captureSpanData) {
225
228
  // update last sample context if needed
226
229
  this._enter()
227
230
  this._lastSampleCount = 0
228
231
  }
229
- const profile = this._pprof.time.stop(restart, this._withContexts ? generateLabels : undefined)
232
+ const profile = this._pprof.time.stop(restart, this._generateLabels)
230
233
  if (restart) {
231
234
  const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
232
235
  if (v8BugDetected !== 0) {
@@ -236,6 +239,29 @@ class NativeWallProfiler {
236
239
  return profile
237
240
  }
238
241
 
242
+ _generateLabels ({ context: { spanId, rootSpanId, webTags, endpoint }, timestamp }) {
243
+ const labels = this._timelineEnabled ? {
244
+ [THREAD_NAME]: threadName,
245
+ // Incoming timestamps are in microseconds, we emit nanos.
246
+ [END_TIMESTAMP]: timestamp * 1000n
247
+ } : {}
248
+
249
+ if (spanId) {
250
+ labels['span id'] = spanId
251
+ }
252
+ if (rootSpanId) {
253
+ labels['local root span id'] = rootSpanId
254
+ }
255
+ if (webTags && Object.keys(webTags).length !== 0) {
256
+ labels['trace endpoint'] = endpointNameFromTags(webTags)
257
+ } else if (endpoint) {
258
+ // fallback to endpoint computed when sample was taken
259
+ labels['trace endpoint'] = endpoint
260
+ }
261
+
262
+ return labels
263
+ }
264
+
239
265
  profile () {
240
266
  return this._stop(true)
241
267
  }
@@ -248,7 +274,7 @@ class NativeWallProfiler {
248
274
  if (!this._started) return
249
275
 
250
276
  const profile = this._stop(false)
251
- if (this._withContexts) {
277
+ if (this._captureSpanData) {
252
278
  beforeCh.unsubscribe(this._enter)
253
279
  enterCh.unsubscribe(this._enter)
254
280
  spanFinishCh.unsubscribe(this._spanFinished)
@@ -10,6 +10,7 @@ const PluginManager = require('./plugin_manager')
10
10
  const remoteConfig = require('./appsec/remote_config')
11
11
  const AppsecSdk = require('./appsec/sdk')
12
12
  const dogstatsd = require('./dogstatsd')
13
+ const spanleak = require('./spanleak')
13
14
 
14
15
  class Tracer extends NoopProxy {
15
16
  constructor () {
@@ -37,6 +38,15 @@ class Tracer extends NoopProxy {
37
38
  }, 10 * 1000).unref()
38
39
  }
39
40
 
41
+ if (config.spanLeakDebug > 0) {
42
+ if (config.spanLeakDebug === spanleak.MODES.LOG) {
43
+ spanleak.enableLogging()
44
+ } else if (config.spanLeakDebug === spanleak.MODES.GC_AND_LOG) {
45
+ spanleak.enableGarbageCollection()
46
+ }
47
+ spanleak.startScrubber()
48
+ }
49
+
40
50
  if (config.remoteConfig.enabled && !config.isCiVisibility) {
41
51
  const rc = remoteConfig.enable(config)
42
52
 
@@ -62,11 +72,14 @@ class Tracer extends NoopProxy {
62
72
  // do not stop tracer initialization if the profiler fails to be imported
63
73
  try {
64
74
  const profiler = require('./profiler')
65
- profiler.start(config)
75
+ this._profilerStarted = profiler.start(config)
66
76
  } catch (e) {
67
77
  log.error(e)
68
78
  }
69
79
  }
80
+ if (!this._profilerStarted) {
81
+ this._profilerStarted = Promise.resolve(false)
82
+ }
70
83
 
71
84
  if (config.runtimeMetrics) {
72
85
  runtimeMetrics.start(config)
@@ -104,6 +117,13 @@ class Tracer extends NoopProxy {
104
117
  return this
105
118
  }
106
119
 
120
+ profilerStarted () {
121
+ if (!this._profilerStarted) {
122
+ throw new Error('profilerStarted() must be called after init()')
123
+ }
124
+ return this._profilerStarted
125
+ }
126
+
107
127
  use () {
108
128
  this._pluginManager.configurePlugin(...arguments)
109
129
  return this
@@ -37,6 +37,11 @@ const redisConfig = {
37
37
 
38
38
  const storage = {
39
39
  client: {
40
+ aerospike: {
41
+ opName: () => 'aerospike.command',
42
+ serviceName: ({ tracerService, pluginConfig }) =>
43
+ pluginConfig.service || `${tracerService}-aerospike`
44
+ },
40
45
  'cassandra-driver': {
41
46
  opName: () => 'cassandra.query',
42
47
  serviceName: ({ tracerService, pluginConfig, system }) =>
@@ -22,6 +22,10 @@ function withFunction ({ tracerService, pluginConfig, params }) {
22
22
 
23
23
  const storage = {
24
24
  client: {
25
+ aerospike: {
26
+ opName: () => 'aerospike.command',
27
+ serviceName: configWithFallback
28
+ },
25
29
  'cassandra-driver': {
26
30
  opName: () => 'cassandra.query',
27
31
  serviceName: configWithFallback
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable no-console */
4
+
5
+ const SortedSet = require('tlhunter-sorted-set')
6
+
7
+ const INTERVAL = 1000 // look for expired spans every 1s
8
+ const LIFETIME = 60 * 1000 // all spans have a max lifetime of 1m
9
+
10
+ const MODES = {
11
+ DISABLED: 0,
12
+ // METRICS_ONLY
13
+ LOG: 1,
14
+ GC_AND_LOG: 2
15
+ // GC
16
+ }
17
+
18
+ module.exports.MODES = MODES
19
+
20
+ const spans = new SortedSet()
21
+
22
+ // TODO: should these also be delivered as runtime metrics?
23
+
24
+ // const registry = new FinalizationRegistry(name => {
25
+ // spans.del(span) // there is no span
26
+ // })
27
+
28
+ let interval
29
+ let mode = MODES.DISABLED
30
+
31
+ module.exports.disable = function () {
32
+ mode = MODES.DISABLED
33
+ }
34
+
35
+ module.exports.enableLogging = function () {
36
+ mode = MODES.LOG
37
+ }
38
+
39
+ module.exports.enableGarbageCollection = function () {
40
+ mode = MODES.GC_AND_LOG
41
+ }
42
+
43
+ module.exports.startScrubber = function () {
44
+ if (!isEnabled()) return
45
+
46
+ interval = setInterval(() => {
47
+ const now = Date.now()
48
+ const expired = spans.rangeByScore(0, now)
49
+
50
+ if (!expired.length) return
51
+
52
+ const gc = isGarbageCollecting()
53
+
54
+ const expirationsByType = Object.create(null) // { [spanType]: count }
55
+
56
+ for (const wrapped of expired) {
57
+ spans.del(wrapped)
58
+ const span = wrapped.deref()
59
+
60
+ if (!span) continue // span has already been garbage collected
61
+
62
+ // TODO: Should we also do things like record the route to help users debug leaks?
63
+ if (!expirationsByType[span._name]) expirationsByType[span._name] = 0
64
+ expirationsByType[span._name]++
65
+
66
+ if (!gc) continue // everything after this point is related to manual GC
67
+
68
+ // TODO: what else can we do to alleviate memory usage
69
+ span.context()._tags = Object.create(null)
70
+ }
71
+
72
+ console.log('expired spans:' +
73
+ Object.keys(expirationsByType).reduce((a, c) => `${a} ${c}: ${expirationsByType[c]}`, ''))
74
+ }, INTERVAL)
75
+ }
76
+
77
+ module.exports.stopScrubber = function () {
78
+ clearInterval(interval)
79
+ }
80
+
81
+ module.exports.addSpan = function (span) {
82
+ if (!isEnabled()) return
83
+
84
+ const now = Date.now()
85
+ const expiration = now + LIFETIME
86
+ // eslint-disable-next-line no-undef
87
+ const wrapped = new WeakRef(span)
88
+ spans.add(wrapped, expiration)
89
+ // registry.register(span, span._name)
90
+ }
91
+
92
+ function isEnabled () {
93
+ return mode > MODES.DISABLED
94
+ }
95
+
96
+ function isGarbageCollecting () {
97
+ return mode >= MODES.GC_AND_LOG
98
+ }
@@ -6,6 +6,7 @@ const os = require('os')
6
6
  const { inspect } = require('util')
7
7
  const tracerVersion = require('../../../package.json').version
8
8
 
9
+ const errors = {}
9
10
  let config
10
11
  let pluginManager
11
12
  let samplingRules = []
@@ -89,6 +90,10 @@ function startupLog ({ agentError } = {}) {
89
90
  info('DATADOG TRACER CONFIGURATION - ' + out)
90
91
  if (agentError) {
91
92
  warn('DATADOG TRACER DIAGNOSTIC - Agent Error: ' + agentError.message)
93
+ errors.agentError = {
94
+ code: agentError.code ? agentError.code : '',
95
+ message: `Agent Error:${agentError.message}`
96
+ }
92
97
  }
93
98
 
94
99
  config = undefined
@@ -112,5 +117,6 @@ module.exports = {
112
117
  startupLog,
113
118
  setStartupLogConfig,
114
119
  setStartupLogPluginManager,
115
- setSamplingRules
120
+ setSamplingRules,
121
+ errors
116
122
  }
@@ -6,6 +6,7 @@ const requirePackageJson = require('../require-package-json')
6
6
  const { sendData } = require('./send-data')
7
7
  const dc = require('dc-polyfill')
8
8
  const { fileURLToPath } = require('url')
9
+ const { isTrue } = require('../../src/util')
9
10
 
10
11
  const savedDependenciesToSend = new Set()
11
12
  const detectedDependencyKeys = new Set()
@@ -14,20 +15,57 @@ const detectedDependencyVersions = new Set()
14
15
  const FILE_URI_START = `file://`
15
16
  const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart')
16
17
 
17
- let immediate, config, application, host
18
+ let immediate, config, application, host, initialLoad
18
19
  let isFirstModule = true
20
+ let getRetryData
21
+ let updateRetryData
19
22
 
23
+ function createBatchPayload (payload) {
24
+ const batchPayload = []
25
+ payload.map(item => {
26
+ batchPayload.push({
27
+ request_type: item.reqType,
28
+ payload: item.payload
29
+ })
30
+ })
31
+
32
+ return batchPayload
33
+ }
20
34
  function waitAndSend (config, application, host) {
21
35
  if (!immediate) {
22
36
  immediate = setImmediate(() => {
23
37
  immediate = null
24
38
  if (savedDependenciesToSend.size > 0) {
25
- const dependencies = Array.from(savedDependenciesToSend.values()).splice(0, 1000).map(pair => {
26
- savedDependenciesToSend.delete(pair)
27
- const [name, version] = pair.split(' ')
28
- return { name, version }
29
- })
30
- sendData(config, application, host, 'app-dependencies-loaded', { dependencies })
39
+ const dependencies = Array.from(savedDependenciesToSend.values())
40
+ // if a depencdency is from the initial load, *always* send the event
41
+ // Otherwise, only send if dependencyCollection is enabled
42
+ .filter(dep => {
43
+ const initialLoadModule = isTrue(dep.split(' ')[2])
44
+ const sendModule = initialLoadModule || (config.telemetry?.dependencyCollection)
45
+
46
+ if (!sendModule) savedDependenciesToSend.delete(dep) // we'll never send it
47
+ return sendModule
48
+ })
49
+ .splice(0, 2000) // v2 documentation specifies up to 2000 dependencies can be sent at once
50
+ .map(pair => {
51
+ savedDependenciesToSend.delete(pair)
52
+ const [name, version] = pair.split(' ')
53
+ return { name, version }
54
+ })
55
+ let currPayload
56
+ const retryData = getRetryData()
57
+ if (retryData) {
58
+ currPayload = { reqType: 'app-dependencies-loaded', payload: { dependencies } }
59
+ } else {
60
+ if (!dependencies.length) return // no retry data and no dependencies, nothing to send
61
+ currPayload = { dependencies }
62
+ }
63
+
64
+ const payload = retryData ? createBatchPayload([currPayload, retryData]) : currPayload
65
+ const reqType = retryData ? 'message-batch' : 'app-dependencies-loaded'
66
+
67
+ sendData(config, application, host, reqType, payload, updateRetryData)
68
+
31
69
  if (savedDependenciesToSend.size > 0) {
32
70
  waitAndSend(config, application, host)
33
71
  }
@@ -76,7 +114,7 @@ function onModuleLoad (data) {
76
114
  const dependencyAndVersion = `${name} ${version}`
77
115
 
78
116
  if (!detectedDependencyVersions.has(dependencyAndVersion)) {
79
- savedDependenciesToSend.add(dependencyAndVersion)
117
+ savedDependenciesToSend.add(`${dependencyAndVersion} ${initialLoad}`)
80
118
  detectedDependencyVersions.add(dependencyAndVersion)
81
119
 
82
120
  waitAndSend(config, application, host)
@@ -89,11 +127,19 @@ function onModuleLoad (data) {
89
127
  }
90
128
  }
91
129
  }
92
- function start (_config, _application, _host) {
130
+ function start (_config = {}, _application, _host, getRetryDataFunction, updateRetryDatafunction) {
93
131
  config = _config
94
132
  application = _application
95
133
  host = _host
134
+ initialLoad = true
135
+ getRetryData = getRetryDataFunction
136
+ updateRetryData = updateRetryDatafunction
96
137
  moduleLoadStartChannel.subscribe(onModuleLoad)
138
+
139
+ // try and capture intially loaded modules in the first tick
140
+ // since, ideally, the tracer (and this module) should be loaded first,
141
+ // this should capture any first-tick dependencies
142
+ queueMicrotask(() => { initialLoad = false })
97
143
  }
98
144
 
99
145
  function isDependency (filename, request) {