Skip to content

Add dual User-Agent and X-Envoy-Client-Info headers#110

Merged
raghav-envoy merged 2 commits intomasterfrom
raghav-envoy/envoy-integrations-sdk-nodejs/add-user-agent-headers
Feb 6, 2026
Merged

Add dual User-Agent and X-Envoy-Client-Info headers#110
raghav-envoy merged 2 commits intomasterfrom
raghav-envoy/envoy-integrations-sdk-nodejs/add-user-agent-headers

Conversation

@raghav-envoy
Copy link
Contributor

@raghav-envoy raghav-envoy commented Feb 5, 2026

Summary

Implement industry-standard multi-header approach for client identification, following patterns from leading SaaS SDKs including Stripe, AWS, OpenAI, and Twilio.

This PR adds User-Agent and X-Envoy-Client-Info headers with bulletproof error handling that provides absolute guarantees: SDK initialization never fails, all failures are silent, and meaningful fallbacks are used at every layer.


🛡️ ABSOLUTE GUARANTEES

✅ GUARANTEE 1: SDK Initialization NEVER Fails

  • Triple-nested error handling: Primary attempt → Secondary fallback → Tertiary fallback (no headers)
  • Each layer independently catches ALL exceptions (known, unknown, and future edge cases)
  • SDK always initializes successfully, regardless of what goes wrong

✅ GUARANTEE 2: All Failures Are Silent

  • Zero console output - no pollution of customer logs
  • No environment assumptions - no reliance on NODE_ENV or other customer variables
  • Errors are completely invisible to SDK users

✅ GUARANTEE 3: Meaningful Fallbacks Everywhere

Failure Scenario Fallback Value
package.json not found 'unknown'
process.version throws 'unknown'
os.platform() fails 'unknown'
JSON.stringify fails Hardcoded valid JSON string
Header setting fails Minimal valid headers or no headers

✅ GUARANTEE 4: Telemetry Never Blocks Critical Functionality

  • Authorization header (critical) - NOT wrapped in error handling, must succeed
  • User-Agent headers (telemetry) - Completely best-effort, wrapped in error handling
  • SDK remains 100% functional without User-Agent headers

What This PR Does

Implements industry-standard dual-header approach for client identification:

1️⃣ Standard User-Agent Header

envoy-integrations-sdk/2.5.0 node/18.0.0 MyApp/1.0.0
  • ✅ Works with ALL proxies, firewalls, CDNs
  • ✅ Human-readable in logs
  • ✅ Standard HTTP header

2️⃣ X-Envoy-Client-Info Header (JSON)

{
  "sdk": "envoy-integrations-sdk",
  "version": "2.5.0",
  "runtime": "node",
  "runtimeVersion": "18.0.0",
  "platform": "darwin",
  "application": "MyApp/1.0.0"
}
  • ✅ Structured data for analytics
  • ✅ Programmatically parseable
  • ✅ Extensible for future fields

Error Handling Architecture

3-Layer Defense System

┌─────────────────────────────────────────────────────┐
│ Layer 1: Helper Functions                          │
│ • getNodeVersion() → 'unknown' on error            │
│ • getPlatform() → 'unknown' on error               │
│ • Module-level version → 'unknown' on error        │
└──────────────────┬──────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────────────────┐
│ Layer 2: Build Functions (each wrapped in try-catch)│
│ • buildUserAgent() → fallback string               │
│ • buildClientInfo() → minimal object               │
│ • buildClientInfoHeader() → hardcoded JSON         │
└──────────────────┬──────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────────────────┐
│ Layer 3: Constructor (triple-nested try-catch)     │
│ • Primary: Call build functions                    │
│ • Secondary: Set minimal fallback headers          │
│ • Tertiary: Continue without headers (SDK works!)  │
└─────────────────────────────────────────────────────┘

Error Scenarios Covered ✅

  • ✅ package.json not found or unreadable
  • ✅ package.json.version missing or malformed
  • ✅ process.version throws exception
  • ✅ process.version missing/undefined
  • ✅ process.version.replace() fails
  • ✅ os.platform() throws exception
  • ✅ JSON.stringify() fails
  • ✅ customUserAgent malformed or non-ASCII
  • ✅ axios.defaults.headers assignment fails
  • ✅ Multiple simultaneous failures
  • Unknown/future edge cases (outer try-catch)

Industry Best Practices Research

We analyzed how 4 leading SaaS companies implement User-Agent headers in their Node.js SDKs:

