---
description: Network request patterns with retry logic and error handling
globs: ["**/*network*.js", "**/*request*.js"]
alwaysApply: false
---

# Network Operations Patterns

## Retry Logic Pattern (MANDATORY)
ALL network requests MUST implement exponential backoff retry:

```javascript
export const MixpanelNetwork = (() => {
  const maxRetries = 5;
  
  const sendRequest = async ({
    token,
    endpoint,
    data,
    serverURL,
    useIPAddressForGeoLocation,
    retryCount = 0
  }) => {
    const url = `${serverURL}${endpoint}${useIPAddressForGeoLocation ? '?ip=1' : ''}`;
    
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: `data=${encodeURIComponent(JSON.stringify(data))}`,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const result = await response.json();
      
      if (result !== 1) {
        throw new Error(`Mixpanel API returned: ${result}`);
      }

      MixpanelLogger.log(token, `Successfully sent ${data.length} events`);
      return result;
      
    } catch (error) {
      MixpanelLogger.error(token, `Network request failed (attempt ${retryCount + 1}): ${error.message}`);
      
      // Don't retry on 400 errors (corrupted data)
      if (error.message.includes('400') || error.message.includes('Bad Request')) {
        throw error; // Let caller handle corrupted data
      }
      
      // Retry with exponential backoff
      if (retryCount < maxRetries) {
        const backoff = Math.min(2 ** retryCount * 2000, 60000); // Cap at 60s
        MixpanelLogger.log(token, `Retrying in ${backoff / 1000} seconds...`);
        
        await new Promise((resolve) => setTimeout(resolve, backoff));
        return sendRequest({
          token,
          endpoint,
          data,
          serverURL,
          useIPAddressForGeoLocation,
          retryCount: retryCount + 1
        });
      }
      
      // Max retries exceeded
      throw error;
    }
  };

  return { sendRequest };
})();
```

## Request Configuration Pattern
Build requests with proper headers and encoding:

```javascript
const buildRequest = (serverURL, endpoint, data, useIPAddressForGeoLocation) => {
  const url = `${serverURL}${endpoint}${useIPAddressForGeoLocation ? '?ip=1' : ''}`;
  
  return {
    url,
    options: {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        // Add compression header if available
        'Accept-Encoding': 'gzip, deflate',
      },
      body: `data=${encodeURIComponent(JSON.stringify(data))}`,
    }
  };
};
```

## Error Classification Pattern
Handle different error types appropriately:

```javascript
const classifyError = (error) => {
  // Network connectivity issues
  if (error.message.includes('Network request failed') || 
      error.message.includes('timeout')) {
    return 'NETWORK_ERROR';
  }
  
  // Server errors (retry-able)
  if (error.message.includes('500') || 
      error.message.includes('502') || 
      error.message.includes('503')) {
    return 'SERVER_ERROR';
  }
  
  // Client errors (don't retry)
  if (error.message.includes('400') || 
      error.message.includes('401') || 
      error.message.includes('403')) {
    return 'CLIENT_ERROR';
  }
  
  // Corrupted data (don't retry)
  if (error.message.includes('JSON') || 
      error.message.includes('parse')) {
    return 'DATA_ERROR';
  }
  
  return 'UNKNOWN_ERROR';
};

const shouldRetry = (error, retryCount, maxRetries) => {
  if (retryCount >= maxRetries) return false;
  
  const errorType = classifyError(error);
  
  // Don't retry client errors or data errors
  if (errorType === 'CLIENT_ERROR' || errorType === 'DATA_ERROR') {
    return false;
  }
  
  // Retry network and server errors
  return errorType === 'NETWORK_ERROR' || errorType === 'SERVER_ERROR' || errorType === 'UNKNOWN_ERROR';
};
```

## Batch Request Pattern
Send multiple events efficiently:

