Go

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.

Install the package

Terminal
go get firstrun.dev/go

Create the client

Go
import 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.

Write events

Go
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, idempotent

Not 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.

DistinctID is yours to supply. A browser has a visitor id and a desktop install has one on disk. A server has neither, so pass an id you already have per call. An event without one is dropped and reported rather than sent under an invented id: a loud failure beats a silently wrong number nobody can spot from a dashboard. Set Options.DistinctID only when the process really is the subject, such as a CLI or a device agent.