Skip to content

Commit

Permalink
telemetry(amazonq): calculate % of non-generated (user-written) code a…
Browse files Browse the repository at this point in the history
…ws#5991

## Problem

With the release of many Q features(Inline Suggestion, chat, inline
chat, /dev, /test, /doc, /review, /transform), we need to know the %
code written by all Q features. This requires calculating and reporting
the user written code. The reporting of the code contribution of each Q
features was already implemented.


## Solution

Calculate and report the user written code for each language by
listening to document change events while Q is not making changes to the
editor.

We add flags to know whether Q is making temporary changes for
suggestion rendering or Q suggestion is accepted, by doing so, the
document change events are coming from the user.

We ignore certain document changes when their length of new characters
exceeds 50. Previous data driven research has shown that user tend to
copy a huge file from one place to another, making the user written code
count skyrocketing but that is actually some existing code not written
by the user.

We plan to first collect data from IDEs and let it run in the background
in shadow mode before we finish the service side aggregation, fix
possible bugs and eventually present the AI code written % to the
customers.

Note: The JB PR aws/aws-toolkit-jetbrains#5215.
The JB implementation depends on a reliable JB internal message bus to
pass information. Using VSC event listener might mess up the boolean
state of Q editing or not.
  • Loading branch information
leigaol authored Jan 16, 2025
1 parent 86cd492 commit f805f9b
Show file tree
Hide file tree
Showing 18 changed files with 431 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { computeDecorations } from '../decorations/computeDecorations'
import { CodelensProvider } from '../codeLenses/codeLenseProvider'
import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat'
import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer'
import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer'
import {
codicon,
getIcon,
Expand Down Expand Up @@ -84,6 +85,7 @@ export class InlineChatController {
await this.updateTaskAndLenses(task)
this.referenceLogController.addReferenceLog(task.codeReferences, task.replacement ? task.replacement : '')
await this.reset()
UserWrittenCodeTracker.instance.onQFinishesEdits()
}

public async rejectAllChanges(task = this.task, userInvoked: boolean): Promise<void> {
Expand Down Expand Up @@ -199,7 +201,7 @@ export class InlineChatController {
getLogger().info('inlineQuickPick query is empty')
return
}

UserWrittenCodeTracker.instance.onQStartsMakingEdits()
this.userQuery = query
await textDocumentUtil.addEofNewline(editor)
this.task = await this.createTask(query, editor.document, editor.selection)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'assert'
import * as sinon from 'sinon'
import * as vscode from 'vscode'
import { UserWrittenCodeTracker, TelemetryHelper, AuthUtil } from 'aws-core-vscode/codewhisperer'
import { createMockDocument, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test'

describe('userWrittenCodeTracker', function () {
describe('isActive()', function () {
afterEach(async function () {
await resetCodeWhispererGlobalVariables()
UserWrittenCodeTracker.instance.reset()
sinon.restore()
})

it('inactive case: telemetryEnable = true, isConnected = false', function () {
sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true)
sinon.stub(AuthUtil.instance, 'isConnected').returns(false)
assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), false)
})

it('inactive case: telemetryEnabled = false, isConnected = false', function () {
sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false)
sinon.stub(AuthUtil.instance, 'isConnected').returns(false)
assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), false)
})

it('active case: telemetryEnabled = true, isConnected = true', function () {
sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true)
sinon.stub(AuthUtil.instance, 'isConnected').returns(true)
assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), true)
})
})

describe('onDocumentChange', function () {
let tracker: UserWrittenCodeTracker | undefined

beforeEach(async function () {
await resetCodeWhispererGlobalVariables()
tracker = UserWrittenCodeTracker.instance
if (tracker) {
sinon.stub(tracker, 'isActive').returns(true)
}
})

afterEach(function () {
sinon.restore()
UserWrittenCodeTracker.instance.reset()
})

it('Should skip when content change size is more than 50', function () {
if (!tracker) {
assert.fail()
}
tracker.onQFeatureInvoked()
tracker.onTextDocumentChange({
reason: undefined,
document: createMockDocument(),
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 600),
rangeOffset: 0,
rangeLength: 600,
text: 'def twoSum(nums, target):\nfor '.repeat(20),
},
],
})
assert.strictEqual(tracker.getUserWrittenCharacters('python'), 0)
assert.strictEqual(tracker.getUserWrittenLines('python'), 0)
})

