From 5814d183fe67ab17d367fea3ad9f2c7498c7aebc Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 6 Nov 2023 13:06:55 -0500 Subject: [PATCH] Show "submit issue" button, silence telnet after 2 clicks --- src/DebugConfigurationProvider.spec.ts | 49 ++++++++++++++++++++++---- src/DebugConfigurationProvider.ts | 47 +++++++++++++++++------- src/GlobalStateManager.ts | 27 +++++++++----- src/constants.ts | 8 +++++ src/extension.ts | 3 +- src/managers/TelemetryManager.ts | 33 +++++++++++------ src/mockVscode.spec.ts | 3 ++ src/util.ts | 28 +++++++++++++++ 8 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 src/constants.ts diff --git a/src/DebugConfigurationProvider.spec.ts b/src/DebugConfigurationProvider.spec.ts index 663f9c6c..c4a7c9ce 100644 --- a/src/DebugConfigurationProvider.spec.ts +++ b/src/DebugConfigurationProvider.spec.ts @@ -15,6 +15,7 @@ import type { RokuDeviceDetails } from './ActiveDeviceManager'; import { ActiveDeviceManager } from './ActiveDeviceManager'; import { rokuDeploy } from 'roku-deploy'; import { GlobalStateManager } from './GlobalStateManager'; +import { util } from './util'; const sinon = createSandbox(); const Module = require('module'); @@ -464,7 +465,7 @@ describe('BrightScriptConfigurationProvider', () => { let value: string; let stub: SinonStub; beforeEach(() => { - stub = sinon.stub(vscode.window, 'showWarningMessage').callsFake(() => { + stub = sinon.stub(vscode.window, 'showInformationMessage').callsFake(() => { return Promise.resolve(value) as any; }); }); @@ -477,14 +478,18 @@ describe('BrightScriptConfigurationProvider', () => { it('sets true and flips global state when clicked "okay"', async () => { value = `Okay (and dont warn again)`; - expect(globalStateManager.suppressDebugProtocolAutoEnabledMessage).to.eql(false); + expect(globalStateManager.debugProtocolPopupSnoozeUntilDate).to.eql(undefined); const config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); expect(config.enableDebugProtocol).to.eql(true); - expect(globalStateManager.suppressDebugProtocolAutoEnabledMessage).to.eql(true); + //2 weeks after now + expect( + globalStateManager.debugProtocolPopupSnoozeUntilDate.getTime() + ).closeTo(Date.now() + (14 * 24 * 60 * 60 * 1000), 1000); + expect(globalStateManager.debugProtocolPopupSnoozeValue).to.eql(true); }); it('sets false when clicked "No, use the telnet debugger"', async () => { - value = 'No, use the telnet debugger'; + value = 'Use telnet'; const config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); expect(config.enableDebugProtocol).to.eql(false); }); @@ -500,12 +505,44 @@ describe('BrightScriptConfigurationProvider', () => { expect(ex?.message).to.eql('Debug session cancelled'); }); - it('sets to true and does not prompt when "dont show again" was clicked"', async () => { + it('sets to true and does not prompt when "dont show again" was clicked', async () => { value = `Okay (and dont warn again)`; - globalStateManager.suppressDebugProtocolAutoEnabledMessage = true; + globalStateManager.debugProtocolPopupSnoozeUntilDate = new Date(Date.now() + (60 * 1000)); + globalStateManager.debugProtocolPopupSnoozeValue = true; let config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); expect(config.enableDebugProtocol).to.eql(true); expect(stub.called).to.be.false; }); + + it('shows the alternate telnet prompt after 2 debug sessions', async () => { + value = `Use telnet`; + + await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); + expect(stub.getCall(stub.callCount - 1).args[4]).to.eql('Use telnet'); + + await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); + expect(stub.getCall(stub.callCount - 1).args[4]).to.eql('Use telnet'); + + value = 'Use telnet (and ask less often)'; + await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); + expect(stub.getCall(stub.callCount - 1).args[4]).to.eql('Use telnet (and ask less often)'); + }); + + it('shows the issue picker when selected', async () => { + value = `Report an issue`; + const reportStub = sinon.stub(util, 'openIssueReporter').returns(Promise.resolve()); + + try { + await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' }); + } catch (e) { } + + expect(reportStub.called).to.be.true; + }); + + it('turns truthy values into true', async () => { + value = `Report an issue`; + const config = await configProvider['processEnableDebugProtocolParameter']({ enableDebugProtocol: {} } as any, { softwareVersion: '12.5.0' }); + expect(config.enableDebugProtocol).to.be.true; + }); }); }); diff --git a/src/DebugConfigurationProvider.ts b/src/DebugConfigurationProvider.ts index b40d0074..22305a8e 100644 --- a/src/DebugConfigurationProvider.ts +++ b/src/DebugConfigurationProvider.ts @@ -82,15 +82,21 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio private configDefaults: any; + /** + * A counter used to track how often the user clicks the "use telnet" button in the popup + */ + private useTelnetCounter = 0; + /** * Massage a debug configuration just before a debug session is being launched, * e.g. add all missing attributes to the debug configuration. */ public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, config: BrightScriptLaunchConfiguration, token?: CancellationToken): Promise { let deviceInfo: DeviceInfo; + let result: BrightScriptLaunchConfiguration; try { // merge user and workspace settings into the config - let result = this.processUserWorkspaceSettings(config); + result = this.processUserWorkspaceSettings(config); //force a specific staging folder path because sometimes this conflicts with bsconfig.json result.stagingFolderPath = path.join('${outDir}/.roku-deploy-staging'); @@ -126,6 +132,7 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio //send telemetry about this debug session (don't worry, it gets sanitized...we're just checking if certain features are being used) this.telemetryManager?.sendStartDebugSessionEvent( this.processUserWorkspaceSettings(config) as any, + result, deviceInfo ); } @@ -137,26 +144,42 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio return config; } - if (this.globalStateManager.suppressDebugProtocolAutoEnabledMessage) { - config.enableDebugProtocol = true; + //auto-pick a value if user chose to snooze the popup + if (this.globalStateManager.debugProtocolPopupSnoozeUntilDate && this.globalStateManager.debugProtocolPopupSnoozeUntilDate > new Date()) { + config.enableDebugProtocol = this.globalStateManager.debugProtocolPopupSnoozeValue; return config; } //enable the debug protocol by default if the user hasn't defined this prop, and the target RokuOS is 12.5 or greater - const result = await vscode.window.showWarningMessage(`We have just auto-enabled Roku's new debug protocol for this debug session. The debug protocol will soon become the default option without warning, so please be sure to notify us about any issues you encounter while using this feature during this testing phase.`, { - modal: true - }, 'Okay', `Okay (and dont warn again)`, 'No, use the telnet debugger'); - //cancel - if (result === undefined) { - throw new Error('Debug session cancelled'); - } else if (result === 'Okay') { + const result = await vscode.window.showInformationMessage('New Debug Protocol Enabled', { + modal: true, + detail: `We've activated Roku's debug protocol for this session. This will become the default choice in the future and may be implemented without additional notice. Your feedback during this testing phase is invaluable.` + }, 'Okay', `Okay (and dont warn again)`, this.useTelnetCounter < 2 ? 'Use telnet' : 'Use telnet (and ask less often)', 'Report an issue'); + + + if (result === 'Okay') { config.enableDebugProtocol = true; } else if (result === `Okay (and dont warn again)`) { config.enableDebugProtocol = true; - this.globalStateManager.suppressDebugProtocolAutoEnabledMessage = true; - } else if (result === 'No, use the telnet debugger') { + this.globalStateManager.debugProtocolPopupSnoozeValue = config.enableDebugProtocol; + //snooze for 2 weeks + this.globalStateManager.debugProtocolPopupSnoozeUntilDate = new Date(Date.now() + (14 * 24 * 60 * 60 * 1000)); + } else if (result === 'Use telnet') { + this.useTelnetCounter++; + config.enableDebugProtocol = false; + } else if (result === 'Use telnet (and ask less often)') { + this.useTelnetCounter = 0; config.enableDebugProtocol = false; + this.globalStateManager.debugProtocolPopupSnoozeValue = config.enableDebugProtocol; + //snooze for 12 hours + this.globalStateManager.debugProtocolPopupSnoozeUntilDate = new Date(Date.now() + (12 * 60 * 60 * 1000)); + } else if (result === 'Report an issue') { + await util.openIssueReporter({ deviceInfo: deviceInfo }); + throw new Error('Debug session cancelled'); + } else { + throw new Error('Debug session cancelled'); } + return config; } diff --git a/src/GlobalStateManager.ts b/src/GlobalStateManager.ts index 05b45179..29c2f12d 100644 --- a/src/GlobalStateManager.ts +++ b/src/GlobalStateManager.ts @@ -12,7 +12,8 @@ export class GlobalStateManager { lastRunExtensionVersion: 'lastRunExtensionVersion', lastSeenReleaseNotesVersion: 'lastSeenReleaseNotesVersion', sendRemoteTextHistory: 'sendRemoteTextHistory', - suppressDebugProtocolAutoEnabledMessage: 'suppressDebugProtocolAutoEnabledMessage' + debugProtocolPopupSnoozeUntilDate: 'debugProtocolPopupSnoozeUntilDate', + debugProtocolPopupSnoozeValue: 'debugProtocolPopupSnoozeValue' }; private remoteTextHistoryLimit: number; private remoteTextHistoryEnabled: boolean; @@ -37,16 +38,26 @@ export class GlobalStateManager { void this.context.globalState.update(this.keys.lastSeenReleaseNotesVersion, value); } - /** - * Should the "we auto-enabled the debug protocol for you" message be suppressed? Defaults to false. - */ - public get suppressDebugProtocolAutoEnabledMessage() { - return this.context.globalState.get(this.keys.suppressDebugProtocolAutoEnabledMessage) === true; + + public get debugProtocolPopupSnoozeUntilDate(): Date { + const epoch = this.context.globalState.get(this.keys.debugProtocolPopupSnoozeUntilDate); + if (epoch) { + return new Date(epoch); + } } - public set suppressDebugProtocolAutoEnabledMessage(value: boolean) { - void this.context.globalState.update(this.keys.suppressDebugProtocolAutoEnabledMessage, value); + public set debugProtocolPopupSnoozeUntilDate(value: Date) { + void this.context.globalState.update(this.keys.debugProtocolPopupSnoozeUntilDate, value?.getTime()); } + + public get debugProtocolPopupSnoozeValue(): boolean { + return this.context.globalState.get(this.keys.debugProtocolPopupSnoozeValue); + } + public set debugProtocolPopupSnoozeValue(value: boolean) { + void this.context.globalState.update(this.keys.debugProtocolPopupSnoozeValue, value); + } + + public get sendRemoteTextHistory(): string[] { return this.context.globalState.get(this.keys.sendRemoteTextHistory) ?? []; } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..05eccaf9 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +import * as fsExtra from 'fs-extra'; + +export let ROKU_DEBUG_VERSION: string; +try { + ROKU_DEBUG_VERSION = fsExtra.readJsonSync(__dirname + '/../node_modules/roku-debug/package.json').version; +} catch (e) { } + +export const EXTENSION_ID = 'RokuCommunity.brightscript'; diff --git a/src/extension.ts b/src/extension.ts index 3155a901..d67efc30 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,8 +27,7 @@ import { RtaManager } from './managers/RtaManager'; import { WebviewViewProviderManager } from './managers/WebviewViewProviderManager'; import { ViewProviderId } from './viewProviders/ViewProviderId'; import { DiagnosticManager } from './managers/DiagnosticManager'; - -const EXTENSION_ID = 'RokuCommunity.brightscript'; +import { EXTENSION_ID } from './constants'; export class Extension { public outputChannel: vscode.OutputChannel; diff --git a/src/managers/TelemetryManager.ts b/src/managers/TelemetryManager.ts index e025929b..948fd27e 100644 --- a/src/managers/TelemetryManager.ts +++ b/src/managers/TelemetryManager.ts @@ -32,18 +32,29 @@ export class TelemetryManager implements Disposable { /** * Track when a debug session has been started */ - public sendStartDebugSessionEvent(event: BrightScriptLaunchConfiguration & { preLaunchTask: string }, deviceInfo?: DeviceInfo) { + public sendStartDebugSessionEvent(initialConfig: BrightScriptLaunchConfiguration & { preLaunchTask: string }, finalConfig: BrightScriptLaunchConfiguration, deviceInfo?: DeviceInfo) { + let debugConnectionType: 'debugProtocol' | 'telnet'; + let enableDebugProtocol = finalConfig?.enableDebugProtocol ?? initialConfig?.enableDebugProtocol; + if (enableDebugProtocol === true) { + debugConnectionType = 'debugProtocol'; + } else if (enableDebugProtocol === false) { + debugConnectionType = 'telnet'; + } else { + debugConnectionType = undefined; + } + this.reporter.sendTelemetryEvent('startDebugSession', { - enableDebugProtocol: boolToString(event.enableDebugProtocol), - retainDeploymentArchive: boolToString(event.retainDeploymentArchive), - retainStagingFolder: boolToString(event.retainStagingFolder), - injectRaleTrackerTask: boolToString(event.injectRaleTrackerTask), - isFilesDefined: isDefined(event.files), - isPreLaunchTaskDefined: isDefined(event.preLaunchTask), - isComponentLibrariesDefined: isDefined(event.componentLibraries), - isDeepLinkUrlDefined: isDefined(event.deepLinkUrl), - isStagingFolderPathDefined: isDefined(event.stagingFolderPath), - isLogfilePathDefined: isDefined(event.logfilePath), + enableDebugProtocol: boolToString(initialConfig.enableDebugProtocol), + debugConnectionType: debugConnectionType?.toString(), + retainDeploymentArchive: boolToString(initialConfig.retainDeploymentArchive), + retainStagingFolder: boolToString(initialConfig.retainStagingFolder), + injectRaleTrackerTask: boolToString(initialConfig.injectRaleTrackerTask), + isFilesDefined: isDefined(initialConfig.files), + isPreLaunchTaskDefined: isDefined(initialConfig.preLaunchTask), + isComponentLibrariesDefined: isDefined(initialConfig.componentLibraries), + isDeepLinkUrlDefined: isDefined(initialConfig.deepLinkUrl), + isStagingFolderPathDefined: isDefined(initialConfig.stagingFolderPath), + isLogfilePathDefined: isDefined(initialConfig.logfilePath), isExtensionLogfilePathDefined: isDefined( vscode.workspace.getConfiguration('brightscript').get('extensionLogfilePath') ), diff --git a/src/mockVscode.spec.ts b/src/mockVscode.spec.ts index 267f7954..33b03e5d 100644 --- a/src/mockVscode.spec.ts +++ b/src/mockVscode.spec.ts @@ -206,6 +206,9 @@ export let vscode = { } as OutputChannel; }, registerTreeDataProvider: function(viewId: string, treeDataProvider: TreeDataProvider) { }, + showInformationMessage: function(message: string) { + + }, showWarningMessage: function(message: string) { }, diff --git a/src/util.ts b/src/util.ts index 8469a356..e0624090 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,6 +5,9 @@ import * as url from 'url'; import { debounce } from 'debounce'; import * as vscode from 'vscode'; import { Cache } from 'brighterscript/dist/Cache'; +import undent from 'undent'; +import { EXTENSION_ID, ROKU_DEBUG_VERSION } from './constants'; +import type { DeviceInfo } from 'roku-deploy'; class Util { public async readDir(dirPath: string) { @@ -381,6 +384,31 @@ class Util { public escapeRegex(text: string) { return text?.toString().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } + + public async openIssueReporter(options: { title?: string; body?: string; deviceInfo?: DeviceInfo }) { + if (!options.body) { + options.body = undent` + Please describe the issue you are experiencing: + + Steps to reproduce: + + Additional feedback: + `; + } + options.body += `\n\nroku-debug version: ${ROKU_DEBUG_VERSION}`; + if (options.deviceInfo) { + options.body += '\n' + undent` + Device firmware: ${options.deviceInfo.softwareVersion}.${options.deviceInfo.softwareBuild} + Debug protocol version: ${options.deviceInfo.brightscriptDebuggerVersion} + Device model: ${options.deviceInfo.modelNumber} + `; + } + await vscode.commands.executeCommand('vscode.openIssueReporter', { + extensionId: EXTENSION_ID, + issueTitle: options.title ?? 'Problem with Debug Protocol', + issueBody: options.body + }); + } } const util = new Util();