Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:20 +08:00
commit b1b19cb098
22 changed files with 9598 additions and 0 deletions

View File

@@ -0,0 +1,551 @@
---
name: sleeptrack-android
description: This skill helps Android developers integrate the Asleep SDK for sleep tracking functionality. Use this skill when developers ask about Android implementation, MVVM architecture patterns, permission handling, Jetpack Compose UI, Kotlin coroutines integration, or Android-specific sleep tracking features. This skill provides working code examples from the official sample app.
---
# Sleeptrack Android
## Overview
This skill provides comprehensive guidance for integrating the Asleep sleep tracking SDK into Android applications. It covers Android-specific implementation details including MVVM architecture, permission handling, state management, UI patterns, and lifecycle management.
Use this skill when developers need to:
- Set up Asleep SDK in Android projects
- Implement sleep tracking with proper Android architecture
- Handle Android-specific permissions (microphone, notifications, battery optimization)
- Manage tracking lifecycle with state machines
- Build UI with ViewBinding or Jetpack Compose
- Handle errors and edge cases in Android environment
- Implement foreground services for background tracking
**Prerequisites**: Review the `sleeptrack-foundation` skill first for core concepts including session lifecycle, error codes, and API fundamentals.
## Quick Start
### 1. Add Dependencies
Add to your app-level `build.gradle`:
```gradle
dependencies {
// Core dependencies
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
// Required for Asleep SDK
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.google.code.gson:gson:2.10'
implementation 'ai.asleep:asleepsdk:3.1.4'
// Optional: Hilt for DI
implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-compiler:2.48"
}
```
**Minimum Requirements**:
- minSdk: 24 (Android 7.0)
- targetSdk: 34
- Java: 17
- Kotlin: 1.9.24+
See `references/gradle_setup.md` for complete Gradle configuration.
### 2. Configure Permissions
Add to `AndroidManifest.xml`:
```xml
<manifest>
<!-- Essential permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<!-- Battery optimization -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Notifications (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
```
### 3. Initialize SDK
```kotlin
Asleep.initAsleepConfig(
context = applicationContext,
apiKey = "your_api_key_here",
userId = "unique_user_id",
baseUrl = "https://api.asleep.ai",
callbackUrl = null, // Optional webhook URL
service = "YourAppName",
asleepConfigListener = object : Asleep.AsleepConfigListener {
override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) {
// SDK initialized successfully
}
override fun onFail(errorCode: Int, detail: String) {
Log.e("Asleep", "Init failed: $errorCode - $detail")
}
}
)
```
### 4. Start Sleep Tracking
```kotlin
Asleep.beginSleepTracking(
asleepConfig = asleepConfig,
asleepTrackingListener = object : Asleep.AsleepTrackingListener {
override fun onStart(sessionId: String) {
// Tracking started successfully
}
override fun onPerform(sequence: Int) {
// Called every ~30 seconds (1 sequence)
}
override fun onFinish(sessionId: String?) {
// Tracking completed
}
override fun onFail(errorCode: Int, detail: String) {
// Handle tracking errors
}
},
notificationTitle = "Sleep Tracking Active",
notificationText = "Tap to return to app",
notificationIcon = R.drawable.ic_notification,
notificationClass = MainActivity::class.java
)
```
### 5. Stop Tracking
```kotlin
Asleep.endSleepTracking()
```
## Android Architecture Pattern (MVVM + Hilt)
The recommended architecture follows Android best practices with MVVM pattern, Hilt dependency injection, and proper state management.
### State Management
Define tracking states using sealed classes:
```kotlin
sealed class AsleepState {
data object STATE_IDLE: AsleepState()
data object STATE_INITIALIZING : AsleepState()
data object STATE_INITIALIZED : AsleepState()
data object STATE_TRACKING_STARTING : AsleepState()
data object STATE_TRACKING_STARTED : AsleepState()
data object STATE_TRACKING_STOPPING : AsleepState()
data class STATE_ERROR(val errorCode: AsleepError) : AsleepState()
}
data class AsleepError(val code: Int, val message: String)
```
### Basic ViewModel Pattern
```kotlin
@HiltViewModel
class SleepTrackingViewModel @Inject constructor(
@ApplicationContext private val app: Application
) : ViewModel() {
private val _trackingState = MutableStateFlow<AsleepState>(AsleepState.STATE_IDLE)
val trackingState: StateFlow<AsleepState> = _trackingState
private var config: AsleepConfig? = null
fun initializeSDK(userId: String) {
Asleep.initAsleepConfig(
context = app,
apiKey = BuildConfig.ASLEEP_API_KEY,
userId = userId,
baseUrl = "https://api.asleep.ai",
callbackUrl = null,
service = "MyApp",
asleepConfigListener = object : Asleep.AsleepConfigListener {
override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) {
config = asleepConfig
_trackingState.value = AsleepState.STATE_INITIALIZED
}
override fun onFail(errorCode: Int, detail: String) {
_trackingState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail))
}
}
)
}
fun startTracking() {
config?.let {
Asleep.beginSleepTracking(
asleepConfig = it,
asleepTrackingListener = trackingListener,
notificationTitle = "Sleep Tracking",
notificationText = "Active",
notificationIcon = R.drawable.ic_notification,
notificationClass = MainActivity::class.java
)
}
}
fun stopTracking() {
Asleep.endSleepTracking()
}
}
```
**For complete production-ready ViewModel with real-time data, error handling, and lifecycle management**, see `references/complete_viewmodel_implementation.md`.
### Basic Activity Pattern
```kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: SleepTrackingViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Setup button
binding.btnTrack.setOnClickListener {
when (viewModel.trackingState.value) {
AsleepState.STATE_INITIALIZED -> viewModel.startTracking()
AsleepState.STATE_TRACKING_STARTED -> viewModel.stopTracking()
else -> {}
}
}
// Observe state
lifecycleScope.launch {
viewModel.trackingState.collect { state ->
updateUI(state)
}
}
}
}
```
**For complete Activity implementation with permission handling**, see `references/complete_viewmodel_implementation.md`.
## Permission Handling
Android requires multiple runtime permissions for sleep tracking:
### Required Permissions
1. **RECORD_AUDIO**: Microphone access for sleep sound recording
2. **POST_NOTIFICATIONS**: Android 13+ notification permission
3. **Battery Optimization**: Prevent app from being killed during tracking
### Permission Request Flow
```kotlin
// Request permissions sequentially
when {
!hasMicrophonePermission() -> requestMicrophone()
!hasNotificationPermission() -> requestNotification()
!isBatteryOptimizationIgnored() -> requestBatteryOptimization()
else -> allPermissionsGranted()
}
```
### Best Practices
1. **Request in sequence**: Request one permission at a time for better UX
2. **Show rationale**: Explain why each permission is needed before requesting
3. **Handle denial**: Provide fallback or guide users to settings
4. **Check on resume**: Re-check permissions when app resumes
```kotlin
override fun onResume() {
super.onResume()
if (!hasRequiredPermissions() && Asleep.isSleepTrackingAlive(applicationContext)) {
handlePermissionLoss()
}
}
```
**For complete PermissionManager implementation**, see `references/complete_viewmodel_implementation.md`.
## Error Handling
Distinguish between critical errors and warnings:
### Error Classification
```kotlin
fun isWarning(errorCode: Int): Boolean {
return errorCode in setOf(
AsleepErrorCode.ERR_AUDIO_SILENCED,
AsleepErrorCode.ERR_UPLOAD_FAILED
)
}
fun handleError(error: AsleepError) {
if (isWarning(error.code)) {
// Show warning, continue tracking
Toast.makeText(context, getUserFriendlyMessage(error), Toast.LENGTH_SHORT).show()
} else {
// Critical error - stop tracking
stopTracking()
showErrorDialog(getUserFriendlyMessage(error))
}
}
```
## UI Patterns
### ViewBinding Example
```kotlin
class TrackingFragment : Fragment() {
private var _binding: FragmentTrackingBinding? = null
private val binding get() = _binding!!
private val viewModel: SleepTrackingViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.trackingState.collect { state ->
when (state) {
AsleepState.STATE_TRACKING_STARTED -> {
binding.btnTrack.text = "Stop Tracking"
}
AsleepState.STATE_INITIALIZED -> {
binding.btnTrack.text = "Start Tracking"
}
else -> {}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
```
### Jetpack Compose Example
```kotlin
@Composable
fun SleepTrackingScreen(
viewModel: SleepTrackingViewModel = hiltViewModel()
) {
val trackingState by viewModel.trackingState.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when (trackingState) {
AsleepState.STATE_TRACKING_STARTED -> {
Button(onClick = { viewModel.stopTracking() }) {
Text("Stop Tracking")
}
}
AsleepState.STATE_INITIALIZED -> {
Button(onClick = { viewModel.startTracking() }) {
Text("Start Sleep Tracking")
}
}
is AsleepState.STATE_ERROR -> {
ErrorDisplay(error = (trackingState as AsleepState.STATE_ERROR).errorCode)
}
else -> {
CircularProgressIndicator()
}
}
}
}
```
**For complete UI implementations with Material3 components**, see `references/ui_implementation_guide.md`.
## Lifecycle Management
### Handle App Lifecycle Events
```kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
// Reconnect to existing tracking session
if (Asleep.isSleepTrackingAlive(applicationContext)) {
viewModel.reconnectToTracking()
}
}
override fun onStop() {
super.onStop()
// Tracking continues in background via foreground service
// No action needed
}
override fun onDestroy() {
super.onDestroy()
// Do NOT call endSleepTracking() here
// Service continues in background
}
}
```
### Foreground Service
The Asleep SDK automatically manages a foreground service during tracking. The notification keeps the service alive:
```kotlin
Asleep.beginSleepTracking(
asleepConfig = config,
asleepTrackingListener = listener,
notificationTitle = "Sleep Tracking Active",
notificationText = "Tracking your sleep patterns",
notificationIcon = R.drawable.ic_sleep,
notificationClass = MainActivity::class.java // Tapping notification opens this
)
```
The service will:
- Keep the app alive during sleep tracking
- Show persistent notification
- Maintain microphone access
- Continue even if user swipes away the app
## Real-Time Sleep Data
Access preliminary sleep data during tracking:
```kotlin
// In ViewModel
override fun onPerform(sequence: Int) {
// Check after sequence 10, then every 10 sequences
if (sequence > 10 && sequence % 10 == 0) {
getCurrentSleepData()
}
}
private fun getCurrentSleepData() {
Asleep.getCurrentSleepData(
asleepSleepDataListener = object : Asleep.AsleepSleepDataListener {
override fun onSleepDataReceived(session: Session) {
val currentSleepStage = session.sleepStages?.lastOrNull()
val currentSnoringStage = session.snoringStages?.lastOrNull()
Log.d("Sleep", "Current stage: $currentSleepStage")
}
override fun onFail(errorCode: Int, detail: String) {
Log.e("Sleep", "Failed to get current data: $errorCode")
}
}
)
}
```
**Note**: Real-time data is preliminary and may differ from final report after processing.
## Testing
### Unit Testing
```kotlin
class SleepTrackingViewModelTest {
@get:Rule val mainDispatcherRule = MainDispatcherRule()
@Test
fun `startTracking should fail if not initialized`() = runTest {
viewModel.startTracking()
assertNotEquals(AsleepState.STATE_TRACKING_STARTED, viewModel.trackingState.value)
}
}
```
### Integration Testing
```kotlin
@Test
fun trackingFlow_complete() {
onView(withId(R.id.btn_start_stop)).perform(click())
onView(withId(R.id.tracking_indicator)).check(matches(isDisplayed()))
}
```
**For complete testing guide with Compose UI tests and test utilities**, see `references/testing_guide.md`.
## Common Issues & Solutions
### Tracking stops unexpectedly
**Causes**: Battery optimization, notification dismissed, permission revoked, microphone conflict
**Solution**: Check battery optimization and permissions on resume:
```kotlin
override fun onResume() {
super.onResume()
if (!hasRequiredPermissions() && Asleep.isSleepTrackingAlive(applicationContext)) {
handlePermissionLoss()
}
}
```
### No real-time data available
**Cause**: Checking before sequence 10
**Solution**: Only call `getCurrentSleepData()` after sequence 10
### ERR_UPLOAD_FORBIDDEN error
**Cause**: Same user_id tracking on multiple devices
**Solution**: Use unique user IDs per device or check for active sessions before starting
## Resources
This skill includes detailed reference documentation:
- `references/complete_viewmodel_implementation.md`: Complete ViewModel, Activity, and PermissionManager implementations
- `references/ui_implementation_guide.md`: Complete ViewBinding and Jetpack Compose UI examples
- `references/testing_guide.md`: Comprehensive unit, integration, and UI testing guides
- `references/android_architecture_patterns.md`: Complete architecture examples from the official sample app
- `references/gradle_setup.md`: Comprehensive Gradle configuration including dependencies and ProGuard rules
### Official Documentation
- **Android Getting Started**: https://docs-en.asleep.ai/docs/android-get-started.md
- **AsleepConfig Reference**: https://docs-en.asleep.ai/docs/android-asleep-config.md
- **SleepTrackingManager**: https://docs-en.asleep.ai/docs/android-sleep-tracking-manager.md
- **Error Codes**: https://docs-en.asleep.ai/docs/android-error-codes.md
### Android Resources
- **Android Permissions**: https://developer.android.com/guide/topics/permissions/overview
- **Foreground Services**: https://developer.android.com/develop/background-work/services/foreground-services
- **StateFlow Guide**: https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
- **Hilt Documentation**: https://developer.android.com/training/dependency-injection/hilt-android
## Next Steps
After implementing Android sleep tracking:
1. **Test thoroughly**: Test on different Android versions (especially 13+ for notifications)
2. **Handle edge cases**: Low battery, airplane mode, app updates during tracking
3. **Fetch reports**: Use REST API or backend integration to retrieve sleep reports
4. **Build UI**: Create compelling visualizations of sleep data
5. **Analytics**: Track user engagement and sleep patterns
For backend report fetching and webhook integration, use the `sleeptrack-be` skill.

View File

