Skip to content

Commit

Permalink
Interactive Tutorial: Add telemetry, improve command, and adjust flag…
Browse files Browse the repository at this point in the history
… logic (#4068)
  • Loading branch information
umpox committed May 7, 2024
1 parent a57b544 commit 7124d80
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 56 deletions.
3 changes: 2 additions & 1 deletion vscode/src/services/AuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { logDebug } from '../log'

import { telemetryRecorder } from '@sourcegraph/cody-shared'
import { closeAuthProgressIndicator } from '../auth/auth-progress-indicator'
import { maybeStartInteractiveTutorial } from '../tutorial/helpers'
import { AuthMenu, showAccessTokenInputBox, showInstanceURLInputBox } from './AuthMenus'
import { getAuthReferralCode } from './AuthProviderSimplified'
import { localStorage } from './LocalStorageProvider'
Expand Down Expand Up @@ -498,8 +499,8 @@ export class AuthProvider {
return
}
telemetryRecorder.recordEvent('cody.auth.login', 'firstEver')
void vscode.commands.executeCommand('cody.tutorial.start')
this.setHasAuthenticatedBefore()
void maybeStartInteractiveTutorial()
}

private setHasAuthenticatedBefore() {
Expand Down
88 changes: 60 additions & 28 deletions vscode/src/tutorial/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ps } from '@sourcegraph/cody-shared'
import { ps, telemetryRecorder } from '@sourcegraph/cody-shared'
import * as vscode from 'vscode'
import { executeEdit } from '../edit/execute'
import { type TextChange, updateRangeMultipleChanges } from '../non-stop/tracked-range'
import { CodyTaskState } from '../non-stop/utils'
import { TODO_DECORATION } from './constants'
import type { TutorialStep } from './content'
Expand All @@ -25,6 +26,8 @@ export const setFixDiagnostic = (
])
}

export type TutorialSource = 'link' | 'editor'

