diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 8d254749d..55ec199f4 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -72,6 +72,7 @@ export default class MDBExtensionController implements vscode.Disposable { _editDocumentCodeLensProvider: EditDocumentCodeLensProvider; _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; _participantController: ParticipantController; + _startupNotificationShown = false; constructor( context: vscode.ExtensionContext, @@ -166,6 +167,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommands(); this.showOverviewPageIfRecentlyInstalled(); void this.showSurveyForEstablishedUsers(); + void this.showCopilotIntroductionForEstablishedUsers(); } registerCommands = (): void => { @@ -909,6 +911,62 @@ export default class MDBExtensionController implements vscode.Disposable { } } + async showCopilotIntroductionForEstablishedUsers(): Promise { + const copilotIntroductionShown = + this._storageController.get( + StorageVariables.GLOBAL_COPILOT_INTRODUCTION_SHOWN + ) === true; + + // Show the toast when startup notifications have not been shown + // to the user yet and they have saved connections + // -> they haven't just started using this extension. + if ( + this._startupNotificationShown || + copilotIntroductionShown || + !this._connectionStorage.hasSavedConnections() + ) { + return; + } + + this._startupNotificationShown = true; + + const action = 'Chat with @MongoDB'; + const text = + 'Generate queries, interact with documentation, and explore your database schema using the MongoDB Copilot extension. Give it a try!'; + const result = await vscode.window.showInformationMessage( + text, + {}, + { + title: action, + } + ); + + const copilot = vscode.extensions.getExtension('github.copilot-chat'); + if (result?.title === action) { + await vscode.commands.executeCommand('workbench.action.chat.newChat'); + await vscode.commands.executeCommand( + 'workbench.action.chat.clearHistory' + ); + await vscode.commands.executeCommand('workbench.action.chat.open', { + query: '@MongoDB', + isPartialQuery: true, + }); + this._telemetryService.trackCopilotIntroductionClicked({ + is_copilot_active: !!copilot?.isActive, + }); + } else { + this._telemetryService.trackCopilotIntroductionDismissed({ + is_copilot_active: !!copilot?.isActive, + }); + } + + // Whether action was taken or the prompt dismissed, we won't show this again. + void this._storageController.update( + StorageVariables.GLOBAL_COPILOT_INTRODUCTION_SHOWN, + true + ); + } + async showSurveyForEstablishedUsers(): Promise { const surveyId = '9viN9wcbsC3zvHyg7'; @@ -916,16 +974,19 @@ export default class MDBExtensionController implements vscode.Disposable { this._storageController.get(StorageVariables.GLOBAL_SURVEY_SHOWN) === surveyId; - // Show the survey when it hasn't been show to the - // user yet, and they have saved connections + // Show the toast when startup notifications have not been shown + // to the user yet and they have saved connections // -> they haven't just started using this extension if ( + this._startupNotificationShown || hasBeenShownSurveyAlready || !this._connectionStorage.hasSavedConnections() ) { return; } + this._startupNotificationShown = true; + const action = 'Share your thoughts'; const text = 'How can we make the MongoDB extension better for you?'; const link = 'https://forms.gle/9viN9wcbsC3zvHyg7'; diff --git a/src/storage/storageController.ts b/src/storage/storageController.ts index e19eae149..9bce41701 100644 --- a/src/storage/storageController.ts +++ b/src/storage/storageController.ts @@ -7,6 +7,7 @@ export enum StorageVariables { // Only exists on globalState. GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW = 'GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW', GLOBAL_SURVEY_SHOWN = 'GLOBAL_SURVEY_SHOWN', + GLOBAL_COPILOT_INTRODUCTION_SHOWN = 'GLOBAL_COPILOT_INTRODUCTION_SHOWN', GLOBAL_SAVED_CONNECTIONS = 'GLOBAL_SAVED_CONNECTIONS', // Analytics user identify. GLOBAL_USER_ID = 'GLOBAL_USER_ID', @@ -53,6 +54,7 @@ interface StorageVariableContents { [StorageVariables.GLOBAL_ANONYMOUS_ID]: string; [StorageVariables.GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW]: boolean; [StorageVariables.GLOBAL_SURVEY_SHOWN]: string; + [StorageVariables.GLOBAL_COPILOT_INTRODUCTION_SHOWN]: boolean; [StorageVariables.GLOBAL_SAVED_CONNECTIONS]: ConnectionsFromStorage; [StorageVariables.WORKSPACE_SAVED_CONNECTIONS]: ConnectionsFromStorage; [StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE]: boolean; diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index d4514581b..1d4238e66 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -126,6 +126,10 @@ export type ParticipantResponseProperties = { output_length: number; }; +export type CopilotIntroductionProperties = { + is_copilot_active: boolean; +}; + export function chatResultFeedbackKindToTelemetryValue( kind: vscode.ChatResultFeedbackKind ): TelemetryFeedbackKind { @@ -157,7 +161,8 @@ type TelemetryEventProperties = | ParticipantFeedbackProperties | ParticipantResponseFailedProperties | ParticipantPromptProperties - | ParticipantResponseProperties; + | ParticipantResponseProperties + | CopilotIntroductionProperties; export enum TelemetryEventTypes { PLAYGROUND_CODE_EXECUTED = 'Playground Code Executed', @@ -181,6 +186,8 @@ export enum TelemetryEventTypes { PARTICIPANT_RESPONSE_FAILED = 'Participant Response Failed', PARTICIPANT_PROMPT_SUBMITTED = 'Participant Prompt Submitted', PARTICIPANT_RESPONSE_GENERATED = 'Participant Response Generated', + COPILOT_INTRODUCTION_CLICKED = 'Copilot Introduction Clicked', + COPILOT_INTRODUCTION_DISMISSED = 'Copilot Introduction Dismissed', } /** @@ -475,4 +482,14 @@ export default class TelemetryService { trackCopilotParticipantResponse(props: ParticipantResponseProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_RESPONSE_GENERATED, props); } + + trackCopilotIntroductionClicked(props: CopilotIntroductionProperties): void { + this.track(TelemetryEventTypes.COPILOT_INTRODUCTION_CLICKED, props); + } + + trackCopilotIntroductionDismissed( + props: CopilotIntroductionProperties + ): void { + this.track(TelemetryEventTypes.COPILOT_INTRODUCTION_DISMISSED, props); + } } diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index d45cfd291..dac0bb16a 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -1716,6 +1716,26 @@ suite('MDBExtensionController Test Suite', function () { }); suite('survey prompt', function () { + suite( + 'when a user has been shown the startup notification already', + function () { + beforeEach(() => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .get(function getterFn() { + return true; + }); + }); + + test('they are not shown the survey prompt', () => { + assert(showInformationMessageStub.notCalled); + }); + } + ); + suite( "when a user hasn't been shown the survey prompt yet, and they have connections saved", () => { @@ -1730,6 +1750,15 @@ suite('MDBExtensionController Test Suite', function () { let connectionsUpdateStub: SinonStub; let uriParseStub: SinonStub; beforeEach(async () => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .set(function setterFn() {}) + .get(function getterFn() { + return false; + }); showInformationMessageStub.resolves(reaction.value); openExternalStub.resolves(undefined); sandbox.replace( @@ -1796,6 +1825,15 @@ suite('MDBExtensionController Test Suite', function () { suite('when a user has been shown the survey prompt already', () => { let connectionsUpdateStub: SinonStub; beforeEach(() => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .set(function setterFn() {}) + .get(function getterFn() { + return false; + }); sandbox.replace( mdbTestExtension.testExtensionController._storageController, 'get', @@ -1823,6 +1861,15 @@ suite('MDBExtensionController Test Suite', function () { suite('when a has no connections saved', () => { let connectionsUpdateStub: SinonStub; beforeEach(() => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .set(function setterFn() {}) + .get(function getterFn() { + return false; + }); sandbox.replace( mdbTestExtension.testExtensionController._storageController, 'get', @@ -1847,6 +1894,190 @@ suite('MDBExtensionController Test Suite', function () { }); }); }); + + suite('copilot introduction prompt', function () { + suite( + 'when a user has been shown the startup notification already', + function () { + beforeEach(() => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .get(function getterFn() { + return true; + }); + }); + + test('they are not shown the copilot introduction prompt', () => { + assert(showInformationMessageStub.notCalled); + }); + } + ); + + suite( + "when a user hasn't been shown the copilot introduction prompt yet, and they have connections saved", + () => { + [ + { + description: 'clicked the button', + value: { title: 'Chat with @MongoDB' }, + }, + { description: 'dismissed', value: undefined }, + ].forEach((reaction) => { + suite(`user ${reaction.description}`, () => { + let connectionsUpdateStub: SinonStub; + let executeCommandStub: SinonStub; + beforeEach(async () => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .set(function setterFn() {}) + .get(function getterFn() { + return false; + }); + showInformationMessageStub.resolves(reaction.value); + executeCommandStub = sandbox.stub( + vscode.commands, + 'executeCommand' + ); + sandbox.replace( + mdbTestExtension.testExtensionController._storageController, + 'get', + sandbox.fake.returns(undefined) + ); + sandbox.replace( + mdbTestExtension.testExtensionController._connectionStorage, + 'hasSavedConnections', + sandbox.fake.returns(true) + ); + connectionsUpdateStub = sandbox.stub( + mdbTestExtension.testExtensionController._storageController, + 'update' + ); + connectionsUpdateStub.resolves(undefined); + await mdbTestExtension.testExtensionController.showCopilotIntroductionForEstablishedUsers(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + test('they are shown the copilot introduction prompt', () => { + assert(showInformationMessageStub.called); + assert.strictEqual( + showInformationMessageStub.firstCall.args[0], + 'Generate queries, interact with documentation, and explore your database schema using the MongoDB Copilot extension. Give it a try!' + ); + }); + + test('the link was open if and only if they click the button', () => { + if (reaction.value === undefined) { + assert(executeCommandStub.notCalled); + } + if (reaction.value) { + assert(executeCommandStub.called); + assert.strictEqual( + executeCommandStub.firstCall.args[0], + 'workbench.action.chat.newChat' + ); + } + }); + + test("it sets that they've been shown the copilot introduction", () => { + assert(connectionsUpdateStub.called); + assert.strictEqual( + connectionsUpdateStub.firstCall.args[0], + StorageVariables.GLOBAL_COPILOT_INTRODUCTION_SHOWN + ); + assert.strictEqual( + connectionsUpdateStub.firstCall.args[1], + true + ); + }); + }); + }); + } + ); + + suite( + 'when a user has been shown the copilot introduction prompt already', + () => { + let connectionsUpdateStub: SinonStub; + beforeEach(() => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .set(function setterFn() {}) + .get(function getterFn() { + return false; + }); + sandbox.replace( + mdbTestExtension.testExtensionController._storageController, + 'get', + sandbox.fake.returns(true) // copilot introduction has been shown + ); + sandbox.replace( + mdbTestExtension.testExtensionController._connectionStorage, + 'hasSavedConnections', + sandbox.fake.returns(true) + ); + connectionsUpdateStub = sandbox.stub( + mdbTestExtension.testExtensionController._storageController, + 'update' + ); + connectionsUpdateStub.resolves(undefined); + + void mdbTestExtension.testExtensionController.showCopilotIntroductionForEstablishedUsers(); + }); + + test('they are not shown the copilot introduction prompt', () => { + assert(showInformationMessageStub.notCalled); + }); + } + ); + + suite('when a has no connections saved', () => { + let connectionsUpdateStub: SinonStub; + beforeEach(() => { + sandbox + .stub( + mdbTestExtension.testExtensionController, + '_startupNotificationShown' + ) + .set(function setterFn() {}) + .get(function getterFn() { + return false; + }); + sandbox.replace( + mdbTestExtension.testExtensionController._storageController, + 'get', + sandbox.fake.returns(undefined) + ); + sandbox.replace( + mdbTestExtension.testExtensionController._connectionStorage, + 'hasSavedConnections', + sandbox.fake.returns(false) // no connections yet - this might be the first install + ); + connectionsUpdateStub = sandbox.stub( + mdbTestExtension.testExtensionController._storageController, + 'update' + ); + connectionsUpdateStub.resolves(undefined); + + void mdbTestExtension.testExtensionController.showCopilotIntroductionForEstablishedUsers(); + }); + + test('they are not shown the copilot introduction prompt', () => { + assert(showInformationMessageStub.notCalled); + }); + }); + }); }); test('mdb.participantViewRawSchemaOutput command opens a json document with the output', async () => {