Introduzione
Questa guida presenta le migliori pratiche per integrare efficacemente con l’API Lovi, ottimizzando performance, affidabilità e manutenibilità delle tue implementazioni.🏗️ Architettura e Design
Progettazione dell’Integrazione
Separazione delle Responsabilità:class LoviService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://cloud.lovi.ai/functions/v1';
}
// Gestione notifiche
async sendNotification(data) {
return this.request('/notify', data);
}
// Gestione template
async getTemplates() {
return this.request('/templates');
}
// Metodo privato per richieste HTTP
async request(endpoint, data = null) {
const url = `${this.baseUrl}${endpoint}?access_key=${this.apiKey}`;
// Implementazione richiesta...
}
}
const config = {
api: {
baseUrl: 'https://cloud.lovi.ai/functions/v1',
timeout: 30000,
retryAttempts: 3
},
rateLimit: {
requestsPerMinute: 100,
burstLimit: 1000
}
};
Gestione delle Chiavi API
Sicurezza delle Chiavi:// ❌ NON fare questo
const apiKey = "sk_live_123456789"; // Chiave hardcoded
// ✅ Fai questo invece
const apiKey = process.env.LOVI_API_KEY; // Variabile d'ambiente
// O usa un gestore di segreti
const apiKey = await secretManager.getSecret('lovi-api-key');
class KeyManager {
async getValidKey() {
const key = await this.getCurrentKey();
if (this.isExpiringSoon(key)) {
await this.rotateKey(key);
}
return key;
}
}
🚀 Ottimizzazione delle Performance
Gestione dei Limiti di Tariffa
Implementazione Bucket Token:class RateLimiter {
constructor(requestsPerMinute = 100) {
this.capacity = requestsPerMinute;
this.tokens = requestsPerMinute;
this.lastRefill = Date.now();
this.refillRate = requestsPerMinute / 60; // token/secondo
}
async acquireToken() {
this.refill();
if (this.tokens < 1) {
throw new Error('Rate limit exceeded');
}
this.tokens -= 1;
}
refill() {
const now = Date.now();
const timePassed = (now - this.lastRefill) / 1000;
const tokensToAdd = timePassed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
class PriorityQueue {
constructor() {
this.queues = {
high: [],
normal: [],
low: []
};
}
async process() {
while (true) {
let request = null;
if (this.queues.high.length > 0) {
request = this.queues.high.shift();
} else if (this.queues.normal.length > 0) {
request = this.queues.normal.shift();
} else if (this.queues.low.length > 0) {
request = this.queues.low.shift();
}
if (request) {
await this.processRequest(request);
} else {
await sleep(100); // Attendi nuove richieste
}
}
}
}
Ottimizzazione delle Richieste
Batch Processing:async function sendBulkNotifications(notifications) {
const batches = chunk(notifications, 10); // 10 notifiche per batch
for (const batch of batches) {
try {
await Promise.all(
batch.map(notification => sendNotification(notification))
);
await sleep(1000); // Pausa tra batch
} catch (error) {
console.error('Batch failed:', error);
// Gestisci errori parziali
}
}
}
const data = {
contact: { number: '34666033135' },
template: 'welcome_user',
variables: { name: 'Mario', company: 'TechCorp' }
};
// Compressione automatica per payload grandi
if (JSON.stringify(data).length > 1000) {
headers['Content-Encoding'] = 'gzip';
body = gzipSync(JSON.stringify(data));
}
Caching Intelligente
Cache Template:class TemplateCache {
constructor(ttl = 3600000) { // 1 ora
this.cache = new Map();
this.ttl = ttl;
}
async getTemplate(name, language = 'it_IT') {
const key = `${name}_${language}`;
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const template = await this.fetchTemplate(name, language);
this.cache.set(key, {
data: template,
timestamp: Date.now()
});
return template;
}
}
const memoize = (fn, ttl = 300000) => { // 5 minuti
const cache = new Map();
return async (...args) => {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.result;
}
const result = await fn(...args);
cache.set(key, {
result,
timestamp: Date.now()
});
return result;
};
};
🛡️ Affidabilità e Monitoraggio
Gestione degli Errori
Strategie di Retry:async function withRetry(operation, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts || !isRetryable(error)) {
throw error;
}
const delay = Math.pow(2, attempt) * 1000; // Backoff esponenziale
await sleep(delay);
}
}
}
function isRetryable(error) {
return [429, 500, 502, 503].includes(error.statusCode);
}
class CircuitBreaker {
constructor(failureThreshold = 5, timeout = 60000) {
this.failureThreshold = failureThreshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}
Monitoraggio e Logging
Struttura Log Completa:const logRequest = (request) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
request_id: request.id,
method: request.method,
endpoint: request.endpoint,
user_agent: request.userAgent,
ip_address: request.ip,
company_id: request.companyId,
user_id: request.userId,
request_size: JSON.stringify(request.body).length,
response_time: Date.now() - request.startTime,
status_code: request.statusCode,
error_code: request.error?.code,
template_name: request.templateName,
recipient_count: request.recipientCount
}));
};
const metrics = {
requests_total: 0,
requests_success: 0,
requests_error: 0,
response_time_avg: 0,
rate_limit_hits: 0,
template_cache_hits: 0,
template_cache_misses: 0
};
const updateMetrics = (request) => {
metrics.requests_total++;
if (request.statusCode < 400) {
metrics.requests_success++;
} else {
metrics.requests_error++;
}
// Aggiorna tempo di risposta medio
const responseTime = Date.now() - request.startTime;
metrics.response_time_avg =
(metrics.response_time_avg + responseTime) / 2;
};
🔧 Manutenibilità
Testing
Test Unitari:describe('LoviService', () => {
let service;
beforeEach(() => {
service = new LoviService('test_key');
});
test('sendNotification should handle success', async () => {
const mockResponse = { success: true, notification_id: '123' };
// Mock della richiesta HTTP
const result = await service.sendNotification({
contact: { number: '34666033135' },
template: 'welcome'
});
expect(result).toEqual(mockResponse);
});
test('should handle rate limiting', async () => {
// Simula errore 429
await expect(service.sendNotification({}))
.rejects.toThrow('Rate limit exceeded');
});
});
describe('API Integration', () => {
test('end-to-end notification flow', async () => {
// 1. Crea template
const template = await createTemplate(testTemplateData);
// 2. Invia notifica
const notification = await sendNotification({
contact: testContact,
template: template.name
});
// 3. Verifica consegna (se possibile)
expect(notification.success).toBe(true);
});
});
Documentazione
Documentazione Automatica:/**
* Invia una notifica WhatsApp usando un template
* @param {Object} data - Dati della notifica
* @param {Object} data.contact - Informazioni del contatto
* @param {string} data.contact.number - Numero di telefono (solo numeri)
* @param {string} data.template - Nome del template
* @param {Object} [data.variables] - Variabili del template
* @param {string} [data.datetime_sending] - Data/ora programmata (ISO 8601)
* @returns {Promise<Object>} Risultato dell'invio
*/
async function sendNotification(data) {
// Implementazione...
}
📊 Ottimizzazione per Diversi Casi d’Uso
E-commerce
Carrello Abbandonato:async function sendAbandonedCartReminder(userId, cartItems) {
const variables = {
name: await getUserName(userId),
items: cartItems.map(item => item.name).join(', '),
total: calculateTotal(cartItems),
discount_code: generateDiscountCode()
};
return await loviService.sendNotification({
contact: { number: await getUserPhone(userId) },
template: 'cart_reminder',
variables,
datetime_sending: moment().add(1, 'hour').toISOString()
});
}
Assistenza Clienti
Escalation Automatica:async function handleCustomerEscalation(ticket) {
if (ticket.priority === 'high' && !ticket.agent_assigned) {
await notifyAvailableAgents(ticket);
// Riprova dopo 5 minuti se nessun agente risponde
setTimeout(async () => {
if (!ticket.agent_assigned) {
await escalateToSupervisor(ticket);
}
}, 5 * 60 * 1000);
}
}
Marketing
Campagne Personalizzate:async function sendPersonalizedCampaign(segment) {
const users = await getUsersInSegment(segment);
const batches = chunk(users, 50); // Batch da 50
for (const batch of batches) {
await Promise.all(
batch.map(async (user) => {
const variables = await generatePersonalizedContent(user);
return loviService.sendNotification({
contact: { number: user.phone },
template: 'personalized_offer',
variables
});
})
);
await sleep(60000); // Pausa 1 minuto tra batch
}
}
🔒 Sicurezza
Validazione Input
Sanitizzazione:function sanitizeInput(input) {
return {
contact: {
number: input.contact.number.replace(/\D/g, '') // Solo numeri
},
template: input.template.replace(/[^a-zA-Z0-9_]/g, ''), // Solo caratteri sicuri
variables: Object.keys(input.variables || {}).reduce((acc, key) => {
acc[key] = String(input.variables[key]).substring(0, 100); // Limita lunghezza
return acc;
}, {})
};
}
const notificationSchema = {
type: 'object',
required: ['contact', 'template'],
properties: {
contact: {
type: 'object',
required: ['number'],
properties: {
number: {
type: 'string',
pattern: '^[0-9]{8,15}$'
}
}
},
template: {
type: 'string',
minLength: 1,
maxLength: 100
},
variables: {
type: 'object',
maxProperties: 20
}
}
};
Gestione Segreti
Rotazione Automatica:class SecretManager {
async getSecret(name) {
const secret = await this.fetchSecret(name);
if (this.isExpiring(secret)) {
await this.rotateSecret(name);
return this.fetchSecret(name);
}
return secret;
}
isExpiring(secret) {
const expiry = new Date(secret.expiry);
const now = new Date();
const daysUntilExpiry = (expiry - now) / (1000 * 60 * 60 * 24);
return daysUntilExpiry < 30; // Ruota 30 giorni prima
}
}
