Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"fmt"
"io"
"net/http"
"runtime"
"time"

"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
"github.com/DataDog/pup/internal/version"
"github.com/DataDog/pup/pkg/auth/storage"
"github.com/DataDog/pup/pkg/config"
)
Expand Down Expand Up @@ -71,6 +73,9 @@ func New(cfg *config.Config) (*Client, error) {
configuration := datadog.NewConfiguration()
configuration.Host = fmt.Sprintf("api.%s", cfg.Site)

// Set custom user agent to identify requests as coming from pup CLI
configuration.UserAgent = getUserAgent()

// Enable all unstable operations to suppress warnings
// These are beta/preview features that we want to use
unstableOps := []string{
Expand Down Expand Up @@ -131,6 +136,7 @@ func (c *Client) RawRequest(method, path string, body io.Reader) (*http.Response

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", getUserAgent())

// Set auth headers from context
if token, ok := c.ctx.Value(datadog.ContextAccessToken).(string); ok && token != "" {
Expand All @@ -152,3 +158,14 @@ func (c *Client) RawRequest(method, path string, body io.Reader) (*http.Response

return resp, nil
}

// getUserAgent returns a custom user agent string identifying the pup CLI
func getUserAgent() string {
return fmt.Sprintf(
"pup/%s (go %s; os %s; arch %s)",
version.Version,
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
)
}
137 changes: 137 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import (
"io"
"net/http"
"net/http/httptest"
"runtime"
"strings"
"testing"

"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
datadogV1 "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
"github.com/DataDog/pup/internal/version"
"github.com/DataDog/pup/pkg/config"
)

Expand Down Expand Up @@ -489,3 +492,137 @@ func TestClient_APIConfiguration(t *testing.T) {
t.Errorf("Configuration site = %s, want datadoghq.eu", client.config.Site)
}
}

func TestGetUserAgent(t *testing.T) {
userAgent := getUserAgent()

// Check that it starts with "pup/"
if !strings.HasPrefix(userAgent, "pup/") {
t.Errorf("User-Agent should start with 'pup/', got: %s", userAgent)
}

// Check that it contains the version
if !strings.Contains(userAgent, version.Version) {
t.Errorf("User-Agent should contain version '%s', got: %s", version.Version, userAgent)
}

// Check that it contains Go version
if !strings.Contains(userAgent, runtime.Version()) {
t.Errorf("User-Agent should contain Go version '%s', got: %s", runtime.Version(), userAgent)
}

// Check that it contains OS
if !strings.Contains(userAgent, runtime.GOOS) {
t.Errorf("User-Agent should contain OS '%s', got: %s", runtime.GOOS, userAgent)
}

// Check that it contains architecture
if !strings.Contains(userAgent, runtime.GOARCH) {
t.Errorf("User-Agent should contain arch '%s', got: %s", runtime.GOARCH, userAgent)
}

// Verify format: pup/<version> (go <version>; os <os>; arch <arch>)
if !strings.Contains(userAgent, "(go ") {
t.Errorf("User-Agent should contain '(go ', got: %s", userAgent)
}
if !strings.Contains(userAgent, "; os ") {
t.Errorf("User-Agent should contain '; os ', got: %s", userAgent)
}
if !strings.Contains(userAgent, "; arch ") {
t.Errorf("User-Agent should contain '; arch ', got: %s", userAgent)
}

t.Logf("User-Agent: %s", userAgent)
}

// captureTransport is a custom HTTP RoundTripper that captures request headers
type captureTransport struct {
transport http.RoundTripper
capturedHeader http.Header
}

func (c *captureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Capture the headers before the request is sent
c.capturedHeader = req.Header.Clone()

// Use the underlying transport or default
transport := c.transport
if transport == nil {
transport = http.DefaultTransport
}

return transport.RoundTrip(req)
}

func TestClient_IntegrationUserAgentInAPIClient(t *testing.T) {
// Integration test: verify User-Agent is automatically set by API client configuration
// This captures actual requests made through the Datadog API client

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Return a minimal valid API response
w.Write([]byte(`{"data":{"type":"monitor","id":"12345","attributes":{"name":"test"}}}`))
}))
defer server.Close()

// Extract host from server URL
serverURL := strings.TrimPrefix(server.URL, "http://")
serverURL = strings.TrimPrefix(serverURL, "https://")

// Create capture transport to intercept requests
capture := &captureTransport{}

cfg := &config.Config{
APIKey: "test-api-key",
AppKey: "test-app-key",
Site: serverURL,
}

// Create client - this sets configuration.UserAgent = getUserAgent()
client, err := New(cfg)
if err != nil {
t.Fatalf("New() failed: %v", err)
}

// Replace the HTTP client with our capturing version
// This allows us to intercept requests made by the API client
client.api.GetConfig().HTTPClient = &http.Client{
Transport: capture,
}

// Make a request through the Datadog API client
// This tests that the API client uses the custom User-Agent we configured
ctx := client.Context()
monitorsApi := datadogV1.NewMonitorsApi(client.V1())
_, _, _ = monitorsApi.GetMonitor(ctx, 12345)

// We expect an error since we're not returning valid responses,
// but we should have captured the headers
if capture.capturedHeader == nil {
t.Fatal("Failed to capture request headers")
}

// Verify User-Agent header was set by the API client
userAgent := capture.capturedHeader.Get("User-Agent")
if userAgent == "" {
t.Fatal("User-Agent header not set by API client")
}

// Verify it's our custom user agent (not the default datadog-api-client-go one)
if !strings.HasPrefix(userAgent, "pup/") {
t.Errorf("User-Agent should start with 'pup/', got: %s", userAgent)
}

expectedUA := getUserAgent()
if userAgent != expectedUA {
t.Errorf("User-Agent = %q, want %q", userAgent, expectedUA)
}

// Verify it contains expected components
if !strings.Contains(userAgent, version.Version) {
t.Errorf("User-Agent should contain version, got: %s", userAgent)
}

t.Logf("✓ Integration test passed - API client uses custom User-Agent: %s", userAgent)
}