newrelic 13.2.1 → 13.3.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 (56) hide show
  1. package/NEWS.md +44 -10
  2. package/api.js +1 -1
  3. package/lib/instrumentation/@azure/functions.js +8 -46
  4. package/lib/instrumentation/core/http-outbound.js +26 -10
  5. package/lib/instrumentation/core/http.js +20 -118
  6. package/lib/instrumentations.js +0 -14
  7. package/lib/otel/logs/index.js +80 -0
  8. package/lib/otel/metrics/index.js +119 -0
  9. package/lib/otel/setup-signal.js +37 -0
  10. package/lib/otel/setup.js +15 -25
  11. package/lib/otel/{attr-reconciler.js → traces/attr-reconciler.js} +2 -2
  12. package/lib/otel/{exception-mapping.js → traces/exception-mapping.js} +1 -1
  13. package/lib/otel/traces/index.js +40 -0
  14. package/lib/otel/{rules.js → traces/rules.js} +1 -1
  15. package/lib/otel/{segment-synthesis.js → traces/segment-synthesis.js} +5 -4
  16. package/lib/otel/{segments → traces/segments}/consumer.js +4 -4
  17. package/lib/otel/{segments → traces/segments}/database.js +6 -6
  18. package/lib/otel/{segments → traces/segments}/http-external.js +11 -8
  19. package/lib/otel/traces/segments/index.js +22 -0
  20. package/lib/otel/{segments → traces/segments}/internal.js +1 -1
  21. package/lib/otel/{segments → traces/segments}/producer.js +2 -2
  22. package/lib/otel/{segments → traces/segments}/server.js +4 -4
  23. package/lib/otel/{segments → traces/segments}/utils.js +1 -1
  24. package/lib/otel/{span-processor.js → traces/span-processor.js} +13 -19
  25. package/lib/otel/{transformation-rules.json → traces/transformation-rules.json} +3 -2
  26. package/lib/otel/{utils.js → traces/utils.js} +2 -1
  27. package/lib/serverless/aws-lambda.js +7 -68
  28. package/lib/shim/webframework-shim/middleware.js +1 -1
  29. package/lib/shimmer.js +1 -1
  30. package/lib/subscriber-configs.js +2 -1
  31. package/lib/subscribers/base.js +30 -0
  32. package/lib/subscribers/elasticsearch/config.js +1 -0
  33. package/lib/subscribers/mcp-sdk/client-request.js +66 -0
  34. package/lib/subscribers/mcp-sdk/config.js +13 -75
  35. package/lib/subscribers/openai/base.js +21 -0
  36. package/lib/subscribers/openai/chat-responses.js +15 -0
  37. package/lib/subscribers/openai/chat.js +85 -0
  38. package/lib/subscribers/openai/client.js +31 -0
  39. package/lib/subscribers/openai/config.js +85 -0
  40. package/lib/subscribers/openai/embeddings.js +54 -0
  41. package/lib/subscribers/openai/utils.js +349 -0
  42. package/lib/transaction/index.js +157 -56
  43. package/lib/transaction/trace/segment.js +5 -2
  44. package/lib/util/urltils.js +158 -192
  45. package/lib/utilization/common.js +0 -6
  46. package/package.json +1 -1
  47. package/lib/instrumentation/openai.js +0 -482
  48. package/lib/otel/logs/bootstrap-logs.js +0 -84
  49. package/lib/otel/metrics/bootstrap-metrics.js +0 -117
  50. package/lib/otel/segments/index.js +0 -22
  51. package/lib/subscribers/mcp-sdk/client-prompt.js +0 -23
  52. package/lib/subscribers/mcp-sdk/client-resource.js +0 -24
  53. package/lib/subscribers/mcp-sdk/client-tool.js +0 -23
  54. package/lib/subscribers/mcp-sdk/client.js +0 -29
  55. /package/lib/otel/{constants.js → traces/constants.js} +0 -0
  56. /package/lib/otel/{sampler.js → traces/sampler.js} +0 -0
