dd-trace 4.46.0 → 4.47.1

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 (89) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +20 -8
  3. package/package.json +9 -3
  4. package/packages/datadog-instrumentations/src/cucumber.js +290 -53
  5. package/packages/datadog-instrumentations/src/jest.js +3 -1
  6. package/packages/datadog-instrumentations/src/kafkajs.js +67 -31
  7. package/packages/datadog-instrumentations/src/microgateway-core.js +3 -1
  8. package/packages/datadog-instrumentations/src/mocha/main.js +139 -54
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -15
  10. package/packages/datadog-instrumentations/src/mocha/worker.js +29 -1
  11. package/packages/datadog-instrumentations/src/openai.js +4 -2
  12. package/packages/datadog-instrumentations/src/pg.js +59 -4
  13. package/packages/datadog-instrumentations/src/vitest.js +184 -9
  14. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +33 -0
  16. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  17. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  19. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  20. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +36 -6
  21. package/packages/datadog-plugin-cypress/src/support.js +4 -1
  22. package/packages/datadog-plugin-http/src/client.js +1 -42
  23. package/packages/datadog-plugin-http2/src/client.js +1 -26
  24. package/packages/datadog-plugin-jest/src/index.js +17 -1
  25. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +20 -0
  26. package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -2
  27. package/packages/datadog-plugin-kafkajs/src/index.js +3 -1
  28. package/packages/datadog-plugin-mocha/src/index.js +18 -0
  29. package/packages/datadog-plugin-openai/src/index.js +27 -18
  30. package/packages/datadog-plugin-playwright/src/index.js +9 -0
  31. package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
  32. package/packages/datadog-plugin-vitest/src/index.js +68 -3
  33. package/packages/dd-trace/src/appsec/addresses.js +3 -1
  34. package/packages/dd-trace/src/appsec/channels.js +4 -2
  35. package/packages/dd-trace/src/appsec/rasp/index.js +103 -0
  36. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +86 -0
  37. package/packages/dd-trace/src/appsec/rasp/ssrf.js +37 -0
  38. package/packages/dd-trace/src/appsec/rasp/utils.js +63 -0
  39. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
  40. package/packages/dd-trace/src/appsec/remote_config/index.js +16 -7
  41. package/packages/dd-trace/src/appsec/remote_config/manager.js +89 -51
  42. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +33 -14
  43. package/packages/dd-trace/src/appsec/waf/waf_manager.js +2 -1
  44. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -0
  46. package/packages/dd-trace/src/config.js +61 -10
  47. package/packages/dd-trace/src/constants.js +11 -1
  48. package/packages/dd-trace/src/data_streams_context.js +3 -0
  49. package/packages/dd-trace/src/datastreams/fnv.js +23 -0
  50. package/packages/dd-trace/src/datastreams/pathway.js +12 -5
  51. package/packages/dd-trace/src/datastreams/processor.js +35 -0
  52. package/packages/dd-trace/src/datastreams/schemas/schema.js +8 -0
  53. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +125 -0
  54. package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +29 -0
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +24 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/index.js +57 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/inspector_promises_polyfill.js +23 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +164 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/send.js +28 -0
  60. package/packages/dd-trace/src/debugger/devtools_client/session.js +7 -0
  61. package/packages/dd-trace/src/debugger/devtools_client/state.js +47 -0
  62. package/packages/dd-trace/src/debugger/devtools_client/status.js +109 -0
  63. package/packages/dd-trace/src/debugger/index.js +92 -0
  64. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +29 -2
  65. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  66. package/packages/dd-trace/src/payload-tagging/config/aws.json +30 -0
  67. package/packages/dd-trace/src/payload-tagging/config/index.js +30 -0
  68. package/packages/dd-trace/src/payload-tagging/index.js +93 -0
  69. package/packages/dd-trace/src/payload-tagging/tagging.js +83 -0
  70. package/packages/dd-trace/src/plugin_manager.js +11 -10
  71. package/packages/dd-trace/src/plugins/ci_plugin.js +33 -8
  72. package/packages/dd-trace/src/plugins/util/env.js +5 -2
  73. package/packages/dd-trace/src/plugins/util/test.js +26 -2
  74. package/packages/dd-trace/src/profiling/config.js +5 -0
  75. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  76. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns.js +13 -0
  77. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookup.js +16 -0
  78. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookupservice.js +16 -0
  79. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_resolve.js +24 -0
  80. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_reverse.js +16 -0
  81. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +48 -0
  82. package/packages/dd-trace/src/profiling/profilers/event_plugins/net.js +24 -0
  83. package/packages/dd-trace/src/profiling/profilers/events.js +108 -32
  84. package/packages/dd-trace/src/profiling/profilers/shared.js +5 -0
  85. package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
  86. package/packages/dd-trace/src/profiling/ssi-heuristics.js +10 -2
  87. package/packages/dd-trace/src/proxy.js +10 -3
  88. package/packages/dd-trace/src/span_stats.js +4 -2
  89. package/packages/dd-trace/src/appsec/rasp.js +0 -176
