dd-trace 2.4.0 → 2.5.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 (60) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/init.js +32 -2
  3. package/ci/jest/env.js +16 -3
  4. package/ext/exporters.d.ts +2 -1
  5. package/ext/exporters.js +2 -1
  6. package/package.json +3 -2
  7. package/packages/datadog-instrumentations/index.js +2 -0
  8. package/packages/datadog-instrumentations/src/amqplib.js +1 -1
  9. package/packages/datadog-instrumentations/src/cucumber.js +25 -12
  10. package/packages/datadog-instrumentations/src/cypress.js +8 -0
  11. package/packages/datadog-instrumentations/src/helpers/hook.js +44 -0
  12. package/packages/datadog-instrumentations/src/helpers/instrument.js +30 -57
  13. package/packages/datadog-instrumentations/src/http/client.js +170 -0
  14. package/packages/datadog-instrumentations/src/http/server.js +61 -0
  15. package/packages/datadog-instrumentations/src/http.js +4 -0
  16. package/packages/datadog-instrumentations/src/mocha.js +28 -11
  17. package/packages/datadog-instrumentations/src/net.js +117 -0
  18. package/packages/datadog-plugin-aws-sdk/src/helpers.js +4 -4
  19. package/packages/datadog-plugin-aws-sdk/src/index.js +1 -1
  20. package/packages/datadog-plugin-cucumber/src/index.js +24 -12
  21. package/packages/datadog-plugin-cypress/src/index.js +10 -5
  22. package/packages/datadog-plugin-cypress/src/plugin.js +13 -1
  23. package/packages/datadog-plugin-elasticsearch/src/index.js +4 -2
  24. package/packages/datadog-plugin-http/src/client.js +112 -252
  25. package/packages/datadog-plugin-http/src/index.js +29 -3
  26. package/packages/datadog-plugin-http/src/server.js +54 -32
  27. package/packages/datadog-plugin-jest/src/jest-environment.js +3 -3
  28. package/packages/datadog-plugin-mocha/src/index.js +10 -1
  29. package/packages/datadog-plugin-net/src/index.js +65 -121
  30. package/packages/datadog-plugin-next/src/index.js +10 -10
  31. package/packages/dd-trace/lib/version.js +1 -1
  32. package/packages/dd-trace/src/appsec/callbacks/ddwaf.js +3 -1
  33. package/packages/dd-trace/src/appsec/recommended.json +119 -210
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +32 -0
  35. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +51 -0
  36. package/packages/dd-trace/src/config.js +8 -1
  37. package/packages/dd-trace/src/encode/0.4.js +0 -1
  38. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +193 -0
  39. package/packages/dd-trace/src/encode/tags-processors.js +116 -0
  40. package/packages/dd-trace/src/exporter.js +3 -0
  41. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  42. package/packages/dd-trace/src/exporters/agent/writer.js +7 -32
  43. package/packages/dd-trace/src/exporters/{agent → common}/docker.js +0 -0
  44. package/packages/dd-trace/src/exporters/common/request.js +83 -0
  45. package/packages/dd-trace/src/exporters/common/writer.js +36 -0
  46. package/packages/dd-trace/src/exporters/{agent/scheduler.js → scheduler.js} +0 -0
  47. package/packages/dd-trace/src/iitm.js +5 -1
  48. package/packages/dd-trace/src/instrumenter.js +3 -0
  49. package/packages/dd-trace/src/loader.js +6 -4
  50. package/packages/dd-trace/src/opentracing/span.js +34 -0
  51. package/packages/dd-trace/src/pkg.js +11 -6
  52. package/packages/dd-trace/src/plugin_manager.js +4 -0
  53. package/packages/dd-trace/src/plugins/plugin.js +3 -1
  54. package/packages/dd-trace/src/plugins/util/test.js +60 -1
  55. package/packages/dd-trace/src/plugins/util/web.js +99 -93
  56. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  57. package/packages/dd-trace/src/proxy.js +2 -0
  58. package/packages/dd-trace/src/ritm.js +60 -25
  59. package/packages/dd-trace/src/telemetry.js +187 -0
  60. package/packages/dd-trace/src/exporters/agent/request.js +0 -86
