dd-trace 5.101.0 → 5.103.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 (235) hide show
  1. package/ext/exporters.js +1 -0
  2. package/package.json +20 -17
  3. package/packages/datadog-esbuild/src/utils.js +2 -2
  4. package/packages/datadog-instrumentations/src/aerospike.js +2 -2
  5. package/packages/datadog-instrumentations/src/ai.js +9 -9
  6. package/packages/datadog-instrumentations/src/amqplib.js +6 -7
  7. package/packages/datadog-instrumentations/src/anthropic.js +10 -10
  8. package/packages/datadog-instrumentations/src/apollo-server-core.js +3 -3
  9. package/packages/datadog-instrumentations/src/apollo-server.js +5 -5
  10. package/packages/datadog-instrumentations/src/avsc.js +6 -6
  11. package/packages/datadog-instrumentations/src/aws-sdk.js +151 -67
  12. package/packages/datadog-instrumentations/src/azure-durable-functions.js +8 -8
  13. package/packages/datadog-instrumentations/src/bluebird.js +2 -2
  14. package/packages/datadog-instrumentations/src/body-parser.js +2 -2
  15. package/packages/datadog-instrumentations/src/cassandra-driver.js +7 -7
  16. package/packages/datadog-instrumentations/src/child_process.js +12 -12
  17. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +41 -24
  18. package/packages/datadog-instrumentations/src/connect.js +7 -7
  19. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  20. package/packages/datadog-instrumentations/src/cookie.js +2 -2
  21. package/packages/datadog-instrumentations/src/couchbase.js +73 -238
  22. package/packages/datadog-instrumentations/src/crypto.js +4 -4
  23. package/packages/datadog-instrumentations/src/cucumber.js +78 -17
  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/electron/preload.js +42 -0
  27. package/packages/datadog-instrumentations/src/electron.js +240 -0
  28. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +6 -6
  29. package/packages/datadog-instrumentations/src/express-session.js +4 -4
  30. package/packages/datadog-instrumentations/src/express.js +10 -11
  31. package/packages/datadog-instrumentations/src/fastify.js +2 -2
  32. package/packages/datadog-instrumentations/src/fetch.js +5 -5
  33. package/packages/datadog-instrumentations/src/fs.js +14 -14
  34. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +5 -7
  35. package/packages/datadog-instrumentations/src/google-genai.js +4 -4
  36. package/packages/datadog-instrumentations/src/graphql.js +13 -12
  37. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  38. package/packages/datadog-instrumentations/src/hapi.js +2 -2
  39. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +9 -9
  40. package/packages/datadog-instrumentations/src/helpers/hook.js +4 -1
  41. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  42. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -2
  43. package/packages/datadog-instrumentations/src/helpers/kafka.js +41 -0
  44. package/packages/datadog-instrumentations/src/helpers/promise.js +2 -2
  45. package/packages/datadog-instrumentations/src/hono.js +2 -2
  46. package/packages/datadog-instrumentations/src/http/client.js +6 -6
  47. package/packages/datadog-instrumentations/src/http/server.js +9 -9
  48. package/packages/datadog-instrumentations/src/ioredis.js +16 -12
  49. package/packages/datadog-instrumentations/src/jest.js +382 -81
  50. package/packages/datadog-instrumentations/src/kafkajs.js +165 -174
  51. package/packages/datadog-instrumentations/src/knex.js +17 -17
  52. package/packages/datadog-instrumentations/src/koa.js +12 -12
  53. package/packages/datadog-instrumentations/src/ldapjs.js +5 -5
  54. package/packages/datadog-instrumentations/src/light-my-request.js +2 -2
  55. package/packages/datadog-instrumentations/src/limitd-client.js +4 -4
  56. package/packages/datadog-instrumentations/src/lodash.js +4 -4
  57. package/packages/datadog-instrumentations/src/mariadb.js +13 -13
  58. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  59. package/packages/datadog-instrumentations/src/microgateway-core.js +2 -2
  60. package/packages/datadog-instrumentations/src/mocha/common.js +3 -3
  61. package/packages/datadog-instrumentations/src/mocha/main.js +85 -11
  62. package/packages/datadog-instrumentations/src/mocha/utils.js +133 -16
  63. package/packages/datadog-instrumentations/src/mocha/worker.js +7 -5
  64. package/packages/datadog-instrumentations/src/mongodb-core.js +42 -30
  65. package/packages/datadog-instrumentations/src/mongodb.js +5 -5
  66. package/packages/datadog-instrumentations/src/mongoose.js +21 -21
  67. package/packages/datadog-instrumentations/src/mquery.js +5 -5
  68. package/packages/datadog-instrumentations/src/multer.js +4 -4
  69. package/packages/datadog-instrumentations/src/mysql.js +16 -16
  70. package/packages/datadog-instrumentations/src/mysql2.js +4 -4
  71. package/packages/datadog-instrumentations/src/net.js +14 -8
  72. package/packages/datadog-instrumentations/src/nyc.js +5 -5
  73. package/packages/datadog-instrumentations/src/openai.js +19 -19
  74. package/packages/datadog-instrumentations/src/oracledb.js +6 -6
  75. package/packages/datadog-instrumentations/src/passport-utils.js +5 -5
  76. package/packages/datadog-instrumentations/src/pg.js +39 -25
  77. package/packages/datadog-instrumentations/src/pino.js +6 -10
  78. package/packages/datadog-instrumentations/src/playwright.js +445 -68
  79. package/packages/datadog-instrumentations/src/protobufjs.js +16 -16
  80. package/packages/datadog-instrumentations/src/redis.js +20 -12
  81. package/packages/datadog-instrumentations/src/restify.js +2 -2
  82. package/packages/datadog-instrumentations/src/router.js +12 -12
  83. package/packages/datadog-instrumentations/src/stripe.js +12 -12
  84. package/packages/datadog-instrumentations/src/vitest.js +107 -26
  85. package/packages/datadog-instrumentations/src/winston.js +4 -4
  86. package/packages/datadog-instrumentations/src/ws.js +7 -7
  87. package/packages/datadog-plugin-apollo/src/gateway/request.js +1 -21
  88. package/packages/datadog-plugin-aws-sdk/src/base.js +70 -28
  89. package/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js +1 -1
  90. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +20 -13
  91. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +46 -36
  92. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +34 -23
  93. package/packages/datadog-plugin-aws-sdk/src/services/redshift.js +1 -1
  94. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -1
  95. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +14 -15
  96. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +74 -55
  97. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +20 -18
  98. package/packages/datadog-plugin-aws-sdk/src/util.js +22 -0
  99. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -6
  100. package/packages/datadog-plugin-couchbase/src/index.js +58 -52
  101. package/packages/datadog-plugin-cucumber/src/index.js +5 -0
  102. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +215 -26
  103. package/packages/datadog-plugin-cypress/src/support.js +13 -1
  104. package/packages/datadog-plugin-electron/src/index.js +17 -0
  105. package/packages/datadog-plugin-electron/src/ipc.js +143 -0
  106. package/packages/datadog-plugin-electron/src/net.js +82 -0
  107. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -5
  108. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +27 -18
  109. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +3 -1
  110. package/packages/datadog-plugin-graphql/src/execute.js +6 -28
  111. package/packages/datadog-plugin-graphql/src/resolve.js +30 -35
  112. package/packages/datadog-plugin-graphql/src/tools/signature.js +32 -7
  113. package/packages/datadog-plugin-graphql/src/tools/transforms.js +118 -100
  114. package/packages/datadog-plugin-graphql/src/utils.js +29 -0
  115. package/packages/datadog-plugin-grpc/src/client.js +6 -7
  116. package/packages/datadog-plugin-grpc/src/util.js +57 -22
  117. package/packages/datadog-plugin-http/src/client.js +3 -7
  118. package/packages/datadog-plugin-jest/src/index.js +92 -50
  119. package/packages/datadog-plugin-jest/src/util.js +1 -2
  120. package/packages/datadog-plugin-mocha/src/index.js +5 -0
  121. package/packages/datadog-plugin-mongodb-core/src/index.js +36 -69
  122. package/packages/datadog-plugin-mysql/src/index.js +1 -1
  123. package/packages/datadog-plugin-openai/src/services.js +2 -1
  124. package/packages/datadog-plugin-openai/src/tracing.js +12 -23
  125. package/packages/datadog-plugin-pg/src/index.js +3 -3
  126. package/packages/datadog-plugin-playwright/src/index.js +5 -1
  127. package/packages/datadog-plugin-redis/src/index.js +18 -23
  128. package/packages/datadog-plugin-vitest/src/index.js +8 -1
  129. package/packages/datadog-shimmer/src/shimmer.js +7 -1
  130. package/packages/dd-trace/src/aiguard/index.js +3 -1
  131. package/packages/dd-trace/src/aiguard/sdk.js +36 -30
  132. package/packages/dd-trace/src/aiguard/tags.js +20 -11
  133. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
  134. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +81 -81
  135. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  136. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  137. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +4 -4
  138. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -2
  139. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  140. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
  141. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +83 -48
  142. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -1
  143. package/packages/dd-trace/src/appsec/index.js +21 -24
  144. package/packages/dd-trace/src/appsec/reporter.js +3 -1
  145. package/packages/dd-trace/src/appsec/rule_manager.js +4 -2
  146. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +31 -16
  147. package/packages/dd-trace/src/azure_metadata.js +17 -6
  148. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +4 -4
  149. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  150. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +6 -4
  151. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +1 -1
  152. package/packages/dd-trace/src/config/defaults.js +3 -14
  153. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -1
  154. package/packages/dd-trace/src/config/git_properties.js +2 -2
  155. package/packages/dd-trace/src/config/helper.js +4 -0
  156. package/packages/dd-trace/src/config/index.js +2 -2
  157. package/packages/dd-trace/src/config/major-overrides.js +98 -0
  158. package/packages/dd-trace/src/config/parsers.js +7 -1
  159. package/packages/dd-trace/src/config/supported-configurations.json +51 -38
  160. package/packages/dd-trace/src/datastreams/checkpointer.js +2 -2
  161. package/packages/dd-trace/src/datastreams/index.js +2 -1
  162. package/packages/dd-trace/src/datastreams/manager.js +1 -1
  163. package/packages/dd-trace/src/datastreams/processor.js +3 -4
  164. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +2 -2
  165. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +1 -0
  166. package/packages/dd-trace/src/debugger/devtools_client/source-maps.js +1 -1
  167. package/packages/dd-trace/src/debugger/devtools_client/state.js +2 -1
  168. package/packages/dd-trace/src/debugger/index.js +7 -7
  169. package/packages/dd-trace/src/dogstatsd.js +2 -2
  170. package/packages/dd-trace/src/encode/0.4.js +748 -232
  171. package/packages/dd-trace/src/encode/0.5.js +47 -10
  172. package/packages/dd-trace/src/encode/agentless-json.js +1 -1
  173. package/packages/dd-trace/src/exporter.js +2 -0
  174. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  175. package/packages/dd-trace/src/exporters/agentless/index.js +3 -2
  176. package/packages/dd-trace/src/exporters/agentless/writer.js +2 -2
  177. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -1
  178. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  179. package/packages/dd-trace/src/exporters/electron/index.js +49 -0
  180. package/packages/dd-trace/src/external-logger/src/index.js +2 -1
  181. package/packages/dd-trace/src/git_metadata.js +10 -8
  182. package/packages/dd-trace/src/lambda/handler-paths.js +52 -0
  183. package/packages/dd-trace/src/lambda/index.js +62 -14
  184. package/packages/dd-trace/src/lambda/runtime/patch.js +21 -46
  185. package/packages/dd-trace/src/llmobs/index.js +13 -2
  186. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -2
  187. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +45 -15
  188. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +6 -3
  189. package/packages/dd-trace/src/llmobs/sdk.js +24 -26
  190. package/packages/dd-trace/src/llmobs/span_processor.js +25 -5
  191. package/packages/dd-trace/src/llmobs/util.js +1 -0
  192. package/packages/dd-trace/src/llmobs/writers/base.js +2 -1
  193. package/packages/dd-trace/src/msgpack/chunk.js +6 -3
  194. package/packages/dd-trace/src/openfeature/noop.js +40 -36
  195. package/packages/dd-trace/src/openfeature/writers/base.js +2 -1
  196. package/packages/dd-trace/src/openfeature/writers/exposures.js +33 -52
  197. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
  198. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +1 -2
  199. package/packages/dd-trace/src/opentelemetry/tracer.js +0 -22
  200. package/packages/dd-trace/src/opentracing/propagation/text_map.js +20 -9
  201. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +2 -11
  202. package/packages/dd-trace/src/payload-tagging/config/index.js +2 -2
  203. package/packages/dd-trace/src/plugins/ci_plugin.js +49 -4
  204. package/packages/dd-trace/src/plugins/database.js +54 -12
  205. package/packages/dd-trace/src/plugins/index.js +1 -0
  206. package/packages/dd-trace/src/plugins/plugin.js +2 -4
  207. package/packages/dd-trace/src/plugins/util/ci.js +9 -9
  208. package/packages/dd-trace/src/plugins/util/git-cache.js +23 -23
  209. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -2
  210. package/packages/dd-trace/src/plugins/util/test.js +56 -12
  211. package/packages/dd-trace/src/plugins/util/url.js +1 -3
  212. package/packages/dd-trace/src/plugins/util/user-provided-git.js +18 -16
  213. package/packages/dd-trace/src/plugins/util/web.js +5 -7
  214. package/packages/dd-trace/src/priority_sampler.js +1 -1
  215. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  216. package/packages/dd-trace/src/profiling/profilers/events.js +3 -23
  217. package/packages/dd-trace/src/profiling/profilers/wall.js +5 -6
  218. package/packages/dd-trace/src/profiling/ssi-heuristics.js +1 -1
  219. package/packages/dd-trace/src/rate_limiter.js +1 -1
  220. package/packages/dd-trace/src/remote_config/scheduler.js +1 -1
  221. package/packages/dd-trace/src/ritm.js +2 -1
  222. package/packages/dd-trace/src/runtime_metrics/index.js +2 -2
  223. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -8
  224. package/packages/dd-trace/src/scope.js +3 -10
  225. package/packages/dd-trace/src/serverless.js +6 -6
  226. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +27 -1
  227. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  228. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +24 -0
  229. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  230. package/packages/dd-trace/src/span_stats.js +1 -1
  231. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  232. package/packages/dd-trace/src/telemetry/endpoints.js +1 -1
  233. package/packages/dd-trace/src/telemetry/telemetry.js +2 -2
  234. package/packages/dd-trace/src/tracer.js +7 -7
  235. package/packages/dd-trace/src/lambda/runtime/ritm.js +0 -133