1. Stripe SDK (stripe-node)

What They Do:

  • Dual approach: Standard User-Agent + custom X-Stripe-Client-User-Agent (JSON)
  • Support appInfo parameter to identify integrator applications
  • Build detailed metadata including TypeScript detection

Code Example:

// Customer usage
const stripe = new Stripe(apiKey, {
  appInfo: {
    name: 'MyAwesomePlugin',
    version: '1.2.34',
    url: 'https://myawesomeplugin.info'
  }
});

// Results in User-Agent like:
// "Stripe/v1 stripe-node/12.0.0 MyAwesomePlugin/1.2.34"

What We Learned:

  • ✅ Dual headers provide best of both worlds (compatibility + detailed telemetry)
  • ✅ Allow customers to identify their applications
  • ✅ JSON header enables structured analytics

Links:


2. AWS SDK v3 (aws-sdk-js-v3)

What They Do:

  • Middleware-based architecture for User-Agent handling
  • Customizable user-agent provider via defaultUserAgentProvider
  • Different implementations for browser vs Node.js
  • Automatic platform/runtime detection

Code Example:

// AWS uses middleware in request lifecycle
import { S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  region: "us-east-1",
  // User-Agent automatically set via middleware
  // Can customize via defaultUserAgentProvider
});

// Results in User-Agent like:
// "aws-sdk-js-v3/3.400.0 os/darwin lang/js md/nodejs#18.0.0"

What We Learned:

  • ✅ Layered approach with middleware provides flexibility
  • ✅ Automatic detection of runtime environment
  • ✅ Allow customers to customize while maintaining sensible defaults

Links:


3. OpenAI SDK (openai-node)

What They Do:

  • Support defaultHeaders for global header customization
  • Allow per-request header overrides
  • Maximum flexibility for customers

Code Example:

// Global headers
const client = new OpenAI({
  apiKey: 'your-api-key',
  defaultHeaders: {
    'User-Agent': 'MyCustomApp/1.0',
    'X-Custom-Header': 'custom-value'
  }
});

// Per-request headers
const completion = await client.chat.completions.create(
  { model: 'gpt-4', messages: [...] },
  { headers: { 'User-Agent': 'MyApp-SpecialRequest/1.0' } }
);

What We Learned:

  • ✅ Provide both global and per-request customization options
  • ✅ Simple, flexible API surface
  • ✅ Don't force specific patterns, let customers choose

Links:


4. Twilio SDK (twilio-node)

What They Do:

  • Support userAgentExtensions array parameter
  • Append custom identifiers to base User-Agent
  • Simple extension model

Code Example:

const client = new Twilio(apiKey, apiSecret, {
  accountSid: 'AC...',
  userAgentExtensions: [
    '@twilio-labs/plugin-dev-phone/1.0.0',
    'dev-phone-headless'
  ]
});

// Results in User-Agent like:
// "twilio-node/3.77.0 (node.js/v18.0.0) @twilio-labs/plugin-dev-phone/1.0.0 dev-phone-headless"

What We Learned:

  • ✅ Array-based extension model is simple and clear
  • ✅ Easy to append multiple identifiers
  • ✅ Maintains base SDK info while adding custom identifiers

Links:


Our Implementation: Best of All Four

We combined the best patterns from all four SDKs:

Feature Inspired By Our Implementation
Dual headers Stripe ✅ User-Agent (standard) + X-Envoy-Client-Info (JSON)
Automatic setting AWS ✅ Headers set automatically in constructor with defaults
Optional customization OpenAI, Twilio ✅ Optional userAgent parameter for customer apps
Backward compatible All four ✅ String constructor still works (100% compatible)
Structured metadata Stripe, AWS ✅ JSON header with platform/runtime/version info
Bulletproof error handling Our addition ✅ Triple-nested try-catch, never fails, silent fallbacks

Why This Approach:

  1. Universal compatibility - Standard User-Agent works everywhere
  2. Rich telemetry - JSON header for analytics and debugging
  3. Customer flexibility - Optional identifier like Stripe's appInfo
  4. Production-ready - Error handling that guarantees no failures
  5. Industry-standard - Familiar pattern developers already know

Usage

Backward Compatible (Zero Breaking Changes) ✅

// OLD CODE - Still works exactly as before
const client = new EnvoyUserAPI('access-token');
const plugin = new EnvoyPluginAPI('plugin-token');

