Integración con API Backend
Visión General
El frontend del CDP se comunica con un backend FastAPI que gestiona 41 sistemas SQL diferentes, procesando datos de más de 202,808 consumidores. La integración está diseñada para ser resiliente, eficiente y segura.
Configuración Base
URL del API
// Configuración en variables de entorno
const API_URL = import.meta.env.VITE_API_URL ||
'https://nerdistan-datalake-production.up.railway.app';
Headers Estándar
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
Endpoints Principales
1. Analytics RFM
Obtener Análisis RFM
GET /api/v2/cdp/analytics/rfm?tenant_id={tenant_id}
Parámetros Query:
tenant_id(required): ID del tenant (56, 52, 53, 55, 1)
Response:
{
"success": true,
"data": {
"overview": {
"total_customers": 65226,
"total_revenue": 45000000,
"avg_customer_value": 690.12,
"avg_recency": 45.2,
"avg_frequency": 3.4
},
"distribution": [
{
"rfm_segment": "Champions",
"customer_count": 12000,
"avg_total_spent": 2500.00,
"avg_total_orders": 10,
"avg_clv": 5000.00,
"avg_churn_risk": 0.15
}
]
}
}
Implementación Frontend:
const loadRFMAnalysis = async (tenantId) => {
try {
const response = await fetch(
`${API_URL}/api/v2/cdp/analytics/rfm?tenant_id=${tenantId}`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success && result.data) {
return result.data;
} else {
throw new Error(result.message || 'Error loading RFM data');
}
} catch (error) {
console.error('RFM Analysis Error:', error);
throw error;
}
};
2. Churn Prediction
Análisis de Churn
GET /api/v2/cdp/analytics/churn?tenant_id={tenant_id}
Response:
{
"success": true,
"data": {
"summary": {
"high_risk_count": 5000,
"medium_risk_count": 10000,
"low_risk_count": 50226,
"avg_churn_probability": 0.23
},
"segments": [
{
"risk_level": "high",
"customers": 5000,
"avg_probability": 0.75,
"recommended_actions": [
"Send win-back campaign",
"Offer special discount"
]
}
]
}
}
3. Customer Profiles
Búsqueda de Clientes
POST /api/v2/cdp/analytics/customer-search
Body:
{
"tenant_id": 56,
"query": "john@example.com",
"limit": 50
}
Response:
{
"success": true,
"data": {
"customers": [
{
"customer_id": "12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"total_orders": 15,
"total_spent": 3500.00,
"last_order_date": "2024-01-15",
"rfm_segment": "Loyal",
"clv_score": 5500.00,
"churn_probability": 0.15
}
],
"total_found": 1
}
}
4. Tenant Integrations
VTEX Configuration
GET /api/v2/tenant-integrations/{tenant_id}/vtex
POST /api/v2/tenant-integrations/{tenant_id}/vtex
PUT /api/v2/tenant-integrations/{tenant_id}/vtex
DELETE /api/v2/tenant-integrations/{tenant_id}/vtex
POST/PUT Body:
{
"account": "chelseaio-exit",
"app_key": "vtexappkey-xxxxx",
"app_token": "XXXXXX",
"environment": "vtexcommercestable"
}
Test VTEX Connection
POST /api/v2/tenant-integrations/{tenant_id}/vtex/test
Response:
{
"success": true,
"message": "Connection successful",
"details": {
"account_name": "chelseaio-exit",
"total_products": 15000,
"total_orders": 10863
}
}
5. Google Ads Integration
Obtener Tenants con Estadísticas
GET /api/google-ads/tenants
Response:
{
"success": true,
"data": [
{
"tenant_id": 56,
"tenant_name": "Chelsea IO - Exit",
"total_customers": 65226,
"ready_for_upload": 45000,
"in_progress": 20226,
"last_upload": "2024-01-15T10:30:00Z",
"status": "ready"
}
]
}
Upload de Audiencias
POST /api/google-ads/upload
Body:
{
"tenant_id": 56,
"google_ads_account_id": "123-456-7890",
"options": {
"hash_emails": true,
"include_phones": false,
"segment_filter": "Champions,Loyal"
}
}
Patrones de Comunicación
1. Error Handling Pattern
const apiCall = async (url, options = {}) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...defaultHeaders,
...options.headers
}
});
clearTimeout(timeoutId);
// Check HTTP status
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new APIError(
response.status,
errorData.message || `HTTP ${response.status}`,
errorData
);
}
// Parse response
const data = await response.json();
// Check business logic success
if (data.success === false) {
throw new BusinessError(data.message || 'Operation failed', data);
}
return data;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new TimeoutError('Request timed out after 30 seconds');
}
throw error;
}
};
2. Retry Pattern
const fetchWithRetry = async (url, options = {}, maxRetries = 3) => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await apiCall(url, options);
} catch (error) {
lastError = error;
// Don't retry on client errors (4xx)
if (error.status >= 400 && error.status < 500) {
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, i), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
3. Cache Pattern
class APICache {
constructor(ttl = 300000) { // 5 minutes default
this.cache = new Map();
this.ttl = ttl;
}
getCacheKey(url, params) {
return `${url}?${JSON.stringify(params)}`;
}
get(url, params) {
const key = this.getCacheKey(url, params);
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
set(url, params, data) {
const key = this.getCacheKey(url, params);
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
clear() {
this.cache.clear();
}
}
const apiCache = new APICache();
4. Batch Request Pattern
const batchRequests = async (requests) => {
const results = await Promise.allSettled(
requests.map(req => apiCall(req.url, req.options))
);
return results.map((result, index) => ({
request: requests[index],
success: result.status === 'fulfilled',
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason : null
}));
};
// Usage
const results = await batchRequests([
{ url: '/api/v2/cdp/analytics/rfm?tenant_id=56' },
{ url: '/api/v2/cdp/analytics/churn?tenant_id=56' },
{ url: '/api/v2/cdp/analytics/segments?tenant_id=56' }
]);
Hooks Personalizados
useAPI Hook
import { useState, useEffect } from 'react';
const useAPI = (url, options = {}, dependencies = []) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await apiCall(url, options);
setData(result.data || result);
} catch (err) {
setError(err);
setData(null);
} finally {
setLoading(false);
}
};
fetchData();
}, dependencies);
const refetch = async () => {
setLoading(true);
try {
const result = await apiCall(url, options);
setData(result.data || result);
setError(null);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
return { data, loading, error, refetch };
};
// Usage
const Component = ({ tenantId }) => {
const { data, loading, error, refetch } = useAPI(
`${API_URL}/api/v2/cdp/analytics/rfm?tenant_id=${tenantId}`,
{},
[tenantId]
);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <DataDisplay data={data} onRefresh={refetch} />;
};
usePagination Hook
const usePagination = (url, pageSize = 20) => {
const [page, setPage] = useState(1);
const [data, setData] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const result = await apiCall(
`${url}?page=${page}&page_size=${pageSize}`
);
const newData = result.data || [];
setData(prev => [...prev, ...newData]);
setHasMore(newData.length === pageSize);
setPage(prev => prev + 1);
} catch (error) {
console.error('Pagination error:', error);
setHasMore(false);
} finally {
setLoading(false);
}
};
const reset = () => {
setPage(1);
setData([]);
setHasMore(true);
};
return { data, loading, hasMore, loadMore, reset };
};
Websocket Integration (Futuro)
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.listeners = new Map();
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.emit('connected');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.emit(data.type, data.payload);
};
this.ws.onclose = () => {
this.emit('disconnected');
this.attemptReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.emit('error', error);
};
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, data) {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(cb => cb(data));
}
send(type, payload) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
Seguridad
1. Sanitización de Inputs
const sanitizeInput = (input) => {
// Remove HTML tags
const withoutTags = input.replace(/<[^>]*>/g, '');
// Escape special characters
const escaped = withoutTags
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
return escaped;
};
2. CORS Configuration
// Backend debe configurar CORS apropiadamente
const corsOptions = {
origin: [
'http://localhost:5173',
'https://nerdistan-cdp-frontend-production.up.railway.app'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
3. Rate Limiting
class RateLimiter {
constructor(maxRequests = 10, windowMs = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
canMakeRequest() {
const now = Date.now();
// Remove old requests outside window
this.requests = this.requests.filter(
time => now - time < this.windowMs
);
if (this.requests.length < this.maxRequests) {
this.requests.push(now);
return true;
}
return false;
}
timeUntilNextRequest() {
if (this.requests.length < this.maxRequests) return 0;
const oldestRequest = this.requests[0];
const now = Date.now();
return Math.max(0, this.windowMs - (now - oldestRequest));
}
}
const rateLimiter = new RateLimiter();
Monitoreo y Logging
Request Interceptor
const requestInterceptor = (url, options) => {
const requestId = generateRequestId();
const startTime = Date.now();
console.log(`[${requestId}] API Request:`, {
url,
method: options.method || 'GET',
timestamp: new Date().toISOString()
});
return apiCall(url, options)
.then(response => {
const duration = Date.now() - startTime;
console.log(`[${requestId}] API Success:`, {
duration: `${duration}ms`,
status: 'success'
});
return response;
})
.catch(error => {
const duration = Date.now() - startTime;
console.error(`[${requestId}] API Error:`, {
duration: `${duration}ms`,
error: error.message,
status: error.status
});
throw error;
});
};
Performance Tracking
const trackAPIPerformance = () => {
if ('performance' in window && 'PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'fetch') {
console.log('API Performance:', {
url: entry.name,
duration: entry.duration,
size: entry.transferSize,
cached: entry.transferSize === 0
});
}
}
});
observer.observe({ entryTypes: ['resource'] });
}
};
Testing API Calls
Mock Service Worker Setup
// mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/v2/cdp/analytics/rfm', (req, res, ctx) => {
const tenantId = req.url.searchParams.get('tenant_id');
return res(
ctx.status(200),
ctx.json({
success: true,
data: {
overview: {
total_customers: 1000,
total_revenue: 100000
}
}
})
);
})
];
Component Testing
import { render, screen, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
import Component from './Component';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('loads and displays data', async () => {
render(<Component tenantId={56} />);
await waitFor(() => {
expect(screen.getByText('1000 customers')).toBeInTheDocument();
});
});
Troubleshooting
Common Issues
-
CORS Errors
- Verificar configuración CORS en backend
- Usar proxy en desarrollo
-
Timeouts
- Aumentar timeout para queries pesadas
- Implementar paginación
-
Rate Limiting
- Implementar cache local
- Batch requests cuando sea posible
-
Network Errors
- Implementar retry logic
- Mostrar mensajes de error claros