respectlytics-react-native 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +160 -0
- package/package.json +94 -0
- package/src/EventQueue.ts +182 -0
- package/src/NetworkClient.ts +165 -0
- package/src/Respectlytics.ts +199 -0
- package/src/SessionManager.ts +64 -0
- package/src/Storage.ts +49 -0
- package/src/UserManager.ts +74 -0
- package/src/index.ts +15 -0
- package/src/types.ts +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Respectlytics SDK License
|
|
2
|
+
Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
3
|
+
|
|
4
|
+
PERMITTED USES:
|
|
5
|
+
- View and read the source code for transparency and security review
|
|
6
|
+
- Install the SDK via official package managers (SPM, npm, pub.dev)
|
|
7
|
+
- Use the SDK to send analytics data to the official Respectlytics service
|
|
8
|
+
|
|
9
|
+
PROHIBITED USES:
|
|
10
|
+
- Copying, forking, or redistributing the source code
|
|
11
|
+
- Modifying the SDK source code
|
|
12
|
+
- Using the SDK with any backend other than the official Respectlytics API
|
|
13
|
+
- Creating derivative works based on this SDK
|
|
14
|
+
- Reverse engineering beyond what is necessary for interoperability
|
|
15
|
+
|
|
16
|
+
NO WARRANTY:
|
|
17
|
+
This SDK is provided "as is" without warranty of any kind. Respectlytics
|
|
18
|
+
shall not be liable for any damages arising from the use of this SDK.
|
|
19
|
+
|
|
20
|
+
TERMINATION:
|
|
21
|
+
This license terminates automatically if you violate any of its terms.
|
|
22
|
+
Upon termination, you must cease all use and destroy all copies.
|
|
23
|
+
|
|
24
|
+
For licensing inquiries: respectlytics@loheden.com
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Respectlytics React Native SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/respectlytics-react-native)
|
|
4
|
+
[](https://github.com/respectlytics/respectlytics-react-native)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Official Respectlytics SDK for React Native. Privacy-first analytics with automatic session management, offline event queuing, and zero device identifier collection.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔒 **Privacy-First**: No device identifiers (IDFA, GAID, Android ID)
|
|
12
|
+
- ⚡ **Simple Integration**: 3 lines of code to get started
|
|
13
|
+
- 📡 **Offline Support**: Events queue automatically and sync when online
|
|
14
|
+
- 🔄 **Automatic Sessions**: 30-minute inactivity timeout, handled internally
|
|
15
|
+
- 🎯 **Cross-Session Tracking**: Optional persistent user IDs
|
|
16
|
+
- 📱 **Cross-Platform**: iOS and Android support
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- React Native 0.70+
|
|
21
|
+
- iOS 15.0+ / Android API 21+
|
|
22
|
+
- Node.js 16+
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install respectlytics-react-native @react-native-async-storage/async-storage @react-native-community/netinfo
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
or with Yarn:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
yarn add respectlytics-react-native @react-native-async-storage/async-storage @react-native-community/netinfo
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### iOS Setup
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd ios && pod install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Android Setup
|
|
43
|
+
|
|
44
|
+
No additional setup required - autolinking handles everything.
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import Respectlytics from 'respectlytics-react-native';
|
|
50
|
+
|
|
51
|
+
// 1. Configure at app startup
|
|
52
|
+
Respectlytics.configure('your-api-key');
|
|
53
|
+
|
|
54
|
+
// 2. Enable user tracking (optional)
|
|
55
|
+
Respectlytics.identify();
|
|
56
|
+
|
|
57
|
+
// 3. Track events
|
|
58
|
+
Respectlytics.track('purchase');
|
|
59
|
+
Respectlytics.track('view_product', 'ProductScreen');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### `configure(apiKey: string)`
|
|
65
|
+
|
|
66
|
+
Initialize the SDK with your API key. Call once at app startup.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
Respectlytics.configure('your-api-key');
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `track(eventName: string, screen?: string)`
|
|
73
|
+
|
|
74
|
+
Track an event with an optional screen name.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
Respectlytics.track('button_clicked');
|
|
78
|
+
Respectlytics.track('checkout_started', 'CartScreen');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `identify()`
|
|
82
|
+
|
|
83
|
+
Enable cross-session user tracking. Generates and persists a random user ID.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
Respectlytics.identify();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `reset()`
|
|
90
|
+
|
|
91
|
+
Clear the user ID. Call when user logs out.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
Respectlytics.reset();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `flush()`
|
|
98
|
+
|
|
99
|
+
Force send all queued events immediately. Rarely needed - the SDK auto-flushes.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
await Respectlytics.flush();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Privacy by Design
|
|
106
|
+
|
|
107
|
+
| What we DON'T collect | Why |
|
|
108
|
+
|----------------------|-----|
|
|
109
|
+
| IDFA / GAID | Device advertising IDs can track users across apps |
|
|
110
|
+
| Device fingerprints | Can be used to identify users without consent |
|
|
111
|
+
| IP addresses | Used only for geolocation lookup, then discarded |
|
|
112
|
+
| Custom properties | Prevents accidental PII collection |
|
|
113
|
+
|
|
114
|
+
| What we DO collect | Purpose |
|
|
115
|
+
|-------------------|---------|
|
|
116
|
+
| Event name | Analytics |
|
|
117
|
+
| Screen name | Navigation analytics |
|
|
118
|
+
| Random session ID | Group events in a session |
|
|
119
|
+
| Random user ID (opt-in) | Cross-session analytics |
|
|
120
|
+
| Platform, OS version | Debugging |
|
|
121
|
+
| App version | Debugging |
|
|
122
|
+
|
|
123
|
+
## Automatic Behaviors
|
|
124
|
+
|
|
125
|
+
The SDK handles these automatically - no developer action needed:
|
|
126
|
+
|
|
127
|
+
| Feature | Behavior |
|
|
128
|
+
|---------|----------|
|
|
129
|
+
| **Session Management** | New session ID generated on first event, rotates after 30 min inactivity |
|
|
130
|
+
| **Event Batching** | Events queued and sent in batches (max 10 events or 30 seconds) |
|
|
131
|
+
| **Offline Support** | Events queued when offline, sent when connectivity returns |
|
|
132
|
+
| **Retry Logic** | Failed requests retry with exponential backoff (max 3 attempts) |
|
|
133
|
+
| **Background Sync** | Events flushed when app enters background |
|
|
134
|
+
|
|
135
|
+
## Offline Support
|
|
136
|
+
|
|
137
|
+
Events are automatically queued when offline and sent when connectivity returns:
|
|
138
|
+
|
|
139
|
+
1. Events are immediately persisted to AsyncStorage
|
|
140
|
+
2. Network status is monitored via NetInfo
|
|
141
|
+
3. Queue is flushed when connectivity is restored
|
|
142
|
+
4. Failed sends are retried with exponential backoff
|
|
143
|
+
|
|
144
|
+
## Session Management
|
|
145
|
+
|
|
146
|
+
Sessions are managed automatically:
|
|
147
|
+
|
|
148
|
+
- New session starts on first event
|
|
149
|
+
- Session rotates after 30 minutes of inactivity
|
|
150
|
+
- No developer action required
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
This SDK is provided under a proprietary license. See [LICENSE](LICENSE) for details.
|
|
155
|
+
|
|
156
|
+
## Support
|
|
157
|
+
|
|
158
|
+
- Documentation: [https://respectlytics.com/docs/](https://respectlytics.com/docs/)
|
|
159
|
+
- Issues: [https://github.com/respectlytics/respectlytics-react-native/issues](https://github.com/respectlytics/respectlytics-react-native/issues)
|
|
160
|
+
- Email: respectlytics@loheden.com
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "respectlytics-react-native",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Official Respectlytics SDK for React Native. Privacy-first analytics with automatic session management, offline event queuing, and zero device identifier collection.",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
|
+
"types": "lib/typescript/src/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"lib",
|
|
13
|
+
"!**/__tests__",
|
|
14
|
+
"!**/__fixtures__",
|
|
15
|
+
"!**/__mocks__",
|
|
16
|
+
"!**/.*"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typescript": "tsc --noEmit",
|
|
20
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
21
|
+
"build": "bob build",
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:integration": "jest --config jest.integration.config.js",
|
|
24
|
+
"clean": "rm -rf lib"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"react-native",
|
|
28
|
+
"analytics",
|
|
29
|
+
"privacy",
|
|
30
|
+
"tracking",
|
|
31
|
+
"events",
|
|
32
|
+
"respectlytics",
|
|
33
|
+
"ios",
|
|
34
|
+
"android"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/respectlytics/respectlytics-react-native.git"
|
|
39
|
+
},
|
|
40
|
+
"author": "Respectlytics <respectlytics@loheden.com>",
|
|
41
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/respectlytics/respectlytics-react-native/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/respectlytics/respectlytics-react-native#readme",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
51
|
+
"@react-native-community/netinfo": "^11.3.0",
|
|
52
|
+
"@types/jest": "^29.5.12",
|
|
53
|
+
"@types/react": "^18.2.0",
|
|
54
|
+
"@types/react-native": "^0.72.0",
|
|
55
|
+
"eslint": "^8.56.0",
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"react": "^18.2.0",
|
|
58
|
+
"react-native": "^0.73.0",
|
|
59
|
+
"react-native-builder-bob": "^0.23.2",
|
|
60
|
+
"tsx": "^4.20.6",
|
|
61
|
+
"typescript": "^5.3.3"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@react-native-async-storage/async-storage": ">=1.17.0",
|
|
65
|
+
"@react-native-community/netinfo": ">=9.0.0",
|
|
66
|
+
"react": ">=18.0.0",
|
|
67
|
+
"react-native": ">=0.70.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@react-native-async-storage/async-storage": {
|
|
71
|
+
"optional": false
|
|
72
|
+
},
|
|
73
|
+
"@react-native-community/netinfo": {
|
|
74
|
+
"optional": false
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=16.0.0"
|
|
79
|
+
},
|
|
80
|
+
"react-native-builder-bob": {
|
|
81
|
+
"source": "src",
|
|
82
|
+
"output": "lib",
|
|
83
|
+
"targets": [
|
|
84
|
+
"commonjs",
|
|
85
|
+
"module",
|
|
86
|
+
[
|
|
87
|
+
"typescript",
|
|
88
|
+
{
|
|
89
|
+
"project": "tsconfig.build.json"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventQueue.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Manages event batching, persistence, and automatic flushing.
|
|
6
|
+
* Events are NEVER lost - they are persisted immediately and retried on failure.
|
|
7
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AppState, AppStateStatus } from 'react-native';
|
|
11
|
+
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
|
|
12
|
+
import { Event } from './types';
|
|
13
|
+
import { Storage } from './Storage';
|
|
14
|
+
import { NetworkClient } from './NetworkClient';
|
|
15
|
+
|
|
16
|
+
const MAX_QUEUE_SIZE = 10;
|
|
17
|
+
const FLUSH_INTERVAL_MS = 30000; // 30 seconds
|
|
18
|
+
const QUEUE_STORAGE_KEY = 'com.respectlytics.eventQueue';
|
|
19
|
+
|
|
20
|
+
export class EventQueue {
|
|
21
|
+
private events: Event[] = [];
|
|
22
|
+
private isOnline = true;
|
|
23
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
24
|
+
private networkClient: NetworkClient;
|
|
25
|
+
private isFlushing = false;
|
|
26
|
+
private unsubscribeNetInfo: (() => void) | null = null;
|
|
27
|
+
private appStateSubscription: { remove: () => void } | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(networkClient: NetworkClient) {
|
|
30
|
+
this.networkClient = networkClient;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize the queue - load persisted events and set up listeners
|
|
35
|
+
*/
|
|
36
|
+
async start(): Promise<void> {
|
|
37
|
+
await this.loadPersistedQueue();
|
|
38
|
+
this.setupNetworkMonitor();
|
|
39
|
+
this.setupAppStateMonitor();
|
|
40
|
+
this.scheduleFlush();
|
|
41
|
+
console.log('[Respectlytics] ✓ Event queue started');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stop the queue and clean up resources
|
|
46
|
+
*/
|
|
47
|
+
stop(): void {
|
|
48
|
+
if (this.flushTimer) {
|
|
49
|
+
clearInterval(this.flushTimer);
|
|
50
|
+
this.flushTimer = null;
|
|
51
|
+
}
|
|
52
|
+
if (this.unsubscribeNetInfo) {
|
|
53
|
+
this.unsubscribeNetInfo();
|
|
54
|
+
this.unsubscribeNetInfo = null;
|
|
55
|
+
}
|
|
56
|
+
if (this.appStateSubscription) {
|
|
57
|
+
this.appStateSubscription.remove();
|
|
58
|
+
this.appStateSubscription = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add an event to the queue
|
|
64
|
+
* CRITICAL: Events are persisted IMMEDIATELY before any async operations
|
|
65
|
+
*/
|
|
66
|
+
async add(event: Event): Promise<void> {
|
|
67
|
+
this.events.push(event);
|
|
68
|
+
|
|
69
|
+
// IMMEDIATELY persist before any async operations
|
|
70
|
+
await this.persistQueue();
|
|
71
|
+
|
|
72
|
+
// Check if we should flush
|
|
73
|
+
if (this.events.length >= MAX_QUEUE_SIZE) {
|
|
74
|
+
this.flush();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Force flush all queued events
|
|
80
|
+
*/
|
|
81
|
+
async flush(): Promise<void> {
|
|
82
|
+
if (this.isFlushing || this.events.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!this.isOnline) {
|
|
87
|
+
console.log('[Respectlytics] Offline, skipping flush');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!this.networkClient.isConfigured()) {
|
|
92
|
+
console.log('[Respectlytics] ⚠️ SDK not configured, skipping flush');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.isFlushing = true;
|
|
97
|
+
|
|
98
|
+
// Take a snapshot of events to send
|
|
99
|
+
const batch = [...this.events];
|
|
100
|
+
this.events = [];
|
|
101
|
+
await this.persistQueue();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await this.networkClient.send(batch);
|
|
105
|
+
console.log(`[Respectlytics] ✓ Sent ${batch.length} event(s)`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Re-add failed events to the front of the queue
|
|
108
|
+
this.events = [...batch, ...this.events];
|
|
109
|
+
await this.persistQueue();
|
|
110
|
+
console.log('[Respectlytics] Failed to send events, will retry later');
|
|
111
|
+
} finally {
|
|
112
|
+
this.isFlushing = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get current queue size (for testing)
|
|
118
|
+
*/
|
|
119
|
+
getQueueSize(): number {
|
|
120
|
+
return this.events.length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MARK: - Private Helpers
|
|
124
|
+
|
|
125
|
+
private scheduleFlush(): void {
|
|
126
|
+
if (this.flushTimer) {
|
|
127
|
+
clearInterval(this.flushTimer);
|
|
128
|
+
}
|
|
129
|
+
this.flushTimer = setInterval(() => {
|
|
130
|
+
this.flush();
|
|
131
|
+
}, FLUSH_INTERVAL_MS);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private setupNetworkMonitor(): void {
|
|
135
|
+
this.unsubscribeNetInfo = NetInfo.addEventListener((state: NetInfoState) => {
|
|
136
|
+
const wasOffline = !this.isOnline;
|
|
137
|
+
this.isOnline = state.isConnected ?? false;
|
|
138
|
+
|
|
139
|
+
// If we just came online, try to flush
|
|
140
|
+
if (wasOffline && this.isOnline) {
|
|
141
|
+
console.log('[Respectlytics] Network restored, flushing queue');
|
|
142
|
+
this.flush();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private setupAppStateMonitor(): void {
|
|
148
|
+
this.appStateSubscription = AppState.addEventListener(
|
|
149
|
+
'change',
|
|
150
|
+
(nextAppState: AppStateStatus) => {
|
|
151
|
+
// Flush when app goes to background
|
|
152
|
+
if (nextAppState === 'background' || nextAppState === 'inactive') {
|
|
153
|
+
this.flush();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async persistQueue(): Promise<void> {
|
|
160
|
+
try {
|
|
161
|
+
await Storage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(this.events));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.log('[Respectlytics] Failed to persist queue:', error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async loadPersistedQueue(): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
const data = await Storage.getItem(QUEUE_STORAGE_KEY);
|
|
170
|
+
if (data) {
|
|
171
|
+
const parsed = JSON.parse(data);
|
|
172
|
+
if (Array.isArray(parsed)) {
|
|
173
|
+
this.events = parsed;
|
|
174
|
+
console.log(`[Respectlytics] Loaded ${this.events.length} persisted event(s)`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.log('[Respectlytics] Failed to load persisted queue:', error);
|
|
179
|
+
this.events = [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NetworkClient.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Handles HTTP communication with the Respectlytics API.
|
|
6
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Event } from './types';
|
|
10
|
+
|
|
11
|
+
const API_ENDPOINT = 'https://respectlytics.com/api/v1/events/';
|
|
12
|
+
const MAX_RETRIES = 3;
|
|
13
|
+
const TIMEOUT_MS = 30000;
|
|
14
|
+
|
|
15
|
+
export enum NetworkError {
|
|
16
|
+
NotConfigured = 'NOT_CONFIGURED',
|
|
17
|
+
InvalidResponse = 'INVALID_RESPONSE',
|
|
18
|
+
Unauthorized = 'UNAUTHORIZED',
|
|
19
|
+
BadRequest = 'BAD_REQUEST',
|
|
20
|
+
RateLimited = 'RATE_LIMITED',
|
|
21
|
+
ServerError = 'SERVER_ERROR',
|
|
22
|
+
NetworkError = 'NETWORK_ERROR',
|
|
23
|
+
Timeout = 'TIMEOUT',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class NetworkClient {
|
|
27
|
+
private apiKey: string | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configure the network client with an API key
|
|
31
|
+
*/
|
|
32
|
+
configure(apiKey: string): void {
|
|
33
|
+
this.apiKey = apiKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if the client is configured
|
|
38
|
+
*/
|
|
39
|
+
isConfigured(): boolean {
|
|
40
|
+
return this.apiKey !== null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send events to the API
|
|
45
|
+
*/
|
|
46
|
+
async send(events: Event[]): Promise<void> {
|
|
47
|
+
if (!this.apiKey) {
|
|
48
|
+
throw new Error(NetworkError.NotConfigured);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const event of events) {
|
|
52
|
+
await this.sendEvent(event, 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Send a single event with retry logic
|
|
58
|
+
*/
|
|
59
|
+
private async sendEvent(event: Event, attempt: number): Promise<void> {
|
|
60
|
+
if (!this.apiKey) {
|
|
61
|
+
throw new Error(NetworkError.NotConfigured);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(API_ENDPOINT, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'X-App-Key': this.apiKey,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(this.eventToPayload(event)),
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
|
|
80
|
+
if (response.ok) {
|
|
81
|
+
return; // Success
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (response.status) {
|
|
85
|
+
case 401:
|
|
86
|
+
throw new Error(NetworkError.Unauthorized);
|
|
87
|
+
case 400:
|
|
88
|
+
throw new Error(NetworkError.BadRequest);
|
|
89
|
+
case 429:
|
|
90
|
+
// Rate limited - retry with backoff
|
|
91
|
+
if (attempt < MAX_RETRIES) {
|
|
92
|
+
await this.delay(Math.pow(2, attempt) * 1000);
|
|
93
|
+
return this.sendEvent(event, attempt + 1);
|
|
94
|
+
}
|
|
95
|
+
throw new Error(NetworkError.RateLimited);
|
|
96
|
+
default:
|
|
97
|
+
if (response.status >= 500) {
|
|
98
|
+
// Server error - retry with backoff
|
|
99
|
+
if (attempt < MAX_RETRIES) {
|
|
100
|
+
await this.delay(Math.pow(2, attempt) * 1000);
|
|
101
|
+
return this.sendEvent(event, attempt + 1);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(NetworkError.ServerError);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(NetworkError.InvalidResponse);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
|
|
110
|
+
if (error instanceof Error) {
|
|
111
|
+
// Don't retry auth or bad request errors
|
|
112
|
+
if (
|
|
113
|
+
error.message === NetworkError.Unauthorized ||
|
|
114
|
+
error.message === NetworkError.BadRequest
|
|
115
|
+
) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for abort (timeout)
|
|
120
|
+
if (error.name === 'AbortError') {
|
|
121
|
+
if (attempt < MAX_RETRIES) {
|
|
122
|
+
await this.delay(Math.pow(2, attempt) * 1000);
|
|
123
|
+
return this.sendEvent(event, attempt + 1);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(NetworkError.Timeout);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Network error - retry with backoff
|
|
130
|
+
if (attempt < MAX_RETRIES) {
|
|
131
|
+
await this.delay(Math.pow(2, attempt) * 1000);
|
|
132
|
+
return this.sendEvent(event, attempt + 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new Error(NetworkError.NetworkError);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert Event object to API payload format
|
|
141
|
+
*/
|
|
142
|
+
private eventToPayload(event: Event): Record<string, unknown> {
|
|
143
|
+
return {
|
|
144
|
+
event_name: event.eventName,
|
|
145
|
+
timestamp: event.timestamp,
|
|
146
|
+
session_id: event.sessionId,
|
|
147
|
+
user_id: event.userId || undefined,
|
|
148
|
+
screen: event.screen || undefined,
|
|
149
|
+
platform: event.platform,
|
|
150
|
+
os_version: event.osVersion,
|
|
151
|
+
app_version: event.appVersion,
|
|
152
|
+
locale: event.locale,
|
|
153
|
+
device_type: event.deviceType,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Helper to delay for exponential backoff
|
|
159
|
+
*/
|
|
160
|
+
private delay(ms: number): Promise<void> {
|
|
161
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const networkClient = new NetworkClient();
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Respectlytics.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Main entry point for the SDK.
|
|
6
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Platform, Dimensions, NativeModules } from 'react-native';
|
|
10
|
+
import { Event } from './types';
|
|
11
|
+
import { SessionManager } from './SessionManager';
|
|
12
|
+
import { UserManager } from './UserManager';
|
|
13
|
+
import { NetworkClient } from './NetworkClient';
|
|
14
|
+
import { EventQueue } from './EventQueue';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Main entry point for the Respectlytics SDK.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // 1. Configure at app launch
|
|
22
|
+
* Respectlytics.configure('your-api-key');
|
|
23
|
+
*
|
|
24
|
+
* // 2. Enable user tracking (optional)
|
|
25
|
+
* Respectlytics.identify();
|
|
26
|
+
*
|
|
27
|
+
* // 3. Track events
|
|
28
|
+
* Respectlytics.track('purchase');
|
|
29
|
+
* Respectlytics.track('view_product', 'ProductScreen');
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
class RespectlyticsSDK {
|
|
33
|
+
private isConfigured = false;
|
|
34
|
+
private networkClient: NetworkClient;
|
|
35
|
+
private eventQueue: EventQueue;
|
|
36
|
+
private sessionManager: SessionManager;
|
|
37
|
+
private userManager: UserManager;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.networkClient = new NetworkClient();
|
|
41
|
+
this.eventQueue = new EventQueue(this.networkClient);
|
|
42
|
+
this.sessionManager = new SessionManager();
|
|
43
|
+
this.userManager = new UserManager();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the SDK with your API key.
|
|
48
|
+
* Call once at app startup.
|
|
49
|
+
*
|
|
50
|
+
* @param apiKey Your Respectlytics API key from the dashboard
|
|
51
|
+
*/
|
|
52
|
+
configure(apiKey: string): void {
|
|
53
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
54
|
+
console.log('[Respectlytics] ⚠️ API key cannot be empty');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.networkClient.configure(apiKey);
|
|
59
|
+
this.eventQueue.start();
|
|
60
|
+
this.userManager.loadUserId();
|
|
61
|
+
this.isConfigured = true;
|
|
62
|
+
|
|
63
|
+
console.log('[Respectlytics] ✓ SDK configured');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Track an event with an optional screen name.
|
|
68
|
+
*
|
|
69
|
+
* The SDK automatically collects privacy-safe metadata:
|
|
70
|
+
* - timestamp, session_id, platform, os_version, app_version, locale
|
|
71
|
+
*
|
|
72
|
+
* @param eventName Name of the event (e.g., "purchase", "button_clicked")
|
|
73
|
+
* @param screen Optional screen name where the event occurred
|
|
74
|
+
*/
|
|
75
|
+
track(eventName: string, screen?: string): void {
|
|
76
|
+
if (!this.isConfigured) {
|
|
77
|
+
console.log('[Respectlytics] ⚠️ SDK not configured. Call configure(apiKey) first.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!eventName || eventName.trim() === '') {
|
|
82
|
+
console.log('[Respectlytics] ⚠️ Event name cannot be empty');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (eventName.length > 100) {
|
|
87
|
+
console.log('[Respectlytics] ⚠️ Event name too long (max 100 characters)');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const event = this.createEvent(eventName, screen);
|
|
92
|
+
this.eventQueue.add(event);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Enable cross-session user tracking.
|
|
97
|
+
* Generates and persists a random user ID that will be included in all subsequent events.
|
|
98
|
+
*
|
|
99
|
+
* Note: User IDs are auto-generated and cannot be overridden. This is by design for privacy.
|
|
100
|
+
*/
|
|
101
|
+
async identify(): Promise<void> {
|
|
102
|
+
await this.userManager.identify();
|
|
103
|
+
console.log('[Respectlytics] ✓ User identified');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear the user ID.
|
|
108
|
+
* Call when the user logs out. Subsequent events will be anonymous until identify() is called again.
|
|
109
|
+
*/
|
|
110
|
+
async reset(): Promise<void> {
|
|
111
|
+
await this.userManager.reset();
|
|
112
|
+
console.log('[Respectlytics] ✓ User reset');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Force send all queued events immediately.
|
|
117
|
+
* Rarely needed - the SDK auto-flushes every 30 seconds or when the queue reaches 10 events.
|
|
118
|
+
*/
|
|
119
|
+
async flush(): Promise<void> {
|
|
120
|
+
await this.eventQueue.flush();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MARK: - Private Helpers
|
|
124
|
+
|
|
125
|
+
private createEvent(eventName: string, screen?: string): Event {
|
|
126
|
+
const metadata = this.collectMetadata();
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
eventName,
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
132
|
+
userId: this.userManager.getUserId(),
|
|
133
|
+
screen: screen || null,
|
|
134
|
+
...metadata,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private collectMetadata(): {
|
|
139
|
+
platform: string;
|
|
140
|
+
osVersion: string;
|
|
141
|
+
appVersion: string;
|
|
142
|
+
locale: string;
|
|
143
|
+
deviceType: string;
|
|
144
|
+
} {
|
|
145
|
+
// Determine platform
|
|
146
|
+
const platform = Platform.OS === 'ios' ? 'iOS' : 'Android';
|
|
147
|
+
|
|
148
|
+
// Get OS version
|
|
149
|
+
const osVersion = String(Platform.Version);
|
|
150
|
+
|
|
151
|
+
// Get app version - try to get from native modules
|
|
152
|
+
let appVersion = 'unknown';
|
|
153
|
+
try {
|
|
154
|
+
// React Native provides app info through different native modules
|
|
155
|
+
const { PlatformConstants } = NativeModules;
|
|
156
|
+
if (PlatformConstants?.reactNativeVersion) {
|
|
157
|
+
// This is React Native version, not app version
|
|
158
|
+
// App version should come from the host app
|
|
159
|
+
}
|
|
160
|
+
// For now, use 'unknown' as we can't reliably get app version without additional dependencies
|
|
161
|
+
// In a real app, the developer would configure this
|
|
162
|
+
} catch {
|
|
163
|
+
appVersion = 'unknown';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get locale
|
|
167
|
+
let locale = 'en_US';
|
|
168
|
+
try {
|
|
169
|
+
// React Native doesn't expose locale directly, but we can get it from platform
|
|
170
|
+
if (Platform.OS === 'ios') {
|
|
171
|
+
locale = NativeModules.SettingsManager?.settings?.AppleLocale ||
|
|
172
|
+
NativeModules.SettingsManager?.settings?.AppleLanguages?.[0] ||
|
|
173
|
+
'en_US';
|
|
174
|
+
} else {
|
|
175
|
+
locale = NativeModules.I18nManager?.localeIdentifier || 'en_US';
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
locale = 'en_US';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Determine device type based on screen size
|
|
182
|
+
const { width, height } = Dimensions.get('window');
|
|
183
|
+
const minDimension = Math.min(width, height);
|
|
184
|
+
const deviceType = minDimension >= 600 ? 'tablet' : 'phone';
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
platform,
|
|
188
|
+
osVersion,
|
|
189
|
+
appVersion,
|
|
190
|
+
locale,
|
|
191
|
+
deviceType,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Export singleton instance
|
|
197
|
+
const Respectlytics = new RespectlyticsSDK();
|
|
198
|
+
export default Respectlytics;
|
|
199
|
+
export { RespectlyticsSDK };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Manages session ID generation and rotation.
|
|
6
|
+
* Sessions automatically rotate after 30 minutes of inactivity.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a UUID v4 string
|
|
13
|
+
*/
|
|
14
|
+
function generateUUID(): string {
|
|
15
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
16
|
+
const r = (Math.random() * 16) | 0;
|
|
17
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
18
|
+
return v.toString(16);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manages session ID generation and rotation
|
|
24
|
+
*/
|
|
25
|
+
export class SessionManager {
|
|
26
|
+
private sessionId: string | null = null;
|
|
27
|
+
private lastEventTime: number | null = null;
|
|
28
|
+
|
|
29
|
+
// 30 minutes in milliseconds
|
|
30
|
+
private readonly SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get current session ID, rotating if necessary.
|
|
34
|
+
* Session rotates after 30 minutes of inactivity.
|
|
35
|
+
*/
|
|
36
|
+
getSessionId(): string {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
|
|
39
|
+
// Check if session expired
|
|
40
|
+
if (
|
|
41
|
+
this.lastEventTime !== null &&
|
|
42
|
+
now - this.lastEventTime > this.SESSION_TIMEOUT_MS
|
|
43
|
+
) {
|
|
44
|
+
// Force new session
|
|
45
|
+
this.sessionId = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Generate new session if needed
|
|
49
|
+
if (this.sessionId === null) {
|
|
50
|
+
this.sessionId = this.generateSessionId();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.lastEventTime = now;
|
|
54
|
+
return this.sessionId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate a new session ID (32 lowercase hex characters)
|
|
59
|
+
* UUID without dashes, all lowercase
|
|
60
|
+
*/
|
|
61
|
+
private generateSessionId(): string {
|
|
62
|
+
return generateUUID().toLowerCase().replace(/-/g, '');
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/Storage.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Wrapper around AsyncStorage for persisting SDK data.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Storage wrapper providing typed access to AsyncStorage
|
|
14
|
+
*/
|
|
15
|
+
export class Storage {
|
|
16
|
+
/**
|
|
17
|
+
* Get a string value from storage
|
|
18
|
+
*/
|
|
19
|
+
static async getItem(key: string): Promise<string | null> {
|
|
20
|
+
try {
|
|
21
|
+
return await AsyncStorage.getItem(key);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.log(`[Respectlytics] Failed to read from storage: ${key}`);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set a string value in storage
|
|
30
|
+
*/
|
|
31
|
+
static async setItem(key: string, value: string): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
await AsyncStorage.setItem(key, value);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.log(`[Respectlytics] Failed to write to storage: ${key}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Remove a value from storage
|
|
41
|
+
*/
|
|
42
|
+
static async removeItem(key: string): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
await AsyncStorage.removeItem(key);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log(`[Respectlytics] Failed to remove from storage: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserManager.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Manages user ID generation, persistence, and reset.
|
|
6
|
+
* User IDs are auto-generated and cannot be overridden.
|
|
7
|
+
* This is by design for maximum privacy.
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Storage } from './Storage';
|
|
13
|
+
import { STORAGE_KEYS } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manages user ID generation, persistence, and reset
|
|
17
|
+
*/
|
|
18
|
+
export class UserManager {
|
|
19
|
+
private _userId: string | null = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Current user ID (null if not identified)
|
|
23
|
+
*/
|
|
24
|
+
getUserId(): string | null {
|
|
25
|
+
return this._userId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load any persisted user ID from storage
|
|
30
|
+
*/
|
|
31
|
+
async loadUserId(): Promise<void> {
|
|
32
|
+
this._userId = await Storage.getItem(STORAGE_KEYS.USER_ID);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate or retrieve user ID.
|
|
37
|
+
* If already identified, returns existing ID.
|
|
38
|
+
* If not, generates a new ID and persists it.
|
|
39
|
+
*/
|
|
40
|
+
async identify(): Promise<void> {
|
|
41
|
+
// Check storage first
|
|
42
|
+
const stored = await Storage.getItem(STORAGE_KEYS.USER_ID);
|
|
43
|
+
if (stored) {
|
|
44
|
+
this._userId = stored;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Generate new ID (32 lowercase hex chars)
|
|
49
|
+
const newId = this.generateUserId();
|
|
50
|
+
await Storage.setItem(STORAGE_KEYS.USER_ID, newId);
|
|
51
|
+
this._userId = newId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clear user ID. Call on logout.
|
|
56
|
+
*/
|
|
57
|
+
async reset(): Promise<void> {
|
|
58
|
+
await Storage.removeItem(STORAGE_KEYS.USER_ID);
|
|
59
|
+
this._userId = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a new user ID (32 lowercase hex characters)
|
|
64
|
+
* UUID without dashes, all lowercase
|
|
65
|
+
*/
|
|
66
|
+
private generateUserId(): string {
|
|
67
|
+
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
68
|
+
const r = (Math.random() * 16) | 0;
|
|
69
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
70
|
+
return v.toString(16);
|
|
71
|
+
});
|
|
72
|
+
return uuid.toLowerCase().replace(/-/g, '');
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Respectlytics React Native SDK
|
|
3
|
+
*
|
|
4
|
+
* Official SDK for privacy-first analytics.
|
|
5
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Respectlytics from './Respectlytics';
|
|
9
|
+
|
|
10
|
+
// Default export - the main SDK instance
|
|
11
|
+
export default Respectlytics;
|
|
12
|
+
|
|
13
|
+
// Named exports for advanced usage
|
|
14
|
+
export { RespectlyticsSDK } from './Respectlytics';
|
|
15
|
+
export { Event } from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts
|
|
3
|
+
* Respectlytics React Native SDK
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2025 Respectlytics. All rights reserved.
|
|
6
|
+
* This SDK is provided under a proprietary license.
|
|
7
|
+
* See LICENSE file for details.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents an analytics event - flat structure matching API payload
|
|
12
|
+
*
|
|
13
|
+
* This interface only contains fields accepted by the Respectlytics API.
|
|
14
|
+
* The API uses a strict allowlist for privacy protection.
|
|
15
|
+
* Custom properties are NOT supported - this is by design for privacy.
|
|
16
|
+
*/
|
|
17
|
+
export interface Event {
|
|
18
|
+
eventName: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
userId: string | null;
|
|
22
|
+
screen: string | null;
|
|
23
|
+
platform: string;
|
|
24
|
+
osVersion: string;
|
|
25
|
+
appVersion: string;
|
|
26
|
+
locale: string;
|
|
27
|
+
deviceType: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Storage keys used by the SDK
|
|
32
|
+
*/
|
|
33
|
+
export const STORAGE_KEYS = {
|
|
34
|
+
USER_ID: 'com.respectlytics.userId',
|
|
35
|
+
EVENT_QUEUE: 'com.respectlytics.eventQueue',
|
|
36
|
+
} as const;
|