Common Issues and Solutions
This guide provides solutions for common issues you might encounter when implementing ATP in your applications.Connection Issues
Unable to Connect to ATP Server
- Symptoms
- Causes
- Solutions
- Connection timeout errors
- Network-related exceptions
- “Cannot reach server” errors
- Invalid API endpoint
- Network connectivity issues
- Firewall blocking outbound connections
- Incorrect SSL/TLS configuration
Copy
// Check your endpoint configuration
// Ensure you're using the correct endpoint for your environment
// Development environment
const client = new ATPClient({
apiKey: 'your_test_api_key',
endpoint: 'https://api.test.atp.example.com',
environment: 'development'
});
// Production environment
const client = new ATPClient({
apiKey: 'your_live_api_key',
endpoint: 'https://api.atp.example.com',
environment: 'production'
});
// Test connectivity with a ping
async function testConnection() {
try {
await client.ping();
console.log('Connection successful!');
return true;
} catch (error) {
console.error('Connection failed:', error.message);
// Check for specific error types
if (error.code === 'ECONNREFUSED') {
console.error('Server could not be reached. Check your network connection.');
} else if (error.code === 'ETIMEDOUT') {
console.error('Connection timed out. Check your firewall settings.');
} else if (error.code === 'CERT_HAS_EXPIRED') {
console.error('SSL certificate issue. Check your TLS configuration.');
}
return false;
}
}
WebSocket Connection Failures
- Symptoms
- Causes
- Solutions
- WebSocket connection errors
- Frequent disconnections
- Notification delays or missing notifications
- Firewall blocking WebSocket traffic
- Proxy servers not configured for WebSockets
- Client-side network changes (switching Wi-Fi, etc.)
Copy
// Implement robust connection handling with retries
class RobustWebSocketManager {
constructor(atpClient, options = {}) {
this.atpClient = atpClient;
this.connection = null;
this.backoffTime = options.initialBackoff || 1000; // Start with 1 second
this.maxBackoff = options.maxBackoff || 30000; // Max 30 seconds
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.listeners = {
onNotification: options.onNotification || (() => {}),
onConnected: options.onConnected || (() => {}),
onDisconnected: options.onDisconnected || (() => {}),
onError: options.onError || (() => {})
};
// Auto-reconnect on network status change
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', () => this.reconnect());
}
}
connect() {
if (this.connection) {
return; // Already connected
}
try {
this.connection = this.atpClient.connectToNotificationStream({
onNotification: (notification) => {
this.listeners.onNotification(notification);
},
onError: (error) => {
this.listeners.onError(error);
this.handleConnectionFailure(error);
},
onClose: () => {
this.listeners.onDisconnected();
this.connection = null;
this.reconnect();
}
});
// Connection successful
this.backoffTime = 1000; // Reset backoff
this.reconnectAttempts = 0;
this.listeners.onConnected();
} catch (error) {
this.listeners.onError(error);
this.handleConnectionFailure(error);
}
}
handleConnectionFailure(error) {
console.error('WebSocket connection failed:', error);
this.connection = null;
// Check for specific WebSocket errors
if (error.code === 1006) {
console.error('Abnormal closure. Possible network issue.');
} else if (error.code === 1008) {
console.error('Policy violation. Check your API permissions.');
} else if (error.code === 1011) {
console.error('Server error. Check server status.');
}
this.reconnect();
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Maximum reconnection attempts reached. Giving up.');
this.listeners.onError(new Error('Maximum reconnection attempts reached'));
return;
}
// Implement exponential backoff
const delay = Math.min(
this.backoffTime * Math.pow(1.5, this.reconnectAttempts),
this.maxBackoff
);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
disconnect() {
if (this.connection) {
this.connection.disconnect();
this.connection = null;
}
}
}
// Usage
const wsManager = new RobustWebSocketManager(atpClient, {
onNotification: (notification) => {
console.log('Received notification:', notification.title);
// Process notification...
},
onConnected: () => {
console.log('WebSocket connected successfully');
// Update UI to show connected status
},
onDisconnected: () => {
console.log('WebSocket disconnected');
// Update UI to show disconnected status
},
onError: (error) => {
console.error('WebSocket error:', error);
// Display error to user
}
});
wsManager.connect();
Authentication Issues
API Key Authentication Failures
- Symptoms
- Causes
- Solutions
- 401 Unauthorized errors
- “Invalid API key” error messages
- Authentication failures when sending notifications
- Incorrect API key
- Expired API key
- Using development key in production or vice versa
- API key has insufficient permissions
Copy
// Verify your API key is correct and has the right permissions
async function verifyAPIKey() {
try {
const client = new ATPClient({
apiKey: 'your_api_key',
endpoint: 'https://api.atp.example.com'
});
// Try to get account info (a lightweight operation)
const accountInfo = await client.getAccountInfo();
console.log('API key is valid. Account:', accountInfo.name);
console.log('Permissions:', accountInfo.permissions);
console.log('Environment:', accountInfo.environment);
// Check if it's a development key in production or vice versa
if (accountInfo.environment === 'development' &&
process.env.NODE_ENV === 'production') {
console.warn('WARNING: Using development API key in production environment!');
}
return true;
} catch (error) {
console.error('API key verification failed:', error.message);
if (error.status === 401) {
console.error('Invalid API key. Please check your key.');
} else if (error.message.includes('permissions')) {
console.error('API key has insufficient permissions.');
} else if (error.message.includes('expired')) {
console.error('API key has expired.');
}
return false;
}
}
// Implementation of key rotation
class RotatingKeyManager {
constructor(keys, options = {}) {
this.keys = Array.isArray(keys) ? keys : [keys];
this.currentKeyIndex = 0;
this.lastRotation = Date.now();
this.rotationInterval = options.rotationInterval || (24 * 60 * 60 * 1000); // 24 hours
this.onKeyFailure = options.onKeyFailure || (() => {});
}
getCurrentKey() {
// Check if it's time to rotate
if (Date.now() - this.lastRotation > this.rotationInterval) {
this.rotate();
}
return this.keys[this.currentKeyIndex];
}
rotate() {
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.keys.length;
this.lastRotation = Date.now();
}
handleKeyFailure(key, error) {
// Remove the failed key if we have others
if (this.keys.length > 1) {
this.keys = this.keys.filter(k => k !== key);
this.currentKeyIndex = 0; // Reset to first key
}
this.onKeyFailure(key, error);
}
}
// Usage
const keyManager = new RotatingKeyManager([
'primary_api_key',
'backup_api_key'
], {
onKeyFailure: (key, error) => {
// Send alert to administrators
console.error(`API key ${key.substring(0, 8)}... failed:`, error);
alertAdmins(`API key failure: ${error.message}`);
}
});
// Create client with key manager
const client = new ATPClient({
apiKey: keyManager.getCurrentKey(),
endpoint: 'https://api.atp.example.com'
});
// Handle key failure in API calls
async function sendNotificationWithKeyFailureHandling(notification) {
try {
return await client.sendNotification(notification);
} catch (error) {
if (error.status === 401) {
const currentKey = keyManager.getCurrentKey();
keyManager.handleKeyFailure(currentKey, error);
// Retry with a new key
client.setApiKey(keyManager.getCurrentKey());
return await client.sendNotification(notification);
}
throw error;
}
}
Webhook Signature Verification Failures
- Symptoms
- Causes
- Solutions
- Webhook verification errors
- “Invalid signature” messages in logs
- Missing webhook responses
- Incorrect webhook secret
- Mismatched signature computation algorithm
- Request body modification by middleware
- Non-timing-safe comparison method
Copy
// Ensure your signature verification is correct
function verifyWebhookSignature(payload, signature, secret) {
// Convert payload to string if it's an object
const payloadString = typeof payload === 'string'
? payload
: JSON.stringify(payload);
// Create a HMAC SHA-256 hash using the secret
const computedSignature = crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(signature)
);
}
// Express middleware to preserve raw body for signature verification
function preserveRawBody(req, res, next) {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
req.rawBody = data;
next();
});
}
// Express webhook handler with proper verification
app.post('/webhook', preserveRawBody, (req, res) => {
const signature = req.headers['x-atp-signature'];
const webhookSecret = process.env.WEBHOOK_SECRET;
if (!signature) {
console.error('Missing signature header');
return res.status(401).json({ error: 'Missing signature' });
}
try {
// Use the raw body for verification
const isValid = verifyWebhookSignature(req.rawBody, signature, webhookSecret);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse the body if needed
const payload = typeof req.body === 'object' ? req.body : JSON.parse(req.rawBody);
// Process webhook
processWebhook(payload);
res.status(200).json({ status: 'success' });
} catch (error) {
console.error('Webhook verification error:', error);
res.status(500).json({ error: 'Webhook processing error' });
}
});
// Debug webhook issues
function debugWebhook(req) {
const signature = req.headers['x-atp-signature'];
const webhookSecret = process.env.WEBHOOK_SECRET;
console.log('Received webhook:');
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Raw body length:', req.rawBody?.length);
console.log('Parsed body:', JSON.stringify(req.body, null, 2));
// Try multiple signature methods to debug issues
const hmacSha256 = crypto
.createHmac('sha256', webhookSecret)
.update(req.rawBody)
.digest('hex');
const hmacSha256Json = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(req.body))
.digest('hex');
const hmacSha256SortedJson = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(req.body, Object.keys(req.body).sort()))
.digest('hex');
console.log('Expected signature (raw):', hmacSha256);
console.log('Expected signature (JSON):', hmacSha256Json);
console.log('Expected signature (sorted JSON):', hmacSha256SortedJson);
console.log('Actual signature:', signature);
}
Notification Issues
Notifications Not Being Received
- Symptoms
- Causes
- Solutions
- Notifications are sent but never received by clients
- Missing notifications in client applications
- Notification count discrepancies
- Incorrect client configuration
- Connection issues between client and ATP server
- Push notification registration issues
- Expired notification tokens
Copy
// Verify notification delivery
async function verifyNotificationDelivery(notificationId) {
try {
// Check notification status
const status = await client.getNotificationStatus(notificationId);
console.log('Notification status:', status.status);
if (status.status === 'delivered') {
console.log('Notification was delivered successfully at:', status.delivered_at);
} else if (status.status === 'pending') {
console.log('Notification is still pending delivery');
} else if (status.status === 'failed') {
console.error('Notification delivery failed:', status.error);
}
// Check client registration
const clients = await client.getRegisteredClients();
console.log(`Found ${clients.length} registered clients`);
if (clients.length === 0) {
console.error('No clients registered to receive notifications');
}
return status;
} catch (error) {
console.error('Failed to verify notification delivery:', error);
throw error;
}
}
// For mobile: verify push notification configuration
async function verifyPushNotificationSetup() {
// Check for push registration
const pushToken = await getPushToken();
if (!pushToken) {
console.error('No push token available. Push notifications are not configured.');
return false;
}
// Verify token is registered with ATP
try {
const result = await client.verifyPushToken(pushToken);
if (result.registered) {
console.log('Push token is registered and valid');
console.log('Last notification received:', result.last_notification_at);
return true;
} else {
console.error('Push token is not registered with ATP');
// Re-register the token
await client.registerPushToken(pushToken);
console.log('Push token has been re-registered');
return true;
}
} catch (error) {
console.error('Failed to verify push token:', error);
return false;
}
}
// For web: verify WebSocket connection
function verifyWebSocketConnection() {
if (!client.isConnected()) {
console.error('WebSocket is not connected. Notifications cannot be received.');
// Attempt to reconnect
client.connect();
return false;
}
console.log('WebSocket is connected and ready to receive notifications');
return true;
}
Notification Display Issues
- Symptoms
- Causes
- Solutions
- Notifications render incorrectly
- Missing action buttons
- Broken layouts or UI issues
- Styling conflicts
- Incorrect response type handling
- Missing UI components for specific response types
Copy
// Create a notification tester to verify display
function testNotificationDisplay() {
// Create test notifications for all response types
const testNotifications = [
{
id: 'test_simple',
title: 'Simple Response Test',
description: 'Testing simple response buttons',
actions: [
{ id: 'approve', label: 'Approve', response_type: 'simple' },
{ id: 'reject', label: 'Reject', response_type: 'simple' }
]
},
{
id: 'test_text',
title: 'Text Response Test',
description: 'Testing text input response',
actions: [
{
id: 'comment',
label: 'Comment',
response_type: 'text',
response_options: {
placeholder: 'Enter your comment',
multiline: true,
max_length: 500
}
}
]
},
{
id: 'test_select',
title: 'Select Response Test',
description: 'Testing dropdown selection response',
actions: [
{
id: 'choose',
label: 'Choose Option',
response_type: 'select',
response_options: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
],
allow_multiple: false,
required: true
}
}
]
},
{
id: 'test_form',
title: 'Form Response Test',
description: 'Testing form input response',
actions: [
{
id: 'submit_form',
label: 'Submit Form',
response_type: 'form',
response_options: {
fields: [
{
id: 'name',
label: 'Name',
type: 'text',
required: true
},
{
id: 'age',
label: 'Age',
type: 'number',
min: 18,
max: 120
},
{
id: 'notes',
label: 'Notes',
type: 'textarea'
}
]
}
}
]
}
];
// Display each test notification
testNotifications.forEach(notification => {
displayNotification(notification);
});
}
// Test response handling without sending actual responses
function testResponseHandling(notification) {
// Mock response handler
function mockResponseHandler(actionId, responseData) {
console.log('Action handled:', actionId);
console.log('Response data:', responseData);
// Verify response data format based on response type
const action = notification.actions.find(a => a.id === actionId);
if (!action) {
console.error('Unknown action ID:', actionId);
return false;
}
switch (action.response_type) {
case 'simple':
console.log('Simple response validation: PASS');
return true;
case 'text':
if (typeof responseData !== 'string') {
console.error('Text response should be a string');
return false;
}
const { min_length, max_length } = action.response_options || {};
if (min_length && responseData.length < min_length) {
console.error(`Text too short (min: ${min_length})`);
return false;
}
if (max_length && responseData.length > max_length) {
console.error(`Text too long (max: ${max_length})`);
return false;
}
console.log('Text response validation: PASS');
return true;
case 'select':
const { options, allow_multiple } = action.response_options || {};
if (allow_multiple) {
if (!Array.isArray(responseData)) {
console.error('Multiple select response should be an array');
return false;
}
const allValid = responseData.every(value =>
options.some(opt => opt.value === value)
);
if (!allValid) {
console.error('Invalid option selected');
return false;
}
} else {
const isValid = options.some(opt => opt.value === responseData);
if (!isValid) {
console.error('Invalid option selected');
return false;
}
}
console.log('Select response validation: PASS');
return true;
// Handle other response types...
default:
console.warn(`No validation for response type: ${action.response_type}`);
return true;
}
}
// Return the mock handler for use in display tests
return mockResponseHandler;
}
Response Handling Issues
Response Submission Failures
- Symptoms
- Causes
- Solutions
- “Failed to submit response” errors
- Timeouts when submitting responses
- Responses not being registered by ATP server
- Network connectivity issues
- Expired notifications
- Invalid response format
- Rate limiting
Copy
// Implement robust response submission with retries
async function submitResponseWithRetry(notificationId, actionId, responseData, options = {}) {
const maxRetries = options.maxRetries || 3;
const backoffFactor = options.backoffFactor || 1.5;
const initialBackoff = options.initialBackoff || 1000;
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Check if notification is still valid before attempting submission
const status = await client.getNotificationStatus(notificationId);
if (status.status === 'expired') {
console.error('Cannot respond to expired notification');
return { success: false, error: 'Notification expired' };
}
if (status.status === 'responded') {
console.warn('Notification already has a response');
return { success: true, alreadyResponded: true };
}
// Submit the response
const result = await client.respondToNotification(
notificationId,
actionId,
responseData
);
console.log('Response submitted successfully');
return { success: true, result };
} catch (error) {
console.error(`Response submission attempt ${attempt + 1} failed:`, error);
lastError = error;
// Check for permanent errors that shouldn't be retried
if (error.status === 404) {
console.error('Notification not found');
return { success: false, error: 'Notification not found' };
}
if (error.status === 400) {
console.error('Invalid response format');
return { success: false, error: 'Invalid response format' };
}
// Handle rate limiting
if (error.status === 429) {
const retryAfter = parseInt(error.headers?.['retry-after'] || '5', 10);
console.warn(`Rate limited. Retrying after ${retryAfter} seconds.`);
// Wait for the specified time before retrying
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
// For other errors, use exponential backoff
const backoffTime = initialBackoff * Math.pow(backoffFactor, attempt);
console.log(`Retrying in ${backoffTime}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffTime));
}
}
// If we get here, all retries failed
console.error(`Failed to submit response after ${maxRetries} attempts`);
return { success: false, error: lastError?.message || 'Max retries exceeded' };
}
// Queue responses for offline scenarios
class ResponseQueue {
constructor(atpClient) {
this.atpClient = atpClient;
this.queue = [];
this.storageKey = 'atp_response_queue';
// Load queue from storage
this.loadQueue();
// Process queue when online
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', () => this.processQueue());
}
}
// Add response to queue
enqueue(notificationId, actionId, responseData) {
const item = {
id: Date.now().toString(),
notificationId,
actionId,
responseData,
timestamp: Date.now(),
attempts: 0
};
this.queue.push(item);
this.saveQueue();
// Try to process immediately if online
if (navigator.onLine) {
this.processQueue();
}
return item.id;
}
// Process all queued responses
async processQueue() {
if (!navigator.onLine || this.queue.length === 0) {
return;
}
console.log(`Processing ${this.queue.length} queued responses`);
const currentQueue = [...this.queue];
// Process each item
for (const item of currentQueue) {
try {
// Check if notification is still valid
const status = await this.atpClient.getNotificationStatus(item.notificationId);
if (status.status === 'expired') {
console.warn(`Notification ${item.notificationId} has expired. Removing from queue.`);
this.removeFromQueue(item.id);
continue;
}
if (status.status === 'responded') {
console.warn(`Notification ${item.notificationId} already has a response. Removing from queue.`);
this.removeFromQueue(item.id);
continue;
}
// Submit the response
await this.atpClient.respondToNotification(
item.notificationId,
item.actionId,
item.responseData
);
console.log(`Successfully processed queued response: ${item.id}`);
this.removeFromQueue(item.id);
} catch (error) {
console.error(`Failed to process queued response ${item.id}:`, error);
// Update attempt count
this.updateAttemptCount(item.id);
// Remove if too many attempts or too old
const updatedItem = this.queue.find(i => i.id === item.id);
if (!updatedItem) continue;
const ageInHours = (Date.now() - updatedItem.timestamp) / (1000 * 60 * 60);
if (updatedItem.attempts >= 5 || ageInHours > 24) {
console.warn(`Removing failed response from queue after ${updatedItem.attempts} attempts`);
this.removeFromQueue(item.id);
}
}
}
this.saveQueue();
}
// Remove item from queue
removeFromQueue(id) {
this.queue = this.queue.filter(item => item.id !== id);
this.saveQueue();
}
// Update attempt count
updateAttemptCount(id) {
this.queue = this.queue.map(item => {
if (item.id === id) {
return { ...item, attempts: item.attempts + 1 };
}
return item;
});
this.saveQueue();
}
// Save queue to storage
saveQueue() {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
}
}
// Load queue from storage
loadQueue() {
if (typeof localStorage !== 'undefined') {
try {
const storedQueue = localStorage.getItem(this.storageKey);
if (storedQueue) {
this.queue = JSON.parse(storedQueue);
console.log(`Loaded ${this.queue.length} responses from queue`);
}
} catch (error) {
console.error('Failed to load response queue:', error);
this.queue = [];
}
}
}
}
Webhook Processing Errors
- Symptoms
- Causes
- Solutions
- Webhook endpoint returning errors
- Duplicate webhook deliveries
- Missing webhook responses
- Webhook signature verification issues
- Server errors when processing webhooks
- Non-idempotent webhook handling
Copy
// Implement idempotent webhook processing
function processWebhookIdempotently(payload) {
const { notification_id, action_id, response_id, response_data } = payload;
// Create a unique key for this webhook delivery
const webhookKey = `webhook:${response_id}`;
// Check if we've already processed this webhook
if (hasProcessedWebhook(webhookKey)) {
console.log(`Webhook ${response_id} already processed. Ignoring duplicate.`);
return { success: true, duplicate: true };
}
try {
// Process the webhook
// ...your business logic here...
// Mark webhook as processed
markWebhookAsProcessed(webhookKey);
return { success: true };
} catch (error) {
console.error('Error processing webhook:', error);
// Don't mark as processed if there was an error
// This allows retry attempts
return { success: false, error: error.message };
}
}
// Use a cache or database to track processed webhooks
function hasProcessedWebhook(key) {
// Using Redis example
return redisClient.exists(key);
}
function markWebhookAsProcessed(key) {
// Set with a TTL of 24 hours
redisClient.set(key, '1', 'EX', 86400);
}
// Express middleware for handling webhook errors
function webhookErrorHandler(err, req, res, next) {
console.error('Webhook error:', err);
// Log request details for debugging
console.error('Webhook request details:');
console.error('Headers:', req.headers);
console.error('Body:', req.body);
// Return appropriate status based on error type
if (err.name === 'SignatureVerificationError') {
return res.status(401).json({ error: 'Invalid signature' });
}
if (err.name === 'WebhookValidationError') {
return res.status(400).json({ error: 'Invalid webhook payload' });
}
// For server errors, return 500 but acknowledge receipt
// This prevents the ATP server from retrying
res.status(500).json({
error: 'Webhook processing error',
acknowledged: true
});
// Log to error monitoring service
if (process.env.NODE_ENV === 'production') {
errorMonitoringService.captureException(err, {
tags: { type: 'webhook' },
extra: {
headers: req.headers,
body: req.body
}
});
}
}
Mobile-Specific Issues
Push Notification Issues
- Symptoms
- Causes
- Solutions
- Push notifications not arriving on device
- Push notifications arriving but not displaying
- “Missing notification data” errors
- Incorrect FCM/APNS configuration
- Missing notification permissions
- Expired push tokens
- Background restrictions
Copy
// iOS Push Notification Troubleshooting
// Check notification permissions
func checkNotificationPermissions() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .authorized:
print("Notification permissions granted")
self.registerForPushNotifications()
case .denied:
print("ERROR: Notification permissions denied")
// Show alert to direct user to settings
DispatchQueue.main.async {
self.showNotificationPermissionAlert()
}
case .notDetermined:
print("Notification permissions not determined")
self.requestNotificationPermissions()
case .provisional:
print("Notification permissions granted provisionally")
self.registerForPushNotifications()
default:
print("Unknown permission status")
}
}
}
// Request permissions
func requestNotificationPermissions() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("ERROR: Failed to request notification permissions: \(error)")
return
}
if granted {
print("Notification permissions granted")
DispatchQueue.main.async {
self.registerForPushNotifications()
}
} else {
print("ERROR: Notification permissions denied")
}
}
}
// Register for push notifications
func registerForPushNotifications() {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
// Handle token registration
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Push token: \(tokenString)")
// Register with ATP
ATPClient.shared.registerPushToken(token: tokenString, type: "apns") { success, error in
if success {
print("Push token registered with ATP successfully")
} else if let error = error {
print("ERROR: Failed to register push token: \(error)")
}
}
}
// Handle registration failure
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("ERROR: Failed to register for remote notifications: \(error)")
// Check for common errors
if let nsError = error as NSError? {
if nsError.code == 3010 {
print("Push notifications are not available in the simulator")
} else {
print("Unknown error: \(nsError.code)")
}
}
}
// Show alert to direct user to settings
func showNotificationPermissionAlert() {
let alert = UIAlertController(
title: "Notifications Disabled",
message: "Enable notifications in Settings to receive important updates.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
self.present(alert, animated: true)
}
Copy
// Android Push Notification Troubleshooting
class PushNotificationManager(private val context: Context) {
private val TAG = "PushManager"
// Check if FCM is available and properly configured
fun checkFcmAvailability() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
Log.d(TAG, "FCM token: $token")
registerTokenWithAtp(token)
} else {
Log.e(TAG, "Failed to get FCM token", task.exception)
// Check for common errors
val exception = task.exception
when {
exception is FirebaseMessagingException && exception.errorCode == "MISSING_INSTANCEID_SERVICE" ->
Log.e(TAG, "Firebase not properly initialized")
exception is IOException ->
Log.e(TAG, "Network error, check connectivity")
else ->
Log.e(TAG, "Unknown error: ${exception?.message}")
}
}
}
}
// Register token with ATP
private fun registerTokenWithAtp(token: String) {
val atpClient = (context.applicationContext as MyApplication).atpClient
atpClient.registerPushToken(token, "fcm")
.addOnSuccessListener {
Log.d(TAG, "Token registered with ATP successfully")
}
.addOnFailureListener { e ->
Log.e(TAG, "Failed to register token with ATP", e)
}
}
// Check notification permissions (Android 13+)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun checkNotificationPermissions() {
val hasPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
if (hasPermission) {
Log.d(TAG, "Notification permission granted")
} else {
Log.e(TAG, "Notification permission not granted")
requestNotificationPermission()
}
}
// Request notification permission (Android 13+)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun requestNotificationPermission() {
if (context is Activity) {
ActivityCompat.requestPermissions(
context,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
} else {
Log.e(TAG, "Context is not an Activity, cannot request permissions")
}
}
// Check if notifications are enabled in app settings
fun areNotificationsEnabled(): Boolean {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Check channel settings for Android 8+
val channel = notificationManager.getNotificationChannel(CHANNEL_ID)
channel?.importance != NotificationManager.IMPORTANCE_NONE
} else {
// Check app notification settings for older Android versions
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
// Create or update notification channel (Android 8+)
@RequiresApi(Build.VERSION_CODES.O)
fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"ATP Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications from AI services"
enableLights(true)
lightColor = Color.BLUE
enableVibration(true)
}
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created/updated")
}
// Display a test notification
fun sendTestNotification() {
val notificationManager = NotificationManagerCompat.from(context)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Test Notification")
.setContentText("This is a test notification to verify push notification setup")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
try {
notificationManager.notify(1, builder.build())
Log.d(TAG, "Test notification sent")
} catch (e: SecurityException) {
Log.e(TAG, "Failed to send test notification: ${e.message}")
}
}
companion object {
private const val CHANNEL_ID = "atp_notifications"
private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 123
}
}
Background Processing Limitations
- Symptoms
- Causes
- Solutions
- Notifications not processed when app is in background
- Delayed notifications
- Missing webhook responses
- Mobile OS background restrictions
- Battery optimization settings
- Background task limitations
Copy
// Android WorkManager for reliable background processing
class NotificationProcessor : Worker(appContext, workerParams) {
override fun doWork(): Result {
val notificationId = inputData.getString("notification_id") ?: return Result.failure()
return try {
val atpClient = (applicationContext as MyApplication).atpClient
// Fetch notification details
val notification = atpClient.getNotificationSync(notificationId)
// Process the notification
processNotification(notification)
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to process notification", e)
// Retry with backoff for transient errors
if (isTransientError(e)) {
Result.retry()
} else {
Result.failure()
}
}
}
private fun isTransientError(e: Exception): Boolean {
return e is IOException || e is TimeoutException
}
private fun processNotification(notification: ATPNotification) {
// Create a system notification
val notificationManager = NotificationManagerCompat.from(applicationContext)
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(notification.title)
.setContentText(notification.description)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
// Add action buttons if appropriate
addActionButtons(builder, notification)
// Create a PendingIntent to open the notification details
val intent = Intent(applicationContext, NotificationActivity::class.java).apply {
putExtra("notification_id", notification.id)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
builder.setContentIntent(pendingIntent)
// Show the notification
notificationManager.notify(notification.id.hashCode(), builder.build())
}
private fun addActionButtons(builder: NotificationCompat.Builder, notification: ATPNotification) {
// Only add buttons for simple actions
notification.actions
.filter { it.responseType == "simple" }
.take(3) // Android limits to 3 action buttons
.forEach { action ->
val actionIntent = Intent(applicationContext, NotificationActionReceiver::class.java).apply {
action = ACTION_RESPOND
putExtra("notification_id", notification.id)
putExtra("action_id", action.id)
}
val pendingIntent = PendingIntent.getBroadcast(
applicationContext,
action.id.hashCode(),
actionIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
builder.addAction(0, action.label, pendingIntent)
}
}
companion object {
private const val TAG = "NotificationProcessor"
private const val CHANNEL_ID = "atp_notifications"
private const val ACTION_RESPOND = "com.example.app.ACTION_RESPOND"
fun enqueue(context: Context, notificationId: String) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val inputData = workDataOf("notification_id" to notificationId)
val request = OneTimeWorkRequestBuilder<NotificationProcessor>()
.setConstraints(constraints)
.setInputData(inputData)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(
"notification_$notificationId",
ExistingWorkPolicy.REPLACE,
request
)
}
}
}
// Receiver for notification actions
class NotificationActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getStringExtra("notification_id") ?: return
val actionId = intent.getStringExtra("action_id") ?: return
// Process response in background
ResponseProcessor.enqueue(context, notificationId, actionId, null)
// Cancel the notification
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.cancel(notificationId.hashCode())
}
}
// Worker for processing responses
class ResponseProcessor : Worker(appContext, workerParams) {
override fun doWork(): Result {
val notificationId = inputData.getString("notification_id") ?: return Result.failure()
val actionId = inputData.getString("action_id") ?: return Result.failure()
val responseData = inputData.getString("response_data")
return try {
val atpClient = (applicationContext as MyApplication).atpClient
// Submit the response
atpClient.respondToNotificationSync(notificationId, actionId, responseData)
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to submit response", e)
// Retry with backoff for transient errors
if (isTransientError(e)) {
Result.retry()
} else {
Result.failure()
}
}
}
private fun isTransientError(e: Exception): Boolean {
return e is IOException || e is TimeoutException
}
companion object {
private const val TAG = "ResponseProcessor"
fun enqueue(context: Context, notificationId: String, actionId: String, responseData: String?) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val inputDataBuilder = Data.Builder()
.putString("notification_id", notificationId)
.putString("action_id", actionId)
if (responseData != null) {
inputDataBuilder.putString("response_data", responseData)
}
val request = OneTimeWorkRequestBuilder<ResponseProcessor>()
.setConstraints(constraints)
.setInputData(inputDataBuilder.build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(
"response_${notificationId}_$actionId",
ExistingWorkPolicy.REPLACE,
request
)
}
}
}
Copy
// iOS Background Processing
class BackgroundTaskManager {
static let shared = BackgroundTaskManager()
private let atpClient = ATPClient.shared
private let queue = OperationQueue()
// Register background tasks
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.notification-processing",
using: nil
) { task in
self.handleNotificationProcessingTask(task: task as! BGProcessingTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.response-submission",
using: nil
) { task in
self.handleResponseSubmissionTask(task: task as! BGProcessingTask)
}
}
// Schedule background task for notification processing
func scheduleNotificationProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.example.app.notification-processing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
do {
try BGTaskScheduler.shared.submit(request)
print("Scheduled notification processing task")
} catch {
print("Failed to schedule notification processing: \(error)")
}
}
// Schedule background task for response submission
func scheduleResponseSubmission() {
let request = BGProcessingTaskRequest(identifier: "com.example.app.response-submission")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
do {
try BGTaskScheduler.shared.submit(request)
print("Scheduled response submission task")
} catch {
print("Failed to schedule response submission: \(error)")
}
}
// Handle notification processing task
private func handleNotificationProcessingTask(task: BGProcessingTask) {
// Schedule a new task for next time
scheduleNotificationProcessing()
// Create a task expiration handler
task.expirationHandler = {
self.queue.cancelAllOperations()
}
// Create an operation to process pending notifications
let operation = BlockOperation {
// Process any pending notifications
let pendingNotifications = self.getPendingNotifications()
for notification in pendingNotifications {
self.processNotification(notification)
}
}
// Set completion handler
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}
// Add operation to queue
queue.addOperation(operation)
}
// Handle response submission task
private func handleResponseSubmissionTask(task: BGProcessingTask) {
// Schedule a new task for next time
scheduleResponseSubmission()
// Create a task expiration handler
task.expirationHandler = {
self.queue.cancelAllOperations()
}
// Create an operation to submit pending responses
let operation = BlockOperation {
// Process any pending responses
let pendingResponses = self.getPendingResponses()
for response in pendingResponses {
self.submitResponse(
notificationId: response.notificationId,
actionId: response.actionId,
responseData: response.responseData
)
}
}
// Set completion handler
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}
// Add operation to queue
queue.addOperation(operation)
}
// Process notification
private func processNotification(_ notification: PendingNotification) {
// Your notification processing logic
}
// Submit response
private func submitResponse(notificationId: String, actionId: String, responseData: Any?) {
// Your response submission logic
}
// Get pending notifications from storage
private func getPendingNotifications() -> [PendingNotification] {
// Your logic to retrieve pending notifications
return []
}
// Get pending responses from storage
private func getPendingResponses() -> [PendingResponse] {
// Your logic to retrieve pending responses
return []
}
}
// Pending notification model
struct PendingNotification {
let id: String
let receivedAt: Date
}
// Pending response model
struct PendingResponse {
let notificationId: String
let actionId: String
let responseData: Any?
let createdAt: Date
let attempts: Int
}
Verification: Knowledge Check
Before proceeding, let’s verify your understanding of ATP troubleshooting:Next Steps
Now that you know how to troubleshoot common ATP issues, explore these additional resources:Best Practices
Learn recommended patterns and practices for robust ATP implementations
API Reference
Detailed API documentation for ATP implementation