dd-trace 6.18.0 → 6.20.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 (123) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +166 -24
  4. package/package.json +7 -7
  5. package/packages/datadog-esbuild/index.js +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +26 -0
  7. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  9. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +40 -4
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
  16. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  17. package/packages/datadog-instrumentations/src/jest.js +3 -10
  18. package/packages/datadog-instrumentations/src/playwright.js +26 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  21. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  22. package/packages/datadog-plugin-cucumber/src/index.js +19 -4
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  25. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  26. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  27. package/packages/datadog-plugin-undici/src/index.js +5 -0
  28. package/packages/dd-trace/src/aiguard/client.js +38 -19
  29. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  30. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  31. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  32. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  33. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  34. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
  35. package/packages/dd-trace/src/config/remote_config.js +12 -0
  36. package/packages/dd-trace/src/config/supported-configurations.json +26 -5
  37. package/packages/dd-trace/src/constants.js +2 -0
  38. package/packages/dd-trace/src/debugger/constants.js +13 -4
  39. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  41. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  43. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  45. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  49. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  50. package/packages/dd-trace/src/debugger/index.js +60 -28
  51. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  52. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  53. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  54. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  55. package/packages/dd-trace/src/knuth-hash.js +17 -0
  56. package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
  57. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  58. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  59. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  60. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  61. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  62. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  63. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  64. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  65. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  66. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  67. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  68. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  69. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  71. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  73. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  74. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  75. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  76. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
  77. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  78. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  79. package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
  80. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  81. package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
  82. package/packages/dd-trace/src/llmobs/tagger.js +37 -36
  83. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  84. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  85. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  86. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  87. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  88. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  89. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  90. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  91. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  92. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  93. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  94. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  95. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  96. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  97. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  98. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  99. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  100. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  101. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  102. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  103. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  104. package/packages/dd-trace/src/opentracing/span.js +14 -9
  105. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  106. package/packages/dd-trace/src/otel-sampling.js +171 -0
  107. package/packages/dd-trace/src/plugins/index.js +0 -1
  108. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  109. package/packages/dd-trace/src/priority_sampler.js +155 -33
  110. package/packages/dd-trace/src/profiler.js +51 -5
  111. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  112. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  113. package/packages/dd-trace/src/ritm.js +12 -9
  114. package/packages/dd-trace/src/sampler.js +2 -5
  115. package/packages/dd-trace/src/sampling_rule.js +4 -8
  116. package/packages/dd-trace/src/span_format.js +6 -0
  117. package/packages/dd-trace/src/span_processor.js +32 -3
  118. package/packages/dd-trace/src/standalone/index.js +5 -4
  119. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  120. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  121. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  122. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  123. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -0,0 +1,181 @@
