dd-trace 5.0.0-pre-471478b → 5.0.0-pre-2fed3ce

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.0.0-pre-471478b",
3
+ "version": "5.0.0-pre-2fed3ce",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -28,7 +28,8 @@ addHook({ name: 'cassandra-driver', versions: ['>=3.0.0'] }, cassandra => {
28
28
  }
29
29
 
30
30
  return asyncResource.runInAsyncScope(() => {
31
- startCh.publish({ keyspace: this.keyspace, query: queries, contactPoints: this.options?.contactPoints })
31
+ const contactPoints = this.options && this.options.contactPoints
32
+ startCh.publish({ keyspace: this.keyspace, query: queries, contactPoints })
32
33
  try {
33
34
  const res = batch.apply(this, arguments)
34
35
  if (typeof res === 'function' || !res) {
@@ -56,7 +57,8 @@ addHook({ name: 'cassandra-driver', versions: ['>=4.4'] }, cassandra => {
56
57
  }
57
58
  const asyncResource = new AsyncResource('bound-anonymous-fn')
58
59
  return asyncResource.runInAsyncScope(() => {
59
- startCh.publish({ keyspace: this.keyspace, query, contactPoints: this.options?.contactPoints })
60
+ const contactPoints = this.options && this.options.contactPoints
61
+ startCh.publish({ keyspace: this.keyspace, query, contactPoints })
60
62
  const promise = _execute.apply(this, arguments)
61
63
 
62
64
  const promiseAsyncResource = new AsyncResource('bound-anonymous-fn')
@@ -88,7 +90,8 @@ addHook({ name: 'cassandra-driver', versions: ['3 - 4.3'] }, cassandra => {
88
90
  }
89
91
 
90
92
  return asyncResource.runInAsyncScope(() => {
91
- startCh.publish({ keyspace: this.keyspace, query, contactPoints: this.options?.contactPoints })
93
+ const contactPoints = this.options && this.options.contactPoints
94
+ startCh.publish({ keyspace: this.keyspace, query, contactPoints })
92
95
 
93
96
  const lastIndex = arguments.length - 1
94
97
  let cb = arguments[lastIndex]
@@ -34,7 +34,7 @@ function createWrapGetConnection (name) {
34
34
  return function wrapRequest (request) {
35
35
  return function () {
36
36
  const connection = request.apply(this, arguments)
37
- if (connectCh.hasSubscribers && connection?.url) {
37
+ if (connectCh.hasSubscribers && connection && connection.url) {
38
38
  connectCh.publish(connection.url)
39
39
  }
40
40
  return connection
@@ -49,7 +49,7 @@ function createWrapSelect () {
49
49
  if (arguments.length === 1) {
50
50
  const cb = arguments[0]
51
51
  arguments[0] = function (err, connection) {
52
- if (connectCh.hasSubscribers && connection?.host) {
52
+ if (connectCh.hasSubscribers && connection && connection.host) {
53
53
  connectCh.publish({ hostname: connection.host.host, port: connection.host.port })
54
54
  }
55
55
  cb(err, connection)
@@ -1,32 +1,27 @@
1
1
  'use strict'
2
2
 
3
3
  const shimmer = require('../../../datadog-shimmer')
4
- const { addHook, channel, AsyncResource } = require('../helpers/instrument')
4
+ const { addHook, channel } = require('../helpers/instrument')
5
5
 
6
6
  const connectChannel = channel('apm:http2:client:connect:start')
7
7
  const startChannel = channel('apm:http2:client:request:start')
8
- const finishChannel = channel('apm:http2:client:request:finish')
8
+ const endChannel = channel('apm:http2:client:request:end')
9
+ const asyncStartChannel = channel('apm:http2:client:request:asyncStart')
10
+ const asyncEndChannel = channel('apm:http2:client:request:asyncEnd')
9
11
  const errorChannel = channel('apm:http2:client:request:error')
10
- const responseChannel = channel('apm:http2:client:response')
11
12
 
12
- function createWrapEmit (requestResource, parentResource) {
13
+ function createWrapEmit (ctx) {
13
14
  return function wrapEmit (emit) {
14
15
  return function (event, arg1) {
15
- requestResource.runInAsyncScope(() => {
16
- switch (event) {
17
- case 'response':
18
- responseChannel.publish(arg1)
19
- break
20
- case 'error':
21
- errorChannel.publish(arg1)
22
- case 'close': // eslint-disable-line no-fallthrough
23
- finishChannel.publish()
24
- break
25
- }
26
- })
16
+ ctx.eventName = event
17
+ ctx.eventData = arg1
27
18
 
28
- return parentResource.runInAsyncScope(() => {
29
- return emit.apply(this, arguments)
19
+ return asyncStartChannel.runStores(ctx, () => {
20
+ try {
21
+ return emit.apply(this, arguments)
22
+ } finally {
23
+ asyncEndChannel.publish(ctx)
24
+ }
30
25
  })
31
26
  }
32
27
  }
@@ -35,17 +30,21 @@ function createWrapEmit (requestResource, parentResource) {
35
30
  function createWrapRequest (authority, options) {
36
31
  return function wrapRequest (request) {
37
32
  return function (headers) {
38
- const parentResource = new AsyncResource('bound-anonymous-fn')
39
- const requestResource = new AsyncResource('bound-anonymous-fn')
33
+ const ctx = { headers, authority, options }
40
34
 
41
- return requestResource.runInAsyncScope(() => {
42
- startChannel.publish({ headers, authority, options })
35
+ return startChannel.runStores(ctx, () => {
36
+ try {
37
+ const req = request.apply(this, arguments)
43
38
 
44
- const req = request.apply(this, arguments)
39
+ shimmer.wrap(req, 'emit', createWrapEmit(ctx))
45
40
 
46
- shimmer.wrap(req, 'emit', createWrapEmit(requestResource, parentResource))
47
-
48
- return req
41
+ return req
42
+ } catch (e) {
43
+ ctx.error = e
44
+ errorChannel.publish(ctx)
45
+ } finally {
46
+ endChannel.publish(ctx)
47
+ }
49
48
  })
50
49
  }
51
50
  }
@@ -16,10 +16,19 @@ const consumerFinishCh = channel('apm:kafkajs:consume:finish')
16
16
  const consumerErrorCh = channel('apm:kafkajs:consume:error')
17
17
 
18
18
  addHook({ name: 'kafkajs', versions: ['>=1.4'] }, (obj) => {
19
- const Kafka = obj.Kafka
19
+ class Kafka extends obj.Kafka {
20
+ constructor (options) {
21
+ super(options)
22
+ this._brokers = (options.brokers && typeof options.brokers !== 'function')
23
+ ? options.brokers.join(',') : undefined
24
+ }
25
+ }
26
+ obj.Kafka = Kafka
27
+
20
28
  shimmer.wrap(Kafka.prototype, 'producer', createProducer => function () {
21
29
  const producer = createProducer.apply(this, arguments)
22
30
  const send = producer.send
31
+ const bootstrapServers = this._brokers
23
32
 
24
33
  producer.send = function () {
25
34
  const innerAsyncResource = new AsyncResource('bound-anonymous-fn')
@@ -36,7 +45,7 @@ addHook({ name: 'kafkajs', versions: ['>=1.4'] }, (obj) => {
36
45
  message.headers = message.headers || {}
37
46
  }
38
47
  }
39
- producerStartCh.publish({ topic, messages })
48
+ producerStartCh.publish({ topic, messages, bootstrapServers })
40
49
 
41
50
  const result = send.apply(this, arguments)
42
51
 
@@ -11,6 +11,8 @@ const startCh = channel('apm:redis:command:start')
11
11
  const finishCh = channel('apm:redis:command:finish')
12
12
  const errorCh = channel('apm:redis:command:error')
13
13
 
14
+ let createClientUrl
15
+
14
16
  function wrapAddCommand (addCommand) {
15
17
  return function (command) {
16
18
  if (!startCh.hasSubscribers) {
@@ -22,7 +24,7 @@ function wrapAddCommand (addCommand) {
22
24
 
23
25
  const asyncResource = new AsyncResource('bound-anonymous-fn')
24
26
  return asyncResource.runInAsyncScope(() => {
25
- start(this, name, args)
27
+ start(this, name, args, this._url)
26
28
 
27
29
  const res = addCommand.apply(this, arguments)
28
30
  const onResolve = asyncResource.bind(() => finish(finishCh, errorCh))
@@ -35,12 +37,53 @@ function wrapAddCommand (addCommand) {
35
37
  }
36
38
  }
37
39
 
38
- addHook({ name: '@redis/client', file: 'dist/lib/client/commands-queue.js', versions: ['>=1.1'] }, redis => {
40
+ function wrapCommandQueueClass (cls) {
41
+ const ret = class RedisCommandQueue extends cls {
42
+ constructor () {
43
+ super(arguments)
44
+ if (createClientUrl) {
45
+ try {
46
+ const parsed = new URL(createClientUrl)
47
+ if (parsed) {
48
+ this._url = { host: parsed.hostname, port: +parsed.port || 6379 }
49
+ }
50
+ } catch (error) {
51
+ // ignore
52
+ }
53
+ }
54
+ this._url = this._url || { host: 'localhost', port: 6379 }
55
+ }
56
+ }
57
+ return ret
58
+ }
59
+
60
+ function wrapCreateClient (request) {
61
+ return function (opts) {
62
+ createClientUrl = opts && opts.url
63
+ const ret = request.apply(this, arguments)
64
+ createClientUrl = undefined
65
+ return ret
66
+ }
67
+ }
68
+
69
+ addHook({ name: '@node-redis/client', file: 'dist/lib/client/commands-queue.js', versions: ['>=1'] }, redis => {
70
+ redis.default = wrapCommandQueueClass(redis.default)
39
71
  shimmer.wrap(redis.default.prototype, 'addCommand', wrapAddCommand)
40
72
  return redis
41
73
  })
42
74
 
43
- addHook({ name: '@node-redis/client', file: 'dist/lib/client/commands-queue.js', versions: ['>=1'] }, redis => {
75
+ addHook({ name: '@node-redis/client', file: 'dist/lib/client/index.js', versions: ['>=1'] }, redis => {
76
+ shimmer.wrap(redis.default, 'create', wrapCreateClient)
77
+ return redis
78
+ })
79
+
80
+ addHook({ name: '@redis/client', file: 'dist/lib/client/index.js', versions: ['>=1.1'] }, redis => {
81
+ shimmer.wrap(redis.default, 'create', wrapCreateClient)
82
+ return redis
83
+ })
84
+
85
+ addHook({ name: '@redis/client', file: 'dist/lib/client/commands-queue.js', versions: ['>=1.1'] }, redis => {
86
+ redis.default = wrapCommandQueueClass(redis.default)
44
87
  shimmer.wrap(redis.default.prototype, 'addCommand', wrapAddCommand)
45
88
  return redis
46
89
  })
@@ -106,9 +149,9 @@ addHook({ name: 'redis', versions: ['>=0.12 <2.6'] }, redis => {
106
149
  return redis
107
150
  })
108
151
 
109
- function start (client, command, args) {
152
+ function start (client, command, args, url = {}) {
110
153
  const db = client.selected_db
111
- const connectionOptions = client.connection_options || client.connection_option || client.connectionOption || {}
154
+ const connectionOptions = client.connection_options || client.connection_option || client.connectionOption || url
112
155
  startCh.publish({ db, command, args, connectionOptions })
113
156
  }
114
157
 
@@ -37,7 +37,7 @@ const CYPRESS_STATUS_TO_TEST_STATUS = {
37
37
  function getTestSpanMetadata (tracer, testName, testSuite, cypressConfig) {
38
38
  const childOf = getTestParentSpan(tracer)
39
39
 
40
- const commonTags = getTestCommonTags(testName, testSuite, cypressConfig.version)
40
+ const commonTags = getTestCommonTags(testName, testSuite, cypressConfig.version, TEST_FRAMEWORK_NAME)
41
41
 
42
42
  return {
43
43
  childOf,
@@ -8,7 +8,6 @@ const log = require('../../dd-trace/src/log')
8
8
  const tags = require('../../../ext/tags')
9
9
  const kinds = require('../../../ext/kinds')
10
10
  const formats = require('../../../ext/formats')
11
- const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
12
11
  const { COMPONENT, CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
13
12
  const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter')
14
13
 
@@ -25,32 +24,11 @@ const HTTP2_HEADER_STATUS = ':status'
25
24
  const HTTP2_METHOD_GET = 'GET'
26
25
 
27
26
  class Http2ClientPlugin extends ClientPlugin {
28
- static get id () {
29
- return 'http2'
30
- }
31
-
32
- constructor (...args) {
33
- super(...args)
34
-
35
- this.addSub('apm:http2:client:response', (headers) => {
36
- const span = storage.getStore().span
37
- const status = headers && headers[HTTP2_HEADER_STATUS]
38
-
39
- span.setTag(HTTP_STATUS_CODE, status)
40
-
41
- if (!this.config.validateStatus(status)) {
42
- this.addError()
43
- }
27
+ static get id () { return 'http2' }
28
+ static get prefix () { return `apm:http2:client:request` }
44
29
 
45
- addHeaderTags(span, headers, HTTP_RESPONSE_HEADERS, this.config)
46
- })
47
- }
48
-
49
- addTraceSub (eventName, handler) {
50
- this.addSub(`apm:${this.constructor.id}:client:${this.operation}:${eventName}`, handler)
51
- }
52
-
53
- start ({ authority, options, headers = {} }) {
30
+ bindStart (message) {
31
+ const { authority, options, headers = {} } = message
54
32
  const sessionDetails = extractSessionDetails(authority, options)
55
33
  const path = headers[HTTP2_HEADER_PATH] || '/'
56
34
  const pathname = path.split(/[?#]/)[0]
@@ -75,7 +53,7 @@ class Http2ClientPlugin extends ClientPlugin {
75
53
  metrics: {
76
54
  [CLIENT_PORT_KEY]: parseInt(sessionDetails.port)
77
55
  }
78
- })
56
+ }, false)
79
57
 
80
58
  // TODO: Figure out a better way to do this for any span.
81
59
  if (!allowed) {
@@ -88,14 +66,53 @@ class Http2ClientPlugin extends ClientPlugin {
88
66
  this.tracer.inject(span, HTTP_HEADERS, headers)
89
67
  }
90
68
 
91
- analyticsSampler.sample(span, this.config.measured)
69
+ message.parentStore = store
70
+ message.currentStore = { ...store, span }
71
+
72
+ return message.currentStore
73
+ }
74
+
75
+ bindAsyncStart ({ eventName, eventData, currentStore, parentStore }) {
76
+ switch (eventName) {
77
+ case 'response':
78
+ this._onResponse(currentStore, eventData)
79
+ return parentStore
80
+ case 'error':
81
+ this._onError(currentStore, eventData)
82
+ return parentStore
83
+ case 'close':
84
+ this._onClose(currentStore, eventData)
85
+ return parentStore
86
+ }
92
87
 
93
- this.enter(span, store)
88
+ return storage.getStore()
94
89
  }
95
90
 
96
91
  configure (config) {
97
92
  return super.configure(normalizeConfig(config))
98
93
  }
94
+
95
+ _onResponse (store, headers) {
96
+ const status = headers && headers[HTTP2_HEADER_STATUS]
97
+
98
+ store.span.setTag(HTTP_STATUS_CODE, status)
99
+
100
+ if (!this.config.validateStatus(status)) {
101
+ storage.run(store, () => this.addError())
102
+ }
103
+
104
+ addHeaderTags(store.span, headers, HTTP_RESPONSE_HEADERS, this.config)
105
+ }
106
+
107
+ _onError ({ span }, error) {
108
+ span.setTag('error', error)
109
+ span.finish()
110
+ }
111
+
112
+ _onClose ({ span }) {
113
+ this.tagPeerService(span)
114
+ span.finish()
115
+ }
99
116
  }
100
117
 
101
118
  function extractSessionDetails (authority, options) {
@@ -2,12 +2,14 @@
2
2
 
3
3
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
4
4
  const { encodePathwayContext } = require('../../dd-trace/src/datastreams/pathway')
5
+ const BOOTSTRAP_SERVERS_KEY = 'messaging.kafka.bootstrap.servers'
5
6
 
6
7
  class KafkajsProducerPlugin extends ProducerPlugin {
7
8
  static get id () { return 'kafkajs' }
8
9
  static get operation () { return 'produce' }
10
+ static get peerServicePrecursors () { return [BOOTSTRAP_SERVERS_KEY] }
9
11
 
10
- start ({ topic, messages }) {
12
+ start ({ topic, messages, bootstrapServers }) {
11
13
  let pathwayCtx
12
14
  if (this.config.dsmEnabled) {
13
15
  const dataStreamsContext = this.tracer
@@ -24,6 +26,9 @@ class KafkajsProducerPlugin extends ProducerPlugin {
24
26
  'kafka.batch_size': messages.length
25
27
  }
26
28
  })
29
+ if (bootstrapServers) {
30
+ span.setTag(BOOTSTRAP_SERVERS_KEY, bootstrapServers)
31
+ }
27
32
  for (const message of messages) {
28
33
  if (typeof message === 'object') {
29
34
  if (this.config.dsmEnabled) message.headers['dd-pathway-ctx'] = pathwayCtx
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const DogStatsDClient = require('../../dd-trace/src/dogstatsd')
3
+ const { DogStatsDClient, NoopDogStatsDClient } = require('../../dd-trace/src/dogstatsd')
4
4
  const ExternalLogger = require('../../dd-trace/src/external-logger/src')
5
5
 
6
6
  const FLUSH_INTERVAL = 10 * 1000
@@ -10,15 +10,19 @@ let logger = null
10
10
  let interval = null
11
11
 
12
12
  module.exports.init = function (tracerConfig) {
13
- metrics = new DogStatsDClient({
14
- host: tracerConfig.dogstatsd.hostname,
15
- port: tracerConfig.dogstatsd.port,
16
- tags: [
17
- `service:${tracerConfig.tags.service}`,
18
- `env:${tracerConfig.tags.env}`,
19
- `version:${tracerConfig.tags.version}`
20
- ]
21
- })
13
+ if (tracerConfig.dogstatsd) {
14
+ metrics = new DogStatsDClient({
15
+ host: tracerConfig.dogstatsd.hostname,
16
+ port: tracerConfig.dogstatsd.port,
17
+ tags: [
18
+ `service:${tracerConfig.tags.service}`,
19
+ `env:${tracerConfig.tags.env}`,
20
+ `version:${tracerConfig.tags.version}`
21
+ ]
22
+ })
23
+ } else {
24
+ metrics = new NoopDogStatsDClient()
25
+ }
22
26
 
23
27
  logger = new ExternalLogger({
24
28
  ddsource: 'openai',
@@ -156,7 +156,7 @@ function isMoreSpecificThan (routeA, routeB) {
156
156
  }
157
157
 
158
158
  function routeIsRegex (route) {
159
- return route.includes('(/') && route.includes('/)')
159
+ return route.includes('(/')
160
160
  }
161
161
 
162
162
  module.exports = RouterPlugin
@@ -143,4 +143,17 @@ class DogStatsDClient {
143
143
  }
144
144
  }
145
145
 
146
- module.exports = DogStatsDClient
146
+ class NoopDogStatsDClient {
147
+ gauge () { }
148
+
149
+ increment () { }
150
+
151
+ distribution () { }
152
+
153
+ flush () { }
154
+ }
155
+
156
+ module.exports = {
157
+ DogStatsDClient,
158
+ NoopDogStatsDClient
159
+ }
@@ -5,7 +5,7 @@
5
5
  const { URL, format } = require('url')
6
6
  const v8 = require('v8')
7
7
  const os = require('os')
8
- const Client = require('./dogstatsd')
8
+ const { DogStatsDClient } = require('./dogstatsd')
9
9
  const log = require('./log')
10
10
  const Histogram = require('./histogram')
11
11
  const { performance } = require('perf_hooks')
@@ -67,7 +67,7 @@ module.exports = {
67
67
  }))
68
68
  }
69
69
 
70
- client = new Client(clientConfig)
70
+ client = new DogStatsDClient(clientConfig)
71
71
 
72
72
  time = process.hrtime()
73
73
 
@@ -111,7 +111,12 @@ module.exports = class CiPlugin extends Plugin {
111
111
  const childOf = getTestParentSpan(this.tracer)
112
112
 
113
113
  let testTags = {
114
- ...getTestCommonTags(testName, testSuite, this.frameworkVersion),
114
+ ...getTestCommonTags(
115
+ testName,
116
+ testSuite,
117
+ this.frameworkVersion,
118
+ this.constructor.id
119
+ ),
115
120
  [COMPONENT]: this.constructor.id,
116
121
  ...extraTags
117
122
  }
@@ -62,13 +62,17 @@ class OutboundPlugin extends TracingPlugin {
62
62
 
63
63
  finish () {
64
64
  const span = this.activeSpan
65
+ this.tagPeerService(span)
66
+ super.finish(...arguments)
67
+ }
68
+
69
+ tagPeerService (span) {
65
70
  if (this.tracer._computePeerService) {
66
71
  const peerData = this.getPeerService(span.context()._tags)
67
72
  if (peerData) {
68
73
  span.addTags(peerData)
69
74
  }
70
75
  }
71
- super.finish(...arguments)
72
76
  }
73
77
 
74
78
  connect (url) {
@@ -25,9 +25,31 @@ class Subscription {
25
25
  }
26
26
  }
27
27
 
28
+ class StoreBinding {
29
+ constructor (event, transform) {
30
+ this._channel = dc.channel(event)
31
+ this._transform = data => {
32
+ const store = storage.getStore()
33
+
34
+ return !store || !store.noop
35
+ ? transform(data)
36
+ : store
37
+ }
38
+ }
39
+
40
+ enable () {
41
+ this._channel.bindStore(storage, this._transform)
42
+ }
43
+
44
+ disable () {
45
+ this._channel.unbindStore(storage, this._transform)
46
+ }
47
+ }
48
+
28
49
  module.exports = class Plugin {
29
50
  constructor (tracer, tracerConfig) {
30
51
  this._subscriptions = []
52
+ this._bindings = []
31
53
  this._enabled = false
32
54
  this._tracer = tracer
33
55
  this.config = {} // plugin-specific configuration, unset until .configure() is called
@@ -53,6 +75,10 @@ module.exports = class Plugin {
53
75
  this._subscriptions.push(new Subscription(channelName, handler))
54
76
  }
55
77
 
78
+ addBind (channelName, transform) {
79
+ this._bindings.push(new StoreBinding(channelName, transform))
80
+ }
81
+
56
82
  addError (error) {
57
83
  const store = storage.getStore()
58
84
 
@@ -71,9 +97,11 @@ module.exports = class Plugin {
71
97
  if (config.enabled && !this._enabled) {
72
98
  this._enabled = true
73
99
  this._subscriptions.forEach(sub => sub.enable())
100
+ this._bindings.forEach(sub => sub.enable())
74
101
  } else if (!config.enabled && this._enabled) {
75
102
  this._enabled = false
76
103
  this._subscriptions.forEach(sub => sub.disable())
104
+ this._bindings.forEach(sub => sub.disable())
77
105
  }
78
106
  }
79
107
  }
@@ -13,17 +13,7 @@ class TracingPlugin extends Plugin {
13
13
  this.component = this.constructor.component || this.constructor.id
14
14
  this.operation = this.constructor.operation
15
15
 
16
- this.addTraceSub('start', message => {
17
- this.start(message)
18
- })
19
-
20
- this.addTraceSub('error', err => {
21
- this.error(err)
22
- })
23
-
24
- this.addTraceSub('finish', message => {
25
- this.finish(message)
26
- })
16
+ this.addTraceSubs()
27
17
  }
28
18
 
29
19
  get activeSpan () {
@@ -65,19 +55,45 @@ class TracingPlugin extends Plugin {
65
55
  this.addError(error)
66
56
  }
67
57
 
58
+ addTraceSubs () {
59
+ const events = ['start', 'end', 'asyncStart', 'asyncEnd', 'error', 'finish']
60
+
61
+ for (const event of events) {
62
+ const bindName = `bind${event.charAt(0).toUpperCase()}${event.slice(1)}`
63
+
64
+ if (this[event]) {
65
+ this.addTraceSub(event, message => {
66
+ this[event](message)
67
+ })
68
+ }
69
+
70
+ if (this[bindName]) {
71
+ this.addTraceBind(event, message => this[bindName](message))
72
+ }
73
+ }
74
+ }
75
+
68
76
  addTraceSub (eventName, handler) {
69
- this.addSub(`apm:${this.component}:${this.operation}:${eventName}`, handler)
77
+ const prefix = this.constructor.prefix || `apm:${this.component}:${this.operation}`
78
+ this.addSub(`${prefix}:${eventName}`, handler)
79
+ }
80
+
81
+ addTraceBind (eventName, transform) {
82
+ const prefix = this.constructor.prefix || `apm:${this.component}:${this.operation}`
83
+ this.addBind(`${prefix}:${eventName}`, transform)
70
84
  }
71
85
 
72
86
  addError (error) {
73
87
  const span = this.activeSpan
74
88
 
75
89
  if (!span._spanContext._tags['error']) {
90
+ // Errors may be wrapped in a context.
91
+ error = (error && error.error) || error
76
92
  span.setTag('error', error || 1)
77
93
  }
78
94
  }
79
95
 
80
- startSpan (name, { childOf, kind, meta, metrics, service, resource, type } = {}) {
96
+ startSpan (name, { childOf, kind, meta, metrics, service, resource, type } = {}, enter = true) {
81
97
  const store = storage.getStore()
82
98
 
83
99
  if (store && childOf === undefined) {
@@ -100,7 +116,10 @@ class TracingPlugin extends Plugin {
100
116
 
101
117
  analyticsSampler.sample(span, this.config.measured)
102
118
 
103
- storage.enterWith({ ...store, span })
119
+ // TODO: Remove this after migration to TracingChannel is done.
120
+ if (enter) {
121
+ storage.enterWith({ ...store, span })
122
+ }
104
123
 
105
124
  return span
106
125
  }
@@ -77,7 +77,7 @@ function filterSensitiveInfoFromRepository (repositoryUrl) {
77
77
 
78
78
  return `${protocol}//${hostname}${pathname}`
79
79
  } catch (e) {
80
- return repositoryUrl
80
+ return ''
81
81
  }
82
82
  }
83
83
 
@@ -1,11 +1,13 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
+ const { URL } = require('url')
4
+ const log = require('../../log')
3
5
 
4
6
  const istanbul = require('istanbul-lib-coverage')
5
7
  const ignore = require('ignore')
6
8
 
7
9
  const { getGitMetadata } = require('./git')
8
- const { getUserProviderGitMetadata } = require('./user-provided-git')
10
+ const { getUserProviderGitMetadata, validateGitRepositoryUrl, validateGitCommitSha } = require('./user-provided-git')
9
11
  const { getCIMetadata } = require('./ci')
10
12
  const { getRuntimeAndOSMetadata } = require('./env')
11
13
  const {
@@ -16,7 +18,8 @@ const {
16
18
  GIT_COMMIT_AUTHOR_EMAIL,
17
19
  GIT_COMMIT_AUTHOR_NAME,
18
20
  GIT_COMMIT_MESSAGE,
19
- CI_WORKSPACE_PATH
21
+ CI_WORKSPACE_PATH,
22
+ CI_PIPELINE_URL
20
23
  } = require('./tags')
21
24
  const id = require('../../id')
22
25
 
@@ -104,7 +107,8 @@ module.exports = {
104
107
  mergeCoverage,
105
108
  fromCoverageMapToCoverage,
106
109
  getTestLineStart,
107
- getCallSites
110
+ getCallSites,
111
+ removeInvalidMetadata
108
112
  }
109
113
 
110
114
  // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
@@ -116,6 +120,39 @@ function getPkgManager () {
116
120
  }
117
121
  }
118
122
 
123
+ function validateUrl (url) {
124
+ try {
125
+ const urlObject = new URL(url)
126
+ return (urlObject.protocol === 'https:' || urlObject.protocol === 'http:')
127
+ } catch (e) {
128
+ return false
129
+ }
130
+ }
131
+
132
+ function removeInvalidMetadata (metadata) {
133
+ return Object.keys(metadata).reduce((filteredTags, tag) => {
134
+ if (tag === GIT_REPOSITORY_URL) {
135
+ if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
136
+ log.error('DD_GIT_REPOSITORY_URL must be a valid URL')
137
+ return filteredTags
138
+ }
139
+ }
140
+ if (tag === GIT_COMMIT_SHA) {
141
+ if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
142
+ log.error('DD_GIT_COMMIT_SHA must be a full-length git SHA')
143
+ return filteredTags
144
+ }
145
+ }
146
+ if (tag === CI_PIPELINE_URL) {
147
+ if (!validateUrl(metadata[CI_PIPELINE_URL])) {
148
+ return filteredTags
149
+ }
150
+ }
151
+ filteredTags[tag] = metadata[tag]
152
+ return filteredTags
153
+ }, {})
154
+ }
155
+
119
156
  function getTestEnvironmentMetadata (testFramework, config) {
120
157
  // TODO: eventually these will come from the tracer (generally available)
121
158
  const ciMetadata = getCIMetadata()
@@ -155,7 +192,7 @@ function getTestEnvironmentMetadata (testFramework, config) {
155
192
  if (config && config.service) {
156
193
  metadata['service.name'] = config.service
157
194
  }
158
- return metadata
195
+ return removeInvalidMetadata(metadata)
159
196
  }
160
197
 
161
198
  function getTestParametersString (parametersByTestName, testName) {
@@ -173,6 +210,13 @@ function getTestParametersString (parametersByTestName, testName) {
173
210
  }
174
211
  }
175
212
 
213
+ function getTestTypeFromFramework (testFramework) {
214
+ if (testFramework === 'playwright' || testFramework === 'cypress') {
215
+ return 'browser'
216
+ }
217
+ return 'test'
218
+ }
219
+
176
220
  function finishAllTraceSpans (span) {
177
221
  span.context()._trace.started.forEach(traceSpan => {
178
222
  if (traceSpan !== span) {
@@ -188,10 +232,10 @@ function getTestParentSpan (tracer) {
188
232
  })
189
233
  }
190
234
 
191
- function getTestCommonTags (name, suite, version) {
235
+ function getTestCommonTags (name, suite, version, testFramework) {
192
236
  return {
193
237
  [SPAN_TYPE]: 'test',
194
- [TEST_TYPE]: 'test',
238
+ [TEST_TYPE]: getTestTypeFromFramework(testFramework),
195
239
  [SAMPLING_RULE_DECISION]: 1,
196
240
  [SAMPLING_PRIORITY]: AUTO_KEEP,
197
241
  [TEST_NAME]: name,
@@ -269,12 +313,12 @@ function getCodeOwnersForFilename (filename, entries) {
269
313
  return null
270
314
  }
271
315
 
272
- function getTestLevelCommonTags (command, testFrameworkVersion) {
316
+ function getTestLevelCommonTags (command, testFrameworkVersion, testFramework) {
273
317
  return {
274
318
  [TEST_FRAMEWORK_VERSION]: testFrameworkVersion,
275
319
  [LIBRARY_VERSION]: ddTraceVersion,
276
320
  [TEST_COMMAND]: command,
277
- [TEST_TYPE]: 'test'
321
+ [TEST_TYPE]: getTestTypeFromFramework(testFramework)
278
322
  }
279
323
  }
280
324
 
@@ -284,7 +328,7 @@ function getTestSessionCommonTags (command, testFrameworkVersion, testFramework)
284
328
  [RESOURCE_NAME]: `test_session.${command}`,
285
329
  [TEST_MODULE]: testFramework,
286
330
  [TEST_TOOLCHAIN]: getPkgManager(),
287
- ...getTestLevelCommonTags(command, testFrameworkVersion)
331
+ ...getTestLevelCommonTags(command, testFrameworkVersion, testFramework)
288
332
  }
289
333
  }
290
334
 
@@ -293,7 +337,7 @@ function getTestModuleCommonTags (command, testFrameworkVersion, testFramework)
293
337
  [SPAN_TYPE]: 'test_module_end',
294
338
  [RESOURCE_NAME]: `test_module.${command}`,
295
339
  [TEST_MODULE]: testFramework,
296
- ...getTestLevelCommonTags(command, testFrameworkVersion)
340
+ ...getTestLevelCommonTags(command, testFrameworkVersion, testFramework)
297
341
  }
298
342
  }
299
343
 
@@ -303,7 +347,7 @@ function getTestSuiteCommonTags (command, testFrameworkVersion, testSuite, testF
303
347
  [RESOURCE_NAME]: `test_suite.${testSuite}`,
304
348
  [TEST_MODULE]: testFramework,
305
349
  [TEST_SUITE]: testSuite,
306
- ...getTestLevelCommonTags(command, testFrameworkVersion)
350
+ ...getTestLevelCommonTags(command, testFrameworkVersion, testFramework)
307
351
  }
308
352
  }
309
353
 
@@ -13,7 +13,6 @@ const {
13
13
  } = require('./tags')
14
14
 
15
15
  const { normalizeRef } = require('./ci')
16
- const log = require('../../log')
17
16
  const { URL } = require('url')
18
17
 
19
18
  function removeEmptyValues (tags) {
@@ -53,25 +52,6 @@ function validateGitCommitSha (gitCommitSha) {
53
52
  return isValidSha1 || isValidSha256
54
53
  }
55
54
 
56
- function removeInvalidGitMetadata (metadata) {
57
- return Object.keys(metadata).reduce((filteredTags, tag) => {
58
- if (tag === GIT_REPOSITORY_URL) {
59
- if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
60
- log.error('DD_GIT_REPOSITORY_URL must be a valid URL')
61
- return filteredTags
62
- }
63
- }
64
- if (tag === GIT_COMMIT_SHA) {
65
- if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
66
- log.error('DD_GIT_COMMIT_SHA must be a full-length git SHA')
67
- return filteredTags
68
- }
69
- }
70
- filteredTags[tag] = metadata[tag]
71
- return filteredTags
72
- }, {})
73
- }
74
-
75
55
  function getUserProviderGitMetadata () {
76
56
  const {
77
57
  DD_GIT_COMMIT_SHA,
@@ -95,7 +75,7 @@ function getUserProviderGitMetadata () {
95
75
  tag = normalizeRef(DD_GIT_BRANCH)
96
76
  }
97
77
 
98
- const metadata = removeEmptyValues({
78
+ return removeEmptyValues({
99
79
  [GIT_COMMIT_SHA]: DD_GIT_COMMIT_SHA,
100
80
  [GIT_BRANCH]: branch,
101
81
  [GIT_REPOSITORY_URL]: filterSensitiveInfoFromRepository(DD_GIT_REPOSITORY_URL),
@@ -108,7 +88,6 @@ function getUserProviderGitMetadata () {
108
88
  [GIT_COMMIT_AUTHOR_EMAIL]: DD_GIT_COMMIT_AUTHOR_EMAIL,
109
89
  [GIT_COMMIT_AUTHOR_DATE]: DD_GIT_COMMIT_AUTHOR_DATE
110
90
  })
111
- return removeInvalidGitMetadata(metadata)
112
91
  }
113
92
 
114
93
  module.exports = { getUserProviderGitMetadata, validateGitRepositoryUrl, validateGitCommitSha }
@@ -1,9 +1,10 @@
1
1
  const { identityService } = require('../util')
2
+ const { DD_MAJOR } = require('../../../../../../version')
2
3
 
3
4
  const web = {
4
5
  client: {
5
6
  grpc: {
6
- opName: () => 'grpc.client',
7
+ opName: () => DD_MAJOR <= 2 ? 'grpc.request' : 'grpc.client',
7
8
  serviceName: identityService
8
9
  },
9
10
  moleculer: {
@@ -13,7 +14,7 @@ const web = {
13
14
  },
14
15
  server: {
15
16
  grpc: {
16
- opName: () => 'grpc.server',
17
+ opName: () => DD_MAJOR <= 2 ? 'grpc.request' : 'grpc.server',
17
18
  serviceName: identityService
18
19
  },
19
20
  moleculer: {
@@ -89,6 +89,26 @@ class CountMetric extends Metric {
89
89
  }
90
90
  }
91
91
 
92
+ class DistributionMetric extends Metric {
93
+ get type () {
94
+ return 'distribution'
95
+ }
96
+
97
+ track (value = 1) {
98
+ this.points.push(value)
99
+ }
100
+
101
+ toJSON () {
102
+ const { metric, points, tags, common } = this
103
+ return {
104
+ metric,
105
+ points,
106
+ common,
107
+ tags
108
+ }
109
+ }
110
+ }
111
+
92
112
  class GaugeMetric extends Metric {
93
113
  get type () {
94
114
  return 'gauge'
@@ -129,11 +149,12 @@ class RateMetric extends Metric {
129
149
 
130
150
  const metricsTypes = {
131
151
  count: CountMetric,
152
+ distribution: DistributionMetric,
132
153
  gauge: GaugeMetric,
133
154
  rate: RateMetric
134
155
  }
135
156
 
136
- class Namespace extends Map {
157
+ class MetricsCollection extends Map {
137
158
  constructor (namespace) {
138
159
  super()
139
160
  this.namespace = namespace
@@ -146,43 +167,68 @@ class Namespace extends Map {
146
167
  }
147
168
 
148
169
  toString () {
149
- return `dd.instrumentation_telemetry_data.${this.namespace}`
170
+ return this.namespace
171
+ }
172
+
173
+ toJSON () {
174
+ if (!this.size) return
175
+ const { namespace } = this
176
+ return {
177
+ namespace,
178
+ series: mapToJsonArray(this)
179
+ }
150
180
  }
181
+ }
151
182
 
152
- getMetric (type, name, tags, interval) {
153
- const metricId = getId(type, this, name, tags)
183
+ function getMetric (collection, type, name, tags, interval) {
184
+ const metricId = getId(type, collection, name, tags)
154
185
 
155
- let metric = this.get(metricId)
156
- if (metric) return metric
186
+ let metric = collection.get(metricId)
187
+ if (metric) return metric
157
188
 
158
- const Factory = metricsTypes[type]
159
- if (!Factory) {
160
- throw new Error(`Unknown metric type ${type}`)
161
- }
189
+ const Factory = metricsTypes[type]
190
+ if (!Factory) {
191
+ throw new Error(`Unknown metric type ${type}`)
192
+ }
193
+
194
+ metric = new Factory(collection, name, true, tags, interval)
195
+ collection.set(metricId, metric)
162
196
 
163
- metric = new Factory(this, name, true, tags, interval)
164
- this.set(metricId, metric)
197
+ return metric
198
+ }
165
199
 
166
- return metric
200
+ class Namespace {
201
+ constructor (namespace) {
202
+ this.distributions = new MetricsCollection(namespace)
203
+ this.metrics = new MetricsCollection(namespace)
204
+ }
205
+
206
+ reset () {
207
+ this.metrics.reset()
208
+ this.distributions.reset()
167
209
  }
168
210
 
169
211
  count (name, tags) {
170
- return this.getMetric('count', name, tags)
212
+ return getMetric(this.metrics, 'count', name, tags)
171
213
  }
172
214
 
173
215
  gauge (name, tags) {
174
- return this.getMetric('gauge', name, tags)
216
+ return getMetric(this.metrics, 'gauge', name, tags)
175
217
  }
176
218
 
177
219
  rate (name, interval, tags) {
178
- return this.getMetric('rate', name, tags, interval)
220
+ return getMetric(this.metrics, 'rate', name, tags, interval)
221
+ }
222
+
223
+ distribution (name, tags) {
224
+ return getMetric(this.distributions, 'distribution', name, tags)
179
225
  }
180
226
 
181
227
  toJSON () {
182
- const { namespace } = this
228
+ const { distributions, metrics } = this
183
229
  return {
184
- namespace,
185
- series: mapToJsonArray(this)
230
+ distributions: distributions.toJSON(),
231
+ metrics: metrics.toJSON()
186
232
  }
187
233
  }
188
234
  }
@@ -203,7 +249,15 @@ class NamespaceManager extends Map {
203
249
 
204
250
  send (config, application, host) {
205
251
  for (const namespace of this.values()) {
206
- sendData(config, application, host, 'generate-metrics', namespace.toJSON())
252
+ const { metrics, distributions } = namespace.toJSON()
253
+
254
+ if (metrics) {
255
+ sendData(config, application, host, 'generate-metrics', metrics)
256
+ }
257
+
258
+ if (distributions) {
259
+ sendData(config, application, host, 'distributions', distributions)
260
+ }
207
261
 
208
262
  // TODO: This could also be clear() but then it'd have to rebuild all
209
263
  // metric instances on every send. This may be desirable if we want tags
@@ -217,8 +271,10 @@ const manager = new NamespaceManager()
217
271
 
218
272
  module.exports = {
219
273
  CountMetric,
274
+ DistributionMetric,
220
275
  GaugeMetric,
221
276
  RateMetric,
277
+ MetricsCollection,
222
278
  Namespace,
223
279
  NamespaceManager,
224
280
  manager
@@ -54,4 +54,68 @@ if (major === '19' && minor === '9') {
54
54
  }
55
55
  }
56
56
 
57
+ if (!Channel.prototype.runStores) {
58
+ const ActiveChannelPrototype = getActiveChannelPrototype()
59
+
60
+ Channel.prototype.bindStore = ActiveChannelPrototype.bindStore = function (store, transform) {
61
+ if (!this._stores) {
62
+ this._stores = new Map()
63
+ }
64
+ this._stores.set(store, transform)
65
+ }
66
+
67
+ Channel.prototype.unbindStore = ActiveChannelPrototype.runStores = function (store) {
68
+ if (!this._stores) return
69
+ this._stores.delete(store)
70
+ }
71
+
72
+ Channel.prototype.runStores = ActiveChannelPrototype.runStores = function (data, fn, thisArg, ...args) {
73
+ if (!this._stores) return Reflect.apply(fn, thisArg, args)
74
+
75
+ let run = () => {
76
+ this.publish(data)
77
+ return Reflect.apply(fn, thisArg, args)
78
+ }
79
+
80
+ for (const entry of this._stores.entries()) {
81
+ const store = entry[0]
82
+ const transform = entry[1]
83
+ run = wrapStoreRun(store, data, run, transform)
84
+ }
85
+
86
+ return run()
87
+ }
88
+ }
89
+
90
+ function defaultTransform (data) {
91
+ return data
92
+ }
93
+
94
+ function wrapStoreRun (store, data, next, transform = defaultTransform) {
95
+ return () => {
96
+ let context
97
+ try {
98
+ context = transform(data)
99
+ } catch (err) {
100
+ process.nextTick(() => {
101
+ throw err
102
+ })
103
+ return next()
104
+ }
105
+
106
+ return store.run(context, next)
107
+ }
108
+ }
109
+
110
+ function getActiveChannelPrototype () {
111
+ const dummyChannel = channel('foo')
112
+ const listener = () => {}
113
+
114
+ dummyChannel.subscribe(listener)
115
+ const ActiveChannelPrototype = Object.getPrototypeOf(dummyChannel)
116
+ dummyChannel.unsubscribe(listener)
117
+
118
+ return ActiveChannelPrototype
119
+ }
120
+
57
121
  module.exports = dc