mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
feat: add anonymous usage reporting, matching traefikoidc
Release builds send one anonymous "plugin loaded" ping per process, so the project can see roughly how many installs exist and which versions are in use. The payload is project name, version and timestamp. Nothing else leaves the pod: no identifiers, no hostname, no configuration, no provider names, no keys, no request data. Three things keep it from firing when it should not. Builds from source carry the devPluginVersion sentinel and stay silent, so developer checkouts and every test run never phone home. The ping is sent only after the plugin constructs successfully, so a rejected configuration is never counted as an install. A sync.Once holds it to one per process, because Traefik calls New once per route and oss-telemetry does not deduplicate on the client side. Operators of a release build opt out with DO_NOT_TRACK=1, OSS_TELEMETRY_DISABLED=1, or TRAEFIK_LLMGATEWAY_DISABLE_TELEMETRY=1. README.md documents the full payload and the opt-out; a plugin that phones home without saying so in its own README would be indefensible. oss-telemetry is the plugin's first non-stdlib RUNTIME dependency, which has consequences for the Yaegi gate: - vendor/ is no longer excluded from yaegi-check's GOPATH copy. It was excluded on the reasoning that it held only test-only testify and _test.go files are never interpreted. That reasoning no longer holds: leaving vendor out would make the interpreted copy fail to resolve an import the real plugin tree resolves fine, so the gate would stop representing production. - yaegi-check now stamps a fake release version into its GOPATH copy so New's telemetry gate is PASSED rather than short-circuited, and the vendored package is actually called under the interpreter. Import resolution alone is not coverage; a cross-package call into interpreted third-party code is its own risk. Telemetry is suppressed by env vars set before anything is interpreted, so the gate never emits a real ping. - A new assertion reads the version back through GET /admin/api/overview. Without it a silently failed stamp would leave the dev sentinel in place and the harness would still print OK while covering nothing. Verified by disabling the stamp and watching the gate fail. shouldSendTelemetry is split out from sendStartupTelemetry so the decision is table-testable without a network, since oss-telemetry's endpoint is deliberately not overridable from consuming code. Also brings the version-gated documentation up to date, since the same README edit touches it. The latest release is v1.0.3, so the pre-1.0 experimental warning has met its own stated condition and is replaced by a semver stability statement in README.md and dropped from .traefik.yml's catalog summary. The catalog now serves the released tags (verified: the v1.0.3 download endpoint returns 200), so the "not yet published" and "analyzer has not been run" claims are corrected, the catalog form of the static config is documented as usable, and examples/kubernetes.yaml pins v1.0.3 instead of v0.1.0.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -853,3 +854,74 @@ func TestNewGateway_SetsPricingWarnFn(t *testing.T) {
|
||||
t.Fatalf("want the pricing warning routed through the gateway's own logf, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestShouldSendTelemetry pins the guard that keeps unstamped builds from
|
||||
// phoning home. Every developer checkout and every CI run carries
|
||||
// devPluginVersion, so the false cases are the state this code is in
|
||||
// nearly all of the time: if the guard inverts, `go test` itself starts
|
||||
// emitting pings.
|
||||
func TestShouldSendTelemetry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
want bool
|
||||
}{
|
||||
{"dev sentinel stays silent", devPluginVersion, false},
|
||||
{"empty version stays silent", "", false},
|
||||
{"stamped release sends", "0.2.38", true},
|
||||
{"stamped prerelease sends", "1.0.0-rc1", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := shouldSendTelemetry(tt.version); got != tt.want {
|
||||
t.Errorf("shouldSendTelemetry(%q) = %v, want %v", tt.version, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPluginVersion_UnstampedInSourceTree guards the committed state: the
|
||||
// repository must never carry a stamped version, or a developer's own
|
||||
// build would phone home. workflow-prepare.sh stamps a COPY at release
|
||||
// time; if a stamped value is ever committed by mistake, this fails.
|
||||
func TestPluginVersion_UnstampedInSourceTree(t *testing.T) {
|
||||
if pluginVersion != devPluginVersion {
|
||||
t.Errorf("committed pluginVersion = %q, want the %q sentinel — a stamped version must never be committed", pluginVersion, devPluginVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNew_DoesNotPanicOnRepeatedConstruction covers the telemetry
|
||||
// sync.Once gate under the real call pattern: Traefik constructs this
|
||||
// middleware once PER ROUTE, so New runs many times in one process.
|
||||
// Construction must stay safe and cheap on every later call.
|
||||
func TestNew_DoesNotPanicOnRepeatedConstruction(t *testing.T) {
|
||||
t.Setenv("DO_NOT_TRACK", "1")
|
||||
cfg := CreateConfig()
|
||||
cfg.Providers = map[string]*ProviderConfig{"openai": {Type: "openai", APIKey: "k"}}
|
||||
for i := 0; i < 5; i++ {
|
||||
h, err := New(context.Background(), http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}), cfg, "llmgw")
|
||||
if err != nil {
|
||||
t.Fatalf("New #%d: %v", i, err)
|
||||
}
|
||||
if h == nil {
|
||||
t.Fatalf("New #%d returned a nil handler", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendStartupTelemetry_OnceAcrossConcurrentCalls pins the sync.Once
|
||||
// gate itself: Traefik can construct several routes concurrently at
|
||||
// startup, and oss-telemetry does not deduplicate client-side, so a
|
||||
// missing gate would emit one ping per route.
|
||||
func TestSendStartupTelemetry_OnceAcrossConcurrentCalls(t *testing.T) {
|
||||
t.Setenv("DO_NOT_TRACK", "1")
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sendStartupTelemetry()
|
||||
}()
|
||||
}
|
||||
wg.Wait() // -race proves the Once is the only synchronisation needed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user