dd-trace 5.121.0 → 5.122.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 (137) hide show
  1. package/README.md +0 -17
  2. package/index.d.ts +108 -3
  3. package/initialize.mjs +28 -17
  4. package/openfeature.js +2 -2
  5. package/package.json +11 -8
  6. package/packages/datadog-esbuild/index.js +0 -34
  7. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  9. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  10. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  11. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  12. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  13. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  14. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  15. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  16. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  17. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  18. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +9 -4
  19. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  20. package/packages/datadog-instrumentations/src/jest.js +25 -17
  21. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  22. package/packages/datadog-instrumentations/src/mocha/main.js +539 -55
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +130 -17
  24. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  25. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  26. package/packages/datadog-instrumentations/src/router.js +2 -0
  27. package/packages/datadog-instrumentations/src/vitest-main.js +11 -3
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  30. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -8
  31. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  32. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  33. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  34. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  35. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  36. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +81 -29
  38. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  39. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  40. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  41. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  42. package/packages/datadog-plugin-electron/src/index.js +2 -0
  43. package/packages/datadog-plugin-fs/src/index.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +6 -0
  45. package/packages/datadog-plugin-mocha/src/index.js +56 -1
  46. package/packages/datadog-plugin-nats/src/index.js +1 -1
  47. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  48. package/packages/datadog-plugin-openai-agents/src/integration.js +1 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  50. package/packages/datadog-plugin-vitest/src/index.js +6 -0
  51. package/packages/datadog-webpack/index.js +0 -11
  52. package/packages/dd-trace/index.js +0 -1
  53. package/packages/dd-trace/src/agent/info.js +6 -5
  54. package/packages/dd-trace/src/aiguard/client.js +100 -1
  55. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  56. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  57. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  58. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  59. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  60. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  61. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  62. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  63. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  64. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  65. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  66. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  67. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  68. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  69. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  70. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  71. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  72. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  73. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  74. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  75. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  76. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  77. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  78. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  79. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  80. package/packages/dd-trace/src/config/index.js +13 -2
  81. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  82. package/packages/dd-trace/src/config/supported-configurations.json +14 -0
  83. package/packages/dd-trace/src/datastreams/index.js +2 -0
  84. package/packages/dd-trace/src/datastreams/manager.js +9 -2
  85. package/packages/dd-trace/src/datastreams/processor.js +15 -13
  86. package/packages/dd-trace/src/datastreams/size.js +5 -0
  87. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  88. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  89. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  90. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  91. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  92. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  93. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  94. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  95. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  96. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  97. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  98. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  99. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  100. package/packages/dd-trace/src/noop/proxy.js +3 -4
  101. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  102. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  103. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  104. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  105. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  106. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  107. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  108. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  109. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  110. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  111. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  112. package/packages/dd-trace/src/plugin_manager.js +3 -3
  113. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  114. package/packages/dd-trace/src/plugins/index.js +1 -0
  115. package/packages/dd-trace/src/plugins/util/jest.js +4 -3
  116. package/packages/dd-trace/src/plugins/util/test.js +9 -9
  117. package/packages/dd-trace/src/plugins/util/web.js +3 -0
  118. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  119. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  120. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  121. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  122. package/packages/dd-trace/src/proxy.js +38 -29
  123. package/packages/dd-trace/src/ritm.js +9 -2
  124. package/packages/dd-trace/src/serverless.js +37 -0
  125. package/packages/dd-trace/src/span_format.js +1 -0
  126. package/packages/dd-trace/src/span_stats.js +30 -3
  127. package/packages/dd-trace/src/storage-channels.js +86 -0
  128. package/packages/dd-trace/src/tracer.js +9 -2
  129. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  130. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  131. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  133. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  134. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  135. package/packages/dd-trace/src/feature-registry.js +0 -29
  136. package/packages/dd-trace/src/openfeature/register.js +0 -35
  137. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -1,35 +1,90 @@
1
1
  'use strict'
2
+
3
+ const { DsmPathwayCodec, PATHWAY_FIELD_BYTES } = require('../../../dd-trace/src/datastreams')
2
4
  const log = require('../../../dd-trace/src/log')
3
5
  const BaseAwsSdkPlugin = require('../base')
4
6
 
7
+ const DEFAULT_EVENT_BUS = 'default'
8
+ const DEFAULT_DETAIL_TYPE = 'unknown'
5
9
  // EventBridge enforces this limit over the whole PutEvents request (the sum of
6
- // every entry), not over a single entry. 1 MiB == 1,048,576 bytes.
10
+ // every entry), not over a single entry.
7
11
  const MAX_PUT_EVENTS_BYTES = 1024 * 1024
