Skip to content

Commit

Permalink
Adds basic local usage tracking to aid discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
eamodio committed Aug 21, 2022
1 parent 3805b17 commit d43ccec
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 0 deletions.
10 changes: 10 additions & 0 deletions package.json
Expand Up @@ -207,6 +207,7 @@
"onCommand:gitlens.externalDiffAll",
"onCommand:gitlens.resetAvatarCache",
"onCommand:gitlens.resetSuppressedWarnings",
"onCommand:gitlens.resetTrackedUsage",
"onCommand:gitlens.inviteToLiveShare",
"onCommand:gitlens.browseRepoAtRevision",
"onCommand:gitlens.browseRepoAtRevisionInNewWindow",
Expand Down Expand Up @@ -4719,6 +4720,11 @@
"title": "Reset Suppressed Warnings",
"category": "GitLens"
},
{
"command": "gitlens.resetTrackedUsage",
"title": "Reset Tracked Usage",
"category": "GitLens"
},
{
"command": "gitlens.inviteToLiveShare",
"title": "Invite to Live Share",
Expand Down Expand Up @@ -6759,6 +6765,10 @@
"command": "gitlens.resetSuppressedWarnings",
"when": "gitlens:enabled"
},
{
"command": "gitlens.resetTrackedUsage",
"when": "gitlens:enabled"
},
{
"command": "gitlens.inviteToLiveShare",
"when": "false"
Expand Down
11 changes: 11 additions & 0 deletions src/commands/resets.ts
Expand Up @@ -27,3 +27,14 @@ export class ResetSuppressedWarningsCommand extends Command {
await configuration.update('advanced.messages', undefined, ConfigurationTarget.Global);
}
}

