Skip to content

Commit b0eb729

Browse files
authored
feat(telemetry): emit on opt-out and opt-in (aws#5259)
- Emits 1 final metric on opt-out that will let us know the user opted out. - Also, on opt out it will emit any telemetry that was previously recorded and not sent.
1 parent fadc65c commit b0eb729

File tree

8 files changed

+70
-44
lines changed

8 files changed

+70
-44
lines changed

packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ describe('codewhisperer', async function () {
3636
telemetryEnabledDefault = globals.telemetry.telemetryEnabled
3737
})
3838

39-
afterEach(function () {
39+
afterEach(async function () {
4040
sinon.restore()
41-
globals.telemetry.telemetryEnabled = telemetryEnabledDefault
41+
await globals.telemetry.setTelemetryEnabled(telemetryEnabledDefault)
4242
})
4343

4444
it('sendTelemetryEvent for userTriggerDecision should respect telemetry optout status', async function () {
@@ -137,7 +137,7 @@ describe('codewhisperer', async function () {
137137
} as Request<SendTelemetryEventResponse, AWSError>)
138138

139139
const authUtilStub = sinon.stub(AuthUtil.instance, 'isValidEnterpriseSsoInUse').returns(isSso)
140-
globals.telemetry.telemetryEnabled = isTelemetryEnabled
140+
await globals.telemetry.setTelemetryEnabled(isTelemetryEnabled)
141141
await codeWhispererClient.sendTelemetryEvent({ telemetryEvent: payload })
142142
const expectedOptOutPreference = isTelemetryEnabled ? 'OPTIN' : 'OPTOUT'
143143
if (isSso || isTelemetryEnabled) {

packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ describe('codewhispererTracker', function () {
3434
assert.ok(!pushSpy.neverCalledWith(suggestion))
3535
})
3636

37-
it('Should not enque when telemetry is disabled', function () {
38-
globals.telemetry.telemetryEnabled = false
37+
it('Should not enque when telemetry is disabled', async function () {
38+
await globals.telemetry.setTelemetryEnabled(false)
3939
const suggestion = createAcceptedSuggestionEntry()
4040
const pushSpy = sinon.spy(Array.prototype, 'push')
4141
CodeWhispererTracker.getTracker().enqueue(suggestion)
4242
assert.ok(pushSpy.neverCalledWith(suggestion))
43-
globals.telemetry.telemetryEnabled = true
43+
await globals.telemetry.setTelemetryEnabled(true)
4444
})
4545
})
4646

@@ -67,11 +67,11 @@ describe('codewhispererTracker', function () {
6767
})
6868

6969
it('Should skip if telemetry is disabled', async function () {
70-
globals.telemetry.telemetryEnabled = false
70+
await globals.telemetry.setTelemetryEnabled(false)
7171
const getTimeSpy = sinon.spy(Date.prototype, 'getTime')
7272
await CodeWhispererTracker.getTracker().flush()
7373
assert.ok(!getTimeSpy.called)
74-
globals.telemetry.telemetryEnabled = true
74+
await globals.telemetry.setTelemetryEnabled(true)
7575
})
7676
})
7777

packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ describe('editorContext', function () {
1818
telemetryEnabledDefault = globals.telemetry.telemetryEnabled
1919
})
2020

21-
afterEach(function () {
22-
globals.telemetry.telemetryEnabled = telemetryEnabledDefault
21+
afterEach(async function () {
22+
await globals.telemetry.setTelemetryEnabled(telemetryEnabledDefault)
2323
})
2424

2525
describe('extractContextForCodeWhisperer', function () {
@@ -136,7 +136,7 @@ describe('editorContext', function () {
136136
it('Should return expected fields for optOut, nextToken and reference config', async function () {
137137
const nextToken = 'testToken'
138138
const optOutPreference = false
139-
globals.telemetry.telemetryEnabled = false
139+
await globals.telemetry.setTelemetryEnabled(false)
140140
const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17)
141141
const actual = await EditorContext.buildListRecommendationRequest(editor, nextToken, optOutPreference)
142142

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { TelemetryConfig, setupTelemetryId } from './util'
1818
import { isAutomation, isReleaseVersion } from '../vscode/env'
1919
import { AWSProduct } from './clienttelemetry'
2020
import { DefaultTelemetryClient } from './telemetryClient'
21+
import { telemetry } from './telemetry'
2122
import { Commands } from '../vscode/commands2'
2223

2324
export const noticeResponseViewSettings = localize('AWS.telemetry.notificationViewSettings', 'Settings')
@@ -48,18 +49,32 @@ export async function activate(
4849
DefaultTelemetryClient.productName = productName
4950
globals.telemetry = await DefaultTelemetryService.create(extensionContext, awsContext, getComputeRegion())
5051

52+
const isAmazonQExt = isAmazonQ()
5153
try {
52-
globals.telemetry.telemetryEnabled = config.isEnabled()
54+
await globals.telemetry.setTelemetryEnabled(config.isEnabled())
5355

5456
extensionContext.subscriptions.push(
55-
(isAmazonQ() ? config.amazonQConfig : config.toolkitConfig).onDidChange(event => {
57+
(isAmazonQExt ? config.amazonQConfig : config.toolkitConfig).onDidChange(async event => {
5658
if (event.key === 'telemetry') {
57-
globals.telemetry.telemetryEnabled = config.isEnabled()
59+
const val = config.isEnabled()
60+
const settingId = isAmazonQExt ? 'amazonQ.telemetry' : 'aws.telemetry'
61+
62+
// Record 'disabled' right before its turned off, so we can send this + the batch we have already.
63+
if (!val) {
64+
telemetry.aws_modifySetting.emit({ settingId, settingState: 'false', result: 'Succeeded' })
65+
}
66+
67+
await globals.telemetry.setTelemetryEnabled(val)
68+
69+
// Record 'enabled' after its turned on, otherwise this is ignored.
70+
if (val) {
71+
telemetry.aws_modifySetting.emit({ settingId, settingState: 'true', result: 'Succeeded' })
72+
}
5873
}
5974
})
6075
)
6176

62-
if (isAmazonQ()) {
77+
if (isAmazonQExt) {
6378
extensionContext.subscriptions.push(
6479
Commands.register('aws.amazonq.setupTelemetryId', async () => {
6580
await setupTelemetryId(extensionContext)

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

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,16 @@ export class DefaultTelemetryService {
112112
public get telemetryEnabled(): boolean {
113113
return this._telemetryEnabled
114114
}
115-
public set telemetryEnabled(value: boolean) {
115+
public async setTelemetryEnabled(value: boolean) {
116116
if (this._telemetryEnabled !== value) {
117-
getLogger().verbose(`Telemetry is now ${value ? 'enabled' : 'disabled'}`)
118-
}
117+
this._telemetryEnabled = value
119118

120-
// clear the queue on explicit disable
121-
if (!value) {
122-
this.clearRecords()
119+
// send all the gathered data that the user was opted-in for, prior to disabling
120+
if (!value) {
121+
await this._flushRecords()
122+
}
123123
}
124-
this._telemetryEnabled = value
124+
getLogger().verbose(`Telemetry is ${value ? 'enabled' : 'disabled'}`)
125125
}
126126

127127
public get timer(): NodeJS.Timer | undefined {
@@ -172,16 +172,26 @@ export class DefaultTelemetryService {
172172
}
173173
}
174174

175+
/**
176+
* Publish metrics to the Telemetry Service.
177+
*/
175178
private async flushRecords(): Promise<void> {
176179
if (this.telemetryEnabled) {
177-
if (this.publisher === undefined) {
178-
await this.createDefaultPublisherAndClient()
179-
}
180-
if (this.publisher !== undefined) {
181-
this.publisher.enqueue(...this._eventQueue)
182-
await this.publisher.flush()
183-
this.clearRecords()
184-
}
180+
await this._flushRecords()
181+
}
182+
}
183+
184+
/**
185+
* @warning DO NOT USE DIRECTLY, use `flushRecords()` instead.
186+
*/
187+
private async _flushRecords(): Promise<void> {
188+
if (this.publisher === undefined) {
189+
await this.createDefaultPublisherAndClient()
190+
}
191+
if (this.publisher !== undefined) {
192+
this.publisher.enqueue(...this._eventQueue)
193+
await this.publisher.flush()
194+
this.clearRecords()
185195
}
186196
}
187197

packages/core/src/test/globalSetup.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const mochaHooks = {
7979
}
8080

8181
// Enable telemetry features for tests. The metrics won't actually be posted.
82-
globals.telemetry.telemetryEnabled = true
82+
await globals.telemetry.setTelemetryEnabled(true)
8383
globals.telemetry.clearRecords()
8484
globals.telemetry.logger.clear()
8585
TelemetryDebounceInfo.instance.clear()

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('TelemetryService', function () {
4747
async function initService(awsContext = new FakeAwsContext()): Promise<DefaultTelemetryService> {
4848
const newService = await DefaultTelemetryService.create(mockContext, awsContext, undefined, mockPublisher)
4949
newService.flushPeriod = testFlushPeriod
50-
newService.telemetryEnabled = true
50+
await newService.setTelemetryEnabled(true)
5151

5252
return newService
5353
}
@@ -80,7 +80,7 @@ describe('TelemetryService', function () {
8080
})
8181

8282
it('posts feedback', async function () {
83-
service.telemetryEnabled = false
83+
await service.setTelemetryEnabled(false)
8484
const feedback = { comment: '', sentiment: '' }
8585
await service.postFeedback(feedback)
8686

@@ -238,23 +238,24 @@ describe('TelemetryService', function () {
238238
assertMetadataContainsTestAccount(metricData[0], AccountStatus.NotSet)
239239
})
240240

241-
it('events are never recorded if telemetry has been disabled', async function () {
241+
it('new events are not recorded if telemetry has been disabled', async function () {
242242
stubGlobal()
243243

244-
service.telemetryEnabled = false
244+
service.record(fakeMetric({ metricName: 'first' })) // emits 1 metric
245+
await service.setTelemetryEnabled(false)
245246
await service.start()
246247

247-
// telemetry off: events are never recorded
248-
service.record(fakeMetric({ metricName: 'name' }))
248+
// telemetry off: new events are never recorded
249+
service.record(fakeMetric({ metricName: 'second' })) // would emit 1 metric
249250
await service.shutdown()
250251
await clock.tickAsync(testFlushPeriod * 2)
251252

252-
// events are never flushed
253-
assert.strictEqual(mockPublisher.flushCount, 0)
254-
assert.strictEqual(mockPublisher.enqueueCount, 0)
255-
assert.strictEqual(mockPublisher.queue.length, 0)
256-
// and events are not kept in memory
257-
assert.strictEqual(logger.metricCount, 0)
253+
// only the events prior to disabling are recorded
254+
assert.strictEqual(mockPublisher.flushCount, 1)
255+
assert.strictEqual(mockPublisher.enqueueCount, 1)
256+
assert.strictEqual(mockPublisher.queue.length, 1)
257+
// and newer events are not kept in memory
258+
assert.strictEqual(logger.metricCount, 1)
258259
})
259260

260261
function assertMetadataContainsTestAccount(

packages/core/src/testInteg/schema/schema.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe('getDefaultSchemas()', () => {
7373

7474
it('uses cache after initial fetch for CFN/SAM schema', async () => {
7575
fs.removeSync(GlobalStorage.samAndCfnSchemaDestinationUri().fsPath)
76-
globals.telemetry.telemetryEnabled = true
76+
await globals.telemetry.setTelemetryEnabled(true)
7777
globals.telemetry.clearRecords()
7878
globals.telemetry.logger.clear()
7979
await getDefaultSchemas()

0 commit comments

Comments
 (0)