@@ -7,30 +7,222 @@ const { normalizeSpan } = require('./tags-processors')
7
7
 
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
9
9
 
10
- function formatSpan (span, config) {
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_ERROR = buildKey('error')
44
+ const KEY_START = buildKey('start')
45
+ const KEY_DURATION = buildKey('duration')
46
+ const KEY_SPAN_EVENTS = buildKey('span_events')
47
+ const KEY_META_STRUCT = buildKey('meta_struct')
48
+ const KEY_TRACE_ID_PREFIX = buildKeyWithPrefix('trace_id', 0xCF)
49
+ const KEY_SPAN_ID_PREFIX = buildKeyWithPrefix('span_id', 0xCF)
50
+ const KEY_PARENT_ID_PREFIX = buildKeyWithPrefix('parent_id', 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) {
11
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,121 @@ 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
+ bytes.set(KEY_ERROR)
348
+ this._encodeIntOrFloat(bytes, span.error)
349
+ bytes.set(KEY_START)
350
+ this._encodeIntOrFloat(bytes, span.start)
351
+ bytes.set(KEY_DURATION)
352
+ this._encodeIntOrFloat(bytes, span.duration)
353
+
354
+ this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta)
355
+ this.#encodeMetaEntries(bytes, KEY_METRICS_PREFIX, span.metrics)
102
356
 
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
357
  if (span.span_events) {
126
- this._encodeString(bytes, 'span_events')
127
- this._encodeObjectAsArray(bytes, span.span_events, new Set())
358
+ bytes.set(KEY_SPAN_EVENTS)
359
+ this.#encodeSpanEvents(bytes, span.span_events)
128
360
  }
129
361
  if (span.meta_struct) {
130
- this._encodeString(bytes, 'meta_struct')
131
- this._encodeMetaStruct(bytes, span.meta_struct)
362
+ bytes.set(KEY_META_STRUCT)
363
+ this.#encodeMetaStruct(bytes, span.meta_struct)
132
364
  }
133
365
  }
