dd-trace 5.72.0 → 5.74.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 (53) hide show
  1. package/LICENSE-3rdparty.csv +3 -0
  2. package/index.d.ts +49 -0
  3. package/package.json +11 -6
  4. package/packages/datadog-core/src/utils/src/set.js +5 -1
  5. package/packages/datadog-esbuild/index.js +112 -36
  6. package/packages/datadog-esbuild/src/utils.js +198 -0
  7. package/packages/datadog-instrumentations/src/azure-service-bus.js +49 -22
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +2 -0
  9. package/packages/datadog-instrumentations/src/express-session.js +1 -0
  10. package/packages/datadog-instrumentations/src/express.js +82 -0
  11. package/packages/datadog-instrumentations/src/helpers/router-helper.js +238 -0
  12. package/packages/datadog-instrumentations/src/jest.js +60 -14
  13. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -4
  14. package/packages/datadog-instrumentations/src/playwright.js +110 -56
  15. package/packages/datadog-instrumentations/src/router.js +63 -6
  16. package/packages/datadog-instrumentations/src/ws.js +3 -3
  17. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -1
  18. package/packages/datadog-plugin-azure-functions/src/index.js +24 -14
  19. package/packages/datadog-plugin-azure-service-bus/src/index.js +1 -1
  20. package/packages/datadog-plugin-azure-service-bus/src/producer.js +60 -12
  21. package/packages/datadog-plugin-express/src/code_origin.js +2 -0
  22. package/packages/datadog-plugin-jest/src/index.js +53 -18
  23. package/packages/datadog-plugin-ws/src/close.js +2 -2
  24. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  25. package/packages/datadog-plugin-ws/src/receiver.js +1 -1
  26. package/packages/dd-trace/src/appsec/index.js +9 -1
  27. package/packages/dd-trace/src/appsec/reporter.js +2 -3
  28. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -0
  29. package/packages/dd-trace/src/config-helper.js +3 -1
  30. package/packages/dd-trace/src/config.js +437 -446
  31. package/packages/dd-trace/src/config_defaults.js +5 -12
  32. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +8 -3
  33. package/packages/dd-trace/src/llmobs/plugins/base.js +11 -12
  34. package/packages/dd-trace/src/llmobs/sdk.js +20 -4
  35. package/packages/dd-trace/src/llmobs/tagger.js +12 -0
  36. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +7 -127
  37. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +19 -134
  38. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +720 -0
  39. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +78 -0
  40. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +177 -0
  41. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +163 -0
  42. package/packages/dd-trace/src/opentelemetry/{protos → otlp}/protobuf_loader.js +24 -6
  43. package/packages/dd-trace/src/plugins/util/ci.js +3 -2
  44. package/packages/dd-trace/src/plugins/util/stacktrace.js +16 -1
  45. package/packages/dd-trace/src/supported-configurations.json +2 -0
  46. package/packages/dd-trace/src/telemetry/endpoints.js +27 -1
  47. package/packages/dd-trace/src/telemetry/index.js +16 -13
  48. package/scripts/preinstall.js +3 -1
  49. package/version.js +2 -1
  50. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/common.proto +0 -0
  51. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/logs.proto +0 -0
  52. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/logs_service.proto +0 -0
  53. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/resource.proto +0 -0
@@ -7,7 +7,8 @@ const shimmer = require('../../datadog-shimmer')
7
7
  const {
8
8
  parseAnnotations,
9
9
  getTestSuitePath,
10
- PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE
10
+ PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
11
+ getIsFaultyEarlyFlakeDetection
11
12
  } = require('../../dd-trace/src/plugins/util/test')
12
13
  const log = require('../../dd-trace/src/log')
13
14
  const { DD_MAJOR } = require('../../../version')
@@ -52,6 +53,7 @@ let isKnownTestsEnabled = false
52
53
  let isEarlyFlakeDetectionEnabled = false
53
54
  let earlyFlakeDetectionNumRetries = 0
54
55
  let isEarlyFlakeDetectionFaulty = false
56
+ let earlyFlakeDetectionFaultyThreshold = 0
55
57
  let isFlakyTestRetriesEnabled = false
56
58
  let flakyTestRetriesCount = 0
57
59
  let knownTests = {}