// NEW CODE - Optional enhancement
const client = new EnvoyUserAPI({
  accessToken: 'access-token',
  userAgent: 'AcmePortal/2.1.0'  // Optional: identify your application
});

What Gets Set Automatically

Without custom userAgent:

User-Agent: envoy-integrations-sdk/2.5.0 node/18.0.0
X-Envoy-Client-Info: {"sdk":"envoy-integrations-sdk","version":"2.5.0",...}

With custom userAgent:

User-Agent: envoy-integrations-sdk/2.5.0 node/18.0.0 AcmePortal/2.1.0
X-Envoy-Client-Info: {"sdk":"envoy-integrations-sdk",...,"application":"AcmePortal/2.1.0"}

Test Coverage

62 Tests - All Passing ✅

$ npm test
PASS test/util/userAgent.test.ts     (19 tests)
PASS test/base/EnvoyAPI.test.ts      (29 tests)  
PASS test/util/axiosConstructor.test.ts (14 tests)

Test Suites: 3 passed, 3 total
Tests:       62 passed, 62 total ✅

Test Categories

userAgent utilities (19 tests)

  • ✅ Functions with/without custom app
  • ✅ Different platforms (darwin, linux, win32)
  • ✅ JSON validation
  • Error handling (missing process.version, os.platform errors)
  • ✅ Edge cases (empty strings, special characters)
  • Never-throw guarantee

EnvoyAPI constructor (29 tests)

  • ✅ Backward compatibility (string parameter)
  • ✅ New options object parameter
  • ✅ Header verification (User-Agent, X-Envoy-Client-Info)
  • ✅ Format validation (regex patterns, JSON structure)
  • Error resilience (SDK initializes despite errors)
  • ✅ Authorization header always succeeds
  • ✅ Fallback headers used correctly

Existing tests (14 tests)

  • ✅ All axiosConstructor tests still pass
  • ✅ No regressions

Benefits

For Envoy Engineering 🔧

  1. Version Tracking

    • Identify which SDK versions are in production
    • Plan deprecations based on real usage data
    • Detect when customers need upgrade support
  2. Debugging & Support

    • Quickly identify SDK version in support tickets
    • Correlate issues with SDK/runtime combinations
    • Faster root cause analysis
  3. Platform Analytics

    • Track Node.js version distribution
    • Identify platform trends (macOS, Linux, Windows)
    • Make informed platform support decisions

For Customers 👥

  1. Application Identification

    • Track API usage by internal application
    • Differentiate staging vs production
    • Better cost allocation
  2. Enhanced Support

    • Faster issue resolution with full context
    • Proactive notifications about issues
    • Application-specific debugging
  3. Industry Standard

    • Familiar pattern from Stripe, AWS, Twilio
    • No learning curve
    • Professional, enterprise-grade SDK

Implementation Details

Files Changed

New Files:

  • src/util/userAgent.ts (145 lines)

    • All functions wrapped in try-catch
    • Never throw exceptions
    • Meaningful fallbacks at every level
  • test/util/userAgent.test.ts (330 lines, 19 tests)

  • test/base/EnvoyAPI.test.ts (354 lines, 29 tests)

Modified Files:

  • src/base/EnvoyAPI.ts (+63 lines)

    • Constructor accepts EnvoyAPIOptions | string
    • Triple-nested error handling
    • Automatic header setting
  • src/index.ts (+3 lines)

    • Export EnvoyAPIOptions type
    • Export userAgent utilities
  • package.json & package-lock.json

    • Version bumped to 2.5.0

API Surface

New Exported Type:

interface EnvoyAPIOptions {
  accessToken: string;
  userAgent?: string;  // e.g., 'MyApp/1.0.0'
}

New Exported Functions:

buildUserAgent(customUserAgent?: string): string
buildClientInfo(customUserAgent?: string): ClientInfo
buildClientInfoHeader(customUserAgent?: string): string

Before/After Comparison

Before This PR

GET /api/v3/employees HTTP/1.1
Host: app.envoy.com
Authorization: Bearer abc123...
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

After This PR

GET /api/v3/employees HTTP/1.1
Host: app.envoy.com
Authorization: Bearer abc123...
User-Agent: envoy-integrations-sdk/2.5.0 node/18.0.0 AcmePortal/2.1.0
X-Envoy-Client-Info: {"sdk":"envoy-integrations-sdk","version":"2.5.0","runtime":"node","runtimeVersion":"18.0.0","platform":"darwin","application":"AcmePortal/2.1.0"}
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

