From edef14efb29e7e927f4cb65a34f60efa11e59cea Mon Sep 17 00:00:00 2001 From: Lucas Giordano Date: Wed, 4 Feb 2026 15:28:12 +0100 Subject: [PATCH 1/2] feat: add request headers for tracking (x-notte-request-origin, x-notte-sdk-version) Add two headers to all API requests: - x-notte-request-origin: "cli" - x-notte-sdk-version: CLI version string Co-Authored-By: Claude Opus 4.5 --- internal/api/client.go | 10 ++++++++-- internal/api/client_test.go | 6 +++--- internal/cmd/root.go | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 1b5b8b7..c89c36c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -41,11 +41,11 @@ func WithCircuitBreaker(cb *CircuitBreaker) NotteClientOption { // NewClient creates a new Notte API client func NewClient(apiKey string, opts ...NotteClientOption) (*NotteClient, error) { - return NewClientWithURL(apiKey, DefaultBaseURL, opts...) + return NewClientWithURL(apiKey, DefaultBaseURL, "", opts...) } // NewClientWithURL creates a client with custom base URL -func NewClientWithURL(apiKey, baseURL string, opts ...NotteClientOption) (*NotteClient, error) { +func NewClientWithURL(apiKey, baseURL, version string, opts ...NotteClientOption) (*NotteClient, error) { if apiKey == "" { return nil, fmt.Errorf("API key is required") } @@ -67,6 +67,7 @@ func NewClientWithURL(apiKey, baseURL string, opts ...NotteClientOption) (*Notte Timeout: 45 * time.Second, Transport: &resilientTransport{ apiKey: apiKey, + version: version, retryConfig: nc.retryConfig, circuitBreaker: nc.circuitBreaker, base: &http.Transport{ @@ -92,6 +93,7 @@ func NewClientWithURL(apiKey, baseURL string, opts ...NotteClientOption) (*Notte // resilientTransport wraps http.RoundTripper with auth, retry, and circuit breaker type resilientTransport struct { apiKey string + version string retryConfig *RetryConfig circuitBreaker *CircuitBreaker base http.RoundTripper @@ -108,6 +110,10 @@ func (t *resilientTransport) RoundTrip(req *http.Request) (*http.Response, error // Add auth header req.Header.Set("Authorization", "Bearer "+t.apiKey) + // Add tracking headers + req.Header.Set("x-notte-request-origin", "cli") + req.Header.Set("x-notte-sdk-version", t.version) + // Add idempotency key for mutating requests AddIdempotencyKey(req) diff --git a/internal/api/client_test.go b/internal/api/client_test.go index a915b21..25e3954 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -39,7 +39,7 @@ func TestNewClient_Success(t *testing.T) { } func TestNewClientWithURL_CustomURL(t *testing.T) { - client, err := NewClientWithURL("test-key", "https://custom.api.com") + client, err := NewClientWithURL("test-key", "https://custom.api.com", "v1.0.0") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -70,7 +70,7 @@ func TestResilientTransport_AddsAuthHeader(t *testing.T) { })) defer server.Close() - client, err := NewClientWithURL("test-api-key", server.URL) + client, err := NewClientWithURL("test-api-key", server.URL, "v1.0.0") if err != nil { t.Fatalf("failed to create client: %v", err) } @@ -101,7 +101,7 @@ func TestResilientTransport_RecordsFailureOn5xx(t *testing.T) { MaxBackoff: 10 * time.Millisecond, Jitter: false, } - client, err := NewClientWithURL("test-key", server.URL, + client, err := NewClientWithURL("test-key", server.URL, "v1.0.0", WithCircuitBreaker(cb), WithRetryConfig(fastRetry)) if err != nil { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 41a8457..1cb90b5 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -109,7 +109,7 @@ func GetClient() (*api.NotteClient, error) { baseURL = api.DefaultBaseURL } - return api.NewClientWithURL(apiKey, baseURL) + return api.NewClientWithURL(apiKey, baseURL, Version) } // GetContextWithTimeout wraps the provided context with a timeout From 0020d8755d2b7c88bf61732cb8176573f76fc76c Mon Sep 17 00:00:00 2001 From: Lucas Giordano Date: Wed, 4 Feb 2026 15:33:09 +0100 Subject: [PATCH 2/2] test: add test coverage for tracking headers Co-Authored-By: Claude Opus 4.5 --- internal/api/client_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/api/client_test.go b/internal/api/client_test.go index 25e3954..ef98dac 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -87,6 +87,38 @@ func TestResilientTransport_AddsAuthHeader(t *testing.T) { } } +func TestResilientTransport_AddsTrackingHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("x-notte-request-origin") + if origin != "cli" { + t.Errorf("got x-notte-request-origin %q, want %q", origin, "cli") + } + version := r.Header.Get("x-notte-sdk-version") + if version != "v1.2.3" { + t.Errorf("got x-notte-sdk-version %q, want %q", version, "v1.2.3") + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + client, err := NewClientWithURL("test-api-key", server.URL, "v1.2.3") + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + req, _ := http.NewRequest("GET", server.URL+"/test", nil) + resp, err := client.httpClient.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("got status %d, want 200", resp.StatusCode) + } +} + func TestResilientTransport_RecordsFailureOn5xx(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError)