dd-trace 5.70.0 → 5.72.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 (72) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/index.d.ts +110 -1
  3. package/initialize.mjs +7 -1
  4. package/package.json +21 -2
  5. package/packages/datadog-instrumentations/src/anthropic.js +115 -0
  6. package/packages/datadog-instrumentations/src/azure-event-hubs.js +37 -0
  7. package/packages/datadog-instrumentations/src/azure-functions.js +3 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +7 -7
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  10. package/packages/datadog-instrumentations/src/jest.js +29 -36
  11. package/packages/datadog-instrumentations/src/mocha/main.js +8 -9
  12. package/packages/datadog-instrumentations/src/mocha/utils.js +1 -1
  13. package/packages/datadog-instrumentations/src/mocha/worker.js +2 -2
  14. package/packages/datadog-instrumentations/src/pg.js +1 -1
  15. package/packages/datadog-instrumentations/src/playwright.js +5 -5
  16. package/packages/datadog-instrumentations/src/vitest.js +8 -8
  17. package/packages/datadog-plugin-anthropic/src/index.js +17 -0
  18. package/packages/datadog-plugin-anthropic/src/tracing.js +30 -0
  19. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +73 -27
  20. package/packages/datadog-plugin-azure-event-hubs/src/index.js +15 -0
  21. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +82 -0
  22. package/packages/datadog-plugin-azure-functions/src/index.js +37 -0
  23. package/packages/datadog-plugin-cucumber/src/index.js +3 -3
  24. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -9
  25. package/packages/datadog-plugin-jest/src/util.js +10 -2
  26. package/packages/datadog-plugin-mocha/src/index.js +2 -2
  27. package/packages/datadog-plugin-playwright/src/index.js +2 -2
  28. package/packages/datadog-plugin-vitest/src/index.js +2 -2
  29. package/packages/datadog-plugin-ws/src/server.js +5 -3
  30. package/packages/dd-trace/src/appsec/reporter.js +70 -21
  31. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
  32. package/packages/dd-trace/src/config.js +110 -26
  33. package/packages/dd-trace/src/config_defaults.js +14 -0
  34. package/packages/dd-trace/src/git_properties.js +90 -5
  35. package/packages/dd-trace/src/llmobs/plugins/anthropic.js +282 -0
  36. package/packages/dd-trace/src/llmobs/tagger.js +35 -0
  37. package/packages/dd-trace/src/noop/proxy.js +3 -0
  38. package/packages/dd-trace/src/openfeature/constants/constants.js +51 -0
  39. package/packages/dd-trace/src/openfeature/flagging_provider.js +45 -0
  40. package/packages/dd-trace/src/openfeature/index.js +77 -0
  41. package/packages/dd-trace/src/openfeature/noop.js +101 -0
  42. package/packages/dd-trace/src/openfeature/writers/base.js +181 -0
  43. package/packages/dd-trace/src/openfeature/writers/exposures.js +173 -0
  44. package/packages/dd-trace/src/openfeature/writers/util.js +43 -0
  45. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +100 -0
  46. package/packages/dd-trace/src/opentelemetry/logs/index.js +87 -0
  47. package/packages/dd-trace/src/opentelemetry/logs/logger.js +77 -0
  48. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +126 -0
  49. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +173 -0
  50. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +367 -0
  51. package/packages/dd-trace/src/opentelemetry/protos/common.proto +116 -0
  52. package/packages/dd-trace/src/opentelemetry/protos/logs.proto +226 -0
  53. package/packages/dd-trace/src/opentelemetry/protos/logs_service.proto +78 -0
  54. package/packages/dd-trace/src/opentelemetry/protos/protobuf_loader.js +48 -0
  55. package/packages/dd-trace/src/opentelemetry/protos/resource.proto +45 -0
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +7 -6
  57. package/packages/dd-trace/src/plugins/index.js +2 -0
  58. package/packages/dd-trace/src/plugins/util/test.js +6 -5
  59. package/packages/dd-trace/src/profiling/config.js +21 -1
  60. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -2
  61. package/packages/dd-trace/src/profiling/profiler.js +44 -22
  62. package/packages/dd-trace/src/profiling/profilers/events.js +12 -3
  63. package/packages/dd-trace/src/profiling/profilers/space.js +35 -24
  64. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -6
  65. package/packages/dd-trace/src/proxy.js +22 -1
  66. package/packages/dd-trace/src/remote_config/capabilities.js +2 -0
  67. package/packages/dd-trace/src/remote_config/index.js +3 -0
  68. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +4 -0
  69. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +8 -0
  70. package/packages/dd-trace/src/supported-configurations.json +18 -0
  71. package/packages/dd-trace/src/telemetry/telemetry.js +13 -1
  72. package/register.js +9 -1