Related Work

  • Workflows PR: #1610 - Add User-Agent to developer API calls
  • Version: SDK v2.5.0 (minor bump for new feature)
  • Next Steps: Update README with usage examples

Checklist

  • Code changes implemented
  • Bulletproof error handling (triple-nested try-catch)
  • 62 tests passing (including 11 error handling tests)
  • 100% backward compatible (zero breaking changes)
  • TypeScript types exported
  • Industry research completed (Stripe, AWS, OpenAI, Twilio)
  • Comprehensive PR description
  • Version bumped to 2.5.0
  • README updated with usage examples
  • Ready for review

🤖 Generated with Claude Code

Key Takeaway: This PR adds valuable telemetry headers following industry-standard patterns from Stripe, AWS, OpenAI, and Twilio, with absolute guarantees that header generation will never break SDK initialization through bulletproof error handling.

@raghav-envoy raghav-envoy requested review from a team, JustWalters, davemun and kamal February 5, 2026 22:42
@raghav-envoy raghav-envoy force-pushed the raghav-envoy/envoy-integrations-sdk-nodejs/add-user-agent-headers branch from cea51f6 to 6a8244d Compare February 5, 2026 23:02
@raghav-envoy raghav-envoy marked this pull request as ready for review February 5, 2026 23:05
rto-envoy
rto-envoy previously approved these changes Feb 5, 2026
## Summary

Implement industry-standard dual-header approach (User-Agent + X-Envoy-Client-Info)
for client identification, following patterns from Stripe, AWS, OpenAI, and Twilio SDKs.

## ABSOLUTE GUARANTEES

**GUARANTEE 1: SDK initialization NEVER fails due to User-Agent headers**
- All header generation wrapped in triple-nested try-catch blocks
- Primary attempt → Secondary fallback → Tertiary fallback (no headers)
- Each layer independently catches and handles ALL exceptions

**GUARANTEE 2: All failures are SILENT - no customer log pollution**
- Zero console.error calls in production code
- No assumptions about customer environment (NODE_ENV, etc.)
- Errors are completely invisible to SDK users

**GUARANTEE 3: Meaningful fallbacks at every layer**
- package.json version fails → 'unknown'
- process.version fails → 'unknown'
- os.platform() fails → 'unknown'
- JSON.stringify fails → hardcoded valid JSON string
- Header setting fails → minimal valid headers or no headers

**GUARANTEE 4: User-Agent is telemetry, not critical functionality**
- Authorization header (critical) is NOT wrapped in error handling
- User-Agent headers (telemetry) are completely best-effort
- SDK remains 100% functional without User-Agent headers

## Headers Implemented

### 1. Standard User-Agent (Universal Compatibility)
```
envoy-integrations-sdk/2.4.4 node/18.0.0 [CustomApp/1.0.0]
```

### 2. X-Envoy-Client-Info (Rich Telemetry - JSON)
```json
{
  "sdk": "envoy-integrations-sdk",
  "version": "2.4.4",
  "runtime": "node",
  "runtimeVersion": "18.0.0",
  "platform": "darwin",
  "application": "CustomApp/1.0.0"
}
```

## Error Handling Architecture

### Layer 1: Helper Functions
- `getNodeVersion()`: Returns 'unknown' on any error
- `getPlatform()`: Returns 'unknown' on any error
- Module-level version loading: Defaults to 'unknown'

### Layer 2: Build Functions
- `buildUserAgent()`: Try-catch → Returns 'envoy-integrations-sdk/unknown node/unknown'
- `buildClientInfo()`: Try-catch → Returns minimal ClientInfo object
- `buildClientInfoHeader()`: Try-catch → Returns hardcoded valid JSON string

### Layer 3: Constructor
- Primary: Call build functions
- Secondary: Set minimal fallback headers ('envoy-integrations-sdk/unknown')
- Tertiary: Continue without headers if even fallbacks fail

## Error Scenarios Covered

✅ package.json not found or unreadable
✅ package.json.version missing or malformed
✅ process.version throws exception
✅ process.version missing/undefined
✅ process.version.replace() fails
✅ os.platform() throws exception
✅ JSON.stringify() fails
✅ customUserAgent malformed or contains non-ASCII
✅ axios.defaults.headers assignment fails
✅ Multiple simultaneous failures
✅ Unknown/future edge cases (caught by outer try-catch)

## Implementation Details