@@ -0,0 +1,164 @@
1
+ 'use strict'
2
+
3
+ const { workerData: { rcPort } } = require('node:worker_threads')
4
+ const { getScript, probes, breakpoints } = require('./state')
5
+ const session = require('./session')
6
+ const { ackReceived, ackInstalled, ackError } = require('./status')
7
+ const log = require('../../log')
8
+
9
+ let sessionStarted = false
10
+
11
+ // Example log line probe (simplified):
12
+ // {
13
+ // id: '100c9a5c-45ad-49dc-818b-c570d31e11d1',
14
+ // version: 0,
15
+ // type: 'LOG_PROBE',
16
+ // where: { sourceFile: 'index.js', lines: ['25'] }, // only use first array element
17
+ // template: 'Hello World 2',
18
+ // segments: [...],
19
+ // captureSnapshot: true,
20
+ // capture: { maxReferenceDepth: 1 },
21
+ // sampling: { snapshotsPerSecond: 1 },
22
+ // evaluateAt: 'EXIT' // only used for method probes
23
+ // }
24
+ //
25
+ // Example log method probe (simplified):
26
+ // {
27
+ // id: 'd692ee6d-5734-4df7-9d86-e3bc6449cc8c',
28
+ // version: 0,
29
+ // type: 'LOG_PROBE',
30
+ // where: { typeName: 'index.js', methodName: 'handlerA' },
31
+ // template: 'Executed index.js.handlerA, it took {@duration}ms',
32
+ // segments: [...],
33
+ // captureSnapshot: false,
34
+ // capture: { maxReferenceDepth: 3 },
35
+ // sampling: { snapshotsPerSecond: 5000 },
36
+ // evaluateAt: 'EXIT' // only used for method probes
37
+ // }
38
+ rcPort.on('message', async ({ action, conf: probe, ackId }) => {
39
+ try {
40
+ await processMsg(action, probe)
41
+ rcPort.postMessage({ ackId })
42
+ } catch (err) {
43
+ rcPort.postMessage({ ackId, error: err })
44
+ ackError(err, probe)
45
+ }
46
+ })
47
+ rcPort.on('messageerror', (err) => log.error(err))
48
+
49
+ async function start () {
50
+ sessionStarted = true
51
+ return session.post('Debugger.enable') // return instead of await to reduce number of promises created
52
+ }
53
+
54
+ async function stop () {
55
+ sessionStarted = false
56
+ return session.post('Debugger.disable') // return instead of await to reduce number of promises created
57
+ }
58
+
59
+ async function processMsg (action, probe) {
60
+ log.debug(`Received request to ${action} ${probe.type} probe (id: ${probe.id}, version: ${probe.version})`)
61
+
62
+ if (action !== 'unapply') ackReceived(probe)
63
+
64
+ if (probe.type !== 'LOG_PROBE') {
65
+ throw new Error(`Unsupported probe type: ${probe.type} (id: ${probe.id}, version: ${probe.version})`)
66
+ }
67
+ if (!probe.where.sourceFile && !probe.where.lines) {
68
+ throw new Error(
69
+ // eslint-disable-next-line max-len
70
+ `Unsupported probe insertion point! Only line-based probes are supported (id: ${probe.id}, version: ${probe.version})`
71
+ )
72
+ }
73
+
74
+ // This lock is to ensure that we don't get the following race condition:
75
+ //
76
+ // When a breakpoint is being removed and there are no other breakpoints, we disable the debugger by calling
77
+ // `Debugger.disable` to free resources. However, if a new breakpoint is being added around the same time, we might
78
+ // have a race condition where the new breakpoint thinks that the debugger is already enabled because the removal of
79
+ // the other breakpoint hasn't had a chance to call `Debugger.disable` yet. Then once the code that's adding the new
80
+ // breakpoints tries to call `Debugger.setBreakpoint` it fails because in the meantime `Debugger.disable` was called.
81
+ //
82
+ // If the code is ever refactored to not tear down the debugger if there's no active breakpoints, we can safely remove
83
+ // this lock.
84
+ const release = await lock()
85
+
86
+ try {
87
+ switch (action) {
88
+ case 'unapply':
89
+ await removeBreakpoint(probe)
90
+ break
91
+ case 'apply':
92
+ await addBreakpoint(probe)
93
+ break
94
+ case 'modify':
95
+ // TODO: Can we modify in place?
96
+ await removeBreakpoint(probe)
97
+ await addBreakpoint(probe)
98
+ break
99
+ default:
100
+ throw new Error(
101
+ // eslint-disable-next-line max-len
102
+ `Cannot process probe ${probe.id} (version: ${probe.version}) - unknown remote configuration action: ${action}`
103
+ )
104
+ }
105
+ } finally {
106
+ release()
107
+ }
108
+ }
109
+
110
+ async function addBreakpoint (probe) {
111
+ if (!sessionStarted) await start()
112
+
113
+ const file = probe.where.sourceFile
114
+ const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints
115
+
116
+ // Optimize for sending data to /debugger/v1/input endpoint
117
+ probe.location = { file, lines: [line] }
118
+ delete probe.where
119
+
120
+ // TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached.
121
+ // Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will
122
+ // not continue untill all scripts have been parsed?
123
+ const script = getScript(file)
124
+ if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`)
125
+ const [path, scriptId] = script
126
+
127
+ log.debug(`Adding breakpoint at ${path}:${line} (probe: ${probe.id}, version: ${probe.version})`)
128
+
129
+ const { breakpointId } = await session.post('Debugger.setBreakpoint', {
130
+ location: {
131
+ scriptId,
132
+ lineNumber: line - 1 // Beware! lineNumber is zero-indexed
133
+ }
134
+ })
135
+
136
+ probes.set(probe.id, breakpointId)
137
+ breakpoints.set(breakpointId, probe)
138
+
139
+ ackInstalled(probe)
140
+ }
141
+
142
+ async function removeBreakpoint ({ id }) {
143
+ if (!sessionStarted) {
144
+ // We should not get in this state, but abort if we do, so the code doesn't fail unexpected
145
+ throw Error(`Cannot remove probe ${id}: Debugger not started`)
146
+ }
147
+ if (!probes.has(id)) {
148
+ throw Error(`Unknown probe id: ${id}`)
149
+ }
150
+
151
+ const breakpointId = probes.get(id)
152
+ await session.post('Debugger.removeBreakpoint', { breakpointId })
153
+ probes.delete(id)
154
+ breakpoints.delete(breakpointId)
155
+
156
+ if (breakpoints.size === 0) await stop()
157
+ }
158
+
159
+ async function lock () {
160
+ if (lock.p) await lock.p
161
+ let resolve
162
+ lock.p = new Promise((_resolve) => { resolve = _resolve }).then(() => { lock.p = null })
163
+ return resolve
164
+ }
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ const config = require('./config')
4
+ const request = require('../../exporters/common/request')
5
+
6
+ module.exports = send
7
+
8
+ const ddsource = 'dd_debugger'
9
+ const service = config.service
10
+
11
+ function send (message, logger, snapshot, cb) {
12
+ const opts = {
13
+ method: 'POST',
14
+ url: config.url,
15
+ path: '/debugger/v1/input',
16
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
17
+ }
18
+
19
+ const payload = {
20
+ ddsource,
21
+ service,
22
+ message,
23
+ logger,
24
+ 'debugger.snapshot': snapshot
25
+ }
26
+
27
+ request(JSON.stringify(payload), opts, cb)
28
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const inspector = require('./inspector_promises_polyfill')
4
+
5
+ const session = module.exports = new inspector.Session()
6
+
7
+ session.connectToMainThread()
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const session = require('./session')
4
+
5
+ const scripts = []
6
+
7
+ module.exports = {
8
+ probes: new Map(),
9
+ breakpoints: new Map(),
10
+
11
+ /**
12
+ * Find the matching script that can be inspected based on a partial path.
13
+ *
14
+ * Algorithm: Find the sortest url that ends in the requested path.
15
+ *
16
+ * Will identify the correct script as long as Node.js doesn't load a module from a `node_modules` folder outside the
17
+ * project root. If so, there's a risk that this path is shorter than the expected path inside the project root.
18
+ * Example of mismatch where path = `index.js`:
19
+ *
20
+ * Expected match: /www/code/my-projects/demo-project1/index.js
21
+ * Actual shorter match: /www/node_modules/dd-trace/index.js
22
+ *
23
+ * To fix this, specify a more unique file path, e.g `demo-project1/index.js` instead of `index.js`
24
+ *
25
+ * @param {string} path
26
+ * @returns {[string, string] | undefined}
27
+ */
28
+ getScript (path) {
29
+ return scripts
30
+ .filter(([url]) => url.endsWith(path))
31
+ .sort(([a], [b]) => a.length - b.length)[0]
32
+ }
33
+ }
34
+
35
+ // Known params.url protocols:
36
+ // - `node:` - Ignored, as we don't want to instrument Node.js internals
37
+ // - `wasm:` - Ignored, as we don't support instrumenting WebAssembly
38
+ // - `file:` - Regular on-disk file
39
+ // Unknown params.url values:
40
+ // - `structured-stack` - Not sure what this is, but should just be ignored
41
+ // - `` - Not sure what this is, but should just be ignored
42
+ // TODO: Event fired for all files, every time debugger is enabled. So when we disable it, we need to reset the state
43
+ session.on('Debugger.scriptParsed', ({ params }) => {
44
+ if (params.url.startsWith('file:')) {
45
+ scripts.push([params.url, params.scriptId])
46
+ }
47
+ })
@@ -0,0 +1,109 @@
1
+ 'use strict'
2
+
3
+ const LRUCache = require('lru-cache')
4
+ const config = require('./config')
5
+ const request = require('../../exporters/common/request')
6
+ const FormData = require('../../exporters/common/form-data')
7
+ const log = require('../../log')
8
+
9
+ module.exports = {
10
+ ackReceived,
11
+ ackInstalled,
12
+ ackEmitting,
13
+ ackError
14
+ }
15
+
16
+ const ddsource = 'dd_debugger'
17
+ const service = config.service
18
+ const runtimeId = config.runtimeId
19
+
20
+ const cache = new LRUCache({
21
+ ttl: 1000 * 60 * 60, // 1 hour
22
+ // Unfortunate requirement when using LRUCache:
23
+ // It will emit a warning unless `ttlAutopurge`, `max`, or `maxSize` is set when using `ttl`.
24
+ // TODO: Consider alternative as this is NOT performant :(
25
+ ttlAutopurge: true
26
+ })
27
+
28
+ const STATUSES = {
29
+ RECEIVED: 'RECEIVED',
30
+ INSTALLED: 'INSTALLED',
31
+ EMITTING: 'EMITTING',
32
+ ERROR: 'ERROR',
33
+ BLOCKED: 'BLOCKED' // TODO: Implement once support for allow list, deny list or max probe limit has been added
34
+ }
35
+
36
+ function ackReceived ({ id: probeId, version }) {
37
+ onlyUniqueUpdates(
38
+ STATUSES.RECEIVED, probeId, version,
39
+ () => send(statusPayload(probeId, version, STATUSES.RECEIVED))
40
+ )
41
+ }
42
+
43
+ function ackInstalled ({ id: probeId, version }) {
44
+ onlyUniqueUpdates(
45
+ STATUSES.INSTALLED, probeId, version,
46
+ () => send(statusPayload(probeId, version, STATUSES.INSTALLED))
47
+ )
48
+ }
49
+
50
+ function ackEmitting ({ id: probeId, version }) {
51
+ onlyUniqueUpdates(
52
+ STATUSES.EMITTING, probeId, version,
53
+ () => send(statusPayload(probeId, version, STATUSES.EMITTING))
54
+ )
55
+ }
56
+
57
+ function ackError (err, { id: probeId, version }) {
58
+ log.error(err)
59
+
60
+ onlyUniqueUpdates(STATUSES.ERROR, probeId, version, () => {
61
+ const payload = statusPayload(probeId, version, STATUSES.ERROR)
62
+
63
+ payload.debugger.diagnostics.exception = {
64
+ type: err.code,
65
+ message: err.message,
66
+ stacktrace: err.stack
67
+ }
68
+
69
+ send(payload)
70
+ })
71
+ }
72
+
73
+ function send (payload) {
74
+ const form = new FormData()
75
+
76
+ form.append(
77
+ 'event',
78
+ JSON.stringify(payload),
79
+ { filename: 'event.json', contentType: 'application/json; charset=utf-8' }
80
+ )
81
+
82
+ const options = {
83
+ method: 'POST',
84
+ url: config.url,
85
+ path: '/debugger/v1/diagnostics',
86
+ headers: form.getHeaders()
87
+ }
88
+
89
+ request(form, options, (err) => {
90
+ if (err) log.error(err)
91
+ })
92
+ }
93
+
94
+ function statusPayload (probeId, version, status) {
95
+ return {
96
+ ddsource,
97
+ service,
98
+ debugger: {
99
+ diagnostics: { probeId, runtimeId, version, status }
100
+ }
101
+ }
102
+ }
103
+
104
+ function onlyUniqueUpdates (type, id, version, fn) {
105
+ const key = `${type}-${id}-${version}`
106
+ if (cache.has(key)) return
107
+ fn()
108
+ cache.set(key)
109
+ }
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ const { join } = require('path')
4
+ const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_threads')
5
+ const log = require('../log')
6
+
7
+ let worker = null
8
+ let configChannel = null
9
+
10
+ const { NODE_OPTIONS, ...env } = process.env
11
+
12
+ module.exports = {
13
+ start,
14
+ configure
15
+ }
16
+
17
+ function start (config, rc) {
18
+ if (worker !== null) return
19
+
20
+ log.debug('Starting Dynamic Instrumentation client...')
21
+
22
+ const rcAckCallbacks = new Map()
23
+ const rcChannel = new MessageChannel()
24
+ configChannel = new MessageChannel()
25
+
26
+ rc.setProductHandler('LIVE_DEBUGGING', (action, conf, id, ack) => {
27
+ const ackId = `${id}-${conf.version}`
28
+ rcAckCallbacks.set(ackId, ack)
29
+ rcChannel.port2.postMessage({ action, conf, ackId })
30
+ })
31
+
32
+ rcChannel.port2.on('message', ({ ackId, error }) => {
33
+ rcAckCallbacks.get(ackId)(error)
34
+ rcAckCallbacks.delete(ackId)
35
+ })
36
+ rcChannel.port2.on('messageerror', (err) => log.error(err))
37
+
38
+ worker = new Worker(
39
+ join(__dirname, 'devtools_client', 'index.js'),
40
+ {
41
+ execArgv: [], // Avoid worker thread inheriting the `-r` command line argument
42
+ env, // Avoid worker thread inheriting the `NODE_OPTIONS` environment variable (in case it contains `-r`)
43
+ workerData: {
44
+ config: serializableConfig(config),
45
+ parentThreadId,
46
+ rcPort: rcChannel.port1,
47
+ configPort: configChannel.port1
48
+ },
49
+ transferList: [rcChannel.port1, configChannel.port1]
50
+ }
51
+ )
52
+
53
+ worker.unref()
54
+
55
+ worker.on('online', () => {
56
+ log.debug(`Dynamic Instrumentation worker thread started successfully (thread id: ${worker.threadId})`)
57
+ })
58
+
59
+ worker.on('error', (err) => log.error(err))
60
+ worker.on('messageerror', (err) => log.error(err))
61
+
62
+ worker.on('exit', (code) => {
63
+ const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`)
64
+
65
+ log.error(error)
66
+
67
+ // Be nice, clean up now that the worker thread encounted an issue and we can't continue
68
+ rc.removeProductHandler('LIVE_DEBUGGING')
69
+ worker.removeAllListeners()
70
+ configChannel = null
71
+ for (const ackId of rcAckCallbacks.keys()) {
72
+ rcAckCallbacks.get(ackId)(error)
73
+ rcAckCallbacks.delete(ackId)
74
+ }
75
+ })
76
+ }
77
+
78
+ function configure (config) {
79
+ if (configChannel === null) return
80
+ configChannel.port2.postMessage(serializableConfig(config))
81
+ }
82
+
83
+ // TODO: Refactor the Config class so it never produces any config objects that are incompatible with MessageChannel
84
+ function serializableConfig (config) {
85
+ // URL objects cannot be serialized over the MessageChannel, so we need to convert them to strings first
86
+ if (config.url instanceof URL) {
87
+ config = { ...config }
88
+ config.url = config.url.toString()
89
+ }
90
+
91
+ return config
92
+ }
@@ -43,9 +43,15 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
43
43
  // length of `payload.events` when calling `makePayload`