@@ -0,0 +1,349 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const {
8
+ LlmChatCompletionMessage,
9
+ LlmChatCompletionSummary,
10
+ LlmEmbedding,
11
+ LlmErrorMessage
12
+ } = require('../../llm-events/openai')
13
+ const { extractLlmContext } = require('../../util/llm-utils')
14
+
15
+ const { AI } = require('../../metrics/names')
16
+ const { OPENAI } = AI
17
+ const { DESTINATIONS } = require('../../config/attribute-filter')
18
+ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
19
+
20
+ /**
21
+ * Parses the response from OpenAI and extracts the message content and role.
22
+ *
23
+ * @param {object} response The OpenAI SDK response object
24
+ * @returns {{ content: string, role: string }} the message object with fields `content` and `role`
25
+ */
26
+ function getMessageFromResponse(response) {
27
+ let content
28
+ let role
29
+ if (response?.object === 'response') {
30
+ content = response?.output?.[0]?.content?.[0]?.text
31
+ role = response?.output?.[0]?.role
32
+ } else {
33
+ content = response?.choices?.[0]?.message?.content
34
+ role = response?.choices?.[0]?.message?.role
35
+ }
36
+
37
+ return { content, role }
38
+ }
39
+
40
+ /**
41
+ * Parses all messages from the OpenAI request object.
42
+ *
43
+ * @param {object} request The OpenAI SDK request object
44
+ * @param logger
45
+ * @returns {Array<object>} an array of message objects with fields `content` and `role`
46
+ */
47
+ function getMessagesFromRequest(request, logger) {
48
+ // There are a few different ways to pass messages to OpenAI SDK.
49
+ //
50
+ // For langchain and `chat.completions.create`, messages are passed
51
+ // as an array of objects with `content` and `role` properties
52
+ // to the `messages` field of the request.
53
+ //
54
+ // For `responses.create`, messages are passed as an array of objects
55
+ // with `content` and `role` properties OR as a single string (implied
56
+ // to be a user message) to the `input` field of the request.
57
+ let messages = []
58
+
59
+ if (Array.isArray(request?.input)) {
60
+ // Handle array of input messages
61
+ messages = request.input.filter((msg) => msg?.content && msg?.role)
62
+ } else if (typeof request?.input === 'string') {
63
+ // Handle single string input as a user message
64
+ messages = [{ content: request.input, role: 'user' }]
65
+ } else if (Array.isArray(request?.messages)) {
66
+ // Handle array of messages
67
+ messages = request.messages.filter((msg) => msg?.content && msg?.role)
68
+ } else {
69
+ logger.warn('No valid messages found in OpenAI request object.')
70
+ }
71
+
72
+ return messages
73
+ }
74
+
75
+ /**
76
+ * Enqueues a LLM event to the custom event aggregator
77
+ *
78
+ * @param {object} params input params
79
+ * @param {Agent} params.agent NR agent instance
80
+ * @param {string} params.type LLM event type
81
+ * @param {object} params.msg LLM event
82
+ */
83
+ function recordEvent({ agent, type, msg }) {
84
+ const llmContext = extractLlmContext(agent)
85
+
86
+ agent.customEventAggregator.add([
87
+ { type, timestamp: Date.now() },
88
+ Object.assign({}, msg, llmContext)
89
+ ])
90
+ }
91
+
92
+ /**
93
+ * Increments the tracking metric and sets the llm attribute on transactions
94
+ *
95
+ * @param {object} params input params
96
+ * @param {Agent} params.agent NR agent instance
97
+ * @param {Transaction} params.transaction active transaction
98
+ * @param params.version
99
+ */
100
+ function addLlmMeta({ agent, transaction, version }) {
101
+ TRACKING_METRIC = `${TRACKING_METRIC}/${version}`
102
+ agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
103
+ transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
104
+ }
105
+
106
+ /**
107
+ * Generates LlmChatCompletionSummary for a chat completion creation.
108
+ * Also iterates over both input messages and the first response message
109
+ * and creates LlmChatCompletionMessage.
110
+ *
111
+ * Also assigns relevant ids by response id for LlmFeedbackEvent creation
112
+ *
113
+ * @param {object} params input params
114
+ * @param {Agent} params.agent NR agent instance
115
+ * @param {TraceSegment} params.segment active segment from chat completion
116
+ * @param {object} params.request chat completion params
117
+ * @param {object} params.response chat completion response
118
+ * @param {boolean} [params.err] err if it exists
119
+ * @param {Transaction} params.transaction active transaction
120
+ * @param params.headers
121
+ * @param params.logger
122
+ */
123
+ function recordChatCompletionMessages({
124
+ agent,
125
+ headers,
126
+ logger,
127
+ segment,
128
+ request,
129
+ response,
130
+ err,
131
+ transaction
132
+ }) {
133
+ if (shouldSkipInstrumentation(agent.config, logger) === true) {
134
+ logger.debug('skipping sending of ai data')
135
+ return
136
+ }
137
+
138
+ if (!response) {
139
+ // If we get an error, it is possible that `response = null`.
140
+ // In that case, we define it to be an empty object.
141
+ response = {}
142
+ }
143
+
144
+ response.headers = headers
145
+ // explicitly end segment to consistent duration
146
+ // for both LLM events and the segment
147
+ segment.end()
148
+ const completionSummary = new LlmChatCompletionSummary({
149
+ agent,
150
+ segment,
151
+ transaction,
152
+ request,
153
+ response,
154
+ withError: err != null
155
+ })
156
+
157
+ // Note: langchain still expects a message event even
158
+ // when the response is empty, so we will not filter
159
+ // the response message.
160
+ const messages = [
161
+ ...getMessagesFromRequest(request, logger),
162
+ getMessageFromResponse(response)
163
+ ]
164
+
165
+ messages.forEach((message, index) => {
166
+ const completionMsg = new LlmChatCompletionMessage({
167
+ agent,
168
+ segment,
169
+ transaction,
170
+ request,
171
+ response,
172
+ index,
173
+ completionId: completionSummary.id,
174
+ message
175
+ })
176
+
177
+ recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
178
+ })
179
+
180
+ recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
181
+
182
+ if (err) {
183
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
184
+ agent.errors.add(transaction, err, llmError)
185
+ }
186
+
187
+ delete response.headers
188
+ }
189
+
190
+ /**
191
+ * Checks if we should skip instrumentation.
192
+ * Currently, it checks if `ai_monitoring.enabled` is true
193
+ * and the package version >= 4.0.0
194
+ *
195
+ * @param {object} config agent config
196
+ * @param logger
197
+ * @returns {boolean} flag if instrumentation should be skipped
198
+ */
199
+ function shouldSkipInstrumentation(config, logger) {
200
+ if (config?.ai_monitoring?.enabled !== true) {
201
+ logger.debug('config.ai_monitoring.enabled is set to false.')
202
+ return true
203
+ }
204
+ }
205
+
206
+ /**
207
+ * `chat.completions.create` can return a stream once promise resolves.
208
+ * This wraps the iterator which is a generator function.
209
+ * We will call the original iterator, intercept chunks and yield
210
+ * to the original. On complete, we will construct the new message object
211
+ * with what we have seen in the stream and create the chat completion
212
+ * messages.
213
+ * @param {object} params input params
214
+ * @param {Agent} params.agent NR agent instance
215
+ * @param {Shim} params.shim the current shim instance
216
+ * @param {object} params.request chat completion params
217
+ * @param {object} params.response chat completion response
218
+ * @param {TraceSegment} params.segment active segment from chat completion
219
+ * @param {Transaction} params.transaction active transaction
220
+ * @param {Error} params.err error if it exists
221
+ * @param params.headers
222
+ * @param params.logger
223
+ */
224
+ function instrumentStream({ agent, headers, logger, request, response, segment, transaction, err = null }) {
225
+ if (!agent.config.ai_monitoring.streaming.enabled) {
226
+ logger.warn(
227
+ '`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
228
+ )
229
+ agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
230
+ return
231
+ }
232
+
233
+ if (err) {
234
+ // If there is an error already e.g. APIConnectionError,
235
+ // the iterator will not be called, so we have to
236
+ // record the chat completion messages with the error now.
237
+ segment.touch()
238
+ recordChatCompletionMessages({
239
+ agent,
240
+ headers,
241
+ logger,
242
+ segment,
243
+ transaction,
244
+ request,
245
+ response,
246
+ err
247
+ })
248
+ return
249
+ }
250
+
251
+ const orig = response.iterator
252
+ response.iterator = async function * wrappedIterator() {
253
+ let content = ''
254
+ let role = ''
255
+ let chunk
256
+ try {
257
+ const iterator = orig.apply(this, arguments)
258
+
259
+ for await (chunk of iterator) {
260
+ if (chunk.choices?.[0]?.delta?.role) {
261
+ role = chunk.choices[0].delta.role
262
+ }
263
+
264
+ content += chunk.choices?.[0]?.delta?.content ?? ''
265
+ yield chunk
266
+ }
267
+ } catch (streamErr) {
268
+ err = streamErr
269
+ throw err
270
+ } finally {
271
+ if (chunk?.choices) {
272
+ chunk.choices[0].message = { role, content }
273
+ } else if (chunk?.response) {
274
+ chunk = chunk.response
275
+ }
276
+
277
+ // update segment duration since we want to extend the time it took to
278
+ // handle the stream
279
+ segment.touch()
280
+
281
+ recordChatCompletionMessages({
282
+ agent,
283
+ headers,
284
+ logger,
285
+ segment,
286
+ transaction,
287
+ request,
288
+ response: chunk,
289
+ err
290
+ })
291
+ }
292
+ }
293
+ }
294
+
295
+ function recordEmbeddingMessage({
296
+ agent,
297
+ logger,
298
+ request,
299
+ headers,
300
+ response,
301
+ segment,
302
+ transaction,
303
+ err
304
+ }) {
305
+ if (!response) {
306
+ // If we get an error, it is possible that `response = null`.
307
+ // In that case, we define it to be an empty object.
308
+ response = {}
309
+ }
310
+
311
+ segment.end()
312
+ if (shouldSkipInstrumentation(agent.config, logger) === true) {
313
+ // We need this check inside the wrapper because it is possible for monitoring
314
+ // to be disabled at the account level. In such a case, the value is set
315
+ // after the instrumentation has been initialized.
316
+ logger.debug('skipping sending of ai data')
317
+ return
318
+ }
319
+
320
+ response.headers = headers
321
+ // explicitly end segment to get consistent duration
322
+ // for both LLM events and the segment
323
+
324
+ const embedding = new LlmEmbedding({
325
+ agent,
326
+ segment,
327
+ transaction,
328
+ request,
329
+ response,
330
+ withError: err != null
331
+ })
332
+
333
+ recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
334
+
335
+ if (err) {
336
+ const llmError = new LlmErrorMessage({ cause: err, embedding, response })
337
+ agent.errors.add(transaction, err, llmError)
338
+ }
339
+
340
+ // cleanup keys on response before returning to user code
341
+ delete response.headers
342
+ }
343
+
344
+ module.exports = {
345
+ addLlmMeta,
346
+ instrumentStream,
347
+ recordChatCompletionMessages,
348
+ recordEmbeddingMessage
349
+ }
@@ -16,8 +16,11 @@ const Timer = require('../timer')
16
16
  const Trace = require('./trace')
