Server-side Go 1.21+, standard library only, one sender goroutine.
Every call puts an event on a bounded channel and returns, taking no lock a request path would contend on: nothing panics into your code, nothing blocks, and if firstrun is unreachable your program keeps working. No dependencies.
go get firstrun.dev/goimport firstrun "firstrun.dev/go"
analytics, err := firstrun.New(firstrun.Options{
SourceKey: os.Getenv("FIRSTRUN_SOURCE_KEY"), // fr_server_...
Host: "https://app.firstrun.app",
ServiceVersion: os.Getenv("GIT_SHA"),
OnDiagnostic: func(d firstrun.Diagnostic) {
slog.Warn("firstrun", "code", d.Code, "msg", d.Message)
},
})Bad configuration returns an error and a usable, disabled client, so ignoring the error is safe and a typo in an environment variable cannot stop your service booting. OnDiagnostic is the only reporting channel and runs inline on the calling goroutine, so keep it cheap and safe for concurrent use.
analytics.Event("exported_csv", firstrun.Attrs{"rows": len(rows)},
firstrun.With{DistinctID: user.ID})
analytics.Error(err, firstrun.Attrs{"http.route": "/reports/{id}"},
firstrun.With{DistinctID: user.ID})
analytics.Log(firstrun.Entry{
Name: "queue_depth",
Severity: firstrun.INFO,
DistinctID: "worker-3",
Attributes: firstrun.Attrs{"firstrun.metric": "queue_depth", "firstrun.value": depth},
})
_ = analytics.Close(ctx) // on shutdown. Bounded by the context, idempotentNot deferred, not waited on, and nothing here can fail. 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, so an int stays a number. A zero Time means now. This client installs no signal handler of its own, so call Close on the shutdown path you already have.
Options.DistinctID only when the process really is the subject, such as a CLI or a device agent.