dd-trace 5.11.0 → 5.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.11.0",
3
+ "version": "5.12.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -7,11 +7,16 @@ exports.createWrapFetch = function createWrapFetch (Request, ch) {
7
7
  return function (input, init) {
8
8
  if (!ch.start.hasSubscribers) return fetch.apply(this, arguments)
9
9
 
10
- const req = new Request(input, init)
11
- const headers = req.headers
12
- const ctx = { req, headers }
10
+ if (input instanceof Request) {
11
+ const ctx = { req: input }
13
12
 
14
- return ch.tracePromise(() => fetch.call(this, req, { headers: ctx.headers }), ctx)
13
+ return ch.tracePromise(() => fetch.call(this, input, init), ctx)
14
+ } else {
15
+ const req = new Request(input, init)
16
+ const ctx = { req }
17
+
18
+ return ch.tracePromise(() => fetch.call(this, req), ctx)
19
+ }
15
20
  }
16
21
  }
17
22
  }
@@ -10,6 +10,98 @@ const startCh = channel('apm:openai:request:start')
10
10
  const finishCh = channel('apm:openai:request:finish')
11
11
  const errorCh = channel('apm:openai:request:error')
12
12
 
13
+ const V4_PACKAGE_SHIMS = [
14
+ {
15
+ file: 'resources/chat/completions.js',
16
+ targetClass: 'Completions',
17
+ baseResource: 'chat.completions',
18
+ methods: ['create']
19
+ },
20
+ {
21
+ file: 'resources/completions.js',
22
+ targetClass: 'Completions',
23
+ baseResource: 'completions',
24
+ methods: ['create']
25
+ },
26
+ {
27
+ file: 'resources/embeddings.js',
28
+ targetClass: 'Embeddings',
29
+ baseResource: 'embeddings',
30
+ methods: ['create']
31
+ },
32
+ {
33
+ file: 'resources/files.js',
34
+ targetClass: 'Files',
35
+ baseResource: 'files',
36
+ methods: ['create', 'del', 'list', 'retrieve']
37
+ },
38
+ {
39
+ file: 'resources/files.js',
40
+ targetClass: 'Files',
41
+ baseResource: 'files',
42
+ methods: ['retrieveContent'],
43
+ versions: ['>=4.0.0 <4.17.1']
44
+ },
45
+ {
46
+ file: 'resources/files.js',
47
+ targetClass: 'Files',
48
+ baseResource: 'files',
49
+ methods: ['content'], // replaced `retrieveContent` in v4.17.1
50
+ versions: ['>=4.17.1']
51
+ },
52
+ {
53
+ file: 'resources/images.js',
54
+ targetClass: 'Images',
55
+ baseResource: 'images',
56
+ methods: ['createVariation', 'edit', 'generate']
57
+ },
58
+ {
59
+ file: 'resources/fine-tuning/jobs/jobs.js',
60
+ targetClass: 'Jobs',
61
+ baseResource: 'fine_tuning.jobs',
62
+ methods: ['cancel', 'create', 'list', 'listEvents', 'retrieve'],
63
+ versions: ['>=4.34.0'] // file location changed in 4.34.0
64
+ },
65
+ {
66
+ file: 'resources/fine-tuning/jobs.js',
67
+ targetClass: 'Jobs',
68
+ baseResource: 'fine_tuning.jobs',
69
+ methods: ['cancel', 'create', 'list', 'listEvents', 'retrieve'],
70
+ versions: ['>=4.1.0 <4.34.0']
71
+ },
72
+ {
73
+ file: 'resources/fine-tunes.js', // deprecated after 4.1.0
74
+ targetClass: 'FineTunes',
75
+ baseResource: 'fine-tune',
76
+ methods: ['cancel', 'create', 'list', 'listEvents', 'retrieve'],
77
+ versions: ['>=4.0.0 <4.1.0']
78
+ },
79
+ {
80
+ file: 'resources/models.js',
81
+ targetClass: 'Models',
82
+ baseResource: 'models',
83
+ methods: ['del', 'list', 'retrieve']
84
+ },
85
+ {
86
+ file: 'resources/moderations.js',
87
+ targetClass: 'Moderations',
88
+ baseResource: 'moderations',
89
+ methods: ['create']
90
+ },
91
+ {
92
+ file: 'resources/audio/transcriptions.js',
93
+ targetClass: 'Transcriptions',
94
+ baseResource: 'audio.transcriptions',
95
+ methods: ['create']
96
+ },
97
+ {
98
+ file: 'resources/audio/translations.js',
99
+ targetClass: 'Translations',
100
+ baseResource: 'audio.translations',
101
+ methods: ['create']
102
+ }
103
+ ]
104
+
13
105
  addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, exports => {
14
106
  const methodNames = Object.getOwnPropertyNames(exports.OpenAIApi.prototype)
15
107
  methodNames.shift() // remove leading 'constructor' method
@@ -48,3 +140,60 @@ addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, expor
48
140
 
49
141
  return exports
50
142
  })