@@ -268,6 +268,32 @@ class LLMObsTagger {
268
268
  return filteredToolCalls
269
269
  }
270
270
 
271
+ #filterToolResults (toolResults) {
272
+ if (!Array.isArray(toolResults)) {
273
+ toolResults = [toolResults]
274
+ }
275
+
276
+ const filteredToolResults = []
277
+ for (const toolResult of toolResults) {
278
+ if (typeof toolResult !== 'object') {
279
+ this.#handleFailure('Tool result must be an object.', 'invalid_io_messages')
280
+ continue
281
+ }
282
+
283
+ const { result, toolId, type } = toolResult
284
+ const toolResultObj = {}
285
+
286
+ const condition1 = this.#tagConditionalString(result, 'Tool result', toolResultObj, 'result')
287
+ const condition2 = this.#tagConditionalString(toolId, 'Tool ID', toolResultObj, 'tool_id')
288
+ const condition3 = this.#tagConditionalString(type, 'Tool type', toolResultObj, 'type')
289
+
290
+ if (condition1 && condition2 && condition3) {
291
+ filteredToolResults.push(toolResultObj)
292
+ }
293
+ }
294
+ return filteredToolResults
295
+ }
296
+
271
297
  #tagMessages (span, data, key) {
272
298
  if (!data) {
273
299
  return
@@ -290,6 +316,7 @@ class LLMObsTagger {
290
316
 
291
317
  const { content = '', role } = message
292
318
  const toolCalls = message.toolCalls
319
+ const toolResults = message.toolResults
293
320
  const toolId = message.toolId
294
321
  const messageObj = { content }
295
322
 
@@ -308,6 +335,14 @@ class LLMObsTagger {
308
335
  }
309
336
  }
310
337
 
338
+ if (toolResults) {
339
+ const filteredToolResults = this.#filterToolResults(toolResults)
340
+
341
+ if (filteredToolResults.length) {
342
+ messageObj.tool_results = filteredToolResults
343
+ }
344
+ }
345
+
311
346
  if (toolId) {
312
347
  if (role === 'tool') {
313
348
  condition = this.#tagConditionalString(toolId, 'Tool ID', messageObj, 'tool_id')
@@ -4,12 +4,14 @@ const NoopTracer = require('./tracer')
4
4
  const NoopAppsecSdk = require('../appsec/sdk/noop')
5
5
  const NoopDogStatsDClient = require('./dogstatsd')
6
6
  const NoopLLMObsSDK = require('../llmobs/noop')
7
+ const NoopFlaggingProvider = require('../openfeature/noop')
7
8
  const NoopAIGuardSDK = require('../aiguard/noop')
8
9
 
9
10
  const noop = new NoopTracer()
10
11
  const noopAppsec = new NoopAppsecSdk()
11
12
  const noopDogStatsDClient = new NoopDogStatsDClient()
12
13
  const noopLLMObs = new NoopLLMObsSDK(noop)
14
+ const noopOpenFeatureProvider = new NoopFlaggingProvider()
13
15
  const noopAIGuard = new NoopAIGuardSDK()
14
16
 
15
17
  /** @type {import('../../src/index')} Proxy */
@@ -19,6 +21,7 @@ class NoopProxy {
19
21
  this.appsec = noopAppsec
20
22
  this.dogstatsd = noopDogStatsDClient
21
23
  this.llmobs = noopLLMObs
24
+ this.openfeature = noopOpenFeatureProvider
22
25
  this.aiguard = noopAIGuard
23
26
  this.setBaggageItem = () => {}
24
27
  this.getBaggageItem = () => {}
@@ -0,0 +1,51 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ /**
5
+ * @constant
6
+ * @type {string} Base path for EVP proxy agent endpoint
7
+ */
8
+ EVP_PROXY_AGENT_BASE_PATH: '/evp_proxy/v2/',
9
+
10
+ /**
11
+ * @constant
12
+ * @type {string} HTTP header name for EVP subdomain routing
13
+ */
14
+ EVP_SUBDOMAIN_HEADER_NAME: 'X-Datadog-EVP-Subdomain',
15
+
16
+ /**
17
+ * @constant
18
+ * @type {string} EVP subdomain value for event platform intake
19
+ */
20
+ EVP_SUBDOMAIN_VALUE: 'event-platform-intake',
21
+
22
+ /**
23
+ * @constant
24
+ * @type {string} API endpoint for exposure events EVP track
25
+ */
26
+ EXPOSURES_ENDPOINT: '/api/v2/exposures',
27
+
28
+ /**
29
+ * @constant
30
+ * @type {number} Maximum payload size for EVP intake (5MB, actual limit is 5.1MB)
31
+ */
32
+ EVP_PAYLOAD_SIZE_LIMIT: 5 << 20,
33
+
34
+ /**
35
+ * @constant
36
+ * @type {number} Maximum individual event size (999KB, actual limit is 1MB)
37
+ */
38
+ EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024,
39
+
40
+ /**
41
+ * @constant
42
+ * @type {string} Channel name for exposure event submission
43
+ */
44
+ EXPOSURE_CHANNEL: 'ffe:exposure:submit',
45
+
46
+ /**
47
+ * @constant
48
+ * @type {string} Reason code for noop provider evaluations
49
+ */
50
+ NOOP_REASON: 'STATIC'
51
+ }
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server')
4
+ const { channel } = require('dc-polyfill')
5
+ const log = require('../log')
6
+ const { EXPOSURE_CHANNEL } = require('./constants/constants')
7
+
8
+ /**
9
+ * OpenFeature provider that integrates with Datadog's feature flagging system.
10
+ * Extends DatadogNodeServerProvider to add tracer integration and configuration management.
11
+ */
12
+ class FlaggingProvider extends DatadogNodeServerProvider {
13
+ /**
14
+ * @param {import('../tracer')} tracer - Datadog tracer instance
15
+ * @param {import('../config')} config - Tracer configuration object
16
+ */
17
+ constructor (tracer, config) {
18
+ // Call parent constructor with required options
19
+ super({
20
+ exposureChannel: channel(EXPOSURE_CHANNEL)
21
+ })
22
+
23
+ this._tracer = tracer
24
+ this._config = config
25
+
26
+ log.debug(this.constructor.name + ' created')
27
+ }
28
+
29
+ /**
30
+ * Internal method to update flag configuration from Remote Config.
31
+ * This method is called automatically when Remote Config delivers UFC updates.
32
+ *
33
+ * @internal
34
+ * @param {import('@datadog/openfeature-node-server').UniversalFlagConfigurationV1} ufc
35
+ * - Universal Flag Configuration object
36
+ */
37
+ _setConfiguration (ufc) {
38
+ if (typeof this.setConfiguration === 'function') {
39
+ this.setConfiguration(ufc)
40
+ }
41
+ log.debug(this.constructor.name + ' provider configuration updated')
42
+ }
43
+ }
44
+
45
+ module.exports = FlaggingProvider
@@ -0,0 +1,77 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+ const ExposuresWriter = require('./writers/exposures')
5
+ const { setAgentStrategy } = require('./writers/util')
6
+ const { channel } = require('dc-polyfill')
7
+
8
+ const exposureSubmitCh = channel('ffe:exposure:submit')
9
+ const flushCh = channel('ffe:writers:flush')
10
+
11
+ let exposuresWriter = null
12
+
13
+ /**
14
+ * @private
15
+ * @param {Object|Array<Object>} exposureEvents - Exposure events channel subscriber
16
+ * @returns {void}
17
+ */
18
+ function _handleExposureSubmit (exposureEvents) {
19
+ if (!exposuresWriter) return
20
+ exposuresWriter.append(exposureEvents)
21
+ }
22
+
23
+ /**
24
+ * Channel subscriber for manually flushing the exposures writer
25
+ * @private
26
+ * @returns {void}
27
+ */
28
+ function _handleFlush () {
29
+ exposuresWriter?.flush()
30
+ }
31
+
32
+ /**
33
+ * Enables the OpenFeature module and sets up FF&E writer and channel subscribers
34
+ * @param {import('../config')} config - Tracer configuration object
35
+ * @returns {void}
36
+ */
37
+ function enable (config) {
38
+ if (exposuresWriter) {
39
+ log.warn(exposuresWriter.constructor.name + ' already enabled')
40
+ return
41
+ }
42
+
43
+ exposuresWriter = new ExposuresWriter(config)
44
+ exposureSubmitCh.subscribe(_handleExposureSubmit)
45
+ flushCh.subscribe(_handleFlush)
46
+
47
+ setAgentStrategy(config, hasAgent => {
48
+ exposuresWriter?.setEnabled(hasAgent)
49
+ })
50
+
51
+ log.debug('OpenFeature module enabled')
52
+ }
53
+
54
+ /**
55
+ * Disables the OpenFeature module and cleans up resources
56
+ * @returns {void}
57
+ */
58
+ function disable () {
59
+ if (!exposuresWriter) return
60
+
61
+ if (exposureSubmitCh.hasSubscribers) {
62
+ exposureSubmitCh.unsubscribe(_handleExposureSubmit)
63
+ }
64
+ if (flushCh.hasSubscribers) {
65
+ flushCh.unsubscribe(_handleFlush)
66
+ }
67
+
68
+ exposuresWriter.destroy?.()
69
+ exposuresWriter = null
70
+
71
+ log.debug('OpenFeature module disabled')
72
+ }
73
+
74
+ module.exports = {
75
+ enable,
76
+ disable
77
+ }
@@ -0,0 +1,101 @@
1
+ 'use strict'
2
+
3
+ const { NOOP_REASON } = require('./constants/constants')
4
+
5
+ /**
6
+ * No-op implementation of OpenFeature provider that always returns default values.
7
+ * Used when the OpenFeature provider is not initialized or disabled.
8
+ * https://openfeature.dev/docs/reference/concepts/provider/
9
+ */
10
+ class NoopFlaggingProvider {
11
+ /**
12
+ * @param {Object} [noopTracer] - Optional noop tracer instance
13
+ */
14
+ constructor (noopTracer) {
15
+ this._tracer = noopTracer
16
+ this._config = {}
17
+ this.metadata = { name: 'NoopFlaggingProvider' }
18
+ this.status = 'NOT_READY'
19
+ this.runsOn = 'server'
20
+ }
21
+
22
+ /**
23
+ * @param {string} flagKey - Flag key
24
+ * @param {boolean} defaultValue - Default value to return
25
+ * @param {Object} context - Evaluation context
26
+ * @param {Object} logger - Logger instance
27
+ * @returns {Promise<{value: boolean, reason: string}>} Resolution details
28
+ */
29
+ resolveBooleanEvaluation (flagKey, defaultValue, context, logger) {
30
+ return Promise.resolve({
31
+ value: defaultValue,
32
+ reason: NOOP_REASON
33
+ })
34
+ }
35
+
36
+ /**
37
+ * @param {string} flagKey - Flag key
38
+ * @param {string} defaultValue - Default value to return
39
+ * @param {Object} context - Evaluation context
40
+ * @param {Object} logger - Logger instance
41
+ * @returns {Promise<{value: string, reason: string}>} Resolution details
42
+ */
43
+ resolveStringEvaluation (flagKey, defaultValue, context, logger) {
44
+ return Promise.resolve({
45
+ value: defaultValue,
46
+ reason: NOOP_REASON
47
+ })
48
+ }
49
+
50
+ /**
51
+ * @param {string} flagKey - Flag key
52
+ * @param {number} defaultValue - Default value to return
53
+ * @param {Object} context - Evaluation context
54
+ * @param {Object} logger - Logger instance
55
+ * @returns {Promise<{value: number, reason: string}>} Resolution details
56
+ */
57
+ resolveNumberEvaluation (flagKey, defaultValue, context, logger) {
58
+ return Promise.resolve({
59
+ value: defaultValue,
60
+ reason: NOOP_REASON,
61
+ })
62
+ }
63
+
64
+ /**
65
+ * @param {string} flagKey - Flag key
66
+ * @param {Object} defaultValue - Default value to return
67
+ * @param {Object} context - Evaluation context
68
+ * @param {Object} logger - Logger instance
69
+ * @returns {Promise<{value: Object, reason: string}>} Resolution details
70
+ */
71
+ resolveObjectEvaluation (flagKey, defaultValue, context, logger) {
72
+ return Promise.resolve({
73
+ value: defaultValue,
74
+ reason: NOOP_REASON
75
+ })
76
+ }
77
+
78
+ /**
79
+ * @returns {Object} Current configuration
80
+ */
81
+ getConfiguration () {
82
+ return this._config
83
+ }
84
+
85
+ /**
86
+ * @param {Object} config - Configuration to set
87
+ */
88
+ setConfiguration (config) {
89
+ this._config = config
90
+ }
91
+
92
+ /**
93
+ * @internal
94
+ * @param {Object} ufc - Universal Flag Configuration object
95
+ */
96
+ _setConfiguration (ufc) {
97
+ this.setConfiguration(ufc)
98
+ }
99
+ }
100
+
101
+ module.exports = NoopFlaggingProvider
@@ -0,0 +1,181 @@
1
+ 'use strict'
2
+
3
+ const request = require('../../exporters/common/request')
4
+ const { safeJSONStringify } = require('../../exporters/common/util')
5
+ const { URL, format } = require('node:url')
6
+
7
+ const log = require('../../log')
8
+
9
+ /**
10
+ * @typedef {Object} BaseFFEWriterOptions
11
+ * @property {number} [interval] - Flush interval in milliseconds
12
+ * @property {number} [timeout] - Request timeout in milliseconds
13
+ * @property {Object} config - Tracer configuration object
14
+ * @property {string} endpoint - API endpoint path
15
+ * @property {URL} [agentUrl] - Base URL for the agent
16
+ * @property {number} [payloadSizeLimit] - Maximum payload size in bytes
17
+ * @property {number} [eventSizeLimit] - Maximum individual event size in bytes
18
+ * @property {Object} [headers] - Additional HTTP headers
19
+ */
20
+
21
+ /**
22
+ * BaseFFEWriter is the base class for sending Feature Flagging & Exposure Events payloads to the Datadog Agent.
23
+ * @class BaseFFEWriter
24
+ */
25
+ class BaseFFEWriter {
26
+ /**
27
+ * @param {BaseFFEWriterOptions} options - Writer configuration options
28
+ */
29
+ constructor ({ interval, timeout, config, endpoint, agentUrl, payloadSizeLimit, eventSizeLimit, headers }) {
30
+ this._interval = interval ?? 1000
31
+ this._timeout = timeout ?? 5000
32
+
33
+ this._buffer = []
34
+ this._bufferLimit = 1000
35
+ this._bufferSize = 0
36
+
37
+ this._config = config
38
+ this._endpoint = endpoint
39
+ this._baseUrl = agentUrl ?? this._getAgentUrl()
40
+ this._payloadSizeLimit = payloadSizeLimit
41
+ this._eventSizeLimit = eventSizeLimit
42
+ this._headers = headers || {}
43
+
44
+ this._requestOptions = {
45
+ headers: {
46
+ ...this._headers,
47
+ 'Content-Type': 'application/json'
48
+ },
49
+ method: 'POST',
50
+ timeout: this._timeout,
51
+ url: this._baseUrl,
52
+ path: this._endpoint
53
+ }
54
+
55
+ this._periodic = setInterval(() => {
56
+ this.flush()
57
+ }, this._interval).unref()
58
+
59
+ this._beforeExitHandler = () => {
60
+ this.destroy()
61
+ }
62
+ process.once('beforeExit', this._beforeExitHandler)
63
+
64
+ this._destroyed = false
65
+ this._droppedEvents = 0
66
+ }
67
+
68
+ /**
69
+ * Appends an event array to the buffer
70
+ * @param {Array|Object} events - Event object(s) to append to buffer
71
+ */
72
+ append (events) {
73
+ const eventArray = Array.isArray(events) ? events : [events]
74
+
75
+ for (const event of eventArray) {
76
+ if (this._buffer.length >= this._bufferLimit) {
77
+ log.warn(`${this.constructor.name} event buffer full (limit is ${this._bufferLimit}), dropping event`)
78
+ this._droppedEvents++
79
+ continue
80
+ }
81
+
82
+ const eventSizeBytes = Buffer.byteLength(JSON.stringify(event))
83
+
84
+ // Check individual event size limit if configured
85
+ if (this._eventSizeLimit && eventSizeBytes > this._eventSizeLimit) {
86
+ log.warn(`${this.constructor.name} event size
87
+ ${eventSizeBytes} bytes exceeds limit ${this._eventSizeLimit}, dropping event`)
88
+ this._droppedEvents++
89
+ continue
90
+ }
91
+
92
+ // Check if adding this event would exceed payload size limit if configured
93
+ if (this._payloadSizeLimit && this._bufferSize + eventSizeBytes > this._payloadSizeLimit) {
94
+ log.debug(() => `${this.constructor.name}
95
+ buffer size would exceed ${this._payloadSizeLimit} bytes, flushing first`)
96
+ this.flush()
97
+ }
98
+
99
+ this._bufferSize += eventSizeBytes
100
+ this._buffer.push(event)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Flushes all buffered events to the agent
106
+ */
107
+ flush () {
108
+ if (this._buffer.length === 0) {
109
+ return
110
+ }
111
+ const events = this._buffer
112
+ this._buffer = []
113
+ this._bufferSize = 0
114
+
115
+ const payload = this._encode(this.makePayload(events))
116
+
117
+ log.debug(() => `${this.constructor.name} flushing payload: ${safeJSONStringify(payload)}`)
118
+
119
+ request(payload, this._requestOptions, (err, resp, code) => {
120
+ if (err) {
121
+ log.error(`Failed to send events to ${this._baseUrl.href}${this._endpoint}: ${err.message}`)
122
+ } else if (code >= 200 && code < 300) {
123
+ log.debug(() => `Successfully sent ${events.length} events`)
124
+ } else {
125
+ log.warn(`Events request returned status ${code}`)
126
+ }
127
+ })
128
+ }
129
+
130
+ /**
131
+ * Override in subclass to customize payload structure
132
+ * @param {Array} events - Array of events to be sent
133
+ * @returns {object} Formatted payload
134
+ */
135
+ makePayload (events) {
136
+ // Override in subclass
137
+ return events
138
+ }
139
+
140
+ /**
141
+ * Cleans up resources and flushes remaining events
142
+ */
143
+ destroy () {
144
+ if (!this._destroyed) {
145
+ log.debug(() => `Stopping ${this.constructor.name}`)
146
+ clearInterval(this._periodic)
147
+ process.removeListener('beforeExit', this._beforeExitHandler)
148
+ this.flush()
149
+ this._destroyed = true
150
+
151
+ if (this._droppedEvents > 0) {
152
+ log.warn(`${this.constructor.name} dropped ${this._droppedEvents} events due to buffer overflow`)
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @private
159
+ * @returns {URL} Constructs agent URL from config
160
+ */
161
+ _getAgentUrl () {
162
+ const { hostname, port } = this._config
163
+
164
+ return this._config.url ?? new URL(format({
165
+ protocol: 'http:',
166
+ hostname: hostname || 'localhost',
167
+ port: port || 8126
168
+ }))
169
+ }
170
+
171
+ /**
172
+ * @private
173
+ * @param {Array<object>} payload - Payload to encode
174
+ * @returns {string} JSON-stringified payload
175
+ */
176
+ _encode (payload) {
177
+ return JSON.stringify(payload)
178
+ }
179
+ }
180
+
181
+ module.exports = BaseFFEWriter
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ const BaseFFEWriter = require('./base')
4
+ const {
5
+ EXPOSURES_ENDPOINT,
6
+ EVP_PROXY_AGENT_BASE_PATH,
7
+ EVP_SUBDOMAIN_HEADER_NAME,
8
+ EVP_SUBDOMAIN_VALUE,
9
+ EVP_PAYLOAD_SIZE_LIMIT,
10
+ EVP_EVENT_SIZE_LIMIT
11
+ } = require('../constants/constants')
12
+
13
+ /**
14
+ * @typedef {Object} ExposureEvent
15
+ * @property {number} timestamp - Unix timestamp in milliseconds
16
+ * @property {Object} allocation - Allocation information
17
+ * @property {string} allocation.key - Allocation key
18
+ * @property {Object} flag - Flag information
19
+ * @property {string} flag.key - Flag key
20
+ * @property {Object} variant - Variant information
21
+ * @property {string} variant.key - Variant key
22
+ * @property {Object} subject - Subject (user/entity) information
23
+ * @property {string} subject.id - Subject identifier
24
+ * @property {string} [subject.type] - Subject type
25
+ * @property {Object} [subject.attributes] - Additional subject attributes
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} ExposureContext
30
+ * @property {string} service_name - Service name
31
+ * @property {string} [version] - Service version
32
+ * @property {string} [env] - Service environment
33
+ */
34
+
35
+ /**
36
+ * @typedef {Object} ExposureEventPayload
37
+ * @property {ExposureContext} context - Service context metadata
38
+ * @property {ExposureEvent[]} exposures - Formatted exposure events
39
+ */
40
+
41
+ /**
42
+ * ExposuresWriter is responsible for sending exposure events to the Datadog Agent.
43
+ */
44
+ class ExposuresWriter extends BaseFFEWriter {
45
+ /**
46
+ * @param {import('../../config')} config - Tracer configuration object
47
+ */
48
+ constructor (config) {
49
+ // Build full EVP endpoint path
50
+ const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/+$/, '')
51
+ const endpoint = EXPOSURES_ENDPOINT.replace(/^\/+/, '')
52
+ const fullEndpoint = `${basePath}/${endpoint}`
53
+
54
+ super({
55
+ config,
56
+ endpoint: fullEndpoint,
57
+ payloadSizeLimit: EVP_PAYLOAD_SIZE_LIMIT,
58
+ eventSizeLimit: EVP_EVENT_SIZE_LIMIT,
59
+ headers: {
60
+ [EVP_SUBDOMAIN_HEADER_NAME]: EVP_SUBDOMAIN_VALUE
61
+ }
62
+ })
63
+ this._enabled = false // Start disabled until agent strategy is set
64
+ this._pendingEvents = [] // Buffer events until enabled
65
+ this._context = this._buildContext()
66
+ }
67
+
68
+ /**
69
+ * @param {boolean} enabled - Whether to enable the writer
70
+ */
71
+ setEnabled (enabled) {
72
+ this._enabled = enabled
73
+
74
+ if (enabled && this._pendingEvents.length > 0) {
75
+ // Flush all pending events as a batch
76
+ super.append(this._pendingEvents)
77
+ this._pendingEvents = []
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Appends exposure event(s) to the buffer
83
+ * @param {ExposureEvent|ExposureEvent[]} events - Exposure event(s) to append
84
+ */
85
+ append (events) {
86
+ if (!this._enabled) {
87
+ // Buffer events until writer is ready
88
+ if (Array.isArray(events)) {
89
+ this._pendingEvents.push(...events)
90
+ } else {
91
+ this._pendingEvents.push(events)
92
+ }
93
+ return
94
+ }
95
+ super.append(events)
96
+ }
97
+
98
+ /**
99
+ * Flushes buffered exposure events to the agent
100
+ */
101
+ flush () {
102
+ if (!this._enabled) {
103
+ // Don't flush when disabled
104
+ return
105
+ }
106
+ super.flush()
107
+ }
108
+
109
+ /**
110
+ * Formats exposure events with service context metadata
111
+ * @param {Array<ExposureEvent>} events - Array of exposure events
112
+ * @returns {ExposureEventPayload} Formatted payload with service context
113
+ */
114
+ makePayload (events) {
115
+ const formattedEvents = events.map(event => this._formatExposureEvent(event))
116
+
117
+ return {
118
+ context: this._context,
119
+ exposures: formattedEvents
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Builds service context metadata
125
+ * @private
126
+ * @returns {ExposureContext} Service context
127
+ */
128
+ _buildContext () {
129
+ const context = {
130
+ service_name: this._config.service || 'unknown'
131
+ }
132
+
133
+ // Only include version and env if they are defined
134
+ if (this._config.version !== undefined) {
135
+ context.version = this._config.version
136
+ }
137
+
138
+ if (this._config.env !== undefined) {
139
+ context.env = this._config.env
140
+ }
141
+
142
+ return context
143
+ }
144
+
145
+ /**
146
+ * @private
147
+ * @param {ExposureEvent} event - Raw exposure event
148
+ * @returns {ExposureEvent} Formatted exposure event
149
+ */
150
+ _formatExposureEvent (event) {
151
+ // Ensure the event matches the expected schema
152
+ const formattedEvent = {
153
+ timestamp: event.timestamp || Date.now(),
154
+ allocation: {
155
+ key: event.allocation?.key || event['allocation.key']
156
+ },
157
+ flag: {
158
+ key: event.flag?.key || event['flag.key']
159
+ },
160
+ variant: {
161
+ key: event.variant?.key || event['variant.key']
162
+ },
163
+ subject: {
164
+ id: event.subject?.id || event['subject.id'],
165
+ type: event.subject?.type,
166
+ attributes: event.subject?.attributes
167
+ }
168
+ }
169
+ return formattedEvent
170
+ }
171
+ }
172
+
173
+ module.exports = ExposuresWriter