Skip to content

Commit a692fb6

Browse files
gunoooo박건우claude
authored
feat: add session tracking (#7)
Co-authored-by: 박건우 <gunwoo@baggeon-uui-MacBookPro.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c2b80a6 commit a692fb6

7 files changed

Lines changed: 149 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.0.4] - 2026-02-12
8+
9+
### Added
10+
11+
- Session tracking with automatic SESSION_START lifecycle events
12+
713
## [0.0.3] - 2025-12-01
814

915
### Added

lib/src/core/clix.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../services/device_service.dart';
66
import '../services/event_api_service.dart';
77
import '../services/event_service.dart';
88
import '../services/notification_service.dart';
9+
import '../services/session_service.dart';
910
import '../services/storage_service.dart';
1011
import '../services/token_service.dart';
1112
import '../utils/clix_error.dart';
@@ -31,6 +32,7 @@ class Clix {
3132
StorageService? _storageService;
3233
EventService? _eventService;
3334
DeviceService? _deviceService;
35+
SessionService? _sessionService;
3436
NotificationService? _notificationService;
3537

3638
Clix._();
@@ -101,14 +103,25 @@ class Clix {
101103
deviceService: _deviceService!,
102104
);
103105

106+
// Initialize session service
107+
_sessionService = SessionService(
108+
storageService: _storageService!,
109+
eventService: _eventService!,
110+
sessionTimeoutMs: config.sessionTimeoutMs,
111+
);
112+
104113
// Initialize notification service
105114
_notificationService = NotificationService();
106115
await _notificationService!.initialize(
107116
eventService: _eventService!,
108117
storageService: _storageService!,
109118
deviceService: _deviceService!,
110119
tokenService: tokenService,
120+
sessionService: _sessionService,
111121
);
122+
123+
// Start session (after notification service so initial notification can set pendingMessageId)
124+
await _sessionService!.start();
112125
}
113126

114127
/// Wait for initialization with timeout protection

