.NET

One package for WPF, WinForms, Avalonia, MAUI, console tools and ASP.NET.

Every call appends to a bounded in-memory queue and returns: nothing throws into your code, nothing blocks your thread, and if firstrun is unreachable your application is unaffected. No dependencies on any target framework.

Install the package

Terminal
dotnet add package Firstrun
dotnet add package Firstrun.Extensions.Hosting   # ASP.NET and IHost only

Create the client

A desktop app
App.xaml.cs
using Firstrun;

Analytics = new FirstrunClient(new FirstrunOptions
{
    SourceKey   = "fr_xxxxxxxxxxxxxxxx",
    Host        = "https://app.firstrun.app",
    ServiceName = "YourApp",   // names the folder the anonymous id lives in
    Channel     = "stable",
});

app_install on the first run ever and app_launch on everyrun are queued by the constructor. ServiceVersion defaults to the entry assembly, and the operating system, architecture and locale to the machine. Set ServiceName so the anonymous id on disk survives a key rotation. For ASP.NET, builder.Services.AddFirstrunServer(sourceKey, host) registers the same client as a singleton and flushes on shutdown.

Write events

C#
Analytics.Event("exported_project", new FirstrunAttributes()
    .Set("format", "pdf")
    .Set("pages", 12));

Analytics.Error(ex);

Analytics.Log(new FirstrunEvent("render_stalled")
{
    Severity   = Severity.Warn,
    Attributes = new FirstrunAttributes().Set("frames_dropped", 41),
});

Analytics.Identify("acct_8812");   // your own id, when they sign in
Analytics.Dispose();               // on exit. Optional: the worker is a background thread

Event writes at INFO and Error at ERROR, both filling in the conventional attributes; Log takes any name, any severity and any attributes. Values go on the wire as JSON and Set converts with the invariant culture, so a German machine does not send 1,5.

On a server, distinctId is yours to supply. A desktop install has a per-machine id on disk. A server process serves thousands of different people and has nothing correct to default to, so pass an id you already have per call: a cookie, a session key, an account id. Leave it out and every event in your fleet collapses onto a handful of ids, and your unique counts become a count of your processes.