366
+
367
+ if (this._stringBytes.buffer !== stringBufferAtStart) {
368
+ this.#refreshStringCache()
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Rebuild the cached string subarrays in `_stringMap` against the current
374
+ * `_stringBytes.buffer`. After `MsgpackChunk.reserve()` resizes, the prior
375
+ * subarrays still reference the old `Buffer`'s `ArrayBuffer` and pin it
376
+ * until `_reset()` clears the map; for a payload that grows 2 → 4 → 8 MB
377
+ * that is up to 6 MB of avoidable peak memory. `Buffer.allocUnsafe(>= 2
378
+ * MB)` bypasses the small-allocation pool and starts at offset 0 in its
379
+ * `ArrayBuffer`, so the old subarray's `byteOffset` is the entry's start
380
+ * position in the new buffer.
381
+ */
382
+ #refreshStringCache () {
383
+ const newBuffer = this._stringBytes.buffer
384
+ const stringMap = this._stringMap
385
+ for (const key of Object.keys(stringMap)) {
386
+ const old = stringMap[key]
387
+ stringMap[key] = newBuffer.subarray(old.byteOffset, old.byteOffset + old.length)
388
+ }
134
389
  }
135
390
 
136
391
  _reset () {
@@ -144,123 +399,290 @@ class AgentEncoder {
144
399
  }
145
400
 
146
401
  _encodeBuffer (bytes, buffer) {
147
- this._msgpack.encodeBin(bytes, buffer)
402
+ this.#msgpack.encodeBin(bytes, buffer)
148
403
  }
149
404
 
150
405
  _encodeBool (bytes, value) {
151
- this._msgpack.encodeBoolean(bytes, value)
406
+ this.#msgpack.encodeBoolean(bytes, value)
152
407
  }
153
408
 
154
409
  _encodeArrayPrefix (bytes, value) {
155
- this._msgpack.encodeArrayPrefix(bytes, value)
410
+ this.#msgpack.encodeArrayPrefix(bytes, value)
156
411
  }
157
412
 
158
413
  _encodeMapPrefix (bytes, keysLength) {
159
- this._msgpack.encodeMapPrefix(bytes, keysLength)
414
+ this.#msgpack.encodeMapPrefix(bytes, keysLength)
160
415
  }
161
416
 
162
417
  _encodeByte (bytes, value) {
163
- this._msgpack.encodeByte(bytes, value)
418
+ this.#msgpack.encodeByte(bytes, value)
164
419
  }
165
420
 
166
421
  // TODO: Use BigInt instead.
167
- _encodeId (bytes, id) {
422
+ _encodeId (bytes, identifier) {
423
+ const idBuffer = identifier.toBuffer()
424
+ const start = idBuffer.length - 8
168
425
  const offset = bytes.length
169
426
 
170
427
  bytes.reserve(9)
171
428
 
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]
429
+ const target = bytes.buffer
430
+ target[offset] = 0xCF
431
+ target[offset + 1] = idBuffer[start]
432
+ target[offset + 2] = idBuffer[start + 1]
433
+ target[offset + 3] = idBuffer[start + 2]
434
+ target[offset + 4] = idBuffer[start + 3]
435
+ target[offset + 5] = idBuffer[start + 4]
436
+ target[offset + 6] = idBuffer[start + 5]
437
+ target[offset + 7] = idBuffer[start + 6]
438
+ target[offset + 8] = idBuffer[start + 7]
183
439
  }
184
440
 
185
441
  _encodeNumber (bytes, value) {
186
- this._msgpack.encodeNumber(bytes, value)
442
+ this.#msgpack.encodeNumber(bytes, value)
187
443
  }
188
444
 
189
445
  _encodeInteger (bytes, value) {
190
- this._msgpack.encodeInteger(bytes, value)
446
+ this.#msgpack.encodeInteger(bytes, value)
191
447
  }
192
448
 
193
449
  _encodeLong (bytes, value) {
194
- this._msgpack.encodeLong(bytes, value)
450
+ this.#msgpack.encodeLong(bytes, value)
195
451
  }
196
452
 
453
+ // Single pass: reserve the count slot, encode entries while counting, patch the count.
454
+ // Subclasses (0.5, CI visibility encoders) inherit this; the wire stays on float64
455
+ // for numeric values to keep their established trace / events intake unchanged.
197
456
  _encodeMap (bytes, value) {
198
- const keys = Object.keys(value)
199
- const validKeys = keys.filter(key => typeof value[key] === 'string' || typeof value[key] === 'number')
457
+ const offset = bytes.length
458
+ bytes.reserve(5)
459
+ bytes.buffer[offset] = 0xDF
460
+
461
+ let count = 0
462
+ for (const key of Object.keys(value)) {
463
+ const entryValue = value[key]
464
+ if (typeof entryValue === 'string') {
465
+ this._encodeString(bytes, key)
466
+ this._encodeString(bytes, entryValue)
467
+ count++
468
+ } else if (typeof entryValue === 'number') {
469
+ this._encodeString(bytes, key)
470
+ this.#encodeFloat(bytes, entryValue)
471
+ count++
472
+ }
473
+ }
200
474
 
201
- this._encodeMapPrefix(bytes, validKeys.length)
475
+ const target = bytes.buffer
476
+ target[offset + 1] = count >>> 24
477
+ target[offset + 2] = count >>> 16
478
+ target[offset + 3] = count >>> 8
479
+ target[offset + 4] = count
480
+ }
202
481
 
203
- for (const key of validKeys) {
204
- this._encodeString(bytes, key)
205
- this._encodeValue(bytes, value[key])
482
+ _encodeString (bytes, value = '') {
483
+ const entry = this._stringMap[value] ?? this._cacheString(value)
484
+ const length = entry.length
485
+ const offset = bytes.length
486
+ bytes.reserve(length)
487
+ bytes.buffer.set(entry, offset)
488
+ }
489
+
490
+ _cacheString (value) {
491
+ let entry = this._stringMap[value]
492
+ if (entry === undefined) {
493
+ this._stringCount++
494
+ const start = this._stringBytes.length
495
+ const written = this._stringBytes.write(value)
496
+ entry = this._stringBytes.buffer.subarray(start, start + written)
497
+ this._stringMap[value] = entry
498
+ }
499
+ return entry
500
+ }
501
+
502
+ _writeArrayPrefix (buffer, offset, count) {
503
+ buffer[offset++] = 0xDD
504
+ buffer.writeUInt32BE(count, offset)
505
+
506
+ return offset + 4
507
+ }
508
+
509
+ _writeTraces (buffer, offset = 0) {
510
+ offset = this._writeArrayPrefix(buffer, offset, this._traceCount)
511
+ offset += this._traceBytes.buffer.copy(buffer, offset, 0, this._traceBytes.length)
512
+
513
+ return offset
514
+ }
515
+
516
+ /**
517
+ * Fast path for `span.meta` / `span.metrics`. Inlines the string cache so
518
+ * each entry is one reserve (not two) and skips the polymorphic dispatch.
519
+ *
520
+ * @param {MsgpackChunk} bytes
521
+ * @param {Buffer} keyPrefix Precomputed `[key, 0xDF]`.
522
+ * @param {Record<string, unknown>} value
523
+ */
524
+ #encodeMetaEntries (bytes, keyPrefix, value) {
525
+ const keyPrefixLen = keyPrefix.length
526
+ const headerOffset = bytes.length
527
+ bytes.reserve(keyPrefixLen + 4)
528
+ bytes.buffer.set(keyPrefix, headerOffset)
529
+ const countOffset = headerOffset + keyPrefixLen
530
+
531
+ const stringMap = this._stringMap
532
+ let count = 0
533
+
534
+ for (const key of Object.keys(value)) {
535
+ const entryValue = value[key]
536
+ if (typeof entryValue !== 'string' && typeof entryValue !== 'number') continue
537
+
538
+ const keyEntry = stringMap[key] ?? this._cacheString(key)
539
+ const keyEntryLen = keyEntry.length
540
+ const writeOffset = bytes.length
541
+
542
+ if (typeof entryValue === 'string') {
543
+ const valueEntry = stringMap[entryValue] ?? this._cacheString(entryValue)
544
+ const valueEntryLen = valueEntry.length
545
+ bytes.reserve(keyEntryLen + valueEntryLen)
546
+ const target = bytes.buffer
547
+ target.set(keyEntry, writeOffset)
548
+ target.set(valueEntry, writeOffset + keyEntryLen)
549
+ } else {
550
+ bytes.reserve(keyEntryLen)
551
+ bytes.buffer.set(keyEntry, writeOffset)
552
+ this._encodeIntOrFloat(bytes, entryValue)
553
+ }
554
+ count++
555
+ }
556
+
557
+ const target = bytes.buffer
558
+ target[countOffset] = count >>> 24
559
+ target[countOffset + 1] = count >>> 16
560
+ target[countOffset + 2] = count >>> 8
561
+ target[countOffset + 3] = count
562
+ }
563
+
564
+ /**
565
+ * Write `[keyPrefix, 8-byte uint64 id]` into `target` at `offset` and
566
+ * return the new cursor. Caller is responsible for having reserved enough
567
+ * room — this is the no-reserve variant used inside `_encode`'s combined
568
+ * fixed-fields block.
569
+ *
570
+ * @param {Uint8Array} target
571
+ * @param {number} offset
572
+ * @param {Buffer} keyPrefix Precomputed `[key, 0xCF]`.
573
+ * @param {{ toBuffer: () => Uint8Array | number[] }} identifier
574
+ * @returns {number}
575
+ */
576
+ #writeIdAt (target, offset, keyPrefix, identifier) {
577
+ target.set(keyPrefix, offset)
578
+ offset += keyPrefix.length
579
+ const idBuffer = identifier.toBuffer()
580
+ const start = idBuffer.length - 8
581
+ target[offset] = idBuffer[start]
582
+ target[offset + 1] = idBuffer[start + 1]
583
+ target[offset + 2] = idBuffer[start + 2]
584
+ target[offset + 3] = idBuffer[start + 3]
585
+ target[offset + 4] = idBuffer[start + 4]
586
+ target[offset + 5] = idBuffer[start + 5]
587
+ target[offset + 6] = idBuffer[start + 6]
588
+ target[offset + 7] = idBuffer[start + 7]
589
+ return offset + 8
590
+ }
591
+
592
+ /**
593
+ * Emit `value` as the smallest valid msgpack number encoding: compact
594
+ * unsigned/signed int when integer, float64 otherwise. Unlike
595
+ * `MsgpackEncoder#encodeNumber`, NaN keeps its float64 bits instead of
596
+ * coercing to fixint 0.
597
+ *
598
+ * Underscore-protected so the 0.5 subclass can call it from its own
599
+ * `_encode` / `_encodeMap` overrides.
600
+ *
601
+ * @param {MsgpackChunk} bytes
602
+ * @param {number} value
603
+ */
604
+ _encodeIntOrFloat (bytes, value) {
605
+ // Fast path: positive fixint (0..127). `value === (value & 0x7F)` is true
606
+ // iff `value` is an exact integer in that range — covers `error: 0/1`,
607
+ // priority flags, attribute counts, HTTP status codes mapped to numbers,
608
+ // and most small metrics. NaN, ±Infinity, negatives, and any non-integer
609
+ // float fall through.
610
+ if (value === (value & 0x7F)) {
611
+ const offset = bytes.length
612
+ bytes.reserve(1)
613
+ bytes.buffer[offset] = value
614
+ return
615
+ }
616
+ if (Number.isInteger(value)) {
617
+ if (value >= 0) {
618
+ this.#msgpack.encodeUnsigned(bytes, value)
619
+ } else {
620
+ this.#msgpack.encodeSigned(bytes, value)
621
+ }
622
+ } else {
623
+ this.#encodeFloat(bytes, value)
206
624
  }
207
625
  }