@@ -111,8 +113,10 @@ function deepCloneSuite (suite, filterTest, tags = []) {
111
113
  if (filterTest(entry)) {
112
114
  const copiedTest = entry._clone()
113
115
  tags.forEach(tag => {
114
- if (tag) {
115
- copiedTest[tag] = true
116
+ const resolvedTag = typeof tag === 'function' ? tag(entry) : tag
117
+
118
+ if (resolvedTag) {
119
+ copiedTest[resolvedTag] = true
116
120
  }
117
121
  })
118
122
  copy._addTest(copiedTest)
@@ -542,6 +546,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
542
546
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
543
547
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
544
548
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
549
+ earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
545
550
  isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
546
551
  flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
547
552
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
@@ -761,6 +766,30 @@ addHook({
761
766
  return suiteUtilsPackage
762
767
  })
763
768
 
769
+ /**
770
+ * We could repeat the logic of `applyRepeatEachIndex` here, but it'd be more risky
771
+ * as playwright could change it at any time.
772
+ *
773
+ * `applyRepeatEachIndex` goes through all the tests in a suite and applies the "repeat" logic
774
+ * for a single repeat index.
775
+ *
776
+ * This means that the clone logic is cumbersome:
777
+ * - we grab the unique file suites that have new tests
778
+ * - we store its project suite
779
+ * - we clone each of these file suites for each repeat index
780
+ * - we execute `applyRepeatEachIndex` for each of these cloned file suites
781
+ * - we add the cloned file suites to the project suite
782
+ */
783
+ function applyRetriesToTests (fileSuitesWithTestsToRetry, filterTest, tagsToApply, numRetries) {
784
+ for (const [fileSuite, projectSuite] of fileSuitesWithTestsToRetry.entries()) {
785
+ for (let repeatEachIndex = 1; repeatEachIndex <= numRetries; repeatEachIndex++) {
786
+ const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply)
787
+ applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
788
+ projectSuite._addSuite(copyFileSuite)
789
+ }
790
+ }
791
+ }
792
+
764
793
  addHook({
765
794
  name: 'playwright',
766
795
  file: 'lib/runner/loadUtils.js',
@@ -780,87 +809,112 @@ addHook({
780
809
  const allTests = rootSuite.allTests()
781
810
 
782
811
  if (isTestManagementTestsEnabled) {
812
+ const fileSuitesWithManagedTestsToProjects = new Map()
783
813
  for (const test of allTests) {
784
814
  const testProperties = getTestProperties(test)
815
+ // Disabled tests are skipped and not retried
785
816
  if (testProperties.disabled) {
786
817
  test._ddIsDisabled = true
787
- } else if (testProperties.quarantined) {
818
+ test.expectedStatus = 'skipped'
819
+ continue
820
+ }
821
+ if (testProperties.quarantined) {
788
822
  test._ddIsQuarantined = true
823
+ if (!testProperties.attemptToFix) {
824
+ // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
825
+ const testFqn = getTestFullyQualifiedName(test)
826
+ quarantinedButNotAttemptToFixFqns.add(testFqn)
827
+ }
789
828
  }
790
829
  if (testProperties.attemptToFix) {
791
830
  test._ddIsAttemptToFix = true
792
831
  const fileSuite = getSuiteType(test, 'file')
793
- const projectSuite = getSuiteType(test, 'project')
794
- const isAttemptToFix = test => getTestProperties(test).attemptToFix
795
- for (let repeatEachIndex = 1; repeatEachIndex <= testManagementAttemptToFixRetries; repeatEachIndex++) {
796
- const copyFileSuite = deepCloneSuite(fileSuite, isAttemptToFix, [
797
- testProperties.disabled && '_ddIsDisabled',
798
- testProperties.quarantined && '_ddIsQuarantined',
799
- '_ddIsAttemptToFix',
800
- '_ddIsAttemptToFixRetry'
801
- ])
802
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
803
- projectSuite._addSuite(copyFileSuite)
832
+
833
+ if (!fileSuitesWithManagedTestsToProjects.has(fileSuite)) {
834
+ fileSuitesWithManagedTestsToProjects.set(fileSuite, getSuiteType(test, 'project'))
804
835
  }
805
836
  if (testProperties.disabled || testProperties.quarantined) {
806
837
  quarantinedOrDisabledTestsAttemptToFix.push(test)
807
838
  }
808
- } else if (testProperties.disabled) {
809
- test.expectedStatus = 'skipped'
810
- } else if (testProperties.quarantined) {
811
- // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
812
- const testFqn = getTestFullyQualifiedName(test)
813
- quarantinedButNotAttemptToFixFqns.add(testFqn)
814
839
  }
815
840
  }
841
+ applyRetriesToTests(
842
+ fileSuitesWithManagedTestsToProjects,
843
+ (test) => test._ddIsAttemptToFix,
844
+ [
845
+ (test) => test._ddIsQuarantined && '_ddIsQuarantined',
846
+ '_ddIsAttemptToFix',
847
+ '_ddIsAttemptToFixRetry'
848
+ ],
849
+ testManagementAttemptToFixRetries
850
+ )
816
851
  }
817
852
 
818
853
  if (isImpactedTestsEnabled) {
819
- await Promise.all(allTests.map(async (test) => {
820
- const { isModified } = await getChannelPromise(isModifiedCh, {
854
+ const impactedTests = allTests.filter(test => {
855
+ let isImpacted = false
856
+ isModifiedCh.publish({
821
857
  filePath: test._requireFile,
822
- modifiedFiles
858
+ modifiedFiles,
859
+ onDone: (isModified) => { isImpacted = isModified }
823
860
  })
824
- if (isModified) {
825
- test._ddIsModified = true
826
- }
827
- if (isEarlyFlakeDetectionEnabled && test.expectedStatus !== 'skipped') {
828
- const isNew = isKnownTestsEnabled && isNewTest(test)
829
- const fileSuite = getSuiteType(test, 'file')
830
- const projectSuite = getSuiteType(test, 'project')
831
- // If something change in the file, all tests in the file are impacted
832
- const isModifiedTest = () => isModified
833
- for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
834
- const copyFileSuite = deepCloneSuite(fileSuite, isModifiedTest, [
835
- isNew && '_ddIsNew',
836
- '_ddIsModified',
837
- '_ddIsEfdRetry'
838
- ])
839
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
840
- projectSuite._addSuite(copyFileSuite)
861
+ return isImpacted
862
+ })
863
+
864
+ const fileSuitesWithImpactedTestsToProjects = new Map()
865
+ impactedTests.forEach(impactedTest => {
866
+ impactedTest._ddIsModified = true
867
+ if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
868
+ const fileSuite = getSuiteType(impactedTest, 'file')
869
+ if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
870
+ fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
841
871
  }
842
872
  }
843
- }))
873
+ })
874
+ // If something change in the file, all tests in the file are impacted, hence the () => true filter
875
+ applyRetriesToTests(
876
+ fileSuitesWithImpactedTestsToProjects,
877
+ () => true,
878
+ [
879
+ '_ddIsModified',
880
+ '_ddIsEfdRetry',
881
+ (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null)
882
+ ],
883
+ earlyFlakeDetectionNumRetries
884
+ )
844
885
  }
845
886
 
846
887
  if (isKnownTestsEnabled) {
847
888
  const newTests = allTests.filter(isNewTest)
848
889
 
849
- for (const newTest of newTests) {
850
- // No need to filter out attempt to fix tests here because attempt to fix tests are never new
851
- newTest._ddIsNew = true
852
- if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
853
- const fileSuite = getSuiteType(newTest, 'file')
854
- const projectSuite = getSuiteType(newTest, 'project')
855
- for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
856
- const copyFileSuite = deepCloneSuite(fileSuite, isNewTest, [
857
- '_ddIsNew',
858
- '_ddIsEfdRetry'
859
- ])
860
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
861
- projectSuite._addSuite(copyFileSuite)
890
+ const isFaulty = getIsFaultyEarlyFlakeDetection(
891
+ allTests.map(test => getTestSuitePath(test._requireFile, rootDir)),
892
+ knownTests.playwright,
893
+ earlyFlakeDetectionFaultyThreshold
894
+ )
895
+
896
+ if (isFaulty) {
897
+ isEarlyFlakeDetectionEnabled = false
898
+ isKnownTestsEnabled = false
899
+ isEarlyFlakeDetectionFaulty = true
900
+ } else {
901
+ const fileSuitesWithNewTestsToProjects = new Map()
902
+ newTests.forEach(newTest => {
903
+ newTest._ddIsNew = true
904
+ if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
905
+ const fileSuite = getSuiteType(newTest, 'file')
906
+ if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) {
907
+ fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project'))
908
+ }
862
909
  }
863
- }
910
+ })
911
+
912
+ applyRetriesToTests(
913
+ fileSuitesWithNewTestsToProjects,
914
+ isNewTest,
915
+ ['_ddIsNew', '_ddIsEfdRetry'],
916
+ earlyFlakeDetectionNumRetries
917
+ )
864
918
  }
865
919
  }
866
920
 
@@ -5,6 +5,19 @@ const pathToRegExp = require('path-to-regexp')
5
5
  const shimmer = require('../../datadog-shimmer')
6
6
  const { addHook, channel } = require('./helpers/instrument')
7
7
 
8
+ const {
9
+ getRouterMountPaths,
10
+ joinPath,
11
+ getLayerMatchers,
12
+ setLayerMatchers,
13
+ isAppMounted,
14
+ setRouterMountPath,
15
+ extractMountPaths,
16
+ getRouteFullPaths,
17
+ wrapRouteMethodsAndPublish,
18
+ collectRoutesFromRouter
19
+ } = require('./helpers/router-helper')
20
+
8
21
  function isFastStar (layer, matchers) {
9
22
  return layer.regexp?.fast_star ?? matchers.some(matcher => matcher.path === '*')
10
23
  }
@@ -22,7 +35,6 @@ function createWrapRouterMethod (name) {
22
35
  const nextChannel = channel(`apm:${name}:middleware:next`)
23
36
  const routeAddedChannel = channel(`apm:${name}:route:added`)
24
37
 
25
- const layerMatchers = new WeakMap()
26
38
  const regexpCache = Object.create(null)
27
39
 
28
40
  function wrapLayerHandle (layer, original) {
@@ -31,7 +43,7 @@ function createWrapRouterMethod (name) {
31
43
  return shimmer.wrapFunction(original, original => function () {
32
44
  if (!enterChannel.hasSubscribers) return original.apply(this, arguments)
33
45
 
34
- const matchers = layerMatchers.get(layer)
46
+ const matchers = getLayerMatchers(layer)
35
47
  const lastIndex = arguments.length - 1
36
48
  const name = original._name || original.name
37
49
  const req = arguments[arguments.length > 3 ? 1 : 0]
@@ -78,7 +90,7 @@ function createWrapRouterMethod (name) {
78
90
  layer.handle = wrapLayerHandle(layer, layer.handle)
79
91
  }
80
92
 
81
- layerMatchers.set(layer, matchers)
93
+ setLayerMatchers(layer, matchers)
82
94
 
83
95
  if (layer.route) {
84
96
  METHODS.forEach(method => {
@@ -115,7 +127,7 @@ function createWrapRouterMethod (name) {
115
127
  return arg.map(pattern => ({
116
128
  path: pattern instanceof RegExp ? `(${pattern})` : pattern,
117
129
  test: layer => {
118
- const matchers = layerMatchers.get(layer)
130
+ const matchers = getLayerMatchers(layer)
119
131
  return !isFastStar(layer, matchers) &&
120
132
  !isFastSlash(layer, matchers) &&
121
133
  cachedPathToRegExp(pattern).test(layer.path)
@@ -134,12 +146,12 @@ function createWrapRouterMethod (name) {
134
146
  }
135
147
 
136
148
  function wrapMethod (original) {
137
- return shimmer.wrapFunction(original, original => function methodWithTrace (fn) {
149
+ return shimmer.wrapFunction(original, original => function methodWithTrace (fn, ...otherArgs) {
138
150
  let offset = 0
139
151
  if (this.stack) {
140
152
  offset = Array.isArray(this.stack) ? this.stack.length : 1
141
153
  }
142
- const router = original.apply(this, arguments)
154
+ const router = original.call(this, fn, ...otherArgs)
143
155
 
144
156
  if (typeof this.stack === 'function') {
145
157
  this.stack = [{ handle: this.stack }]
@@ -149,6 +161,51 @@ function createWrapRouterMethod (name) {
149
161
  routeAddedChannel.publish({ topOfStackFunc: methodWithTrace, layer: this.stack.at(-1) })
150
162
  }
151
163
 
164
+ // Publish only if this router was mounted by app.use() (prevents early '/sub/...')
165
+ if (routeAddedChannel.hasSubscribers && isAppMounted(this) && this.stack?.length > offset) {
166
+ // Handle nested router mounting for 'use' method
167
+ if (original.name === 'use' && otherArgs.length >= 1) {
168
+ const { mountPaths, startIdx } = extractMountPaths(fn)
169
+
170
+ if (mountPaths.length) {
171
+ const parentPaths = getRouterMountPaths(this)
172
+ const callArgs = [fn, ...otherArgs]
173
+
174
+ for (let i = startIdx; i < callArgs.length; i++) {
175
+ const nestedRouter = callArgs[i]
176
+
177
+ if (!nestedRouter || typeof nestedRouter !== 'function') continue
178
+
179
+ for (const parentPath of parentPaths) {
180
+ for (const normalizedMountPath of mountPaths) {
181
+ const fullMountPath = joinPath(parentPath, normalizedMountPath)
182
+ if (fullMountPath === null) continue
183
+
184
+ setRouterMountPath(nestedRouter, fullMountPath)
185
+ collectRoutesFromRouter(nestedRouter, fullMountPath)
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ const mountPaths = getRouterMountPaths(this)
193
+
194
+ if (mountPaths.length) {
195
+ const layer = this.stack.at(-1)
196
+
197
+ if (layer?.route) {
198
+ const route = layer.route
199
+
200
+ const fullPaths = mountPaths.flatMap(mountPath => getRouteFullPaths(route, mountPath))
201
+
202
+ wrapRouteMethodsAndPublish(route, fullPaths, (payload) => {
203
+ routeAddedChannel.publish(payload)
204
+ })
205
+ }
206
+ }
207
+ }
208
+
152
209
  if (this.stack.length > offset) {
153
210
  wrapStack(this.stack.slice(offset), extractMatchers(fn))
154
211
  }
@@ -41,7 +41,7 @@ function wrapSend (send) {
41
41
 
42
42
  const [data, options, cb] = arguments
43
43
 
44
- const ctx = { data, socket: this._sender._socket }
44
+ const ctx = { data, socket: this._sender?._socket }
45
45
 
46
46
  return typeof cb === 'function'
47
47
  ? producerCh.traceCallback(send, undefined, ctx, this, data, options, cb)
@@ -70,7 +70,7 @@ function createWrappedHandler (handler) {
70
70
  return function wrappedMessageHandler (data, binary) {
71
71
  const byteLength = dataLength(data)
72
72
 
73
- const ctx = { data, binary, socket: this._sender._socket, byteLength }
73
+ const ctx = { data, binary, socket: this._sender?._socket, byteLength }
74
74
 
75
75
  return receiverCh.traceSync(handler, ctx, this, data, binary)
76
76
  }
@@ -93,7 +93,7 @@ function wrapClose (close) {
93
93
  // if both are true then the self is sending the close event
94
94
  const isPeerClose = this._closeFrameReceived === true && this._closeFrameSent === false
95
95
 
96
- const ctx = { code, data, socket: this._sender._socket, isPeerClose }
96
+ const ctx = { code, data, socket: this._sender?._socket, isPeerClose }
97
97
 
98
98
  return closeCh.traceSync(close, ctx, this, ...arguments)
99
99
  }
@@ -10,7 +10,7 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
10
10
  static operation = 'consume'
11
11
 
12
12
  bindStart (ctx) {
13
- const { method, fields, message, queue } = ctx
13
+ const { method, fields = {}, message, queue } = ctx
14
14
 
15
15
  if (method !== 'basic.deliver' && method !== 'basic.get') return
16
16
 
@@ -24,10 +24,10 @@ class AzureFunctionsPlugin extends TracingPlugin {
24
24
  static prefix = 'tracing:datadog:azure:functions:invoke'
25
25
 
26
26
  bindStart (ctx) {
27
- const childOf = extractTraceContext(this._tracer, ctx)
28
27
  const meta = getMetaForTrigger(ctx)
29
28
  const triggerType = triggerMap[ctx.methodName]
30
29
  const isMessagingService = (triggerType === 'ServiceBus' || triggerType === 'EventHubs')
30
+ const childOf = isMessagingService ? null : extractTraceContext(this._tracer, ctx)
31
31
  const span = this.startSpan(this.operationName(), {
32
32
  childOf,
33
33
  service: this.serviceName(),
@@ -36,7 +36,7 @@ class AzureFunctionsPlugin extends TracingPlugin {
36
36
  }, ctx)
37
37
 
38
38
  if (isMessagingService) {
39
- setSpanLinks(this.tracer, span, ctx)
39
+ setSpanLinks(triggerType, this.tracer, span, ctx)
40
40
  }
41
41
 
42
42
  ctx.span = span
@@ -116,29 +116,39 @@ function extractTraceContext (tracer, ctx) {
116
116
  switch (String(triggerMap[ctx.methodName])) {
117
117
  case 'Http':
118
118
  return tracer.extract('http_headers', Object.fromEntries(ctx.httpRequest.headers))
119
- case 'ServiceBus':
120
- return tracer.extract('text_map', ctx.invocationContext.triggerMetadata.applicationProperties)
121
119
  default:
122
120
  null
123
121
  }
124
122
  }
125
123
 
126
- function setSpanLinks (tracer, span, ctx) {
124
+ // message & messages & batch with cardinality of 1 == applicationProperties
125
+ // messages with cardinality of many == applicationPropertiesArray
126
+ function setSpanLinks (triggerType, tracer, span, ctx) {
127
127
  const cardinality = ctx.invocationContext.options.trigger.cardinality
128
128
  const triggerMetadata = ctx.invocationContext.triggerMetadata
129
- if (cardinality === 'many' && triggerMetadata.propertiesArray.length > 0) {
130
- triggerMetadata.propertiesArray.forEach(event => {
131
- // Check for possible empty event when span links are disabled
132
- if (Object.keys(event).length > 0) {
133
- span.addLink(tracer.extract('text_map', event))
134
- }
135
- })
136
- } else if (cardinality === 'one') {
137
- const spanContext = tracer.extract('text_map', triggerMetadata.properties)
129
+ const isServiceBus = triggerType === 'ServiceBus'
130
+
131
+ const properties = isServiceBus
132
+ ? triggerMetadata.applicationProperties
133
+ : triggerMetadata.properties
134
+
135
+ const propertiesArray = isServiceBus
136
+ ? triggerMetadata.applicationPropertiesArray
137
+ : triggerMetadata.propertiesArray
138
+
139
+ const addLinkFromProperties = (props) => {
140
+ if (!props || Object.keys(props).length === 0) return
141
+ const spanContext = tracer.extract('text_map', props)
138
142
  if (spanContext) {
139
143
  span.addLink(spanContext)
140
144
  }
141
145
  }
146
+
147
+ if (cardinality === 'many' && propertiesArray?.length > 0) {
148
+ propertiesArray.forEach(addLinkFromProperties)
149
+ } else if (cardinality === 'one') {
150
+ addLinkFromProperties(properties)
151
+ }
142
152
  }
143
153
 
144
154
  module.exports = AzureFunctionsPlugin
@@ -4,7 +4,7 @@ const ProducerPlugin = require('./producer')
4
4
  const CompositePlugin = require('../../dd-trace/src/plugins/composite')
5
5
 
6
6
  class AzureServiceBusPlugin extends CompositePlugin {
7
- static id = 'azure-service-bus'
7
+ static get id () { return 'azure-service-bus' }
8
8
  static get plugins () {
9
9
  return {
10
10
  producer: ProducerPlugin
@@ -1,36 +1,84 @@
1
1
  'use strict'
2
2
 
3
+ const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
3
4
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
4
5
 
5
6
  class AzureServiceBusProducerPlugin extends ProducerPlugin {
6
- static id = 'azure-service-bus'
7
- static operation = 'send'
7
+ static get id () { return 'azure-service-bus' }
8
+ static get operation () { return 'send' }
9
+ static get prefix () { return 'tracing:apm:azure-service-bus:send' }
8
10
 
9
11
  bindStart (ctx) {
10
- const { sender, msg } = ctx
11
- const qualifiedSenderNamespace = sender._sender.audience.replace('sb://', '')
12
+ // we do not want to make these spans when batch linking is disabled.
13
+ if (!batchLinksAreEnabled() && ctx.functionName === 'tryAddMessage') {
14
+ return ctx.currentStore
15
+ }
16
+
17
+ const qualifiedSenderNamespace = ctx.config.host
12
18
  const span = this.startSpan({
13
- resource: sender.entityPath,
19
+ resource: ctx.entityPath,
14
20
  type: 'messaging',
15
21
  meta: {
16
22
  component: 'azure-service-bus',
17
- 'messaging.destination.name': sender.entityPath,
23
+ 'messaging.destination.name': ctx.entityPath,
18
24
  'messaging.operation': 'send',
19
25
  'messaging.system': 'servicebus',
20
26
  'network.destination.name': qualifiedSenderNamespace,
21
27
  }
22
28
  }, ctx)
23
29
 
24
- // This is the correct key for injecting trace context into Azure Service Bus messages
25
- // It may not be present in the message properties, so we ensure it exists
26
- if (!msg.applicationProperties) {
27
- msg.applicationProperties = {}
28
- }
30
+ if (ctx.functionName === 'tryAddMessage') {
31
+ span._spanContext._name = 'azure.servicebus.create'
32
+ span.setTag('messaging.operation', 'create')
29
33
 
30
- this.tracer.inject(span, 'text_map', msg.applicationProperties)
34
+ if (ctx.msg.messageID !== undefined) {
35
+ span.setTag('message.id', ctx.msg)
36
+ }
31
37
 
38
+ if (batchLinksAreEnabled()) {
39
+ ctx.batch._spanContexts.push(span.context())
40
+ injectTraceContext(this.tracer, span, ctx.msg)
41
+ }
42
+ }
43
+
44
+ if (ctx.functionName === 'send' || ctx.functionName === 'sendBatch' || ctx.functionName === 'scheduleMessages') {
45
+ const messages = ctx.msg
46
+ const isBatch = messages.constructor?.name === 'ServiceBusMessageBatchImpl'
47
+ if (isBatch) {
48
+ span.setTag('messaging.batch.message_count', messages.count)
49
+ if (batchLinksAreEnabled()) {
50
+ messages._spanContexts.forEach(spanContext => {
51
+ span.addLink(spanContext)
52
+ })
53
+ }
54
+ } else if (Array.isArray(messages)) {
55
+ span.setTag('messaging.batch.message_count', messages.length)
56
+ messages.forEach(event => {
57
+ injectTraceContext(this.tracer, span, event)
58
+ })
59
+ } else {
60
+ injectTraceContext(this.tracer, span, messages)
61
+ }
62
+ }
32
63
  return ctx.currentStore
33
64
  }
65
+
66
+ asyncEnd (ctx) {
67
+ super.finish()
68
+ }
69
+ }
70
+
71
+ function injectTraceContext (tracer, span, msg) {
72
+ if (!msg.applicationProperties) {
73
+ msg.applicationProperties = {}
74
+ }
75
+
76
+ tracer.inject(span, 'text_map', msg.applicationProperties)
77
+ }
78
+
79
+ function batchLinksAreEnabled () {
80
+ const sb = getEnvironmentVariable('DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED')
81
+ return sb !== 'false'
34
82
  }
35
83
 
36
84
  module.exports = AzureServiceBusProducerPlugin
@@ -19,6 +19,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
19
19
  })
20
20
 
21
21
  this.addSub('apm:express:route:added', ({ topOfStackFunc, layer }) => {
22
+ if (!layer) return
22
23
  if (layerTags.has(layer)) return
23
24
  layerTags.set(layer, entryTags(topOfStackFunc))
24
25
  })
@@ -30,6 +31,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
30
31
  })
31
32
 
32
33
  this.addSub('apm:router:route:added', ({ topOfStackFunc, layer }) => {
34
+ if (!layer) return
33
35
  if (layerTags.has(layer)) return
34
36
  layerTags.set(layer, entryTags(topOfStackFunc))
35
37
  })