@@ -0,0 +1,364 @@
# Android Architecture Patterns for Asleep SDK
This document contains key architecture patterns from the Asleep Android sample app.
## 1. Application Setup with Hilt
```kotlin
@HiltAndroidApp
class SampleApplication : Application() {
companion object {
private lateinit var instance: SampleApplication
val ACTION_AUTO_TRACKING: String by lazy {
instance.packageName + ".ACTION_AUTO_TRACKING"
}
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
```
## 2. State Management with Sealed Classes
```kotlin
sealed class AsleepState {
data object STATE_IDLE: AsleepState()
data object STATE_INITIALIZING : AsleepState()
data object STATE_INITIALIZED : AsleepState()
data object STATE_TRACKING_STARTING : AsleepState()
data object STATE_TRACKING_STARTED : AsleepState()
data object STATE_TRACKING_STOPPING : AsleepState()
data class STATE_ERROR(val errorCode: AsleepError) : AsleepState()
}
```
## 3. ViewModel with Asleep Integration
```kotlin
@HiltViewModel
class AsleepViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Application
) : ViewModel() {
// State management
private var _asleepState = MutableStateFlow<AsleepState>(AsleepState.STATE_IDLE)
val asleepState: StateFlow<AsleepState> get() = _asleepState
// User and session data
private var _asleepUserId = MutableLiveData<String?>(null)
val asleepUserId: LiveData<String?> get() = _asleepUserId
private var _sessionId = MutableStateFlow<String?>(null)
val sessionId: StateFlow<String?> get() = _sessionId
// Initialize SDK
fun initAsleepConfig() {
if (_asleepState.value != AsleepState.STATE_IDLE) return
_asleepState.value = AsleepState.STATE_INITIALIZING
val storedUserId = PreferenceHelper.getAsleepUserId(applicationContext)
Asleep.initAsleepConfig(
context = applicationContext,
apiKey = Constants.ASLEEP_API_KEY,
userId = storedUserId,
baseUrl = Constants.BASE_URL,
callbackUrl = Constants.CALLBACK_URL,
service = Constants.SERVICE_NAME,
asleepConfigListener = object : Asleep.AsleepConfigListener {
override fun onFail(errorCode: Int, detail: String) {
_asleepErrorCode.value = AsleepError(errorCode, detail)
_asleepState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail))
}
override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) {
_asleepConfig.value = asleepConfig
_asleepUserId.value = userId
userId?.let { PreferenceHelper.putAsleepUserId(applicationContext, it) }
_asleepState.value = AsleepState.STATE_INITIALIZED
}
}
)
}
// Tracking lifecycle
private val asleepTrackingListener = object: Asleep.AsleepTrackingListener {
override fun onStart(sessionId: String) {
_asleepState.value = AsleepState.STATE_TRACKING_STARTED
}
override fun onPerform(sequence: Int) {
_sequence.postValue(sequence)
if (sequence > 10 && (sequence % 10 == 1 || sequence - (_analyzedSeq ?: 0) > 10)) {
getCurrentSleepData(sequence)
}
}
override fun onFinish(sessionId: String?) {
_sessionId.value = sessionId
if (asleepState.value is AsleepState.STATE_ERROR) {
// Exit due to Error
} else {
// Successful Finish
_asleepState.value = AsleepState.STATE_IDLE
}
}
override fun onFail(errorCode: Int, detail: String) {
handleErrorOrWarning(AsleepError(errorCode, detail))
}
}
fun beginSleepTracking() {
if (_asleepState.value == AsleepState.STATE_INITIALIZED) {
_asleepState.value = AsleepState.STATE_TRACKING_STARTING
_asleepConfig.value?.let {
Asleep.beginSleepTracking(
asleepConfig = it,
asleepTrackingListener = asleepTrackingListener,
notificationTitle = applicationContext.getString(R.string.app_name),
notificationText = "",
notificationIcon = R.mipmap.ic_app,
notificationClass = MainActivity::class.java
)
}
PreferenceHelper.saveStartTrackingTime(applicationContext, System.currentTimeMillis())
}
}
fun endSleepTracking() {
if (Asleep.isSleepTrackingAlive(applicationContext)) {
_asleepState.value = AsleepState.STATE_TRACKING_STOPPING
Asleep.endSleepTracking()
}
}
fun connectSleepTracking() {
Asleep.connectSleepTracking(asleepTrackingListener)
_asleepUserId.value = PreferenceHelper.getAsleepUserId(applicationContext)
_asleepState.value = AsleepState.STATE_TRACKING_STARTED
}
// Error handling
fun handleErrorOrWarning(asleepError: AsleepError) {
val code = asleepError.code
val message = asleepError.message
if (isWarning(code)) {
// Log warning, continue tracking
_warningMessage.postValue("$existingMessage\n${getCurrentTime()} $code - $message")
} else {
// Critical error, stop tracking
_asleepErrorCode.postValue(asleepError)
_asleepState.value = AsleepState.STATE_ERROR(asleepError)
}
}
}
```
## 4. Activity State Handling
```kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var permissionManager: PermissionManager
private val asleepViewModel: AsleepViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
permissionManager = PermissionManager(this)
setPermissionObserver()
permissionManager.checkAllPermissions()
// State management with coroutines
lifecycleScope.launch {
asleepViewModel.asleepState.collect { state ->
when (state) {
AsleepState.STATE_IDLE -> {
checkRunningService()
}
AsleepState.STATE_INITIALIZING -> {
binding.btnControlTracking.text = "No user id"
binding.btnControlTracking.isEnabled = false
}
AsleepState.STATE_INITIALIZED -> {
binding.btnControlTracking.apply {
isEnabled = true
text = getString(R.string.button_text_start_tracking)
setOnClickListener {
if (permissionManager.allPermissionsGranted.value == true) {
asleepViewModel.beginSleepTracking()
} else {
permissionManager.checkAndRequestPermissions()
}
}
}
}
AsleepState.STATE_TRACKING_STARTED -> {
binding.btnControlTracking.apply {
isEnabled = true
text = getString(R.string.button_text_stop_tracking)
setOnClickListener {
if (asleepViewModel.isEnoughTrackingTime()) {
asleepViewModel.endSleepTracking()
} else {
showInsufficientTimeDialog()
}
}
}
}
is AsleepState.STATE_ERROR -> {
binding.btnControlTracking.isEnabled = false
showErrorDialog(supportFragmentManager)
}
}
}
}
}
private fun checkRunningService() {
val isRunningService = Asleep.isSleepTrackingAlive(applicationContext)
if (isRunningService) {
asleepViewModel.connectSleepTracking()
} else {
asleepViewModel.initAsleepConfig()
}
}
}
```
## 5. Permission Management
```kotlin
class PermissionManager(private val activity: AppCompatActivity) {
private val context = activity.applicationContext
private val batteryOptimizationLauncher =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
checkAndRequestPermissions()
}
private val micPermissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
checkAndRequestPermissions()
}
private val notificationPermissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
checkAndRequestPermissions()
}
private val _allPermissionsGranted = MutableLiveData(false)
val allPermissionsGranted: LiveData<Boolean> = _allPermissionsGranted
fun checkAndRequestPermissions() {
when {
!isBatteryOptimizationIgnored() -> {
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
batteryOptimizationLauncher.launch(intent)
}
context.checkSelfPermission(android.Manifest.permission.RECORD_AUDIO)
!= android.content.pm.PackageManager.PERMISSION_GRANTED -> {
if (shouldShowRequestPermissionRationale(activity,
android.Manifest.permission.RECORD_AUDIO)) {
showPermissionDialog()
} else {
micPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
}
}
hasNotificationPermission().not() -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
shouldShowRequestPermissionRationale(activity,
android.Manifest.permission.POST_NOTIFICATIONS)) {
showPermissionDialog()
} else {
notificationPermissionLauncher.launch(
android.Manifest.permission.POST_NOTIFICATIONS)
}
}
else -> {
checkAllPermissions()
}
}
}
fun checkAllPermissions() {
_batteryOptimized.value = isBatteryOptimizationIgnored()
_micPermission.value = context.checkSelfPermission(
android.Manifest.permission.RECORD_AUDIO
) == android.content.pm.PackageManager.PERMISSION_GRANTED
_notificationPermission.value = hasNotificationPermission()
_allPermissionsGranted.value =
(_batteryOptimized.value == true) &&
(_micPermission.value == true) &&
(_notificationPermission.value == true)
}
private fun isBatteryOptimizationIgnored(): Boolean {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val packageName = context.packageName
return powerManager.isIgnoringBatteryOptimizations(packageName)
}
private fun hasNotificationPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)
== android.content.pm.PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
}
```
## 6. Error Handling Utilities
```kotlin
data class AsleepError(val code: Int, val message: String)
internal fun isWarning(errorCode: Int): Boolean {
return errorCode in setOf(
AsleepErrorCode.ERR_AUDIO_SILENCED,
AsleepErrorCode.ERR_AUDIO_UNSILENCED,
AsleepErrorCode.ERR_UPLOAD_FAILED,
)
}
internal fun getDebugMessage(errorCode: AsleepError): String {
if (isNetworkError(errorCode.message)) {
return "Please check your network connection."
}
if (isMethodFormatInvalid(errorCode.message)) {
return "Please check the method format, including argument values and types."
}
return when (errorCode.code) {
AsleepErrorCode.ERR_MIC_PERMISSION ->
"The app does not have microphone access permission."
AsleepErrorCode.ERR_AUDIO ->
"Another app is using the microphone, or there is an issue with the microphone settings."
AsleepErrorCode.ERR_INVALID_URL ->
"Please check the URL format."
AsleepErrorCode.ERR_COMMON_EXPIRED ->
"The API rate limit has been exceeded, or the plan has expired."
AsleepErrorCode.ERR_UPLOAD_FORBIDDEN ->
"initAsleepConfig() was performed elsewhere with the same ID during the tracking."
AsleepErrorCode.ERR_UPLOAD_NOT_FOUND, AsleepErrorCode.ERR_CLOSE_NOT_FOUND ->
"The session has already ended."
else -> ""
}
}
```

View File

