Before we dive in: I share practical insights like this weekly. Join developers and founders getting my newsletter with real solutions to engineering and business challenges.
I've built a few CLI tools that have picked up some decent usage. CC Switch helps developers manage multiple Claude Code sessions, and it's being used by quite a few people now. Every so often, someone asks: "Do you track usage? Would be interesting to know how people use it."
The answer is no, I don't track it. Privacy first. But it's a fair question, and one that kept coming up as I've built more tools. So I spent some time digging into how you'd actually integrate Google Analytics with Go if you wanted to. Turns out there are two completely different directions people care about: sending events to Google Analytics from your Go backend, and pulling analytics data out to build custom dashboards.
Most people searching for "golang google analytics" want to send data. That's what we'll focus on here.
The Two Directions
When people talk about Google Analytics integration with Go, they're usually asking about one of two things:
Sending events to Google Analytics - This is server-side tracking. Your Go application sends events to GA4 using the Measurement Protocol. Use cases include tracking CLI tool usage, API calls, backend conversions, or any server-side events you want to measure. No JavaScript required, no browser needed.
Pulling data from Google Analytics - This is about fetching your analytics data programmatically to build custom dashboards or automate reports. You use the Reporting API to query GA4 and get data back. More complex to set up, requires OAuth, but useful when you want to do something with your analytics data beyond what the GA4 interface offers.
The key difference: sending is straightforward, pulling is complicated. If you're tracking a CLI tool or backend service, you only need the first one.
Why Track a CLI Tool or Backend Service
Before we get into implementation, it's worth thinking about whether you should track at all. For CC Switch, I decided against it. Privacy matters more than knowing which commands people use most.
But there are legitimate reasons to track:
Understanding feature usage - Which commands get used? Which flags are popular? This helps prioritise development effort on features people actually use rather than ones you think are interesting.
Error and crash reporting - Knowing when and where things break helps fix issues faster. If a specific command fails frequently on a particular OS, that's useful signal.
Version adoption - Are people updating? If everyone's stuck on an old version, you might have a release process problem.
Geographic distribution - If you're making decisions about CDN locations or regional support, knowing where your users are helps.
The key is tracking events, not people. Anonymous client IDs, no personal information, easy opt-out. When I built GitPilotAI, I considered adding tracking to understand how the tool was being used in different workflows. Didn't implement it, but the technical approach would have been identical to what we're covering here.
The Go Library Landscape
There are a few libraries for working with Google Analytics in Go. Here's what's available:
| Library | Stars | Last Update | GA4 Support | Purpose |
|---|---|---|---|---|
| jpillora/go-ogle-analytics | 60+ | 2019 | No (UA only) | Sending events |
| olebedev/go-gamp | 10+ | 2021 | No (UA only) | Sending events |
| ozgur-yalcin/google-analytics | 5+ | 2023 | No (UA only) | Sending events |
| Raw HTTP | N/A | Current | Yes | Sending events |
| google.golang.org/api/analytics | Official | Active | v3 only | Pulling data |
| google.golang.org/api/analyticsdata | Official | Active | Yes (GA4) | Pulling data |
The problem: most third-party libraries were built for Universal Analytics, which Google sunset in July 2024. They don't work with GA4, which is what you need now.
The practical reality: You're better off using raw HTTP requests to GA4's Measurement Protocol. It's simpler than you'd think, gives you full control, and you're not depending on abandoned libraries. For pulling data, use the official Google client libraries.
Sending Events: The Measurement Protocol
The Measurement Protocol is Google's API for sending events to Analytics from anywhere. No JavaScript needed, just HTTP POST requests. It's designed for server-side tracking.
Here's what you need:
- Measurement ID - Format:
G-XXXXXXXXXX, found in GA4 under Admin > Data Streams - API Secret - Generated in GA4 under Admin > Data Streams > Measurement Protocol
- Client ID - A UUID you generate and persist for each unique user/instance
The endpoint is straightforward:
https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET
Notice there's no OAuth, no service accounts, no complex authentication. Just include your measurement ID and API secret in the URL. This is intentionally simple because Google wants you to send them data.
Implementation: Tracking CLI Commands
Let's build a basic tracker that you could drop into any Go CLI tool. This is production-ready code, not a toy example.
package analytics
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
)
type Tracker struct {
MeasurementID string
APISecret string
ClientID string
Enabled bool
client *http.Client
}
type Event struct {
Name string `json:"name"`
Params map[string]interface{} `json:"params"`
}
type Payload struct {
ClientID string `json:"client_id"`
Events []Event `json:"events"`
}
func NewTracker(measurementID, apiSecret, clientID string) *Tracker {
return &Tracker{
MeasurementID: measurementID,
APISecret: apiSecret,
ClientID: clientID,
Enabled: true,
client: &http.Client{
Timeout: 2 * time.Second,
},
}
}
func (t *Tracker) TrackEvent(ctx context.Context, eventName string, params map[string]interface{}) error {
if !t.Enabled {
return nil
}
// Run async so we don't block the CLI
go func() {
if err := t.sendEvent(context.Background(), eventName, params); err != nil {
// Log error but don't crash
// In production, you'd use proper logging
fmt.Printf("Analytics error: %v\n", err)
}
}()
return nil
}
func (t *Tracker) sendEvent(ctx context.Context, eventName string, params map[string]interface{}) error {
payload := Payload{
ClientID: t.ClientID,
Events: []Event{
{
Name: eventName,
Params: params,
},
},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
url := fmt.Sprintf(
"https://www.google-analytics.com/mp/collect?measurement_id=%s&api_secret=%s",
t.MeasurementID,
t.APISecret,
)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := t.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}
// GenerateClientID creates a new anonymous client ID
func GenerateClientID() string {
return uuid.New().String()
}
This tracker does a few important things:
Non-blocking execution - Tracking happens in a goroutine so it never slows down your CLI. If tracking fails, your tool still works.
Timeout handling - 2-second timeout means you won't wait forever if GA4 is down. The request fails fast and your tool continues.
Error handling - Errors are logged but don't crash the application. Tracking is secondary to functionality.
Context support - Proper context handling for cancellation and timeouts.
Using the Tracker
Here's how you'd integrate this into a CLI tool:
package main
import (
"context"
"flag"
"fmt"
"os"
"your-project/analytics"
)
var (
measurementID = "G-XXXXXXXXXX"
apiSecret = "your_api_secret_here"
version = "0.1.0"
)
func main() {
// Check for opt-out
if os.Getenv("ANALYTICS_DISABLE") == "1" {
// Skip tracker initialisation
runCommand(nil)
return
}
// Get or create client ID
clientID, err := getClientID()
if err != nil {
// If we can't get a client ID, just run without tracking
runCommand(nil)
return
}
tracker := analytics.NewTracker(measurementID, apiSecret, clientID)
runCommand(tracker)
}
func runCommand(tracker *analytics.Tracker) {
command := flag.String("command", "", "Command to run")
flag.Parse()
// Track command execution
if tracker != nil {
ctx := context.Background()
tracker.TrackEvent(ctx, "command_executed", map[string]interface{}{
"command": *command,
"version": version,
})
}
// Actually run your command
switch *command {
case "hello":
fmt.Println("Hello, World!")
default:
fmt.Println("Unknown command")
}
}
func getClientID() (string, error) {
// Check if client ID exists in config file
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := filepath.Join(homeDir, ".your-cli")
clientIDFile := filepath.Join(configDir, "client-id")
// Try to read existing ID
data, err := os.ReadFile(clientIDFile)
if err == nil {
return string(data), nil
}
// Generate new ID
clientID := analytics.GenerateClientID()
// Create config directory
if err := os.MkdirAll(configDir, 0755); err != nil {
return clientID, nil // Return ID but don't persist
}
// Save client ID
if err := os.WriteFile(clientIDFile, []byte(clientID), 0644); err != nil {
return clientID, nil // Return ID but don't persist
}
return clientID, nil
}
Key points here:
Environment variable opt-out - ANALYTICS_DISABLE=1 completely disables tracking. Simple and discoverable.
Graceful degradation - If anything fails (can't get client ID, can't write config), the tool still works. Tracking is optional.
Persistent client ID - Stored in ~/.your-cli/client-id so the same instance gets the same ID across runs. This is anonymous but consistent.
Testing Without Polluting Production
GA4 has a debug endpoint that validates your events without recording them:
func (t *Tracker) sendEventDebug(ctx context.Context, eventName string, params map[string]interface{}) error {
// Same payload construction as before
payload := Payload{
ClientID: t.ClientID,
Events: []Event{
{
Name: eventName,
Params: params,
},
},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
// Use debug endpoint instead
url := fmt.Sprintf(
"https://www.google-analytics.com/debug/mp/collect?measurement_id=%s&api_secret=%s",
t.MeasurementID,
t.APISecret,
)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := t.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Read and print validation response
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
fmt.Printf("Validation result: %+v\n", result)
return nil
}
The debug endpoint returns validation messages telling you if your event structure is correct. Use this during development, then switch to the production endpoint when you're ready.
You can also verify events in GA4's real-time reports. They show up within seconds, so you can test and see results immediately.
Privacy Considerations
When I decided not to track CC Switch, it came down to a few principles:
Anonymous by default - Client IDs should be random UUIDs, not tied to any user identity. No names, no emails, no IP addresses beyond what GA4 automatically collects.
Easy opt-out - ANALYTICS_DISABLE=1 is simple and discoverable. Document it clearly in your README.
No sensitive data - Track command names and flags, but not the values. If someone runs your-cli connect --server prod-db-01, track that they used the connect command with the --server flag, but not which server.
Be transparent - Tell people you're tracking. In your README, explain what you collect and why. Give them the opt-out mechanism upfront.
Respect context - Open source tools? Privacy matters more. Internal corporate tools? Different expectations. SaaS products? You're probably already collecting data anyway.
For context on security considerations when building production tools, I've written about avoiding critical mistakes in AI-generated code, which includes thinking through these trade-offs carefully.
What You Actually Track
Here's what a typical event payload looks like:
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"events": [
{
"name": "command_executed",
"params": {
"command": "hello",
"version": "0.1.0",
"flags_used": "name,output",
"engagement_time_msec": 100
}
}
]
}
The engagement_time_msec parameter helps GA4 calculate active users. Set it to something small like 100ms for CLI tools.
You can add custom parameters for anything you want to track:
params := map[string]interface{}{
"command": "build",
"version": version,
"flags_used": strings.Join(flagsUsed, ","),
"execution_time_ms": executionTime,
"success": success,
"error_type": errorType, // Only if error occurred
}
GA4 allows up to 25 parameters per event and 25 events per request. For CLI tools, you'll rarely hit these limits.
Authentication Deep Dive
The Measurement Protocol is deliberately simple. You need three things:
- Measurement ID (G-XXXXXXXXXX)
- API Secret
- Client ID (you generate this)
No OAuth, no service accounts, no JWT tokens. Just include the measurement ID and API secret in the URL, and you're authenticated.
This works because you're sending data TO Google, not requesting data FROM Google. They want to make sending events as frictionless as possible.
Getting your credentials:
- Go to GA4 property
- Admin > Data Streams > Select your stream
- Note your Measurement ID
- Scroll down to "Measurement Protocol API secrets"
- Create new secret
- Copy the secret value
Store these as environment variables:
export GA_MEASUREMENT_ID="G-XXXXXXXXXX"
export GA_API_SECRET="your_secret_here"
Never commit these to version control. Use environment variables or a config file that's in your .gitignore.
Pulling Data: The Reporting API
If you want to pull data out of GA4 - build dashboards, automate reports, analyse trends - you need the Reporting API. This is more complex than sending events.
Authentication complexity:
You need a service account with the right permissions. This involves:
- Create a service account in Google Cloud Console
- Download the JSON key file
- Grant the service account access to your GA4 property
- Use the JSON key to authenticate your requests
Using the official Go library:
import (
"context"
"google.golang.org/api/analyticsdata/v1beta"
"google.golang.org/api/option"
)
func getAnalyticsData() error {
ctx := context.Background()
// Authenticate using service account JSON key
client, err := analyticsdata.NewService(ctx,
option.WithCredentialsFile("service-account-key.json"))
if err != nil {
return err
}
// Build your report request
request := &analyticsdata.RunReportRequest{
DateRanges: []*analyticsdata.DateRange{
{
StartDate: "7daysAgo",
EndDate: "today",
},
},
Metrics: []*analyticsdata.Metric{
{Name: "activeUsers"},
},
Dimensions: []*analyticsdata.Dimension{
{Name: "city"},
},
}
// Execute the request
response, err := client.Properties.RunReport(
"properties/YOUR_PROPERTY_ID",
request,
).Do()
if err != nil {
return err
}
// Process response
for _, row := range response.Rows {
// Handle your data
}
return nil
}
This is significantly more involved than sending events. You're dealing with OAuth flows, service account permissions, and complex API responses. For most CLI tool use cases, you don't need this.
If you're building internal dashboards or need to automate reporting, the Reporting API makes sense. For simple usage tracking, stick with sending events.
When Tracking Makes Sense
After building several tools and thinking through when to implement tracking, here's my framework:
Track when:
- You're building internal tools for your company (different privacy expectations)
- You're running a SaaS product (you're already collecting data)
- Understanding usage helps prioritise development (limited time, want to focus on used features)
- Error tracking helps users (knowing what breaks helps fix it faster)
- You can be transparent about it (clear documentation, easy opt-out)
Don't track when:
- Privacy matters more than the data (open source tools, security-focused applications)
- You're collecting for vanity metrics (number of users doesn't help you build better software)
- You can't be transparent (if you wouldn't want to document what you're tracking, don't track it)
- The data won't change your decisions (tracking for the sake of tracking adds complexity)
- Your users expect privacy (tools for security researchers, privacy advocates, etc.)
For CC Switch, privacy won out. The tool helps developers work more efficiently, and I don't need to know how they use it to keep improving it. User feedback and GitHub issues tell me what matters.
For a SaaS product? Different calculation. You're already running authentication, storing user data, and providing a service. Tracking feature usage helps build a better product.
Alternatives to Google Analytics
If you want tracking but don't want Google involved:
Self-hosted options:
- Plausible - Privacy-focused, simple, clean interface. Open source with paid hosted option.
- Umami - Minimalist analytics, easy to self-host, PostgreSQL or MySQL backend.
- Matomo - Full-featured, open source, been around forever. More complex but powerful.
Simple approaches:
- Log to your own backend - POST to an endpoint you control, store in PostgreSQL, query as needed.
- Telemetry libraries - Segment, PostHog, or similar services designed for product analytics.
- Simple ping - HTTP GET to your server with query parameters, log to file, analyse later.
The implementation for any of these is similar to what we've covered. You're still sending HTTP requests with event data, just to different endpoints with different authentication.
The Reality
Most people searching for "golang google analytics" want to send events from their backend to GA4. The implementation is simpler than you'd expect - no complex authentication, just HTTP POST requests to the Measurement Protocol endpoint.
The hard part isn't the code, it's the decision. Should you track? What should you track? How do you respect privacy while getting useful data?
I still don't track CC Switch. The privacy trade-off doesn't make sense for that tool. But if I were building a SaaS product or internal tooling where usage data helps prioritise development, this is exactly how I'd implement it.
The code is straightforward: generate a client ID, build your event payload, POST to GA4's endpoint. Run it async so it never blocks your application. Handle errors gracefully so tracking failures don't crash your tool. Provide an easy opt-out so users control their privacy.
That's the entire implementation. No fancy libraries needed, no complex authentication flows, just practical Go code that solves the problem.
If you're building similar tools and want to see how I approach these problems, I've written about automating Git workflows with GitPilotAI and managing multiple development sessions with CC Switch. The pattern is the same: identify the problem, build the simplest solution that works, respect user privacy.
