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 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
+ [![npm version](https://img.shields.io/npm/v/respectlytics-react-native.svg)](https://www.npmjs.com/package/respectlytics-react-native)
4
+ [![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20Android-lightgrey.svg)](https://github.com/respectlytics/respectlytics-react-native)
5
+ [![License](https://img.shields.io/badge/license-Proprietary-blue.svg)](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;