@@ -0,0 +1,540 @@
# Complete ViewModel and Activity Implementation
This reference provides complete, production-ready implementations for Android MVVM architecture with the Asleep SDK.
## Complete ViewModel with State Management
```kotlin
@HiltViewModel
class SleepTrackingViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Application
) : ViewModel() {
// State management
private val _trackingState = MutableStateFlow<AsleepState>(AsleepState.STATE_IDLE)
val trackingState: StateFlow<AsleepState> = _trackingState.asStateFlow()
private val _userId = MutableLiveData<String?>()
val userId: LiveData<String?> = _userId
private val _sessionId = MutableStateFlow<String?>(null)
val sessionId: StateFlow<String?> = _sessionId.asStateFlow()
private val _sequence = MutableLiveData<Int>()
val sequence: LiveData<Int> = _sequence
private var asleepConfig: AsleepConfig? = null
// Tracking listener
private val trackingListener = object : Asleep.AsleepTrackingListener {
override fun onStart(sessionId: String) {
_sessionId.value = sessionId
_trackingState.value = AsleepState.STATE_TRACKING_STARTED
}
override fun onPerform(sequence: Int) {
_sequence.postValue(sequence)
// Check real-time data every 10 sequences after sequence 10
if (sequence > 10 && sequence % 10 == 0) {
getCurrentSleepData()
}
}
override fun onFinish(sessionId: String?) {
_sessionId.value = sessionId
_trackingState.value = AsleepState.STATE_IDLE
}
override fun onFail(errorCode: Int, detail: String) {
handleError(AsleepError(errorCode, detail))
}
}
fun initializeSDK(userId: String) {
if (_trackingState.value != AsleepState.STATE_IDLE) return
_trackingState.value = AsleepState.STATE_INITIALIZING
Asleep.initAsleepConfig(
context = applicationContext,
apiKey = BuildConfig.ASLEEP_API_KEY,
userId = userId,
baseUrl = "https://api.asleep.ai",
callbackUrl = null,
service = applicationContext.getString(R.string.app_name),
asleepConfigListener = object : Asleep.AsleepConfigListener {
override fun onSuccess(userId: String?, config: AsleepConfig?) {
asleepConfig = config
_userId.value = userId
_trackingState.value = AsleepState.STATE_INITIALIZED
}
override fun onFail(errorCode: Int, detail: String) {
_trackingState.value = AsleepState.STATE_ERROR(
AsleepError(errorCode, detail)
)
}
}
)
}
fun startTracking() {
val config = asleepConfig ?: return
if (_trackingState.value != AsleepState.STATE_INITIALIZED) return
_trackingState.value = AsleepState.STATE_TRACKING_STARTING
Asleep.beginSleepTracking(
asleepConfig = config,
asleepTrackingListener = trackingListener,
notificationTitle = "Sleep Tracking",
notificationText = "Recording your sleep",
notificationIcon = R.drawable.ic_notification,
notificationClass = MainActivity::class.java
)
}
fun stopTracking() {
if (Asleep.isSleepTrackingAlive(applicationContext)) {
_trackingState.value = AsleepState.STATE_TRACKING_STOPPING
Asleep.endSleepTracking()
}
}
fun reconnectToTracking() {
if (Asleep.isSleepTrackingAlive(applicationContext)) {
Asleep.connectSleepTracking(trackingListener)
_trackingState.value = AsleepState.STATE_TRACKING_STARTED
}
}
private fun getCurrentSleepData() {
Asleep.getCurrentSleepData(
asleepSleepDataListener = object : Asleep.AsleepSleepDataListener {
override fun onSleepDataReceived(session: Session) {
// Process real-time sleep data
val currentStage = session.sleepStages?.lastOrNull()
Log.d("Sleep", "Current stage: $currentStage")
}
override fun onFail(errorCode: Int, detail: String) {
Log.e("Sleep", "Failed to get data: $errorCode")
}
}
)
}
private fun handleError(error: AsleepError) {
if (isWarning(error.code)) {
// Log warning but continue tracking
Log.w("Sleep", "Warning: ${error.code} - ${error.message}")
} else {
// Critical error - stop tracking
_trackingState.value = AsleepState.STATE_ERROR(error)
}
}
}
```
## Complete Activity Implementation
```kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: SleepTrackingViewModel by viewModels()
private lateinit var permissionManager: PermissionManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
permissionManager = PermissionManager(this)
setupUI()
observeState()
checkExistingSession()
}
private fun setupUI() {
binding.btnStartStop.setOnClickListener {
when (viewModel.trackingState.value) {
AsleepState.STATE_INITIALIZED -> {
if (permissionManager.allPermissionsGranted.value == true) {
viewModel.startTracking()
} else {
permissionManager.requestPermissions()
}
}
AsleepState.STATE_TRACKING_STARTED -> {
viewModel.stopTracking()
}
else -> {
// Button disabled
}
}
}
}
private fun observeState() {
lifecycleScope.launch {
viewModel.trackingState.collect { state ->
updateUI(state)
}
}
viewModel.sequence.observe(this) { sequence ->
binding.tvSequence.text = "Sequence: $sequence"
}
}
private fun updateUI(state: AsleepState) {
when (state) {
AsleepState.STATE_IDLE -> {
binding.btnStartStop.isEnabled = false
binding.btnStartStop.text = "Initializing..."
}
AsleepState.STATE_INITIALIZED -> {
binding.btnStartStop.isEnabled = true
binding.btnStartStop.text = "Start Tracking"
}
AsleepState.STATE_TRACKING_STARTED -> {
binding.btnStartStop.isEnabled = true
binding.btnStartStop.text = "Stop Tracking"
binding.trackingIndicator.visibility = View.VISIBLE
}
AsleepState.STATE_TRACKING_STOPPING -> {
binding.btnStartStop.isEnabled = false
binding.btnStartStop.text = "Stopping..."
}
is AsleepState.STATE_ERROR -> {
showErrorDialog(state.errorCode)
}
else -> {}
}
}
private fun checkExistingSession() {
if (Asleep.isSleepTrackingAlive(applicationContext)) {
viewModel.reconnectToTracking()
} else {
viewModel.initializeSDK(getUserId())
}
}
private fun getUserId(): String {
// Load from SharedPreferences or generate
val prefs = getSharedPreferences("asleep", MODE_PRIVATE)
return prefs.getString("user_id", null) ?: run {
val newId = UUID.randomUUID().toString()
prefs.edit().putString("user_id", newId).apply()
newId
}
}
}
```
## Complete PermissionManager
```kotlin
class PermissionManager(private val activity: AppCompatActivity) {
private val context = activity.applicationContext
// Permission launchers
private val batteryOptimizationLauncher =
activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
checkAndRequestNext()
}
private val micPermissionLauncher =
activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) checkAndRequestNext()
else showPermissionDeniedDialog("Microphone")
}
private val notificationPermissionLauncher =
activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) checkAndRequestNext()
else showPermissionDeniedDialog("Notifications")
}
private val _allPermissionsGranted = MutableLiveData(false)
val allPermissionsGranted: LiveData<Boolean> = _allPermissionsGranted
fun requestPermissions() {
checkAndRequestNext()
}
private fun checkAndRequestNext() {
when {
!isBatteryOptimizationIgnored() -> requestBatteryOptimization()
!hasMicrophonePermission() -> requestMicrophone()
!hasNotificationPermission() -> requestNotification()
else -> {
_allPermissionsGranted.value = true
}
}
}
private fun requestBatteryOptimization() {
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
batteryOptimizationLauncher.launch(intent)
}
private fun requestMicrophone() {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.RECORD_AUDIO
)) {
showPermissionRationale(
"Microphone access is required to record sleep sounds"
)
} else {
micPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}
private fun requestNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionLauncher.launch(
Manifest.permission.POST_NOTIFICATIONS
)
} else {
checkAndRequestNext()
}
}
private fun isBatteryOptimizationIgnored(): Boolean {
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(context.packageName)
}
private fun hasMicrophonePermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
private fun hasNotificationPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
private fun showPermissionRationale(message: String) {
AlertDialog.Builder(activity)
.setTitle("Permission Required")
.setMessage(message)
.setPositiveButton("OK") { _, _ ->
checkAndRequestNext()
}
.show()
}
private fun showPermissionDeniedDialog(permissionName: String) {
AlertDialog.Builder(activity)
.setTitle("Permission Denied")
.setMessage("$permissionName permission is required for sleep tracking. Please grant it in app settings.")
.setPositiveButton("Settings") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${context.packageName}")
}
activity.startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
}
```
## Minimal Production Templates
### Minimal ViewModel
```kotlin
@HiltViewModel
class SleepTrackingViewModel @Inject constructor(
@ApplicationContext private val app: Application
) : ViewModel() {
private val _trackingState = MutableStateFlow<AsleepState>(AsleepState.STATE_IDLE)
val trackingState: StateFlow<AsleepState> = _trackingState
private var config: AsleepConfig? = null
private val listener = object : Asleep.AsleepTrackingListener {
override fun onStart(sessionId: String) {
_trackingState.value = AsleepState.STATE_TRACKING_STARTED
}
override fun onPerform(sequence: Int) {}
override fun onFinish(sessionId: String?) {
_trackingState.value = AsleepState.STATE_IDLE
}
override fun onFail(errorCode: Int, detail: String) {
_trackingState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail))
}
}
fun initializeSDK(userId: String) {
Asleep.initAsleepConfig(
context = app,
apiKey = BuildConfig.ASLEEP_API_KEY,
userId = userId,
baseUrl = "https://api.asleep.ai",
callbackUrl = null,
service = "MyApp",
asleepConfigListener = object : Asleep.AsleepConfigListener {
override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) {
config = asleepConfig
_trackingState.value = AsleepState.STATE_INITIALIZED
}
override fun onFail(errorCode: Int, detail: String) {
_trackingState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail))
}
}
)
}
fun startTracking() {
config?.let {
Asleep.beginSleepTracking(
asleepConfig = it,
asleepTrackingListener = listener,
notificationTitle = "Sleep Tracking",
notificationText = "Active",
notificationIcon = R.drawable.ic_notification,
notificationClass = SleepActivity::class.java
)
}
}
fun stopTracking() {
Asleep.endSleepTracking()
}
}
```
### Minimal Activity
```kotlin
@AndroidEntryPoint
class SleepActivity : AppCompatActivity() {
private lateinit var binding: ActivitySleepBinding
private val viewModel: SleepTrackingViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySleepBinding.inflate(layoutInflater)
setContentView(binding.root)
// Initialize SDK
viewModel.initializeSDK(loadUserId())
// Setup button
binding.btnTrack.setOnClickListener {
when (viewModel.trackingState.value) {
AsleepState.STATE_INITIALIZED -> viewModel.startTracking()
AsleepState.STATE_TRACKING_STARTED -> viewModel.stopTracking()
else -> {}
}
}
// Observe state
lifecycleScope.launch {
viewModel.trackingState.collect { state ->
binding.btnTrack.text = when (state) {
AsleepState.STATE_INITIALIZED -> "Start"
AsleepState.STATE_TRACKING_STARTED -> "Stop"
else -> "Loading..."
}
}
}
}
private fun loadUserId(): String {
val prefs = getSharedPreferences("app", MODE_PRIVATE)
return prefs.getString("user_id", null) ?: UUID.randomUUID().toString().also {
prefs.edit().putString("user_id", it).apply()
}
}
}
```
## State Management Pattern
Define states using sealed classes:
```kotlin
sealed class AsleepState {
data object STATE_IDLE: AsleepState()
data object STATE_INITIALIZING : AsleepState()
data object STATE_INITIALIZED : AsleepState()
data object STATE_TRACKING_STARTING : AsleepState()
data object STATE_TRACKING_STARTED : AsleepState()
data object STATE_TRACKING_STOPPING : AsleepState()
data class STATE_ERROR(val errorCode: AsleepError) : AsleepState()
}
data class AsleepError(val code: Int, val message: String)
```
## Real-Time Data Access
```kotlin
// In ViewModel
private var lastCheckedSequence = 0
private val trackingListener = object : Asleep.AsleepTrackingListener {
override fun onPerform(sequence: Int) {
_sequence.postValue(sequence)
// Check after sequence 10, then every 10 sequences
if (sequence > 10 && sequence - lastCheckedSequence >= 10) {
getCurrentSleepData(sequence)
}
}
// ... other callbacks
}
private fun getCurrentSleepData(sequence: Int) {
Asleep.getCurrentSleepData(
asleepSleepDataListener = object : Asleep.AsleepSleepDataListener {
override fun onSleepDataReceived(session: Session) {
lastCheckedSequence = sequence
// Extract current data
val currentSleepStage = session.sleepStages?.lastOrNull()
val currentSnoringStage = session.snoringStages?.lastOrNull()
_currentSleepStage.postValue(currentSleepStage)
Log.d("Sleep", "Current stage: $currentSleepStage")
Log.d("Sleep", "Snoring: $currentSnoringStage")
}
override fun onFail(errorCode: Int, detail: String) {
Log.e("Sleep", "Failed to get current data: $errorCode")
}
}
)
}
```

View File

@@ -0,0 +1,146 @@
# Gradle Setup for Asleep SDK
## Project-level build.gradle
```gradle
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.48'
}
}
plugins {
id 'com.android.application' version '8.2.2' apply false
id 'com.android.library' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
id 'com.google.dagger.hilt.android' version '2.48' apply false
}
```
## App-level build.gradle
```gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
namespace 'com.example.yourapp'
compileSdk 34
defaultConfig {
applicationId "com.example.yourapp"
minSdk 24 // Minimum SDK 24 required for Asleep SDK
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Store API key securely in local.properties
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
buildConfigField "String", "ASLEEP_API_KEY", properties['asleep_api_key']
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
viewBinding true
buildConfig = true
}
}
dependencies {
// Core Android dependencies
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Hilt for dependency injection
implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-compiler:2.48"
// Activity and Fragment KTX
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
// Required for Asleep SDK
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
debugImplementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
implementation 'com.google.code.gson:gson:2.10'
// Asleep SDK (check for latest version)
implementation 'ai.asleep:asleepsdk:3.1.4'
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
```
## local.properties
```properties
# Store your Asleep API key securely (never commit this file to version control)
asleep_api_key="your_api_key_here"
```
## Key Requirements
1. **Minimum SDK**: 24 (Android 7.0)
2. **Target SDK**: 34 recommended
3. **Java Version**: 17
4. **Kotlin Version**: 1.9.24 or higher
5. **Gradle Plugin**: 8.2.2 or higher
## Dependency Notes
- **OkHttp**: Required for network operations
- **Gson**: Required for JSON parsing
- **Hilt**: Recommended for dependency injection (not strictly required)
- **ViewBinding**: Recommended for type-safe view access
## ProGuard Rules
If using ProGuard/R8, add these rules to `proguard-rules.pro`:
```proguard
# Asleep SDK
-keep class ai.asleep.asleepsdk.** { *; }
-dontwarn ai.asleep.asleepsdk.**
# Gson
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class * implements com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
```

View File