12
+ // Sizing runs before any checkpoint exists, so entries that carry nothing but a pathway are
13
+ // measured against this stand-in.
14
+ const PATHWAY_SIZE_PROBE = { hash: Buffer.alloc(8), pathwayStartNs: 0, edgeStartNs: 0 }
15
+
16
+ /**
17
+ * @typedef {object} PutEventsRequestEntry
18
+ * @property {string} [Detail]
19
+ * @property {string} [DetailType]
20
+ * @property {string} [EventBusName]
21
+ * @property {string[]} [Resources]
22
+ * @property {string} [Source]
23
+ * @property {Date} [Time]
24
+ * @property {string} [TraceHeader]
25
+ */
26
+
27
+ /**
28
+ * Size a single entry the way DSM reports payloads across tracers: the UTF-8 byte length of every
29
+ * string field the caller supplied. `Time` is excluded because it is not part of the message the
30
+ * consumer receives, which is also why this differs from `putEventEntrySize`.
31
+ *
32
+ * @param {PutEventsRequestEntry} entry whose `Detail` the caller has already checked is a string.
33
+ * @returns {number}
34
+ */
35
+ function dsmPayloadSize (entry) {
36
+ let size = Buffer.byteLength(entry.Detail)
37
+ if (typeof entry.DetailType === 'string') size += Buffer.byteLength(entry.DetailType)
38
+ if (typeof entry.EventBusName === 'string') size += Buffer.byteLength(entry.EventBusName)
39
+ if (typeof entry.Source === 'string') size += Buffer.byteLength(entry.Source)
40
+ if (typeof entry.TraceHeader === 'string') size += Buffer.byteLength(entry.TraceHeader)
41
+ if (Array.isArray(entry.Resources)) {
42
+ for (const resource of entry.Resources) {
43
+ if (typeof resource === 'string') size += Buffer.byteLength(resource)
44
+ }
45
+ }
46
+ return size
47
+ }
8
48
 
9
49
  /**
10
50
  * Size a single `PutEventsRequestEntry` the way EventBridge does server-side:
11
51
  * the UTF-8 byte length of `Source`, `DetailType`, `Detail`, and each
12
52
  * `Resources` ARN, plus a flat 14 bytes when `Time` is set.
13
53
  *
14
- * @param {object} entry a single PutEvents request entry
54
+ * @param {PutEventsRequestEntry} entry
15
55
  * @param {string} [detail] overrides `entry.Detail`, used to size the entry as
16
56
  * it would be sent with the injected `_datadog` context
17
- * @returns {number}
18
57
  * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevents.html
19
58
  */