44
44
  this._eventCount = 0
45
45
 
46
+ this.metadataTags = {}
47
+
46
48
  this.reset()
47
49
  }
48
50
 
51
+ setMetadataTags (tags) {
52
+ this.metadataTags = tags
53
+ }
54
+
49
55
  _encodeTestSuite (bytes, content) {
50
56
  let keysLength = TEST_SUITE_KEYS_LENGTH
51
57
  const itrCorrelationId = content.meta[ITR_CORRELATION_ID]
@@ -277,6 +283,10 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
277
283
  }
278
284
 
279
285
  _encode (bytes, trace) {
286
+ if (this._isReset) {
287
+ this._encodePayloadStart(bytes)
288
+ this._isReset = false
289
+ }
280
290
  const startTime = Date.now()
281
291
 
282
292
  const rawEvents = trace.map(formatSpan)
@@ -330,7 +340,8 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
330
340
  '*': {
331
341
  language: 'javascript',
332
342
  library_version: ddTraceVersion
333
- }
343
+ },
344
+ ...this.metadataTags
334
345
  },
335
346
  events: []
336
347
  }
@@ -349,6 +360,22 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
349
360
  this._encodeMapPrefix(bytes, Object.keys(payload.metadata).length)
350
361
  this._encodeString(bytes, '*')