1
+ 'use strict'
2
+
3
+ const { EVP_EVENT_SIZE_LIMIT, EVP_PAYLOAD_SIZE_LIMIT } = require('../constants/constants')
4
+ const { validateContextSnapshot } = require('./flag-evaluation-context')
5
+ const { prefixedTargetingKeyDigest, normalizeTargetingKey, protectedErrorCode } = require('./flag-evaluation-pii')
6
+ const { recordDegraded, recordDropped, recordPayloadSplit } = require('./flag-evaluation-telemetry')
7
+
8
+ /** @typedef {import('./flag-evaluation-aggregation').AggregationEntry} AggregationEntry */
9
+ /** @typedef {import('./flag-evaluation-context').ContextSnapshot} ContextSnapshot */
10
+ /**
11
+ * @typedef {object} FlagEvaluationBatchContext
12
+ * @property {string} service
13
+ * @property {string} [env]
14
+ * @property {string} [version]
15
+ */
16
+ /**
17
+ * @typedef {object} FlagEvaluationRow
18
+ * @property {number} timestamp
19
+ * @property {{ key: string }} flag
20
+ * @property {number} first_evaluation
21
+ * @property {number} last_evaluation
22
+ * @property {number} evaluation_count
23
+ * @property {boolean} [runtime_default_used]
24
+ * @property {string} [targeting_key]
25
+ * @property {{ evaluation: ContextSnapshot }} [context]
26
+ * @property {{ key: string }} [variant]
27
+ * @property {{ key: string }} [allocation]
28
+ * @property {{ message: string }} [error]
29
+ */
30
+ /** @typedef {{ encoded: string, rows: number, evaluations: number }} EncodedFlagEvaluationPayload */
31
+
32
+ /**
33
+ * @param {AggregationEntry} entry
34
+ * @param {number} timestamp
35
+ * @param {boolean} degraded
36
+ * @returns {FlagEvaluationRow | undefined}
37
+ */
38
+ function makeRow (entry, timestamp, degraded) {
39
+ const flagKey = normalizeTargetingKey(entry.flagKey)
40
+ if (flagKey === undefined || !Number.isSafeInteger(entry.first) || !Number.isSafeInteger(entry.last) ||
41
+ !Number.isSafeInteger(entry.count) || entry.count < 1) return
42
+ /** @type {FlagEvaluationRow} */
43
+ const row = {
44
+ timestamp,
45
+ flag: { key: flagKey },
46
+ first_evaluation: entry.first,
47
+ last_evaluation: entry.last,
48
+ evaluation_count: entry.count,
49
+ }
50
+ if (entry.runtimeDefault === true) row.runtime_default_used = true
51
+ const variant = normalizeTargetingKey(entry.variant)
52
+ const allocation = normalizeTargetingKey(entry.allocation)
53
+ if (variant) row.variant = { key: variant }
54
+ if (allocation) row.allocation = { key: allocation }
55
+ const error = protectedErrorCode(entry.error)
56
+ if (error !== undefined) row.error = { message: error }
57
+ if (!degraded) {
58
+ const targetingKey = entry.consent === true
59
+ ? normalizeTargetingKey(entry.rawTargetingKey)
60
+ : prefixedTargetingKeyDigest(entry.rawTargetingKey)
61
+ if (targetingKey !== undefined) row.targeting_key = targetingKey
62
+ const attrs = entry.consent === true ? validateContextSnapshot(entry.attrs) : undefined
63
+ if (attrs !== undefined) row.context = { evaluation: attrs }
64
+ }
65
+ return row
66
+ }
67
+
68
+ /**
69
+ * Advance serialization only when the transport has room for another complete envelope.
70
+ * Takes ownership of the detached aggregate maps and clears them when iteration starts.
71
+ *
72
+ * @param {Map<string, AggregationEntry>} full
73
+ * @param {Map<string, AggregationEntry>} degraded
74
+ * @param {FlagEvaluationBatchContext} context
75
+ * @param {number} timestamp
76
+ * @param {(count: number) => void} [onConsumed] - Transfer observation ownership on encoding or discard
77
+ * @yields {EncodedFlagEvaluationPayload} One envelope bounded by the existing EVP payload limit.
78
+ */
79
+ function * iterateFlagEvaluationPayloads (full, degraded, context, timestamp, onConsumed) {
80
+ const prefix = '{"context":' + JSON.stringify(context) + ',"flagEvaluations":['
81
+ const suffix = ']}'
82
+ /** @type {string[]} */
83
+ let encodedRows = []
84
+ let evaluations = 0
85
+ let payloadCount = 0
86
+ let size = Buffer.byteLength(prefix) + Buffer.byteLength(suffix)
87
+
88
+ const close = () => {
89
+ const payload = { encoded: prefix + encodedRows.join(',') + suffix, rows: encodedRows.length, evaluations }
90
+ if (payloadCount > 0) recordPayloadSplit()
91
+ payloadCount++
92
+ evaluations = 0
93
+ encodedRows = []
94
+ size = Buffer.byteLength(prefix) + Buffer.byteLength(suffix)
95
+ return payload
96
+ }
97
+
98
+ /**
99
+ * @param {string} reason
100
+ * @param {number} count
101
+ */
102
+ const drop = (reason, count) => {
103
+ onConsumed?.(count)
104
+ recordDropped(reason, count)
105
+ }
106
+
107
+ /** @type {Array<[AggregationEntry, boolean]>} */
108
+ const entries = []
109
+ for (const entry of full.values()) entries.push([entry, false])
110
+ for (const entry of degraded.values()) entries.push([entry, true])
111
+
112
+ // Lookup keys are no longer needed after aggregation. Pop entries in their original
113
+ // order so a paused flush does not retain entries that have already been encoded.
114
+ full.clear()
115
+ degraded.clear()
116
+ entries.reverse()
117
+ while (entries.length > 0) {
118
+ const [entry, aggregateDegraded] = /** @type {[AggregationEntry, boolean]} */ (entries.pop())
119
+ const row = makeRow(entry, timestamp, aggregateDegraded)
120
+ if (row === undefined) {
121
+ drop('serialization_error', entry.count)
122
+ continue
123
+ }
124
+ let serializedRow = row
125
+ let encoded
126
+ try {
127
+ encoded = JSON.stringify(serializedRow)
128
+ } catch {
129
+ drop('serialization_error', serializedRow.evaluation_count)
130
+ continue
131
+ }
132
+
133
+ let canDegrade = serializedRow.targeting_key !== undefined || serializedRow.context !== undefined
134
+ const degrade = () => {
135
+ if (!canDegrade) return false
136
+ serializedRow = { ...serializedRow }
137
+ delete serializedRow.targeting_key
138
+ delete serializedRow.context
139
+ encoded = JSON.stringify(serializedRow)
140
+ recordDegraded('payload_limit', serializedRow.evaluation_count)
141
+ canDegrade = false
142
+ return true
143
+ }
144
+
145
+ try {
146
+ if (Buffer.byteLength(encoded) > EVP_EVENT_SIZE_LIMIT) degrade()
147
+ } catch {
148
+ drop('serialization_error', serializedRow.evaluation_count)
149
+ continue
150
+ }
151
+ if (Buffer.byteLength(encoded) > EVP_EVENT_SIZE_LIMIT) {
152
+ drop('payload_limit', serializedRow.evaluation_count)
153
+ continue
154
+ }
155
+
156
+ let addition = Buffer.byteLength(encoded) + Number(encodedRows.length > 0)
157
+ if (size + addition > EVP_PAYLOAD_SIZE_LIMIT && encodedRows.length > 0) {
158
+ yield close()
159
+ addition = Buffer.byteLength(encoded)
160
+ }
161
+ if (size + addition > EVP_PAYLOAD_SIZE_LIMIT) {
162
+ try {
163
+ if (degrade()) addition = Buffer.byteLength(encoded)
164
+ } catch {
165
+ drop('serialization_error', serializedRow.evaluation_count)
166
+ continue
167
+ }
168
+ }
169
+ if (size + addition > EVP_PAYLOAD_SIZE_LIMIT) {
170
+ drop('payload_limit', serializedRow.evaluation_count)
171
+ continue
172
+ }
173
+ encodedRows.push(encoded)
174
+ onConsumed?.(serializedRow.evaluation_count)
175
+ evaluations += serializedRow.evaluation_count
176
+ size += addition
177
+ }
178
+ if (encodedRows.length > 0) yield close()
179
+ }
180
+
181
+ module.exports = { iterateFlagEvaluationPayloads }
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+
3
+ const { hashTargetingKey: digestTargetingKey } = require('../encoding')
4
+
5
+ // Keep this vocabulary explicit: an upstream enum addition needs a privacy review.
6
+ const ERROR_CODES = new Set([
7
+ 'PROVIDER_NOT_READY',
8
+ 'PROVIDER_FATAL',
9
+ 'FLAG_NOT_FOUND',
10
+ 'PARSE_ERROR',
11
+ 'TYPE_MISMATCH',
12
+ 'TARGETING_KEY_MISSING',
13
+ 'INVALID_CONTEXT',
14
+ 'GENERAL',
15
+ ])
16
+
17
+ /**
18
+ * Validate targeting text before UTF-8 encoding, which would repair lone surrogates.
19
+ * Empty text is valid and distinct from a missing key. Do not coerce customer input.
20
+ *
21
+ * @param {unknown} value - Evaluation targeting key
22
+ * @returns {string | undefined}
23
+ */
24
+ function normalizeTargetingKey (value) {
25
+ if (typeof value !== 'string') return
26
+
27
+ for (let i = 0; i < value.length; i++) {
28
+ const code = value.charCodeAt(i)
29
+ if (code >= 0xD8_00 && code <= 0xDB_FF) {
30
+ if (++i === value.length) return
31
+ const low = value.charCodeAt(i)
32
+ if (low < 0xDC_00 || low > 0xDF_FF) return
33
+ } else if (code >= 0xDC_00 && code <= 0xDF_FF) {
34
+ return
35
+ }
36
+ }
37
+
38
+ return value
39
+ }
40
+
41
+ /**
42
+ * Apply EVP's protected targeting-key policy using the shared SHA-256 primitive.
43
+ * The cross-SDK contract deliberately uses an unsalted, stable digest for subject counts.
44
+ * This is pseudonymization, not anonymity: guessable keys remain guessable and equal
45
+ * keys produce equal hashes across services and organizations. An org-scoped salt
46
+ * requires a coordinated configuration/backend contract, not a JS-only secret.
47
+ * This runs after queue handoff; hashing must not move onto the evaluation hot path.
48
+ *
49
+ * @param {unknown} value - Raw evaluation targeting key, never a previously emitted hash
50
+ * @returns {string | undefined}
51
+ */
52
+ function prefixedTargetingKeyDigest (value) {
53
+ const key = normalizeTargetingKey(value)
54
+ if (key === undefined || key === '') return key
55
+ return 'sha256_' + digestTargetingKey(key)
56
+ }
57
+
58
+ /**
59
+ * Preserve only approved OpenFeature codes on the wire, in every consent mode.
60
+ * Absence means no error; malformed or unknown supplied codes become GENERAL.
61
+ *
62
+ * @param {unknown} value - OpenFeature errorCode, never errorMessage
63
+ * @returns {string | undefined}
64
+ */
65
+ function protectedErrorCode (value) {
66
+ if (value === undefined || value === null || value === '') return
67
+ return typeof value === 'string' && ERROR_CODES.has(value) ? value : 'GENERAL'
68
+ }
69
+
70
+ /**
71
+ * Optional metadata dimensions omit empty text; targeting keys deliberately preserve it.
72
+ *
73
+ * @param {unknown} value
74
+ * @returns {string | undefined}
75
+ */
76
+ function optionalKey (value) {
77
+ const key = normalizeTargetingKey(value)
78
+ return key === undefined || key.length === 0 ? undefined : key
79
+ }
80
+
81
+ /**
82
+ * Shape observations at both admission boundaries; each caller counts omitted targeting keys.
83
+ *
84
+ * @param {import('./flag-evaluation-aggregation').FlagEvaluationEvent} event
85
+ * @param {string | undefined} targetingKey - Targeting key already validated by the caller
86
+ */
87
+ function normalizeFlagEvaluationEvent (event, targetingKey) {
88
+ const consent = event.observeFullEvaluationData === true
89
+ return {
90
+ flagKey: normalizeTargetingKey(event.flagKey),
91
+ variant: optionalKey(event.variant),
92
+ allocationKey: optionalKey(event.allocationKey),
93
+ runtimeDefault: event.runtimeDefault === true,
94
+ errorCode: protectedErrorCode(event.errorCode),
95
+ targetingKey,
96
+ attrs: consent ? event.attrs : undefined,
97
+ observeFullEvaluationData: consent,
98
+ timestamp: typeof event.timestamp === 'number' ? event.timestamp : NaN,
99
+ }
100
+ }
101
+
102
+ module.exports = {
103
+ normalizeFlagEvaluationEvent,
104
+ normalizeTargetingKey,
105
+ optionalKey,
106
+ prefixedTargetingKeyDigest,
107
+ protectedErrorCode,
108
+ }
@@ -0,0 +1,154 @@
1
+ 'use strict'
2
+
3
+ let metrics
4
+ /** @type {Int32Array | undefined} */
5
+ let workerState
6
+
7
+ // Namespace resets clear points, not metric instances. Names and reasons are fixed below.
8
+ /** @type {Map<string, { inc: (value: number) => void }>} */
9
+ const countMetrics = new Map()
10
+
11
+ const DROP_REASONS = new Set([
12
+ 'pre_queue_overflow',
13
+ 'queue_overflow',
14
+ 'closed',
15
+ 'unavailable',
16
+ 'degraded_cap',
17
+ 'payload_limit',
18
+ 'serialization_error',
19
+ // Discarded after failed delivery; an ambiguous response does not prove the receiver lost the batch.
20
+ 'delivery_failure',
21
+ 'worker_failure',
22
+ 'shutdown_timeout',
23
+ ])
24
+ const DEGRADE_REASONS = new Set(['cardinality_cap', 'payload_limit'])
25
+ const CONTEXT_REASONS = new Set([
26
+ 'max_context_fields',
27
+ 'max_key_length',
28
+ 'max_value_length',
29
+ 'max_list_elements',
30
+ 'max_structure_properties',
31
+ 'max_snapshot_depth',
32
+ 'max_visited_nodes',
33
+ 'cycle',
34
+ 'invalid_encoding',
35
+ 'unsupported_type',
36
+ 'snapshot_error',
37
+ ])
38
+
39
+ // Slot 0 owns admitted input until aggregation; slot 1 owns all accepted observations until delivery/drop.
40
+ // Fixed metric slots follow. Only the application drains them; no message per event or flush.
41
+ /** @type {Array<[string, string | undefined]>} */
42
+ const metricKeys = [
43
+ ...[...DROP_REASONS].map(reason => /** @type {[string, string]} */ (['flagevaluation.rows.dropped', reason])),
44
+ ...[...DEGRADE_REASONS].map(reason => /** @type {[string, string]} */ (['flagevaluation.rows.degraded', reason])),
45
+ ...[...CONTEXT_REASONS].map(reason => /** @type {[string, string]} */ (['flagevaluation.context.truncated', reason])),
46
+ ['flagevaluation.payload.splits', undefined],
47
+ ['flagevaluation.targeting_key.omitted', 'invalid'],
48
+ ['flagevaluation.hook.errors', undefined],
49
+ ]
50
+ const metricSlots = new Map(metricKeys.map(([name, reason], index) => [name + ':' + reason, index + 2]))
51
+
52
+ /** @param {Int32Array} state */
53
+ function configureWorkerTelemetry (state) {
54
+ workerState = state
55
+ }
56
+
57
+ function createWorkerState () {
58
+ return new Int32Array(new SharedArrayBuffer((metricKeys.length + 2) * Int32Array.BYTES_PER_ELEMENT))
59
+ }
60
+
61
+ /** @param {Int32Array} state */
62
+ function collectWorkerTelemetry (state) {
63
+ for (let i = 0; i < metricKeys.length; i++) {
64
+ const value = Atomics.exchange(state, i + 2, 0)
65
+ const [name, reason] = metricKeys[i]
66
+ count(name, value, reason)
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Best-effort telemetry must never affect an evaluation or writer drain.
72
+ *
73
+ * @param {string} name - Bounded metric name
74
+ * @param {number} value - Count to add
75
+ * @param {string} [reason] - Bounded reason tag
76
+ */
77
+ function count (name, value, reason) {
78
+ if (!Number.isSafeInteger(value) || value <= 0) return
79
+ if (workerState) {
80
+ const slot = /** @type {number} */ (metricSlots.get(name + ':' + reason))
81
+ // Saturate instead of wrapping if the application stops collecting metrics for a long time.
82
+ let previous = Atomics.load(workerState, slot)
83
+ while (Atomics.compareExchange(workerState, slot, previous,
84
+ Math.min(0x7F_FF_FF_FF, previous + value)) !== previous) {
85
+ previous = Atomics.load(workerState, slot)
86
+ }
87
+ return
88
+ }
89
+ try {
90
+ const id = name + ':' + reason
91
+ let metric = countMetrics.get(id)
92
+ if (metric === undefined) {
93
+ metrics ??= require('../../telemetry/metrics').manager.namespace('general')
94
+ metric = /** @type {{ inc: (value: number) => void }} */ (
95
+ metrics.count(name, reason === undefined ? undefined : { reason })
96
+ )
97
+ countMetrics.set(id, metric)
98
+ }
99
+ metric.inc(value)
100
+ } catch {}
101
+ }
102
+
103
+ /**
104
+ * @param {string} reason
105
+ * @param {number} [value]
106
+ */
107
+ function recordDropped (reason, value = 1) {
108
+ if (!DROP_REASONS.has(reason)) return
109
+ if (workerState && Number.isSafeInteger(value) && value > 0) Atomics.sub(workerState, 1, value)
110
+ count('flagevaluation.rows.dropped', value, reason)
111
+ }
112
+
113
+ /**
114
+ * @param {string} reason
115
+ * @param {number} [value]
116
+ */
117
+ function recordDegraded (reason, value = 1) {
118
+ if (DEGRADE_REASONS.has(reason)) count('flagevaluation.rows.degraded', value, reason)
119
+ }
120
+
121
+ /** @param {number} [value] */
122
+ function recordPayloadSplit (value = 1) {
123
+ count('flagevaluation.payload.splits', value)
124
+ }
125
+
126
+ /**
127
+ * @param {string} reason
128
+ * @param {number} [value]
129
+ */
130
+ function recordContextTruncated (reason, value = 1) {
131
+ if (CONTEXT_REASONS.has(reason)) count('flagevaluation.context.truncated', value, reason)
132
+ }
133
+
134
+ /** @param {number} [value] */
135
+ function recordTargetingKeyOmitted (value = 1) {
136
+ count('flagevaluation.targeting_key.omitted', value, 'invalid')
137
+ }
138
+
139
+ /** @param {number} [value] */
140
+ function recordHookError (value = 1) {
141
+ count('flagevaluation.hook.errors', value)
142
+ }
143
+
144
+ module.exports = {
145
+ collectWorkerTelemetry,
146
+ configureWorkerTelemetry,
147
+ createWorkerState,
148
+ recordContextTruncated,
149
+ recordDegraded,
150
+ recordDropped,
151
+ recordHookError,
152
+ recordPayloadSplit,
153
+ recordTargetingKeyOmitted,
154
+ }
@@ -0,0 +1,77 @@
1
+ 'use strict'
2
+
3
+ const { parentPort, workerData } = require('node:worker_threads')
4
+ const port = /** @type {import('node:worker_threads').MessagePort} */ (parentPort)
5
+
6
+ // Bootstrap only the lifecycle registry, never the tracer or its preload entrypoints.
7
+ globalThis[Symbol.for('dd-trace')] = { beforeExitHandlers: new Set() }
8
+
9
+ const logWriter = require('../../log/writer')
10
+ const { configureWorkerTelemetry } = require('./flag-evaluation-telemetry')
11
+ const state = new Int32Array(workerData.state)
12
+ configureWorkerTelemetry(state)
13
+ const FlagEvaluationConsumer = require('./flag-evaluation-consumer')
14
+
15
+ /** @param {'debug' | 'info' | 'warn' | 'error'} level */
16
+ function forwardLog (level) {
17
+ /** @param {unknown} message */
18
+ return message => {
19
+ try {
20
+ port.postMessage({ type: 'log', level, message: message instanceof Error ? message.stack : String(message) })
21
+ } catch {
22
+ // Diagnostics are best-effort when the parent port is closing.
23
+ }
24
+ }
25
+ }
26
+
27
+ // Use the parent's effective settings, not a second environment-only configuration.
28
+ // The custom logger itself stays in the parent isolate because functions cannot be cloned.
29
+ const { enabled = false, level } = workerData.logging ?? {}
30
+ logWriter.configure(enabled, level, {
31
+ debug: forwardLog('debug'),
32
+ info: forwardLog('info'),
33
+ warn: forwardLog('warn'),
34
+ error: forwardLog('error'),
35
+ })
36
+
37
+ /** @param {import('./flag-evaluations').SerializedFlagEvaluationRoute} route */
38
+ function deserializeRoute (route) {
39
+ return {
40
+ url: new URL(route.url),
41
+ basePath: route.basePath,
42
+ headers: route.headers,
43
+ fallback: route.fallback && deserializeRoute(route.fallback),
44
+ onFallback: route.onFallback
45
+ ? () => port.postMessage({ type: 'route', id: route.id, status: 'fallback' })
46
+ : undefined,
47
+ onUnavailable: route.onUnavailable
48
+ ? () => port.postMessage({ type: 'route', id: route.id, status: 'unavailable' })
49
+ : undefined,
50
+ }
51
+ }
52
+
53
+ const route = deserializeRoute(workerData.route)
54
+ const consumer = new FlagEvaluationConsumer({ ...workerData.context, url: route.url }, route, {
55
+ onProcessed: count => Atomics.sub(state, 0, count),
56
+ onDelivered: count => Atomics.sub(state, 1, count),
57
+ onIdle: () => port.close(),
58
+ })
59
+ consumer.setEnabled(true, route)
60
+ port.on('message', message => {
61
+ switch (message.type) {
62
+ case 'batch':
63
+ for (const event of message.events) {
64
+ if (!consumer.enqueue(event)) Atomics.sub(state, 0, 1)
65
+ }
66
+ break
67
+ case 'enabled':
68
+ consumer.setEnabled(message.enabled, message.route && deserializeRoute(message.route))
69
+ break
70
+ case 'flush':
71
+ consumer.flush()
72
+ break
73
+ case 'close':
74
+ consumer.destroy()
75
+ break
76
+ }
77
+ })