dd-trace 5.120.0 → 5.121.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/ci/vitest-no-worker-init-setup.mjs +112 -10
- package/index.d.ts +42 -0
- package/package.json +4 -3
- package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
- package/packages/datadog-instrumentations/src/jest.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/utils.js +2 -8
- package/packages/datadog-instrumentations/src/mysql.js +45 -15
- package/packages/datadog-instrumentations/src/mysql2.js +119 -18
- package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
- package/packages/datadog-instrumentations/src/pg.js +116 -92
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
- package/packages/datadog-instrumentations/src/vitest-main.js +169 -17
- package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
- package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
- package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
- package/packages/datadog-plugin-jest/src/index.js +5 -4
- package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
- package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
- package/packages/datadog-plugin-mocha/src/index.js +550 -39
- package/packages/datadog-plugin-mysql/src/index.js +37 -0
- package/packages/datadog-plugin-next/src/index.js +1 -19
- package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
- package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
- package/packages/datadog-plugin-pg/src/index.js +1 -0
- package/packages/datadog-plugin-vitest/src/index.js +8 -20
- package/packages/datadog-plugin-ws/src/util.js +2 -1
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
- package/packages/dd-trace/src/carrier.js +356 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
- package/packages/dd-trace/src/config/supported-configurations.json +10 -0
- package/packages/dd-trace/src/datastreams/context.js +6 -1
- package/packages/dd-trace/src/datastreams/manager.js +5 -1
- package/packages/dd-trace/src/datastreams/pathway.js +23 -22
- package/packages/dd-trace/src/datastreams/processor.js +3 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
- package/packages/dd-trace/src/llmobs/index.js +34 -2
- package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
- package/packages/dd-trace/src/llmobs/tagger.js +96 -0
- package/packages/dd-trace/src/llmobs/util.js +78 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +1 -62
- package/packages/dd-trace/src/plugins/util/test.js +84 -5
- package/packages/dd-trace/src/plugins/util/web.js +53 -2
|
@@ -3,16 +3,19 @@ import { afterEach, beforeAll, beforeEach, inject } from 'vitest'
|
|
|
3
3
|
// Instrumentation-less setup for DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT.
|
|
4
4
|
// It applies Test Optimization execution changes without initializing dd-trace and also supports Browser Mode.
|
|
5
5
|
const VITEST_NO_WORKER_INIT_ACTIVE_ENV = 'DD_TEST_OPT_VITEST_NO_WORKER_INIT_ACTIVE'
|
|
6
|
+
const SERIALIZED_CONTEXT_PREFIX = '\u0000dd-vitest-context:'
|
|
6
7
|
const providedContext = getProvidedContext()
|
|
7
8
|
const isNoWorkerInitActive = providedContext.isActive ?? getIsNoWorkerInitActive()
|
|
8
9
|
const attemptToFixTests = providedContext.attemptToFixTests || {}
|
|
9
10
|
const attemptToFixRetries = providedContext.attemptToFixRetries || 0
|
|
10
11
|
const disabledTests = providedContext.disabledTests || {}
|
|
12
|
+
const efdSuiteAdmissionBrowserCommand = providedContext.efdSuiteAdmissionBrowserCommand
|
|
11
13
|
const earlyFlakeDetectionRetryPolicy = providedContext.earlyFlakeDetectionRetryPolicy || {
|
|
12
14
|
durationRetryCounts: [],
|
|
13
15
|
schedulingRetryCount: 0,
|
|
14
16
|
}
|
|
15
17
|
const earlyFlakeDetectionRetries = earlyFlakeDetectionRetryPolicy.schedulingRetryCount
|
|
18
|
+
const isEfdSuiteAdmissionEnabled = providedContext.isEfdSuiteAdmissionEnabled === true
|
|
16
19
|
const isEarlyFlakeDetectionEnabled = providedContext.isEarlyFlakeDetectionEnabled === true
|
|
17
20
|
const knownTests = providedContext.knownTests || {}
|
|
18
21
|
const modifiedFiles = providedContext.modifiedFiles || {}
|
|
@@ -34,6 +37,7 @@ const earlyFlakeDetectionStartByTask = new WeakMap()
|
|
|
34
37
|
const nextAttemptIndexByTask = new WeakMap()
|
|
35
38
|
const retryAttemptIndexByTask = new WeakMap()
|
|
36
39
|
const usedRumTestExecutionIds = new Set()
|
|
40
|
+
let browserCommands
|
|
37
41
|
let now
|
|
38
42
|
let timeOrigin
|
|
39
43
|
// Use an unfaked monotonic clock in Node and Vitest's parent frame in Browser Mode.
|
|
@@ -48,18 +52,42 @@ if (typeof globalThis.process?.uptime === 'function') {
|
|
|
48
52
|
timeOrigin = Number.isFinite(clock?.timeOrigin) ? clock.timeOrigin : Date.now() - now()
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Requests permission from the Vitest main process to schedule Browser Mode EFD retries for one suite.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} testSuite
|
|
59
|
+
* @param {boolean} hasNewTest
|
|
60
|
+
* @returns {Promise<boolean>}
|
|
61
|
+
*/
|
|
62
|
+
async function requestBrowserEfdSuiteAdmission (testSuite, hasNewTest) {
|
|
63
|
+
try {
|
|
64
|
+
if (!browserCommands) {
|
|
65
|
+
const vitestBrowser = await import('@vitest/browser/context')
|
|
66
|
+
browserCommands = vitestBrowser.commands
|
|
67
|
+
}
|
|
68
|
+
return await browserCommands[efdSuiteAdmissionBrowserCommand](testSuite, hasNewTest) === true
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Browser Mode setup runs without dd-trace, so the tracer logger is unavailable.
|
|
71
|
+
globalThis.console?.error('Datadog Test Optimization could not request Vitest EFD suite admission.', error)
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
if (isNoWorkerInitActive) {
|
|
52
77
|
// eslint-disable-next-line no-empty-pattern
|
|
53
|
-
beforeAll(function ({}, suite) {
|
|
78
|
+
beforeAll(async function ({}, suite) {
|
|
54
79
|
suite ||= arguments[0]
|
|
55
|
-
|
|
80
|
+
const efdSuiteCandidate = getEarlyFlakeDetectionSuiteCandidate(suite)
|
|
81
|
+
const isEfdSuiteAdmissionAllowed = !efdSuiteCandidate || !isEfdSuiteAdmissionEnabled ||
|
|
82
|
+
await requestBrowserEfdSuiteAdmission(efdSuiteCandidate.testSuite, efdSuiteCandidate.hasNewTest)
|
|
83
|
+
applyExecutionChanges(suite, isEfdSuiteAdmissionAllowed)
|
|
56
84
|
})
|
|
57
85
|
|
|
58
86
|
beforeEach(function ({ onTestFinished, task, skip }) {
|
|
59
87
|
const testSuite = getTestSuite(task)
|
|
60
88
|
const testName = getTestName(task)
|
|
61
89
|
const isAttemptToFixTest = attemptToFixTests[testSuite]?.[testName]
|
|
62
|
-
const isEarlyFlakeDetectionTestAttempt =
|
|
90
|
+
const isEarlyFlakeDetectionTestAttempt = isEarlyFlakeDetectionTask(task)
|
|
63
91
|
const isQuarantinedTest = quarantinedTests[testSuite]?.[testName] && !isAttemptToFixTest
|
|
64
92
|
const attemptIndex = getNextAttemptIndex(task)
|
|
65
93
|
const attemptStart = now()
|
|
@@ -108,12 +136,12 @@ if (isNoWorkerInitActive) {
|
|
|
108
136
|
})
|
|
109
137
|
}
|
|
110
138
|
|
|
111
|
-
function applyExecutionChanges (suite) {
|
|
139
|
+
function applyExecutionChanges (suite, isEfdSuiteAdmissionAllowed) {
|
|
112
140
|
const tasks = suite?.tasks
|
|
113
141
|
if (tasks) {
|
|
114
142
|
for (const task of tasks) {
|
|
115
143
|
if (task.type === 'suite') {
|
|
116
|
-
applyExecutionChanges(task)
|
|
144
|
+
applyExecutionChanges(task, isEfdSuiteAdmissionAllowed)
|
|
117
145
|
continue
|
|
118
146
|
}
|
|
119
147
|
|
|
@@ -125,7 +153,7 @@ function applyExecutionChanges (suite) {
|
|
|
125
153
|
task.meta.__ddTestOptAtfRetries = attemptToFixRetries
|
|
126
154
|
} else if (disabledTests[testSuite]?.[testName]) {
|
|
127
155
|
task.mode = 'skip'
|
|
128
|
-
} else if (isEarlyFlakeDetectionTest(testSuite, testName)) {
|
|
156
|
+
} else if (isEfdSuiteAdmissionAllowed && isEarlyFlakeDetectionTest(testSuite, testName)) {
|
|
129
157
|
task.retry = 0
|
|
130
158
|
task.repeats = earlyFlakeDetectionRetries
|
|
131
159
|
task.meta.__ddTestOptEfdRetries = earlyFlakeDetectionRetries
|
|
@@ -333,7 +361,7 @@ function recordTestOptimizationStatus (task, attemptIndex = task.result?.repeatC
|
|
|
333
361
|
|
|
334
362
|
if (attemptToFixTests[testSuite]?.[testName]) {
|
|
335
363
|
recordAttemptToFixStatus(task, attemptIndex, onlyIfNewErrors)
|
|
336
|
-
} else if (
|
|
364
|
+
} else if (isEarlyFlakeDetectionTask(task)) {
|
|
337
365
|
recordEarlyFlakeDetectionStatus(task, attemptIndex, onlyIfNewErrors)
|
|
338
366
|
} else if (task.repeats > 0) {
|
|
339
367
|
recordManualRepeatStatus(task, attemptIndex)
|
|
@@ -510,7 +538,7 @@ function getFinalAttemptIndex (task) {
|
|
|
510
538
|
if (attemptToFixTests[testSuite]?.[testName]) {
|
|
511
539
|
return getAttemptToFixRetryCount(task)
|
|
512
540
|
}
|
|
513
|
-
if (
|
|
541
|
+
if (isEarlyFlakeDetectionTask(task)) {
|
|
514
542
|
return getEarlyFlakeDetectionRetryCountForTask(task)
|
|
515
543
|
}
|
|
516
544
|
|
|
@@ -671,6 +699,64 @@ function isEarlyFlakeDetectionTest (testSuite, testName) {
|
|
|
671
699
|
return !testsForSuite.includes(testName)
|
|
672
700
|
}
|
|
673
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Returns whether Datadog admitted this task for EFD retries.
|
|
704
|
+
*
|
|
705
|
+
* @param {object} task
|
|
706
|
+
* @returns {boolean}
|
|
707
|
+
*/
|
|
708
|
+
function isEarlyFlakeDetectionTask (task) {
|
|
709
|
+
return task.meta.__ddTestOptEfdRetries !== undefined
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Returns suite admission data when a collected suite contains an EFD candidate.
|
|
714
|
+
*
|
|
715
|
+
* @param {object} suite
|
|
716
|
+
* @returns {{ hasNewTest: boolean, testSuite: string }|undefined}
|
|
717
|
+
*/
|
|
718
|
+
function getEarlyFlakeDetectionSuiteCandidate (suite) {
|
|
719
|
+
const tasks = suite?.tasks
|
|
720
|
+
if (!tasks) return
|
|
721
|
+
|
|
722
|
+
let candidate
|
|
723
|
+
for (const task of tasks) {
|
|
724
|
+
if (task.type === 'suite') {
|
|
725
|
+
const nestedCandidate = getEarlyFlakeDetectionSuiteCandidate(task)
|
|
726
|
+
if (nestedCandidate) {
|
|
727
|
+
candidate ||= nestedCandidate
|
|
728
|
+
candidate.hasNewTest ||= nestedCandidate.hasNewTest
|
|
729
|
+
}
|
|
730
|
+
continue
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (task.mode === 'skip' || task.mode === 'todo') continue
|
|
734
|
+
|
|
735
|
+
const testSuite = getTestSuite(task)
|
|
736
|
+
const testName = getTestName(task)
|
|
737
|
+
if (attemptToFixTests[testSuite]?.[testName] || disabledTests[testSuite]?.[testName]) continue
|
|
738
|
+
if (!isEarlyFlakeDetectionTest(testSuite, testName)) continue
|
|
739
|
+
|
|
740
|
+
candidate ||= {
|
|
741
|
+
hasNewTest: false,
|
|
742
|
+
testSuite,
|
|
743
|
+
}
|
|
744
|
+
candidate.hasNewTest ||= isNewTest(testSuite, testName)
|
|
745
|
+
}
|
|
746
|
+
return candidate
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Returns whether a collected test is absent from the known-tests response.
|
|
751
|
+
*
|
|
752
|
+
* @param {string} testSuite
|
|
753
|
+
* @param {string} testName
|
|
754
|
+
* @returns {boolean}
|
|
755
|
+
*/
|
|
756
|
+
function isNewTest (testSuite, testName) {
|
|
757
|
+
return !(knownTests[testSuite] || []).includes(testName)
|
|
758
|
+
}
|
|
759
|
+
|
|
674
760
|
function isModifiedTest (testSuite) {
|
|
675
761
|
return modifiedFiles[testSuite]?.length > 0
|
|
676
762
|
}
|
|
@@ -712,12 +798,28 @@ function getIsNoWorkerInitActive () {
|
|
|
712
798
|
|
|
713
799
|
function getProvidedContext () {
|
|
714
800
|
try {
|
|
715
|
-
return inject('_ddVitestWorkerSetup')
|
|
801
|
+
return parseProvidedContextValue(inject('_ddVitestWorkerSetup'))
|
|
716
802
|
} catch {
|
|
717
803
|
try {
|
|
718
|
-
return globalThis.__vitest_worker__.providedContext._ddVitestWorkerSetup
|
|
804
|
+
return parseProvidedContextValue(globalThis.__vitest_worker__.providedContext._ddVitestWorkerSetup)
|
|
719
805
|
} catch {
|
|
720
806
|
return {}
|
|
721
807
|
}
|
|
722
808
|
}
|
|
723
809
|
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Restore context serialized to keep Vitest's inline browser bootstrap script valid.
|
|
813
|
+
*
|
|
814
|
+
* @param {object|string|undefined} value
|
|
815
|
+
* @returns {object}
|
|
816
|
+
*/
|
|
817
|
+
function parseProvidedContextValue (value) {
|
|
818
|
+
if (typeof value !== 'string' || !value.startsWith(SERIALIZED_CONTEXT_PREFIX)) return value || {}
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
return JSON.parse(value.slice(SERIALIZED_CONTEXT_PREFIX.length))
|
|
822
|
+
} catch {
|
|
823
|
+
return {}
|
|
824
|
+
}
|
|
825
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -4296,6 +4296,11 @@ declare namespace tracer {
|
|
|
4296
4296
|
* Audio segments attached to the message (e.g. speech input/output)
|
|
4297
4297
|
*/
|
|
4298
4298
|
audioParts?: AudioPart[],
|
|
4299
|
+
|
|
4300
|
+
/**
|
|
4301
|
+
* Images attached to the message (e.g. vision input, generated output)
|
|
4302
|
+
*/
|
|
4303
|
+
imageParts?: ImagePart[],
|
|
4299
4304
|
}
|
|
4300
4305
|
|
|
4301
4306
|
/**
|
|
@@ -4313,6 +4318,43 @@ declare namespace tracer {
|
|
|
4313
4318
|
content: string,
|
|
4314
4319
|
}
|
|
4315
4320
|
|
|
4321
|
+
/**
|
|
4322
|
+
* Represents an image attached to an LLM chat model message, carrying exactly
|
|
4323
|
+
* one of inline `content` or an `attachmentKey`. Supplying neither or both is
|
|
4324
|
+
* rejected by the tagger, and the union keeps both shapes from type-checking.
|
|
4325
|
+
*/
|
|
4326
|
+
type ImagePart = {
|
|
4327
|
+
/**
|
|
4328
|
+
* The MIME type of the image (e.g. "image/png", "image/jpeg")
|
|
4329
|
+
*/
|
|
4330
|
+
mimeType: string,
|
|
4331
|
+
|
|
4332
|
+
/**
|
|
4333
|
+
* The image content as a base64-encoded string
|
|
4334
|
+
*/
|
|
4335
|
+
content: string,
|
|
4336
|
+
|
|
4337
|
+
/**
|
|
4338
|
+
* Explicitly excluded when inline content is present to maintain type safety.
|
|
4339
|
+
*/
|
|
4340
|
+
attachmentKey?: never,
|
|
4341
|
+
} | {
|
|
4342
|
+
/**
|
|
4343
|
+
* The MIME type of the image (e.g. "image/png", "image/jpeg")
|
|
4344
|
+
*/
|
|
4345
|
+
mimeType: string,
|
|
4346
|
+
|
|
4347
|
+
/**
|
|
4348
|
+
* Key of an already-uploaded image, in place of inline content
|
|
4349
|
+
*/
|
|
4350
|
+
attachmentKey: string,
|
|
4351
|
+
|
|
4352
|
+
/**
|
|
4353
|
+
* Explicitly excluded when an attachment key is present to maintain type safety.
|
|
4354
|
+
*/
|
|
4355
|
+
content?: never,
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4316
4358
|
/**
|
|
4317
4359
|
* Represents a single tool call for an LLM chat model message
|
|
4318
4360
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.121.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
"type:check": "tsc --noEmit -p tsconfig.dev.json",
|
|
23
23
|
"type:doc:build": "cd docs && yarn && yarn build",
|
|
24
24
|
"type:doc:test": "cd docs && yarn && yarn test",
|
|
25
|
-
"lint": "node scripts/check_licenses.js && node scripts/check-no-coverage-artifacts.js && node scripts/check-no-mcr-images.js && node scripts/check-docker-image-shas.js && eslint . --concurrency=auto --max-warnings 0 && npm run lint:codeowners:ci && npm run verify-exercised-tests",
|
|
25
|
+
"lint": "node scripts/check_licenses.js && node scripts/check-no-coverage-artifacts.js && node scripts/check-no-mcr-images.js && node scripts/check-docker-image-shas.js && eslint . --concurrency=auto --max-warnings 0 && npm run verify:carrier-fields && npm run lint:codeowners:ci && npm run verify-exercised-tests",
|
|
26
26
|
"lint:fix": "node scripts/check_licenses.js && node scripts/check-no-coverage-artifacts.js && node scripts/check-no-mcr-images.js && node scripts/check-docker-image-shas.js && eslint . --concurrency=auto --max-warnings 0 --fix",
|
|
27
|
+
"verify:carrier-fields": "node scripts/verify-carrier-fields.mjs",
|
|
27
28
|
"lint:inspect": "npx @eslint/config-inspector@latest",
|
|
28
29
|
"lint:codeowners": "codeowners-audit",
|
|
29
30
|
"lint:codeowners:ci": "codeowners-audit --glob='**/*.spec.js' --glob='benchmark/sirun/**' --glob='.agents/**' --glob='.claude/**' --glob='integration-tests/**' --glob='packages/dd-trace/src/guardrails/**'",
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"test:appsec:plugins:ci": "yarn services && node scripts/c8-ci.js test:appsec:plugins",
|
|
40
41
|
"test:debugger": "mocha \"packages/dd-trace/test/debugger/**/*.spec.js\"",
|
|
41
42
|
"test:debugger:ci": "node scripts/c8-ci.js test:debugger",
|
|
42
|
-
"test:eslint-rules": "node eslint-rules/*.test.mjs",
|
|
43
|
+
"test:eslint-rules": "node scripts/run-eslint-rule-tests.mjs eslint-rules/*.test.mjs",
|
|
43
44
|
"test:trace:core": "node scripts/mocha-parallel-files.js --expose-gc --timeout 30000 -- \"packages/dd-trace/test/*.spec.js\" \"packages/dd-trace/test/{agent,ci-visibility,config,crashtracking,datastreams,encode,exporters,msgpack,opentelemetry,opentracing,payload-tagging,plugins,remote_config,service-naming,standalone,telemetry,external-logger}/**/*.spec.js\"",
|
|
44
45
|
"test:trace:core:ci": "node scripts/c8-ci.js test:trace:core",
|
|
45
46
|
"test:trace:guardrails": "mocha \"packages/dd-trace/test/guardrails/**/*.spec.js\"",
|
|
@@ -29,6 +29,17 @@ const channels = {
|
|
|
29
29
|
|
|
30
30
|
const disabledHeaderWeakSet = new WeakSet()
|
|
31
31
|
|
|
32
|
+
const EMPTY_CARRIER = Object.create(null)
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const NO_MERGEABLE_KEYS = []
|
|
35
|
+
|
|
36
|
+
/** @typedef {string | Buffer} NativeHeaderValue */
|
|
37
|
+
/** @typedef {Array<Record<string, NativeHeaderValue>>} NativeHeaders */
|
|
38
|
+
/** @typedef {Record<string, NativeHeaderValue | NativeHeaderValue[]>} NativeHeaderCarrier */
|
|
39
|
+
|
|
40
|
+
/** @type {NativeHeaders} */
|
|
41
|
+
const EMPTY_NATIVE_HEADERS = []
|
|
42
|
+
|
|
32
43
|
// we need to store the offset per partition per topic for the consumer to track offsets for DSM
|
|
33
44
|
const latestConsumerOffsets = new Map()
|
|
34
45
|
|
|
@@ -60,17 +71,47 @@ function instrumentBaseModule (module) {
|
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
const brokers = this.globalConfig?.['bootstrap.servers']
|
|
74
|
+
const nativeHeaders = Array.isArray(headers) ? headers : EMPTY_NATIVE_HEADERS
|
|
75
|
+
|
|
76
|
+
let carrier
|
|
77
|
+
let forwardCallerHeaders = false
|
|
78
|
+
let applicationValues = EMPTY_CARRIER
|
|
79
|
+
let headerKeys = NO_MERGEABLE_KEYS
|
|
80
|
+
if (nativeHeaders.length !== 0) {
|
|
81
|
+
// `__proto__` is a legal Kafka header key and propagation never
|
|
82
|
+
// writes it, so only the seeded carrier needs a null prototype.
|
|
83
|
+
const candidateCarrier = Object.create(null)
|
|
84
|
+
const candidateValues = Object.create(null)
|
|
85
|
+
const candidateKeys = seedNativeHeaderCarrier(nativeHeaders, candidateCarrier, candidateValues)
|
|
86
|
+
if (candidateKeys === undefined) {
|
|
87
|
+
forwardCallerHeaders = true
|
|
88
|
+
} else if (candidateKeys.length !== 0) {
|
|
89
|
+
carrier = candidateCarrier
|
|
90
|
+
applicationValues = candidateValues
|
|
91
|
+
headerKeys = candidateKeys
|
|
92
|
+
}
|
|
93
|
+
}
|
|
63
94
|
|
|
64
95
|
const ctx = {
|
|
65
96
|
topic,
|
|
66
|
-
messages: [{
|
|
97
|
+
messages: [{
|
|
98
|
+
key,
|
|
99
|
+
value: message,
|
|
100
|
+
headers: carrier,
|
|
101
|
+
}],
|
|
67
102
|
bootstrapServers: brokers,
|
|
68
103
|
}
|
|
69
104
|
|
|
70
105
|
return channels.producerStart.runStores(ctx, () => {
|
|
71
106
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
107
|
+
const producedHeaders = forwardCallerHeaders
|
|
108
|
+
? nativeHeaders
|
|
109
|
+
: mergeNativeHeaders(
|
|
110
|
+
nativeHeaders, headerKeys, applicationValues, ctx.messages[0].headers ?? EMPTY_CARRIER
|
|
111
|
+
)
|
|
112
|
+
const result = produce.apply(this, [
|
|
113
|
+
topic, partition, message, key, timestamp, opaque, producedHeaders,
|
|
114
|
+
])
|
|
74
115
|
|
|
75
116
|
ctx.result = result
|
|
76
117
|
channels.producerCommit.publish(ctx)
|
|
@@ -432,7 +473,71 @@ function getLatestOffsets () {
|
|
|
432
473
|
return [...latestConsumerOffsets.values()]
|
|
433
474
|
}
|
|
434
475
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
476
|
+
/**
|
|
477
|
+
* `Producer::NodeProduce` reads the first *enumerable* own key of each entry and
|
|
478
|
+
* validates the value itself. A `null` or `undefined` entry aborts the process
|
|
479
|
+
* there, so those are the only entries we refuse to hand back to it.
|
|
480
|
+
*
|
|
481
|
+
* @param {NativeHeaders} headers Caller-owned.
|
|
482
|
+
* @param {NativeHeaderCarrier} carrier
|
|
483
|
+
* @param {NativeHeaderCarrier} applicationValues
|
|
484
|
+
* @returns {string[] | undefined} `undefined` when the binding has to reject the
|
|
485
|
+
* array itself; empty when it cannot consume the array at all.
|
|
486
|
+
* @see https://github.com/confluentinc/confluent-kafka-javascript/blob/v1.9.1/src/producer.cc
|
|
487
|
+
*/
|
|
488
|
+
function seedNativeHeaderCarrier (headers, carrier, applicationValues) {
|
|
489
|
+
const keys = new Array(headers.length)
|
|
490
|
+
for (let i = 0; i < headers.length; i++) {
|
|
491
|
+
const header = headers[i]
|
|
492
|
+
if (header === null || header === undefined) return NO_MERGEABLE_KEYS
|
|
493
|
+
|
|
494
|
+
const key = Object.keys(header)[0]
|
|
495
|
+
if (key === undefined) return
|
|
496
|
+
|
|
497
|
+
keys[i] = key
|
|
498
|
+
const value = header[key]
|
|
499
|
+
if (typeof value !== 'string' && !Buffer.isBuffer(value)) return
|
|
500
|
+
|
|
501
|
+
if (i === 0) {
|
|
502
|
+
carrier[key] = value
|
|
503
|
+
applicationValues[key] = value
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const previousValue = carrier[key]
|
|
508
|
+
if (previousValue === undefined) {
|
|
509
|
+
carrier[key] = value
|
|
510
|
+
} else if (Array.isArray(previousValue)) {
|
|
511
|
+
previousValue.push(value)
|
|
512
|
+
} else {
|
|
513
|
+
carrier[key] = [previousValue, value]
|
|
514
|
+
}
|
|
515
|
+
applicationValues[key] = carrier[key]
|
|
516
|
+
}
|
|
517
|
+
return keys
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {NativeHeaders} headers Caller-owned; surviving entries are forwarded
|
|
522
|
+
* by reference, never copied or mutated.
|
|
523
|
+
* @param {string[]} headerKeys Parallel to `headers`.
|
|
524
|
+
* @param {NativeHeaderCarrier} applicationValues Carrier contents before
|
|
525
|
+
* propagation ran.
|
|
526
|
+
* @param {NativeHeaderCarrier} carrier
|
|
527
|
+
*/
|
|
528
|
+
function mergeNativeHeaders (headers, headerKeys, applicationValues, carrier) {
|
|
529
|
+
const merged = []
|
|
530
|
+
for (let i = 0; i < headerKeys.length; i++) {
|
|
531
|
+
const key = headerKeys[i]
|
|
532
|
+
if (carrier[key] === applicationValues[key]) {
|
|
533
|
+
merged.push(headers[i])
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
for (const key of Object.keys(carrier)) {
|
|
538
|
+
if (carrier[key] !== applicationValues[key]) {
|
|
539
|
+
merged.push({ [key]: carrier[key] })
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return merged
|
|
438
543
|
}
|