it('Should not skip when content change size is less than 50', function () {
if (!tracker) {
assert.fail()
}
tracker.onQFeatureInvoked()
tracker.onTextDocumentChange({
reason: undefined,
document: createMockDocument(),
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 49),
rangeOffset: 0,
rangeLength: 49,
text: 'a = 123'.repeat(7),
},
],
})
tracker.onTextDocumentChange({
reason: undefined,
document: createMockDocument('', 'test.java', 'java'),
contentChanges: [
{
range: new vscode.Range(0, 0, 1, 3),
rangeOffset: 0,
rangeLength: 11,
text: 'a = 123\nbcd',
},
],
})
assert.strictEqual(tracker.getUserWrittenCharacters('python'), 49)
assert.strictEqual(tracker.getUserWrittenLines('python'), 0)
assert.strictEqual(tracker.getUserWrittenCharacters('java'), 11)
assert.strictEqual(tracker.getUserWrittenLines('java'), 1)
assert.strictEqual(tracker.getUserWrittenLines('cpp'), 0)
})

it('Should skip when Q is editing', function () {
if (!tracker) {
assert.fail()
}
tracker.onQFeatureInvoked()
tracker.onQStartsMakingEdits()
tracker.onTextDocumentChange({
reason: undefined,
document: createMockDocument(),
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 30),
rangeOffset: 0,
rangeLength: 30,
text: 'def twoSum(nums, target):\nfor',
},
],
})
tracker.onQFinishesEdits()
tracker.onTextDocumentChange({
reason: undefined,
document: createMockDocument(),
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 2),
rangeOffset: 0,
rangeLength: 2,
text: '\na',
},
],
})
assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2)
assert.strictEqual(tracker.getUserWrittenLines('python'), 1)
})

it('Should not reduce tokens when delete', function () {
if (!tracker) {
assert.fail()
}
const doc = createMockDocument('import math', 'test.py', 'python')

tracker.onQFeatureInvoked()
tracker.onTextDocumentChange({
reason: undefined,
document: doc,
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 1),
rangeOffset: 0,
rangeLength: 0,
text: 'a',
},
],
})
tracker.onTextDocumentChange({
reason: undefined,
document: doc,
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 1),
rangeOffset: 0,
rangeLength: 0,
text: 'b',
},
],
})
assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2)
tracker.onTextDocumentChange({
reason: undefined,
document: doc,
contentChanges: [
{
range: new vscode.Range(0, 0, 0, 1),
rangeOffset: 1,
rangeLength: 1,
text: '',
},
],
})
assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getSelectionFromRange,
} from '../../../shared/utilities/textDocumentUtilities'
import { extractFileAndCodeSelectionFromMessage, fs, getErrorMsg, ToolkitError } from '../../../shared'
import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker'