17
17
  const synthetics = require('../synthetics')
18
18
  const urltils = require('../util/urltils')
19
+ const cat = require('../util/cat')
19
20
  const TraceContext = require('./tracecontext').TraceContext
20
21
  const Logs = require('./logs')
22
+ const headerAttributes = require('../header-attributes')
23
+ const headerProcessing = require('../header-processing')
21
24
  const DT_ACCEPT_PAYLOAD_EXCEPTION_METRIC = 'DistributedTrace/AcceptPayload/Exception'
22
25
  const DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC = 'DistributedTrace/AcceptPayload/ParseException'
23
26
  const REQUEST_PARAMS_PATH = 'request.parameters.'
@@ -119,7 +122,6 @@ function Transaction(agent, traceId) {
119
122
  this.syntheticsData = null
120
123
  this.syntheticsInfoData = null
121
124
  this.url = null
122
- this.parsedUrl = null
123
125
  this.verb = null
124
126
  this.baseSegment = null
125
127
  this.type = TYPES.WEB
@@ -317,8 +319,9 @@ Transaction.prototype.setPartialName = function setPartialName(name) {
317
319
  * boolean flag in `ignore`.
318
320
  */
319
321
  Transaction.prototype._partialNameFromUri = _partialNameFromUri
320
- function _partialNameFromUri(requestUrl, status) {
321
- const scrubbedUrl = urltils.scrub(requestUrl)
322
+ function _partialNameFromUri() {
323
+ const scrubbedUrl = this.url
324
+ const status = this.statusCode
322
325
 
323
326
  // 0. If there is a name in the name-state stack, use it.
324
327
  let partialName = this._partialName
@@ -386,21 +389,69 @@ Transaction.prototype.isIgnored = function getIgnore() {
386
389
  }
387
390
 
388
391
  /**
389
- * Derives the transaction's name from the given URL and status code.
392
+ * Finalize a web transaction. This includes naming the transaction, collecting
393
+ * response headers, and ending the transaction and its base segment.
394
+ *
395
+ * @param {object} params The parameters for finalizing the transaction.
396
+ * @param {number} params.statusCode The response status code.
397
+ * @param {string} [params.statusMessage] The response status message.
398
+ * @param {object} [params.headers] The response headers.
399
+ * @param {Boolean} params.end Whether to end the transaction and baseSegment.
400
+ */
401
+ Transaction.prototype.finalizeWeb = function finalizeWeb({ statusCode, statusMessage, headers, end }) {
402
+ // Naming must happen before the segment and transaction are ended
403
+ // because metrics recording depends on naming's side effects.
404
+ this.finalizeNameFromWeb(statusCode)
405
+ if (statusCode != null) {
406
+ const responseCode = String(statusCode)
407
+
408
+ if (/^\d+$/.test(responseCode)) {
409
+ this.trace.attributes.addAttribute(
410
+ DESTS.TRANS_COMMON,
411
+ 'http.statusCode',
412
+ responseCode
413
+ )
414
+
415
+ this.baseSegment.addSpanAttribute('http.statusCode', responseCode)
416
+ }
417
+ }
418
+
419
+ if (statusMessage !== undefined) {
420
+ this.trace.attributes.addAttribute(
421
+ DESTS.TRANS_COMMON,
422
+ 'http.statusText',
423
+ statusMessage
424
+ )
425
+
426
+ this.baseSegment.addSpanAttribute('http.statusText', statusMessage)
427
+ }
428
+
429
+ if (headers) {
430
+ headerAttributes.collectResponseHeaders(headers, this)
431
+ }
432
+
433
+ if (end) {
434
+ // And we are done! End the segment and transaction.
435
+ this.baseSegment.end()
436
+ this.end()
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Derives the transaction's name from `transaction.url` and status code.
390
442
  *
391
443
  * The transaction's name will be set after this as well as its ignored status
392
444
  * based on the derived name.
393
445
  *
394
- * @param {string} requestURL - The URL to derive the request's name and status from.
395
446
  * @param {number} statusCode - The response status code.
396
447
  */
397
- Transaction.prototype.finalizeNameFromUri = finalizeNameFromUri
448
+ Transaction.prototype.finalizeNameFromWeb = finalizeNameFromWeb
398
449
 
399
- function finalizeNameFromUri(requestURL, statusCode) {
450
+ function finalizeNameFromWeb(statusCode) {
400
451
  if (logger.traceEnabled()) {
401
452
  logger.trace(
402
453
  {
403
- requestURL,
454
+ requestURL: this.url,
404
455
  statusCode,
405
456
  transactionId: this.id,
406
457
  transactionName: this.name
@@ -409,10 +460,6 @@ function finalizeNameFromUri(requestURL, statusCode) {
409
460
  )
410
461
  }
411
462
 
412
- if (!this.agent.config.url_obfuscation.enabled) {
413
- this.url = urltils.scrub(requestURL)
414
- }
415
-
416
463
  this.statusCode = statusCode
417
464
  this.name = this.getFullName()
418
465
 
@@ -426,7 +473,16 @@ function finalizeNameFromUri(requestURL, statusCode) {
426
473
  this.ignore = this.forceIgnore
427
474
  }
428
475
 
429
- this.baseSegment && this._markAsWeb(requestURL)
476
+ const obfuscatedUrl = urltils.obfuscatePath(this.agent.config, this.url)
477
+ this.url = obfuscatedUrl
478
+ // URL is sent as an agent attribute with transaction events
479
+ this.trace.attributes.addAttribute(
480
+ DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
481
+ 'request.uri',
482
+ obfuscatedUrl
483
+ )
484
+
485
+ this?.baseSegment?.markAsWeb(this, obfuscatedUrl)
430
486
 
431
487
  this._copyNameToActiveSpan(this.name)
432
488
 
@@ -471,37 +527,6 @@ Transaction.prototype._copyNameToActiveSpan = function _copyNameToActiveSpan(nam
471
527
  spanContext.addIntrinsicAttribute('transaction.name', name)
472
528
  }
473
529
 
474
- /**
475
- * Copies final base segment parameters to trace attributes before reapplying
476
- * them to the segment.
477
- *
478
- * Handles adding query parameters to `request.parameter.*` attributes
479
- *
480
- * @param {string} rawURL The URL, as it came in, for parameter extraction.
481
- */
482
- Transaction.prototype._markAsWeb = function _markAsWeb(rawURL) {
483
- // Because we are assured we have the URL here, lets grab query params.
484
- const params = urltils.parseParameters(rawURL)
485
- for (const key in params) {
486
- if (props.hasOwn(params, key)) {
487
- this.trace.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, params[key])
488
-
489
- const segment = this.agent.tracer.getSegment()
490
-
491
- if (!segment) {
492
- logger.trace(
493
- 'Active segment not available, not adding request.parameters span attribute for %s',
494
- key
495
- )
496
- } else {
497
- segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, params[key])
498
- }
499
- }
500
- }
501
-
502
- this.baseSegment.markAsWeb(this)
503
- }
504
-
505
530
  /**
506
531
  * Sets the transaction's name and determines if it will be ignored.
507
532
  *
@@ -514,7 +539,7 @@ Transaction.prototype.finalizeName = function finalizeName(name) {
514
539
  // If no name is given, and this is a web transaction with a url, then
515
540
  // finalize the name using the stored url.
516
541
  if (name == null && this.isWeb() && this.url) {
517
- return this.finalizeNameFromUri(this.url, this.statusCode)
542
+ return this.finalizeNameFromWeb(this.statusCode)
518
543
  }
519
544
 
520
545
  // this may seem out of place but certain API methods
@@ -566,7 +591,7 @@ Transaction.prototype.finalizeName = function finalizeName(name) {
566
591
  */
567
592
  Transaction.prototype.getName = function getName() {
568
593
  if (this.isWeb() && this.url) {
569
- const finalName = this._partialNameFromUri(this.url, this.statusCode)
594
+ const finalName = this._partialNameFromUri()
570
595
  if (finalName.ignore) {
571
596
  this.ignore = true
572
597
  }
@@ -1427,18 +1452,16 @@ Transaction.prototype.addRequestParameters = addRequestParameters
1427
1452
  * @param {Object<string, string>} requestParameters of the request object
1428
1453
  */
1429
1454
  function addRequestParameters(requestParameters) {
1430
- for (const key in requestParameters) {
1431
- if (props.hasOwn(requestParameters, key)) {
1432
- this.trace.attributes.addAttribute(
1433
- DESTS.NONE,
1434
- REQUEST_PARAMS_PATH + key,
1435
- requestParameters[key]
1436
- )
1455
+ for (const [key, value] of Object.entries(requestParameters)) {
1456
+ this.trace.attributes.addAttribute(
1457
+ DESTS.NONE,
1458
+ REQUEST_PARAMS_PATH + key,
1459
+ value
1460
+ )
1437
1461
 
1438
- const segment = this.baseSegment
1462
+ const segment = this.baseSegment
1439
1463
 
1440
- segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, requestParameters[key])
1441
- }
1464
+ segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, value)
1442
1465
  }
1443
1466
  }
1444
1467
 
@@ -1453,4 +1476,82 @@ Transaction.prototype.incrementCounters = function incrementCounters() {
1453
1476
  this.agent.incrementCounters()
1454
1477
  }
1455
1478
 
1479
+ /**
1480
+ * Initializes a web transaction with request information:
1481
+ * Assigns type(web), verb, url, port attributes on transaction.
1482
+ * Collects request headers as attributes on the transaction.
1483
+ * Applies user naming rules to the transaction.
1484
+ * Calculates queue time if possible.
1485
+ * Assigns synthetics information if present.
1486
+ * Adds distributed trace or cat headers if present.
1487
+ *
1488
+ * @param {object} params - Parameters to initialize the web transaction.
1489
+ * @param {string} params.absoluteUrl - Full URL of the request.
1490
+ * @param {string} params.method - HTTP method of the request.
1491
+ * @param {number} params.port - Port of the request.
1492
+ * @param {object} [params.headers] - HTTP headers of the request.
1493
+ * @param {string} [params.transport] - Transport type that delivered the request.
1494
+ */
1495
+ Transaction.prototype.initializeWeb = function initializeWeb({ absoluteUrl, method, port, headers = {}, transport }) {
1496
+ this.type = TYPES.WEB
1497
+ headerAttributes.collectRequestHeaders(headers, this)
1498
+
1499
+ if (method != null) {
1500
+ this.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'request.method', method)
1501
+ this.baseSegment.addSpanAttribute('request.method', method)
1502
+ this.verb = method
1503
+ }
1504
+
1505
+ this.port = port
1506
+
1507
+ // the error tracer needs a URL for tracing, even though naming overwrites
1508
+ try {
1509
+ const parsedUrl = new URL(absoluteUrl)
1510
+ const data = urltils.scrubAndParseParameters(parsedUrl)
1511
+ this.url = data.path
1512
+ this.addRequestParameters(data.parameters)
1513
+ } catch (err) {
1514
+ logger.debug('Could not parse URL %s: %s', absoluteUrl, err.message)
1515
+ this.url = '/unknown'
1516
+ }
1517
+
1518
+ // need to set any config-driven names early for RUM
1519
+ logger.trace({ url: this.url, transaction: this.id },
1520
+ 'Appplying user naming rules for RUM.')
1521
+
1522
+ this.applyUserNamingRules(this.url)
1523
+
1524
+ const queueTimeStamp = headerProcessing.getQueueTime(logger, headers)
1525
+ if (queueTimeStamp) {
1526
+ this.queueTime = Date.now() - queueTimeStamp
1527
+ }
1528
+
1529
+ synthetics.assignHeadersToTransaction(this.agent.config, this, headers)
1530
+ this.addDtCatHeaders({ headers, transport })
1531
+ }
1532
+
1533
+ /**
1534
+ * Handles accepting upstream DT headers(traceparent/tracestate or newrelic) if present and DT is enabled.
1535
+ * If CAT is enabled, handles accepting CAT headers and assigns to transaction.
1536
+ *
1537
+ * @param {object} params - Parameters for adding DT or CAT headers.
1538
+ * @param {object} params.headers - HTTP headers of the request(must be lower-cased, by default http instrumentation has headers lower case, other libraries must pass this in lowercased).
1539
+ * @param {string} [params.transport] - Transport type that delivered the request.
1540
+ */
1541
+ Transaction.prototype.addDtCatHeaders = function addDtCatHeaders({ headers, transport }) {
1542
+ if (this.agent.config.distributed_tracing.enabled) {
1543
+ // Node http request headers are automatically lowercase
1544
+ // need to pass in lower case for other instrumentation
1545
+ this.acceptDistributedTraceHeaders(transport, headers)
1546
+ } else if (this.agent.config.cross_application_tracer.enabled) {
1547
+ const { id, transactionId } = cat.extractCatHeaders(headers)
1548
+ const { externalId, externalTransaction } = cat.parseCatData(
1549
+ id,
1550
+ transactionId,
1551
+ this.agent.config.encoding_key
1552
+ )
1553
+ cat.assignCatToTransaction(externalId, externalTransaction, this)
1554
+ }
1555
+ }
1556
+
1456
1557
  module.exports = Transaction
@@ -133,13 +133,14 @@ TraceSegment.prototype.setNameFromTransaction = function setNameFromTransaction(
133
133
  /**
134
134
  * Once a transaction is named, the web segment also needs to be updated to
135
135
  * match it (which implies this method must be called subsequent to
136
- * transaction.finalizeNameFromUri). To properly name apdex metrics during metric
136
+ * transaction.finalizeNameFromWeb). To properly name apdex metrics during metric
137
137
  * recording, it's also necessary to copy the transaction's partial name. And
138
138
  * finally, marking the trace segment as being a web segment copies the
139
139
  * segment's parameters onto the transaction.
140
140
  * @param {Transaction} transaction The transaction to which this segment will be bound.
141
+ * @param obfuscatedUrl
141
142
  */
142
- TraceSegment.prototype.markAsWeb = function markAsWeb(transaction) {
143
+ TraceSegment.prototype.markAsWeb = function markAsWeb(transaction, obfuscatedUrl) {
143
144
  this.setNameFromTransaction(transaction)
144
145
 
145
146
  const traceAttrs = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE)
@@ -148,6 +149,8 @@ TraceSegment.prototype.markAsWeb = function markAsWeb(transaction) {
148
149
  this.addAttribute(key, traceAttrs[key])
149
150
  }
150
151
  })
152
+
153
+ this.addSpanAttribute('request.uri', obfuscatedUrl)
151
154
  }
152
155
 
153
156
  /**