Documentation Index
Fetch the complete documentation index at: https://atp.hypertext.studio/llms.txt
Use this file to discover all available pages before exploring further.
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
// Environment variables
const apiKey = process.env.ATP_API_KEY;
// Secure storage services
const apiKey = await secretsManager.getSecret('atp/api_key');
// NEVER hardcode API keys
const apiKey = "sk_live_abc123xyz456";
// NEVER commit API keys to version control
const config = {
apiKey: "sk_live_abc123xyz456"
};
Webhook Security
Always verify webhook signatures to confirm the authenticity of incoming webhooks:- Node.js
- Python
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)
);
}
import hmac
import hashlib
import json
def verify_webhook_signature(payload, signature, secret):
# Create a HMAC SHA-256 hash using the secret
computed_signature = hmac.new(
secret.encode(),
json.dumps(payload, sort_keys=True).encode(),
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(computed_signature, signature)
User Authentication
For client applications that display notifications, always implement proper authentication:- Best Practice
// 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
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
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
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
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
// 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" }
]
};
// Vague title, unclear description
const notification = {
title: "Approval needed",
description: "Please review and approve this item.",
priority: "high", // Overusing high priority
actions: [
{ id: "y", label: "Yes", response_type: "simple" },
{ id: "n", label: "No", response_type: "simple" }
]
};
Accessibility
Ensure your notification UI is accessible:- Best Practice
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
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
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
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
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
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
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
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