Skip to main content

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

// 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:
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:
// 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:
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:
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:
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:
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:
// 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:
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:
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:
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:
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:
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:
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:
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:
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