dd-trace 5.100.0 → 5.102.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 (189) hide show
  1. package/index.d.ts +14 -0
  2. package/package.json +11 -9
  3. package/packages/datadog-instrumentations/src/aerospike.js +2 -2
  4. package/packages/datadog-instrumentations/src/ai.js +8 -8
  5. package/packages/datadog-instrumentations/src/amqplib.js +6 -7
  6. package/packages/datadog-instrumentations/src/anthropic.js +10 -10
  7. package/packages/datadog-instrumentations/src/apollo-server-core.js +3 -3
  8. package/packages/datadog-instrumentations/src/apollo-server.js +5 -5
  9. package/packages/datadog-instrumentations/src/avsc.js +6 -6
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +151 -67
  11. package/packages/datadog-instrumentations/src/azure-durable-functions.js +8 -8
  12. package/packages/datadog-instrumentations/src/bluebird.js +2 -2
  13. package/packages/datadog-instrumentations/src/body-parser.js +2 -2
  14. package/packages/datadog-instrumentations/src/cassandra-driver.js +7 -7
  15. package/packages/datadog-instrumentations/src/child_process.js +12 -12
  16. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +9 -9
  17. package/packages/datadog-instrumentations/src/connect.js +7 -7
  18. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  19. package/packages/datadog-instrumentations/src/cookie.js +2 -2
  20. package/packages/datadog-instrumentations/src/couchbase.js +16 -30
  21. package/packages/datadog-instrumentations/src/crypto.js +4 -4
  22. package/packages/datadog-instrumentations/src/cucumber.js +77 -16
  23. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  24. package/packages/datadog-instrumentations/src/dns.js +0 -3
  25. package/packages/datadog-instrumentations/src/elasticsearch.js +8 -11
  26. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +6 -6
  27. package/packages/datadog-instrumentations/src/express-session.js +4 -4
  28. package/packages/datadog-instrumentations/src/express.js +10 -11
  29. package/packages/datadog-instrumentations/src/fastify.js +2 -2
  30. package/packages/datadog-instrumentations/src/fs.js +14 -14
  31. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +5 -7
  32. package/packages/datadog-instrumentations/src/google-genai.js +4 -4
  33. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  34. package/packages/datadog-instrumentations/src/hapi.js +2 -2
  35. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +8 -8
  36. package/packages/datadog-instrumentations/src/helpers/promise.js +2 -2
  37. package/packages/datadog-instrumentations/src/hono.js +2 -2
  38. package/packages/datadog-instrumentations/src/http/client.js +26 -9
  39. package/packages/datadog-instrumentations/src/http/server.js +9 -9
  40. package/packages/datadog-instrumentations/src/jest.js +93 -63
  41. package/packages/datadog-instrumentations/src/kafkajs.js +9 -9
  42. package/packages/datadog-instrumentations/src/knex.js +17 -17
  43. package/packages/datadog-instrumentations/src/koa.js +12 -12
  44. package/packages/datadog-instrumentations/src/ldapjs.js +5 -5
  45. package/packages/datadog-instrumentations/src/light-my-request.js +2 -2
  46. package/packages/datadog-instrumentations/src/limitd-client.js +4 -4
  47. package/packages/datadog-instrumentations/src/lodash.js +4 -4
  48. package/packages/datadog-instrumentations/src/mariadb.js +13 -13
  49. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  50. package/packages/datadog-instrumentations/src/microgateway-core.js +2 -2
  51. package/packages/datadog-instrumentations/src/mocha/common.js +7 -4
  52. package/packages/datadog-instrumentations/src/mocha/main.js +37 -14
  53. package/packages/datadog-instrumentations/src/mocha/utils.js +133 -16
  54. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -7
  55. package/packages/datadog-instrumentations/src/mongodb-core.js +9 -22
  56. package/packages/datadog-instrumentations/src/mongodb.js +5 -5
  57. package/packages/datadog-instrumentations/src/mongoose.js +21 -21
  58. package/packages/datadog-instrumentations/src/mquery.js +5 -5
  59. package/packages/datadog-instrumentations/src/multer.js +4 -4
  60. package/packages/datadog-instrumentations/src/mysql.js +16 -16
  61. package/packages/datadog-instrumentations/src/mysql2.js +4 -4
  62. package/packages/datadog-instrumentations/src/net.js +14 -8
  63. package/packages/datadog-instrumentations/src/nyc.js +5 -5
  64. package/packages/datadog-instrumentations/src/openai.js +19 -19
  65. package/packages/datadog-instrumentations/src/oracledb.js +6 -6
  66. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  67. package/packages/datadog-instrumentations/src/passport-utils.js +5 -5
  68. package/packages/datadog-instrumentations/src/pg.js +15 -15
  69. package/packages/datadog-instrumentations/src/pino.js +6 -10
  70. package/packages/datadog-instrumentations/src/playwright.js +20 -15
  71. package/packages/datadog-instrumentations/src/protobufjs.js +16 -16
  72. package/packages/datadog-instrumentations/src/redis.js +1 -2
  73. package/packages/datadog-instrumentations/src/restify.js +2 -2
  74. package/packages/datadog-instrumentations/src/router.js +12 -12
  75. package/packages/datadog-instrumentations/src/stripe.js +12 -12
  76. package/packages/datadog-instrumentations/src/vitest.js +107 -26
  77. package/packages/datadog-instrumentations/src/winston.js +4 -4
  78. package/packages/datadog-instrumentations/src/ws.js +7 -7
  79. package/packages/datadog-plugin-aws-sdk/src/base.js +52 -4
  80. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +19 -12
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +45 -35
  82. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +33 -22
  83. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +12 -13
  84. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +73 -54
  85. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +19 -17
  86. package/packages/datadog-plugin-aws-sdk/src/util.js +22 -0
  87. package/packages/datadog-plugin-bullmq/src/consumer.js +2 -2
  88. package/packages/datadog-plugin-bullmq/src/producer.js +14 -20
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -6
  90. package/packages/datadog-plugin-cucumber/src/index.js +4 -0
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -4
  92. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  93. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -5
  94. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +3 -1
  95. package/packages/datadog-plugin-http/src/client.js +1 -5
  96. package/packages/datadog-plugin-jest/src/util.js +1 -2
  97. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  99. package/packages/datadog-plugin-mocha/src/index.js +4 -0
  100. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -1
  101. package/packages/datadog-plugin-openai/src/tracing.js +12 -23
  102. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  103. package/packages/datadog-plugin-vitest/src/index.js +8 -1
  104. package/packages/datadog-shimmer/src/shimmer.js +7 -1
  105. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
  106. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +81 -81
  107. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +2 -2
  110. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -2
  111. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  112. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
  113. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +83 -48
  114. package/packages/dd-trace/src/appsec/index.js +21 -24
  115. package/packages/dd-trace/src/appsec/reporter.js +7 -2
  116. package/packages/dd-trace/src/appsec/rule_manager.js +4 -2
  117. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +31 -16
  118. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  119. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  120. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  121. package/packages/dd-trace/src/config/git_properties.js +2 -2
  122. package/packages/dd-trace/src/config/index.js +1 -55
  123. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  124. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  125. package/packages/dd-trace/src/datastreams/index.js +2 -1
  126. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  127. package/packages/dd-trace/src/datastreams/processor.js +18 -17
  128. package/packages/dd-trace/src/datastreams/size.js +6 -2
  129. package/packages/dd-trace/src/debugger/config.js +5 -2
  130. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  131. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  132. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +1 -0
  133. package/packages/dd-trace/src/dogstatsd.js +10 -7
  134. package/packages/dd-trace/src/encode/0.4.js +759 -234
  135. package/packages/dd-trace/src/encode/0.5.js +15 -9
  136. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  137. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  138. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  139. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  140. package/packages/dd-trace/src/git_metadata.js +66 -0
  141. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  142. package/packages/dd-trace/src/id.js +15 -26
  143. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -2
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  146. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  147. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +33 -13
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  149. package/packages/dd-trace/src/llmobs/sdk.js +29 -27
  150. package/packages/dd-trace/src/llmobs/span_processor.js +52 -6
  151. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  153. package/packages/dd-trace/src/llmobs/util.js +81 -5
  154. package/packages/dd-trace/src/msgpack/chunk.js +6 -3
  155. package/packages/dd-trace/src/openfeature/noop.js +40 -36
  156. package/packages/dd-trace/src/openfeature/writers/exposures.js +33 -52
  157. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  158. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  159. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -2
  160. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +1 -2
  161. package/packages/dd-trace/src/opentelemetry/span-helpers.js +188 -50
  162. package/packages/dd-trace/src/opentelemetry/span.js +42 -80
  163. package/packages/dd-trace/src/opentelemetry/tracer.js +0 -22
  164. package/packages/dd-trace/src/opentracing/propagation/text_map.js +65 -27
  165. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +2 -11
  166. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +58 -22
  167. package/packages/dd-trace/src/opentracing/span.js +56 -48
  168. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  169. package/packages/dd-trace/src/plugins/util/ci.js +1 -1
  170. package/packages/dd-trace/src/plugins/util/git-cache.js +3 -5
  171. package/packages/dd-trace/src/plugins/util/test.js +19 -7
  172. package/packages/dd-trace/src/plugins/util/url.js +1 -3
  173. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  174. package/packages/dd-trace/src/plugins/util/web.js +5 -7
  175. package/packages/dd-trace/src/priority_sampler.js +6 -4
  176. package/packages/dd-trace/src/profiling/config.js +5 -4
  177. package/packages/dd-trace/src/profiling/profilers/events.js +3 -23
  178. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -5
  179. package/packages/dd-trace/src/remote_config/index.js +5 -3
  180. package/packages/dd-trace/src/runtime_metrics/index.js +2 -2
  181. package/packages/dd-trace/src/scope.js +3 -10
  182. package/packages/dd-trace/src/serverless.js +1 -4
  183. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +7 -1
  184. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +4 -0
  185. package/packages/dd-trace/src/span_format.js +52 -5
  186. package/packages/dd-trace/src/span_processor.js +0 -4
  187. package/packages/dd-trace/src/spanleak.js +0 -1
  188. package/packages/dd-trace/src/tracer.js +7 -7
  189. package/packages/dd-trace/src/util.js +17 -0