208
626
 
209
- _encodeValue (bytes, value) {
627
+ /**
628
+ * @param {MsgpackChunk} bytes
629
+ * @param {string | number | boolean} value
630
+ */
631
+ #encodeValue (bytes, value) {
210
632
  switch (typeof value) {
211
633
  case 'string':
212
634
  this._encodeString(bytes, value)
213
635
  break
214
636
  case 'number':
215
- this._encodeFloat(bytes, value)
637
+ this.#encodeFloat(bytes, value)
216
638
  break
217
639
  case 'boolean':
218
640
  this._encodeBool(bytes, value)
219
641
  break
220
- default:
221
- // should not happen
222
642
  }
223
643
  }
224
644
 
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)
645
+ #encodeFloat (bytes, value) {
646
+ this.#msgpack.encodeFloat(bytes, value)
235
647
  }
236
648
 
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)
649
+ #encodeMetaStruct (bytes, value) {
650
+ if (Array.isArray(value)) {
651
+ this._encodeMapPrefix(bytes, 0)
652
+ return
653
+ }
247
654
 
248
- for (const key of validKeys) {
249
- const v = value[key]
250
- this._encodeString(bytes, key)
251
- this._encodeObjectAsByteArray(bytes, v)
655
+ const offset = bytes.length
656
+ bytes.reserve(5)
657
+ bytes.buffer[offset] = 0xDF
658
+
659
+ let count = 0
660
+ for (const key of Object.keys(value)) {
661
+ const entryValue = value[key]
662
+ if (typeof entryValue === 'string' || typeof entryValue === 'number' ||
663
+ (entryValue !== null && typeof entryValue === 'object')) {
664
+ this._encodeString(bytes, key)
665
+ this.#encodeObjectAsByteArray(bytes, entryValue)
666
+ count++
667
+ }
252
668
  }
669
+
670
+ const target = bytes.buffer
671
+ target[offset + 1] = count >>> 24
672
+ target[offset + 2] = count >>> 16
673
+ target[offset + 3] = count >>> 8
674
+ target[offset + 4] = count
253
675
  }
