dd-trace 6.11.0 → 6.13.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.
- package/LICENSE-3rdparty.csv +5 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
- package/index.d.ts +143 -25
- package/package.json +26 -25
- package/packages/datadog-instrumentations/src/bunyan.js +4 -2
- package/packages/datadog-instrumentations/src/cucumber.js +4 -4
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/jest.js +140 -18
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
- package/packages/datadog-instrumentations/src/mariadb.js +7 -0
- package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
- package/packages/datadog-instrumentations/src/pino.js +15 -4
- package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
- package/packages/datadog-instrumentations/src/playwright.js +67 -53
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +13 -12
- package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
- package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
- package/packages/datadog-instrumentations/src/winston.js +6 -3
- package/packages/datadog-plugin-cypress/src/support.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +2 -15
- package/packages/datadog-plugin-http2/src/client.js +2 -15
- package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
- package/packages/datadog-plugin-playwright/src/index.js +13 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -15
- package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
- package/packages/dd-trace/src/aiguard/noop.js +9 -1
- package/packages/dd-trace/src/aiguard/redaction.js +183 -0
- package/packages/dd-trace/src/aiguard/sdk.js +7 -2
- package/packages/dd-trace/src/aiguard/tags.js +2 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
- package/packages/dd-trace/src/appsec/reporter.js +2 -0
- package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
- package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
- package/packages/dd-trace/src/carrier.js +107 -48
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
- package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
- package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
- package/packages/dd-trace/src/config/index.js +2 -1
- package/packages/dd-trace/src/config/supported-configurations.json +27 -0
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
- package/packages/dd-trace/src/debugger/index.js +4 -2
- package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
- package/packages/dd-trace/src/dogstatsd.js +45 -18
- package/packages/dd-trace/src/encode/agentless-json.js +5 -4
- package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
- package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
- package/packages/dd-trace/src/evp_proxy/path.js +22 -0
- package/packages/dd-trace/src/exporters/agent/index.js +22 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
- package/packages/dd-trace/src/exporters/common/request.js +5 -2
- package/packages/dd-trace/src/exporters/common/writer.js +24 -2
- package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
- package/packages/dd-trace/src/external-logger/src/index.js +4 -3
- package/packages/dd-trace/src/flush.js +102 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +10 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +301 -14
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +440 -112
- package/packages/dd-trace/src/llmobs/experiments/index.js +68 -44
- package/packages/dd-trace/src/llmobs/experiments/noop.js +103 -12
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +81 -4
- package/packages/dd-trace/src/llmobs/index.js +80 -17
- package/packages/dd-trace/src/llmobs/noop.js +2 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
- package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
- package/packages/dd-trace/src/noop/span.js +5 -0
- package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
- package/packages/dd-trace/src/openfeature/index.js +7 -4
- package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
- package/packages/dd-trace/src/openfeature/writers/exposures.js +70 -15
- package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
- package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
- package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +429 -352
- package/packages/dd-trace/src/opentracing/span.js +28 -0
- package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
- package/packages/dd-trace/src/plugins/util/test.js +74 -21
- package/packages/dd-trace/src/plugins/util/web.js +2 -65
- package/packages/dd-trace/src/profiling/oom.js +4 -3
- package/packages/dd-trace/src/profiling/profiler.js +7 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
- package/packages/dd-trace/src/proxy.js +36 -8
- package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
- package/packages/dd-trace/src/serverless/vercel.js +122 -0
- package/packages/dd-trace/src/serverless.js +37 -20
- package/packages/dd-trace/src/span_stats.js +40 -3
- package/packages/dd-trace/src/telemetry/metrics.js +62 -8
- package/packages/dd-trace/src/telemetry/send-data.js +1 -0
- package/packages/dd-trace/src/tracer.js +22 -0
- package/packages/dd-trace/src/web-tags-cache.js +67 -13
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/https-proxy-agent/LICENSE +22 -0
- package/vendor/dist/https-proxy-agent/index.js +4 -0
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/proxy-from-env/LICENSE +20 -0
- package/vendor/dist/proxy-from-env/index.js +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
let NoopExperiments
|
|
4
4
|
|
|
5
5
|
class NoopLLMObs {
|
|
6
6
|
constructor (noopTracer) {
|
|
@@ -12,6 +12,7 @@ class NoopLLMObs {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
get experiments () {
|
|
15
|
+
NoopExperiments ??= require('./experiments/noop')
|
|
15
16
|
return new NoopExperiments('LLM Observability is not enabled')
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -38,16 +38,20 @@ function formatAnthropicToolResultContent (content) {
|
|
|
38
38
|
if (typeof content === 'string') {
|
|
39
39
|
return content
|
|
40
40
|
} else if (Array.isArray(content)) {
|
|
41
|
-
|
|
41
|
+
let formattedContent = ''
|
|
42
42
|
for (const toolResultBlock of content) {
|
|
43
|
+
let part
|
|
43
44
|
if (toolResultBlock.text) {
|
|
44
|
-
|
|
45
|
+
part = toolResultBlock.text
|
|
45
46
|
} else if (toolResultBlock.type === 'image') {
|
|
46
|
-
|
|
47
|
+
part = '([IMAGE DETECTED])'
|
|
47
48
|
}
|
|
49
|
+
if (part === undefined) continue
|
|
50
|
+
if (formattedContent) formattedContent += ','
|
|
51
|
+
formattedContent += part
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
return formattedContent
|
|
54
|
+
return formattedContent
|
|
51
55
|
}
|
|
52
56
|
return JSON.stringify(content)
|
|
53
57
|
}
|
|
@@ -18,12 +18,15 @@ function getToolOutputText (raw) {
|
|
|
18
18
|
if (raw == null) return
|
|
19
19
|
|
|
20
20
|
if (Array.isArray(raw)) {
|
|
21
|
-
|
|
21
|
+
let output = ''
|
|
22
|
+
let separator = ''
|
|
22
23
|
for (const block of raw) {
|
|
23
24
|
const text = getToolOutputText(block)
|
|
24
|
-
if (text)
|
|
25
|
+
if (!text) continue
|
|
26
|
+
output += separator + text
|
|
27
|
+
separator = '\n'
|
|
25
28
|
}
|
|
26
|
-
return output
|
|
29
|
+
return output
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
if (raw.type === 'tool_result') return getToolOutputText(raw.content)
|
|
@@ -134,15 +134,17 @@ function hasMultimodalInputs (variables) {
|
|
|
134
134
|
* @returns {{ content: string, audioParts: Array<{ mimeType: string, content: string }> }}
|
|
135
135
|
*/
|
|
136
136
|
function extractContentParts (parts) {
|
|
137
|
-
|
|
137
|
+
let content = ''
|
|
138
|
+
let hasContent = false
|
|
138
139
|
const audioParts = []
|
|
139
140
|
|
|
140
141
|
for (const part of parts) {
|
|
141
142
|
const partType = part?.type ?? ''
|
|
143
|
+
let extracted
|
|
142
144
|
if (partType === 'text') {
|
|
143
|
-
extracted
|
|
145
|
+
extracted = part.text ?? ''
|
|
144
146
|
} else if (partType === 'image_url') {
|
|
145
|
-
extracted
|
|
147
|
+
extracted = IMAGE_FALLBACK
|
|
146
148
|
} else if (partType === 'input_audio') {
|
|
147
149
|
const inputAudio = part.input_audio ?? {}
|
|
148
150
|
const data = inputAudio.data
|
|
@@ -151,14 +153,19 @@ function extractContentParts (parts) {
|
|
|
151
153
|
// is needed. Only fall back to "[audio]" when there's no audio to capture.
|
|
152
154
|
audioParts.push(formatAudioPart(data, audioMimeTypeFromFormat(inputAudio.format, AUDIO_MIME_TYPES)))
|
|
153
155
|
} else {
|
|
154
|
-
extracted
|
|
156
|
+
extracted = AUDIO_FALLBACK
|
|
155
157
|
}
|
|
156
158
|
} else {
|
|
157
|
-
extracted
|
|
159
|
+
extracted = `[${partType}]`
|
|
158
160
|
}
|
|
161
|
+
|
|
162
|
+
if (extracted === undefined) continue
|
|
163
|
+
if (hasContent) content += '\n'
|
|
164
|
+
content += extracted
|
|
165
|
+
hasContent = true
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
return { content
|
|
168
|
+
return { content, audioParts }
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
/**
|
|
@@ -11,23 +11,23 @@ const { safeJsonParse } = require('../../util')
|
|
|
11
11
|
* @returns {{ content: string, audioParts: Array<{ mimeType: string, content: string }> }}
|
|
12
12
|
*/
|
|
13
13
|
function extractMessageContent (parts) {
|
|
14
|
-
|
|
14
|
+
let content = ''
|
|
15
15
|
const audioParts = []
|
|
16
16
|
|
|
17
17
|
for (const part of parts) {
|
|
18
18
|
if (!part) continue
|
|
19
19
|
const text = extractTextFromContentItem(part)
|
|
20
20
|
if (text) {
|
|
21
|
-
|
|
21
|
+
content += text
|
|
22
22
|
continue
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const extracted = extractContentParts([part])
|
|
26
|
-
if (extracted.content)
|
|
26
|
+
if (extracted.content) content += extracted.content
|
|
27
27
|
if (extracted.audioParts.length > 0) audioParts.push(...extracted.audioParts)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return { content
|
|
30
|
+
return { content, audioParts }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -167,13 +167,11 @@ function extractOutputMessages (result) {
|
|
|
167
167
|
if (item.type === 'message') {
|
|
168
168
|
let content = ''
|
|
169
169
|
if (Array.isArray(item.content)) {
|
|
170
|
-
const textParts = []
|
|
171
170
|
for (const contentItem of item.content) {
|
|
172
171
|
if (contentItem?.type === 'output_text' && contentItem.text) {
|
|
173
|
-
|
|
172
|
+
content += contentItem.text
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
|
-
content = textParts.join('')
|
|
177
175
|
} else if (typeof item.content === 'string') {
|
|
178
176
|
content = item.content
|
|
179
177
|
}
|
|
@@ -4,6 +4,7 @@ const { URL, format } = require('node:url')
|
|
|
4
4
|
const path = require('node:path')
|
|
5
5
|
const request = require('../../exporters/common/request')
|
|
6
6
|
const { getEnvironmentVariable } = require('../../config/helper')
|
|
7
|
+
const { createServerlessDeliveryTracker } = require('../../serverless')
|
|
7
8
|
|
|
8
9
|
const logger = require('../../log')
|
|
9
10
|
|
|
@@ -33,6 +34,9 @@ class LLMObsBuffer {
|
|
|
33
34
|
|
|
34
35
|
class BaseLLMObsWriter {
|
|
35
36
|
#destroyer
|
|
37
|
+
/** @type {Function[]} */
|
|
38
|
+
#pendingFlushes = []
|
|
39
|
+
#serverlessDeliveryTracker = createServerlessDeliveryTracker()
|
|
36
40
|
/** @type {Map<string, LLMObsBuffer>} */
|
|
37
41
|
#multiTenantBuffers = new Map()
|
|
38
42
|
|
|
@@ -111,35 +115,52 @@ class BaseLLMObsWriter {
|
|
|
111
115
|
return true
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Drains buffered events and joins requests active at this flush boundary.
|
|
120
|
+
* @param {Function} [done]
|
|
121
|
+
*/
|
|
122
|
+
flush (done) {
|
|
115
123
|
if (this._agentless == null) {
|
|
124
|
+
if (done) this.#pendingFlushes.push(done)
|
|
116
125
|
return
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
|
|
120
|
-
if (this
|
|
121
|
-
|
|
122
|
-
this
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
const requests = this.#drainBuffers()
|
|
129
|
+
if (!this.#serverlessDeliveryTracker) {
|
|
130
|
+
// Only invocation-retaining platforms need completion-aware delivery.
|
|
131
|
+
for (const request of requests) this.#sendSafely(request)
|
|
132
|
+
done?.()
|
|
133
|
+
return
|
|
134
|
+
}
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
for (const request of requests) this.#sendSafely(request)
|
|
137
|
+
this.#serverlessDeliveryTracker.waitForIdle(done)
|
|
138
|
+
}
|
|
127
139
|
|
|
128
|
-
|
|
140
|
+
#sendSafely (requestToSend) {
|
|
141
|
+
try {
|
|
142
|
+
if (this.#serverlessDeliveryTracker) {
|
|
143
|
+
this.#serverlessDeliveryTracker.track(done => this.#send(requestToSend, done))
|
|
144
|
+
} else {
|
|
145
|
+
this.#send(requestToSend)
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
logger.error('Failed to send LLMObs %s events: %s', this._eventType, error.message)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
#drainBuffers () {
|
|
153
|
+
const requests = []
|
|
154
|
+
if (this._buffer.events.length > 0) {
|
|
155
|
+
const events = this._buffer.events
|
|
156
|
+
this._buffer.clear()
|
|
157
|
+
requests.push({ events, options: this._getOptions(), url: this.url })
|
|
133
158
|
}
|
|
134
159
|
|
|
135
|
-
// Flush multi-tenant buffers
|
|
136
160
|
for (const [apiKey, buffer] of this.#multiTenantBuffers) {
|
|
137
161
|
if (buffer.events.length === 0) continue
|
|
138
|
-
|
|
139
162
|
const events = buffer.events
|
|
140
163
|
buffer.clear()
|
|
141
|
-
|
|
142
|
-
const payload = this._encode(this.makePayload(events))
|
|
143
164
|
const site = buffer.routing.site || this._config.site
|
|
144
165
|
const options = {
|
|
145
166
|
headers: {
|
|
@@ -156,16 +177,21 @@ class BaseLLMObsWriter {
|
|
|
156
177
|
}
|
|
157
178
|
const url = this.#buildUrl(options.url.href, options.path)
|
|
158
179
|
const maskedApiKey = apiKey ? `****${apiKey.slice(-4)}` : ''
|
|
159
|
-
|
|
160
180
|
log.debug('Encoding and flushing multi-tenant buffer for %s', maskedApiKey)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
request(payload, options, (err, resp, code) => {
|
|
164
|
-
parseResponseAndLog(err, code, events.length, url, this._eventType)
|
|
165
|
-
})
|
|
181
|
+
requests.push({ events, options, url })
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
this.#cleanupEmptyBuffers()
|
|
185
|
+
return requests
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#send ({ events, options, url }, done) {
|
|
189
|
+
const payload = this._encode(this.makePayload(events))
|
|
190
|
+
log.debug('Encoded LLMObs payload: %s', payload)
|
|
191
|
+
request(payload, options, (err, resp, code) => {
|
|
192
|
+
parseResponseAndLog(err, code, events.length, url, this._eventType)
|
|
193
|
+
done?.()
|
|
194
|
+
})
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
#cleanupEmptyBuffers () {
|
|
@@ -178,13 +204,19 @@ class BaseLLMObsWriter {
|
|
|
178
204
|
|
|
179
205
|
makePayload (events) {}
|
|
180
206
|
|
|
181
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Stops periodic flushing and drains buffered events.
|
|
209
|
+
* @param {Function} [done] Called after queued and active deliveries complete.
|
|
210
|
+
*/
|
|
211
|
+
destroy (done) {
|
|
182
212
|
if (this.#destroyer) {
|
|
183
213
|
logger.debug(`Stopping ${this.constructor.name}`)
|
|
184
214
|
clearInterval(this._periodic)
|
|
185
215
|
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.delete(this.#destroyer)
|
|
186
|
-
this.flush()
|
|
216
|
+
this.flush(done)
|
|
187
217
|
this.#destroyer = undefined
|
|
218
|
+
} else {
|
|
219
|
+
done?.()
|
|
188
220
|
}
|
|
189
221
|
}
|
|
190
222
|
|
|
@@ -196,6 +228,10 @@ class BaseLLMObsWriter {
|
|
|
196
228
|
this._endpoint = endpoint
|
|
197
229
|
|
|
198
230
|
logger.debug(`Configuring ${this.constructor.name} to ${this.url}`)
|
|
231
|
+
|
|
232
|
+
const pendingFlushes = this.#pendingFlushes
|
|
233
|
+
this.#pendingFlushes = []
|
|
234
|
+
for (const done of pendingFlushes) this.flush(done)
|
|
199
235
|
}
|
|
200
236
|
|
|
201
237
|
_getUrlAndPath () {
|
|
@@ -26,6 +26,11 @@ class NoopSpan {
|
|
|
26
26
|
addLink (link) { return this }
|
|
27
27
|
addLinks (links) { return this }
|
|
28
28
|
addSpanPointer (ptrKind, ptrDir, ptrHash) { return this }
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('../../../../index').Exception} exception
|
|
31
|
+
* @param {import('../../../../index').SpanEventAttributes} [attributes]
|
|
32
|
+
*/
|
|
33
|
+
recordException (exception, attributes) {}
|
|
29
34
|
log () { return this }
|
|
30
35
|
logEvent () {}
|
|
31
36
|
finish (finishTime) {}
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
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
4
|
/**
|
|
23
5
|
* @constant
|
|
24
6
|
* @type {string} API endpoint for exposure events EVP track
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
const log = require('../log')
|
|
5
5
|
const ExposuresWriter = require('./writers/exposures')
|
|
6
|
-
const {
|
|
6
|
+
const { setExposureDeliveryStrategy } = require('./writers/util')
|
|
7
7
|
|
|
8
8
|
const exposureSubmitCh = channel('ffe:exposure:submit')
|
|
9
9
|
const flushCh = channel('ffe:writers:flush')
|
|
@@ -40,12 +40,15 @@ function enable (config) {
|
|
|
40
40
|
return
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const writer = new ExposuresWriter(config)
|
|
44
|
+
exposuresWriter = writer
|
|
44
45
|
exposureSubmitCh.subscribe(_handleExposureSubmit)
|
|
45
46
|
flushCh.subscribe(_handleFlush)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
exposuresWriter
|
|
48
|
+
setExposureDeliveryStrategy(config, (enabled, route) => {
|
|
49
|
+
if (exposuresWriter !== writer) return
|
|
50
|
+
|
|
51
|
+
writer.setEnabled(enabled, route)
|
|
49
52
|
})
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -11,14 +11,52 @@ const log = require('../../log')
|
|
|
11
11
|
* @property {number} [timeout] - Request timeout in milliseconds
|
|
12
12
|
* @property {object} config - Tracer configuration object
|
|
13
13
|
* @property {string} endpoint - API endpoint path
|
|
14
|
-
* @property {URL} [agentUrl] -
|
|
14
|
+
* @property {URL} [agentUrl] - Initial delivery URL
|
|
15
15
|
* @property {number} [payloadSizeLimit] - Maximum payload size in bytes
|
|
16
16
|
* @property {number} [eventSizeLimit] - Maximum individual event size in bytes
|
|
17
17
|
* @property {object} [headers] - Additional HTTP headers
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* @typedef {object} WriterRoute
|
|
22
|
+
* @property {URL} url - Route base URL
|
|
23
|
+
* @property {string} endpoint - Route endpoint
|
|
24
|
+
* @property {object} headers - Route-specific headers
|
|
25
|
+
* @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ActiveWriterRoute
|
|
30
|
+
* @property {URL} url - Route base URL
|
|
31
|
+
* @property {string} endpoint - Route endpoint
|
|
32
|
+
* @property {object} requestOptions - HTTP request options
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tests whether a local route definitively rejected an event batch.
|
|
37
|
+
*
|
|
38
|
+
* @param {Error | null} error - Request error
|
|
39
|
+
* @param {number | undefined} statusCode - HTTP response status
|
|
40
|
+
* @returns {boolean} Whether direct retry is safe
|
|
41
|
+
*/
|
|
42
|
+
function isDefinitiveRejection (error, statusCode) {
|
|
43
|
+
return error?.code === 'EAI_AGAIN' || error?.code === 'ECONNREFUSED' ||
|
|
44
|
+
error?.code === 'ENOENT' || error?.code === 'ENOTFOUND' ||
|
|
45
|
+
statusCode === 403 || statusCode === 404 || statusCode === 405
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tests whether a local route can have accepted an event batch before failing.
|
|
50
|
+
*
|
|
51
|
+
* @param {Error | null} error - Request error
|
|
52
|
+
* @returns {boolean} Whether the delivery result is ambiguous
|
|
53
|
+
*/
|
|
54
|
+
function isAmbiguousNetworkFailure (error) {
|
|
55
|
+
return error?.code === 'ECONNRESET' || error?.code === 'EPIPE' || error?.code === 'ETIMEDOUT'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Base writer for Feature Flagging and Experimentation event delivery.
|
|
22
60
|
* @class BaseFFEWriter
|
|
23
61
|
*/
|
|
24
62
|
class BaseFFEWriter {
|
|
@@ -40,6 +78,7 @@ class BaseFFEWriter {
|
|
|
40
78
|
this._payloadSizeLimit = payloadSizeLimit
|
|
41
79
|
this._eventSizeLimit = eventSizeLimit
|
|
42
80
|
this._headers = headers || {}
|
|
81
|
+
this._fallbackRoute = undefined
|
|
43
82
|
|
|
44
83
|
this._requestOptions = {
|
|
45
84
|
headers: {
|
|
@@ -47,6 +86,7 @@ class BaseFFEWriter {
|
|
|
47
86
|
'Content-Type': 'application/json',
|
|
48
87
|
},
|
|
49
88
|
method: 'POST',
|
|
89
|
+
retry: true,
|
|
50
90
|
timeout: this._timeout,
|
|
51
91
|
url: this._baseUrl,
|
|
52
92
|
path: this._endpoint,
|
|
@@ -115,15 +155,8 @@ class BaseFFEWriter {
|
|
|
115
155
|
// eslint-disable-next-line eslint-rules/eslint-log-printf-style
|
|
116
156
|
log.debug(() => `${this.constructor.name} flushing payload: ${safeJSONStringify(payload)}`)
|
|
117
157
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
log.error('Failed to send events to %s%s', this._baseUrl.href, this._endpoint, err)
|
|
121
|
-
} else if (code >= 200 && code < 300) {
|
|
122
|
-
log.debug('Successfully sent %d events', events.length)
|
|
123
|
-
} else {
|
|
124
|
-
log.warn('Events request returned status %d', code)
|
|
125
|
-
}
|
|
126
|
-
})
|
|
158
|
+
const route = this.#createActiveRoute()
|
|
159
|
+
this.#sendRequest(payload, events.length, route, this._fallbackRoute)
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
/**
|
|
@@ -161,6 +194,115 @@ class BaseFFEWriter {
|
|
|
161
194
|
_encode (payload) {
|
|
162
195
|
return JSON.stringify(payload)
|
|
163
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Applies the active route and an optional direct fallback route.
|
|
200
|
+
*
|
|
201
|
+
* @param {WriterRoute} route - Active route
|
|
202
|
+
* @param {WriterRoute} [fallbackRoute] - Direct fallback route
|
|
203
|
+
* @returns {void}
|
|
204
|
+
*/
|
|
205
|
+
_setRoutes (route, fallbackRoute) {
|
|
206
|
+
this.#activateRoute(this.#createRoute(route))
|
|
207
|
+
this._fallbackRoute = fallbackRoute ? this.#createRoute(fallbackRoute) : undefined
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Creates request state for a configured writer route.
|
|
212
|
+
*
|
|
213
|
+
* @param {WriterRoute} route - Configured route
|
|
214
|
+
* @returns {ActiveWriterRoute} Active route state
|
|
215
|
+
*/
|
|
216
|
+
#createRoute (route) {
|
|
217
|
+
return {
|
|
218
|
+
url: route.url,
|
|
219
|
+
endpoint: route.endpoint,
|
|
220
|
+
requestOptions: {
|
|
221
|
+
...(route.agent && { agent: route.agent }),
|
|
222
|
+
headers: {
|
|
223
|
+
...route.headers,
|
|
224
|
+
'Content-Type': 'application/json',
|
|
225
|
+
},
|
|
226
|
+
method: 'POST',
|
|
227
|
+
retry: true,
|
|
228
|
+
timeout: this._timeout,
|
|
229
|
+
url: route.url,
|
|
230
|
+
path: route.endpoint,
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Captures the current route for one event batch.
|
|
237
|
+
*
|
|
238
|
+
* @returns {ActiveWriterRoute} Active route state
|
|
239
|
+
*/
|
|
240
|
+
#createActiveRoute () {
|
|
241
|
+
return {
|
|
242
|
+
url: this._baseUrl,
|
|
243
|
+
endpoint: this._endpoint,
|
|
244
|
+
requestOptions: this._requestOptions,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Makes a route active for future event batches.
|
|
250
|
+
*
|
|
251
|
+
* @param {ActiveWriterRoute} route - Route state
|
|
252
|
+
* @returns {void}
|
|
253
|
+
*/
|
|
254
|
+
#activateRoute (route) {
|
|
255
|
+
this._baseUrl = route.url
|
|
256
|
+
this._endpoint = route.endpoint
|
|
257
|
+
this._requestOptions = route.requestOptions
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Sends an encoded batch and retries it through direct intake after a local route failure.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} payload - Encoded event batch
|
|
264
|
+
* @param {number} eventCount - Event count
|
|
265
|
+
* @param {ActiveWriterRoute} route - Selected route
|
|
266
|
+
* @param {ActiveWriterRoute} [fallbackRoute] - Direct fallback route
|
|
267
|
+
* @returns {void}
|
|
268
|
+
*/
|
|
269
|
+
#sendRequest (payload, eventCount, route, fallbackRoute) {
|
|
270
|
+
request(payload, route.requestOptions, (error, response, statusCode) => {
|
|
271
|
+
if (fallbackRoute && isDefinitiveRejection(error, statusCode)) {
|
|
272
|
+
log.debug(
|
|
273
|
+
'%s switching from %s%s to direct intake after definitive rejection',
|
|
274
|
+
this.constructor.name,
|
|
275
|
+
route.url.href,
|
|
276
|
+
route.endpoint
|
|
277
|
+
)
|
|
278
|
+
this.#activateRoute(fallbackRoute)
|
|
279
|
+
this._fallbackRoute = undefined
|
|
280
|
+
this.#sendRequest(payload, eventCount, fallbackRoute)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (fallbackRoute && isAmbiguousNetworkFailure(error)) {
|
|
285
|
+
log.debug(
|
|
286
|
+
'%s retrying through direct intake and switching future batches from %s%s after ambiguous failure',
|
|
287
|
+
this.constructor.name,
|
|
288
|
+
route.url.href,
|
|
289
|
+
route.endpoint
|
|
290
|
+
)
|
|
291
|
+
this.#activateRoute(fallbackRoute)
|
|
292
|
+
this._fallbackRoute = undefined
|
|
293
|
+
this.#sendRequest(payload, eventCount, fallbackRoute)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (error) {
|
|
298
|
+
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, error.message)
|
|
299
|
+
} else if (statusCode >= 200 && statusCode < 300) {
|
|
300
|
+
log.debug('Successfully sent %d events', eventCount)
|
|
301
|
+
} else {
|
|
302
|
+
log.warn('Events request returned status %d', statusCode)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
}
|
|
164
306
|
}
|
|
165
307
|
|
|
166
308
|
module.exports = BaseFFEWriter
|