Saltar al contenido principal

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');

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

  1. CORS Errors

    • Verificar configuración CORS en backend
    • Usar proxy en desarrollo
  2. Timeouts

    • Aumentar timeout para queries pesadas
    • Implementar paginación
  3. Rate Limiting

    • Implementar cache local
    • Batch requests cuando sea posible
  4. Network Errors

    • Implementar retry logic
    • Mostrar mensajes de error claros