Skip to main content

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');

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)
  );
}

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" }
  ]
};

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
I