Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

telemetry(amazonq): add doc generation V2 telemetry #6427

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
10 changes: 8 additions & 2 deletions packages/core/src/amazonq/commons/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ export function createAmazonQUri(path: string, tabId: string, scheme: string) {
return vscode.Uri.from({ scheme: scheme, path, query: `tabID=${tabId}` })
}

export async function computeDiff(leftPath: string, rightPath: string, tabId: string, scheme: string) {
export async function computeDiff(
leftPath: string,
rightPath: string,
tabId: string,
scheme: string,
reportedChanges?: string
) {
const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId, scheme)
const leftFile = await vscode.workspace.openTextDocument(left)
const rightFile = await vscode.workspace.openTextDocument(right)

const changes = diffLines(leftFile.getText(), rightFile.getText(), {
const changes = diffLines(reportedChanges ?? leftFile.getText(), rightFile.getText(), {
ignoreWhitespace: true,
})

Expand Down
34 changes: 22 additions & 12 deletions packages/core/src/amazonqDoc/controllers/chat/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
import { getPathsFromZipFilePath } from '../../../amazonqFeatureDev/util/files'
import { FollowUpTypes } from '../../../amazonq/commons/types'
import { DocGenerationTask } from '../docGenerationTask'
import { DevPhase } from '../../types'

export interface ChatControllerEventEmitters {
readonly processHumanChatMessage: EventEmitter<any>
Expand Down Expand Up @@ -227,8 +228,6 @@ export class DocController {
return
}

this.docGenerationTask.userIdentity = AuthUtil.instance.conn?.id

const sendFolderConfirmationMessage = (message: string) => {
this.messenger.sendFolderConfirmationMessage(
data.tabID,
Expand Down Expand Up @@ -288,12 +287,12 @@ export class DocController {
break
case FollowUpTypes.AcceptChanges:
this.docGenerationTask.userDecision = 'ACCEPT'
await this.sendDocGenerationEvent(data)
await this.sendDocAcceptanceEvent(data)
await this.insertCode(data)
return
case FollowUpTypes.RejectChanges:
this.docGenerationTask.userDecision = 'REJECT'
await this.sendDocGenerationEvent(data)
await this.sendDocAcceptanceEvent(data)
this.messenger.sendAnswer({
type: 'answer',
tabID: data?.tabID,
Expand Down Expand Up @@ -323,7 +322,7 @@ export class DocController {
}
break
case FollowUpTypes.CancelFolderSelection:
this.docGenerationTask.reset()
this.docGenerationTask.folderLevel = 'ENTIRE_WORKSPACE'
return this.tabOpened(data)
}
})
Expand Down Expand Up @@ -488,7 +487,7 @@ export class DocController {
session.isAuthenticating = true
return
}
this.docGenerationTask.numberOfNavigation += 1
this.docGenerationTask.numberOfNavigations += 1
this.messenger.sendAnswer({
type: 'answer',
tabID: message.tabID,
Expand Down Expand Up @@ -595,6 +594,17 @@ export class DocController {
tabID: tabID,
})
}
if (session?.state.phase === DevPhase.CODEGEN) {
const { totalGeneratedChars, totalGeneratedLines, totalGeneratedFiles } =
await session.countGeneratedContent(this.docGenerationTask.interactionType)
this.docGenerationTask.conversationId = session.conversationId
this.docGenerationTask.numberOfGeneratedChars = totalGeneratedChars
this.docGenerationTask.numberOfGeneratedLines = totalGeneratedLines
this.docGenerationTask.numberOfGeneratedFiles = totalGeneratedFiles
const docGenerationEvent = this.docGenerationTask.docGenerationEventBase()

await session.sendDocTelemetryEvent(docGenerationEvent, 'generation')
}
} finally {
if (session?.state?.tokenSource?.token.isCancellationRequested) {
await this.newTask({ tabID })
Expand Down Expand Up @@ -652,18 +662,18 @@ export class DocController {
)
}
}
private async sendDocGenerationEvent(message: any) {
private async sendDocAcceptanceEvent(message: any) {
const session = await this.sessionStorage.getSession(message.tabID)
this.docGenerationTask.conversationId = session.conversationId
const { totalAddedChars, totalAddedLines, totalAddedFiles } = await session.countAddedContent(
this.docGenerationTask.interactionType
)
this.docGenerationTask.numberOfAddChars = totalAddedChars
this.docGenerationTask.numberOfAddLines = totalAddedLines
this.docGenerationTask.numberOfAddFiles = totalAddedFiles
const docGenerationEvent = this.docGenerationTask.docGenerationEventBase()
this.docGenerationTask.numberOfAddedChars = totalAddedChars
this.docGenerationTask.numberOfAddedLines = totalAddedLines
this.docGenerationTask.numberOfAddedFiles = totalAddedFiles
const docAcceptanceEvent = this.docGenerationTask.docAcceptanceEventBase()

await session.sendDocGenerationTelemetryEvent(docGenerationEvent)
await session.sendDocTelemetryEvent(docAcceptanceEvent, 'acceptance')
}
private processLink(message: any) {
void openUrl(vscode.Uri.parse(message.link))
Expand Down
74 changes: 49 additions & 25 deletions packages/core/src/amazonqDoc/controllers/docGenerationTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,27 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
DocGenerationEvent,
DocGenerationFolderLevel,
DocGenerationInteractionType,
DocGenerationUserDecision,
DocFolderLevel,
DocInteractionType,
DocUserDecision,
DocV2AcceptanceEvent,
DocV2GenerationEvent,
} from '../../codewhisperer/client/codewhispereruserclient'
import { getLogger } from '../../shared'

export class DocGenerationTask {
// Telemetry fields
public conversationId?: string
public numberOfAddChars?: number
public numberOfAddLines?: number
public numberOfAddFiles?: number
public userDecision?: DocGenerationUserDecision
public interactionType?: DocGenerationInteractionType
public userIdentity?: string
public numberOfNavigation = 0
public folderLevel: DocGenerationFolderLevel = 'ENTIRE_WORKSPACE'
public numberOfAddedChars?: number
public numberOfAddedLines?: number
public numberOfAddedFiles?: number
public numberOfGeneratedChars?: number
public numberOfGeneratedLines?: number
public numberOfGeneratedFiles?: number
public userDecision?: DocUserDecision
public interactionType?: DocInteractionType
public numberOfNavigations = 0
public folderLevel: DocFolderLevel = 'ENTIRE_WORKSPACE'

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

if (undefinedProps.length > 0) {
getLogger().debug(`DocGenerationEvent has undefined properties: ${undefinedProps.join(', ')}`)
getLogger().debug(`DocV2GenerationEvent has undefined properties: ${undefinedProps.join(', ')}`)
}
const event: DocGenerationEvent = {
const event: DocV2GenerationEvent = {
conversationId: this.conversationId ?? '',
numberOfAddChars: this.numberOfAddChars,
numberOfAddLines: this.numberOfAddLines,
numberOfAddFiles: this.numberOfAddFiles,
userDecision: this.userDecision,
numberOfGeneratedChars: this.numberOfGeneratedChars ?? 0,
numberOfGeneratedLines: this.numberOfGeneratedLines ?? 0,
numberOfGeneratedFiles: this.numberOfGeneratedFiles ?? 0,
interactionType: this.interactionType,
userIdentity: this.userIdentity,
numberOfNavigation: this.numberOfNavigation,
numberOfNavigations: this.numberOfNavigations,
folderLevel: this.folderLevel,
}
return event
}

public docAcceptanceEventBase() {
const undefinedProps = Object.entries(this)
.filter(([key, value]) => value === undefined)
.map(([key]) => key)

if (undefinedProps.length > 0) {
getLogger().debug(`DocV2AcceptanceEvent has undefined properties: ${undefinedProps.join(', ')}`)
}
const event: DocV2AcceptanceEvent = {
conversationId: this.conversationId ?? '',
numberOfAddedChars: this.numberOfAddedChars ?? 0,
numberOfAddedLines: this.numberOfAddedLines ?? 0,
numberOfAddedFiles: this.numberOfAddedFiles ?? 0,
userDecision: this.userDecision ?? 'ACCEPTED',
interactionType: this.interactionType ?? 'GENERATE_README',
numberOfNavigations: this.numberOfNavigations ?? 0,
folderLevel: this.folderLevel,
}
return event
}

public reset() {
this.conversationId = undefined
this.numberOfAddChars = undefined
this.numberOfAddLines = undefined
this.numberOfAddFiles = undefined
this.numberOfAddedChars = undefined
this.numberOfAddedLines = undefined
this.numberOfAddedFiles = undefined
this.numberOfGeneratedChars = undefined
this.numberOfGeneratedLines = undefined
this.numberOfGeneratedFiles = undefined
this.userDecision = undefined
this.interactionType = undefined
this.userIdentity = undefined
this.numberOfNavigation = 0
this.numberOfNavigations = 0
this.folderLevel = 'ENTIRE_WORKSPACE'
}
}
95 changes: 70 additions & 25 deletions packages/core/src/amazonqDoc/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { featureName, Mode } from '../constants'
import { docScheme, featureName, Mode } from '../constants'
import { DeletedFileInfo, Interaction, NewFileInfo, SessionState, SessionStateConfig } from '../types'
import { PrepareCodeGenState } from './sessionState'
import { telemetry } from '../../shared/telemetry/telemetry'
Expand All @@ -19,13 +19,14 @@ import { logWithConversationId } from '../../amazonqFeatureDev/userFacingText'
import { ConversationIdNotFoundError } from '../../amazonqFeatureDev/errors'
import { referenceLogText } from '../../amazonqFeatureDev/constants'
import {
DocGenerationEvent,
DocGenerationInteractionType,
DocInteractionType,
DocV2AcceptanceEvent,
DocV2GenerationEvent,
SendTelemetryEventRequest,
} from '../../codewhisperer/client/codewhispereruserclient'
import { getDiffCharsAndLines } from '../../shared/utilities/diffUtils'
import { getClientId, getOperatingSystem, getOptOutPreference } from '../../shared/telemetry/util'
import { DocMessenger } from '../messenger'
import { computeDiff } from '../../amazonq/commons/diff'

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

// Used to keep track of whether or not the current session is currently authenticating/needs authenticating
public isAuthenticating: boolean
private _reportedDocChanges: string | undefined = undefined

constructor(
public readonly config: SessionConfig,
Expand Down Expand Up @@ -177,41 +179,83 @@ export class Session {
}
}

public async countAddedContent(interactionType?: DocGenerationInteractionType) {
public async countGeneratedContent(interactionType?: DocInteractionType) {
KevinDing1 marked this conversation as resolved.
Show resolved Hide resolved
let totalGeneratedChars = 0
let totalGeneratedLines = 0
let totalGeneratedFiles = 0
const filePaths = this.state.filePaths ?? []

for (const filePath of filePaths) {
if (interactionType === 'GENERATE_README') {
if (this._reportedDocChanges) {
const { charsAdded, linesAdded } = await this.computeFilePathDiff(
filePath,
this._reportedDocChanges
)
totalGeneratedChars += charsAdded
totalGeneratedLines += linesAdded
} else {
const fileContent = filePath.fileContent
totalGeneratedChars += fileContent.length
totalGeneratedLines += fileContent.split('\n').length
}
} else {
const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, this._reportedDocChanges)
totalGeneratedChars += charsAdded
totalGeneratedLines += linesAdded
}
this._reportedDocChanges = filePath.fileContent
KevinDing1 marked this conversation as resolved.
Show resolved Hide resolved
totalGeneratedFiles += 1
}
return {
totalGeneratedChars,
totalGeneratedLines,
totalGeneratedFiles,
}
}

public async countAddedContent(interactionType?: DocInteractionType) {
let totalAddedChars = 0
let totalAddedLines = 0
let totalAddedFiles = 0

for (const filePath of this.state.filePaths?.filter((i) => !i.rejected) ?? []) {
const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath)
const uri = filePath.virtualMemoryUri
const content = await this.config.fs.readFile(uri)
const decodedContent = new TextDecoder().decode(content)
totalAddedFiles += 1

if ((await fs.exists(absolutePath)) && interactionType === 'UPDATE_README') {
const existingContent = await fs.readFileText(absolutePath)
const { addedChars, addedLines } = getDiffCharsAndLines(existingContent, decodedContent)
totalAddedChars += addedChars
totalAddedLines += addedLines
const newFilePaths =
this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? []

for (const filePath of newFilePaths) {
if (interactionType === 'GENERATE_README') {
const fileContent = filePath.fileContent
totalAddedChars += fileContent.length
totalAddedLines += fileContent.split('\n').length
} else {
totalAddedChars += decodedContent.length
totalAddedLines += decodedContent.split('\n').length
const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath)
totalAddedChars += charsAdded
totalAddedLines += linesAdded
}
totalAddedFiles += 1
}

return {
totalAddedChars,
totalAddedLines,
totalAddedFiles,
}
}
public async sendDocGenerationTelemetryEvent(docGenerationEvent: DocGenerationEvent) {

public async computeFilePathDiff(filePath: NewFileInfo, reportedChanges?: string) {
const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}`
const rightPath = filePath.virtualMemoryUri.path
const diff = await computeDiff(leftPath, rightPath, this.tabID, docScheme, reportedChanges)
return { leftPath, rightPath, ...diff }
}

public async sendDocTelemetryEvent(
telemetryEvent: DocV2GenerationEvent | DocV2AcceptanceEvent,
eventType: 'generation' | 'acceptance'
) {
const client = await this.proxyClient.getClient()
try {
const params: SendTelemetryEventRequest = {
telemetryEvent: {
docGenerationEvent,
[eventType === 'generation' ? 'docV2GenerationEvent' : 'docV2AcceptanceEvent']: telemetryEvent,
},
optOutPreference: getOptOutPreference(),
userContext: {
Expand All @@ -222,13 +266,14 @@ export class Session {
ideVersion: extensionVersion,
},
}

const response = await client.sendTelemetryEvent(params).promise()
getLogger().debug(
`${featureName}: successfully sent docGenerationEvent: ConversationId: ${docGenerationEvent.conversationId} RequestId: ${response.$response.requestId}`
`${featureName}: successfully sent docV2${eventType === 'generation' ? 'GenerationEvent' : 'AcceptanceEvent'}: ConversationId: ${telemetryEvent.conversationId} RequestId: ${response.$response.requestId}`
)
} catch (e) {
getLogger().error(
`${featureName}: failed to send doc generation telemetry: ${(e as Error).name}: ${
`${featureName}: failed to send doc ${eventType} telemetry: ${(e as Error).name}: ${
(e as Error).message
} RequestId: ${(e as any).requestId}`
)
Expand Down
Loading
Loading