From d30e0611f602a3f483ecf883a443d635a6fca1d5 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Tue, 15 Apr 2025 14:47:07 +0530 Subject: [PATCH 1/5] updated telemetry prefs and reporter --- vscode/esbuild.js | 6 + vscode/package.json | 5 +- vscode/src/telemetry/config.ts | 11 +- vscode/src/telemetry/constants.ts | 3 + vscode/src/telemetry/impl/cacheServiceImpl.ts | 1 + .../src/telemetry/impl/telemetryEventQueue.ts | 17 +- vscode/src/telemetry/impl/telemetryPrefs.ts | 172 +++++++++++++++--- .../telemetry/impl/telemetryReporterImpl.ts | 13 +- vscode/src/telemetry/impl/telemetryRetry.ts | 24 ++- vscode/src/telemetry/telemetry.ts | 2 +- vscode/src/telemetry/telemetryManager.ts | 38 ++-- vscode/src/telemetry/types.ts | 14 +- 12 files changed, 246 insertions(+), 60 deletions(-) create mode 100644 vscode/src/telemetry/constants.ts diff --git a/vscode/esbuild.js b/vscode/esbuild.js index 80873aa..61db24a 100644 --- a/vscode/esbuild.js +++ b/vscode/esbuild.js @@ -57,6 +57,9 @@ const createTelemetryConfig = () => { baseUrl: null, baseEndpoint: "/vscode/java/sendTelemetry", version: "/v1" + }, + metadata: { + consentSchemaVersion: "v1" } } @@ -73,6 +76,9 @@ const createTelemetryConfig = () => { baseUrl: process.env.TELEMETRY_API_BASE_URL, baseEndpoint: process.env.TELEMETRY_API_ENDPOINT, version: process.env.TELEMETRY_API_VERSION + }, + metadata: { + consentSchemaVersion: process.env.CONSENT_SCHEMA_VERSION } }); diff --git a/vscode/package.json b/vscode/package.json index 230b8d4..0a9a7e7 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -245,7 +245,10 @@ "jdk.telemetry.enabled": { "type": "boolean", "description": "%jdk.configuration.telemetry.enabled.description%", - "default": false + "default": false, + "tags": [ + "telemetry" + ] } } }, diff --git a/vscode/src/telemetry/config.ts b/vscode/src/telemetry/config.ts index b087bae..eda1b56 100644 --- a/vscode/src/telemetry/config.ts +++ b/vscode/src/telemetry/config.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RetryConfig, TelemetryApi } from "./types"; +import { RetryConfig, TelemetryApi, TelemetryConfigMetadata } from "./types"; import * as path from 'path'; import * as fs from 'fs'; import { LOGGER } from "../logger"; @@ -24,6 +24,7 @@ export class TelemetryConfiguration { private static instance: TelemetryConfiguration; private retryConfig!: RetryConfig; private apiConfig!: TelemetryApi; + private metadata!: TelemetryConfigMetadata; public constructor() { this.initialize(); @@ -54,6 +55,11 @@ export class TelemetryConfiguration { baseEndpoint: config.telemetryApi.baseEndpoint, version: config.telemetryApi.version }); + + this.metadata = Object.freeze({ + consentSchemaVersion: config.metadata.consentSchemaVersion + }); + } catch (error: any) { LOGGER.error("Error occurred while setting up telemetry config"); LOGGER.error(error.message); @@ -68,4 +74,7 @@ export class TelemetryConfiguration { return this.apiConfig; } + public getTelemetryConfigMetadata(): Readonly { + return this.metadata; + } } \ No newline at end of file diff --git a/vscode/src/telemetry/constants.ts b/vscode/src/telemetry/constants.ts new file mode 100644 index 0000000..e90d441 --- /dev/null +++ b/vscode/src/telemetry/constants.ts @@ -0,0 +1,3 @@ +export const TELEMETRY_CONSENT_VERSION_SCHEMA_KEY = "telemetryConsentSchemaVersion"; +export const TELEMETRY_CONSENT_POPUP_TIME_KEY = "telemetryConsentPopupTime"; +export const TELEMETRY_SETTING_VALUE_KEY = "telemetrySettingValue"; \ No newline at end of file diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts index e7aaf6a..94c12d8 100644 --- a/vscode/src/telemetry/impl/cacheServiceImpl.ts +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -32,6 +32,7 @@ class CacheServiceImpl implements CacheService { try { const vscGlobalState = globalState.getExtensionContextInfo().getVscGlobalState(); vscGlobalState.update(key, value); + LOGGER.debug(`Updating key: ${key} to ${value}`); return true; } catch (err) { LOGGER.error(`Error while storing ${key} in cache: ${(err as Error).message}`); diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index af5fd7d..667e8f7 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -15,7 +15,7 @@ */ import { BaseEvent } from "../events/baseEvent"; -export class TelemetryEventQueue { +export class TelemetryEventQueue { private events: BaseEvent[] = []; public enqueue = (e: BaseEvent): void => { @@ -35,4 +35,19 @@ export class TelemetryEventQueue { this.events = []; return queue; } + + public decreaseSizeOnMaxOverflow = () => { + const seen = new Set(); + const newQueueStart = Math.floor(this.size() / 2); + + const secondHalf = this.events.slice(newQueueStart); + + const uniqueEvents = secondHalf.filter(event => { + if (seen.has(event.NAME)) return false; + seen.add(event.NAME); + return true; + }); + + this.events = [...uniqueEvents, ...secondHalf]; + } } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts index c7213f0..eae6c5f 100644 --- a/vscode/src/telemetry/impl/telemetryPrefs.ts +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -13,52 +13,166 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConfigurationChangeEvent, env, workspace } from "vscode"; +import { ConfigurationChangeEvent, env, workspace, Disposable } from "vscode"; import { getConfigurationValue, inspectConfiguration, updateConfigurationValue } from "../../configurations/handlers"; import { configKeys } from "../../configurations/configuration"; import { appendPrefixToCommand } from "../../utils"; +import { ExtensionContextInfo } from "../../extensionContextInfo"; +import { TelemetryPreference } from "../types"; +import { cacheService } from "./cacheServiceImpl"; +import { TELEMETRY_CONSENT_POPUP_TIME_KEY, TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TELEMETRY_SETTING_VALUE_KEY } from "../constants"; +import { TelemetryConfiguration } from "../config"; +import { LOGGER } from "../../logger"; -export class TelemetryPrefs { - public isExtTelemetryEnabled: boolean; +export class TelemetrySettings { + private isTelemetryEnabled: boolean; + private extensionPrefs: ExtensionTelemetryPreference; + private vscodePrefs: VscodeTelemetryPreference; + + constructor( + extensionContext: ExtensionContextInfo, + private onTelemetryEnableCallback: () => void, + private onTelemetryDisableCallback: () => void, + private triggerPopup: () => void) { + + this.extensionPrefs = new ExtensionTelemetryPreference(); + this.vscodePrefs = new VscodeTelemetryPreference(); + + extensionContext.pushSubscription( + this.extensionPrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) + ); + extensionContext.pushSubscription( + this.vscodePrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) + ); + + this.isTelemetryEnabled = this.checkTelemetryStatus(); + this.updateGlobalState(); + this.checkConsentVersion(); + } + + private checkTelemetryStatus = (): boolean => this.extensionPrefs.getIsTelemetryEnabled() && this.vscodePrefs.getIsTelemetryEnabled(); + + private onChangeTelemetrySettingCallback = () => { + const newTelemetryStatus = this.checkTelemetryStatus(); + if (newTelemetryStatus !== this.isTelemetryEnabled) { + this.isTelemetryEnabled = newTelemetryStatus; + cacheService.put(TELEMETRY_SETTING_VALUE_KEY, newTelemetryStatus.toString()); + + if (newTelemetryStatus) { + this.onTelemetryEnableCallback(); + } else { + this.onTelemetryDisableCallback(); + } + } else if (this.vscodePrefs.getIsTelemetryEnabled() && !this.extensionPrefs.isTelemetrySettingSet()) { + this.triggerPopup(); + } + } + + public getIsTelemetryEnabled = (): boolean => this.isTelemetryEnabled; + + public isConsentPopupToBeTriggered = (): boolean => { + const isExtensionSettingSet = this.extensionPrefs.isTelemetrySettingSet(); + const isVscodeSettingEnabled = this.vscodePrefs.getIsTelemetryEnabled(); + + const showPopup = !isExtensionSettingSet && isVscodeSettingEnabled; + + if (showPopup) { + cacheService.put(TELEMETRY_CONSENT_POPUP_TIME_KEY, Date.now().toString()); + } + + return showPopup; + } + + public updateTelemetrySetting = (value: boolean | undefined): void => { + this.extensionPrefs.updateTelemetryConfig(value); + } + + private updateGlobalState(): void { + const cachedValue = cacheService.get(TELEMETRY_SETTING_VALUE_KEY); + + if (this.isTelemetryEnabled.toString() !== cachedValue) { + cacheService.put(TELEMETRY_SETTING_VALUE_KEY, this.isTelemetryEnabled.toString()); + } + } + + private checkConsentVersion(): void { + const cachedVersion = cacheService.get(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY); + const currentVersion = TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion; + + if (cachedVersion !== currentVersion) { + cacheService.put(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, currentVersion); + LOGGER.debug("Removing telemetry config from user settings"); + if (this.extensionPrefs.isTelemetrySettingSet()) { + this.updateTelemetrySetting(undefined); + } + this.isTelemetryEnabled = false; + } + } +} + +class ExtensionTelemetryPreference implements TelemetryPreference { + private isTelemetryEnabled: boolean | undefined; + private readonly CONFIG = appendPrefixToCommand(configKeys.telemetryEnabled); constructor() { - this.isExtTelemetryEnabled = this.checkTelemetryStatus(); + this.isTelemetryEnabled = getConfigurationValue(configKeys.telemetryEnabled, false); } - private checkTelemetryStatus = (): boolean => { - return getConfigurationValue(configKeys.telemetryEnabled, false); + public getIsTelemetryEnabled = (): boolean => this.isTelemetryEnabled === undefined ? false : this.isTelemetryEnabled; + + public onChangeTelemetrySetting = (callback: () => void): Disposable => workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { + if (e.affectsConfiguration(this.CONFIG)) { + this.isTelemetryEnabled = getConfigurationValue(configKeys.telemetryEnabled, false); + callback(); + } + }); + + public updateTelemetryConfig = (value: boolean | undefined): void => { + this.isTelemetryEnabled = value; + updateConfigurationValue(configKeys.telemetryEnabled, value, true); } - private configPref = (configCommand: string): boolean => { - const config = inspectConfiguration(configCommand); + public isTelemetrySettingSet = (): boolean => { + if (this.isTelemetryEnabled === undefined) return false; + const config = inspectConfiguration(this.CONFIG); return ( - config?.workspaceFolderValue !== undefined || - config?.workspaceFolderLanguageValue !== undefined || - config?.workspaceValue !== undefined || - config?.workspaceLanguageValue !== undefined || config?.globalValue !== undefined || config?.globalLanguageValue !== undefined ); } +} - public isExtTelemetryConfigured = (): boolean => { - return this.configPref(appendPrefixToCommand(configKeys.telemetryEnabled)); - } +class VscodeTelemetryPreference implements TelemetryPreference { + private isTelemetryEnabled: boolean; - public updateTelemetryEnabledConfig = (value: boolean): void => { - this.isExtTelemetryEnabled = value; - updateConfigurationValue(configKeys.telemetryEnabled, value, true); + constructor() { + this.isTelemetryEnabled = env.isTelemetryEnabled; } - public didUserDisableVscodeTelemetry = (): boolean => { - return !env.isTelemetryEnabled; - } + public getIsTelemetryEnabled = (): boolean => this.isTelemetryEnabled; - public onDidChangeTelemetryEnabled = () => workspace.onDidChangeConfiguration( - (e: ConfigurationChangeEvent) => { - if (e.affectsConfiguration(appendPrefixToCommand(configKeys.telemetryEnabled))) { - this.isExtTelemetryEnabled = this.checkTelemetryStatus(); - } - } - ); -} \ No newline at end of file + public onChangeTelemetrySetting = (callback: () => void): Disposable => env.onDidChangeTelemetryEnabled((newSetting: boolean) => { + this.isTelemetryEnabled = newSetting; + callback(); + }); +} + +// Question: +// When consent version is changed, we have to show popup to all the users or only those who had accepted earlier? + +// Test cases: +// 1. User accepts consent and VSCode telemetry is set to 'all'. Output: enabled telemetry +// 2. User accepts consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 3. User rejects consent and VSCode telemetry is set to 'all'. Output: disabled telemetry +// 4. User rejects consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 5. User changes from accept to reject consent and VSCode telemetry is set to 'all'. Output: disabled telemetry +// 6. User changes from accept to reject consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 7. User changes from reject to accept consent and VSCode telemetry is set to 'all'. Output: enabled telemetry +// 8. User changes from reject to accept consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 9. User accepts consent and VSCode telemetry is changed from 'all' to 'error'. Output: disabled telemetry +// 10. User accepts consent and VSCode telemetry is changed from 'error' to 'all'. Output: enabled telemetry +// 11. When consent schema version updated, pop up should trigger again. +// 12. When consent schema version updated, pop up should trigger again, if closed without selecting any value and again reloading the screen, it should pop-up again.: Disabled telemetry in settings +// 13. When consent schema version updated, pop up should trigger again, if selected yes and again reloading the screen, it shouldn't pop-up again. Output: Enabled telemetry in settings +// 14. When consent schema version updated, pop up should trigger again, if selected no and again reloading the screen, it shouldn't pop-up again. Output: Disabled telemetry in settings +// 15. When VSCode setting is changed from reject to accept, our pop-up should come. diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index ac78297..20af65d 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -46,7 +46,6 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public closeEvent = (): void => { - const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); this.addEventToQueue(extensionCloseEvent); @@ -59,6 +58,10 @@ export class TelemetryReporterImpl implements TelemetryReporter { this.queue.enqueue(event); if (this.retryManager.isQueueOverflow(this.queue.size())) { LOGGER.debug(`Send triggered to queue size overflow`); + if(this.retryManager.IsMaxRetryReached()){ + LOGGER.debug('Decreasing size of the queue'); + this.queue.decreaseSizeOnMaxOverflow(); + } this.sendEvents(); } } @@ -81,9 +84,9 @@ export class TelemetryReporterImpl implements TelemetryReporter { LOGGER.debug(`Number of events successfully sent: ${response.success.length}`); LOGGER.debug(`Number of events failed to send: ${response.failures.length}`); - this.handlePostTelemetryResponse(response); + const isResetRetryParams = this.handlePostTelemetryResponse(response); - this.retryManager.startTimer(); + this.retryManager.startTimer(isResetRetryParams); } catch (err: any) { this.disableReporter = true; LOGGER.debug(`Error while sending telemetry: ${isError(err) ? err.message : err}`); @@ -98,11 +101,13 @@ export class TelemetryReporterImpl implements TelemetryReporter { return [...removedJdkFeatureEvents, ...concatedEvents]; } - private handlePostTelemetryResponse = (response: TelemetryPostResponse) => { + private handlePostTelemetryResponse = (response: TelemetryPostResponse): boolean => { const eventsToBeEnqueued = this.retryManager.eventsToBeEnqueuedAgain(response); this.queue.concatQueue(eventsToBeEnqueued); LOGGER.debug(`Number of failed events enqueuing again: ${eventsToBeEnqueued.length}`); + + return eventsToBeEnqueued.length === 0; } } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 0ee6302..d2f904c 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -33,7 +33,7 @@ export class TelemetryRetry { this.callbackHandler = callbackHandler; } - public startTimer = (): void => { + public startTimer = (resetParameters: boolean = true): void => { if (!this.callbackHandler) { LOGGER.debug("Callback handler is not set for telemetry retry mechanism"); return; @@ -41,10 +41,16 @@ export class TelemetryRetry { if (this.timeout) { LOGGER.debug("Overriding current timeout"); } + if(resetParameters){ + this.resetTimerParameters(); + this.resetQueueCapacity(); + } this.timeout = setInterval(this.callbackHandler, this.timePeriod); } private resetTimerParameters = () => { + LOGGER.debug("Resetting time period to default"); + this.numOfAttemptsWhenTimerHits = 1; this.timePeriod = this.TELEMETRY_RETRY_CONFIG.baseTimer; this.clearTimer(); @@ -56,7 +62,7 @@ export class TelemetryRetry { this.numOfAttemptsWhenTimerHits++; return; } - throw new Error("Number of retries exceeded"); + LOGGER.debug("Keeping timer same as max retries exceeded"); } public clearTimer = (): void => { @@ -82,10 +88,16 @@ export class TelemetryRetry { this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity * Math.pow(this.TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); } - throw new Error("Number of retries exceeded"); + LOGGER.debug("Keeping queue capacity same as max retries exceeded"); } + public IsMaxRetryReached = (): boolean => + this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries || + this.numOfAttemptsWhenTimerHits > this.TELEMETRY_RETRY_CONFIG.maxRetries + private resetQueueCapacity = (): void => { + LOGGER.debug("Resetting queue capacity to default"); + this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity; this.numOfAttemptsWhenQueueIsFull = 1; this.triggeredDueToQueueOverflow = false; @@ -108,10 +120,7 @@ export class TelemetryRetry { res.event.onSuccessPostEventCallback(); }); - if (eventResponses.failures.length === 0) { - this.resetQueueCapacity(); - this.resetTimerParameters(); - } else { + if (eventResponses.failures.length) { const eventsToBeEnqueuedAgain: BaseEvent[] = []; eventResponses.failures.forEach((eventRes) => { if (this.isEventRetryable(eventRes.statusCode)) @@ -119,6 +128,7 @@ export class TelemetryRetry { }); if (eventsToBeEnqueuedAgain.length) { + this.triggeredDueToQueueOverflow ? this.increaseQueueCapacity() : this.increaseTimePeriod(); diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts index 4e61d65..116cdda 100644 --- a/vscode/src/telemetry/telemetry.ts +++ b/vscode/src/telemetry/telemetry.ts @@ -43,7 +43,7 @@ export namespace Telemetry { } const enqueueEvent = (cbFunction: (reporter: TelemetryReporter) => void) => { - if (telemetryManager.isExtTelemetryEnabled() && getIsTelemetryFeatureAvailable()) { + if (telemetryManager.isTelemetryEnabled() && getIsTelemetryFeatureAvailable()) { const reporter = telemetryManager.getReporter(); if (reporter) { cbFunction(reporter); diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index eb1c40e..e18dff4 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -14,7 +14,7 @@ limitations under the License. */ import { window } from "vscode"; -import { TelemetryPrefs } from "./impl/telemetryPrefs"; +import { TelemetrySettings } from "./impl/telemetryPrefs"; import { TelemetryEventQueue } from "./impl/telemetryEventQueue"; import { TelemetryReporterImpl } from "./impl/telemetryReporterImpl"; import { TelemetryReporter } from "./types"; @@ -25,24 +25,27 @@ import { TelemetryRetry } from "./impl/telemetryRetry"; export class TelemetryManager { private extensionContextInfo: ExtensionContextInfo; - private settings: TelemetryPrefs = new TelemetryPrefs(); + private settings: TelemetrySettings; private reporter?: TelemetryReporter; private telemetryRetryManager: TelemetryRetry = new TelemetryRetry() constructor(extensionContextInfo: ExtensionContextInfo) { this.extensionContextInfo = extensionContextInfo; + this.settings = new TelemetrySettings(extensionContextInfo, + this.onTelemetryEnable, + this.onTelemetryDisable, + this.openTelemetryDialog); } - public isExtTelemetryEnabled = (): boolean => { - return this.settings.isExtTelemetryEnabled; + public isTelemetryEnabled = (): boolean => { + return this.settings.getIsTelemetryEnabled(); } public initializeReporter = (): void => { const queue = new TelemetryEventQueue(); - this.extensionContextInfo.pushSubscription(this.settings.onDidChangeTelemetryEnabled()); this.reporter = new TelemetryReporterImpl(queue, this.telemetryRetryManager); - this.openTelemetryDialog(); + this.isTelemetryEnabled() ? this.onTelemetryEnable() : this.openTelemetryDialog(); } public getReporter = (): TelemetryReporter | null => { @@ -50,7 +53,7 @@ export class TelemetryManager { } private openTelemetryDialog = async () => { - if (!this.settings.isExtTelemetryConfigured() && !this.settings.didUserDisableVscodeTelemetry()) { + if (this.settings?.isConsentPopupToBeTriggered()) { LOGGER.log('Telemetry not enabled yet'); const yesLabel = l10n.value("jdk.downloader.message.confirmation.yes"); @@ -62,14 +65,19 @@ export class TelemetryManager { return; } - this.settings.updateTelemetryEnabledConfig(enable === yesLabel); - if (enable === yesLabel) { - LOGGER.log("Telemetry is now enabled"); - } - } - if (this.settings.isExtTelemetryEnabled) { - this.telemetryRetryManager.startTimer(); - this.reporter?.startEvent(); + this.settings.updateTelemetrySetting(enable === yesLabel); } } + + private onTelemetryEnable = () => { + LOGGER.log("Telemetry is now enabled"); + this.telemetryRetryManager.startTimer(); + this.reporter?.startEvent(); + } + + private onTelemetryDisable = () => { + // Remaining: Check what needs to be done when disabled + LOGGER.log("Telemetry is now disabled"); + this.telemetryRetryManager.clearTimer(); + } }; \ No newline at end of file diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts index 30a663f..18bc6e2 100644 --- a/vscode/src/telemetry/types.ts +++ b/vscode/src/telemetry/types.ts @@ -14,6 +14,7 @@ limitations under the License. */ import { BaseEvent } from "./events/baseEvent"; +import { Disposable } from "vscode"; export interface TelemetryReporter { startEvent(): void; @@ -48,4 +49,15 @@ export interface TelemetryApi { baseUrl: string | null; baseEndpoint: string; version: string; -} \ No newline at end of file +} + +export interface TelemetryConfigMetadata { + consentSchemaVersion: string; +} + +export interface TelemetryPreference { + getIsTelemetryEnabled(): boolean; + onChangeTelemetrySetting(cb: () => void): Disposable; + updateTelemetryConfig?(value: boolean): void; + isTelemetrySettingSet?: () => boolean; +} From efab04db21d6e282617c7e4ce167710ead5dd0f2 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Wed, 16 Apr 2025 15:35:45 +0530 Subject: [PATCH 2/5] added telemetry disable strategy --- .../telemetry/impl/telemetryReporterImpl.ts | 70 ++++++++++++++----- vscode/src/telemetry/impl/telemetryRetry.ts | 7 +- vscode/src/telemetry/telemetryManager.ts | 4 +- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index 20af65d..f302ff7 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -27,8 +27,9 @@ import { PostTelemetry, TelemetryPostResponse } from "./postTelemetry"; export class TelemetryReporterImpl implements TelemetryReporter { private activationTime: number = getCurrentUTCDateInSeconds(); - private disableReporter: boolean = false; private postTelemetry: PostTelemetry = new PostTelemetry(); + private onCloseEventState: { status: boolean, numOfRetries: number } = { status: false, numOfRetries: 0 }; + private readonly MAX_RETRY_ON_CLOSE = 5; constructor( private queue: TelemetryEventQueue, @@ -38,14 +39,22 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public startEvent = (): void => { + this.resetOnCloseEventState(); + this.retryManager.startTimer(); + const extensionStartEvent = ExtensionStartEvent.builder(); - if(extensionStartEvent != null){ + if (extensionStartEvent != null) { this.addEventToQueue(extensionStartEvent); LOGGER.debug(`Start event enqueued: ${extensionStartEvent.getPayload}`); - } + } } public closeEvent = (): void => { + this.onCloseEventState = { + status: true, + numOfRetries: 0 + }; + const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); this.addEventToQueue(extensionCloseEvent); @@ -54,22 +63,46 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public addEventToQueue = (event: BaseEvent): void => { - if (!this.disableReporter) { - this.queue.enqueue(event); - if (this.retryManager.isQueueOverflow(this.queue.size())) { - LOGGER.debug(`Send triggered to queue size overflow`); - if(this.retryManager.IsMaxRetryReached()){ - LOGGER.debug('Decreasing size of the queue'); - this.queue.decreaseSizeOnMaxOverflow(); - } - this.sendEvents(); + this.resetOnCloseEventState(); + + this.queue.enqueue(event); + if (this.retryManager.isQueueOverflow(this.queue.size())) { + LOGGER.debug(`Send triggered to queue size overflow`); + if (this.retryManager.IsQueueMaxCapacityReached()) { + LOGGER.debug('Decreasing size of the queue as max capacity reached'); + this.queue.decreaseSizeOnMaxOverflow(); + } + this.sendEvents(); + } + } + + private resetOnCloseEventState = () => { + this.onCloseEventState = { + status: false, + numOfRetries: 0 + }; + } + + private increaseRetryCountOrDisableRetry = () => { + if (this.onCloseEventState.status) { + if (this.onCloseEventState.numOfRetries < this.MAX_RETRY_ON_CLOSE && this.queue.size()) { + LOGGER.debug("Telemetry disabled state: Increasing retry count"); + this.onCloseEventState.numOfRetries++; + } else { + LOGGER.debug(`Telemetry disabled state: ${this.queue.size() ? 'Max retries reached': 'queue is empty'}, resetting timer`); + this.retryManager.clearTimer(); + this.queue.flush(); + this.onCloseEventState = { + status: false, + numOfRetries: 0 + }; } } } private sendEvents = async (): Promise => { try { - if(!this.queue.size()){ + if (!this.queue.size()) { LOGGER.debug(`Queue is empty nothing to send`); return; } @@ -84,20 +117,21 @@ export class TelemetryReporterImpl implements TelemetryReporter { LOGGER.debug(`Number of events successfully sent: ${response.success.length}`); LOGGER.debug(`Number of events failed to send: ${response.failures.length}`); - const isResetRetryParams = this.handlePostTelemetryResponse(response); + const isAllEventsSuccess = this.handlePostTelemetryResponse(response); - this.retryManager.startTimer(isResetRetryParams); + this.retryManager.startTimer(isAllEventsSuccess); + + this.increaseRetryCountOrDisableRetry(); } catch (err: any) { - this.disableReporter = true; LOGGER.debug(`Error while sending telemetry: ${isError(err) ? err.message : err}`); } } - + private transformEvents = (events: BaseEvent[]): BaseEvent[] => { const jdkFeatureEvents = events.filter(event => event.NAME === JdkFeatureEvent.NAME); const concatedEvents = JdkFeatureEvent.concatEvents(jdkFeatureEvents); const removedJdkFeatureEvents = events.filter(event => event.NAME !== JdkFeatureEvent.NAME); - + return [...removedJdkFeatureEvents, ...concatedEvents]; } diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index d2f904c..5147e85 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -57,7 +57,7 @@ export class TelemetryRetry { } private increaseTimePeriod = (): void => { - if (this.numOfAttemptsWhenTimerHits <= this.TELEMETRY_RETRY_CONFIG.maxRetries) { + if (this.numOfAttemptsWhenTimerHits < this.TELEMETRY_RETRY_CONFIG.maxRetries) { this.timePeriod = this.calculateDelay(); this.numOfAttemptsWhenTimerHits++; return; @@ -91,9 +91,8 @@ export class TelemetryRetry { LOGGER.debug("Keeping queue capacity same as max retries exceeded"); } - public IsMaxRetryReached = (): boolean => - this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries || - this.numOfAttemptsWhenTimerHits > this.TELEMETRY_RETRY_CONFIG.maxRetries + public IsQueueMaxCapacityReached = (): boolean => + this.numOfAttemptsWhenQueueIsFull > this.TELEMETRY_RETRY_CONFIG.maxRetries; private resetQueueCapacity = (): void => { LOGGER.debug("Resetting queue capacity to default"); diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index e18dff4..7fdaceb 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -71,13 +71,11 @@ export class TelemetryManager { private onTelemetryEnable = () => { LOGGER.log("Telemetry is now enabled"); - this.telemetryRetryManager.startTimer(); this.reporter?.startEvent(); } private onTelemetryDisable = () => { - // Remaining: Check what needs to be done when disabled LOGGER.log("Telemetry is now disabled"); - this.telemetryRetryManager.clearTimer(); + this.reporter?.closeEvent(); } }; \ No newline at end of file From 2c20449e367eb093c89887d80d2d61bd6fd7e64c Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Thu, 17 Apr 2025 14:31:49 +0530 Subject: [PATCH 3/5] Improved retry mechanism, closeEvent handling and fixed some bugs --- vscode/src/telemetry/constants.ts | 2 +- vscode/src/telemetry/events/baseEvent.ts | 8 +-- vscode/src/telemetry/impl/cacheServiceImpl.ts | 4 +- .../src/telemetry/impl/telemetryEventQueue.ts | 34 ++++++---- vscode/src/telemetry/impl/telemetryPrefs.ts | 63 +++++++++---------- .../telemetry/impl/telemetryReporterImpl.ts | 62 +++++++++--------- vscode/src/telemetry/impl/telemetryRetry.ts | 9 +-- vscode/src/telemetry/types.ts | 2 +- 8 files changed, 94 insertions(+), 90 deletions(-) diff --git a/vscode/src/telemetry/constants.ts b/vscode/src/telemetry/constants.ts index e90d441..a076338 100644 --- a/vscode/src/telemetry/constants.ts +++ b/vscode/src/telemetry/constants.ts @@ -1,3 +1,3 @@ export const TELEMETRY_CONSENT_VERSION_SCHEMA_KEY = "telemetryConsentSchemaVersion"; -export const TELEMETRY_CONSENT_POPUP_TIME_KEY = "telemetryConsentPopupTime"; +export const TELEMETRY_CONSENT_RESPONSE_TIME_KEY = "telemetryConsentResponseTime"; export const TELEMETRY_SETTING_VALUE_KEY = "telemetrySettingValue"; \ No newline at end of file diff --git a/vscode/src/telemetry/events/baseEvent.ts b/vscode/src/telemetry/events/baseEvent.ts index 87a2087..be32f71 100644 --- a/vscode/src/telemetry/events/baseEvent.ts +++ b/vscode/src/telemetry/events/baseEvent.ts @@ -42,7 +42,7 @@ export abstract class BaseEvent { get getPayload(): T & BaseEventPayload { return this._payload; } - + get getData(): T { return this._data; } @@ -58,8 +58,8 @@ export abstract class BaseEvent { protected addEventToCache = (): void => { const dataString = JSON.stringify(this.getData); const calculatedHashVal = getHashCode(dataString); - const isAdded = cacheService.put(this.NAME, calculatedHashVal); - - LOGGER.debug(`${this.NAME} added in cache ${isAdded ? "Successfully" : "Unsucessfully"}`); + cacheService.put(this.NAME, calculatedHashVal).then((isAdded: boolean) => { + LOGGER.debug(`${this.NAME} added in cache ${isAdded ? "Successfully" : "Unsucessfully"}`); + }); } } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts index 94c12d8..0d7b758 100644 --- a/vscode/src/telemetry/impl/cacheServiceImpl.ts +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -28,10 +28,10 @@ class CacheServiceImpl implements CacheService { } } - public put = (key: string, value: string): boolean => { + public put = async (key: string, value: string): Promise => { try { const vscGlobalState = globalState.getExtensionContextInfo().getVscGlobalState(); - vscGlobalState.update(key, value); + await vscGlobalState.update(key, value); LOGGER.debug(`Updating key: ${key} to ${value}`); return true; } catch (err) { diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index 667e8f7..0bec18b 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { LOGGER } from "../../logger"; import { BaseEvent } from "../events/baseEvent"; export class TelemetryEventQueue { @@ -36,18 +37,25 @@ export class TelemetryEventQueue { return queue; } - public decreaseSizeOnMaxOverflow = () => { - const seen = new Set(); - const newQueueStart = Math.floor(this.size() / 2); - - const secondHalf = this.events.slice(newQueueStart); - - const uniqueEvents = secondHalf.filter(event => { - if (seen.has(event.NAME)) return false; - seen.add(event.NAME); - return true; - }); - - this.events = [...uniqueEvents, ...secondHalf]; + public decreaseSizeOnMaxOverflow = (maxNumberOfEventsToBeKept: number) => { + const excess = this.size() - maxNumberOfEventsToBeKept; + + if (excess > 0) { + LOGGER.debug('Decreasing size of the queue as max capacity reached'); + + const seen = new Set(); + const deduplicated = []; + + for (let i = 0; i < excess; i++) { + const event = this.events[i]; + if (!seen.has(event.NAME)) { + deduplicated.push(event); + seen.add(event.NAME); + } + } + + this.events = [...deduplicated, ...this.events.slice(excess)]; + } } + } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts index eae6c5f..2c6893d 100644 --- a/vscode/src/telemetry/impl/telemetryPrefs.ts +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -20,7 +20,7 @@ import { appendPrefixToCommand } from "../../utils"; import { ExtensionContextInfo } from "../../extensionContextInfo"; import { TelemetryPreference } from "../types"; import { cacheService } from "./cacheServiceImpl"; -import { TELEMETRY_CONSENT_POPUP_TIME_KEY, TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TELEMETRY_SETTING_VALUE_KEY } from "../constants"; +import { TELEMETRY_CONSENT_RESPONSE_TIME_KEY, TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TELEMETRY_SETTING_VALUE_KEY } from "../constants"; import { TelemetryConfiguration } from "../config"; import { LOGGER } from "../../logger"; @@ -37,17 +37,11 @@ export class TelemetrySettings { this.extensionPrefs = new ExtensionTelemetryPreference(); this.vscodePrefs = new VscodeTelemetryPreference(); - - extensionContext.pushSubscription( - this.extensionPrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) - ); - extensionContext.pushSubscription( - this.vscodePrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) - ); - + extensionContext.pushSubscription(this.extensionPrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback)); + extensionContext.pushSubscription(this.vscodePrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback)); + this.isTelemetryEnabled = this.checkTelemetryStatus(); - this.updateGlobalState(); - this.checkConsentVersion(); + this.syncTelemetrySettingGlobalState(); } private checkTelemetryStatus = (): boolean => this.extensionPrefs.getIsTelemetryEnabled() && this.vscodePrefs.getIsTelemetryEnabled(); @@ -56,14 +50,16 @@ export class TelemetrySettings { const newTelemetryStatus = this.checkTelemetryStatus(); if (newTelemetryStatus !== this.isTelemetryEnabled) { this.isTelemetryEnabled = newTelemetryStatus; - cacheService.put(TELEMETRY_SETTING_VALUE_KEY, newTelemetryStatus.toString()); + this.updateGlobalStates(); if (newTelemetryStatus) { this.onTelemetryEnableCallback(); } else { this.onTelemetryDisableCallback(); } - } else if (this.vscodePrefs.getIsTelemetryEnabled() && !this.extensionPrefs.isTelemetrySettingSet()) { + } else if (this.vscodePrefs.getIsTelemetryEnabled() + && !this.extensionPrefs.isTelemetrySettingSet() + && !cacheService.get(TELEMETRY_CONSENT_RESPONSE_TIME_KEY)) { this.triggerPopup(); } } @@ -74,38 +70,38 @@ export class TelemetrySettings { const isExtensionSettingSet = this.extensionPrefs.isTelemetrySettingSet(); const isVscodeSettingEnabled = this.vscodePrefs.getIsTelemetryEnabled(); - const showPopup = !isExtensionSettingSet && isVscodeSettingEnabled; - - if (showPopup) { - cacheService.put(TELEMETRY_CONSENT_POPUP_TIME_KEY, Date.now().toString()); - } - - return showPopup; + return !isExtensionSettingSet && isVscodeSettingEnabled; } public updateTelemetrySetting = (value: boolean | undefined): void => { this.extensionPrefs.updateTelemetryConfig(value); } - private updateGlobalState(): void { - const cachedValue = cacheService.get(TELEMETRY_SETTING_VALUE_KEY); + private syncTelemetrySettingGlobalState (): void { + const cachedSettingValue = cacheService.get(TELEMETRY_SETTING_VALUE_KEY); + const cachedConsentSchemaVersion = cacheService.get(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY); - if (this.isTelemetryEnabled.toString() !== cachedValue) { - cacheService.put(TELEMETRY_SETTING_VALUE_KEY, this.isTelemetryEnabled.toString()); + if (this.isTelemetryEnabled.toString() !== cachedSettingValue) { + this.updateGlobalStates(); } + this.checkConsentVersionSchemaGlobalState(cachedConsentSchemaVersion); } - private checkConsentVersion(): void { - const cachedVersion = cacheService.get(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY); - const currentVersion = TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion; + private updateGlobalStates(): void { + cacheService.put(TELEMETRY_CONSENT_RESPONSE_TIME_KEY, Date.now().toString()); + cacheService.put(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion); + cacheService.put(TELEMETRY_SETTING_VALUE_KEY, this.isTelemetryEnabled.toString()); + } - if (cachedVersion !== currentVersion) { - cacheService.put(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, currentVersion); - LOGGER.debug("Removing telemetry config from user settings"); - if (this.extensionPrefs.isTelemetrySettingSet()) { + private checkConsentVersionSchemaGlobalState(consentSchemaVersion: string | undefined): void { + if (this.extensionPrefs.isTelemetrySettingSet()) { + const currentExtConsentSchemaVersion = TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion; + + if (consentSchemaVersion !== currentExtConsentSchemaVersion) { + LOGGER.debug("Removing telemetry config from user settings due to consent schema version change"); + this.isTelemetryEnabled = false; this.updateTelemetrySetting(undefined); } - this.isTelemetryEnabled = false; } } } @@ -157,9 +153,6 @@ class VscodeTelemetryPreference implements TelemetryPreference { }); } -// Question: -// When consent version is changed, we have to show popup to all the users or only those who had accepted earlier? - // Test cases: // 1. User accepts consent and VSCode telemetry is set to 'all'. Output: enabled telemetry // 2. User accepts consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index f302ff7..6e8a5b3 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -39,7 +39,7 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public startEvent = (): void => { - this.resetOnCloseEventState(); + this.setOnCloseEventState(); this.retryManager.startTimer(); const extensionStartEvent = ExtensionStartEvent.builder(); @@ -50,11 +50,6 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public closeEvent = (): void => { - this.onCloseEventState = { - status: true, - numOfRetries: 0 - }; - const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); this.addEventToQueue(extensionCloseEvent); @@ -63,42 +58,49 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public addEventToQueue = (event: BaseEvent): void => { - this.resetOnCloseEventState(); + this.setOnCloseEventState(event); this.queue.enqueue(event); if (this.retryManager.isQueueOverflow(this.queue.size())) { LOGGER.debug(`Send triggered to queue size overflow`); - if (this.retryManager.IsQueueMaxCapacityReached()) { - LOGGER.debug('Decreasing size of the queue as max capacity reached'); - this.queue.decreaseSizeOnMaxOverflow(); - } + const numOfeventsToBeDropped = this.retryManager.getNumberOfEventsToBeDropped(); this.sendEvents(); + if (numOfeventsToBeDropped) { + this.queue.decreaseSizeOnMaxOverflow(numOfeventsToBeDropped); + } } } - private resetOnCloseEventState = () => { - this.onCloseEventState = { - status: false, - numOfRetries: 0 - }; + private setOnCloseEventState = (event?: BaseEvent) => { + if (event?.NAME === ExtensionCloseEvent.NAME) { + this.onCloseEventState = { + status: true, + numOfRetries: 0 + }; + } else { + this.onCloseEventState = { + status: false, + numOfRetries: 0 + }; + } } private increaseRetryCountOrDisableRetry = () => { - if (this.onCloseEventState.status) { - if (this.onCloseEventState.numOfRetries < this.MAX_RETRY_ON_CLOSE && this.queue.size()) { - LOGGER.debug("Telemetry disabled state: Increasing retry count"); - this.onCloseEventState.numOfRetries++; - } else { - LOGGER.debug(`Telemetry disabled state: ${this.queue.size() ? 'Max retries reached': 'queue is empty'}, resetting timer`); - this.retryManager.clearTimer(); - this.queue.flush(); - this.onCloseEventState = { - status: false, - numOfRetries: 0 - }; - } + if (!this.onCloseEventState.status) return; + + const queueEmpty = this.queue.size() === 0; + const retriesExceeded = this.onCloseEventState.numOfRetries >= this.MAX_RETRY_ON_CLOSE; + + if (queueEmpty || retriesExceeded) { + LOGGER.debug(`Telemetry disabled state: ${queueEmpty ? 'Queue is empty' : 'Max retries reached'}, clearing timer`); + this.retryManager.clearTimer(); + this.queue.flush(); + this.setOnCloseEventState(); + } else { + LOGGER.debug("Telemetry disabled state: Increasing retry count"); + this.onCloseEventState.numOfRetries++; } - } + }; private sendEvents = async (): Promise => { try { diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 5147e85..bb87153 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -62,7 +62,7 @@ export class TelemetryRetry { this.numOfAttemptsWhenTimerHits++; return; } - LOGGER.debug("Keeping timer same as max retries exceeded"); + LOGGER.debug("Keeping timer same as max capactiy reached"); } public clearTimer = (): void => { @@ -88,11 +88,12 @@ export class TelemetryRetry { this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity * Math.pow(this.TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); } - LOGGER.debug("Keeping queue capacity same as max retries exceeded"); + LOGGER.debug("Keeping queue capacity same as max capacity reached"); } - public IsQueueMaxCapacityReached = (): boolean => - this.numOfAttemptsWhenQueueIsFull > this.TELEMETRY_RETRY_CONFIG.maxRetries; + public getNumberOfEventsToBeDropped = (): number => + this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries ? + this.queueCapacity/2 : 0; private resetQueueCapacity = (): void => { LOGGER.debug("Resetting queue capacity to default"); diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts index 18bc6e2..d04059f 100644 --- a/vscode/src/telemetry/types.ts +++ b/vscode/src/telemetry/types.ts @@ -27,7 +27,7 @@ export interface TelemetryReporter { export interface CacheService { get(key: string): string | undefined; - put(key: string, value: string): boolean; + put(key: string, value: string): Promise; } export interface TelemetryEventQueue { From 7aa08bba156d74e388d4acae0784dcf879f97caa Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Thu, 17 Apr 2025 14:34:39 +0530 Subject: [PATCH 4/5] updated license headers --- vscode/src/telemetry/config.ts | 2 +- vscode/src/telemetry/constants.ts | 15 +++++++++++++++ vscode/src/telemetry/events/baseEvent.ts | 2 +- vscode/src/telemetry/events/close.ts | 2 +- vscode/src/telemetry/events/jdkDownload.ts | 2 +- vscode/src/telemetry/events/jdkFeature.ts | 2 +- vscode/src/telemetry/events/start.ts | 2 +- vscode/src/telemetry/events/workspaceChange.ts | 2 +- vscode/src/telemetry/impl/AnonymousIdManager.ts | 2 +- vscode/src/telemetry/impl/cacheServiceImpl.ts | 2 +- vscode/src/telemetry/impl/enviromentDetails.ts | 2 +- vscode/src/telemetry/impl/postTelemetry.ts | 2 +- vscode/src/telemetry/impl/telemetryEventQueue.ts | 2 +- vscode/src/telemetry/impl/telemetryPrefs.ts | 2 +- .../src/telemetry/impl/telemetryReporterImpl.ts | 2 +- vscode/src/telemetry/impl/telemetryRetry.ts | 2 +- vscode/src/telemetry/telemetry.ts | 2 +- vscode/src/telemetry/telemetryManager.ts | 2 +- vscode/src/telemetry/types.ts | 2 +- vscode/src/telemetry/utils.ts | 2 +- 20 files changed, 34 insertions(+), 19 deletions(-) diff --git a/vscode/src/telemetry/config.ts b/vscode/src/telemetry/config.ts index eda1b56..f18a171 100644 --- a/vscode/src/telemetry/config.ts +++ b/vscode/src/telemetry/config.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/constants.ts b/vscode/src/telemetry/constants.ts index a076338..f21dbf0 100644 --- a/vscode/src/telemetry/constants.ts +++ b/vscode/src/telemetry/constants.ts @@ -1,3 +1,18 @@ +/* + Copyright (c) 2024-2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ export const TELEMETRY_CONSENT_VERSION_SCHEMA_KEY = "telemetryConsentSchemaVersion"; export const TELEMETRY_CONSENT_RESPONSE_TIME_KEY = "telemetryConsentResponseTime"; export const TELEMETRY_SETTING_VALUE_KEY = "telemetrySettingValue"; \ No newline at end of file diff --git a/vscode/src/telemetry/events/baseEvent.ts b/vscode/src/telemetry/events/baseEvent.ts index be32f71..224dd2a 100644 --- a/vscode/src/telemetry/events/baseEvent.ts +++ b/vscode/src/telemetry/events/baseEvent.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/close.ts b/vscode/src/telemetry/events/close.ts index 3e9d394..0f5abc3 100644 --- a/vscode/src/telemetry/events/close.ts +++ b/vscode/src/telemetry/events/close.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/jdkDownload.ts b/vscode/src/telemetry/events/jdkDownload.ts index f227b12..b6eb158 100644 --- a/vscode/src/telemetry/events/jdkDownload.ts +++ b/vscode/src/telemetry/events/jdkDownload.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/jdkFeature.ts b/vscode/src/telemetry/events/jdkFeature.ts index 24c8e97..9d89226 100644 --- a/vscode/src/telemetry/events/jdkFeature.ts +++ b/vscode/src/telemetry/events/jdkFeature.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/start.ts b/vscode/src/telemetry/events/start.ts index 4ae74ff..9334064 100644 --- a/vscode/src/telemetry/events/start.ts +++ b/vscode/src/telemetry/events/start.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/workspaceChange.ts b/vscode/src/telemetry/events/workspaceChange.ts index 7d57760..23f3b43 100644 --- a/vscode/src/telemetry/events/workspaceChange.ts +++ b/vscode/src/telemetry/events/workspaceChange.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/AnonymousIdManager.ts b/vscode/src/telemetry/impl/AnonymousIdManager.ts index d1186d6..092475c 100644 --- a/vscode/src/telemetry/impl/AnonymousIdManager.ts +++ b/vscode/src/telemetry/impl/AnonymousIdManager.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts index 0d7b758..8374687 100644 --- a/vscode/src/telemetry/impl/cacheServiceImpl.ts +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/enviromentDetails.ts b/vscode/src/telemetry/impl/enviromentDetails.ts index 2b6d427..c14d267 100644 --- a/vscode/src/telemetry/impl/enviromentDetails.ts +++ b/vscode/src/telemetry/impl/enviromentDetails.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/postTelemetry.ts b/vscode/src/telemetry/impl/postTelemetry.ts index 1385a91..2fbe8b5 100644 --- a/vscode/src/telemetry/impl/postTelemetry.ts +++ b/vscode/src/telemetry/impl/postTelemetry.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index 0bec18b..fced412 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts index 2c6893d..620a677 100644 --- a/vscode/src/telemetry/impl/telemetryPrefs.ts +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index 6e8a5b3..1b696b6 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index bb87153..22faee0 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts index 116cdda..fc55d4c 100644 --- a/vscode/src/telemetry/telemetry.ts +++ b/vscode/src/telemetry/telemetry.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index 7fdaceb..076801c 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts index d04059f..bb273d2 100644 --- a/vscode/src/telemetry/types.ts +++ b/vscode/src/telemetry/types.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/utils.ts b/vscode/src/telemetry/utils.ts index 7b69256..fcf1a0f 100644 --- a/vscode/src/telemetry/utils.ts +++ b/vscode/src/telemetry/utils.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 13182aaf357e922bc263c3525b38dc6c7a8b480b Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Fri, 18 Apr 2025 11:23:41 +0530 Subject: [PATCH 5/5] added some unit test --- .../src/telemetry/impl/telemetryEventQueue.ts | 4 +- .../telemetry/impl/telemetryReporterImpl.ts | 6 +- vscode/src/telemetry/impl/telemetryRetry.ts | 4 +- .../unit/telemetry/cacheService.unit.test.ts | 86 ++++++++ .../test/unit/telemetry/queue.unit.test.ts | 184 ++++++++++++++++++ 5 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 vscode/src/test/unit/telemetry/cacheService.unit.test.ts create mode 100644 vscode/src/test/unit/telemetry/queue.unit.test.ts diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index fced412..5a6e44f 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -37,8 +37,8 @@ export class TelemetryEventQueue { return queue; } - public decreaseSizeOnMaxOverflow = (maxNumberOfEventsToBeKept: number) => { - const excess = this.size() - maxNumberOfEventsToBeKept; + public adjustQueueSize = (maxNumOfEventsToRetain: number) => { + const excess = this.size() - maxNumOfEventsToRetain; if (excess > 0) { LOGGER.debug('Decreasing size of the queue as max capacity reached'); diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index 1b696b6..7478acd 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -63,10 +63,10 @@ export class TelemetryReporterImpl implements TelemetryReporter { this.queue.enqueue(event); if (this.retryManager.isQueueOverflow(this.queue.size())) { LOGGER.debug(`Send triggered to queue size overflow`); - const numOfeventsToBeDropped = this.retryManager.getNumberOfEventsToBeDropped(); + const numOfEventsToBeRetained = this.retryManager.getNumberOfEventsToBeRetained(); this.sendEvents(); - if (numOfeventsToBeDropped) { - this.queue.decreaseSizeOnMaxOverflow(numOfeventsToBeDropped); + if (numOfEventsToBeRetained !== -1) { + this.queue.adjustQueueSize(numOfEventsToBeRetained); } } } diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 22faee0..8580a79 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -91,9 +91,9 @@ export class TelemetryRetry { LOGGER.debug("Keeping queue capacity same as max capacity reached"); } - public getNumberOfEventsToBeDropped = (): number => + public getNumberOfEventsToBeRetained = (): number => this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries ? - this.queueCapacity/2 : 0; + this.queueCapacity/2 : -1; private resetQueueCapacity = (): void => { LOGGER.debug("Resetting queue capacity to default"); diff --git a/vscode/src/test/unit/telemetry/cacheService.unit.test.ts b/vscode/src/test/unit/telemetry/cacheService.unit.test.ts new file mode 100644 index 0000000..8ab10d7 --- /dev/null +++ b/vscode/src/test/unit/telemetry/cacheService.unit.test.ts @@ -0,0 +1,86 @@ +/* + Copyright (c) 2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import { globalState } from "../../../globalState"; +import { LOGGER } from "../../../logger"; +import { describe, it, beforeEach, afterEach } from "mocha"; +import { cacheService } from "../../../telemetry/impl/cacheServiceImpl"; + +describe("CacheServiceImpl", () => { + let getStub: sinon.SinonStub; + let updateStub: sinon.SinonStub; + let loggerErrorStub: sinon.SinonStub; + let loggerDebugStub: sinon.SinonStub; + + const fakeState = { + get: (key: string) => `value-${key}`, + update: async (key: string, value: string) => {}, + }; + + beforeEach(() => { + getStub = sinon.stub(fakeState, "get").callThrough(); + updateStub = sinon.stub(fakeState, "update").resolves(); + + sinon.stub(globalState, "getExtensionContextInfo").returns({ + getVscGlobalState: () => fakeState, + } as any); + + loggerErrorStub = sinon.stub(LOGGER, "error"); + loggerDebugStub = sinon.stub(LOGGER, "debug"); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("get", () => { + it("should return the cached value for a key", () => { + const key = "example"; + const value = cacheService.get(key); + expect(value).to.equal(`value-${key}`); + expect(getStub.calledOnceWith(key)).to.be.true; + }); + + it("should log and return undefined on error", () => { + getStub.throws(new Error("key not found error")); + + const result = cacheService.get("notPresent"); + expect(result).to.be.undefined; + expect(loggerErrorStub.calledOnce).to.be.true; + }); + }); + + describe("put", () => { + it("should store the value and return true", async () => { + const key = "example"; + const value = "example-value" + const result = await cacheService.put(key, value); + expect(result).to.be.true; + expect(updateStub.calledOnceWith(key, value)).to.be.true; + expect(loggerDebugStub.calledOnce).to.be.true; + }); + + it("should log and return false on error", async () => { + updateStub.rejects(new Error("Error while storing key")); + + const result = await cacheService.put("badKey", "value"); + expect(result).to.be.false; + expect(loggerErrorStub.calledOnce).to.be.true; + }); + }); +}); diff --git a/vscode/src/test/unit/telemetry/queue.unit.test.ts b/vscode/src/test/unit/telemetry/queue.unit.test.ts new file mode 100644 index 0000000..8e195ae --- /dev/null +++ b/vscode/src/test/unit/telemetry/queue.unit.test.ts @@ -0,0 +1,184 @@ +/* + Copyright (c) 2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { TelemetryEventQueue } from '../../../telemetry/impl/telemetryEventQueue'; +import { BaseEvent } from '../../../telemetry/events/baseEvent'; +import { LOGGER } from '../../../logger'; +import { describe, it, beforeEach, afterEach } from 'mocha'; + +describe('TelemetryEventQueue', () => { + let queue: TelemetryEventQueue; + let loggerStub: sinon.SinonStub; + + class MockEvent extends BaseEvent { + public static readonly NAME = "mock"; + public static readonly ENDPOINT = "/mock"; + + + constructor(name?: string, data?: any) { + super(name || MockEvent.NAME, MockEvent.ENDPOINT, data || {}); + } + } + + beforeEach(() => { + queue = new TelemetryEventQueue(); + + loggerStub = sinon.stub(LOGGER, 'debug'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('enqueue', () => { + it('should add an event to the queue', () => { + const event = new MockEvent(); + queue.enqueue(event); + expect(queue.size()).to.equal(1); + }); + + it('should add multiple events in order', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + + queue.enqueue(event1); + queue.enqueue(event2); + + const firstEvent = queue.dequeue(); + expect(firstEvent).to.equal(event1); + expect(queue.size()).to.equal(1); + }); + }); + + describe('dequeue', () => { + it('should remove and return the first event from the queue', () => { + const event = new MockEvent(); + queue.enqueue(event); + + const dequeuedEvent = queue.dequeue(); + expect(dequeuedEvent).to.equal(event); + expect(queue.size()).to.equal(0); + }); + + it('should return undefined if queue is empty', () => { + const dequeuedEvent = queue.dequeue(); + expect(dequeuedEvent).to.be.undefined; + }); + }); + + describe('concatQueue', () => { + it('should append events to the end of the queue by default', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + const event3 = new MockEvent('event3'); + + queue.enqueue(event1); + queue.concatQueue([event2, event3]); + + expect(queue.size()).to.equal(3); + expect(queue.dequeue()).to.equal(event1); + expect(queue.dequeue()).to.equal(event2); + expect(queue.dequeue()).to.equal(event3); + }); + + it('should prepend events to the start of the queue when mergeAtStarting is true', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + const event3 = new MockEvent('event3'); + + queue.enqueue(event1); + queue.concatQueue([event2, event3], true); + + expect(queue.size()).to.equal(3); + expect(queue.dequeue()).to.equal(event2); + expect(queue.dequeue()).to.equal(event3); + expect(queue.dequeue()).to.equal(event1); + }); + }); + + describe('size', () => { + it('should return the number of events in the queue', () => { + expect(queue.size()).to.equal(0); + + queue.enqueue(new MockEvent('event1')); + expect(queue.size()).to.equal(1); + + queue.enqueue(new MockEvent('event2')); + expect(queue.size()).to.equal(2); + + queue.dequeue(); + expect(queue.size()).to.equal(1); + }); + }); + + describe('flush', () => { + it('should return all events and empty the queue', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + + queue.enqueue(event1); + queue.enqueue(event2); + + const flushedEvents = queue.flush(); + + expect(flushedEvents).to.deep.equal([event1, event2]); + expect(queue.size()).to.equal(0); + }); + }); + + describe('decreaseSizeOnMaxOverflow', () => { + it('should do nothing if queue size is below the max', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + + queue.enqueue(event1); + queue.enqueue(event2); + + queue.adjustQueueSize(5); + + expect(queue.size()).to.equal(2); + expect(loggerStub.called).to.be.false; + }); + + it('should log and deduplicate events when queue exceeds max size', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + const event3 = new MockEvent('event1'); + const event4 = new MockEvent('event3'); + const event5 = new MockEvent('event4'); + const event6 = new MockEvent('event5'); + + queue.enqueue(event1); + queue.enqueue(event2); + queue.enqueue(event3); + queue.enqueue(event4); + queue.enqueue(event5); + queue.enqueue(event6); + + queue.adjustQueueSize(3); + + expect(queue.size()).to.equal(5); + expect(loggerStub.calledOnce).to.be.true; + + const remainingEvents = queue.flush(); + const eventNames = remainingEvents.map(e => e.NAME); + expect(eventNames).to.deep.equal(['event1', 'event2', 'event3', 'event4', 'event5']); + }); + }); + +}); \ No newline at end of file