From 8ece808e38b839e27df4ca76efecca86ed7561c0 Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Fri, 25 Apr 2025 12:14:49 -0400
Subject: [PATCH 1/2] fix(amazonq): Warn user Developer Profile not selected
(#7160)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem:
If a user has not selected Q Developer Profile after signing in, their
features will not work.
Some existing users, before the introduction of Q Developer Profiles,
who were already signed in had their features stop working because they
didn't select a profile.
## Solution:
On startup if we detect the user did not select a profile, then send a
warning message that their features will not work until they select one.
The message will have a button to allow them to select a profile through
quickpick, or entirly ignore the message and not show it again.
---
- 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.
---------
Signed-off-by: nkomonen-amazon
---
...-489e72aa-bb0d-4964-bd84-002f25db6b5f.json | 4 ++
packages/amazonq/package.json | 4 ++
packages/core/src/codewhisperer/activation.ts | 5 ++
.../core/src/codewhisperer/commands/types.ts | 3 ++
.../core/src/codewhisperer/region/utils.ts | 49 +++++++++++++++++++
.../core/src/codewhisperer/util/authUtil.ts | 1 +
.../core/src/shared/settings-amazonq.gen.ts | 3 +-
7 files changed, 68 insertions(+), 1 deletion(-)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-489e72aa-bb0d-4964-bd84-002f25db6b5f.json
create mode 100644 packages/core/src/codewhisperer/region/utils.ts
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-489e72aa-bb0d-4964-bd84-002f25db6b5f.json b/packages/amazonq/.changes/next-release/Bug Fix-489e72aa-bb0d-4964-bd84-002f25db6b5f.json
new file mode 100644
index 00000000000..0cd71188f81
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-489e72aa-bb0d-4964-bd84-002f25db6b5f.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Toast message to warn users if Developer Profile is not selected"
+}
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 20fdc3610ef..cb74db00761 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -131,6 +131,10 @@
"amazonQChatDisclaimer": {
"type": "boolean",
"default": false
+ },
+ "amazonQSelectDeveloperProfile": {
+ "type": "boolean",
+ "default": false
}
},
"additionalProperties": false
diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts
index efebb01e179..b0aa54e17a0 100644
--- a/packages/core/src/codewhisperer/activation.ts
+++ b/packages/core/src/codewhisperer/activation.ts
@@ -95,6 +95,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 { notifySelectDeveloperProfile } from './region/utils'
let localize: nls.LocalizeFunc
@@ -380,6 +381,10 @@ export async function activate(context: ExtContext): Promise {
await auth.notifySessionConfiguration()
}
}
+
+ if (auth.requireProfileSelection()) {
+ await notifySelectDeveloperProfile()
+ }
},
{ emit: false, functionId: { name: 'activateCwCore' } }
)
diff --git a/packages/core/src/codewhisperer/commands/types.ts b/packages/core/src/codewhisperer/commands/types.ts
index e211ae76f9a..cec28829507 100644
--- a/packages/core/src/codewhisperer/commands/types.ts
+++ b/packages/core/src/codewhisperer/commands/types.ts
@@ -18,6 +18,8 @@ export const firstStartUpSource = ExtStartUpSources.firstStartUp
export const cwEllipsesMenu = 'ellipsesMenu'
/** Indicates a CodeWhisperer command was executed from the command palette */
export const commandPalette = 'commandPalette'
+/** Indicates a CodeWhisperer command was executed as a result of a toast message interaction */
+export const toastMessage = 'toastMessage'
/**
* Indicates what caused the CodeWhisperer command to be executed, since a command can be executed from different "sources"
@@ -35,3 +37,4 @@ export type CodeWhispererSource =
| typeof firstStartUpSource
| typeof cwEllipsesMenu
| typeof commandPalette
+ | typeof toastMessage
diff --git a/packages/core/src/codewhisperer/region/utils.ts b/packages/core/src/codewhisperer/region/utils.ts
new file mode 100644
index 00000000000..dd988f74a30
--- /dev/null
+++ b/packages/core/src/codewhisperer/region/utils.ts
@@ -0,0 +1,49 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import * as nls from 'vscode-nls'
+const localize = nls.loadMessageBundle()
+import { AmazonQPromptSettings } from '../../shared/settings'
+import { telemetry } from '../../shared/telemetry/telemetry'
+import vscode from 'vscode'
+import { selectRegionProfileCommand } from '../commands/basicCommands'
+import { placeholder } from '../../shared/vscode/commands2'
+import { toastMessage } from '../commands/types'
+
+/**
+ * Creates a toast message telling the user they need to select a Developer Profile
+ */
+export async function notifySelectDeveloperProfile() {
+ const suppressId = 'amazonQSelectDeveloperProfile'
+ const settings = AmazonQPromptSettings.instance
+ const shouldShow = settings.isPromptEnabled(suppressId)
+ if (!shouldShow) {
+ return
+ }
+
+ const message = localize(
+ 'aws.amazonq.profile.mustSelectMessage',
+ 'You must select a Q Developer Profile for Amazon Q features to work.'
+ )
+ const selectProfile = 'Select Profile'
+ const dontShowAgain = 'Dont Show Again'
+
+ await telemetry.toolkit_showNotification.run(async () => {
+ telemetry.record({ id: 'mustSelectDeveloperProfileMessage' })
+ void vscode.window.showWarningMessage(message, selectProfile, dontShowAgain).then(async (resp) => {
+ await telemetry.toolkit_invokeAction.run(async () => {
+ if (resp === selectProfile) {
+ // Show Profile
+ telemetry.record({ action: 'select' })
+ void selectRegionProfileCommand.execute(placeholder, toastMessage)
+ } else if (resp === dontShowAgain) {
+ telemetry.record({ action: 'dontShowAgain' })
+ await settings.disablePrompt(suppressId)
+ } else {
+ telemetry.record({ action: 'ignore' })
+ }
+ })
+ })
+ })
+}
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index 0898493b6db..f4cc90a5293 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -46,6 +46,7 @@ import { withTelemetryContext } from '../../shared/telemetry/util'
import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands'
import { throttle } from 'lodash'
import { RegionProfileManager } from '../region/regionProfileManager'
+
/** Backwards compatibility for connections w pre-chat scopes */
export const codeWhispererCoreScopes = [...scopesCodeWhispererCore]
export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat]
diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts
index f0a3d47f989..bee57f9aa82 100644
--- a/packages/core/src/shared/settings-amazonq.gen.ts
+++ b/packages/core/src/shared/settings-amazonq.gen.ts
@@ -21,7 +21,8 @@ export const amazonqSettings = {
"ssoCacheError": {},
"amazonQLspManifestMessage": {},
"amazonQWorkspaceLspManifestMessage": {},
- "amazonQChatDisclaimer": {}
+ "amazonQChatDisclaimer": {},
+ "amazonQSelectDeveloperProfile": {}
},
"amazonQ.showCodeWithReferences": {},
"amazonQ.allowFeatureDevelopmentToRunCodeAndTests": {},
From 638778beee1a88783753a93c70766c2ee55b789d Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Fri, 25 Apr 2025 12:48:47 -0400
Subject: [PATCH 2/2] feat(core): add value length cap to partialClone (#7150)
## Problem
If we want to log a large json object with giant strings (think whole
files), it can make the logs difficult to read. This is especially
relevant when the underlying string values are not very important.
## Solution
- allow the string values to be truncated with `maxStringLength` option.
## Notes
I am planning to port this utility to Flare to improve the logging
experience there, and want this functionality to exist there however I
thought this could be useful here too.
---
- 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.
---
.../amazonq/test/e2e/inline/inline.test.ts | 2 +-
packages/core/src/auth/sso/clients.ts | 4 +-
.../src/shared/utilities/collectionUtils.ts | 21 ++++-
.../src/shared/utilities/textUtilities.ts | 2 +-
packages/core/src/shared/vscode/commands2.ts | 2 +-
.../shared/utilities/collectionUtils.test.ts | 91 ++++++++++++-------
6 files changed, 80 insertions(+), 42 deletions(-)
diff --git a/packages/amazonq/test/e2e/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts
index 57c6e1c4996..43a9f67ab73 100644
--- a/packages/amazonq/test/e2e/inline/inline.test.ts
+++ b/packages/amazonq/test/e2e/inline/inline.test.ts
@@ -122,7 +122,7 @@ describe('Amazon Q Inline', async function () {
.query({
metricName: 'codewhisperer_userTriggerDecision',
})
- .map((e) => collectionUtil.partialClone(e, 3, ['credentialStartUrl'], '[omitted]'))
+ .map((e) => collectionUtil.partialClone(e, 3, ['credentialStartUrl'], { replacement: '[omitted]' }))
}
for (const [name, invokeCompletion] of [
diff --git a/packages/core/src/auth/sso/clients.ts b/packages/core/src/auth/sso/clients.ts
index 01d0e031d04..e050bdc793e 100644
--- a/packages/core/src/auth/sso/clients.ts
+++ b/packages/core/src/auth/sso/clients.ts
@@ -258,7 +258,7 @@ function addLoggingMiddleware(client: SSOOIDCClient) {
args.input as unknown as Record,
3,
['clientSecret', 'accessToken', 'refreshToken'],
- '[omitted]'
+ { replacement: '[omitted]' }
)
getLogger().debug('API request (%s %s): %O', hostname, path, input)
}
@@ -288,7 +288,7 @@ function addLoggingMiddleware(client: SSOOIDCClient) {
result.output as unknown as Record,
3,
['clientSecret', 'accessToken', 'refreshToken'],
- '[omitted]'
+ { replacement: '[omitted]' }
)
getLogger().debug('API response (%s %s): %O', hostname, path, output)
}
diff --git a/packages/core/src/shared/utilities/collectionUtils.ts b/packages/core/src/shared/utilities/collectionUtils.ts
index 9f9fe9875b9..8a428b8e8b7 100644
--- a/packages/core/src/shared/utilities/collectionUtils.ts
+++ b/packages/core/src/shared/utilities/collectionUtils.ts
@@ -7,6 +7,7 @@ import { isWeb } from '../extensionGlobals'
import { inspect as nodeInspect } from 'util'
import { AsyncCollection, toCollection } from './asyncCollection'
import { SharedProp, AccumulableKeys, Coalesce, isNonNullable } from './tsUtils'
+import { truncate } from './textUtilities'
export function union(a: Iterable, b: Iterable): Set {
const result = new Set()
@@ -304,10 +305,22 @@ export function assign, U extends Partial>(data: T
* @param depth
* @param omitKeys Omit properties matching these names (at any depth).
* @param replacement Replacement for object whose fields extend beyond `depth`, and properties matching `omitKeys`.
+ * @param maxStringLength truncates string values that exceed this threshold (includes values in nested arrays)
*/
-export function partialClone(obj: any, depth: number = 3, omitKeys: string[] = [], replacement?: any): any {
+export function partialClone(
+ obj: any,
+ depth: number = 3,
+ omitKeys: string[] = [],
+ options?: {
+ replacement?: any
+ maxStringLength?: number
+ }
+): any {
// Base case: If input is not an object or has no children, return it.
if (typeof obj !== 'object' || obj === null || 0 === Object.getOwnPropertyNames(obj).length) {
+ if (typeof obj === 'string' && options?.maxStringLength) {
+ return truncate(obj, options?.maxStringLength, '...')
+ }
return obj
}
@@ -315,15 +328,15 @@ export function partialClone(obj: any, depth: number = 3, omitKeys: string[] = [
const clonedObj = Array.isArray(obj) ? [] : {}
if (depth === 0) {
- return replacement ? replacement : clonedObj
+ return options?.replacement ? options.replacement : clonedObj
}
// Recursively clone properties of the input object
for (const key in obj) {
if (omitKeys.includes(key)) {
- ;(clonedObj as any)[key] = replacement ? replacement : Array.isArray(obj) ? [] : {}
+ ;(clonedObj as any)[key] = options?.replacement ? options.replacement : Array.isArray(obj) ? [] : {}
} else if (Object.prototype.hasOwnProperty.call(obj, key)) {
- ;(clonedObj as any)[key] = partialClone(obj[key], depth - 1, omitKeys, replacement)
+ ;(clonedObj as any)[key] = partialClone(obj[key], depth - 1, omitKeys, options)
}
}
diff --git a/packages/core/src/shared/utilities/textUtilities.ts b/packages/core/src/shared/utilities/textUtilities.ts
index 53c2e2be32c..ed1e1619122 100644
--- a/packages/core/src/shared/utilities/textUtilities.ts
+++ b/packages/core/src/shared/utilities/textUtilities.ts
@@ -10,7 +10,7 @@ import { default as stripAnsi } from 'strip-ansi'
import { getLogger } from '../logger/logger'
/**
- * Truncates string `s` if it exceeds `n` chars.
+ * Truncates string `s` if it has or exceeds `n` chars.
*
* If `n` is negative, truncates at start instead of end.
*
diff --git a/packages/core/src/shared/vscode/commands2.ts b/packages/core/src/shared/vscode/commands2.ts
index c55cd66cc7a..b40134c2afa 100644
--- a/packages/core/src/shared/vscode/commands2.ts
+++ b/packages/core/src/shared/vscode/commands2.ts
@@ -653,7 +653,7 @@ async function runCommand(fn: T, info: CommandInfo): Prom
logger.debug(
`command: running ${label} with arguments: %O`,
- partialClone(args, 3, ['clientSecret', 'accessToken', 'refreshToken', 'tooltip'], '[omitted]')
+ partialClone(args, 3, ['clientSecret', 'accessToken', 'refreshToken', 'tooltip'], { replacement: '[omitted]' })
)
try {
diff --git a/packages/core/src/test/shared/utilities/collectionUtils.test.ts b/packages/core/src/test/shared/utilities/collectionUtils.test.ts
index 53ddc39eff8..34aacb9f28e 100644
--- a/packages/core/src/test/shared/utilities/collectionUtils.test.ts
+++ b/packages/core/src/test/shared/utilities/collectionUtils.test.ts
@@ -710,8 +710,10 @@ describe('CollectionUtils', async function () {
})
describe('partialClone', function () {
- it('omits properties by depth', function () {
- const testObj = {
+ let multipleTypedObj: object
+
+ before(async function () {
+ multipleTypedObj = {
a: 34234234234,
b: '123456789',
c: new Date(2023, 1, 1),
@@ -724,57 +726,80 @@ describe('CollectionUtils', async function () {
throw Error()
},
}
+ })
- assert.deepStrictEqual(partialClone(testObj, 1), {
- ...testObj,
+ it('omits properties by depth', function () {
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 1), {
+ ...multipleTypedObj,
d: {},
e: {},
})
- assert.deepStrictEqual(partialClone(testObj, 0, [], '[replaced]'), '[replaced]')
- assert.deepStrictEqual(partialClone(testObj, 1, [], '[replaced]'), {
- ...testObj,
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 0, [], { replacement: '[replaced]' }), '[replaced]')
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 1, [], { replacement: '[replaced]' }), {
+ ...multipleTypedObj,
d: '[replaced]',
e: '[replaced]',
})
- assert.deepStrictEqual(partialClone(testObj, 3), {
- ...testObj,
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 3), {
+ ...multipleTypedObj,
d: { d1: { d2: {} } },
})
- assert.deepStrictEqual(partialClone(testObj, 4), testObj)
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 4), multipleTypedObj)
})
it('omits properties by name', function () {
- const testObj = {
- a: 34234234234,
- b: '123456789',
- c: new Date(2023, 1, 1),
- d: { d1: { d2: { d3: 'deep' } } },
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 2, ['c', 'e2'], { replacement: '[replaced]' }), {
+ ...multipleTypedObj,
+ c: '[replaced]',
+ d: { d1: '[replaced]' },
+ e: {
+ e1: '[replaced]',
+ e2: '[replaced]',
+ },
+ })
+ assert.deepStrictEqual(partialClone(multipleTypedObj, 3, ['c', 'e2'], { replacement: '[replaced]' }), {
+ ...multipleTypedObj,
+ c: '[replaced]',
+ d: { d1: { d2: '[replaced]' } },
e: {
e1: [4, 3, 7],
- e2: 'loooooooooo \n nnnnnnnnnnn \n gggggggg \n string',
+ e2: '[replaced]',
},
- f: () => {
- throw Error()
+ })
+ })
+
+ it('truncates properties by maxLength', function () {
+ const testObj = {
+ strValue: '1',
+ boolValue: true,
+ longString: '11111',
+ nestedObj: {
+ nestedObjAgain: {
+ longNestedStr: '11111',
+ shortNestedStr: '11',
+ },
+ },
+ nestedObj2: {
+ functionValue: (_: unknown) => {},
},
+ nestedObj3: {
+ myArray: ['1', '11111', '1'],
+ },
+ objInArray: [{ shortString: '11', longString: '11111' }],
}
-
- assert.deepStrictEqual(partialClone(testObj, 2, ['c', 'e2'], '[omitted]'), {
+ assert.deepStrictEqual(partialClone(testObj, 5, [], { maxStringLength: 2 }), {
...testObj,
- c: '[omitted]',
- d: { d1: '[omitted]' },
- e: {
- e1: '[omitted]',
- e2: '[omitted]',
+ longString: '11...',
+ nestedObj: {
+ nestedObjAgain: {
+ longNestedStr: '11...',
+ shortNestedStr: '11',
+ },
},
- })
- assert.deepStrictEqual(partialClone(testObj, 3, ['c', 'e2'], '[omitted]'), {
- ...testObj,
- c: '[omitted]',
- d: { d1: { d2: '[omitted]' } },
- e: {
- e1: [4, 3, 7],
- e2: '[omitted]',
+ nestedObj3: {
+ myArray: ['1', '11...', '1'],
},
+ objInArray: [{ shortString: '11', longString: '11...' }],
})
})
})