143
+
144
+ for (const shim of V4_PACKAGE_SHIMS) {
145
+ const { file, targetClass, baseResource, methods } = shim
146
+ addHook({ name: 'openai', file, versions: shim.versions || ['>=4'] }, exports => {
147
+ const targetPrototype = exports[targetClass].prototype
148
+
149
+ for (const methodName of methods) {
150
+ shimmer.wrap(targetPrototype, methodName, methodFn => function () {
151
+ if (!startCh.hasSubscribers) {
152
+ return methodFn.apply(this, arguments)
153
+ }
154
+
155
+ const client = this._client || this.client
156
+
157
+ startCh.publish({
158
+ methodName: `${baseResource}.${methodName}`,
159
+ args: arguments,
160
+ basePath: client.baseURL,
161
+ apiKey: client.apiKey
162
+ })
163
+
164
+ const apiProm = methodFn.apply(this, arguments)
165
+
166
+ // wrapping `parse` avoids problematic wrapping of `then` when trying to call
167
+ // `withResponse` in userland code after. This way, we can return the whole `APIPromise`
168
+ shimmer.wrap(apiProm, 'parse', origApiPromParse => function () {
169
+ return origApiPromParse.apply(this, arguments)
170
+ // the original response is wrapped in a promise, so we need to unwrap it
171
+ .then(body => Promise.all([this.responsePromise, body]))
172
+ .then(([{ response, options }, body]) => {
173
+ finishCh.publish({
174
+ headers: response.headers,
175
+ body,
176
+ path: response.url,
177
+ method: options.method
178
+ })
179
+
180
+ return body
181
+ })
182
+ .catch(err => {
183
+ errorCh.publish({ err })
184
+
185
+ throw err
186
+ })
187
+ .finally(() => {
188
+ // maybe we don't want to unwrap here in case the promise is re-used?
189
+ // other hand: we want to avoid resource leakage
190
+ shimmer.unwrap(apiProm, 'parse')
191
+ })
192
+ })
193
+
194
+ return apiProm
195
+ })
196
+ }
197
+ return exports
198
+ })
199
+ }
@@ -17,8 +17,11 @@ class FetchPlugin extends HttpClientPlugin {
17
17
 
18
18
  const store = super.bindStart(ctx)
19
19
 
20
- ctx.headers = headers
21
- ctx.req = new globalThis.Request(req, { headers })
20
+ for (const name in headers) {
21
+ if (!req.headers.has(name)) {
22
+ req.headers.set(name, headers[name])
23
+ }
24
+ }
22
25
 
23
26
  return store
24
27
  }