@@ -3,34 +3,226 @@
3
3
  const getConfig = require('../config')
4
4
  const { MsgpackChunk, MsgpackEncoder } = require('../msgpack')
5
5
  const log = require('../log')
6
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
6
+ const { normalizeSpan } = require('./tags-processors')
7
7
 
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
9
9
 
10
- function formatSpan (span, config) {
11
- span = normalizeSpan(truncateSpan(span, false))
10
+ // Pre-encoded static keys + value-prefix bytes; the hot encode loop emits
11
+ // each via one Uint8Array.set instead of routing through the string cache.
12
+
13
+ /**
14
+ * @param {string} key fixstr key, must be < 32 UTF-8 bytes.
15
+ * @returns {Buffer}
16
+ */
17
+ function buildKey (key) {
18
+ const length = Buffer.byteLength(key)
19
+ const buffer = Buffer.allocUnsafe(length + 1)
20
+ buffer[0] = length | 0xA0
21
+ buffer.utf8Write(key, 1, length)
22
+ return buffer
23
+ }
24
+
25
+ /**
26
+ * @param {string} key fixstr key, must be < 32 UTF-8 bytes.
27
+ * @param {number} prefix msgpack prefix byte for the value that follows the key.
28
+ * @returns {Buffer}
29
+ */
30
+ function buildKeyWithPrefix (key, prefix) {
31
+ const length = Buffer.byteLength(key)
32
+ const buffer = Buffer.allocUnsafe(length + 2)
33
+ buffer[0] = length | 0xA0
34
+ buffer.utf8Write(key, 1, length)
35
+ buffer[length + 1] = prefix
36
+ return buffer
37
+ }
38
+
39
+ const KEY_TYPE = buildKey('type')
40
+ const KEY_NAME = buildKey('name')
41
+ const KEY_RESOURCE = buildKey('resource')
42
+ const KEY_SERVICE = buildKey('service')
43
+ const KEY_SPAN_EVENTS = buildKey('span_events')
44
+ const KEY_META_STRUCT = buildKey('meta_struct')
45
+ const KEY_TRACE_ID_PREFIX = buildKeyWithPrefix('trace_id', 0xCF)
46
+ const KEY_SPAN_ID_PREFIX = buildKeyWithPrefix('span_id', 0xCF)
47
+ const KEY_PARENT_ID_PREFIX = buildKeyWithPrefix('parent_id', 0xCF)
48
+ const KEY_ERROR_PREFIX = buildKeyWithPrefix('error', 0xCE)
49
+ const KEY_START_PREFIX = buildKeyWithPrefix('start', 0xCF)
50
+ const KEY_DURATION_PREFIX = buildKeyWithPrefix('duration', 0xCF)
51
+ const KEY_META_PREFIX = buildKeyWithPrefix('meta', 0xDF)
52
+ const KEY_METRICS_PREFIX = buildKeyWithPrefix('metrics', 0xDF)
53
+
54
+ // Span-event field keys — `name` is shared with the span-level KEY_NAME.
55
+ const KEY_EVENT_TIME = buildKey('time_unix_nano')
56
+ const KEY_EVENT_ATTRIBUTES = buildKey('attributes')
57
+
58
+ // Pre-encoded prefix for a span-event-attribute typed wrapper:
59
+ // `[0x82 fixmap(2), 'type' fixstr, type fixint, '<value>_value' fixstr]`.
60
+ // The hot path emits one of these constants + the raw value, so the encoder
61
+ // never has to allocate `{ type: N, *_value: ... }` wrappers.
62
+ function buildAttrPrefix (typeByte, valueKey) {
63
+ const valueKeyBuf = buildKey(valueKey)
64
+ const buf = Buffer.allocUnsafe(7 + valueKeyBuf.length)
65
+ buf[0] = 0x82
66
+ buf[1] = 0xA4
67
+ buf[2] = 0x74 // t
68
+ buf[3] = 0x79 // y
69
+ buf[4] = 0x70 // p
70
+ buf[5] = 0x65 // e
71
+ buf[6] = typeByte
72
+ valueKeyBuf.copy(buf, 7)
73
+ return buf
74
+ }
75
+
76
+ const ATTR_PREFIX_STRING = buildAttrPrefix(0x00, 'string_value')
77
+ const ATTR_PREFIX_BOOL = buildAttrPrefix(0x01, 'bool_value')
78
+ const ATTR_PREFIX_INT = buildAttrPrefix(0x02, 'int_value')
79
+ const ATTR_PREFIX_DOUBLE = buildAttrPrefix(0x03, 'double_value')
80
+
81
+ // Outer array attribute is the only nested case: `[0x82, 'type', 4,
82
+ // 'array_value', 0x81 fixmap(1), 'values', 0xDD array32-prefix]`. The 4-byte
83
+ // length slot follows.
84
+ const ATTR_PREFIX_ARRAY = Buffer.concat([
85
+ buildAttrPrefix(0x04, 'array_value'),
86
+ Buffer.from([0x81]),
87
+ buildKey('values'),
88
+ Buffer.from([0xDD]),
89
+ ])
90
+
91
+ // Pre-encoded boolean payloads: `[ATTR_PREFIX_BOOL, 0xC3 / 0xC2]`. Used by
92
+ // `#emitAttribute` and `#emitArrayItem` to emit the whole bool wrapper in a
93
+ // single `bytes.set`.
94
+ const ATTR_PAYLOAD_BOOL_TRUE = Buffer.concat([ATTR_PREFIX_BOOL, Buffer.from([0xC3])])
95
+ const ATTR_PAYLOAD_BOOL_FALSE = Buffer.concat([ATTR_PREFIX_BOOL, Buffer.from([0xC2])])
96
+
97
+ function formatSpanWithLegacyEvents (span) {
98
+ span = normalizeSpan(span)
12
99
  if (span.span_events) {
13
- // ensure span events are encoded as tags if agent doesn't support native top level span events
14
- if (config.DD_TRACE_NATIVE_SPAN_EVENTS) {
15
- formatSpanEvents(span)
100
+ span.meta.events = stringifySpanEvents(span.span_events)
101
+ // `= undefined` over `delete` to keep the span's hidden class — `delete`
102
+ // would push every event-bearing span into V8 dictionary mode.
103
+ span.span_events = undefined
104
+ }
105
+ return span
106
+ }
107
+
108
+ /**
109
+ * Hand-written stringifier for `span.span_events`. The shape is fixed by
110
+ * `extractSpanEvents` (`{ name, time_unix_nano, attributes? }`) and attribute
111
+ * values are pre-sanitized to primitives or arrays of primitives, so we can
112
+ * skip everything `JSON.stringify` does for the generic case (toJSON probing,
113
+ * key iteration over the prototype chain, replacer hooks). Output matches
114
+ * `JSON.stringify(spanEvents)` byte-for-byte for the post-sanitization shape.
115
+ *
116
+ * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
117
+ * @returns {string}
118
+ */
119
+ function stringifySpanEvents (spanEvents) {
120
+ let result = '['
121
+ for (let index = 0; index < spanEvents.length; index++) {
122
+ if (index > 0) result += ','
123
+ const event = spanEvents[index]
124
+ // `addEvent` does not type-check `name`; defer the unusual cases to
125
+ // `JSON.stringify` so non-string names match the prior behaviour
126
+ // instead of throwing in `escapeJsonString`.
127
+ if (typeof event.name !== 'string') {
128
+ result += JSON.stringify(event)
129
+ continue
130
+ }
131
+ result += '{"name":' + escapeJsonString(event.name) +
132
+ ',"time_unix_nano":' + jsonNumber(event.time_unix_nano)
133
+ if (event.attributes) {
134
+ result += ',"attributes":' + stringifyAttributes(event.attributes)
135
+ }
136
+ result += '}'
137
+ }
138
+ return result + ']'
139
+ }
140
+
141
+ function stringifyAttributes (attributes) {
142
+ let result = '{'
143
+ let first = true
144
+ for (const key of Object.keys(attributes)) {
145
+ if (first) {
146
+ first = false
16
147
  } else {
17
- span.meta.events = JSON.stringify(span.span_events)
18
- delete span.span_events
148
+ result += ','
19
149
  }
150
+ result += escapeJsonString(key) + ':' + stringifyAttributeValue(attributes[key])
20
151
  }
21
- return span
152
+ return result + '}'
153
+ }
154
+
155
+ function stringifyAttributeValue (value) {
156
+ if (typeof value === 'string') return escapeJsonString(value)
157
+ if (typeof value === 'number') return jsonNumber(value)
158
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
159
+ if (Array.isArray(value)) {
160
+ let result = '['
161
+ for (let index = 0; index < value.length; index++) {
162
+ if (index > 0) result += ','
163
+ result += stringifyAttributeValue(value[index])
164
+ }
165
+ return result + ']'
166
+ }
167
+ // Sanitization rejects everything else, but keep the safety net.
168
+ return 'null'
169
+ }
170
+
171
+ /**
172
+ * Match `JSON.stringify` for numbers: `NaN` and `±Infinity` collapse to the
173
+ * literal `null`, everything else uses ECMAScript's default `Number → String`
174
+ * conversion (which is what `JSON.stringify` calls internally).
175
+ *
176
+ * @param {number} value
177
+ * @returns {string}
178
+ */
179
+ function jsonNumber (value) {
180
+ if (Number.isFinite(value)) return String(value)
181
+ return 'null'
182
+ }
183
+
184
+ /**
185
+ * Fast path: scan once, and if no character in the string requires JSON
186
+ * escaping, emit `"<str>"` as-is. The scanned chars are `"`, `\`, and any
187
+ * control char in the U+0000–U+001F range. Anything else delegates to
188
+ * `JSON.stringify` for full spec-compliant escaping (surrogate pairs,
189
+ * lone surrogates, etc.).
190
+ *
191
+ * @param {string} value
192
+ * @returns {string}
193
+ */
194
+ function escapeJsonString (value) {
195
+ for (let index = 0; index < value.length; index++) {
196
+ const code = value.charCodeAt(index)
197
+ if (code < 0x20 || code === 0x22 || code === 0x5C) {
198
+ return JSON.stringify(value)
199
+ }
200
+ }
201
+ return '"' + value + '"'
22
202
  }
23
203
 
24
204
  class AgentEncoder {
205
+ #msgpack = new MsgpackEncoder()
206
+ #limit
207
+ #writer
208
+ #config
209
+ #debugEncoding
210
+ #formatSpan
211
+
25
212
  constructor (writer, limit = SOFT_LIMIT) {
26
- this._msgpack = new MsgpackEncoder()
27
- this._limit = limit
213
+ this.#limit = limit
28
214
  this._traceBytes = new MsgpackChunk()
29
215
  this._stringBytes = new MsgpackChunk()
30
- this._writer = writer
216
+ this.#writer = writer
31
217
  this._reset()
32
- this._config = getConfig()
33
- this._debugEncoding = this._config.DD_TRACE_ENCODING_DEBUG
218
+ this.#config = getConfig()
219
+ this.#debugEncoding = this.#config.DD_TRACE_ENCODING_DEBUG
220
+ // Pick the per-span formatter once so the hot loop pays no per-span
221
+ // config check. The native path doesn't need to reshape `span_events`
222
+ // because `#encodeSpanEvents` works directly on the raw attributes.
223
+ this.#formatSpan = this.#config.DD_TRACE_NATIVE_SPAN_EVENTS
224
+ ? normalizeSpan
225
+ : formatSpanWithLegacyEvents
34
226
  }
35
227
 
36
228
  count () {
@@ -45,21 +237,19 @@ class AgentEncoder {
45
237
 
46
238
  this._encode(bytes, trace)
47
239
 
48
- const end = bytes.length
49
-
50
- if (this._debugEncoding) {
240
+ if (this.#debugEncoding) {
241
+ const end = bytes.length
51
242
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
52
243
  log.debug(() => {
53
244
  const hex = bytes.buffer.subarray(start, end).toString('hex').match(/../g).join(' ')
54
-
55
245
  return `Adding encoded trace to buffer: ${hex}`
56
246
  })
57
247
  }
58
248
 
59
- // we can go over the soft limit since the agent has a 50MB hard limit
60
- if (this._traceBytes.length > this._limit || this._stringBytes.length > this._limit) {
249
+ // Soft limit overshoot is fine the agent caps at 50 MB.
250
+ if (this._traceBytes.length > this.#limit || this._stringBytes.length > this.#limit) {
61
251
  log.debug('Buffer went over soft limit, flushing')
62
- this._writer.flush()
252
+ this.#writer.flush()
63
253
  }
64
254
  }
65
255
 
@@ -81,56 +271,118 @@ class AgentEncoder {
81
271
  _encode (bytes, trace) {
82
272
  this._encodeArrayPrefix(bytes, trace)
83
273
 
274
+ const formatSpan = this.#formatSpan
275
+ const stringMap = this._stringMap
276
+ // Snapshot the string buffer so we can detect a mid-encode resize and
277
+ // release the old backing store afterwards (see `#refreshStringCache`).
278
+ const stringBufferAtStart = this._stringBytes.buffer
279
+
84
280
  for (let span of trace) {
85
- span = formatSpan(span, this._config)
86
- bytes.reserve(1)
281
+ span = formatSpan(span)
87
282
 
88
- // this is the original size of the fixed map for span attributes that always exist
89
283
  let mapSize = 11
284
+ if (span.type) mapSize++
285
+ if (span.meta_struct) mapSize++
286
+ if (span.span_events) mapSize++
287
+
288
+ // Pre-fetch the cached string entries up front and fuse the map prefix,
289
+ // optional `type`, three IDs, and `name` / `resource` / `service`
290
+ // emissions into a single `bytes.reserve` + sequential native writes.
291
+ // Replaces seven `bytes.reserve` calls per span (one each for the
292
+ // header, type, three IDs, three strings) with one.
293
+ let typeEntry
294
+ if (span.type) {
295
+ typeEntry = stringMap[span.type] ?? this._cacheString(span.type)
296
+ }
297
+ const nameEntry = stringMap[span.name] ?? this._cacheString(span.name)
298
+ const resourceEntry = stringMap[span.resource] ?? this._cacheString(span.resource)
299
+ const serviceEntry = stringMap[span.service] ?? this._cacheString(span.service)
300
+ const nameLen = nameEntry.length
301
+ const resourceLen = resourceEntry.length
302
+ const serviceLen = serviceEntry.length
303
+
304
+ // 1 byte map prefix + 3 ID fields (10/9/11 bytes prefix + 8 bytes value
305
+ // each) + the three string fields.
306
+ let blockSize = 1 +
307
+ KEY_TRACE_ID_PREFIX.length + 8 +
308
+ KEY_SPAN_ID_PREFIX.length + 8 +
309
+ KEY_PARENT_ID_PREFIX.length + 8 +
310
+ KEY_NAME.length + nameLen +
311
+ KEY_RESOURCE.length + resourceLen +
312
+ KEY_SERVICE.length + serviceLen
313
+ if (typeEntry) blockSize += KEY_TYPE.length + typeEntry.length
314
+
315
+ const blockOffset = bytes.length
316
+ bytes.reserve(blockSize)
317
+ const target = bytes.buffer
318
+ let cursor = blockOffset
319
+
320
+ target[cursor++] = 0x80 + mapSize
321
+
322
+ if (typeEntry) {
323
+ target.set(KEY_TYPE, cursor)
324
+ cursor += KEY_TYPE.length
325
+ target.set(typeEntry, cursor)
326
+ cursor += typeEntry.length
327
+ }
90
328
 
91
- // increment the payload map size depending on if some optional fields exist
92
- if (span.type) mapSize += 1
93
- if (span.meta_struct) mapSize += 1
94
- if (span.span_events) mapSize += 1
329
+ cursor = this.#writeIdAt(target, cursor, KEY_TRACE_ID_PREFIX, span.trace_id)
330
+ cursor = this.#writeIdAt(target, cursor, KEY_SPAN_ID_PREFIX, span.span_id)
331
+ cursor = this.#writeIdAt(target, cursor, KEY_PARENT_ID_PREFIX, span.parent_id)
95
332
 
96
- bytes.buffer[bytes.length - 1] = 0x80 + mapSize
333
+ target.set(KEY_NAME, cursor)
334
+ cursor += KEY_NAME.length
335
+ target.set(nameEntry, cursor)
336
+ cursor += nameLen
97
337
 
98
- if (span.type) {
99
- this._encodeString(bytes, 'type')
100
- this._encodeString(bytes, span.type)
101
- }
338
+ target.set(KEY_RESOURCE, cursor)
339
+ cursor += KEY_RESOURCE.length
340
+ target.set(resourceEntry, cursor)
341
+ cursor += resourceLen
342
+
343
+ target.set(KEY_SERVICE, cursor)
344
+ cursor += KEY_SERVICE.length
345
+ target.set(serviceEntry, cursor)
346
+
347
+ this.#writeIntegerField(bytes, KEY_ERROR_PREFIX, span.error)
348
+ this.#writeLongField(bytes, KEY_START_PREFIX, span.start)
349
+ this.#writeLongField(bytes, KEY_DURATION_PREFIX, span.duration)
350
+
351
+ this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta)
352
+ this.#encodeMetaEntries(bytes, KEY_METRICS_PREFIX, span.metrics)
102
353
 
103
- this._encodeString(bytes, 'trace_id')
104
- this._encodeId(bytes, span.trace_id)
105
- this._encodeString(bytes, 'span_id')
106
- this._encodeId(bytes, span.span_id)
107
- this._encodeString(bytes, 'parent_id')
108
- this._encodeId(bytes, span.parent_id)
109
- this._encodeString(bytes, 'name')
110
- this._encodeString(bytes, span.name)
111
- this._encodeString(bytes, 'resource')
112
- this._encodeString(bytes, span.resource)
113
- this._encodeString(bytes, 'service')
114
- this._encodeString(bytes, span.service)
115
- this._encodeString(bytes, 'error')
116
- this._encodeInteger(bytes, span.error)
117
- this._encodeString(bytes, 'start')
118
- this._encodeLong(bytes, span.start)
119
- this._encodeString(bytes, 'duration')
120
- this._encodeLong(bytes, span.duration)
121
- this._encodeString(bytes, 'meta')
122
- this._encodeMap(bytes, span.meta)
123
- this._encodeString(bytes, 'metrics')
124
- this._encodeMap(bytes, span.metrics)
125
354
  if (span.span_events) {
126
- this._encodeString(bytes, 'span_events')
127
- this._encodeObjectAsArray(bytes, span.span_events, new Set())
355
+ bytes.set(KEY_SPAN_EVENTS)
356
+ this.#encodeSpanEvents(bytes, span.span_events)
128
357
  }
129
358
  if (span.meta_struct) {
130
- this._encodeString(bytes, 'meta_struct')
131
- this._encodeMetaStruct(bytes, span.meta_struct)
359
+ bytes.set(KEY_META_STRUCT)
360
+ this.#encodeMetaStruct(bytes, span.meta_struct)
132
361
  }
133
362
  }
363
+
364
+ if (this._stringBytes.buffer !== stringBufferAtStart) {
365
+ this.#refreshStringCache()
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Rebuild the cached string subarrays in `_stringMap` against the current
371
+ * `_stringBytes.buffer`. After `MsgpackChunk.reserve()` resizes, the prior
372
+ * subarrays still reference the old `Buffer`'s `ArrayBuffer` and pin it
373
+ * until `_reset()` clears the map; for a payload that grows 2 → 4 → 8 MB
374
+ * that is up to 6 MB of avoidable peak memory. `Buffer.allocUnsafe(>= 2
375
+ * MB)` bypasses the small-allocation pool and starts at offset 0 in its
376
+ * `ArrayBuffer`, so the old subarray's `byteOffset` is the entry's start
377
+ * position in the new buffer.
378
+ */
379
+ #refreshStringCache () {
380
+ const newBuffer = this._stringBytes.buffer
381
+ const stringMap = this._stringMap
382
+ for (const key of Object.keys(stringMap)) {
383
+ const old = stringMap[key]
384
+ stringMap[key] = newBuffer.subarray(old.byteOffset, old.byteOffset + old.length)
385
+ }
134
386
  }
135
387
 
136
388
  _reset () {
@@ -144,123 +396,302 @@ class AgentEncoder {
144
396
  }
145
397
 
146
398
  _encodeBuffer (bytes, buffer) {
147
- this._msgpack.encodeBin(bytes, buffer)
399
+ this.#msgpack.encodeBin(bytes, buffer)
148
400
  }
149
401
 
150
402
  _encodeBool (bytes, value) {
151
- this._msgpack.encodeBoolean(bytes, value)
403
+ this.#msgpack.encodeBoolean(bytes, value)
152
404
  }
153
405
 
154
406
  _encodeArrayPrefix (bytes, value) {
155
- this._msgpack.encodeArrayPrefix(bytes, value)
407
+ this.#msgpack.encodeArrayPrefix(bytes, value)
156
408
  }
157
409
 
158
410
  _encodeMapPrefix (bytes, keysLength) {
159
- this._msgpack.encodeMapPrefix(bytes, keysLength)
411
+ this.#msgpack.encodeMapPrefix(bytes, keysLength)
160
412
  }
161
413
 
162
414
  _encodeByte (bytes, value) {
163
- this._msgpack.encodeByte(bytes, value)
415
+ this.#msgpack.encodeByte(bytes, value)
164
416
  }
165
417
 
166
418
  // TODO: Use BigInt instead.
167
- _encodeId (bytes, id) {
419
+ _encodeId (bytes, identifier) {
420
+ const idBuffer = identifier.toBuffer()
421
+ const start = idBuffer.length - 8
168
422
  const offset = bytes.length
169
423
 
170
424
  bytes.reserve(9)
171
425
 
172
- id = id.toArray()
173
-
174
- bytes.buffer[offset] = 0xCF
175
- bytes.buffer[offset + 1] = id[0]
176
- bytes.buffer[offset + 2] = id[1]
177
- bytes.buffer[offset + 3] = id[2]
178
- bytes.buffer[offset + 4] = id[3]
179
- bytes.buffer[offset + 5] = id[4]
180
- bytes.buffer[offset + 6] = id[5]
181
- bytes.buffer[offset + 7] = id[6]
182
- bytes.buffer[offset + 8] = id[7]
426
+ const target = bytes.buffer
427
+ target[offset] = 0xCF
428
+ target[offset + 1] = idBuffer[start]
429
+ target[offset + 2] = idBuffer[start + 1]
430
+ target[offset + 3] = idBuffer[start + 2]
431
+ target[offset + 4] = idBuffer[start + 3]
432
+ target[offset + 5] = idBuffer[start + 4]
433
+ target[offset + 6] = idBuffer[start + 5]
434
+ target[offset + 7] = idBuffer[start + 6]
435
+ target[offset + 8] = idBuffer[start + 7]
183
436
  }
184
437
 
185
438
  _encodeNumber (bytes, value) {
186
- this._msgpack.encodeNumber(bytes, value)
439
+ this.#msgpack.encodeNumber(bytes, value)
187
440
  }
188
441
 
189
442
  _encodeInteger (bytes, value) {
190
- this._msgpack.encodeInteger(bytes, value)
443
+ this.#msgpack.encodeInteger(bytes, value)
191
444
  }
192
445
 
193
446
  _encodeLong (bytes, value) {
194
- this._msgpack.encodeLong(bytes, value)
447
+ this.#msgpack.encodeLong(bytes, value)
195
448
  }
196
449
 
450
+ // Single pass: reserve the count slot, encode entries while counting, patch the count.
197
451
  _encodeMap (bytes, value) {
198
- const keys = Object.keys(value)
199
- const validKeys = keys.filter(key => typeof value[key] === 'string' || typeof value[key] === 'number')
452
+ const offset = bytes.length
453
+ bytes.reserve(5)
454
+ bytes.buffer[offset] = 0xDF
455
+
456
+ let count = 0
457
+ for (const key of Object.keys(value)) {
458
+ const entryValue = value[key]
459
+ if (typeof entryValue === 'string') {
460
+ this._encodeString(bytes, key)
461
+ this._encodeString(bytes, entryValue)
462
+ count++
463
+ } else if (typeof entryValue === 'number') {
464
+ this._encodeString(bytes, key)
465
+ this.#encodeFloat(bytes, entryValue)
466
+ count++
467
+ }
468
+ }
200
469
 
201
- this._encodeMapPrefix(bytes, validKeys.length)
470
+ const target = bytes.buffer
471
+ target[offset + 1] = count >>> 24
472
+ target[offset + 2] = count >>> 16
473
+ target[offset + 3] = count >>> 8
474
+ target[offset + 4] = count
475
+ }
202
476
 
203
- for (const key of validKeys) {
204
- this._encodeString(bytes, key)
205
- this._encodeValue(bytes, value[key])
477
+ _encodeString (bytes, value = '') {
478
+ const entry = this._stringMap[value] ?? this._cacheString(value)
479
+ const length = entry.length
480
+ const offset = bytes.length
481
+ bytes.reserve(length)
482
+ bytes.buffer.set(entry, offset)
483
+ }
484
+
485
+ _cacheString (value) {
486
+ let entry = this._stringMap[value]
487
+ if (entry === undefined) {
488
+ this._stringCount++
489
+ const start = this._stringBytes.length
490
+ const written = this._stringBytes.write(value)
491
+ entry = this._stringBytes.buffer.subarray(start, start + written)
492
+ this._stringMap[value] = entry
206
493
  }
494
+ return entry
207
495
  }
208
496
 
209
- _encodeValue (bytes, value) {
497
+ _writeArrayPrefix (buffer, offset, count) {
498
+ buffer[offset++] = 0xDD
499
+ buffer.writeUInt32BE(count, offset)
500
+
501
+ return offset + 4
502
+ }
503
+
504
+ _writeTraces (buffer, offset = 0) {
505
+ offset = this._writeArrayPrefix(buffer, offset, this._traceCount)
506
+ offset += this._traceBytes.buffer.copy(buffer, offset, 0, this._traceBytes.length)
507
+
508
+ return offset
509
+ }
510
+
511
+ /**
512
+ * Fast path for `span.meta` / `span.metrics`. Inlines the string cache so
513
+ * each entry is one reserve (not two) and skips the polymorphic dispatch.
514
+ *
515
+ * @param {MsgpackChunk} bytes
516
+ * @param {Buffer} keyPrefix Precomputed `[key, 0xDF]`.
517
+ * @param {Record<string, unknown>} value
518
+ */
519
+ #encodeMetaEntries (bytes, keyPrefix, value) {
520
+ const keyPrefixLen = keyPrefix.length
521
+ const headerOffset = bytes.length
522
+ bytes.reserve(keyPrefixLen + 4)
523
+ bytes.buffer.set(keyPrefix, headerOffset)
524
+ const countOffset = headerOffset + keyPrefixLen
525
+
526
+ const stringMap = this._stringMap
527
+ let count = 0
528
+
529
+ for (const key of Object.keys(value)) {
530
+ const entryValue = value[key]
531
+ if (typeof entryValue !== 'string' && typeof entryValue !== 'number') continue
532
+
533
+ const keyEntry = stringMap[key] ?? this._cacheString(key)
534
+ const keyEntryLen = keyEntry.length
535
+ const writeOffset = bytes.length
536
+
537
+ if (typeof entryValue === 'string') {
538
+ const valueEntry = stringMap[entryValue] ?? this._cacheString(entryValue)
539
+ const valueEntryLen = valueEntry.length
540
+ bytes.reserve(keyEntryLen + valueEntryLen)
541
+ const target = bytes.buffer
542
+ target.set(keyEntry, writeOffset)
543
+ target.set(valueEntry, writeOffset + keyEntryLen)
544
+ } else {
545
+ bytes.reserve(keyEntryLen + 9)
546
+ const target = bytes.buffer
547
+ target.set(keyEntry, writeOffset)
548
+ const valueOffset = writeOffset + keyEntryLen
549
+ target[valueOffset] = 0xCB
550
+ bytes.view.setFloat64(valueOffset + 1, entryValue)
551
+ }
552
+ count++
553
+ }
554
+
555
+ const target = bytes.buffer
556
+ target[countOffset] = count >>> 24
557
+ target[countOffset + 1] = count >>> 16
558
+ target[countOffset + 2] = count >>> 8
559
+ target[countOffset + 3] = count
560
+ }
561
+
562
+ /**
563
+ * Write `[keyPrefix, 8-byte uint64 id]` into `target` at `offset` and
564
+ * return the new cursor. Caller is responsible for having reserved enough
565
+ * room — this is the no-reserve variant used inside `_encode`'s combined
566
+ * fixed-fields block.
567
+ *
568
+ * @param {Uint8Array} target
569
+ * @param {number} offset
570
+ * @param {Buffer} keyPrefix Precomputed `[key, 0xCF]`.
571
+ * @param {{ toBuffer: () => Uint8Array | number[] }} identifier
572
+ * @returns {number}
573
+ */
574
+ #writeIdAt (target, offset, keyPrefix, identifier) {
575
+ target.set(keyPrefix, offset)
576
+ offset += keyPrefix.length
577
+ const idBuffer = identifier.toBuffer()
578
+ const start = idBuffer.length - 8
579
+ target[offset] = idBuffer[start]
580
+ target[offset + 1] = idBuffer[start + 1]
581
+ target[offset + 2] = idBuffer[start + 2]
582
+ target[offset + 3] = idBuffer[start + 3]
583
+ target[offset + 4] = idBuffer[start + 4]
584
+ target[offset + 5] = idBuffer[start + 5]
585
+ target[offset + 6] = idBuffer[start + 6]
586
+ target[offset + 7] = idBuffer[start + 7]
587
+ return offset + 8
588
+ }
589
+
590
+ /**
591
+ * @param {MsgpackChunk} bytes
592
+ * @param {Buffer} keyPrefix Precomputed `[key, 0xCE]`.
593
+ * @param {number} value
594
+ */
595
+ #writeIntegerField (bytes, keyPrefix, value) {
596
+ const keyPrefixLen = keyPrefix.length
597
+ const offset = bytes.length
598
+ bytes.reserve(keyPrefixLen + 4)
599
+
600
+ const target = bytes.buffer
601
+ target.set(keyPrefix, offset)
602
+
603
+ const valueOffset = offset + keyPrefixLen
604
+ target[valueOffset] = value >> 24
605
+ target[valueOffset + 1] = value >> 16
606
+ target[valueOffset + 2] = value >> 8
607
+ target[valueOffset + 3] = value
608
+ }
609
+
610
+ /**
611
+ * @param {MsgpackChunk} bytes
612
+ * @param {Buffer} keyPrefix Precomputed `[key, 0xCF]`.
613
+ * @param {number} value Up to a 53-bit safe integer.
614
+ */
615
+ #writeLongField (bytes, keyPrefix, value) {
616
+ const high = (value / 2 ** 32) >> 0
617
+ const low = value >>> 0
618
+ const keyPrefixLen = keyPrefix.length
619
+ const offset = bytes.length
620
+ bytes.reserve(keyPrefixLen + 8)
621
+
622
+ const target = bytes.buffer
623
+ target.set(keyPrefix, offset)
624
+
625
+ const valueOffset = offset + keyPrefixLen
626
+ target[valueOffset] = high >> 24
627
+ target[valueOffset + 1] = high >> 16
628
+ target[valueOffset + 2] = high >> 8
629
+ target[valueOffset + 3] = high
630
+ target[valueOffset + 4] = low >> 24
631
+ target[valueOffset + 5] = low >> 16
632
+ target[valueOffset + 6] = low >> 8
633
+ target[valueOffset + 7] = low
634
+ }
635
+
636
+ /**
637
+ * @param {MsgpackChunk} bytes
638
+ * @param {string | number | boolean} value
639
+ */
640
+ #encodeValue (bytes, value) {
210
641
  switch (typeof value) {
211
642
  case 'string':
212
643
  this._encodeString(bytes, value)
213
644
  break
214
645
  case 'number':
215
- this._encodeFloat(bytes, value)
646
+ this.#encodeFloat(bytes, value)
216
647
  break
217
648
  case 'boolean':
218
649
  this._encodeBool(bytes, value)
219
650
  break
220
- default:
221
- // should not happen
222
651
  }
223
652
  }
224
653
 
225
- _encodeString (bytes, value = '') {
226
- this._cacheString(value)
227
-
228
- const { start, end } = this._stringMap[value]
229
-
230
- this._stringBytes.copy(bytes, start, end)
231
- }
232
-
233
- _encodeFloat (bytes, value) {
234
- this._msgpack.encodeFloat(bytes, value)
654
+ #encodeFloat (bytes, value) {
655
+ this.#msgpack.encodeFloat(bytes, value)
235
656
  }
236
657
 
237
- _encodeMetaStruct (bytes, value) {
238
- const keys = Array.isArray(value) ? [] : Object.keys(value)
239
- const validKeys = keys.filter(key => {
240
- const v = value[key]
241
- return typeof v === 'string' ||
242
- typeof v === 'number' ||
243
- (v !== null && typeof v === 'object')
244
- })
245
-
246
- this._encodeMapPrefix(bytes, validKeys.length)
658
+ #encodeMetaStruct (bytes, value) {
659
+ if (Array.isArray(value)) {
660
+ this._encodeMapPrefix(bytes, 0)
661
+ return
662
+ }
247
663
 
248
- for (const key of validKeys) {
249
- const v = value[key]
250
- this._encodeString(bytes, key)
251
- this._encodeObjectAsByteArray(bytes, v)
664
+ const offset = bytes.length
665
+ bytes.reserve(5)
666
+ bytes.buffer[offset] = 0xDF
667
+
668
+ let count = 0
669
+ for (const key of Object.keys(value)) {
670
+ const entryValue = value[key]
671
+ if (typeof entryValue === 'string' || typeof entryValue === 'number' ||
672
+ (entryValue !== null && typeof entryValue === 'object')) {
673
+ this._encodeString(bytes, key)
674
+ this.#encodeObjectAsByteArray(bytes, entryValue)
675
+ count++
676
+ }
252
677
  }
678
+
679
+ const target = bytes.buffer
680
+ target[offset + 1] = count >>> 24
681
+ target[offset + 2] = count >>> 16
682
+ target[offset + 3] = count >>> 8
683
+ target[offset + 4] = count
253
684
  }
254
685
 
255
- _encodeObjectAsByteArray (bytes, value) {
686
+ #encodeObjectAsByteArray (bytes, value) {
256
687
  const prefixLength = 5
257
688
  const offset = bytes.length
258
689
 
259
690
  bytes.reserve(prefixLength)
260
691
 
261
- this._encodeObject(bytes, value)
692
+ this.#encodeObject(bytes, value)
262
693
 
263
- // we should do it after encoding the object to know the real length
694
+ // The byte length isn't known until the inner object has been encoded.
264
695
  const length = bytes.length - offset - prefixLength
265
696
  bytes.buffer[offset] = 0xC6
266
697
  bytes.buffer[offset + 1] = length >> 24
@@ -269,157 +700,251 @@ class AgentEncoder {
269
700
  bytes.buffer[offset + 4] = length
270
701
  }
271
702
 
272
- _encodeObject (bytes, value, circularReferencesDetector = new Set()) {
703
+ #encodeObject (bytes, value, circularReferencesDetector = new Set()) {
273
704
  circularReferencesDetector.add(value)
274
705
  if (Array.isArray(value)) {
275
- this._encodeObjectAsArray(bytes, value, circularReferencesDetector)
706
+ this.#encodeObjectAsArray(bytes, value, circularReferencesDetector)
276
707
  } else if (value !== null && typeof value === 'object') {
277
- this._encodeObjectAsMap(bytes, value, circularReferencesDetector)
708
+ this.#encodeObjectAsMap(bytes, value, circularReferencesDetector)
278
709
  } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
279
- this._encodeValue(bytes, value)
710
+ this.#encodeValue(bytes, value)
280
711
  }
281
712
  }
282
713
 
283
- _encodeObjectAsMap (bytes, value, circularReferencesDetector) {
284
- const keys = Object.keys(value)
285
- const validKeys = keys.filter(key => {
286
- const v = value[key]
287
- return typeof v === 'string' ||
288
- typeof v === 'number' || typeof v === 'boolean' ||
289
- (v !== null && typeof v === 'object' && !circularReferencesDetector.has(v))
290
- })
291
-
292
- this._encodeMapPrefix(bytes, validKeys.length)
293
-
294
- for (const key of validKeys) {
295
- const v = value[key]
296
- this._encodeString(bytes, key)
297
- this._encodeObject(bytes, v, circularReferencesDetector)
714
+ #encodeObjectAsMap (bytes, value, circularReferencesDetector) {
715
+ const offset = bytes.length
716
+ bytes.reserve(5)
717
+ bytes.buffer[offset] = 0xDF
718
+
719
+ let count = 0
720
+ for (const key of Object.keys(value)) {
721
+ const entryValue = value[key]
722
+ if (typeof entryValue === 'string' || typeof entryValue === 'number' || typeof entryValue === 'boolean' ||
723
+ (entryValue !== null && typeof entryValue === 'object' &&
724
+ !circularReferencesDetector.has(entryValue))) {
725
+ this._encodeString(bytes, key)
726
+ this.#encodeObject(bytes, entryValue, circularReferencesDetector)
727
+ count++
728
+ }
298
729
  }
299
- }
300
-
301
- _encodeObjectAsArray (bytes, value, circularReferencesDetector) {
302
- const validValue = value.filter(item =>
303
- typeof item === 'string' ||
304
- typeof item === 'number' ||
305
- (item !== null && typeof item === 'object' && !circularReferencesDetector.has(item)))
306
-
307
- this._encodeArrayPrefix(bytes, validValue)
308
730
 
309
- for (const item of validValue) {
310
- this._encodeObject(bytes, item, circularReferencesDetector)
311
- }
731
+ const target = bytes.buffer
732
+ target[offset + 1] = count >>> 24
733
+ target[offset + 2] = count >>> 16
734
+ target[offset + 3] = count >>> 8
735
+ target[offset + 4] = count
312
736
  }
313
737
 
314
- _cacheString (value) {
315
- if (!(value in this._stringMap)) {
316
- this._stringCount++
317
- this._stringMap[value] = {
318
- start: this._stringBytes.length,
319
- end: this._stringBytes.length + this._stringBytes.write(value),
738
+ #encodeObjectAsArray (bytes, value, circularReferencesDetector) {
739
+ const offset = bytes.length
740
+ bytes.reserve(5)
741
+ bytes.buffer[offset] = 0xDD
742
+
743
+ let count = 0
744
+ for (const item of value) {
745
+ if (typeof item === 'string' || typeof item === 'number' ||
746
+ (item !== null && typeof item === 'object' && !circularReferencesDetector.has(item))) {
747
+ this.#encodeObject(bytes, item, circularReferencesDetector)
748
+ count++
320
749
  }
321
750
  }
322
- }
323
751
 
324
- _writeArrayPrefix (buffer, offset, count) {
325
- buffer[offset++] = 0xDD
326
- buffer.writeUInt32BE(count, offset)
752
+ const target = bytes.buffer
753
+ target[offset + 1] = count >>> 24
754
+ target[offset + 2] = count >>> 16
755
+ target[offset + 3] = count >>> 8
756
+ target[offset + 4] = count
757
+ }
758
+
759
+ /**
760
+ * Specialized encoder for `span.span_events`. Walks the events directly,
761
+ * emitting OTel attribute typed wrappers inline from the raw attribute
762
+ * values — no `formatSpanEvents` pre-pass and no recursive generic walk.
763
+ *
764
+ * @param {MsgpackChunk} bytes
765
+ * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
766
+ */
767
+ #encodeSpanEvents (bytes, spanEvents) {
768
+ const offset = bytes.length
769
+ bytes.reserve(5)
770
+ bytes.buffer[offset] = 0xDD
327
771
 
328
- return offset + 4
329
- }
772
+ let arrayCount = 0
773
+ for (const event of spanEvents) {
774
+ // `addEvent` and the OTel bridge do not type-check `name`, and a
775
+ // non-string would throw downstream in `Buffer.byteLength`. Drop the
776
+ // bad event silently so the rest of the trace still encodes.
777
+ if (event === null || typeof event !== 'object' || typeof event.name !== 'string') continue
330
778
 
331
- _writeTraces (buffer, offset = 0) {
332
- offset = this._writeArrayPrefix(buffer, offset, this._traceCount)
333
- offset += this._traceBytes.buffer.copy(buffer, offset, 0, this._traceBytes.length)
779
+ const eventHeaderOffset = bytes.length
780
+ bytes.reserve(1)
781
+ bytes.buffer[eventHeaderOffset] = 0x82
334
782
 
335
- return offset
336
- }
337
- }
783
+ bytes.set(KEY_NAME)
784
+ this._encodeString(bytes, event.name)
785
+ bytes.set(KEY_EVENT_TIME)
786
+ this.#encodeFloat(bytes, event.time_unix_nano)
338
787
 
339
- const seenKeys = new Set()
340
- const memoizedLogDebug = (key, message) => {
341
- if (!seenKeys.has(key)) {
342
- seenKeys.add(key)
343
- log.debug(message)
344
- }
345
- }
346
-
347
- function formatSpanEvents (span) {
348
- for (const spanEvent of span.span_events) {
349
- if (spanEvent.attributes) {
350
- let hasAttributes = false
351
- for (const [key, value] of Object.entries(spanEvent.attributes)) {
352
- const newValue = convertSpanEventAttributeValues(key, value)
353
- if (newValue === undefined) {
354
- delete spanEvent.attributes[key] // delete from attributes if undefined
355
- } else {
356
- hasAttributes = true
357
- spanEvent.attributes[key] = newValue
358
- }
359
- }
360
- if (!hasAttributes) {
361
- delete spanEvent.attributes
788
+ const attributes = event.attributes
789
+ if (attributes !== null && typeof attributes === 'object') {
790
+ this.#encodeAttributesIfAny(bytes, attributes, eventHeaderOffset)
362
791
  }
792
+ arrayCount++
363
793
  }
364
- }
365
- }
366
794
 
367
- function convertSpanEventAttributeValues (key, value, depth = 0) {
368
- if (typeof value === 'string') {
369
- return {
370
- type: 0,
371
- string_value: value,
795
+ const target = bytes.buffer
796
+ target[offset + 1] = arrayCount >>> 24
797
+ target[offset + 2] = arrayCount >>> 16
798
+ target[offset + 3] = arrayCount >>> 8
799
+ target[offset + 4] = arrayCount
800
+ }
801
+
802
+ /**
803
+ * Optimistically emits the `'attributes'` slot for an event. If every entry
804
+ * filters out, the partial output is rewound and the event's map header
805
+ * stays at 2 entries.
806
+ *
807
+ * @param {MsgpackChunk} bytes
808
+ * @param {Record<string, unknown>} attributes
809
+ * @param {number} eventHeaderOffset
810
+ */
811
+ #encodeAttributesIfAny (bytes, attributes, eventHeaderOffset) {
812
+ const sectionStart = bytes.length
813
+
814
+ bytes.set(KEY_EVENT_ATTRIBUTES)
815
+ const countOffset = bytes.length
816
+ bytes.reserve(5)
817
+ bytes.buffer[countOffset] = 0xDF
818
+
819
+ let count = 0
820
+ for (const key of Object.keys(attributes)) {
821
+ if (this.#emitAttribute(bytes, key, attributes[key])) count++
372
822
  }
373
- }
374
823
 
375
- if (typeof value === 'boolean') {
376
- return {
377
- type: 1,
378
- bool_value: value,
824
+ if (count === 0) {
825
+ bytes.length = sectionStart
826
+ return
379
827
  }
380
- }
381
828
 
382
- if (typeof value === 'number') {
383
- if (Number.isInteger(value)) {
384
- return {
385
- type: 2,
386
- int_value: value,
387
- }
829
+ const target = bytes.buffer
830
+ target[countOffset + 1] = count >>> 24
831
+ target[countOffset + 2] = count >>> 16
832
+ target[countOffset + 3] = count >>> 8
833
+ target[countOffset + 4] = count
834
+ bytes.buffer[eventHeaderOffset] = 0x83
835
+ }
836
+
837
+ /**
838
+ * Emit `<key, typed_wrapper>` for a single attribute. Returns true on a
839
+ * supported type, false (with a memoized debug log) otherwise.
840
+ *
841
+ * @param {MsgpackChunk} bytes
842
+ * @param {string} key
843
+ * @param {unknown} value
844
+ * @returns {boolean}
845
+ */
846
+ #emitAttribute (bytes, key, value) {
847
+ if (typeof value === 'string') {
848
+ this._encodeString(bytes, key)
849
+ bytes.set(ATTR_PREFIX_STRING)
850
+ this._encodeString(bytes, value)
851
+ return true
388
852
  }
389
- return {
390
- type: 3,
391
- double_value: value,
853
+ if (typeof value === 'number') {
854
+ this._encodeString(bytes, key)
855
+ bytes.set(Number.isInteger(value) ? ATTR_PREFIX_INT : ATTR_PREFIX_DOUBLE)
856
+ this.#encodeFloat(bytes, value)
857
+ return true
858
+ }
859
+ if (typeof value === 'boolean') {
860
+ this._encodeString(bytes, key)
861
+ bytes.set(value ? ATTR_PAYLOAD_BOOL_TRUE : ATTR_PAYLOAD_BOOL_FALSE)
862
+ return true
863
+ }
864
+ if (Array.isArray(value)) {
865
+ return this.#emitArrayAttribute(bytes, key, value)
866
+ }
867
+ memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' +
868
+ `${key}: with value: ${typeof value}. Skipping encoding of pair.`
869
+ )
870
+ return false
871
+ }
872
+
873
+ /**
874
+ * Emit `<key, { type: 4, array_value: { values: [...] } }>` from a raw
875
+ * array of primitives. Filters nested arrays / unsupported items; if no
876
+ * items remain the whole entry is rewound.
877
+ *
878
+ * @param {MsgpackChunk} bytes
879
+ * @param {string} key
880
+ * @param {Array<unknown>} array
881
+ * @returns {boolean}
882
+ */
883
+ #emitArrayAttribute (bytes, key, array) {
884
+ const sectionStart = bytes.length
885
+
886
+ this._encodeString(bytes, key)
887
+ bytes.set(ATTR_PREFIX_ARRAY)
888
+ const arrayCountOffset = bytes.length
889
+ bytes.reserve(4)
890
+
891
+ let count = 0
892
+ for (const item of array) {
893
+ if (this.#emitArrayItem(bytes, key, item)) count++
392
894
  }
393
- }
394
895
 
395
- if (Array.isArray(value)) {
396
- if (depth === 0) {
397
- const convertedArray = []
398
- for (const val of value) {
399
- const convertedVal = convertSpanEventAttributeValues(key, val, 1)
400
- if (convertedVal !== undefined) {
401
- convertedArray.push(convertedVal)
402
- }
403
- }
896
+ if (count === 0) {
897
+ bytes.length = sectionStart
898
+ return false
899
+ }
404
900
 
405
- // Only include array_value if there are valid elements
406
- if (convertedArray.length > 0) {
407
- return {
408
- type: 4,
409
- array_value: { values: convertedArray },
410
- }
411
- }
412
- // If all elements were unsupported, return undefined
413
- } else {
901
+ const target = bytes.buffer
902
+ target[arrayCountOffset] = count >>> 24
903
+ target[arrayCountOffset + 1] = count >>> 16
904
+ target[arrayCountOffset + 2] = count >>> 8
905
+ target[arrayCountOffset + 3] = count
906
+ return true
907
+ }
908
+
909
+ /**
910
+ * Emit a single typed wrapper inside an `array_value.values` array. No
911
+ * recursion: nested arrays are dropped with a memoized debug log.
912
+ *
913
+ * @param {MsgpackChunk} bytes
914
+ * @param {string} key
915
+ * @param {unknown} value
916
+ * @returns {boolean}
917
+ */
918
+ #emitArrayItem (bytes, key, value) {
919
+ if (typeof value === 'string') {
920
+ bytes.set(ATTR_PREFIX_STRING)
921
+ this._encodeString(bytes, value)
922
+ return true
923
+ }
924
+ if (typeof value === 'number') {
925
+ bytes.set(Number.isInteger(value) ? ATTR_PREFIX_INT : ATTR_PREFIX_DOUBLE)
926
+ this.#encodeFloat(bytes, value)
927
+ return true
928
+ }
929
+ if (typeof value === 'boolean') {
930
+ bytes.set(value ? ATTR_PAYLOAD_BOOL_TRUE : ATTR_PAYLOAD_BOOL_FALSE)
931
+ return true
932
+ }
933
+ if (Array.isArray(value)) {
414
934
  memoizedLogDebug(key, 'Encountered nested array data type for span event v0.4 encoding. ' +
415
935
  `Skipping encoding key: ${key}: with value: ${typeof value}.`
416
936
  )
417
937
  }
418
- } else {
419
- memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' +
420
- `${key}: with value: ${typeof value}. Skipping encoding of pair.`
421
- )
938
+ return false
939
+ }
940
+ }
941
+
942
+ const seenKeys = new Set()
943
+ function memoizedLogDebug (key, message) {
944
+ if (!seenKeys.has(key)) {
945
+ seenKeys.add(key)
946
+ log.debug(message)
422
947
  }
423
948
  }
424
949
 
425
- module.exports = { AgentEncoder }
950
+ module.exports = { AgentEncoder, stringifySpanEvents }