Building a production-ready integration with the Sprinklr API requires more than just making successful API calls. This guide shares battle-tested patterns, common pitfalls to avoid, and techniques that separate hobby projects from enterprise-grade integrations.
The difference between a working integration and a reliable one often comes down to how you handle the details:
- A working integration makes API calls and gets data back
- A reliable integration handles errors gracefully, respects rate limits, protects credentials, scales efficiently, and degrades gracefully under load
Following these best practices will help you build integrations that:
- Don't wake you up at 3 AM with production incidents
- Scale from 10 requests/day to 10,000 without rewriting
- Handle errors without losing data
- Perform efficiently and stay under rate limits
- Are maintainable by your team (and future you)
Let's dive into the patterns that make this possible.
Security isn't optional—it's fundamental. A single exposed credential can compromise your entire integration and your customers' data.
❌ Never do this:
// DANGER: Credentials in code
const accessToken = "SPRD_tjACKHVlwb3D7Xd2WDZI3VxgBnI3Lmk2mwFfZasVpkDNzZSRzRp6lbHpxLqOCJ8Gomp6iYXkl4JGLr91cpUxfw==";
const apiKey = "ebgw8cwygvgc7vptgxty2ekq";
// This ends up in:
// - Your git repository (forever)
// - Your build artifacts
// - Your error logs
// - Anyone who can access your code✅ Do this instead:
Node.js:
// Use environment variables
const accessToken = process.env.SPRINKLR_ACCESS_TOKEN;
const apiKey = process.env.SPRINKLR_API_KEY;
if (!accessToken || !apiKey) {
throw new Error('Missing required credentials. Set SPRINKLR_ACCESS_TOKEN and SPRINKLR_API_KEY environment variables.');
}
// Or use a secrets manager (production)
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
async function getCredentials() {
const secret = await secretsManager.getSecretValue({
SecretId: 'sprinklr/api/credentials'
}).promise();
return JSON.parse(secret.SecretString);
}Python:
import os
from typing import Dict
# Use environment variables
access_token = os.environ.get('SPRINKLR_ACCESS_TOKEN')
api_key = os.environ.get('SPRINKLR_API_KEY')
if not access_token or not api_key:
raise ValueError('Missing required credentials')
# Or use a secrets manager
import boto3
def get_credentials() -> Dict[str, str]:
client = boto3.client('secretsmanager')
secret = client.get_secret_value(SecretId='sprinklr/api/credentials')
return json.loads(secret['SecretString'])PHP:
<?php
// Use environment variables
$accessToken = getenv('SPRINKLR_ACCESS_TOKEN');
$apiKey = getenv('SPRINKLR_API_KEY');
if (!$accessToken || !$apiKey) {
throw new Exception('Missing required credentials');
}
// Or use a secrets manager
use Aws\SecretsManager\SecretsManagerClient;
function getCredentials() {
$client = new SecretsManagerClient([
'region' => 'us-east-1',
'version' => 'latest'
]);
$result = $client->getSecretValue([
'SecretId' => 'sprinklr/api/credentials'
]);
return json_decode($result['SecretString'], true);
}
?>Java:
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
public class CredentialManager {
public static Credentials getCredentials() {
// Use environment variables
String accessToken = System.getenv("SPRINKLR_ACCESS_TOKEN");
String apiKey = System.getenv("SPRINKLR_API_KEY");
if (accessToken == null || apiKey == null) {
throw new IllegalStateException("Missing required credentials");
}
return new Credentials(accessToken, apiKey);
}
// Or use secrets manager
public static Credentials getCredentialsFromSecretsManager() {
SecretsManagerClient client = SecretsManagerClient.create();
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId("sprinklr/api/credentials")
.build();
String secret = client.getSecretValue(request).secretString();
return parseCredentials(secret);
}
}Use different credentials for development, staging, and production:
// config.js
module.exports = {
development: {
apiUrl: 'https://qa-api-v3.sprinklr.com',
accessToken: process.env.DEV_SPRINKLR_ACCESS_TOKEN,
apiKey: process.env.DEV_SPRINKLR_API_KEY
},
production: {
apiUrl: 'https://api-v3.sprinklr.com',
accessToken: process.env.PROD_SPRINKLR_ACCESS_TOKEN,
apiKey: process.env.PROD_SPRINKLR_API_KEY
}
};
const config = module.exports[process.env.NODE_ENV || 'development'];Why this matters:
- Development bugs don't affect production data
- You can test freely without worry
- Compromised dev credentials don't expose production
- You can revoke environment-specific credentials independently
Implement a credential rotation schedule:
class CredentialManager {
constructor() {
this.currentCredentials = null;
this.credentialAge = null;
this.rotationWarningDays = 80; // Warn before 90-day expiry
}
async getCredentials() {
if (!this.currentCredentials) {
this.currentCredentials = await this.loadCredentials();
this.credentialAge = await this.getCredentialAge();
}
// Check if credentials need rotation
if (this.credentialAge > this.rotationWarningDays) {
console.warn(`⚠️ Credentials are ${this.credentialAge} days old. Consider rotating soon.`);
this.alertOps('Credential rotation needed');
}
return this.currentCredentials;
}
async rotateCredentials(newAccessToken, newApiKey) {
// Implement gradual rotation with fallback
const oldCredentials = this.currentCredentials;
try {
// Test new credentials
await this.testCredentials(newAccessToken, newApiKey);
// Switch to new credentials
this.currentCredentials = {
accessToken: newAccessToken,
apiKey: newApiKey
};
// Keep old credentials for 24 hours as fallback
setTimeout(() => {
console.log('Old credentials expired from fallback');
}, 86400000);
console.log('✅ Credentials rotated successfully');
} catch (error) {
console.error('Credential rotation failed, keeping old credentials');
throw error;
}
}
async testCredentials(accessToken, apiKey) {
// Make a test API call to verify credentials work
const response = await fetch('https://api-v3.sprinklr.com/api/v3/account', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
}
}Never make API calls over plain HTTP:
// ✅ Correct - HTTPS
const apiUrl = 'https://api-v3.sprinklr.com';
// ❌ Wrong - Plain HTTP (credentials visible in plain text!)
const apiUrl = 'http://api-v3.sprinklr.com';
// ✅ Enforce HTTPS in code
if (!apiUrl.startsWith('https://')) {
throw new Error('API URL must use HTTPS');
}Efficient integrations use fewer API requests, process faster, and stay well under rate limits.
This is the single most effective optimization you can make.
Impact comparison:
Task: Fetch 100 accounts
Without batching:
- 100 API requests
- ~30 seconds total time
- 100 of your rate limit quota
- More error opportunities
With batching:
- 2 API requests (2 batches of 50)
- ~600ms total time
- 2 of your rate limit quota
- 50x more efficientImplementation:
Node.js:
// ❌ Inefficient
async function getAccountsOneByOne(accountIds) {
const accounts = [];
for (const id of accountIds) {
const account = await fetch(
`https://api-v3.sprinklr.com/api/v3/account?accountId=${id}`,
{ headers: { 'Authorization': `Bearer ${token}`, 'Key': key } }
);
accounts.push(await account.json());
}
return accounts;
}
// ✅ Efficient
async function getAccountsInBatches(accountIds) {
const batchSize = 50;
const batches = [];
// Split into batches of 50
for (let i = 0; i < accountIds.length; i += batchSize) {
batches.push(accountIds.slice(i, i + batchSize));
}
// Fetch all batches
const results = await Promise.all(
batches.map(batch =>
fetch('https://api-v3.sprinklr.com/api/v3/account/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Key': key,
'Content-Type': 'application/json'
},
body: JSON.stringify({ accountIds: batch })
}).then(r => r.json())
)
);
// Flatten results
return results.flatMap(r => r.data);
}Not all data needs to be fresh every time.
Caching strategy by data type:
| Data Type | Recommended TTL | Rationale |
|---|---|---|
| Account metadata | 30-60 minutes | Changes infrequently |
| User profiles | 15-30 minutes | May update but not constantly |
| Messages/posts | 1-5 minutes | More dynamic |
| Cases | 1-2 minutes | Changes as agents work |
| Configuration | 1-24 hours | Rarely changes |
Production-ready cache implementation:
class SmartCache {
constructor() {
this.cache = new Map();
this.stats = { hits: 0, misses: 0, evictions: 0 };
}
set(key, value, ttlMs = 300000) {
// Limit cache size
if (this.cache.size > 10000) {
// Evict oldest entry
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
this.stats.evictions++;
}
this.cache.set(key, {
value,
expiresAt: Date.now() + ttlMs,
createdAt: Date.now()
});
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
return null;
}
// Check expiry
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
this.stats.misses++;
return null;
}
this.stats.hits++;
return entry.value;
}
getStats() {
const total = this.stats.hits + this.stats.misses;
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(2) : 0;
return {
...this.stats,
total,
hitRate: `${hitRate}%`,
size: this.cache.size
};
}
clear() {
this.cache.clear();
}
// Cache with automatic revalidation
async getOrFetch(key, fetchFn, ttl = 300000) {
const cached = this.get(key);
if (cached !== null) {
return cached;
}
const value = await fetchFn();
this.set(key, value, ttl);
return value;
}
}
// Usage
const cache = new SmartCache();
async function getAccount(accountId) {
return cache.getOrFetch(
`account:${accountId}`,
async () => {
const response = await fetch(`https://api-v3.sprinklr.com/api/v3/account?accountId=${accountId}`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
});
return response.json();
},
1800000 // 30 minutes
);
}
// Monitor cache performance
setInterval(() => {
console.log('Cache stats:', cache.getStats());
// Output: { hits: 847, misses: 153, evictions: 2, total: 1000, hitRate: '84.70%', size: 892 }
}, 60000);Prevent overwhelming the API with concurrent requests:
class RequestQueue {
constructor(options = {}) {
this.concurrency = options.concurrency || 5;
this.requestsPerMinute = options.requestsPerMinute || 60;
this.queue = [];
this.running = 0;
this.requestTimes = [];
}
async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.process();
});
}
async process() {
// Check concurrency limit
if (this.running >= this.concurrency) {
return;
}
// Check rate limit
const now = Date.now();
this.requestTimes = this.requestTimes.filter(t => t > now - 60000);
if (this.requestTimes.length >= this.requestsPerMinute) {
// Wait until oldest request is outside the 1-minute window
const oldestRequest = this.requestTimes[0];
const waitTime = 60000 - (now - oldestRequest);
setTimeout(() => this.process(), waitTime);
return;
}
// Get next request
const item = this.queue.shift();
if (!item) return;
this.running++;
this.requestTimes.push(now);
try {
const result = await item.fn();
item.resolve(result);
} catch (error) {
item.reject(error);
} finally {
this.running--;
// Process next request
setImmediate(() => this.process());
}
}
getQueueSize() {
return this.queue.length;
}
}
// Usage
const queue = new RequestQueue({
concurrency: 5, // Max 5 concurrent requests
requestsPerMinute: 60 // Stay under rate limit
});
// All requests automatically queued and rate-limited
async function fetchAccount(accountId) {
return queue.add(async () => {
const response = await fetch(
`https://api-v3.sprinklr.com/api/v3/account?accountId=${accountId}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
}
);
return response.json();
});
}Production systems fail. The question is whether your integration fails gracefully or catastrophically.
async function retryWithBackoff(fn, options = {}) {
const {
maxRetries = 5,
baseDelay = 1000,
maxDelay = 30000,
retryableErrors = [408, 429, 500, 502, 503, 504]
} = options;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Check if error is retryable
const statusCode = error.response?.status;
if (!retryableErrors.includes(statusCode)) {
throw error; // Don't retry non-retryable errors
}
// Don't retry on last attempt
if (attempt === maxRetries - 1) {
break;
}
// Calculate delay with exponential backoff + jitter
const exponentialDelay = Math.min(
baseDelay * Math.pow(2, attempt),
maxDelay
);
const jitter = Math.random() * 1000; // Add randomness
const delay = exponentialDelay + jitter;
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage
const account = await retryWithBackoff(
async () => {
const response = await fetch(
`https://api-v3.sprinklr.com/api/v3/account?accountId=${accountId}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
}
);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.response = response;
throw error;
}
return response.json();
},
{
maxRetries: 5,
retryableErrors: [429, 500, 502, 503, 504]
}
);class ErrorLogger {
static log(error, context = {}) {
const errorInfo = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
...context
};
// Log different details based on error type
if (error.response) {
errorInfo.status = error.response.status;
errorInfo.statusText = error.response.statusText;
errorInfo.headers = Object.fromEntries(error.response.headers);
}
// Log to console (development)
console.error('API Error:', JSON.stringify(errorInfo, null, 2));
// Send to error tracking service (production)
if (process.env.NODE_ENV === 'production') {
this.sendToErrorTracker(errorInfo);
}
return errorInfo;
}
static sendToErrorTracker(errorInfo) {
// Send to Sentry, Datadog, CloudWatch, etc.
// Example with hypothetical service:
errorTracker.captureException(errorInfo);
}
}
// Usage
try {
const response = await makeAPICall();
} catch (error) {
ErrorLogger.log(error, {
operation: 'fetchAccount',
accountId: '600057112',
userId: currentUser.id,
environment: process.env.NODE_ENV
});
// Rethrow or handle
throw error;
}Never expose raw API errors to end users:
class UserFriendlyError extends Error {
constructor(apiError, userMessage) {
super(userMessage);
this.apiError = apiError;
this.userMessage = userMessage;
}
static fromAPIError(apiError, context = {}) {
const status = apiError.response?.status;
const code = apiError.code;
let userMessage;
switch (code) {
case 'INVALID_PARAMETER':
userMessage = 'The information provided is invalid. Please check and try again.';
break;
case 'NOT_FOUND':
userMessage = `The ${context.resourceType || 'item'} you're looking for doesn't exist.`;
break;
case 'UNAUTHORIZED':
userMessage = 'Your session has expired. Please log in again.';
break;
case 'FORBIDDEN':
userMessage = "You don't have permission to access this resource.";
break;
case 'RATE_LIMIT_EXCEEDED':
userMessage = 'Too many requests. Please wait a moment and try again.';
break;
case 'VALIDATION_ERROR':
userMessage = 'Some required information is missing or incorrect.';
break;
default:
if (status >= 500) {
userMessage = 'Our service is temporarily unavailable. Please try again in a few minutes.';
} else {
userMessage = 'Something went wrong. Please try again.';
}
}
return new UserFriendlyError(apiError, userMessage);
}
}
// Usage in your application
try {
const account = await getAccount(accountId);
} catch (error) {
const friendlyError = UserFriendlyError.fromAPIError(error, {
resourceType: 'account'
});
// Show to user
showErrorToUser(friendlyError.userMessage);
// Log technical details
console.error(friendlyError.apiError);
}Respecting rate limits isn't just polite—it's essential for a reliable integration.
class RateLimitMonitor {
constructor(alertThreshold = 0.8) {
this.alertThreshold = alertThreshold;
this.requests = [];
}
logRequest(response) {
const limit = parseInt(response.headers.get('X-RateLimit-Limit'));
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const reset = parseInt(response.headers.get('X-RateLimit-Reset'));
this.requests.push({
timestamp: Date.now(),
limit,
remaining,
reset
});
// Alert if approaching limit
const usagePercentage = 1 - (remaining / limit);
if (usagePercentage > this.alertThreshold) {
this.alert({
message: `Rate limit warning: ${Math.round(usagePercentage * 100)}% used`,
remaining,
limit,
resetAt: new Date(reset * 1000)
});
}
return { limit, remaining, reset };
}
alert(info) {
console.warn('⚠️ RATE LIMIT WARNING:', info);
// Send to monitoring system
if (process.env.NODE_ENV === 'production') {
this.notifyOpsTeam(info);
}
}
getUsageStats() {
const now = Date.now();
const lastMinute = this.requests.filter(r => r.timestamp > now - 60000);
const lastHour = this.requests.filter(r => r.timestamp > now - 3600000);
return {
lastMinute: {
count: lastMinute.length,
avgRemaining: lastMinute.reduce((sum, r) => sum + r.remaining, 0) / lastMinute.length
},
lastHour: {
count: lastHour.length
}
};
}
}
// Usage
const monitor = new RateLimitMonitor(0.8); // Alert at 80% usage
async function makeAPICall(endpoint) {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
});
monitor.logRequest(response);
return response.json();
}Validate both inputs and outputs to catch issues early.
class RequestValidator {
static validateAccountId(accountId) {
if (!accountId) {
throw new ValidationError('Account ID is required');
}
if (typeof accountId !== 'string') {
throw new ValidationError('Account ID must be a string');
}
if (accountId.length < 1 || accountId.length > 100) {
throw new ValidationError('Account ID must be 1-100 characters');
}
return true;
}
static validateSearchParams(params) {
if (params.page !== undefined) {
if (!Number.isInteger(params.page) || params.page < 0) {
throw new ValidationError('Page must be a non-negative integer');
}
}
if (params.size !== undefined) {
if (!Number.isInteger(params.size) || params.size < 1 || params.size > 100) {
throw new ValidationError('Size must be between 1 and 100');
}
}
if (params.startDate && params.endDate) {
const start = new Date(params.startDate);
const end = new Date(params.endDate);
if (end < start) {
throw new ValidationError('End date must be after start date');
}
}
return true;
}
}
// Usage
async function searchCases(params) {
RequestValidator.validateSearchParams(params);
const response = await fetch('https://api-v3.sprinklr.com/api/v3/case/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
return response.json();
}class ResponseValidator {
static validate(response, expectedSchema) {
if (!response || typeof response !== 'object') {
throw new ValidationError('Invalid response format');
}
if (response.success === false) {
if (!response.errors || !Array.isArray(response.errors)) {
throw new ValidationError('Error response missing errors array');
}
// This is a valid error response
return response;
}
if (response.success !== true) {
throw new ValidationError('Response missing success field');
}
if (!response.data) {
throw new ValidationError('Success response missing data field');
}
// Validate against expected schema
if (expectedSchema) {
this.validateSchema(response.data, expectedSchema);
}
return response;
}
static validateSchema(data, schema) {
for (const [field, type] of Object.entries(schema)) {
if (Array.isArray(data)) {
// Validate each item in array
data.forEach(item => {
if (typeof item[field] !== type && item[field] !== null) {
console.warn(`Field ${field} has unexpected type. Expected ${type}, got ${typeof item[field]}`);
}
});
} else {
if (typeof data[field] !== type && data[field] !== null) {
console.warn(`Field ${field} has unexpected type. Expected ${type}, got ${typeof data[field]}`);
}
}
}
}
}
// Usage
const response = await fetch('https://api-v3.sprinklr.com/api/v3/account', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
});
const data = await response.json();
// Validate response structure
ResponseValidator.validate(data, {
accountId: 'string',
name: 'string',
channelType: 'string',
status: 'string'
});Comprehensive testing catches issues before users do.
describe('Sprinklr API Client', () => {
test('handles 404 errors gracefully', async () => {
const client = new SprinklrClient(token, key);
await expect(
client.getAccount('nonexistent-id')
).rejects.toThrow('not found');
});
test('retries on 429 rate limit', async () => {
const client = new SprinklrClient(token, key);
// Mock API to return 429 then 200
mockAPI
.onGet('/account').replyOnce(429)
.onGet('/account').replyOnce(200, { success: true, data: {} });
const result = await client.getAccount('123');
expect(result).toBeDefined();
});
test('handles network timeouts', async () => {
const client = new SprinklrClient(token, key, { timeout: 100 });
mockAPI.onGet('/account').timeout();
await expect(
client.getAccount('123')
).rejects.toThrow('timeout');
});
test('validates response structure', async () => {
const client = new SprinklrClient(token, key);
mockAPI.onGet('/account').reply(200, { invalid: 'response' });
await expect(
client.getAccount('123')
).rejects.toThrow('Invalid response');
});
});// config/test.js
module.exports = {
apiUrl: process.env.QA_API_URL || 'https://qa-api-v3.sprinklr.com',
accessToken: process.env.QA_ACCESS_TOKEN,
apiKey: process.env.QA_API_KEY
};
// tests/integration.test.js
describe('Integration Tests (QA Environment)', () => {
const client = new SprinklrClient(
process.env.QA_ACCESS_TOKEN,
process.env.QA_API_KEY
);
test('can fetch accounts from QA', async () => {
const accounts = await client.getAccounts();
expect(accounts).toBeDefined();
expect(Array.isArray(accounts.data)).toBe(true);
});
test('pagination works correctly', async () => {
const page1 = await client.searchCases({ page: 0, size: 10 });
const page2 = await client.searchCases({ page: 1, size: 10 });
expect(page1.pagination.page).toBe(0);
expect(page2.pagination.page).toBe(1);
expect(page1.data).not.toEqual(page2.data);
});
});Slow integrations frustrate users and waste resources.
class SprinklrClient {
constructor(accessToken, apiKey, options = {}) {
this.accessToken = accessToken;
this.apiKey = apiKey;
this.timeout = options.timeout || 30000; // 30 seconds default
}
async request(endpoint, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(endpoint, {
...options,
signal: controller.signal,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Key': this.apiKey,
...options.headers
}
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${this.timeout}ms`);
}
throw error;
}
}
}const https = require('https');
const fetch = require('node-fetch');
// Create agent with connection pooling
const agent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 30000,
maxSockets: 50,
maxFreeSockets: 10
});
// Use agent for all requests
async function makeRequest(url) {
return fetch(url, {
agent,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
});
}You can't fix what you can't see.
class MetricsCollector {
constructor() {
this.metrics = {
requests: {
total: 0,
successful: 0,
failed: 0
},
latency: {
total: 0,
measurements: []
},
errors: new Map(),
endpoints: new Map()
};
}
recordRequest(endpoint, latency, success, error = null) {
// Track request counts
this.metrics.requests.total++;
if (success) {
this.metrics.requests.successful++;
} else {
this.metrics.requests.failed++;
}
// Track latency
this.metrics.latency.total += latency;
this.metrics.latency.measurements.push(latency);
// Keep only last 1000 measurements
if (this.metrics.latency.measurements.length > 1000) {
this.metrics.latency.measurements.shift();
}
// Track errors
if (error) {
const errorKey = `${error.code || error.message}`;
this.metrics.errors.set(
errorKey,
(this.metrics.errors.get(errorKey) || 0) + 1
);
}
// Track per-endpoint metrics
if (!this.metrics.endpoints.has(endpoint)) {
this.metrics.endpoints.set(endpoint, {
requests: 0,
failures: 0,
totalLatency: 0
});
}
const endpointMetrics = this.metrics.endpoints.get(endpoint);
endpointMetrics.requests++;
endpointMetrics.totalLatency += latency;
if (!success) endpointMetrics.failures++;
}
getReport() {
const { requests, latency, errors, endpoints } = this.metrics;
return {
requests: {
...requests,
errorRate: ((requests.failed / requests.total) * 100).toFixed(2) + '%'
},
latency: {
avg: (latency.total / requests.total).toFixed(2) + 'ms',
p50: this.getPercentile(latency.measurements, 0.5) + 'ms',
p95: this.getPercentile(latency.measurements, 0.95) + 'ms',
p99: this.getPercentile(latency.measurements, 0.99) + 'ms'
},
topErrors: Array.from(errors.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([error, count]) => ({ error, count })),
topEndpoints: Array.from(endpoints.entries())
.map(([endpoint, metrics]) => ({
endpoint,
requests: metrics.requests,
avgLatency: (metrics.totalLatency / metrics.requests).toFixed(2) + 'ms',
errorRate: ((metrics.failures / metrics.requests) * 100).toFixed(2) + '%'
}))
.sort((a, b) => b.requests - a.requests)
.slice(0, 5)
};
}
getPercentile(arr, percentile) {
if (arr.length === 0) return 0;
const sorted = arr.slice().sort((a, b) => a - b);
const index = Math.floor(sorted.length * percentile);
return sorted[index];
}
}
// Usage
const metrics = new MetricsCollector();
async function makeAPICall(endpoint) {
const startTime = Date.now();
let success = false;
let error = null;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Key': apiKey
}
});
success = response.ok;
if (!success) {
error = await response.json();
}
return response.json();
} catch (err) {
error = err;
throw err;
} finally {
const latency = Date.now() - startTime;
metrics.recordRequest(endpoint, latency, success, error);
}
}
// Generate hourly reports
setInterval(() => {
console.log('=== API Metrics Report ===');
console.log(JSON.stringify(metrics.getReport(), null, 2));
}, 3600000); // Every hourBefore deploying to production, verify these items:
- Credentials stored in secrets manager (not in code)
- Different credentials for each environment
- HTTPS enforced for all requests
- API keys not logged or exposed in error messages
- Credential rotation schedule implemented
- Exponential backoff retry logic implemented
- Circuit breakers for cascading failures
- Timeouts configured (30s for API calls)
- Error handling covers all scenarios
- Graceful degradation when API is unavailable
- Batch operations used wherever possible
- Caching implemented with appropriate TTLs
- Connection pooling enabled
- Request queuing to prevent overwhelming API
- Pagination implemented for large datasets
- Request/error metrics tracked
- Rate limit usage monitored
- Alerts configured for anomalies
- Logs centralized and searchable
- Performance metrics tracked (p50, p95, p99 latency)
- Unit tests cover happy paths
- Integration tests cover error scenarios
- Load testing performed
- QA environment tested thoroughly
- Failure scenarios tested (network issues, timeouts, rate limits)
- Error Handling - Deep dive into error handling strategies
- Rate Limiting - Understand and optimize for rate limits
- Pagination - Handle large datasets efficiently
- Webhooks - Real-time event notifications
- Authentication - Secure credential management
If you're building a production integration:
- Review this checklist - Ensure you've implemented all critical patterns
- Test in QA first - Never deploy untested code to production
- Monitor from day one - Set up metrics and alerts before launch
- Start small - Begin with conservative rate limits and scale up
Support resources:
- Technical support: support.sprinklr.com
- API documentation: This guide and related docs
- Community forums: Share patterns with other developers