@@ -84,7 +84,7 @@ class OpenApiPlugin extends TracingPlugin {
84
84
  const tags = {} // The remaining tags are added one at a time
85
85
 
86
86
  // createChatCompletion, createCompletion, createImage, createImageEdit, createTranscription, createTranslation
87
- if ('prompt' in payload) {
87
+ if (payload.prompt) {
88
88
  const prompt = payload.prompt
89
89
  store.prompt = prompt
90
90
  if (typeof prompt === 'string' || (Array.isArray(prompt) && typeof prompt[0] === 'number')) {
@@ -99,7 +99,7 @@ class OpenApiPlugin extends TracingPlugin {
99
99
  }
100
100
 
101
101
  // createEdit, createEmbedding, createModeration
102
- if ('input' in payload) {
102
+ if (payload.input) {
103
103
  const normalized = normalizeStringOrTokenArray(payload.input, false)
104
104
  tags['openai.request.input'] = truncateText(normalized)
105
105
  store.input = normalized
@@ -114,41 +114,60 @@ class OpenApiPlugin extends TracingPlugin {
114
114
 
115
115
  switch (methodName) {
116
116
  case 'createFineTune':
117
+ case 'fine_tuning.jobs.create':
118
+ case 'fine-tune.create':
117
119
  createFineTuneRequestExtraction(tags, payload)
118
120
  break
119
121
 
120
122
  case 'createImage':
123
+ case 'images.generate':
121
124
  case 'createImageEdit':
125
+ case 'images.edit':
122
126
  case 'createImageVariation':
127
+ case 'images.createVariation':
123
128
  commonCreateImageRequestExtraction(tags, payload, store)
124
129
  break
125
130
 
126
131
  case 'createChatCompletion':
132
+ case 'chat.completions.create':
127
133
  createChatCompletionRequestExtraction(tags, payload, store)
128
134
  break
129
135
 
130
136
  case 'createFile':
137
+ case 'files.create':
131
138
  case 'retrieveFile':
139
+ case 'files.retrieve':
132
140
  commonFileRequestExtraction(tags, payload)
133
141
  break
134
142
 
135
143
  case 'createTranscription':
144
+ case 'audio.transcriptions.create':
136
145
  case 'createTranslation':
146
+ case 'audio.translations.create':
137
147
  commonCreateAudioRequestExtraction(tags, payload, store)
138
148
  break
139
149
 
140
150
  case 'retrieveModel':
151
+ case 'models.retrieve':
141
152
  retrieveModelRequestExtraction(tags, payload)
142
153
  break
143
154
 
144
155
  case 'listFineTuneEvents':
156
+ case 'fine_tuning.jobs.listEvents':
157
+ case 'fine-tune.listEvents':
145
158
  case 'retrieveFineTune':
159
+ case 'fine_tuning.jobs.retrieve':
160
+ case 'fine-tune.retrieve':
146
161
  case 'deleteModel':
162
+ case 'models.del':
147
163
  case 'cancelFineTune':
164
+ case 'fine_tuning.jobs.cancel':
165
+ case 'fine-tune.cancel':
148
166
  commonLookupFineTuneRequestExtraction(tags, payload)
149
167
  break
150
168
 
151
169
  case 'createEdit':
170
+ case 'edits.create':
152
171
  createEditRequestExtraction(tags, payload, store)
153
172
  break
154
173
  }
@@ -157,6 +176,10 @@ class OpenApiPlugin extends TracingPlugin {
157
176
  }
158
177
 
159
178
  finish ({ headers, body, method, path }) {
179
+ if (headers.constructor.name === 'Headers') {
180
+ headers = Object.fromEntries(headers)
181
+ }
182
+
160
183
  const span = this.activeSpan
161
184
  const methodName = span._spanContext._tags['resource.name']
162
185
 
@@ -165,11 +188,16 @@ class OpenApiPlugin extends TracingPlugin {
165
188
  const fullStore = storage.getStore()
166
189
  const store = fullStore.openai
167
190
 
191
+ if (path.startsWith('https://') || path.startsWith('http://')) {
192
+ // basic checking for if the path was set as a full URL
193
+ // not using a full regex as it will likely be "https://api.openai.com/..."
194
+ path = new URL(path).pathname
195
+ }
168
196
  const endpoint = lookupOperationEndpoint(methodName, path)
169
197
 
170
198
  const tags = {
171
199
  'openai.request.endpoint': endpoint,
172
- 'openai.request.method': method,
200
+ 'openai.request.method': method.toUpperCase(),
173
201
 
174
202
  'openai.organization.id': body.organization_id, // only available in fine-tunes endpoints
175
203
  'openai.organization.name': headers['openai-organization'],
@@ -220,7 +248,7 @@ class OpenApiPlugin extends TracingPlugin {
220
248
 
221
249
  this.metrics.distribution('openai.request.duration', duration * 1000, tags)
222
250
 
223
- if (body && ('usage' in body)) {
251
+ if (body && body.usage) {
224
252
  const promptTokens = body.usage.prompt_tokens
225
253
  const completionTokens = body.usage.completion_tokens
226
254
  this.metrics.distribution('openai.tokens.prompt', promptTokens, tags)
@@ -228,19 +256,19 @@ class OpenApiPlugin extends TracingPlugin {
228
256
  this.metrics.distribution('openai.tokens.total', promptTokens + completionTokens, tags)
229
257
  }
230
258
 
231
- if ('x-ratelimit-limit-requests' in headers) {
259
+ if (headers['x-ratelimit-limit-requests']) {
232
260
  this.metrics.gauge('openai.ratelimit.requests', Number(headers['x-ratelimit-limit-requests']), tags)
233
261
  }
234
262
 
235
- if ('x-ratelimit-remaining-requests' in headers) {
263
+ if (headers['x-ratelimit-remaining-requests']) {
236
264
  this.metrics.gauge('openai.ratelimit.remaining.requests', Number(headers['x-ratelimit-remaining-requests']), tags)
237
265
  }
238
266
 
239
- if ('x-ratelimit-limit-tokens' in headers) {
267
+ if (headers['x-ratelimit-limit-tokens']) {
240
268
  this.metrics.gauge('openai.ratelimit.tokens', Number(headers['x-ratelimit-limit-tokens']), tags)
241
269
  }
242
270
 
243
- if ('x-ratelimit-remaining-tokens' in headers) {
271
+ if (headers['x-ratelimit-remaining-tokens']) {
244
272
  this.metrics.gauge('openai.ratelimit.remaining.tokens', Number(headers['x-ratelimit-remaining-tokens']), tags)
245
273
  }
246
274
  }
@@ -275,17 +303,18 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
275
303
  store.messages = payload.messages
276
304
  for (let i = 0; i < payload.messages.length; i++) {
277
305
  const message = payload.messages[i]
278
- tags[`openai.request.${i}.content`] = truncateText(message.content)
279
- tags[`openai.request.${i}.role`] = message.role
280
- tags[`openai.request.${i}.name`] = message.name
281
- tags[`openai.request.${i}.finish_reason`] = message.finish_reason
306
+ tags[`openai.request.messages.${i}.content`] = truncateText(message.content)
307
+ tags[`openai.request.messages.${i}.role`] = message.role
308
+ tags[`openai.request.messages.${i}.name`] = message.name
309
+ tags[`openai.request.messages.${i}.finish_reason`] = message.finish_reason
282
310
  }
283
311
  }
284
312
 
285
313
  function commonCreateImageRequestExtraction (tags, payload, store) {
286
314
  // createImageEdit, createImageVariation
287
- if (payload.file && typeof payload.file === 'object' && payload.file.path) {
288
- const file = path.basename(payload.file.path)
315
+ const img = payload.file || payload.image
316
+ if (img && typeof img === 'object' && img.path) {
317
+ const file = path.basename(img.path)
289
318
  tags['openai.request.image'] = file
290
319
  store.file = file
291
320
  }
@@ -305,60 +334,88 @@ function commonCreateImageRequestExtraction (tags, payload, store) {
305
334
  function responseDataExtractionByMethod (methodName, tags, body, store) {
306
335
  switch (methodName) {
307
336
  case 'createModeration':
337
+ case 'moderations.create':
308
338
  createModerationResponseExtraction(tags, body)
309
339
  break
310
340
 
311
341
  case 'createCompletion':
342
+ case 'completions.create':
312
343
  case 'createChatCompletion':
344
+ case 'chat.completions.create':
313
345
  case 'createEdit':
346
+ case 'edits.create':
314
347
  commonCreateResponseExtraction(tags, body, store)
315
348
  break
316
349
 
317
350
  case 'listFiles':
351
+ case 'files.list':
318
352
  case 'listFineTunes':
353
+ case 'fine_tuning.jobs.list':
354
+ case 'fine-tune.list':
319
355
  case 'listFineTuneEvents':
356
+ case 'fine_tuning.jobs.listEvents':
357
+ case 'fine-tune.listEvents':
320
358
  commonListCountResponseExtraction(tags, body)
321
359
  break
322
360
 
323
361
  case 'createEmbedding':
362
+ case 'embeddings.create':
324
363
  createEmbeddingResponseExtraction(tags, body)
325
364
  break
326
365
 
327
366
  case 'createFile':
367
+ case 'files.create':
328
368
  case 'retrieveFile':
369
+ case 'files.retrieve':
329
370
  createRetrieveFileResponseExtraction(tags, body)
330
371
  break
331
372
 
332
373
  case 'deleteFile':
374
+ case 'files.del':
333
375
  deleteFileResponseExtraction(tags, body)
334
376
  break
335
377
 
336
378
  case 'downloadFile':
379
+ case 'files.retrieveContent':
380
+ case 'files.content':
337
381
  downloadFileResponseExtraction(tags, body)
338
382
  break
339
383
 
340
384
  case 'createFineTune':
385
+ case 'fine_tuning.jobs.create':
386
+ case 'fine-tune.create':
341
387
  case 'retrieveFineTune':
388
+ case 'fine_tuning.jobs.retrieve':
389
+ case 'fine-tune.retrieve':
342
390
  case 'cancelFineTune':
391
+ case 'fine_tuning.jobs.cancel':
392
+ case 'fine-tune.cancel':
343
393
  commonFineTuneResponseExtraction(tags, body)
344
394
  break
345
395
 
346
396
  case 'createTranscription':
397
+ case 'audio.transcriptions.create':
347
398
  case 'createTranslation':
399
+ case 'audio.translations.create':
348
400
  createAudioResponseExtraction(tags, body)
349
401
  break
350
402
 
351
403
  case 'createImage':
404
+ case 'images.generate':
352
405
  case 'createImageEdit':
406
+ case 'images.edit':
353
407
  case 'createImageVariation':
408
+ case 'images.createVariation':
354
409
  commonImageResponseExtraction(tags, body)
355
410
  break
356
411
 
357
412
  case 'listModels':
413
+ case 'models.list':
358
414
  listModelsResponseExtraction(tags, body)
359
415
  break
360
416
 
361
417
  case 'retrieveModel':
418
+ case 'models.retrieve':
362
419
  retrieveModelResponseExtraction(tags, body)
363
420
  break
364
421
  }
@@ -431,15 +488,19 @@ function createFineTuneRequestExtraction (tags, body) {
431
488
  function commonFineTuneResponseExtraction (tags, body) {
432
489
  tags['openai.response.events_count'] = defensiveArrayLength(body.events)
433
490
  tags['openai.response.fine_tuned_model'] = body.fine_tuned_model
434
- if (body.hyperparams) {
435
- tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs
436
- tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size
437
- tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight
438
- tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier
491
+
492
+ const hyperparams = body.hyperparams || body.hyperparameters
493
+ const hyperparamsKey = body.hyperparams ? 'hyperparams' : 'hyperparameters'
494
+
495
+ if (hyperparams) {
496
+ tags[`openai.response.${hyperparamsKey}.n_epochs`] = hyperparams.n_epochs
497
+ tags[`openai.response.${hyperparamsKey}.batch_size`] = hyperparams.batch_size
498
+ tags[`openai.response.${hyperparamsKey}.prompt_loss_weight`] = hyperparams.prompt_loss_weight
499
+ tags[`openai.response.${hyperparamsKey}.learning_rate_multiplier`] = hyperparams.learning_rate_multiplier
439
500
  }
440
- tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files)
501
+ tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files || body.training_file)
441
502
  tags['openai.response.result_files_count'] = defensiveArrayLength(body.result_files)
442
- tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files)
503
+ tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files || body.validation_file)
443
504
  tags['openai.response.updated_at'] = body.updated_at
444
505
  tags['openai.response.status'] = body.status
445
506
  }
@@ -528,18 +589,31 @@ function commonCreateResponseExtraction (tags, body, store) {
528
589
 
529
590
  store.choices = body.choices
530
591
 
531
- for (let i = 0; i < body.choices.length; i++) {
532
- const choice = body.choices[i]
533
- tags[`openai.response.choices.${i}.finish_reason`] = choice.finish_reason
534
- tags[`openai.response.choices.${i}.logprobs`] = ('logprobs' in choice) ? 'returned' : undefined
535
- tags[`openai.response.choices.${i}.text`] = truncateText(choice.text)
592
+ for (let choiceIdx = 0; choiceIdx < body.choices.length; choiceIdx++) {
593
+ const choice = body.choices[choiceIdx]
594
+
595
+ // logprobs can be nullm and we still want to tag it as 'returned' even when set to 'null'
596
+ const specifiesLogProb = Object.keys(choice).indexOf('logprobs') !== -1
597
+
598
+ tags[`openai.response.choices.${choiceIdx}.finish_reason`] = choice.finish_reason
599
+ tags[`openai.response.choices.${choiceIdx}.logprobs`] = specifiesLogProb ? 'returned' : undefined
600
+ tags[`openai.response.choices.${choiceIdx}.text`] = truncateText(choice.text)
536
601
 
537
602
  // createChatCompletion only
538
- if ('message' in choice) {
603
+ if (choice.message) {
539
604
  const message = choice.message
540
- tags[`openai.response.choices.${i}.message.role`] = message.role
541
- tags[`openai.response.choices.${i}.message.content`] = truncateText(message.content)
542
- tags[`openai.response.choices.${i}.message.name`] = truncateText(message.name)
605
+ tags[`openai.response.choices.${choiceIdx}.message.role`] = message.role
606
+ tags[`openai.response.choices.${choiceIdx}.message.content`] = truncateText(message.content)
607
+ tags[`openai.response.choices.${choiceIdx}.message.name`] = truncateText(message.name)
608
+ if (message.tool_calls) {
609
+ const toolCalls = message.tool_calls
610
+ for (let toolIdx = 0; toolIdx < toolCalls.length; toolIdx++) {
611
+ tags[`openai.response.choices.${choiceIdx}.message.tool_calls.${toolIdx}.name`] =
612
+ toolCalls[toolIdx].function.name
613
+ tags[`openai.response.choices.${choiceIdx}.message.tool_calls.${toolIdx}.arguments`] =
614
+ toolCalls[toolIdx].function.arguments
615
+ }
616
+ }
543
617
  }
544
618
  }
545
619
  }
@@ -577,34 +651,62 @@ function truncateText (text) {
577
651
  function coerceResponseBody (body, methodName) {
578
652
  switch (methodName) {
579
653
  case 'downloadFile':
654
+ case 'files.retrieveContent':
655
+ case 'files.content':
580
656
  return { file: body }
581
657
  }
582
658
 
583
- return typeof body === 'object' ? body : {}
659
+ const type = typeof body
660
+ if (type === 'string') {
661
+ try {
662
+ return JSON.parse(body)
663
+ } catch {
664
+ return body
665
+ }
666
+ } else if (type === 'object') {
667
+ return body
668
+ } else {
669
+ return {}
670
+ }
584
671
  }
585
672
 
586
673
  // This method is used to replace a dynamic URL segment with an asterisk
587
674
  function lookupOperationEndpoint (operationId, url) {
588
675
  switch (operationId) {
589
676
  case 'deleteModel':
677
+ case 'models.del':
590
678
  case 'retrieveModel':
679
+ case 'models.retrieve':
591
680
  return '/v1/models/*'
592
681
 
593
682
  case 'deleteFile':
683
+ case 'files.del':
594
684
  case 'retrieveFile':
685
+ case 'files.retrieve':
595
686
  return '/v1/files/*'
596
687
 
597
688
  case 'downloadFile':
689
+ case 'files.retrieveContent':
690
+ case 'files.content':
598
691
  return '/v1/files/*/content'
599
692
 
600
693
  case 'retrieveFineTune':
694
+ case 'fine-tune.retrieve':
601
695
  return '/v1/fine-tunes/*'
696
+ case 'fine_tuning.jobs.retrieve':
697
+ return '/v1/fine_tuning/jobs/*'
602
698
 
603
699
  case 'listFineTuneEvents':
700
+ case 'fine-tune.listEvents':
604
701
  return '/v1/fine-tunes/*/events'
702
+ case 'fine_tuning.jobs.listEvents':
703
+ return '/v1/fine_tuning/jobs/*/events'
605
704
 
606
705
  case 'cancelFineTune':
706
+ case 'fine-tune.cancel':
607
707
  return '/v1/fine-tunes/*/cancel'
708
+ case 'fine_tuning.jobs.cancel':
709
+ return '/v1/fine_tuning/jobs/*/cancel'
608
710
  }
609
711
 
610
712
  return url
@@ -618,12 +720,17 @@ function lookupOperationEndpoint (operationId, url) {
618
720
  function normalizeRequestPayload (methodName, args) {
619
721
  switch (methodName) {
620
722
  case 'listModels':
723
+ case 'models.list':
621
724
  case 'listFiles':
725
+ case 'files.list':
622
726
  case 'listFineTunes':
727
+ case 'fine_tuning.jobs.list':
728
+ case 'fine-tune.list':
623
729
  // no argument
624
730
  return {}
625
731
 
626
732
  case 'retrieveModel':
733
+ case 'models.retrieve':
627
734
  return { id: args[0] }
628
735
 
629
736
  case 'createFile':
@@ -633,19 +740,30 @@ function normalizeRequestPayload (methodName, args) {
633
740
  }
634
741
 
635
742
  case 'deleteFile':
743
+ case 'files.del':
636
744
  case 'retrieveFile':
745
+ case 'files.retrieve':
637
746
  case 'downloadFile':
747
+ case 'files.retrieveContent':
748
+ case 'files.content':
638
749
  return { file_id: args[0] }
639
750
 
640
751
  case 'listFineTuneEvents':
752
+ case 'fine_tuning.jobs.listEvents':
753
+ case 'fine-tune.listEvents':
641
754
  return {
642
755
  fine_tune_id: args[0],
643
756
  stream: args[1] // undocumented
644
757
  }
645
758
 
646
759
  case 'retrieveFineTune':
760
+ case 'fine_tuning.jobs.retrieve':
761
+ case 'fine-tune.retrieve':
647
762
  case 'deleteModel':
763
+ case 'models.del':
648
764
  case 'cancelFineTune':
765
+ case 'fine_tuning.jobs.cancel':
766
+ case 'fine-tune.cancel':
649
767
  return { fine_tune_id: args[0] }
650
768
 
651
769
  case 'createImageEdit':
@@ -702,7 +820,16 @@ function normalizeStringOrTokenArray (input, truncate) {
702
820
  }
703
821
 
704
822
  function defensiveArrayLength (maybeArray) {
705
- return Array.isArray(maybeArray) ? maybeArray.length : undefined
823
+ if (maybeArray) {
824
+ if (Array.isArray(maybeArray)) {
825
+ return maybeArray.length
826
+ } else {
827
+ // case of a singular item (ie body.training_file vs body.training_files)
828
+ return 1
829
+ }
830
+ }
831
+
832
+ return undefined
706
833
  }
707
834
 
708
835
  module.exports = OpenApiPlugin
@@ -4,7 +4,7 @@ const TaintedUtils = require('@datadog/native-iast-taint-tracking')
4
4
  const { IAST_TRANSACTION_ID } = require('../iast-context')
5
5
  const iastLog = require('../iast-log')
6
6
 
7
- function taintObject (iastContext, object, type, keyTainting, keyType) {
7
+ function taintObject (iastContext, object, type) {
8
8
  let result = object
9
9
  const transactionId = iastContext?.[IAST_TRANSACTION_ID]
10
10
  if (transactionId) {
@@ -22,9 +22,6 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
22
22
  const tainted = TaintedUtils.newTaintedString(transactionId, value, property, type)
23
23
  if (!parent) {
24
24
  result = tainted
25
- } else if (keyTainting && key) {
26
- const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
27
- parent[taintedProperty] = tainted
28
25
  } else {
29
26
  parent[key] = tainted
30
27
  }
@@ -34,11 +31,6 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
34
31
  for (const key of Object.keys(value)) {
35
32
  queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key], key })
36
33
  }
37
-
38
- if (parent && keyTainting && key) {
39
- const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
40
- parent[taintedProperty] = value
41
- }
42
34
  }
43
35
  } catch (e) {
44
36
  iastLog.error(`Error visiting property : ${property}`).errorAndPublish(e)
@@ -95,12 +95,14 @@ class TaintTrackingPlugin extends SourceIastPlugin {
95
95
 
96
96
  _cookiesTaintTrackingHandler (target) {
97
97
  const iastContext = getIastContext(storage.getStore())
98
- taintObject(iastContext, target, HTTP_REQUEST_COOKIE_VALUE, true, HTTP_REQUEST_COOKIE_NAME)
98
+ // Prevent tainting cookie names since it leads to taint literal string with same value.
99
+ taintObject(iastContext, target, HTTP_REQUEST_COOKIE_VALUE)
99
100
  }
100
101
 
101
102
  taintHeaders (headers, iastContext) {
103
+ // Prevent tainting header names since it leads to taint literal string with same value.
102
104
  this.execSource({
103
- handler: () => taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE, true, HTTP_REQUEST_HEADER_NAME),
105
+ handler: () => taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE),
104
106
  tags: REQ_HEADER_TAGS,
105
107
  iastContext
106
108
  })
@@ -8,7 +8,7 @@ const apiSecuritySampler = require('../api_security_sampler')
8
8
 
9
9
  let rc
10
10
 
11
- function enable (config) {
11
+ function enable (config, appsec) {
12
12
  rc = new RemoteConfigManager(config)
13
13
  rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true)
14
14
  rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true)
@@ -31,7 +31,7 @@ function enable (config) {
31
31
  if (!rcConfig) return
32
32
 
33
33
  if (activation === Activation.ONECLICK) {
34
- enableOrDisableAppsec(action, rcConfig, config)
34
+ enableOrDisableAppsec(action, rcConfig, config, appsec)
35
35
  }
36
36
 
37
37
  apiSecuritySampler.setRequestSampling(rcConfig.api_security?.request_sample_rate)
@@ -41,7 +41,7 @@ function enable (config) {
41
41
  return rc
42
42
  }
43
43
 
44
- function enableOrDisableAppsec (action, rcConfig, config) {
44
+ function enableOrDisableAppsec (action, rcConfig, config, appsec) {
45
45
  if (typeof rcConfig.asm?.enabled === 'boolean') {
46
46
  let shouldEnable
47
47
 
@@ -52,9 +52,9 @@ function enableOrDisableAppsec (action, rcConfig, config) {
52
52
  }
53
53
 
54
54
  if (shouldEnable) {
55
- require('..').enable(config)
55
+ appsec.enable(config)
56
56
  } else {
57
- require('..').disable()
57
+ appsec.disable()
58
58
  }
59
59
  }
60
60
  }
@@ -15,6 +15,21 @@ const NoopDogStatsDClient = require('./noop/dogstatsd')
15
15
  const spanleak = require('./spanleak')
16
16
  const { SSITelemetry } = require('./profiling/ssi-telemetry')
17
17
 
18
+ class LazyModule {
19
+ constructor (provider) {
20
+ this.provider = provider
21
+ }
22
+
23
+ enable (...args) {
24
+ this.module = this.provider()
25
+ this.module.enable(...args)
26
+ }
27
+
28
+ disable () {
29
+ this.module?.disable()
30
+ }
31
+ }
32
+
18
33
  class Tracer extends NoopProxy {
19
34
  constructor () {
20
35
  super()
@@ -24,6 +39,12 @@ class Tracer extends NoopProxy {
24
39
  this._pluginManager = new PluginManager(this)
25
40
  this.dogstatsd = new NoopDogStatsDClient()
26
41
  this._tracingInitialized = false
42
+
43
+ // these requires must work with esm bundler
44
+ this._modules = {
45
+ appsec: new LazyModule(() => require('./appsec')),
46
+ iast: new LazyModule(() => require('./appsec/iast'))
47
+ }
27
48
  }
28
49
 
29
50
  init (options) {
@@ -58,7 +79,7 @@ class Tracer extends NoopProxy {
58
79
  }
59
80
 
60
81
  if (config.remoteConfig.enabled && !config.isCiVisibility) {
61
- const rc = remoteConfig.enable(config)
82
+ const rc = remoteConfig.enable(config, this._modules.appsec)
62
83
 
63
84
  rc.on('APM_TRACING', (action, conf) => {
64
85
  if (action === 'unapply') {
@@ -113,9 +134,8 @@ class Tracer extends NoopProxy {
113
134
 
114
135
  _enableOrDisableTracing (config) {
115
136
  if (config.tracing !== false) {
116
- // dirty require for now so zero appsec code is executed unless explicitly enabled
117
137
  if (config.appsec.enabled) {
118
- require('./appsec').enable(config)
138
+ this._modules.appsec.enable(config)
119
139
  }
120
140
  if (!this._tracingInitialized) {
121
141
  this._tracer = new DatadogTracer(config)
@@ -123,11 +143,11 @@ class Tracer extends NoopProxy {
123
143
  this._tracingInitialized = true
124
144
  }
125
145
  if (config.iast.enabled) {
126
- require('./appsec/iast').enable(config, this._tracer)
146
+ this._modules.iast.enable(config, this._tracer)
127
147
  }
128
148
  } else if (this._tracingInitialized) {
129
- require('./appsec').disable()
130
- require('./appsec/iast').disable()
149
+ this._modules.appsec.disable()
150
+ this._modules.iast.disable()
131
151
  }
132
152
 
133
153
  if (this._tracingInitialized) {