Skip to content

Commit 15ad617

Browse files
Merge master into feature/flare-inline
2 parents f14533d + fdaaae1 commit 15ad617

File tree

8 files changed

+332
-60
lines changed

8 files changed

+332
-60
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*!
2+
* Copyright 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 * as vscode from 'vscode'
8+
import { getDiagnosticsType, getDiagnosticsDifferences } from 'aws-core-vscode/codewhisperer'
9+
describe('diagnosticsUtil', function () {
10+
describe('getDiagnosticsType', function () {
11+
it('should identify SYNTAX_ERROR correctly', function () {
12+
assert.strictEqual(getDiagnosticsType('Expected semicolon'), 'SYNTAX_ERROR')
13+
assert.strictEqual(getDiagnosticsType('Incorrect indent level'), 'SYNTAX_ERROR')
14+
assert.strictEqual(getDiagnosticsType('Syntax error in line 5'), 'SYNTAX_ERROR')
15+
})
16+
17+
it('should identify TYPE_ERROR correctly', function () {
18+
assert.strictEqual(getDiagnosticsType('Type mismatch'), 'TYPE_ERROR')
19+
assert.strictEqual(getDiagnosticsType('Invalid type cast'), 'TYPE_ERROR')
20+
})
21+
22+
it('should identify REFERENCE_ERROR correctly', function () {
23+
assert.strictEqual(getDiagnosticsType('Variable is undefined'), 'REFERENCE_ERROR')
24+
assert.strictEqual(getDiagnosticsType('Variable not defined'), 'REFERENCE_ERROR')
25+
assert.strictEqual(getDiagnosticsType('Reference error occurred'), 'REFERENCE_ERROR')
26+
})
27+
28+
it('should identify BEST_PRACTICE correctly', function () {
29+
assert.strictEqual(getDiagnosticsType('Using deprecated method'), 'BEST_PRACTICE')
30+
assert.strictEqual(getDiagnosticsType('Variable is unused'), 'BEST_PRACTICE')
31+
assert.strictEqual(getDiagnosticsType('Variable not initialized'), 'BEST_PRACTICE')
32+
})
33+
34+
it('should identify SECURITY correctly', function () {
35+
assert.strictEqual(getDiagnosticsType('Potential security vulnerability'), 'SECURITY')
36+
assert.strictEqual(getDiagnosticsType('Security risk detected'), 'SECURITY')
37+
})
38+
39+
it('should return OTHER for unrecognized messages', function () {
40+
assert.strictEqual(getDiagnosticsType('Random message'), 'OTHER')
41+
assert.strictEqual(getDiagnosticsType(''), 'OTHER')
42+
})
43+
})
44+
45+
describe('getDiagnosticsDifferences', function () {
46+
const createDiagnostic = (message: string): vscode.Diagnostic => {
47+
return {
48+
message,
49+
severity: vscode.DiagnosticSeverity.Error,
50+
range: new vscode.Range(0, 0, 0, 1),
51+
source: 'test',
52+
}
53+
}
54+
55+
it('should return empty arrays when both inputs are undefined', function () {
56+
const result = getDiagnosticsDifferences(undefined, undefined)
57+
assert.deepStrictEqual(result, { added: [], removed: [] })
58+
})
59+
60+
it('should return empty arrays when filepaths are different', function () {
61+
const oldDiagnostics = {
62+
filepath: '/path/to/file1',
63+
diagnostics: [createDiagnostic('error1')],
64+
}
65+
const newDiagnostics = {
66+
filepath: '/path/to/file2',
67+
diagnostics: [createDiagnostic('error1')],
68+
}
69+
const result = getDiagnosticsDifferences(oldDiagnostics, newDiagnostics)
70+
assert.deepStrictEqual(result, { added: [], removed: [] })
71+
})
72+
73+
it('should correctly identify added and removed diagnostics', function () {
74+
const diagnostic1 = createDiagnostic('error1')
75+
const diagnostic2 = createDiagnostic('error2')
76+
const diagnostic3 = createDiagnostic('error3')
77+
78+
const oldDiagnostics = {
79+
filepath: '/path/to/file',
80+
diagnostics: [diagnostic1, diagnostic2],
81+
}
82+
const newDiagnostics = {
83+
filepath: '/path/to/file',
84+
diagnostics: [diagnostic2, diagnostic3],
85+
}
86+
87+
const result = getDiagnosticsDifferences(oldDiagnostics, newDiagnostics)
88+
assert.deepStrictEqual(result.added, [diagnostic3])
89+
assert.deepStrictEqual(result.removed, [diagnostic1])
90+
})
91+
})
92+
})

