dd-trace 5.33.0 → 5.34.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.
@@ -35,6 +35,7 @@ dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Mete
35
35
  dev,@types/node,MIT,Copyright Authors
36
36
  dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
37
37
  dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
38
+ dev,@msgpack/msgpack,ISC,Copyright 2019 The MessagePack Community
38
39
  dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
39
40
  dev,autocannon,MIT,Copyright 2016 Matteo Collina
40
41
  dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
@@ -58,12 +59,10 @@ dev,get-port,MIT,Copyright Sindre Sorhus
58
59
  dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors
59
60
  dev,globals,MIT,Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
60
61
  dev,graphql,MIT,Copyright 2015 Facebook Inc.
61
- dev,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki
62
62
  dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors
63
63
  dev,knex,MIT,Copyright (c) 2013-present Tim Griesser
64
64
  dev,mkdirp,MIT,Copyright 2010 James Halliday
65
65
  dev,mocha,MIT,Copyright 2011-2018 JS Foundation and contributors https://js.foundation
66
- dev,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki
67
66
  dev,multer,MIT,Copyright 2014 Hage Yaapa
68
67
  dev,nock,MIT,Copyright 2017 Pedro Teixeira and other contributors
69
68
  dev,nyc,ISC,Copyright 2015 Contributors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.33.0",
3
+ "version": "5.34.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -86,7 +86,7 @@
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",
89
- "@datadog/pprof": "5.5.0",
89
+ "@datadog/pprof": "5.5.1",
90
90
  "@datadog/sketches-js": "^2.1.0",
91
91
  "@isaacs/ttlcache": "^1.4.1",
92
92
  "@opentelemetry/api": ">=1.0.0 <1.9.0",
@@ -118,6 +118,7 @@
118
118
  "@apollo/server": "^4.11.0",
119
119
  "@eslint/eslintrc": "^3.1.0",
120
120
  "@eslint/js": "^9.11.1",
121
+ "@msgpack/msgpack": "^3.0.0-beta3",
121
122
  "@stylistic/eslint-plugin-js": "^2.8.0",
122
123
  "@types/node": "^16.0.0",
123
124
  "autocannon": "^4.5.2",
@@ -142,12 +143,10 @@
142
143
  "glob": "^7.1.6",
143
144
  "globals": "^15.10.0",
144
145
  "graphql": "0.13.2",
145
- "int64-buffer": "^0.1.9",
146
146
  "jszip": "^3.5.0",
147
147
  "knex": "^2.4.2",
148
148
  "mkdirp": "^3.0.1",
149
149
  "mocha": "^10",
150
- "msgpack-lite": "^0.1.26",
151
150
  "multer": "^1.4.5-lts.1",
152
151
  "nock": "^11.3.3",
153
152
  "nyc": "^15.1.0",
