From 24c359a253cf268fcad03bf66933fed46fa38b00 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 10:32:02 +0100 Subject: [PATCH 1/9] feat: add plugin installed event --- src/snyk/common/analytics/AnalyticsEvent.ts | 75 +++++++++++++++++++ src/snyk/common/analytics/AnalyticsSender.ts | 75 +++++++++++++++++++ .../common/configuration/configuration.ts | 21 ++++++ src/snyk/common/constants/commands.ts | 1 + src/snyk/common/constants/languageServer.ts | 2 +- src/snyk/common/constants/settings.ts | 2 + src/snyk/extension.ts | 14 ++++ 7 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/snyk/common/analytics/AnalyticsEvent.ts create mode 100644 src/snyk/common/analytics/AnalyticsSender.ts diff --git a/src/snyk/common/analytics/AnalyticsEvent.ts b/src/snyk/common/analytics/AnalyticsEvent.ts new file mode 100644 index 000000000..e4855a0d6 --- /dev/null +++ b/src/snyk/common/analytics/AnalyticsEvent.ts @@ -0,0 +1,75 @@ +import { AbstractAnalyticsEvent } from './AnalyticsSender'; + +export class AnalyticsEvent implements AbstractAnalyticsEvent { + private readonly interactionType: string; + private readonly category: string[]; + private readonly status: string; + private readonly targetId: string; + private readonly timestampMs: number; + private readonly durationMs: number; + private readonly results: Map; + private readonly errors: any[]; + private readonly extension: Map; + + constructor( + deviceId: string, + interactionType: string, + category: string[], + status: string = 'success', + targetId: string = 'pkg:filesystem/scrubbed', + timestampMs: number = Date.now(), + durationMs: number = 0, + results: Map = new Map(), + errors: any[] = [], + extension: Map = new Map(), + ) { + this.interactionType = interactionType; + this.category = category; + this.status = status ?? 'success'; + this.targetId = targetId ?? 'pkg:filesystem/scrubbed'; + this.timestampMs = timestampMs ?? Date.now(); + this.durationMs = durationMs ?? 0; + this.results = results ?? new Map(); + this.errors = errors ?? []; + this.extension = extension ?? new Map(); + if (deviceId && deviceId.length > 0) { + this.extension.set('device_id', deviceId); + } + } + + public getInteractionType(): string { + return this.interactionType; + } + + public getCategory(): string[] { + return this.category; + } + + public getStatus(): string { + return this.status; + } + + public getTargetId(): string { + return this.targetId; + } + + public getTimestampMs(): number { + return this.timestampMs; + } + + public getDurationMs(): number { + return this.durationMs; + } + + public getResults(): Map { + return this.results; + } + + public getErrors(): any[] { + return this.errors; + } + + public getExtension(): Map { + return this.extension; + } +} diff --git a/src/snyk/common/analytics/AnalyticsSender.ts b/src/snyk/common/analytics/AnalyticsSender.ts new file mode 100644 index 000000000..b0319c3df --- /dev/null +++ b/src/snyk/common/analytics/AnalyticsSender.ts @@ -0,0 +1,75 @@ +import { ILog } from '../logger/interfaces'; +import { IConfiguration } from '../configuration/configuration'; +import { sleep } from '@amplitude/experiment-node-server/dist/src/util/time'; +import { IVSCodeCommands } from '../vscode/commands'; +import { SNYK_REPORT_ANALTYICS } from '../constants/commands'; + +interface EventPair { + event: AbstractAnalyticsEvent; + callback: (value: void) => void; +} + +// This is just a marker interface, to ensure type security when sending events +export interface AbstractAnalyticsEvent {} + +export class AnalyticsSender { + private static instance: AnalyticsSender; + private eventQueue: EventPair[] = []; + private configuration: IConfiguration; + private logger: ILog; + private commandExecutor: IVSCodeCommands; + + private constructor(logger: ILog, configuration: IConfiguration, commandExecutor: IVSCodeCommands) { + this.logger = logger; + this.configuration = configuration; + this.commandExecutor = commandExecutor; + + void this.start(); + } + + public static getInstance( + logger: ILog, + configuration: IConfiguration, + commandExecutor: IVSCodeCommands, + ): AnalyticsSender { + if (!AnalyticsSender.instance) { + AnalyticsSender.instance = new AnalyticsSender(logger, configuration, commandExecutor); + } + return AnalyticsSender.instance; + } + + private async start(): Promise { + // noinspection InfiniteLoopJS + while (true) { + const authToken = await this.configuration.getToken(); + + if (this.eventQueue.length === 0 || !authToken || authToken.trim() === '') { + await sleep(1000); + continue; + } + + const copyForSending = [...this.eventQueue]; + for (let i = 0; i < copyForSending.length; i++) { + const eventPair = copyForSending[i]; + try { + const args = []; + args.push(eventPair.event); + await this.commandExecutor.executeCommand(SNYK_REPORT_ANALTYICS, args); + eventPair.callback(); + } catch (e) { + this.logger.error(e); + } finally { + // let's not rely on indexes in the eventQueue array not having changed + const index = this.eventQueue.indexOf(eventPair); + if (index > -1) { + this.eventQueue.splice(index, 1); + } + } + } + } + } + + public logEvent(event: AbstractAnalyticsEvent, callback: (value: void) => void): void { + this.eventQueue.push({ event, callback }); + } +} diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 15b8aeddc..315f91042 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -12,6 +12,7 @@ import { ADVANCED_CUSTOM_ENDPOINT, ADVANCED_CUSTOM_LS_PATH, ADVANCED_ORGANIZATION, + ANALYTICS_PLUGIN_INSTALLED_SENT, CODE_QUALITY_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, CONFIGURATION_IDENTIFIER, @@ -140,6 +141,10 @@ export interface IConfiguration { getFolderConfigs(): FolderConfig[]; setFolderConfigs(folderConfig: FolderConfig[]): Promise; + + getAnalyticsPluginInstalledSent(): boolean; + + setAnalyticsPluginInstalledSent(b: boolean): Promise; } export class Configuration implements IConfiguration { @@ -152,6 +157,22 @@ export class Configuration implements IConfiguration { constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} + getAnalyticsPluginInstalledSent(): boolean { + const sent = this.workspace.getConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ANALYTICS_PLUGIN_INSTALLED_SENT), + ); + return sent ?? false; + } + + async setAnalyticsPluginInstalledSent(b: boolean): Promise { + return this.workspace.updateConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ANALYTICS_PLUGIN_INSTALLED_SENT), + b, + ); + } + getOssQuickFixCodeActionsEnabled(): boolean { return this.getPreviewFeatures().ossQuickfixes ?? false; } diff --git a/src/snyk/common/constants/commands.ts b/src/snyk/common/constants/commands.ts index ae2aca170..37c6ffbf0 100644 --- a/src/snyk/common/constants/commands.ts +++ b/src/snyk/common/constants/commands.ts @@ -30,6 +30,7 @@ export const SNYK_FEATURE_FLAG_COMMAND = 'snyk.getFeatureFlagStatus'; export const SNYK_CLEAR_CACHE_COMMAND = 'snyk.clearCache'; export const SNYK_CLEAR_PERSISTED_CACHE_COMMAND = 'snyk.clearPersistedCache'; export const SNYK_GENERATE_ISSUE_DESCRIPTION = 'snyk.generateIssueDescription'; +export const SNYK_REPORT_ANALTYICS = 'snyk.reportAnalytics'; // custom Snyk constants used in commands export const SNYK_CONTEXT_PREFIX = 'snyk:'; diff --git a/src/snyk/common/constants/languageServer.ts b/src/snyk/common/constants/languageServer.ts index 17b4686e8..eb96907da 100644 --- a/src/snyk/common/constants/languageServer.ts +++ b/src/snyk/common/constants/languageServer.ts @@ -2,7 +2,7 @@ // Language Server name, used e.g. for the output channel export const SNYK_LANGUAGE_SERVER_NAME = 'Snyk Language Server'; // The internal language server protocol version for custom messages and configuration -export const PROTOCOL_VERSION = 16; +export const PROTOCOL_VERSION = 17; // LS protocol methods (needed for not having to rely on vscode dependencies in testing) export const DID_CHANGE_CONFIGURATION_METHOD = 'workspace/didChangeConfiguration'; diff --git a/src/snyk/common/constants/settings.ts b/src/snyk/common/constants/settings.ts index 1e82f8c6a..dec7a402e 100644 --- a/src/snyk/common/constants/settings.ts +++ b/src/snyk/common/constants/settings.ts @@ -29,3 +29,5 @@ export const FOLDER_CONFIGS = `${CONFIGURATION_IDENTIFIER}.folderConfigs`; export const SCANNING_MODE = `${CONFIGURATION_IDENTIFIER}.scanningMode`; export const DELTA_FINDINGS = `${CONFIGURATION_IDENTIFIER}.allIssuesVsNetNewIssues`; + +export const ANALYTICS_PLUGIN_INSTALLED_SENT = `${CONFIGURATION_IDENTIFIER}.pluginInstalledSent`; diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 345812415..01195b208 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -81,6 +81,8 @@ import { CodeIssueData, IacIssueData, OssIssueData } from './common/languageServ import { ClearCacheService } from './common/services/CacheService'; import { InMemory, Persisted } from './common/constants/general'; import { GitAPI, GitExtension, Repository } from './common/git'; +import { AnalyticsSender } from './common/analytics/AnalyticsSender'; +import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { @@ -429,6 +431,18 @@ class SnykExtension extends SnykLib implements IExtension { // initialize contexts await this.contextService.setContext(SNYK_CONTEXT.INITIALIZED, true); + // start analytics sender and send plugin installed event + const analyticsSender = AnalyticsSender.getInstance(Logger, configuration, this.commandController); + + if (!configuration.getAnalyticsPluginInstalledSent()) { + const category = []; + category.push('install'); + const pluginInstalleEvent = new AnalyticsEvent(this.user.anonymousId, 'plugin installed', category); + analyticsSender.logEvent(pluginInstalleEvent, () => { + void configuration.setAnalyticsPluginInstalledSent(true); + }); + } + // Actually start analysis this.runScan(); } From f482442e4fd58fe4eb48623088aee3d79795855f Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 10:37:59 +0100 Subject: [PATCH 2/9] fix: warnings --- src/snyk/common/configuration/configuration.ts | 2 -- src/snyk/extension.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 315f91042..b518af36a 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -148,8 +148,6 @@ export interface IConfiguration { } export class Configuration implements IConfiguration { - // These attributes are used in tests - private readonly defaultSnykCodeBaseURL = 'https://deeproxy.snyk.io'; private readonly defaultAuthHost = 'https://app.snyk.io'; private readonly defaultApiEndpoint = 'https://api.snyk.io'; diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 01195b208..4bdda06f9 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -48,7 +48,6 @@ import { NotificationService } from './common/services/notificationService'; import { User } from './common/user'; import { CodeActionAdapter } from './common/vscode/codeAction'; import { vsCodeCommands } from './common/vscode/commands'; -import { vsCodeEnv } from './common/vscode/env'; import { extensionContext } from './common/vscode/extensionContext'; import { LanguageClientAdapter } from './common/vscode/languageClient'; import { vsCodeLanguages } from './common/vscode/languages'; @@ -383,7 +382,7 @@ class SnykExtension extends SnykLib implements IExtension { e.removed.forEach(folder => { this.snykCode.resetResult(folder.uri.fsPath); }); - this.runScan(false); + this.runScan(); }); this.editorsWatcher.activate(this); From 9c3d454b12514b59933c3a715213e3e5166562fd Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 10:44:00 +0100 Subject: [PATCH 3/9] fix: use global extension state --- .../common/configuration/configuration.ts | 21 +------------------ src/snyk/common/constants/globalState.ts | 1 + src/snyk/common/constants/settings.ts | 2 -- src/snyk/extension.ts | 9 ++++++-- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index b518af36a..fcaa85229 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -31,6 +31,7 @@ import { } from '../constants/settings'; import SecretStorageAdapter from '../vscode/secretStorage'; import { IVSCodeWorkspace } from '../vscode/workspace'; +import { MEMENTO_LS_CHECKSUM } from '../constants/globalState'; const NEWISSUES = 'Net new issues'; @@ -141,10 +142,6 @@ export interface IConfiguration { getFolderConfigs(): FolderConfig[]; setFolderConfigs(folderConfig: FolderConfig[]): Promise; - - getAnalyticsPluginInstalledSent(): boolean; - - setAnalyticsPluginInstalledSent(b: boolean): Promise; } export class Configuration implements IConfiguration { @@ -155,22 +152,6 @@ export class Configuration implements IConfiguration { constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} - getAnalyticsPluginInstalledSent(): boolean { - const sent = this.workspace.getConfiguration( - CONFIGURATION_IDENTIFIER, - this.getConfigName(ANALYTICS_PLUGIN_INSTALLED_SENT), - ); - return sent ?? false; - } - - async setAnalyticsPluginInstalledSent(b: boolean): Promise { - return this.workspace.updateConfiguration( - CONFIGURATION_IDENTIFIER, - this.getConfigName(ANALYTICS_PLUGIN_INSTALLED_SENT), - b, - ); - } - getOssQuickFixCodeActionsEnabled(): boolean { return this.getPreviewFeatures().ossQuickfixes ?? false; } diff --git a/src/snyk/common/constants/globalState.ts b/src/snyk/common/constants/globalState.ts index ff12e3a06..64b00fb10 100644 --- a/src/snyk/common/constants/globalState.ts +++ b/src/snyk/common/constants/globalState.ts @@ -2,3 +2,4 @@ export const MEMENTO_ANONYMOUS_ID = 'snyk.anonymousId'; export const MEMENTO_LS_LAST_UPDATE_DATE = 'snyk.lsLastUpdateDate'; export const MEMENTO_LS_PROTOCOL_VERSION = 'snyk.lsProtocolVersion'; export const MEMENTO_LS_CHECKSUM = 'snyk.lsChecksum'; +export const MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT = 'snyk.pluginInstalledSent'; diff --git a/src/snyk/common/constants/settings.ts b/src/snyk/common/constants/settings.ts index dec7a402e..1e82f8c6a 100644 --- a/src/snyk/common/constants/settings.ts +++ b/src/snyk/common/constants/settings.ts @@ -29,5 +29,3 @@ export const FOLDER_CONFIGS = `${CONFIGURATION_IDENTIFIER}.folderConfigs`; export const SCANNING_MODE = `${CONFIGURATION_IDENTIFIER}.scanningMode`; export const DELTA_FINDINGS = `${CONFIGURATION_IDENTIFIER}.allIssuesVsNetNewIssues`; - -export const ANALYTICS_PLUGIN_INSTALLED_SENT = `${CONFIGURATION_IDENTIFIER}.pluginInstalledSent`; diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 4bdda06f9..1be195d8e 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -82,6 +82,8 @@ import { InMemory, Persisted } from './common/constants/general'; import { GitAPI, GitExtension, Repository } from './common/git'; import { AnalyticsSender } from './common/analytics/AnalyticsSender'; import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; +import { MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT, MEMENTO_LS_CHECKSUM } from './common/constants/globalState'; +import { ANALYTICS_PLUGIN_INSTALLED_SENT, CONFIGURATION_IDENTIFIER } from './common/constants/settings'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { @@ -433,12 +435,15 @@ class SnykExtension extends SnykLib implements IExtension { // start analytics sender and send plugin installed event const analyticsSender = AnalyticsSender.getInstance(Logger, configuration, this.commandController); - if (!configuration.getAnalyticsPluginInstalledSent()) { + const pluginInstalledSent = + extensionContext.getGlobalStateValue(MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT) ?? false; + + if (!pluginInstalledSent) { const category = []; category.push('install'); const pluginInstalleEvent = new AnalyticsEvent(this.user.anonymousId, 'plugin installed', category); analyticsSender.logEvent(pluginInstalleEvent, () => { - void configuration.setAnalyticsPluginInstalledSent(true); + void extensionContext.updateGlobalStateValue(MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT, true); }); } From 404406b862342226c675ae55efc6e38f64bb2ea3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 10:44:32 +0100 Subject: [PATCH 4/9] fix: use global extension state --- src/snyk/extension.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 1be195d8e..bb2f167ee 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -82,8 +82,7 @@ import { InMemory, Persisted } from './common/constants/general'; import { GitAPI, GitExtension, Repository } from './common/git'; import { AnalyticsSender } from './common/analytics/AnalyticsSender'; import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; -import { MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT, MEMENTO_LS_CHECKSUM } from './common/constants/globalState'; -import { ANALYTICS_PLUGIN_INSTALLED_SENT, CONFIGURATION_IDENTIFIER } from './common/constants/settings'; +import { MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT } from './common/constants/globalState'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { From c95a4c2ad02d3c16caa1ade648cf8e4154a01df5 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 11:04:41 +0100 Subject: [PATCH 5/9] fix: a few lint warnings --- src/snyk/common/analytics/AnalyticsEvent.ts | 22 +++++++++---------- src/snyk/common/analytics/AnalyticsSender.ts | 12 +++++++--- .../common/configuration/configuration.ts | 1 - .../providers/ossDetailPanelProvider.ts | 4 +--- .../providers/ossVulnerabilityTreeProvider.ts | 4 ++-- .../vulnerabilityCountEmitter.ts | 6 +---- src/test/integration/configuration.test.ts | 1 - .../services/authenticationService.test.ts | 11 ---------- .../common/commands/commandController.test.ts | 3 +-- src/test/unit/mocks/uri.mock.ts | 16 -------------- src/test/unit/mocks/workspace.mock.ts | 2 -- .../codeIssuesActionsProvider.test.ts | 1 - src/test/unit/snykCode/codeSettings.test.ts | 2 -- .../providers/ossCodeActionsProvider.test.ts | 3 +++ .../vulnerabilityCountProvider.test.ts | 2 +- 15 files changed, 29 insertions(+), 61 deletions(-) delete mode 100644 src/test/unit/mocks/uri.mock.ts diff --git a/src/snyk/common/analytics/AnalyticsEvent.ts b/src/snyk/common/analytics/AnalyticsEvent.ts index e4855a0d6..2e219f4e7 100644 --- a/src/snyk/common/analytics/AnalyticsEvent.ts +++ b/src/snyk/common/analytics/AnalyticsEvent.ts @@ -7,9 +7,9 @@ export class AnalyticsEvent implements AbstractAnalyticsEvent { private readonly targetId: string; private readonly timestampMs: number; private readonly durationMs: number; - private readonly results: Map; - private readonly errors: any[]; - private readonly extension: Map; + private readonly results: Map; + private readonly errors: unknown[]; + private readonly extension: Map; constructor( deviceId: string, @@ -19,9 +19,9 @@ export class AnalyticsEvent implements AbstractAnalyticsEvent { targetId: string = 'pkg:filesystem/scrubbed', timestampMs: number = Date.now(), durationMs: number = 0, - results: Map = new Map(), - errors: any[] = [], - extension: Map = new Map(), + results: Map = new Map(), + errors: unknown[] = [], + extension: Map = new Map(), ) { this.interactionType = interactionType; this.category = category; @@ -29,9 +29,9 @@ export class AnalyticsEvent implements AbstractAnalyticsEvent { this.targetId = targetId ?? 'pkg:filesystem/scrubbed'; this.timestampMs = timestampMs ?? Date.now(); this.durationMs = durationMs ?? 0; - this.results = results ?? new Map(); + this.results = results ?? new Map(); this.errors = errors ?? []; - this.extension = extension ?? new Map(); + this.extension = extension ?? new Map(); if (deviceId && deviceId.length > 0) { this.extension.set('device_id', deviceId); } @@ -61,15 +61,15 @@ export class AnalyticsEvent implements AbstractAnalyticsEvent { return this.durationMs; } - public getResults(): Map { + public getResults(): Map { return this.results; } - public getErrors(): any[] { + public getErrors(): unknown[] { return this.errors; } - public getExtension(): Map { + public getExtension(): Map { return this.extension; } } diff --git a/src/snyk/common/analytics/AnalyticsSender.ts b/src/snyk/common/analytics/AnalyticsSender.ts index b0319c3df..8ec2c74e7 100644 --- a/src/snyk/common/analytics/AnalyticsSender.ts +++ b/src/snyk/common/analytics/AnalyticsSender.ts @@ -1,3 +1,5 @@ +// noinspection InfiniteLoopJS + import { ILog } from '../logger/interfaces'; import { IConfiguration } from '../configuration/configuration'; import { sleep } from '@amplitude/experiment-node-server/dist/src/util/time'; @@ -39,11 +41,13 @@ export class AnalyticsSender { } private async start(): Promise { - // noinspection InfiniteLoopJS + // eslint-disable-next-line no-constant-condition while (true) { + // eslint-disable-next-line no-await-in-loop const authToken = await this.configuration.getToken(); if (this.eventQueue.length === 0 || !authToken || authToken.trim() === '') { + // eslint-disable-next-line no-await-in-loop await sleep(1000); continue; } @@ -54,10 +58,12 @@ export class AnalyticsSender { try { const args = []; args.push(eventPair.event); + // eslint-disable-next-line no-await-in-loop await this.commandExecutor.executeCommand(SNYK_REPORT_ANALTYICS, args); eventPair.callback(); - } catch (e) { - this.logger.error(e); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + this.logger.error(`could not send ${eventPair.event} ${error}`); } finally { // let's not rely on indexes in the eventQueue array not having changed const index = this.eventQueue.indexOf(eventPair); diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index fcaa85229..91ab2ddce 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -12,7 +12,6 @@ import { ADVANCED_CUSTOM_ENDPOINT, ADVANCED_CUSTOM_LS_PATH, ADVANCED_ORGANIZATION, - ANALYTICS_PLUGIN_INSTALLED_SENT, CODE_QUALITY_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, CONFIGURATION_IDENTIFIER, diff --git a/src/snyk/snykOss/providers/ossDetailPanelProvider.ts b/src/snyk/snykOss/providers/ossDetailPanelProvider.ts index 5febe11ad..659a50fbf 100644 --- a/src/snyk/snykOss/providers/ossDetailPanelProvider.ts +++ b/src/snyk/snykOss/providers/ossDetailPanelProvider.ts @@ -62,8 +62,7 @@ export class OssDetailPanelProvider ); this.registerListeners(); } - - const images: Record = [ + [ ['icon-code', 'svg'], ['dark-critical-severity', 'svg'], ['dark-high-severity', 'svg'], @@ -76,7 +75,6 @@ export class OssDetailPanelProvider accumulator[name] = uri.toString(); return accumulator; }, {}); - let html: string = ''; // TODO: delete this when SNYK_GENERATE_ISSUE_DESCRIPTION command is in stable CLI. if (issue.additionalData.details) { diff --git a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts index dfb99b55a..66dd6d00e 100644 --- a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts +++ b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts @@ -53,7 +53,7 @@ export default class OssIssueTreeProvider extends ProductIssueTreeProvider[]): Issue[] { return issues.filter(vuln => { - switch (vuln.severity.toLowerCase()) { + switch (vuln.severity) { case IssueSeverity.Critical: return this.configuration.severityFilter.critical; case IssueSeverity.High: diff --git a/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts index dcde88813..d2b66dc38 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts @@ -2,7 +2,6 @@ import EventEmitter from 'events'; import { ImportedModule, ModuleVulnerabilityCount } from './importedModule'; export enum VulnerabilityCountEvents { - PackageJsonFound = 'packageJsonFound', Start = 'start', Scanned = 'scanned', Done = 'done', @@ -10,10 +9,6 @@ export enum VulnerabilityCountEvents { } export class VulnerabilityCountEmitter extends EventEmitter { - packageJsonFound(fileName: string): void { - this.emit(VulnerabilityCountEvents.PackageJsonFound, fileName); - } - startScanning(importedModules: ImportedModule[]): void { this.emit(VulnerabilityCountEvents.Start, importedModules); } @@ -26,6 +21,7 @@ export class VulnerabilityCountEmitter extends EventEmitter { this.emit(VulnerabilityCountEvents.Done, moduleInfos); } + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents error(error: Error | unknown): void { this.emit(VulnerabilityCountEvents.Error, error); } diff --git a/src/test/integration/configuration.test.ts b/src/test/integration/configuration.test.ts index 2a0c94146..5c3e5d43a 100644 --- a/src/test/integration/configuration.test.ts +++ b/src/test/integration/configuration.test.ts @@ -3,7 +3,6 @@ import { FeaturesConfiguration } from '../../snyk/common/configuration/configura import { configuration } from '../../snyk/common/configuration/instance'; import vscode from 'vscode'; import { ADVANCED_CUSTOM_ENDPOINT } from '../../snyk/common/constants/settings'; -import { extensionContext } from '../../snyk/common/vscode/extensionContext'; suite('Configuration', () => { test('settings change is reflected', async () => { diff --git a/src/test/unit/base/services/authenticationService.test.ts b/src/test/unit/base/services/authenticationService.test.ts index f77004874..f0a78cd2a 100644 --- a/src/test/unit/base/services/authenticationService.test.ts +++ b/src/test/unit/base/services/authenticationService.test.ts @@ -25,17 +25,6 @@ suite('AuthenticationService', () => { let clearTokenSpy: sinon.SinonSpy; let previewFeaturesSpy: sinon.SinonSpy; - const NEEDLE_DEFAULT_TIMEOUT = 1000; - - const overrideNeedleTimeoutOptions = { - // eslint-disable-next-line camelcase - open_timeout: NEEDLE_DEFAULT_TIMEOUT, - // eslint-disable-next-line camelcase - response_timeout: NEEDLE_DEFAULT_TIMEOUT, - // eslint-disable-next-line camelcase - read_timeout: NEEDLE_DEFAULT_TIMEOUT, - }; - setup(() => { baseModule = {} as IBaseSnykModule; setContextSpy = sinon.fake(); diff --git a/src/test/unit/common/commands/commandController.test.ts b/src/test/unit/common/commands/commandController.test.ts index 3da22aa27..6d37814c7 100644 --- a/src/test/unit/common/commands/commandController.test.ts +++ b/src/test/unit/common/commands/commandController.test.ts @@ -16,8 +16,7 @@ import { IConfiguration } from '../../../../snyk/common/configuration/configurat import { IFolderConfigs } from '../../../../snyk/common/configuration/folderConfigs'; suite('CommandController', () => { - const sleep = util.promisify(setTimeout); - + util.promisify(setTimeout); let controller: CommandController; setup(() => { diff --git a/src/test/unit/mocks/uri.mock.ts b/src/test/unit/mocks/uri.mock.ts deleted file mode 100644 index 929b380a2..000000000 --- a/src/test/unit/mocks/uri.mock.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Uri } from '../../../snyk/common/vscode/types'; -import { IUriAdapter } from '../../../snyk/common/vscode/uri'; - -class UriAdapterMock implements IUriAdapter { - file(path: string): Uri { - return { - path: path, - } as Uri; - } - - parse(path: string): Uri { - return { - path: path, - } as Uri; - } -} diff --git a/src/test/unit/mocks/workspace.mock.ts b/src/test/unit/mocks/workspace.mock.ts index a46d64348..2e1ee2329 100644 --- a/src/test/unit/mocks/workspace.mock.ts +++ b/src/test/unit/mocks/workspace.mock.ts @@ -1,5 +1,3 @@ -import * as os from 'os'; -import path from 'path'; import { IVSCodeWorkspace } from '../../../snyk/common/vscode/workspace'; export function stubWorkspaceConfiguration(configSetting: string, returnValue: T | undefined): IVSCodeWorkspace { diff --git a/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts b/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts index 18c5a9ad3..3a787cf1a 100644 --- a/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts +++ b/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts @@ -9,7 +9,6 @@ import { CodeActionContext, CodeActionKind, Range, TextDocument } from '../../.. import { SnykCodeActionsProvider } from '../../../../snyk/snykCode/codeActions/codeIssuesActionsProvider'; import { IssueUtils } from '../../../../snyk/snykCode/utils/issueUtils'; import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; -import { FEATURE_FLAGS } from '../../../../snyk/common/constants/featureFlags'; suite('Snyk Code actions provider', () => { let issuesActionsProvider: SnykCodeActionsProvider; diff --git a/src/test/unit/snykCode/codeSettings.test.ts b/src/test/unit/snykCode/codeSettings.test.ts index 56fd82c0b..604e1e5b3 100644 --- a/src/test/unit/snykCode/codeSettings.test.ts +++ b/src/test/unit/snykCode/codeSettings.test.ts @@ -10,12 +10,10 @@ import { CodeSettings, ICodeSettings } from '../../../snyk/snykCode/codeSettings suite('Snyk Code Settings', () => { let settings: ICodeSettings; let setContextFake: SinonSpy; - let setFeatureFlagFake: SinonSpy; let contextService: IContextService; setup(() => { setContextFake = sinon.fake(); - setFeatureFlagFake = sinon.fake(); contextService = { setContext: setContextFake, diff --git a/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts b/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts index c311a7b8c..42f55e676 100644 --- a/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts +++ b/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts @@ -96,8 +96,11 @@ suite('OSS code actions provider', () => { sinon.stub(ossActionsProvider, 'getIssueRange').returns(rangeMock); // stubbing private methods workaround is to cast to any + // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(ossActionsProvider, 'getVulnerabilities').returns(vulnerabilities); + // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(ossActionsProvider, 'getMostSevereVulnerability').returns(mostSevereVulnerability); + // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(ossActionsProvider, 'getActions').returns(codeActions); // act diff --git a/src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts b/src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts index df42b0df7..6bda8c3bc 100644 --- a/src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts +++ b/src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts @@ -60,7 +60,7 @@ suite('OSS VulnerabilityCountProvider', () => { let sampleFileName = 'package.json'; const sameplUri = `file:///Users/some.user/Documents/some-project/${sampleFileName}`; - let languageClientStub: { sendRequest: any }; + let languageClientStub: { sendRequest: unknown }; let uriStub; setup(() => { From 612967d27589bb0698803267124d97fb9aa5af49 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 14:49:53 +0100 Subject: [PATCH 6/9] chore: set hover verbosity to 1 --- src/snyk/common/languageServer/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snyk/common/languageServer/settings.ts b/src/snyk/common/languageServer/settings.ts index 59ce5198a..723bea05a 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -87,7 +87,7 @@ export class LanguageServerSettings { requiredProtocolVersion: `${PROTOCOL_VERSION}`, folderConfigs: configuration.getFolderConfigs(), enableSnykOSSQuickFixCodeActions: `${configuration.getPreviewFeatures().ossQuickfixes}`, - hoverVerbosity: 0, + hoverVerbosity: 1, }; } } From 6e5b187ae8cc0820649b68c23c84d7e83440b215 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 16:01:59 +0100 Subject: [PATCH 7/9] fix: test --- src/test/unit/common/languageServer/languageServer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 561b6b3b3..11449b01f 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -234,7 +234,7 @@ suite('Language Server', () => { folderConfigs: [], authenticationMethod: 'oauth', enableSnykOSSQuickFixCodeActions: 'false', - hoverVerbosity: 0, + hoverVerbosity: 1, }; deepStrictEqual(await languageServer.getInitializationOptions(), expectedInitializationOptions); From 5e52237f17bbc15e357e7eaed0f7a1fb649daba0 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Nov 2024 16:03:02 +0100 Subject: [PATCH 8/9] docs: updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a64de19a3..29d8850b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Snyk Security Changelog ## [2.20.0] -- disable hovers over issues +- reduce hover verbosity to only title and description - If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings. - Delete sentry reporting. +- send analytics event "plugin installed" the first time the extension is started ## [2.19.2] - Update download endpoint to downloads.snyk.io. From 32038b84ab1ba8bd0cdb7886b4ddada8882ec80e Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 11 Nov 2024 11:01:03 +0100 Subject: [PATCH 9/9] fix: send plugin installed event after init --- src/snyk/common/analytics/AnalyticsSender.ts | 33 ++++++++++--------- .../common/configuration/configuration.ts | 1 - src/snyk/common/constants/commands.ts | 2 +- src/snyk/common/views/issueTreeProvider.ts | 4 +-- src/snyk/extension.ts | 21 +++++++----- .../codeSuggestionWebviewProvider.ts | 4 +-- 6 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/snyk/common/analytics/AnalyticsSender.ts b/src/snyk/common/analytics/AnalyticsSender.ts index 8ec2c74e7..32fdcc0a7 100644 --- a/src/snyk/common/analytics/AnalyticsSender.ts +++ b/src/snyk/common/analytics/AnalyticsSender.ts @@ -4,7 +4,9 @@ import { ILog } from '../logger/interfaces'; import { IConfiguration } from '../configuration/configuration'; import { sleep } from '@amplitude/experiment-node-server/dist/src/util/time'; import { IVSCodeCommands } from '../vscode/commands'; -import { SNYK_REPORT_ANALTYICS } from '../constants/commands'; +import { SNYK_REPORT_ANALYTICS } from '../constants/commands'; +import { IContextService } from '../services/contextService'; +import { SNYK_CONTEXT } from '../constants/views'; interface EventPair { event: AbstractAnalyticsEvent; @@ -17,15 +19,13 @@ export interface AbstractAnalyticsEvent {} export class AnalyticsSender { private static instance: AnalyticsSender; private eventQueue: EventPair[] = []; - private configuration: IConfiguration; - private logger: ILog; - private commandExecutor: IVSCodeCommands; - - private constructor(logger: ILog, configuration: IConfiguration, commandExecutor: IVSCodeCommands) { - this.logger = logger; - this.configuration = configuration; - this.commandExecutor = commandExecutor; + constructor( + private logger: ILog, + private configuration: IConfiguration, + private commandExecutor: IVSCodeCommands, + private contextService: IContextService, + ) { void this.start(); } @@ -33,9 +33,10 @@ export class AnalyticsSender { logger: ILog, configuration: IConfiguration, commandExecutor: IVSCodeCommands, + contextService: IContextService, ): AnalyticsSender { if (!AnalyticsSender.instance) { - AnalyticsSender.instance = new AnalyticsSender(logger, configuration, commandExecutor); + AnalyticsSender.instance = new AnalyticsSender(logger, configuration, commandExecutor, contextService); } return AnalyticsSender.instance; } @@ -45,10 +46,14 @@ export class AnalyticsSender { while (true) { // eslint-disable-next-line no-await-in-loop const authToken = await this.configuration.getToken(); + const initialized: boolean = (this.contextService.viewContext[SNYK_CONTEXT.INITIALIZED] as boolean) ?? false; + const hasEvents = this.eventQueue.length > 0; + const authenticated = authToken && authToken.trim() !== ''; + const iAmTired = !(initialized && authenticated && hasEvents); - if (this.eventQueue.length === 0 || !authToken || authToken.trim() === '') { + if (iAmTired) { // eslint-disable-next-line no-await-in-loop - await sleep(1000); + await sleep(5000); continue; } @@ -56,10 +61,8 @@ export class AnalyticsSender { for (let i = 0; i < copyForSending.length; i++) { const eventPair = copyForSending[i]; try { - const args = []; - args.push(eventPair.event); // eslint-disable-next-line no-await-in-loop - await this.commandExecutor.executeCommand(SNYK_REPORT_ANALTYICS, args); + await this.commandExecutor.executeCommand(SNYK_REPORT_ANALYTICS, JSON.stringify(eventPair.event)); eventPair.callback(); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-base-to-string diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 91ab2ddce..066ebbb2f 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -30,7 +30,6 @@ import { } from '../constants/settings'; import SecretStorageAdapter from '../vscode/secretStorage'; import { IVSCodeWorkspace } from '../vscode/workspace'; -import { MEMENTO_LS_CHECKSUM } from '../constants/globalState'; const NEWISSUES = 'Net new issues'; diff --git a/src/snyk/common/constants/commands.ts b/src/snyk/common/constants/commands.ts index 37c6ffbf0..b49959089 100644 --- a/src/snyk/common/constants/commands.ts +++ b/src/snyk/common/constants/commands.ts @@ -30,7 +30,7 @@ export const SNYK_FEATURE_FLAG_COMMAND = 'snyk.getFeatureFlagStatus'; export const SNYK_CLEAR_CACHE_COMMAND = 'snyk.clearCache'; export const SNYK_CLEAR_PERSISTED_CACHE_COMMAND = 'snyk.clearPersistedCache'; export const SNYK_GENERATE_ISSUE_DESCRIPTION = 'snyk.generateIssueDescription'; -export const SNYK_REPORT_ANALTYICS = 'snyk.reportAnalytics'; +export const SNYK_REPORT_ANALYTICS = 'snyk.reportAnalytics'; // custom Snyk constants used in commands export const SNYK_CONTEXT_PREFIX = 'snyk:'; diff --git a/src/snyk/common/views/issueTreeProvider.ts b/src/snyk/common/views/issueTreeProvider.ts index f1084c960..0eb42114b 100644 --- a/src/snyk/common/views/issueTreeProvider.ts +++ b/src/snyk/common/views/issueTreeProvider.ts @@ -1,10 +1,10 @@ import _, { flatten } from 'lodash'; import * as vscode from 'vscode'; // todo: invert dependency import { IConfiguration, IssueViewOptions } from '../../common/configuration/configuration'; -import { Issue, IssueSeverity, ScanProduct, LsErrorMessage } from '../../common/languageServer/types'; +import { Issue, IssueSeverity, LsErrorMessage } from '../../common/languageServer/types'; import { messages as commonMessages } from '../../common/messages/analysisMessages'; import { IContextService } from '../../common/services/contextService'; -import { IProductService, ProductService } from '../../common/services/productService'; +import { IProductService } from '../../common/services/productService'; import { AnalysisTreeNodeProvider } from '../../common/views/analysisTreeNodeProvider'; import { INodeIcon, InternalType, NODE_ICONS, TreeNode } from '../../common/views/treeNode'; import { IVSCodeLanguages } from '../../common/vscode/languages'; diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index bb2f167ee..d4fedb230 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -39,7 +39,6 @@ import { } from './common/constants/views'; import { ErrorHandler } from './common/error/errorHandler'; import { ExperimentService } from './common/experiment/services/experimentService'; -import { LanguageServer } from './common/languageServer/languageServer'; import { StaticLsApi } from './common/languageServer/staticLsApi'; import { Logger } from './common/logger/logger'; import { DownloadService } from './common/services/downloadService'; @@ -81,8 +80,9 @@ import { ClearCacheService } from './common/services/CacheService'; import { InMemory, Persisted } from './common/constants/general'; import { GitAPI, GitExtension, Repository } from './common/git'; import { AnalyticsSender } from './common/analytics/AnalyticsSender'; -import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; import { MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT } from './common/constants/globalState'; +import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; +import { LanguageServer } from './common/languageServer/languageServer'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { @@ -422,17 +422,23 @@ class SnykExtension extends SnykLib implements IExtension { // The codeEnabled context depends on an LS command await this.languageServer.start(); + // initialize contexts + await this.contextService.setContext(SNYK_CONTEXT.INITIALIZED, true); + + // Fetch feature flag to determine whether to use the new LSP-based rendering. // feature flags depend on the language server this.featureFlagService = new FeatureFlagService(vsCodeCommands); await this.setupFeatureFlags(); - // Fetch feature flag to determine whether to use the new LSP-based rendering. + this.sendPluginInstalledEvent(); - // initialize contexts - await this.contextService.setContext(SNYK_CONTEXT.INITIALIZED, true); + // Actually start analysis + this.runScan(); + } + private sendPluginInstalledEvent() { // start analytics sender and send plugin installed event - const analyticsSender = AnalyticsSender.getInstance(Logger, configuration, this.commandController); + const analyticsSender = AnalyticsSender.getInstance(Logger, configuration, vsCodeCommands, this.contextService); const pluginInstalledSent = extensionContext.getGlobalStateValue(MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT) ?? false; @@ -445,9 +451,6 @@ class SnykExtension extends SnykLib implements IExtension { void extensionContext.updateGlobalStateValue(MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT, true); }); } - - // Actually start analysis - this.runScan(); } public async deactivate(): Promise { diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts index 1d4bb55e2..206a9d477 100644 --- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts +++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts @@ -93,8 +93,6 @@ export class CodeSuggestionWebviewProvider } async showPanel(issue: Issue): Promise { - const isIgnoresEnabled = configuration.getFeatureFlag(FEATURE_FLAGS.consistentIgnores); - try { await this.focusSecondEditorGroup(); if (this.panel) { @@ -120,7 +118,7 @@ export class CodeSuggestionWebviewProvider 'snyk-code.svg', ); // TODO: delete this when SNYK_GENERATE_ISSUE_DESCRIPTION command is in stable CLI. - let html: string = ''; + let html: string; if (issue.additionalData.details) { html = issue.additionalData.details; } else {