Skip to content

Commit b30db52

Browse files
committed
feat: add validation for missing fields in telemetry
1 parent b60036b commit b30db52

File tree

3 files changed

+153
-20
lines changed

3 files changed

+153
-20
lines changed

packages/core/src/shared/logger/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type LogTopic =
1919
| 'stepfunctions'
2020
| 'unknown'
2121
| 'resourceCache'
22+
| 'telemetry'
2223

2324
class ErrorLog {
2425
constructor(

packages/core/src/shared/telemetry/util.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from '../vscode/env'
2020
import { addTypeName } from '../utilities/typeConstructors'
2121
import globals, { isWeb } from '../extensionGlobals'
22-
import { mapMetadata } from './telemetryLogger'
22+
import { mapMetadata, MetadataObj } from './telemetryLogger'
2323
import { Result } from './telemetry.gen'
2424
import { MetricDatum } from './clienttelemetry'
2525
import { isValidationExemptMetric } from './exemptMetrics'
@@ -312,32 +312,40 @@ export async function getComputeEnvType(): Promise<EnvType> {
312312

313313
/**
314314
* Validates that emitted telemetry metrics
315-
* 1. contain a result property and
315+
* 1. contain a result property
316316
* 2. contain a reason propery if result = 'Failed'.
317+
* 3. are not missing fields
317318
*/
318-
export function validateMetricEvent(event: MetricDatum, fatal: boolean) {
319+
export function validateMetricEvent(event: MetricDatum, fatal: boolean, isExempt = isValidationExemptMetric) {
320+
if (!isExempt(event.MetricName) && event.Metadata) {
321+
const metadata = mapMetadata([])(event.Metadata)
322+
validateMetadata(event.MetricName, metadata, fatal)
323+
}
324+
}
325+
326+
function validateMetadata(metricName: string, metadata: MetadataObj, fatal: boolean) {
319327
const failedStr: Result = 'Failed'
320-
const telemetryRunDocsStr =
328+
const preferRunSuffix =
321329
' Consider using `.run()` instead of `.emit()`, which will set these properties automatically. ' +
322330
'See https://github.com/aws/aws-toolkit-vscode/blob/master/docs/telemetry.md#guidelines'
323-
324-
if (!isValidationExemptMetric(event.MetricName) && event.Metadata) {
325-
const metadata = mapMetadata([])(event.Metadata)
326-
let msg = 'telemetry: invalid Metric: '
327-
328-
if (metadata.result === undefined) {
329-
msg += `"${event.MetricName}" emitted without the \`result\` property, which is always required.`
330-
} else if (metadata.result === failedStr && metadata.reason === undefined) {
331-
msg += `"${event.MetricName}" emitted with result=Failed but without the \`reason\` property.`
332-
} else {
333-
return // Validation passed.
334-
}
335-
336-
msg += telemetryRunDocsStr
331+
const msgPrefix = 'invalid Metric: '
332+
const logger = getLogger('telemetry')
333+
const logOrThrow = (msg: string, includeSuffix: boolean) => {
334+
const fullMsg = msgPrefix + msg + (includeSuffix ? preferRunSuffix : '')
335+
logger.warn(fullMsg)
337336
if (fatal) {
338-
throw new Error(msg)
337+
throw new Error(fullMsg)
339338
}
340-
getLogger().warn(msg)
339+
}
340+
341+
if (metadata.result === undefined) {
342+
logOrThrow(`"${metricName}" emitted without the \`result\` property, which is always required.`, true)
343+
} else if (metadata.result === failedStr && metadata.reason === undefined) {
344+
logOrThrow(`"${metricName}" emitted with result=Failed but without the \`reason\` property.`, true)
345+
}
346+
347+
if (metadata.missingFields) {
348+
logOrThrow(`"${metricName} emitted with missing fields: ${metadata.missingFields}`, false)
341349
}
342350
}
343351

packages/core/src/test/shared/telemetry/util.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ import {
1515
SessionId,
1616
telemetryClientIdEnvKey,
1717
TelemetryConfig,
18+
validateMetricEvent,
1819
} from '../../../shared/telemetry/util'
1920
import { extensionVersion } from '../../../shared/vscode/env'
2021
import { FakeMemento } from '../../fakeExtensionContext'
2122
import { GlobalState } from '../../../shared/globalState'
2223
import { randomUUID } from 'crypto'
2324
import { isUuid } from '../../../shared/crypto'
25+
import { MetricDatum } from '../../../shared/telemetry/clienttelemetry'
26+
import { assertLogsContain } from '../../globalSetup.test'
2427

2528
describe('TelemetryConfig', function () {
2629
const settingKey = 'aws.telemetry'
@@ -281,3 +284,124 @@ describe('getUserAgent', function () {
281284
assert.strictEqual(beforeClient, platformPair())
282285
})
283286
})
287+
288+
describe('validateMetricEvent', function () {
289+
it('does not validate exempt metrics', function () {
290+
const metricEvent: MetricDatum = {
291+
MetricName: 'exempt_metric',
292+
Value: 1,
293+
Unit: 'None',
294+
Metadata: [{ Key: 'result', Value: 'Succeeded' }],
295+
} as MetricDatum
296+
297+
validateMetricEvent(metricEvent, true, (_) => true)
298+
assert.throws(() => assertLogsContain('invalid Metric', false, 'warn'))
299+
})
300+
301+
it('passes validation for metrics with proper result property', function () {
302+
const metricEvent: MetricDatum = {
303+
MetricName: 'valid_metric',
304+
Value: 1,
305+
Unit: 'None',
306+
Metadata: [{ Key: 'result', Value: 'Succeeded' }],
307+
} as MetricDatum
308+
309+
validateMetricEvent(metricEvent, true, (_) => false)
310+
assert.throws(() => assertLogsContain('invalid Metric', false, 'warn'))
311+
})
312+
313+
it('passes validation for metrics with Failed result and reason property', function () {
314+
const metricEvent: MetricDatum = {
315+
MetricName: 'valid_failed_metric',
316+
Value: 1,
317+
Unit: 'None',
318+
Metadata: [
319+
{ Key: 'result', Value: 'Failed' },
320+
{ Key: 'reason', Value: 'Something went wrong' },
321+
],
322+
} as MetricDatum
323+
324+
validateMetricEvent(metricEvent, true, (_) => false)
325+
})
326+
327+
it('fails validation for metrics missing result property when fatal=true', function () {
328+
const metricEvent: MetricDatum = {
329+
MetricName: 'invalid_metric_no_result',
330+
Value: 1,
331+
Unit: 'None',
332+
Metadata: [{ Key: 'someOtherProperty', Value: 'value' }],
333+
} as MetricDatum
334+
335+
assert.throws(
336+
() => validateMetricEvent(metricEvent, true, (_) => false),
337+
/emitted without the `result` property/
338+
)
339+
})
340+
341+
it('logs warning for metrics missing result property when fatal=false', function () {
342+
const metricEvent: MetricDatum = {
343+
MetricName: 'invalid_metric_no_result',
344+
Value: 1,
345+
Unit: 'None',
346+
Metadata: [{ Key: 'someOtherProperty', Value: 'value' }],
347+
} as MetricDatum
348+
349+
validateMetricEvent(metricEvent, false, (_) => false)
350+
assertLogsContain('invalid Metric', false, 'warn')
351+
})
352+
353+
it('fails validation for metrics with Failed result but missing reason property when fatal=true', function () {
354+
const metricEvent: MetricDatum = {
355+
MetricName: 'invalid_metric_failed_no_reason',
356+
Value: 1,
357+
Unit: 'None',
358+
Metadata: [{ Key: 'result', Value: 'Failed' }],
359+
} as MetricDatum
360+
361+
assert.throws(
362+
() => validateMetricEvent(metricEvent, true),
363+
/emitted with result=Failed but without the `reason` property/
364+
)
365+
})
366+
367+
it('logs warning for metrics with Failed result but missing reason property when fatal=false', function () {
368+
const metricEvent: MetricDatum = {
369+
MetricName: 'invalid_metric_failed_no_reason',
370+
Value: 1,
371+
Unit: 'None',
372+
Metadata: [{ Key: 'result', Value: 'Failed' }],
373+
} as MetricDatum
374+
375+
validateMetricEvent(metricEvent, false)
376+
assertLogsContain('invalid Metric', false, 'warn')
377+
})
378+
379+
it('fails validation for metrics with missing fields when fatal=true', function () {
380+
const metricEvent: MetricDatum = {
381+
MetricName: 'invalid_metric_missing_fields',
382+
Value: 1,
383+
Unit: 'None',
384+
Metadata: [
385+
{ Key: 'result', Value: 'Succeeded' },
386+
{ Key: 'missingFields', Value: 'field1,field2' },
387+
],
388+
} as MetricDatum
389+
390+
assert.throws(() => validateMetricEvent(metricEvent, true), /emitted with missing fields/)
391+
})
392+
393+
it('logs warning for metrics with missing fields when fatal=false', function () {
394+
const metricEvent: MetricDatum = {
395+
MetricName: 'invalid_metric_missing_fields',
396+
Value: 1,
397+
Unit: 'None',
398+
Metadata: [
399+
{ Key: 'result', Value: 'Succeeded' },
400+
{ Key: 'missingFields', Value: 'field1,field2' },
401+
],
402+
} as MetricDatum
403+
404+
validateMetricEvent(metricEvent, false)
405+
assertLogsContain('invalid Metric', false, 'warn')
406+
})
407+
})

0 commit comments

Comments
 (0)