@@ -36,9 +36,17 @@ class AmqplibProducerPlugin extends ProducerPlugin {
36
36
  if (this.config.dsmEnabled) {
37
37
  const hasRoutingKey = fields.routingKey != null
38
38
  const payloadSize = getAmqpMessageSize({ content: message, headers: fields.headers })
39
+
40
+ // there are two ways to send messages in RabbitMQ:
41
+ // 1. using an exchange and a routing key in which DSM connects via the exchange
42
+ // 2. using an unnamed exchange and a routing key in which DSM connects via the topic
43
+ const exchangeOrTopicTag = hasRoutingKey && !fields.exchange
44
+ ? `topic:${fields.routingKey}`
45
+ : `exchange:${fields.exchange}`
46
+
39
47
  const dataStreamsContext = this.tracer
40
48
  .setCheckpoint(
41
- ['direction:out', `exchange:${fields.exchange}`, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq']
49
+ ['direction:out', exchangeOrTopicTag, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq']
42
50
  , span, payloadSize)
43
51
  DsmPathwayCodec.encode(dataStreamsContext, fields.headers)
44
52
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { extractErrorIntoSpanEvent } = require('./utils')
4
5
 
5
6
  let tools
6
7
 
@@ -34,6 +35,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
34
35
  finish ({ res, args }) {
35
36
  const span = this.activeSpan
36
37
  this.config.hooks.execute(span, args, res)
38
+ if (res?.errors) {
39
+ for (const err of res.errors) {
40
+ extractErrorIntoSpanEvent(this._tracerConfig, span, err)
41
+ }
42
+ }
37
43
  super.finish()
38
44
  }
39
45
  }
@@ -0,0 +1,40 @@
1
+ function extractErrorIntoSpanEvent (config, span, exc) {
2
+ const attributes = {}
3
+
4
+ if (exc.name) {
5
+ attributes.type = exc.name
6
+ }
7
+
8
+ if (exc.stack) {
9
+ attributes.stacktrace = exc.stack
10
+ }
11
+
12
+ if (exc.locations) {
13
+ attributes.locations = []
14
+ for (const location of exc.locations) {
15
+ attributes.locations.push(`${location.line}:${location.column}`)
16
+ }
17
+ }
18
+
19
+ if (exc.path) {
20
+ attributes.path = exc.path.map(String)
21
+ }
22
+
23
+ if (exc.message) {
24
+ attributes.message = exc.message
25
+ }
26
+
27
+ if (config.graphqlErrorExtensions) {
28
+ for (const ext of config.graphqlErrorExtensions) {
29
+ if (exc.extensions?.[ext]) {
30
+ attributes[`extensions.${ext}`] = exc.extensions[ext].toString()
31
+ }
32
+ }
33
+ }
34
+
35
+ span.addEvent('dd.graphql.query.error', attributes, Date.now())
36
+ }
37
+
38
+ module.exports = {
39
+ extractErrorIntoSpanEvent
40
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { extractErrorIntoSpanEvent } = require('./utils')
4
5
 
5
6
  class GraphQLValidatePlugin extends TracingPlugin {
6
7
  static get id () { return 'graphql' }
@@ -21,6 +22,11 @@ class GraphQLValidatePlugin extends TracingPlugin {
21
22
  finish ({ document, errors }) {
22
23
  const span = this.activeSpan
23
24
  this.config.hooks.validate(span, document, errors)
25
+ if (errors) {
26
+ for (const err of errors) {
27
+ extractErrorIntoSpanEvent(this._tracerConfig, span, err)
28
+ }
29
+ }
24
30
  super.finish()
25
31
  }
26
32
  }
@@ -1,16 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const Sampler = require('../../../dd-trace/src/sampler')
3
+ const makeUtilities = require('../../../dd-trace/src/plugins/util/llm')
4
4
 
5
- const RE_NEWLINE = /\n/g
6
- const RE_TAB = /\t/g
7
-
8
- // TODO: should probably refactor the OpenAI integration to use a shared LLMTracingPlugin base class
9
- // This logic isn't particular to LangChain
10
5
  class LangChainHandler {
11
- constructor (config) {
12
- this.config = config
13
- this.sampler = new Sampler(config.spanPromptCompletionSampleRate)
6
+ constructor (tracerConfig) {
7
+ const utilities = makeUtilities('langchain', tracerConfig)
8
+
9
+ this.normalize = utilities.normalize
10
+ this.isPromptCompletionSampled = utilities.isPromptCompletionSampled
14
11
  }
15
12
 
16
13
  // no-op for default handler
@@ -27,27 +24,6 @@ class LangChainHandler {
27
24
 
28
25
  // no-op for default handler
29
26
  extractModel (instance) {}
30
-
31
- normalize (text) {
32
- if (!text) return
33
- if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
34
-
35
- const max = this.config.spanCharLimit
36
-
37
- text = text
38
- .replace(RE_NEWLINE, '\\n')
39
- .replace(RE_TAB, '\\t')
40
-
41
- if (text.length > max) {
42
- return text.substring(0, max) + '...'
43
- }
44
-
45
- return text
46
- }
47
-
48
- isPromptCompletionSampled () {
49
- return this.sampler.isSampled()
50
- }
51
27
  }
52
28
 
53
29
  module.exports = LangChainHandler
@@ -26,13 +26,12 @@ class LangChainTracingPlugin extends TracingPlugin {
26
26
  constructor () {
27
27
  super(...arguments)
28
28
 
29
- const langchainConfig = this._tracerConfig.langchain || {}
30
29
  this.handlers = {
31
- chain: new LangChainChainHandler(langchainConfig),
32
- chat_model: new LangChainChatModelHandler(langchainConfig),
33
- llm: new LangChainLLMHandler(langchainConfig),
34
- embedding: new LangChainEmbeddingHandler(langchainConfig),
35
- default: new LangChainHandler(langchainConfig)
30
+ chain: new LangChainChainHandler(this._tracerConfig),
31
+ chat_model: new LangChainChatModelHandler(this._tracerConfig),
32
+ llm: new LangChainLLMHandler(this._tracerConfig),
33
+ embedding: new LangChainEmbeddingHandler(this._tracerConfig),
34
+ default: new LangChainHandler(this._tracerConfig)
36
35
  }
37
36
  }
38
37
 
@@ -9,12 +9,9 @@ const Sampler = require('../../dd-trace/src/sampler')
9
9
  const { MEASURED } = require('../../../ext/tags')
10
10
  const { estimateTokens } = require('./token-estimator')
11
11
 
12
- // String#replaceAll unavailable on Node.js@v14 (dd-trace@<=v3)
13
- const RE_NEWLINE = /\n/g
14
- const RE_TAB = /\t/g
12
+ const makeUtilities = require('../../dd-trace/src/plugins/util/llm')
15
13
 
16
- // TODO: In the future we should refactor config.js to make it requirable
17
- let MAX_TEXT_LEN = 128
14
+ let normalize
18
15
 
19
16
  function safeRequire (path) {
20
17
  try {
@@ -44,9 +41,11 @@ class OpenAiTracingPlugin extends TracingPlugin {
44
41
 
45
42
  this.sampler = new Sampler(0.1) // default 10% log sampling
46
43
 
47
- // hoist the max length env var to avoid making all of these functions a class method
44
+ // hoist the normalize function to avoid making all of these functions a class method
48
45
  if (this._tracerConfig) {
49
- MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit
46
+ const utilities = makeUtilities('openai', this._tracerConfig)
47
+
48
+ normalize = utilities.normalize
50
49
  }
51
50
  }
52
51
 
@@ -116,7 +115,7 @@ class OpenAiTracingPlugin extends TracingPlugin {
116
115
  // createEdit, createEmbedding, createModeration
117
116
  if (payload.input) {
118
117
  const normalized = normalizeStringOrTokenArray(payload.input, false)
119
- tags['openai.request.input'] = truncateText(normalized)
118
+ tags['openai.request.input'] = normalize(normalized)
120
119
  openaiStore.input = normalized
121
120
  }
122
121
 
@@ -594,7 +593,7 @@ function commonImageResponseExtraction (tags, body) {
594
593
  for (let i = 0; i < body.data.length; i++) {
595
594
  const image = body.data[i]
596
595
  // exactly one of these two options is provided
597
- tags[`openai.response.images.${i}.url`] = truncateText(image.url)
596
+ tags[`openai.response.images.${i}.url`] = normalize(image.url)
598
597
  tags[`openai.response.images.${i}.b64_json`] = image.b64_json && 'returned'
599
598
  }
600
599
  }
@@ -731,14 +730,14 @@ function commonCreateResponseExtraction (tags, body, openaiStore, methodName) {
731
730
 
732
731
  tags[`openai.response.choices.${choiceIdx}.finish_reason`] = choice.finish_reason
733
732
  tags[`openai.response.choices.${choiceIdx}.logprobs`] = specifiesLogProb ? 'returned' : undefined
734
- tags[`openai.response.choices.${choiceIdx}.text`] = truncateText(choice.text)
733
+ tags[`openai.response.choices.${choiceIdx}.text`] = normalize(choice.text)
735
734
 
736
735
  // createChatCompletion only
737
736
  const message = choice.message || choice.delta // delta for streamed responses
738
737
  if (message) {
739
738
  tags[`openai.response.choices.${choiceIdx}.message.role`] = message.role
740
- tags[`openai.response.choices.${choiceIdx}.message.content`] = truncateText(message.content)
741
- tags[`openai.response.choices.${choiceIdx}.message.name`] = truncateText(message.name)
739
+ tags[`openai.response.choices.${choiceIdx}.message.content`] = normalize(message.content)
740
+ tags[`openai.response.choices.${choiceIdx}.message.name`] = normalize(message.name)
742
741
  if (message.tool_calls) {
743
742
  const toolCalls = message.tool_calls
744
743
  for (let toolIdx = 0; toolIdx < toolCalls.length; toolIdx++) {
@@ -795,24 +794,6 @@ function truncateApiKey (apiKey) {
795
794
  return apiKey && `sk-...${apiKey.substr(apiKey.length - 4)}`
796
795
  }
797
796
 
798
- /**
799
- * for cleaning up prompt and response
800
- */
801
- function truncateText (text) {
802
- if (!text) return
803
- if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
804
-
805
- text = text
806
- .replace(RE_NEWLINE, '\\n')
807
- .replace(RE_TAB, '\\t')
808
-
809
- if (text.length > MAX_TEXT_LEN) {
810
- return text.substring(0, MAX_TEXT_LEN) + '...'
811
- }
812
-
813
- return text
814
- }
815
-
816
797
  function tagChatCompletionRequestContent (contents, messageIdx, tags) {
817
798
  if (typeof contents === 'string') {
818
799
  tags[`openai.request.messages.${messageIdx}.content`] = contents
@@ -824,10 +805,10 @@ function tagChatCompletionRequestContent (contents, messageIdx, tags) {
824
805
  const type = content.type
825
806
  tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.type`] = content.type
826
807
  if (type === 'text') {
827
- tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.text`] = truncateText(content.text)
808
+ tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.text`] = normalize(content.text)
828
809
  } else if (type === 'image_url') {
829
810
  tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.image_url.url`] =
830
- truncateText(content.image_url.url)
811
+ normalize(content.image_url.url)
831
812
  }
832
813
  // unsupported type otherwise, won't be tagged
833
814
  }
@@ -1004,7 +985,7 @@ function normalizeStringOrTokenArray (input, truncate) {
1004
985
  const normalized = Array.isArray(input)
1005
986
  ? `[${input.join(', ')}]` // "[1, 2, 999]"
1006
987
  : input // "foo"
1007
- return truncate ? truncateText(normalized) : normalized
988
+ return truncate ? normalize(normalized) : normalized
1008
989
  }
1009
990
 
1010
991
  function defensiveArrayLength (maybeArray) {
@@ -2,14 +2,32 @@
2
2
 
3
3
  const InjectionAnalyzer = require('./injection-analyzer')
4
4
  const { CODE_INJECTION } = require('../vulnerabilities')
5
+ const { INSTRUMENTED_SINK } = require('../telemetry/iast-metric')
6
+ const { storage } = require('../../../../../datadog-core')
7
+ const { getIastContext } = require('../iast-context')
5
8
 
6
9
  class CodeInjectionAnalyzer extends InjectionAnalyzer {
7
10
  constructor () {
8
11
  super(CODE_INJECTION)
12
+ this.evalInstrumentedInc = false
9
13
  }
10
14
 
11
15
  onConfigure () {
12
- this.addSub('datadog:eval:call', ({ script }) => this.analyze(script))
16
+ this.addSub('datadog:eval:call', ({ script }) => {
17
+ if (!this.evalInstrumentedInc) {
18
+ const store = storage.getStore()
19
+ const iastContext = getIastContext(store)
20
+ const tags = INSTRUMENTED_SINK.formatTags(CODE_INJECTION)
21
+
22
+ for (const tag of tags) {
23
+ INSTRUMENTED_SINK.inc(iastContext, tag)
24
+ }
25
+
26
+ this.evalInstrumentedInc = true
27
+ }
28
+
29
+ this.analyze(script)
30
+ })
13
31
  this.addSub('datadog:vm:run-script:start', ({ code }) => this.analyze(code))
14
32
  this.addSub('datadog:vm:source-text-module:start', ({ code }) => this.analyze(code))
15
33
  }
@@ -482,6 +482,7 @@ class Config {
482
482
  this._setValue(defaults, 'flushInterval', 2000)
483
483
  this._setValue(defaults, 'flushMinSpans', 1000)
484
484
  this._setValue(defaults, 'gitMetadataEnabled', true)
485
+ this._setValue(defaults, 'graphqlErrorExtensions', [])
485
486
  this._setValue(defaults, 'grpc.client.error.statuses', GRPC_CLIENT_ERROR_STATUSES)
486
487
  this._setValue(defaults, 'grpc.server.error.statuses', GRPC_SERVER_ERROR_STATUSES)
487
488
  this._setValue(defaults, 'headerTags', [])
@@ -521,8 +522,9 @@ class Config {
521
522
  this._setValue(defaults, 'lookup', undefined)
522
523
  this._setValue(defaults, 'inferredProxyServicesEnabled', false)
523
524
  this._setValue(defaults, 'memcachedCommandEnabled', false)
525
+ this._setValue(defaults, 'middlewareTracingEnabled', true)
524
526
  this._setValue(defaults, 'openAiLogsEnabled', false)
525
- this._setValue(defaults, 'openaiSpanCharLimit', 128)
527
+ this._setValue(defaults, 'openai.spanCharLimit', 128)
526
528
  this._setValue(defaults, 'peerServiceMapping', {})
527
529
  this._setValue(defaults, 'plugins', true)
528
530
  this._setValue(defaults, 'port', '8126')
@@ -669,9 +671,11 @@ class Config {
669
671
  DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED,
670
672
  DD_TRACE_GIT_METADATA_ENABLED,
671
673
  DD_TRACE_GLOBAL_TAGS,
674
+ DD_TRACE_GRAPHQL_ERROR_EXTENSIONS,
672
675
  DD_TRACE_HEADER_TAGS,
673
676
  DD_TRACE_LEGACY_BAGGAGE_ENABLED,
674
677
  DD_TRACE_MEMCACHED_COMMAND_ENABLED,
678
+ DD_TRACE_MIDDLEWARE_TRACING_ENABLED,
675
679
  DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP,
676
680
  DD_TRACE_PARTIAL_FLUSH_MIN_SPANS,
677
681
  DD_TRACE_PEER_SERVICE_MAPPING,
@@ -804,8 +808,9 @@ class Config {
804
808
  this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
805
809
  // Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent
806
810
  this._setBoolean(env, 'memcachedCommandEnabled', DD_TRACE_MEMCACHED_COMMAND_ENABLED)
811
+ this._setBoolean(env, 'middlewareTracingEnabled', DD_TRACE_MIDDLEWARE_TRACING_ENABLED)
807
812
  this._setBoolean(env, 'openAiLogsEnabled', DD_OPENAI_LOGS_ENABLED)
808
- this._setValue(env, 'openaiSpanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT))
813
+ this._setValue(env, 'openai.spanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT))
809
814
  this._envUnprocessed.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT
810
815
  if (DD_TRACE_PEER_SERVICE_MAPPING) {
811
816
  this._setValue(env, 'peerServiceMapping', fromEntries(
@@ -895,6 +900,7 @@ class Config {
895
900
  this._setString(env, 'version', DD_VERSION || tags.version)
896
901
  this._setBoolean(env, 'inferredProxyServicesEnabled', DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED)
897
902
  this._setString(env, 'aws.dynamoDb.tablePrimaryKeys', DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS)
903
+ this._setArray(env, 'graphqlErrorExtensions', DD_TRACE_GRAPHQL_ERROR_EXTENSIONS)
898
904
  }
899
905
 
900
906
  _applyOptions (options) {
@@ -986,6 +992,7 @@ class Config {
986
992
  this._setString(opts, 'llmobs.mlApp', options.llmobs?.mlApp)
987
993
  this._setBoolean(opts, 'logInjection', options.logInjection)
988
994
  this._setString(opts, 'lookup', options.lookup)
995
+ this._setBoolean(opts, 'middlewareTracingEnabled', options.middlewareTracingEnabled)
989
996
  this._setBoolean(opts, 'openAiLogsEnabled', options.openAiLogsEnabled)
990
997
  this._setValue(opts, 'peerServiceMapping', options.peerServiceMapping)
991
998
  this._setBoolean(opts, 'plugins', options.plugins)
@@ -1020,6 +1027,7 @@ class Config {
1020
1027
  this._setBoolean(opts, 'traceId128BitLoggingEnabled', options.traceId128BitLoggingEnabled)
1021
1028
  this._setString(opts, 'version', options.version || tags.version)
1022
1029
  this._setBoolean(opts, 'inferredProxyServicesEnabled', options.inferredProxyServicesEnabled)
1030
+ this._setBoolean(opts, 'graphqlErrorExtensions', options.graphqlErrorExtensions)
1023
1031
 
1024
1032
  // For LLMObs, we want the environment variable to take precedence over the options.
1025
1033
  // This is reliant on environment config being set before options.
@@ -2,6 +2,8 @@
2
2
 
3
3
  const session = require('./session')
4
4
 
5
+ const WINDOWS_DRIVE_LETTER_REGEX = /[a-zA-Z]/
6
+
5
7
  const scriptIds = []
6
8
  const scriptUrls = new Map()
7
9
 
@@ -10,26 +12,74 @@ module.exports = {
10
12
  breakpoints: new Map(),
11
13
 
12
14
  /**
13
- * Find the matching script that can be inspected based on a partial path.
14
- *
15
- * Algorithm: Find the sortest url that ends in the requested path.
16
- *
17
- * Will identify the correct script as long as Node.js doesn't load a module from a `node_modules` folder outside the
18
- * project root. If so, there's a risk that this path is shorter than the expected path inside the project root.
19
- * Example of mismatch where path = `index.js`:
20
- *
21
- * Expected match: /www/code/my-projects/demo-project1/index.js
22
- * Actual shorter match: /www/node_modules/dd-trace/index.js
15
+ * Find the script to inspect based on a partial or absolute path. Handles both Windows and POSIX paths.
23
16
  *
24
- * To fix this, specify a more unique file path, e.g `demo-project1/index.js` instead of `index.js`
25
- *
26
- * @param {string} path
27
- * @returns {[string, string] | undefined}
17
+ * @param {string} path - Partial or absolute path to match against loaded scripts
18
+ * @returns {[string, string, string | undefined] | null} - Array containing [url, scriptId, sourceMapURL]
19
+ * or null if no match
28
20
  */
29
21
  findScriptFromPartialPath (path) {
30
- return scriptIds
31
- .filter(([url]) => url.endsWith(path))
32
- .sort(([a], [b]) => a.length - b.length)[0]
22
+ if (!path) return null // This shouldn't happen, but better safe than sorry
23
+
24
+ const bestMatch = new Array(3)
25
+ let maxMatchLength = -1
26
+
27
+ for (const [url, scriptId, sourceMapURL] of scriptIds) {
28
+ let i = url.length - 1
29
+ let j = path.length - 1
30
+ let matchLength = 0
31
+ let lastBoundaryPos = -1
32
+ let atBoundary = false
33
+
34
+ // Compare characters from the end
35
+ while (i >= 0 && j >= 0) {
36
+ const urlChar = url[i]
37
+ const pathChar = path[j]
38
+
39
+ // Check if both characters is a path boundary
40
+ const isBoundary = (urlChar === '/' || urlChar === '\\') && (pathChar === '/' || pathChar === '\\' ||
41
+ (j === 1 && pathChar === ':' && WINDOWS_DRIVE_LETTER_REGEX.test(path[0])))
42
+
43
+ // If both are boundaries, or if characters match exactly
44
+ if (isBoundary || urlChar === pathChar) {
45
+ if (isBoundary) {
46
+ lastBoundaryPos = matchLength
47
+ atBoundary = true
48
+ } else {
49
+ atBoundary = false
50
+ }
51
+ matchLength++
52
+ i--
53
+ j--
54
+ } else {
55
+ break
56
+ }
57
+ }
58
+
59
+ // If we've matched the entire path pattern, ensure it starts at a path boundary
60
+ if (j === -1) {
61
+ if (i >= 0) {
62
+ // If there are more characters in the URL, the next one must be a slash
63
+ if (url[i] === '/' || url[i] === '\\') {
64
+ atBoundary = true
65
+ lastBoundaryPos = matchLength
66
+ }
67
+ } else {
68
+ atBoundary = true
69
+ lastBoundaryPos = matchLength
70
+ }
71
+ }
72
+
73
+ // If we found a valid match and it's better than our previous best
74
+ if (atBoundary && lastBoundaryPos !== -1 && lastBoundaryPos > maxMatchLength) {
75
+ maxMatchLength = lastBoundaryPos
76
+ bestMatch[0] = url
77
+ bestMatch[1] = scriptId
78
+ bestMatch[2] = sourceMapURL
79
+ }
80
+ }
81
+
82
+ return maxMatchLength > -1 ? bestMatch : null
33
83
  },
34
84
 
35
85
  getStackFromCallFrames (callFrames) {
@@ -15,7 +15,6 @@ let batch = 0
15
15
  // Internal representation of a trace or span ID.
16
16
  class Identifier {
17
17
  constructor (value, radix = 16) {
18
- this._isUint64BE = true // msgpack-lite compatibility
19
18
  this._buffer = radix === 16
20
19
  ? createBuffer(value)
21
20
  : fromString(value, radix)
@@ -31,7 +30,6 @@ class Identifier {
31
30
  return this._buffer
32
31
  }
33
32
 
34
- // msgpack-lite compatibility
35
33
  toArray () {
36
34
  if (this._buffer.length === 8) {
37
35
  return this._buffer
@@ -139,7 +139,8 @@ module.exports = class PluginManager {
139
139
  memcachedCommandEnabled,
140
140
  ciVisibilityTestSessionName,
141
141
  ciVisAgentlessLogSubmissionEnabled,
142
- isTestDynamicInstrumentationEnabled
142
+ isTestDynamicInstrumentationEnabled,
143
+ middlewareTracingEnabled
143
144
  } = this._tracerConfig
144
145
 
145
146
  const sharedConfig = {
@@ -170,6 +171,13 @@ module.exports = class PluginManager {
170
171
  sharedConfig.clientIpEnabled = clientIpEnabled
171
172
  }
172
173
 
174
+ // For the global setting, we use the name `middlewareTracingEnabled`, but
175
+ // for the plugin-specific setting, we use `middleware`. They mean the same
176
+ // to an individual plugin, so we normalize them here.
177
+ if (middlewareTracingEnabled !== undefined) {
178
+ sharedConfig.middleware = middlewareTracingEnabled
179
+ }
180
+
173
181
  return sharedConfig
174
182
  }
175
183
  }
@@ -0,0 +1,35 @@
1
+ const Sampler = require('../../sampler')
2
+
3
+ const RE_NEWLINE = /\n/g
4
+ const RE_TAB = /\t/g
5
+
6
+ function normalize (text, limit = 128) {
7
+ if (!text) return
8
+ if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
9
+
10
+ text = text
11
+ .replace(RE_NEWLINE, '\\n')
12
+ .replace(RE_TAB, '\\t')
13
+
14
+ if (text.length > limit) {
15
+ return text.substring(0, limit) + '...'
16
+ }
17
+
18
+ return text
19
+ }
20
+
21
+ function isPromptCompletionSampled (sampler) {
22
+ return sampler.isSampled()
23
+ }
24
+
25
+ module.exports = function (integrationName, tracerConfig) {
26
+ const integrationConfig = tracerConfig[integrationName] || {}
27
+ const { spanCharLimit, spanPromptCompletionSampleRate } = integrationConfig
28
+
29
+ const sampler = new Sampler(spanPromptCompletionSampleRate ?? 1.0)
30
+
31
+ return {
32
+ normalize: str => normalize(str, spanCharLimit),
33
+ isPromptCompletionSampled: () => isPromptCompletionSampled(sampler)
34
+ }
35
+ }