From 108cf8923efb499b6c128de492a94117f5593f95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Ch=C3=BDlek?= <info@chylex.com>
Date: Thu, 2 Nov 2017 03:08:43 +0100
Subject: [PATCH] Implement analytics (#176)

* Implement analytics report generation w/ user config and basic system info

* Add HW and plugin info to analytics report generation

* Add a way of displaying the full analytics report

* Fix issues in analytics report and include design theme

* Ensure tab config is saved when switching tabs

* Fix compilation error in TabSettingsFeedback and safeguard nulls

* Add locale to analytics report

* Work on analytics (utils, last collection label, dependency refactoring)

* Add analytics state file and implement sending reports every week

* Send an analytics report after each update
---
 Core/FormBrowser.cs                           |  25 +-
 Core/Other/Analytics/AnalyticsFile.cs         |  56 ++++
 Core/Other/Analytics/AnalyticsManager.cs      | 110 +++++++
 Core/Other/Analytics/AnalyticsReport.cs       |  57 ++++
 .../Analytics/AnalyticsReportGenerator.cs     | 280 ++++++++++++++++++
 Core/Other/FormSettings.cs                    |  17 +-
 .../DialogSettingsAnalytics.Designer.cs       |  90 ++++++
 .../Dialogs/DialogSettingsAnalytics.cs        |  24 ++
 .../Settings/TabSettingsFeedback.Designer.cs  |  56 +++-
 Core/Other/Settings/TabSettingsFeedback.cs    |  24 +-
 Core/Utils/BrowserUtils.cs                    |   7 +-
 Core/Utils/StringUtils.cs                     |  11 +
 Program.cs                                    |   1 +
 Resources/Plugins/edit-design/browser.js      |  11 +-
 TweetDuck.csproj                              |  10 +
 15 files changed, 748 insertions(+), 31 deletions(-)
 create mode 100644 Core/Other/Analytics/AnalyticsFile.cs
 create mode 100644 Core/Other/Analytics/AnalyticsManager.cs
 create mode 100644 Core/Other/Analytics/AnalyticsReport.cs
 create mode 100644 Core/Other/Analytics/AnalyticsReportGenerator.cs
 create mode 100644 Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs
 create mode 100644 Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs

diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index b22670ba..240f2318 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -8,6 +8,7 @@
 using TweetDuck.Core.Notification;
 using TweetDuck.Core.Notification.Screenshot;
 using TweetDuck.Core.Other;
+using TweetDuck.Core.Other.Analytics;
 using TweetDuck.Core.Other.Management;
 using TweetDuck.Core.Other.Settings;
 using TweetDuck.Core.Utils;
@@ -47,10 +48,11 @@ public bool IsWaiting{
 
         private bool isLoaded;
         private FormWindowState prevState;
-
+        
         private TweetScreenshotManager notificationScreenshotManager;
         private SoundNotification soundNotification;
         private VideoPlayer videoPlayer;
+        private AnalyticsManager analytics;
 
         public FormBrowser(UpdaterSettings updaterSettings){
             InitializeComponent();
@@ -77,6 +79,7 @@ public FormBrowser(UpdaterSettings updaterSettings){
                 notificationScreenshotManager?.Dispose();
                 soundNotification?.Dispose();
                 videoPlayer?.Dispose();
+                analytics?.Dispose();
             };
 
             this.trayIcon.ClickRestore += trayIcon_ClickRestore;
@@ -108,6 +111,10 @@ private void RestoreWindow(){
             Config.BrowserWindow.Restore(this, true);
             prevState = WindowState;
             isLoaded = true;
+
+            if (Config.AllowDataCollection){
+                analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
+            }
         }
 
         private void UpdateTrayIcon(){
@@ -321,6 +328,10 @@ public void OnIntroductionClosed(bool showGuide, bool allowDataCollection){
                 Config.FirstRun = false;
                 Config.AllowDataCollection = allowDataCollection;
                 Config.Save();
+
+                if (allowDataCollection && analytics == null){
+                    analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
+                }
             }
 
             if (showGuide){
@@ -340,7 +351,7 @@ public void OpenSettings(Type startTab){
             if (!FormManager.TryBringToFront<FormSettings>()){
                 bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
 
-                FormSettings form = new FormSettings(this, plugins, updates, startTab);
+                FormSettings form = new FormSettings(this, plugins, updates, analytics, startTab);
 
                 form.FormClosed += (sender, args) => {
                     if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
@@ -355,6 +366,16 @@ public void OpenSettings(Type startTab){
                     }
 
                     browser.RefreshMemoryTracker();
+
+                    if (Config.AllowDataCollection){
+                        if (analytics == null){
+                            analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
+                        }
+                    }
+                    else if (analytics != null){
+                        analytics.Dispose();
+                        analytics = null;
+                    }
                     
                     if (form.ShouldReloadBrowser){
                         FormManager.TryFind<FormPlugins>()?.Close();
diff --git a/Core/Other/Analytics/AnalyticsFile.cs b/Core/Other/Analytics/AnalyticsFile.cs
new file mode 100644
index 00000000..3730b41d
--- /dev/null
+++ b/Core/Other/Analytics/AnalyticsFile.cs
@@ -0,0 +1,56 @@
+using System;
+using System.IO;
+using TweetDuck.Core.Utils;
+using TweetDuck.Data.Serialization;
+
+namespace TweetDuck.Core.Other.Analytics{
+    sealed class AnalyticsFile{
+        private static readonly FileSerializer<AnalyticsFile> Serializer = new FileSerializer<AnalyticsFile>();
+
+        static AnalyticsFile(){
+            Serializer.RegisterTypeConverter(typeof(DateTime), new SingleTypeConverter<DateTime>{
+                ConvertToString = value => value.ToBinary().ToString(),
+                ConvertToObject = value => DateTime.FromBinary(long.Parse(value))
+            });
+        }
+
+        // STATE PROPERTIES
+        
+        public DateTime LastDataCollection  { get; set; } = DateTime.MinValue;
+        public string LastCollectionVersion { get; set; } = null;
+        public string LastCollectionMessage { get; set; } = null;
+
+        // END OF DATA
+        
+        private readonly string file;
+        
+        private AnalyticsFile(string file){
+            this.file = file;
+        }
+
+        public bool Save(){
+            try{
+                WindowsUtils.CreateDirectoryForFile(file);
+                Serializer.Write(file, this);
+                return true;
+            }catch(Exception e){
+                Program.Reporter.HandleException("Analytics File Error", "Could not save the analytics file.", true, e);
+                return false;
+            }
+        }
+        
+        public static AnalyticsFile Load(string file){
+            AnalyticsFile config = new AnalyticsFile(file);
+            
+            try{
+                Serializer.Read(file, config);
+            }catch(FileNotFoundException){
+            }catch(DirectoryNotFoundException){
+            }catch(Exception e){
+                Program.Reporter.HandleException("Analytics File Error", "Could not open the analytics file.", true, e);
+            }
+
+            return config;
+        }
+    }
+}
diff --git a/Core/Other/Analytics/AnalyticsManager.cs b/Core/Other/Analytics/AnalyticsManager.cs
new file mode 100644
index 00000000..9d85cdb2
--- /dev/null
+++ b/Core/Other/Analytics/AnalyticsManager.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using System.Timers;
+using TweetDuck.Core.Controls;
+using TweetDuck.Core.Utils;
+using TweetDuck.Plugins;
+
+namespace TweetDuck.Core.Other.Analytics{
+    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;
+        
+        private readonly FormBrowser browser;
+        private readonly PluginManager plugins;
+        private readonly AnalyticsFile file;
+        private readonly Timer currentTimer;
+
+        public AnalyticsManager(FormBrowser browser, PluginManager plugins, string file){
+            this.browser = browser;
+            this.plugins = plugins;
+            this.file = AnalyticsFile.Load(file);
+
+            this.currentTimer = new Timer{ SynchronizingObject = browser };
+            this.currentTimer.Elapsed += currentTimer_Elapsed;
+
+            if (this.file.LastCollectionVersion != Program.VersionTag){
+                ScheduleReportIn(TimeSpan.FromHours(12), string.Empty);
+            }
+            else{
+                RestartTimer();
+            }
+        }
+
+        public void Dispose(){
+            currentTimer.Dispose();
+        }
+
+        private void ScheduleReportIn(TimeSpan delay, string message = null){
+            SetLastDataCollectionTime(DateTime.Now.Subtract(CollectionInterval).Add(delay), message);
+        }
+
+        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.Save();
+            RestartTimer();
+        }
+
+        private void RestartTimer(){
+            TimeSpan diff = DateTime.Now.Subtract(file.LastDataCollection);
+            int minutesTillNext = (int)(CollectionInterval.TotalMinutes-Math.Floor(diff.TotalMinutes));
+            
+            currentTimer.Interval = Math.Max(minutesTillNext, 1)*60000;
+            currentTimer.Start();
+        }
+
+        private void currentTimer_Elapsed(object sender, ElapsedEventArgs e){
+            currentTimer.Stop();
+
+            TimeSpan diff = DateTime.Now.Subtract(file.LastDataCollection);
+            
+            if (Math.Floor(diff.TotalMinutes) >= CollectionInterval.TotalMinutes){
+                SendReport();
+            }
+            else{
+                RestartTimer();
+            }
+        }
+        
+        private void SendReport(){
+            AnalyticsReportGenerator.ExternalInfo info = AnalyticsReportGenerator.ExternalInfo.From(browser);
+
+            Task.Factory.StartNew(() => {
+                AnalyticsReport report = AnalyticsReportGenerator.Create(info, plugins);
+                BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
+            }).ContinueWith(task => browser.InvokeAsyncSafe(() => {
+                if (task.Status == TaskStatus.RanToCompletion){
+                    SetLastDataCollectionTime(DateTime.Now);
+                }
+                else if (task.Exception != null){
+                    string message = null;
+
+                    if (task.Exception.InnerException is WebException e){
+                        switch(e.Status){
+                            case WebExceptionStatus.ConnectFailure:
+                                message = "Connection Error";
+                                break;
+
+                            case WebExceptionStatus.NameResolutionFailure:
+                                message = "DNS Error";
+                                break;
+
+                            case WebExceptionStatus.ProtocolError:
+                                HttpWebResponse response = e.Response as HttpWebResponse;
+                                message = "HTTP Error "+(response != null ? $"{(int)response.StatusCode} ({response.StatusDescription})" : "(unknown code)");
+                                break;
+                        }
+                    }
+
+                    ScheduleReportIn(TimeSpan.FromHours(4), message ?? "Error: "+(task.Exception.InnerException?.Message ?? task.Exception.Message));
+                }
+            }));
+        }
+    }
+}
diff --git a/Core/Other/Analytics/AnalyticsReport.cs b/Core/Other/Analytics/AnalyticsReport.cs
new file mode 100644
index 00000000..fc7b8033
--- /dev/null
+++ b/Core/Other/Analytics/AnalyticsReport.cs
@@ -0,0 +1,57 @@
+using System.Collections;
+using System.Collections.Specialized;
+using System.Text;
+
+namespace TweetDuck.Core.Other.Analytics{
+    sealed class AnalyticsReport : IEnumerable{
+        private OrderedDictionary data = new OrderedDictionary(32);
+        private int separators;
+
+        public void Add(int ignored){ // adding separators to pretty print
+            data.Add((++separators).ToString(), null);
+        }
+
+        public void Add(string key, string value){
+            data.Add(key, value);
+        }
+
+        public AnalyticsReport FinalizeReport(){
+            if (!data.IsReadOnly){
+                data = data.AsReadOnly();
+            }
+
+            return this;
+        }
+
+        public IEnumerator GetEnumerator(){
+            return data.GetEnumerator();
+        }
+
+        public NameValueCollection ToNameValueCollection(){
+            NameValueCollection collection = new NameValueCollection();
+
+            foreach(DictionaryEntry entry in data){
+                if (entry.Value != null){
+                    collection.Add((string)entry.Key, (string)entry.Value);
+                }
+            }
+
+            return collection;
+        }
+
+        public override string ToString(){
+            StringBuilder build = new StringBuilder();
+
+            foreach(DictionaryEntry entry in data){
+                if (entry.Value == null){
+                    build.AppendLine();
+                }
+                else{
+                    build.AppendLine(entry.Key+": "+entry.Value);
+                }
+            }
+
+            return build.ToString();
+        }
+    }
+}
diff --git a/Core/Other/Analytics/AnalyticsReportGenerator.cs b/Core/Other/Analytics/AnalyticsReportGenerator.cs
new file mode 100644
index 00000000..64a32cfc
--- /dev/null
+++ b/Core/Other/Analytics/AnalyticsReportGenerator.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Windows.Forms;
+using Microsoft.Win32;
+using TweetDuck.Configuration;
+using System.Linq;
+using System.Management;
+using System.Text.RegularExpressions;
+using TweetDuck.Core.Notification;
+using TweetDuck.Core.Utils;
+using TweetDuck.Plugins;
+
+namespace TweetDuck.Core.Other.Analytics{
+    static class AnalyticsReportGenerator{
+        public static AnalyticsReport Create(ExternalInfo info, PluginManager plugins){
+            Dictionary<string, string> editLayoutDesign = EditLayoutDesignPluginData;
+
+            return new AnalyticsReport{
+                { "App Version" , Program.VersionTag },
+                { "App Type"    , Program.IsPortable ? "portable" : "installed" },
+                0,
+                { "System Name"        , SystemName },
+                { "System Edition"     , SystemEdition },
+                { "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" },
+                { "System Build"       , SystemBuild },
+                { "System Locale"      , Program.Culture.Name.ToLower() },
+                0,
+                { "RAM" , Exact(RamSize) },
+                { "GPU" , GpuVendor },
+                0,
+                { "Screen Count"      , Exact(Screen.AllScreens.Length) },
+                { "Screen Resolution" , info.Resolution ?? "(unknown)" },
+                { "Screen DPI"        , info.DPI != null ? Exact(info.DPI.Value) : "(unknown)" },
+                0,
+                { "Hardware Acceleration" , Bool(SysConfig.HardwareAcceleration) },
+                { "Browser GC Reload"     , Bool(SysConfig.EnableBrowserGCReload) },
+                { "Browser GC Threshold"  , Exact(SysConfig.BrowserMemoryThreshold) },
+                0,
+                { "Expand Links"             , Bool(UserConfig.ExpandLinksOnHover) },
+                { "Switch Account Selectors" , Bool(UserConfig.SwitchAccountSelectors) },
+                { "Search In First Column"   , Bool(UserConfig.OpenSearchInFirstColumn) },
+                { "Best Image Quality"       , Bool(UserConfig.BestImageQuality) },
+                { "Spell Check"              , Bool(UserConfig.EnableSpellCheck) },
+                { "Zoom"                     , Exact(UserConfig.ZoomLevel) },
+                0,
+                { "Updates"          , Bool(UserConfig.EnableUpdateCheck) },
+                { "Update Dismissed" , Bool(!string.IsNullOrEmpty(UserConfig.DismissedUpdate)) },
+                0,
+                { "Tray"           , TrayMode },
+                { "Tray Highlight" , Bool(UserConfig.EnableTrayHighlight) },
+                0,
+                { "Notification Position"       , NotificationPosition },
+                { "Notification Size"           , NotificationSize },
+                { "Notification Timer"          , NotificationTimer },
+                { "Notification Timer Speed"    , RoundUp(UserConfig.NotificationDurationValue, 5) },
+                { "Notification Scroll Speed"   , Exact(UserConfig.NotificationScrollSpeed) },
+                { "Notification Column Title"   , Bool(UserConfig.DisplayNotificationColumn) },
+                { "Notification Media Previews" , Bool(UserConfig.NotificationMediaPreviews) },
+                { "Notification Link Skip"      , Bool(UserConfig.NotificationSkipOnLinkClick) },
+                { "Notification Non-Intrusive"  , Bool(UserConfig.NotificationNonIntrusiveMode) },
+                { "Notification Idle Pause"     , Exact(UserConfig.NotificationIdlePauseSeconds) },
+                { "Custom Sound Notification"   , string.IsNullOrEmpty(UserConfig.NotificationSoundPath) ? "off" : Path.GetExtension(UserConfig.NotificationSoundPath) },
+                0,
+                { "Program Arguments"       , List(ProgramArguments) },
+                { "Custom CEF Arguments"    , RoundUp((UserConfig.CustomCefArgs ?? string.Empty).Length, 10) },
+                { "Custom Browser CSS"      , RoundUp((UserConfig.CustomBrowserCSS ?? string.Empty).Length, 50) },
+                { "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
+                0,
+                { "Plugins All"     , List(plugins.Plugins.Select(plugin => plugin.Identifier)) },
+                { "Plugins Enabled" , List(plugins.Plugins.Where(plugin => plugins.Config.IsEnabled(plugin)).Select(plugin => plugin.Identifier)) },
+                0,
+                { "Theme"               , Dict(editLayoutDesign, "_theme",                  "light/def") },
+                { "Column Width"        , Dict(editLayoutDesign, "columnWidth",             "310px/def") },
+                { "Font Size"           , Dict(editLayoutDesign, "fontSize",                "12px/def") },
+                { "Large Quote Font"    , Dict(editLayoutDesign, "increaseQuoteTextSize",   "false/def") },
+                { "Small Compose Font"  , Dict(editLayoutDesign, "smallComposeTextSize",    "false/def") },
+                { "Avatar Radius"       , Dict(editLayoutDesign, "avatarRadius",            "2/def") },
+                { "Hide Tweet Actions"  , Dict(editLayoutDesign, "hideTweetActions",        "true/def") },
+                { "Move Tweet Actions"  , Dict(editLayoutDesign, "moveTweetActionsToRight", "true/def") },
+                { "Theme Color Tweaks"  , Dict(editLayoutDesign, "themeColorTweaks",        "true/def") },
+                { "Revert Icons"        , Dict(editLayoutDesign, "revertIcons",             "true/def") },
+                { "Optimize Animations" , Dict(editLayoutDesign, "optimizeAnimations",      "true/def") },
+                { "Reply Account Mode"  , ReplyAccountConfigFromPlugin },
+                { "Template Count"      , Exact(TemplateCountFromPlugin) },
+            }.FinalizeReport();
+        }
+
+        private static UserConfig UserConfig => Program.UserConfig;
+        private static SystemConfig SysConfig => Program.SystemConfig;
+
+        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 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)"));
+
+        private static string SystemName { get; }
+        private static string SystemEdition { get; }
+        private static string SystemBuild { get; }
+        private static int RamSize { get; }
+        private static string GpuVendor { get; }
+        private static string[] ProgramArguments { get; }
+
+        static AnalyticsReportGenerator(){
+            string osName, osEdition, osBuild;
+
+            try{
+                using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false)){
+                    // ReSharper disable once PossibleNullReferenceException
+                    osName = key.GetValue("ProductName") as string;
+                    osEdition = key.GetValue("EditionID") as string;
+                    osBuild = key.GetValue("CurrentBuild") as string;
+                    
+                    if (osName != null && osEdition != null){
+                        osName = osName.Replace(osEdition, "").TrimEnd();
+                    }
+                }
+            }catch{
+                osName = osEdition = osBuild = null;
+            }
+
+            SystemName = osName ?? "Windows (unknown)";
+            SystemEdition = osEdition ?? "(unknown)";
+            SystemBuild = osBuild ?? "(unknown)";
+
+            try{
+                using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory")){
+                    foreach(ManagementBaseObject obj in searcher.Get()){
+                        RamSize += (int)((ulong)obj["Capacity"]/(1024L*1024L));
+                    }
+                }
+            }catch{
+                RamSize = 0;
+            }
+
+            string gpu = null;
+
+            try{
+                using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController")){
+                    foreach(ManagementBaseObject obj in searcher.Get()){
+                        string vendor = obj["Caption"] as string;
+
+                        if (!string.IsNullOrEmpty(vendor)){
+                            gpu = vendor;
+                        }
+                    }
+                }
+            }catch{
+                // rip
+            }
+
+            GpuVendor = gpu ?? "(unknown)";
+
+            Dictionary<string, string> args = new Dictionary<string, string>();
+            Arguments.GetCurrentClean().ToDictionary(args);
+            ProgramArguments = args.Keys.Select(key => key.TrimStart('-')).ToArray();
+        }
+
+        private static string TrayMode{
+            get{
+                switch(UserConfig.TrayBehavior){
+                    case TrayIcon.Behavior.DisplayOnly: return "icon";
+                    case TrayIcon.Behavior.MinimizeToTray: return "minimize";
+                    case TrayIcon.Behavior.CloseToTray: return "close";
+                    case TrayIcon.Behavior.Combined: return "combined";
+                    default: return "off";
+                }
+            }
+        }
+
+        private static string NotificationPosition{
+            get{
+                switch(UserConfig.NotificationPosition){
+                    case TweetNotification.Position.TopLeft: return "top left";
+                    case TweetNotification.Position.TopRight: return "top right";
+                    case TweetNotification.Position.BottomLeft: return "bottom left";
+                    case TweetNotification.Position.BottomRight: return "bottom right";
+                    default: return "custom";
+                }
+            }
+        }
+
+        private static string NotificationSize{
+            get{
+                switch(UserConfig.NotificationSize){
+                    case TweetNotification.Size.Auto: return "auto";
+                    default: return RoundUp(UserConfig.CustomNotificationSize.Width, 20)+"x"+RoundUp(UserConfig.CustomNotificationSize.Height, 20);
+                }
+            }
+        }
+
+        private static string NotificationTimer{
+            get{
+                if (!UserConfig.DisplayNotificationTimer){
+                    return "off";
+                }
+                else{
+                    return UserConfig.NotificationTimerCountDown ? "count down" : "count up";
+                }
+            }
+        }
+
+        private static Dictionary<string, string> EditLayoutDesignPluginData{
+            get{
+                Dictionary<string, string> dict = new Dictionary<string, string>();
+                
+                try{
+                    string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "edit-design", "config.json"));
+
+                    foreach(Match match in Regex.Matches(data, "\"(\\w+?)\":(.*?)[,}]")){
+                        dict[match.Groups[1].Value] = match.Groups[2].Value.Trim('"');
+                    }
+                }catch{
+                    // rip
+                }
+
+                return dict;
+            }
+        }
+
+        private static int TemplateCountFromPlugin{
+            get{
+                try{
+                    string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "templates", "config.json"));
+                    return Math.Min(StringUtils.CountOccurrences(data, "{\"name\":"), StringUtils.CountOccurrences(data, ",\"contents\":"));
+                }catch{
+                    return 0;
+                }
+            }
+        }
+
+        private static string ReplyAccountConfigFromPlugin{
+            get{
+                try{
+                    string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "reply-account", "configuration.js")).Replace(" ", "");
+
+                    Match matchType = Regex.Match(data, "defaultAccount:\"([#@])(.*?)\"(?:,|$)");
+                    Match matchAdvanced = Regex.Match(data, "useAdvancedSelector:(.*?)(?:,|$)", RegexOptions.Multiline);
+
+                    if (!matchType.Success){
+                        return "(unknown)";
+                    }
+
+                    string accType = matchType.Groups[1].Value == "#" ? matchType.Groups[2].Value : "account";
+                    return matchAdvanced.Success && !matchAdvanced.Value.Contains("false") ? "advanced/"+accType : accType;
+                }catch{
+                    return "(unknown)";
+                }
+            }
+        }
+
+        public class ExternalInfo{
+            public static ExternalInfo From(Form form){
+                if (form == null){
+                    return new ExternalInfo();
+                }
+                else{
+                    Screen screen = Screen.FromControl(form);
+                    int dpi;
+
+                    using(Graphics graphics = form.CreateGraphics()){
+                        dpi = (int)graphics.DpiY;
+                    }
+
+                    return new ExternalInfo{
+                        Resolution = screen.Bounds.Width+"x"+screen.Bounds.Height,
+                        DPI = dpi
+                    };
+                }
+            }
+
+            public string Resolution { get; private set; }
+            public int? DPI { get; private set; }
+
+            private ExternalInfo(){}
+        }
+    }
+}
diff --git a/Core/Other/FormSettings.cs b/Core/Other/FormSettings.cs
index 8840039b..66ae4885 100644
--- a/Core/Other/FormSettings.cs
+++ b/Core/Other/FormSettings.cs
@@ -4,6 +4,7 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Notification.Example;
+using TweetDuck.Core.Other.Analytics;
 using TweetDuck.Core.Other.Settings;
 using TweetDuck.Core.Other.Settings.Dialogs;
 using TweetDuck.Core.Utils;