@@ -0,0 +1,523 @@
# Testing Guide for Android Sleep Tracking
This guide provides comprehensive testing patterns for Android sleep tracking applications.
## Unit Testing
### ViewModel Testing Setup
```kotlin
@ExperimentalCoroutinesApi
class SleepTrackingViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private lateinit var viewModel: SleepTrackingViewModel
private lateinit var mockContext: Application
@Before
fun setup() {
mockContext = mock(Application::class.java)
viewModel = SleepTrackingViewModel(mockContext)
}
@Test
fun `initializeSDK should transition to INITIALIZED on success`() = runTest {
// Given
val userId = "test_user_123"
// When
viewModel.initializeSDK(userId)
// Simulate success callback
// (requires mocking Asleep SDK)
// Then
assertEquals(AsleepState.STATE_INITIALIZED, viewModel.trackingState.value)
assertEquals(userId, viewModel.userId.value)
}
@Test
fun `startTracking should fail if not initialized`() = runTest {
// When
viewModel.startTracking()
// Then
assertNotEquals(AsleepState.STATE_TRACKING_STARTED, viewModel.trackingState.value)
}
@Test
fun `stopTracking should transition state correctly`() = runTest {
// Given
viewModel.initializeSDK("test_user")
// Simulate initialization success
viewModel.startTracking()
// Simulate tracking started
// When
viewModel.stopTracking()
// Then
assertEquals(AsleepState.STATE_TRACKING_STOPPING, viewModel.trackingState.value)
}
@Test
fun `error handling should classify warnings correctly`() = runTest {
// Given
val warningError = AsleepError(
AsleepErrorCode.ERR_AUDIO_SILENCED,
"Microphone temporarily unavailable"
)
// When
val isWarning = isWarning(warningError.code)
// Then
assertTrue(isWarning)
}
}
```
### MainDispatcherRule for Coroutines
```kotlin
@ExperimentalCoroutinesApi
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
```
### Testing StateFlow
```kotlin
@Test
fun `trackingState should emit correct states during tracking lifecycle`() = runTest {
// Given
val states = mutableListOf<AsleepState>()
val job = launch {
viewModel.trackingState.collect { state ->
states.add(state)
}
}
// When
viewModel.initializeSDK("test_user")
advanceUntilIdle()
// Then
assertTrue(states.contains(AsleepState.STATE_INITIALIZING))
assertTrue(states.contains(AsleepState.STATE_INITIALIZED))
job.cancel()
}
```
## Integration Testing
### Activity Testing
```kotlin
@RunWith(AndroidJUnit4::class)
class SleepTrackingIntegrationTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun trackingFlow_complete() {
// Check permissions granted
onView(withId(R.id.btn_start_stop))
.check(matches(isEnabled()))
// Start tracking
onView(withId(R.id.btn_start_stop))
.perform(click())
// Verify tracking started
onView(withId(R.id.tracking_indicator))
.check(matches(isDisplayed()))
// Wait for sequences
Thread.sleep(60000) // 1 minute
// Stop tracking
onView(withId(R.id.btn_start_stop))
.perform(click())
// Verify stopped
onView(withId(R.id.tracking_indicator))
.check(matches(not(isDisplayed())))
}
@Test
fun errorDisplay_showsCorrectMessage() {
// Simulate error state
// Verify error message displayed
onView(withId(R.id.error_text))
.check(matches(isDisplayed()))
.check(matches(withText(containsString("Microphone permission"))))
}
}
```
### Fragment Testing
```kotlin
@RunWith(AndroidJUnit4::class)
class TrackingFragmentTest {
@Test
fun fragmentLaunch_displaysCorrectUI() {
// Launch fragment in container
launchFragmentInContainer<TrackingFragment>(
themeResId = R.style.Theme_SleepTracking
)
// Verify initial UI state
onView(withId(R.id.btnTrack))
.check(matches(isDisplayed()))
.check(matches(withText("Start Tracking")))
}
@Test
fun clickStartButton_requestsPermissions() {
launchFragmentInContainer<TrackingFragment>()
// Click start button
onView(withId(R.id.btnTrack))
.perform(click())
// Verify permission dialog or state change
// This depends on permission state
}
}
```
## Compose UI Testing
### Basic Compose Testing
```kotlin
@RunWith(AndroidJUnit4::class)
class SleepTrackingScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun initialState_showsStartButton() {
// Given
val viewModel = mockViewModel(AsleepState.STATE_INITIALIZED)
// When
composeTestRule.setContent {
SleepTrackingScreen(viewModel = viewModel)
}
// Then
composeTestRule.onNodeWithText("Start Sleep Tracking")
.assertIsDisplayed()
}
@Test
fun trackingActive_showsStopButton() {
// Given
val viewModel = mockViewModel(AsleepState.STATE_TRACKING_STARTED)
// When
composeTestRule.setContent {
SleepTrackingScreen(viewModel = viewModel)
}
// Then
composeTestRule.onNodeWithText("Stop Tracking")
.assertIsDisplayed()
composeTestRule.onNodeWithText("Tracking in progress")
.assertIsDisplayed()
}
@Test
fun errorState_displaysErrorMessage() {
// Given
val error = AsleepError(AsleepErrorCode.ERR_MIC_PERMISSION, "Permission denied")
val viewModel = mockViewModel(AsleepState.STATE_ERROR(error))
// When
composeTestRule.setContent {
SleepTrackingScreen(viewModel = viewModel)
}
// Then
composeTestRule.onNodeWithText("Microphone permission is required")
.assertIsDisplayed()
}
private fun mockViewModel(state: AsleepState): SleepTrackingViewModel {
return mock(SleepTrackingViewModel::class.java).apply {
whenever(trackingState).thenReturn(MutableStateFlow(state))
whenever(sequence).thenReturn(MutableLiveData(0))
}
}
}
```
### Testing User Interactions
```kotlin
@Test
fun clickStartButton_startsTracking() {
// Given
val viewModel = SleepTrackingViewModel(mockContext)
composeTestRule.setContent {
SleepTrackingScreen(viewModel = viewModel)
}
// When
composeTestRule.onNodeWithText("Start Sleep Tracking")
.performClick()
// Then
verify(viewModel).startTracking()
}
@Test
fun clickStopButton_stopsTracking() {
// Given
val viewModel = mockViewModel(AsleepState.STATE_TRACKING_STARTED)
composeTestRule.setContent {
SleepTrackingScreen(viewModel = viewModel)
}
// When
composeTestRule.onNodeWithText("Stop Tracking")
.performClick()
// Then
verify(viewModel).stopTracking()
}
```
## Permission Testing
### Testing Permission Manager
```kotlin
@RunWith(AndroidJUnit4::class)
class PermissionManagerTest {
private lateinit var activity: AppCompatActivity
private lateinit var permissionManager: PermissionManager
@Before
fun setup() {
val scenario = ActivityScenario.launch(TestActivity::class.java)
scenario.onActivity { act ->
activity = act
permissionManager = PermissionManager(activity)
}
}
@Test
fun requestPermissions_checksAllPermissions() {
// When
permissionManager.requestPermissions()
// Then
// Verify permission checks called
verify(permissionManager).isBatteryOptimizationIgnored()
verify(permissionManager).hasMicrophonePermission()
}
@Test
fun allPermissionsGranted_updatesLiveData() {
// Given
// Mock all permissions as granted
// When
permissionManager.checkAndRequestNext()
// Then
assertTrue(permissionManager.allPermissionsGranted.value == true)
}
}
```
## Instrumentation Testing
### Testing Foreground Service
```kotlin
@RunWith(AndroidJUnit4::class)
class ForegroundServiceTest {
@get:Rule
val serviceRule = ServiceTestRule()
@Test
fun trackingService_startsForeground() {
// Given
val context = InstrumentationRegistry.getInstrumentation().targetContext
// When
val serviceIntent = Intent(context, SleepTrackingService::class.java)
serviceRule.startService(serviceIntent)
// Then
// Verify service is in foreground
assertTrue(isServiceForeground(context, SleepTrackingService::class.java))
}
private fun isServiceForeground(context: Context, serviceClass: Class<*>): Boolean {
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return manager.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == serviceClass.name && it.foreground }
}
}
```
## Mock SDK Testing
### Creating SDK Mocks
```kotlin
class MockAsleepSDK {
companion object {
fun mockInitSuccess(userId: String, listener: Asleep.AsleepConfigListener) {
val config = mock(AsleepConfig::class.java)
listener.onSuccess(userId, config)
}
fun mockInitFail(errorCode: Int, listener: Asleep.AsleepConfigListener) {
listener.onFail(errorCode, "Test error")
}
fun mockTrackingStart(sessionId: String, listener: Asleep.AsleepTrackingListener) {
listener.onStart(sessionId)
}
fun mockTrackingPerform(sequence: Int, listener: Asleep.AsleepTrackingListener) {
listener.onPerform(sequence)
}
}
}
```
### Using Mocks in Tests
```kotlin
@Test
fun initializeSDK_successFlow() = runTest {
// Given
val userId = "test_user"
// Mock Asleep SDK
MockAsleepSDK.mockInitSuccess(userId, any())
// When
viewModel.initializeSDK(userId)
advanceUntilIdle()
// Then
assertEquals(AsleepState.STATE_INITIALIZED, viewModel.trackingState.value)
}
```
## Test Dependencies
Add to your `build.gradle`:
```gradle
dependencies {
// Unit testing
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.0.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
// Android instrumentation testing
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
// Fragment testing
debugImplementation 'androidx.fragment:fragment-testing:1.6.1'
// Compose testing
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.5.4'
debugImplementation 'androidx.compose.ui:ui-test-manifest:1.5.4'
}
```
## Testing Best Practices
1. **Unit Tests**: Test ViewModels, business logic, and state management
2. **Integration Tests**: Test UI interactions and component integration
3. **Use Test Rules**: Leverage JUnit rules for setup/teardown
4. **Mock External Dependencies**: Mock Asleep SDK calls for predictable tests
5. **Test Coroutines**: Use `runTest` and `TestDispatcher` for coroutine testing
6. **Test Permissions**: Verify permission flows but avoid actual permission dialogs
7. **Compose Tests**: Use semantic properties and avoid hardcoded strings
8. **CI/CD**: Run tests in continuous integration pipeline
## Debugging Tests
### Enable Debug Logging
```kotlin
@Before
fun setup() {
// Enable debug logging for tests
Log.setDebug(true)
}
```
### Capture Screenshots on Failure
```kotlin
@Rule
fun testRule = TestRule { base, description ->
object : Statement() {
override fun evaluate() {
try {
base.evaluate()
} catch (t: Throwable) {
// Capture screenshot
Screenshot.capture()
throw t
}
}
}
}
```
## Test Coverage
Aim for:
- **Unit Tests**: 80%+ coverage of ViewModels and business logic
- **Integration Tests**: Key user flows and state transitions
- **UI Tests**: Critical user interactions
Use JaCoCo for coverage reporting:
```gradle
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.10"
}
```

View File

@@ -0,0 +1,524 @@
# UI Implementation Guide
This guide provides complete UI implementations for Android sleep tracking with both ViewBinding and Jetpack Compose.
## ViewBinding Implementation
### Complete Fragment with ViewBinding
```kotlin
class TrackingFragment : Fragment() {
private var _binding: FragmentTrackingBinding? = null
private val binding get() = _binding!!
private val viewModel: SleepTrackingViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTrackingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnTrack.setOnClickListener {
viewModel.startTracking()
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.trackingState.collect { state ->
when (state) {
AsleepState.STATE_TRACKING_STARTED -> {
binding.progressIndicator.visibility = View.VISIBLE
binding.btnTrack.text = "Stop Tracking"
}
AsleepState.STATE_INITIALIZED -> {
binding.progressIndicator.visibility = View.GONE
binding.btnTrack.text = "Start Tracking"
}
else -> {}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
```
### Layout XML (fragment_tracking.xml)
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<ProgressBar
android:id="@+id/progressIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnTrack" />
<TextView
android:id="@+id/tvSequence"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sequence: 0"
android:textSize="18sp"
app:layout_constraintTop_toBottomOf="@id/progressIndicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnTrack" />
<Button
android:id="@+id/btnTrack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Start Tracking"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
## Jetpack Compose Implementation
### Complete Tracking Screen
```kotlin
@Composable
fun SleepTrackingScreen(
viewModel: SleepTrackingViewModel = hiltViewModel()
) {
val trackingState by viewModel.trackingState.collectAsState()
val sequence by viewModel.sequence.observeAsState(0)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when (trackingState) {
AsleepState.STATE_TRACKING_STARTED -> {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text("Tracking in progress")
Text("Sequence: $sequence")
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = { viewModel.stopTracking() }) {
Text("Stop Tracking")
}
}
AsleepState.STATE_INITIALIZED -> {
Button(onClick = { viewModel.startTracking() }) {
Text("Start Sleep Tracking")
}
}
is AsleepState.STATE_ERROR -> {
val error = (trackingState as AsleepState.STATE_ERROR).errorCode
ErrorDisplay(error = error)
}
else -> {
CircularProgressIndicator()
Text("Initializing...")
}
}
}
}
@Composable
fun ErrorDisplay(error: AsleepError) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Error",
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = getUserFriendlyMessage(error),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
}
}
```
### Advanced Compose UI with Sleep Stages
```kotlin
@Composable
fun DetailedTrackingScreen(
viewModel: SleepTrackingViewModel = hiltViewModel()
) {
val trackingState by viewModel.trackingState.collectAsState()
val sequence by viewModel.sequence.observeAsState(0)
val sessionId by viewModel.sessionId.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Sleep Tracking") }
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
) {
when (trackingState) {
AsleepState.STATE_TRACKING_STARTED -> {
TrackingActiveContent(
sequence = sequence,
sessionId = sessionId,
onStop = { viewModel.stopTracking() }
)
}
AsleepState.STATE_INITIALIZED -> {
TrackingIdleContent(
onStart = { viewModel.startTracking() }
)
}
else -> {
LoadingContent()
}
}
}
}
}
@Composable
fun TrackingActiveContent(
sequence: Int,
sessionId: String?,
onStop: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
// Status card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Tracking Active",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Session: ${sessionId?.take(8)}...",
style = MaterialTheme.typography.bodyMedium
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Progress indicator
Box(
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(120.dp),
strokeWidth = 8.dp
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "$sequence",
style = MaterialTheme.typography.headlineLarge
)
Text(
text = "sequences",
style = MaterialTheme.typography.bodySmall
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Duration estimate
val minutes = sequence / 2
Text(
text = "Approximately $minutes minutes",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.weight(1f))
// Stop button
Button(
onClick = onStop,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Stop Tracking")
}
}
}
@Composable
fun TrackingIdleContent(onStart: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.NightsStay,
contentDescription = "Sleep",
modifier = Modifier.size(120.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Ready to Track Sleep",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Make sure you're in a quiet environment",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onStart,
modifier = Modifier.fillMaxWidth(0.8f)
) {
Text("Start Tracking")
}
}
}
@Composable
fun LoadingContent() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text("Initializing SDK...")
}
}
}
```
## Material Design 3 Theming
```kotlin
// Theme setup for Compose
@Composable
fun SleepTrackingTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
darkColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6),
error = Color(0xFFCF6679)
)
} else {
lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6),
error = Color(0xFFB00020)
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
```
## Custom Views (XML)
### CircularProgressView
```kotlin
class CircularProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var progress = 0
private val paint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 20f
color = Color.BLUE
isAntiAlias = true
}
fun setProgress(value: Int) {
progress = value
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
val radius = (min(width, height) / 2f) - 20f
// Draw circle
canvas.drawCircle(centerX, centerY, radius, paint)
// Draw progress arc
val rectF = RectF(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
paint.style = Paint.Style.FILL
canvas.drawArc(rectF, -90f, (progress * 360f / 100f), true, paint)
}
}
```
## Permission UI Patterns
### Permission Request Dialog (Compose)
```kotlin
@Composable
fun PermissionRequestDialog(
permissionName: String,
rationale: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Permission Required") },
text = { Text(rationale) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Grant Permission")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
```
### Using with Accompanist Permissions
```kotlin
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionScreen(
onPermissionsGranted: () -> Unit
) {
val micPermissionState = rememberPermissionState(
Manifest.permission.RECORD_AUDIO
)
when {
micPermissionState.status.isGranted -> {
onPermissionsGranted()
}
micPermissionState.status.shouldShowRationale -> {
PermissionRequestDialog(
permissionName = "Microphone",
rationale = "Microphone access is required to record sleep sounds",
onConfirm = { micPermissionState.launchPermissionRequest() },
onDismiss = { /* Handle dismissal */ }
)
}
else -> {
Button(onClick = { micPermissionState.launchPermissionRequest() }) {
Text("Grant Microphone Permission")
}
}
}
}
```
## Navigation Integration
### Jetpack Navigation with Compose
```kotlin
@Composable
fun SleepTrackingNavHost(
navController: NavHostController = rememberNavController()
) {
NavHost(
navController = navController,
startDestination = "tracking"
) {
composable("tracking") {
SleepTrackingScreen(
onSessionComplete = { sessionId ->
navController.navigate("results/$sessionId")
}
)
}
composable(
"results/{sessionId}",
arguments = listOf(navArgument("sessionId") { type = NavType.StringType })
) { backStackEntry ->
ResultsScreen(
sessionId = backStackEntry.arguments?.getString("sessionId")
)
}
}
}
```
## Best Practices
1. **ViewBinding**: Always nullify binding in `onDestroyView()` for fragments
2. **Compose**: Use `collectAsState()` for StateFlow and `observeAsState()` for LiveData
3. **State Management**: Handle all possible states in UI, including loading and error states
4. **Accessibility**: Add content descriptions for all interactive elements
5. **Dark Mode**: Support both light and dark themes
6. **Orientation**: Handle configuration changes with ViewModels
7. **Touch Targets**: Ensure buttons are at least 48dp in size

