diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index d47cfcea0..cbf876683 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import path from 'path'; import { URL } from 'url'; import { IDE_NAME_SHORT, SNYK_TOKEN_KEY } from '../constants/general'; +import { SNYK_FEATURE_FLAG_COMMAND } from '../constants/commands'; import { ADVANCED_ADDITIONAL_PARAMETERS_SETTING, ADVANCED_ADVANCED_MODE_SETTING, @@ -54,6 +55,10 @@ export interface IConfiguration { authHost: string; + getFeatureFlag(flagName: string): boolean; + + setFeatureFlag(flagName: string, value: boolean): void; + getToken(): Promise; setToken(token: string | undefined): Promise; @@ -115,6 +120,8 @@ export class Configuration implements IConfiguration { private readonly defaultAuthHost = 'https://snyk.io'; private readonly defaultOssApiEndpoint = `${this.defaultAuthHost}/api/v1`; + private featureFlag: { [key: string]: boolean } = {}; + constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} getInsecure(): boolean { @@ -134,6 +141,14 @@ export class Configuration implements IConfiguration { return preview; } + getFeatureFlag(flagName: string): boolean { + return this.featureFlag[flagName] ?? false; + } + + setFeatureFlag(flagName: string, value: boolean): void { + this.featureFlag[flagName] = value; + } + private static async getPackageJsonConfig(): Promise<{ version: string; preview: boolean }> { return (await import(path.join('../../../..', 'package.json'))) as { version: string; preview: boolean }; } diff --git a/src/snyk/common/constants/commands.ts b/src/snyk/common/constants/commands.ts index ee3420077..6a118f9d4 100644 --- a/src/snyk/common/constants/commands.ts +++ b/src/snyk/common/constants/commands.ts @@ -24,6 +24,7 @@ export const SNYK_WORKSPACE_SCAN_COMMAND = 'snyk.workspace.scan'; export const SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND = 'snyk.trustWorkspaceFolders'; export const SNYK_GET_ACTIVE_USER = 'snyk.getActiveUser'; export const SNYK_CODE_FIX_DIFFS_COMMAND = 'snyk.code.fixDiffs'; +export const SNYK_FEATURE_FLAG_COMMAND = 'snyk.getFeatureFlagStatus'; // custom Snyk constants used in commands export const SNYK_CONTEXT_PREFIX = 'snyk:'; diff --git a/src/snyk/common/constants/featureFlags.ts b/src/snyk/common/constants/featureFlags.ts new file mode 100644 index 000000000..d6281b34e --- /dev/null +++ b/src/snyk/common/constants/featureFlags.ts @@ -0,0 +1,3 @@ +export const FEATURE_FLAGS = { + consistentIgnores: 'snykCodeConsistentIgnores', +}; diff --git a/src/snyk/common/languageServer/types.ts b/src/snyk/common/languageServer/types.ts index ea5219a0e..34075ff6f 100644 --- a/src/snyk/common/languageServer/types.ts +++ b/src/snyk/common/languageServer/types.ts @@ -51,6 +51,7 @@ export type CodeIssueData = { isSecurityType: boolean; priorityScore: number; hasAIFix: boolean; + details: string; // HTML from the LSP }; export type ExampleCommitFix = { diff --git a/src/snyk/common/types.ts b/src/snyk/common/types.ts index 7a5a667c4..0e39872f2 100644 --- a/src/snyk/common/types.ts +++ b/src/snyk/common/types.ts @@ -37,3 +37,8 @@ export function languageToString(language: Language): string { return PJSON; } } + +export type FeatureFlagStatus = { + ok: boolean; + userMessage?: string; +}; diff --git a/src/snyk/common/watchers/configurationWatcher.ts b/src/snyk/common/watchers/configurationWatcher.ts index 265611129..022fcea26 100644 --- a/src/snyk/common/watchers/configurationWatcher.ts +++ b/src/snyk/common/watchers/configurationWatcher.ts @@ -11,20 +11,29 @@ import { CODE_QUALITY_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, IAC_ENABLED_SETTING, + ADVANCED_ORGANIZATION, OSS_ENABLED_SETTING, SEVERITY_FILTER_SETTING, TRUSTED_FOLDERS, } from '../constants/settings'; +import { FEATURE_FLAGS } from '../constants/featureFlags'; import { ErrorHandler } from '../error/errorHandler'; import { ILog } from '../logger/interfaces'; import { errorsLogs } from '../messages/errors'; import SecretStorageAdapter from '../vscode/secretStorage'; import { IWatcher } from './interfaces'; +import { IVSCodeCommands } from '../vscode/commands'; +import { SNYK_FEATURE_FLAG_COMMAND } from '../constants/commands'; +import { FeatureFlagStatus } from '../types'; class ConfigurationWatcher implements IWatcher { - constructor(private readonly logger: ILog) {} + constructor(private readonly logger: ILog, private commandExecutor: IVSCodeCommands) {} private async onChangeConfiguration(extension: IExtension, key: string): Promise { + if (key === ADVANCED_ORGANIZATION) { + const isEnabled = await this.fetchFeatureFlag(FEATURE_FLAGS.consistentIgnores); + configuration.setFeatureFlag(FEATURE_FLAGS.consistentIgnores, isEnabled); + } if (key === ADVANCED_ADVANCED_MODE_SETTING) { return extension.checkAdvancedMode(); } else if (key === OSS_ENABLED_SETTING) { @@ -63,6 +72,7 @@ class ConfigurationWatcher implements IWatcher { const change = [ ADVANCED_ADVANCED_MODE_SETTING, ADVANCED_AUTOSCAN_OSS_SETTING, + ADVANCED_ORGANIZATION, OSS_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, CODE_QUALITY_ENABLED_SETTING, @@ -88,6 +98,19 @@ class ConfigurationWatcher implements IWatcher { } }); } + + private async fetchFeatureFlag(flagName: string): Promise { + try { + const ffStatus = await this.commandExecutor.executeCommand( + SNYK_FEATURE_FLAG_COMMAND, + flagName, + ); + return ffStatus?.ok ?? false; + } catch (error) { + console.warn(`Failed to fetch feature flag ${flagName}: ${error}`); + return false; + } + } } export default ConfigurationWatcher; diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 99a102d4c..60f25db5d 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -35,6 +35,7 @@ import { SNYK_VIEW_SUPPORT, SNYK_VIEW_WELCOME, } from './common/constants/views'; +import { FEATURE_FLAGS } from './common/constants/featureFlags'; import { ErrorHandler } from './common/error/errorHandler'; import { ErrorReporter } from './common/error/errorReporter'; import { ExperimentService } from './common/experiment/services/experimentService'; @@ -118,7 +119,7 @@ class SnykExtension extends SnykLib implements IExtension { SecretStorageAdapter.init(vscodeContext); - this.configurationWatcher = new ConfigurationWatcher(Logger); + this.configurationWatcher = new ConfigurationWatcher(Logger, vsCodeCommands); this.notificationService = new NotificationService(vsCodeWindow, vsCodeCommands, configuration, Logger); this.statusBarItem.show(); @@ -366,6 +367,8 @@ class SnykExtension extends SnykLib implements IExtension { // The codeEnabled context depends on an LS command await this.languageServer.start(); + // Fetch feature flag to determine whether to use the new LSP-based rendering. + // initialize contexts await this.contextService.setContext(SNYK_CONTEXT.INITIALIZED, true); diff --git a/src/test/unit/common/services/learnService.test.ts b/src/test/unit/common/services/learnService.test.ts index 8607f06e0..779a38106 100644 --- a/src/test/unit/common/services/learnService.test.ts +++ b/src/test/unit/common/services/learnService.test.ts @@ -36,6 +36,7 @@ suite('LearnService', () => { isSecurityType: true, priorityScore: 880, hasAIFix: false, + details: 'not used', }, title: 'not used', severity: IssueSeverity.Critical,