Skip to content

Commit 9afc587

Browse files
authored
telemetry(amazonq): add doc generation V2 telemetry (aws#6427)
## Problem Add doc generation V2 telemetry --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 5277848 commit 9afc587

File tree

9 files changed

+1371
-194
lines changed

9 files changed

+1371
-194
lines changed

packages/core/src/amazonq/commons/diff.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ export function createAmazonQUri(path: string, tabId: string, scheme: string) {
3333
return vscode.Uri.from({ scheme: scheme, path, query: `tabID=${tabId}` })
3434
}
3535

36-
export async function computeDiff(leftPath: string, rightPath: string, tabId: string, scheme: string) {
36+
export async function computeDiff(
37+
leftPath: string,
38+
rightPath: string,
39+
tabId: string,
40+
scheme: string,
41+
reportedChanges?: string
42+
) {
3743
const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId, scheme)
3844
const leftFile = await vscode.workspace.openTextDocument(left)
3945
const rightFile = await vscode.workspace.openTextDocument(right)
4046

41-
const changes = diffLines(leftFile.getText(), rightFile.getText(), {
47+
const changes = diffLines(reportedChanges ?? leftFile.getText(), rightFile.getText(), {
4248
ignoreWhitespace: true,
4349
})
4450

packages/core/src/amazonqDoc/controllers/chat/controller.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
import { getPathsFromZipFilePath } from '../../../amazonqFeatureDev/util/files'
4545
import { FollowUpTypes } from '../../../amazonq/commons/types'
4646
import { DocGenerationTask } from '../docGenerationTask'
47+
import { DevPhase } from '../../types'
4748

4849
export interface ChatControllerEventEmitters {
4950
readonly processHumanChatMessage: EventEmitter<any>
@@ -227,8 +228,6 @@ export class DocController {
227228
return
228229
}
229230

230-
this.docGenerationTask.userIdentity = AuthUtil.instance.conn?.id
231-
232231
const sendFolderConfirmationMessage = (message: string) => {
233232
this.messenger.sendFolderConfirmationMessage(
234233
data.tabID,
@@ -288,12 +287,12 @@ export class DocController {
288287
break
289288
case FollowUpTypes.AcceptChanges:
290289
this.docGenerationTask.userDecision = 'ACCEPT'
291-
await this.sendDocGenerationEvent(data)
290+
await this.sendDocAcceptanceEvent(data)
292291
await this.insertCode(data)
293292
return
294293
case FollowUpTypes.RejectChanges:
295294
this.docGenerationTask.userDecision = 'REJECT'
296-
await this.sendDocGenerationEvent(data)
295+
await this.sendDocAcceptanceEvent(data)
297296
this.messenger.sendAnswer({
298297
type: 'answer',
299298
tabID: data?.tabID,
@@ -323,7 +322,7 @@ export class DocController {
323322
}
324323
break
325324
case FollowUpTypes.CancelFolderSelection:
326-
this.docGenerationTask.reset()
325+
this.docGenerationTask.folderLevel = 'ENTIRE_WORKSPACE'
327326
return this.tabOpened(data)
328327
}
329328
})
@@ -488,7 +487,7 @@ export class DocController {
488487
session.isAuthenticating = true
489488
return
490489
}
491-
this.docGenerationTask.numberOfNavigation += 1
490+
this.docGenerationTask.numberOfNavigations += 1
492491
this.messenger.sendAnswer({
493492
type: 'answer',
494493
tabID: message.tabID,
@@ -595,6 +594,17 @@ export class DocController {
595594
tabID: tabID,
596595
})
597596
}
597+
if (session?.state.phase === DevPhase.CODEGEN) {
598+
const { totalGeneratedChars, totalGeneratedLines, totalGeneratedFiles } =
599+
await session.countGeneratedContent(this.docGenerationTask.interactionType)
600+
this.docGenerationTask.conversationId = session.conversationId
601+
this.docGenerationTask.numberOfGeneratedChars = totalGeneratedChars
602+
this.docGenerationTask.numberOfGeneratedLines = totalGeneratedLines
603+
this.docGenerationTask.numberOfGeneratedFiles = totalGeneratedFiles
604+
const docGenerationEvent = this.docGenerationTask.docGenerationEventBase()
605+
606+
await session.sendDocTelemetryEvent(docGenerationEvent, 'generation')
607+
}
598608
} finally {
599609
if (session?.state?.tokenSource?.token.isCancellationRequested) {
600610
await this.newTask({ tabID })
@@ -652,18 +662,18 @@ export class DocController {
652662
)
653663
}
654664
}
655-
private async sendDocGenerationEvent(message: any) {
665+
private async sendDocAcceptanceEvent(message: any) {
656666
const session = await this.sessionStorage.getSession(message.tabID)
657667
this.docGenerationTask.conversationId = session.conversationId
658668
const { totalAddedChars, totalAddedLines, totalAddedFiles } = await session.countAddedContent(
659669
this.docGenerationTask.interactionType
660670
)
661-
this.docGenerationTask.numberOfAddChars = totalAddedChars
662-
this.docGenerationTask.numberOfAddLines = totalAddedLines
663-
this.docGenerationTask.numberOfAddFiles = totalAddedFiles
664-
const docGenerationEvent = this.docGenerationTask.docGenerationEventBase()
671+
this.docGenerationTask.numberOfAddedChars = totalAddedChars
672+
this.docGenerationTask.numberOfAddedLines = totalAddedLines
673+
this.docGenerationTask.numberOfAddedFiles = totalAddedFiles
674+
const docAcceptanceEvent = this.docGenerationTask.docAcceptanceEventBase()
665675

666-
await session.sendDocGenerationTelemetryEvent(docGenerationEvent)
676+
await session.sendDocTelemetryEvent(docAcceptanceEvent, 'acceptance')
667677
}
668678
private processLink(message: any) {
669679
void openUrl(vscode.Uri.parse(message.link))

packages/core/src/amazonqDoc/controllers/docGenerationTask.ts

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,27 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import {
6-
DocGenerationEvent,
7-
DocGenerationFolderLevel,
8-
DocGenerationInteractionType,
9-
DocGenerationUserDecision,
6+
DocFolderLevel,
7+
DocInteractionType,
8+
DocUserDecision,
9+
DocV2AcceptanceEvent,
10+
DocV2GenerationEvent,
1011
} from '../../codewhisperer/client/codewhispereruserclient'
1112
import { getLogger } from '../../shared'
1213

1314
export class DocGenerationTask {
1415
// Telemetry fields
1516
public conversationId?: string
16-
public numberOfAddChars?: number
17-
public numberOfAddLines?: number
18-
public numberOfAddFiles?: number
19-
public userDecision?: DocGenerationUserDecision
20-
public interactionType?: DocGenerationInteractionType
21-
public userIdentity?: string
22-
public numberOfNavigation = 0
23-
public folderLevel: DocGenerationFolderLevel = 'ENTIRE_WORKSPACE'
17+
public numberOfAddedChars?: number
18+
public numberOfAddedLines?: number
19+
public numberOfAddedFiles?: number
20+
public numberOfGeneratedChars?: number
21+
public numberOfGeneratedLines?: number
22+
public numberOfGeneratedFiles?: number
23+
public userDecision?: DocUserDecision
24+
public interactionType?: DocInteractionType
25+
public numberOfNavigations = 0
26+
public folderLevel: DocFolderLevel = 'ENTIRE_WORKSPACE'
2427

2528
constructor(conversationId?: string) {
2629
this.conversationId = conversationId
@@ -32,31 +35,52 @@ export class DocGenerationTask {
3235
.map(([key]) => key)
3336

3437
if (undefinedProps.length > 0) {
35-
getLogger().debug(`DocGenerationEvent has undefined properties: ${undefinedProps.join(', ')}`)
38+
getLogger().debug(`DocV2GenerationEvent has undefined properties: ${undefinedProps.join(', ')}`)
3639
}
37-
const event: DocGenerationEvent = {
40+
const event: DocV2GenerationEvent = {
3841
conversationId: this.conversationId ?? '',
39-
numberOfAddChars: this.numberOfAddChars,
40-
numberOfAddLines: this.numberOfAddLines,
41-
numberOfAddFiles: this.numberOfAddFiles,
42-
userDecision: this.userDecision,
42+
numberOfGeneratedChars: this.numberOfGeneratedChars ?? 0,
43+
numberOfGeneratedLines: this.numberOfGeneratedLines ?? 0,
44+
numberOfGeneratedFiles: this.numberOfGeneratedFiles ?? 0,
4345
interactionType: this.interactionType,
44-
userIdentity: this.userIdentity,
45-
numberOfNavigation: this.numberOfNavigation,
46+
numberOfNavigations: this.numberOfNavigations,
47+
folderLevel: this.folderLevel,
48+
}
49+
return event
50+
}
51+
52+
public docAcceptanceEventBase() {
53+
const undefinedProps = Object.entries(this)
54+
.filter(([key, value]) => value === undefined)
55+
.map(([key]) => key)
56+
57+
if (undefinedProps.length > 0) {
58+
getLogger().debug(`DocV2AcceptanceEvent has undefined properties: ${undefinedProps.join(', ')}`)
59+
}
60+
const event: DocV2AcceptanceEvent = {
61+
conversationId: this.conversationId ?? '',
62+
numberOfAddedChars: this.numberOfAddedChars ?? 0,
63+
numberOfAddedLines: this.numberOfAddedLines ?? 0,
64+
numberOfAddedFiles: this.numberOfAddedFiles ?? 0,
65+
userDecision: this.userDecision ?? 'ACCEPTED',
66+
interactionType: this.interactionType ?? 'GENERATE_README',
67+
numberOfNavigations: this.numberOfNavigations ?? 0,
4668
folderLevel: this.folderLevel,
4769
}
4870
return event
4971
}
5072

5173
public reset() {
5274
this.conversationId = undefined
53-
this.numberOfAddChars = undefined
54-
this.numberOfAddLines = undefined
55-
this.numberOfAddFiles = undefined
75+
this.numberOfAddedChars = undefined
76+
this.numberOfAddedLines = undefined
77+
this.numberOfAddedFiles = undefined
78+
this.numberOfGeneratedChars = undefined
79+
this.numberOfGeneratedLines = undefined
80+
this.numberOfGeneratedFiles = undefined
5681
this.userDecision = undefined
5782
this.interactionType = undefined
58-
this.userIdentity = undefined
59-
this.numberOfNavigation = 0
83+
this.numberOfNavigations = 0
6084
this.folderLevel = 'ENTIRE_WORKSPACE'
6185
}
6286
}

packages/core/src/amazonqDoc/session/session.ts

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { featureName, Mode } from '../constants'
6+
import { docScheme, featureName, Mode } from '../constants'
77
import { DeletedFileInfo, Interaction, NewFileInfo, SessionState, SessionStateConfig } from '../types'
88
import { PrepareCodeGenState } from './sessionState'
99
import { telemetry } from '../../shared/telemetry/telemetry'
@@ -19,13 +19,14 @@ import { logWithConversationId } from '../../amazonqFeatureDev/userFacingText'
1919
import { ConversationIdNotFoundError } from '../../amazonqFeatureDev/errors'
2020
import { referenceLogText } from '../../amazonqFeatureDev/constants'
2121
import {
22-
DocGenerationEvent,
23-
DocGenerationInteractionType,
22+
DocInteractionType,
23+
DocV2AcceptanceEvent,
24+
DocV2GenerationEvent,
2425
SendTelemetryEventRequest,
2526
} from '../../codewhisperer/client/codewhispereruserclient'
26-
import { getDiffCharsAndLines } from '../../shared/utilities/diffUtils'
2727
import { getClientId, getOperatingSystem, getOptOutPreference } from '../../shared/telemetry/util'
2828
import { DocMessenger } from '../messenger'
29+
import { computeDiff } from '../../amazonq/commons/diff'
2930

3031
export class Session {
3132
private _state?: SessionState | Omit<SessionState, 'uploadId'>
@@ -38,6 +39,7 @@ export class Session {
3839

3940
// Used to keep track of whether or not the current session is currently authenticating/needs authenticating
4041
public isAuthenticating: boolean
42+
private _reportedDocChanges: { [key: string]: string } = {}
4143

4244
constructor(
4345
public readonly config: SessionConfig,
@@ -177,41 +179,92 @@ export class Session {
177179
}
178180
}
179181

180-
public async countAddedContent(interactionType?: DocGenerationInteractionType) {
181-
let totalAddedChars = 0
182-
let totalAddedLines = 0
183-
let totalAddedFiles = 0
182+
private getFromReportedChanges(filepath: NewFileInfo) {
183+
const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}`
184+
return this._reportedDocChanges[key]
185+
}
184186

185-
for (const filePath of this.state.filePaths?.filter((i) => !i.rejected) ?? []) {
186-
const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath)
187-
const uri = filePath.virtualMemoryUri
188-
const content = await this.config.fs.readFile(uri)
189-
const decodedContent = new TextDecoder().decode(content)
190-
totalAddedFiles += 1
187+
private addToReportedChanges(filepath: NewFileInfo) {
188+
const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}`
189+
this._reportedDocChanges[key] = filepath.fileContent
190+
}
191191

192-
if ((await fs.exists(absolutePath)) && interactionType === 'UPDATE_README') {
193-
const existingContent = await fs.readFileText(absolutePath)
194-
const { addedChars, addedLines } = getDiffCharsAndLines(existingContent, decodedContent)
195-
totalAddedChars += addedChars
196-
totalAddedLines += addedLines
192+
public async countGeneratedContent(interactionType?: DocInteractionType) {
193+
let totalGeneratedChars = 0
194+
let totalGeneratedLines = 0
195+
let totalGeneratedFiles = 0
196+
const filePaths = this.state.filePaths ?? []
197+
198+
for (const filePath of filePaths) {
199+
const reportedDocChange = this.getFromReportedChanges(filePath)
200+
if (interactionType === 'GENERATE_README') {
201+
if (reportedDocChange) {
202+
const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange)
203+
totalGeneratedChars += charsAdded
204+
totalGeneratedLines += linesAdded
205+
} else {
206+
// If no changes are reported, this is the initial README generation and no comparison with existing files is needed
207+
const fileContent = filePath.fileContent
208+
totalGeneratedChars += fileContent.length
209+
totalGeneratedLines += fileContent.split('\n').length
210+
}
197211
} else {
198-
totalAddedChars += decodedContent.length
199-
totalAddedLines += decodedContent.split('\n').length
212+
const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange)
213+
totalGeneratedChars += charsAdded
214+
totalGeneratedLines += linesAdded
200215
}
216+
this.addToReportedChanges(filePath)
217+
totalGeneratedFiles += 1
218+
}
219+
return {
220+
totalGeneratedChars,
221+
totalGeneratedLines,
222+
totalGeneratedFiles,
201223
}
224+
}
202225

226+
public async countAddedContent(interactionType?: DocInteractionType) {
227+
let totalAddedChars = 0
228+
let totalAddedLines = 0
229+
let totalAddedFiles = 0
230+
const newFilePaths =
231+
this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? []
232+
233+
for (const filePath of newFilePaths) {
234+
if (interactionType === 'GENERATE_README') {
235+
const fileContent = filePath.fileContent
236+
totalAddedChars += fileContent.length
237+
totalAddedLines += fileContent.split('\n').length
238+
} else {
239+
const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath)
240+
totalAddedChars += charsAdded
241+
totalAddedLines += linesAdded
242+
}
243+
totalAddedFiles += 1
244+
}
203245
return {
204246
totalAddedChars,
205247
totalAddedLines,
206248
totalAddedFiles,
207249
}
208250
}
209-
public async sendDocGenerationTelemetryEvent(docGenerationEvent: DocGenerationEvent) {
251+
252+
public async computeFilePathDiff(filePath: NewFileInfo, reportedChanges?: string) {
253+
const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}`
254+
const rightPath = filePath.virtualMemoryUri.path
255+
const diff = await computeDiff(leftPath, rightPath, this.tabID, docScheme, reportedChanges)
256+
return { leftPath, rightPath, ...diff }
257+
}
258+
259+
public async sendDocTelemetryEvent(
260+
telemetryEvent: DocV2GenerationEvent | DocV2AcceptanceEvent,
261+
eventType: 'generation' | 'acceptance'
262+
) {
210263
const client = await this.proxyClient.getClient()
211264
try {
212265
const params: SendTelemetryEventRequest = {
213266
telemetryEvent: {
214-
docGenerationEvent,
267+
[eventType === 'generation' ? 'docV2GenerationEvent' : 'docV2AcceptanceEvent']: telemetryEvent,
215268
},
216269
optOutPreference: getOptOutPreference(),
217270
userContext: {
@@ -222,13 +275,14 @@ export class Session {
222275
ideVersion: extensionVersion,
223276
},
224277
}
278+
225279
const response = await client.sendTelemetryEvent(params).promise()
226280
getLogger().debug(
227-
`${featureName}: successfully sent docGenerationEvent: ConversationId: ${docGenerationEvent.conversationId} RequestId: ${response.$response.requestId}`
281+
`${featureName}: successfully sent docV2${eventType === 'generation' ? 'GenerationEvent' : 'AcceptanceEvent'}: ConversationId: ${telemetryEvent.conversationId} RequestId: ${response.$response.requestId}`
228282
)
229283
} catch (e) {
230284
getLogger().error(
231-
`${featureName}: failed to send doc generation telemetry: ${(e as Error).name}: ${
285+
`${featureName}: failed to send doc ${eventType} telemetry: ${(e as Error).name}: ${
232286
(e as Error).message
233287
} RequestId: ${(e as any).requestId}`
234288
)

0 commit comments

Comments
 (0)