View 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

View 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;
```

View 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'])
}
```

View 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)}")
```

View 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/`

View 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
)
```

View 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

View File

@@ -0,0 +1,485 @@
---
name: sleeptrack-foundation
description: This skill provides foundational knowledge about Asleep sleep tracking platform, covering core concepts, authentication, data structures, error handling, and platform-agnostic best practices. Use this skill when developers ask about Asleep fundamentals, API concepts, error codes, sleep data structures, or need to understand how the platform works before implementing platform-specific integration. This skill serves as prerequisite knowledge for sleeptrack-ios, sleeptrack-android, and sleeptrack-be skills.
---
# Sleeptrack Foundation
## Overview
This skill provides essential foundational knowledge for integrating the Asleep sleep tracking platform. It covers core concepts, authentication patterns, data structures, error handling, and platform-agnostic best practices that apply across all implementation approaches (iOS, Android, and backend API).
Use this skill when developers need to understand:
- What Asleep is and how sleep tracking works
- API authentication and key management
- Sleep session concepts and lifecycle
- Data structures (Sessions, Reports, Statistics)
- Error codes and troubleshooting
- Platform-agnostic integration patterns
## Core Concepts
### What is Asleep?
Asleep is a sleep tracking platform that analyzes sleep using audio-based monitoring through device microphones. The platform provides:
- **Real-time sleep stage analysis**: Wake, Light, Deep, REM detection
- **Comprehensive sleep metrics**: Efficiency, latency, total sleep time, wake after sleep onset
- **Snoring detection and analysis**: Snoring stages and patterns
- **Multi-platform SDKs**: Native iOS and Android SDKs plus REST API
- **Dashboard analytics**: Web-based analytics and user management
### User Management
Each application user must be registered with Asleep before tracking sleep.
**Key Points**:
- User ID is managed by the host application (not generated by Asleep)
- One user can have multiple sleep sessions
- User data persists across sessions for trend analysis
- Users can be created, retrieved, updated, and deleted via API
**Example User ID Schemes**:
```
- UUID: "550e8400-e29b-41d4-a716-446655440000"
- Email-based: "user@example.com"
- App-specific: "app_user_12345"
```
### Sleep Sessions
A session represents one complete sleep tracking period from start to stop.
**Session Lifecycle States**:
1. **IDLE**: No tracking in progress
2. **INITIALIZING**: SDK preparing resources
3. **INITIALIZED**: Ready to start tracking
4. **TRACKING_STARTED**: Active tracking in progress
5. **TRACKING_STOPPING**: Ending session and uploading data
**Session Requirements**:
- Minimum tracking duration: 5 minutes for valid session
- Microphone access required throughout tracking
- Network connectivity needed for data upload
- One active session per user at a time
**Real-time Data Access**:
- Available after sequence 10
- Check every 10 sequences thereafter
- Provides preliminary sleep stage data during tracking
### Sleep Reports
Reports contain comprehensive analysis of completed sleep sessions.
**Report Structure**:
```
Report
├── Session Metadata
│ ├── session_id
│ ├── user_id
│ ├── start_time
│ └── end_time
├── Sleep Stages Timeline
│ ├── Wake periods
│ ├── Light sleep periods
│ ├── Deep sleep periods
│ └── REM sleep periods
├── Sleep Statistics
│ ├── Total sleep time
│ ├── Time in bed
│ ├── Sleep efficiency (%)
│ ├── Sleep latency (time to fall asleep)
│ ├── Wake after sleep onset (WASO)
│ ├── Sleep stage durations
│ └── Sleep stage ratios
└── Snoring Analysis
├── Snoring detected (yes/no)
├── Snoring stages timeline
└── Snoring statistics
```
**Key Metrics Explained**:
- **Sleep Efficiency**: (Total sleep time / Time in bed) × 100%
- Good: > 85%
- Fair: 75-85%
- Poor: < 75%
- **Sleep Latency**: Time from lying down to falling asleep
- Normal: 10-20 minutes
- Fast: < 10 minutes (may indicate sleep deprivation)
- Slow: > 20 minutes (may indicate insomnia)
- **WASO**: Total wake time after initial sleep onset
- Lower is better (indicates fewer disruptions)
### Statistics
Aggregated metrics across multiple sessions for trend analysis.
**Available Statistics**:
- Average sleep duration over time range
- Average sleep efficiency
- Sleep stage distribution averages
- Trends and patterns
**Statistics API**:
```
GET /users/{user_id}/statistics/average?from={date}&to={date}
```
## Authentication
### API Key Management
All Asleep integrations require an API key for authentication.
**Obtaining an API Key**:
1. Sign up at https://dashboard.asleep.ai
2. Navigate to API key generation section
3. Create API key for the application
4. Store securely (never commit to version control)
**Using API Keys**:
For SDK Integration (iOS/Android):
```kotlin
// Android
AsleepConfig.init(
apiKey = "your_api_key_here",
userId = "user_unique_id",
...
)
```
```swift
// iOS
AsleepConfig.init(
apiKey: "your_api_key_here",
userId: "user_unique_id",
...
)
```
For REST API Integration:
```http
GET /sessions/{session_id}
X-API-Key: your_api_key_here
```
**Security Best Practices**:
- Store API keys in environment variables or secure storage
- Never hardcode keys in source code
- Use different keys for development and production
- Rotate keys periodically
- Monitor usage in Dashboard to detect unauthorized access
- Revoke compromised keys immediately
## Error Handling
Understanding error codes is critical for robust integration.
### Error Categories
**Critical Errors** (must stop tracking):
These errors indicate conditions that prevent continued tracking and require user intervention or code fixes.
**Warning Errors** (can continue tracking):
These are transient issues that the SDK handles automatically while tracking continues.
### Error Code Reference
#### Critical Errors
**ERR_MIC_PERMISSION**
- **Cause**: App lacks microphone access permission
- **Action**: Request microphone permission from user
- **Platform Notes**:
- Android: Check RECORD_AUDIO permission
- iOS: Check NSMicrophoneUsageDescription and authorization status
**ERR_AUDIO**
- **Cause**: Microphone unavailable or in use by another app
- **Action**:
- Close conflicting apps using microphone
- Check microphone hardware functionality
- Verify no audio conflicts in device settings
**ERR_INVALID_URL**
- **Cause**: Malformed API endpoint URL in configuration
- **Action**: Verify AsleepConfig URL format
- **Example Fix**: Ensure base URL is valid HTTPS endpoint
**ERR_COMMON_EXPIRED**
- **Cause**: API rate limit exceeded or subscription plan expired
- **Action**:
- Check Dashboard for plan status
- Review API usage patterns
- Upgrade plan if needed
- Implement rate limiting in application
**ERR_UPLOAD_FORBIDDEN**
- **Cause**: Multiple simultaneous tracking attempts with same user_id
- **Action**:
- Ensure only one device tracks per user at a time
- Check for orphaned sessions
- Implement proper session cleanup
**ERR_UPLOAD_NOT_FOUND** / **ERR_CLOSE_NOT_FOUND**
- **Cause**: Attempting to interact with non-existent or already-ended session
- **Action**:
- Verify session exists before operations
- Handle session expiration properly
- Implement session state management
#### Warning Errors
**ERR_AUDIO_SILENCED**
- **Cause**: Audio temporarily unavailable but tracking continues
- **Impact**: Minimal, SDK handles gracefully
- **Action**: Log for monitoring, tracking continues
**ERR_AUDIO_UNSILENCED**
- **Cause**: Audio restored after silence period
- **Impact**: Tracking resumes normally
- **Action**: Log for monitoring
**ERR_UPLOAD_FAILED**
- **Cause**: Network connectivity issue during data upload
- **Impact**: SDK will retry automatically
- **Action**:
- Verify network connectivity
- Check for firewall/proxy issues
- Monitor retry success
### Error Handling Patterns
**Distinguish Error Severity**:
```kotlin
// Android example
when (errorCode) {
in criticalErrors -> {
// Stop tracking, notify user
stopTracking()
showErrorDialog(errorCode)
}
in warningErrors -> {
// Log and continue
logWarning(errorCode)
}
}
```
**Provide User-Friendly Messages**:
```kotlin
fun getUserFriendlyMessage(errorCode: AsleepErrorCode): String {
return when (errorCode) {
ERR_MIC_PERMISSION -> "Please allow microphone access to track sleep"
ERR_AUDIO -> "Another app is using the microphone. Please close it and try again"
ERR_COMMON_EXPIRED -> "Your subscription has expired. Please renew to continue tracking"
// ... more mappings
}
}
```
**Implement Retry Logic**:
```kotlin
// For network-related errors
suspend fun uploadWithRetry(maxRetries: Int = 3) {
repeat(maxRetries) { attempt ->
try {
upload()
return
} catch (e: NetworkException) {
if (attempt == maxRetries - 1) throw e
delay(2.0.pow(attempt) * 1000) // Exponential backoff
}
}
}
```
## Data Plan Considerations
Asleep operates on different data plans that affect API usage.
**Plan Limits**:
- Request rate limits
- Total sessions per month
- Data retention periods
- Feature availability
**Monitoring Usage**:
- Check Dashboard regularly
- Implement usage tracking in application
- Set up alerts for approaching limits
- Plan capacity for user growth
## Integration Workflows
### Typical Integration Flow
1. **Setup Phase**:
- Obtain API key from Dashboard
- Install SDK (iOS/Android) or configure REST API client
- Configure authentication
2. **User Registration**:
- Create user in Asleep system
- Store user_id mapping in application
3. **Sleep Tracking Session**:
- Request necessary permissions
- Initialize SDK with user credentials
- Start tracking session
- Monitor session state
- Handle real-time data (optional)
- Stop tracking session
4. **Report Generation**:
- Wait for report processing (automatic)
- Fetch completed report
- Display sleep analysis to user
5. **Statistics & Trends**:
- Query historical sessions
- Calculate aggregated statistics
- Display trends over time
### Common Integration Scenarios
**Scenario 1: First-Time User Setup**
```
User downloads app
→ Request microphone permission
→ Create Asleep user (POST /users)
→ Initialize SDK with API key + user_id
→ Guide user through first tracking session
```
**Scenario 2: Returning User**
```
User opens app
→ Load user_id from local storage
→ Initialize SDK with credentials
→ Check for existing running session (reconnect if found)
→ Display previous sleep reports
```
**Scenario 3: Background Tracking**
```
User starts tracking before sleep
→ Start foreground service (Android) / background mode (iOS)
→ Maintain microphone access
→ Handle app lifecycle events
→ Continue tracking through sleep
→ Stop tracking in morning
→ Process and display report
```
## Platform Selection Guide
Choose the appropriate integration approach based on application type:
**Use iOS SDK (sleeptrack-ios)**:
- Native iOS application
- Need deep iOS integration (Siri, HealthKit, etc.)
- Require iOS-specific UI patterns
- Swift/SwiftUI development
**Use Android SDK (sleeptrack-android)**:
- Native Android application
- Need Android-specific features (foreground service, etc.)
- Require Android UI patterns
- Kotlin/Jetpack Compose development
**Use REST API (sleeptrack-be)**:
- Backend/server-side integration
- Multi-platform web application
- Data aggregation and analytics
- Webhook-based event processing
- Cross-platform mobile framework (React Native, Flutter) with custom bridge
## Resources
### Official Documentation
- **Main Documentation**: https://docs-en.asleep.ai
- **LLM-Optimized Reference**: https://docs-en.asleep.ai/llms.txt
- **Dashboard**: https://dashboard.asleep.ai
### Key Documentation Pages
- **QuickStart Guide**: https://docs-en.asleep.ai/docs/quickstart.md
- **Sleep Data Overview**: https://docs-en.asleep.ai/docs/sleep-data.md
- **System Overview**: https://docs-en.asleep.ai/docs/system-overview.md
- **API Basics**: https://docs-en.asleep.ai/docs/api-basics.md
- **Webhook Guide**: https://docs-en.asleep.ai/docs/webhook.md
- **Sleep Environment Guidelines**: https://docs-en.asleep.ai/docs/sleep-environment-guideline.md
### Platform-Specific Documentation
**Android**:
- Get Started: https://docs-en.asleep.ai/docs/android-get-started.md
- Error Codes: https://docs-en.asleep.ai/docs/android-error-codes.md
- AsleepConfig: https://docs-en.asleep.ai/docs/android-asleep-config.md
- SleepTrackingManager: https://docs-en.asleep.ai/docs/android-sleep-tracking-manager.md
**iOS**:
- Get Started: https://docs-en.asleep.ai/docs/ios-get-started.md
- Error Codes: https://docs-en.asleep.ai/docs/ios-error-codes.md
- AsleepConfig: https://docs-en.asleep.ai/docs/ios-asleep-config.md
- SleepTrackingManager: https://docs-en.asleep.ai/docs/ios-sleep-tracking-manager.md
### Reference Files
This skill includes detailed API reference documentation:
- `references/asleep_api_reference.md`: Comprehensive API endpoint reference, data structures, and integration patterns
To load this reference for detailed API information:
```
Read references/asleep_api_reference.md
```
## Best Practices Summary
### Security
- Never expose API keys in client code
- Implement secure storage for credentials
- Use HTTPS for all API communications
- Validate user permissions before operations
### Performance
- Cache reports when appropriate
- Batch API requests to respect rate limits
- Implement efficient session state management
- Monitor real-time data access patterns
### User Experience
- Provide clear permission rationales
- Show friendly error messages
- Display progress during tracking
- Handle app lifecycle gracefully
### Reliability
- Implement comprehensive error handling
- Add retry logic for transient failures
- Log errors for debugging
- Test edge cases (interruptions, low battery, etc.)
### Data Management
- Clean up old sessions appropriately
- Respect user privacy and data retention
- Implement proper user deletion flows
- Backup critical session data
## Next Steps
After understanding these foundational concepts, proceed to platform-specific skills:
- **iOS Development**: Use `sleeptrack-ios` skill
- **Android Development**: Use `sleeptrack-android` skill
- **Backend API Integration**: Use `sleeptrack-be` skill
Each platform-specific skill builds on this foundation with implementation details, code examples, and platform-specific patterns.

View File

@@ -0,0 +1,259 @@
# Asleep API Reference
## Official Documentation
- **Main Documentation**: https://docs-en.asleep.ai
- **LLM-Optimized Docs**: https://docs-en.asleep.ai/llms.txt
- **Dashboard**: https://dashboard.asleep.ai
## API Authentication
All Asleep API requests require authentication using an API key.
### Getting an API Key
1. Sign up for the Asleep Dashboard at https://dashboard.asleep.ai
2. Navigate to the API key generation section
3. Generate your API key for your application
4. Store the API key securely (never commit to version control)
### Authentication Method
Include the API key in request headers:
```
X-API-Key: your_api_key_here
```
## Core Concepts
### User Management
Each application user must be registered with Asleep before tracking sleep data.
- **User ID**: Unique identifier for each user (managed by your application)
- User data is associated with sessions and reports
- Users can have multiple sleep tracking sessions
### Sleep Sessions
A sleep session represents one tracking period from start to stop.
**Session States**:
- IDLE: Not tracking
- INITIALIZING: Preparing to track
- INITIALIZED: Ready to track
- TRACKING_STARTED: Active tracking in progress
- TRACKING_STOPPING: Ending tracking session
**Key Session Characteristics**:
- Minimum tracking time: 5 minutes for valid session
- Real-time data available after sequence 10, then every 10 sequences
- Each session generates a comprehensive sleep report
### Sleep Reports
Reports contain detailed analysis of a completed sleep session.
**Core Metrics**:
- Sleep stages: Wake, Light, Deep, REM
- Sleep efficiency: Percentage of time actually sleeping
- Sleep latency: Time to fall asleep
- Total sleep time
- Time in bed
- Wake after sleep onset (WASO)
- Sleep stage ratios and durations
- Snoring detection and analysis
### Statistics
Aggregated sleep metrics across multiple sessions.
**Available Statistics**:
- Average sleep duration
- Average sleep efficiency
- Sleep stage distribution
- Trends over time periods
## Error Codes
### Critical Errors (Stop Tracking)
- **ERR_MIC_PERMISSION**: Microphone access permission denied
- **ERR_AUDIO**: Microphone in use by another app or hardware issue
- **ERR_INVALID_URL**: Malformed API endpoint URL
- **ERR_COMMON_EXPIRED**: API rate limit exceeded or plan expired
- **ERR_UPLOAD_FORBIDDEN**: Multiple simultaneous tracking attempts with same user ID
- **ERR_UPLOAD_NOT_FOUND**: Session does not exist or already ended
- **ERR_CLOSE_NOT_FOUND**: Attempted to close non-existent session
### Warning Errors (Continue Tracking)
- **ERR_AUDIO_SILENCED**: Audio temporarily unavailable but tracking continues
- **ERR_AUDIO_UNSILENCED**: Audio restored after silence
- **ERR_UPLOAD_FAILED**: Network issue during upload, will retry
## REST API Endpoints
### User Management
**Create User**
```
POST /users
Body: { "user_id": "string" }
```
**Get User**
```
GET /users/{user_id}
```
**Update User**
```
PUT /users/{user_id}
Body: { /* user properties */ }
```
**Delete User**
```
DELETE /users/{user_id}
```
### Session Management
**Get Session**
```
GET /sessions/{session_id}
```
**List Sessions**
```
GET /sessions?user_id={user_id}&from={date}&to={date}
```
**Delete Session**
```
DELETE /sessions/{session_id}
```
### Statistics
**Get Average Stats**
```
GET /users/{user_id}/statistics/average?from={date}&to={date}
```
## Webhooks
Asleep supports webhooks for real-time event notifications.
**Supported Events**:
- Session started
- Session completed
- Report generated
- User created/updated/deleted
**Webhook Configuration**:
Configure webhook URLs in the Asleep Dashboard.
## Platform SDKs
### Android SDK
- **Language**: Kotlin
- **Minimum SDK**: Check latest documentation
- **Key Classes**: AsleepConfig, SleepTrackingManager
- **Architecture**: MVVM patterns, Hilt dependency injection
- **Permissions Required**:
- RECORD_AUDIO (microphone)
- POST_NOTIFICATIONS
- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
- FOREGROUND_SERVICE
### iOS SDK
- **Language**: Swift
- **Minimum iOS Version**: Check latest documentation
- **Key Classes**: AsleepConfig, SleepTrackingManager
- **Architecture**: Delegate patterns, Combine framework
- **Permissions Required**:
- Microphone access (NSMicrophoneUsageDescription)
- Notifications
- Background modes
## Data Structures
### Session
Represents a sleep tracking session with metadata and tracking state information.
### Report
Comprehensive sleep analysis including:
- Sleep stages timeline
- Statistical metrics
- Snoring analysis
- Quality indicators
### Statistics
Aggregated metrics across multiple sessions for trend analysis.
## Best Practices
### API Key Security
- Never hardcode API keys in source code
- Use environment variables or secure storage
- Rotate keys periodically
- Monitor usage in Dashboard
### User ID Management
- Use consistent user IDs across sessions
- Consider user privacy in ID scheme
- Implement proper user consent flows
### Error Handling
- Distinguish between critical errors (stop tracking) and warnings (continue)
- Provide user-friendly error messages
- Implement retry logic for network failures
- Log errors for debugging
### Session Management
- Validate minimum tracking time (5 minutes)
- Handle app lifecycle properly (don't lose sessions)
- Implement reconnection logic for interrupted sessions
- Clean up resources when stopping tracking
### Performance
- Check real-time data appropriately (after sequence 10, every 10 sequences)
- Cache reports when appropriate
- Batch API requests when possible
- Monitor API rate limits
## Common Use Cases
### Health & Fitness Apps
Integrate sleep tracking alongside activity tracking for comprehensive health insights.
### Healthcare & Wellness
Clinical-grade sleep monitoring for patient care and wellness programs.
### Sleep Tech
Dedicated sleep improvement applications with detailed analysis and recommendations.
### Smart Home & IoT
Integrate sleep data with smart home automation for optimized sleep environment.
## Support & Resources
- **Documentation**: https://docs-en.asleep.ai
- **Dashboard**: https://dashboard.asleep.ai
- **Email**: Contact through dashboard for technical support

View File

@@ -0,0 +1,526 @@
---
name: sleeptrack-ios
description: This skill helps iOS developers integrate the Asleep SDK for sleep tracking functionality. Use this skill when building native iOS apps with Swift/SwiftUI that need sleep tracking capabilities, implementing delegate patterns, configuring iOS permissions (microphone, notifications, background modes), managing tracking lifecycle, integrating Siri Shortcuts, or working with Combine framework for reactive state management.
---
# Sleeptrack iOS
## Overview
This skill provides comprehensive guidance for integrating the Asleep SDK into native iOS applications using Swift and SwiftUI. It covers SDK setup, iOS-specific permissions, delegate-based architecture, tracking lifecycle management, Combine framework integration, and Siri Shortcuts support.
Use this skill when:
- Building native iOS sleep tracking applications
- Implementing SwiftUI-based tracking interfaces
- Managing iOS permissions and background modes
- Working with delegate patterns for SDK callbacks
- Integrating Siri Shortcuts for voice-activated tracking
- Using Combine framework for reactive state management
**Prerequisites**: Developers should first review the `sleeptrack-foundation` skill to understand core Asleep concepts, authentication, data structures, and error handling before implementing iOS-specific integration.
## Quick Start
### 1. Installation
Add AsleepSDK to your Xcode project using Swift Package Manager:
```swift
// In Xcode: File Add Packages
// Enter package URL: https://github.com/asleep-ai/asleep-sdk-ios
```
Or add to `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/asleep-ai/asleep-sdk-ios", from: "2.0.0")
]
```
### 2. Configure iOS Permissions
Add required permissions to `Info.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Microphone access for audio-based sleep tracking -->
<key>NSMicrophoneUsageDescription</key>
<string>This app uses your microphone to track sleep stages and detect snoring during sleep.</string>
<!-- Background audio mode for continuous tracking -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<!-- Optional: For notification reminders -->
<key>NSUserNotificationsUsageDescription</key>
<string>Get reminders to start and stop sleep tracking.</string>
</dict>
</plist>
```
### 3. Basic Setup
```swift
import SwiftUI
import AsleepSDK
@main
struct SleepTrackerApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
}
}
```
## SDK Architecture
The Asleep iOS SDK follows a delegate-based architecture with three main components:
### 1. AsleepConfig - Configuration and User Management
**Purpose**: Initialize SDK with API credentials and manage user lifecycle.
**Key Delegate**: `AsleepConfigDelegate`
```swift
protocol AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config)
func didFailUserJoin(error: Asleep.AsleepError)
func userDidDelete(userId: String)
}
```
### 2. SleepTrackingManager - Tracking Lifecycle
**Purpose**: Control sleep tracking start, stop, and monitor session state.
**Key Delegate**: `AsleepSleepTrackingManagerDelegate`
```swift
protocol AsleepSleepTrackingManagerDelegate {
func didCreate() // Session created
func didUpload(sequence: Int) // Data uploaded
func didClose(sessionId: String) // Tracking stopped
func didFail(error: Asleep.AsleepError) // Error occurred
func didInterrupt() // Interrupted (e.g., phone call)
func didResume() // Resumed after interruption
func micPermissionWasDenied() // Mic permission denied
func analysing(session: Asleep.Model.Session) // Real-time data (optional)
}
```
### 3. Reports - Retrieving Sleep Data
**Purpose**: Fetch sleep reports and session lists after tracking completes.
```swift
// Reports API is async/await based, not delegate-driven
let reports = Asleep.createReports(config: config)
// Get single report
let report = try await reports.report(sessionId: "session_id")
// Get report list
let reportList = try await reports.reports(
fromDate: "2024-01-01",
toDate: "2024-01-31"
)
```
## Implementation Overview
### Minimal ViewModel Example
```swift
import Foundation
import Combine
import AsleepSDK
final class SleepTrackingViewModel: ObservableObject {
private(set) var trackingManager: Asleep.SleepTrackingManager?
private(set) var reports: Asleep.Reports?
@Published var isTracking = false
@Published var error: String?
@Published private(set) var config: Asleep.Config?
func initAsleepConfig(apiKey: String, userId: String) {
Asleep.initAsleepConfig(
apiKey: apiKey,
userId: userId,
delegate: self
)
}
func startTracking() {
trackingManager?.startTracking()
}
func stopTracking() {
trackingManager?.stopTracking()
}
}
// Implement delegates
extension SleepTrackingViewModel: AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config) {
Task { @MainActor in
self.config = config
self.trackingManager = Asleep.createSleepTrackingManager(
config: config,
delegate: self
)
}
}
func didFailUserJoin(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = error.localizedDescription
}
}
func userDidDelete(userId: String) {
// Handle user deletion
}
}
extension SleepTrackingViewModel: AsleepSleepTrackingManagerDelegate {
func didCreate() {
Task { @MainActor in
self.isTracking = true
}
}
func didClose(sessionId: String) {
Task { @MainActor in
self.isTracking = false
// Initialize reports to fetch session data
self.reports = Asleep.createReports(config: config!)
}
}
func didFail(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = error.localizedDescription
}
}
// Implement other delegate methods as needed
}
```
For complete ViewModel implementation with all delegate methods, see [references/complete_viewmodel_implementation.md](references/complete_viewmodel_implementation.md)
## iOS-Specific Features
### 1. Siri Shortcuts
Enable voice-activated tracking with App Intents (iOS 16+). Users can say "Hey Siri, start sleep" or "Hey Siri, stop sleep".
For complete Siri Shortcuts implementation, see [references/ios_specific_features.md](references/ios_specific_features.md#siri-shortcuts-integration)
### 2. Background Audio Mode
Configure background audio to maintain tracking during sleep. Simply add `audio` to `UIBackgroundModes` in Info.plist - iOS handles the rest automatically.
For details, see [references/ios_specific_features.md](references/ios_specific_features.md#background-audio-mode)
### 3. Microphone Permission
Request microphone permission before starting tracking:
```swift
import AVFoundation
func requestMicrophonePermission() async -> Bool {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted: return true
case .denied: return false
case .undetermined:
return await AVAudioSession.sharedInstance().requestRecordPermission()
@unknown default: return false
}
}
```
For complete permission handling, see [references/ios_specific_features.md](references/ios_specific_features.md#microphone-permission-handling)
### 4. App Lifecycle Management
Handle app state transitions using SwiftUI's `scenePhase`:
```swift
struct SleepTrackingView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
// ... view content ...
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active: print("App is active")
case .inactive: print("App is inactive")
case .background: print("App in background - tracking continues")
@unknown default: break
}
}
}
}
```
For advanced lifecycle patterns, see [references/ios_specific_features.md](references/ios_specific_features.md#app-lifecycle-management)
### 5. Persistent Storage
Store configuration using AppStorage:
```swift
struct SleepTrackingView: View {
@AppStorage("sleepapp+apikey") private var apiKey = ""
@AppStorage("sleepapp+userid") private var userId = ""
// Values automatically persist across app launches
}
```
## Error Handling
### Common Error Patterns
```swift
func handleError(_ error: Asleep.AsleepError) {
switch error {
case .micPermission:
// Guide user to Settings
showMicPermissionAlert()
case .audioSessionError:
// Another app is using microphone
showAudioUnavailableAlert()
case let .httpStatus(code, _, message):
switch code {
case 403: // Session already active on another device
case 404: // Session not found
default: break
}
default:
showGenericError(error.localizedDescription)
}
}
```
### Retry with Exponential Backoff
```swift
func startTrackingWithRetry() {
trackingManager?.startTracking()
}
func didFail(error: Asleep.AsleepError) {
if isTransientError(error) && retryCount < maxRetries {
retryCount += 1
DispatchQueue.main.asyncAfter(deadline: .now() + pow(2.0, Double(retryCount))) {
self.startTrackingWithRetry()
}
} else {
handleError(error)
}
}
```
For comprehensive error handling patterns, see [references/advanced_patterns.md](references/advanced_patterns.md#error-recovery-patterns)
## Best Practices
### 1. State Management
Use `@Published` properties for reactive UI updates:
```swift
final class SleepTrackingViewModel: ObservableObject {
@Published var isTracking = false
@Published var error: String?
// UI automatically updates when values change
}
```
### 2. Main Thread Safety
Always update UI on main thread:
```swift
func didCreate() {
Task { @MainActor in // Ensures main thread
self.isTracking = true
}
}
```
### 3. Resource Cleanup
```swift
final class SleepTrackingViewModel: ObservableObject {
deinit {
trackingManager = nil
reports = nil
}
}
```
### 4. User Experience
Provide clear visual feedback with loading states, progress indicators, and error messages. Disable controls appropriately during tracking.
### 5. Testing Considerations
Use dependency injection for testable code:
```swift
protocol SleepTrackingManagerProtocol {
func startTracking()
func stopTracking()
}
// Production and mock implementations
```
For complete testing patterns, see [references/advanced_patterns.md](references/advanced_patterns.md#testing-patterns)
## Common Integration Patterns
### Pattern 1: Simple Single-View App
Best for basic sleep tracking with minimal features. Single view with tracking controls.
### Pattern 2: Multi-View App with Navigation
Best for apps with reports, settings, and history. Uses TabView for navigation between Track, History, and Settings.
### Pattern 3: Centralized SDK Manager
Best for complex apps sharing SDK instance across views. Single source of truth with `AsleepSDKManager.shared`.
For complete implementation of all patterns, see [references/advanced_patterns.md](references/advanced_patterns.md)
## Real-time Data Access
Access preliminary sleep data during tracking (available after sequence 10):
```swift
func analysing(session: Asleep.Model.Session) {
Task { @MainActor in
if let sleepStages = session.sleepStages {
updateRealtimeChart(stages: sleepStages)
}
}
}
func didUpload(sequence: Int) {
// Real-time data available every 10 sequences after sequence 10
if sequence >= 10 && sequence % 10 == 0 {
// SDK automatically calls analysing() delegate
}
}
```
## Fetching Reports
Retrieve sleep session data after tracking:
```swift
func fetchReport(sessionId: String) async {
do {
let report = try await reports?.report(sessionId: sessionId)
// Process report data
} catch {
// Handle error
}
}
// Fetch multiple sessions
func fetchReportList() async {
let reportList = try await reports?.reports(
fromDate: "2024-01-01",
toDate: "2024-01-31"
)
}
```
## Troubleshooting
### Tracking Doesn't Start
**Causes**: Missing microphone permission, empty API key/user ID, another app using microphone
**Solution**: Validate configuration and check microphone permission before starting
### Background Tracking Stops
**Causes**: Background audio mode not configured, memory pressure, force-closed app
**Solution**: Ensure `UIBackgroundModes` includes `audio` in Info.plist
### Reports Not Available
**Causes**: Session processing incomplete (takes time), minimum duration not met (5 minutes), network issues
**Solution**: Implement retry logic with exponential backoff when fetching reports
For detailed troubleshooting, see the complete implementation examples in references/
## Sample Code Reference
This skill is based on the official Asleep iOS sample app:
- **MainViewModel.swift**: Complete ViewModel with all delegates
- **MainView.swift**: SwiftUI view with tracking controls
- **StartSleepIntent.swift / StopSleepIntent.swift**: Siri Shortcuts
- **ReportView.swift**: Sleep report display
- **Info.plist**: Required iOS permissions
Sample app: [Asleep iOS Sample App](https://github.com/asleep-ai/asleep-sdk-ios-sampleapp-public)
## Resources
### Official Documentation
- **iOS Get Started**: https://docs-en.asleep.ai/docs/ios-get-started.md
- **iOS Error Codes**: https://docs-en.asleep.ai/docs/ios-error-codes.md
- **AsleepConfig Reference**: https://docs-en.asleep.ai/docs/ios-asleep-config.md
- **SleepTrackingManager Reference**: https://docs-en.asleep.ai/docs/ios-sleep-tracking-manager.md
- **Sample App Guide**: https://docs-en.asleep.ai/docs/sample-app.md
### Apple Documentation
- **SwiftUI**: https://developer.apple.com/documentation/swiftui
- **Combine**: https://developer.apple.com/documentation/combine
- **AVAudioSession**: https://developer.apple.com/documentation/avfaudio/avaudiosession
- **App Intents**: https://developer.apple.com/documentation/appintents
- **Background Modes**: https://developer.apple.com/documentation/xcode/configuring-background-execution-modes
### Related Skills
- **sleeptrack-foundation**: Core Asleep concepts, authentication, and data structures
- **sleeptrack-android**: Android-specific implementation guide
- **sleeptrack-be**: Backend API integration
## Next Steps
After integrating the iOS SDK:
1. Test thoroughly across different iOS devices and versions
2. Implement proper error handling for all edge cases
3. Add user-friendly error messages and recovery flows
4. Consider HealthKit integration for data export
5. Implement notification reminders for tracking
6. Add data visualization for sleep trends
7. Consider Apple Watch companion app
8. Submit to App Store with proper privacy declarations

View File

@@ -0,0 +1,460 @@
# Advanced Integration Patterns
Comprehensive patterns for different app architectures and advanced error handling strategies.
## Pattern 1: Simple Single-View App
Best for: Basic sleep tracking app with minimal features.
```swift
@main
struct SimpleSleepApp: App {
var body: some Scene {
WindowGroup {
SleepTrackingView()
}
}
}
```
### Advantages
- Simple architecture
- Easy to understand and maintain
- Minimal code overhead
- Fast development
### Use Cases
- MVP or prototype apps
- Single-purpose sleep trackers
- Learning/demo applications
## Pattern 2: Multi-View App with Navigation
Best for: Apps with reports, settings, and history.
```swift
struct ContentView: View {
var body: some View {
TabView {
SleepTrackingView()
.tabItem {
Label("Track", systemImage: "moon.zzz")
}
ReportHistoryView()
.tabItem {
Label("History", systemImage: "chart.bar")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gear")
}
}
}
}
```
### Supporting Views
```swift
struct ReportHistoryView: View {
@StateObject private var viewModel = ReportHistoryViewModel()
var body: some View {
NavigationView {
List(viewModel.sessions) { session in
NavigationLink {
ReportDetailView(sessionId: session.id)
} label: {
SessionRow(session: session)
}
}
.navigationTitle("Sleep History")
.onAppear {
viewModel.loadSessions()
}
}
}
}
struct SettingsView: View {
@AppStorage("sleepapp+apikey") private var apiKey = ""
@AppStorage("sleepapp+userid") private var userId = ""
@AppStorage("sleepapp+notifications") private var notificationsEnabled = false
var body: some View {
NavigationView {
Form {
Section("Account") {
TextField("User ID", text: $userId)
SecureField("API Key", text: $apiKey)
}
Section("Preferences") {
Toggle("Enable Notifications", isOn: $notificationsEnabled)
}
}
.navigationTitle("Settings")
}
}
}
```
### Advantages
- Clear separation of concerns
- Scalable for additional features
- Familiar tab-based navigation
- Easy to add new sections
## Pattern 3: Centralized SDK Manager
Best for: Complex apps sharing SDK instance across views.
```swift
final class AsleepSDKManager: ObservableObject {
static let shared = AsleepSDKManager()
@Published var config: Asleep.Config?
@Published var isInitialized = false
@Published var error: String?
private var trackingManager: Asleep.SleepTrackingManager?
private var reports: Asleep.Reports?
private init() {}
func initialize(apiKey: String, userId: String) {
Asleep.initAsleepConfig(
apiKey: apiKey,
userId: userId,
delegate: self
)
}
func getTrackingManager() -> Asleep.SleepTrackingManager? {
guard let config else { return nil }
if trackingManager == nil {
trackingManager = Asleep.createSleepTrackingManager(
config: config,
delegate: self
)
}
return trackingManager
}
func getReports() -> Asleep.Reports? {
guard let config else { return nil }
if reports == nil {
reports = Asleep.createReports(config: config)
}
return reports
}
func reset() {
trackingManager = nil
reports = nil
config = nil
isInitialized = false
}
}
// MARK: - Delegates
extension AsleepSDKManager: AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config) {
Task { @MainActor in
self.config = config
self.isInitialized = true
}
}
func didFailUserJoin(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = "Failed to join: \(error.localizedDescription)"
}
}
func userDidDelete(userId: String) {
Task { @MainActor in
reset()
}
}
}
// Usage in views
struct SleepTrackingView: View {
@ObservedObject var sdkManager = AsleepSDKManager.shared
@StateObject private var trackingState = TrackingStateViewModel()
var body: some View {
VStack {
if sdkManager.isInitialized {
TrackingControls(manager: sdkManager.getTrackingManager())
} else {
ConfigurationView()
}
}
}
}
struct ReportHistoryView: View {
@ObservedObject var sdkManager = AsleepSDKManager.shared
var body: some View {
NavigationView {
if sdkManager.isInitialized {
ReportList(reports: sdkManager.getReports())
} else {
Text("Please configure the app first")
}
}
}
}
```
### Advantages
- Single source of truth for SDK state
- Prevents duplicate SDK instances
- Centralized error handling
- Easy to manage lifecycle across app
### Testing Strategy
```swift
protocol AsleepSDKManagerProtocol {
var config: Asleep.Config? { get }
var isInitialized: Bool { get }
func initialize(apiKey: String, userId: String)
func getTrackingManager() -> Asleep.SleepTrackingManager?
}
// For testing
class MockSDKManager: AsleepSDKManagerProtocol, ObservableObject {
@Published var config: Asleep.Config?
@Published var isInitialized = false
func initialize(apiKey: String, userId: String) {
isInitialized = true
}
func getTrackingManager() -> Asleep.SleepTrackingManager? {
nil
}
}
```
## Error Recovery Patterns
### Automatic Retry with Exponential Backoff
```swift
final class SleepTrackingViewModel: ObservableObject {
private var retryCount = 0
private let maxRetries = 3
func startTrackingWithRetry() {
trackingManager?.startTracking()
}
func didFail(error: Asleep.AsleepError) {
if isTransientError(error) && retryCount < maxRetries {
retryCount += 1
let delay = pow(2.0, Double(retryCount))
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.startTrackingWithRetry()
}
} else {
retryCount = 0
handleError(error)
}
}
private func isTransientError(_ error: Asleep.AsleepError) -> Bool {
switch error {
case .networkError, .uploadFailed:
return true
default:
return false
}
}
}
```
### Error Categorization and Handling
```swift
extension SleepTrackingViewModel {
func handleError(_ error: Asleep.AsleepError) {
switch error {
case .micPermission:
showAlert(
title: "Microphone Access Required",
message: "Please enable microphone access in Settings to track sleep.",
action: openSettings
)
case .audioSessionError:
showAlert(
title: "Audio Unavailable",
message: "Another app is using the microphone. Please close it and try again."
)
case let .httpStatus(code, _, message):
switch code {
case 403:
showAlert(
title: "Session Already Active",
message: "Another device is tracking with this user ID."
)
case 404:
showAlert(
title: "Session Not Found",
message: "The tracking session could not be found."
)
default:
showAlert(
title: "Error \(code)",
message: message ?? "An unknown error occurred"
)
}
default:
showAlert(
title: "Error",
message: error.localizedDescription
)
}
}
private func openSettings() {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
```
### Report Fetching with Retry
```swift
func fetchReportWithRetry(sessionId: String, maxAttempts: Int = 5) async {
for attempt in 1...maxAttempts {
do {
let report = try await reports?.report(sessionId: sessionId)
await MainActor.run {
self.currentReport = report
}
return
} catch {
if attempt < maxAttempts {
// Wait before retrying (exponential backoff)
let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
try? await Task.sleep(nanoseconds: delay)
} else {
await MainActor.run {
self.error = "Report not ready. Please try again later."
}
}
}
}
}
```
### Graceful Degradation
```swift
struct SleepTrackingView: View {
@StateObject private var viewModel = SleepTrackingViewModel()
@State private var offlineMode = false
var body: some View {
VStack {
if offlineMode {
OfflineModeView()
} else {
OnlineModeView(viewModel: viewModel)
}
}
.onReceive(viewModel.$error) { error in
if let error = error, isNetworkError(error) {
offlineMode = true
}
}
}
private func isNetworkError(_ error: String) -> Bool {
error.contains("network") || error.contains("connection")
}
}
struct OfflineModeView: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "wifi.slash")
.font(.system(size: 60))
.foregroundColor(.secondary)
Text("Offline Mode")
.font(.headline)
Text("Sleep tracking data will sync when connection is restored")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
}
```
## Testing Patterns
### Dependency Injection for Testing
```swift
protocol SleepTrackingManagerProtocol {
func startTracking()
func stopTracking()
}
final class SleepTrackingViewModel: ObservableObject {
private let trackingManager: SleepTrackingManagerProtocol
init(trackingManager: SleepTrackingManagerProtocol) {
self.trackingManager = trackingManager
}
func startTracking() {
trackingManager.startTracking()
}
}
// For testing
class MockTrackingManager: SleepTrackingManagerProtocol {
var startTrackingCalled = false
var stopTrackingCalled = false
func startTracking() {
startTrackingCalled = true
}
func stopTracking() {
stopTrackingCalled = true
}
}
// Usage in tests
func testStartTracking() {
let mockManager = MockTrackingManager()
let viewModel = SleepTrackingViewModel(trackingManager: mockManager)
viewModel.startTracking()
XCTAssertTrue(mockManager.startTrackingCalled)
}
```

View File

@@ -0,0 +1,421 @@
# Complete ViewModel Implementation
This reference provides full implementation examples for iOS sleep tracking using Combine and SwiftUI.
## Full ViewModel with Combine
```swift
import Foundation
import Combine
import AsleepSDK
final class SleepTrackingViewModel: ObservableObject {
// MARK: - SDK Components
private(set) var trackingManager: Asleep.SleepTrackingManager?
private(set) var reports: Asleep.Reports?
// MARK: - Published State
@Published var userId: String?
@Published var sessionId: String?
@Published var sequenceNumber: Int?
@Published var error: String?
@Published var isTracking = false
@Published var currentReport: Asleep.Model.Report?
@Published var reportList: [Asleep.Model.SleepSession]?
@Published private(set) var config: Asleep.Config?
// MARK: - Initialization
func initAsleepConfig(
apiKey: String,
userId: String,
baseUrl: URL? = nil,
callbackUrl: URL? = nil
) {
Asleep.initAsleepConfig(
apiKey: apiKey,
userId: userId.isEmpty ? nil : userId,
baseUrl: baseUrl,
callbackUrl: callbackUrl,
delegate: self
)
// Optional: Enable debug logging
Asleep.setDebugLoggerDelegate(self)
}
func initSleepTrackingManager() {
guard let config else { return }
trackingManager = Asleep.createSleepTrackingManager(
config: config,
delegate: self
)
}
func initReports() {
guard let config else { return }
reports = Asleep.createReports(config: config)
}
// MARK: - Tracking Control
func startTracking() {
trackingManager?.startTracking()
}
func stopTracking() {
trackingManager?.stopTracking()
initReports()
}
}
// MARK: - AsleepConfigDelegate
extension SleepTrackingViewModel: AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config) {
Task { @MainActor in
self.config = config
self.userId = userId
initSleepTrackingManager()
}
}
func didFailUserJoin(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = "Failed to join: \(error.localizedDescription)"
}
}
func userDidDelete(userId: String) {
Task { @MainActor in
self.userId = nil
self.config = nil
}
}
}
// MARK: - AsleepSleepTrackingManagerDelegate
extension SleepTrackingViewModel: AsleepSleepTrackingManagerDelegate {
func didCreate() {
Task { @MainActor in
self.isTracking = true
self.error = nil
}
}
func didUpload(sequence: Int) {
Task { @MainActor in
self.sequenceNumber = sequence
}
}
func didClose(sessionId: String) {
Task { @MainActor in
self.isTracking = false
self.sessionId = sessionId
}
}
func didFail(error: Asleep.AsleepError) {
switch error {
case let .httpStatus(code, _, message) where code == 403 || code == 404:
Task { @MainActor in
self.isTracking = false
self.error = "\(code): \(message ?? "Unknown error")"
}
default:
Task { @MainActor in
self.error = error.localizedDescription
}
}
}
func didInterrupt() {
Task { @MainActor in
self.error = "Tracking interrupted (e.g., phone call)"
}
}
func didResume() {
Task { @MainActor in
self.error = nil
}
}
func micPermissionWasDenied() {
Task { @MainActor in
self.isTracking = false
self.error = "Microphone permission denied. Please enable in Settings."
}
}
func analysing(session: Asleep.Model.Session) {
// Optional: Handle real-time analysis data
print("Real-time analysis:", session)
}
}
// MARK: - AsleepDebugLoggerDelegate (Optional)
extension SleepTrackingViewModel: AsleepDebugLoggerDelegate {
func didPrint(message: String) {
print("[Asleep SDK]", message)
}
}
```
## Complete SwiftUI View
```swift
import SwiftUI
struct SleepTrackingView: View {
@StateObject private var viewModel = SleepTrackingViewModel()
@AppStorage("sampleapp+apikey") private var apiKey = ""
@AppStorage("sampleapp+userid") private var userId = ""
@State private var startTime: Date?
@State private var showingReport = false
var body: some View {
VStack(spacing: 20) {
// Configuration Section
VStack(alignment: .leading, spacing: 8) {
Text("Configuration")
.font(.headline)
TextField("API Key", text: $apiKey)
.textFieldStyle(.roundedBorder)
.disabled(viewModel.isTracking)
TextField("User ID", text: $userId)
.textFieldStyle(.roundedBorder)
.disabled(viewModel.isTracking)
}
.padding()
// Status Section
VStack(alignment: .leading, spacing: 8) {
Text("Status")
.font(.headline)
if let error = viewModel.error {
Text("Error: \(error)")
.foregroundColor(.red)
.font(.caption)
}
if viewModel.isTracking {
HStack {
ProgressView()
Text("Tracking...")
}
if let sequence = viewModel.sequenceNumber {
Text("Sequence: \(sequence)")
.font(.caption)
}
}
if let sessionId = viewModel.sessionId {
Text("Session ID: \(sessionId)")
.font(.caption)
.lineLimit(1)
}
}
.padding()
// Tracking Control
Button(action: {
if viewModel.isTracking {
stopTracking()
} else {
startTracking()
}
}) {
Text(viewModel.isTracking ? "Stop Tracking" : "Start Tracking")
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.isTracking ? Color.red : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(apiKey.isEmpty || userId.isEmpty)
.padding(.horizontal)
// Report Access
if viewModel.sessionId != nil {
Button("View Report") {
fetchReport()
}
.padding()
}
Spacer()
}
.padding()
.sheet(isPresented: $showingReport) {
ReportView(report: viewModel.currentReport)
}
}
private func startTracking() {
viewModel.sessionId = nil
viewModel.sequenceNumber = nil
if viewModel.config == nil {
viewModel.initAsleepConfig(
apiKey: apiKey,
userId: userId
)
} else {
viewModel.startTracking()
}
startTime = Date()
}
private func stopTracking() {
viewModel.stopTracking()
}
private func fetchReport() {
guard let sessionId = viewModel.sessionId else { return }
Task {
do {
let report = try await viewModel.reports?.report(sessionId: sessionId)
await MainActor.run {
viewModel.currentReport = report
showingReport = true
}
} catch {
await MainActor.run {
viewModel.error = "Failed to fetch report: \(error.localizedDescription)"
}
}
}
}
}
```
## Complete Report View
```swift
import SwiftUI
import AsleepSDK
struct ReportView: View {
@Environment(\.dismiss) private var dismiss
let report: Asleep.Model.Report?
var body: some View {
NavigationView {
ScrollView {
if let report = report {
VStack(alignment: .leading, spacing: 16) {
// Session Information
Section("Session Information") {
InfoRow(label: "Session ID", value: report.session.id)
InfoRow(label: "Start Time", value: report.session.startTime.formatted())
if let endTime = report.session.endTime {
InfoRow(label: "End Time", value: endTime.formatted())
}
InfoRow(label: "State", value: report.session.state.rawValue)
}
Divider()
// Sleep Statistics
if let stat = report.stat {
Section("Sleep Statistics") {
StatRow(label: "Sleep Efficiency", value: stat.sleepEfficiency, unit: "%")
StatRow(label: "Sleep Latency", value: stat.sleepLatency, unit: "min")
StatRow(label: "Total Sleep Time", value: stat.sleepTime, unit: "min")
StatRow(label: "Time in Bed", value: stat.timeInBed, unit: "min")
}
Divider()
Section("Sleep Stages") {
StatRow(label: "Deep Sleep", value: stat.timeInDeep, unit: "min")
StatRow(label: "Light Sleep", value: stat.timeInLight, unit: "min")
StatRow(label: "REM Sleep", value: stat.timeInRem, unit: "min")
StatRow(label: "Wake Time", value: stat.timeInWake, unit: "min")
}
Divider()
Section("Snoring Analysis") {
StatRow(label: "Time Snoring", value: stat.timeInSnoring, unit: "min")
StatRow(label: "Snoring Count", value: stat.snoringCount, unit: "times")
}
}
}
.padding()
} else {
Text("No report available")
.foregroundColor(.secondary)
}
}
.navigationTitle("Sleep Report")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
}
struct InfoRow: View {
let label: String
let value: String
var body: some View {
HStack {
Text(label)
.foregroundColor(.secondary)
Spacer()
Text(value)
}
}
}
struct StatRow: View {
let label: String
let value: Int?
let unit: String
var body: some View {
HStack {
Text(label)
.foregroundColor(.secondary)
Spacer()
if let value = value {
Text("\(value) \(unit)")
} else {
Text("N/A")
.foregroundColor(.secondary)
}
}
}
}
struct Section<Content: View>: View {
let title: String
let content: Content
init(_ title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
content
}
}
}
```

View File

@@ -0,0 +1,284 @@
# iOS-Specific Features
Detailed implementation guides for iOS platform features including Siri Shortcuts, background modes, permissions, lifecycle management, and persistent storage.
## Siri Shortcuts Integration
Enable voice-activated tracking with App Intents (iOS 16+):
```swift
// StartSleepIntent.swift
import AppIntents
@available(iOS 16, *)
struct StartSleepIntent: AppIntent {
static var title: LocalizedStringResource = "Start Sleep"
static var description = IntentDescription("Start Sleep Tracking")
func perform() async throws -> some IntentResult {
NotificationCenter.default.post(name: .startSleep, object: nil)
return .result()
}
}
// StopSleepIntent.swift
@available(iOS 16, *)
struct StopSleepIntent: AppIntent {
static var title: LocalizedStringResource = "Stop Sleep"
static var description = IntentDescription("Stop Sleep Tracking")
func perform() async throws -> some IntentResult {
NotificationCenter.default.post(name: .stopSleep, object: nil)
return .result()
}
}
// Notification extensions
extension Notification.Name {
static let startSleep = Notification.Name("startSleep")
static let stopSleep = Notification.Name("stopSleep")
}
```
### Handling Shortcuts in Views
```swift
struct SleepTrackingView: View {
@StateObject private var viewModel = SleepTrackingViewModel()
var body: some View {
// ... view content ...
.onReceive(NotificationCenter.default.publisher(for: .startSleep)) { _ in
if !viewModel.isTracking {
startTracking()
}
}
.onReceive(NotificationCenter.default.publisher(for: .stopSleep)) { _ in
if viewModel.isTracking {
stopTracking()
}
}
}
}
```
Users can then say: "Hey Siri, start sleep" or "Hey Siri, stop sleep"
## Background Audio Mode
Configure background audio to maintain tracking during sleep.
### Info.plist Configuration
```xml
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
```
### Implementation Notes
- iOS automatically maintains background audio session during tracking
- App remains active in background while microphone is in use
- User sees audio indicator (red bar/pill) showing active recording
- No additional code needed beyond Info.plist configuration
- System handles audio session management automatically
### Best Practices
1. Inform users why the app needs background audio mode
2. Display clear status indicators when tracking is active
3. Handle audio interruptions gracefully (phone calls, other apps)
4. Test background behavior thoroughly on physical devices
## Microphone Permission Handling
Request and handle microphone permission properly:
```swift
import AVFoundation
func requestMicrophonePermission() async -> Bool {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted:
return true
case .denied:
return false
case .undetermined:
return await AVAudioSession.sharedInstance().requestRecordPermission()
@unknown default:
return false
}
}
// Usage in SwiftUI
Button("Start Tracking") {
Task {
let hasPermission = await requestMicrophonePermission()
if hasPermission {
startTracking()
} else {
showPermissionAlert = true
}
}
}
```
### Permission Alert Handling
```swift
struct PermissionAlert: ViewModifier {
@Binding var showAlert: Bool
func body(content: Content) -> some View {
content
.alert("Microphone Access Required", isPresented: $showAlert) {
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Please enable microphone access in Settings to track sleep.")
}
}
}
```
### Checking Permission Status
```swift
func checkMicrophonePermission() -> Bool {
let status = AVAudioSession.sharedInstance().recordPermission
return status == .granted
}
```
## App Lifecycle Management
Handle app state transitions gracefully:
```swift
struct SleepTrackingView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
// ... view content ...
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("App is active")
// Refresh UI state if needed
case .inactive:
print("App is inactive")
// Prepare for potential backgrounding
case .background:
// Tracking continues in background with audio mode
print("App is in background")
// Minimal operations only
@unknown default:
break
}
}
}
}
```
### Advanced Lifecycle Handling
```swift
class AppLifecycleObserver: ObservableObject {
@Published var isActive = true
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.handleEnterBackground()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.handleEnterForeground()
}
.store(in: &cancellables)
}
private func handleEnterBackground() {
isActive = false
// Save state, reduce operations
}
private func handleEnterForeground() {
isActive = true
// Refresh state, resume operations
}
}
```
## Persistent Storage with AppStorage
Store configuration persistently across app launches:
```swift
struct SleepTrackingView: View {
@AppStorage("sleepapp+apikey") private var apiKey = ""
@AppStorage("sleepapp+userid") private var userId = ""
@AppStorage("sleepapp+baseurl") private var baseUrl = ""
// Values automatically persist across app launches
// Uses UserDefaults under the hood
}
```
### Custom Storage Keys
```swift
extension String {
static let apiKeyStorage = "sleepapp+apikey"
static let userIdStorage = "sleepapp+userid"
static let baseUrlStorage = "sleepapp+baseurl"
}
struct SleepTrackingView: View {
@AppStorage(.apiKeyStorage) private var apiKey = ""
@AppStorage(.userIdStorage) private var userId = ""
@AppStorage(.baseUrlStorage) private var baseUrl = ""
}
```
### Advanced Persistent Storage
```swift
class PersistentSettings: ObservableObject {
@AppStorage("sleepapp+apikey") var apiKey = ""
@AppStorage("sleepapp+userid") var userId = ""
@AppStorage("sleepapp+baseurl") var baseUrl = ""
@AppStorage("sleepapp+notifications") var notificationsEnabled = false
@AppStorage("sleepapp+lastSessionId") var lastSessionId = ""
func clearAll() {
apiKey = ""
userId = ""
baseUrl = ""
notificationsEnabled = false
lastSessionId = ""
}
}
// Usage
struct SettingsView: View {
@StateObject private var settings = PersistentSettings()
var body: some View {
Form {
TextField("API Key", text: $settings.apiKey)
TextField("User ID", text: $settings.userId)
Toggle("Notifications", isOn: $settings.notificationsEnabled)
}
}
}
```