254
676
 
255
- _encodeObjectAsByteArray (bytes, value) {
677
+ #encodeObjectAsByteArray (bytes, value) {
256
678
  const prefixLength = 5
257
679
  const offset = bytes.length
258
680
 
259
681
  bytes.reserve(prefixLength)
260
682
 
261
- this._encodeObject(bytes, value)
683
+ this.#encodeObject(bytes, value)
262
684
 
263
- // we should do it after encoding the object to know the real length
685
+ // The byte length isn't known until the inner object has been encoded.
264
686
  const length = bytes.length - offset - prefixLength
265
687
  bytes.buffer[offset] = 0xC6
266
688
  bytes.buffer[offset + 1] = length >> 24
@@ -269,157 +691,251 @@ class AgentEncoder {
269
691
  bytes.buffer[offset + 4] = length
270
692
  }
271
693
 
272
- _encodeObject (bytes, value, circularReferencesDetector = new Set()) {
694
+ #encodeObject (bytes, value, circularReferencesDetector = new Set()) {
273
695
  circularReferencesDetector.add(value)
274
696
  if (Array.isArray(value)) {
275
- this._encodeObjectAsArray(bytes, value, circularReferencesDetector)
697
+ this.#encodeObjectAsArray(bytes, value, circularReferencesDetector)
276
698
  } else if (value !== null && typeof value === 'object') {
277
- this._encodeObjectAsMap(bytes, value, circularReferencesDetector)
699
+ this.#encodeObjectAsMap(bytes, value, circularReferencesDetector)
278
700
  } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
279
- this._encodeValue(bytes, value)
701
+ this.#encodeValue(bytes, value)
280
702
  }
281
703
  }
