Skip to content

Commit

Permalink
Merge branch 'master' into selective_transform
Browse files Browse the repository at this point in the history
  • Loading branch information
dhasani23 authored Nov 19, 2024
2 parents d57d3f8 + f81d9a1 commit da35eed
Show file tree
Hide file tree
Showing 36 changed files with 654 additions and 188 deletions.
82 changes: 48 additions & 34 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,23 @@ Finally, if `setupStep2()` was the thing that failed we would see a metric like:

## Adding a "Stack Trace" to your metric

### Problem
When errors are thrown we do not attach the stack trace in telemetry. We only know about the error itself, but
not the path it took to get there. We sometimes need this stack trace to debug, and only have telemetry to get insight on what happened since we do not have access to logs.

### Scenario

Common example: _"I have a function, `thisFailsSometimes()` that is called in multiple places. The function sometimes fails, I know from telemetry, but I do not know if it is failing when it is a specific caller. If I knew the call stack/trace that it took to call my function that would help me debug."_

```typescript
function outerA() {
function runsSuccessfully() {
thisFailsSometimes(1) // this succeeds
}

function outerB() {
function thisThrows() {
thisFailsSometimes(0) // this fails
}

function thisFailsSometimes(num: number) {
function failsDependingOnInput(num: number) {
return telemetry.my_Metric.run(() => {
if (number === 0) {
throw Error('Cannot be 0')
Expand All @@ -167,31 +170,61 @@ function thisFailsSometimes(num: number) {

### Solution

Add a value to `function` in the options of a `run()`. This will result in a stack of functions identifiers that were previously called
before `thisFailsSometimes()` was run. You can then retrieve the stack in the `run()` of your final metric using `getFunctionStack()`.
On class methods, use the `@withTelemetryContext()` decorator to add context to the execution. Depending on the args set, it provides features like emitting the result, or adding it's context to errors.

> NOTE: Decorators are currently only supported for methods and not functions
```typescript
class MyClass {
@withTelemetryContext({ name: 'runsSuccessfully', class: 'MyClass' })
public runsSuccessfully() {
failsDependingOnInput(1)
}

@withTelemetryContext({ name: 'thisThrows', class: 'MyClass', errorCtx: true })
public thisThrows() {
failsDependingOnInput(0)
}

@withTelemetryContext({ name: 'failsDependingOnInput' class: 'MyClass', emit: true, errorCtx: true})
private failsDependingOnInput(num: number) {
if (number === 0) {
throw Error('Cannot be 0')
}
...
}
}

