Files
coder/coderd/httpmw/clitelemetry.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

92 lines
2.1 KiB
Go

package httpmw
import (
"encoding/base64"
"encoding/json"
"net/http"
"sync"
"time"
"golang.org/x/exp/maps"
"tailscale.com/tstime/rate"
"cdr.dev/slog/v3"
clitelemetry "github.com/coder/coder/v2/cli/telemetry"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/codersdk"
)
func ReportCLITelemetry(log slog.Logger, rep telemetry.Reporter) func(http.Handler) http.Handler {
var (
mu sync.Mutex
// We send telemetry at most once per minute.
limiter = rate.NewLimiter(rate.Every(time.Minute), 1)
// We map by timestamp to deduplicate invocations, since one invocation
// will send multiple requests, each with a duplicate header. It's still
// possible for duplicates to reach the telemetry service since requests
// can get processed by different coderds, but our analysis tools
// will deduplicate by timestamp as well.
//
// This approach just helps us reduce storage and ingest fees, and doesn't
// change the correctness.
queue = make(map[string]clitelemetry.Invocation)
)
log = log.Named("cli-telemetry")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// No matter what, we proceed with the request.
defer next.ServeHTTP(rw, r)
payload := r.Header.Get(codersdk.CLITelemetryHeader)
if payload == "" {
return
}
byt, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
log.Error(
r.Context(),
"base64 decode",
slog.Error(err),
)
return
}
var inv clitelemetry.Invocation
err = json.Unmarshal(byt, &inv)
if err != nil {
log.Error(
r.Context(),
"unmarshal header",
slog.Error(err),
)
return
}
// We do expensive work in a goroutine so we don't block the
// request.
go func() {
mu.Lock()
defer mu.Unlock()
queue[inv.InvokedAt.String()] = inv
if !limiter.Allow() && len(queue) < 1024 {
return
}
rep.Report(&telemetry.Snapshot{
CLIInvocations: maps.Values(queue),
})
log.Debug(
r.Context(),
"report sent", slog.F("count", len(queue)),
)
maps.Clear(queue)
}()
})
}
}