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. Screenshot 2025-04-24 at 5 42 51 PM --- - 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...' }], }) }) })