/**
* States at which we consider an Edit to be "terminated" for the purposes of the tutorial.
* Considers both "Applied" and "Error" to be terminal, so that we can encourage the user along
Expand All @@ -34,40 +37,69 @@ const TERMINAL_EDIT_STATES = [CodyTaskState.Applied, CodyTaskState.Finished, Cod

export const registerEditTutorialCommand = (
editor: vscode.TextEditor,
onComplete: () => void
): vscode.Disposable => {
const disposable = vscode.commands.registerCommand('cody.tutorial.edit', async document => {
// Clear the existing decoration, the user has actioned this step,
// we're just waiting for the response.
editor.setDecorations(TODO_DECORATION, [])

const task = await executeEdit({
configuration: {
document: editor.document,
preInstruction: ps`Function that finds logs in a dir`,
},
})

if (!task) {
onComplete: () => void,
range: vscode.Range
): vscode.Disposable[] => {
let trackedRange = range
const rangeTracker = vscode.workspace.onDidChangeTextDocument(event => {
if (event.document !== editor.document) {
return
}

// Poll for task.state being applied
const interval = setInterval(async () => {
if (TERMINAL_EDIT_STATES.includes(task.state)) {
clearInterval(interval)
onComplete()
}
}, 100)
const changes = new Array<TextChange>(...event.contentChanges)
const updatedRange = updateRangeMultipleChanges(trackedRange, changes)
if (!updatedRange.isEqual(trackedRange)) {
trackedRange = updatedRange
}
})
return disposable

const editCommand = vscode.commands.registerCommand(
'cody.tutorial.edit',
async (_document: vscode.TextDocument, source: TutorialSource = 'editor') => {
telemetryRecorder.recordEvent('cody.interactiveTutorial', 'edit', {
privateMetadata: { source },
})

// Clear the existing decoration, the user has actioned this step,
// we're just waiting for the response.
editor.setDecorations(TODO_DECORATION, [])

const task = await executeEdit({
configuration: {
document: editor.document,
range: trackedRange,
preInstruction: ps`Function that finds logs in a dir`,
},
})

if (!task) {
return
}

// Poll for task.state being applied
const interval = setInterval(async () => {
if (TERMINAL_EDIT_STATES.includes(task.state)) {
clearInterval(interval)
onComplete()
}
}, 100)
}
)

return [rangeTracker, editCommand]
}

export const registerChatTutorialCommand = (onComplete: () => void): vscode.Disposable => {
const disposable = vscode.commands.registerCommand('cody.tutorial.chat', async () => {
await vscode.commands.executeCommand('cody.chat.panel.new')
onComplete()
})
const disposable = vscode.commands.registerCommand(
'cody.tutorial.chat',
async (_document: vscode.TextDocument, source: TutorialSource = 'editor') => {
telemetryRecorder.recordEvent('cody.interactiveTutorial', 'chat', {
privateMetadata: { source },
})
await vscode.commands.executeCommand('cody.chat.panel.new')
onComplete()
}
)
return disposable
}

Expand Down
4 changes: 2 additions & 2 deletions vscode/src/tutorial/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const getStepContent = (step: TutorialStepKey): string => {
We've pre-filled the instruction, all you need to do is choose Submit.
"""
# ^ Edit: Place cursor above and press Opt+K
# ^ Start an Edit (Opt+K)
`
break
case 'fix':
Expand Down Expand Up @@ -99,7 +99,7 @@ export const getStepData = (
}
}
case 'edit': {
const triggerText = findRangeOfText(document, '^ Edit:')
const triggerText = findRangeOfText(document, 'Start an Edit')
if (!triggerText) {
return null
}
Expand Down
19 changes: 19 additions & 0 deletions vscode/src/tutorial/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import path from 'node:path'
import { FeatureFlag, featureFlagProvider, telemetryRecorder } from '@sourcegraph/cody-shared'
import * as vscode from 'vscode'
import { telemetryService } from '../services/telemetry'
import { logFirstEnrollmentEvent } from '../services/utils/enrollment-event'

let tutorialDocumentUri: vscode.Uri

Expand All @@ -18,3 +21,19 @@ export const isInTutorial = (document: vscode.TextDocument): boolean => {
// True if the users target document matches our tutorial document
return document.uri.toString() === tutorialDocumentUri.toString()
}

// A/B testing logic for the interactive tutorial
// Ensure that the featureFlagProvider has the latest auth status,
// and then trigger the tutorial.
// This will either noop or open the tutorial depending on the feature flag.
export const maybeStartInteractiveTutorial = async () => {
telemetryService.log('CodyVSCodeExtension:cody:interactiveTutorial:attemptingStart')
telemetryRecorder.recordEvent('cody.interactiveTutorial', 'attemptingStart')
await featureFlagProvider.syncAuthStatus()
const enabled = await featureFlagProvider.evaluateFeatureFlag(FeatureFlag.CodyInteractiveTutorial)
logFirstEnrollmentEvent(FeatureFlag.CodyInteractiveTutorial, enabled)
if (!enabled) {
return
}
return vscode.commands.executeCommand('cody.tutorial.start')
}
27 changes: 9 additions & 18 deletions vscode/src/tutorial/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { FeatureFlag, featureFlagProvider, telemetryRecorder } from '@sourcegraph/cody-shared'
import { telemetryRecorder } from '@sourcegraph/cody-shared'
import * as vscode from 'vscode'
import { type TextChange, updateRangeMultipleChanges } from '../../src/non-stop/tracked-range'
import { isRunningInsideAgent } from '../jsonrpc/isRunningInsideAgent'
import { logSidebarClick } from '../services/SidebarCommands'
import { logFirstEnrollmentEvent } from '../services/utils/enrollment-event'
import {
registerAutocompleteListener,
registerChatTutorialCommand,
Expand All @@ -21,7 +20,7 @@ import {
resetDocument,
} from './content'
import { setTutorialUri } from './helpers'
import { ChatLinkProvider, ResetLensProvider } from './providers'
import { ResetLensProvider, TutorialLinkProvider } from './providers'

export const startTutorial = async (document: vscode.TextDocument): Promise<vscode.Disposable> => {
const disposables: vscode.Disposable[] = []
Expand Down Expand Up @@ -127,7 +126,9 @@ export const startTutorial = async (document: vscode.TextDocument): Promise<vsco
setFixDiagnostic(diagnosticCollection, editor.document.uri, activeStep.range)
break
case 'edit':
disposables.push(registerEditTutorialCommand(editor, progressToNextStep))
disposables.push(
...registerEditTutorialCommand(editor, progressToNextStep, activeStep.range)
)
break
case 'chat':
disposables.push(registerChatTutorialCommand(progressToNextStep))
Expand Down Expand Up @@ -171,7 +172,10 @@ export const startTutorial = async (document: vscode.TextDocument): Promise<vsco
disposables.push(
startListeningForSuccess('autocomplete'),
new ResetLensProvider(editor),
vscode.languages.registerDocumentLinkProvider(editor.document.uri, new ChatLinkProvider(editor)),
vscode.languages.registerDocumentLinkProvider(
editor.document.uri,
new TutorialLinkProvider(editor)
),
vscode.window.onDidChangeVisibleTextEditors(editors => {
const tutorialIsActive = editors.find(
editor => editor.document.uri.toString() === document.uri.toString()
Expand Down Expand Up @@ -212,14 +216,6 @@ export const registerInteractiveTutorial = async (

let cleanup: vscode.Disposable | undefined
const start = async () => {
const enabled = await featureFlagProvider.evaluateFeatureFlag(
FeatureFlag.CodyInteractiveTutorial
)
logFirstEnrollmentEvent(FeatureFlag.CodyInteractiveTutorial, enabled)
if (!enabled) {
return
}

status = 'starting'
cleanup = await startTutorial(document)
disposables.push(cleanup)
Expand Down Expand Up @@ -263,11 +259,6 @@ export const registerInteractiveTutorial = async (
if (status === 'started') {
return vscode.window.showTextDocument(documentUri)
}
// Refresh any flags when a manual start is triggered
// This is primarily so, when a user authenticates for the first time,
// We ensure we use the correct feature flag value before determining if they should
// see the tutorial.
await featureFlagProvider.refreshFeatureFlags()
return start()
}),
vscode.commands.registerCommand('cody.tutorial.reset', async () => {
Expand Down
38 changes: 31 additions & 7 deletions vscode/src/tutorial/providers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as vscode from 'vscode'
import type { TutorialSource } from './commands'
import { findRangeOfText } from './utils'

export class ChatLinkProvider implements vscode.DocumentLinkProvider {
export class TutorialLinkProvider implements vscode.DocumentLinkProvider {
constructor(public editor: vscode.TextEditor) {}

provideDocumentLinks(
Expand All @@ -12,17 +13,40 @@ export class ChatLinkProvider implements vscode.DocumentLinkProvider {
return []
}

const linkRange = findRangeOfText(document, 'Start a Chat')
if (!linkRange) {
return []
const links: vscode.DocumentLink[] = []

const editRange = findRangeOfText(document, 'Start an Edit')
if (editRange) {
const params = [document, 'link' satisfies TutorialSource]
links.push(
new vscode.DocumentLink(
editRange,
vscode.Uri.parse(
`command:cody.tutorial.edit?${encodeURIComponent(JSON.stringify(params))}`
)
)
)
}

const chatRange = findRangeOfText(document, 'Start a Chat')
if (chatRange) {
const params = [document, 'link' satisfies TutorialSource]
links.push(
new vscode.DocumentLink(
chatRange,
vscode.Uri.parse(
`command:cody.tutorial.chat?${encodeURIComponent(JSON.stringify(params))}`
)
)
)
}

const decorationType = vscode.window.createTextEditorDecorationType({
const linkDecoration = vscode.window.createTextEditorDecorationType({
color: new vscode.ThemeColor('textLink.activeForeground'),
})
this.editor.setDecorations(decorationType, [{ range: linkRange }])
this.editor.setDecorations(linkDecoration, links)

return [new vscode.DocumentLink(linkRange, vscode.Uri.parse('command:cody.tutorial.chat'))]
return links
}
}

Expand Down

0 comments on commit 7124d80

Please sign in to comment.