diff --git a/src/GitHub.App/Models/RepositoryHost.cs b/src/GitHub.App/Models/RepositoryHost.cs index bea24afa2c..4367e0f8c1 100644 --- a/src/GitHub.App/Models/RepositoryHost.cs +++ b/src/GitHub.App/Models/RepositoryHost.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Reactive.Threading.Tasks; using System.Collections.Generic; +using GitHub.Extensions; namespace GitHub.Models { @@ -241,7 +242,7 @@ IObservable LoginWithApiUser(UserAndScopes userAndScopes) if (result.IsSuccess()) { var accountCacheItem = new AccountCacheItem(userAndScopes.User); - usage.IncrementLoginCount(); + usage.IncrementLoginCount().Forget(); return ModelService.InsertUser(accountCacheItem).Select(_ => result); } diff --git a/src/GitHub.App/Services/PullRequestService.cs b/src/GitHub.App/Services/PullRequestService.cs index f90fcd251c..6fefc8d4e0 100644 --- a/src/GitHub.App/Services/PullRequestService.cs +++ b/src/GitHub.App/Services/PullRequestService.cs @@ -91,7 +91,7 @@ async Task PushAndCreatePR(IRepositoryHost host, await Task.Delay(TimeSpan.FromSeconds(5)); var ret = await host.ModelService.CreatePullRequest(sourceRepository, targetRepository, sourceBranch, targetBranch, title, body); - usageTracker.IncrementUpstreamPullRequestCount(); + await usageTracker.IncrementUpstreamPullRequestCount(); return ret; } diff --git a/src/GitHub.App/ViewModels/GistCreationViewModel.cs b/src/GitHub.App/ViewModels/GistCreationViewModel.cs index d0974ad98c..203407ee7e 100644 --- a/src/GitHub.App/ViewModels/GistCreationViewModel.cs +++ b/src/GitHub.App/ViewModels/GistCreationViewModel.cs @@ -78,7 +78,7 @@ IObservable OnCreateGist(object unused) newGist.Files.Add(FileName, SelectedText); return gistPublishService.PublishGist(apiClient, newGist) - .Do(_ => usageTracker.IncrementCreateGistCount()) + .Do(_ => usageTracker.IncrementCreateGistCount().Forget()) .Catch(ex => { if (!ex.IsCriticalException()) diff --git a/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs b/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs index 27e2bd7926..1a608e0b3b 100644 --- a/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs +++ b/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs @@ -199,7 +199,7 @@ IObservable OnCloneRepository(object state) return cloneService.CloneRepository(repository.CloneUrl, repository.Name, BaseRepositoryPath) .ContinueAfter(() => { - usageTracker.IncrementCloneCount(); + usageTracker.IncrementCloneCount().Forget(); return Observable.Return(Unit.Default); }); }) diff --git a/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs b/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs index bf7623581c..2691490a29 100644 --- a/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs +++ b/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs @@ -268,7 +268,7 @@ IObservable OnCreateRepository(object state) SelectedAccount, BaseRepositoryPath, repositoryHost.ApiClient) - .Do(_ => usageTracker.IncrementCreateCount()); + .Do(_ => usageTracker.IncrementCreateCount().Forget()); } ReactiveCommand InitializeCreateRepositoryCommand() diff --git a/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs b/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs index 5f818c9d52..16b00d4c07 100644 --- a/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs +++ b/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs @@ -153,7 +153,7 @@ IObservable OnPublishRepository(object arg) var account = SelectedAccount; return repositoryPublishService.PublishRepository(newRepository, account, SelectedHost.ApiClient) - .Do(_ => usageTracker.IncrementPublishCount()) + .Do(_ => usageTracker.IncrementPublishCount().Forget()) .Select(_ => ProgressState.Success) .Catch(ex => { diff --git a/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs b/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs index 15baf45480..05527c1d20 100644 --- a/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs +++ b/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs @@ -105,7 +105,7 @@ IObservable OnCloneRepository(object state) return cloneService.CloneRepository(repository.CloneUrl, repository.Name, BaseRepositoryPath) .ContinueAfter(() => { - usageTracker.IncrementCloneCount(); + usageTracker.IncrementCloneCount().Forget(); return Observable.Return(Unit.Default); }); }) diff --git a/src/GitHub.Exports/Services/IUsageTracker.cs b/src/GitHub.Exports/Services/IUsageTracker.cs index 4b43cdb202..08afb257dc 100644 --- a/src/GitHub.Exports/Services/IUsageTracker.cs +++ b/src/GitHub.Exports/Services/IUsageTracker.cs @@ -1,19 +1,20 @@ using GitHub.VisualStudio; using System.Runtime.InteropServices; +using System.Threading.Tasks; namespace GitHub.Services { [Guid(Guids.UsageTrackerId)] public interface IUsageTracker { - void IncrementLaunchCount(); - void IncrementCloneCount(); - void IncrementCreateCount(); - void IncrementPublishCount(); - void IncrementOpenInGitHubCount(); - void IncrementLinkToGitHubCount(); - void IncrementCreateGistCount(); - void IncrementUpstreamPullRequestCount(); - void IncrementLoginCount(); + Task IncrementLaunchCount(); + Task IncrementCloneCount(); + Task IncrementCreateCount(); + Task IncrementPublishCount(); + Task IncrementOpenInGitHubCount(); + Task IncrementLinkToGitHubCount(); + Task IncrementCreateGistCount(); + Task IncrementUpstreamPullRequestCount(); + Task IncrementLoginCount(); } } diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index a1b9ec9e56..9675136cca 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -298,6 +298,7 @@ + diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs index 7d96b837cc..961a52cf9c 100644 --- a/src/GitHub.VisualStudio/GitHubPackage.cs +++ b/src/GitHub.VisualStudio/GitHubPackage.cs @@ -52,8 +52,9 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke await base.InitializeAsync(cancellationToken, progress); await EnsurePackageLoaded(new Guid(ServiceProviderPackage.ServiceProviderPackageId)); - //var usageTracker = await GetServiceAsync(typeof(IUsageTracker)) as IUsageTracker; - //usageTracker.IncrementLaunchCount(); + // Activate the usage tracker by forcing an instance to be created. + GetServiceAsync(typeof(IUsageTracker)).Forget(); + InitializeMenus().Forget(); } @@ -178,6 +179,11 @@ async Task CreateService(IAsyncServiceContainer container, CancellationT var sp = await GetServiceAsync(typeof(IUIProvider)) as IUIProvider; return new MenuProvider(sp); } + else if (serviceType == typeof(IUsageTracker)) + { + var uiProvider = await GetServiceAsync(typeof(IUIProvider)) as IUIProvider; + return new UsageTracker(uiProvider); + } // go the mef route else { diff --git a/src/GitHub.VisualStudio/Menus/CopyLink.cs b/src/GitHub.VisualStudio/Menus/CopyLink.cs index 889906000a..b1f30af91f 100644 --- a/src/GitHub.VisualStudio/Menus/CopyLink.cs +++ b/src/GitHub.VisualStudio/Menus/CopyLink.cs @@ -31,7 +31,7 @@ public async void Activate([AllowNull]object data = null) Clipboard.SetText(link); var ns = ServiceProvider.TryGetService(); ns?.ShowMessage(Resources.LinkCopiedToClipboardMessage); - UsageTracker.IncrementLinkToGitHubCount(); + await UsageTracker.IncrementLinkToGitHubCount(); } catch { diff --git a/src/GitHub.VisualStudio/Menus/OpenLink.cs b/src/GitHub.VisualStudio/Menus/OpenLink.cs index dac26b948d..b0e9bd7ab6 100644 --- a/src/GitHub.VisualStudio/Menus/OpenLink.cs +++ b/src/GitHub.VisualStudio/Menus/OpenLink.cs @@ -26,7 +26,7 @@ public async void Activate([AllowNull]object data = null) var browser = ServiceProvider.TryGetService(); browser?.OpenUrl(link.ToUri()); - UsageTracker.IncrementOpenInGitHubCount(); + await UsageTracker.IncrementOpenInGitHubCount(); } public bool CanShow() diff --git a/src/GitHub.VisualStudio/Services/UsageTracker.cs b/src/GitHub.VisualStudio/Services/UsageTracker.cs index b166b2c7fc..9f0caff5a9 100644 --- a/src/GitHub.VisualStudio/Services/UsageTracker.cs +++ b/src/GitHub.VisualStudio/Services/UsageTracker.cs @@ -11,23 +11,24 @@ using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; using GitHub.Extensions; +using System.Threading.Tasks; namespace GitHub.Services { - [Export(typeof(IUsageTracker))] - [PartCreationPolicy(CreationPolicy.Shared)] public class UsageTracker : IUsageTracker { const string StoreFileName = "ghfvs.usage"; static readonly Calendar cal = CultureInfo.InvariantCulture.Calendar; + readonly IUIProvider uiProvider; + readonly DispatcherTimer timer; + IMetricsService client; - readonly IConnectionManager connectionManager; + IConnectionManager connectionManager; IPackageSettings userSettings; - readonly IVSServices vsservices; - readonly DispatcherTimer timer; - readonly string storePath; - readonly IServiceProvider serviceProvider; + IVSServices vsservices; + string storePath; + bool firstRun = true; Func fileExists; Func readAllText; @@ -35,13 +36,9 @@ public class UsageTracker : IUsageTracker Action dirCreate; [ImportingConstructor] - public UsageTracker( - IProgram program, - IConnectionManager connectionManager, - IVSServices vsservices, - [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + public UsageTracker(IUIProvider uiProvider) { - this.serviceProvider = serviceProvider; + this.uiProvider = uiProvider; fileExists = (path) => System.IO.File.Exists(path); readAllText = (path, encoding) => @@ -65,88 +62,105 @@ public UsageTracker( }; dirCreate = (path) => System.IO.Directory.CreateDirectory(path); - this.connectionManager = connectionManager; - this.vsservices = vsservices; this.timer = new DispatcherTimer( - TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(3), DispatcherPriority.Background, TimerTick, Dispatcher.CurrentDispatcher); - this.storePath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - program.ApplicationName, - StoreFileName); RunTimer(); } - public void IncrementLaunchCount() + public async Task IncrementLaunchCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfStartups; ++usage.Model.NumberOfStartupsWeek; ++usage.Model.NumberOfStartupsMonth; SaveUsage(usage); } - public void IncrementCloneCount() + public async Task IncrementCloneCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfClones; SaveUsage(usage); } - public void IncrementCreateCount() + public async Task IncrementCreateCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfReposCreated; SaveUsage(usage); } - public void IncrementPublishCount() + public async Task IncrementPublishCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfReposPublished; SaveUsage(usage); } - public void IncrementOpenInGitHubCount() + public async Task IncrementOpenInGitHubCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfOpenInGitHub; SaveUsage(usage); } - public void IncrementLinkToGitHubCount() + public async Task IncrementLinkToGitHubCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfLinkToGitHub; SaveUsage(usage); } - public void IncrementCreateGistCount() + public async Task IncrementCreateGistCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfGists; SaveUsage(usage); } - public void IncrementUpstreamPullRequestCount() + public async Task IncrementUpstreamPullRequestCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfUpstreamPullRequests; SaveUsage(usage); } - public void IncrementLoginCount() + public async Task IncrementLoginCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfLogins; SaveUsage(usage); } - UsageStore LoadUsage() + async Task Initialize() + { + // The services needed by the usage tracker are loaded when they are first needed to + // improve the startup time of the extension. + if (userSettings == null) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + client = uiProvider.GetService(); + connectionManager = uiProvider.GetService(); + userSettings = uiProvider.GetService(); + vsservices = uiProvider.GetService(); + + var program = uiProvider.GetService(); + storePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + program.ApplicationName, + StoreFileName); + } + } + + async Task LoadUsage() { + await Initialize(); + var json = fileExists(storePath) ? readAllText(storePath, Encoding.UTF8) : null; UsageStore result = null; try @@ -178,7 +192,6 @@ void RunTimer() { // The timer first ticks after 3 minutes to allow things to settle down after startup. // This will be changed to 8 hours after the first tick by the TimerTick method. - timer.Interval = TimeSpan.FromMinutes(3); timer.Start(); } @@ -194,31 +207,26 @@ void TimerTick(object sender, EventArgs e) async Task TimerTick() { - Debug.Assert(client != null, "TimerTick should not be triggered when there is no IMetricsService"); + await Initialize(); - // Subsequent timer ticks should occur every 8 hours. - timer.Interval = TimeSpan.FromHours(8); - - if (userSettings == null) + if (firstRun) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - client = serviceProvider.GetExportedValue(); - if (client == null) - { - timer.Stop(); - return; - } - userSettings = serviceProvider.GetExportedValue(); + await IncrementLaunchCount(); + timer.Interval = TimeSpan.FromHours(8); + firstRun = false; } - if (!userSettings.CollectMetrics) + if (client == null || !userSettings.CollectMetrics) + { + timer.Stop(); return; + } // Every time we increment the launch count we increment both daily and weekly // launch count but we only submit (and clear) the weekly launch count when we've // transitioned into a new week. We've defined a week by the ISO8601 definition, // i.e. week starting on Monday and ending on Sunday. - var usage = LoadUsage(); + var usage = await LoadUsage(); var lastDate = usage.LastUpdated; var currentDate = DateTimeOffset.Now; var includeWeekly = GetIso8601WeekOfYear(lastDate) != GetIso8601WeekOfYear(currentDate); diff --git a/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs b/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs new file mode 100644 index 0000000000..854eb74e76 --- /dev/null +++ b/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs @@ -0,0 +1,30 @@ +using Microsoft.VisualStudio.Shell; +using System; +using System.ComponentModel.Composition; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.Services +{ + [Export(typeof(IUsageTracker))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class UsageTrackerDispatcher : IUsageTracker + { + readonly IUsageTracker inner; + + [ImportingConstructor] + public UsageTrackerDispatcher([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + { + inner = serviceProvider.GetService(typeof(IUsageTracker)) as IUsageTracker; + } + + public Task IncrementCloneCount() => inner.IncrementCloneCount(); + public Task IncrementCreateCount() => inner.IncrementCreateCount(); + public Task IncrementCreateGistCount() => inner.IncrementCreateGistCount(); + public Task IncrementLaunchCount() => inner.IncrementLaunchCount(); + public Task IncrementLinkToGitHubCount() => inner.IncrementLinkToGitHubCount(); + public Task IncrementLoginCount() => inner.IncrementLoginCount(); + public Task IncrementOpenInGitHubCount() => inner.IncrementOpenInGitHubCount(); + public Task IncrementPublishCount() => inner.IncrementPublishCount(); + public Task IncrementUpstreamPullRequestCount() => inner.IncrementUpstreamPullRequestCount(); + } +} diff --git a/src/UnitTests/GitHub.App/ViewModels/RepositoryCloneViewModelTests.cs b/src/UnitTests/GitHub.App/ViewModels/RepositoryCloneViewModelTests.cs index 6186f3902f..ae990f16f3 100644 --- a/src/UnitTests/GitHub.App/ViewModels/RepositoryCloneViewModelTests.cs +++ b/src/UnitTests/GitHub.App/ViewModels/RepositoryCloneViewModelTests.cs @@ -13,6 +13,7 @@ using GitHub.Api; using Akavache; using GitHub.Collections; +using GitHub.Extensions; public class RepositoryCloneViewModelTests { @@ -358,7 +359,7 @@ public async Task UpdatesMetricsWhenRepositoryCloned() vm.SelectedRepository = Substitute.For(); await vm.CloneCommand.ExecuteAsync(); - usageTracker.Received().IncrementCloneCount(); + usageTracker.Received().IncrementCloneCount().Forget(); } } }