282
704
 
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)
705
+ #encodeObjectAsMap (bytes, value, circularReferencesDetector) {
706
+ const offset = bytes.length
707
+ bytes.reserve(5)
708
+ bytes.buffer[offset] = 0xDF
709
+
710
+ let count = 0
711
+ for (const key of Object.keys(value)) {
712
+ const entryValue = value[key]
713
+ if (typeof entryValue === 'string' || typeof entryValue === 'number' || typeof entryValue === 'boolean' ||
714
+ (entryValue !== null && typeof entryValue === 'object' &&
715
+ !circularReferencesDetector.has(entryValue))) {
716
+ this._encodeString(bytes, key)
717
+ this.#encodeObject(bytes, entryValue, circularReferencesDetector)
718
+ count++
719
+ }
298
720
  }
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
721
 
309
- for (const item of validValue) {
310
- this._encodeObject(bytes, item, circularReferencesDetector)
311
- }
722
+ const target = bytes.buffer
723
+ target[offset + 1] = count >>> 24
724
+ target[offset + 2] = count >>> 16
725
+ target[offset + 3] = count >>> 8
726
+ target[offset + 4] = count
312
727
  }
313
728
 
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),
729
+ #encodeObjectAsArray (bytes, value, circularReferencesDetector) {
730
+ const offset = bytes.length
731
+ bytes.reserve(5)
732
+ bytes.buffer[offset] = 0xDD
733
+
734
+ let count = 0
735
+ for (const item of value) {
736
+ if (typeof item === 'string' || typeof item === 'number' ||
737
+ (item !== null && typeof item === 'object' && !circularReferencesDetector.has(item))) {
738
+ this.#encodeObject(bytes, item, circularReferencesDetector)
739
+ count++
320
740
  }
321
741
  }