@command()
export class ResetTrackedUsageCommand extends Command {
constructor(private readonly container: Container) {
super(Commands.ResetTrackedUsage);
}

async execute() {
await this.container.usage.reset();
}
}
1 change: 1 addition & 0 deletions src/constants.ts
Expand Up @@ -155,6 +155,7 @@ export const enum Commands {
RefreshHover = 'gitlens.refreshHover',
ResetAvatarCache = 'gitlens.resetAvatarCache',
ResetSuppressedWarnings = 'gitlens.resetSuppressedWarnings',
ResetTrackedUsage = 'gitlens.resetTrackedUsage',
RevealCommitInView = 'gitlens.revealCommitInView',
SearchCommits = 'gitlens.showCommitSearch',
SearchCommitsInView = 'gitlens.views.searchAndCompare.searchCommits',
Expand Down
7 changes: 7 additions & 0 deletions src/container.ts
Expand Up @@ -32,6 +32,7 @@ import { memoize } from './system/decorators/memoize';
import { GitTerminalLinkProvider } from './terminal/linkProvider';
import { GitDocumentTracker } from './trackers/gitDocumentTracker';
import { GitLineTracker } from './trackers/gitLineTracker';
import { UsageTracker } from './usageTracker';
import { BranchesView } from './views/branchesView';
import { CommitsView } from './views/commitsView';
import { ContributorsView } from './views/contributorsView';
Expand Down Expand Up @@ -144,6 +145,7 @@ export class Container {
this.ensureModeApplied();

context.subscriptions.push((this._storage = storage));
context.subscriptions.push((this._usage = new UsageTracker(storage)));

context.subscriptions.push(configuration.onWillChange(this.onConfigurationChanging, this));

Expand Down Expand Up @@ -544,6 +546,11 @@ export class Container {
return this._tracker;
}

private readonly _usage: UsageTracker;
get usage(): UsageTracker {
return this._usage;
}

@memoize()
get version(): string {
return this.context.extension.packageJSON.version as string;
Expand Down
1 change: 1 addition & 0 deletions src/plus/webviews/graph/graphWebview.ts
Expand Up @@ -62,6 +62,7 @@ export class GraphWebview extends WebviewWithConfigBase<State> {
override async show(column: ViewColumn = ViewColumn.Active, ...args: any[]): Promise<void> {
if (!(await ensurePlusFeaturesEnabled())) return;

void this.container.usage.track('graphWebview:shown');
return super.show(column, ...args);
}

Expand Down
2 changes: 2 additions & 0 deletions src/plus/webviews/timeline/timelineWebview.ts
Expand Up @@ -64,6 +64,8 @@ export class TimelineWebview extends WebviewBase<State> {

override async show(column: ViewColumn = ViewColumn.Beside, ...args: any[]): Promise<void> {
if (!(await ensurePlusFeaturesEnabled())) return;

void this.container.usage.track('timelineWebview:shown');
return super.show(column, ...args);
}

Expand Down
2 changes: 2 additions & 0 deletions src/plus/webviews/timeline/timelineWebviewView.ts
Expand Up @@ -57,6 +57,8 @@ export class TimelineWebviewView extends WebviewViewBase<State> {

override async show(options?: { preserveFocus?: boolean | undefined }): Promise<void> {
if (!(await ensurePlusFeaturesEnabled())) return;

void this.container.usage.track('timelineView:shown');
return super.show(options);
}

Expand Down
2 changes: 2 additions & 0 deletions src/storage.ts
Expand Up @@ -3,6 +3,7 @@ import { EventEmitter } from 'vscode';
import type { GraphColumnConfig, ViewShowBranchComparison } from './config';
import type { SearchPattern } from './git/search';
import type { Subscription } from './subscription';
import type { TrackedUsage, TrackedUsageKeys } from './usageTracker';
import type { CompletedActions } from './webviews/home/protocol';

export type StorageChangeEvent =
Expand Down Expand Up @@ -131,6 +132,7 @@ export interface GlobalStorage {
synced: {
version?: string;
};
usages?: Record<TrackedUsageKeys, TrackedUsage>;
version?: string;
views: {
welcome: {
Expand Down
22 changes: 22 additions & 0 deletions src/system/object.ts
Expand Up @@ -54,3 +54,25 @@ export function paths(o: Record<string, any>, path?: string): string[] {

return results;
}

export function updateRecordValue<T>(
obj: Record<string, T> | undefined,
key: string,
value: T | undefined,
): Record<string, T> {
if (obj == null) {
obj = Object.create(null) as Record<string, T>;
}

if (value != null && (typeof value !== 'boolean' || value)) {
if (typeof value === 'object') {
obj[key] = { ...value };
} else {
obj[key] = value;
}
} else {
const { [key]: _, ...rest } = obj;
obj = rest;
}
return obj;
}
77 changes: 77 additions & 0 deletions src/usageTracker.ts
@@ -0,0 +1,77 @@
import type { Disposable, Event } from 'vscode';
import { EventEmitter } from 'vscode';
import type { Storage } from './storage';
import { updateRecordValue } from './system/object';

export interface TrackedUsage {
count: number;
firstUsedAt: number;
lastUsedAt: number;
}
export type TrackedUsageKeys = 'graphWebview:shown' | 'timelineWebview:shown' | 'timelineView:shown';

export type UsageChangeEvent = {
/**
* The key of the action/event/feature who's usage was tracked
*/
readonly key: TrackedUsageKeys;
readonly usage?: TrackedUsage;
};

export class UsageTracker implements Disposable {
private _onDidChange = new EventEmitter<UsageChangeEvent | undefined>();
get onDidChange(): Event<UsageChangeEvent | undefined> {
return this._onDidChange.event;
}

constructor(private readonly storage: Storage) {}

dispose(): void {}

get(key: TrackedUsageKeys): TrackedUsage | undefined {
return this.storage.get('usages')?.[key];
}

async reset(key?: TrackedUsageKeys): Promise<void> {
let usages = this.storage.get('usages');
if (usages == null) return;

if (key == null) {
await this.storage.delete('usages');
this._onDidChange.fire(undefined);

return;
}

usages = updateRecordValue(usages, key, undefined);

await this.storage.store('usages', usages);
this._onDidChange.fire({ key: key, usage: undefined });
}

async track(key: TrackedUsageKeys): Promise<void> {
let usages = this.storage.get('usages');
if (usages == null) {
usages = Object.create(null) as NonNullable<typeof usages>;
}

const usedAt = Date.now();

let usage = usages[key];
if (usage == null) {
usage = {
count: 0,
firstUsedAt: usedAt,
lastUsedAt: usedAt,
};
usages[key] = usage;
} else {
usage.count++;
usage.lastUsedAt = usedAt;
}

await this.storage.store('usages', usages);

this._onDidChange.fire({ key: key, usage: usage });
}
}

0 comments on commit d43ccec

Please sign in to comment.