@@ -22,7 +23,7 @@ sealed partial class FormSettings : Form{
 
         public bool ShouldReloadBrowser { get; private set; }
 
-        public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, Type startTab){
+        public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, AnalyticsManager analytics, Type startTab){
             InitializeComponent();
 
             Text = Program.BrandName+" Options";
@@ -38,16 +39,17 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler up
             AddButton("System Tray", () => new TabSettingsTray());
             AddButton("Notifications", () => new TabSettingsNotifications(new FormNotificationExample(browser, plugins)));
             AddButton("Sounds", () => new TabSettingsSounds());
-            AddButton("Feedback", () => new TabSettingsFeedback());
-            AddButton("Advanced", () => new TabSettingsAdvanced(browser.ReinjectCustomCSS));
+            AddButton("Feedback", () => new TabSettingsFeedback(analytics, AnalyticsReportGenerator.ExternalInfo.From(this.browser), this.plugins));
+            AddButton("Advanced", () => new TabSettingsAdvanced(this.browser.ReinjectCustomCSS));
 
             SelectTab(tabs[startTab ?? typeof(TabSettingsGeneral)]);
         }
 
         private void FormSettings_FormClosing(object sender, FormClosingEventArgs e){
+            currentTab.Control.OnClosing();
+
             foreach(SettingsTab tab in tabs.Values){
                 if (tab.IsInitialized){
-                    tab.Control.OnClosing();
                     tab.Control.Dispose();
                 }
             }
@@ -57,11 +59,7 @@ private void FormSettings_FormClosing(object sender, FormClosingEventArgs e){
         }
 
         private void btnManageOptions_Click(object sender, EventArgs e){
-            foreach(SettingsTab tab in tabs.Values){
-                if (tab.IsInitialized){
-                    tab.Control.OnClosing();
-                }
-            }
+            currentTab.Control.OnClosing();
 
             using(DialogSettingsManage dialog = new DialogSettingsManage(plugins)){
                 if (dialog.ShowDialog() == DialogResult.OK){
@@ -114,6 +112,7 @@ private void SelectTab<T>() where T : BaseTabSettings{
         private void SelectTab(SettingsTab tab){
             if (currentTab != null){
                 currentTab.Button.BackColor = SystemColors.Control;
+                currentTab.Control.OnClosing();
             }
             
             tab.Button.BackColor = tab.Button.FlatAppearance.MouseDownBackColor;
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs
new file mode 100644
index 00000000..5af8b77f
--- /dev/null
+++ b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs
@@ -0,0 +1,90 @@
+namespace TweetDuck.Core.Other.Settings.Dialogs {
+    partial class DialogSettingsAnalytics {
+        /// <summary>
+        /// Required designer variable.
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary>
+        /// Clean up any resources being used.
+        /// </summary>
+        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+        protected override void Dispose(bool disposing) {
+            if (disposing && (components != null)) {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Windows Form Designer generated code
+
+        /// <summary>
+        /// Required method for Designer support - do not modify
+        /// the contents of this method with the code editor.
+        /// </summary>
+        private void InitializeComponent() {
+            this.textBoxReport = new System.Windows.Forms.TextBox();
+            this.btnClose = new System.Windows.Forms.Button();
+            this.labelInfo = new System.Windows.Forms.Label();
+            this.SuspendLayout();
+            // 
+            // textBoxReport
+            // 
+            this.textBoxReport.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
+            | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.textBoxReport.Font = new System.Drawing.Font("Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
+            this.textBoxReport.Location = new System.Drawing.Point(12, 41);
+            this.textBoxReport.Multiline = true;
+            this.textBoxReport.Name = "textBoxReport";
+            this.textBoxReport.ReadOnly = true;
+            this.textBoxReport.ScrollBars = System.Windows.Forms.ScrollBars.Both;
+            this.textBoxReport.Size = new System.Drawing.Size(460, 480);
+            this.textBoxReport.TabIndex = 1;
+            // 
+            // btnClose
+            // 
+            this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+            this.btnClose.Location = new System.Drawing.Point(416, 527);
+            this.btnClose.Name = "btnClose";
+            this.btnClose.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0);
+            this.btnClose.Size = new System.Drawing.Size(56, 23);
+            this.btnClose.TabIndex = 2;
+            this.btnClose.Text = "Close";
+            this.btnClose.UseVisualStyleBackColor = true;
+            this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
+            // 
+            // labelInfo
+            // 
+            this.labelInfo.Location = new System.Drawing.Point(12, 9);
+            this.labelInfo.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
+            this.labelInfo.Name = "labelInfo";
+            this.labelInfo.Size = new System.Drawing.Size(460, 26);
+            this.labelInfo.TabIndex = 0;
+            this.labelInfo.Text = "When enabled, this data will be sent over a secure network roughly once every wee" +
+    "k.\r\nSome numbers in the report were made imprecise on purpose.";
+            // 
+            // DialogSettingsAnalytics
+            // 
+            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+            this.ClientSize = new System.Drawing.Size(484, 562);
+            this.Controls.Add(this.labelInfo);
+            this.Controls.Add(this.btnClose);
+            this.Controls.Add(this.textBoxReport);
+            this.MinimumSize = new System.Drawing.Size(450, 340);
+            this.Name = "DialogSettingsAnalytics";
+            this.ShowIcon = false;
+            this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
+            this.ResumeLayout(false);
+            this.PerformLayout();
+
+        }
+
+        #endregion
+
+        private System.Windows.Forms.TextBox textBoxReport;
+        private System.Windows.Forms.Button btnClose;
+        private System.Windows.Forms.Label labelInfo;
+    }
+}
\ No newline at end of file
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs
new file mode 100644
index 00000000..3cd169cf
--- /dev/null
+++ b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Windows.Forms;
+using TweetDuck.Core.Controls;
+using TweetDuck.Core.Other.Analytics;
+
+namespace TweetDuck.Core.Other.Settings.Dialogs{
+    sealed partial class DialogSettingsAnalytics : Form{
+        public string CefArgs => textBoxReport.Text;
+
+        public DialogSettingsAnalytics(AnalyticsReport report){
+            InitializeComponent();
+            
+            Text = Program.BrandName+" Options - Analytics Report";
+            
+            textBoxReport.EnableMultilineShortcuts();
+            textBoxReport.Text = report.ToString().TrimEnd();
+            textBoxReport.Select(0, 0);
+        }
+
+        private void btnClose_Click(object sender, EventArgs e){
+            Close();
+        }
+    }
+}
diff --git a/Core/Other/Settings/TabSettingsFeedback.Designer.cs b/Core/Other/Settings/TabSettingsFeedback.Designer.cs
index 013db5bc..bcbc5615 100644
--- a/Core/Other/Settings/TabSettingsFeedback.Designer.cs
+++ b/Core/Other/Settings/TabSettingsFeedback.Designer.cs
@@ -25,12 +25,14 @@ protected override void Dispose(bool disposing) {
         private void InitializeComponent() {
             this.components = new System.ComponentModel.Container();
             this.panelFeedback = new System.Windows.Forms.Panel();
+            this.labelDataCollectionMessage = new System.Windows.Forms.Label();
+            this.btnViewReport = new System.Windows.Forms.Button();
+            this.btnSendFeedback = new System.Windows.Forms.Button();
             this.labelDataCollectionLink = new System.Windows.Forms.LinkLabel();
             this.checkDataCollection = new System.Windows.Forms.CheckBox();
             this.labelDataCollection = new System.Windows.Forms.Label();
             this.labelFeedback = new System.Windows.Forms.Label();
             this.toolTip = new System.Windows.Forms.ToolTip(this.components);
-            this.btnSendFeedback = new System.Windows.Forms.Button();
             this.panelFeedback.SuspendLayout();
             this.SuspendLayout();
             // 
@@ -38,15 +40,48 @@ private void InitializeComponent() {
             // 
             this.panelFeedback.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
             | System.Windows.Forms.AnchorStyles.Right)));
+            this.panelFeedback.Controls.Add(this.labelDataCollectionMessage);
+            this.panelFeedback.Controls.Add(this.btnViewReport);
             this.panelFeedback.Controls.Add(this.btnSendFeedback);
             this.panelFeedback.Controls.Add(this.labelDataCollectionLink);
             this.panelFeedback.Controls.Add(this.checkDataCollection);
             this.panelFeedback.Controls.Add(this.labelDataCollection);
             this.panelFeedback.Location = new System.Drawing.Point(9, 31);
             this.panelFeedback.Name = "panelFeedback";
-            this.panelFeedback.Size = new System.Drawing.Size(322, 80);
+            this.panelFeedback.Size = new System.Drawing.Size(322, 188);
             this.panelFeedback.TabIndex = 1;
             // 
+            // labelDataCollectionMessage
+            // 
+            this.labelDataCollectionMessage.Location = new System.Drawing.Point(6, 114);
+            this.labelDataCollectionMessage.Margin = new System.Windows.Forms.Padding(6);
+            this.labelDataCollectionMessage.Name = "labelDataCollectionMessage";
+            this.labelDataCollectionMessage.Size = new System.Drawing.Size(310, 67);
+            this.labelDataCollectionMessage.TabIndex = 5;
+            // 
+            // btnViewReport
+            // 
+            this.btnViewReport.AutoSize = true;
+            this.btnViewReport.Location = new System.Drawing.Point(6, 82);
+            this.btnViewReport.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3);
+            this.btnViewReport.Name = "btnViewReport";
+            this.btnViewReport.Size = new System.Drawing.Size(144, 23);
+            this.btnViewReport.TabIndex = 4;
+            this.btnViewReport.Text = "View My Analytics Report";
+            this.btnViewReport.UseVisualStyleBackColor = true;
+            // 
+            // btnSendFeedback
+            // 
+            this.btnSendFeedback.AutoSize = true;
+            this.btnSendFeedback.Location = new System.Drawing.Point(5, 3);
+            this.btnSendFeedback.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3);
+            this.btnSendFeedback.Name = "btnSendFeedback";
+            this.btnSendFeedback.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0);
+            this.btnSendFeedback.Size = new System.Drawing.Size(164, 23);
+            this.btnSendFeedback.TabIndex = 0;
+            this.btnSendFeedback.Text = "Send Feedback / Bug Report";
+            this.btnSendFeedback.UseVisualStyleBackColor = true;
+            // 
             // labelDataCollectionLink
             // 
             this.labelDataCollectionLink.AutoSize = true;
@@ -60,7 +95,6 @@ private void InitializeComponent() {
             this.labelDataCollectionLink.TabStop = true;
             this.labelDataCollectionLink.Text = "(learn more)";
             this.labelDataCollectionLink.UseCompatibleTextRendering = true;
-            this.labelDataCollectionLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.labelDataCollectionLink_LinkClicked);
             // 
             // checkDataCollection
             // 
@@ -94,18 +128,6 @@ private void InitializeComponent() {
             this.labelFeedback.TabIndex = 0;
             this.labelFeedback.Text = "Feedback";
             // 
-            // btnSendFeedback
-            // 
-            this.btnSendFeedback.AutoSize = true;
-            this.btnSendFeedback.Location = new System.Drawing.Point(5, 3);
-            this.btnSendFeedback.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3);
-            this.btnSendFeedback.Name = "btnSendFeedback";
-            this.btnSendFeedback.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0);
-            this.btnSendFeedback.Size = new System.Drawing.Size(164, 23);
-            this.btnSendFeedback.TabIndex = 0;
-            this.btnSendFeedback.Text = "Send Feedback / Bug Report";
-            this.btnSendFeedback.UseVisualStyleBackColor = true;
-            // 
             // TabSettingsFeedback
             // 
             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -113,7 +135,7 @@ private void InitializeComponent() {
             this.Controls.Add(this.panelFeedback);
             this.Controls.Add(this.labelFeedback);
             this.Name = "TabSettingsFeedback";
-            this.Size = new System.Drawing.Size(340, 122);
+            this.Size = new System.Drawing.Size(340, 230);
             this.panelFeedback.ResumeLayout(false);
             this.panelFeedback.PerformLayout();
             this.ResumeLayout(false);
@@ -130,5 +152,7 @@ private void InitializeComponent() {
         private System.Windows.Forms.ToolTip toolTip;
         private System.Windows.Forms.LinkLabel labelDataCollectionLink;
         private System.Windows.Forms.Button btnSendFeedback;
+        private System.Windows.Forms.Button btnViewReport;
+        private System.Windows.Forms.Label labelDataCollectionMessage;
     }
 }
diff --git a/Core/Other/Settings/TabSettingsFeedback.cs b/Core/Other/Settings/TabSettingsFeedback.cs
index e0bf74da..3c265f3d 100644
--- a/Core/Other/Settings/TabSettingsFeedback.cs
+++ b/Core/Other/Settings/TabSettingsFeedback.cs
@@ -1,18 +1,34 @@
 using System;
 using System.Windows.Forms;
+using TweetDuck.Core.Other.Analytics;
+using TweetDuck.Core.Other.Settings.Dialogs;
 using TweetDuck.Core.Utils;
+using TweetDuck.Plugins;
 
 namespace TweetDuck.Core.Other.Settings{
     sealed partial class TabSettingsFeedback : BaseTabSettings{
-        public TabSettingsFeedback(){
+        private readonly AnalyticsReportGenerator.ExternalInfo analyticsInfo;
+        private readonly PluginManager plugins;
+
+        public TabSettingsFeedback(AnalyticsManager analytics, AnalyticsReportGenerator.ExternalInfo analyticsInfo, PluginManager plugins){
             InitializeComponent();
             
+            this.analyticsInfo = analyticsInfo;
+            this.plugins = plugins;
+
             checkDataCollection.Checked = Config.AllowDataCollection;
+
+            if (analytics != null){
+                string collectionTime = analytics.LastCollectionMessage;
+                labelDataCollectionMessage.Text = string.IsNullOrEmpty(collectionTime) ? "No collection yet" : "Last collection: "+collectionTime;
+            }
         }
 
         public override void OnReady(){
             btnSendFeedback.Click += btnSendFeedback_Click;
             checkDataCollection.CheckedChanged += checkDataCollection_CheckedChanged;
+            labelDataCollectionLink.LinkClicked += labelDataCollectionLink_LinkClicked;
+            btnViewReport.Click += btnViewReport_Click;
         }
 
         private void btnSendFeedback_Click(object sender, EventArgs e){
@@ -26,5 +42,11 @@ private void checkDataCollection_CheckedChanged(object sender, EventArgs e){
         private void labelDataCollectionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e){
             BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki/Send-anonymous-data");
         }
+
+        private void btnViewReport_Click(object sender, EventArgs e){
+            using(DialogSettingsAnalytics dialog = new DialogSettingsAnalytics(AnalyticsReportGenerator.Create(analyticsInfo, plugins))){
+                dialog.ShowDialog();
+            }
+        }
     }
 }
diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index ffabc543..226c9207 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -95,9 +95,14 @@ public static string GetErrorName(CefErrorCode code){
             return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
         }
 
-        public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){
+        public static WebClient CreateWebClient(){
             WebClient client = new WebClient{ Proxy = null };
             client.Headers[HttpRequestHeader.UserAgent] = HeaderUserAgent;
+            return client;
+        }
+
+        public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){
+            WebClient client = CreateWebClient();
 
             client.DownloadFileCompleted += (sender, args) => {
                 if (args.Cancelled){
diff --git a/Core/Utils/StringUtils.cs b/Core/Utils/StringUtils.cs
index 79bf0e95..156c03e6 100644
--- a/Core/Utils/StringUtils.cs
+++ b/Core/Utils/StringUtils.cs
@@ -18,5 +18,16 @@ public static int[] ParseInts(string str, char separator){
         public static string ConvertPascalCaseToScreamingSnakeCase(string str){
             return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpper();
         }
+
+        public static int CountOccurrences(string source, string substring){
+            int count = 0, index = 0;
+
+            while((index = source.IndexOf(substring, index)) != -1){
+                index += substring.Length;
+                ++count;
+            }
+
+            return count;
+        }
     }
 }
diff --git a/Program.cs b/Program.cs
index 68c0c931..a60d8088 100644
--- a/Program.cs
+++ b/Program.cs
@@ -36,6 +36,7 @@ static class Program{
         public static string UserConfigFilePath => Path.Combine(StoragePath, "TD_UserConfig.cfg");
         public static string SystemConfigFilePath => Path.Combine(StoragePath, "TD_SystemConfig.cfg");
         public static string PluginConfigFilePath => Path.Combine(StoragePath, "TD_PluginConfig.cfg");
+        public static string AnalyticsFilePath => Path.Combine(StoragePath, "TD_Analytics.cfg");
 
         private static string ErrorLogFilePath => Path.Combine(StoragePath, "TD_Log.txt");
         private static string ConsoleLogFilePath => Path.Combine(StoragePath, "TD_Console.txt");
diff --git a/Resources/Plugins/edit-design/browser.js b/Resources/Plugins/edit-design/browser.js
index a8bb7574..27d027e6 100644
--- a/Resources/Plugins/edit-design/browser.js
+++ b/Resources/Plugins/edit-design/browser.js
@@ -6,6 +6,7 @@ enabled(){
   this.config = null;
   
   this.defaultConfig = {
+    _theme: "light",
     columnWidth: "310px",
     fontSize: "12px",
     hideTweetActions: true,
@@ -38,11 +39,13 @@ enabled(){
       this.currentStage = 1;
     }
     else if (this.tmpConfig !== null){
+      let needsResave = !("_theme" in this.tmpConfig);
+      
       this.config = $.extend(this.defaultConfig, this.tmpConfig);
       this.tmpConfig = null;
       this.reinjectAll();
       
-      if (this.firstTimeLoad){
+      if (this.firstTimeLoad || needsResave){
         $TDP.writeFile(this.$token, configFile, JSON.stringify(this.config));
       }
     }
@@ -60,6 +63,8 @@ enabled(){
   }
   else{
     $(document).one("dataSettingsValues", () => {
+      this.defaultConfig._theme = TD.settings.getTheme();
+      
       switch(TD.settings.getColumnWidth()){
         case "wide": this.defaultConfig.columnWidth = "350px"; break;
         case "narrow": this.defaultConfig.columnWidth = "270px"; break;
@@ -234,9 +239,11 @@ enabled(){
     modal.find("[data-td-theme='"+TD.settings.getTheme()+"']").prop("checked", true);
     
     modal.find("[data-td-theme]").change(function(){
+      me.config._theme = $(this).attr("data-td-theme");
+      
       setTimeout(function(){
-        TD.settings.setTheme($(this).attr("data-td-theme"));
         $(document).trigger("uiToggleTheme");
+        me.saveConfig();
         me.reinjectAll();
       }, 1);
     });
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 36f77695..1e2d05b3 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -131,6 +131,10 @@
     </Compile>
     <Compile Include="Core\Notification\SoundNotification.cs" />
     <Compile Include="Core\Notification\TweetNotification.cs" />
+    <Compile Include="Core\Other\Analytics\AnalyticsFile.cs" />
+    <Compile Include="Core\Other\Analytics\AnalyticsManager.cs" />
+    <Compile Include="Core\Other\Analytics\AnalyticsReport.cs" />
+    <Compile Include="Core\Other\Analytics\AnalyticsReportGenerator.cs" />
     <Compile Include="Core\Other\FormAbout.cs">
       <SubType>Form</SubType>
     </Compile>
@@ -156,6 +160,12 @@
       <DependentUpon>FormPlugins.cs</DependentUpon>
     </Compile>
     <Compile Include="Core\Other\Management\VideoPlayer.cs" />
+    <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsAnalytics.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsAnalytics.Designer.cs">
+      <DependentUpon>DialogSettingsAnalytics.cs</DependentUpon>
+    </Compile>
     <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsCSS.cs">
       <SubType>Form</SubType>
     </Compile>