Initial commit
This commit is contained in:
519
skills/sleeptrack-be/SKILL.md
Normal file
519
skills/sleeptrack-be/SKILL.md
Normal file
@@ -0,0 +1,519 @@
|
||||
---
|
||||
name: sleeptrack-be
|
||||
description: This skill provides comprehensive backend REST API integration for Asleep sleep tracking platform. Use this skill when building server-side applications, API proxies for mobile apps, webhook event handlers, cross-platform backends (React Native, Flutter), analytics dashboards, or multi-tenant sleep tracking systems. Covers authentication, user management, session retrieval, statistics, webhook integration, and production-ready patterns with code examples in Python, Node.js, and curl.
|
||||
---
|
||||
|
||||
# Sleeptrack Backend API Integration
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides comprehensive guidance for integrating the Asleep REST API into backend applications. It covers server-side user management, session data retrieval, statistics aggregation, webhook event handling, and production-ready patterns for building robust sleep tracking backends.
|
||||
|
||||
**Use this skill when:**
|
||||
- Building backend/server-side sleep tracking integrations
|
||||
- Creating API proxies for mobile applications
|
||||
- Implementing webhook handlers for real-time sleep data
|
||||
- Developing cross-platform backends (React Native, Flutter)
|
||||
- Building analytics dashboards and reporting systems
|
||||
- Creating multi-tenant sleep tracking applications
|
||||
- Integrating sleep data with other health platforms
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Get Your API Key
|
||||
|
||||
1. Sign up at https://dashboard.asleep.ai
|
||||
2. Generate an API key for your application
|
||||
3. Store securely in environment variables (never commit to version control)
|
||||
|
||||
### 2. Basic Authentication
|
||||
|
||||
All API requests require the `x-api-key` header:
|
||||
|
||||
**curl:**
|
||||
```bash
|
||||
curl -X GET "https://api.asleep.ai/ai/v1/users/USER_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
import requests
|
||||
|
||||
headers = {"x-api-key": "YOUR_API_KEY"}
|
||||
response = requests.get(
|
||||
"https://api.asleep.ai/ai/v1/users/USER_ID",
|
||||
headers=headers
|
||||
)
|
||||
```
|
||||
|
||||
**Node.js:**
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
|
||||
const response = await axios.get(
|
||||
'https://api.asleep.ai/ai/v1/users/USER_ID',
|
||||
{
|
||||
headers: { 'x-api-key': 'YOUR_API_KEY' }
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## API Client Structure
|
||||
|
||||
Build a reusable API client to handle authentication, error handling, and common operations.
|
||||
|
||||
**Key Components:**
|
||||
- Base URL configuration (`https://api.asleep.ai`)
|
||||
- API key authentication in headers
|
||||
- Error handling for common HTTP status codes (401, 403, 404)
|
||||
- Request methods for all API endpoints
|
||||
- Session management with persistent connections
|
||||
|
||||
**For complete implementations:**
|
||||
- Python: See `references/python_client_implementation.md`
|
||||
- Node.js: See `references/nodejs_client_implementation.md`
|
||||
- REST API details: See `references/rest_api_reference.md`
|
||||
|
||||
**Basic Client Structure:**
|
||||
```python
|
||||
class AsleepClient:
|
||||
def __init__(self, api_key: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.asleep.ai"
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"x-api-key": api_key})
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs):
|
||||
# Handle authentication and errors
|
||||
# See python_client_implementation.md for full code
|
||||
pass
|
||||
```
|
||||
|
||||
## User Management
|
||||
|
||||
### Creating Users
|
||||
|
||||
Create users before tracking sleep. User IDs are managed by your application.
|
||||
|
||||
```python
|
||||
# Create user with metadata
|
||||
user_id = client.create_user(metadata={
|
||||
"birth_year": 1990,
|
||||
"gender": "male",
|
||||
"height": 175.5, # cm
|
||||
"weight": 70.0 # kg
|
||||
})
|
||||
```
|
||||
|
||||
**Available Metadata Fields:**
|
||||
- `birth_year` (Integer): Birth year
|
||||
- `birth_month` (Integer): 1-12
|
||||
- `birth_day` (Integer): 1-31
|
||||
- `gender` (String): `male`, `female`, `non_binary`, `other`, `prefer_not_to_say`
|
||||
- `height` (Float): Height in cm (0-300)
|
||||
- `weight` (Float): Weight in kg (0-1000)
|
||||
|
||||
### Retrieving User Information
|
||||
|
||||
```python
|
||||
user_data = client.get_user(user_id)
|
||||
# Returns: user_id, to_be_deleted status, last_session_info, metadata
|
||||
```
|
||||
|
||||
**Response includes:**
|
||||
- User ID and deletion status
|
||||
- Last session information (if available)
|
||||
- User metadata (demographic information)
|
||||
|
||||
### Deleting Users
|
||||
|
||||
Permanently removes user and all associated data (sessions, reports).
|
||||
|
||||
```python
|
||||
client.delete_user(user_id)
|
||||
```
|
||||
|
||||
**For detailed examples and response structures:**
|
||||
- See `references/rest_api_reference.md` (User Management section)
|
||||
- See `references/python_client_implementation.md` or `references/nodejs_client_implementation.md`
|
||||
|
||||
## Session Management
|
||||
|
||||
### Retrieving Session Details
|
||||
|
||||
Get comprehensive sleep analysis for a specific session.
|
||||
|
||||
```python
|
||||
session = client.get_session(
|
||||
session_id="session123",
|
||||
user_id="user123",
|
||||
timezone="America/New_York"
|
||||
)
|
||||
|
||||
# Access key metrics
|
||||
print(f"Sleep efficiency: {session['stat']['sleep_efficiency']:.1f}%")
|
||||
print(f"Total sleep time: {session['stat']['sleep_time']}")
|
||||
print(f"Sleep stages: {session['session']['sleep_stages']}")
|
||||
```
|
||||
|
||||
**Sleep Stage Values:**
|
||||
- `-1`: Unknown/No data
|
||||
- `0`: Wake
|
||||
- `1`: Light sleep
|
||||
- `2`: Deep sleep
|
||||
- `3`: REM sleep
|
||||
|
||||
**Key Metrics:**
|
||||
- `sleep_efficiency`: (Total sleep time / Time in bed) × 100%
|
||||
- `sleep_latency`: Time to fall asleep (seconds)
|
||||
- `waso_count`: Wake after sleep onset episodes
|
||||
- `time_in_bed`: Total time in bed (seconds)
|
||||
- `time_in_sleep`: Actual sleep time (seconds)
|
||||
- `time_in_light/deep/rem`: Stage durations (seconds)
|
||||
|
||||
### Listing Sessions
|
||||
|
||||
Retrieve multiple sessions with date filtering and pagination.
|
||||
|
||||
```python
|
||||
sessions = client.list_sessions(
|
||||
user_id="user123",
|
||||
date_gte="2024-01-01",
|
||||
date_lte="2024-01-31",
|
||||
limit=50,
|
||||
order_by="DESC"
|
||||
)
|
||||
```
|
||||
|
||||
**Pagination Example:**
|
||||
```python
|
||||
all_sessions = []
|
||||
offset = 0
|
||||
limit = 100
|
||||
|
||||
while True:
|
||||
result = client.list_sessions(
|
||||
user_id="user123",
|
||||
offset=offset,
|
||||
limit=limit
|
||||
)
|
||||
sessions = result['sleep_session_list']
|
||||
all_sessions.extend(sessions)
|
||||
|
||||
if len(sessions) < limit:
|
||||
break
|
||||
offset += limit
|
||||
```
|
||||
|
||||
### Deleting Sessions
|
||||
|
||||
```python
|
||||
client.delete_session(session_id="session123", user_id="user123")
|
||||
```
|
||||
|
||||
**For detailed session data structures and examples:**
|
||||
- See `references/rest_api_reference.md` (Session Management section)
|
||||
- See client implementation references for language-specific examples
|
||||
|
||||
## Statistics and Analytics
|
||||
|
||||
### Average Statistics
|
||||
|
||||
Get aggregated sleep metrics over a time period (max 100 days).
|
||||
|
||||
```python
|
||||
stats = client.get_average_stats(
|
||||
user_id="user123",
|
||||
start_date="2024-01-01",
|
||||
end_date="2024-01-31",
|
||||
timezone="UTC"
|
||||
)
|
||||
|
||||
avg = stats['average_stats']
|
||||
print(f"Average sleep time: {avg['sleep_time']}")
|
||||
print(f"Average efficiency: {avg['sleep_efficiency']:.1f}%")
|
||||
print(f"Light sleep ratio: {avg['light_ratio']:.1%}")
|
||||
print(f"Number of sessions: {len(stats['slept_sessions'])}")
|
||||
```
|
||||
|
||||
**Returned Metrics:**
|
||||
- Average sleep time, bedtime, wake time
|
||||
- Average sleep efficiency
|
||||
- Sleep stage ratios (light, deep, REM)
|
||||
- List of sessions included in calculation
|
||||
|
||||
**For trend analysis and advanced analytics:**
|
||||
- See `references/python_client_implementation.md` (Analytics section)
|
||||
- See `references/rest_api_reference.md` (Statistics section)
|
||||
|
||||
## Webhook Integration
|
||||
|
||||
Webhooks enable real-time notifications for sleep session events.
|
||||
|
||||
### Webhook Event Types
|
||||
|
||||
1. **INFERENCE_COMPLETE**: Incremental sleep data during tracking (every 5-40 minutes)
|
||||
2. **SESSION_COMPLETE**: Final comprehensive sleep analysis when session ends
|
||||
|
||||
### Setting Up Webhook Endpoint
|
||||
|
||||
**Basic Structure:**
|
||||
```python
|
||||
@app.route('/asleep-webhook', methods=['POST'])
|
||||
def asleep_webhook():
|
||||
# 1. Verify authentication (x-api-key header)
|
||||
# 2. Parse event data
|
||||
# 3. Handle event type (INFERENCE_COMPLETE or SESSION_COMPLETE)
|
||||
# 4. Process and store data
|
||||
# 5. Return 200 response
|
||||
pass
|
||||
```
|
||||
|
||||
**Key Implementation Points:**
|
||||
- Verify `x-api-key` header matches your API key
|
||||
- Validate `x-user-id` header
|
||||
- Handle both INFERENCE_COMPLETE and SESSION_COMPLETE events
|
||||
- Implement idempotency (check if event already processed)
|
||||
- Process asynchronously for better performance
|
||||
- Return 200 status immediately
|
||||
|
||||
**For complete webhook implementations:**
|
||||
- Python (Flask): See `references/webhook_implementation_guide.md`
|
||||
- Node.js (Express): See `references/webhook_implementation_guide.md`
|
||||
- Webhook payloads: See `references/webhook_reference.md`
|
||||
|
||||
### Webhook Best Practices
|
||||
|
||||
**Idempotency:**
|
||||
Check if webhook event was already processed to avoid duplicates.
|
||||
|
||||
**Asynchronous Processing:**
|
||||
Queue webhook events for background processing and respond immediately.
|
||||
|
||||
**Error Handling:**
|
||||
Return 200 even if processing fails internally to prevent retries.
|
||||
|
||||
**For detailed patterns:**
|
||||
- See `references/webhook_implementation_guide.md`
|
||||
- See `references/production_patterns.md` (Background Jobs section)
|
||||
|
||||
## Common Backend Patterns
|
||||
|
||||
### 1. API Proxy for Mobile Apps
|
||||
|
||||
Create a backend proxy to:
|
||||
- Hide API keys from mobile clients
|
||||
- Add custom authentication
|
||||
- Implement business logic
|
||||
- Track usage and analytics
|
||||
|
||||
**Key endpoints:**
|
||||
- POST `/api/users` - Create Asleep user for authenticated app user
|
||||
- GET `/api/sessions/{id}` - Proxy session retrieval with auth
|
||||
- GET `/api/sessions` - List sessions with filtering
|
||||
- GET `/api/statistics` - Get aggregated statistics
|
||||
|
||||
**For complete implementation:**
|
||||
- See `references/python_client_implementation.md` (API Proxy section)
|
||||
|
||||
### 2. Analytics Dashboard Backend
|
||||
|
||||
Aggregate and analyze sleep data across multiple users:
|
||||
- Calculate comprehensive sleep scores
|
||||
- Generate weekly/monthly reports
|
||||
- Analyze cohort sleep patterns
|
||||
- Provide personalized insights
|
||||
|
||||
**Key features:**
|
||||
- Sleep score calculation (efficiency + consistency + duration)
|
||||
- Trend analysis over time
|
||||
- Multi-user aggregation
|
||||
- Report generation
|
||||
|
||||
**For complete implementation:**
|
||||
- See `references/python_client_implementation.md` (Analytics section)
|
||||
|
||||
### 3. Multi-Tenant Application
|
||||
|
||||
Manage sleep tracking for multiple organizations or teams:
|
||||
- Organization-level user management
|
||||
- Aggregated organization statistics
|
||||
- Role-based access control
|
||||
- Per-organization settings
|
||||
|
||||
**For complete implementation:**
|
||||
- See `references/python_client_implementation.md` (Multi-Tenant section)
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
- **401 Unauthorized**: Invalid API key
|
||||
- **403 Forbidden**: Rate limit exceeded or insufficient permissions
|
||||
- **404 Not Found**: Resource does not exist
|
||||
- **422 Unprocessable Entity**: Invalid request parameters
|
||||
|
||||
### Retry Logic with Exponential Backoff
|
||||
|
||||
```python
|
||||
def retry_with_exponential_backoff(func, max_retries=3, base_delay=1.0):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 403 and "rate limit" in str(e):
|
||||
if attempt < max_retries - 1:
|
||||
delay = min(base_delay * (2 ** attempt), 60.0)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
raise
|
||||
```
|
||||
|
||||
### Custom Exception Classes
|
||||
|
||||
```python
|
||||
class AsleepAPIError(Exception):
|
||||
"""Base exception for Asleep API errors"""
|
||||
pass
|
||||
|
||||
class RateLimitError(AsleepAPIError):
|
||||
"""Rate limit exceeded"""
|
||||
pass
|
||||
|
||||
class ResourceNotFoundError(AsleepAPIError):
|
||||
"""Resource not found"""
|
||||
pass
|
||||
```
|
||||
|
||||
**For comprehensive error handling:**
|
||||
- See `references/python_client_implementation.md` or `references/nodejs_client_implementation.md`
|
||||
- See `references/production_patterns.md` (Error Recovery section)
|
||||
|
||||
## Testing
|
||||
|
||||
### Local Webhook Testing
|
||||
|
||||
Use ngrok to expose local server for webhook testing:
|
||||
|
||||
```bash
|
||||
# Start local server
|
||||
python app.py # or npm start
|
||||
|
||||
# Expose with ngrok
|
||||
ngrok http 5000
|
||||
|
||||
# Use ngrok URL in webhook configuration
|
||||
# Example: https://abc123.ngrok.io/asleep-webhook
|
||||
```
|
||||
|
||||
### Mock API Responses
|
||||
|
||||
```python
|
||||
@patch('requests.Session.request')
|
||||
def test_create_user(mock_request):
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
"result": {"user_id": "test_user_123"}
|
||||
}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
user_id = client.create_user()
|
||||
assert user_id == "test_user_123"
|
||||
```
|
||||
|
||||
**For complete testing examples:**
|
||||
- See `references/python_client_implementation.md` (Testing section)
|
||||
|
||||
## Production Best Practices
|
||||
|
||||
### Security
|
||||
|
||||
1. **API Key Management:**
|
||||
- Store in environment variables or secret management system
|
||||
- Never commit to version control
|
||||
- Rotate keys periodically
|
||||
- Use different keys for dev/staging/production
|
||||
|
||||
2. **Webhook Security:**
|
||||
- Verify `x-api-key` header
|
||||
- Use HTTPS endpoints only
|
||||
- Implement rate limiting
|
||||
- Log all webhook attempts
|
||||
|
||||
3. **User Data Privacy:**
|
||||
- Encrypt sensitive data at rest
|
||||
- Implement proper access controls
|
||||
- Handle data deletion requests
|
||||
- Comply with GDPR/CCPA
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Caching:** Cache immutable session data
|
||||
2. **Rate Limiting:** Protect your backend from overload
|
||||
3. **Connection Pooling:** Reuse HTTP connections
|
||||
4. **Batch Processing:** Process multiple requests in parallel
|
||||
|
||||
### Monitoring
|
||||
|
||||
1. **Logging:** Structured logging for all API requests
|
||||
2. **Metrics:** Track request duration, error rates, throughput
|
||||
3. **Health Checks:** Implement `/health`, `/ready`, `/live` endpoints
|
||||
4. **Alerting:** Alert on error rate spikes or API failures
|
||||
|
||||
### Deployment
|
||||
|
||||
1. **Configuration:** Environment-based settings with validation
|
||||
2. **Health Checks:** Support Kubernetes liveness/readiness probes
|
||||
3. **Graceful Shutdown:** Handle termination signals properly
|
||||
4. **Error Recovery:** Circuit breaker pattern for API failures
|
||||
|
||||
**For comprehensive production patterns:**
|
||||
- Caching strategies: See `references/production_patterns.md`
|
||||
- Rate limiting: See `references/production_patterns.md`
|
||||
- Monitoring: See `references/production_patterns.md`
|
||||
- Deployment: See `references/production_patterns.md`
|
||||
|
||||
## Resources
|
||||
|
||||
### Reference Documentation
|
||||
|
||||
This skill includes comprehensive reference files:
|
||||
|
||||
- `references/python_client_implementation.md`: Complete Python client with all methods, analytics classes, and examples
|
||||
- `references/nodejs_client_implementation.md`: Complete Node.js client with Express integration
|
||||
- `references/webhook_implementation_guide.md`: Full webhook handlers in Python and Node.js with best practices
|
||||
- `references/rest_api_reference.md`: Complete REST API endpoint documentation with request/response examples
|
||||
- `references/webhook_reference.md`: Webhook integration guide with payload structures
|
||||
- `references/production_patterns.md`: Caching, rate limiting, monitoring, deployment, and performance optimization
|
||||
|
||||
To access detailed information:
|
||||
```
|
||||
Read references/python_client_implementation.md
|
||||
Read references/nodejs_client_implementation.md
|
||||
Read references/webhook_implementation_guide.md
|
||||
Read references/rest_api_reference.md
|
||||
Read references/webhook_reference.md
|
||||
Read references/production_patterns.md
|
||||
```
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- **Main Documentation**: https://docs-en.asleep.ai
|
||||
- **API Basics**: https://docs-en.asleep.ai/docs/api-basics.md
|
||||
- **Webhook Guide**: https://docs-en.asleep.ai/docs/webhook.md
|
||||
- **Dashboard**: https://dashboard.asleep.ai
|
||||
- **LLM-Optimized Reference**: https://docs-en.asleep.ai/llms.txt
|
||||
|
||||
### Related Skills
|
||||
|
||||
- **sleeptrack-foundation**: Core concepts, authentication, data structures, and platform-agnostic patterns
|
||||
- **sleeptrack-ios**: iOS SDK integration for native iOS applications
|
||||
- **sleeptrack-android**: Android SDK integration for native Android applications
|
||||
|
||||
### Support
|
||||
|
||||
For technical support and API issues:
|
||||
- Check Dashboard for API usage and status
|
||||
- Review error logs and response codes
|
||||
- Contact support through Asleep Dashboard
|
||||
467
skills/sleeptrack-be/references/nodejs_client_implementation.md
Normal file
467
skills/sleeptrack-be/references/nodejs_client_implementation.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# Node.js Client Implementation Guide
|
||||
|
||||
This reference provides complete Node.js client implementations for the Asleep API, including webhook servers and production patterns.
|
||||
|
||||
## Complete Node.js API Client
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
|
||||
class AsleepClient {
|
||||
constructor(apiKey, baseURL = 'https://api.asleep.ai') {
|
||||
this.apiKey = apiKey;
|
||||
this.baseURL = baseURL;
|
||||
this.client = axios.create({
|
||||
baseURL: baseURL,
|
||||
headers: { 'x-api-key': apiKey }
|
||||
});
|
||||
}
|
||||
|
||||
async _request(method, path, options = {}) {
|
||||
try {
|
||||
const response = await this.client.request({
|
||||
method,
|
||||
url: path,
|
||||
...options
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const detail = error.response.data?.detail || 'Unknown error';
|
||||
|
||||
if (status === 401) {
|
||||
throw new Error('Invalid API key');
|
||||
} else if (status === 403) {
|
||||
throw new Error(`API access error: ${detail}`);
|
||||
} else if (status === 404) {
|
||||
throw new Error('Resource not found');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// User management
|
||||
async createUser(metadata = null) {
|
||||
const data = metadata ? { metadata } : {};
|
||||
const result = await this._request('POST', '/ai/v1/users', { data });
|
||||
return result.result.user_id;
|
||||
}
|
||||
|
||||
async getUser(userId) {
|
||||
const result = await this._request('GET', `/ai/v1/users/${userId}`);
|
||||
return result.result;
|
||||
}
|
||||
|
||||
async deleteUser(userId) {
|
||||
await this._request('DELETE', `/ai/v1/users/${userId}`);
|
||||
}
|
||||
|
||||
// Session management
|
||||
async getSession(sessionId, userId, timezone = 'UTC') {
|
||||
const result = await this._request('GET', `/data/v3/sessions/${sessionId}`, {
|
||||
headers: { 'x-user-id': userId, 'timezone': timezone }
|
||||
});
|
||||
return result.result;
|
||||
}
|
||||
|
||||
async listSessions(userId, options = {}) {
|
||||
const { dateGte, dateLte, offset = 0, limit = 20, orderBy = 'DESC' } = options;
|
||||
const params = { offset, limit, order_by: orderBy };
|
||||
if (dateGte) params.date_gte = dateGte;
|
||||
if (dateLte) params.date_lte = dateLte;
|
||||
|
||||
const result = await this._request('GET', '/data/v1/sessions', {
|
||||
headers: { 'x-user-id': userId },
|
||||
params
|
||||
});
|
||||
return result.result;
|
||||
}
|
||||
|
||||
async deleteSession(sessionId, userId) {
|
||||
await this._request('DELETE', `/ai/v1/sessions/${sessionId}`, {
|
||||
headers: { 'x-user-id': userId }
|
||||
});
|
||||
}
|
||||
|
||||
// Statistics
|
||||
async getAverageStats(userId, startDate, endDate, timezone = 'UTC') {
|
||||
const result = await this._request('GET', `/data/v1/users/${userId}/average-stats`, {
|
||||
headers: { 'timezone': timezone },
|
||||
params: { start_date: startDate, end_date: endDate }
|
||||
});
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const client = new AsleepClient(process.env.ASLEEP_API_KEY);
|
||||
```
|
||||
|
||||
## Express Webhook Server
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const EXPECTED_API_KEY = process.env.ASLEEP_API_KEY;
|
||||
|
||||
app.post('/asleep-webhook', async (req, res) => {
|
||||
// Verify authentication
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const userId = req.headers['x-user-id'];
|
||||
|
||||
if (apiKey !== EXPECTED_API_KEY) {
|
||||
console.warn('Unauthorized webhook attempt');
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { event, session_id, stat } = req.body;
|
||||
console.log(`Received ${event} event for user ${userId}`);
|
||||
|
||||
try {
|
||||
switch (event) {
|
||||
case 'INFERENCE_COMPLETE':
|
||||
await handleInferenceComplete(req.body);
|
||||
break;
|
||||
case 'SESSION_COMPLETE':
|
||||
await handleSessionComplete(req.body);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown event type: ${event}`);
|
||||
}
|
||||
|
||||
res.status(200).json({ status: 'success' });
|
||||
} catch (error) {
|
||||
console.error('Webhook processing error:', error);
|
||||
res.status(500).json({ error: 'Processing failed' });
|
||||
}
|
||||
});
|
||||
|
||||
async function handleInferenceComplete(event) {
|
||||
const { session_id, user_id, sleep_stages } = event;
|
||||
|
||||
// Update real-time dashboard
|
||||
await updateLiveDashboard(session_id, sleep_stages);
|
||||
|
||||
// Store incremental data
|
||||
await db.collection('incremental_data').insertOne(event);
|
||||
|
||||
console.log(`Processed INFERENCE_COMPLETE for session ${session_id}`);
|
||||
}
|
||||
|
||||
async function handleSessionComplete(event) {
|
||||
const { session_id, user_id, stat, session } = event;
|
||||
|
||||
// Store complete report
|
||||
await db.collection('sleep_reports').insertOne({
|
||||
user_id,
|
||||
session_id,
|
||||
date: session.start_time,
|
||||
statistics: stat,
|
||||
session_data: session,
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
// Send user notification
|
||||
await sendPushNotification(user_id, {
|
||||
title: 'Sleep Report Ready',
|
||||
body: `Sleep time: ${stat.sleep_time}, Efficiency: ${stat.sleep_efficiency.toFixed(1)}%`
|
||||
});
|
||||
|
||||
// Update user statistics
|
||||
await updateUserAggregatedStats(user_id);
|
||||
|
||||
console.log(`Processed SESSION_COMPLETE for session ${session_id}`);
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Webhook server listening on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Retry with Exponential Backoff
|
||||
|
||||
```javascript
|
||||
async function retryWithExponentialBackoff(
|
||||
func,
|
||||
maxRetries = 3,
|
||||
baseDelay = 1000,
|
||||
maxDelay = 60000
|
||||
) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
return await func();
|
||||
} catch (error) {
|
||||
if (error.response?.status === 403) {
|
||||
const detail = error.response.data?.detail || '';
|
||||
if (detail.toLowerCase().includes('rate limit')) {
|
||||
if (attempt < maxRetries - 1) {
|
||||
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
||||
console.log(`Rate limited, retrying in ${delay}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const result = await retryWithExponentialBackoff(
|
||||
() => client.getSession('session123', 'user123')
|
||||
);
|
||||
```
|
||||
|
||||
## Basic Usage Examples
|
||||
|
||||
### Creating Users
|
||||
|
||||
```javascript
|
||||
// Create user with metadata
|
||||
const userId = await client.createUser({
|
||||
birth_year: 1990,
|
||||
gender: 'male',
|
||||
height: 175.5,
|
||||
weight: 70.0
|
||||
});
|
||||
console.log(`Created user: ${userId}`);
|
||||
|
||||
// Create user without metadata
|
||||
const userId = await client.createUser();
|
||||
```
|
||||
|
||||
### Getting Sessions
|
||||
|
||||
```javascript
|
||||
// Get sessions for date range
|
||||
const sessions = await client.listSessions('user123', {
|
||||
dateGte: '2024-01-01',
|
||||
dateLte: '2024-01-31',
|
||||
limit: 50,
|
||||
orderBy: 'DESC'
|
||||
});
|
||||
|
||||
console.log(`Found ${sessions.sleep_session_list.length} sessions`);
|
||||
|
||||
sessions.sleep_session_list.forEach(session => {
|
||||
console.log(`Session ${session.session_id}: ${session.session_start_time}`);
|
||||
console.log(` State: ${session.state}, Time in bed: ${session.time_in_bed}s`);
|
||||
});
|
||||
```
|
||||
|
||||
### Getting Session Details
|
||||
|
||||
```javascript
|
||||
const session = await client.getSession(
|
||||
'session123',
|
||||
'user123',
|
||||
'America/New_York'
|
||||
);
|
||||
|
||||
console.log(`Sleep efficiency: ${session.stat.sleep_efficiency.toFixed(1)}%`);
|
||||
console.log(`Total sleep time: ${session.stat.sleep_time}`);
|
||||
console.log(`Sleep stages: ${session.session.sleep_stages}`);
|
||||
console.log(`Sleep cycles: ${session.stat.sleep_cycle.length}`);
|
||||
```
|
||||
|
||||
### Getting Statistics
|
||||
|
||||
```javascript
|
||||
const stats = await client.getAverageStats(
|
||||
'user123',
|
||||
'2024-01-01',
|
||||
'2024-01-31',
|
||||
'UTC'
|
||||
);
|
||||
|
||||
const avg = stats.average_stats;
|
||||
console.log(`Average sleep time: ${avg.sleep_time}`);
|
||||
console.log(`Average efficiency: ${avg.sleep_efficiency.toFixed(1)}%`);
|
||||
console.log(`Average bedtime: ${avg.start_time}`);
|
||||
console.log(`Average wake time: ${avg.end_time}`);
|
||||
console.log(`Light sleep ratio: ${(avg.light_ratio * 100).toFixed(1)}%`);
|
||||
console.log(`Deep sleep ratio: ${(avg.deep_ratio * 100).toFixed(1)}%`);
|
||||
console.log(`REM sleep ratio: ${(avg.rem_ratio * 100).toFixed(1)}%`);
|
||||
console.log(`Number of sessions: ${stats.slept_sessions.length}`);
|
||||
```
|
||||
|
||||
## Asynchronous Webhook Processing
|
||||
|
||||
```javascript
|
||||
const Queue = require('bull');
|
||||
|
||||
const webhookQueue = new Queue('asleep-webhooks', {
|
||||
redis: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/webhook', async (req, res) => {
|
||||
const event = req.body;
|
||||
|
||||
// Queue for async processing
|
||||
await webhookQueue.add(event);
|
||||
|
||||
// Respond immediately
|
||||
res.status(200).json({ status: 'queued' });
|
||||
});
|
||||
|
||||
// Process queued webhooks
|
||||
webhookQueue.process(async (job) => {
|
||||
const event = job.data;
|
||||
|
||||
if (event.event === 'SESSION_COMPLETE') {
|
||||
await handleSessionComplete(event);
|
||||
} else if (event.event === 'INFERENCE_COMPLETE') {
|
||||
await handleInferenceComplete(event);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Idempotency Pattern
|
||||
|
||||
```javascript
|
||||
async function handleSessionComplete(event) {
|
||||
const sessionId = event.session_id;
|
||||
|
||||
// Check if already processed
|
||||
const existing = await db.collection('processed_webhooks').findOne({
|
||||
session_id: sessionId,
|
||||
event: 'SESSION_COMPLETE'
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(`Session ${sessionId} already processed, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process event
|
||||
await saveSleepReport(event);
|
||||
|
||||
// Mark as processed
|
||||
await db.collection('processed_webhooks').insertOne({
|
||||
session_id: sessionId,
|
||||
event: 'SESSION_COMPLETE',
|
||||
processed_at: new Date()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Comprehensive Error Handling
|
||||
|
||||
```javascript
|
||||
class AsleepAPIError extends Error {
|
||||
constructor(message, statusCode, detail) {
|
||||
super(message);
|
||||
this.name = 'AsleepAPIError';
|
||||
this.statusCode = statusCode;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
class RateLimitError extends AsleepAPIError {
|
||||
constructor(detail) {
|
||||
super('Rate limit exceeded', 403, detail);
|
||||
this.name = 'RateLimitError';
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceNotFoundError extends AsleepAPIError {
|
||||
constructor(detail) {
|
||||
super('Resource not found', 404, detail);
|
||||
this.name = 'ResourceNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
async function safeApiRequest(requestFunc) {
|
||||
try {
|
||||
return await requestFunc();
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const detail = error.response.data?.detail || 'Unknown error';
|
||||
|
||||
if (status === 401) {
|
||||
throw new AsleepAPIError('Authentication failed', 401, detail);
|
||||
} else if (status === 403) {
|
||||
if (detail.toLowerCase().includes('rate limit')) {
|
||||
throw new RateLimitError(detail);
|
||||
} else {
|
||||
throw new AsleepAPIError('Access forbidden', 403, detail);
|
||||
}
|
||||
} else if (status === 404) {
|
||||
throw new ResourceNotFoundError(detail);
|
||||
} else {
|
||||
throw new AsleepAPIError(`API error (${status})`, status, detail);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
try {
|
||||
const user = await safeApiRequest(() => client.getUser('user123'));
|
||||
} catch (error) {
|
||||
if (error instanceof ResourceNotFoundError) {
|
||||
console.log('User not found, creating new user...');
|
||||
const userId = await client.createUser();
|
||||
} else if (error instanceof RateLimitError) {
|
||||
console.log('Rate limited, try again later');
|
||||
} else if (error instanceof AsleepAPIError) {
|
||||
console.error(`API error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Production Configuration
|
||||
|
||||
```javascript
|
||||
// config.js
|
||||
require('dotenv').config();
|
||||
|
||||
class Config {
|
||||
static get ASLEEP_API_KEY() {
|
||||
return process.env.ASLEEP_API_KEY;
|
||||
}
|
||||
|
||||
static get ASLEEP_BASE_URL() {
|
||||
return process.env.ASLEEP_BASE_URL || 'https://api.asleep.ai';
|
||||
}
|
||||
|
||||
static get DATABASE_URL() {
|
||||
return process.env.DATABASE_URL;
|
||||
}
|
||||
|
||||
static get REDIS_URL() {
|
||||
return process.env.REDIS_URL;
|
||||
}
|
||||
|
||||
static get WEBHOOK_SECRET() {
|
||||
return process.env.WEBHOOK_SECRET;
|
||||
}
|
||||
|
||||
static get ENABLE_CACHING() {
|
||||
return process.env.ENABLE_CACHING !== 'false';
|
||||
}
|
||||
|
||||
static validate() {
|
||||
if (!this.ASLEEP_API_KEY) {
|
||||
throw new Error('ASLEEP_API_KEY environment variable required');
|
||||
}
|
||||
if (!this.DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL environment variable required');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Config;
|
||||
```
|
||||
696
skills/sleeptrack-be/references/production_patterns.md
Normal file
696
skills/sleeptrack-be/references/production_patterns.md
Normal file
@@ -0,0 +1,696 @@
|
||||
# Production Patterns and Best Practices
|
||||
|
||||
This reference provides comprehensive production-ready patterns for deploying and maintaining Asleep API integrations in production environments.
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Session Caching
|
||||
|
||||
Sessions are immutable once complete, making them ideal for caching:
|
||||
|
||||
```python
|
||||
from functools import lru_cache
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import redis
|
||||
|
||||
redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
||||
|
||||
class CachedAsleepClient(AsleepClient):
|
||||
"""Client with response caching"""
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_session_cached(self, session_id: str, user_id: str) -> Dict:
|
||||
"""Get session with caching (sessions are immutable once complete)"""
|
||||
return self.get_session(session_id, user_id)
|
||||
|
||||
def get_recent_sessions(self, user_id: str, days: int = 7) -> List[Dict]:
|
||||
"""Get recent sessions with Redis caching"""
|
||||
cache_key = f"sessions:{user_id}:{days}"
|
||||
cached = redis_client.get(cache_key)
|
||||
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
result = self.list_sessions(
|
||||
user_id=user_id,
|
||||
date_gte=start_date.strftime("%Y-%m-%d"),
|
||||
date_lte=end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
# Cache for 5 minutes
|
||||
redis_client.setex(cache_key, 300, json.dumps(result))
|
||||
|
||||
return result
|
||||
|
||||
def invalidate_user_cache(self, user_id: str):
|
||||
"""Invalidate all caches for a user"""
|
||||
pattern = f"sessions:{user_id}:*"
|
||||
for key in redis_client.scan_iter(match=pattern):
|
||||
redis_client.delete(key)
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Application-Level Rate Limiting
|
||||
|
||||
Protect your backend from being overwhelmed:
|
||||
|
||||
```python
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
limiter = Limiter(
|
||||
app,
|
||||
key_func=get_remote_address,
|
||||
default_limits=["100 per hour"]
|
||||
)
|
||||
|
||||
@app.route('/api/sessions/<session_id>')
|
||||
@limiter.limit("10 per minute")
|
||||
def get_session(session_id):
|
||||
"""Rate-limited session endpoint"""
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
@app.route('/api/statistics')
|
||||
@limiter.limit("5 per minute")
|
||||
def get_statistics():
|
||||
"""Statistics endpoint with stricter rate limiting"""
|
||||
# Implementation
|
||||
pass
|
||||
```
|
||||
|
||||
### API Request Rate Limiting
|
||||
|
||||
Respect Asleep API rate limits with request throttling:
|
||||
|
||||
```python
|
||||
import time
|
||||
from collections import deque
|
||||
from threading import Lock
|
||||
|
||||
class RateLimitedClient(AsleepClient):
|
||||
"""Client with built-in rate limiting"""
|
||||
|
||||
def __init__(self, api_key: str, requests_per_second: int = 10):
|
||||
super().__init__(api_key)
|
||||
self.requests_per_second = requests_per_second
|
||||
self.request_times = deque()
|
||||
self.lock = Lock()
|
||||
|
||||
def _wait_for_rate_limit(self):
|
||||
"""Wait if necessary to stay within rate limits"""
|
||||
with self.lock:
|
||||
now = time.time()
|
||||
|
||||
# Remove requests older than 1 second
|
||||
while self.request_times and self.request_times[0] < now - 1:
|
||||
self.request_times.popleft()
|
||||
|
||||
# If at limit, wait
|
||||
if len(self.request_times) >= self.requests_per_second:
|
||||
sleep_time = 1 - (now - self.request_times[0])
|
||||
if sleep_time > 0:
|
||||
time.sleep(sleep_time)
|
||||
self.request_times.popleft()
|
||||
|
||||
self.request_times.append(time.time())
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs):
|
||||
"""Rate-limited request"""
|
||||
self._wait_for_rate_limit()
|
||||
return super()._request(method, path, **kwargs)
|
||||
```
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
### HTTP Session with Connection Pool
|
||||
|
||||
Reuse connections for better performance:
|
||||
|
||||
```python
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
def create_session_with_retries():
|
||||
"""Create session with connection pooling and retries"""
|
||||
session = requests.Session()
|
||||
|
||||
retry_strategy = Retry(
|
||||
total=3,
|
||||
backoff_factor=1,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
method_whitelist=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
|
||||
)
|
||||
|
||||
adapter = HTTPAdapter(
|
||||
max_retries=retry_strategy,
|
||||
pool_connections=10,
|
||||
pool_maxsize=20
|
||||
)
|
||||
|
||||
session.mount("https://", adapter)
|
||||
session.mount("http://", adapter)
|
||||
|
||||
return session
|
||||
|
||||
class PooledAsleepClient(AsleepClient):
|
||||
"""Client with connection pooling"""
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.asleep.ai"
|
||||
self.session = create_session_with_retries()
|
||||
self.session.headers.update({"x-api-key": api_key})
|
||||
```
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```python
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
class StructuredLogger:
|
||||
"""Structured logging for API requests"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.logger = logging.getLogger(name)
|
||||
|
||||
def log_request(self, method: str, path: str, user_id: str = None):
|
||||
"""Log API request"""
|
||||
self.logger.info(json.dumps({
|
||||
'event': 'api_request',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'method': method,
|
||||
'path': path,
|
||||
'user_id': user_id
|
||||
}))
|
||||
|
||||
def log_response(self, method: str, path: str, status_code: int, duration: float):
|
||||
"""Log API response"""
|
||||
self.logger.info(json.dumps({
|
||||
'event': 'api_response',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'method': method,
|
||||
'path': path,
|
||||
'status_code': status_code,
|
||||
'duration_ms': duration * 1000
|
||||
}))
|
||||
|
||||
def log_error(self, method: str, path: str, error: Exception, duration: float):
|
||||
"""Log API error"""
|
||||
self.logger.error(json.dumps({
|
||||
'event': 'api_error',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'method': method,
|
||||
'path': path,
|
||||
'error_type': type(error).__name__,
|
||||
'error_message': str(error),
|
||||
'duration_ms': duration * 1000
|
||||
}))
|
||||
|
||||
class MonitoredAsleepClient(AsleepClient):
|
||||
"""Client with comprehensive logging"""
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
super().__init__(api_key)
|
||||
self.logger = StructuredLogger(__name__)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs):
|
||||
"""Monitored API request"""
|
||||
start_time = datetime.now()
|
||||
user_id = kwargs.get('headers', {}).get('x-user-id')
|
||||
|
||||
self.logger.log_request(method, path, user_id)
|
||||
|
||||
try:
|
||||
result = super()._request(method, path, **kwargs)
|
||||
duration = (datetime.now() - start_time).total_seconds()
|
||||
self.logger.log_response(method, path, 200, duration)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
duration = (datetime.now() - start_time).total_seconds()
|
||||
self.logger.log_error(method, path, e, duration)
|
||||
raise
|
||||
```
|
||||
|
||||
### Metrics Collection
|
||||
|
||||
```python
|
||||
from datadog import statsd
|
||||
|
||||
class MetricsClient(AsleepClient):
|
||||
"""Client with metrics collection"""
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs):
|
||||
"""Request with metrics"""
|
||||
start_time = datetime.now()
|
||||
|
||||
try:
|
||||
result = super()._request(method, path, **kwargs)
|
||||
|
||||
duration = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Record success metrics
|
||||
statsd.increment('asleep_api.request.success')
|
||||
statsd.timing('asleep_api.request.duration', duration)
|
||||
statsd.histogram('asleep_api.response_time', duration)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
duration = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Record error metrics
|
||||
statsd.increment('asleep_api.request.error')
|
||||
statsd.increment(f'asleep_api.error.{type(e).__name__}')
|
||||
statsd.timing('asleep_api.request.duration', duration)
|
||||
|
||||
raise
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### API Key Management
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
class SecureConfig:
|
||||
"""Secure configuration management"""
|
||||
|
||||
def __init__(self):
|
||||
load_dotenv()
|
||||
self._validate_config()
|
||||
|
||||
def _validate_config(self):
|
||||
"""Validate required environment variables"""
|
||||
required = ['ASLEEP_API_KEY', 'DATABASE_URL']
|
||||
missing = [var for var in required if not os.getenv(var)]
|
||||
|
||||
if missing:
|
||||
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
|
||||
|
||||
@property
|
||||
def asleep_api_key(self) -> str:
|
||||
"""Get API key from environment"""
|
||||
return os.getenv('ASLEEP_API_KEY')
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Get database URL from environment"""
|
||||
return os.getenv('DATABASE_URL')
|
||||
|
||||
@property
|
||||
def redis_url(self) -> str:
|
||||
"""Get Redis URL from environment"""
|
||||
return os.getenv('REDIS_URL', 'redis://localhost:6379')
|
||||
```
|
||||
|
||||
### Webhook Security
|
||||
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||||
"""Verify webhook payload signature"""
|
||||
expected_signature = hmac.new(
|
||||
secret.encode(),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
@app.route('/asleep-webhook', methods=['POST'])
|
||||
def secure_webhook():
|
||||
"""Webhook endpoint with signature verification"""
|
||||
# Verify API key
|
||||
api_key = request.headers.get('x-api-key')
|
||||
if api_key != EXPECTED_API_KEY:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
# Verify signature (if implemented)
|
||||
signature = request.headers.get('x-signature')
|
||||
if signature:
|
||||
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
|
||||
return jsonify({"error": "Invalid signature"}), 401
|
||||
|
||||
# Process webhook
|
||||
event = request.json
|
||||
process_webhook(event)
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
```
|
||||
|
||||
## Deployment Configuration
|
||||
|
||||
### Environment-Based Configuration
|
||||
|
||||
```python
|
||||
import os
|
||||
from enum import Enum
|
||||
|
||||
class Environment(Enum):
|
||||
DEVELOPMENT = "development"
|
||||
STAGING = "staging"
|
||||
PRODUCTION = "production"
|
||||
|
||||
class Config:
|
||||
"""Environment-based configuration"""
|
||||
|
||||
def __init__(self):
|
||||
self.env = Environment(os.getenv('ENVIRONMENT', 'development'))
|
||||
self.asleep_api_key = os.getenv('ASLEEP_API_KEY')
|
||||
self.asleep_base_url = os.getenv('ASLEEP_BASE_URL', 'https://api.asleep.ai')
|
||||
self.database_url = os.getenv('DATABASE_URL')
|
||||
self.redis_url = os.getenv('REDIS_URL')
|
||||
|
||||
# Feature flags
|
||||
self.enable_caching = self._parse_bool('ENABLE_CACHING', True)
|
||||
self.enable_webhooks = self._parse_bool('ENABLE_WEBHOOKS', True)
|
||||
self.enable_metrics = self._parse_bool('ENABLE_METRICS', True)
|
||||
|
||||
# Performance settings
|
||||
self.max_connections = int(os.getenv('MAX_CONNECTIONS', '100'))
|
||||
self.request_timeout = int(os.getenv('REQUEST_TIMEOUT', '30'))
|
||||
|
||||
self._validate()
|
||||
|
||||
def _parse_bool(self, key: str, default: bool) -> bool:
|
||||
"""Parse boolean environment variable"""
|
||||
value = os.getenv(key, str(default)).lower()
|
||||
return value in ('true', '1', 'yes')
|
||||
|
||||
def _validate(self):
|
||||
"""Validate configuration"""
|
||||
if not self.asleep_api_key:
|
||||
raise ValueError("ASLEEP_API_KEY is required")
|
||||
|
||||
if self.env == Environment.PRODUCTION:
|
||||
if not self.database_url:
|
||||
raise ValueError("DATABASE_URL is required in production")
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
return self.env == Environment.PRODUCTION
|
||||
|
||||
@property
|
||||
def is_development(self) -> bool:
|
||||
return self.env == Environment.DEVELOPMENT
|
||||
```
|
||||
|
||||
### Health Check Endpoint
|
||||
|
||||
```python
|
||||
from flask import Flask, jsonify
|
||||
import requests
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
"""Health check endpoint for load balancers"""
|
||||
checks = {
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'environment': config.env.value,
|
||||
'checks': {}
|
||||
}
|
||||
|
||||
# Check database connection
|
||||
try:
|
||||
db.command('ping')
|
||||
checks['checks']['database'] = 'ok'
|
||||
except Exception as e:
|
||||
checks['status'] = 'unhealthy'
|
||||
checks['checks']['database'] = f'error: {str(e)}'
|
||||
|
||||
# Check Redis connection
|
||||
try:
|
||||
redis_client.ping()
|
||||
checks['checks']['redis'] = 'ok'
|
||||
except Exception as e:
|
||||
checks['status'] = 'unhealthy'
|
||||
checks['checks']['redis'] = f'error: {str(e)}'
|
||||
|
||||
# Check Asleep API connectivity
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{config.asleep_base_url}/health",
|
||||
headers={"x-api-key": config.asleep_api_key},
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 200:
|
||||
checks['checks']['asleep_api'] = 'ok'
|
||||
else:
|
||||
checks['checks']['asleep_api'] = f'status: {response.status_code}'
|
||||
except Exception as e:
|
||||
checks['status'] = 'unhealthy'
|
||||
checks['checks']['asleep_api'] = f'error: {str(e)}'
|
||||
|
||||
status_code = 200 if checks['status'] == 'healthy' else 503
|
||||
return jsonify(checks), status_code
|
||||
|
||||
@app.route('/ready')
|
||||
def readiness_check():
|
||||
"""Readiness check for Kubernetes"""
|
||||
# Check if app is ready to serve traffic
|
||||
if not app.initialized:
|
||||
return jsonify({'status': 'not ready'}), 503
|
||||
|
||||
return jsonify({'status': 'ready'}), 200
|
||||
|
||||
@app.route('/live')
|
||||
def liveness_check():
|
||||
"""Liveness check for Kubernetes"""
|
||||
# Simple check that app is running
|
||||
return jsonify({'status': 'alive'}), 200
|
||||
```
|
||||
|
||||
## Error Recovery
|
||||
|
||||
### Circuit Breaker Pattern
|
||||
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class CircuitBreaker:
|
||||
"""Circuit breaker for API calls"""
|
||||
|
||||
def __init__(self, failure_threshold: int = 5, timeout: int = 60):
|
||||
self.failure_threshold = failure_threshold
|
||||
self.timeout = timeout
|
||||
self.failure_count = 0
|
||||
self.last_failure_time = None
|
||||
self.state = 'closed' # closed, open, half_open
|
||||
|
||||
def call(self, func, *args, **kwargs):
|
||||
"""Execute function with circuit breaker"""
|
||||
if self.state == 'open':
|
||||
if datetime.now() - self.last_failure_time > timedelta(seconds=self.timeout):
|
||||
self.state = 'half_open'
|
||||
else:
|
||||
raise Exception("Circuit breaker is open")
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
if self.state == 'half_open':
|
||||
self.state = 'closed'
|
||||
self.failure_count = 0
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.failure_count += 1
|
||||
self.last_failure_time = datetime.now()
|
||||
|
||||
if self.failure_count >= self.failure_threshold:
|
||||
self.state = 'open'
|
||||
|
||||
raise
|
||||
|
||||
class ResilientAsleepClient(AsleepClient):
|
||||
"""Client with circuit breaker"""
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
super().__init__(api_key)
|
||||
self.circuit_breaker = CircuitBreaker()
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs):
|
||||
"""Request with circuit breaker"""
|
||||
return self.circuit_breaker.call(
|
||||
super()._request,
|
||||
method,
|
||||
path,
|
||||
**kwargs
|
||||
)
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### Session Storage
|
||||
|
||||
```python
|
||||
from pymongo import MongoClient
|
||||
from datetime import datetime
|
||||
|
||||
class SessionStore:
|
||||
"""Store and retrieve sleep sessions"""
|
||||
|
||||
def __init__(self, db):
|
||||
self.collection = db.sleep_sessions
|
||||
self._create_indexes()
|
||||
|
||||
def _create_indexes(self):
|
||||
"""Create database indexes for performance"""
|
||||
self.collection.create_index([('user_id', 1), ('session_start_time', -1)])
|
||||
self.collection.create_index([('session_id', 1)], unique=True)
|
||||
self.collection.create_index([('created_at', 1)])
|
||||
|
||||
def store_session(self, session_data: Dict):
|
||||
"""Store session in database"""
|
||||
doc = {
|
||||
'session_id': session_data['session']['id'],
|
||||
'user_id': session_data['user_id'],
|
||||
'session_start_time': session_data['session']['start_time'],
|
||||
'session_end_time': session_data['session']['end_time'],
|
||||
'statistics': session_data['stat'],
|
||||
'sleep_stages': session_data['session']['sleep_stages'],
|
||||
'created_at': datetime.now(),
|
||||
'updated_at': datetime.now()
|
||||
}
|
||||
|
||||
self.collection.update_one(
|
||||
{'session_id': doc['session_id']},
|
||||
{'$set': doc},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
def get_user_sessions(self, user_id: str, limit: int = 10) -> List[Dict]:
|
||||
"""Get recent sessions for user"""
|
||||
return list(
|
||||
self.collection
|
||||
.find({'user_id': user_id})
|
||||
.sort('session_start_time', -1)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
def get_sessions_by_date_range(
|
||||
self,
|
||||
user_id: str,
|
||||
start_date: str,
|
||||
end_date: str
|
||||
) -> List[Dict]:
|
||||
"""Get sessions within date range"""
|
||||
return list(
|
||||
self.collection.find({
|
||||
'user_id': user_id,
|
||||
'session_start_time': {
|
||||
'$gte': start_date,
|
||||
'$lte': end_date
|
||||
}
|
||||
})
|
||||
.sort('session_start_time', -1)
|
||||
)
|
||||
```
|
||||
|
||||
## Background Job Processing
|
||||
|
||||
### Celery Task Queue
|
||||
|
||||
```python
|
||||
from celery import Celery
|
||||
|
||||
celery = Celery('tasks', broker='redis://localhost:6379')
|
||||
|
||||
@celery.task(bind=True, max_retries=3)
|
||||
def process_webhook_task(self, webhook_data: Dict):
|
||||
"""Process webhook asynchronously"""
|
||||
try:
|
||||
if webhook_data['event'] == 'SESSION_COMPLETE':
|
||||
# Store in database
|
||||
store_session(webhook_data)
|
||||
|
||||
# Send notification
|
||||
send_notification(webhook_data['user_id'], webhook_data)
|
||||
|
||||
# Update analytics
|
||||
update_user_stats(webhook_data['user_id'])
|
||||
|
||||
except Exception as e:
|
||||
# Retry with exponential backoff
|
||||
raise self.retry(exc=e, countdown=2 ** self.request.retries)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
"""Webhook endpoint with async processing"""
|
||||
event = request.json
|
||||
|
||||
# Queue for background processing
|
||||
process_webhook_task.delay(event)
|
||||
|
||||
# Respond immediately
|
||||
return jsonify({"status": "queued"}), 200
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
def fetch_sessions_batch(client: AsleepClient, user_ids: List[str]) -> Dict[str, List]:
|
||||
"""Fetch sessions for multiple users in parallel"""
|
||||
results = {}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_user = {
|
||||
executor.submit(client.list_sessions, user_id): user_id
|
||||
for user_id in user_ids
|
||||
}
|
||||
|
||||
for future in as_completed(future_to_user):
|
||||
user_id = future_to_user[future]
|
||||
try:
|
||||
results[user_id] = future.result()
|
||||
except Exception as e:
|
||||
print(f"Error fetching sessions for {user_id}: {e}")
|
||||
results[user_id] = []
|
||||
|
||||
return results
|
||||
```
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```python
|
||||
def get_user_summary_optimized(client: AsleepClient, user_id: str) -> Dict:
|
||||
"""Get user summary with optimized queries"""
|
||||
# Fetch only what's needed
|
||||
user_data = client.get_user(user_id)
|
||||
|
||||
# Use average stats instead of fetching all sessions
|
||||
stats = client.get_average_stats(
|
||||
user_id=user_id,
|
||||
start_date=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"),
|
||||
end_date=datetime.now().strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
return {
|
||||
'user_id': user_id,
|
||||
'last_session': user_data.get('last_session_info'),
|
||||
'monthly_average': stats['average_stats'],
|
||||
'session_count': len(stats['slept_sessions'])
|
||||
}
|
||||
```
|
||||
561
skills/sleeptrack-be/references/python_client_implementation.md
Normal file
561
skills/sleeptrack-be/references/python_client_implementation.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# Python Client Implementation Guide
|
||||
|
||||
This reference provides complete Python client implementations for the Asleep API, including advanced patterns for analytics, production usage, and multi-tenant applications.
|
||||
|
||||
## Complete Python API Client
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class AsleepClient:
|
||||
"""Asleep API client for backend integration"""
|
||||
|
||||
def __init__(self, api_key: str, base_url: str = "https://api.asleep.ai"):
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"x-api-key": api_key})
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Make authenticated API request with error handling"""
|
||||
url = f"{self.base_url}{path}"
|
||||
req_headers = self.session.headers.copy()
|
||||
if headers:
|
||||
req_headers.update(headers)
|
||||
|
||||
try:
|
||||
response = self.session.request(method, url, headers=req_headers, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
# Handle API errors
|
||||
if e.response.status_code == 401:
|
||||
raise ValueError("Invalid API key")
|
||||
elif e.response.status_code == 403:
|
||||
error_detail = e.response.json().get("detail", "Access forbidden")
|
||||
raise ValueError(f"API access error: {error_detail}")
|
||||
elif e.response.status_code == 404:
|
||||
raise ValueError("Resource not found")
|
||||
else:
|
||||
raise
|
||||
|
||||
# User management methods
|
||||
def create_user(self, metadata: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Create new user and return user_id"""
|
||||
data = {"metadata": metadata} if metadata else {}
|
||||
result = self._request("POST", "/ai/v1/users", json=data)
|
||||
return result["result"]["user_id"]
|
||||
|
||||
def get_user(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get user information"""
|
||||
result = self._request("GET", f"/ai/v1/users/{user_id}")
|
||||
return result["result"]
|
||||
|
||||
def delete_user(self, user_id: str) -> None:
|
||||
"""Delete user and all associated data"""
|
||||
self._request("DELETE", f"/ai/v1/users/{user_id}")
|
||||
|
||||
# Session management methods
|
||||
def get_session(self, session_id: str, user_id: str, timezone: str = "UTC") -> Dict[str, Any]:
|
||||
"""Get detailed session data"""
|
||||
headers = {"x-user-id": user_id, "timezone": timezone}
|
||||
result = self._request("GET", f"/data/v3/sessions/{session_id}", headers=headers)
|
||||
return result["result"]
|
||||
|
||||
def list_sessions(
|
||||
self,
|
||||
user_id: str,
|
||||
date_gte: Optional[str] = None,
|
||||
date_lte: Optional[str] = None,
|
||||
offset: int = 0,
|
||||
limit: int = 20,
|
||||
order_by: str = "DESC"
|
||||
) -> Dict[str, Any]:
|
||||
"""List user sessions with filtering"""
|
||||
headers = {"x-user-id": user_id}
|
||||
params = {"offset": offset, "limit": limit, "order_by": order_by}
|
||||
if date_gte:
|
||||
params["date_gte"] = date_gte
|
||||
if date_lte:
|
||||
params["date_lte"] = date_lte
|
||||
|
||||
result = self._request("GET", "/data/v1/sessions", headers=headers, params=params)
|
||||
return result["result"]
|
||||
|
||||
def delete_session(self, session_id: str, user_id: str) -> None:
|
||||
"""Delete session and all associated data"""
|
||||
headers = {"x-user-id": user_id}
|
||||
self._request("DELETE", f"/ai/v1/sessions/{session_id}", headers=headers)
|
||||
|
||||
# Statistics methods
|
||||
def get_average_stats(
|
||||
self,
|
||||
user_id: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
timezone: str = "UTC"
|
||||
) -> Dict[str, Any]:
|
||||
"""Get average statistics for date range (max 100 days)"""
|
||||
headers = {"timezone": timezone}
|
||||
params = {"start_date": start_date, "end_date": end_date}
|
||||
result = self._request(
|
||||
"GET",
|
||||
f"/data/v1/users/{user_id}/average-stats",
|
||||
headers=headers,
|
||||
params=params
|
||||
)
|
||||
return result["result"]
|
||||
|
||||
# Usage
|
||||
client = AsleepClient(api_key=os.getenv("ASLEEP_API_KEY"))
|
||||
```
|
||||
|
||||
## Advanced Analytics Implementation
|
||||
|
||||
```python
|
||||
from typing import List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class SleepAnalytics:
|
||||
"""Backend analytics for sleep tracking platform"""
|
||||
|
||||
def __init__(self, client: AsleepClient, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
|
||||
def get_user_sleep_score(self, user_id: str, days: int = 30) -> Dict:
|
||||
"""Calculate comprehensive sleep score for user"""
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
stats = self.client.get_average_stats(
|
||||
user_id=user_id,
|
||||
start_date=start_date.strftime("%Y-%m-%d"),
|
||||
end_date=end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
avg = stats['average_stats']
|
||||
|
||||
# Calculate weighted sleep score (0-100)
|
||||
efficiency_score = avg['sleep_efficiency'] # Already 0-100
|
||||
consistency_score = self._calculate_consistency_score(stats)
|
||||
duration_score = self._calculate_duration_score(avg)
|
||||
|
||||
overall_score = (
|
||||
efficiency_score * 0.4 +
|
||||
consistency_score * 0.3 +
|
||||
duration_score * 0.3
|
||||
)
|
||||
|
||||
return {
|
||||
'overall_score': round(overall_score, 1),
|
||||
'efficiency_score': round(efficiency_score, 1),
|
||||
'consistency_score': round(consistency_score, 1),
|
||||
'duration_score': round(duration_score, 1),
|
||||
'period_days': days,
|
||||
'session_count': len(stats['slept_sessions'])
|
||||
}
|
||||
|
||||
def _calculate_consistency_score(self, stats: Dict) -> float:
|
||||
"""Score based on sleep schedule consistency"""
|
||||
# Implement consistency scoring based on variance in sleep times
|
||||
# Placeholder implementation
|
||||
return 80.0
|
||||
|
||||
def _calculate_duration_score(self, avg: Dict) -> float:
|
||||
"""Score based on sleep duration (7-9 hours optimal)"""
|
||||
sleep_hours = avg['time_in_sleep'] / 3600
|
||||
|
||||
if 7 <= sleep_hours <= 9:
|
||||
return 100.0
|
||||
elif 6 <= sleep_hours < 7 or 9 < sleep_hours <= 10:
|
||||
return 80.0
|
||||
elif 5 <= sleep_hours < 6 or 10 < sleep_hours <= 11:
|
||||
return 60.0
|
||||
else:
|
||||
return 40.0
|
||||
|
||||
def get_cohort_analysis(self, user_ids: List[str], days: int = 30) -> Dict:
|
||||
"""Analyze sleep patterns across user cohort"""
|
||||
cohort_data = []
|
||||
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
score = self.get_user_sleep_score(user_id, days)
|
||||
cohort_data.append({
|
||||
'user_id': user_id,
|
||||
'score': score['overall_score'],
|
||||
'efficiency': score['efficiency_score'],
|
||||
'sessions': score['session_count']
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error fetching data for user {user_id}: {e}")
|
||||
|
||||
if not cohort_data:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'cohort_size': len(cohort_data),
|
||||
'avg_score': sum(u['score'] for u in cohort_data) / len(cohort_data),
|
||||
'avg_efficiency': sum(u['efficiency'] for u in cohort_data) / len(cohort_data),
|
||||
'total_sessions': sum(u['sessions'] for u in cohort_data),
|
||||
'users': cohort_data
|
||||
}
|
||||
|
||||
def generate_weekly_report(self, user_id: str) -> Dict:
|
||||
"""Generate comprehensive weekly sleep report"""
|
||||
stats = self.client.get_average_stats(
|
||||
user_id=user_id,
|
||||
start_date=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d"),
|
||||
end_date=datetime.now().strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
avg = stats['average_stats']
|
||||
|
||||
return {
|
||||
'period': 'Last 7 days',
|
||||
'summary': {
|
||||
'avg_sleep_time': avg['sleep_time'],
|
||||
'avg_bedtime': avg['start_time'],
|
||||
'avg_wake_time': avg['end_time'],
|
||||
'avg_efficiency': avg['sleep_efficiency']
|
||||
},
|
||||
'sleep_stages': {
|
||||
'light_hours': avg['time_in_light'] / 3600,
|
||||
'deep_hours': avg['time_in_deep'] / 3600,
|
||||
'rem_hours': avg['time_in_rem'] / 3600
|
||||
},
|
||||
'insights': self._generate_insights(avg),
|
||||
'session_count': len(stats['slept_sessions'])
|
||||
}
|
||||
|
||||
def _generate_insights(self, avg: Dict) -> List[str]:
|
||||
"""Generate personalized sleep insights"""
|
||||
insights = []
|
||||
|
||||
if avg['sleep_efficiency'] < 75:
|
||||
insights.append("Your sleep efficiency is below average. Try establishing a consistent bedtime routine.")
|
||||
|
||||
if avg['deep_ratio'] < 0.15:
|
||||
insights.append("You're getting less deep sleep than optimal. Avoid caffeine after 2 PM.")
|
||||
|
||||
if avg['waso_count'] > 3:
|
||||
insights.append("You're waking up frequently during the night. Consider reducing screen time before bed.")
|
||||
|
||||
return insights
|
||||
|
||||
# Usage
|
||||
analytics = SleepAnalytics(client, db)
|
||||
score = analytics.get_user_sleep_score("user123", days=30)
|
||||
print(f"Sleep score: {score['overall_score']}/100")
|
||||
|
||||
report = analytics.generate_weekly_report("user123")
|
||||
print(f"Weekly report: {report}")
|
||||
```
|
||||
|
||||
## Multi-Tenant Application Implementation
|
||||
|
||||
```python
|
||||
class MultiTenantSleepTracker:
|
||||
"""Multi-tenant sleep tracking backend"""
|
||||
|
||||
def __init__(self, client: AsleepClient, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
|
||||
def create_organization(self, org_id: str, name: str, settings: Dict) -> Dict:
|
||||
"""Create new organization"""
|
||||
org = {
|
||||
'org_id': org_id,
|
||||
'name': name,
|
||||
'settings': settings,
|
||||
'created_at': datetime.now(),
|
||||
'user_count': 0
|
||||
}
|
||||
self.db.organizations.insert_one(org)
|
||||
return org
|
||||
|
||||
def add_user_to_organization(self, org_id: str, user_email: str, metadata: Dict = None) -> str:
|
||||
"""Add user to organization and create Asleep user"""
|
||||
# Verify organization exists
|
||||
org = self.db.organizations.find_one({'org_id': org_id})
|
||||
if not org:
|
||||
raise ValueError(f"Organization {org_id} not found")
|
||||
|
||||
# Create Asleep user
|
||||
asleep_user_id = self.client.create_user(metadata=metadata)
|
||||
|
||||
# Store user mapping
|
||||
self.db.users.insert_one({
|
||||
'org_id': org_id,
|
||||
'user_email': user_email,
|
||||
'asleep_user_id': asleep_user_id,
|
||||
'metadata': metadata,
|
||||
'created_at': datetime.now()
|
||||
})
|
||||
|
||||
# Update organization user count
|
||||
self.db.organizations.update_one(
|
||||
{'org_id': org_id},
|
||||
{'$inc': {'user_count': 1}}
|
||||
)
|
||||
|
||||
return asleep_user_id
|
||||
|
||||
def get_organization_statistics(self, org_id: str, days: int = 30) -> Dict:
|
||||
"""Get aggregated statistics for entire organization"""
|
||||
# Get all users in organization
|
||||
users = list(self.db.users.find({'org_id': org_id}))
|
||||
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
org_stats = {
|
||||
'org_id': org_id,
|
||||
'user_count': len(users),
|
||||
'period_days': days,
|
||||
'users_data': []
|
||||
}
|
||||
|
||||
total_efficiency = 0
|
||||
total_sleep_time = 0
|
||||
total_sessions = 0
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
stats = self.client.get_average_stats(
|
||||
user_id=user['asleep_user_id'],
|
||||
start_date=start_date.strftime("%Y-%m-%d"),
|
||||
end_date=end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
avg = stats['average_stats']
|
||||
session_count = len(stats['slept_sessions'])
|
||||
|
||||
org_stats['users_data'].append({
|
||||
'user_email': user['user_email'],
|
||||
'efficiency': avg['sleep_efficiency'],
|
||||
'sleep_time': avg['sleep_time'],
|
||||
'session_count': session_count
|
||||
})
|
||||
|
||||
total_efficiency += avg['sleep_efficiency']
|
||||
total_sleep_time += avg['time_in_sleep']
|
||||
total_sessions += session_count
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching stats for user {user['user_email']}: {e}")
|
||||
|
||||
if users:
|
||||
org_stats['avg_efficiency'] = total_efficiency / len(users)
|
||||
org_stats['avg_sleep_hours'] = (total_sleep_time / len(users)) / 3600
|
||||
org_stats['total_sessions'] = total_sessions
|
||||
|
||||
return org_stats
|
||||
|
||||
# Usage
|
||||
tracker = MultiTenantSleepTracker(client, db)
|
||||
|
||||
# Create organization
|
||||
tracker.create_organization(
|
||||
org_id="acme-corp",
|
||||
name="Acme Corporation",
|
||||
settings={'timezone': 'America/New_York'}
|
||||
)
|
||||
|
||||
# Add users
|
||||
tracker.add_user_to_organization("acme-corp", "john@acme.com")
|
||||
tracker.add_user_to_organization("acme-corp", "jane@acme.com")
|
||||
|
||||
# Get organization stats
|
||||
org_stats = tracker.get_organization_statistics("acme-corp", days=30)
|
||||
print(f"Organization average efficiency: {org_stats['avg_efficiency']:.1f}%")
|
||||
```
|
||||
|
||||
## FastAPI Backend Example
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
app = FastAPI()
|
||||
asleep_client = AsleepClient(api_key=os.getenv("ASLEEP_API_KEY"))
|
||||
|
||||
# Authentication dependency
|
||||
async def verify_app_token(authorization: str = Header(...)):
|
||||
"""Verify mobile app authentication"""
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Invalid authorization header")
|
||||
|
||||
token = authorization[7:]
|
||||
# Verify token with your auth system
|
||||
user = verify_jwt_token(token)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
return user
|
||||
|
||||
# Proxy endpoints
|
||||
@app.post("/api/users")
|
||||
async def create_user(
|
||||
metadata: Optional[dict] = None,
|
||||
user: dict = Depends(verify_app_token)
|
||||
):
|
||||
"""Create Asleep user for authenticated app user"""
|
||||
try:
|
||||
# Create user in Asleep
|
||||
asleep_user_id = asleep_client.create_user(metadata=metadata)
|
||||
|
||||
# Store mapping in your database
|
||||
db.user_mappings.insert_one({
|
||||
'app_user_id': user['id'],
|
||||
'asleep_user_id': asleep_user_id,
|
||||
'created_at': datetime.now()
|
||||
})
|
||||
|
||||
return {"user_id": asleep_user_id}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/sessions/{session_id}")
|
||||
async def get_session(
|
||||
session_id: str,
|
||||
user: dict = Depends(verify_app_token)
|
||||
):
|
||||
"""Get session data for authenticated user"""
|
||||
# Get Asleep user ID from mapping
|
||||
mapping = db.user_mappings.find_one({'app_user_id': user['id']})
|
||||
if not mapping:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
asleep_user_id = mapping['asleep_user_id']
|
||||
|
||||
# Fetch session from Asleep
|
||||
try:
|
||||
session = asleep_client.get_session(session_id, asleep_user_id)
|
||||
return session
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@app.get("/api/sessions")
|
||||
async def list_sessions(
|
||||
date_gte: Optional[str] = None,
|
||||
date_lte: Optional[str] = None,
|
||||
user: dict = Depends(verify_app_token)
|
||||
):
|
||||
"""List sessions for authenticated user"""
|
||||
mapping = db.user_mappings.find_one({'app_user_id': user['id']})
|
||||
if not mapping:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
asleep_user_id = mapping['asleep_user_id']
|
||||
|
||||
sessions = asleep_client.list_sessions(
|
||||
user_id=asleep_user_id,
|
||||
date_gte=date_gte,
|
||||
date_lte=date_lte
|
||||
)
|
||||
|
||||
return sessions
|
||||
|
||||
@app.get("/api/statistics")
|
||||
async def get_statistics(
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
user: dict = Depends(verify_app_token)
|
||||
):
|
||||
"""Get average statistics for authenticated user"""
|
||||
mapping = db.user_mappings.find_one({'app_user_id': user['id']})
|
||||
if not mapping:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
asleep_user_id = mapping['asleep_user_id']
|
||||
|
||||
stats = asleep_client.get_average_stats(
|
||||
user_id=asleep_user_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
return stats
|
||||
```
|
||||
|
||||
## Monthly Trends Analysis
|
||||
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict
|
||||
|
||||
def get_monthly_trends(client, user_id: str, months: int = 6) -> List[Dict]:
|
||||
"""Get monthly sleep trends for the past N months"""
|
||||
trends = []
|
||||
today = datetime.now()
|
||||
|
||||
for i in range(months):
|
||||
# Calculate month boundaries
|
||||
end_date = today.replace(day=1) - timedelta(days=i * 30)
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
try:
|
||||
stats = client.get_average_stats(
|
||||
user_id=user_id,
|
||||
start_date=start_date.strftime("%Y-%m-%d"),
|
||||
end_date=end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
trends.append({
|
||||
'month': end_date.strftime("%Y-%m"),
|
||||
'avg_sleep_time': stats['average_stats']['sleep_time'],
|
||||
'avg_efficiency': stats['average_stats']['sleep_efficiency'],
|
||||
'session_count': len(stats['slept_sessions'])
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error fetching stats for {end_date.strftime('%Y-%m')}: {e}")
|
||||
|
||||
return trends
|
||||
|
||||
# Usage
|
||||
trends = get_monthly_trends(client, "user123", months=6)
|
||||
for trend in trends:
|
||||
print(f"{trend['month']}: {trend['avg_sleep_time']} sleep, "
|
||||
f"{trend['avg_efficiency']:.1f}% efficiency, "
|
||||
f"{trend['session_count']} sessions")
|
||||
```
|
||||
|
||||
## Pagination Pattern
|
||||
|
||||
```python
|
||||
# Fetch all sessions with pagination
|
||||
all_sessions = []
|
||||
offset = 0
|
||||
limit = 100
|
||||
|
||||
while True:
|
||||
result = client.list_sessions(
|
||||
user_id="user123",
|
||||
date_gte="2024-01-01",
|
||||
date_lte="2024-12-31",
|
||||
offset=offset,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
sessions = result['sleep_session_list']
|
||||
all_sessions.extend(sessions)
|
||||
|
||||
if len(sessions) < limit:
|
||||
break
|
||||
|
||||
offset += limit
|
||||
|
||||
print(f"Total sessions: {len(all_sessions)}")
|
||||
```
|
||||
725
skills/sleeptrack-be/references/rest_api_reference.md
Normal file
725
skills/sleeptrack-be/references/rest_api_reference.md
Normal file
@@ -0,0 +1,725 @@
|
||||
# Asleep REST API Reference
|
||||
|
||||
This reference provides comprehensive documentation for the Asleep REST API endpoints.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://api.asleep.ai
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require authentication via the `x-api-key` header:
|
||||
|
||||
```http
|
||||
x-api-key: YOUR_API_KEY
|
||||
```
|
||||
|
||||
Obtain your API key from the [Asleep Dashboard](https://dashboard.asleep.ai).
|
||||
|
||||
## Common Headers
|
||||
|
||||
| Header | Type | Required | Description |
|
||||
|--------|------|----------|-------------|
|
||||
| x-api-key | String | Yes | API authentication key |
|
||||
| x-user-id | String | Conditional | Required for session operations |
|
||||
| timezone | String | No | Response timezone (default: UTC) |
|
||||
|
||||
## Response Format
|
||||
|
||||
All responses follow this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "message about the result",
|
||||
"result": { /* response data */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Common Error Codes
|
||||
|
||||
| Status | Error | Description |
|
||||
|--------|-------|-------------|
|
||||
| 401 | Unauthorized | API Key missing or invalid |
|
||||
| 403 | Plan expired | Subscription period ended |
|
||||
| 403 | Rate limit exceeded | Request quota temporarily exceeded |
|
||||
| 403 | Quota exceeded | Total usage limit surpassed |
|
||||
| 404 | Not Found | Resource doesn't exist |
|
||||
|
||||
---
|
||||
|
||||
## User Management APIs
|
||||
|
||||
### [POST] Create User
|
||||
|
||||
Creates a new user for sleep tracking.
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
POST https://api.asleep.ai/ai/v1/users
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
```
|
||||
|
||||
**Request Body (Optional):**
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"birth_year": 1990,
|
||||
"birth_month": 5,
|
||||
"birth_day": 15,
|
||||
"gender": "male",
|
||||
"height": 175.5,
|
||||
"weight": 70.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Metadata Fields:**
|
||||
- `birth_year` (Integer): User's birth year
|
||||
- `birth_month` (Integer): User's birth month (1-12)
|
||||
- `birth_day` (Integer): User's birth day (1-31)
|
||||
- `gender` (String): One of: `male`, `female`, `non_binary`, `other`, `prefer_not_to_say`
|
||||
- `height` (Float): Height in cm (0-300)
|
||||
- `weight` (Float): Weight in kg (0-1000)
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"detail": "success",
|
||||
"result": {
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X POST "https://api.asleep.ai/ai/v1/users" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"metadata": {
|
||||
"birth_year": 1990,
|
||||
"gender": "male",
|
||||
"height": 175.5,
|
||||
"weight": 70.0
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
"https://api.asleep.ai/ai/v1/users",
|
||||
headers={"x-api-key": "YOUR_API_KEY"},
|
||||
json={
|
||||
"metadata": {
|
||||
"birth_year": 1990,
|
||||
"gender": "male",
|
||||
"height": 175.5,
|
||||
"weight": 70.0
|
||||
}
|
||||
}
|
||||
)
|
||||
user_id = response.json()["result"]["user_id"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [GET] Get User
|
||||
|
||||
Retrieves user information and last session data.
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
GET https://api.asleep.ai/ai/v1/users/{user_id}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
- `user_id` (String): User identifier
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"detail": "success",
|
||||
"result": {
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"to_be_deleted": false,
|
||||
"last_session_info": {
|
||||
"session_id": "abc123",
|
||||
"state": "COMPLETE",
|
||||
"session_start_time": "2024-01-20T22:00:00+00:00",
|
||||
"session_end_time": "2024-01-21T06:30:00+00:00"
|
||||
},
|
||||
"metadata": {
|
||||
"birth_year": 1990,
|
||||
"birth_month": 5,
|
||||
"birth_day": 15,
|
||||
"gender": "male",
|
||||
"height": 175.5,
|
||||
"weight": 70.0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Session States:**
|
||||
- `OPEN`: Session in progress, audio uploads available
|
||||
- `CLOSED`: Session terminated, analysis in progress
|
||||
- `COMPLETE`: All analysis completed
|
||||
|
||||
**Error Response (404):**
|
||||
```json
|
||||
{
|
||||
"detail": "user does not exist"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X GET "https://api.asleep.ai/ai/v1/users/USER_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.asleep.ai/ai/v1/users/{user_id}",
|
||||
headers={"x-api-key": "YOUR_API_KEY"}
|
||||
)
|
||||
user_data = response.json()["result"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [DELETE] Delete User
|
||||
|
||||
Permanently removes a user and all associated data.
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
DELETE https://api.asleep.ai/ai/v1/users/{user_id}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
- `user_id` (String): User identifier
|
||||
|
||||
**Response (204 No Content):**
|
||||
User information successfully deleted.
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
401 Unauthorized:
|
||||
```json
|
||||
{
|
||||
"detail": "user_id is invalid"
|
||||
}
|
||||
```
|
||||
|
||||
404 Not Found:
|
||||
```json
|
||||
{
|
||||
"detail": "user does not exist"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X DELETE "https://api.asleep.ai/ai/v1/users/USER_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.delete(
|
||||
f"https://api.asleep.ai/ai/v1/users/{user_id}",
|
||||
headers={"x-api-key": "YOUR_API_KEY"}
|
||||
)
|
||||
# 204 No Content on success
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management APIs
|
||||
|
||||
### [GET] Get Session
|
||||
|
||||
Retrieves comprehensive sleep analysis data for a specific session.
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
GET https://api.asleep.ai/data/v3/sessions/{session_id}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
x-user-id: USER_ID
|
||||
timezone: Asia/Seoul # Optional, defaults to UTC
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
- `session_id` (String): Session identifier
|
||||
|
||||
**Query Parameters:**
|
||||
None
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"detail": "success",
|
||||
"result": {
|
||||
"timezone": "UTC",
|
||||
"peculiarities": [],
|
||||
"missing_data_ratio": 0.0,
|
||||
"session": {
|
||||
"id": "session123",
|
||||
"state": "COMPLETE",
|
||||
"start_time": "2024-01-20T22:00:00+00:00",
|
||||
"end_time": "2024-01-21T06:30:00+00:00",
|
||||
"sleep_stages": [0, 0, 1, 1, 2, 3, 2, 1, 0],
|
||||
"snoring_stages": [0, 0, 0, 1, 1, 0, 0, 0, 0]
|
||||
},
|
||||
"stat": {
|
||||
"sleep_time": "06:30:00",
|
||||
"sleep_index": 85.5,
|
||||
"sleep_latency": 900,
|
||||
"time_in_bed": 30600,
|
||||
"time_in_sleep": 27000,
|
||||
"time_in_light": 13500,
|
||||
"time_in_deep": 6750,
|
||||
"time_in_rem": 6750,
|
||||
"sleep_efficiency": 88.24,
|
||||
"waso_count": 2,
|
||||
"longest_waso": 300,
|
||||
"sleep_cycle": [
|
||||
{
|
||||
"order": 1,
|
||||
"start_time": "2024-01-20T22:15:00+00:00",
|
||||
"end_time": "2024-01-21T01:30:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Sleep Stages Values:**
|
||||
- `-1`: Unknown/No data
|
||||
- `0`: Wake
|
||||
- `1`: Light sleep
|
||||
- `2`: Deep sleep
|
||||
- `3`: REM sleep
|
||||
|
||||
**Snoring Stages Values:**
|
||||
- `0`: No snoring
|
||||
- `1`: Snoring detected
|
||||
|
||||
**Peculiarities:**
|
||||
- `IN_PROGRESS`: Session still being analyzed
|
||||
- `NEVER_SLEPT`: No sleep detected in session
|
||||
- `TOO_SHORT_FOR_ANALYSIS`: Session duration < 5 minutes
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
400 Bad Request:
|
||||
```json
|
||||
{
|
||||
"detail": "Invalid timezone format"
|
||||
}
|
||||
```
|
||||
|
||||
404 Not Found:
|
||||
```json
|
||||
{
|
||||
"detail": "Session not found"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X GET "https://api.asleep.ai/data/v3/sessions/SESSION_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "x-user-id: USER_ID" \
|
||||
-H "timezone: UTC"
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.asleep.ai/data/v3/sessions/{session_id}",
|
||||
headers={
|
||||
"x-api-key": "YOUR_API_KEY",
|
||||
"x-user-id": user_id,
|
||||
"timezone": "UTC"
|
||||
}
|
||||
)
|
||||
session_data = response.json()["result"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [GET] List Sessions
|
||||
|
||||
Retrieves multiple sessions with filtering and pagination.
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
GET https://api.asleep.ai/data/v1/sessions
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
x-user-id: USER_ID
|
||||
timezone: UTC # Optional
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| date_gte | String (YYYY-MM-DD) | No | - | Sessions on or after this date |
|
||||
| date_lte | String (YYYY-MM-DD) | No | - | Sessions on or before this date |
|
||||
| order_by | String (ASC/DESC) | No | DESC | Sort direction by start time |
|
||||
| offset | Integer | No | 0 | Number of records to skip |
|
||||
| limit | Integer (0-100) | No | 20 | Maximum records per request |
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"detail": "success",
|
||||
"result": {
|
||||
"timezone": "UTC",
|
||||
"sleep_session_list": [
|
||||
{
|
||||
"session_id": "session123",
|
||||
"state": "COMPLETE",
|
||||
"session_start_time": "2024-01-20T22:00:00+00:00",
|
||||
"session_end_time": "2024-01-21T06:30:00+00:00",
|
||||
"created_timezone": "UTC",
|
||||
"unexpected_end_time": null,
|
||||
"last_received_seq_num": 156,
|
||||
"time_in_bed": 30600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response (400):**
|
||||
```json
|
||||
{
|
||||
"detail": "Invalid timezone"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X GET "https://api.asleep.ai/data/v1/sessions?date_gte=2024-01-01&limit=10" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "x-user-id: USER_ID"
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get(
|
||||
"https://api.asleep.ai/data/v1/sessions",
|
||||
headers={
|
||||
"x-api-key": "YOUR_API_KEY",
|
||||
"x-user-id": user_id
|
||||
},
|
||||
params={
|
||||
"date_gte": "2024-01-01",
|
||||
"date_lte": "2024-01-31",
|
||||
"limit": 50,
|
||||
"order_by": "DESC"
|
||||
}
|
||||
)
|
||||
sessions = response.json()["result"]["sleep_session_list"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [DELETE] Delete Session
|
||||
|
||||
Permanently removes a session and all associated data.
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
DELETE https://api.asleep.ai/ai/v1/sessions/{session_id}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
x-user-id: USER_ID
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
- `session_id` (String): Session identifier
|
||||
|
||||
**Response (204 No Content):**
|
||||
Session, uploaded audio, and analysis data successfully deleted.
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
401 Unauthorized:
|
||||
```json
|
||||
{
|
||||
"detail": "x-user-id is invalid"
|
||||
}
|
||||
```
|
||||
|
||||
404 Not Found (User):
|
||||
```json
|
||||
{
|
||||
"detail": "user does not exist"
|
||||
}
|
||||
```
|
||||
|
||||
404 Not Found (Session):
|
||||
```json
|
||||
{
|
||||
"detail": "session does not exist"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X DELETE "https://api.asleep.ai/ai/v1/sessions/SESSION_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "x-user-id: USER_ID"
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.delete(
|
||||
f"https://api.asleep.ai/ai/v1/sessions/{session_id}",
|
||||
headers={
|
||||
"x-api-key": "YOUR_API_KEY",
|
||||
"x-user-id": user_id
|
||||
}
|
||||
)
|
||||
# 204 No Content on success
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Statistics APIs
|
||||
|
||||
### [GET] Get Average Stats
|
||||
|
||||
Retrieves average sleep metrics over a specified time period (up to 100 days).
|
||||
|
||||
**Endpoint:**
|
||||
```
|
||||
GET https://api.asleep.ai/data/v1/users/{user_id}/average-stats
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-api-key: YOUR_API_KEY
|
||||
timezone: UTC # Optional
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
- `user_id` (String): User identifier
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| start_date | String (YYYY-MM-DD) | Yes | Period start date |
|
||||
| end_date | String (YYYY-MM-DD) | Yes | Period end date (max 100 days from start) |
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"detail": "success",
|
||||
"result": {
|
||||
"period": {
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-01-31",
|
||||
"days": 31
|
||||
},
|
||||
"peculiarities": [],
|
||||
"average_stats": {
|
||||
"start_time": "22:30:00",
|
||||
"end_time": "06:45:00",
|
||||
"sleep_time": "07:15:00",
|
||||
"wake_time": "06:45:00",
|
||||
"sleep_latency": 900,
|
||||
"wakeup_latency": 300,
|
||||
"time_in_bed": 30600,
|
||||
"time_in_sleep_period": 29700,
|
||||
"time_in_sleep": 26100,
|
||||
"time_in_wake": 3600,
|
||||
"time_in_light": 13050,
|
||||
"time_in_deep": 6525,
|
||||
"time_in_rem": 6525,
|
||||
"time_in_snoring": 1800,
|
||||
"time_in_no_snoring": 24300,
|
||||
"sleep_efficiency": 85.29,
|
||||
"wake_ratio": 0.12,
|
||||
"sleep_ratio": 0.88,
|
||||
"light_ratio": 0.50,
|
||||
"deep_ratio": 0.25,
|
||||
"rem_ratio": 0.25,
|
||||
"snoring_ratio": 0.07,
|
||||
"no_snoring_ratio": 0.93,
|
||||
"waso_count": 2.5,
|
||||
"longest_waso": 420,
|
||||
"sleep_cycle_count": 4.2,
|
||||
"snoring_count": 15.3
|
||||
},
|
||||
"never_slept_sessions": [],
|
||||
"slept_sessions": [
|
||||
{
|
||||
"session_id": "session123",
|
||||
"session_start_time": "2024-01-20T22:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Metrics Explanation:**
|
||||
|
||||
**Time Metrics** (HH:MM:SS format or seconds):
|
||||
- `start_time`: Average bedtime
|
||||
- `end_time`: Average wake time
|
||||
- `sleep_time`: Average time of falling asleep
|
||||
- `wake_time`: Average time of waking up
|
||||
- `sleep_latency`: Average time to fall asleep (seconds)
|
||||
- `wakeup_latency`: Average time from wake to getting up (seconds)
|
||||
- `time_in_bed`: Average total time in bed (seconds)
|
||||
- `time_in_sleep_period`: Average time from sleep onset to wake (seconds)
|
||||
- `time_in_sleep`: Average actual sleep time (seconds)
|
||||
- `time_in_wake`: Average wake time during sleep period (seconds)
|
||||
|
||||
**Sleep Stage Durations** (seconds):
|
||||
- `time_in_light`: Average light sleep duration
|
||||
- `time_in_deep`: Average deep sleep duration
|
||||
- `time_in_rem`: Average REM sleep duration
|
||||
|
||||
**Snoring Metrics** (seconds):
|
||||
- `time_in_snoring`: Average snoring duration
|
||||
- `time_in_no_snoring`: Average non-snoring duration
|
||||
|
||||
**Ratio Metrics** (0-1 decimal):
|
||||
- `sleep_efficiency`: Sleep time / Time in bed
|
||||
- `wake_ratio`, `sleep_ratio`: Wake/sleep proportions
|
||||
- `light_ratio`, `deep_ratio`, `rem_ratio`: Sleep stage proportions
|
||||
- `snoring_ratio`, `no_snoring_ratio`: Snoring proportions
|
||||
|
||||
**Event Counts**:
|
||||
- `waso_count`: Average wake after sleep onset episodes
|
||||
- `longest_waso`: Average longest wake episode (seconds)
|
||||
- `sleep_cycle_count`: Average number of sleep cycles
|
||||
- `snoring_count`: Average snoring episodes
|
||||
|
||||
**Peculiarities:**
|
||||
- `NO_BREATHING_STABILITY`: Inconsistent breathing data
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
400 Bad Request:
|
||||
```json
|
||||
{
|
||||
"detail": "The period should be less than or equal to 100 days"
|
||||
}
|
||||
```
|
||||
|
||||
404 Not Found:
|
||||
```json
|
||||
{
|
||||
"detail": "Unable to find the user of id {user_id}"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (curl):**
|
||||
```bash
|
||||
curl -X GET "https://api.asleep.ai/data/v1/users/USER_ID/average-stats?start_date=2024-01-01&end_date=2024-01-31" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "timezone: UTC"
|
||||
```
|
||||
|
||||
**Example (Python):**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.asleep.ai/data/v1/users/{user_id}/average-stats",
|
||||
headers={
|
||||
"x-api-key": "YOUR_API_KEY",
|
||||
"timezone": "UTC"
|
||||
},
|
||||
params={
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-01-31"
|
||||
}
|
||||
)
|
||||
stats = response.json()["result"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
The Asleep API implements rate limiting to ensure fair usage:
|
||||
|
||||
- **Rate Limit Exceeded (403)**: Temporary quota exceeded
|
||||
- **Quota Exceeded (403)**: Total usage limit reached
|
||||
- **Plan Expired (403)**: Subscription period ended
|
||||
|
||||
Monitor your usage in the [Asleep Dashboard](https://dashboard.asleep.ai).
|
||||
|
||||
**Best Practices:**
|
||||
- Implement exponential backoff for retries
|
||||
- Cache responses when appropriate
|
||||
- Batch requests when possible
|
||||
- Monitor usage proactively
|
||||
|
||||
---
|
||||
|
||||
## API Versioning
|
||||
|
||||
The Asleep API uses versioned endpoints (e.g., `/v1/`, `/v3/`). Version upgrades occur when:
|
||||
|
||||
- Renaming response object fields
|
||||
- Modifying data types or enum values
|
||||
- Restructuring response objects
|
||||
- Introducing breaking changes
|
||||
|
||||
Non-breaking changes (like adding new fields) don't trigger version upgrades.
|
||||
|
||||
**Current Versions:**
|
||||
- User Management: `/ai/v1/`
|
||||
- Session Data: `/data/v3/` (Get Session), `/data/v1/` (List Sessions)
|
||||
- Statistics: `/data/v1/`
|
||||
594
skills/sleeptrack-be/references/webhook_implementation_guide.md
Normal file
594
skills/sleeptrack-be/references/webhook_implementation_guide.md
Normal file
@@ -0,0 +1,594 @@
|
||||
# Webhook Implementation Guide
|
||||
|
||||
This reference provides complete webhook handler implementations for both Python and Node.js, including best practices and payload examples.
|
||||
|
||||
## Flask Webhook Handler (Python)
|
||||
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
import os
|
||||
import logging
|
||||
|
||||
app = Flask(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXPECTED_API_KEY = os.getenv("ASLEEP_API_KEY")
|
||||
|
||||
@app.route('/asleep-webhook', methods=['POST'])
|
||||
def asleep_webhook():
|
||||
"""Handle Asleep webhook events"""
|
||||
|
||||
# Verify authentication
|
||||
api_key = request.headers.get('x-api-key')
|
||||
user_id = request.headers.get('x-user-id')
|
||||
|
||||
if api_key != EXPECTED_API_KEY:
|
||||
logger.warning(f"Unauthorized webhook attempt from {request.remote_addr}")
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
# Parse event
|
||||
event = request.json
|
||||
event_type = event.get('event')
|
||||
|
||||
logger.info(f"Received {event_type} event for user {user_id}")
|
||||
|
||||
try:
|
||||
if event_type == 'INFERENCE_COMPLETE':
|
||||
handle_inference_complete(event)
|
||||
elif event_type == 'SESSION_COMPLETE':
|
||||
handle_session_complete(event)
|
||||
else:
|
||||
logger.warning(f"Unknown event type: {event_type}")
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook processing error: {e}", exc_info=True)
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
def handle_inference_complete(event):
|
||||
"""Process incremental sleep data"""
|
||||
session_id = event['session_id']
|
||||
user_id = event['user_id']
|
||||
sleep_stages = event['sleep_stages']
|
||||
|
||||
# Update real-time dashboard
|
||||
update_live_dashboard(session_id, sleep_stages)
|
||||
|
||||
# Store incremental data
|
||||
db.incremental_data.insert_one(event)
|
||||
|
||||
logger.info(f"Processed INFERENCE_COMPLETE for session {session_id}")
|
||||
|
||||
def handle_session_complete(event):
|
||||
"""Process complete sleep report"""
|
||||
session_id = event['session_id']
|
||||
user_id = event['user_id']
|
||||
stat = event['stat']
|
||||
|
||||
# Store complete report
|
||||
db.sleep_reports.insert_one({
|
||||
'user_id': user_id,
|
||||
'session_id': session_id,
|
||||
'date': event['session']['start_time'],
|
||||
'statistics': stat,
|
||||
'session_data': event['session'],
|
||||
'created_at': datetime.now()
|
||||
})
|
||||
|
||||
# Send user notification
|
||||
send_push_notification(user_id, {
|
||||
'title': 'Sleep Report Ready',
|
||||
'body': f"Sleep time: {stat['sleep_time']}, Efficiency: {stat['sleep_efficiency']:.1f}%"
|
||||
})
|
||||
|
||||
# Update user statistics
|
||||
update_user_aggregated_stats(user_id)
|
||||
|
||||
logger.info(f"Processed SESSION_COMPLETE for session {session_id}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
```
|
||||
|
||||
## Express Webhook Handler (Node.js)
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const EXPECTED_API_KEY = process.env.ASLEEP_API_KEY;
|
||||
|
||||
app.post('/asleep-webhook', async (req, res) => {
|
||||
// Verify authentication
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const userId = req.headers['x-user-id'];
|
||||
|
||||
if (apiKey !== EXPECTED_API_KEY) {
|
||||
console.warn('Unauthorized webhook attempt');
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { event, session_id, stat } = req.body;
|
||||
console.log(`Received ${event} event for user ${userId}`);
|
||||
|
||||
try {
|
||||
switch (event) {
|
||||
case 'INFERENCE_COMPLETE':
|
||||
await handleInferenceComplete(req.body);
|
||||
break;
|
||||
case 'SESSION_COMPLETE':
|
||||
await handleSessionComplete(req.body);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown event type: ${event}`);
|
||||
}
|
||||
|
||||
res.status(200).json({ status: 'success' });
|
||||
} catch (error) {
|
||||
console.error('Webhook processing error:', error);
|
||||
res.status(500).json({ error: 'Processing failed' });
|
||||
}
|
||||
});
|
||||
|
||||
async function handleInferenceComplete(event) {
|
||||
const { session_id, user_id, sleep_stages } = event;
|
||||
|
||||
// Update real-time dashboard
|
||||
await updateLiveDashboard(session_id, sleep_stages);
|
||||
|
||||
// Store incremental data
|
||||
await db.collection('incremental_data').insertOne(event);
|
||||
|
||||
console.log(`Processed INFERENCE_COMPLETE for session ${session_id}`);
|
||||
}
|
||||
|
||||
async function handleSessionComplete(event) {
|
||||
const { session_id, user_id, stat, session } = event;
|
||||
|
||||
// Store complete report
|
||||
await db.collection('sleep_reports').insertOne({
|
||||
user_id,
|
||||
session_id,
|
||||
date: session.start_time,
|
||||
statistics: stat,
|
||||
session_data: session,
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
// Send user notification
|
||||
await sendPushNotification(user_id, {
|
||||
title: 'Sleep Report Ready',
|
||||
body: `Sleep time: ${stat.sleep_time}, Efficiency: ${stat.sleep_efficiency.toFixed(1)}%`
|
||||
});
|
||||
|
||||
// Update user statistics
|
||||
await updateUserAggregatedStats(user_id);
|
||||
|
||||
console.log(`Processed SESSION_COMPLETE for session ${session_id}`);
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Webhook server listening on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Webhook Event Payloads
|
||||
|
||||
### INFERENCE_COMPLETE Event
|
||||
|
||||
Sent every 5-40 minutes during sleep tracking with incremental data.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "INFERENCE_COMPLETE",
|
||||
"version": "V3",
|
||||
"timestamp": "2024-01-21T06:15:00Z",
|
||||
"user_id": "user123",
|
||||
"session_id": "session123",
|
||||
"seq_num": 60,
|
||||
"inference_seq_num": 12,
|
||||
"sleep_stages": [1, 1, 2, 2, 2],
|
||||
"snoring_stages": [0, 0, 1, 1, 0]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `event`: Event type identifier
|
||||
- `version`: API version (V3)
|
||||
- `timestamp`: Event timestamp in ISO 8601 format
|
||||
- `user_id`: User identifier
|
||||
- `session_id`: Sleep session identifier
|
||||
- `seq_num`: Sequence number for raw data
|
||||
- `inference_seq_num`: Sequence number for inference results
|
||||
- `sleep_stages`: Array of sleep stage values (see sleep stages reference)
|
||||
- `snoring_stages`: Array of snoring detection values (0 = no snoring, 1 = snoring)
|
||||
|
||||
### SESSION_COMPLETE Event
|
||||
|
||||
Sent when sleep session ends with complete analysis.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "SESSION_COMPLETE",
|
||||
"version": "V3",
|
||||
"timestamp": "2024-01-21T06:30:00Z",
|
||||
"user_id": "user123",
|
||||
"session_id": "session123",
|
||||
"session": {
|
||||
"id": "session123",
|
||||
"state": "COMPLETE",
|
||||
"start_time": "2024-01-20T22:00:00+00:00",
|
||||
"end_time": "2024-01-21T06:30:00+00:00",
|
||||
"sleep_stages": [0, 0, 1, 1, 2, 3, 2, 1, 0],
|
||||
"snoring_stages": [0, 0, 0, 1, 1, 0, 0, 0, 0]
|
||||
},
|
||||
"stat": {
|
||||
"sleep_time": "06:30:00",
|
||||
"sleep_efficiency": 88.24,
|
||||
"time_in_bed": 30600,
|
||||
"time_in_sleep": 27000,
|
||||
"time_in_wake": 3600,
|
||||
"time_in_light": 14400,
|
||||
"time_in_deep": 7200,
|
||||
"time_in_rem": 5400,
|
||||
"waso_count": 2,
|
||||
"sleep_latency": 900,
|
||||
"sleep_cycle": [
|
||||
{
|
||||
"index": 0,
|
||||
"start_time": "2024-01-20T22:15:00+00:00",
|
||||
"end_time": "2024-01-21T01:45:00+00:00"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"start_time": "2024-01-21T01:45:00+00:00",
|
||||
"end_time": "2024-01-21T05:15:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Sleep Stage Values:**
|
||||
- `-1`: Unknown/No data
|
||||
- `0`: Wake
|
||||
- `1`: Light sleep
|
||||
- `2`: Deep sleep
|
||||
- `3`: REM sleep
|
||||
|
||||
## Webhook Best Practices
|
||||
|
||||
### 1. Idempotency
|
||||
|
||||
Handle duplicate webhook deliveries gracefully:
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
session_id = event['session_id']
|
||||
|
||||
# Check if already processed
|
||||
if db.processed_webhooks.find_one({'session_id': session_id, 'event': 'SESSION_COMPLETE'}):
|
||||
logger.info(f"Session {session_id} already processed, skipping")
|
||||
return
|
||||
|
||||
# Process event
|
||||
save_sleep_report(event)
|
||||
|
||||
# Mark as processed
|
||||
db.processed_webhooks.insert_one({
|
||||
'session_id': session_id,
|
||||
'event': 'SESSION_COMPLETE',
|
||||
'processed_at': datetime.now()
|
||||
})
|
||||
```
|
||||
|
||||
**Node.js:**
|
||||
```javascript
|
||||
async function handleSessionComplete(event) {
|
||||
const sessionId = event.session_id;
|
||||
|
||||
// Check if already processed
|
||||
const existing = await db.collection('processed_webhooks').findOne({
|
||||
session_id: sessionId,
|
||||
event: 'SESSION_COMPLETE'
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(`Session ${sessionId} already processed, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process event
|
||||
await saveSleepReport(event);
|
||||
|
||||
// Mark as processed
|
||||
await db.collection('processed_webhooks').insertOne({
|
||||
session_id: sessionId,
|
||||
event: 'SESSION_COMPLETE',
|
||||
processed_at: new Date()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Asynchronous Processing
|
||||
|
||||
Process webhooks asynchronously to respond quickly:
|
||||
|
||||
**Python (Celery):**
|
||||
```python
|
||||
from celery import Celery
|
||||
|
||||
celery = Celery('tasks', broker='redis://localhost:6379')
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
event = request.json
|
||||
|
||||
# Queue for async processing
|
||||
process_webhook_async.delay(event)
|
||||
|
||||
# Respond immediately
|
||||
return jsonify({"status": "queued"}), 200
|
||||
|
||||
@celery.task
|
||||
def process_webhook_async(event):
|
||||
"""Process webhook asynchronously"""
|
||||
if event['event'] == 'SESSION_COMPLETE':
|
||||
handle_session_complete(event)
|
||||
```
|
||||
|
||||
**Node.js (Bull):**
|
||||
```javascript
|
||||
const Queue = require('bull');
|
||||
|
||||
const webhookQueue = new Queue('asleep-webhooks', {
|
||||
redis: { host: 'localhost', port: 6379 }
|
||||
});
|
||||
|
||||
app.post('/webhook', async (req, res) => {
|
||||
const event = req.body;
|
||||
|
||||
// Queue for async processing
|
||||
await webhookQueue.add(event);
|
||||
|
||||
// Respond immediately
|
||||
res.status(200).json({ status: 'queued' });
|
||||
});
|
||||
|
||||
// Process queued webhooks
|
||||
webhookQueue.process(async (job) => {
|
||||
const event = job.data;
|
||||
|
||||
if (event.event === 'SESSION_COMPLETE') {
|
||||
await handleSessionComplete(event);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Security
|
||||
|
||||
Always verify webhook authenticity:
|
||||
|
||||
```python
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
# Verify API key
|
||||
api_key = request.headers.get('x-api-key')
|
||||
if api_key != EXPECTED_API_KEY:
|
||||
logger.warning(f"Unauthorized webhook from {request.remote_addr}")
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
# Verify user ID presence
|
||||
user_id = request.headers.get('x-user-id')
|
||||
if not user_id:
|
||||
logger.warning("Missing x-user-id header")
|
||||
return jsonify({"error": "Missing user ID"}), 400
|
||||
|
||||
# Process webhook
|
||||
# ...
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
Implement robust error handling:
|
||||
|
||||
```python
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
try:
|
||||
event = request.json
|
||||
event_type = event.get('event')
|
||||
|
||||
if event_type == 'SESSION_COMPLETE':
|
||||
handle_session_complete(event)
|
||||
elif event_type == 'INFERENCE_COMPLETE':
|
||||
handle_inference_complete(event)
|
||||
else:
|
||||
logger.warning(f"Unknown event type: {event_type}")
|
||||
return jsonify({"error": "Unknown event type"}), 400
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Processing error: {e}", exc_info=True)
|
||||
return jsonify({"error": "Internal server error"}), 500
|
||||
```
|
||||
|
||||
### 5. Logging
|
||||
|
||||
Log all webhook events for debugging:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('webhooks.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
user_id = request.headers.get('x-user-id')
|
||||
event = request.json
|
||||
event_type = event.get('event')
|
||||
|
||||
logger.info(f"Webhook received - Type: {event_type}, User: {user_id}, Session: {event.get('session_id')}")
|
||||
|
||||
try:
|
||||
# Process webhook
|
||||
# ...
|
||||
logger.info(f"Webhook processed successfully - Session: {event.get('session_id')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook processing failed - Session: {event.get('session_id')}, Error: {e}", exc_info=True)
|
||||
```
|
||||
|
||||
## Testing Webhooks Locally
|
||||
|
||||
### Using ngrok
|
||||
|
||||
```bash
|
||||
# Start your local server
|
||||
python app.py # or npm start
|
||||
|
||||
# In another terminal, expose with ngrok
|
||||
ngrok http 5000
|
||||
|
||||
# Use the ngrok URL as webhook URL in Asleep Dashboard
|
||||
# Example: https://abc123.ngrok.io/asleep-webhook
|
||||
```
|
||||
|
||||
### Mock Webhook for Testing
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
def send_test_webhook(url, event_type='SESSION_COMPLETE'):
|
||||
"""Send test webhook to local server"""
|
||||
|
||||
if event_type == 'SESSION_COMPLETE':
|
||||
payload = {
|
||||
"event": "SESSION_COMPLETE",
|
||||
"version": "V3",
|
||||
"timestamp": "2024-01-21T06:30:00Z",
|
||||
"user_id": "test_user",
|
||||
"session_id": "test_session",
|
||||
"session": {
|
||||
"id": "test_session",
|
||||
"state": "COMPLETE",
|
||||
"start_time": "2024-01-20T22:00:00+00:00",
|
||||
"end_time": "2024-01-21T06:30:00+00:00",
|
||||
"sleep_stages": [0, 1, 2, 3, 2, 1, 0]
|
||||
},
|
||||
"stat": {
|
||||
"sleep_time": "06:30:00",
|
||||
"sleep_efficiency": 88.24,
|
||||
"time_in_bed": 30600,
|
||||
"time_in_sleep": 27000
|
||||
}
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"event": "INFERENCE_COMPLETE",
|
||||
"version": "V3",
|
||||
"timestamp": "2024-01-21T06:15:00Z",
|
||||
"user_id": "test_user",
|
||||
"session_id": "test_session",
|
||||
"seq_num": 60,
|
||||
"inference_seq_num": 12,
|
||||
"sleep_stages": [1, 1, 2, 2, 2]
|
||||
}
|
||||
|
||||
headers = {
|
||||
'x-api-key': 'your_api_key',
|
||||
'x-user-id': 'test_user',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {response.json()}")
|
||||
|
||||
# Test locally
|
||||
send_test_webhook('http://localhost:5000/asleep-webhook')
|
||||
```
|
||||
|
||||
## Common Webhook Patterns
|
||||
|
||||
### Real-time Dashboard Updates
|
||||
|
||||
```python
|
||||
def handle_inference_complete(event):
|
||||
"""Update real-time dashboard with incremental data"""
|
||||
session_id = event['session_id']
|
||||
user_id = event['user_id']
|
||||
sleep_stages = event['sleep_stages']
|
||||
|
||||
# Broadcast to connected clients via WebSocket
|
||||
socketio.emit('sleep_update', {
|
||||
'session_id': session_id,
|
||||
'sleep_stages': sleep_stages,
|
||||
'timestamp': event['timestamp']
|
||||
}, room=user_id)
|
||||
```
|
||||
|
||||
### Sleep Report Notifications
|
||||
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
"""Send notification when sleep report is ready"""
|
||||
user_id = event['user_id']
|
||||
stat = event['stat']
|
||||
|
||||
# Send push notification
|
||||
send_push_notification(user_id, {
|
||||
'title': 'Your Sleep Report is Ready',
|
||||
'body': f"You slept for {stat['sleep_time']} with {stat['sleep_efficiency']:.1f}% efficiency",
|
||||
'data': {
|
||||
'session_id': event['session_id'],
|
||||
'action': 'view_report'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Data Aggregation
|
||||
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
"""Update aggregated user statistics"""
|
||||
user_id = event['user_id']
|
||||
stat = event['stat']
|
||||
|
||||
# Update rolling averages
|
||||
db.user_stats.update_one(
|
||||
{'user_id': user_id},
|
||||
{
|
||||
'$inc': {
|
||||
'total_sessions': 1,
|
||||
'total_sleep_time': stat['time_in_sleep']
|
||||
},
|
||||
'$push': {
|
||||
'recent_efficiency': {
|
||||
'$each': [stat['sleep_efficiency']],
|
||||
'$slice': -30 # Keep last 30 sessions
|
||||
}
|
||||
}
|
||||
},
|
||||
upsert=True
|
||||
)
|
||||
```
|
||||
821
skills/sleeptrack-be/references/webhook_reference.md
Normal file
821
skills/sleeptrack-be/references/webhook_reference.md
Normal file
@@ -0,0 +1,821 @@
|
||||
# Asleep Webhook Reference
|
||||
|
||||
This reference provides comprehensive documentation for implementing Asleep webhooks in backend applications.
|
||||
|
||||
## Overview
|
||||
|
||||
Asleep webhooks enable real-time notifications about sleep session events. The system sends HTTP POST requests to your configured callback URL when specific events occur.
|
||||
|
||||
## Webhook Configuration
|
||||
|
||||
Webhooks are configured by providing a callback URL during session operations (via SDK) or through the Asleep Dashboard.
|
||||
|
||||
**Callback URL Requirements:**
|
||||
- Must be publicly accessible HTTPS endpoint
|
||||
- Should respond with 2xx status code
|
||||
- Should handle requests within 30 seconds
|
||||
|
||||
## Authentication
|
||||
|
||||
Webhook requests include authentication headers:
|
||||
|
||||
```http
|
||||
x-api-key: YOUR_API_KEY
|
||||
x-user-id: USER_ID
|
||||
```
|
||||
|
||||
**Security Best Practices:**
|
||||
- Verify the `x-api-key` matches your expected API key
|
||||
- Validate the `x-user-id` belongs to your system
|
||||
- Use HTTPS for your webhook endpoint
|
||||
- Implement request signing if needed
|
||||
- Log all webhook attempts for audit
|
||||
|
||||
## Supported Events
|
||||
|
||||
Asleep webhooks support two primary event types:
|
||||
|
||||
### 1. INFERENCE_COMPLETE
|
||||
|
||||
Triggered during sleep session analysis at regular intervals (every 5 or 40 minutes).
|
||||
|
||||
**Use Cases:**
|
||||
- Real-time sleep stage monitoring
|
||||
- Live dashboard updates
|
||||
- Progressive data analysis
|
||||
- User notifications during tracking
|
||||
|
||||
**Timing:**
|
||||
- Fires every 5 minutes during active tracking
|
||||
- May also fire at 40-minute intervals
|
||||
- Multiple events per session
|
||||
|
||||
### 2. SESSION_COMPLETE
|
||||
|
||||
Triggered when complete sleep session analysis finishes.
|
||||
|
||||
**Use Cases:**
|
||||
- Final report generation
|
||||
- User notifications
|
||||
- Data storage
|
||||
- Statistics calculation
|
||||
- Integration with other systems
|
||||
|
||||
**Timing:**
|
||||
- Fires once per session
|
||||
- Occurs after session end
|
||||
- Contains complete analysis
|
||||
|
||||
## Webhook Payload Schemas
|
||||
|
||||
### INFERENCE_COMPLETE Payload
|
||||
|
||||
Provides incremental sleep analysis data.
|
||||
|
||||
**Structure:**
|
||||
```json
|
||||
{
|
||||
"event": "INFERENCE_COMPLETE",
|
||||
"version": "V3",
|
||||
"timestamp": "2024-01-21T06:15:00Z",
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"session_id": "session123",
|
||||
"seq_num": 60,
|
||||
"inference_seq_num": 12,
|
||||
"sleep_stages": [1, 1, 2, 2, 2],
|
||||
"breath_stages": [0, 0, 0, 0, 0],
|
||||
"snoring_stages": [0, 0, 1, 1, 0],
|
||||
"time_window": {
|
||||
"start": "2024-01-21T06:10:00Z",
|
||||
"end": "2024-01-21T06:15:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| event | String | Always "INFERENCE_COMPLETE" |
|
||||
| version | String | API version (V1, V2, V3) |
|
||||
| timestamp | String (ISO 8601) | Event generation time |
|
||||
| user_id | String | User identifier |
|
||||
| session_id | String | Session identifier |
|
||||
| seq_num | Integer | Audio data upload sequence number |
|
||||
| inference_seq_num | Integer | Analysis sequence (5-minute increments) |
|
||||
| sleep_stages | Array[Integer] | Sleep stage values for time window |
|
||||
| breath_stages | Array[Integer] | Breathing stability indicators |
|
||||
| snoring_stages | Array[Integer] | Snoring detection values |
|
||||
| time_window | Object | Time range for this analysis chunk |
|
||||
|
||||
**Sleep Stage Values:**
|
||||
- `-1`: Unknown/No data
|
||||
- `0`: Wake
|
||||
- `1`: Light sleep
|
||||
- `2`: Deep sleep
|
||||
- `3`: REM sleep
|
||||
|
||||
**Snoring Stage Values:**
|
||||
- `0`: No snoring
|
||||
- `1`: Snoring detected
|
||||
|
||||
**Example Handler (Python):**
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def handle_inference():
|
||||
# Verify authentication
|
||||
api_key = request.headers.get('x-api-key')
|
||||
user_id = request.headers.get('x-user-id')
|
||||
|
||||
if api_key != EXPECTED_API_KEY:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
# Parse payload
|
||||
data = request.json
|
||||
|
||||
if data['event'] == 'INFERENCE_COMPLETE':
|
||||
session_id = data['session_id']
|
||||
sleep_stages = data['sleep_stages']
|
||||
|
||||
# Process incremental data
|
||||
update_live_dashboard(session_id, sleep_stages)
|
||||
|
||||
# Store for real-time analysis
|
||||
store_incremental_data(data)
|
||||
|
||||
return jsonify({"status": "received"}), 200
|
||||
```
|
||||
|
||||
**Example Handler (Node.js):**
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/webhook', async (req, res) => {
|
||||
// Verify authentication
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const userId = req.headers['x-user-id'];
|
||||
|
||||
if (apiKey !== process.env.ASLEEP_API_KEY) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { event, session_id, sleep_stages } = req.body;
|
||||
|
||||
if (event === 'INFERENCE_COMPLETE') {
|
||||
// Update real-time dashboard
|
||||
await updateLiveDashboard(session_id, sleep_stages);
|
||||
|
||||
// Store incremental data
|
||||
await storeIncrementalData(req.body);
|
||||
}
|
||||
|
||||
res.status(200).json({ status: 'received' });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SESSION_COMPLETE Payload
|
||||
|
||||
Provides comprehensive final sleep analysis.
|
||||
|
||||
**Structure:**
|
||||
```json
|
||||
{
|
||||
"event": "SESSION_COMPLETE",
|
||||
"version": "V3",
|
||||
"timestamp": "2024-01-21T06:30:00Z",
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"session_id": "session123",
|
||||
"session": {
|
||||
"id": "session123",
|
||||
"state": "COMPLETE",
|
||||
"start_time": "2024-01-20T22:00:00+00:00",
|
||||
"end_time": "2024-01-21T06:30:00+00:00",
|
||||
"timezone": "UTC",
|
||||
"sleep_stages": [0, 0, 1, 1, 2, 3, 2, 1, 0],
|
||||
"snoring_stages": [0, 0, 0, 1, 1, 0, 0, 0, 0]
|
||||
},
|
||||
"stat": {
|
||||
"sleep_time": "06:30:00",
|
||||
"sleep_index": 85.5,
|
||||
"sleep_latency": 900,
|
||||
"time_in_bed": 30600,
|
||||
"time_in_sleep": 27000,
|
||||
"time_in_light": 13500,
|
||||
"time_in_deep": 6750,
|
||||
"time_in_rem": 6750,
|
||||
"sleep_efficiency": 88.24,
|
||||
"waso_count": 2,
|
||||
"longest_waso": 300,
|
||||
"sleep_cycle": [
|
||||
{
|
||||
"order": 1,
|
||||
"start_time": "2024-01-20T22:15:00+00:00",
|
||||
"end_time": "2024-01-21T01:30:00+00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"peculiarities": []
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| event | String | Always "SESSION_COMPLETE" |
|
||||
| version | String | API version (V1, V2, V3) |
|
||||
| timestamp | String (ISO 8601) | Event generation time |
|
||||
| user_id | String | User identifier |
|
||||
| session_id | String | Session identifier |
|
||||
| session | Object | Complete session data |
|
||||
| stat | Object | Comprehensive sleep statistics |
|
||||
| peculiarities | Array[String] | Special session conditions |
|
||||
|
||||
**Session Object Fields:**
|
||||
- `id`: Session identifier
|
||||
- `state`: Always "COMPLETE" for this event
|
||||
- `start_time`, `end_time`: Session timestamps (ISO 8601)
|
||||
- `timezone`: Timezone of the session
|
||||
- `sleep_stages`: Complete sleep stage timeline
|
||||
- `snoring_stages`: Complete snoring timeline
|
||||
|
||||
**Stat Object Fields:**
|
||||
- `sleep_time`: Total sleep duration (HH:MM:SS)
|
||||
- `sleep_index`: Overall sleep quality score (0-100)
|
||||
- `sleep_latency`: Time to fall asleep (seconds)
|
||||
- `time_in_bed`: Total time in bed (seconds)
|
||||
- `time_in_sleep`: Total actual sleep time (seconds)
|
||||
- `time_in_light/deep/rem`: Stage durations (seconds)
|
||||
- `sleep_efficiency`: Percentage of time spent sleeping
|
||||
- `waso_count`: Wake after sleep onset episodes
|
||||
- `longest_waso`: Longest wake episode (seconds)
|
||||
- `sleep_cycle`: Array of sleep cycle objects
|
||||
|
||||
**Peculiarities:**
|
||||
- `IN_PROGRESS`: Analysis still ongoing (shouldn't occur for COMPLETE)
|
||||
- `NEVER_SLEPT`: No sleep detected
|
||||
- `TOO_SHORT_FOR_ANALYSIS`: Session < 5 minutes
|
||||
- `NO_BREATHING_STABILITY`: Inconsistent breathing data
|
||||
|
||||
**Example Handler (Python):**
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
import logging
|
||||
|
||||
app = Flask(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def handle_session_complete():
|
||||
# Verify authentication
|
||||
api_key = request.headers.get('x-api-key')
|
||||
user_id = request.headers.get('x-user-id')
|
||||
|
||||
if api_key != EXPECTED_API_KEY:
|
||||
logger.warning(f"Unauthorized webhook attempt from {request.remote_addr}")
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
# Parse payload
|
||||
data = request.json
|
||||
|
||||
if data['event'] == 'SESSION_COMPLETE':
|
||||
session_id = data['session_id']
|
||||
stat = data['stat']
|
||||
|
||||
# Store complete report
|
||||
save_sleep_report(user_id, session_id, data)
|
||||
|
||||
# Send user notification
|
||||
notify_user(user_id, {
|
||||
'session_id': session_id,
|
||||
'sleep_time': stat['sleep_time'],
|
||||
'sleep_efficiency': stat['sleep_efficiency'],
|
||||
'sleep_index': stat['sleep_index']
|
||||
})
|
||||
|
||||
# Update user statistics
|
||||
update_user_statistics(user_id)
|
||||
|
||||
# Trigger integrations
|
||||
sync_to_health_platform(user_id, data)
|
||||
|
||||
logger.info(f"Processed SESSION_COMPLETE for {session_id}")
|
||||
|
||||
return jsonify({"status": "processed"}), 200
|
||||
```
|
||||
|
||||
**Example Handler (Node.js):**
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/webhook', async (req, res) => {
|
||||
// Verify authentication
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const userId = req.headers['x-user-id'];
|
||||
|
||||
if (apiKey !== process.env.ASLEEP_API_KEY) {
|
||||
console.warn('Unauthorized webhook attempt');
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { event, session_id, stat } = req.body;
|
||||
|
||||
if (event === 'SESSION_COMPLETE') {
|
||||
try {
|
||||
// Store complete report
|
||||
await saveSleepReport(userId, session_id, req.body);
|
||||
|
||||
// Send user notification
|
||||
await notifyUser(userId, {
|
||||
sessionId: session_id,
|
||||
sleepTime: stat.sleep_time,
|
||||
sleepEfficiency: stat.sleep_efficiency,
|
||||
sleepIndex: stat.sleep_index
|
||||
});
|
||||
|
||||
// Update statistics
|
||||
await updateUserStatistics(userId);
|
||||
|
||||
// Sync to integrations
|
||||
await syncToHealthPlatform(userId, req.body);
|
||||
|
||||
console.log(`Processed SESSION_COMPLETE for ${session_id}`);
|
||||
|
||||
res.status(200).json({ status: 'processed' });
|
||||
} catch (error) {
|
||||
console.error('Webhook processing error:', error);
|
||||
res.status(500).json({ error: 'Processing failed' });
|
||||
}
|
||||
} else {
|
||||
res.status(200).json({ status: 'received' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webhook Versioning
|
||||
|
||||
Webhooks support three format versions for backward compatibility:
|
||||
|
||||
### V1 (Legacy)
|
||||
Original webhook format. Use V3 for new implementations.
|
||||
|
||||
### V2 (Legacy)
|
||||
Updated format with additional fields. Use V3 for new implementations.
|
||||
|
||||
### V3 (Current)
|
||||
Latest format with comprehensive data structures. Recommended for all new integrations.
|
||||
|
||||
**Version Selection:**
|
||||
Configure webhook version through SDK initialization or Dashboard settings.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### 1. Set Up Webhook Endpoint
|
||||
|
||||
Create a public HTTPS endpoint to receive webhook events:
|
||||
|
||||
**Python (Flask):**
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/asleep-webhook', methods=['POST'])
|
||||
def asleep_webhook():
|
||||
# Verify authentication
|
||||
if not verify_webhook(request):
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
# Parse event
|
||||
event = request.json
|
||||
event_type = event.get('event')
|
||||
|
||||
# Route to appropriate handler
|
||||
if event_type == 'INFERENCE_COMPLETE':
|
||||
handle_inference_complete(event)
|
||||
elif event_type == 'SESSION_COMPLETE':
|
||||
handle_session_complete(event)
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
def verify_webhook(request):
|
||||
api_key = request.headers.get('x-api-key')
|
||||
return api_key == EXPECTED_API_KEY
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=443, ssl_context='adhoc')
|
||||
```
|
||||
|
||||
**Node.js (Express):**
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/asleep-webhook', async (req, res) => {
|
||||
// Verify authentication
|
||||
if (!verifyWebhook(req)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { event } = req.body;
|
||||
|
||||
try {
|
||||
switch (event) {
|
||||
case 'INFERENCE_COMPLETE':
|
||||
await handleInferenceComplete(req.body);
|
||||
break;
|
||||
case 'SESSION_COMPLETE':
|
||||
await handleSessionComplete(req.body);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown event type: ${event}`);
|
||||
}
|
||||
|
||||
res.status(200).json({ status: 'success' });
|
||||
} catch (error) {
|
||||
console.error('Webhook error:', error);
|
||||
res.status(500).json({ error: 'Processing failed' });
|
||||
}
|
||||
});
|
||||
|
||||
function verifyWebhook(req) {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
return apiKey === process.env.ASLEEP_API_KEY;
|
||||
}
|
||||
|
||||
// HTTPS server
|
||||
const options = {
|
||||
key: fs.readFileSync('private-key.pem'),
|
||||
cert: fs.readFileSync('certificate.pem')
|
||||
};
|
||||
|
||||
https.createServer(options, app).listen(443);
|
||||
```
|
||||
|
||||
### 2. Configure Webhook URL
|
||||
|
||||
Configure your webhook URL through:
|
||||
- SDK initialization (for mobile apps)
|
||||
- Asleep Dashboard (for backend integrations)
|
||||
|
||||
**SDK Example (Android):**
|
||||
```kotlin
|
||||
AsleepConfig.init(
|
||||
apiKey = "YOUR_API_KEY",
|
||||
userId = "user123",
|
||||
callbackUrl = "https://your-domain.com/asleep-webhook"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Handle Webhook Events
|
||||
|
||||
Implement handlers for each event type:
|
||||
|
||||
**Python Example:**
|
||||
```python
|
||||
def handle_inference_complete(event):
|
||||
"""Process incremental sleep data"""
|
||||
session_id = event['session_id']
|
||||
sleep_stages = event['sleep_stages']
|
||||
|
||||
# Update real-time dashboard
|
||||
redis_client.set(f"session:{session_id}:latest", json.dumps(sleep_stages))
|
||||
|
||||
# Notify connected clients via WebSocket
|
||||
websocket_broadcast(session_id, sleep_stages)
|
||||
|
||||
# Store for analysis
|
||||
db.incremental_data.insert_one(event)
|
||||
|
||||
def handle_session_complete(event):
|
||||
"""Process complete sleep report"""
|
||||
user_id = event['user_id']
|
||||
session_id = event['session_id']
|
||||
stat = event['stat']
|
||||
|
||||
# Store complete report
|
||||
db.sleep_reports.insert_one({
|
||||
'user_id': user_id,
|
||||
'session_id': session_id,
|
||||
'date': event['session']['start_time'],
|
||||
'statistics': stat,
|
||||
'created_at': datetime.now()
|
||||
})
|
||||
|
||||
# Update user's latest statistics
|
||||
update_user_stats(user_id)
|
||||
|
||||
# Send push notification
|
||||
send_notification(user_id, {
|
||||
'title': 'Sleep Report Ready',
|
||||
'body': f"Sleep time: {stat['sleep_time']}, Efficiency: {stat['sleep_efficiency']:.1f}%"
|
||||
})
|
||||
|
||||
# Trigger downstream processes
|
||||
calculate_weekly_trends(user_id)
|
||||
check_sleep_goals(user_id, stat)
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
Implement robust error handling:
|
||||
|
||||
**Retry Logic:**
|
||||
```python
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
|
||||
def process_webhook(event):
|
||||
"""Process webhook with automatic retry"""
|
||||
# Your processing logic here
|
||||
pass
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook_endpoint():
|
||||
try:
|
||||
event = request.json
|
||||
process_webhook(event)
|
||||
return jsonify({"status": "success"}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook processing failed: {e}")
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
```
|
||||
|
||||
**Idempotency:**
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
session_id = event['session_id']
|
||||
|
||||
# Check if already processed
|
||||
if db.processed_webhooks.find_one({'session_id': session_id}):
|
||||
logger.info(f"Session {session_id} already processed")
|
||||
return
|
||||
|
||||
# Process event
|
||||
save_sleep_report(event)
|
||||
|
||||
# Mark as processed
|
||||
db.processed_webhooks.insert_one({
|
||||
'session_id': session_id,
|
||||
'processed_at': datetime.now()
|
||||
})
|
||||
```
|
||||
|
||||
### 5. Testing
|
||||
|
||||
Test webhook handling locally:
|
||||
|
||||
**ngrok for Local Testing:**
|
||||
```bash
|
||||
# Start your local server
|
||||
python app.py
|
||||
|
||||
# In another terminal, expose with ngrok
|
||||
ngrok http 5000
|
||||
|
||||
# Use the ngrok URL as your webhook URL
|
||||
# Example: https://abc123.ngrok.io/webhook
|
||||
```
|
||||
|
||||
**Mock Webhook Requests:**
|
||||
```bash
|
||||
# Test INFERENCE_COMPLETE
|
||||
curl -X POST http://localhost:5000/webhook \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "x-user-id: test_user" \
|
||||
-d '{
|
||||
"event": "INFERENCE_COMPLETE",
|
||||
"version": "V3",
|
||||
"session_id": "test123",
|
||||
"sleep_stages": [1, 1, 2]
|
||||
}'
|
||||
|
||||
# Test SESSION_COMPLETE
|
||||
curl -X POST http://localhost:5000/webhook \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "x-user-id: test_user" \
|
||||
-d '{
|
||||
"event": "SESSION_COMPLETE",
|
||||
"version": "V3",
|
||||
"session_id": "test123",
|
||||
"stat": {
|
||||
"sleep_time": "07:30:00",
|
||||
"sleep_efficiency": 88.5
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security
|
||||
- Always verify `x-api-key` header
|
||||
- Use HTTPS for webhook endpoints
|
||||
- Implement request signing if handling sensitive data
|
||||
- Rate limit webhook endpoint
|
||||
- Log all webhook attempts
|
||||
|
||||
### Reliability
|
||||
- Respond quickly (< 5 seconds ideal)
|
||||
- Process asynchronously if needed
|
||||
- Implement idempotency checks
|
||||
- Handle duplicate events gracefully
|
||||
- Return 2xx status even if processing fails (retry logic)
|
||||
|
||||
### Performance
|
||||
- Use message queues for heavy processing
|
||||
- Implement caching where appropriate
|
||||
- Batch database operations
|
||||
- Monitor webhook response times
|
||||
- Scale horizontally if needed
|
||||
|
||||
### Monitoring
|
||||
- Log all webhook events
|
||||
- Track processing success/failure rates
|
||||
- Monitor response times
|
||||
- Set up alerts for failures
|
||||
- Dashboard for webhook metrics
|
||||
|
||||
### Error Handling
|
||||
- Catch and log all exceptions
|
||||
- Return appropriate HTTP status codes
|
||||
- Implement exponential backoff
|
||||
- Dead letter queue for failed events
|
||||
- Manual review process for failures
|
||||
|
||||
---
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Real-Time Dashboard Updates
|
||||
|
||||
```python
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
event = request.json
|
||||
|
||||
if event['event'] == 'INFERENCE_COMPLETE':
|
||||
# Broadcast to connected WebSocket clients
|
||||
socketio.emit('sleep_update', {
|
||||
'session_id': event['session_id'],
|
||||
'sleep_stages': event['sleep_stages'],
|
||||
'timestamp': event['timestamp']
|
||||
}, room=event['user_id'])
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
```
|
||||
|
||||
### User Notifications
|
||||
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
user_id = event['user_id']
|
||||
stat = event['stat']
|
||||
|
||||
# Generate insights
|
||||
insights = generate_sleep_insights(stat)
|
||||
|
||||
# Send push notification
|
||||
send_push_notification(user_id, {
|
||||
'title': 'Your Sleep Report is Ready!',
|
||||
'body': f"You slept for {stat['sleep_time']} with {stat['sleep_efficiency']:.0f}% efficiency",
|
||||
'data': {
|
||||
'session_id': event['session_id'],
|
||||
'insights': insights
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Data Analytics Pipeline
|
||||
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
# Store in data warehouse
|
||||
bigquery_client.insert_rows_json('sleep_data.sessions', [{
|
||||
'user_id': event['user_id'],
|
||||
'session_id': event['session_id'],
|
||||
'date': event['session']['start_time'],
|
||||
'statistics': json.dumps(event['stat']),
|
||||
'ingested_at': datetime.now().isoformat()
|
||||
}])
|
||||
|
||||
# Trigger analytics jobs
|
||||
trigger_weekly_report_job(event['user_id'])
|
||||
update_cohort_analysis()
|
||||
```
|
||||
|
||||
### Integration with Other Systems
|
||||
|
||||
```python
|
||||
def handle_session_complete(event):
|
||||
user_id = event['user_id']
|
||||
stat = event['stat']
|
||||
|
||||
# Sync to Apple Health
|
||||
sync_to_apple_health(user_id, {
|
||||
'sleep_analysis': stat,
|
||||
'date': event['session']['start_time']
|
||||
})
|
||||
|
||||
# Update CRM
|
||||
update_crm_profile(user_id, {
|
||||
'last_sleep_date': event['session']['start_time'],
|
||||
'avg_sleep_efficiency': calculate_avg_efficiency(user_id)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Webhook Not Received
|
||||
|
||||
**Check:**
|
||||
- Endpoint is publicly accessible
|
||||
- HTTPS is properly configured
|
||||
- Firewall allows incoming requests
|
||||
- Webhook URL is correctly configured
|
||||
- Server is running and healthy
|
||||
|
||||
### Authentication Failures
|
||||
|
||||
**Check:**
|
||||
- `x-api-key` validation logic
|
||||
- API key matches dashboard
|
||||
- Headers are correctly parsed
|
||||
- Case sensitivity of header names
|
||||
|
||||
### Duplicate Events
|
||||
|
||||
**Solution:**
|
||||
```python
|
||||
def handle_webhook(event):
|
||||
event_id = f"{event['session_id']}:{event['event']}:{event['timestamp']}"
|
||||
|
||||
# Check if already processed
|
||||
if redis_client.exists(f"processed:{event_id}"):
|
||||
return
|
||||
|
||||
# Process event
|
||||
process_event(event)
|
||||
|
||||
# Mark as processed (expire after 24 hours)
|
||||
redis_client.setex(f"processed:{event_id}", 86400, "1")
|
||||
```
|
||||
|
||||
### Processing Delays
|
||||
|
||||
**Solution:**
|
||||
```python
|
||||
from celery import Celery
|
||||
|
||||
celery = Celery('tasks', broker='redis://localhost:6379')
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
event = request.json
|
||||
|
||||
# Queue for async processing
|
||||
process_webhook_async.delay(event)
|
||||
|
||||
# Respond immediately
|
||||
return jsonify({"status": "queued"}), 200
|
||||
|
||||
@celery.task
|
||||
def process_webhook_async(event):
|
||||
# Heavy processing here
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **Official Documentation**: https://docs-en.asleep.ai/docs/webhook.md
|
||||
- **API Basics**: https://docs-en.asleep.ai/docs/api-basics.md
|
||||
- **Dashboard**: https://dashboard.asleep.ai
|
||||
Reference in New Issue
Block a user