ATP Implementation Best Practices
This guide provides recommended patterns and practices to ensure your ATP implementation is secure, performant, user-friendly, and maintainable.Security Best Practices
API Key Management
- Do
- Don't
Copy
// Environment variables
const apiKey = process.env.ATP_API_KEY;
// Secure storage services
const apiKey = await secretsManager.getSecret('atp/api_key');
Webhook Security
Always verify webhook signatures to confirm the authenticity of incoming webhooks:- Node.js
- Python
Copy
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Create a HMAC SHA-256 hash using the secret
const computedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(signature)
);
}
User Authentication
For client applications that display notifications, always implement proper authentication:- Best Practice
Copy
// Exchange user auth token for ATP client token
async function getATPClientToken(userAuthToken) {
// Authenticate with your backend first
const response = await fetch('/api/atp/token', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userAuthToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to get ATP client token');
}
const { atpToken } = await response.json();
return atpToken;
}
// Initialize ATP client with the token
const atpToken = await getATPClientToken(userAuthToken);
const atpClient = new ATPClient({
token: atpToken,
// other options...
});
Input Validation
Always validate user input before submitting responses:- JavaScript
Copy
function validateResponse(responseData, responseType, options = {}) {
if (!responseData && options.required) {
return { valid: false, error: 'Response is required' };
}
switch (responseType) {
case 'text':
if (typeof responseData !== 'string') {
return { valid: false, error: 'Response must be a string' };
}
if (options.minLength && responseData.length < options.minLength) {
return {
valid: false,
error: `Response must be at least ${options.minLength} characters`
};
}
if (options.maxLength && responseData.length > options.maxLength) {
return {
valid: false,
error: `Response must be at most ${options.maxLength} characters`
};
}
return { valid: true };
case 'select':
if (options.allowMultiple) {
if (!Array.isArray(responseData)) {
return { valid: false, error: 'Response must be an array' };
}
// Check if all selected values are valid options
const validValues = options.options.map(opt => opt.value);
const allValid = responseData.every(value => validValues.includes(value));
if (!allValid) {
return { valid: false, error: 'Response contains invalid options' };
}
} else {
// Check if the selected value is a valid option
const validValues = options.options.map(opt => opt.value);
if (!validValues.includes(responseData)) {
return { valid: false, error: 'Response is not a valid option' };
}
}
return { valid: true };
// Handle other response types...
default:
return { valid: true };
}
}
Performance Best Practices
Connection Management
For real-time notifications, manage connections efficiently:- Best Practice
Copy
class ConnectionManager {
constructor(atpClient) {
this.atpClient = atpClient;
this.connection = null;
this.backoffTime = 1000; // Start with 1 second
this.maxBackoff = 30000; // Max 30 seconds
this.connectionAttempts = 0;
this.maxAttempts = 10;
}
connect() {
if (this.connection) {
return; // Already connected
}
this.connection = this.atpClient.connectToNotificationStream({
onNotification: this.handleNotification.bind(this),
onError: this.handleError.bind(this),
onClose: this.handleClose.bind(this)
});
// Reset backoff on successful connection
this.backoffTime = 1000;
this.connectionAttempts = 0;
console.log('ATP notification stream connected');
}
disconnect() {
if (this.connection) {
this.connection.disconnect();
this.connection = null;
console.log('ATP notification stream disconnected');
}
}
handleNotification(notification) {
// Process notification...
console.log('Received notification:', notification.title);
}
handleError(error) {
console.error('ATP connection error:', error);
// Auto-reconnect with exponential backoff
this.reconnect();
}
handleClose() {
console.log('ATP connection closed');
this.connection = null;
// Auto-reconnect with exponential backoff
this.reconnect();
}
reconnect() {
if (this.connectionAttempts >= this.maxAttempts) {
console.error('Maximum reconnection attempts reached');
return;
}
this.connectionAttempts++;
// Use exponential backoff
const timeout = Math.min(this.backoffTime * Math.pow(1.5, this.connectionAttempts - 1), this.maxBackoff);
console.log(`Reconnecting in ${timeout}ms (attempt ${this.connectionAttempts})`);
setTimeout(() => {
this.connect();
}, timeout);
}
}
Batching Responses
When processing multiple responses, use batching for better performance:- Best Practice
Copy
class ResponseBatchProcessor {
constructor(atpClient, options = {}) {
this.atpClient = atpClient;
this.batchSize = options.batchSize || 10;
this.flushInterval = options.flushInterval || 5000; // 5 seconds
this.queue = [];
this.processing = false;
// Set up periodic flushing
this.intervalId = setInterval(() => this.flushIfNeeded(), 1000);
}
addResponse(notificationId, actionId, responseData) {
this.queue.push({
notificationId,
actionId,
responseData
});
// Flush immediately if batch size reached
if (this.queue.length >= this.batchSize) {
this.flush();
}
return new Promise((resolve, reject) => {
// Store resolve/reject functions with the queued item
this.queue[this.queue.length - 1].resolve = resolve;
this.queue[this.queue.length - 1].reject = reject;
});
}
flushIfNeeded() {
const now = Date.now();
const oldestItem = this.queue[0];
// If queue has items and oldest item is older than flush interval
if (oldestItem && now - oldestItem.timestamp >= this.flushInterval) {
this.flush();
}
}
async flush() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
const batch = this.queue.splice(0, this.batchSize);
try {
// Process batch items in parallel
const results = await Promise.all(
batch.map(item =>
this.atpClient.respondToNotification(
item.notificationId,
item.actionId,
item.responseData
)
)
);
// Resolve promises for each item
results.forEach((result, index) => {
batch[index].resolve(result);
});
} catch (error) {
// Reject all promises on error
batch.forEach(item => {
item.reject(error);
});
} finally {
this.processing = false;
// Process next batch if queue still has items
if (this.queue.length > 0) {
this.flush();
}
}
}
destroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
// Flush any remaining items
if (this.queue.length > 0) {
this.flush();
}
}
}
Caching Strategies
Implement caching for better performance:- Best Practice
Copy
class NotificationCache {
constructor(options = {}) {
this.cache = new Map();
this.maxSize = options.maxSize || 100;
this.ttl = options.ttl || 60 * 60 * 1000; // 1 hour in milliseconds
}
set(notificationId, notification) {
// Evict oldest entries if cache is full
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(notificationId, {
data: notification,
timestamp: Date.now()
});
}
get(notificationId) {
const entry = this.cache.get(notificationId);
if (!entry) {
return null;
}
// Check if entry has expired
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(notificationId);
return null;
}
return entry.data;
}
clear() {
this.cache.clear();
}
// Clean up expired entries
cleanup() {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.ttl) {
this.cache.delete(key);
}
}
}
}
// Usage with ATP client
class CachedATPClient {
constructor(atpClient) {
this.atpClient = atpClient;
this.cache = new NotificationCache();
// Set up periodic cache cleanup
this.cleanupInterval = setInterval(() => {
this.cache.cleanup();
}, 60 * 1000); // Every minute
}
async getNotification(notificationId) {
// Check cache first
const cachedNotification = this.cache.get(notificationId);
if (cachedNotification) {
return cachedNotification;
}
// Fetch from API if not in cache
const notification = await this.atpClient.getNotification(notificationId);
// Cache the result
this.cache.set(notificationId, notification);
return notification;
}
// Other methods that proxy to the real ATP client...
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
}
}
User Experience Best Practices
Notification Design
Create user-friendly notifications:- Do
- Don't
Copy
// Clear, specific title
const notification = {
title: "Approve expense report for Q2 client meeting ($530)",
description: "Includes: Hotel ($350), Meals ($120), and Transportation ($60). All receipts verified.",
priority: "normal",
actions: [
{ id: "approve", label: "Approve", response_type: "simple" },
{ id: "reject", label: "Reject with reason", response_type: "text" }
]
};
Accessibility
Ensure your notification UI is accessible:- Best Practice
Copy
function AccessibleNotification({ notification, onAction }) {
return (
<div
role="dialog"
aria-labelledby={`notification-${notification.id}-title`}
aria-describedby={`notification-${notification.id}-desc`}
className="notification"
>
<h2 id={`notification-${notification.id}-title`}>
{notification.title}
</h2>
<p id={`notification-${notification.id}-desc`}>
{notification.description}
</p>
<div className="notification-actions">
{notification.actions.map(action => (
<button
key={action.id}
onClick={() => onAction(action.id)}
aria-label={action.label}
>
{action.label}
</button>
))}
</div>
</div>
);
}
Offline Support
Implement robust offline handling:- Best Practice
Copy
class OfflineResponseQueue {
constructor(atpClient) {
this.atpClient = atpClient;
this.queue = [];
this.storageKey = 'atp_offline_responses';
// Load saved responses from storage
this.loadFromStorage();
// Listen for online/offline events
window.addEventListener('online', this.processQueue.bind(this));
window.addEventListener('offline', () => {
console.log('Device is offline. Responses will be queued.');
});
// Try to process queue on startup
if (navigator.onLine) {
this.processQueue();
}
}
async respondToNotification(notificationId, actionId, responseData) {
if (navigator.onLine) {
// If online, send directly
return this.atpClient.respondToNotification(
notificationId,
actionId,
responseData
);
} else {
// If offline, queue the response
const responseItem = {
id: Date.now().toString(),
notificationId,
actionId,
responseData,
timestamp: Date.now()
};
this.queue.push(responseItem);
this.saveToStorage();
return {
success: true,
status: 'queued',
message: 'Response queued for later submission'
};
}
}
async processQueue() {
if (!navigator.onLine || this.queue.length === 0) {
return;
}
console.log(`Processing ${this.queue.length} queued responses`);
const processingQueue = [...this.queue];
this.queue = [];
// Process each queued response
for (const item of processingQueue) {
try {
await this.atpClient.respondToNotification(
item.notificationId,
item.actionId,
item.responseData
);
console.log(`Successfully processed queued response: ${item.id}`);
} catch (error) {
console.error(`Failed to process queued response: ${item.id}`, error);
// Put back in queue unless it's too old
const ageInHours = (Date.now() - item.timestamp) / (1000 * 60 * 60);
if (ageInHours < 24) { // Only retry if less than 24 hours old
this.queue.push(item);
} else {
console.warn(`Discarding old queued response: ${item.id}`);
}
}
}
this.saveToStorage();
}
loadFromStorage() {
try {
const savedQueue = localStorage.getItem(this.storageKey);
if (savedQueue) {
this.queue = JSON.parse(savedQueue);
console.log(`Loaded ${this.queue.length} responses from storage`);
}
} catch (error) {
console.error('Failed to load queued responses from storage:', error);
}
}
saveToStorage() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
} catch (error) {
console.error('Failed to save queued responses to storage:', error);
}
}
}
Testing Best Practices
Mock Services
Create mock services for testing:- JavaScript
Copy
class MockATPClient {
constructor(options = {}) {
this.notifications = options.notifications || [];
this.responses = [];
this.connected = false;
this.onNotificationCallback = null;
}
// Mock connection to notification stream
connectToNotificationStream({ onNotification, onError }) {
this.connected = true;
this.onNotificationCallback = onNotification;
// Simulate sending notifications
setTimeout(() => {
this.notifications.forEach(notification => {
if (this.onNotificationCallback) {
this.onNotificationCallback(notification);
}
});
}, 1000);
return {
disconnect: () => {
this.connected = false;
this.onNotificationCallback = null;
}
};
}
// Mock get notification
async getNotification(notificationId) {
const notification = this.notifications.find(n => n.id === notificationId);
if (!notification) {
throw new Error(`Notification not found: ${notificationId}`);
}
return notification;
}
// Mock respond to notification
async respondToNotification(notificationId, actionId, responseData) {
const notification = await this.getNotification(notificationId);
const response = {
notificationId,
actionId,
responseData,
timestamp: new Date().toISOString()
};
this.responses.push(response);
return {
success: true,
response_id: `resp_${Date.now()}`
};
}
// Helper method to add a test notification
addNotification(notification) {
this.notifications.push(notification);
// Send to callback if connected
if (this.connected && this.onNotificationCallback) {
setTimeout(() => {
this.onNotificationCallback(notification);
}, 500);
}
}
// Get responses for testing assertions
getResponses() {
return this.responses;
}
}
Automated UI Testing
Test your notification UI components:- Jest/React Testing Library
Copy
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import NotificationComponent from './NotificationComponent';
import { MockATPClient } from './test-utils';
describe('NotificationComponent', () => {
const mockNotification = {
id: 'test-notification-1',
title: 'Test Notification',
description: 'This is a test notification',
actions: [
{ id: 'approve', label: 'Approve', response_type: 'simple' },
{ id: 'reject', label: 'Reject', response_type: 'simple' }
]
};
let mockClient;
beforeEach(() => {
mockClient = new MockATPClient();
mockClient.addNotification(mockNotification);
});
it('renders notification with correct title and description', () => {
render(
<NotificationComponent
notification={mockNotification}
atpClient={mockClient}
/>
);
expect(screen.getByText('Test Notification')).toBeInTheDocument();
expect(screen.getByText('This is a test notification')).toBeInTheDocument();
});
it('renders action buttons correctly', () => {
render(
<NotificationComponent
notification={mockNotification}
atpClient={mockClient}
/>
);
expect(screen.getByRole('button', { name: 'Approve' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Reject' })).toBeInTheDocument();
});
it('calls respondToNotification when action button is clicked', async () => {
// Create spy on respondToNotification method
const respondSpy = jest.spyOn(mockClient, 'respondToNotification');
render(
<NotificationComponent
notification={mockNotification}
atpClient={mockClient}
/>
);
// Click the approve button
fireEvent.click(screen.getByRole('button', { name: 'Approve' }));
// Wait for the response to be processed
await waitFor(() => {
expect(respondSpy).toHaveBeenCalledWith(
'test-notification-1',
'approve',
null
);
});
});
it('handles text input responses correctly', async () => {
const textNotification = {
...mockNotification,
actions: [
{
id: 'comment',
label: 'Comment',
response_type: 'text',
response_options: {
placeholder: 'Enter your comment'
}
}
]
};
mockClient.addNotification(textNotification);
const respondSpy = jest.spyOn(mockClient, 'respondToNotification');
render(
<NotificationComponent
notification={textNotification}
atpClient={mockClient}
/>
);
// Type in the text input
fireEvent.change(
screen.getByPlaceholderText('Enter your comment'),
{ target: { value: 'This is a test comment' } }
);
// Click the submit button
fireEvent.click(screen.getByRole('button', { name: 'Comment' }));
// Wait for the response to be processed
await waitFor(() => {
expect(respondSpy).toHaveBeenCalledWith(
'test-notification-1',
'comment',
'This is a test comment'
);
});
});
});
Integration Testing
Test the full ATP workflow:- Best Practice
Copy
describe('ATP Integration Tests', () => {
// Use the test environment
const atpClient = new ATPClient({
apiKey: 'sk_test_your_test_key',
endpoint: 'https://api.atp.example.com',
environment: 'test'
});
let testNotificationId;
it('should successfully send a notification', async () => {
const notification = {
title: 'Integration Test Notification',
description: 'This is an automated test notification',
priority: 'low',
actions: [
{ id: 'acknowledge', label: 'Acknowledge', response_type: 'simple' }
],
metadata: {
test_id: `test_${Date.now()}`,
is_integration_test: true
}
};
const response = await atpClient.sendNotification(notification);
expect(response).toHaveProperty('notification_id');
expect(response.success).toBe(true);
testNotificationId = response.notification_id;
});
it('should fetch notification details correctly', async () => {
// Skip if previous test failed
if (!testNotificationId) {
return;
}
const notification = await atpClient.getNotification(testNotificationId);
expect(notification).toHaveProperty('id', testNotificationId);
expect(notification).toHaveProperty('title', 'Integration Test Notification');
expect(notification).toHaveProperty('actions');
expect(notification.actions).toHaveLength(1);
expect(notification.actions[0]).toHaveProperty('id', 'acknowledge');
});
it('should respond to a notification successfully', async () => {
// Skip if previous test failed
if (!testNotificationId) {
return;
}
const response = await atpClient.respondToNotification(
testNotificationId,
'acknowledge',
null
);
expect(response).toHaveProperty('success', true);
expect(response).toHaveProperty('response_id');
});
it('should verify notification status after response', async () => {
// Skip if previous test failed
if (!testNotificationId) {
return;
}
const status = await atpClient.getNotificationStatus(testNotificationId);
expect(status).toHaveProperty('status', 'responded');
expect(status).toHaveProperty('action_id', 'acknowledge');
});
});
Monitoring and Logging
Structured Logging
Implement structured logging for easy debugging:- Node.js
Copy
const winston = require('winston');
// Create a logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'atp-client' },
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'atp-client.log' })
]
});
// Extend ATP client with logging
class LoggingATPClient {
constructor(atpClient) {
this.atpClient = atpClient;
}
async sendNotification(notification) {
logger.debug('Sending notification', {
title: notification.title,
actions: notification.actions.length
});
try {
const result = await this.atpClient.sendNotification(notification);
logger.info('Notification sent successfully', {
notification_id: result.notification_id
});
return result;
} catch (error) {
logger.error('Failed to send notification', {
error: error.message,
stack: error.stack
});
throw error;
}
}
async respondToNotification(notificationId, actionId, responseData) {
logger.debug('Submitting response', {
notification_id: notificationId,
action_id: actionId
});
try {
const result = await this.atpClient.respondToNotification(
notificationId,
actionId,
responseData
);
logger.info('Response submitted successfully', {
notification_id: notificationId,
response_id: result.response_id
});
return result;
} catch (error) {
logger.error('Failed to submit response', {
notification_id: notificationId,
action_id: actionId,
error: error.message,
stack: error.stack
});
throw error;
}
}
// Implement other methods similarly...
}
Health Checks
Implement health checks to monitor ATP client status:- Best Practice
Copy
class ATPHealthMonitor {
constructor(atpClient) {
this.atpClient = atpClient;
this.status = {
connected: false,
lastPingTime: null,
lastPingLatency: null,
errors: []
};
// Start monitoring
this.startMonitoring();
}
async pingServer() {
const startTime = Date.now();
try {
await this.atpClient.ping();
const latency = Date.now() - startTime;
this.status.connected = true;
this.status.lastPingTime = new Date().toISOString();
this.status.lastPingLatency = latency;
// Keep only the 10 most recent errors
if (this.status.errors.length > 10) {
this.status.errors = this.status.errors.slice(-10);
}
return {
success: true,
latency
};
} catch (error) {
this.status.connected = false;
this.status.errors.push({
time: new Date().toISOString(),
message: error.message
});
return {
success: false,
error: error.message
};
}
}
getHealthStatus() {
return {
...this.status,
healthCheck: {
status: this.status.connected ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString()
}
};
}
startMonitoring() {
// Ping every 5 minutes
this.intervalId = setInterval(async () => {
await this.pingServer();
}, 5 * 60 * 1000);
// Initial ping
this.pingServer();
}
stopMonitoring() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
}
Metrics Collection
Collect metrics to monitor performance:- Best Practice
Copy
class ATPMetricsCollector {
constructor() {
this.metrics = {
notificationsSent: 0,
responsesReceived: 0,
errors: 0,
avgResponseTime: 0,
totalResponseTime: 0,
responseTimesSample: []
};
}
recordNotificationSent() {
this.metrics.notificationsSent++;
}
recordResponseReceived(responseTime) {
this.metrics.responsesReceived++;
if (responseTime) {
// Add to total
this.metrics.totalResponseTime += responseTime;
// Add to sample (keep last 100)
this.metrics.responseTimesSample.push(responseTime);
if (this.metrics.responseTimesSample.length > 100) {
this.metrics.responseTimesSample.shift();
}
// Update average
this.metrics.avgResponseTime = this.metrics.totalResponseTime / this.metrics.responsesReceived;
}
}
recordError(errorType) {
this.metrics.errors++;
// Record specific error type
if (!this.metrics[`error_${errorType}`]) {
this.metrics[`error_${errorType}`] = 0;
}
this.metrics[`error_${errorType}`]++;
}
getMetrics() {
return {
...this.metrics,
timestamp: new Date().toISOString()
};
}
reset() {
this.metrics = {
notificationsSent: 0,
responsesReceived: 0,
errors: 0,
avgResponseTime: 0,
totalResponseTime: 0,
responseTimesSample: []
};
}
}
// Usage with ATP client
class MetricsATPClient {
constructor(atpClient, metricsCollector) {
this.atpClient = atpClient;
this.metrics = metricsCollector || new ATPMetricsCollector();
}
async sendNotification(notification) {
try {
const result = await this.atpClient.sendNotification(notification);
this.metrics.recordNotificationSent();
return result;
} catch (error) {
this.metrics.recordError('send_notification');
throw error;
}
}
async respondToNotification(notificationId, actionId, responseData) {
const startTime = Date.now();
try {
const result = await this.atpClient.respondToNotification(
notificationId,
actionId,
responseData
);
const responseTime = Date.now() - startTime;
this.metrics.recordResponseReceived(responseTime);
return result;
} catch (error) {
this.metrics.recordError('respond_to_notification');
throw error;
}
}
// Implement other methods similarly...
getMetrics() {
return this.metrics.getMetrics();
}
}
Verification: Knowledge Check
Before proceeding, let’s verify your understanding:Next Steps
Now that you understand best practices for ATP implementations, explore additional resources:Troubleshooting
Solutions for common issues with ATP client implementations
API Reference
Detailed API documentation for ATP implementation