dd-trace 2.12.2 → 2.15.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 (76) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/ext/tags.d.ts +2 -1
  3. package/ext/tags.js +2 -1
  4. package/index.d.ts +43 -20
  5. package/package.json +5 -3
  6. package/packages/datadog-core/src/storage/async_resource.js +19 -1
  7. package/packages/datadog-instrumentations/index.js +1 -52
  8. package/packages/datadog-instrumentations/src/crypto.js +30 -0
  9. package/packages/datadog-instrumentations/src/cucumber.js +15 -0
  10. package/packages/datadog-instrumentations/src/fs.js +11 -0
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +70 -0
  12. package/packages/datadog-instrumentations/src/helpers/instrument.js +5 -34
  13. package/packages/datadog-instrumentations/src/helpers/instrumentations.js +7 -0
  14. package/packages/datadog-instrumentations/src/helpers/register.js +59 -0
  15. package/packages/datadog-instrumentations/src/http/server.js +1 -1
  16. package/packages/datadog-instrumentations/src/jest.js +33 -11
  17. package/packages/datadog-instrumentations/src/net.js +13 -0
  18. package/packages/datadog-plugin-cucumber/src/index.js +4 -0
  19. package/packages/datadog-plugin-fs/src/index.js +72 -38
  20. package/packages/datadog-plugin-jest/src/index.js +25 -4
  21. package/packages/datadog-plugin-mocha/src/index.js +2 -2
  22. package/packages/datadog-plugin-mongodb-core/src/index.js +32 -8
  23. package/packages/datadog-plugin-oracledb/src/index.js +12 -4
  24. package/packages/dd-trace/index.js +1 -1
  25. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -0
  26. package/packages/dd-trace/src/appsec/iast/analyzers/index.js +20 -0
  27. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +48 -0
  28. package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +24 -0
  29. package/packages/dd-trace/src/appsec/iast/iast-context.js +50 -0
  30. package/packages/dd-trace/src/appsec/iast/index.js +59 -0
  31. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +94 -0
  32. package/packages/dd-trace/src/appsec/iast/path-line.js +70 -0
  33. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +113 -0
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +50 -0
  35. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +53 -8
  36. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +23 -24
  37. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +220 -0
  38. package/packages/dd-trace/src/config.js +89 -10
  39. package/packages/dd-trace/src/constants.js +9 -1
  40. package/packages/dd-trace/src/encode/0.4.js +51 -58
  41. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +13 -34
  42. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +84 -0
  43. package/packages/dd-trace/src/encode/span-stats.js +155 -0
  44. package/packages/dd-trace/src/exporters/agent/index.js +25 -7
  45. package/packages/dd-trace/src/exporters/agent/writer.js +7 -4
  46. package/packages/dd-trace/src/{profiling/exporters → exporters/common}/form-data.js +0 -0
  47. package/packages/dd-trace/src/exporters/common/request.js +25 -10
  48. package/packages/dd-trace/src/exporters/common/writer.js +9 -6
  49. package/packages/dd-trace/src/exporters/span-stats/index.js +20 -0
  50. package/packages/dd-trace/src/exporters/span-stats/writer.js +54 -0
  51. package/packages/dd-trace/src/format.js +2 -0
  52. package/packages/dd-trace/src/id.js +16 -13
  53. package/packages/dd-trace/src/iitm.js +11 -0
  54. package/packages/dd-trace/src/index.js +10 -0
  55. package/packages/dd-trace/src/noop/proxy.js +87 -0
  56. package/packages/dd-trace/src/opentracing/propagation/text_map.js +77 -6
  57. package/packages/dd-trace/src/opentracing/tracer.js +1 -1
  58. package/packages/dd-trace/src/plugin_manager.js +107 -65
  59. package/packages/dd-trace/src/plugins/index.js +58 -45
  60. package/packages/dd-trace/src/plugins/log_plugin.js +16 -9
  61. package/packages/dd-trace/src/plugins/util/ci.js +34 -9
  62. package/packages/dd-trace/src/plugins/util/git.js +52 -2
  63. package/packages/dd-trace/src/plugins/util/ip_blocklist.js +25 -0
  64. package/packages/dd-trace/src/plugins/util/tags.js +4 -1
  65. package/packages/dd-trace/src/plugins/util/web.js +99 -2
  66. package/packages/dd-trace/src/priority_sampler.js +36 -1
  67. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  68. package/packages/dd-trace/src/proxy.js +23 -89
  69. package/packages/dd-trace/src/ritm.js +10 -1
  70. package/packages/dd-trace/src/span_processor.js +7 -1
  71. package/packages/dd-trace/src/span_stats.js +210 -0
  72. package/packages/dd-trace/src/startup-log.js +8 -19
  73. package/packages/dd-trace/src/telemetry/dependencies.js +83 -0
  74. package/packages/dd-trace/src/{telemetry.js → telemetry/index.js} +11 -79
  75. package/packages/dd-trace/src/telemetry/send-data.js +35 -0
  76. package/scripts/install_plugin_modules.js +17 -26