lib/src/core/clix_config.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ class ClixConfig {
1010
final String endpoint;
1111
final ClixLogLevel logLevel;
1212
final Map<String, String>? extraHeaders;
13+
final int sessionTimeoutMs;
1314

1415
const ClixConfig({
1516
required this.projectId,
1617
required this.apiKey,
1718
this.endpoint = 'https://api.clix.so',
1819
this.logLevel = ClixLogLevel.error,
1920
this.extraHeaders,
21+
this.sessionTimeoutMs = 30000,
2022
});
2123

2224
Map<String, dynamic> toJson() => _$ClixConfigToJson(this);

lib/src/core/clix_config.g.dart

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/services/notification_service.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '../services/event_api_service.dart';
1717
import '../utils/logging/clix_logger.dart';
1818
import 'device_service.dart';
1919
import 'event_service.dart';
20+
import 'session_service.dart';
2021
import 'storage_service.dart';
2122
import 'token_service.dart';
2223

@@ -41,6 +42,7 @@ class NotificationService {
4142
StorageService? _storageService;
4243
DeviceService? _deviceService;
4344
TokenService? _tokenService;
45+
SessionService? _sessionService;
4446

4547
bool _isInitialized = false;
4648
String? _currentToken;
@@ -53,6 +55,7 @@ class NotificationService {
5355
required StorageService storageService,
5456
DeviceService? deviceService,
5557
TokenService? tokenService,
58+
SessionService? sessionService,
5659
Function(Map<String, dynamic>)? onPushReceived,
5760
Function(Map<String, dynamic>)? onPushTapped,
5861
}) async {
@@ -62,14 +65,15 @@ class NotificationService {
6265
_storageService = storageService;
6366
_deviceService = deviceService;
6467
_tokenService = tokenService;
68+
_sessionService = sessionService;
6569
this.onPushReceived = onPushReceived;
6670
this.onPushTapped = onPushTapped;
6771

6872
try {
6973
ClixLogger.info('Initializing notification service');
7074

7175
await _initializeLocalNotifications();
72-
_setupMessageHandlers();
76+
await _setupMessageHandlers();
7377
await _getAndUpdateTokenIfPermitted();
7478
_firebaseMessaging.onTokenRefresh.listen(_onTokenRefresh);
7579

@@ -169,11 +173,11 @@ class NotificationService {
169173
return settings;
170174
}
171175

172-
void _setupMessageHandlers() {
176+
Future<void> _setupMessageHandlers() async {
173177
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
174178
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
175179
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
176-
_handleInitialMessage();
180+
await _handleInitialMessage();
177181
}
178182

179183
Future<void> _onForegroundMessage(RemoteMessage message) async {
@@ -343,6 +347,8 @@ class NotificationService {
343347
try {
344348
final clixPayload = parseClixPayload(userInfo);
345349
if (clixPayload != null) {
350+
final messageId = clixPayload['message_id'] as String?;
351+
_sessionService?.setPendingMessageId(messageId);
346352
await _trackPushEvent('PUSH_NOTIFICATION_TAPPED', clixPayload);
347353
}
348354
onPushTapped?.call(userInfo);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
import '../utils/logging/clix_logger.dart';
4+
import 'event_service.dart';
5+
import 'storage_service.dart';
6+
7+
enum SessionEvent {
8+
sessionStart('SESSION_START');
9+
10+
final String value;
11+
const SessionEvent(this.value);
12+
}
13+
14+
class SessionService with WidgetsBindingObserver {
15+
static const String _lastActivityKey = 'clix_session_last_activity';
16+
17+
final StorageService _storageService;
18+
final EventService _eventService;
19+
final int _effectiveTimeoutMs;
20+
21+
String? _pendingMessageId;
22+
23+
SessionService({
24+
required StorageService storageService,
25+
required EventService eventService,
26+
required int sessionTimeoutMs,
27+
}) : _storageService = storageService,
28+
_eventService = eventService,
29+
_effectiveTimeoutMs = sessionTimeoutMs < 5000 ? 5000 : sessionTimeoutMs;
30+
31+
Future<void> start() async {
32+
WidgetsBinding.instance.addObserver(this);
33+
34+
try {
35+
final lastActivity = await _storageService.get<int>(_lastActivityKey);
36+
if (lastActivity != null) {
37+
final elapsed = DateTime.now().millisecondsSinceEpoch - lastActivity;
38+
if (elapsed <= _effectiveTimeoutMs) {
39+
_pendingMessageId = null;
40+
await _updateLastActivity();
41+
ClixLogger.debug('Continuing existing session');
42+
return;
43+
}
44+
}
45+
await _startNewSession();
46+
} catch (e) {
47+
ClixLogger.error('Failed to start session', e);
48+
}
49+
}
50+
51+
@override
52+
void didChangeAppLifecycleState(AppLifecycleState state) {
53+
if (state == AppLifecycleState.resumed) {
54+
_onResumed();
55+
} else if (state == AppLifecycleState.paused) {
56+
_onPaused();
57+
}
58+
}
59+
60+
void setPendingMessageId(String? messageId) {
61+
_pendingMessageId = messageId;
62+
}
63+
64+
Future<void> _onResumed() async {
65+
try {
66+
// Small delay to allow notification tap handlers to set pendingMessageId
67+
await Future.delayed(const Duration(milliseconds: 100));
68+
69+
final lastActivity = await _storageService.get<int>(_lastActivityKey);
70+
if (lastActivity != null) {
71+
final elapsed = DateTime.now().millisecondsSinceEpoch - lastActivity;
72+
if (elapsed <= _effectiveTimeoutMs) {
73+
_pendingMessageId = null;
74+
await _updateLastActivity();
75+
return;
76+
}
77+
}
78+
await _startNewSession();
79+
} catch (e) {
80+
ClixLogger.error('Failed to handle app resumed', e);
81+
}
82+
}
83+
84+
Future<void> _onPaused() async {
85+
try {
86+
await _updateLastActivity();
87+
} catch (e) {
88+
ClixLogger.error('Failed to handle app paused', e);
89+
}
90+
}
91+
92+
Future<void> _startNewSession() async {
93+
final messageId = _pendingMessageId;
94+
_pendingMessageId = null;
95+
await _updateLastActivity();
96+
97+
try {
98+
await _eventService.trackEvent(
99+
SessionEvent.sessionStart.value,
100+
messageId: messageId,
101+
);
102+
ClixLogger.debug('${SessionEvent.sessionStart.value} tracked');
103+
} catch (e) {
104+
ClixLogger.error('Failed to track ${SessionEvent.sessionStart.value}', e);
105+
}
106+
}
107+
108+
void cleanup() {
109+
WidgetsBinding.instance.removeObserver(this);
110+
}
111+
112+
Future<void> _updateLastActivity() async {
113+
await _storageService.set<int>(
114+
_lastActivityKey, DateTime.now().millisecondsSinceEpoch);
115+
}
116+
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: clix_flutter
22
description: Clix - Clix - Mobile push for builders
33

4-
version: 0.0.3
4+
version: 0.0.4
55
homepage: https://clix.so
66
repository: https://github.com/clix-so/clix-flutter-sdk
77

0 commit comments

Comments
 (0)