Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/telemetry-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,15 @@ or

```typescript
{
'step': 'welcome-in-trial' | 'welcome-paid' | 'welcome-in-trial-expired' | 'get-started-community' | 'visualize-code-history' | 'accelerate-pr-reviews' | 'streamline-collaboration' | 'improve-workflows-with-integrations'
'step': 'welcome-in-trial' | 'welcome-paid' | 'welcome-in-trial-expired-eligible' | 'welcome-in-trial-expired' | 'get-started-community' | 'visualize-code-history' | 'accelerate-pr-reviews' | 'streamline-collaboration' | 'improve-workflows-with-integrations'
}
```

### walkthrough/completion

```typescript
{
'context.key': 'gettingStarted' | 'visualizeCodeHistory' | 'prReviews' | 'streamlineCollaboration' | 'integrations'
}
```

Expand Down
4 changes: 4 additions & 0 deletions src/constants.telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CustomEditorTypes, TreeViewTypes, WebviewTypes, WebviewViewTypes }
import type { GitContributionTiers } from './git/models/contributor';
import type { Period } from './plus/webviews/timeline/protocol';
import type { Flatten } from './system/object';
import type { WalkthroughContextKeys } from './telemetry/walkthroughStateProvider';

export type TelemetryGlobalContext = {
'cloudIntegrations.connected.count': number;
Expand Down Expand Up @@ -397,6 +398,9 @@ export type TelemetryEvents = {
walkthrough: {
step?: WalkthroughSteps;
};
'walkthrough/completion': {
'context.key': WalkthroughContextKeys;
};
} & Record<`${WebviewTypes | WebviewViewTypes}/showAborted`, WebviewShownEventData> &
Record<
`${Exclude<WebviewTypes | WebviewViewTypes, 'commitDetails' | 'graph' | 'graphDetails' | 'timeline'>}/shown`,
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export const urls = Object.freeze({
export type WalkthroughSteps =
| 'welcome-in-trial'
| 'welcome-paid'
| 'welcome-in-trial-expired-eligible'
| 'welcome-in-trial-expired'
| 'get-started-community'
| 'visualize-code-history'
Expand Down
2 changes: 1 addition & 1 deletion src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ export class Container {
(this._storage = storage),
(this._telemetry = new TelemetryService(this)),
(this._usage = new UsageTracker(this, storage)),
(this._walkthrough = new WalkthroughStateProvider(this)),
configuration.onDidChangeAny(this.onAnyConfigurationChanged, this),
];

Expand All @@ -190,6 +189,7 @@ export class Container {
);
this._disposables.push((this._uri = new UriService(this)));
this._disposables.push((this._subscription = new SubscriptionService(this, this._connection, previousVersion)));
this._disposables.push((this._walkthrough = new WalkthroughStateProvider(this)));
this._disposables.push((this._organizations = new OrganizationService(this, this._connection)));

this._disposables.push((this._git = new GitProviderService(this)));
Expand Down
259 changes: 197 additions & 62 deletions src/telemetry/walkthroughStateProvider.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Event } from 'vscode';
import { Disposable, EventEmitter } from 'vscode';
import { Commands } from '../constants.commands';
import { SubscriptionState } from '../constants.subscription';
import type { TrackedUsageKeys } from '../constants.telemetry';
import type { Container } from '../container';
import { entries } from '../system/object';
import type { SubscriptionChangeEvent } from '../plus/gk/account/subscriptionService';
import { wait } from '../system/promise';
import { setContext } from '../system/vscode/context';
import type { UsageChangeEvent } from './usageTracker';
Expand All @@ -16,66 +17,118 @@ export enum WalkthroughContextKeys {
Integrations = 'integrations',
}

type WalkthroughUsage = {
subscriptionStates?: SubscriptionState[] | Readonly<SubscriptionState[]>;
subscriptionCommands?: TrackedUsageKeys[] | Readonly<TrackedUsageKeys[]>;
usage: TrackedUsageKeys[];
};

const triedProStates: Readonly<SubscriptionState[]> = [
SubscriptionState.ProTrial,
SubscriptionState.ProTrialExpired,
SubscriptionState.ProTrialReactivationEligible,
SubscriptionState.Paid,
];

const tryProCommands: Readonly<TrackedUsageKeys[]> = [
`command:${Commands.PlusStartPreviewTrial}:executed`,
`command:${Commands.PlusReactivateProTrial}:executed`,
];

const walkthroughRequiredMapping: Readonly<Map<WalkthroughContextKeys, WalkthroughUsage>> = new Map<
WalkthroughContextKeys,
WalkthroughUsage
>([
[
WalkthroughContextKeys.GettingStarted,
{
subscriptionStates: triedProStates,
subscriptionCommands: tryProCommands,
usage: [],
},
],
[
WalkthroughContextKeys.VisualizeCodeHistory,
{
subscriptionStates: triedProStates,
subscriptionCommands: tryProCommands,
usage: [
'graphDetailsView:shown',
'graphView:shown',
'graphWebview:shown',
'commitDetailsView:shown',
`command:${Commands.ShowGraph}:executed`,
`command:${Commands.ShowGraphPage}:executed`,
`command:${Commands.ShowGraphView}:executed`,
`command:${Commands.ShowInCommitGraph}:executed`,
`command:${Commands.ShowInCommitGraphView}:executed`,
],
},
],
[
WalkthroughContextKeys.PrReviews,
{
subscriptionStates: triedProStates,
subscriptionCommands: tryProCommands,
usage: [
'launchpadView:shown',
'worktreesView:shown',
`command:${Commands.ShowLaunchpad}:executed`,
`command:${Commands.ShowLaunchpadView}:executed`,
`command:${Commands.GitCommandsWorktree}:executed`,
`command:${Commands.GitCommandsWorktreeCreate}:executed`,
`command:${Commands.GitCommandsWorktreeDelete}:executed`,
`command:${Commands.GitCommandsWorktreeOpen}:executed`,
],
},
],
[
WalkthroughContextKeys.StreamlineCollaboration,
{
subscriptionStates: triedProStates,
subscriptionCommands: tryProCommands,
usage: [`command:${Commands.CreateCloudPatch}:executed`, `command:${Commands.CreatePatch}:executed`],
},
],
[
WalkthroughContextKeys.Integrations,
{
usage: [
`command:${Commands.PlusConnectCloudIntegrations}:executed`,
`command:${Commands.PlusManageCloudIntegrations}:executed`,
],
},
],
]);

export class WalkthroughStateProvider implements Disposable {
readonly walkthroughSize = walkthroughRequiredMapping.size;
protected disposables: Disposable[] = [];
private readonly state = new Map<WalkthroughContextKeys, boolean>();
readonly walkthroughSize: number;
private isInitialized = false;
private _initPromise: Promise<void> | undefined;
private readonly completed = new Set<WalkthroughContextKeys>();
private subscriptionState: SubscriptionState | undefined;

/**
* using reversed map (instead of direct map as walkthroughToTracking Record<WalkthroughContextKeys, TrackedUsageKeys[]>)
* makes code less readable, but prevents duplicated usageTracker keys
*/
private readonly walkthroughByTracking: Partial<Record<TrackedUsageKeys, WalkthroughContextKeys>> = {
[`command:${Commands.PlusStartPreviewTrial}:executed`]: WalkthroughContextKeys.GettingStarted,
[`command:${Commands.PlusReactivateProTrial}:executed`]: WalkthroughContextKeys.GettingStarted,
[`command:${Commands.OpenWalkthrough}:executed`]: WalkthroughContextKeys.GettingStarted,
[`command:${Commands.GetStarted}:executed`]: WalkthroughContextKeys.GettingStarted,

'graphDetailsView:shown': WalkthroughContextKeys.VisualizeCodeHistory,
'graphView:shown': WalkthroughContextKeys.VisualizeCodeHistory,
'graphWebview:shown': WalkthroughContextKeys.VisualizeCodeHistory,
'commitDetailsView:shown': WalkthroughContextKeys.VisualizeCodeHistory,
[`command:${Commands.ShowGraph}:executed`]: WalkthroughContextKeys.VisualizeCodeHistory,
[`command:${Commands.ShowGraphPage}:executed`]: WalkthroughContextKeys.VisualizeCodeHistory,
[`command:${Commands.ShowGraphView}:executed`]: WalkthroughContextKeys.VisualizeCodeHistory,
[`command:${Commands.ShowInCommitGraph}:executed`]: WalkthroughContextKeys.VisualizeCodeHistory,
[`command:${Commands.ShowInCommitGraphView}:executed`]: WalkthroughContextKeys.VisualizeCodeHistory,

'launchpadView:shown': WalkthroughContextKeys.PrReviews,
'worktreesView:shown': WalkthroughContextKeys.PrReviews,
[`command:${Commands.ShowLaunchpad}:executed`]: WalkthroughContextKeys.PrReviews,
[`command:${Commands.ShowLaunchpadView}:executed`]: WalkthroughContextKeys.PrReviews,
[`command:${Commands.GitCommandsWorktree}:executed`]: WalkthroughContextKeys.PrReviews,
[`command:${Commands.GitCommandsWorktreeCreate}:executed`]: WalkthroughContextKeys.PrReviews,
[`command:${Commands.GitCommandsWorktreeDelete}:executed`]: WalkthroughContextKeys.PrReviews,
[`command:${Commands.GitCommandsWorktreeOpen}:executed`]: WalkthroughContextKeys.PrReviews,

[`command:${Commands.CreateCloudPatch}:executed`]: WalkthroughContextKeys.StreamlineCollaboration,
[`command:${Commands.CreatePatch}:executed`]: WalkthroughContextKeys.StreamlineCollaboration,

[`command:${Commands.PlusConnectCloudIntegrations}:executed`]: WalkthroughContextKeys.Integrations,
[`command:${Commands.PlusManageCloudIntegrations}:executed`]: WalkthroughContextKeys.Integrations,
};
private readonly _onProgressChanged = new EventEmitter<void>();
get onProgressChanged(): Event<void> {
return this._onProgressChanged.event;
}

constructor(private readonly container: Container) {
this.disposables.push(this.container.usage.onDidChange(this.onUsageChanged, this));
this.walkthroughSize = Object.values(WalkthroughContextKeys).length;
this.disposables.push(
this.container.usage.onDidChange(this.onUsageChanged, this),
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
);

this.initializeState();
void this.initializeState();
}

private initializeState() {
for (const key of Object.values(WalkthroughContextKeys)) {
this.state.set(key, false);
}
entries(this.walkthroughByTracking).forEach(([usageKey, walkthroughKey]) => {
if (!this.state.get(walkthroughKey) && this.container.usage.isUsed(usageKey)) {
void this.completeStep(walkthroughKey);
private async initializeState() {
this.subscriptionState = (await this.container.subscription.getSubscription(true)).state;

for (const key of walkthroughRequiredMapping.keys()) {
if (this.validateStep(key)) {
void this.completeStep(key);
}
});
}
this._onProgressChanged.fire(undefined);
}

Expand All @@ -84,25 +137,65 @@ export class WalkthroughStateProvider implements Disposable {
if (!usageTrackingKey) {
return;
}
const walkthroughKey = this.walkthroughByTracking[usageTrackingKey];
if (walkthroughKey) {
void this.completeStep(walkthroughKey);

const stepsToValidate = this.getStepsFromUsage(usageTrackingKey);
let shouldFire = false;
for (const step of stepsToValidate) {
// no need to check if the step is already completed
if (this.completed.has(step)) {
continue;
}

if (this.validateStep(step)) {
void this.completeStep(step);
this.container.telemetry.sendEvent('walkthrough/completion', {
'context.key': step,
});
shouldFire = true;
}
}
if (shouldFire) {
this._onProgressChanged.fire(undefined);
}
}

private onSubscriptionChanged(e: SubscriptionChangeEvent) {
this.subscriptionState = e.current.state;
const stepsToValidate = this.getStepsFromSubscriptionState(e.current.state);
let shouldFire = false;
for (const step of stepsToValidate) {
// no need to check if the step is already completed
if (this.completed.has(step)) {
continue;
}

if (this.validateStep(step)) {
void this.completeStep(step);
this.container.telemetry.sendEvent('walkthrough/completion', {
'context.key': step,
});
shouldFire = true;
}
}
if (shouldFire) {
this._onProgressChanged.fire(undefined);
}
}

private _isInitialized: boolean = false;
private _initPromise: Promise<void> | undefined;
/**
* Walkthrough view is not ready to listen to context changes immediately after opening VSCode with the walkthrough page opened
* As we're not able to check if the walkthrough is ready, we need to add a delay.
* The 1s delay will not be too annoying for user but it's enough to init
*/
private async waitForWalkthroughInitialized() {
if (this.isInitialized) {
if (this._isInitialized) {
return;
}
if (!this._initPromise) {
this._initPromise = wait(1000).then(() => {
this.isInitialized = true;
this._isInitialized = true;
});
}
await this._initPromise;
Expand All @@ -114,17 +207,13 @@ export class WalkthroughStateProvider implements Disposable {
* we don't have an ability to reset the flag
*/
private async completeStep(key: WalkthroughContextKeys) {
this.state.set(key, true);
this.completed.add(key);
await this.waitForWalkthroughInitialized();
void setContext(`gitlens:walkthroughState:${key}`, true);
}

get onProgressChanged(): Event<void> {
return this._onProgressChanged.event;
}

get doneCount() {
return [...this.state.values()].filter(x => x).length;
return this.completed.size;
}

get progress() {
Expand All @@ -134,4 +223,50 @@ export class WalkthroughStateProvider implements Disposable {
dispose(): void {
Disposable.from(...this.disposables).dispose();
}

private getStepsFromUsage(usageKey: TrackedUsageKeys): WalkthroughContextKeys[] {
const keys: WalkthroughContextKeys[] = [];
for (const [key, { subscriptionCommands, usage }] of walkthroughRequiredMapping) {
if (subscriptionCommands?.includes(usageKey) || usage.includes(usageKey)) {
keys.push(key);
}
}

return keys;
}

private getStepsFromSubscriptionState(_state: SubscriptionState): WalkthroughContextKeys[] {
const keys: WalkthroughContextKeys[] = [];
for (const [key, { subscriptionStates }] of walkthroughRequiredMapping) {
if (subscriptionStates != null) {
keys.push(key);
}
}

return keys;
}

private validateStep(key: WalkthroughContextKeys): boolean {
const { subscriptionStates, subscriptionCommands, usage } = walkthroughRequiredMapping.get(key)!;

let subscriptionState: boolean | undefined;
if (subscriptionStates != null && subscriptionStates.length > 0) {
subscriptionState = this.subscriptionState != null && subscriptionStates.includes(this.subscriptionState);
}
let subscriptionCommandState: boolean | undefined;
if (subscriptionCommands != null && subscriptionCommands.length > 0) {
subscriptionCommandState = subscriptionCommands.some(event => this.container.usage.isUsed(event));
}
if (
(subscriptionState === undefined && subscriptionCommandState === false) ||
(subscriptionState === false && subscriptionCommandState !== true)
) {
return false;
}

if (usage.length > 0 && !usage.some(event => this.container.usage.isUsed(event))) {
return false;
}
return true;
}
}
Loading