```javascript
const sendBatch = async (token, endpoint, batch, config) => {
  if (!batch || batch.length === 0) {
    return; // No data to send
  }

  MixpanelLogger.log(token, `Sending batch of ${batch.length} ${endpoint} events`);

  try {
    await sendRequest({
      token,
      endpoint,
      data: batch,
      serverURL: config.getServerURL(token),
      useIPAddressForGeoLocation: config.getUseIpAddressForGeoLocation(token)
    });
    
    return { success: true, sent: batch.length };
    
  } catch (error) {
    MixpanelLogger.error(token, `Batch send failed: ${error.message}`);
    
    // If it's a data error with multiple events, try to identify problematic event
    if (classifyError(error) === 'DATA_ERROR' && batch.length > 1) {
      return await sendIndividually(token, endpoint, batch, config);
    }
    
    throw error;
  }
};

// Fallback: Send events individually to isolate corrupted data
const sendIndividually = async (token, endpoint, batch, config) => {
  let successful = 0;
  let failed = 0;
  
  for (const event of batch) {
    try {
      await sendRequest({
        token,
        endpoint,
        data: [event], // Single event
        serverURL: config.getServerURL(token),
        useIPAddressForGeoLocation: config.getUseIpAddressForGeoLocation(token)
      });
      successful++;
    } catch (error) {
      failed++;
      MixpanelLogger.error(token, `Failed to send individual event: ${error.message}`);
      // Continue with next event
    }
  }
  
  MixpanelLogger.log(token, `Individual send complete: ${successful} successful, ${failed} failed`);
  return { success: successful > 0, sent: successful, failed };
};
```

## Timeout Handling Pattern
Implement request timeouts:

```javascript
const sendRequestWithTimeout = async (requestParams, timeoutMs = 30000) => {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(requestParams.url, {
      ...requestParams.options,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    return response;
    
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeoutMs}ms`);
    }
    
    throw error;
  }
};
```

## Server URL Management
Handle different server endpoints:

```javascript
const getEndpointURL = (serverURL, type) => {
  const endpoints = {
    [MixpanelType.EVENTS]: '/track/',
    [MixpanelType.USER]: '/engage/',
    [MixpanelType.GROUPS]: '/groups/'
  };
  
  const endpoint = endpoints[type];
  if (!endpoint) {
    throw new Error(`Unknown endpoint type: ${type}`);
  }
  
  return `${serverURL}${endpoint}`;
};

const validateServerURL = (url) => {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
};
```

## Request Logging Pattern
Log requests for debugging:

```javascript
const logRequest = (token, method, url, data) => {
  MixpanelLogger.log(token, `${method} ${url}`);
  MixpanelLogger.log(token, `Sending ${data.length} events`);
  
  // Log first event for debugging (without sensitive data)
  if (data.length > 0) {
    const sample = { ...data[0] };
    delete sample.token; // Remove token from logs
    MixpanelLogger.log(token, `Sample event:`, sample);
  }
};

const logResponse = (token, response, duration) => {
  MixpanelLogger.log(token, `Response: ${response.status} (${duration}ms)`);
};
```

## Testing Network Operations
Mock network requests consistently:

```javascript
describe('Network Operations', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  it('should retry on network failures', async () => {
    // Mock failure then success
    global.fetch
      .mockRejectedValueOnce(new Error('Network timeout'))
      .mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve(1)
      });

    await MixpanelNetwork.sendRequest({
      token: 'test-token',
      endpoint: '/track/',
      data: [{ event: 'test' }],
      serverURL: 'https://api.mixpanel.com',
      useIPAddressForGeoLocation: false
    });

    expect(global.fetch).toHaveBeenCalledTimes(2);
  });

  it('should not retry on 400 errors', async () => {
    global.fetch.mockRejectedValue(new Error('HTTP 400: Bad Request'));

    await expect(
      MixpanelNetwork.sendRequest({
        token: 'test-token',
        endpoint: '/track/',
        data: [{ event: 'test' }],
        serverURL: 'https://api.mixpanel.com',
        useIPAddressForGeoLocation: false
      })
    ).rejects.toThrow('HTTP 400: Bad Request');

    expect(global.fetch).toHaveBeenCalledTimes(1);
  });

  it('should include proper headers and encoding', async () => {
    global.fetch.mockResolvedValue({
      ok: true,
      status: 200,
      json: () => Promise.resolve(1)
    });

    const data = [{ event: 'test', properties: { prop: 'value' } }];

    await MixpanelNetwork.sendRequest({
      token: 'test-token',
      endpoint: '/track/',
      data,
      serverURL: 'https://api.mixpanel.com',
      useIPAddressForGeoLocation: true
    });

    expect(global.fetch).toHaveBeenCalledWith(
      'https://api.mixpanel.com/track/?ip=1',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: `data=${encodeURIComponent(JSON.stringify(data))}`
      }
    );
  });
});
```

## Performance Optimization
- Use request pooling when available
- Implement compression for large payloads
- Batch multiple events in single request
- Use efficient JSON serialization
- Cache network status if available
- Implement connection quality detection