351
362
  this._encodeMap(bytes, payload.metadata['*'])
363
+ if (payload.metadata.test) {
364
+ this._encodeString(bytes, 'test')
365
+ this._encodeMap(bytes, payload.metadata.test)
366
+ }
367
+ if (payload.metadata.test_suite_end) {
368
+ this._encodeString(bytes, 'test_suite_end')
369
+ this._encodeMap(bytes, payload.metadata.test_suite_end)
370
+ }
371
+ if (payload.metadata.test_module_end) {
372
+ this._encodeString(bytes, 'test_module_end')
373
+ this._encodeMap(bytes, payload.metadata.test_module_end)
374
+ }
375
+ if (payload.metadata.test_session_end) {
376
+ this._encodeString(bytes, 'test_session_end')
377
+ this._encodeMap(bytes, payload.metadata.test_session_end)
378
+ }
352
379
  this._encodeString(bytes, 'events')
353
380
  // Get offset of the events list to update the length of the array when calling `makePayload`
354
381
  this._eventsOffset = bytes.length
@@ -359,7 +386,7 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
359
386
  reset () {
360
387
  this._reset()
361
388
  this._eventCount = 0
362
- this._encodePayloadStart(this._traceBytes)
389
+ this._isReset = true
363
390
  }
364
391
  }