@@ -0,0 +1,94 @@
1
+ 'use strict'
2
+
3
+ const OVERHEAD_CONTROLLER_CONTEXT_KEY = 'oce'
4
+ const REPORT_VULNERABILITY = 'REPORT_VULNERABILITY'
5
+
6
+ const GLOBAL_OCE_CONTEXT = {}
7
+ let config = {}
8
+ let availableRequest = 0
9
+ const OPERATIONS = {
10
+ REPORT_VULNERABILITY: {
11
+ hasQuota: (context) => {
12
+ const reserved = context && context.tokens && context.tokens[REPORT_VULNERABILITY] > 0
13
+ if (reserved) {
14
+ context.tokens[REPORT_VULNERABILITY]--
15
+ }
16
+ return reserved
17
+ },
18
+ name: REPORT_VULNERABILITY,
19
+ initialTokenBucketSize () {
20
+ return typeof config.maxContextOperations === 'number' ? config.maxContextOperations : 2
21
+ },
22
+ initContext: function (context) {
23
+ context.tokens[REPORT_VULNERABILITY] = this.initialTokenBucketSize()
24
+ }
25
+ }
26
+ }
27
+
28
+ function _getNewContext () {
29
+ const oceContext = {
30
+ tokens: {}
31
+ }
32
+
33
+ for (const operation in OPERATIONS) {
34
+ OPERATIONS[operation].initContext(oceContext)
35
+ }
36
+
37
+ return oceContext
38
+ }
39
+
40
+ function _getContext (iastContext) {
41
+ if (iastContext && iastContext[OVERHEAD_CONTROLLER_CONTEXT_KEY]) {
42
+ return iastContext[OVERHEAD_CONTROLLER_CONTEXT_KEY]
43
+ }
44
+ return GLOBAL_OCE_CONTEXT
45
+ }
46
+
47
+ function _resetGlobalContext () {
48
+ Object.assign(GLOBAL_OCE_CONTEXT, _getNewContext())
49
+ }
50
+
51
+ function acquireRequest (rootSpan) {
52
+ if (availableRequest > 0) {
53
+ const sampling = config && typeof config.requestSampling === 'number'
54
+ ? config.requestSampling : 30
55
+ if (rootSpan.context().toSpanId().slice(-2) <= sampling) {
56
+ availableRequest--
57
+ return true
58
+ }
59
+ }
60
+ return false
61
+ }
62
+
63
+ function releaseRequest () {
64
+ if (availableRequest < config.maxConcurrentRequests) {
65
+ availableRequest++
66
+ }
67
+ }
68
+
69
+ function hasQuota (operation, iastContext) {
70
+ const oceContext = _getContext(iastContext)
71
+ return operation.hasQuota(oceContext)
72
+ }
73
+
74
+ function initializeRequestContext (iastContext) {
75
+ if (iastContext) iastContext[OVERHEAD_CONTROLLER_CONTEXT_KEY] = _getNewContext()
76
+ }
77
+
78
+ function configure (cfg) {
79
+ config = cfg
80
+ availableRequest = config.maxConcurrentRequests
81
+ }
82
+
83
+ _resetGlobalContext()
84
+
85
+ module.exports = {
86
+ OVERHEAD_CONTROLLER_CONTEXT_KEY,
87
+ OPERATIONS,
88
+ _resetGlobalContext,
89
+ initializeRequestContext,
90
+ hasQuota,
91
+ acquireRequest,
92
+ releaseRequest,
93
+ configure
94
+ }
@@ -0,0 +1,70 @@
1
+ const path = require('path')
2
+ const pathLine = {
3
+ getFirstNonDDPathAndLine,
4
+ getFirstNonDDPathAndLineFromCallsites, // Exported only for test purposes
5
+ calculateDDBasePath, // Exported only for test purposes
6
+ ddBasePath: calculateDDBasePath(__dirname) // Only for test purposes
7
+ }
8
+
9
+ const EXCLUDED_PATHS = [
10
+ '/node_modules/diagnostics_channel'
11
+ ]
12
+ const EXCLUDED_PATH_PREFIXES = [
13
+ 'node:diagnostics_channel',
14
+ 'diagnostics_channel'
15
+ ]
16
+
17
+ function calculateDDBasePath (dirname) {
18
+ const dirSteps = dirname.split(path.sep)
19
+ const packagesIndex = dirSteps.indexOf('packages')
20
+ return dirSteps.slice(0, packagesIndex).join(path.sep) + path.sep
21
+ }
22
+
23
+ function getCallSiteInfo () {
24
+ const previousPrepareStackTrace = Error.prepareStackTrace
25
+ let callsiteList
26
+ Error.prepareStackTrace = function (_, callsites) {
27
+ callsiteList = callsites
28
+ }
29
+ const e = new Error()
30
+ e.stack
31
+ Error.prepareStackTrace = previousPrepareStackTrace
32
+ return callsiteList
33
+ }
34
+
35
+ function getFirstNonDDPathAndLineFromCallsites (callsites) {
36
+ if (callsites) {
37
+ for (let i = 0; i < callsites.length; i++) {
38
+ const callsite = callsites[i]
39
+ const path = callsite.getFileName()
40
+ if (!isExcluded(callsite) && path.indexOf(pathLine.ddBasePath) === -1) {
41
+ return {
42
+ path,
43
+ line: callsite.getLineNumber()
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return null
49
+ }
50
+
51
+ function isExcluded (callsite) {
52
+ if (callsite.isNative()) return true
53
+ const filename = callsite.getFileName()
54
+ for (let i = 0; i < EXCLUDED_PATHS.length; i++) {
55
+ if (filename.indexOf(EXCLUDED_PATHS[i]) > -1) {
56
+ return true
57
+ }
58
+ }
59
+ for (let i = 0; i < EXCLUDED_PATH_PREFIXES.length; i++) {
60
+ if (filename.indexOf(EXCLUDED_PATH_PREFIXES[i]) === 0) {
61
+ return true
62
+ }
63
+ }
64
+ return false
65
+ }
66
+
67
+ function getFirstNonDDPathAndLine () {
68
+ return getFirstNonDDPathAndLineFromCallsites(getCallSiteInfo())
69
+ }
70
+ module.exports = pathLine
@@ -0,0 +1,113 @@
1
+ const { MANUAL_KEEP } = require('../../../../../ext/tags')
2
+ const VULNERABILITIES_KEY = 'vulnerabilities'
3
+ const IAST_JSON_TAG_KEY = '_dd.iast.json'
4
+
5
+ function createVulnerability (type, evidence, spanId, location) {
6
+ if (type && evidence && spanId) {
7
+ return {
8
+ type,
9
+ evidence,
10
+ location: {
11
+ spanId,
12
+ ...location
13
+ },
14
+ hash: createHash(type, location)
15
+ }
16
+ }
17
+ return null
18
+ }
19
+
20
+ function createHash (type, location) {
21
+ let hashSource
22
+ if (location) {
23
+ hashSource = `${type}:${location.path}:${location.line}`
24
+ } else {
25
+ hashSource = type
26
+ }
27
+ let hash = 0
28
+ let offset = 0
29
+ const size = hashSource.length
30
+ for (let i = 0; i < size; i++) {
31
+ hash = ((hash << 5) - hash) + hashSource.charCodeAt(offset++)
32
+ }
33
+ return hash
34
+ }
35
+
36
+ function addVulnerability (iastContext, vulnerability) {
37
+ if (iastContext && vulnerability && vulnerability.evidence && vulnerability.type &&
38
+ vulnerability.location && vulnerability.location.spanId) {
39
+ iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || []
40
+ iastContext[VULNERABILITIES_KEY].push(vulnerability)
41
+ }
42
+ }
43
+
44
+ function isValidVulnerability (vulnerability) {
45
+ return vulnerability && vulnerability.type &&
46
+ vulnerability.evidence && vulnerability.evidence.value &&
47
+ vulnerability.location && vulnerability.location.spanId
48
+ }
49
+
50
+ function jsonVulnerabilityFromVulnerability (vulnerability) {
51
+ const jsonVulnerability = {
52
+ type: vulnerability.type,
53
+ hash: vulnerability.hash,
54
+ evidence: {
55
+ value: vulnerability.evidence.value
56
+ },
57
+ location: {
58
+ spanId: vulnerability.location.spanId
59
+ }
60
+ }
61
+ if (vulnerability.location.path) {
62
+ jsonVulnerability.location.path = vulnerability.location.path
63
+ }
64
+ if (vulnerability.location.line) {
65
+ jsonVulnerability.location.line = vulnerability.location.line
66
+ }
67
+ return jsonVulnerability
68
+ }
69
+
70
+ function sendVulnerabilities (iastContext) {
71
+ if (iastContext && iastContext.rootSpan && iastContext[VULNERABILITIES_KEY] &&
72
+ iastContext[VULNERABILITIES_KEY].length && iastContext.rootSpan.addTags) {
73
+ const span = iastContext.rootSpan
74
+ const allVulnerabilities = iastContext[VULNERABILITIES_KEY]
75
+ // TODO support sources and ranges
76
+ const jsonToSend = {
77
+ vulnerabilities: []
78
+ }
79
+
80
+ deduplicateVulnerabilities(allVulnerabilities).forEach((vulnerability) => {
81
+ if (isValidVulnerability(vulnerability)) {
82
+ jsonToSend.vulnerabilities.push(jsonVulnerabilityFromVulnerability(vulnerability))
83
+ }
84
+ })
85
+
86
+ if (jsonToSend.vulnerabilities.length > 0) {
87
+ const tags = {}
88
+ // TODO: Store this outside of the span and set the tag in the exporter.
89
+ tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend)
90
+ tags[MANUAL_KEEP] = 'true'
91
+ span.addTags(tags)
92
+ }
93
+ }
94
+ return IAST_JSON_TAG_KEY
95
+ }
96
+
97
+ function deduplicateVulnerabilities (vulnerabilities) {
98
+ const uniqueVulnerabilities = new Set()
99
+ return vulnerabilities.filter((vulnerability) => {
100
+ const key = `${vulnerability.type}${vulnerability.hash}`
101
+ if (!uniqueVulnerabilities.has(key)) {
102
+ uniqueVulnerabilities.add(key)
103
+ return true
104
+ }
105
+ return false
106
+ })
107
+ }
108
+
109
+ module.exports = {
110
+ createVulnerability,
111
+ addVulnerability,
112
+ sendVulnerabilities
113
+ }
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+ const request = require('../../../exporters/common/request')
3
+ const log = require('../../../log')
4
+
5
+ const { CoverageCIVisibilityEncoder } = require('../../../encode/coverage-ci-visibility')
6
+ const BaseWriter = require('../../../exporters/common/writer')
7
+
8
+ function safeJSONStringify (value) {
9
+ return JSON.stringify(value, (key, value) =>
10
+ key !== 'dd-api-key' ? value : undefined
11
+ )
12
+ }
13
+
14
+ class Writer extends BaseWriter {
15
+ constructor ({ url }) {
16
+ super(...arguments)
17
+ this._url = url
18
+ this._encoder = new CoverageCIVisibilityEncoder()
19
+ }
20
+
21
+ _sendPayload (form, _, done) {
22
+ const options = {
23
+ path: '/api/v2/citestcov',
24
+ method: 'POST',
25
+ headers: {
26
+ 'dd-api-key': process.env.DATADOG_API_KEY || process.env.DD_API_KEY,
27
+ ...form.getHeaders()
28
+ },
29
+ timeout: 15000
30
+ }
31
+
32
+ options.protocol = this._url.protocol
33
+ options.hostname = this._url.hostname
34
+ options.port = this._url.port
35
+
36
+ log.debug(() => `Request to the intake: ${safeJSONStringify(options)}`)
37
+
38
+ request(form, options, (err, res) => {
39
+ if (err) {
40
+ log.error(err)
41
+ done()
42
+ return
43
+ }
44
+ log.debug(`Response from the intake: ${res}`)
45
+ done()
46
+ })
47
+ }
48
+ }
49
+
50
+ module.exports = Writer
@@ -2,30 +2,75 @@
2
2
 
3
3
  const URL = require('url').URL
4
4
  const Writer = require('./writer')
5
- const Scheduler = require('../../../exporters/scheduler')
5
+ const CoverageWriter = require('./coverage-writer')
6
+
7
+ const log = require('../../../log')
6
8
 
7
9
  class AgentlessCiVisibilityExporter {
8
10
  constructor (config) {
9
- const { flushInterval, tags, site, url } = config
11
+ this._config = config
12
+ const { tags, site, url, isIntelligentTestRunnerEnabled } = config
13
+ this._isIntelligentTestRunnerEnabled = isIntelligentTestRunnerEnabled
10
14
  this._url = url || new URL(`https://citestcycle-intake.${site}`)
11
15
  this._writer = new Writer({ url: this._url, tags })
16
+ this._timer = undefined
17
+ this._coverageTimer = undefined
18
+
19
+ this._coverageUrl = url || new URL(`https://event-platform-intake.${site}`)
20
+ this._coverageWriter = new CoverageWriter({ url: this._coverageUrl })
21
+
22
+ process.once('beforeExit', () => {
23
+ this._writer.flush()
24
+ this._coverageWriter.flush()
25
+ })
26
+ }
12
27
 
13
- if (flushInterval > 0) {
14
- this._scheduler = new Scheduler(() => this._writer.flush(), flushInterval)
28
+ exportCoverage ({ testSpan, coverageFiles }) {
29
+ const formattedCoverage = {
30
+ traceId: testSpan.context()._traceId,
31
+ spanId: testSpan.context()._spanId,
32
+ files: coverageFiles
33
+ }
34
+ this._coverageWriter.append(formattedCoverage)
35
+
36
+ const { flushInterval } = this._config
37
+
38
+ if (flushInterval === 0) {
39
+ this._coverageWriter.flush()
40
+ } else if (flushInterval > 0 && !this._coverageTimer) {
41
+ this._coverageTimer = setTimeout(() => {
42
+ this._coverageWriter.flush()
43
+ this._coverageTimer = clearTimeout(this._coverageTimer)
44
+ }, flushInterval).unref()
15
45
  }
16
- this._scheduler && this._scheduler.start()
17
46
  }
18
47
 
19
48
  export (trace) {
20
49
  this._writer.append(trace)
21
50
 
22
- if (!this._scheduler) {
51
+ const { flushInterval } = this._config
52
+
53
+ if (flushInterval === 0) {
23
54
  this._writer.flush()
55
+ } else if (flushInterval > 0 && !this._timer) {
56
+ this._timer = setTimeout(() => {
57
+ this._writer.flush()
58
+ this._timer = clearTimeout(this._timer)
59
+ }, flushInterval).unref()
24
60
  }
25
61
  }
26
62
 
27
- flush () {
28
- this._writer.flush()
63
+ setUrl (url, coverageUrl = url) {
64
+ try {
65
+ url = new URL(url)
66
+ coverageUrl = new URL(coverageUrl)
67
+ this._url = url
68
+ this._coverageUrl = coverageUrl
69
+ this._writer.setUrl(url)
70
+ this._coverageWriter.setUrl(coverageUrl)
71
+ } catch (e) {
72
+ log.error(e)
73
+ }
29
74
  }
30
75
  }
31
76
 
@@ -5,16 +5,37 @@ const log = require('../../../log')
5
5
  const { AgentlessCiVisibilityEncoder } = require('../../../encode/agentless-ci-visibility')
6
6
  const BaseWriter = require('../../../exporters/common/writer')
7
7
 
8
+ function safeJSONStringify (value) {
9
+ return JSON.stringify(value, (key, value) =>
10
+ key !== 'dd-api-key' ? value : undefined
11
+ )
12
+ }
13
+
8
14
  class Writer extends BaseWriter {
9
15
  constructor ({ url, tags }) {
10
16
  super(...arguments)
11
17
  const { 'runtime-id': runtimeId, env, service } = tags
12
18
  this._url = url
13
- this._encoder = new AgentlessCiVisibilityEncoder({ runtimeId, env, service })
19
+ this._encoder = new AgentlessCiVisibilityEncoder(this, { runtimeId, env, service })
14
20
  }
15
21
 
16
22
  _sendPayload (data, _, done) {
17
- makeRequest(data, this._url, (err, res) => {
23
+ const options = {
24
+ path: '/api/v2/citestcycle',
25
+ method: 'POST',
26
+ headers: {
27
+ 'dd-api-key': process.env.DATADOG_API_KEY || process.env.DD_API_KEY,
28
+ 'Content-Type': 'application/msgpack'
29
+ },
30
+ timeout: 15000
31
+ }
32
+
33
+ options.protocol = this._url.protocol
34
+ options.hostname = this._url.hostname
35
+ options.port = this._url.port
36
+
37
+ log.debug(() => `Request to the intake: ${safeJSONStringify(options)}`)
38
+ request(data, options, (err, res) => {
18
39
  if (err) {
19
40
  log.error(err)
20
41
  done()
@@ -26,26 +47,4 @@ class Writer extends BaseWriter {
26
47
  }
27
48
  }
28
49
 
29
- function makeRequest (data, url, cb) {
30
- const options = {
31
- path: '/api/v2/citestcycle',
32
- method: 'POST',
33
- headers: {
34
- 'Content-Type': 'application/msgpack',
35
- 'dd-api-key': process.env.DATADOG_API_KEY || process.env.DD_API_KEY
36
- },
37
- timeout: 15000
38
- }
39
-
40
- options.protocol = url.protocol
41
- options.hostname = url.hostname
42
- options.port = url.port
43
-
44
- log.debug(() => `Request to the intake: ${JSON.stringify(options)}`)
45
-
46
- request(data, options, false, (err, res) => {
47
- cb(err, res)
48
- })
49
- }
50
-
51
50
  module.exports = Writer
@@ -0,0 +1,220 @@
1
+
2
+ const fs = require('fs')
3
+ const https = require('https')
4
+ const path = require('path')
5
+
6
+ const FormData = require('../../../exporters/common/form-data')
7
+
8
+ const log = require('../../../log')
9
+ const {
10
+ getLatestCommits,
11
+ getRepositoryUrl,
12
+ generatePackFilesForCommits,
13
+ getCommitsToUpload
14
+ } = require('../../../plugins/util/git')
15
+
16
+ const isValidSha = (sha) => /[0-9a-f]{40}/.test(sha)
17
+
18
+ function sanitizeCommits (commits) {
19
+ return commits.map(({ id: commitSha, type }) => {
20
+ if (type !== 'commit') {
21
+ throw new Error('Invalid commit response')
22
+ }
23
+ const sanitizedCommit = commitSha.replace(/[^0-9a-f]+/g, '')
24
+ if (sanitizedCommit !== commitSha || !isValidSha(sanitizedCommit)) {
25
+ throw new Error('Invalid commit format')
26
+ }
27
+ return sanitizedCommit
28
+ })
29
+ }
30
+
31
+ function getCommonRequestOptions (url) {
32
+ return {
33
+ method: 'POST',
34
+ headers: {
35
+ 'dd-api-key': process.env.DATADOG_API_KEY || process.env.DD_API_KEY
36
+ },
37
+ timeout: 15000,
38
+ protocol: url.protocol,
39
+ hostname: url.hostname,
40
+ port: url.port
41
+ }
42
+ }
43
+
44
+ /**
45
+ * This function posts the SHAs of the commits of the last month
46
+ * The response are the commits for which the backend already has information
47
+ * This response is used to know which commits can be ignored from there on
48
+ */
49
+ function getCommitsToExclude ({ url, repositoryUrl }, callback) {
50
+ const latestCommits = getLatestCommits()
51
+ const [headCommit] = latestCommits
52
+
53
+ const commonOptions = getCommonRequestOptions(url)
54
+
55
+ const options = {
56
+ ...commonOptions,
57
+ headers: {
58
+ ...commonOptions.headers,
59
+ 'Content-Type': 'application/json'
60
+ },
61
+ path: '/api/v2/git/repository/search_commits'
62
+ }
63
+
64
+ const localCommitData = JSON.stringify({
65
+ meta: {
66
+ repository_url: repositoryUrl
67
+ },
68
+ data: latestCommits.map(commit => ({
69
+ id: commit,
70
+ type: 'commit'
71
+ }))
72
+ })
73
+
74
+ const request = https.request(options, (res) => {
75
+ let responseData = ''
76
+
77
+ res.on('data', chunk => { responseData += chunk })
78
+ res.on('end', () => {
79
+ if (res.statusCode === 200) {
80
+ let commitsToExclude
81
+ try {
82
+ commitsToExclude = sanitizeCommits(JSON.parse(responseData).data)
83
+ } catch (e) {
84
+ callback(new Error(`Can't parse response: ${e.message}`))
85
+ return
86
+ }
87
+ callback(null, commitsToExclude, headCommit)
88
+ } else {
89
+ const error = new Error(`Error getting commits: ${res.statusCode} ${res.statusMessage}`)
90
+ callback(error)
91
+ }
92
+ })
93
+ })
94
+
95
+ request.write(localCommitData)
96
+ request.on('error', callback)
97
+
98
+ request.end()
99
+
100
+ return request
101
+ }
102
+
103
+ /**
104
+ * This function uploads a git packfile
105
+ */
106
+ function uploadPackFile ({ url, packFileToUpload, repositoryUrl, headCommit }, callback) {
107
+ const form = new FormData()
108
+
109
+ const pushedSha = JSON.stringify({
110
+ data: {
111
+ id: headCommit,
112
+ type: 'commit'
113
+ },
114
+ meta: {
115
+ repository_url: repositoryUrl
116
+ }
117
+ })
118
+
119
+ form.append('pushedSha', pushedSha, { contentType: 'application/json' })
120
+
121
+ try {
122
+ const packFileContent = fs.readFileSync(packFileToUpload)
123
+ // The original filename includes a random prefix, so we remove it here
124
+ const [, filename] = path.basename(packFileToUpload).split('-')
125
+ form.append('packfile', packFileContent, {
126
+ filename,
127
+ contentType: 'application/octet-stream'
128
+ })
129
+ } catch (e) {
130
+ callback(new Error(`Error reading packfile: ${packFileToUpload}`))
131
+ return
132
+ }
133
+
134
+ const commonOptions = getCommonRequestOptions(url)
135
+
136
+ const options = {
137
+ ...commonOptions,
138
+ path: '/api/v2/git/repository/packfile',
139
+ headers: {
140
+ ...commonOptions.headers,
141
+ ...form.getHeaders()
142
+ }
143
+ }
144
+
145
+ const req = https.request(options, res => {
146
+ res.on('data', () => {})
147
+ res.on('end', () => {
148
+ if (res.statusCode === 204) {
149
+ callback(null)
150
+ } else {
151
+ const error = new Error(`Error uploading packfiles: ${res.statusCode} ${res.statusMessage}`)
152
+ error.status = res.statusCode
153
+
154
+ callback(error)
155
+ }
156
+ })
157
+ })
158
+
159
+ req.on('error', err => {
160
+ callback(err)
161
+ })
162
+ form.pipe(req)
163
+ }
164
+
165
+ /**
166
+ * This function uploads git metadata to CI Visibility's backend.
167
+ */
168
+ function sendGitMetadata (site, callback) {
169
+ const url = new URL(`https://api.${site}`)
170
+
171
+ const repositoryUrl = getRepositoryUrl()
172
+
173
+ getCommitsToExclude({ url, repositoryUrl }, (err, commitsToExclude, headCommit) => {
174
+ if (err) {
175
+ callback(err)
176
+ return
177
+ }
178
+ const commitsToUpload = getCommitsToUpload(commitsToExclude)
179
+
180
+ if (!commitsToUpload.length) {
181
+ log.debug('No commits to upload')
182
+ callback(null)
183
+ return
184
+ }
185
+
186
+ const packFilesToUpload = generatePackFilesForCommits(commitsToUpload)
187
+
188
+ let packFileIndex = 0
189
+ // This uploads packfiles sequentially
190
+ const uploadPackFileCallback = (err) => {
191
+ if (err || packFileIndex === packFilesToUpload.length) {
192
+ callback(err)
193
+ return
194
+ }
195
+ return uploadPackFile(
196
+ {
197
+ packFileToUpload: packFilesToUpload[packFileIndex++],
198
+ url,
199
+ repositoryUrl,
200
+ headCommit
201
+ },
202
+ uploadPackFileCallback
203
+ )
204
+ }
205
+
206
+ uploadPackFile(
207
+ {
208
+ url,
209
+ packFileToUpload: packFilesToUpload[packFileIndex++],
210
+ repositoryUrl,
211
+ headCommit
212
+ },
213
+ uploadPackFileCallback
214
+ )
215
+ })
216
+ }
217
+
218
+ module.exports = {
219
+ sendGitMetadata
220
+ }