dd-trace 6.17.0 → 6.18.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/index.d.ts +52 -9
- package/package.json +1 -1
- package/packages/datadog-instrumentations/src/cucumber.js +10 -5
- package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
- package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +43 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +4 -0
- package/packages/datadog-instrumentations/src/jest.js +27 -3
- package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
- package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
- package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
- package/packages/datadog-instrumentations/src/playwright.js +47 -6
- package/packages/datadog-instrumentations/src/undici.js +7 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +91 -20
- package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
- package/packages/datadog-instrumentations/src/ws.js +41 -1
- package/packages/datadog-plugin-cucumber/src/index.js +3 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +44 -14
- package/packages/datadog-plugin-cypress/src/index.js +3 -1
- package/packages/datadog-plugin-jest/src/index.js +3 -4
- package/packages/datadog-plugin-mocha/src/index.js +3 -4
- package/packages/datadog-plugin-playwright/src/index.js +11 -6
- package/packages/datadog-plugin-vitest/src/index.js +3 -4
- package/packages/datadog-plugin-ws/src/producer.js +1 -1
- package/packages/datadog-shimmer/src/shimmer.js +14 -10
- package/packages/datadog-turbopack/index.js +4 -1
- package/packages/datadog-turbopack/src/loader.js +6 -2
- package/packages/dd-trace/src/agent/info.js +5 -4
- package/packages/dd-trace/src/config/generated-config-types.d.ts +15 -15
- package/packages/dd-trace/src/config/index.js +7 -6
- package/packages/dd-trace/src/config/supported-configurations.json +11 -0
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/debugger/index.js +1 -1
- package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
- package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
- package/packages/dd-trace/src/evp_proxy/path.js +21 -7
- package/packages/dd-trace/src/exporters/common/url.js +33 -2
- package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +77 -11
- package/packages/dd-trace/src/llmobs/tagger.js +9 -4
- package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
- package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
- package/packages/dd-trace/src/openfeature/index.js +6 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
- package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
- package/packages/dd-trace/src/opentracing/tracer.js +3 -3
- package/packages/dd-trace/src/plugin_manager.js +27 -8
- package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
- package/packages/dd-trace/src/plugins/util/test.js +9 -4
- package/packages/dd-trace/src/proxy.js +2 -2
- package/packages/dd-trace/src/telemetry/send-data.js +2 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/packages/dd-trace/src/tracer.js +5 -4
- package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
- package/packages/datadog-instrumentations/src/bullmq.js +0 -11
- package/packages/datadog-instrumentations/src/langchain.js +0 -7
- package/packages/datadog-instrumentations/src/langgraph.js +0 -7
- package/packages/datadog-instrumentations/src/mercurius.js +0 -11
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('../../../../../index').llmobs.FormattedPromptMessage} FormattedPromptMessage */
|
|
4
|
+
|
|
3
5
|
const VARIABLE_PATTERN = /(?<!\{)(?:\{\{\s*(\w+)\s*\}\}(?!\})|\{\s*(\w+)\s*\}(?!\}))/g
|
|
4
6
|
|
|
7
|
+
function isMessage (value) {
|
|
8
|
+
return typeof value?.role === 'string' &&
|
|
9
|
+
value?.type !== 'placeholder' &&
|
|
10
|
+
(value?.content == null || typeof value.content === 'string') &&
|
|
11
|
+
(typeof value?.content === 'string' ||
|
|
12
|
+
(Array.isArray(value?.tool_calls) && value.tool_calls.length > 0) ||
|
|
13
|
+
(Array.isArray(value?.tool_results) && value.tool_results.length > 0))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isPlaceholder (value) {
|
|
17
|
+
return value?.type === 'placeholder' && typeof value?.name === 'string'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isObject (value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isTool (value) {
|
|
25
|
+
return isObject(value) &&
|
|
26
|
+
(value.id == null || typeof value.id === 'string') &&
|
|
27
|
+
(value.type == null || typeof value.type === 'string') &&
|
|
28
|
+
(value.tool_id == null || typeof value.tool_id === 'string') &&
|
|
29
|
+
(value.name == null || typeof value.name === 'string')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasValidTools (message) {
|
|
33
|
+
return (message.tool_call_id == null || typeof message.tool_call_id === 'string') &&
|
|
34
|
+
(message.tool_calls == null || (Array.isArray(message.tool_calls) && message.tool_calls.every(call =>
|
|
35
|
+
isTool(call) && (call.function == null ||
|
|
36
|
+
(isObject(call.function) &&
|
|
37
|
+
typeof call.function.name === 'string' && typeof call.function.arguments === 'string'))))) &&
|
|
38
|
+
(message.tool_results == null || (Array.isArray(message.tool_results) && message.tool_results.every(isTool)))
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
function render (template, variables) {
|
|
6
42
|
return template.replaceAll(VARIABLE_PATTERN, (match, doubleName, singleName) => {
|
|
7
43
|
const name = doubleName ?? singleName
|
|
@@ -9,23 +45,40 @@ function render (template, variables) {
|
|
|
9
45
|
})
|
|
10
46
|
}
|
|
11
47
|
|
|
48
|
+
function freezeConfig (config) {
|
|
49
|
+
if (config === undefined) return Object.freeze({})
|
|
50
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
51
|
+
throw new TypeError('Invalid prompt config: expected a JSON object')
|
|
52
|
+
}
|
|
53
|
+
const copy = structuredClone(config)
|
|
54
|
+
const freeze = value => {
|
|
55
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
|
|
56
|
+
for (const nested of Object.values(value)) freeze(nested)
|
|
57
|
+
return Object.freeze(value)
|
|
58
|
+
}
|
|
59
|
+
return freeze(copy)
|
|
60
|
+
}
|
|
12
61
|
class ManagedPrompt {
|
|
13
62
|
/**
|
|
14
63
|
* @param {object} data
|
|
15
64
|
* @param {string} data.id
|
|
16
65
|
* @param {string} data.version
|
|
17
66
|
* @param {'registry'|'cache'|'fallback'|'ff'|'resolve'} data.source
|
|
18
|
-
* @param {string | Array<{role: string, content: string}>} data.template
|
|
67
|
+
* @param {string | Array<{role: string, content: string} | {type: 'placeholder', name: string}>} data.template
|
|
68
|
+
* @param {Record<string, unknown>} [data.config]
|
|
19
69
|
* @param {string} [data.promptUuid]
|
|
20
70
|
* @param {string} [data.promptVersionUuid]
|
|
21
71
|
*/
|
|
22
|
-
constructor ({ id, version, source, template, promptUuid, promptVersionUuid }) {
|
|
72
|
+
constructor ({ id, version, source, template, config, promptUuid, promptVersionUuid }) {
|
|
23
73
|
this.id = id
|
|
24
74
|
this.version = version
|
|
25
75
|
this.source = source
|
|
26
76
|
this.template = Array.isArray(template)
|
|
27
|
-
? Object.freeze(template.map(
|
|
77
|
+
? Object.freeze(template.map(item => Object.freeze(isPlaceholder(item)
|
|
78
|
+
? { type: 'placeholder', name: item.name }
|
|
79
|
+
: { role: item.role, content: item.content })))
|
|
28
80
|
: template
|
|
81
|
+
this.config = freezeConfig(config)
|
|
29
82
|
this.promptUuid = promptUuid
|
|
30
83
|
this.promptVersionUuid = promptVersionUuid
|
|
31
84
|
Object.freeze(this)
|
|
@@ -34,11 +87,21 @@ class ManagedPrompt {
|
|
|
34
87
|
/**
|
|
35
88
|
* Render the prompt without changing its stored template.
|
|
36
89
|
* @param {Record<string, unknown>} [variables]
|
|
37
|
-
* @returns {string |
|
|
90
|
+
* @returns {string | FormattedPromptMessage[]}
|
|
38
91
|
*/
|
|
39
92
|
format (variables = {}) {
|
|
40
93
|
if (typeof this.template === 'string') return render(this.template, variables)
|
|
41
|
-
return this.template.
|
|
94
|
+
return this.template.flatMap(item => {
|
|
95
|
+
if (!isPlaceholder(item)) return [{ ...item, content: render(item.content, variables) }]
|
|
96
|
+
if (!Object.hasOwn(variables, item.name)) {
|
|
97
|
+
throw new TypeError(`Missing message placeholder variable '${item.name}'`)
|
|
98
|
+
}
|
|
99
|
+
const messages = variables[item.name]
|
|
100
|
+
if (!Array.isArray(messages) || !messages.every(message => isMessage(message) && hasValidTools(message))) {
|
|
101
|
+
throw new TypeError(`Invalid message placeholder variable '${item.name}': expected an array of messages`)
|
|
102
|
+
}
|
|
103
|
+
return structuredClone(messages)
|
|
104
|
+
})
|
|
42
105
|
}
|
|
43
106
|
|
|
44
107
|
/**
|
|
@@ -52,7 +115,10 @@ class ManagedPrompt {
|
|
|
52
115
|
version: this.version,
|
|
53
116
|
template: typeof this.template === 'string' ? this.template : this.template.map(message => ({ ...message })),
|
|
54
117
|
}
|
|
55
|
-
const
|
|
118
|
+
const placeholderNames = new Set(Array.isArray(this.template)
|
|
119
|
+
? this.template.filter(isPlaceholder).map(item => item.name)
|
|
120
|
+
: [])
|
|
121
|
+
const entries = Object.entries(variables ?? {}).filter(([name]) => !placeholderNames.has(name))
|
|
56
122
|
if (entries.length) {
|
|
57
123
|
annotation.variables = Object.fromEntries(entries.map(([name, value]) => [name, String(value)]))
|
|
58
124
|
}
|
|
@@ -64,8 +130,8 @@ class ManagedPrompt {
|
|
|
64
130
|
/**
|
|
65
131
|
* Convert a caller fallback to a managed prompt.
|
|
66
132
|
* @param {string} promptId
|
|
67
|
-
* @param {string | object | Array<{role: string, content: string}> |
|
|
68
|
-
* (() => string | object | Array<{role: string, content: string}>)} fallback
|
|
133
|
+
* @param {string | object | Array<{role: string, content: string} | {type: 'placeholder', name: string}> |
|
|
134
|
+
* (() => string | object | Array<{role: string, content: string} | {type: 'placeholder', name: string}>)} fallback
|
|
69
135
|
* @returns {ManagedPrompt}
|
|
70
136
|
*/
|
|
71
137
|
static fromFallback (promptId, fallback) {
|
|
@@ -73,9 +139,8 @@ class ManagedPrompt {
|
|
|
73
139
|
const promptLike = value && !Array.isArray(value) && typeof value === 'object'
|
|
74
140
|
const template = promptLike ? value.template : value
|
|
75
141
|
const validTemplate = typeof template === 'string' || (
|
|
76
|
-
Array.isArray(template) && template.every(
|
|
77
|
-
|
|
78
|
-
})
|
|
142
|
+
Array.isArray(template) && template.every(item =>
|
|
143
|
+
isPlaceholder(item) || (isMessage(item) && typeof item.content === 'string'))
|
|
79
144
|
)
|
|
80
145
|
if (!validTemplate) {
|
|
81
146
|
throw new TypeError('Invalid prompt fallback: expected a string, chat message array, or object with a template')
|
|
@@ -85,6 +150,7 @@ class ManagedPrompt {
|
|
|
85
150
|
version: String(promptLike && value.version ? value.version : 'fallback'),
|
|
86
151
|
source: 'fallback',
|
|
87
152
|
template,
|
|
153
|
+
config: promptLike ? value.config : undefined,
|
|
88
154
|
})
|
|
89
155
|
}
|
|
90
156
|
}
|
|
@@ -505,10 +505,13 @@ class LLMObsTagger {
|
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
if (Array.isArray(template)) {
|
|
508
|
-
for (const
|
|
509
|
-
|
|
508
|
+
for (const item of template) {
|
|
509
|
+
const valid = item?.type === 'placeholder'
|
|
510
|
+
? typeof item.name === 'string'
|
|
511
|
+
: typeof item?.role === 'string' && typeof item?.content === 'string'
|
|
512
|
+
if (!valid) {
|
|
510
513
|
this.#handleFailure(
|
|
511
|
-
'Prompt chat template must
|
|
514
|
+
'Prompt chat template must contain messages or message placeholders.', 'invalid_prompt'
|
|
512
515
|
)
|
|
513
516
|
return
|
|
514
517
|
}
|
|
@@ -532,7 +535,9 @@ class LLMObsTagger {
|
|
|
532
535
|
if (typeof template === 'string') {
|
|
533
536
|
finalTemplate = template
|
|
534
537
|
} else if (Array.isArray(template)) {
|
|
535
|
-
finalChatTemplate = template.map(
|
|
538
|
+
finalChatTemplate = template.map(item => item.type === 'placeholder'
|
|
539
|
+
? { type: 'placeholder', name: item.name }
|
|
540
|
+
: { role: item.role, content: item.content })
|
|
536
541
|
}
|
|
537
542
|
|
|
538
543
|
const validatedPrompt = {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { createSiteUrl } = require('../exporters/common/url')
|
|
3
4
|
const log = require('../log')
|
|
4
5
|
|
|
5
6
|
const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server'
|
|
@@ -60,7 +61,9 @@ function endpoint (config, configuredBaseUrl) {
|
|
|
60
61
|
const configured = configuredBaseUrl?.trim()
|
|
61
62
|
|
|
62
63
|
if (!configured) {
|
|
63
|
-
const url =
|
|
64
|
+
const url = createSiteUrl(config.site, 'ufc-server.ff-cdn')
|
|
65
|
+
if (url === undefined) throw new Error('Invalid DD_SITE for Feature Flagging agentless configuration')
|
|
66
|
+
url.pathname = DEFAULT_AGENTLESS_PATH
|
|
64
67
|
if (config.env) url.searchParams.set('dd_env', config.env)
|
|
65
68
|
return url
|
|
66
69
|
}
|
|
@@ -27,12 +27,12 @@ class FlaggingProvider extends DatadogNodeServerProvider {
|
|
|
27
27
|
constructor (tracer, config) {
|
|
28
28
|
super({
|
|
29
29
|
exposureChannel: channel(EXPOSURE_CHANNEL),
|
|
30
|
-
initializationTimeoutMs: config.
|
|
30
|
+
initializationTimeoutMs: config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS,
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
this.hooks.push(new EvalMetricsHook(config))
|
|
34
34
|
|
|
35
|
-
if (config.
|
|
35
|
+
if (config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED) {
|
|
36
36
|
this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
|
|
37
37
|
// @ts-expect-error The upstream constructor always initializes its optional hooks property.
|
|
38
38
|
this.hooks.push(this.#spanEnrichmentHook)
|
|
@@ -42,7 +42,7 @@ class FlaggingProvider extends DatadogNodeServerProvider {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
log.debug('%s created with timeout: %dms', this.constructor.name,
|
|
45
|
-
config.
|
|
45
|
+
config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS)
|
|
46
46
|
|
|
47
47
|
this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
|
|
48
48
|
this.#configurationSource?.start()
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
const log = require('../log')
|
|
5
5
|
const ExposuresWriter = require('./writers/exposures')
|
|
6
|
-
const {
|
|
6
|
+
const { setEventDeliveryStrategy } = require('./writers/util')
|
|
7
7
|
|
|
8
8
|
const exposureSubmitCh = channel('ffe:exposure:submit')
|
|
9
9
|
const flushCh = channel('ffe:writers:flush')
|
|
10
10
|
|
|
11
11
|
let exposuresWriter = null
|
|
12
|
+
let stopEventDeliveryStrategy
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @private
|
|
@@ -42,7 +43,7 @@ function enable (config) {
|
|
|
42
43
|
exposureSubmitCh.subscribe(_handleExposureSubmit)
|
|
43
44
|
flushCh.subscribe(_handleFlush)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
stopEventDeliveryStrategy = setEventDeliveryStrategy(config, (enabled, route) => {
|
|
46
47
|
if (exposuresWriter !== writer) return
|
|
47
48
|
|
|
48
49
|
writer.setEnabled(enabled, route)
|
|
@@ -55,6 +56,9 @@ function enable (config) {
|
|
|
55
56
|
function disable () {
|
|
56
57
|
if (!exposuresWriter) return
|
|
57
58
|
|
|
59
|
+
stopEventDeliveryStrategy?.()
|
|
60
|
+
stopEventDeliveryStrategy = undefined
|
|
61
|
+
|
|
58
62
|
if (exposureSubmitCh.hasSubscribers) {
|
|
59
63
|
exposureSubmitCh.unsubscribe(_handleExposureSubmit)
|
|
60
64
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const request = require('../../exporters/common/request')
|
|
4
|
-
const { safeJSONStringify } = require('../../exporters/common/util')
|
|
5
4
|
|
|
6
5
|
const log = require('../../log')
|
|
6
|
+
const tracerVersion = require('../../../../../package.json').version
|
|
7
|
+
|
|
8
|
+
const EVP_ORIGIN_HEADERS = {
|
|
9
|
+
'DD-EVP-ORIGIN': 'dd-trace-js',
|
|
10
|
+
'DD-EVP-ORIGIN-VERSION': tracerVersion,
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* @typedef {object} BaseFFEWriterOptions
|
|
@@ -23,6 +28,8 @@ const log = require('../../log')
|
|
|
23
28
|
* @property {string} endpoint - Route endpoint
|
|
24
29
|
* @property {object} headers - Route-specific headers
|
|
25
30
|
* @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
|
|
31
|
+
* @property {Function} [onFallback] - Called after direct fallback becomes active
|
|
32
|
+
* @property {Function} [onUnavailable] - Called after the local route becomes unavailable
|
|
26
33
|
*/
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -30,6 +37,8 @@ const log = require('../../log')
|
|
|
30
37
|
* @property {URL} url - Route base URL
|
|
31
38
|
* @property {string} endpoint - Route endpoint
|
|
32
39
|
* @property {object} requestOptions - HTTP request options
|
|
40
|
+
* @property {Function} [onFallback] - Called after direct fallback becomes active
|
|
41
|
+
* @property {Function} [onUnavailable] - Called after the local route becomes unavailable
|
|
33
42
|
*/
|
|
34
43
|
|
|
35
44
|
/**
|
|
@@ -38,19 +47,31 @@ const log = require('../../log')
|
|
|
38
47
|
* @param {Error | null} error - Request error
|
|
39
48
|
* @param {number | undefined} statusCode - HTTP response status
|
|
40
49
|
*/
|
|
41
|
-
function
|
|
50
|
+
function isSafeToReplay (error, statusCode) {
|
|
42
51
|
return error?.code === 'EAI_AGAIN' || error?.code === 'ECONNREFUSED' ||
|
|
43
52
|
error?.code === 'ENOENT' || error?.code === 'ENOTFOUND' ||
|
|
44
|
-
statusCode ===
|
|
53
|
+
statusCode === 404 || statusCode === 405
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
|
-
* Tests whether a local route
|
|
57
|
+
* Tests whether a local route failed without an authoritative HTTP response.
|
|
49
58
|
*
|
|
50
59
|
* @param {Error | null} error - Request error
|
|
60
|
+
* @param {number | undefined} statusCode - HTTP response status
|
|
61
|
+
*/
|
|
62
|
+
function isTransportFailure (error, statusCode) {
|
|
63
|
+
// SDK backpressure says nothing about the local receiver's health.
|
|
64
|
+
return error !== null && error !== undefined && statusCode === undefined &&
|
|
65
|
+
error.code !== 'ERR_DD_REQUEST_BUFFER_FULL'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Tests whether an HTTP response should move only future Agentless batches.
|
|
70
|
+
*
|
|
71
|
+
* @param {number | undefined} statusCode - HTTP response status
|
|
51
72
|
*/
|
|
52
|
-
function
|
|
53
|
-
return
|
|
73
|
+
function shouldSwitchFutureRoute (statusCode) {
|
|
74
|
+
return statusCode === 403 || statusCode === 429 || statusCode >= 500 && statusCode < 600
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
/**
|
|
@@ -81,10 +102,11 @@ class BaseFFEWriter {
|
|
|
81
102
|
this._requestOptions = {
|
|
82
103
|
headers: {
|
|
83
104
|
...this._headers,
|
|
105
|
+
...EVP_ORIGIN_HEADERS,
|
|
84
106
|
'Content-Type': 'application/json',
|
|
85
107
|
},
|
|
86
108
|
method: 'POST',
|
|
87
|
-
retry:
|
|
109
|
+
retry: false,
|
|
88
110
|
timeout: this._timeout,
|
|
89
111
|
url: this._baseUrl,
|
|
90
112
|
path: this._endpoint,
|
|
@@ -150,11 +172,10 @@ class BaseFFEWriter {
|
|
|
150
172
|
|
|
151
173
|
const payload = this._encode(this.makePayload(events))
|
|
152
174
|
|
|
175
|
+
// Keep byte counting behind the debug callback because payloads can be several megabytes.
|
|
153
176
|
// eslint-disable-next-line eslint-rules/eslint-log-printf-style
|
|
154
|
-
log.debug(() => `${this.constructor.name} flushing
|
|
155
|
-
|
|
156
|
-
const route = this.#createActiveRoute()
|
|
157
|
-
this.#sendRequest(payload, events.length, route, this._fallbackRoute)
|
|
177
|
+
log.debug(() => `${this.constructor.name} flushing ${events.length} events (${Buffer.byteLength(payload)} bytes)`)
|
|
178
|
+
this._sendPayload(payload, events.length)
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
/**
|
|
@@ -192,6 +213,18 @@ class BaseFFEWriter {
|
|
|
192
213
|
return JSON.stringify(payload)
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Sends one encoded event batch through a snapshot of the selected routes.
|
|
218
|
+
*
|
|
219
|
+
* @protected
|
|
220
|
+
* @param {string} payload - Encoded event batch
|
|
221
|
+
* @param {number} eventCount - Event count
|
|
222
|
+
*/
|
|
223
|
+
_sendPayload (payload, eventCount) {
|
|
224
|
+
const route = this.#createActiveRoute()
|
|
225
|
+
this.#sendRequest(payload, eventCount, route, this._fallbackRoute)
|
|
226
|
+
}
|
|
227
|
+
|
|
195
228
|
/**
|
|
196
229
|
* Applies the active route and an optional direct fallback route.
|
|
197
230
|
*
|
|
@@ -213,10 +246,11 @@ class BaseFFEWriter {
|
|
|
213
246
|
const requestOptions = {
|
|
214
247
|
headers: {
|
|
215
248
|
...route.headers,
|
|
249
|
+
...EVP_ORIGIN_HEADERS,
|
|
216
250
|
'Content-Type': 'application/json',
|
|
217
251
|
},
|
|
218
252
|
method: 'POST',
|
|
219
|
-
retry:
|
|
253
|
+
retry: false,
|
|
220
254
|
timeout: this._timeout,
|
|
221
255
|
url: route.url,
|
|
222
256
|
path: route.endpoint,
|
|
@@ -227,6 +261,8 @@ class BaseFFEWriter {
|
|
|
227
261
|
url: route.url,
|
|
228
262
|
endpoint: route.endpoint,
|
|
229
263
|
requestOptions,
|
|
264
|
+
onFallback: route.onFallback,
|
|
265
|
+
onUnavailable: route.onUnavailable,
|
|
230
266
|
}
|
|
231
267
|
}
|
|
232
268
|
|
|
@@ -240,6 +276,8 @@ class BaseFFEWriter {
|
|
|
240
276
|
url: this._baseUrl,
|
|
241
277
|
endpoint: this._endpoint,
|
|
242
278
|
requestOptions: this._requestOptions,
|
|
279
|
+
onFallback: this._onFallback,
|
|
280
|
+
onUnavailable: this._onUnavailable,
|
|
243
281
|
}
|
|
244
282
|
}
|
|
245
283
|
|
|
@@ -252,10 +290,12 @@ class BaseFFEWriter {
|
|
|
252
290
|
this._baseUrl = route.url
|
|
253
291
|
this._endpoint = route.endpoint
|
|
254
292
|
this._requestOptions = route.requestOptions
|
|
293
|
+
this._onFallback = route.onFallback
|
|
294
|
+
this._onUnavailable = route.onUnavailable
|
|
255
295
|
}
|
|
256
296
|
|
|
257
297
|
/**
|
|
258
|
-
* Sends an encoded batch
|
|
298
|
+
* Sends an encoded batch once per route and applies safe local fallback semantics.
|
|
259
299
|
*
|
|
260
300
|
* @param {string} payload - Encoded event batch
|
|
261
301
|
* @param {number} eventCount - Event count
|
|
@@ -264,7 +304,7 @@ class BaseFFEWriter {
|
|
|
264
304
|
*/
|
|
265
305
|
#sendRequest (payload, eventCount, route, fallbackRoute) {
|
|
266
306
|
request(payload, route.requestOptions, (error, response, statusCode) => {
|
|
267
|
-
if (fallbackRoute &&
|
|
307
|
+
if (fallbackRoute && isSafeToReplay(error, statusCode)) {
|
|
268
308
|
log.debug(
|
|
269
309
|
'%s switching from %s%s to direct intake after definitive rejection',
|
|
270
310
|
this.constructor.name,
|
|
@@ -273,20 +313,53 @@ class BaseFFEWriter {
|
|
|
273
313
|
)
|
|
274
314
|
this.#activateRoute(fallbackRoute)
|
|
275
315
|
this._fallbackRoute = undefined
|
|
316
|
+
route.onFallback?.()
|
|
276
317
|
this.#sendRequest(payload, eventCount, fallbackRoute)
|
|
277
318
|
return
|
|
278
319
|
}
|
|
279
320
|
|
|
280
|
-
if (fallbackRoute &&
|
|
321
|
+
if (fallbackRoute && isTransportFailure(error, statusCode)) {
|
|
281
322
|
log.debug(
|
|
282
|
-
'%s
|
|
323
|
+
'%s switching future batches from %s%s to direct intake after an ambiguous failure without replay',
|
|
283
324
|
this.constructor.name,
|
|
284
325
|
route.url.href,
|
|
285
326
|
route.endpoint
|
|
286
327
|
)
|
|
287
328
|
this.#activateRoute(fallbackRoute)
|
|
288
329
|
this._fallbackRoute = undefined
|
|
289
|
-
|
|
330
|
+
route.onFallback?.()
|
|
331
|
+
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, error.message)
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (fallbackRoute && shouldSwitchFutureRoute(statusCode)) {
|
|
336
|
+
log.debug(
|
|
337
|
+
'%s switching future batches from %s%s to direct intake after status %d without replay',
|
|
338
|
+
this.constructor.name,
|
|
339
|
+
route.url.href,
|
|
340
|
+
route.endpoint,
|
|
341
|
+
statusCode
|
|
342
|
+
)
|
|
343
|
+
this.#activateRoute(fallbackRoute)
|
|
344
|
+
this._fallbackRoute = undefined
|
|
345
|
+
route.onFallback?.()
|
|
346
|
+
log.warn('Events request returned status %d', statusCode)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (
|
|
351
|
+
!fallbackRoute &&
|
|
352
|
+
route.onUnavailable &&
|
|
353
|
+
(isSafeToReplay(error, statusCode) ||
|
|
354
|
+
isTransportFailure(error, statusCode) ||
|
|
355
|
+
shouldSwitchFutureRoute(statusCode))
|
|
356
|
+
) {
|
|
357
|
+
route.onUnavailable()
|
|
358
|
+
if (error) {
|
|
359
|
+
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, error.message)
|
|
360
|
+
} else {
|
|
361
|
+
log.warn('Events request returned status %d', statusCode)
|
|
362
|
+
}
|
|
290
363
|
return
|
|
291
364
|
}
|
|
292
365
|
|
|
@@ -26,6 +26,8 @@ const PENDING_MAX_EVENTS = 1000
|
|
|
26
26
|
* @property {object} [headers] - Route-specific headers
|
|
27
27
|
* @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
|
|
28
28
|
* @property {ExposureRoute} [fallback] - Optional direct fallback route
|
|
29
|
+
* @property {Function} [onFallback] - Called after direct fallback becomes active
|
|
30
|
+
* @property {Function} [onUnavailable] - Called after the local route becomes unavailable
|
|
29
31
|
*/
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -150,6 +152,8 @@ class ExposuresWriter extends BaseFFEWriter {
|
|
|
150
152
|
endpoint: joinEVPProxyPath(route.basePath, EXPOSURES_ENDPOINT),
|
|
151
153
|
headers,
|
|
152
154
|
agent: route.agent,
|
|
155
|
+
onFallback: route.onFallback,
|
|
156
|
+
onUnavailable: route.onUnavailable,
|
|
153
157
|
}, fallbackRoute)
|
|
154
158
|
}
|
|
155
159
|
|