365
392
 
@@ -183,7 +183,7 @@ function request (data, options, callback) {
183
183
  }
184
184
 
185
185
  function byteLength (data) {
186
- return data.length > 0 ? data.reduce((prev, next) => prev + next.length, 0) : 0
186
+ return data.length > 0 ? data.reduce((prev, next) => prev + Buffer.byteLength(next, 'utf8'), 0) : 0
187
187
  }
188
188
 
189
189
  Object.defineProperty(request, 'writable', {
@@ -0,0 +1,30 @@
1
+ {
2
+ "sns": {
3
+ "request": [
4
+ "$.Attributes.KmsMasterKeyId",
5
+ "$.Attributes.PlatformCredential",
6
+ "$.Attributes.PlatformPrincipal",
7
+ "$.Attributes.Token",
8
+ "$.AWSAccountId",
9
+ "$.Endpoint",
10
+ "$.OneTimePassword",
11
+ "$.phoneNumber",
12
+ "$.PhoneNumber",
13
+ "$.Token"
14
+ ],
15
+ "response": [
16
+ "$.Attributes.KmsMasterKeyId",
17
+ "$.Attributes.Token",
18
+ "$.Endpoints.*.Token",
19
+ "$.PhoneNumber",
20
+ "$.PhoneNumbers",
21
+ "$.phoneNumbers",
22
+ "$.PlatformApplication.*.PlatformCredential",
23
+ "$.PlatformApplication.*.PlatformPrincipal",
24
+ "$.Subscriptions.*.Endpoint"
25
+ ],
26
+ "expand": [
27
+ "$.MessageAttributes.*.StringValue"
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ const aws = require('./aws.json')
2
+ const sdks = { aws }
3
+
4
+ function getSDKRules (sdk, requestInput, responseInput) {
5
+ return Object.fromEntries(
6
+ Object.entries(sdk).map(([service, serviceRules]) => {
7
+ return [
8
+ service,
9
+ {
10
+ request: serviceRules.request.concat(requestInput || []),
11
+ response: serviceRules.response.concat(responseInput || []),
12
+ expand: serviceRules.expand || []
13
+ }
14
+ ]
15
+ })
16
+ )
17
+ }
18
+
19
+ function appendRules (requestInput, responseInput) {
20
+ return Object.fromEntries(
21
+ Object.entries(sdks).map(([name, sdk]) => {
22
+ return [
23
+ name,
24
+ getSDKRules(sdk, requestInput, responseInput)
25
+ ]
26
+ })
27
+ )
28
+ }
29
+
30
+ module.exports = { appendRules }
@@ -0,0 +1,93 @@
1
+ const rfdc = require('rfdc')({ proto: false, circles: false })
2
+
3
+ const {
4
+ PAYLOAD_TAG_REQUEST_PREFIX,
5
+ PAYLOAD_TAG_RESPONSE_PREFIX
6
+ } = require('../constants')
7
+
8
+ const jsonpath = require('jsonpath-plus').JSONPath
9
+
10
+ const { tagsFromObject } = require('./tagging')
11
+
12
+ /**
13
+ * Given an identified value, attempt to parse it as JSON if relevant
14
+ *
15
+ * @param {any} value
16
+ * @returns {any} the parsed object if parsing was successful, the input if not
17
+ */
18
+ function maybeJSONParseValue (value) {
19
+ if (typeof value !== 'string' || value[0] !== '{') {
20
+ return value
21
+ }
22
+
23
+ try {
24
+ return JSON.parse(value)
25
+ } catch (e) {
26
+ return value
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Apply expansion to all expansion JSONPath queries
32
+ *
33
+ * @param {Object} object
34
+ * @param {[String]} expansionRules list of JSONPath queries
35
+ */
36
+ function expand (object, expansionRules) {
37
+ for (const rule of expansionRules) {
38
+ jsonpath(rule, object, (value, _type, desc) => {
39
+ desc.parent[desc.parentProperty] = maybeJSONParseValue(value)
40
+ })
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Apply redaction to all redaction JSONPath queries
46
+ *
47
+ * @param {Object} object
48
+ * @param {[String]} redactionRules
49
+ */
50
+ function redact (object, redactionRules) {
51
+ for (const rule of redactionRules) {
52
+ jsonpath(rule, object, (_value, _type, desc) => {
53
+ desc.parent[desc.parentProperty] = 'redacted'
54
+ })
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Generate a map of tag names to tag values by performing:
60
+ * 1. Attempting to parse identified fields as JSON
61
+ * 2. Redacting fields identified by redaction rules
62
+ * 3. Flattening the resulting object, producing as many tag name/tag value pairs
63
+ * as there are leaf values in the object
64
+ * This function performs side-effects on a _copy_ of the input object.
65
+ *
66
+ * @param {Object} config sdk configuration for the service
67
+ * @param {[String]} config.expand expansion rules for the service
68
+ * @param {[String]} config.request redaction rules for the request
69
+ * @param {[String]} config.response redaction rules for the response
70
+ * @param {Object} object the input object to generate tags from
71
+ * @param {Object} opts tag generation options
72
+ * @param {String} opts.prefix prefix for all generated tags
73
+ * @param {number} opts.maxDepth maximum depth to traverse the object
74
+ * @returns
75
+ */
76
+ function computeTags (config, object, opts) {
77
+ const payload = rfdc(object)
78
+ const redactionRules = opts.prefix === PAYLOAD_TAG_REQUEST_PREFIX ? config.request : config.response
79
+ const expansionRules = config.expand
80
+ expand(payload, expansionRules)
81
+ redact(payload, redactionRules)
82
+ return tagsFromObject(payload, opts)
83
+ }
84
+
85
+ function tagsFromRequest (config, object, opts) {
86
+ return computeTags(config, object, { ...opts, prefix: PAYLOAD_TAG_REQUEST_PREFIX })
87
+ }
88
+
89
+ function tagsFromResponse (config, object, opts) {
90
+ return computeTags(config, object, { ...opts, prefix: PAYLOAD_TAG_RESPONSE_PREFIX })
91
+ }
92
+
93
+ module.exports = { computeTags, tagsFromRequest, tagsFromResponse }