@@ -1,8 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const semver = require('semver')
4
- const hook = require('./ritm')
5
- const esmHook = require('./iitm')
4
+ const Hook = require('../../datadog-instrumentations/src/helpers/hook')
6
5
  const parse = require('module-details-from-path')
7
6
  const path = require('path')
8
7
  const uniq = require('lodash.uniq')
@@ -18,6 +17,7 @@ class Loader {
18
17
 
19
18
  reload (plugins) {
20
19
  this._plugins = plugins
20
+ this._patched = []
21
21
 
22
22
  const instrumentations = Array.from(this._plugins.keys())
23
23
  .reduce((prev, current) => prev.concat(current), [])
@@ -28,8 +28,10 @@ class Loader {
28
28
  this._names = new Set(instrumentations
29
29
  .map(instrumentation => filename(instrumentation)))
30
30
 
31
- hook(instrumentedModules, this._hookModule.bind(this))
32
- esmHook(instrumentedModules, this._hookModule.bind(this))
31
+ this._hook && this._hook.unhook()
32
+ this._hook = Hook(instrumentedModules, (moduleExports, moduleName, moduleBaseDir) => {
33
+ return this._hookModule(moduleExports, moduleName, moduleBaseDir)
34
+ })
33
35
  }
34
36
 
35
37
  load (instrumentation, config) {
@@ -1,16 +1,23 @@
1
1
  'use strict'
2
2
 
3
+ // TODO (new internal tracer): use DC events for lifecycle metrics and test them
4
+
3
5
  const opentracing = require('opentracing')
4
6
  const now = require('performance-now')
7
+ const semver = require('semver')
5
8
  const Span = opentracing.Span
6
9
  const SpanContext = require('./span_context')
7
10
  const id = require('../id')
8
11
  const tagger = require('../tagger')
12
+ const metrics = require('../metrics')
9
13
  const log = require('../log')
10
14
  const { storage } = require('../../../datadog-core')
11
15
 
12
16
  const { DD_TRACE_EXPERIMENTAL_STATE_TRACKING } = process.env
13
17
 
18
+ const unfinishedRegistry = createRegistry('unfinished')
19
+ const finishedRegistry = createRegistry('finished')
20
+
14
21
  class DatadogSpan extends Span {
15
22
  constructor (tracer, processor, prioritySampler, fields, debug) {
16
23
  super()
@@ -25,6 +32,7 @@ class DatadogSpan extends Span {
25
32
  this._processor = processor
26
33
  this._prioritySampler = prioritySampler
27
34
  this._store = storage.getStore()
35
+ this._name = operationName
28
36
 
29
37
  this._spanContext = this._createContext(parent)
30
38
  this._spanContext._name = operationName
@@ -32,6 +40,13 @@ class DatadogSpan extends Span {
32
40
  this._spanContext._hostname = hostname
33
41
 
34
42
  this._startTime = fields.startTime || this._getTime()
43
+
44
+ if (this._debug && unfinishedRegistry) {
45
+ metrics.increment('runtime.node.spans.unfinished')
46
+ metrics.increment('runtime.node.spans.unfinished.by.name', `span_name:${operationName}`)
47
+
48
+ unfinishedRegistry.register(this, operationName, this)
49
+ }
35
50
  }
36
51
 
37
52
  toString () {
@@ -122,6 +137,16 @@ class DatadogSpan extends Span {
122
137
  }
123
138
  }
124
139
 
140
+ if (this._debug && finishedRegistry) {
141
+ metrics.decrement('runtime.node.spans.unfinished')
142
+ metrics.decrement('runtime.node.spans.unfinished.by.name', `span_name:${this._name}`)
143
+ metrics.increment('runtime.node.spans.finished')
144
+ metrics.increment('runtime.node.spans.finished.by.name', `span_name:${this._name}`)
145
+
146
+ unfinishedRegistry.unregister(this)
147
+ finishedRegistry.register(this, this._name)
148
+ }
149
+
125
150
  finishTime = parseFloat(finishTime) || this._getTime()
126
151
 
127
152
  this._duration = finishTime - this._startTime
@@ -131,4 +156,13 @@ class DatadogSpan extends Span {
131
156
  }
132
157
  }
133
158
 
159
+ function createRegistry (type) {
160
+ if (!semver.satisfies(process.version, '>=14.6')) return
161
+
162
+ return new global.FinalizationRegistry(name => {
163
+ metrics.decrement(`runtime.node.spans.${type}`)
164
+ metrics.decrement(`runtime.node.spans.${type}.by.name`, [`span_name:${name}`])
165
+ })
166
+ }
167
+
134
168
  module.exports = DatadogSpan
@@ -11,7 +11,14 @@ function findRoot () {
11
11
 
12
12
  function findPkg () {
13
13
  const cwd = findRoot()
14
- const filePath = findUp('package.json', cwd)
14
+ const directory = path.resolve(cwd)
15
+ const res = path.parse(directory)
16
+
17
+ if (!res) return {}
18
+
19
+ const { root } = res
20
+
21
+ const filePath = findUp('package.json', root, directory)
15
22
 
16
23
  try {
17
24
  return JSON.parse(fs.readFileSync(filePath, 'utf8'))
@@ -20,18 +27,16 @@ function findPkg () {
20
27
  }
21
28
  }
22
29
 
23
- function findUp (name, cwd) {
24
- let directory = path.resolve(cwd)
25
- const { root } = path.parse(directory)
26
-
30
+ function findUp (name, root, directory) {
27
31
  while (true) {
28
32
  const current = path.resolve(directory, name)
29
33
 
30
34
  if (fs.existsSync(current)) return current
35
+
31
36
  if (directory === root) return
32
37
 
33
38
  directory = path.dirname(directory)
34
39
  }
35
40
  }
36
41
 
37
- module.exports = findPkg()
42
+ module.exports = Object.assign(findPkg(), { findRoot, findUp })
@@ -55,6 +55,10 @@ module.exports = class PluginManager {
55
55
  }
56
56
  this.configurePlugin(name, pluginConfig)
57
57
  }
58
+ } else {
59
+ for (const name in this._pluginsByName) {
60
+ this.configurePlugin(name, false)
61
+ }
58
62
  }
59
63
  }
60
64
 
@@ -8,7 +8,6 @@ class Subscription {
8
8
  this._channel = dc.channel(event)
9
9
  this._handler = (message, name) => {
10
10
  const store = storage.getStore()
11
-
12
11
  if (!store || !store.noop) {
13
12
  handler(message, name)
14
13
  }
@@ -58,6 +57,9 @@ module.exports = class Plugin {
58
57
  }
59
58
 
60
59
  configure (config) {
60
+ if (typeof config === 'boolean') {
61
+ config = { enabled: config }
62
+ }
61
63
  this.config = config
62
64
  if (config.enabled && !this._enabled) {
63
65
  this._enabled = true
@@ -1,4 +1,7 @@
1
1
  const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ const ignore = require('ignore')
2
5
 
3
6
  const { getGitMetadata } = require('./git')
4
7
  const { getUserProviderGitMetadata } = require('./user-provided-git')
@@ -25,6 +28,7 @@ const TEST_STATUS = 'test.status'
25
28
  const TEST_PARAMETERS = 'test.parameters'
26
29
  const TEST_SKIP_REASON = 'test.skip_reason'
27
30
  const TEST_IS_RUM_ACTIVE = 'test.is_rum_active'
31
+ const TEST_CODE_OWNERS = 'test.codeowners'
28
32
 
29
33
  const ERROR_TYPE = 'error.type'
30
34
  const ERROR_MESSAGE = 'error.msg'
@@ -35,6 +39,7 @@ const CI_APP_ORIGIN = 'ciapp-test'
35
39
  const JEST_TEST_RUNNER = 'test.jest.test_runner'
36
40
 
37
41
  module.exports = {
42
+ TEST_CODE_OWNERS,
38
43
  TEST_FRAMEWORK,
39
44
  TEST_FRAMEWORK_VERSION,
40
45
  JEST_TEST_RUNNER,
@@ -53,7 +58,9 @@ module.exports = {
53
58
  getTestParametersString,
54
59
  finishAllTraceSpans,
55
60
  getTestParentSpan,
56
- getTestSuitePath
61
+ getTestSuitePath,
62
+ getCodeOwnersFileEntries,
63
+ getCodeOwnersForFilename
57
64
  }
58
65
 
59
66
  function getTestEnvironmentMetadata (testFramework, config) {
@@ -140,3 +147,55 @@ function getTestSuitePath (testSuiteAbsolutePath, sourceRoot) {
140
147
 
141
148
  return testSuitePath.replace(path.sep, '/')
142
149
  }
150
+
151
+ const POSSIBLE_CODEOWNERS_LOCATIONS = [
152
+ 'CODEOWNERS',
153
+ '.github/CODEOWNERS',
154
+ 'docs/CODEOWNERS',
155
+ '.gitlab/CODEOWNERS'
156
+ ]
157
+
158
+ function getCodeOwnersFileEntries (rootDir = process.cwd()) {
159
+ let codeOwnersContent
160
+
161
+ POSSIBLE_CODEOWNERS_LOCATIONS.forEach(location => {
162
+ try {
163
+ codeOwnersContent = fs.readFileSync(`${rootDir}/${location}`).toString()
164
+ } catch (e) {
165
+ // retry with next path
166
+ }
167
+ })
168
+ if (!codeOwnersContent) {
169
+ return null
170
+ }
171
+
172
+ const entries = []
173
+ const lines = codeOwnersContent.split('\n')
174
+
175
+ for (const line of lines) {
176
+ const [content] = line.split('#')
177
+ const trimmed = content.trim()
178
+ if (trimmed === '') continue
179
+ const [pattern, ...owners] = trimmed.split(/\s+/)
180
+ entries.push({ pattern, owners })
181
+ }
182
+ // Reverse because rules defined last take precedence
183
+ return entries.reverse()
184
+ }
185
+
186
+ function getCodeOwnersForFilename (filename, entries) {
187
+ if (!entries) {
188
+ return null
189
+ }
190
+ for (const entry of entries) {
191
+ try {
192
+ const isResponsible = ignore().add(entry.pattern).ignores(filename)
193
+ if (isResponsible) {
194
+ return JSON.stringify(entry.owners)
195
+ }
196
+ } catch (e) {
197
+ return null
198
+ }
199
+ }
200
+ return null
201
+ }
@@ -52,10 +52,36 @@ const web = {
52
52
  })
53
53
  },
54
54
 
55
+ startSpan (tracer, config, req, res, name) {
56
+ const context = this.patch(req)
57
+ context.config = config
58
+
59
+ let span
60
+
61
+ if (context.span) {
62
+ context.span.context()._name = name
63
+ span = context.span
64
+ } else {
65
+ span = web.startChildSpan(tracer, name, req.headers)
66
+ }
67
+
68
+ context.tracer = tracer
69
+ context.span = span
70
+ context.res = res
71
+
72
+ return span
73
+ },
74
+ wrap (req) {
75
+ const context = contexts.get(req)
76
+ if (!context.instrumented) {
77
+ this.wrapEnd(context)
78
+ this.wrapEvents(context)
79
+ context.instrumented = true
80
+ }
81
+ },
55
82
  // Start a span and activate a scope for a request.
56
83
  instrument (tracer, config, req, res, name, callback) {
57
- const context = this.patch(req)
58
- const span = startSpan(tracer, config, req, res, name)
84
+ const span = this.startSpan(tracer, config, req, res, name)
59
85
 
60
86
  if (!config.filter(req.url)) {
61
87
  span.setTag(MANUAL_DROP, true)
@@ -67,12 +93,7 @@ const web = {
67
93
 
68
94
  analyticsSampler.sample(span, config.measured, true)
69
95
 
70
- if (!context.instrumented) {
71
- wrapEnd(context)
72
- wrapEvents(context)
73
-
74
- context.instrumented = true
75
- }
96
+ this.wrap(req)
76
97
 
77
98
  return callback && tracer.scope().activate(span, () => callback(span))
78
99
  },
@@ -199,6 +220,7 @@ const web = {
199
220
  // Extract the parent span from the headers and start a new span as its child
200
221
  startChildSpan (tracer, name, headers) {
201
222
  const childOf = tracer.scope().active() || tracer.extract(FORMAT_HTTP_HEADERS, headers)
223
+
202
224
  const span = tracer.startSpan(name, { childOf })
203
225
 
204
226
  return span
@@ -221,103 +243,94 @@ const web = {
221
243
  const context = contexts.get(req)
222
244
  context.error = context.error || error
223
245
  }
224
- }
225
- }
226
-
227
- function startSpan (tracer, config, req, res, name) {
228
- const context = contexts.get(req)
229
-
230
- context.config = config
231
-
232
- let span
233
-
234
- if (context.span) {
235
- context.span.context()._name = name
236
- span = context.span
237
- } else {
238
- span = web.startChildSpan(tracer, name, req.headers)
239
- }
240
-
241
- context.tracer = tracer
242
- context.span = span
243
- context.res = res
244
-
245
- return span
246
- }
246
+ },
247
247
 
248
- function finish (context) {
249
- const { req, res } = context
248
+ finishMiddleware (context) {
249
+ if (context.finished) return
250
250
 
251
- if (context.finished && !req.stream) return
251
+ let span
252
252
 
253
- addRequestTags(context)
254
- addResponseTags(context)
253
+ while ((span = context.middleware.pop())) {
254
+ span.finish()
255
+ }
256
+ },
255
257
 
256
- context.config.hooks.request(context.span, req, res)
257
- addResourceTag(context)
258
+ finishSpan (context) {
259
+ const { req, res } = context
258
260
 
259
- context.span.finish()
260
- context.finished = true
261
- }
261
+ if (context.finished && !req.stream) return
262
262
 
263
- function finishMiddleware (context) {
264
- if (context.finished) return
263
+ addRequestTags(context)
264
+ addResponseTags(context)
265
265
 
266
- let span
266
+ context.config.hooks.request(context.span, req, res)
267
+ addResourceTag(context)
267
268
 
268
- while ((span = context.middleware.pop())) {
269
- span.finish()
270
- }
271
- }
269
+ context.span.finish()
270
+ context.finished = true
271
+ },
272
+ wrapWriteHead (context) {
273
+ const { req, res } = context
274
+ const writeHead = res.writeHead
272
275
 
273
- function wrapEnd (context) {
274
- const scope = context.tracer.scope()
275
- const req = context.req
276
- const res = context.res
277
- const end = res.end
276
+ return function (statusCode, statusMessage, headers) {
277
+ headers = typeof statusMessage === 'string' ? headers : statusMessage
278
+ headers = Object.assign(res.getHeaders(), headers)
278
279
 
279
- res.writeHead = wrapWriteHead(context)
280
+ if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) {
281
+ addAllowHeaders(req, res, headers)
282
+ }
280
283
 
281
- ends.set(res, function () {
282
- for (const beforeEnd of context.beforeEnd) {
283
- beforeEnd()
284
+ return writeHead.apply(this, arguments)
284
285
  }
286
+ },
287
+ getContext (req) {
288
+ return contexts.get(req)
289
+ },
290
+ wrapRes (context, req, res, end) {
291
+ return function () {
292
+ for (const beforeEnd of context.beforeEnd) {
293
+ beforeEnd()
294
+ }
285
295
 
286
- finishMiddleware(context)
287
-
288
- if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.publish({ req, res })
289
-
290
- const returnValue = end.apply(res, arguments)
291
-
292
- finish(context)
293
-
294
- return returnValue
295
- })
296
+ web.finishMiddleware(context)
296
297
 
297
- Object.defineProperty(res, 'end', {
298
- configurable: true,
299
- get () {
300
- return ends.get(this)
301
- },
302
- set (value) {
303
- ends.set(this, scope.bind(value, context.span))
304
- }
305
- })
306
- }
298
+ if (incomingHttpRequestEnd.hasSubscribers) {
299
+ incomingHttpRequestEnd.publish({ req, res })
300
+ }
307
301
 
308
- function wrapWriteHead (context) {
309
- const { req, res } = context
310
- const writeHead = res.writeHead
302
+ const returnValue = end.apply(res, arguments)
311
303
 
312
- return function (statusCode, statusMessage, headers) {
313
- headers = typeof statusMessage === 'string' ? headers : statusMessage
314
- headers = Object.assign(res.getHeaders(), headers)
304
+ web.finishSpan(context)
315
305
 
316
- if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) {
317
- addAllowHeaders(req, res, headers)
306
+ return returnValue
318
307
  }
308
+ },
309
+ wrapEnd (context) {
310
+ const scope = context.tracer.scope()
311
+ const req = context.req
312
+ const res = context.res
313
+ const end = res.end
314
+
315
+ res.writeHead = web.wrapWriteHead(context)
316
+
317
+ ends.set(res, this.wrapRes(context, req, res, end))
318
+
319
+ Object.defineProperty(res, 'end', {
320
+ configurable: true,
321
+ get () {
322
+ return ends.get(this)
323
+ },
324
+ set (value) {
325
+ ends.set(this, scope.bind(value, context.span))
326
+ }
327
+ })
328
+ },
329
+ wrapEvents (context) {
330
+ const scope = context.tracer.scope()
331
+ const res = context.res
319
332
 
320
- return writeHead.apply(this, arguments)
333
+ scope.bind(res, context.span)
321
334
  }
322
335
  }
323
336
 
@@ -354,13 +367,6 @@ function splitHeader (str) {
354
367
  return typeof str === 'string' ? str.split(/\s*,\s*/) : []
355
368
  }
356
369
 
357
- function wrapEvents (context) {
358
- const scope = context.tracer.scope()
359
- const res = context.res
360
-
361
- scope.bind(res, context.span)
362
- }
363
-
364
370
  function reactivate (req, fn) {
365
371
  const context = contexts.get(req)
366
372
 
@@ -5,7 +5,7 @@ const { request } = require('http')
5
5
  const FormData = require('form-data')
6
6
 
7
7
  // TODO: avoid using dd-trace internals. Make this a separate module?
8
- const docker = require('../../exporters/agent/docker')
8
+ const docker = require('../../exporters/common/docker')
9
9
  const version = require('../../../lib/version')
10
10
 
11
11
  const containerId = docker.id()
@@ -10,6 +10,7 @@ const metrics = require('./metrics')
10
10
  const log = require('./log')
11
11
  const { isFalse } = require('./util')
12
12
  const { setStartupLogInstrumenter } = require('./startup-log')
13
+ const telemetry = require('./telemetry')
13
14
 
14
15
  const noop = new NoopTracer()
15
16
 
@@ -63,6 +64,7 @@ class Tracer extends BaseTracer {
63
64
  this._instrumenter.enable(config)
64
65
  this._pluginManager.configure(config)
65
66
  setStartupLogInstrumenter(this._instrumenter)
67
+ telemetry.start(config, this._instrumenter, this._pluginManager)
66
68
  }
67
69
  } catch (e) {
68
70
  log.error(e)
@@ -10,9 +10,10 @@ const origRequire = Module.prototype.require
10
10
 
11
11
  module.exports = Hook
12
12
 
13
- Hook.reset = function () {
14
- Module.prototype.require = origRequire
15
- }
13
+ let moduleHooks = Object.create(null)
14
+ let cache = Object.create(null)
15
+ let patching = Object.create(null)
16
+ let patchedRequire = null
16
17
 
17
18
  function Hook (modules, options, onrequire) {
18
19
  if (!(this instanceof Hook)) return new Hook(modules, options, onrequire)
@@ -25,35 +26,40 @@ function Hook (modules, options, onrequire) {
25
26
  options = {}
26
27
  }
27
28
 
29
+ modules = modules || []
28
30
  options = options || {}
29
31
 
30
- this.cache = {}
31
- this._unhooked = false
32
- this._origRequire = Module.prototype.require
32
+ this.modules = modules
33
+ this.options = options
34
+ this.onrequire = onrequire
33
35
 
34
- const self = this
35
- const patching = {}
36
+ if (Array.isArray(modules)) {
37
+ for (const mod of modules) {
38
+ const hooks = moduleHooks[mod]
36
39
 
37
- this._require = Module.prototype.require = function (request) {
38
- if (self._unhooked) {
39
- // if the patched require function could not be removed because
40
- // someone else patched it after it was patched here, we just
41
- // abort and pass the request onwards to the original require
42
- return self._origRequire.apply(this, arguments)
40
+ if (hooks) {
41
+ hooks.push(onrequire)
42
+ } else {
43
+ moduleHooks[mod] = [onrequire]
44
+ }
43
45
  }
46
+ }
44
47
 
48
+ if (patchedRequire) return
49
+
50
+ patchedRequire = Module.prototype.require = function (request) {
45
51
  const filename = Module._resolveFilename(request, this)
46
52
  const core = filename.indexOf(path.sep) === -1
47
- let name, basedir
53
+ let name, basedir, hooks
48
54
 
49
55
  // return known patched modules immediately
50
- if (self.cache.hasOwnProperty(filename)) {
56
+ if (cache[filename]) {
51
57
  // require.cache was potentially altered externally
52
- if (require.cache[filename] && require.cache[filename].exports !== self.cache[filename].original) {
58
+ if (require.cache[filename] && require.cache[filename].exports !== cache[filename].original) {
53
59
  return require.cache[filename].exports
54
60
  }
55
61
 
56
- return self.cache[filename].exports
62
+ return cache[filename].exports
57
63
  }
58
64
 
59
65
  // Check if this module has a patcher in-progress already.
@@ -63,7 +69,7 @@ function Hook (modules, options, onrequire) {
63
69
  patching[filename] = true
64
70
  }
65
71
 
66
- const exports = self._origRequire.apply(this, arguments)
72
+ const exports = origRequire.apply(this, arguments)
67
73
 
68
74
  // If it's already patched, just return it as-is.
69
75
  if (patched) return exports
@@ -73,7 +79,8 @@ function Hook (modules, options, onrequire) {
73
79
  delete patching[filename]
74
80
 
75
81
  if (core) {
76
- if (modules && modules.indexOf(filename) === -1) return exports // abort if module name isn't on whitelist
82
+ hooks = moduleHooks[filename]
83
+ if (!hooks) return exports // abort if module name isn't on whitelist
77
84
  name = filename
78
85
  } else {
79
86
  const stat = parse(filename)
@@ -81,7 +88,8 @@ function Hook (modules, options, onrequire) {
81
88
  name = stat.name
82
89
  basedir = stat.basedir
83
90
 
84
- if (modules && modules.indexOf(name) === -1) return exports // abort if module name isn't on whitelist
91
+ hooks = moduleHooks[name]
92
+ if (!hooks) return exports // abort if module name isn't on whitelist
85
93
 
86
94
  // figure out if this is the main module file, or a file inside the module
87
95
  const paths = Module._resolveLookupPaths(name, this, true)
@@ -99,10 +107,37 @@ function Hook (modules, options, onrequire) {
99
107
 
100
108
  // ensure that the cache entry is assigned a value before calling
101
109
  // onrequire, in case calling onrequire requires the same module.
102
- self.cache[filename] = { exports }
103
- self.cache[filename].original = exports
104
- self.cache[filename].exports = onrequire(exports, name, basedir)
110
+ cache[filename] = { exports }
111
+ cache[filename].original = exports
112
+
113
+ for (const hook of hooks) {
114
+ cache[filename].exports = hook(cache[filename].exports, name, basedir)
115
+ }
116
+
117
+ return cache[filename].exports
118
+ }
119
+ }
120
+
121
+ Hook.reset = function () {
122
+ Module.prototype.require = origRequire
123
+ patchedRequire = null
124
+ patching = Object.create(null)
125
+ cache = Object.create(null)
126
+ moduleHooks = Object.create(null)
127
+ }
128
+
129
+ Hook.prototype.unhook = function () {
130
+ for (const mod of this.modules) {
131
+ const hooks = (moduleHooks[mod] || []).filter(hook => hook !== this.onrequire)
132
+
133
+ if (hooks.length > 0) {
134
+ moduleHooks[mod] = hooks
135
+ } else {
136
+ delete moduleHooks[mod]
137
+ }
138
+ }
105
139
 
106
- return self.cache[filename].exports
140
+ if (Object.keys(moduleHooks).length === 0) {
141
+ Hook.reset()
107
142
  }
108
143
  }