packages/core/src/amazonq/commons/controllers/contentController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ToolkitError, getErrorMsg } from '../../../shared/errors'
1919
import fs from '../../../shared/fs/fs'
2020
import { extractFileAndCodeSelectionFromMessage } from '../../../shared/utilities/textUtilities'
2121
import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker'
22+
import { CWCTelemetryHelper } from '../../../codewhispererChat/controllers/chat/telemetryHelper'
2223
import type { ViewDiff } from '../../../codewhispererChat/controllers/chat/model'
2324
import type { TriggerEvent } from '../../../codewhispererChat/storages/triggerEvents'
2425
import { DiffContentProvider } from './diffContentProvider'
@@ -49,6 +50,7 @@ export class EditorContentController {
4950
) {
5051
const editor = window.activeTextEditor
5152
if (editor) {
53+
CWCTelemetryHelper.instance.setDocumentDiagnostics()
5254
UserWrittenCodeTracker.instance.onQStartsMakingEdits()
5355
const cursorStart = editor.selection.active
5456
const indentRange = new vscode.Range(new vscode.Position(cursorStart.line, 0), cursorStart)

packages/core/src/codewhisperer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export * from './util/securityScanLanguageContext'
8989
export * from './util/importAdderUtil'
9090
export * from './util/globalStateUtil'
9191
export * from './util/zipUtil'
92+
export * from './util/diagnosticsUtil'
9293
export * from './util/commonUtil'
9394
export * from './util/supplementalContext/codeParsingUtil'
9495
export * from './util/supplementalContext/supplementalContextUtil'

packages/core/src/codewhisperer/util/codeWhispererSession.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
65
import {
76
CodewhispererCompletionType,
87
CodewhispererLanguage,
@@ -13,6 +12,7 @@ import {
1312
import { GenerateRecommendationsRequest, ListRecommendationsRequest, Recommendation } from '../client/codewhisperer'
1413
import { Position } from 'vscode'
1514
import { CodeWhispererSupplementalContext, vsCodeState } from '../models/model'
15+
import { FileDiagnostic, getDiagnosticsOfCurrentFile } from './diagnosticsUtil'
1616

1717
class CodeWhispererSession {
1818
static #instance: CodeWhispererSession
@@ -45,6 +45,7 @@ class CodeWhispererSession {
4545
timeToFirstRecommendation = 0
4646
firstSuggestionShowTime = 0
4747
perceivedLatency = 0
48+
diagnosticsBeforeAccept: FileDiagnostic | undefined = undefined
4849

4950
public static get instance() {
5051
return (this.#instance ??= new CodeWhispererSession())
@@ -66,6 +67,7 @@ class CodeWhispererSession {
6667
if (this.invokeSuggestionStartTime) {
6768
this.timeToFirstRecommendation = timeToFirstRecommendation - this.invokeSuggestionStartTime
6869
}
70+
this.diagnosticsBeforeAccept = getDiagnosticsOfCurrentFile()
6971
}
7072

7173
setSuggestionState(index: number, value: string) {
@@ -116,6 +118,7 @@ class CodeWhispererSession {
116118
this.recommendations = []
117119
this.suggestionStates.clear()
118120
this.completionTypes.clear()
121+
this.diagnosticsBeforeAccept = undefined
119122
}
120123
}
121124

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import * as crypto from 'crypto'
7+
import { IdeDiagnostic } from '../client/codewhispereruserclient'
8+
9+
export function getDiagnosticsOfCurrentFile(): FileDiagnostic | undefined {
10+
if (vscode.window.activeTextEditor) {
11+
return {
12+
diagnostics: vscode.languages.getDiagnostics(vscode.window.activeTextEditor.document.uri),
13+
filepath: vscode.window.activeTextEditor.document.uri.fsPath,
14+
}
15+
}
16+
return undefined
17+
}
18+
19+
export type FileDiagnostic = {
20+
filepath: string
21+
diagnostics: vscode.Diagnostic[]
22+
}
23+
24+
export function getDiagnosticsDifferences(
25+
oldDiagnostics: FileDiagnostic | undefined,
26+
newDiagnostics: FileDiagnostic | undefined
27+
): { added: vscode.Diagnostic[]; removed: vscode.Diagnostic[] } {
28+
const result: { added: vscode.Diagnostic[]; removed: vscode.Diagnostic[] } = { added: [], removed: [] }
29+
if (
30+
oldDiagnostics === undefined ||
31+
newDiagnostics === undefined ||
32+
newDiagnostics.filepath !== oldDiagnostics.filepath
33+
) {
34+
return result
35+
}
36+
37+
// Create maps using diagnostic key for uniqueness
38+
const oldMap = new Map(oldDiagnostics.diagnostics.map((d) => [getDiagnosticKey(d), d]))
39+
const newMap = new Map(newDiagnostics.diagnostics.map((d) => [getDiagnosticKey(d), d]))
40+
41+
// Get added diagnostics (in new but not in old)
42+
result.added = [...newMap.values()].filter((d) => !oldMap.has(getDiagnosticKey(d)))
43+
44+
// Get removed diagnostics (in old but not in new)
45+
result.removed = [...oldMap.values()].filter((d) => !newMap.has(getDiagnosticKey(d)))
46+
47+
return result
48+
}
49+
50+
export function toIdeDiagnostics(diagnostic: vscode.Diagnostic): IdeDiagnostic {
51+
const severity =
52+
diagnostic.severity === vscode.DiagnosticSeverity.Error
53+
? 'ERROR'
54+
: diagnostic.severity === vscode.DiagnosticSeverity.Warning
55+
? 'WARNING'
56+
: diagnostic.severity === vscode.DiagnosticSeverity.Hint
57+
? 'HINT'
58+
: 'INFORMATION'
59+
60+
return {
61+
ideDiagnosticType: getDiagnosticsType(diagnostic.message),
62+
severity: severity,
63+
source: diagnostic.source,
64+
range: {
65+
start: {
66+
line: diagnostic.range.start.line,
67+
character: diagnostic.range.start.character,
68+
},
69+
end: {
70+
line: diagnostic.range.end.line,
71+
character: diagnostic.range.end.character,
72+
},
73+
},
74+
}
75+
}
76+
77+
export function getDiagnosticsType(message: string): string {
78+
const errorTypes = new Map([
79+
['SYNTAX_ERROR', ['expected', 'indent', 'syntax']],
80+
['TYPE_ERROR', ['type', 'cast']],
81+
['REFERENCE_ERROR', ['undefined', 'not defined', 'undeclared', 'reference']],
82+
['BEST_PRACTICE', ['deprecated', 'unused', 'uninitialized', 'not initialized']],
83+
['SECURITY', ['security', 'vulnerability']],
84+
])
85+
86+
const lowercaseMessage = message.toLowerCase()
87+
88+
for (const [errorType, keywords] of errorTypes) {
89+
if (keywords.some((keyword) => lowercaseMessage.includes(keyword))) {
90+
return errorType
91+
}
92+
}
93+
94+
return 'OTHER'
95+
}
96+
97+
/**
98+
* Generates a unique MD5 hash key for a VS Code diagnostic object.
99+
*
100+
* @param diagnostic - A VS Code Diagnostic object containing information about a code diagnostic
101+
* @returns A 32-character hexadecimal MD5 hash string that uniquely identifies the diagnostic
102+
*
103+
* @description
104+
* Creates a deterministic hash by combining the diagnostic's message, severity, code, and source.
105+
* This hash can be used as a unique identifier for deduplication or tracking purposes.
106+
* Note: range is not in the hashed string because a diagnostic can move and its range can change within the editor
107+
*/
108+
function getDiagnosticKey(diagnostic: vscode.Diagnostic): string {
109+
const jsonStr = JSON.stringify({
110+
message: diagnostic.message,
111+
severity: diagnostic.severity,
112+
code: diagnostic.code,
113+
source: diagnostic.source,
114+
})
115+
116+
return crypto.createHash('md5').update(jsonStr).digest('hex')
117+
}

packages/core/src/codewhisperer/util/telemetryHelper.ts

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ import { getLogger } from '../../shared/logger/logger'
2626
import { session } from './codeWhispererSession'
2727
import { CodeWhispererSupplementalContext } from '../models/model'
2828
import { FeatureConfigProvider } from '../../shared/featureConfig'
29-
import { CodeScanRemediationsEventType } from '../client/codewhispereruserclient'
29+
import CodeWhispererUserClient, { CodeScanRemediationsEventType } from '../client/codewhispereruserclient'
3030
import { CodeAnalysisScope as CodeAnalysisScopeClientSide } from '../models/constants'
3131
import { Session } from '../../amazonqTest/chat/session/session'
32+
import { sleep } from '../../shared/utilities/timeoutUtils'
33+
import { getDiagnosticsDifferences, getDiagnosticsOfCurrentFile, toIdeDiagnostics } from './diagnosticsUtil'
34+
import { Auth } from '../../auth/auth'
3235

3336
export class TelemetryHelper {
3437
// Some variables for client component latency
@@ -422,46 +425,56 @@ export class TelemetryHelper {
422425
e2eLatency = 0.0
423426
}
424427

425-
client
426-
.sendTelemetryEvent({
427-
telemetryEvent: {
428-
userTriggerDecisionEvent: {
429-
sessionId: sessionId,
430-
requestId: this.sessionDecisions[0].codewhispererFirstRequestId,
431-
customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn,
432-
programmingLanguage: {
433-
languageName: runtimeLanguageContext.toRuntimeLanguage(
434-
this.sessionDecisions[0].codewhispererLanguage
435-
),
436-
},
437-
completionType: this.getSendTelemetryCompletionType(aggregatedCompletionType),
438-
suggestionState: this.getSendTelemetrySuggestionState(aggregatedSuggestionState),
439-
recommendationLatencyMilliseconds: e2eLatency,
440-
triggerToResponseLatencyMilliseconds: session.timeToFirstRecommendation,
441-
perceivedLatencyMilliseconds: session.perceivedLatency,
442-
timestamp: new Date(Date.now()),
443-
suggestionReferenceCount: referenceCount,
444-
generatedLine: generatedLines,
445-
numberOfRecommendations: suggestionCount,
446-
acceptedCharacterCount: acceptedRecommendationContent.length,
447-
},
448-
},
449-
profileArn: profile?.arn,
450-
})
451-
.then()
452-
.catch((error) => {
453-
let requestId: string | undefined
454-
if (isAwsError(error)) {
455-
requestId = error.requestId
456-
}
428+
const userTriggerDecisionEvent: CodeWhispererUserClient.UserTriggerDecisionEvent = {
429+
sessionId: sessionId,
430+
requestId: this.sessionDecisions[0].codewhispererFirstRequestId,
431+
customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn,
432+
programmingLanguage: {
433+
languageName: runtimeLanguageContext.toRuntimeLanguage(this.sessionDecisions[0].codewhispererLanguage),
434+
},
435+
completionType: this.getSendTelemetryCompletionType(aggregatedCompletionType),
436+
suggestionState: this.getSendTelemetrySuggestionState(aggregatedSuggestionState),
437+
recommendationLatencyMilliseconds: e2eLatency,
438+
triggerToResponseLatencyMilliseconds: session.timeToFirstRecommendation,
439+
perceivedLatencyMilliseconds: session.perceivedLatency,
440+
timestamp: new Date(Date.now()),
441+
suggestionReferenceCount: referenceCount,
442+
generatedLine: generatedLines,
443+
numberOfRecommendations: suggestionCount,
444+
acceptedCharacterCount: acceptedRecommendationContent.length,
445+
}
446+
this.resetUserTriggerDecisionTelemetry()
457447

458-
getLogger().debug(
459-
`Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${
460-
error.message
461-
}`
448+
const sendEvent = () =>
449+
client
450+
.sendTelemetryEvent({
451+
telemetryEvent: { userTriggerDecisionEvent: userTriggerDecisionEvent },
452+
profileArn: profile?.arn,
453+
})
454+
.catch((error) => {
455+
const requestId = isAwsError(error) ? error.requestId : undefined
456+
getLogger().debug(
457+
`Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${error.message}`
458+
)
459+
})
460+
461+
if (userTriggerDecisionEvent.suggestionState === 'ACCEPT' && Auth.instance.isInternalAmazonUser()) {
462+
// wait 1 seconds for the user installed 3rd party LSP
463+
// to update its diagnostics.
464+
void sleep(1000).then(() => {
465+
const diagnosticDiff = getDiagnosticsDifferences(
466+
session.diagnosticsBeforeAccept,
467+
getDiagnosticsOfCurrentFile()
468+
)
469+
userTriggerDecisionEvent.addedIdeDiagnostics = diagnosticDiff.added.map((it) => toIdeDiagnostics(it))
470+
userTriggerDecisionEvent.removedIdeDiagnostics = diagnosticDiff.removed.map((it) =>
471+
toIdeDiagnostics(it)
462472
)
473+
void sendEvent()
463474
})
464-
this.resetUserTriggerDecisionTelemetry()
475+
} else {
476+
void sendEvent()
477+
}
465478
}
466479

467480
public getLastTriggerDecisionForClassifier() {

0 commit comments

Comments
 (0)