diff --git a/DesktopAnalytics.sln b/DesktopAnalytics.sln
index 9c086cc..dae6ca4 100644
--- a/DesktopAnalytics.sln
+++ b/DesktopAnalytics.sln
@@ -23,7 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopAnalyticsTests", "src\DesktopAnalyticsTests\DesktopAnalyticsTests.csproj", "{652BCDED-1C97-4BAC-8292-3A3EF605DC59}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleAppWithForm", "SampleAppWithForm\SampleAppWithForm.csproj", "{3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleAppWithForm", "src\SampleAppWithForm\SampleAppWithForm.csproj", "{471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -55,14 +55,14 @@ Global
{652BCDED-1C97-4BAC-8292-3A3EF605DC59}.Release|Any CPU.Build.0 = Release|Any CPU
{652BCDED-1C97-4BAC-8292-3A3EF605DC59}.Release|x86.ActiveCfg = Release|Any CPU
{652BCDED-1C97-4BAC-8292-3A3EF605DC59}.Release|x86.Build.0 = Release|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Debug|x86.Build.0 = Debug|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Release|Any CPU.Build.0 = Release|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Release|x86.ActiveCfg = Release|Any CPU
- {3B405602-752F-4A3D-B31D-DBFB2DB6BEBD}.Release|x86.Build.0 = Release|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Debug|x86.Build.0 = Debug|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Release|x86.ActiveCfg = Release|Any CPU
+ {471C3C0A-EA78-4D2A-AC47-2129A3A91C6D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/DesktopAnalytics.sln.DotSettings b/DesktopAnalytics.sln.DotSettings
new file mode 100644
index 0000000..9b99550
--- /dev/null
+++ b/DesktopAnalytics.sln.DotSettings
@@ -0,0 +1,4 @@
+
+ True
+ True
+ True
\ No newline at end of file
diff --git a/SampleAppWithForm/SampleAppWithForm.csproj b/SampleAppWithForm/SampleAppWithForm.csproj
deleted file mode 100644
index 39ef245..0000000
--- a/SampleAppWithForm/SampleAppWithForm.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- WinExe
- net6.0-windows
- enable
- true
- enable
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/DesktopAnalytics/Analytics.cs b/src/DesktopAnalytics/Analytics.cs
index e82711e..a2dc15b 100644
--- a/src/DesktopAnalytics/Analytics.cs
+++ b/src/DesktopAnalytics/Analytics.cs
@@ -10,10 +10,10 @@
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
-using System.Runtime.Remoting.Contexts;
+using System.Threading;
using System.Xml.Linq;
using System.Xml.XPath;
-using Newtonsoft.Json;
+using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using Segment.Model;
@@ -41,8 +41,9 @@ public class Analytics : IDisposable
private static Traits _traits;
private static UserInfo _userInfo;
private static Analytics _singleton;
- private Dictionary _propertiesThatGoWithEveryEvent;
+ private readonly Dictionary _propertiesThatGoWithEveryEvent;
private static int _exceptionCount = 0;
+
const int MAX_EXCEPTION_REPORTS_PER_RUN = 10;
public Analytics(string apiSecret, UserInfo userInfo, bool allowTracking = true, bool retainPii = false)
@@ -104,6 +105,11 @@ public Analytics(string apiSecret, UserInfo userInfo, Dictionary
}
Segment.Analytics.Initialize(apiSecret);
+ // All these were attempts to prevent deadlock when calling Flush in response to
+ // the main window closing
+ //Segment.Analytics.Client.Config.SetTimeout(TimeSpan.FromMilliseconds(5000));
+ //Segment.Analytics.Client.Config.SetMaxRetryTime(TimeSpan.FromMilliseconds(7500));
+ //Segment.Analytics.Client.Config.SetThreads(2);
Segment.Analytics.Client.Failed += Client_Failed;
Segment.Analytics.Client.Succeeded += Client_Succeeded;
@@ -180,11 +186,10 @@ private void AttemptToGetUserIdSettingsFromDifferentChannel()
// which should work for everyone (though until there is a plugin that supports channels, it will presumably
// never actually find a pre-existing config file from a different channel).
- string settingsLocation;
- string softwareName;
-
for (int attempt = 0; attempt < 2; attempt++)
{
+ string settingsLocation;
+ string softwareName;
if (attempt == 0)
{
if (!TryGetSettingsLocationInfoFromEntryAssembly(out settingsLocation, out softwareName))
@@ -357,6 +362,7 @@ private static void UpdateSegmentIOInformationOnThisUser()
///
/// Use this after showing a registration dialog, so that this stuff is sent right away, rather than the next time you start up Analytics
///
+ [PublicAPI]
public static void IdentifyUpdate(UserInfo userInfo)
{
_userInfo = userInfo;
@@ -383,9 +389,8 @@ private void ReportIpAddressOfThisMachineAsync()
{
try
{
- Uri uri;
bool json = string.IsNullOrEmpty(UrlThatReturnsExternalIpAddress);
- Uri.TryCreate(json ? UrlThatReturnsGeolocationJson : UrlThatReturnsExternalIpAddress, UriKind.Absolute, out uri);
+ Uri.TryCreate(json ? UrlThatReturnsGeolocationJson : UrlThatReturnsExternalIpAddress, UriKind.Absolute, out var uri);
client.DownloadDataCompleted += (object sender, DownloadDataCompletedEventArgs e) =>
{
var launchProperties = new Properties { { "installedUiLangId", CultureInfo.InstalledUICulture.ThreeLetterISOLanguageName } };
@@ -395,7 +400,7 @@ private void ReportIpAddressOfThisMachineAsync()
var result = System.Text.Encoding.UTF8.GetString(e.Result).Trim();
if (json)
{
- Debug.WriteLine(String.Format("DesktopAnalytics: geolocation JSON data = {0}", result));
+ Debug.WriteLine($"DesktopAnalytics: geolocation JSON data = {result}");
var j = JObject.Parse(result);
AddGeolocationProperty(j, "city");
AddGeolocationProperty(j, "country", "countryCode");
@@ -403,7 +408,7 @@ private void ReportIpAddressOfThisMachineAsync()
}
else
{
- Debug.WriteLine(String.Format("DesktopAnalytics: external ip = {0}", result));
+ Debug.WriteLine($"DesktopAnalytics: external ip = {result}");
_options.Context.Add("ip", result);
_propertiesThatGoWithEveryEvent.Add("ip", result);
}
@@ -452,10 +457,16 @@ private IEnumerable> GetLocationPropertiesOfThisMac
}
///
- /// Record an event
+ /// Records an event
///
- /// Analytics.RecordEvent("Save PDF");
- ///
+ ///
+ /// Analytics.Track("Save PDF");
+ ///
+ /// A good event name should be meaningful to a developer or
+ /// analyst, relatively short, and unique within an app. (Of course, you can issue the
+ /// same event from multiple places in your application code if they should be treated as
+ /// equivalent events.)
+ [PublicAPI]
public static void Track(string eventName)
{
if (!AllowTracking)
@@ -468,14 +479,18 @@ public static void Track(string eventName)
/// Record an event with extra properties
///
///
- /// Analytics.RecordEvent("Save PDF", new Dictionary()
+ /// Analytics.Track("Save PDF", new Dictionary<string, string>()
/// {
/// {"Portion", Enum.GetName(typeof(BookletPortions), BookletPortion)},
/// {"Layout", PageLayout.ToString()}
/// });
///
- ///
- ///
+ /// A good event name should be meaningful to a developer or
+ /// analyst, relatively short, and unique within an app. (Of course, you can issue the
+ /// same event from multiple places in your application code if they should be treated as
+ /// equivalent events.)
+ /// A dictionary of key-value pairs that are relevant in
+ /// understanding more about the event being tracked.
public static void Track(string eventName, Dictionary properties)
{
if (!AllowTracking)
@@ -488,6 +503,7 @@ public static void Track(string eventName, Dictionary properties
/// Sends the exception's message and stacktrace
///
///
+ [PublicAPI]
public static void ReportException(Exception e)
{
ReportException(e, null);
@@ -555,8 +571,28 @@ public void Dispose()
// The documentation is ambiguous about whether Flush() needs to be called before Dispose(),
// but source code at https://github.com/segmentio/Analytics.NET/blob/master/Analytics/Client.cs
// clearly says "Note, this does not call Flush() first".
- // So to be sure of getting all our events we should call it.
- Segment.Analytics.Client.Flush();
+ // So to be sure of getting all our events we should call it. Unfortunately, if Flush is called
+ // in response to the main application window closing, it can cause deadlock, and the app hangs.
+ // https://github.com/segmentio/Analytics.NET/issues/200
+ // So instead of calling Flush, if there are events in the queue, we just wait a little while.
+ // The default timeout on the client is 5 seconds, so probably we should never need to wait
+ // longer than that.
+ var stats = Segment.Analytics.Client.Statistics;
+ int totalWait = 0;
+ while (stats.Submitted > stats.Failed + stats.Succeeded)
+ {
+ if (totalWait > 7500)
+ break;
+ // This might seem like a long time to wait, but
+ // a) it will only have to do this is there are unsent events in the queue
+ // b) trying to wait less time doesn't seem to make it go any faster. (I did
+ // try to cut the timeout time way down in hopes that that would cause
+ // Segment to process the queue immediately, but it seemed to have no
+ // effect.)
+ totalWait += 500;
+ Thread.Sleep(500);
+ }
+ //Segment.Analytics.Client.Flush();
Segment.Analytics.Client.Dispose();
}
}
@@ -572,7 +608,7 @@ class Version
private readonly PlatformID _platform;
private readonly int _major;
private readonly int _minor;
- public string Label { get; private set; }
+ public string Label { get; }
public Version(PlatformID platform, int major, int minor, string label)
{
@@ -593,19 +629,21 @@ private static string GetOperatingSystemLabel()
{
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
- return UnixName == "Linux" ? String.Format("{0} / {1}", LinuxVersion, LinuxDesktop) : UnixName;
+ return UnixName == "Linux" ? $"{LinuxVersion} / {LinuxDesktop}" : UnixName;
}
- var list = new List();
- list.Add(new Version(System.PlatformID.Win32NT, 5, 0, "Windows 2000"));
- list.Add(new Version(System.PlatformID.Win32NT, 5, 1, "Windows XP"));
- list.Add(new Version(System.PlatformID.Win32NT, 6, 0, "Vista"));
- list.Add(new Version(System.PlatformID.Win32NT, 6, 1, "Windows 7"));
- // After Windows 8 the Environment.OSVersion started misreporting information unless
- // your app has a manifest which says it supports the OS it is running on. This is not
- // helpful if someone starts using an app built before the OS is released. Anything that
- // reports its self as Windows 8 is suspect, and must get the version info another way.
- list.Add(new Version(PlatformID.Win32NT, 6, 3, "Windows 8.1"));
- list.Add(new Version(PlatformID.Win32NT, 10, 0, "Windows 10"));
+ var list = new List
+ {
+ new Version(PlatformID.Win32NT, 5, 0, "Windows 2000"),
+ new Version(PlatformID.Win32NT, 5, 1, "Windows XP"),
+ new Version(PlatformID.Win32NT, 6, 0, "Vista"),
+ new Version(PlatformID.Win32NT, 6, 1, "Windows 7"),
+ // After Windows 8 the Environment.OSVersion started misreporting information unless
+ // your app has a manifest which says it supports the OS it is running on. This is not
+ // helpful if someone starts using an app built before the OS is released. Anything that
+ // reports itself as Windows 8 is suspect, and must get the version info another way.
+ new Version(PlatformID.Win32NT, 6, 3, "Windows 8.1"),
+ new Version(PlatformID.Win32NT, 10, 0, "Windows 10")
+ };
foreach (var version in list)
{
@@ -725,11 +763,11 @@ private static string LinuxVersion
{
var versionData = File.ReadAllText("/etc/wasta-release");
var versionLines = versionData.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
- for (int i = 0; i < versionLines.Length; ++i)
+ foreach (var line in versionLines)
{
- if (versionLines[i].StartsWith("DESCRIPTION=\""))
+ if (line.StartsWith("DESCRIPTION=\""))
{
- _linuxVersion = versionLines[i].Substring(13).Trim(new char[] { '"' });
+ _linuxVersion = line.Substring(13).Trim('"');
break;
}
}
@@ -811,7 +849,7 @@ private static string LinuxDesktop
if (!string.IsNullOrEmpty(mirSession))
additionalInfo = " [display server: Mir]";
var gdmSession = Environment.GetEnvironmentVariable("GDMSESSION") ?? "not set";
- _linuxDesktop = String.Format("{0} ({1}{2})", currentDesktop, gdmSession, additionalInfo);
+ _linuxDesktop = $"{currentDesktop} ({gdmSession}{additionalInfo})";
}
return _linuxDesktop;
}
diff --git a/src/DesktopAnalytics/DesktopAnalytics.csproj b/src/DesktopAnalytics/DesktopAnalytics.csproj
index da4a6c2..d6c7af8 100644
--- a/src/DesktopAnalytics/DesktopAnalytics.csproj
+++ b/src/DesktopAnalytics/DesktopAnalytics.csproj
@@ -15,10 +15,13 @@
snupkg
+
+ All
+
-
+
all
diff --git a/src/SampleApp/SampleApp.csproj b/src/SampleApp/SampleApp.csproj
index fa8963d..b1959c6 100644
--- a/src/SampleApp/SampleApp.csproj
+++ b/src/SampleApp/SampleApp.csproj
@@ -9,7 +9,7 @@
false
-
+
diff --git a/SampleAppWithForm/Form1.Designer.cs b/src/SampleAppWithForm/Form1.Designer.cs
similarity index 98%
rename from SampleAppWithForm/Form1.Designer.cs
rename to src/SampleAppWithForm/Form1.Designer.cs
index e0bc1bd..1a1bbb8 100644
--- a/SampleAppWithForm/Form1.Designer.cs
+++ b/src/SampleAppWithForm/Form1.Designer.cs
@@ -1,4 +1,6 @@
-namespace SampleAppWithForm
+using System.Windows.Forms;
+
+namespace SampleAppWithForm
{
partial class Form1
{
diff --git a/SampleAppWithForm/Form1.cs b/src/SampleAppWithForm/Form1.cs
similarity index 68%
rename from SampleAppWithForm/Form1.cs
rename to src/SampleAppWithForm/Form1.cs
index 8c69bd6..2cc0db2 100644
--- a/SampleAppWithForm/Form1.cs
+++ b/src/SampleAppWithForm/Form1.cs
@@ -1,5 +1,7 @@
+using System;
+using System.Collections.Generic;
using System.Diagnostics;
-using DesktopAnalytics;
+using System.Windows.Forms;
namespace SampleAppWithForm
{
@@ -28,7 +30,14 @@ private void Form1_FormClosed(object sender, FormClosedEventArgs e)
Debug.WriteLine($"Succeeded: {Segment.Analytics.Client.Statistics.Succeeded}; " +
$"Submitted: {Segment.Analytics.Client.Statistics.Submitted}; " +
$"Failed: {Segment.Analytics.Client.Statistics.Failed}");
- Program.s_analyticsSingleton.Dispose();
+ // This allows us to illustrate the deadlock problem:
+ // https://github.com/segmentio/Analytics.NET/issues/200
+ if (_chkFlush.Checked)
+ Segment.Analytics.Client.Flush();
+ var stopwatch = Stopwatch.StartNew();
+ Program.s_analyticsSingleton?.Dispose();
+ stopwatch.Stop();
+ Debug.WriteLine($"Total wait = {stopwatch.Elapsed}");
}
}
}
\ No newline at end of file
diff --git a/SampleAppWithForm/Form1.resx b/src/SampleAppWithForm/Form1.resx
similarity index 100%
rename from SampleAppWithForm/Form1.resx
rename to src/SampleAppWithForm/Form1.resx
diff --git a/SampleAppWithForm/Program.cs b/src/SampleAppWithForm/Program.cs
similarity index 86%
rename from SampleAppWithForm/Program.cs
rename to src/SampleAppWithForm/Program.cs
index e3b1c5f..b102154 100644
--- a/SampleAppWithForm/Program.cs
+++ b/src/SampleAppWithForm/Program.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Collections.Generic;
+using System.Windows.Forms;
using DesktopAnalytics;
namespace SampleAppWithForm
@@ -11,8 +14,6 @@ internal static class Program
[STAThread]
static void Main(string[] args)
{
- ApplicationConfiguration.Initialize();
-
if (args.Length == 0)
Console.WriteLine("Usage: SampleApp ");
@@ -29,7 +30,9 @@ static void Main(string[] args)
var propertiesThatGoWithEveryEvent = new Dictionary {{"channel", "beta"}};
s_analyticsSingleton = new Analytics(args[0], userInfo, propertiesThatGoWithEveryEvent);
- Application.Run(new Form1());
+
+ var mainWindow = new Form1();
+ Application.Run(mainWindow);
}
}
}
\ No newline at end of file
diff --git a/src/SampleAppWithForm/SampleAppWithForm.csproj b/src/SampleAppWithForm/SampleAppWithForm.csproj
new file mode 100644
index 0000000..492ffcc
--- /dev/null
+++ b/src/SampleAppWithForm/SampleAppWithForm.csproj
@@ -0,0 +1,33 @@
+
+
+
+
+ WinExe
+ net461
+ true
+ SampleAppWithForm
+ SIL International
+ false
+
+
+
+
+
+
+
+
+ {BECACF91-9A84-4667-B678-44ACA350D413}
+ DesktopAnalytics
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file