1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-06 23:34:05 +02:00

Implement feature usage analytics

This commit is contained in:
chylex 2017-11-08 08:15:45 +01:00
parent 51f9ba3642
commit b81e7583eb
13 changed files with 153 additions and 25 deletions

View File

@ -319,6 +319,7 @@ protected override void WndProc(ref Message m){
}
else{
browser.OnMouseClickExtra(m.WParam);
TriggerAnalyticsEvent(AnalyticsFile.Event.BrowserExtraMouseButton);
}
return;
@ -327,7 +328,7 @@ protected override void WndProc(ref Message m){
base.WndProc(ref m);
}
// notification helpers
// bridge methods
public void PauseNotification(){
notification.PauseNotification();
@ -336,9 +337,7 @@ public void PauseNotification(){
public void ResumeNotification(){
notification.ResumeNotification();
}
// browser bridge methods
public void ReinjectCustomCSS(string css){
browser.ReinjectCustomCSS(css);
}
@ -355,6 +354,10 @@ public void ApplyROT13(){
browser.ApplyROT13();
}
public void TriggerAnalyticsEvent(AnalyticsFile.Event e){
analytics?.TriggerEvent(e);
}
// callback handlers
public void OnIntroductionClosed(bool showGuide, bool allowDataCollection){
@ -423,19 +426,22 @@ public void OpenSettings(Type startTab){
notification.RequiresResize = true;
form.Dispose();
};
TriggerAnalyticsEvent(AnalyticsFile.Event.OpenOptions);
ShowChildForm(form);
}
}
public void OpenAbout(){
if (!FormManager.TryBringToFront<FormAbout>()){
TriggerAnalyticsEvent(AnalyticsFile.Event.OpenAbout);
ShowChildForm(new FormAbout());
}
}
public void OpenPlugins(){
if (!FormManager.TryBringToFront<FormPlugins>()){
TriggerAnalyticsEvent(AnalyticsFile.Event.OpenPlugins);
ShowChildForm(new FormPlugins(plugins));
}
}
@ -458,6 +464,8 @@ public void PlayNotificationSound(){
soundNotification.SetVolume(Config.NotificationSoundVolume);
soundNotification.Play(Config.NotificationSoundPath);
TriggerAnalyticsEvent(AnalyticsFile.Event.SoundNotification);
}
public void PlayVideo(string url, string username){
@ -475,6 +483,7 @@ public void PlayVideo(string url, string username){
}
videoPlayer.Launch(url, username);
TriggerAnalyticsEvent(AnalyticsFile.Event.VideoPlay);
}
public bool ProcessBrowserKey(Keys key){
@ -496,6 +505,7 @@ public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl)
notification.FinishCurrentNotification();
browser.ShowTweetDetail(columnId, chirpId, fallbackUrl);
TriggerAnalyticsEvent(AnalyticsFile.Event.TweetDetail);
}
public void OnTweetScreenshotReady(string html, int width, int height){
@ -504,6 +514,7 @@ public void OnTweetScreenshotReady(string html, int width, int height){
}
notificationScreenshotManager.Trigger(html, width, height);
TriggerAnalyticsEvent(AnalyticsFile.Event.TweetScreenshot);
}
public void DisplayTooltip(string text){

View File

@ -2,6 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling{
@ -95,6 +96,8 @@ public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser br
}
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(() => form.TriggerAnalyticsEvent(AnalyticsFile.Event.BrowserContextMenu));
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
@ -163,6 +166,7 @@ public static ContextMenu CreateMenu(FormBrowser form){
menu.Popup += (sender, args) => {
menu.MenuItems[1].Checked = Program.UserConfig.MuteNotifications;
form.TriggerAnalyticsEvent(AnalyticsFile.Event.BrowserContextMenu);
};
return menu;

View File

@ -1,6 +1,7 @@
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuNotification : ContextMenuBase{
@ -54,7 +55,10 @@ public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser br
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(() => form.ContextMenuOpen = true);
form.InvokeAsyncSafe(() => {
form.ContextMenuOpen = true;
form.TriggerAnalyticsEvent(AnalyticsFile.Event.NotificationContextMenu);
});
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){

View File

@ -2,6 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Handling {
sealed class KeyboardHandlerNotification : IKeyboardHandler{
@ -11,19 +12,26 @@ public KeyboardHandlerNotification(FormNotificationBase notification){
this.notification = notification;
}
private void TriggerKeyboardShortcutAnalytics(){
notification.InvokeAsyncSafe(() => notification.TriggerAnalyticsEvent(AnalyticsFile.Event.NotificationKeyboardShortcut));
}
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){
if (type == KeyType.RawKeyDown && !browser.FocusedFrame.Url.StartsWith("chrome-devtools://")){
switch((Keys)windowsKeyCode){
case Keys.Enter:
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
TriggerKeyboardShortcutAnalytics();
return true;
case Keys.Escape:
notification.InvokeAsyncSafe(notification.HideNotification);
TriggerKeyboardShortcutAnalytics();
return true;
case Keys.Space:
notification.InvokeAsyncSafe(() => notification.FreezeTimer = !notification.FreezeTimer);
TriggerKeyboardShortcutAnalytics();
return true;
}
}

View File

@ -8,6 +8,7 @@
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Management;
using TweetDuck.Core.Utils;
@ -158,6 +159,10 @@ protected override void WndProc(ref Message m){
base.WndProc(ref m);
}
public void TriggerAnalyticsEvent(AnalyticsFile.Event e){
owner.TriggerAnalyticsEvent(e);
}
// event handlers
private void owner_FormClosed(object sender, FormClosedEventArgs e){

View File

@ -5,6 +5,7 @@
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins;
@ -127,6 +128,7 @@ private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam){
}
blockXButtonUp = true;
this.InvokeAsyncSafe(() => TriggerAnalyticsEvent(AnalyticsFile.Event.NotificationExtraMouseButton));
return NativeMethods.HOOK_HANDLED;
}
else if (eventType == NativeMethods.WM_XBUTTONUP && blockXButtonUp){

View File

@ -3,6 +3,7 @@
using System.Drawing;
using TweetDuck.Plugins;
using System.Windows.Forms;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Notification{
@ -93,6 +94,7 @@ public override void ShowNotification(TweetNotification notification){
}
needsTrim |= tweetQueue.Count >= TrimMinimum;
TriggerAnalyticsEvent(AnalyticsFile.Event.DesktopNotification);
}
public override void HideNotification(){

View File

@ -12,12 +12,41 @@ static AnalyticsFile(){
});
}
public enum Event{
OpenOptions, OpenPlugins, OpenAbout, OpenGuide,
DesktopNotification, SoundNotification,
BrowserContextMenu, BrowserExtraMouseButton,
NotificationContextMenu, NotificationExtraMouseButton, NotificationKeyboardShortcut,
TweetScreenshot, TweetDetail, VideoPlay, GCReload
}
// STATE PROPERTIES
public DateTime LastDataCollection { get; set; } = DateTime.MinValue;
public string LastCollectionVersion { get; set; } = null;
public string LastCollectionMessage { get; set; } = null;
// USAGE DATA
public int CountOpenOptions { get; private set; } = 0;
public int CountOpenPlugins { get; private set; } = 0;
public int CountOpenAbout { get; private set; } = 0;
public int CountOpenGuide { get; private set; } = 0;
public int CountDesktopNotifications { get; private set; } = 0;
public int CountSoundNotifications { get; private set; } = 0;
public int CountBrowserContextMenus { get; private set; } = 0;
public int CountBrowserExtraMouseButtons { get; private set; } = 0;
public int CountNotificationContextMenus { get; private set; } = 0;
public int CountNotificationExtraMouseButtons { get; private set; } = 0;
public int CountNotificationKeyboardShortcuts { get; private set; } = 0;
public int CountTweetScreenshots { get; private set; } = 0;
public int CountTweetDetails { get; private set; } = 0;
public int CountVideoPlays { get; private set; } = 0;
public int CountGCReloads { get; private set; } = 0;
// END OF DATA
private readonly string file;
@ -26,6 +55,29 @@ private AnalyticsFile(string file){
this.file = file;
}
public void TriggerEvent(Event e){
switch(e){
case Event.OpenOptions: ++CountOpenOptions; break;
case Event.OpenPlugins: ++CountOpenPlugins; break;
case Event.OpenAbout: ++CountOpenAbout; break;
case Event.OpenGuide: ++CountOpenGuide; break;
case Event.DesktopNotification: ++CountDesktopNotifications; break;
case Event.SoundNotification: ++CountSoundNotifications; break;
case Event.BrowserContextMenu: ++CountBrowserContextMenus; break;
case Event.BrowserExtraMouseButton: ++CountBrowserExtraMouseButtons; break;
case Event.NotificationContextMenu: ++CountNotificationContextMenus; break;
case Event.NotificationExtraMouseButton: ++CountNotificationExtraMouseButtons; break;
case Event.NotificationKeyboardShortcut: ++CountNotificationKeyboardShortcuts; break;
case Event.TweetScreenshot: ++CountTweetScreenshots; break;
case Event.TweetDetail: ++CountTweetDetails; break;
case Event.VideoPlay: ++CountVideoPlays; break;
case Event.GCReload: ++CountGCReloads; break;
}
}
public void Save(){
try{
Serializer.Write(file, this);

View File

@ -11,22 +11,24 @@ sealed class AnalyticsManager : IDisposable{
private static readonly TimeSpan CollectionInterval = TimeSpan.FromDays(7);
private static readonly Uri CollectionUrl = new Uri("https://tweetduck.chylex.com/breadcrumb/report");
public string LastCollectionMessage => file.LastCollectionMessage;
public AnalyticsFile File { get; }
private readonly FormBrowser browser;
private readonly PluginManager plugins;
private readonly AnalyticsFile file;
private readonly Timer currentTimer;
private readonly Timer currentTimer, saveTimer;
public AnalyticsManager(FormBrowser browser, PluginManager plugins, string file){
this.browser = browser;
this.plugins = plugins;
this.file = AnalyticsFile.Load(file);
this.File = AnalyticsFile.Load(file);
this.currentTimer = new Timer{ SynchronizingObject = browser };
this.currentTimer.Elapsed += currentTimer_Elapsed;
if (this.file.LastCollectionVersion != Program.VersionTag){
this.saveTimer = new Timer{ SynchronizingObject = browser, Interval = 60000 };
this.saveTimer.Elapsed += saveTimer_Elapsed;
if (this.File.LastCollectionVersion != Program.VersionTag){
ScheduleReportIn(TimeSpan.FromHours(12), string.Empty);
}
else{
@ -35,7 +37,22 @@ public AnalyticsManager(FormBrowser browser, PluginManager plugins, string file)
}
public void Dispose(){
if (saveTimer.Enabled){
File.Save();
}
currentTimer.Dispose();
saveTimer.Dispose();
}
public void TriggerEvent(AnalyticsFile.Event e){
File.TriggerEvent(e);
saveTimer.Enabled = true;
}
private void saveTimer_Elapsed(object sender, ElapsedEventArgs e){
saveTimer.Stop();
File.Save();
}
private void ScheduleReportIn(TimeSpan delay, string message = null){
@ -43,16 +60,16 @@ private void ScheduleReportIn(TimeSpan delay, string message = null){
}
private void SetLastDataCollectionTime(DateTime dt, string message = null){
file.LastDataCollection = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
file.LastCollectionVersion = Program.VersionTag;
file.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture);
File.LastDataCollection = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
File.LastCollectionVersion = Program.VersionTag;
File.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture);
file.Save();
File.Save();
RestartTimer();
}
private void RestartTimer(){
TimeSpan diff = DateTime.Now.Subtract(file.LastDataCollection);
TimeSpan diff = DateTime.Now.Subtract(File.LastDataCollection);
int minutesTillNext = (int)(CollectionInterval.TotalMinutes-Math.Floor(diff.TotalMinutes));
currentTimer.Interval = Math.Max(minutesTillNext, 1)*60000;
@ -62,7 +79,7 @@ private void RestartTimer(){
private void currentTimer_Elapsed(object sender, ElapsedEventArgs e){
currentTimer.Stop();
TimeSpan diff = DateTime.Now.Subtract(file.LastDataCollection);
TimeSpan diff = DateTime.Now.Subtract(File.LastDataCollection);
if (Math.Floor(diff.TotalMinutes) >= CollectionInterval.TotalMinutes){
SendReport();
@ -76,7 +93,7 @@ private void SendReport(){
AnalyticsReportGenerator.ExternalInfo info = AnalyticsReportGenerator.ExternalInfo.From(browser);
Task.Factory.StartNew(() => {
AnalyticsReport report = AnalyticsReportGenerator.Create(info, plugins);
AnalyticsReport report = AnalyticsReportGenerator.Create(File, info, plugins);
BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
}).ContinueWith(task => browser.InvokeAsyncSafe(() => {
if (task.Status == TaskStatus.RanToCompletion){

View File

@ -14,7 +14,7 @@
namespace TweetDuck.Core.Other.Analytics{
static class AnalyticsReportGenerator{
public static AnalyticsReport Create(ExternalInfo info, PluginManager plugins){
public static AnalyticsReport Create(AnalyticsFile file, ExternalInfo info, PluginManager plugins){
Dictionary<string, string> editLayoutDesign = EditLayoutDesignPluginData;
return new AnalyticsReport{
@ -84,6 +84,22 @@ public static AnalyticsReport Create(ExternalInfo info, PluginManager plugins){
{ "Optimize Animations" , Dict(editLayoutDesign, "optimizeAnimations", "true/def") },
{ "Reply Account Mode" , ReplyAccountConfigFromPlugin },
{ "Template Count" , Exact(TemplateCountFromPlugin) },
0,
{ "Opened Options" , LogRound(file.CountOpenOptions, 4) },
{ "Opened Plugins" , LogRound(file.CountOpenPlugins, 4) },
{ "Opened About" , LogRound(file.CountOpenAbout, 4) },
{ "Opened Guide" , LogRound(file.CountOpenGuide, 4) },
{ "Desktop Notifications" , LogRound(file.CountDesktopNotifications, 5) },
{ "Sound Notifications" , LogRound(file.CountSoundNotifications, 5) },
{ "Browser Context Menus" , LogRound(file.CountBrowserContextMenus, 2) },
{ "Browser Extra Mouse Buttons" , LogRound(file.CountBrowserExtraMouseButtons, 2) },
{ "Notification Context Menus" , LogRound(file.CountNotificationContextMenus, 2) },
{ "Notification Extra Mouse Buttons" , LogRound(file.CountNotificationExtraMouseButtons, 2) },
{ "Notification Keyboard Shortcuts" , LogRound(file.CountNotificationKeyboardShortcuts, 2) },
{ "Tweet Screenshots" , LogRound(file.CountTweetScreenshots, 2) },
{ "Tweet Details" , LogRound(file.CountTweetDetails, 2) },
{ "Video Plays" , LogRound(file.CountVideoPlays, 4) },
{ "GC Reloads" , LogRound(file.CountGCReloads, 4) }
}.FinalizeReport();
}
@ -93,6 +109,7 @@ public static AnalyticsReport Create(ExternalInfo info, PluginManager plugins){
private static string Bool(bool value) => value ? "on" : "off";
private static string Exact(int value) => value.ToString();
private static string RoundUp(int value, int multiple) => (multiple*(int)Math.Ceiling((double)value/multiple)).ToString();
private static string LogRound(int value, int logBase) => (value <= 0 ? 0 : (int)Math.Pow(logBase, Math.Floor(Math.Log(value, logBase)))).ToString();
private static string Dict(Dictionary<string, string> dict, string key, string def = "(unknown)") => dict.TryGetValue(key, out string value) ? value : def;
private static string List(IEnumerable<string> list) => string.Join("|", list.DefaultIfEmpty("(none)"));

View File

@ -6,6 +6,7 @@
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Other{
@ -24,6 +25,8 @@ public FormGuide(){
if (owner != null){
Size = new Size(owner.Size.Width*3/4, owner.Size.Height*3/4);
VisibleChanged += (sender, args) => this.MoveToCenter(owner);
owner.TriggerAnalyticsEvent(AnalyticsFile.Event.OpenGuide);
}
this.browser = new ChromiumWebBrowser(GuideUrl){

View File

@ -1,9 +1,9 @@
using System;
using System.Diagnostics;
using System.Timers;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Core.Controls;
using Timer = System.Timers.Timer;
namespace TweetDuck.Core.Other.Management{
@ -13,7 +13,7 @@ sealed class MemoryUsageTracker : IDisposable{
private readonly string script;
private readonly Timer timer;
private Form owner;
private FormBrowser owner;
private IBrowser browser;
private long threshold;
@ -29,7 +29,7 @@ public MemoryUsageTracker(string cleanupFunctionName){
public void Start(ChromiumWebBrowser control, int thresholdMB){
Stop();
this.owner = (Form)control.Parent; // TODO ugly
this.owner = (FormBrowser)control.Parent; // TODO ugly
this.browser = control.GetBrowser();
this.threshold = thresholdMB*1024L*1024L;
this.timer.SynchronizingObject = owner;
@ -71,6 +71,7 @@ private void timer_Elapsed(object sender, ElapsedEventArgs e){
if (response.Success && (response.Result as bool? ?? false)){
SetNeedsCleanup(false);
owner.InvokeAsyncSafe(() => owner.TriggerAnalyticsEvent(Analytics.AnalyticsFile.Event.GCReload));
}
});
}

View File

@ -7,19 +7,21 @@
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsFeedback : BaseTabSettings{
private readonly AnalyticsFile analyticsFile;
private readonly AnalyticsReportGenerator.ExternalInfo analyticsInfo;
private readonly PluginManager plugins;
public TabSettingsFeedback(AnalyticsManager analytics, AnalyticsReportGenerator.ExternalInfo analyticsInfo, PluginManager plugins){
InitializeComponent();
this.analyticsFile = analytics?.File ?? AnalyticsFile.Load(Program.AnalyticsFilePath);
this.analyticsInfo = analyticsInfo;
this.plugins = plugins;
checkDataCollection.Checked = Config.AllowDataCollection;
if (analytics != null){
string collectionTime = analytics.LastCollectionMessage;
string collectionTime = analyticsFile.LastCollectionMessage;
labelDataCollectionMessage.Text = string.IsNullOrEmpty(collectionTime) ? "No collection yet" : "Last collection: "+collectionTime;
}
}
@ -44,7 +46,7 @@ private void labelDataCollectionLink_LinkClicked(object sender, LinkLabelLinkCli
}
private void btnViewReport_Click(object sender, EventArgs e){
using(DialogSettingsAnalytics dialog = new DialogSettingsAnalytics(AnalyticsReportGenerator.Create(analyticsInfo, plugins))){
using(DialogSettingsAnalytics dialog = new DialogSettingsAnalytics(AnalyticsReportGenerator.Create(analyticsFile, analyticsInfo, plugins))){
dialog.ShowDialog();
}
}