dd-trace 5.7.0 → 5.8.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/index.d.ts CHANGED
@@ -137,8 +137,10 @@ interface Tracer extends opentracing.Tracer {
137
137
  // is doesn't need to be exported for Tracer
138
138
  /** @hidden */
139
139
  interface Plugins {
140
+ "aerospike": tracer.plugins.aerospike;
140
141
  "amqp10": tracer.plugins.amqp10;
141
142
  "amqplib": tracer.plugins.amqplib;
143
+ "apollo": tracer.plugins.apollo;
142
144
  "aws-sdk": tracer.plugins.aws_sdk;
143
145
  "bunyan": tracer.plugins.bunyan;
144
146
  "cassandra-driver": tracer.plugins.cassandra_driver;
@@ -1098,6 +1100,12 @@ declare namespace tracer {
1098
1100
  meta?: boolean;
1099
1101
  }
1100
1102
 
1103
+ /**
1104
+ * This plugin automatically instruments the
1105
+ * [aerospike](https://github.com/aerospike/aerospike-client-nodejs) for module versions >= v3.16.2.
1106
+ */
1107
+ interface aerospike extends Instrumentation {}
1108
+
1101
1109
  /**
1102
1110
  * This plugin automatically instruments the
1103
1111
  * [amqp10](https://github.com/noodlefrenzy/node-amqp10) module.
@@ -1110,6 +1118,33 @@ declare namespace tracer {
1110
1118
  */
1111
1119
  interface amqplib extends Instrumentation {}
1112
1120
 
1121
+ /**
1122
+ * Currently this plugin automatically instruments
1123
+ * [@apollo/gateway](https://github.com/apollographql/federation) for module versions >= v2.3.0.
1124
+ * This module uses graphql operations to service requests & thus generates graphql spans.
1125
+ * We recommend disabling the graphql plugin if you only want to trace @apollo/gateway
1126
+ */
1127
+ interface apollo extends Instrumentation {
1128
+ /**
1129
+ * Whether to include the source of the operation within the query as a tag
1130
+ * on every span. This may contain sensitive information and should only be
1131
+ * enabled if sensitive data is always sent as variables and not in the
1132
+ * query text.
1133
+ *
1134
+ * @default false
1135
+ */
1136
+ source?: boolean;
1137
+
1138
+ /**
1139
+ * Whether to enable signature calculation for the resource name. This can
1140
+ * be disabled if your apollo/gateway operations always have a name. Note that when
1141
+ * disabled all queries will need to be named for this to work properly.
1142
+ *
1143
+ * @default true
1144
+ */
1145
+ signature?: boolean;
1146
+ }
1147
+
1113
1148
  /**
1114
1149
  * This plugin automatically instruments the
1115
1150
  * [aws-sdk](https://github.com/aws/aws-sdk-js) module.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.7.0",
3
+ "version": "5.8.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -0,0 +1,101 @@
1
+ const {
2
+ addHook,
3
+ channel
4
+ } = require('./helpers/instrument')
5
+ const shimmer = require('../../datadog-shimmer')
6
+ const tracingChannel = require('dc-polyfill').tracingChannel
7
+
8
+ const CHANNELS = {
9
+ 'gateway.request': tracingChannel('apm:apollo:gateway:request'),
10
+ 'gateway.plan': tracingChannel('apm:apollo:gateway:plan'),
11
+ 'gateway.validate': tracingChannel('apm:apollo:gateway:validate'),
12
+ 'gateway.execute': tracingChannel('apm:apollo:gateway:execute'),
13
+ 'gateway.fetch': tracingChannel('apm:apollo:gateway:fetch'),
14
+ 'gateway.postprocessing': tracingChannel('apm:apollo:gateway:postprocessing')
15
+ }
16
+
17
+ const executorCh = channel('apm:apollo:gateway:request:executor')
18
+ const generalErrorCh = channel('apm:apollo:gateway:general:error')
19
+
20
+ function wrapExecutor (executor) {
21
+ return function (...args) {
22
+ const ctx = { requestContext: args[0], gateway: this }
23
+ executorCh.publish(ctx)
24
+ return executor.apply(this, args)
25
+ }
26
+ }
27
+
28
+ function wrapApolloGateway (ApolloGateway) {
29
+ class ApolloGatewayWrapper extends ApolloGateway {
30
+ constructor (...args) {
31
+ super(...args)
32
+ shimmer.wrap(this, 'executor', wrapExecutor)
33
+ }
34
+ }
35
+ return ApolloGatewayWrapper
36
+ }
37
+
38
+ function wrapRecordExceptions (recordExceptions) {
39
+ return function wrappedRecordExceptions (...args) {
40
+ const errors = args[1]
41
+ // only the last exception in the array of exceptions will be reported on the span,
42
+ // this is mimicking apollo-gateways internal instrumentation
43
+ // TODO: should we consider a mechanism to report all exceptions? since this method aggregates all exceptions
44
+ // where as a span can only have one exception set on it at a time
45
+ generalErrorCh.publish({ error: errors[errors.length - 1] })
46
+ return recordExceptions.apply(this, args)
47
+ }
48
+ }
49
+
50
+ function wrapStartActiveSpan (startActiveSpan) {
51
+ return function (...args) {
52
+ const firstArg = args[0]
53
+ const cb = args[args.length - 1]
54
+ if (typeof firstArg !== 'string' || typeof cb !== 'function') return startActiveSpan.apply(this, args)
55
+
56
+ const method = CHANNELS[firstArg]
57
+ let ctx = {}
58
+ if (firstArg === 'gateway.fetch') {
59
+ ctx = { attributes: args[1].attributes }
60
+ }
61
+
62
+ switch (firstArg) {
63
+ case 'gateway.plan' :
64
+ case 'gateway.validate': {
65
+ args[args.length - 1] = function (...callbackArgs) {
66
+ return method.traceSync(cb, ctx, this, ...callbackArgs)
67
+ }
68
+ break
69
+ }
70
+ case 'gateway.request':
71
+ case 'gateway.execute':
72
+ case 'gateway.postprocessing' :
73
+ case 'gateway.fetch': {
74
+ args[args.length - 1] = function (...callbackArgs) {
75
+ return method.tracePromise(cb, ctx, this, ...callbackArgs)
76
+ }
77
+ break
78
+ }
79
+ }
80
+ return startActiveSpan.apply(this, args)
81
+ }
82
+ }
83
+
84
+ addHook({ name: '@apollo/gateway', file: 'dist/utilities/opentelemetry.js', versions: ['>=2.3.0'] },
85
+ (obj) => {
86
+ const newTracerObj = Object.create(obj.tracer)
87
+ shimmer.wrap(newTracerObj, 'startActiveSpan', wrapStartActiveSpan)
88
+ obj.tracer = newTracerObj
89
+ return obj
90
+ })
91
+
92
+ addHook({ name: '@apollo/gateway', file: 'dist/utilities/opentelemetry.js', versions: ['>=2.6.0'] },
93
+ (obj) => {
94
+ shimmer.wrap(obj, 'recordExceptions', wrapRecordExceptions)
95
+ return obj
96
+ })
97
+
98
+ addHook({ name: '@apollo/gateway', versions: ['>=2.3.0'] }, (gateway) => {
99
+ shimmer.wrap(gateway, 'ApolloGateway', wrapApolloGateway)
100
+ return gateway
101
+ })
@@ -2,6 +2,7 @@
2
2
 
3
3
  module.exports = {
4
4
  '@apollo/server': () => require('../apollo-server'),
5
+ '@apollo/gateway': () => require('../apollo'),
5
6
  'apollo-server-core': () => require('../apollo-server-core'),
6
7
  '@aws-sdk/smithy-client': () => require('../aws-sdk'),
7
8
  '@cucumber/cucumber': () => require('../cucumber'),
@@ -197,7 +197,7 @@ function instrumentPromise (operation, command, ctx, args, server, ns, ops, opti
197
197
 
198
198
  const promise = command.apply(ctx, args)
199
199
 
200
- promise.then(function (res) {
200
+ return promise.then(function (res) {
201
201
  finishCh.publish()
202
202
  return res
203
203
  }, function (err) {
@@ -206,7 +206,5 @@ function instrumentPromise (operation, command, ctx, args, server, ns, ops, opti
206
206
 
207
207
  return Promise.reject(err)
208
208
  })
209
-
210
- return promise
211
209
  })
212
210
  }
@@ -1,6 +1,9 @@
1
+ const semver = require('semver')
2
+
1
3
  const { addHook, channel, AsyncResource } = require('./helpers/instrument')
2
4
  const shimmer = require('../../datadog-shimmer')
3
- const { parseAnnotations } = require('../../dd-trace/src/plugins/util/test')
5
+ const { parseAnnotations, getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
6
+ const log = require('../../dd-trace/src/log')
4
7
 
5
8
  const testStartCh = channel('ci:playwright:test:start')
6
9
  const testFinishCh = channel('ci:playwright:test:finish')
@@ -8,6 +11,9 @@ const testFinishCh = channel('ci:playwright:test:finish')
8
11
  const testSessionStartCh = channel('ci:playwright:session:start')
9
12
  const testSessionFinishCh = channel('ci:playwright:session:finish')
10
13
 
14
+ const libraryConfigurationCh = channel('ci:playwright:library-configuration')
15
+ const knownTestsCh = channel('ci:playwright:known-tests')
16
+
11
17
  const testSuiteStartCh = channel('ci:playwright:test-suite:start')
12
18
  const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
13
19
 
@@ -16,6 +22,8 @@ const testSuiteToAr = new Map()
16
22
  const testSuiteToTestStatuses = new Map()
17
23
  const testSuiteToErrors = new Map()
18
24
 
25
+ let applyRepeatEachIndex = null
26
+
19
27
  let startedSuites = []
20
28
 
21
29
  const STATUS_TO_TEST_STATUS = {
@@ -26,6 +34,44 @@ const STATUS_TO_TEST_STATUS = {
26
34
  }
27
35
 
28
36
  let remainingTestsByFile = {}
37
+ let isEarlyFlakeDetectionEnabled = false
38
+ let earlyFlakeDetectionNumRetries = 0
39
+ let knownTests = []
40
+ let rootDir = ''
41
+ const MINIMUM_SUPPORTED_VERSION_EFD = '1.38.0'
42
+
43
+ function isNewTest (test) {
44
+ const testSuite = getTestSuitePath(test._requireFile, rootDir)
45
+ const testsForSuite = knownTests?.playwright?.[testSuite] || []
46
+
47
+ return !testsForSuite.includes(test.title)
48
+ }
49
+
50
+ function getSuiteType (test, type) {
51
+ let suite = test.parent
52
+ while (suite && suite._type !== type) {
53
+ suite = suite.parent
54
+ }
55
+ return suite
56
+ }
57
+
58
+ // Copy of Suite#_deepClone but with a function to filter tests
59
+ function deepCloneSuite (suite, filterTest) {
60
+ const copy = suite._clone()
61
+ for (const entry of suite._entries) {
62
+ if (entry.constructor.name === 'Suite') {
63
+ copy._addSuite(deepCloneSuite(entry, filterTest))
64
+ } else {
65
+ if (filterTest(entry)) {
66
+ const copiedTest = entry._clone()
67
+ copiedTest._ddIsNew = true
68
+ copiedTest._ddIsEfdRetry = true
69
+ copy._addTest(copiedTest)
70
+ }
71
+ }
72
+ }
73
+ return copy
74
+ }
29
75
 
30
76
  function getTestsBySuiteFromTestGroups (testGroups) {
31
77
  return testGroups.reduce((acc, { requireFile, tests }) => {
@@ -153,8 +199,11 @@ function getTestSuiteError (testSuiteAbsolutePath) {
153
199
  function testBeginHandler (test, browserName) {
154
200
  const {
155
201
  _requireFile: testSuiteAbsolutePath,
156
- title: testName, _type,
157
- location: { line: testSourceLine }
202
+ title: testName,
203
+ _type,
204
+ location: {
205
+ line: testSourceLine
206
+ }
158
207
  } = test
159
208
 
160
209
  if (_type === 'beforeAll' || _type === 'afterAll') {
@@ -198,7 +247,14 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout) {
198
247
  const testResult = results[results.length - 1]
199
248
  const testAsyncResource = testToAr.get(test)
200
249
  testAsyncResource.runInAsyncScope(() => {
201
- testFinishCh.publish({ testStatus, steps: testResult.steps, error, extraTags: annotationTags })
250
+ testFinishCh.publish({
251
+ testStatus,
252
+ steps: testResult.steps,
253
+ error,
254
+ extraTags: annotationTags,
255
+ isNew: test._ddIsNew,
256
+ isEfdRetry: test._ddIsEfdRetry
257
+ })
202
258
  })
203
259
 
204
260
  if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
@@ -309,14 +365,54 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
309
365
 
310
366
  function runnerHook (runnerExport, playwrightVersion) {
311
367
  shimmer.wrap(runnerExport.Runner.prototype, 'runAllTests', runAllTests => async function () {
368
+ let onDone
369
+
312
370
  const testSessionAsyncResource = new AsyncResource('bound-anonymous-fn')
313
- const rootDir = getRootDir(this)
371
+
372
+ rootDir = getRootDir(this)
314
373
 
315
374
  const processArgv = process.argv.slice(2).join(' ')
316
375
  const command = `playwright ${processArgv}`
317
376
  testSessionAsyncResource.runInAsyncScope(() => {
318
377
  testSessionStartCh.publish({ command, frameworkVersion: playwrightVersion, rootDir })
319
378
  })
379
+
380
+ const configurationPromise = new Promise((resolve) => {
381
+ onDone = resolve
382
+ })
383
+
384
+ testSessionAsyncResource.runInAsyncScope(() => {
385
+ libraryConfigurationCh.publish({ onDone })
386
+ })
387
+
388
+ try {
389
+ const { err, libraryConfig } = await configurationPromise
390
+ if (!err) {
391
+ isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
392
+ earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
393
+ }
394
+ } catch (e) {
395
+ log.error(e)
396
+ }
397
+
398
+ if (isEarlyFlakeDetectionEnabled && semver.gte(playwrightVersion, MINIMUM_SUPPORTED_VERSION_EFD)) {
399
+ const knownTestsPromise = new Promise((resolve) => {
400
+ onDone = resolve
401
+ })
402
+ testSessionAsyncResource.runInAsyncScope(() => {
403
+ knownTestsCh.publish({ onDone })
404
+ })
405
+
406
+ try {
407
+ const { err, knownTests: receivedKnownTests } = await knownTestsPromise
408
+ if (!err) {
409
+ knownTests = receivedKnownTests
410
+ }
411
+ } catch (err) {
412
+ log.error(err)
413
+ }
414
+ }
415
+
320
416
  const projects = getProjectsFromRunner(this)
321
417
 
322
418
  const runAllTestsReturn = await runAllTests.apply(this, arguments)
@@ -334,7 +430,6 @@ function runnerHook (runnerExport, playwrightVersion) {
334
430
 
335
431
  const sessionStatus = runAllTestsReturn.status || runAllTestsReturn
336
432
 
337
- let onDone
338
433
  const flushWait = new Promise(resolve => {
339
434
  onDone = resolve
340
435
  })
@@ -394,3 +489,53 @@ addHook({
394
489
  file: 'lib/runner/dispatcher.js',
395
490
  versions: ['>=1.38.0']
396
491
  }, (dispatcher) => dispatcherHookNew(dispatcher, dispatcherRunWrapperNew))
492
+
493
+ // Hook used for early flake detection. EFD only works from >=1.38.0
494
+ addHook({
495
+ name: 'playwright',
496
+ file: 'lib/common/suiteUtils.js',
497
+ versions: [`>=${MINIMUM_SUPPORTED_VERSION_EFD}`]
498
+ }, suiteUtilsPackage => {
499
+ // We grab `applyRepeatEachIndex` to use it later
500
+ // `applyRepeatEachIndex` needs to be applied to a cloned suite
501
+ applyRepeatEachIndex = suiteUtilsPackage.applyRepeatEachIndex
502
+ return suiteUtilsPackage
503
+ })
504
+
505
+ // Hook used for early flake detection. EFD only works from >=1.38.0
506
+ addHook({
507
+ name: 'playwright',
508
+ file: 'lib/runner/loadUtils.js',
509
+ versions: [`>=${MINIMUM_SUPPORTED_VERSION_EFD}`]
510
+ }, (loadUtilsPackage) => {
511
+ const oldCreateRootSuite = loadUtilsPackage.createRootSuite
512
+
513
+ async function newCreateRootSuite () {
514
+ const rootSuite = await oldCreateRootSuite.apply(this, arguments)
515
+ if (!isEarlyFlakeDetectionEnabled) {
516
+ return rootSuite
517
+ }
518
+ const newTests = rootSuite
519
+ .allTests()
520
+ .filter(isNewTest)
521
+
522
+ newTests.forEach(newTest => {
523
+ newTest._ddIsNew = true
524
+ if (newTest.expectedStatus !== 'skipped') {
525
+ const fileSuite = getSuiteType(newTest, 'file')
526
+ const projectSuite = getSuiteType(newTest, 'project')
527
+ for (let repeatEachIndex = 0; repeatEachIndex < earlyFlakeDetectionNumRetries; repeatEachIndex++) {
528
+ const copyFileSuite = deepCloneSuite(fileSuite, isNewTest)
529
+ applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
530
+ projectSuite._addSuite(copyFileSuite)
531
+ }
532
+ }
533
+ })
534
+
535
+ return rootSuite
536
+ }
537
+
538
+ loadUtilsPackage.createRootSuite = newCreateRootSuite
539
+
540
+ return loadUtilsPackage
541
+ })
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
4
+
5
+ class ApolloGatewayExecutePlugin extends ApolloBasePlugin {
6
+ static get operation () { return 'execute' }
7
+ static get prefix () {
8
+ return 'tracing:apm:apollo:gateway:execute'
9
+ }
10
+ }
11
+
12
+ module.exports = ApolloGatewayExecutePlugin
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../../datadog-core')
4
+ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
5
+
6
+ class ApolloGatewayFetchPlugin extends ApolloBasePlugin {
7
+ static get operation () { return 'fetch' }
8
+ static get prefix () {
9
+ return 'tracing:apm:apollo:gateway:fetch'
10
+ }
11
+
12
+ bindStart (ctx) {
13
+ const store = storage.getStore()
14
+ const childOf = store ? store.span : null
15
+
16
+ const spanData = {
17
+ childOf,
18
+ service: this.getServiceName(),
19
+ type: this.constructor.type,
20
+ meta: {}
21
+ }
22
+
23
+ const serviceName = ctx?.attributes?.service
24
+
25
+ if (serviceName) { spanData.meta['serviceName'] = serviceName }
26
+
27
+ const span = this.startSpan(this.getOperationName(), spanData, false)
28
+
29
+ ctx.parentStore = store
30
+ ctx.currentStore = { ...store, span }
31
+
32
+ return ctx.currentStore
33
+ }
34
+ }
35
+
36
+ module.exports = ApolloGatewayFetchPlugin
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../../datadog-core')
4
+ const CompositePlugin = require('../../../dd-trace/src/plugins/composite')
5
+ const ApolloGatewayExecutePlugin = require('./execute')
6
+ const ApolloGatewayPostProcessingPlugin = require('./postprocessing')
7
+ const ApolloGatewayRequestPlugin = require('./request')
8
+ const ApolloGatewayPlanPlugin = require('./plan')
9
+ const ApolloGatewayValidatePlugin = require('./validate')
10
+ const ApolloGatewayFetchPlugin = require('./fetch')
11
+
12
+ class ApolloGatewayPlugin extends CompositePlugin {
13
+ static get id () { return 'gateway' }
14
+ static get plugins () {
15
+ return {
16
+ execute: ApolloGatewayExecutePlugin,
17
+ postprocessing: ApolloGatewayPostProcessingPlugin,
18
+ request: ApolloGatewayRequestPlugin,
19
+ plan: ApolloGatewayPlanPlugin,
20
+ fetch: ApolloGatewayFetchPlugin,
21
+ validate: ApolloGatewayValidatePlugin
22
+ }
23
+ }
24
+
25
+ constructor (...args) {
26
+ super(...args)
27
+ this.addSub('apm:apollo:gateway:general:error', (ctx) => {
28
+ const store = storage.getStore()
29
+ const span = store?.span
30
+ if (!span) return
31
+ span.setTag('error', ctx.error)
32
+ })
33
+ }
34
+ }
35
+
36
+ module.exports = ApolloGatewayPlugin
@@ -0,0 +1,13 @@
1
+
2
+ 'use strict'
3
+
4
+ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
5
+
6
+ class ApolloGatewayPlanPlugin extends ApolloBasePlugin {
7
+ static get operation () { return 'plan' }
8
+ static get prefix () {
9
+ return 'tracing:apm:apollo:gateway:plan'
10
+ }
11
+ }
12
+
13
+ module.exports = ApolloGatewayPlanPlugin
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
4
+
5
+ class ApolloGatewayPostProcessingPlugin extends ApolloBasePlugin {
6
+ static get operation () { return 'postprocessing' }
7
+ static get prefix () {
8
+ return 'tracing:apm:apollo:gateway:postprocessing'
9
+ }
10
+ }
11
+
12
+ module.exports = ApolloGatewayPostProcessingPlugin
@@ -0,0 +1,139 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../../datadog-core')
4
+ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
5
+
6
+ let tools
7
+
8
+ const OPERATION_DEFINITION = 'OperationDefinition'
9
+ const FRAGMENT_DEFINITION = 'FragmentDefinition'
10
+
11
+ class ApolloGatewayRequestPlugin extends ApolloBasePlugin {
12
+ static get operation () { return 'request' }
13
+ static get prefix () {
14
+ return 'tracing:apm:apollo:gateway:request'
15
+ }
16
+
17
+ constructor (...args) {
18
+ super(...args)
19
+ this.addSub('apm:apollo:gateway:request:executor', (ctx) => {
20
+ if (ctx.requestContext || ctx.gateway) {
21
+ this.requestContext = ctx
22
+ } else {
23
+ this.requestContext = {}
24
+ }
25
+ })
26
+ }
27
+
28
+ bindStart (ctx) {
29
+ const store = storage.getStore()
30
+ const childOf = store ? store.span : null
31
+ const spanData = {
32
+ childOf,
33
+ service: this.serviceName(
34
+ { id: `${this.constructor.id}.${this.constructor.operation}`, pluginConfig: this.config }),
35
+ type: this.constructor.type,
36
+ kind: this.constructor.kind,
37
+ meta: {}
38
+ }
39
+
40
+ const { requestContext, gateway } = this.requestContext
41
+
42
+ if (requestContext?.operationName) {
43
+ spanData.meta['graphql.operation.name'] = requestContext.operationName
44
+ }
45
+ if ((this.config.source || gateway?.config?.telemetry?.includeDocument) && requestContext?.source) {
46
+ spanData.meta['graphql.source'] = requestContext.source
47
+ }
48
+
49
+ const operationContext =
50
+ buildOperationContext(gateway.schema, requestContext.document, requestContext.request.operationName)
51
+
52
+ if (operationContext?.operation?.operation) {
53
+ const document = requestContext?.document
54
+ const type = operationContext?.operation?.operation
55
+ const name = operationContext?.operation?.name && operationContext?.operation?.name?.value
56
+
57
+ spanData['resource'] = getSignature(document, name, type, this?.config?.signature)
58
+ spanData.meta['graphql.operation.type'] = type
59
+ }
60
+ const span = this.startSpan(this.operationName({ id: `${this.constructor.id}.${this.constructor.operation}` }),
61
+ spanData, false)
62
+
63
+ ctx.parentStore = store
64
+ ctx.currentStore = { ...store, span }
65
+ return ctx.currentStore
66
+ }
67
+
68
+ asyncStart (ctx) {
69
+ const errors = ctx?.result?.errors
70
+ // apollo gateway catches certain errors and returns them in the result object
71
+ // we want to capture these errors as spans
72
+ if (errors instanceof Array &&
73
+ errors[errors.length - 1] && errors[errors.length - 1].stack && errors[errors.length - 1].message) {
74
+ ctx.currentStore.span.setTag('error', errors[errors.length - 1])
75
+ }
76
+ ctx.currentStore.span.finish()
77
+ return ctx.parentStore
78
+ }
79
+
80
+ end () {
81
+ // do nothing to avoid ApolloBasePlugin's end method
82
+ }
83
+ }
84
+
85
+ function buildOperationContext (schema, operationDocument, operationName) {
86
+ let operation
87
+ let operationCount = 0
88
+ const fragments = Object.create(null)
89
+ try {
90
+ operationDocument.definitions.forEach(definition => {
91
+ switch (definition.kind) {
92
+ case OPERATION_DEFINITION:
93
+ operationCount++
94
+ if (!operationName && operationCount > 1) {
95
+ return
96
+ }
97
+ if (
98
+ !operationName ||
99
+ (definition.name && definition.name.value === operationName)
100
+ ) {
101
+ operation = definition
102
+ }
103
+ break
104
+ case FRAGMENT_DEFINITION:
105
+ fragments[definition.name.value] = definition
106
+ break
107
+ }
108
+ })
109
+ } catch (e) {
110
+ // safety net
111
+ }
112
+
113
+ return {
114
+ schema,
115
+ operation,
116
+ fragments
117
+ }
118
+ }
119
+
120
+ function getSignature (document, operationName, operationType, calculate) {
121
+ if (calculate !== false && tools !== false) {
122
+ try {
123
+ try {
124
+ tools = tools || require('../../../datadog-plugin-graphql/src/tools')
125
+ } catch (e) {
126
+ tools = false
127
+ throw e
128
+ }
129
+
130
+ return tools.defaultEngineReportingSignature(document, operationName)
131
+ } catch (e) {
132
+ // safety net
133
+ }
134
+ }
135
+
136
+ return [operationType, operationName].filter(val => val).join(' ')
137
+ }
138
+
139
+ module.exports = ApolloGatewayRequestPlugin
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
4
+
5
+ class ApolloGatewayValidatePlugin extends ApolloBasePlugin {
6
+ static get operation () { return 'validate' }
7
+ static get prefix () {
8
+ return 'tracing:apm:apollo:gateway:validate'
9
+ }
10
+
11
+ end (ctx) {
12
+ const result = ctx.result
13
+ if (result instanceof Array &&
14
+ result[result.length - 1] && result[result.length - 1].stack && result[result.length - 1].message) {
15
+ ctx.currentStore.span.setTag('error', result[result.length - 1])
16
+ }
17
+ ctx.currentStore.span.finish()
18
+ }
19
+ }
20
+
21
+ module.exports = ApolloGatewayValidatePlugin
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
4
+ const ApolloGatewayPlugin = require('./gateway')
5
+
6
+ class ApolloPlugin extends CompositePlugin {
7
+ static get id () { return 'apollo' }
8
+ static get plugins () {
9
+ return {
10
+ gateway: ApolloGatewayPlugin
11
+ }
12
+ }
13
+ }
14
+
15
+ module.exports = ApolloPlugin
@@ -11,7 +11,9 @@ const {
11
11
  TEST_SOURCE_START,
12
12
  TEST_CODE_OWNERS,
13
13
  TEST_SOURCE_FILE,
14
- TEST_CONFIGURATION_BROWSER_NAME
14
+ TEST_CONFIGURATION_BROWSER_NAME,
15
+ TEST_IS_NEW,
16
+ TEST_IS_RETRY
15
17
  } = require('../../dd-trace/src/plugins/util/test')
16
18
  const { RESOURCE_NAME } = require('../../../ext/tags')
17
19
  const { COMPONENT } = require('../../dd-trace/src/constants')
@@ -109,7 +111,7 @@ class PlaywrightPlugin extends CiPlugin {
109
111
 
110
112
  this.enter(span, store)
111
113
  })
112
- this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags }) => {
114
+ this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags, isNew, isEfdRetry }) => {
113
115
  const store = storage.getStore()
114
116
  const span = store && store.span
115
117
  if (!span) return
@@ -122,6 +124,12 @@ class PlaywrightPlugin extends CiPlugin {
122
124
  if (extraTags) {
123
125
  span.addTags(extraTags)
124
126
  }
127
+ if (isNew) {
128
+ span.setTag(TEST_IS_NEW, 'true')
129
+ if (isEfdRetry) {
130
+ span.setTag(TEST_IS_RETRY, 'true')
131
+ }
132
+ }
125
133
 
126
134
  steps.forEach(step => {
127
135
  const stepStartTime = step.startTime.getTime()
@@ -4,7 +4,6 @@ const { channel } = require('dc-polyfill')
4
4
  const { isFalse } = require('./util')
5
5
  const plugins = require('./plugins')
6
6
  const log = require('./log')
7
- const Nomenclature = require('./service-naming')
8
7
 
9
8
  const loadChannel = channel('dd-trace:instrumentation:load')
10
9
 
@@ -102,7 +101,7 @@ module.exports = class PluginManager {
102
101
  // like instrumenter.enable()
103
102
  configure (config = {}) {
104
103
  this._tracerConfig = config
105
- Nomenclature.configure(config)
104
+ this._tracer._nomenclature.configure(config)
106
105
 
107
106
  for (const name in pluginClasses) {
108
107
  this.loadPlugin(name)
@@ -0,0 +1,50 @@
1
+ const TracingPlugin = require('./tracing')
2
+ const { storage } = require('../../../datadog-core')
3
+
4
+ class ApolloBasePlugin extends TracingPlugin {
5
+ static get id () { return 'apollo.gateway' }
6
+ static get type () { return 'web' }
7
+ static get kind () { return 'server' }
8
+
9
+ bindStart (ctx) {
10
+ const store = storage.getStore()
11
+ const childOf = store ? store.span : null
12
+
13
+ const span = this.startSpan(this.getOperationName(), {
14
+ childOf,
15
+ service: this.getServiceName(),
16
+ type: this.constructor.type,
17
+ kind: this.constructor.kind,
18
+ meta: {}
19
+ }, false)
20
+
21
+ ctx.parentStore = store
22
+ ctx.currentStore = { ...store, span }
23
+
24
+ return ctx.currentStore
25
+ }
26
+
27
+ end (ctx) {
28
+ ctx?.currentStore?.span.finish()
29
+ }
30
+
31
+ asyncStart (ctx) {
32
+ ctx?.currentStore?.span.finish()
33
+ return ctx.parentStore
34
+ }
35
+
36
+ getServiceName () {
37
+ return this.serviceName({
38
+ id: `${this.constructor.id}.${this.constructor.operation}`,
39
+ pluginConfig: this.config
40
+ })
41
+ }
42
+
43
+ getOperationName () {
44
+ return this.operationName({
45
+ id: `${this.constructor.id}.${this.constructor.operation}`
46
+ })
47
+ }
48
+ }
49
+
50
+ module.exports = ApolloBasePlugin
@@ -12,6 +12,7 @@ class CompositePlugin extends Plugin {
12
12
  }
13
13
 
14
14
  configure (config) {
15
+ super.configure(config)
15
16
  for (const name in this.constructor.plugins) {
16
17
  const pluginConfig = config[name] === false ? false : {
17
18
  ...config,
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
+ get '@apollo/gateway' () { return require('../../../datadog-plugin-apollo/src') },
4
5
  get '@aws-sdk/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') },
5
6
  get '@cucumber/cucumber' () { return require('../../../datadog-plugin-cucumber/src') },
6
7
  get '@playwright/test' () { return require('../../../datadog-plugin-playwright/src') },
@@ -4,7 +4,6 @@ const Plugin = require('./plugin')
4
4
  const { storage } = require('../../../datadog-core')
5
5
  const analyticsSampler = require('../analytics_sampler')
6
6
  const { COMPONENT } = require('../constants')
7
- const Nomenclature = require('../service-naming')
8
7
 
9
8
  class TracingPlugin extends Plugin {
10
9
  constructor (...args) {
@@ -29,7 +28,7 @@ class TracingPlugin extends Plugin {
29
28
  kind = this.constructor.kind
30
29
  } = opts
31
30
 
32
- return Nomenclature.serviceName(type, kind, id, opts)
31
+ return this._tracer._nomenclature.serviceName(type, kind, id, opts)
33
32
  }
34
33
 
35
34
  operationName (opts = {}) {
@@ -39,7 +38,7 @@ class TracingPlugin extends Plugin {
39
38
  kind = this.constructor.kind
40
39
  } = opts
41
40
 
42
- return Nomenclature.opName(type, kind, id, opts)
41
+ return this._tracer._nomenclature.opName(type, kind, id, opts)
43
42
  }
44
43
 
45
44
  configure (config) {
@@ -58,8 +57,12 @@ class TracingPlugin extends Plugin {
58
57
  this.activeSpan?.finish()
59
58
  }
60
59
 
61
- error (error) {
62
- this.addError(error)
60
+ error (ctxOrError) {
61
+ if (ctxOrError?.currentStore) {
62
+ ctxOrError.currentStore?.span.setTag('error', ctxOrError?.error)
63
+ return
64
+ }
65
+ this.addError(ctxOrError)
63
66
  }
64
67
 
65
68
  addTraceSubs () {
@@ -15,6 +15,8 @@ const MS_TO_NS = 1000000
15
15
  const pprofValueType = 'timeline'
16
16
  const pprofValueUnit = 'nanoseconds'
17
17
 
18
+ const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
19
+
18
20
  function labelFromStr (stringTable, key, valStr) {
19
21
  return new Label({ key, str: stringTable.dedup(valStr) })
20
22
  }
@@ -146,6 +148,76 @@ if (node16) {
146
148
  decoratorTypes.net = NetDecorator
147
149
  }
148
150
 
151
+ // Translates performance entries into pprof samples.
152
+ class EventSerializer {
153
+ constructor () {
154
+ this.stringTable = new StringTable()
155
+ this.samples = []
156
+ this.locations = []
157
+ this.functions = []
158
+ this.decorators = {}
159
+
160
+ // A synthetic single-frame location to serve as the location for timeline
161
+ // samples. We need these as the profiling backend (mimicking official pprof
162
+ // tool's behavior) ignores these.
163
+ const fn = new Function({ id: this.functions.length + 1, name: this.stringTable.dedup('') })
164
+ this.functions.push(fn)
165
+ const line = new Line({ functionId: fn.id })
166
+ const location = new Location({ id: this.locations.length + 1, line: [line] })
167
+ this.locations.push(location)
168
+ this.locationId = [location.id]
169
+
170
+ this.timestampLabelKey = this.stringTable.dedup(END_TIMESTAMP_LABEL)
171
+ }
172
+
173
+ addEvent (item) {
174
+ const { entryType, startTime, duration } = item
175
+ let decorator = this.decorators[entryType]
176
+ if (!decorator) {
177
+ const DecoratorCtor = decoratorTypes[entryType]
178
+ if (DecoratorCtor) {
179
+ decorator = new DecoratorCtor(this.stringTable)
180
+ decorator.eventTypeLabel = labelFromStrStr(this.stringTable, 'event', entryType)
181
+ this.decorators[entryType] = decorator
182
+ } else {
183
+ // Shouldn't happen but it's better to not rely on observer only getting
184
+ // requested event types.
185
+ return
186
+ }
187
+ }
188
+ const endTime = startTime + duration
189
+ const sampleInput = {
190
+ value: [Math.round(duration * MS_TO_NS)],
191
+ locationId: this.locationId,
192
+ label: [
193
+ decorator.eventTypeLabel,
194
+ new Label({ key: this.timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
195
+ ]
196
+ }
197
+ decorator.decorateSample(sampleInput, item)
198
+ this.samples.push(new Sample(sampleInput))
199
+ }
200
+
201
+ createProfile (startDate, endDate) {
202
+ const timeValueType = new ValueType({
203
+ type: this.stringTable.dedup(pprofValueType),
204
+ unit: this.stringTable.dedup(pprofValueUnit)
205
+ })
206
+
207
+ return new Profile({
208
+ sampleType: [timeValueType],
209
+ timeNanos: endDate.getTime() * MS_TO_NS,
210
+ periodType: timeValueType,
211
+ period: 1,
212
+ durationNanos: (endDate.getTime() - startDate.getTime()) * MS_TO_NS,
213
+ sample: this.samples,
214
+ location: this.locations,
215
+ function: this.functions,
216
+ stringTable: this.stringTable
217
+ })
218
+ }
219
+ }
220
+
149
221
  /**
150
222
  * This class generates pprof files with timeline events sourced from Node.js
151
223
  * performance measurement APIs.
@@ -155,7 +227,7 @@ class EventsProfiler {
155
227
  this.type = 'events'
156
228
  this._flushIntervalNanos = (options.flushInterval || 60000) * 1e6 // 60 sec
157
229
  this._observer = undefined
158
- this.entries = []
230
+ this.eventSerializer = new EventSerializer()
159
231
  }
160
232
 
161
233
  start () {
@@ -163,7 +235,9 @@ class EventsProfiler {
163
235
  if (this._observer) return
164
236
 
165
237
  function add (items) {
166
- this.entries.push(...items.getEntries())
238
+ for (const item of items.getEntries()) {
239
+ this.eventSerializer.addEvent(item)
240
+ }
167
241
  }
168
242
  this._observer = new PerformanceObserver(add.bind(this))
169
243
  this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
@@ -177,89 +251,12 @@ class EventsProfiler {
177
251
  }
178
252
 
179
253
  profile (restart, startDate, endDate) {
180
- if (this.entries.length === 0) {
181
- // No events in the period; don't produce a profile
182
- return null
183
- }
184
-
185
- const stringTable = new StringTable()
186
- const locations = []
187
- const functions = []
188
-
189
- // A synthetic single-frame location to serve as the location for timeline
190
- // samples. We need these as the profiling backend (mimicking official pprof
191
- // tool's behavior) ignores these.
192
- const locationId = (() => {
193
- const fn = new Function({ id: functions.length + 1, name: stringTable.dedup('') })
194
- functions.push(fn)
195
- const line = new Line({ functionId: fn.id })
196
- const location = new Location({ id: locations.length + 1, line: [line] })
197
- locations.push(location)
198
- return [location.id]
199
- })()
200
-
201
- const decorators = {}
202
- for (const [eventType, DecoratorCtor] of Object.entries(decoratorTypes)) {
203
- const decorator = new DecoratorCtor(stringTable)
204
- decorator.eventTypeLabel = labelFromStrStr(stringTable, 'event', eventType)
205
- decorators[eventType] = decorator
206
- }
207
- const timestampLabelKey = stringTable.dedup(END_TIMESTAMP_LABEL)
208
-
209
- const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
210
- const lateEntries = []
211
- const perfEndDate = endDate.getTime() - performance.timeOrigin
212
- const samples = this.entries.map((item) => {
213
- const decorator = decorators[item.entryType]
214
- if (!decorator) {
215
- // Shouldn't happen but it's better to not rely on observer only getting
216
- // requested event types.
217
- return null
218
- }
219
- const { startTime, duration } = item
220
- if (startTime >= perfEndDate) {
221
- // An event past the current recording end date; save it for the next
222
- // profile. Not supposed to happen as long as there's no async activity
223
- // between capture of the endDate value in profiler.js _collect() and
224
- // here, but better be safe than sorry.
225
- lateEntries.push(item)
226
- return null
227
- }
228
- const endTime = startTime + duration
229
- const sampleInput = {
230
- value: [Math.round(duration * MS_TO_NS)],
231
- locationId,
232
- label: [
233
- decorator.eventTypeLabel,
234
- new Label({ key: timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
235
- ]
236
- }
237
- decorator.decorateSample(sampleInput, item)
238
- return new Sample(sampleInput)
239
- }).filter(v => v)
240
-
241
- this.entries = lateEntries
242
-
243
- const timeValueType = new ValueType({
244
- type: stringTable.dedup(pprofValueType),
245
- unit: stringTable.dedup(pprofValueUnit)
246
- })
247
-
248
254
  if (!restart) {
249
255
  this.stop()
250
256
  }
251
-
252
- return new Profile({
253
- sampleType: [timeValueType],
254
- timeNanos: endDate.getTime() * MS_TO_NS,
255
- periodType: timeValueType,
256
- period: 1,
257
- durationNanos: (endDate.getTime() - startDate.getTime()) * MS_TO_NS,
258
- sample: samples,
259
- location: locations,
260
- function: functions,
261
- stringTable: stringTable
262
- })
257
+ const profile = this.eventSerializer.createProfile(startDate, endDate)
258
+ this.eventSerializer = new EventSerializer()
259
+ return profile
263
260
  }
264
261
 
265
262
  encode (profile) {
@@ -6,6 +6,7 @@ const runtimeMetrics = require('./runtime_metrics')
6
6
  const log = require('./log')
7
7
  const { setStartupLogPluginManager } = require('./startup-log')
8
8
  const telemetry = require('./telemetry')
9
+ const nomenclature = require('./service-naming')
9
10
  const PluginManager = require('./plugin_manager')
10
11
  const remoteConfig = require('./appsec/remote_config')
11
12
  const AppsecSdk = require('./appsec/sdk')
@@ -17,6 +18,7 @@ class Tracer extends NoopProxy {
17
18
  super()
18
19
 
19
20
  this._initialized = false
21
+ this._nomenclature = nomenclature
20
22
  this._pluginManager = new PluginManager(this)
21
23
  this.dogstatsd = new dogstatsd.NoopDogStatsDClient()
22
24
  this._tracingInitialized = false
@@ -33,6 +33,30 @@ const web = {
33
33
  }
34
34
  },
35
35
  server: {
36
+ 'apollo.gateway.request': {
37
+ opName: () => 'apollo.gateway.request',
38
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
39
+ },
40
+ 'apollo.gateway.plan': {
41
+ opName: () => 'apollo.gateway.plan',
42
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
43
+ },
44
+ 'apollo.gateway.validate': {
45
+ opName: () => 'apollo.gateway.validate',
46
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
47
+ },
48
+ 'apollo.gateway.execute': {
49
+ opName: () => 'apollo.gateway.execute',
50
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
51
+ },
52
+ 'apollo.gateway.fetch': {
53
+ opName: () => 'apollo.gateway.fetch',
54
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
55
+ },
56
+ 'apollo.gateway.postprocessing': {
57
+ opName: () => 'apollo.gateway.postprocessing',
58
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
59
+ },
36
60
  grpc: {
37
61
  opName: () => DD_MAJOR <= 2 ? 'grpc.request' : 'grpc.server',
38
62
  serviceName: identityService
@@ -32,6 +32,30 @@ const web = {
32
32
  }
33
33
  },
34
34
  server: {
35
+ 'apollo.gateway.request': {
36
+ opName: () => 'apollo.gateway.request',
37
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
38
+ },
39
+ 'apollo.gateway.plan': {
40
+ opName: () => 'apollo.gateway.plan',
41
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
42
+ },
43
+ 'apollo.gateway.validate': {
44
+ opName: () => 'apollo.gateway.validate',
45
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
46
+ },
47
+ 'apollo.gateway.execute': {
48
+ opName: () => 'apollo.gateway.execute',
49
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
50
+ },
51
+ 'apollo.gateway.fetch': {
52
+ opName: () => 'apollo.gateway.fetch',
53
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
54
+ },
55
+ 'apollo.gateway.postprocessing': {
56
+ opName: () => 'apollo.gateway.postprocessing',
57
+ serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
58
+ },
35
59
  grpc: {
36
60
  opName: () => 'grpc.server.request',
37
61
  serviceName: identityService