diff --git a/src/GitHub.App/Models/RepositoryHost.cs b/src/GitHub.App/Models/RepositoryHost.cs index ac496ce565..8bf655e5d3 100644 --- a/src/GitHub.App/Models/RepositoryHost.cs +++ b/src/GitHub.App/Models/RepositoryHost.cs @@ -76,7 +76,8 @@ public IObservable LogInFromCache() { var user = await loginManager.LoginFromCache(Address, ApiClient.GitHubClient); var accountCacheItem = new AccountCacheItem(user); - usage.IncrementLoginCount().Forget(); + + await usage.IncrementCounter(x => x.NumberOfLogins); await ModelService.InsertUser(accountCacheItem); if (user != null) @@ -107,7 +108,8 @@ public IObservable LogIn(string usernameOrEmail, string pa { var user = await loginManager.Login(Address, ApiClient.GitHubClient, usernameOrEmail, password); var accountCacheItem = new AccountCacheItem(user); - usage.IncrementLoginCount().Forget(); + + await usage.IncrementCounter(x => x.NumberOfLogins); await ModelService.InsertUser(accountCacheItem); if (user != null) diff --git a/src/GitHub.App/Services/PullRequestService.cs b/src/GitHub.App/Services/PullRequestService.cs index e87556aa20..98627affe7 100644 --- a/src/GitHub.App/Services/PullRequestService.cs +++ b/src/GitHub.App/Services/PullRequestService.cs @@ -537,7 +537,7 @@ async Task PushAndCreatePR(IRepositoryHost host, await Task.Delay(TimeSpan.FromSeconds(5)); var ret = await host.ModelService.CreatePullRequest(sourceRepository, targetRepository, sourceBranch, targetBranch, title, body); - await usageTracker.IncrementUpstreamPullRequestCount(); + await usageTracker.IncrementCounter(x => x.NumberOfUpstreamPullRequests); return ret; } diff --git a/src/GitHub.App/Services/RepositoryCloneService.cs b/src/GitHub.App/Services/RepositoryCloneService.cs index a127b6d7f5..8952585263 100644 --- a/src/GitHub.App/Services/RepositoryCloneService.cs +++ b/src/GitHub.App/Services/RepositoryCloneService.cs @@ -64,7 +64,7 @@ public async Task CloneRepository( try { await vsGitServices.Clone(cloneUrl, path, true, progress); - await usageTracker.IncrementCloneCount(); + await usageTracker.IncrementCounter(x => x.NumberOfClones); } catch (Exception ex) { diff --git a/src/GitHub.App/ViewModels/GistCreationViewModel.cs b/src/GitHub.App/ViewModels/GistCreationViewModel.cs index b3dfef458e..7935462ac5 100644 --- a/src/GitHub.App/ViewModels/GistCreationViewModel.cs +++ b/src/GitHub.App/ViewModels/GistCreationViewModel.cs @@ -91,7 +91,7 @@ IObservable OnCreateGist(object unused) newGist.Files.Add(FileName, SelectedText); return gistPublishService.PublishGist(apiClient, newGist) - .Do(_ => usageTracker.IncrementCreateGistCount().Forget()) + .Do(_ => usageTracker.IncrementCounter(x => x.NumberOfGists).Forget()) .Catch(ex => { if (!ex.IsCriticalException()) diff --git a/src/GitHub.App/ViewModels/PullRequestDetailViewModel.cs b/src/GitHub.App/ViewModels/PullRequestDetailViewModel.cs index 9302ddb8d8..42b858f87e 100644 --- a/src/GitHub.App/ViewModels/PullRequestDetailViewModel.cs +++ b/src/GitHub.App/ViewModels/PullRequestDetailViewModel.cs @@ -454,7 +454,7 @@ public async Task Load(string remoteRepositoryOwner, IPullRequestModel pullReque if (firstLoad) { - usageTracker.IncrementPullRequestOpened().Forget(); + usageTracker.IncrementCounter(x => x.NumberOfPullRequestsOpened).Forget(); } if (!isInCheckout) @@ -604,19 +604,37 @@ IObservable DoCheckout(object unused) .GetDefaultLocalBranchName(LocalRepository, Model.Number, Model.Title) .SelectMany(x => pullRequestsService.Checkout(LocalRepository, Model, x)); } - }).Do(_ => usageTracker.IncrementPullRequestCheckOutCount(IsFromFork).Forget()); + }).Do(_ => + { + if (IsFromFork) + usageTracker.IncrementCounter(x => x.NumberOfForkPullRequestsCheckedOut).Forget(); + else + usageTracker.IncrementCounter(x => x.NumberOfLocalPullRequestsCheckedOut).Forget(); + }); } IObservable DoPull(object unused) { return pullRequestsService.Pull(LocalRepository) - .Do(_ => usageTracker.IncrementPullRequestPullCount(IsFromFork).Forget()); + .Do(_ => + { + if (IsFromFork) + usageTracker.IncrementCounter(x => x.NumberOfForkPullRequestPulls).Forget(); + else + usageTracker.IncrementCounter(x => x.NumberOfLocalPullRequestPulls).Forget(); + }); } IObservable DoPush(object unused) { return pullRequestsService.Push(LocalRepository) - .Do(_ => usageTracker.IncrementPullRequestPushCount(IsFromFork).Forget()); + .Do(_ => + { + if (IsFromFork) + usageTracker.IncrementCounter(x => x.NumberOfForkPullRequestPushes).Forget(); + else + usageTracker.IncrementCounter(x => x.NumberOfLocalPullRequestPushes).Forget(); + }); } class CheckoutCommandState : IPullRequestCheckoutState diff --git a/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs b/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs index 1fb018166e..10eb1d0890 100644 --- a/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs +++ b/src/GitHub.App/ViewModels/RepositoryCreationViewModel.cs @@ -273,7 +273,7 @@ IObservable OnCreateRepository(object state) SelectedAccount, BaseRepositoryPath, repositoryHost.ApiClient) - .Do(_ => usageTracker.IncrementCreateCount().Forget()); + .Do(_ => usageTracker.IncrementCounter(x => x.NumberOfReposCreated).Forget()); } ReactiveCommand InitializeCreateRepositoryCommand() diff --git a/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs b/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs index fc77dcfee0..eb5d73b99d 100644 --- a/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs +++ b/src/GitHub.App/ViewModels/RepositoryPublishViewModel.cs @@ -159,7 +159,7 @@ IObservable OnPublishRepository(object arg) var account = SelectedAccount; return repositoryPublishService.PublishRepository(newRepository, account, SelectedHost.ApiClient) - .Do(_ => usageTracker.IncrementPublishCount().Forget()) + .Do(_ => usageTracker.IncrementCounter(x => x.NumberOfReposPublished).Forget()) .Select(_ => ProgressState.Success) .Catch(ex => { diff --git a/src/GitHub.Exports/GitHub.Exports.csproj b/src/GitHub.Exports/GitHub.Exports.csproj index 1c6bfe6270..fc81961298 100644 --- a/src/GitHub.Exports/GitHub.Exports.csproj +++ b/src/GitHub.Exports/GitHub.Exports.csproj @@ -147,6 +147,8 @@ + + diff --git a/src/GitHub.Exports/Models/UsageData.cs b/src/GitHub.Exports/Models/UsageData.cs new file mode 100644 index 0000000000..91bba9dcf3 --- /dev/null +++ b/src/GitHub.Exports/Models/UsageData.cs @@ -0,0 +1,20 @@ +using System; + +namespace GitHub.Models +{ + /// + /// Wraps a with a field. + /// + public class UsageData + { + /// + /// Gets or sets the last update time. + /// + public DateTimeOffset LastUpdated { get; set; } + + /// + /// Gets the model containing the current usage data. + /// + public UsageModel Model { get; set; } + } +} diff --git a/src/GitHub.Exports/Models/UsageModel.cs b/src/GitHub.Exports/Models/UsageModel.cs index 64eb222177..79deaf1b01 100644 --- a/src/GitHub.Exports/Models/UsageModel.cs +++ b/src/GitHub.Exports/Models/UsageModel.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace GitHub.Models { @@ -39,41 +40,26 @@ public class UsageModel public UsageModel Clone(bool includeWeekly, bool includeMonthly) { - return new UsageModel + var result = new UsageModel(); + var properties = result.GetType().GetRuntimeProperties(); + + foreach (var property in properties) { - IsGitHubUser = IsGitHubUser, - IsEnterpriseUser = IsEnterpriseUser, - AppVersion = AppVersion, - VSVersion = VSVersion, - Lang = Lang, - NumberOfStartups = NumberOfStartups, - NumberOfStartupsWeek = includeWeekly ? NumberOfStartupsWeek : 0, - NumberOfStartupsMonth = includeMonthly ? NumberOfStartupsMonth : 0, - NumberOfUpstreamPullRequests = NumberOfUpstreamPullRequests, - NumberOfClones = NumberOfClones, - NumberOfReposCreated = NumberOfReposCreated, - NumberOfReposPublished = NumberOfReposPublished, - NumberOfGists = NumberOfGists, - NumberOfOpenInGitHub = NumberOfOpenInGitHub, - NumberOfLinkToGitHub = NumberOfLinkToGitHub, - NumberOfLogins = NumberOfLogins, - NumberOfPullRequestsOpened = NumberOfPullRequestsOpened, - NumberOfLocalPullRequestsCheckedOut = NumberOfLocalPullRequestsCheckedOut, - NumberOfLocalPullRequestPulls = NumberOfLocalPullRequestPulls, - NumberOfLocalPullRequestPushes = NumberOfLocalPullRequestPushes, - NumberOfForkPullRequestsCheckedOut = NumberOfForkPullRequestsCheckedOut, - NumberOfForkPullRequestPulls = NumberOfForkPullRequestPulls, - NumberOfForkPullRequestPushes = NumberOfForkPullRequestPushes, - NumberOfWelcomeDocsClicks = NumberOfWelcomeDocsClicks, - NumberOfWelcomeTrainingClicks = NumberOfWelcomeTrainingClicks, - NumberOfGitHubPaneHelpClicks = NumberOfGitHubPaneHelpClicks, - NumberOfPRDetailsViewChanges = NumberOfPRDetailsViewChanges, - NumberOfPRDetailsViewFile = NumberOfPRDetailsViewFile, - NumberOfPRDetailsCompareWithSolution = NumberOfPRDetailsCompareWithSolution, - NumberOfPRDetailsOpenFileInSolution = NumberOfPRDetailsOpenFileInSolution, - NumberOfPRReviewDiffViewInlineCommentOpen = NumberOfPRReviewDiffViewInlineCommentOpen, - NumberOfPRReviewDiffViewInlineCommentPost = NumberOfPRReviewDiffViewInlineCommentPost, - }; + var cloneValue = property.PropertyType == typeof(int); + + if (property.Name == nameof(result.NumberOfStartupsWeek)) + cloneValue = includeWeekly; + else if (property.Name == nameof(result.NumberOfStartupsMonth)) + cloneValue = includeMonthly; + + if (cloneValue) + { + var value = property.GetValue(this); + property.SetValue(result, value); + } + } + + return result; } } } diff --git a/src/GitHub.Exports/Services/IUsageService.cs b/src/GitHub.Exports/Services/IUsageService.cs new file mode 100644 index 0000000000..3fda07d3d8 --- /dev/null +++ b/src/GitHub.Exports/Services/IUsageService.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using GitHub.Models; + +namespace GitHub.Services +{ + /// + /// Provides services for . + /// + public interface IUsageService + { + /// + /// Checks whether the last updated date is the same day as today. + /// + /// The last updated date. + /// True if the last updated date is the same day as today; otherwise false. + bool IsSameDay(DateTimeOffset lastUpdated); + + /// + /// Checks whether the last updated date is the same week as today. + /// + /// The last updated date. + /// True if the last updated date is the same week as today; otherwise false. + bool IsSameWeek(DateTimeOffset lastUpdated); + + /// + /// Checks whether the last updated date is the same month as today. + /// + /// The last updated date. + /// True if the last updated date is the same month as today; otherwise false. + bool IsSameMonth(DateTimeOffset lastUpdated); + + /// + /// Starts a timer. + /// + /// The callback to call when the timer ticks. + /// The timespan after which the callback will be called the first time. + /// The timespan after which the callback will be called subsequent times. + /// A disposable used to cancel the timer. + IDisposable StartTimer(Func callback, TimeSpan dueTime, TimeSpan period); + + /// + /// Reads the local usage data from disk. + /// + /// A task returning a object. + Task ReadLocalData(); + + /// + /// Writes the local usage data to disk. + /// + Task WriteLocalData(UsageData data); + } +} diff --git a/src/GitHub.Exports/Services/IUsageTracker.cs b/src/GitHub.Exports/Services/IUsageTracker.cs index 747e6d841a..54150ee62b 100644 --- a/src/GitHub.Exports/Services/IUsageTracker.cs +++ b/src/GitHub.Exports/Services/IUsageTracker.cs @@ -1,33 +1,15 @@ using GitHub.VisualStudio; using System.Runtime.InteropServices; using System.Threading.Tasks; +using System; +using System.Linq.Expressions; +using GitHub.Models; namespace GitHub.Services { [Guid(Guids.UsageTrackerId)] public interface IUsageTracker { - Task IncrementLaunchCount(); - Task IncrementCloneCount(); - Task IncrementCreateCount(); - Task IncrementPublishCount(); - Task IncrementOpenInGitHubCount(); - Task IncrementLinkToGitHubCount(); - Task IncrementCreateGistCount(); - Task IncrementUpstreamPullRequestCount(); - Task IncrementLoginCount(); - Task IncrementPullRequestCheckOutCount(bool fork); - Task IncrementPullRequestPullCount(bool fork); - Task IncrementPullRequestPushCount(bool fork); - Task IncrementPullRequestOpened(); - Task IncrementWelcomeDocsClicks(); - Task IncrementWelcomeTrainingClicks(); - Task IncrementGitHubPaneHelpClicks(); - Task IncrementPRDetailsViewChanges(); - Task IncrementPRDetailsViewFile(); - Task IncrementPRDetailsCompareWithSolution(); - Task IncrementPRDetailsOpenFileInSolution(); - Task IncrementPRReviewDiffViewInlineCommentOpen(); - Task IncrementPRReviewDiffViewInlineCommentPost(); + Task IncrementCounter(Expression> counter); } } diff --git a/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs b/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs index cd4db5503e..57a62abf9d 100644 --- a/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs +++ b/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs @@ -157,7 +157,7 @@ Tuple GetLineAndTrackingPoint(ITextView textV ExpandCollapsedRegions(textView, line.Extent); peekBroker.TriggerPeekSession(textView, trackingPoint, InlineCommentPeekRelationship.Instance.Name); - usageTracker.IncrementPRReviewDiffViewInlineCommentOpen().Forget(); + usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentOpen).Forget(); return Tuple.Create(line, trackingPoint); } diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs index 5af60a1f5d..6895ad7cc1 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -293,7 +293,7 @@ public async Task PostReviewComment( path, position); - await usageTracker.IncrementPRReviewDiffViewInlineCommentPost(); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); return new PullRequestReviewCommentModel { @@ -329,7 +329,7 @@ public async Task PostReviewComment( body, inReplyTo); - await usageTracker.IncrementPRReviewDiffViewInlineCommentPost(); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); return new PullRequestReviewCommentModel { diff --git a/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs b/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs index da21efba65..b7cded8a9a 100644 --- a/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs +++ b/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs @@ -154,11 +154,11 @@ void ShowWelcomeMessage() { case "show-training": visualStudioBrowser.OpenUrl(new Uri(TrainingUrl)); - usageTracker.IncrementWelcomeTrainingClicks().Forget(); + usageTracker.IncrementCounter(x => x.NumberOfWelcomeTrainingClicks).Forget(); break; case "show-docs": visualStudioBrowser.OpenUrl(new Uri(GitHubUrls.Documentation)); - usageTracker.IncrementWelcomeDocsClicks().Forget(); + usageTracker.IncrementCounter(x => x.NumberOfWelcomeDocsClicks).Forget(); break; case "dont-show-again": teamExplorerServices.HideNotification(welcomeMessageGuid); diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index db0729ecf3..b8d86da959 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -321,6 +321,7 @@ + diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs index c5bd629725..9effad032c 100644 --- a/src/GitHub.VisualStudio/GitHubPackage.cs +++ b/src/GitHub.VisualStudio/GitHubPackage.cs @@ -230,8 +230,10 @@ async Task CreateService(IAsyncServiceContainer container, CancellationT } else if (serviceType == typeof(IUsageTracker)) { - var uiProvider = await GetServiceAsync(typeof(IGitHubServiceProvider)) as IGitHubServiceProvider; - return new UsageTracker(uiProvider); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var serviceProvider = await GetServiceAsync(typeof(IGitHubServiceProvider)) as IGitHubServiceProvider; + var usageService = serviceProvider.GetService(); + return new UsageTracker(serviceProvider, usageService); } else if (serviceType == typeof(IUIProvider)) { diff --git a/src/GitHub.VisualStudio/Menus/BlameLink.cs b/src/GitHub.VisualStudio/Menus/BlameLink.cs index 73f9329883..9cc094c90c 100644 --- a/src/GitHub.VisualStudio/Menus/BlameLink.cs +++ b/src/GitHub.VisualStudio/Menus/BlameLink.cs @@ -30,7 +30,7 @@ public async void Activate(object data = null) var browser = ServiceProvider.TryGetService(); browser?.OpenUrl(link.ToUri()); - await UsageTracker.IncrementOpenInGitHubCount(); + await UsageTracker.IncrementCounter(x => x.NumberOfOpenInGitHub); } } } diff --git a/src/GitHub.VisualStudio/Menus/CopyLink.cs b/src/GitHub.VisualStudio/Menus/CopyLink.cs index ed16f28cdc..a87b6aeefe 100644 --- a/src/GitHub.VisualStudio/Menus/CopyLink.cs +++ b/src/GitHub.VisualStudio/Menus/CopyLink.cs @@ -32,7 +32,7 @@ public async void Activate(object data = null) Clipboard.SetText(link); var ns = ServiceProvider.TryGetService(); ns?.ShowMessage(Resources.LinkCopiedToClipboardMessage); - await UsageTracker.IncrementLinkToGitHubCount(); + await UsageTracker.IncrementCounter(x => x.NumberOfLinkToGitHub); } catch { diff --git a/src/GitHub.VisualStudio/Menus/OpenLink.cs b/src/GitHub.VisualStudio/Menus/OpenLink.cs index be570acc97..30b68ad5d0 100644 --- a/src/GitHub.VisualStudio/Menus/OpenLink.cs +++ b/src/GitHub.VisualStudio/Menus/OpenLink.cs @@ -28,7 +28,7 @@ public async void Activate(object data = null) var browser = ServiceProvider.TryGetService(); browser?.OpenUrl(link.ToUri()); - await UsageTracker.IncrementOpenInGitHubCount(); + await UsageTracker.IncrementCounter(x => x.NumberOfOpenInGitHub); } } } diff --git a/src/GitHub.VisualStudio/Services/UsageService.cs b/src/GitHub.VisualStudio/Services/UsageService.cs new file mode 100644 index 0000000000..8deec1f61a --- /dev/null +++ b/src/GitHub.VisualStudio/Services/UsageService.cs @@ -0,0 +1,136 @@ +using System; +using System.ComponentModel.Composition; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Helpers; +using GitHub.Models; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.Services +{ + [Export(typeof(IUsageService))] + public class UsageService : IUsageService + { + const string StoreFileName = "ghfvs.usage"; + static readonly Calendar cal = CultureInfo.InvariantCulture.Calendar; + readonly IGitHubServiceProvider serviceProvider; + string storePath; + + [ImportingConstructor] + public UsageService(IGitHubServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public bool IsSameDay(DateTimeOffset lastUpdated) + { + return lastUpdated.Date == DateTimeOffset.Now.Date; + } + + public bool IsSameWeek(DateTimeOffset lastUpdated) + { + return GetIso8601WeekOfYear(lastUpdated) == GetIso8601WeekOfYear(DateTimeOffset.Now); + } + + public bool IsSameMonth(DateTimeOffset lastUpdated) + { + return lastUpdated.Month == DateTimeOffset.Now.Month; + } + + public IDisposable StartTimer(Func callback, TimeSpan dueTime, TimeSpan period) + { + return new Timer( + async _ => + { + try { await callback(); } + catch { /* log.Warn("Failed submitting usage data", ex); */ } + }, + null, + dueTime, + period); + } + + public async Task ReadLocalData() + { + await Initialize(); + + var json = File.Exists(storePath) ? await ReadAllTextAsync(storePath) : null; + + try + { + return json != null ? + SimpleJson.DeserializeObject(json) : + new UsageData { Model = new UsageModel() }; + } + catch + { + return new UsageData { Model = new UsageModel() }; + } + } + + public async Task WriteLocalData(UsageData data) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(storePath)); + var json = SimpleJson.SerializeObject(data); + await WriteAllTextAsync(storePath, json); + } + catch + { + // log.Warn("Failed to write usage data", ex); + } + } + + async Task Initialize() + { + if (storePath == null) + { + await ThreadingHelper.SwitchToMainThreadAsync(); + + var program = serviceProvider.GetService(); + storePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + program.ApplicationName, + StoreFileName); + } + } + + async Task ReadAllTextAsync(string path) + { + using (var s = File.OpenRead(path)) + using (var r = new StreamReader(s, Encoding.UTF8)) + { + return await r.ReadToEndAsync(); + } + } + + async Task WriteAllTextAsync(string path, string text) + { + using (var s = new FileStream(path, FileMode.Create)) + using (var w = new StreamWriter(s, Encoding.UTF8)) + { + await w.WriteAsync(text); + } + } + + // http://blogs.msdn.com/b/shawnste/archive/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net.aspx + static int GetIso8601WeekOfYear(DateTimeOffset time) + { + // Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll + // be the same week# as whatever Thursday, Friday or Saturday are, + // and we always get those right + DayOfWeek day = cal.GetDayOfWeek(time.UtcDateTime); + if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday) + { + time = time.AddDays(3); + } + + // Return the week of our adjusted day + return cal.GetWeekOfYear(time.UtcDateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); + } + } +} diff --git a/src/GitHub.VisualStudio/Services/UsageTracker.cs b/src/GitHub.VisualStudio/Services/UsageTracker.cs index 202ecd7a64..4ee057db37 100644 --- a/src/GitHub.VisualStudio/Services/UsageTracker.cs +++ b/src/GitHub.VisualStudio/Services/UsageTracker.cs @@ -1,71 +1,39 @@ using System; using System.ComponentModel.Composition; -using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Text; -using System.Windows.Threading; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.Helpers; using GitHub.Models; using GitHub.Settings; using Task = System.Threading.Tasks.Task; -using GitHub.Extensions; -using System.Threading.Tasks; -using GitHub.Helpers; -using System.Threading; namespace GitHub.Services { public sealed class UsageTracker : IUsageTracker, IDisposable { - const string StoreFileName = "ghfvs.usage"; - static readonly Calendar cal = CultureInfo.InvariantCulture.Calendar; - readonly IGitHubServiceProvider gitHubServiceProvider; + bool initialized; IMetricsService client; + IUsageService service; IConnectionManager connectionManager; IPackageSettings userSettings; IVSServices vsservices; - Timer timer; - string storePath; - bool firstRun = true; - - Func fileExists; - Func readAllText; - Action writeAllText; - Action dirCreate; + IDisposable timer; + bool firstTick = true; [ImportingConstructor] - public UsageTracker(IGitHubServiceProvider gitHubServiceProvider) + public UsageTracker( + IGitHubServiceProvider gitHubServiceProvider, + IUsageService service) { this.gitHubServiceProvider = gitHubServiceProvider; - - fileExists = (path) => System.IO.File.Exists(path); - readAllText = (path, encoding) => - { - try - { - return System.IO.File.ReadAllText(path, encoding); - } - catch - { - return null; - } - }; - writeAllText = (path, content, encoding) => - { - try - { - System.IO.File.WriteAllText(path, content, encoding); - } - catch {} - }; - dirCreate = (path) => System.IO.Directory.CreateDirectory(path); - - this.timer = new Timer( - TimerTick, - null, - TimeSpan.FromMinutes(3), - TimeSpan.FromHours(8)); + this.service = service; + timer = StartTimer(); } public void Dispose() @@ -73,182 +41,26 @@ public void Dispose() timer?.Dispose(); } - public async Task IncrementLaunchCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfStartups; - ++usage.Model.NumberOfStartupsWeek; - ++usage.Model.NumberOfStartupsMonth; - SaveUsage(usage); - } - - public async Task IncrementCloneCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfClones; - SaveUsage(usage); - } - - public async Task IncrementCreateCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfReposCreated; - SaveUsage(usage); - } - - public async Task IncrementPublishCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfReposPublished; - SaveUsage(usage); - } - - public async Task IncrementOpenInGitHubCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfOpenInGitHub; - SaveUsage(usage); - } - - public async Task IncrementLinkToGitHubCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfLinkToGitHub; - SaveUsage(usage); - } - - public async Task IncrementCreateGistCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfGists; - SaveUsage(usage); - } - - public async Task IncrementUpstreamPullRequestCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfUpstreamPullRequests; - SaveUsage(usage); - } - - public async Task IncrementLoginCount() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfLogins; - SaveUsage(usage); - } - - public async Task IncrementPullRequestCheckOutCount(bool fork) - { - var usage = await LoadUsage(); - - if (fork) - ++usage.Model.NumberOfForkPullRequestsCheckedOut; - else - ++usage.Model.NumberOfLocalPullRequestsCheckedOut; - - SaveUsage(usage); - } - - public async Task IncrementPullRequestPushCount(bool fork) - { - var usage = await LoadUsage(); - - if (fork) - ++usage.Model.NumberOfForkPullRequestPushes; - else - ++usage.Model.NumberOfLocalPullRequestPushes; - - SaveUsage(usage); - } - - public async Task IncrementPullRequestPullCount(bool fork) - { - var usage = await LoadUsage(); - - if (fork) - ++usage.Model.NumberOfForkPullRequestPulls; - else - ++usage.Model.NumberOfLocalPullRequestPulls; - - SaveUsage(usage); - } - - public async Task IncrementWelcomeDocsClicks() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfWelcomeDocsClicks; - SaveUsage(usage); - } - - public async Task IncrementWelcomeTrainingClicks() + public async Task IncrementCounter(Expression> counter) { var usage = await LoadUsage(); - ++usage.Model.NumberOfWelcomeTrainingClicks; - SaveUsage(usage); + var property = (MemberExpression)counter.Body; + var propertyInfo = (PropertyInfo)property.Member; + var value = (int)propertyInfo.GetValue(usage.Model); + propertyInfo.SetValue(usage.Model, value + 1); + await service.WriteLocalData(usage); } - public async Task IncrementGitHubPaneHelpClicks() + IDisposable StartTimer() { - var usage = await LoadUsage(); - ++usage.Model.NumberOfGitHubPaneHelpClicks; - SaveUsage(usage); - } - - public async Task IncrementPullRequestOpened() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPullRequestsOpened; - SaveUsage(usage); - } - - public async Task IncrementPRDetailsViewChanges() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPRDetailsViewChanges; - SaveUsage(usage); - } - - public async Task IncrementPRDetailsViewFile() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPRDetailsViewFile; - SaveUsage(usage); - } - - public async Task IncrementPRDetailsCompareWithSolution() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPRDetailsCompareWithSolution; - SaveUsage(usage); - } - - public async Task IncrementPRDetailsOpenFileInSolution() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPRDetailsOpenFileInSolution; - SaveUsage(usage); - } - - public async Task IncrementPRReviewDiffViewInlineCommentOpen() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPRReviewDiffViewInlineCommentOpen; - SaveUsage(usage); - } - - public async Task IncrementPRReviewDiffViewInlineCommentPost() - { - var usage = await LoadUsage(); - ++usage.Model.NumberOfPRReviewDiffViewInlineCommentPost; - SaveUsage(usage); + return service.StartTimer(TimerTick, TimeSpan.FromMinutes(3), TimeSpan.FromHours(8)); } 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) + if (!initialized) { await ThreadingHelper.SwitchToMainThreadAsync(); @@ -256,64 +68,36 @@ async Task Initialize() connectionManager = gitHubServiceProvider.GetService(); userSettings = gitHubServiceProvider.GetService(); vsservices = gitHubServiceProvider.GetService(); - - var program = gitHubServiceProvider.GetService(); - storePath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - program.ApplicationName, - StoreFileName); + initialized = true; } } - async Task LoadUsage() + async Task IncrementLaunchCount() { - await Initialize(); + var usage = await LoadUsage(); + ++usage.Model.NumberOfStartups; + ++usage.Model.NumberOfStartupsWeek; + ++usage.Model.NumberOfStartupsMonth; + await service.WriteLocalData(usage); + } - var json = fileExists(storePath) ? readAllText(storePath, Encoding.UTF8) : null; - UsageStore result = null; - try - { - result = json != null ? - SimpleJson.DeserializeObject(json) : - new UsageStore { Model = new UsageModel() }; - } - catch - { - result = new UsageStore { Model = new UsageModel() }; - } + async Task LoadUsage() + { + await Initialize(); + var result = await service.ReadLocalData(); result.Model.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; result.Model.AppVersion = AssemblyVersionInformation.Version; result.Model.VSVersion = vsservices.VSVersion; - return result; } - void SaveUsage(UsageStore store) - { - dirCreate(System.IO.Path.GetDirectoryName(storePath)); - var json = SimpleJson.SerializeObject(store); - writeAllText(storePath, json, Encoding.UTF8); - } - - void TimerTick(object state) - { - TimerTick() - .Catch(ex => - { - //log.Warn("Failed submitting usage data", ex); - }) - .Forget(); - } - async Task TimerTick() { - await Initialize(); - - if (firstRun) + if (firstTick) { await IncrementLaunchCount(); - firstRun = false; + firstTick = false; } if (client == null || !userSettings.CollectMetrics) @@ -330,16 +114,16 @@ async Task TimerTick() var usage = await LoadUsage(); var lastDate = usage.LastUpdated; var currentDate = DateTimeOffset.Now; - var includeWeekly = GetIso8601WeekOfYear(lastDate) != GetIso8601WeekOfYear(currentDate); - var includeMonthly = lastDate.Month != currentDate.Month; + var includeWeekly = !service.IsSameWeek(usage.LastUpdated); + var includeMonthly = !service.IsSameMonth(usage.LastUpdated); // Only send stats once a day. - if (lastDate.Date != currentDate.Date) + if (!service.IsSameDay(usage.LastUpdated)) { await SendUsage(usage.Model, includeWeekly, includeMonthly); ClearCounters(usage.Model, includeWeekly, includeMonthly); usage.LastUpdated = DateTimeOffset.Now.UtcDateTime; - SaveUsage(usage); + await service.WriteLocalData(usage); } } @@ -364,61 +148,24 @@ async Task SendUsage(UsageModel usage, bool weekly, bool monthly) await client.PostUsage(model); } - // http://blogs.msdn.com/b/shawnste/archive/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net.aspx - static int GetIso8601WeekOfYear(DateTimeOffset time) - { - // Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll - // be the same week# as whatever Thursday, Friday or Saturday are, - // and we always get those right - DayOfWeek day = cal.GetDayOfWeek(time.UtcDateTime); - if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday) - { - time = time.AddDays(3); - } - - // Return the week of our adjusted day - return cal.GetWeekOfYear(time.UtcDateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); - } - static void ClearCounters(UsageModel usage, bool weekly, bool monthly) { - usage.NumberOfStartups = 0; - usage.NumberOfClones = 0; - usage.NumberOfReposCreated = 0; - usage.NumberOfReposPublished = 0; - usage.NumberOfGists = 0; - usage.NumberOfOpenInGitHub = 0; - usage.NumberOfLinkToGitHub = 0; - usage.NumberOfLogins = 0; - usage.NumberOfUpstreamPullRequests = 0; - usage.NumberOfPullRequestsOpened = 0; - usage.NumberOfLocalPullRequestsCheckedOut = 0; - usage.NumberOfLocalPullRequestPulls = 0; - usage.NumberOfLocalPullRequestPushes = 0; - usage.NumberOfForkPullRequestsCheckedOut = 0; - usage.NumberOfForkPullRequestPulls = 0; - usage.NumberOfForkPullRequestPushes = 0; - usage.NumberOfGitHubPaneHelpClicks = 0; - usage.NumberOfWelcomeTrainingClicks = 0; - usage.NumberOfWelcomeDocsClicks = 0; - usage.NumberOfPRDetailsViewChanges = 0; - usage.NumberOfPRDetailsViewFile = 0; - usage.NumberOfPRDetailsCompareWithSolution = 0; - usage.NumberOfPRDetailsOpenFileInSolution = 0; - usage.NumberOfPRReviewDiffViewInlineCommentOpen = 0; - usage.NumberOfPRReviewDiffViewInlineCommentPost = 0; - - if (weekly) - usage.NumberOfStartupsWeek = 0; + var properties = usage.GetType().GetRuntimeProperties(); - if (monthly) - usage.NumberOfStartupsMonth = 0; - } + foreach (var property in properties) + { + var setValue = property.PropertyType == typeof(int); - class UsageStore - { - public DateTimeOffset LastUpdated { get; set; } - public UsageModel Model { get; set; } + if (property.Name == nameof(usage.NumberOfStartupsWeek)) + setValue = weekly; + else if (property.Name == nameof(usage.NumberOfStartupsMonth)) + setValue = monthly; + + if (setValue) + { + property.SetValue(usage, 0); + } + } } } } diff --git a/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs b/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs index 3bacd6cdac..a785015fb8 100644 --- a/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs +++ b/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs @@ -4,6 +4,8 @@ using GitHub.Exports; using Microsoft.VisualStudio.Shell; using System.Threading.Tasks; +using GitHub.Models; +using System.Linq.Expressions; namespace GitHub.Services { @@ -19,27 +21,6 @@ public UsageTrackerDispatcher([Import(typeof(SVsServiceProvider))] IServiceProvi 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(); - public Task IncrementPullRequestOpened() => inner.IncrementPullRequestOpened(); - public Task IncrementPullRequestCheckOutCount(bool fork) => inner.IncrementPullRequestCheckOutCount(fork); - public Task IncrementPullRequestPullCount(bool fork) => inner.IncrementPullRequestPullCount(fork); - public Task IncrementPullRequestPushCount(bool fork) => inner.IncrementPullRequestPushCount(fork); - public Task IncrementWelcomeDocsClicks() => inner.IncrementWelcomeDocsClicks(); - public Task IncrementWelcomeTrainingClicks() => inner.IncrementWelcomeTrainingClicks(); - public Task IncrementGitHubPaneHelpClicks() => inner.IncrementGitHubPaneHelpClicks(); - public Task IncrementPRDetailsViewChanges() => inner.IncrementPRDetailsViewChanges(); - public Task IncrementPRDetailsViewFile() => inner.IncrementPRDetailsViewFile(); - public Task IncrementPRDetailsCompareWithSolution() => inner.IncrementPRDetailsCompareWithSolution(); - public Task IncrementPRDetailsOpenFileInSolution() => inner.IncrementPRDetailsOpenFileInSolution(); - public Task IncrementPRReviewDiffViewInlineCommentOpen() => inner.IncrementPRReviewDiffViewInlineCommentOpen(); - public Task IncrementPRReviewDiffViewInlineCommentPost() => inner.IncrementPRReviewDiffViewInlineCommentPost(); + public Task IncrementCounter(Expression> counter) => inner.IncrementCounter(counter); } } diff --git a/src/GitHub.VisualStudio/UI/Views/GitHubPaneViewModel.cs b/src/GitHub.VisualStudio/UI/Views/GitHubPaneViewModel.cs index 09c163c345..4eca3d8fa2 100644 --- a/src/GitHub.VisualStudio/UI/Views/GitHubPaneViewModel.cs +++ b/src/GitHub.VisualStudio/UI/Views/GitHubPaneViewModel.cs @@ -142,7 +142,7 @@ public override void Initialize(IServiceProvider serviceProvider) () => { browser.OpenUrl(new Uri(GitHubUrls.Documentation)); - usageTracker.IncrementGitHubPaneHelpClicks().Forget(); + usageTracker.IncrementCounter(x => x.NumberOfGitHubPaneHelpClicks).Forget(); }, true); diff --git a/src/GitHub.VisualStudio/UI/Views/PullRequestDetailView.xaml.cs b/src/GitHub.VisualStudio/UI/Views/PullRequestDetailView.xaml.cs index 2436540919..9b4fea911e 100644 --- a/src/GitHub.VisualStudio/UI/Views/PullRequestDetailView.xaml.cs +++ b/src/GitHub.VisualStudio/UI/Views/PullRequestDetailView.xaml.cs @@ -109,9 +109,9 @@ async Task DoOpenFile(IPullRequestFileNode file, bool workingDirectory) } if (workingDirectory) - await UsageTracker.IncrementPRDetailsOpenFileInSolution(); + await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsOpenFileInSolution); else - await UsageTracker.IncrementPRDetailsViewFile(); + await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsViewFile); } catch (Exception e) { @@ -169,9 +169,9 @@ async Task DoDiffFile(IPullRequestFileNode file, bool workingDirectory) } if (workingDirectory) - await UsageTracker.IncrementPRDetailsCompareWithSolution(); + await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsCompareWithSolution); else - await UsageTracker.IncrementPRDetailsViewChanges(); + await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsViewChanges); } catch (Exception e) { diff --git a/test/UnitTests/GitHub.App/Models/RepositoryHostTests.cs b/test/UnitTests/GitHub.App/Models/RepositoryHostTests.cs index e586daa5a8..bb54620e24 100644 --- a/test/UnitTests/GitHub.App/Models/RepositoryHostTests.cs +++ b/test/UnitTests/GitHub.App/Models/RepositoryHostTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Net; using System.Reactive.Linq; using System.Threading.Tasks; @@ -62,8 +63,11 @@ public async Task IncrementsLoginCount() var host = new RepositoryHost(apiClient, modelService, loginManager, loginCache, usage); var result = await host.LogIn("baymax", "aPassword"); + var model = new UsageModel(); - await usage.Received().IncrementLoginCount(); + await usage.Received().IncrementCounter( + Arg.Is>>(x => + ((MemberExpression)x.Body).Member.Name == nameof(model.NumberOfLogins))); } [Fact] @@ -124,8 +128,11 @@ public async Task IncrementsLoginCount() var host = new RepositoryHost(apiClient, modelService, loginManager, loginCache, usage); var result = await host.LogInFromCache(); + var model = new UsageModel(); - await usage.Received().IncrementLoginCount(); + await usage.Received().IncrementCounter( + Arg.Is>>(x => + ((MemberExpression)x.Body).Member.Name == nameof(model.NumberOfLogins))); } } } diff --git a/test/UnitTests/GitHub.App/Services/RepositoryCloneServiceTests.cs b/test/UnitTests/GitHub.App/Services/RepositoryCloneServiceTests.cs index 44f44aa7da..5cc914b165 100644 --- a/test/UnitTests/GitHub.App/Services/RepositoryCloneServiceTests.cs +++ b/test/UnitTests/GitHub.App/Services/RepositoryCloneServiceTests.cs @@ -4,6 +4,9 @@ using Xunit; using UnitTests; using GitHub.Services; +using System.Linq.Expressions; +using System; +using GitHub.Models; public class RepositoryCloneServiceTests { @@ -20,7 +23,7 @@ public async Task ClonesToRepositoryPath() await cloneService.CloneRepository("https://github.com/foo/bar", "bar", @"c:\dev"); operatingSystem.Directory.Received().CreateDirectory(@"c:\dev\bar"); - vsGitServices.Received().Clone("https://github.com/foo/bar", @"c:\dev\bar", true); + await vsGitServices.Received().Clone("https://github.com/foo/bar", @"c:\dev\bar", true); } [Fact] @@ -33,8 +36,11 @@ public async Task UpdatesMetricsWhenRepositoryCloned() var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, usageTracker); await cloneService.CloneRepository("https://github.com/foo/bar", "bar", @"c:\dev"); + var model = new UsageModel(); - usageTracker.Received().IncrementCloneCount(); + await usageTracker.Received().IncrementCounter( + Arg.Is>>(x => + ((MemberExpression)x.Body).Member.Name == nameof(model.NumberOfClones))); } } } diff --git a/test/UnitTests/GitHub.VisualStudio/Services/UsageTrackerTests.cs b/test/UnitTests/GitHub.VisualStudio/Services/UsageTrackerTests.cs new file mode 100644 index 0000000000..94e8905ce3 --- /dev/null +++ b/test/UnitTests/GitHub.VisualStudio/Services/UsageTrackerTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.ObjectModel; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Services; +using GitHub.Settings; +using NSubstitute; +using Xunit; + +namespace UnitTests.GitHub.VisualStudio.Services +{ + public class UsageTrackerTests + { + public class TheTimer : TestBaseClass + { + [Fact] + public void ShouldStartTimer() + { + var service = Substitute.For(); + var target = new UsageTracker(CreateServiceProvider(), service); + + service.Received(1).StartTimer(Arg.Any>(), TimeSpan.FromMinutes(3), TimeSpan.FromHours(8)); + } + + [Fact] + public async Task FirstTickShouldIncrementLaunchCount() + { + var service = CreateUsageService(); + var targetAndTick = CreateTargetAndGetTick(CreateServiceProvider(), service); + + await targetAndTick.Item2(); + + await service.Received(1).WriteLocalData( + Arg.Is(x => + x.Model.NumberOfStartups == 1 && + x.Model.NumberOfStartupsWeek == 1 && + x.Model.NumberOfStartupsMonth == 1)); + } + + [Fact] + public async Task SubsequentTickShouldNotIncrementLaunchCount() + { + var service = CreateUsageService(); + var targetAndTick = CreateTargetAndGetTick(CreateServiceProvider(), service); + + await targetAndTick.Item2(); + service.ClearReceivedCalls(); + await targetAndTick.Item2(); + + await service.DidNotReceiveWithAnyArgs().WriteLocalData(null); + } + + [Fact] + public async Task ShouldDisposeTimerIfMetricsServiceNotFound() + { + var service = CreateUsageService(); + var disposed = false; + var disposable = Disposable.Create(() => disposed = true); + service.StartTimer(null, new TimeSpan(), new TimeSpan()).ReturnsForAnyArgs(disposable); + + var targetAndTick = CreateTargetAndGetTick( + CreateServiceProvider(hasMetricsService: false), + service); + + await targetAndTick.Item2(); + + Assert.True(disposed); + } + + [Fact] + public async Task TickShouldNotSendDataIfSameDay() + { + var serviceProvider = CreateServiceProvider(); + var targetAndTick = CreateTargetAndGetTick( + serviceProvider, + CreateUsageService()); + + await targetAndTick.Item2(); + + var metricsService = serviceProvider.TryGetService(); + await metricsService.DidNotReceive().PostUsage(Arg.Any()); + } + + [Fact] + public async Task TickShouldSendDataIfDifferentDay() + { + var serviceProvider = CreateServiceProvider(); + var targetAndTick = CreateTargetAndGetTick( + serviceProvider, + CreateUsageService(sameDay: false)); + + await targetAndTick.Item2(); + + var metricsService = serviceProvider.TryGetService(); + await metricsService.Received(1).PostUsage(Arg.Any()); + } + + [Fact] + public async Task NonWeeklyOrMonthlyCountersShouldBeZeroed() + { + var service = CreateUsageService(new UsageModel + { + NumberOfStartups = 1, + NumberOfStartupsWeek = 1, + NumberOfStartupsMonth = 1, + NumberOfClones = 1, + }, sameDay: false); + Func tick = null; + + service.WhenForAnyArgs(x => x.StartTimer(null, new TimeSpan(), new TimeSpan())) + .Do(x => tick = x.ArgAt>(0)); + + var serviceProvider = CreateServiceProvider(); + var target = new UsageTracker(serviceProvider, service); + + await tick(); + + await service.Received().WriteLocalData( + Arg.Is(x => + x.Model.NumberOfStartups == 0 && + x.Model.NumberOfStartupsWeek == 2 && + x.Model.NumberOfStartupsMonth == 2 && + x.Model.NumberOfClones == 0)); + } + + [Fact] + public async Task NonMonthlyCountersShouldBeZeroed() + { + var service = CreateUsageService(new UsageModel + { + NumberOfStartups = 1, + NumberOfStartupsWeek = 1, + NumberOfStartupsMonth = 1, + NumberOfClones = 1, + }, sameDay: false, sameWeek: false); + Func tick = null; + + service.WhenForAnyArgs(x => x.StartTimer(null, new TimeSpan(), new TimeSpan())) + .Do(x => tick = x.ArgAt>(0)); + + var serviceProvider = CreateServiceProvider(); + var target = new UsageTracker(serviceProvider, service); + + await tick(); + + await service.Received().WriteLocalData( + Arg.Is(x => + x.Model.NumberOfStartups == 0 && + x.Model.NumberOfStartupsWeek == 0 && + x.Model.NumberOfStartupsMonth == 2 && + x.Model.NumberOfClones == 0)); + } + + [Fact] + public async Task AllCountersShouldBeZeroed() + { + var service = CreateUsageService(new UsageModel + { + NumberOfStartups = 1, + NumberOfStartupsWeek = 1, + NumberOfStartupsMonth = 1, + NumberOfClones = 1, + }, sameDay: false, sameWeek: false, sameMonth: false); + Func tick = null; + + service.WhenForAnyArgs(x => x.StartTimer(null, new TimeSpan(), new TimeSpan())) + .Do(x => tick = x.ArgAt>(0)); + + var serviceProvider = CreateServiceProvider(); + var target = new UsageTracker(serviceProvider, service); + + await tick(); + + await service.Received().WriteLocalData( + Arg.Is(x => + x.Model.NumberOfStartups == 0 && + x.Model.NumberOfStartupsWeek == 0 && + x.Model.NumberOfStartupsMonth == 0 && + x.Model.NumberOfClones == 0)); + } + } + + public class TheIncrementCounterMethod : TestBaseClass + { + [Fact] + public async Task ShouldIncrementCounter() + { + var model = new UsageModel { NumberOfClones = 4 }; + var target = new UsageTracker( + CreateServiceProvider(), + CreateUsageService(model)); + + await target.IncrementCounter(x => x.NumberOfClones); + + Assert.Equal(5, model.NumberOfClones); + } + + [Fact] + public async Task ShouldWriteUpdatedData() + { + var data = new UsageData { Model = new UsageModel() }; + var service = CreateUsageService(data); + var target = new UsageTracker( + CreateServiceProvider(), + service); + + await target.IncrementCounter(x => x.NumberOfClones); + + await service.Received(1).WriteLocalData(data); + } + } + + static Tuple> CreateTargetAndGetTick( + IGitHubServiceProvider serviceProvider, + IUsageService service) + { + Func tick = null; + + service.WhenForAnyArgs(x => x.StartTimer(null, new TimeSpan(), new TimeSpan())) + .Do(x => tick = x.ArgAt>(0)); + + var target = new UsageTracker(serviceProvider, service); + + return Tuple.Create(target, tick); + } + + static IGitHubServiceProvider CreateServiceProvider(bool hasMetricsService = true) + { + var result = Substitute.For(); + var connectionManager = Substitute.For(); + var metricsService = Substitute.For(); + var packageSettings = Substitute.For(); + + connectionManager.Connections.Returns(new ObservableCollection()); + packageSettings.CollectMetrics.Returns(true); + + result.GetService().Returns(connectionManager); + result.GetService().Returns(packageSettings); + result.TryGetService().Returns(hasMetricsService ? metricsService : null); + + return result; + } + + static IUsageService CreateUsageService( + bool sameDay = true, + bool sameWeek = true, + bool sameMonth = true) + { + return CreateUsageService(new UsageModel(), sameDay, sameWeek, sameMonth); + } + + static IUsageService CreateUsageService( + UsageModel model, + bool sameDay = true, + bool sameWeek = true, + bool sameMonth = true) + { + return CreateUsageService(new UsageData + { + LastUpdated = DateTimeOffset.Now, + Model = model + }, sameDay, sameWeek, sameMonth); + } + + static IUsageService CreateUsageService( + UsageData data, + bool sameDay = true, + bool sameWeek = true, + bool sameMonth = true) + { + var result = Substitute.For(); + result.ReadLocalData().Returns(data); + result.IsSameDay(DateTimeOffset.Now).ReturnsForAnyArgs(sameDay); + result.IsSameWeek(DateTimeOffset.Now).ReturnsForAnyArgs(sameWeek); + result.IsSameMonth(DateTimeOffset.Now).ReturnsForAnyArgs(sameMonth); + return result; + } + } +} diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 81e256311b..e63ac9d429 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -261,6 +261,7 @@ +