20
- function putEventEntrySize (entry, detail = entry.Detail) {
59
+ function putEventEntrySize (entry, detail) {
60
+ if (entry == null) return 0
61
+
62
+ detail ??= entry.Detail
21
63
  let size = entry.Time == null ? 0 : 14
22
- if (entry.Source != null) size += Buffer.byteLength(entry.Source)
23
- if (entry.DetailType != null) size += Buffer.byteLength(entry.DetailType)
24
- if (detail != null) size += Buffer.byteLength(detail)
25
- if (entry.Resources != null) {
64
+ if (typeof entry.Source === 'string') size += Buffer.byteLength(entry.Source)
65
+ if (typeof entry.DetailType === 'string') size += Buffer.byteLength(entry.DetailType)
66
+ if (typeof detail === 'string') size += Buffer.byteLength(detail)
67
+ if (Array.isArray(entry.Resources)) {
26
68
  for (const resource of entry.Resources) {
27
- if (resource != null) size += Buffer.byteLength(resource)
69
+ if (typeof resource === 'string') size += Buffer.byteLength(resource)
28
70
  }
29
71
  }
30
72
  return size
31
73
  }
32
74
 
75
+ /**
76
+ * @param {string} detail
77
+ * @param {Record<string, string>} carrier
78
+ * @returns {string|undefined} `undefined` when `detail` is not a JSON object
79
+ */
80
+ function injectDetail (detail, carrier) {
81
+ try {
82
+ return BaseAwsSdkPlugin.injectFieldIntoJsonObject(detail, '_datadog', carrier)
83
+ } catch (error) {
84
+ log.error('EventBridge error injecting request', error)
85
+ }
86
+ }
87
+
33
88
  class EventBridge extends BaseAwsSdkPlugin {
34
89
  static id = 'eventbridge'
35
90
  static isPayloadReporter = true
@@ -46,44 +101,31 @@ class EventBridge extends BaseAwsSdkPlugin {
46
101
  }
47
102
 
48
103
  /**
49
- * requestInject
50
- * @param {import('../../../..').Span} span
51
- * @param {object} request
104
+ * The context goes into `Detail` because EventBridge reserves an entry's `TraceHeader` for X-Ray.
52
105
  *
53
- * Docs: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEventsRequestEntry.html
54
- * We cannot use the traceHeader field as that's reserved for X-Ray.
55
- * Detail must be a valid JSON string
56
- * Max PutEvents request size is 1mb, summed over all entries
57
- * (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevent-size.html)
106
+ * @param {import('../../../..').Span} span
107
+ * @param {{ operation: string, params?: { Entries?: PutEventsRequestEntry[] } }} request
108
+ * @see https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEventsRequestEntry.html
109
+ * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevent-size.html
58
110
  */
59
111
  requestInject (span, request) {
60
- const operation = request.operation
61
- if (operation === 'putEvents' &&
62
- request.params &&
63
- request.params.Entries &&
64
- request.params.Entries.length > 0 &&
65
- request.params.Entries[0].Detail) {
66
- const entries = request.params.Entries
67
- const injected = {}
68
- this.tracer.inject(span, 'text_map', injected)
69
-
70
- // Only `injectFieldIntoJsonObject` can throw (the slow path
71
- // `JSON.parse` for non-`{...}` payloads). Tighten the catch around
72
- // it so the rest of the body stays in V8's optimisable surface.
73
- let finalData
74
- try {
75
- finalData = BaseAwsSdkPlugin.injectFieldIntoJsonObject(entries[0].Detail, '_datadog', injected)
76
- } catch (error) {
77
- log.error('EventBridge error injecting request', error)
78
- return
79
- }
112
+ const { operation, params } = request
113
+ if (operation !== 'putEvents' || !params?.Entries?.length) return
114
+
115
+ const entries = params.Entries
116
+ const dsmEnabled = this.config.dsmEnabled
117
+ const batchPropagationEnabled = this.config.batchPropagationEnabled
118
+ const traceCarrier = this.tracer.inject(span, 'text_map')
119
+ if (traceCarrier === undefined && !dsmEnabled) return
80
120
 
81
- // EventBridge applies the 1 MiB cap to the whole request, so size every
82
- // entry as it would be sent (the first with `_datadog` injected) and skip
83
- // rather than tip a request AWS would otherwise accept over the limit. The
84
- // running total only needs to clear the cap, so stop summing the moment it
85
- // does instead of byte-counting the rest of a batch we already know is over.
86
- let requestSize = putEventEntrySize(entries[0], finalData)
121
+ // The default configuration injects the first entry alone and records no checkpoint, so it
122
+ // needs none of the deferred-write bookkeeping the paths below depend on.
123
+ if (!dsmEnabled && !batchPropagationEnabled) {
124
+ const entry = entries[0]
125
+ if (typeof entry?.Detail !== 'string') return
126
+ const detail = injectDetail(entry.Detail, traceCarrier)
127
+ if (detail === undefined) return
128
+ let requestSize = putEventEntrySize(entry, detail)
87
129
  for (let i = 1; requestSize < MAX_PUT_EVENTS_BYTES && i < entries.length; i++) {
88
130
  requestSize += putEventEntrySize(entries[i])
89
131
  }
@@ -91,8 +133,85 @@ class EventBridge extends BaseAwsSdkPlugin {
91
133
  log.info('Payload size too large to pass context')
92
134
  return
93
135
  }
94
- entries[0].Detail = finalData
136
+ entry.Detail = detail
137
+ return
95
138
  }
139
+
140
+ // Both carriers are reused across entries; the write pass overwrites the pathway on each one
141
+ // immediately before serializing it, so nothing may come between the two.
142
+ let pathwayOnlyCarrier
143
+ const injectable = []
144
+ let requestSize = 0
145
+
146
+ for (let i = 0; i < entries.length && requestSize < MAX_PUT_EVENTS_BYTES; i++) {
147
+ const entry = entries[i]
148
+ const tracePropagated = traceCarrier !== undefined && (i === 0 || batchPropagationEnabled)
149
+ let carrier = traceCarrier
150
+ if (!tracePropagated) {
151
+ if (dsmEnabled) pathwayOnlyCarrier ??= DsmPathwayCodec.encode(PATHWAY_SIZE_PROBE)
152
+ carrier = pathwayOnlyCarrier
153
+ }
154
+ if (carrier !== undefined && typeof entry?.Detail === 'string') {
155
+ // A malformed Detail is skipped rather than fatal: EventBridge partial-fails that entry,
156
+ // so the rest of the batch still propagates.
157
+ const traceOnlyDetail = injectDetail(entry.Detail, carrier)
158
+ if (traceOnlyDetail !== undefined) {
159
+ // The pathway field is a fixed width, so reserving it is exact and saves building the
160
+ // detail a second time just to measure it.
161
+ const reserved = dsmEnabled && tracePropagated ? PATHWAY_FIELD_BYTES : 0
162
+ requestSize += putEventEntrySize(entry, traceOnlyDetail) + reserved
163
+ injectable.push({
164
+ entry,
165
+ carrier,
166
+ payloadSize: dsmEnabled ? dsmPayloadSize(entry) : 0,
167
+ traceOnlyDetail: tracePropagated ? traceOnlyDetail : undefined,
168
+ })
169
+ continue
170
+ }
171
+ }
172
+ requestSize += putEventEntrySize(entry)
173
+ }
174
+
175
+ if (injectable.length === 0) return
176
+ if (requestSize >= MAX_PUT_EVENTS_BYTES) {
177
+ log.info('Payload size too large to pass context')
178
+ return
179
+ }
180
+
181
+ for (const { entry, carrier, payloadSize, traceOnlyDetail } of injectable) {
182
+ let detail = traceOnlyDetail
183
+ if (dsmEnabled) {
184
+ const dataStreamsContext = this.setDSMCheckpoint(span, entry, payloadSize)
185
+ // Anything short of a real pathway leaves the carrier on the sizing stand-in, which must
186
+ // never ship; a pathway-only entry then has no fallback and keeps the caller's detail.
187
+ if (DsmPathwayCodec.encode(dataStreamsContext, carrier) !== undefined) {
188
+ detail = injectDetail(entry.Detail, carrier)
189
+ }
190
+ }
191
+ if (detail !== undefined) entry.Detail = detail
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Edge tags feed the pathway hash, so they are a cross-tracer contract: renaming or merging one
197
+ * splits the DSM pathway instead of extending it.
198
+ *
199
+ * @param {import('../../../..').Span} span
200
+ * @param {PutEventsRequestEntry} entry
201
+ * @param {number} payloadSize `dsmPayloadSize(entry)`, taken before the `_datadog` context is
202
+ * injected. The trailing `0` opts out of the pathway estimate the processor adds by default,
203
+ * because that context is propagation overhead rather than the caller's payload.
204
+ * @returns {object|undefined}
205
+ */
206
+ setDSMCheckpoint (span, entry, payloadSize) {
207
+ const eventBus = entry.EventBusName || DEFAULT_EVENT_BUS
208
+ const detailType = entry.DetailType ?? DEFAULT_DETAIL_TYPE
209
+ return this.tracer.setCheckpoint(
210
+ ['direction:out', `exchange:${eventBus}`, `topic:${detailType}`, 'type:eventbridge'],
211
+ span,
212
+ payloadSize,
213
+ 0,
214
+ )
96
215
  }
97
216
  }
98
217
  module.exports = EventBridge
@@ -1,5 +1,5 @@
1
1
  'use strict'
2
- const { DsmPathwayCodec, getSizeOrZero } = require('../../../dd-trace/src/datastreams')
2
+ const { DsmPathwayCodec, getSizeOrZero, PATHWAY_FIELD_BYTES } = require('../../../dd-trace/src/datastreams')
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
5
 
@@ -16,12 +16,6 @@ const MAX_TRACKED_SHARD_ITERATORS = 1000
16
16
  // conservatively skip propagation above this point because the request does not expose that limit.
17
17
  const KINESIS_DEFAULT_MAX_RECORD_BYTES = 1_048_576
18
18
 
19
- // The DSM pathway field (`dd-pathway-ctx-base64`) always serializes to a fixed 55 bytes: a
20
- // 21-char key, a 28-char base64 value, and 6 bytes of JSON framing. Mirrors PATHWAY_HEADER_BYTES
21
- // in dd-trace/src/datastreams/processor.js. Reserved in the size gate so setDSMCheckpoint never
22
- // runs for a record that would ship over the cap once the pathway context is attached.
23
- const DSM_PATHWAY_FIELD_BYTES = 55
24
-
25
19
  class Kinesis extends BaseAwsSdkPlugin {
26
20
  static id = 'kinesis'
27
21
  static peerServicePrecursors = ['streamname']
@@ -276,7 +270,7 @@ class Kinesis extends BaseAwsSdkPlugin {
276
270
  // Gate before setDSMCheckpoint: a record we can't ship must not record a checkpoint.
277
271
  // Reserve the pathway field that DSM appends after the gate.
278
272
  let serialized = JSON.stringify(parsedData)
279
- const reservedBytes = dsmEnabled ? DSM_PATHWAY_FIELD_BYTES : 0
273
+ const reservedBytes = dsmEnabled ? PATHWAY_FIELD_BYTES : 0
280
274
  const partitionKeyBytes = Buffer.byteLength(params.PartitionKey ?? '', 'utf8')
281
275
  if (
282
276
  Buffer.byteLength(serialized, 'utf8') + partitionKeyBytes + reservedBytes >
@@ -156,7 +156,7 @@ function setSpanLinks (triggerType, tracer, span, ctx) {
156
156
  : triggerMetadata.propertiesArray
157
157
 
158
158
  const addLinkFromProperties = (props) => {
159
- if (!props || Object.keys(props).length === 0) return
159
+ if (!props) return
160
160
  const spanContext = tracer.extract('text_map', props)
161
161
  if (spanContext) {
162
162
  span.addLink({ context: spanContext })
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const BunyanPlugin = require('../../datadog-plugin-bunyan/src')
4
+
5
+ class BrowserBunyanPlugin extends BunyanPlugin {
6
+ static id = 'browser-bunyan'
7
+ }
8
+
9
+ module.exports = BrowserBunyanPlugin
@@ -8,17 +8,14 @@ class BunyanPlugin extends LogPlugin {
8
8
 
9
9
  constructor (...args) {
10
10
  super(...args)
11
- this.addSub('apm:bunyan:log', (arg) => this.handleLog(arg))
11
+ this.addSub(`apm:${this.constructor.id}:log`, (arg) => this.handleLog(arg))
12
12
  }
13
13
 
14
14
  /**
15
- * Inject `dd` directly on the record bunyan hands us. bunyan builds the
16
- * record inside `mkRecord` via `objCopy(log.fields)` and then copies the
17
- * caller's fields onto the result, so the `rec` object that flows
18
- * through `_emit` is always bunyan-owned, has `Object.prototype` for its
19
- * prototype, and is never the caller's input directly. Mutating it adds
20
- * `dd` for every consumer (JSON streams via `JSON.stringify(rec)`, raw
21
- * streams via the record reference) without paying for a Proxy view.
15
+ * Inject `dd` directly on the record Bunyan implementations hand us. They build the record
16
+ * inside `mkRecord` from a copy of the logger and caller fields, so the `rec` object that flows
17
+ * through `_emit` is always logger-owned. Mutating it adds `dd` for every consumer without
18
+ * paying for a Proxy view.
22
19
  *
23
20
  * @param {{ message: object }} arg
24
21
  */
@@ -72,6 +72,7 @@ class CucumberPlugin extends CiPlugin {
72
72
  isEarlyFlakeDetectionFaulty,
73
73
  isTestManagementTestsEnabled,
74
74
  isParallel,
75
+ error,
75
76
  onDone,
76
77
  }) => {
77
78
  this._exportPendingWorkerTraces()
@@ -115,6 +116,15 @@ class CucumberPlugin extends CiPlugin {
115
116
 
116
117
  this.testSessionSpan.setTag(TEST_STATUS, status)
117
118
  this.testModuleSpan.setTag(TEST_STATUS, status)
119
+ if (error) {
120
+ for (const testSuiteSpan of this._testSuiteSpansByTestSuite.values()) {
121
+ testSuiteSpan.setTag(TEST_STATUS, 'fail')
122
+ testSuiteSpan.setTag('error', error)
123
+ }
124
+ this.testSessionSpan.setTag('error', error)
125
+ this.testModuleSpan.setTag('error', error)
126
+ }
127
+ this.tracer._exporter.exportDeferredTestSuiteSpans?.()
118
128
  this.testModuleSpan.finish()
119
129
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
120
130
  this.testSessionSpan.finish()
@@ -192,6 +202,7 @@ class CucumberPlugin extends CiPlugin {
192
202
  this.addSub('ci:cucumber:test-suite:finish', ({ status, testSuitePath }) => {
193
203
  const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuitePath)
194
204
  testSuiteSpan.setTag(TEST_STATUS, status)
205
+ this.tracer._exporter.deferTestSuiteSpan?.(testSuiteSpan)
195
206
  testSuiteSpan.finish()
196
207
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
197
208
  })
@@ -1,5 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const cypressPlugin = require('./cypress-plugin')
4
+ const { shouldDeferLegacyFinalization } = require('./finalization')
4
5
 
5
- module.exports = cypressPlugin.afterRun.bind(cypressPlugin)
6
+ module.exports = function afterRun () {
7
+ if (shouldDeferLegacyFinalization()) return
8
+ return cypressPlugin.afterRun.apply(cypressPlugin, arguments)
9
+ }
@@ -1,5 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const cypressPlugin = require('./cypress-plugin')
4
+ const { shouldDeferLegacyFinalization } = require('./finalization')
4
5
 
5
- module.exports = cypressPlugin.afterSpec.bind(cypressPlugin)
6
+ module.exports = function afterSpec () {
7
+ if (shouldDeferLegacyFinalization()) return
8
+ return cypressPlugin.afterSpec.apply(cypressPlugin, arguments)
9
+ }
@@ -477,7 +477,7 @@ class CypressPlugin {
477
477
  testsToSkip = []
478
478
  skippedTests = []
479
479
  skippedTestIds = new Set()
480
- skippableTestsCoverage = {}
480
+ skippableTestsCoverage
481
481
  testSessionCoverageMap = createCoverageMap()
482
482
  hasForcedToRunSuites = false
483
483
  hasUnskippableSuites = false
@@ -492,6 +492,7 @@ class CypressPlugin {
492
492
  loggedAttemptToFixTests = new Set()
493
493
  uploadedScreenshotPaths = new Set()
494
494
  screenshotUploadPromisesByTraceId = new Map()
495
+ screenshotUploadAbortControllers = new Set()
495
496
  afterScreenshotHandler = undefined
496
497
  lastFinishedTest = null
497
498
  pendingScreenshotUploads = []
@@ -565,7 +566,7 @@ class CypressPlugin {
565
566
  this.testsToSkip = []
566
567
  this.skippedTests = []
567
568
  this.skippedTestIds = new Set()
568
- this.skippableTestsCoverage = {}
569
+ this.skippableTestsCoverage = undefined
569
570
  this.testSessionCoverageMap = createCoverageMap()
570
571
  this.hasForcedToRunSuites = false
571
572
  this.hasUnskippableSuites = false
@@ -581,10 +582,12 @@ class CypressPlugin {
581
582
  this.loggedAttemptToFixTests = new Set()
582
583
  this.uploadedScreenshotPaths = new Set()
583
584
  this.screenshotUploadPromisesByTraceId = new Map()
585
+ this.screenshotUploadAbortControllers = new Set()
584
586
  this.lastFinishedTest = null
585
587
  this.pendingScreenshotUploads = []
586
588
  this.activeTestSpan = null
587
589
  this.testSuiteSpan = null
590
+ this.finishedTestSuiteSpans = []
588
591
  this.testModuleSpan = null
589
592
  this.testSessionSpan = null
590
593
  this.command = undefined
@@ -643,6 +646,19 @@ class CypressPlugin {
643
646
  return Promise.all(uploadPromises).then(getScreenshotUploadResult)
644
647
  }
645
648
 
649
+ /**
650
+ * Cancels screenshot work that must not outlive an errored after:spec finalization boundary.
651
+ *
652
+ * @param {Error} error - Error that triggered finalization
653
+ * @returns {void}
654
+ */
655
+ abortPendingScreenshotUploads (error) {
656
+ for (const controller of this.screenshotUploadAbortControllers) controller.abort(error)
657
+ this.screenshotUploadAbortControllers.clear()
658
+ this.screenshotUploadPromisesByTraceId.clear()
659
+ this.pendingScreenshotUploads = []
660
+ }
661
+
646
662
  /**
647
663
  * Returns the current time in the same coordinate system used by span
648
664
  * start/finish. Captured at session span creation so it shares the same
@@ -663,17 +679,6 @@ class CypressPlugin {
663
679
  return this.repositoryRoot || this.rootDir || process.cwd()
664
680
  }
665
681
 
666
- /**
667
- * Returns whether the backend supplied skipped-test coverage data.
668
- *
669
- * @returns {boolean}
670
- */
671
- hasSkippableTestsCoverage () {
672
- return !!(this.skippableTestsCoverage &&
673
- typeof this.skippableTestsCoverage === 'object' &&
674
- Object.keys(this.skippableTestsCoverage).length > 0)
675
- }
676
-
677
682
  /**
678
683
  * Returns whether skipped test coverage should be backfilled into the session coverage map.
679
684
  *
@@ -683,7 +688,7 @@ class CypressPlugin {
683
688
  return this.isItrEnabled &&
684
689
  this.isCoverageReportUploadEnabled &&
685
690
  this.isTestsSkipped &&
686
- this.hasSkippableTestsCoverage()
691
+ this.skippableTestsCoverage !== undefined
687
692
  }
688
693
 
689
694
  /**
@@ -800,6 +805,17 @@ class CypressPlugin {
800
805
  // Depending on the received configuration, the Cypress configuration can be modified:
801
806
  // for example, to enable retries for failed tests.
802
807
  init (tracer, cypressConfig) {
808
+ if (this.cypressConfig === cypressConfig && this.hasOriginalCypressRetries) {
809
+ cypressConfig.retries = this.originalCypressRetries !== null &&
810
+ typeof this.originalCypressRetries === 'object'
811
+ ? { ...this.originalCypressRetries }
812
+ : this.originalCypressRetries
813
+ } else {
814
+ this.originalCypressRetries = cypressConfig.retries !== null && typeof cypressConfig.retries === 'object'
815
+ ? { ...cypressConfig.retries }
816
+ : cypressConfig.retries
817
+ this.hasOriginalCypressRetries = true
818
+ }
803
819
  this.resetRunState()
804
820
  this._isInit = true
805
821
  this.tracer = tracer
@@ -1063,9 +1079,13 @@ class CypressPlugin {
1063
1079
  }
1064
1080
 
1065
1081
  async beforeRun (details) {
1066
- // We need to make sure that the plugin is initialized before running the tests
1067
- // This is for the case where the user has not returned the promise from the init function
1068
- await this.libraryConfigurationPromise
1082
+ if (this._isInit) {
1083
+ // The user may not have returned the promise from the init function.
1084
+ await this.libraryConfigurationPromise
1085
+ } else {
1086
+ // Cypress open reuses the same plugin process for every interactive run.
1087
+ await this.init(this.tracer, this.cypressConfig)
1088
+ }
1069
1089
 
1070
1090
  this.command = getCypressCommand(details)
1071
1091
  this.frameworkVersion = getCypressVersion(details)
@@ -1131,7 +1151,7 @@ class CypressPlugin {
1131
1151
  } else {
1132
1152
  const { skippableTests, correlationId, skippableTestsCoverage } = skippableTestsResponse
1133
1153
  this.testsToSkip = skippableTests || []
1134
- this.skippableTestsCoverage = skippableTestsCoverage || {}
1154
+ this.skippableTestsCoverage = skippableTestsCoverage
1135
1155
  this.itrCorrelationId = correlationId
1136
1156
  incrementCountMetric(TELEMETRY_ITR_SKIPPED, { testLevel: 'test' }, this.testsToSkip.length)
1137
1157
  }
@@ -1252,18 +1272,29 @@ class CypressPlugin {
1252
1272
  return details
1253
1273
  }
1254
1274
 
1255
- afterRun (suiteStats) {
1275
+ afterRun (suiteStats, error, shouldFailFinishedSuites = true) {
1256
1276
  if (!this._isInit) {
1257
1277
  log.warn('Attemping to call afterRun without initializating the plugin first')
1258
1278
  return
1259
1279
  }
1260
1280
  if (this.testSessionSpan && this.testModuleSpan) {
1261
- const testStatus = getSessionStatus(suiteStats)
1281
+ const testStatus = error ? 'fail' : getSessionStatus(suiteStats)
1262
1282
  const hasBackfilledCoverage = this.applySkippedCoverageToTestSessionCoverage()
1263
1283
  const testCodeCoverageLinesTotal = this.getTestCodeCoverageLinesTotal(hasBackfilledCoverage)
1264
1284
 
1265
1285
  this.testModuleSpan.setTag(TEST_STATUS, testStatus)
1266
1286
  this.testSessionSpan.setTag(TEST_STATUS, testStatus)
1287
+ for (const span of this.finishedTestSuiteSpans) {
1288
+ if (error && shouldFailFinishedSuites) {
1289
+ span.setTag(TEST_STATUS, 'fail')
1290
+ span.setTag('error', error)
1291
+ }
1292
+ }
1293
+ this.finishedTestSuiteSpans = []
1294
+ if (error) {
1295
+ this.testModuleSpan.setTag('error', error)
1296
+ this.testSessionSpan.setTag('error', error)
1297
+ }
1267
1298
 
1268
1299
  addIntelligentTestRunnerSpanTags(
1269
1300
  this.testSessionSpan,
@@ -1301,6 +1332,7 @@ class CypressPlugin {
1301
1332
  })
1302
1333
 
1303
1334
  finishAllTraceSpans(this.testSessionSpan)
1335
+ this.tracer._tracer._exporter?.exportDeferredTestSuiteSpans?.()
1304
1336
  }
1305
1337
 
1306
1338
  return new Promise(resolve => {
@@ -1358,7 +1390,7 @@ class CypressPlugin {
1358
1390
  }
1359
1391
  }
1360
1392
 
1361
- afterSpec (spec, results) {
1393
+ afterSpec (spec, results, error) {
1362
1394
  const { tests, stats, screenshots } = results || {}
1363
1395
  const cypressTests = tests || []
1364
1396
  const specScreenshots = screenshots || []
@@ -1572,13 +1604,16 @@ class CypressPlugin {
1572
1604
  const screenshotUploadResultPromise = failedTestTraceId
1573
1605
  ? this.getScreenshotUploadResultPromise(failedTestTraceId)
1574
1606
  : undefined
1575
- if (screenshotUploadResultPromise) {
1607
+ if (screenshotUploadResultPromise && !error) {
1576
1608
  testSpanFinishPromises.push(screenshotUploadResultPromise.then((uploadResult) => {
1577
1609
  setScreenshotUploadTags(finishedTest.testSpan, uploadResult)
1578
1610
  this.screenshotUploadPromisesByTraceId.delete(failedTestTraceId)
1579
1611
  finishedTest.testSpan.finish(finishedTest.finishTime)
1580
1612
  }))
1581
1613
  } else {
1614
+ if (screenshotUploadResultPromise) {
1615
+ this.screenshotUploadPromisesByTraceId.delete(failedTestTraceId)
1616
+ }
1582
1617
  finishedTest.testSpan.finish(finishedTest.finishTime)
1583
1618
  }
1584
1619
  }
@@ -1586,13 +1621,18 @@ class CypressPlugin {
1586
1621
 
1587
1622
  const finishSuite = () => {
1588
1623
  if (this.testSuiteSpan) {
1589
- const status = getSuiteStatus(stats)
1624
+ const status = error ? 'fail' : getSuiteStatus(stats)
1590
1625
  this.testSuiteSpan.setTag(TEST_STATUS, status)
1591
1626
 
1592
- if (latestError) {
1593
- this.testSuiteSpan.setTag('error', latestError)
1627
+ if (error || latestError) {
1628
+ this.testSuiteSpan.setTag('error', error || latestError)
1594
1629
  }
1630
+ const canRunAfterRun = this.cypressConfig.isTextTerminal ||
1631
+ this.cypressConfig.experimentalInteractiveRunEvents
1632
+ const exporter = this.tracer._tracer._exporter
1633
+ if (canRunAfterRun && exporter?.deferTestSuiteSpan) exporter.deferTestSuiteSpan(this.testSuiteSpan)
1595
1634
  this.testSuiteSpan.finish()
1635
+ if (canRunAfterRun) this.finishedTestSuiteSpans.push(this.testSuiteSpan)
1596
1636
  this.testSuiteSpan = null
1597
1637
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
1598
1638
  }
@@ -1608,16 +1648,23 @@ class CypressPlugin {
1608
1648
 
1609
1649
  finishSuite()
1610
1650
 
1651
+ if (error) {
1652
+ this.abortPendingScreenshotUploads(error)
1653
+ return this.afterRun(undefined, error, false)
1654
+ }
1655
+
1611
1656
  const screenshotUploadsPromise = waitForScreenshotUploads()
1657
+ let afterSpecPromise = screenshotUploadsPromise
1612
1658
  if (testSpanFinishPromises.length > 0) {
1613
1659
  const testSpansPromise = Promise.all(testSpanFinishPromises).then(() => null)
1614
1660
  if (screenshotUploadsPromise) {
1615
- return Promise.all([testSpansPromise, screenshotUploadsPromise]).then(() => null)
1661
+ afterSpecPromise = Promise.all([testSpansPromise, screenshotUploadsPromise]).then(() => null)
1662
+ } else {
1663
+ afterSpecPromise = testSpansPromise
1616
1664
  }
1617
- return testSpansPromise
1618
1665
  }
1619
1666
 
1620
- return screenshotUploadsPromise
1667
+ return afterSpecPromise
1621
1668
  }
1622
1669
 
1623
1670
  /**
@@ -1637,6 +1684,7 @@ class CypressPlugin {
1637
1684
  }
1638
1685
 
1639
1686
  const uploadPromises = []
1687
+ const abortController = new AbortController()
1640
1688
 
1641
1689
  for (const screenshot of screenshots) {
1642
1690
  const filePath = getScreenshotFilePath(screenshot)
@@ -1655,6 +1703,7 @@ class CypressPlugin {
1655
1703
  traceId,
1656
1704
  idempotencyKey,
1657
1705
  capturedAtMs,
1706
+ signal: abortController.signal,
1658
1707
  }, (err) => {
1659
1708
  resolve(err ? SCREENSHOT_UPLOAD_RESULT_ERROR : SCREENSHOT_UPLOAD_RESULT_UPLOADED)
1660
1709
  })
@@ -1662,7 +1711,10 @@ class CypressPlugin {
1662
1711
  }
1663
1712
 
1664
1713
  if (uploadPromises.length > 0) {
1665
- const uploadPromise = Promise.all(uploadPromises).then(getScreenshotUploadResult)
1714
+ this.screenshotUploadAbortControllers.add(abortController)
1715
+ const uploadPromise = Promise.all(uploadPromises).then(getScreenshotUploadResult).finally(() => {
1716
+ this.screenshotUploadAbortControllers.delete(abortController)
1717
+ })
1666
1718
  this.addScreenshotUploadPromise(traceId, uploadPromise)
1667
1719
  return uploadPromise
1668
1720
  }