Skip to content
52 changes: 52 additions & 0 deletions src/DesktopAnalytics/Analytics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;
using JetBrains.Annotations;
Expand Down Expand Up @@ -738,6 +739,19 @@ public void Dispose()
_client?.ShutDown();
}

/// <summary>
/// Async alternative to <see cref="Dispose"/> for hosts that shut down asynchronously:
/// flushes/shuts down the underlying client without blocking a thread on any in-flight
/// delivery. Bounded just like Dispose -- an offline shutdown still returns promptly, with
/// undelivered events left on disk (Mixpanel) or in the transport library's own store
/// (Segment) for the next launch. Calling <see cref="Dispose"/> afterwards (e.g. from a
/// <c>using</c> block wrapping this object) is a harmless no-op.
/// </summary>
public Task ShutDownAsync()
{
return _client?.ShutDownAsync() ?? Task.CompletedTask;
}

/// <summary>
/// Indicates whether we are tracking or not
/// </summary>
Expand Down Expand Up @@ -765,6 +779,34 @@ public static bool AllowTracking
s_singleton.Initialize(initializationParameters);
return; // Initialize sets s_allowTracking = true
}

// Already initialized and now re-enabled: re-arm any background flush loop that a
// prior PurgeQueuedEvents (consent revocation) paused. See offline-analytics.md.
try
{
s_singleton._client?.ResumeSending();
}
catch (Exception e)
{
Debug.WriteLine("Analytics.AllowTracking: ResumeSending failed: " + e);
}
}

if (!value)
{
// Consent revoked: purge any durable client's on-disk spool immediately (see
// offline-analytics.md, "Consent & lifecycle"). Guarded against a null singleton
// (AllowTracking can technically be set before an Analytics object is
// constructed) and a null client (the Segment/Mixpanel client is only assigned
// inside the Analytics constructor).
try
{
s_singleton?._client?.PurgeQueuedEvents();
}
catch (Exception e)
{
Debug.WriteLine("Analytics.AllowTracking: PurgeQueuedEvents failed: " + e);
}
}

s_allowTracking = value;
Expand Down Expand Up @@ -1084,5 +1126,15 @@ public static void FlushClient()
{
s_singleton._client?.Flush();
}

/// <summary>
/// Async counterpart of <see cref="FlushClient"/>: attempts delivery of anything pending
/// without blocking a thread on the network. Bounded -- returns promptly even while
/// offline, leaving undelivered events queued.
/// </summary>
public static Task FlushClientAsync()
{
return s_singleton._client?.FlushAsync() ?? Task.CompletedTask;
}
}
}
83 changes: 83 additions & 0 deletions src/DesktopAnalytics/AnalyticsEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Text;
using Segment.Serialization;

namespace DesktopAnalytics
{
/// <summary>
/// A single spoolable analytics event in a neutral, JSON-serializable form. This is the
/// unit written to and read from the on-disk event spool (DiskQueue), independent of the
/// eventual transport (Mixpanel, etc). Instances are round-tripped as UTF-8 JSON bytes so
/// the spool stays portable and inspectable (see <see cref="ToBytes"/>/<see cref="FromBytes"/>).
/// </summary>
internal class AnalyticsEvent
{
public string AnalyticsId { get; set; }
public string EventName { get; set; }

/// <summary>Event properties, e.g. Message/Stack Trace for exception reports.</summary>
public JsonObject Properties { get; set; }

/// <summary>Mixpanel's $insert_id, stamped at enqueue time so at-least-once replay can
/// be safely deduplicated by the backend.</summary>
public string InsertId { get; set; }

/// <summary>The original event time, stamped at enqueue time so a later replay is
/// back-dated correctly rather than reported at delivery time.</summary>
public DateTimeOffset Time { get; set; }

public AnalyticsEvent()
{
Properties = new JsonObject();
}

/// <summary>
/// Creates a new event, stamping <see cref="InsertId"/> and <see cref="Time"/>. Both are
/// overridable so callers/tests can be deterministic; production code can omit them to get
/// a real GUID and the real wall-clock time.
/// </summary>
/// <param name="analyticsId">The stable per-user analytics id.</param>
/// <param name="eventName">The event name.</param>
/// <param name="properties">Event properties. May be null, in which case an empty
/// property bag is used.</param>
/// <param name="insertId">The $insert_id. Defaults to a fresh GUID. Never call
/// Guid.NewGuid() directly elsewhere in the stamping path -- pass it here so tests are
/// deterministic.</param>
/// <param name="time">The event time. Defaults to <c>DateTimeOffset.UtcNow</c>. Never
/// call DateTime.Now/DateTimeOffset.UtcNow directly elsewhere in the stamping path --
/// pass it here (e.g. from an injected <see cref="TimeProvider"/>) so tests are
/// deterministic.</param>
public static AnalyticsEvent Create(
string analyticsId,
string eventName,
JsonObject properties = null,
string insertId = null,
DateTimeOffset? time = null
)
{
return new AnalyticsEvent
{
AnalyticsId = analyticsId,
EventName = eventName,
Properties = properties ?? new JsonObject(),
InsertId = insertId ?? Guid.NewGuid().ToString(),
Time = time ?? DateTimeOffset.UtcNow
};
}

/// <summary>Serializes this event as UTF-8 JSON bytes, suitable for enqueuing into the
/// on-disk spool.</summary>
public byte[] ToBytes()
{
var json = JsonUtility.ToJson(this, false);
return Encoding.UTF8.GetBytes(json);
}

/// <summary>Deserializes an event previously produced by <see cref="ToBytes"/>.</summary>
public static AnalyticsEvent FromBytes(byte[] bytes)
{
var json = Encoding.UTF8.GetString(bytes);
return JsonUtility.FromJson<AnalyticsEvent>(json);
}
}
}
3 changes: 3 additions & 0 deletions src/DesktopAnalytics/DesktopAnalytics.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
<AssemblyOriginatorKeyFile>..\..\DesktopAnalytics.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DiskQueue" Version="1.7.2" />
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Bcl.TimeProvider" Version="8.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Polly" Version="8.4.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="mixpanel-csharp" Version="6.0.0" />
Expand Down
Loading
Loading