322
- }
323
742
 
324
- _writeArrayPrefix (buffer, offset, count) {
325
- buffer[offset++] = 0xDD
326
- buffer.writeUInt32BE(count, offset)
743
+ const target = bytes.buffer
744
+ target[offset + 1] = count >>> 24
745
+ target[offset + 2] = count >>> 16
746
+ target[offset + 3] = count >>> 8
747
+ target[offset + 4] = count
748
+ }
749
+
750
+ /**
751
+ * Specialized encoder for `span.span_events`. Walks the events directly,
752
+ * emitting OTel attribute typed wrappers inline from the raw attribute
753
+ * values — no `formatSpanEvents` pre-pass and no recursive generic walk.
754
+ *
755
+ * @param {MsgpackChunk} bytes
756
+ * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
757
+ */
758
+ #encodeSpanEvents (bytes, spanEvents) {
759
+ const offset = bytes.length
760
+ bytes.reserve(5)
761
+ bytes.buffer[offset] = 0xDD
327
762
 
328
- return offset + 4
329
- }
763
+ let arrayCount = 0
764
+ for (const event of spanEvents) {
765
+ // `addEvent` and the OTel bridge do not type-check `name`, and a
766
+ // non-string would throw downstream in `Buffer.byteLength`. Drop the
767
+ // bad event silently so the rest of the trace still encodes.
768
+ if (event === null || typeof event !== 'object' || typeof event.name !== 'string') continue
330
769
 
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)
770
+ const eventHeaderOffset = bytes.length
771
+ bytes.reserve(1)
772
+ bytes.buffer[eventHeaderOffset] = 0x82
334
773
 
335
- return offset
336
- }
337
- }
774
+ bytes.set(KEY_NAME)
775
+ this._encodeString(bytes, event.name)
776
+ bytes.set(KEY_EVENT_TIME)
777
+ this.#encodeFloat(bytes, event.time_unix_nano)
338
778
 
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
779
+ const attributes = event.attributes
780
+ if (attributes !== null && typeof attributes === 'object') {
781
+ this.#encodeAttributesIfAny(bytes, attributes, eventHeaderOffset)
362
782
  }
783
+ arrayCount++
363
784
  }
364
- }
365
- }
366
785
 
