Skip to content

Commit 27a1ce9

Browse files
authored
Fix telemetry cache (#926)
1 parent febe7b3 commit 27a1ce9

File tree

6 files changed

+186
-14
lines changed

6 files changed

+186
-14
lines changed

src/shared/telemetry/defaultTelemetryClient.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export class DefaultTelemetryClient implements TelemetryClient {
2727
*/
2828
public async postMetrics(batch: TelemetryEvent[]): Promise<TelemetryEvent[] | undefined> {
2929
try {
30+
const metricData = toMetricData(batch)
31+
// If our batching logic rejected all of the telemetry, don't try to post
32+
if (metricData.length === 0) {
33+
return undefined
34+
}
35+
3036
await this.client
3137
.postMetrics({
3238
AWSProduct: DefaultTelemetryClient.PRODUCT_NAME,
@@ -36,10 +42,12 @@ export class DefaultTelemetryClient implements TelemetryClient {
3642
OSVersion: os.release(),
3743
ParentProduct: vscode.env.appName,
3844
ParentProductVersion: vscode.version,
39-
MetricData: toMetricData(batch)
45+
MetricData: metricData
4046
})
4147
.promise()
4248
console.info(`Successfully sent a telemetry batch of ${batch.length}`)
49+
50+
return undefined
4351
} catch (err) {
4452
console.error(`Batch error: ${err}`)
4553

src/shared/telemetry/defaultTelemetryService.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as path from 'path'
99
import uuidv4 = require('uuid/v4')
1010
import { ExtensionContext } from 'vscode'
1111
import { AwsContext } from '../awsContext'
12+
import { getLogger } from '../logger'
1213
import { DefaultTelemetryClient } from './defaultTelemetryClient'
1314
import { DefaultTelemetryPublisher } from './defaultTelemetryPublisher'
1415
import { recordSessionEnd, recordSessionStart } from './telemetry'
@@ -48,8 +49,8 @@ export class DefaultTelemetryService implements TelemetryService {
4849
}
4950

5051
this.startTime = new Date()
51-
this._eventQueue = DefaultTelemetryService.readEventsFromCache(this.persistFilePath)
5252

53+
this._eventQueue = []
5354
this._flushPeriod = DefaultTelemetryService.DEFAULT_FLUSH_PERIOD_MILLIS
5455

5556
if (publisher !== undefined) {
@@ -62,6 +63,7 @@ export class DefaultTelemetryService implements TelemetryService {
6263
}
6364

6465
public async start(): Promise<void> {
66+
this._eventQueue.push(...DefaultTelemetryService.readEventsFromCache(this.persistFilePath))
6567
recordSessionStart()
6668
await this.startTimer()
6769
}
@@ -244,14 +246,72 @@ export class DefaultTelemetryService implements TelemetryService {
244246

245247
private static readEventsFromCache(cachePath: string): TelemetryEvent[] {
246248
try {
247-
const events = JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as TelemetryEvent[]
249+
const input = JSON.parse(fs.readFileSync(cachePath, 'utf-8'))
250+
const events = filterTelemetryCacheEvents(input)
248251
events.forEach((element: TelemetryEvent) => {
252+
// This is coercing the createTime into a Date type: it's read in as a string
249253
element.createTime = new Date(element.createTime)
250254
})
251255

252256
return events
253-
} catch {
257+
} catch (error) {
258+
// tslint:disable-next-line: no-unsafe-any
259+
getLogger().error(error)
260+
254261
return []
255262
}
256263
}
257264
}
265+
266+
export function filterTelemetryCacheEvents(input: any): TelemetryEvent[] {
267+
if (!Array.isArray(input)) {
268+
getLogger().error(`Input into filterTelemetryCacheEvents:\n${input}\nis not an array!`)
269+
270+
return []
271+
}
272+
const arr = input as any[]
273+
274+
return arr
275+
.filter((item: any) => {
276+
// Make sure the item is an object
277+
if (item !== Object(item)) {
278+
getLogger().error(`Item in telemetry cache:\n${item}\nis not an object! skipping!`)
279+
280+
return false
281+
}
282+
283+
return true
284+
})
285+
.filter((item: Object) => {
286+
// Only accept objects that have createTime and data because that's what's required by TelemetryEvent
287+
if (!item.hasOwnProperty('createTime') || !item.hasOwnProperty('data')) {
288+
getLogger().warn(`Item in telemetry cache: ${item}\n does not have 'data' or 'createTime'! skipping!`)
289+
290+
return false
291+
}
292+
293+
return true
294+
})
295+
.filter((item: TelemetryEvent) => {
296+
// skip it if data is not an array or empty
297+
if (!Array.isArray(item.data) || item.data.length === 0) {
298+
getLogger().warn(`Item in telemetry cache: ${item}\n has invalid data field: ${item.data}! skipping!`)
299+
300+
return false
301+
}
302+
303+
// Only accept objects that have value and metricname which are the base things required for telemetry
304+
return item.data.every(data => {
305+
// Make sure data is actually an object then check that it has the required properties
306+
if (data !== Object(data) || !data.hasOwnProperty('Value') || !data.hasOwnProperty('MetricName')) {
307+
getLogger().warn(
308+
`Item in telemetry cache: ${item}\n has invalid data in the field 'data': ${data}! skipping!`
309+
)
310+
311+
return false
312+
}
313+
314+
return true
315+
})
316+
}) as TelemetryEvent[]
317+
}

src/shared/telemetry/telemetryEvent.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,26 @@ export function toMetricData(array: TelemetryEvent[]): MetricDatum[] {
1818
.filter(item => {
1919
return item.data !== undefined
2020
})
21-
.map(metricEvent =>
22-
metricEvent.data.map(datum => {
23-
return {
24-
MetricName: datum.MetricName?.replace(NAME_ILLEGAL_CHARS_REGEX, ''),
25-
EpochTimestamp: metricEvent.createTime.getTime(),
26-
Unit: datum.Unit ?? 'None',
27-
Value: datum.Value,
28-
Metadata: datum.Metadata
29-
}
30-
})
21+
.map((metricEvent: TelemetryEvent) =>
22+
metricEvent.data
23+
.filter(datum => {
24+
const name = datum.MetricName?.replace(NAME_ILLEGAL_CHARS_REGEX, '')
25+
// Filter out bad data
26+
if (name === undefined || name === '' || datum.Value === undefined) {
27+
return false
28+
}
29+
30+
return true
31+
})
32+
.map(datum => {
33+
return {
34+
MetricName: datum.MetricName?.replace(NAME_ILLEGAL_CHARS_REGEX, ''),
35+
EpochTimestamp: metricEvent.createTime.getTime(),
36+
Unit: datum.Unit ?? 'None',
37+
Value: datum.Value,
38+
Metadata: datum.Metadata
39+
}
40+
})
3141
)
3242
)
3343
}

src/test/shared/telemetry/defaultTelemetryService.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import * as assert from 'assert'
7+
import * as del from 'del'
78
// tslint:disable-next-line:no-implicit-dependencies
89
import * as lolex from 'lolex'
910
import * as sinon from 'sinon'
@@ -53,6 +54,8 @@ beforeEach(() => {
5354
})
5455

5556
afterEach(() => {
57+
// Remove the persist file as it is saved
58+
del.sync([ext.telemetry.persistFilePath], { force: true })
5659
ext.telemetry = originalTelemetryClient
5760
})
5861

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*!
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as assert from 'assert'
7+
import { filterTelemetryCacheEvents } from '../../../shared/telemetry/defaultTelemetryService'
8+
9+
describe('Telemetry cache', () => {
10+
it('Rejects bad data', () => {
11+
const input = "THis isn't even valid json"
12+
13+
const output = filterTelemetryCacheEvents(input)
14+
assert.strictEqual(output.length, 0)
15+
})
16+
17+
it('Filters out old data', () => {
18+
const input = JSON.parse(
19+
'[{"namespace":"session","createTime":"2020-01-07T22:24:13.356Z","data":[{"name":"end","value":4226661,"unit":"Milliseconds","metadata":{}}]}]'
20+
)
21+
22+
const output = filterTelemetryCacheEvents(input)
23+
assert.strictEqual(output.length, 0)
24+
})
25+
26+
it('Extracts good data when there is bad data present', () => {
27+
const input = JSON.parse(
28+
'["this is a string", {"namespace":"session","createTime":"2020-01-07T22:24:13.356Z","data":[{"name":"end","value":4226661,"unit":"Milliseconds","metadata":{}}]},{"createTime":"2020-02-07T16:54:58.293Z","data":[{"MetricName":"session_end","Value":18709,"Unit":"None","Metadata":[{"Key":"awsAccount","Value":"n/a"}]}]}]'
29+
)
30+
31+
const output = filterTelemetryCacheEvents(input)
32+
assert.strictEqual(output.length, 1)
33+
})
34+
35+
it('Happy path', () => {
36+
const input = JSON.parse(
37+
'[{"createTime":"2020-02-07T16:54:58.293Z","data":[{"MetricName":"session_end","Value":18709,"Unit":"None","Metadata":[{"Key":"awsAccount","Value":"n/a"}]}]}]'
38+
)
39+
const output = filterTelemetryCacheEvents(input)
40+
assert.strictEqual(output.length, 1)
41+
})
42+
})

src/test/shared/telemetry/telemetryEvent.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,55 @@ describe('TelemetryEventArray', () => {
5959
assert.deepStrictEqual(data[0].Metadata, undefined)
6060
})
6161

62+
it('Rejects entries that have null Value', () => {
63+
const eventArray: TelemetryEvent[] = []
64+
const metricEvent = {
65+
createTime: new Date(),
66+
data: [
67+
{
68+
MetricName: 'namespace_event2',
69+
Value: undefined,
70+
Unit: 'Percent',
71+
Metadata: [
72+
{ Key: 'key', Value: 'value' },
73+
{ Key: 'key2', Value: 'value2' }
74+
]
75+
},
76+
{
77+
MetricName: 'namespace_event3',
78+
Unit: 'Percent',
79+
Value: 0.333,
80+
Metadata: [{ Key: 'key3', Value: 'value3' }]
81+
}
82+
]
83+
}
84+
eventArray.push(metricEvent)
85+
const data = toMetricData(eventArray)
86+
assert.strictEqual(data.length, 1)
87+
})
88+
89+
it('Rejects entries that have null MetricName', () => {
90+
const eventArray: TelemetryEvent[] = []
91+
const metricEvent = {
92+
createTime: new Date(),
93+
data: [
94+
{
95+
MetricName: undefined,
96+
Value: 1
97+
},
98+
{
99+
MetricName: 'namespace_event3',
100+
Unit: 'Percent',
101+
Value: 0.333,
102+
Metadata: [{ Key: 'key3', Value: 'value3' }]
103+
}
104+
]
105+
}
106+
eventArray.push(metricEvent)
107+
const data = toMetricData(eventArray)
108+
assert.strictEqual(data.length, 1)
109+
})
110+
62111
it('maps TelemetryEvent with data to a multiple MetricDatum', () => {
63112
const eventArray: TelemetryEvent[] = []
64113
const metricEvent = {

0 commit comments

Comments
 (0)