Skip to content

Commit ec21441

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

6 files changed

Lines changed: 125 additions & 3 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+
## [1.4.0] - 2026-02-12
8+
9+
### Added
10+
11+
- Session tracking with automatic SESSION_START lifecycle events
12+
713
## [1.3.0] - 2026-01-15
814

915
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@clix-so/react-native-sdk",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "Clix - Mobile push for builders",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

src/core/Clix.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EventService } from '../services/EventService';
66
import { LiveActivityAPIService } from '../services/LiveActivityAPIService';
77
import { LiveActivityService } from '../services/LiveActivityService';
88
import { NotificationService } from '../services/NotificationService';
9+
import { SessionService } from '../services/SessionService';
910
import { StorageService } from '../services/StorageService';
1011
import { TokenService } from '../services/TokenService';
1112
import { ClixLogger, ClixLogLevel } from '../utils/logging/ClixLogger';
@@ -29,6 +30,7 @@ export class Clix {
2930
tokenService?: TokenService;
3031
eventService?: EventService;
3132
deviceService?: DeviceService;
33+
sessionService?: SessionService;
3234
notificationService?: NotificationService;
3335
liveActivityService?: LiveActivityService;
3436

@@ -68,10 +70,16 @@ export class Clix {
6870
eventApiService,
6971
this.shared.deviceService
7072
);
73+
this.shared.sessionService = new SessionService(
74+
this.shared.storageService,
75+
this.shared.eventService,
76+
config.sessionTimeoutMs ?? 30000
77+
);
7178
this.shared.notificationService = new NotificationService(
7279
this.shared.deviceService,
7380
this.shared.tokenService,
74-
this.shared.eventService
81+
this.shared.eventService,
82+
this.shared.sessionService
7583
);
7684
this.shared.liveActivityService = new LiveActivityService(
7785
this.shared.deviceService,
@@ -81,6 +89,7 @@ export class Clix {
8189
this.shared.storageService.set(this.configKey, config);
8290
this.shared.liveActivityService.initialize();
8391
await this.shared.notificationService.initialize(); // NOTE(nyanxyz): must be initialized before any await calls
92+
await this.shared.sessionService.start();
8493
await this.shared.deviceService.initialize();
8594

8695
ClixLogger.debug('Clix SDK initialized successfully');

src/core/ClixConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export interface ClixConfig {
66
endpoint: string;
77
logLevel: ClixLogLevel;
88
extraHeaders: Record<string, string>;
9+
sessionTimeoutMs?: number;
910
}

src/services/NotificationService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { ClixPushNotificationPayload } from '../models/ClixPushNotification
1919
import { ClixLogger } from '../utils/logging/ClixLogger';
2020
import { DeviceService } from './DeviceService';
2121
import { EventService } from './EventService';
22+
import { SessionService } from './SessionService';
2223
import { TokenService } from './TokenService';
2324

2425
type NotificationData = Record<string, any>;
@@ -66,7 +67,8 @@ export class NotificationService {
6667
constructor(
6768
private readonly deviceService: DeviceService,
6869
private readonly tokenService: TokenService,
69-
private readonly eventService: EventService
70+
private readonly eventService: EventService,
71+
private readonly sessionService?: SessionService
7072
) {}
7173

7274
async initialize(): Promise<void> {
@@ -438,6 +440,7 @@ export class NotificationService {
438440
payload: ClixPushNotificationPayload
439441
): Promise<void> {
440442
try {
443+
this.sessionService?.setPendingMessageId(payload.messageId);
441444
await this.eventService.trackEvent(
442445
'PUSH_NOTIFICATION_TAPPED',
443446
{},

src/services/SessionService.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { AppState, type AppStateStatus } from 'react-native';
2+
import { ClixLogger } from '../utils/logging/ClixLogger';
3+
import type { EventService } from './EventService';
4+
import type { StorageService } from './StorageService';
5+
6+
enum SessionEvent {
7+
SESSION_START = 'SESSION_START',
8+
}
9+
10+
export class SessionService {
11+
private static readonly LAST_ACTIVITY_KEY = 'clix_session_last_activity';
12+
13+
private pendingMessageId?: string;
14+
private readonly effectiveTimeoutMs: number;
15+
private appStateSubscription?: { remove: () => void };
16+
private lastAppState: AppStateStatus;
17+
18+
constructor(
19+
private readonly storageService: StorageService,
20+
private readonly eventService: EventService,
21+
sessionTimeoutMs: number
22+
) {
23+
this.effectiveTimeoutMs = Math.max(sessionTimeoutMs, 5000);
24+
this.lastAppState = AppState.currentState;
25+
}
26+
27+
async start(): Promise<void> {
28+
this.appStateSubscription = AppState.addEventListener(
29+
'change',
30+
this.handleAppStateChange.bind(this)
31+
);
32+
33+
const lastActivity = this.storageService.get<number>(
34+
SessionService.LAST_ACTIVITY_KEY
35+
);
36+
if (lastActivity) {
37+
const elapsed = Date.now() - lastActivity;
38+
if (elapsed <= this.effectiveTimeoutMs) {
39+
this.pendingMessageId = undefined;
40+
this.updateLastActivity();
41+
ClixLogger.debug('Continuing existing session');
42+
return;
43+
}
44+
}
45+
await this.startNewSession();
46+
}
47+
48+
private async handleAppStateChange(
49+
nextAppState: AppStateStatus
50+
): Promise<void> {
51+
const previousAppState = this.lastAppState;
52+
this.lastAppState = nextAppState;
53+
54+
if (previousAppState === 'background' && nextAppState === 'active') {
55+
// Small delay to allow notification tap handlers to set pendingMessageId
56+
await new Promise((resolve) => setTimeout(resolve, 100));
57+
58+
const lastActivity = this.storageService.get<number>(
59+
SessionService.LAST_ACTIVITY_KEY
60+
);
61+
if (lastActivity) {
62+
const elapsed = Date.now() - lastActivity;
63+
if (elapsed <= this.effectiveTimeoutMs) {
64+
this.pendingMessageId = undefined;
65+
this.updateLastActivity();
66+
return;
67+
}
68+
}
69+
await this.startNewSession();
70+
} else if (nextAppState === 'background') {
71+
this.updateLastActivity();
72+
}
73+
}
74+
75+
setPendingMessageId(messageId?: string): void {
76+
this.pendingMessageId = messageId;
77+
}
78+
79+
private async startNewSession(): Promise<void> {
80+
const messageId = this.pendingMessageId;
81+
this.pendingMessageId = undefined;
82+
this.updateLastActivity();
83+
84+
try {
85+
await this.eventService.trackEvent(
86+
SessionEvent.SESSION_START,
87+
{},
88+
messageId
89+
);
90+
ClixLogger.debug(`${SessionEvent.SESSION_START} tracked`);
91+
} catch (error) {
92+
ClixLogger.error(`Failed to track ${SessionEvent.SESSION_START}`, error);
93+
}
94+
}
95+
96+
private updateLastActivity(): void {
97+
this.storageService.set(SessionService.LAST_ACTIVITY_KEY, Date.now());
98+
}
99+
100+
cleanup(): void {
101+
this.appStateSubscription?.remove();
102+
}
103+
}

0 commit comments

Comments
 (0)