367
- function convertSpanEventAttributeValues (key, value, depth = 0) {
368
- if (typeof value === 'string') {
369
- return {
370
- type: 0,
371
- string_value: value,
786
+ const target = bytes.buffer
787
+ target[offset + 1] = arrayCount >>> 24
788
+ target[offset + 2] = arrayCount >>> 16
789
+ target[offset + 3] = arrayCount >>> 8
790
+ target[offset + 4] = arrayCount
791
+ }
792
+
793
+ /**
794
+ * Optimistically emits the `'attributes'` slot for an event. If every entry
795
+ * filters out, the partial output is rewound and the event's map header
796
+ * stays at 2 entries.
797
+ *
798
+ * @param {MsgpackChunk} bytes
799
+ * @param {Record<string, unknown>} attributes
800
+ * @param {number} eventHeaderOffset
801
+ */
802
+ #encodeAttributesIfAny (bytes, attributes, eventHeaderOffset) {
803
+ const sectionStart = bytes.length
804
+
805
+ bytes.set(KEY_EVENT_ATTRIBUTES)
806
+ const countOffset = bytes.length
807
+ bytes.reserve(5)
808
+ bytes.buffer[countOffset] = 0xDF
809
+
810
+ let count = 0
811
+ for (const key of Object.keys(attributes)) {
812
+ if (this.#emitAttribute(bytes, key, attributes[key])) count++
372
813
  }
373
- }
374
814
 
375
- if (typeof value === 'boolean') {
376
- return {
377
- type: 1,
378
- bool_value: value,
815
+ if (count === 0) {
816
+ bytes.length = sectionStart
817
+ return
379
818
  }
380
- }
381
819
 
382
- if (typeof value === 'number') {
383
- if (Number.isInteger(value)) {
384
- return {
385
- type: 2,
386
- int_value: value,
387
- }
820
+ const target = bytes.buffer
821
+ target[countOffset + 1] = count >>> 24
822
+ target[countOffset + 2] = count >>> 16
823
+ target[countOffset + 3] = count >>> 8
824
+ target[countOffset + 4] = count
825
+ bytes.buffer[eventHeaderOffset] = 0x83
826
+ }
827
+
828
+ /**
829
+ * Emit `<key, typed_wrapper>` for a single attribute. Returns true on a
830
+ * supported type, false (with a memoized debug log) otherwise.
831
+ *
832
+ * @param {MsgpackChunk} bytes
833
+ * @param {string} key
834
+ * @param {unknown} value
835
+ * @returns {boolean}
836
+ */
837
+ #emitAttribute (bytes, key, value) {
838
+ if (typeof value === 'string') {
839
+ this._encodeString(bytes, key)
840
+ bytes.set(ATTR_PREFIX_STRING)
841
+ this._encodeString(bytes, value)
842
+ return true
388
843
  }
389
- return {
390
- type: 3,
391
- double_value: value,
844
+ if (typeof value === 'number') {
845
+ this._encodeString(bytes, key)
846
+ bytes.set(Number.isInteger(value) ? ATTR_PREFIX_INT : ATTR_PREFIX_DOUBLE)
847
+ this._encodeIntOrFloat(bytes, value)
848
+ return true
849
+ }
850
+ if (typeof value === 'boolean') {
851
+ this._encodeString(bytes, key)
852
+ bytes.set(value ? ATTR_PAYLOAD_BOOL_TRUE : ATTR_PAYLOAD_BOOL_FALSE)
853
+ return true
854
+ }
855
+ if (Array.isArray(value)) {
856
+ return this.#emitArrayAttribute(bytes, key, value)
857
+ }
858
+ memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' +
859
+ `${key}: with value: ${typeof value}. Skipping encoding of pair.`
860
+ )
861
+ return false
862
+ }
863
+
864
+ /**
865
+ * Emit `<key, { type: 4, array_value: { values: [...] } }>` from a raw
866
+ * array of primitives. Filters nested arrays / unsupported items; if no
867
+ * items remain the whole entry is rewound.
868
+ *
869
+ * @param {MsgpackChunk} bytes
870
+ * @param {string} key
871
+ * @param {Array<unknown>} array
872
+ * @returns {boolean}
873
+ */
874
+ #emitArrayAttribute (bytes, key, array) {
875
+ const sectionStart = bytes.length
876
+
877
+ this._encodeString(bytes, key)
878
+ bytes.set(ATTR_PREFIX_ARRAY)
879
+ const arrayCountOffset = bytes.length
880
+ bytes.reserve(4)
881
+
882
+ let count = 0
883
+ for (const item of array) {
884
+ if (this.#emitArrayItem(bytes, key, item)) count++
392
885
  }
393
- }
394
886
 
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
- }
887
+ if (count === 0) {
888
+ bytes.length = sectionStart
889
+ return false
890
+ }
404
891
 
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 {
892
+ const target = bytes.buffer
893
+ target[arrayCountOffset] = count >>> 24
894
+ target[arrayCountOffset + 1] = count >>> 16
895
+ target[arrayCountOffset + 2] = count >>> 8
896
+ target[arrayCountOffset + 3] = count
897
+ return true
898
+ }
899
+
900
+ /**
901
+ * Emit a single typed wrapper inside an `array_value.values` array. No
902
+ * recursion: nested arrays are dropped with a memoized debug log.
903
+ *
904
+ * @param {MsgpackChunk} bytes
905
+ * @param {string} key
906
+ * @param {unknown} value
907
+ * @returns {boolean}
908
+ */
909
+ #emitArrayItem (bytes, key, value) {
910
+ if (typeof value === 'string') {
911
+ bytes.set(ATTR_PREFIX_STRING)
912
+ this._encodeString(bytes, value)
913
+ return true
914
+ }
915
+ if (typeof value === 'number') {
916
+ bytes.set(Number.isInteger(value) ? ATTR_PREFIX_INT : ATTR_PREFIX_DOUBLE)
917
+ this._encodeIntOrFloat(bytes, value)
918
+ return true
919
+ }
920
+ if (typeof value === 'boolean') {
921
+ bytes.set(value ? ATTR_PAYLOAD_BOOL_TRUE : ATTR_PAYLOAD_BOOL_FALSE)
922
+ return true
923
+ }
924
+ if (Array.isArray(value)) {
414
925
  memoizedLogDebug(key, 'Encountered nested array data type for span event v0.4 encoding. ' +
415
926
  `Skipping encoding key: ${key}: with value: ${typeof value}.`
416
927
  )
417
928
  }
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
- )
929
+ return false
930
+ }
931
+ }
932
+
933
+ const seenKeys = new Set()
934
+ function memoizedLogDebug (key, message) {
935
+ if (!seenKeys.has(key)) {
936
+ seenKeys.add(key)
937
+ log.debug(message)
422
938
  }
423
939
  }
424
940
 
425
- module.exports = { AgentEncoder }
941
+ module.exports = { AgentEncoder, stringifySpanEvents }