// Results in a metric: { source: 'MyClass#thisThrows,failsDependingOnInput', result: 'Failed' }
// Results in an error that has context about the methods that lead up to it.
new MyClass().thisThrows()
```

Separately if you must use a function, add a value to `function` in the options of a `run()`. This will result in a stack of functions identifiers that were previously called
before `failsDependingOnInput()` was run. You can then retrieve the stack in the `run()` of your final metric using `getFunctionStack()`.

```typescript
function outerA() {
telemetry.my_Metric.run(() => thisFailsSometimes(1), { functionId: { name: 'outerA' }})
function runsSuccessfully() {
telemetry.my_Metric.run(() => failsDependingOnInput(1), { functionId: { name: 'runsSuccessfully' }})
}

function outerB() {
telemetry.my_Metric.run(() => thisFailsSometimes(0), { functionId: { source: 'outerB' }})
function thisThrows() {
telemetry.my_Metric.run(() => failsDependingOnInput(0), { functionId: { source: 'thisThrows' }})
}

function thisFailsSometimes(num: number) {
function failsDependingOnInput(num: number) {
return telemetry.my_Metric.run(() => {
telemetry.record({ theCallStack: asStringifiedStack(telemetry.getFunctionStack())})
if (number === 0) {
throw Error('Cannot be 0')
}
...
}, { functionId: { name: 'thisFailsSometimes' }})
}, { functionId: { name: 'failsDependingOnInput' }})
}

// Results in a metric: { theCallStack: 'outerB:thisFailsSometimes', result: 'Failed' }
// { theCallStack: 'outerB:thisFailsSometimes' } implies 'outerB' was run first, then 'thisFailsSometimes'. See docstrings for more info.
outerB()
// Results in a metric: { theCallStack: 'thisThrows:failsDependingOnInput', result: 'Failed' }
// { theCallStack: 'thisThrows:failsDependingOnInput' } implies 'thisThrows' was run first, then 'failsDependingOnInput'. See docstrings for more info.
thisThrows()
```

### Important Notes
Expand All @@ -216,25 +249,6 @@ outerB()
c() // result: 'a:c', note that 'b' is not included
```

- If you are using `run()` with a class method, you can also add the class to the entry for more context

```typescript
class A {
a() {
return telemetry.my_Metric.run(() => this.b(), { functionId: { name: 'a', class: 'A' } })
}

b() {
return telemetry.my_Metric.run(() => asStringifiedStack(telemetry.getFunctionStack()), {
functionId: { name: 'b', class: 'A' },
})
}
}

const inst = new A()
inst.a() // 'A#a,b'
```

- If you do not want your `run()` to emit telemetry, set `emit: false` in the options

```typescript
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "Feature(Amazon Q Code Transformation): support conversions of embedded SQL from Oracle to PostgreSQL"
}
11 changes: 1 addition & 10 deletions packages/core/src/amazonq/webview/ui/tabs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { isSQLTransformReady } from '../../../../dev/config'
import { TabType } from '../storages/tabsStorage'
import { QuickActionCommandGroup } from '@aws/mynah-ui'

Expand Down Expand Up @@ -47,14 +46,6 @@ What would you like to work on?`,
gumby: {
title: 'Q - Code Transformation',
placeholder: 'Open a new tab to chat with Q',
welcome: isSQLTransformReady
? `Welcome to code transformation!
I can help you with the following tasks:
- Upgrade your Java 8 and Java 11 codebases to Java 17
- Convert embedded SQL from Oracle databases to PostgreSQL
What would you like to do? You can enter 'language upgrade' or 'SQL conversion'.`
: `Welcome to code transformation!`,
welcome: 'Welcome to Code Transformation!',
},
}
47 changes: 35 additions & 12 deletions packages/core/src/amazonqGumby/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
getValidSQLConversionCandidateProjects,
validateSQLMetadataFile,
} from '../../../codewhisperer/commands/startTransformByQ'
import { JDKVersion, transformByQState } from '../../../codewhisperer/models/model'
import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model'
import {
AbsolutePathDetectedError,
AlternateDependencyVersionsNotFoundError,
Expand Down Expand Up @@ -62,7 +62,6 @@ import { getStringHash } from '../../../shared/utilities/textUtilities'
import { getVersionData } from '../../../codewhisperer/service/transformByQ/transformMavenHandler'
import AdmZip from 'adm-zip'
import { AuthError } from '../../../auth/sso/server'
import { isSQLTransformReady } from '../../../dev/config'

// These events can be interactions within the chat,
// or elsewhere in the IDE
Expand Down Expand Up @@ -190,12 +189,31 @@ export class GumbyController {
}

private async transformInitiated(message: any) {
// feature flag for SQL transformations
if (!isSQLTransformReady) {
// silently check for projects eligible for SQL conversion
let embeddedSQLProjects: TransformationCandidateProject[] = []
try {
embeddedSQLProjects = await getValidSQLConversionCandidateProjects()
} catch (err) {
getLogger().error(`CodeTransformation: error validating SQL conversion projects: ${err}`)
}

if (embeddedSQLProjects.length === 0) {
await this.handleLanguageUpgrade(message)
return
}

let javaUpgradeProjects: TransformationCandidateProject[] = []
try {
javaUpgradeProjects = await getValidLanguageUpgradeCandidateProjects()
} catch (err) {
getLogger().error(`CodeTransformation: error validating Java upgrade projects: ${err}`)
}

if (javaUpgradeProjects.length === 0) {
await this.handleSQLConversion(message)
return
}

// if previous transformation was already running, show correct message to user
switch (this.sessionStorage.getSession().conversationState) {
case ConversationState.JOB_SUBMITTED:
Expand Down Expand Up @@ -224,7 +242,10 @@ export class GumbyController {
this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_TRANSFORMATION_OBJECTIVE
this.messenger.sendStaticTextResponse('choose-transformation-objective', message.tabID)
this.messenger.sendChatInputEnabled(message.tabID, true)
this.messenger.sendUpdatePlaceholder(message.tabID, "Enter 'language upgrade' or 'SQL conversion'")
this.messenger.sendUpdatePlaceholder(
message.tabID,
CodeWhispererConstants.chooseTransformationObjectivePlaceholder
)
}

private async beginTransformation(message: any) {
Expand Down Expand Up @@ -310,13 +331,7 @@ export class GumbyController {

private async validateSQLConversionProjects(message: any) {
try {
const validProjects = await telemetry.codeTransform_validateProject.run(async () => {
telemetry.record({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
})
const validProjects = await getValidSQLConversionCandidateProjects()
return validProjects
})
const validProjects = await getValidSQLConversionCandidateProjects()
return validProjects
} catch (e: any) {
if (e instanceof NoJavaProjectsFoundError) {
Expand Down Expand Up @@ -644,6 +659,14 @@ export class GumbyController {

case ConversationState.WAITING_FOR_TRANSFORMATION_OBJECTIVE: {
const objective = data.message.trim().toLowerCase()
// since we're prompting the user, their project(s) must be eligible for both types of transformations, so track how often this happens here
if (objective === 'language upgrade' || objective === 'sql conversion') {
telemetry.codeTransform_submitSelection.emit({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
userChoice: objective,
result: 'Succeeded',
})
}
if (objective === 'language upgrade') {
await this.handleLanguageUpgrade(data)
} else if (objective === 'sql conversion') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,15 @@ export class Messenger {
formItems.push({
id: 'GumbyTransformSQLConversionProjectForm',
type: 'select',
title: 'Choose a project to transform',
title: CodeWhispererConstants.chooseProjectFormTitle,
mandatory: true,
options: projectFormOptions,
})

formItems.push({
id: 'GumbyTransformSQLSchemaForm',
type: 'select',
title: 'Choose the schema of the database',
title: CodeWhispererConstants.chooseSchemaFormTitle,
mandatory: true,
options: Array.from(transformByQState.getSchemaOptions()).map((schema) => ({
value: schema,
Expand All @@ -314,7 +314,7 @@ export class Messenger {
this.dispatcher.sendAsyncEventProgress(
new AsyncEventProgressMessage(tabID, {
inProgress: true,
message: 'I can convert your embedded SQL, but I need some more info from you first.',
message: CodeWhispererConstants.chooseProjectSchemaFormMessage,
})
)

Expand Down Expand Up @@ -432,7 +432,7 @@ export class Messenger {
message = 'I will continue transforming your code without upgrading this dependency.'
break
case 'choose-transformation-objective':
message = 'Choose your transformation objective.'
message = CodeWhispererConstants.chooseTransformationObjective
break
}

Expand Down Expand Up @@ -464,6 +464,7 @@ export class Messenger {
message = CodeWhispererConstants.noJavaProjectsFoundChatMessage
break
case 'no-maven-java-project-found':
// shown when user has no pom.xml, but at this point also means they have no eligible SQL conversion projects
message = CodeWhispererConstants.noPomXmlFoundChatMessage
break
case 'could-not-compile-project':
Expand All @@ -489,23 +490,7 @@ export class Messenger {
break
}

const buttons: ChatItemButton[] = []
buttons.push({
keepCardAfterClick: false,
text: CodeWhispererConstants.startTransformationButtonText,
id: ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW,
})

this.dispatcher.sendChatMessage(
new ChatMessage(
{
message,
messageType: 'ai-prompt',
buttons,
},
tabID
)
)
this.sendJobFinishedMessage(tabID, message)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/amazonqGumby/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
export const gumbyChat = 'gumbyChat'

// This sets the tab name
export const featureName = 'Q - Code Transform'
export const featureName = 'Q - Code Transformation'

export const dependencyNoAvailableVersions = 'no available versions'
2 changes: 0 additions & 2 deletions packages/core/src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ export interface ConnectionStateChangeEvent {
readonly state: ProfileMetadata['connectionState']
}

export type AuthType = Auth

export type DeletedConnection = { connId: Connection['id']; storedProfile?: StoredProfile }
type DeclaredConnection = Pick<SsoProfile, 'ssoRegion' | 'startUrl'> & { source: string }

Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/auth/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const warnOnce = onceChanged((s: string, url: string) => {
void showMessageWithUrl(s, url, undefined, 'warn')
})

// TODO: Refactor all scopes to a central file with minimal dependencies.
export const scopesCodeCatalyst = ['codecatalyst:read_write']
export const scopesSsoAccountAccess = ['sso:account:access']
/** These are the non-chat scopes for CW. */
Expand All @@ -39,6 +40,11 @@ type SsoType =
| 'idc' // AWS Identity Center
| 'builderId'

// TODO: This type is not centralized and there are many routines in the codebase that use some
// variation for these for validation, telemetry, UX, etc. A refactor is needed to align these
// string types.
export type AuthType = 'credentials' | 'builderId' | 'identityCenter' | 'unknown'

export const isIamConnection = (conn?: Connection): conn is IamConnection => conn?.type === 'iam'
export const isSsoConnection = (conn?: Connection, type: SsoType = 'any'): conn is SsoConnection => {
if (conn?.type !== 'sso') {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/auth/sso/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import * as vscode from 'vscode'
import { UnknownError } from '../../shared/errors'
import { AuthType } from '../auth'
import { Auth } from '../auth'
import { SsoConnection, hasScopes, isAnySsoConnection } from '../connection'
import { ssoUrlFormatMessage, ssoUrlFormatRegex } from './constants'

Expand All @@ -19,7 +19,7 @@ export function validateSsoUrlFormat(url: string) {
}

export async function validateIsNewSsoUrlAsync(
auth: AuthType,
auth: Auth,
url: string,
requiredScopes?: string[]
): Promise<string | undefined> {
Expand Down
Loading

0 comments on commit da35eed

Please sign in to comment.