Skip to content

Commit f97d2cd

Browse files
authored
fix: some guardrail telemetry should only be sent once (#6029)
Fixes bug in the guardrail telemetry `hasSeen` function which meant that all telemetry was always sent even though `abort` and `abort.integration` should only be sent once.
1 parent c2a1f8c commit f97d2cd

File tree

5 files changed

+111
-28
lines changed

5 files changed

+111
-28
lines changed

.github/workflows/platform.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ jobs:
283283
- run: yarn test:integration
284284
- run: yarn test:integration:esbuild
285285

286+
unit-guardrails:
287+
runs-on: ubuntu-latest
288+
steps:
289+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
290+
- uses: ./.github/actions/node/active-lts
291+
- uses: ./.github/actions/install
292+
- run: yarn test:trace:guardrails:ci
293+
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
294+
286295
# We'll run these separately for earlier (i.e. unsupported) versions
287296
integration-guardrails:
288297
strategy:

integration-tests/helpers/index.js

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,34 +58,35 @@ let sandbox
5858
// This _must_ be used with the useSandbox function
5959
async function runAndCheckWithTelemetry (filename, expectedOut, expectedTelemetryPoints, expectedSource) {
6060
const cwd = sandbox.folder
61-
const cleanup = telemetryForwarder(expectedTelemetryPoints)
61+
const cleanup = telemetryForwarder(expectedTelemetryPoints.length > 0)
6262
const pid = await runAndCheckOutput(filename, cwd, expectedOut, expectedSource)
6363
const msgs = await cleanup()
6464
if (expectedTelemetryPoints.length === 0) {
6565
// assert no telemetry sent
66-
try {
67-
assert.deepStrictEqual(msgs.length, 0)
68-
} catch (e) {
69-
// This console.log is useful for debugging telemetry. Plz don't remove.
70-
// eslint-disable-next-line no-console
71-
console.error('Expected no telemetry, but got:\n', msgs.map(msg => JSON.stringify(msg[1].points)).join('\n'))
72-
throw e
73-
}
74-
return
66+
assert.strictEqual(msgs.length, 0, `Expected no telemetry, but got:\n${
67+
msgs.map(msg => JSON.stringify(msg[1].points)).join('\n')
68+
}`)
69+
} else {
70+
assertTelemetryPoints(pid, msgs, expectedTelemetryPoints)
7571
}
72+
}
73+
74+
function assertTelemetryPoints (pid, msgs, expectedTelemetryPoints) {
7675
let points = []
7776
for (const [telemetryType, data] of msgs) {
7877
assert.strictEqual(telemetryType, 'library_entrypoint')
7978
assert.deepStrictEqual(data.metadata, meta(pid))
8079
points = points.concat(data.points)
8180
}
82-
let expectedPoints = getPoints(...expectedTelemetryPoints)
83-
// We now have to sort both the expected and actual telemetry points.
84-
// This is because data can come in in any order.
85-
// We'll just contatenate all the data together for each point and sort them.
86-
points = points.map(p => p.name + '\t' + p.tags.join(',')).sort().join('\n')
87-
expectedPoints = expectedPoints.map(p => p.name + '\t' + p.tags.join(',')).sort().join('\n')
88-
assert.strictEqual(points, expectedPoints)
81+
const expectedPoints = getPoints(...expectedTelemetryPoints)
82+
// Sort since data can come in in any order.
83+
assert.deepStrictEqual(points.sort(pointsSorter), expectedPoints.sort(pointsSorter))
84+
85+
function pointsSorter (a, b) {
86+
a = a.name + '\t' + a.tags.join(',')
87+
b = b.name + '\t' + b.tags.join(',')
88+
return a === b ? 0 : a < b ? -1 : 1
89+
}
8990

9091
function getPoints (...args) {
9192
const expectedPoints = []
@@ -94,7 +95,7 @@ async function runAndCheckWithTelemetry (filename, expectedOut, expectedTelemetr
9495
if (!currentPoint.name) {
9596
currentPoint.name = 'library_entrypoint.' + arg
9697
} else {
97-
currentPoint.tags = arg.split(',')
98+
currentPoint.tags = arg.split(',').filter(Boolean)
9899
expectedPoints.push(currentPoint)
99100
currentPoint = {}
100101
}
@@ -239,7 +240,7 @@ async function createSandbox (dependencies = [], isGitRepo = false,
239240
}
240241
}
241242

242-
function telemetryForwarder (expectedTelemetryPoints) {
243+
function telemetryForwarder (shouldExpectTelemetryPoints = true) {
243244
process.env.DD_TELEMETRY_FORWARDER_PATH =
244245
path.join(__dirname, '..', 'telemetry-forwarder.sh')
245246
process.env.FORWARDER_OUT = path.join(__dirname, `forwarder-${Date.now()}.out`)
@@ -257,7 +258,7 @@ function telemetryForwarder (expectedTelemetryPoints) {
257258
try {
258259
msgs = fs.readFileSync(process.env.FORWARDER_OUT, 'utf8').trim().split('\n')
259260
} catch (e) {
260-
if (expectedTelemetryPoints.length && e.code === 'ENOENT' && retries < 10) {
261+
if (shouldExpectTelemetryPoints && e.code === 'ENOENT' && retries < 10) {
261262
return tryAgain()
262263
}
263264
return []
@@ -438,6 +439,8 @@ module.exports = {
438439
assertObjectContains,
439440
assertUUID,
440441
spawnProc,
442+
telemetryForwarder,
443+
assertTelemetryPoints,
441444
runAndCheckWithTelemetry,
442445
createSandbox,
443446
curl,

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"test:eslint-rules": "node eslint-rules/*.test.mjs",
2828
"test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,datastreams,encode,exporters,opentelemetry,opentracing,plugins,service-naming,standalone,telemetry}/**/*.spec.js\"",
2929
"test:trace:core:ci": "npm run test:trace:core -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/**/*.js\"",
30+
"test:trace:guardrails": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/guardrails/**/*.spec.js\"",
31+
"test:trace:guardrails:ci": "nyc --no-clean --include \"packages/dd-trace/src/guardrails/**/*.js\" -- npm run test:trace:guardrails",
3032
"test:instrumentations": "mocha -r 'packages/dd-trace/test/setup/mocha.js' \"packages/datadog-instrumentations/test/@($(echo $PLUGINS)).spec.js\"",
3133
"test:instrumentations:ci": "yarn services && nyc --no-clean --include \"packages/datadog-instrumentations/src/@($(echo $PLUGINS)).js\" --include \"packages/datadog-instrumentations/src/@($(echo $PLUGINS))/**/*.js\" -- npm run test:instrumentations",
3234
"test:instrumentations:misc": "mocha -r 'packages/dd-trace/test/setup/mocha.js' 'packages/datadog-instrumentations/test/*/**/*.spec.js'",

packages/dd-trace/src/guardrails/telemetry.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,23 @@ var metadata = {
2525
pid: process.pid
2626
}
2727

28-
var seen = []
29-
function hasSeen (point) {
28+
var seen = {}
29+
function shouldSend (point) {
3030
if (point.name === 'abort') {
3131
// This one can only be sent once, regardless of tags
32-
return seen.indexOf('abort') !== -1
33-
}
34-
if (point.name === 'abort.integration') {
32+
if (seen.abort) {
33+
return false
34+
}
35+
seen.abort = true
36+
} else if (point.name === 'abort.integration') {
3537
// For now, this is the only other one we want to dedupe
3638
var compiledPoint = point.name + point.tags.join('')
37-
return seen.indexOf(compiledPoint) !== -1
39+
if (seen[compiledPoint]) {
40+
return false
41+
}
42+
seen[compiledPoint] = true
3843
}
39-
return false
44+
return true
4045
}
4146

4247
function sendTelemetry (name, tags) {
@@ -47,7 +52,7 @@ function sendTelemetry (name, tags) {
4752
if (['1', 'true', 'True'].indexOf(process.env.DD_INJECT_FORCE) !== -1) {
4853
points = points.filter(function (p) { return ['error', 'complete'].indexOf(p.name) !== -1 })
4954
}
50-
points = points.filter(function (p) { return !hasSeen(p) })
55+
points = points.filter(function (p) { return shouldSend(p) })
5156
for (var i = 0; i < points.length; i++) {
5257
points[i].name = 'library_entrypoint.' + points[i].name
5358
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict'
2+
3+
process.env.DD_INJECTION_ENABLED = 'true'
4+
5+
const { telemetryForwarder, assertTelemetryPoints } = require('../../../../integration-tests/helpers')
6+
7+
describe('sendTelemetry', () => {
8+
let cleanup, sendTelemetry
9+
10+
beforeEach(() => {
11+
cleanup = telemetryForwarder()
12+
sendTelemetry = proxyquire('../src/guardrails/telemetry', {})
13+
})
14+
15+
it('should send telemetry', async () => {
16+
sendTelemetry([
17+
{ name: 'abort', tags: ['1'] },
18+
{ name: 'abort.integration', tags: ['2'] },
19+
{ name: 'abort.integration', tags: ['3'] },
20+
{ name: 'foo', tags: ['4'] }
21+
])
22+
const msgs = await cleanup()
23+
assertTelemetryPoints(process.pid, msgs, [
24+
'abort', '1',
25+
'abort.integration', '2',
26+
'abort.integration', '3',
27+
'foo', '4'
28+
])
29+
})
30+
31+
describe('no duplicates', () => {
32+
it('should not send `abort` more than once in the same call', async () => {
33+
sendTelemetry([
34+
{ name: 'abort', tags: ['1'] },
35+
{ name: 'abort', tags: ['2'] }
36+
])
37+
const msgs = await cleanup()
38+
assertTelemetryPoints(process.pid, msgs, ['abort', '1'])
39+
})
40+
41+
it('should not send `abort` more than once in a different call', async () => {
42+
sendTelemetry('abort', ['1'])
43+
sendTelemetry('abort', ['2'])
44+
const msgs = await cleanup()
45+
assertTelemetryPoints(process.pid, msgs, ['abort', '1'])
46+
})
47+
48+
it('should not send `abort.integration` more than once if tags are the same in the same call', async () => {
49+
sendTelemetry([
50+
{ name: 'abort.integration', tags: ['1'] },
51+
{ name: 'abort.integration', tags: ['1'] }
52+
])
53+
const msgs = await cleanup()
54+
assertTelemetryPoints(process.pid, msgs, ['abort.integration', '1'])
55+
})
56+
57+
it('should not send `abort.integration` more than once if tags are the same in a different call', async () => {
58+
sendTelemetry('abort.integration', ['1'])
59+
sendTelemetry('abort.integration', ['1'])
60+
const msgs = await cleanup()
61+
assertTelemetryPoints(process.pid, msgs, ['abort.integration', '1'])
62+
})
63+
})
64+
})

0 commit comments

Comments
 (0)