export class ContentProvider implements vscode.TextDocumentContentProvider {
constructor(private uri: vscode.Uri) {}
Expand All @@ -41,6 +42,7 @@ export class EditorContentController {
) {
const editor = window.activeTextEditor
if (editor) {
UserWrittenCodeTracker.instance.onQStartsMakingEdits()
const cursorStart = editor.selection.active
const indentRange = new vscode.Range(new vscode.Position(cursorStart.line, 0), cursorStart)
// use the user editor intent if the position to the left of cursor is just space or tab
Expand All @@ -66,9 +68,11 @@ export class EditorContentController {
if (appliedEdits) {
trackCodeEdit(editor, cursorStart)
}
UserWrittenCodeTracker.instance.onQFinishesEdits()
},
(e) => {
getLogger().error('TextEditor.edit failed: %s', (e as Error).message)
UserWrittenCodeTracker.instance.onQFinishesEdits()
}
)
}
Expand Down Expand Up @@ -97,6 +101,7 @@ export class EditorContentController {

if (filePath && message?.code?.trim().length > 0 && selection) {
try {
UserWrittenCodeTracker.instance.onQStartsMakingEdits()
const doc = await vscode.workspace.openTextDocument(filePath)

const code = getIndentedCode(message, doc, selection)
Expand Down Expand Up @@ -130,6 +135,8 @@ export class EditorContentController {
const wrappedError = ChatDiffError.chain(error, `Failed to Accept Diff`, { code: chatDiffCode })
getLogger().error('%s: Failed to open diff view %s', chatDiffCode, getErrorMsg(wrappedError, true))
throw wrappedError
} finally {
UserWrittenCodeTracker.instance.onQFinishesEdits()
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/amazonqFeatureDev/client/featureDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { createCodeWhispererChatStreamingClient } from '../../shared/clients/cod
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
import { extensionVersion } from '../../shared/vscode/env'
import apiConfig = require('./codewhispererruntime-2022-11-11.json')
import { UserWrittenCodeTracker } from '../../codewhisperer'
import {
FeatureDevCodeAcceptanceEvent,
FeatureDevCodeGenerationEvent,
Expand Down Expand Up @@ -260,6 +261,7 @@ export class FeatureDevClient {
references?: CodeReference[]
}
}
UserWrittenCodeTracker.instance.onQFeatureInvoked()

const newFileContents: { zipFilePath: string; fileContent: string }[] = []
for (const [filePath, fileContent] of Object.entries(newFiles)) {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/amazonqTest/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
TestGenerationBuildStep,
testGenState,
unitTestGenerationCancelMessage,
UserWrittenCodeTracker,
} from '../../../codewhisperer'
import {
fs,
Expand Down Expand Up @@ -664,12 +665,14 @@ export class TestController {
acceptedLines = acceptedLines < 0 ? 0 : acceptedLines
acceptedChars -= originalContent.length
acceptedChars = acceptedChars < 0 ? 0 : acceptedChars
UserWrittenCodeTracker.instance.onQStartsMakingEdits()
const document = await vscode.workspace.openTextDocument(absolutePath)
await applyChanges(
document,
new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end),
updatedContent
)
UserWrittenCodeTracker.instance.onQFinishesEdits()
} else {
await fs.writeFile(absolutePath, updatedContent)
}
Expand Down Expand Up @@ -831,6 +834,7 @@ export class TestController {
const chatRequest = triggerPayloadToChatRequest(triggerPayload)
const client = await createCodeWhispererChatStreamingClient()
const response = await client.generateAssistantResponse(chatRequest)
UserWrittenCodeTracker.instance.onQFeatureInvoked()
await this.messenger.sendAIResponse(
response,
session,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/codewhisperer/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { SecurityIssueTreeViewProvider } from './service/securityIssueTreeViewPr
import { setContext } from '../shared/vscode/setContext'
import { syncSecurityIssueWebview } from './views/securityIssue/securityIssueWebview'
import { detectCommentAboveLine } from '../shared/utilities/commentUtils'
import { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker'

let localize: nls.LocalizeFunc

Expand Down Expand Up @@ -552,7 +553,7 @@ export async function activate(context: ExtContext): Promise<void> {
}

CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e)

UserWrittenCodeTracker.instance.onTextDocumentChange(e)
/**
* Handle this keystroke event only when
* 1. It is not a backspace
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/codewhisperer/client/user-service-2.json
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,9 @@
"timestamp": { "shape": "Timestamp" },
"unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" },
"totalNewCodeCharacterCount": { "shape": "PrimitiveInteger" },
"totalNewCodeLineCount": { "shape": "PrimitiveInteger" }
"totalNewCodeLineCount": { "shape": "PrimitiveInteger" },
"userWrittenCodeCharacterCount": { "shape": "PrimitiveInteger" },
"userWrittenCodeLineCount": { "shape": "PrimitiveInteger" }
}
},
"CodeFixAcceptanceEvent": {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/codewhisperer/commands/basicCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { cancel, confirm } from '../../shared'
import { startCodeFixGeneration } from './startCodeFixGeneration'
import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext'
import path from 'path'
import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker'
import { parsePatch } from 'diff'

const MessageTimeOut = 5_000
Expand Down Expand Up @@ -451,6 +452,7 @@ export const applySecurityFix = Commands.declare(
}
let languageId = undefined
try {
UserWrittenCodeTracker.instance.onQStartsMakingEdits()
const document = await vscode.workspace.openTextDocument(targetFilePath)
languageId = document.languageId
const updatedContent = await getPatchedCode(targetFilePath, suggestedFix.code)
Expand Down Expand Up @@ -565,6 +567,7 @@ export const applySecurityFix = Commands.declare(
applyFixTelemetryEntry.result,
!!targetIssue.suggestedFixes.length
)
UserWrittenCodeTracker.instance.onQFinishesEdits()
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { RecommendationService } from '../service/recommendationService'
import { Container } from '../service/serviceContainer'
import { telemetry } from '../../shared/telemetry'
import { TelemetryHelper } from '../util/telemetryHelper'
import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker'

export const acceptSuggestion = Commands.declare(
'aws.amazonq.accept',
Expand Down Expand Up @@ -126,6 +127,7 @@ export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAccept
acceptanceEntry.editor.document.getText(insertedCoderange),
acceptanceEntry.editor.document.fileName
)
UserWrittenCodeTracker.instance.onQFinishesEdits()
if (acceptanceEntry.references !== undefined) {
const referenceLog = ReferenceLogViewProvider.getReferenceLog(
acceptanceEntry.recommendation,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/codewhisperer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ export * as CodeWhispererConstants from '../codewhisperer/models/constants'
export { getSelectedCustomization, setSelectedCustomization, baseCustomization } from './util/customizationUtil'
export { Container } from './service/serviceContainer'
export * from './util/gitUtil'
export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker'
Loading

0 comments on commit f805f9b

Please sign in to comment.