diff --git a/README.md b/README.md index a8f326a..59dcf6a 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,37 @@ bot := client.New("token", client.WithHTTPClient(fakeDoer{ The library's own generated test suite (`api/methods_gen_test.go`) covers 176 methods × 8 scenarios each: Success, APIError, NetworkError, ParseError, ContextCanceled, MissingRequiredFields, Forbidden, ServerError. +## Telemetry + +On the **first call to `client.New`** in a process, this library sends a single +anonymous HTTP POST to `https://oss.raczylo.com/v1/ping` containing exactly +this body: + +```json +{ "project": "go-telegram", "version": "0.7.11", "ts": 1747782200 } +``` + +This helps us see approximate adoption and version spread. No identifiers, +no telemetry of API calls, no message contents, no tokens, no IPs stored +beyond a short server-side dedupe window. The ping is fire-and-forget — +it never blocks `New`, never panics, never returns errors, and a 2-second +timeout caps any network impact. + +Telemetry source: [`client/telemetry.go`](client/telemetry.go) and the +upstream library [`github.com/lukaszraczylo/oss-telemetry`](https://github.com/lukaszraczylo/oss-telemetry). + +### Opting out + +Any one of these turns it off (case-insensitive truthy values +`1`, `true`, `yes`, `on`): + +| Mechanism | How | +| ------------------------ | -------------------------------------------- | +| Universal opt-out | `DO_NOT_TRACK=1` | +| Library-wide opt-out | `OSS_TELEMETRY_DISABLED=1` | +| Per-library opt-out | `GO_TELEGRAM_DISABLE_TELEMETRY=1` | +| Programmatic | `osstelemetry.Disable()` before `client.New` | + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/client/client.go b/client/client.go index e1cf3ad..e175f64 100644 --- a/client/client.go +++ b/client/client.go @@ -45,6 +45,7 @@ func (b *Bot) Logger() Logger { return b.logger } // NewDefaultHTTPDoer); the default codec wraps encoding/json; the default // logger discards records. func New(token string, opts ...Option) *Bot { + fireTelemetryOnce() b := &Bot{ token: token, base: defaultBaseURL, diff --git a/client/telemetry.go b/client/telemetry.go new file mode 100644 index 0000000..4de5a30 --- /dev/null +++ b/client/telemetry.go @@ -0,0 +1,33 @@ +package client + +import ( + "sync" + + telemetry "github.com/lukaszraczylo/oss-telemetry" +) + +// telemetryOnce guards the single anonymous "library used" ping that is sent +// on the first call to New. Long-running bots typically construct one Bot; +// short-lived programs or test suites may construct many, but the Once gate +// keeps the fire-and-forget call from amplifying into per-construction pings. +var telemetryOnce sync.Once + +// fireTelemetryOnce dispatches a fire-and-forget anonymous adoption ping. +// +// The call is failproof by contract of oss-telemetry: it never blocks New, +// never panics, never returns errors, and silently no-ops if disabled or +// if the network is unavailable. +// +// Opt-out is honored via any of these environment variables (case-insensitive +// truthy values "1", "true", "yes", "on"): +// +// - DO_NOT_TRACK +// - OSS_TELEMETRY_DISABLED +// - GO_TELEGRAM_DISABLE_TELEMETRY +// +// See README §Telemetry for the full disclosure. +func fireTelemetryOnce() { + telemetryOnce.Do(func() { + telemetry.Send("go-telegram", Version) + }) +} diff --git a/client/telemetry_test.go b/client/telemetry_test.go new file mode 100644 index 0000000..fed09c6 --- /dev/null +++ b/client/telemetry_test.go @@ -0,0 +1,54 @@ +package client + +import ( + "os" + "sync" + "testing" + + telemetry "github.com/lukaszraczylo/oss-telemetry" +) + +// TestMain disables outgoing telemetry for the duration of this package's +// test suite. The library's own tests construct many Bot instances; without +// this guard they would each contribute a real ping to the public endpoint. +// End-user test suites that construct Bot are not affected by this — only +// tests inside this package are. +func TestMain(m *testing.M) { + telemetry.Disable() + os.Exit(m.Run()) +} + +// TestFireTelemetryOnce_OnlyFiresOnce verifies the sync.Once gate. Even if +// New is called repeatedly, the underlying telemetry.Send is invoked at most +// once per process. We can't observe the network call directly (telemetry +// is disabled here via TestMain) so we assert on the once-Do count via a +// fresh local sync.Once paralleling the production one. +func TestFireTelemetryOnce_OnlyFiresOnce(t *testing.T) { + // Reset the package-level Once so this test starts from a clean state. + telemetryOnce = sync.Once{} + t.Cleanup(func() { telemetryOnce = sync.Once{} }) + + calls := 0 + probe := func() { telemetryOnce.Do(func() { calls++ }) } + + for i := 0; i < 50; i++ { + probe() + } + if calls != 1 { + t.Fatalf("expected exactly 1 Once execution, got %d", calls) + } +} + +// TestNew_DoesNotPanicUnderRepeatedConstruction is a smoke test that +// telemetry wiring does not affect New's existing contract. New must never +// panic, regardless of telemetry state. +func TestNew_DoesNotPanicUnderRepeatedConstruction(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("New panicked: %v", r) + } + }() + for i := 0; i < 20; i++ { + _ = New("test-token-" + string(rune('A'+i))) + } +} diff --git a/client/version.go b/client/version.go new file mode 100644 index 0000000..674d619 --- /dev/null +++ b/client/version.go @@ -0,0 +1,12 @@ +package client + +// Version is the released version of the go-telegram library. Keep in sync +// with the most recent git tag; this constant is bumped manually on each +// release. Exposed as a var (not const) so downstream applications may +// override it via linker flags: +// +// go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3" +// +// The value is also forwarded as the version field of the anonymous usage +// ping that fires on the first call to New (see fireTelemetryOnce). +var Version = "0.7.11" diff --git a/go.mod b/go.mod index 5a853c8..a9ad2aa 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.25.0 require ( github.com/goccy/go-json v0.10.6 + github.com/lukaszraczylo/oss-telemetry v0.0.0-20260521005811-e02d51419c52 github.com/stretchr/testify v1.9.0 + github.com/valyala/fasthttp v1.71.0 golang.org/x/net v0.54.0 ) @@ -15,6 +17,5 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.71.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5086b21..49bae85 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lukaszraczylo/oss-telemetry v0.0.0-20260521005811-e02d51419c52 h1:HAm1OV/1uYN3VA/HdDNFjwh8KerTLwl1SoxF+IiNf/M= +github.com/lukaszraczylo/oss-telemetry v0.0.0-20260521005811-e02d51419c52/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -16,6 +18,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=