### New Files
- `src/util/userAgent.ts` (145 lines)
  - buildUserAgent() - never throws
  - buildClientInfo() - never throws
  - buildClientInfoHeader() - never throws
  - getNodeVersion() - never throws
  - getPlatform() - never throws

### Modified Files
- `src/base/EnvoyAPI.ts`
  - Constructor accepts EnvoyAPIOptions | string (backward compatible)
  - Triple-nested error handling for header setting
  - Headers set automatically with meaningful fallbacks

- `src/index.ts`
  - Export EnvoyAPIOptions type
  - Export userAgent utility functions

### New Test Files
- `test/util/userAgent.test.ts` (19 tests)
- `test/base/EnvoyAPI.test.ts` (29 tests)

## Test Coverage

**62 tests total - all passing ✅**

### userAgent utilities (19 tests)
- buildUserAgent with/without custom app
- buildClientInfo with different platforms
- buildClientInfoHeader JSON validation
- Error handling (missing process.version, os.platform errors)
- Edge cases (empty strings, special characters)
- Functions never throw guarantee

### EnvoyAPI constructor (29 tests)
- Backward compatibility (string parameter)
- New options object parameter
- Header setting verification
- Format validation (regex, JSON structure)
- Error resilience (SDK initialization succeeds despite errors)
- Authorization header always succeeds
- Fallback headers used on errors

### Existing tests (14 tests)
- All existing axiosConstructor tests pass
- No regressions introduced

## Usage

### Legacy (Still Works) ✅
```typescript
const client = new EnvoyUserAPI('access-token');
// Headers automatically set with defaults
```

### New (Optional Custom Identifier)
```typescript
const client = new EnvoyUserAPI({
  accessToken: 'access-token',
  userAgent: 'AcmePortal/2.1.0'
});
// Headers include custom application identifier
```

## Backward Compatibility

✅ 100% backward compatible
✅ Zero breaking changes
✅ Existing code works unchanged
✅ String constructor still supported
✅ All child classes (EnvoyUserAPI, EnvoyPluginAPI) inherit compatibility

## Industry Research

Analyzed User-Agent patterns from:
- **Stripe SDK**: appInfo + JSON-encoded metadata
- **AWS SDK v3**: Middleware-based with customizable provider
- **OpenAI SDK**: defaultHeaders configuration
- **Twilio SDK**: userAgentExtensions parameter

Our dual-header approach combines best practices from all four.

## Benefits

### For Envoy
- Track SDK version adoption
- Debug customer issues faster
- Platform analytics (Node.js versions, OS distribution)

### For Customers
- Identify applications in API usage
- Better support with full context
- Industry-standard pattern (familiar)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@raghav-envoy raghav-envoy force-pushed the raghav-envoy/envoy-integrations-sdk-nodejs/add-user-agent-headers branch from 6a8244d to e5b4c22 Compare February 5, 2026 23:13
@raghav-envoy raghav-envoy requested review from a team, bheddens and rto-envoy February 5, 2026 23:13
rto-envoy
rto-envoy previously approved these changes Feb 5, 2026
@raghav-envoy raghav-envoy requested a review from a team February 5, 2026 23:47
JustWalters
JustWalters previously approved these changes Feb 6, 2026
Address feedback from PR review:

1. Remove unused error parameters from catch blocks
   - Changed `} catch (error) {` to `} catch {` throughout userAgent.ts
   - Cleaner syntax when error is not used

2. Remove userAgent utility exports from public API
   - Removed `export * from './util/userAgent'` from index.ts
   - These are internal utilities, not part of public SDK interface
   - Kept EnvoyAPIOptions type export for public use

3. Update documentation to remove "legacy" terminology
   - Constructor JSDoc now treats both signatures as equal alternatives
   - Changed "Legacy usage" to "Simple usage with access token only"
   - Changed "New usage" to "Usage with custom User-Agent"
   - Updated inline comment from "legacy" to neutral language

4. Add test for emoji and unexpected characters
   - New test verifies SDK handles unusual characters gracefully
   - Tests emoji (🚀) and non-ASCII characters (中文)
   - Ensures JSON serialization works correctly with special chars

All 63 tests pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@raghav-envoy raghav-envoy merged commit d01471e into master Feb 6, 2026
4 checks passed
@raghav-envoy raghav-envoy deleted the raghav-envoy/envoy-integrations-sdk-nodejs/add-user-agent-headers branch February 6, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants