@datalyr/react-native 1.3.0 → 1.3.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.
@@ -0,0 +1,312 @@
1
+ import { debugLog, errorLog } from './utils';
2
+
3
+ export type NetworkState = {
4
+ isConnected: boolean;
5
+ isInternetReachable: boolean | null;
6
+ type: 'wifi' | 'cellular' | 'ethernet' | 'bluetooth' | 'vpn' | 'none' | 'unknown';
7
+ };
8
+
9
+ export type NetworkStateListener = (state: NetworkState) => void;
10
+
11
+ /**
12
+ * Network status manager that detects online/offline status
13
+ * Uses @react-native-community/netinfo for React Native or expo-network for Expo
14
+ */
15
+ class NetworkStatusManager {
16
+ private state: NetworkState = {
17
+ isConnected: true, // Default to true until we know otherwise
18
+ isInternetReachable: null,
19
+ type: 'unknown',
20
+ };
21
+
22
+ private listeners: Set<NetworkStateListener> = new Set();
23
+ private unsubscribe: (() => void) | null = null;
24
+ private initialized = false;
25
+ private netInfoModule: any = null;
26
+ private expoNetworkModule: any = null;
27
+
28
+ /**
29
+ * Initialize network status monitoring
30
+ * Call this during SDK initialization
31
+ */
32
+ async initialize(): Promise<void> {
33
+ if (this.initialized) {
34
+ return;
35
+ }
36
+
37
+ // Try @react-native-community/netinfo first (most common in RN apps)
38
+ try {
39
+ this.netInfoModule = require('@react-native-community/netinfo').default;
40
+ await this.initializeWithNetInfo();
41
+ this.initialized = true;
42
+ debugLog('Network status initialized with @react-native-community/netinfo');
43
+ return;
44
+ } catch {
45
+ // Module not available, try expo-network
46
+ }
47
+
48
+ // Try expo-network (for Expo apps)
49
+ try {
50
+ this.expoNetworkModule = require('expo-network');
51
+ await this.initializeWithExpoNetwork();
52
+ this.initialized = true;
53
+ debugLog('Network status initialized with expo-network');
54
+ return;
55
+ } catch {
56
+ // Module not available
57
+ }
58
+
59
+ // Fallback: assume online (no network monitoring available)
60
+ debugLog('No network status module available, defaulting to online');
61
+ this.state = {
62
+ isConnected: true,
63
+ isInternetReachable: true,
64
+ type: 'unknown',
65
+ };
66
+ this.initialized = true;
67
+ }
68
+
69
+ /**
70
+ * Initialize with @react-native-community/netinfo
71
+ */
72
+ private async initializeWithNetInfo(): Promise<void> {
73
+ const NetInfo = this.netInfoModule;
74
+
75
+ // Get initial state
76
+ try {
77
+ const netState = await NetInfo.fetch();
78
+ this.updateStateFromNetInfo(netState);
79
+ } catch (error) {
80
+ errorLog('Failed to fetch initial network state:', error as Error);
81
+ }
82
+
83
+ // Subscribe to changes
84
+ this.unsubscribe = NetInfo.addEventListener((netState: any) => {
85
+ this.updateStateFromNetInfo(netState);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Update state from NetInfo response
91
+ */
92
+ private updateStateFromNetInfo(netState: any): void {
93
+ const previouslyConnected = this.state.isConnected;
94
+
95
+ this.state = {
96
+ isConnected: netState.isConnected ?? true,
97
+ isInternetReachable: netState.isInternetReachable,
98
+ type: this.mapNetInfoType(netState.type),
99
+ };
100
+
101
+ // Notify listeners if connection status changed
102
+ if (previouslyConnected !== this.state.isConnected) {
103
+ debugLog(`Network status changed: ${this.state.isConnected ? 'online' : 'offline'} (${this.state.type})`);
104
+ this.notifyListeners();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Map NetInfo type to our simplified type
110
+ */
111
+ private mapNetInfoType(type: string): NetworkState['type'] {
112
+ switch (type) {
113
+ case 'wifi':
114
+ return 'wifi';
115
+ case 'cellular':
116
+ return 'cellular';
117
+ case 'ethernet':
118
+ return 'ethernet';
119
+ case 'bluetooth':
120
+ return 'bluetooth';
121
+ case 'vpn':
122
+ return 'vpn';
123
+ case 'none':
124
+ return 'none';
125
+ default:
126
+ return 'unknown';
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Initialize with expo-network
132
+ */
133
+ private async initializeWithExpoNetwork(): Promise<void> {
134
+ const Network = this.expoNetworkModule;
135
+
136
+ // Get initial state
137
+ try {
138
+ const networkState = await Network.getNetworkStateAsync();
139
+ this.updateStateFromExpoNetwork(networkState);
140
+ } catch (error) {
141
+ errorLog('Failed to fetch initial network state from expo-network:', error as Error);
142
+ }
143
+
144
+ // Note: expo-network doesn't have a listener API like netinfo
145
+ // We'll poll periodically or rely on app state changes
146
+ this.startExpoNetworkPolling();
147
+ }
148
+
149
+ /**
150
+ * Update state from expo-network response
151
+ */
152
+ private updateStateFromExpoNetwork(networkState: any): void {
153
+ const previouslyConnected = this.state.isConnected;
154
+
155
+ this.state = {
156
+ isConnected: networkState.isConnected ?? true,
157
+ isInternetReachable: networkState.isInternetReachable ?? null,
158
+ type: this.mapExpoNetworkType(networkState.type),
159
+ };
160
+
161
+ if (previouslyConnected !== this.state.isConnected) {
162
+ debugLog(`Network status changed: ${this.state.isConnected ? 'online' : 'offline'} (${this.state.type})`);
163
+ this.notifyListeners();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Map expo-network type to our simplified type
169
+ */
170
+ private mapExpoNetworkType(type: string): NetworkState['type'] {
171
+ switch (type) {
172
+ case 'WIFI':
173
+ return 'wifi';
174
+ case 'CELLULAR':
175
+ return 'cellular';
176
+ case 'ETHERNET':
177
+ return 'ethernet';
178
+ case 'BLUETOOTH':
179
+ return 'bluetooth';
180
+ case 'VPN':
181
+ return 'vpn';
182
+ case 'NONE':
183
+ return 'none';
184
+ default:
185
+ return 'unknown';
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Poll expo-network for changes (since it doesn't have a listener API)
191
+ */
192
+ private pollingInterval: NodeJS.Timeout | null = null;
193
+
194
+ private startExpoNetworkPolling(): void {
195
+ // Poll every 5 seconds for network changes
196
+ this.pollingInterval = setInterval(async () => {
197
+ try {
198
+ if (this.expoNetworkModule) {
199
+ const networkState = await this.expoNetworkModule.getNetworkStateAsync();
200
+ this.updateStateFromExpoNetwork(networkState);
201
+ }
202
+ } catch (error) {
203
+ // Ignore polling errors
204
+ }
205
+ }, 5000);
206
+ }
207
+
208
+ /**
209
+ * Notify all listeners of state change
210
+ */
211
+ private notifyListeners(): void {
212
+ this.listeners.forEach((listener) => {
213
+ try {
214
+ listener(this.state);
215
+ } catch (error) {
216
+ errorLog('Error in network state listener:', error as Error);
217
+ }
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Get current network state
223
+ */
224
+ getState(): NetworkState {
225
+ return { ...this.state };
226
+ }
227
+
228
+ /**
229
+ * Check if device is currently online
230
+ */
231
+ isOnline(): boolean {
232
+ // Consider online if connected OR if we're not sure about internet reachability
233
+ return this.state.isConnected && (this.state.isInternetReachable !== false);
234
+ }
235
+
236
+ /**
237
+ * Get current network type
238
+ */
239
+ getNetworkType(): NetworkState['type'] {
240
+ return this.state.type;
241
+ }
242
+
243
+ /**
244
+ * Subscribe to network state changes
245
+ * Returns an unsubscribe function
246
+ */
247
+ subscribe(listener: NetworkStateListener): () => void {
248
+ this.listeners.add(listener);
249
+
250
+ // Immediately call with current state
251
+ try {
252
+ listener(this.state);
253
+ } catch (error) {
254
+ errorLog('Error calling network state listener:', error as Error);
255
+ }
256
+
257
+ // Return unsubscribe function
258
+ return () => {
259
+ this.listeners.delete(listener);
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Refresh network state manually
265
+ * Useful when returning from background
266
+ */
267
+ async refresh(): Promise<NetworkState> {
268
+ if (this.netInfoModule) {
269
+ try {
270
+ const netState = await this.netInfoModule.fetch();
271
+ this.updateStateFromNetInfo(netState);
272
+ } catch (error) {
273
+ errorLog('Failed to refresh network state:', error as Error);
274
+ }
275
+ } else if (this.expoNetworkModule) {
276
+ try {
277
+ const networkState = await this.expoNetworkModule.getNetworkStateAsync();
278
+ this.updateStateFromExpoNetwork(networkState);
279
+ } catch (error) {
280
+ errorLog('Failed to refresh network state from expo-network:', error as Error);
281
+ }
282
+ }
283
+
284
+ return this.state;
285
+ }
286
+
287
+ /**
288
+ * Cleanup and stop monitoring
289
+ */
290
+ destroy(): void {
291
+ if (this.unsubscribe) {
292
+ this.unsubscribe();
293
+ this.unsubscribe = null;
294
+ }
295
+
296
+ if (this.pollingInterval) {
297
+ clearInterval(this.pollingInterval);
298
+ this.pollingInterval = null;
299
+ }
300
+
301
+ this.listeners.clear();
302
+ this.initialized = false;
303
+
304
+ debugLog('Network status manager destroyed');
305
+ }
306
+ }
307
+
308
+ // Export singleton instance
309
+ export const networkStatusManager = new NetworkStatusManager();
310
+
311
+ // Export for direct access
312
+ export default networkStatusManager;
package/src/types.ts CHANGED
@@ -40,37 +40,105 @@ export interface DeferredDeepLinkResult {
40
40
  adId?: string;
41
41
  }
42
42
 
43
- // Core SDK Configuration
43
+ /**
44
+ * Core SDK Configuration
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * await Datalyr.initialize({
49
+ * apiKey: 'dk_your_api_key',
50
+ * debug: true,
51
+ * enableAutoEvents: true,
52
+ * enableAttribution: true,
53
+ * skadTemplate: 'ecommerce',
54
+ * meta: { appId: 'FB_APP_ID' },
55
+ * tiktok: { appId: 'APP_ID', tiktokAppId: 'TIKTOK_APP_ID' },
56
+ * });
57
+ * ```
58
+ */
44
59
  export interface DatalyrConfig {
45
- apiKey: string; // Required for server-side tracking
46
- workspaceId?: string; // Optional for backward compatibility
60
+ /** Required: API key from Datalyr dashboard (starts with 'dk_') */
61
+ apiKey: string;
62
+
63
+ /** Optional: Workspace ID for multi-workspace setups */
64
+ workspaceId?: string;
65
+
66
+ /** Enable console logging for debugging. Default: false */
47
67
  debug?: boolean;
68
+
69
+ /**
70
+ * API endpoint URL. Default: 'https://api.datalyr.com'
71
+ * @deprecated Use `endpoint` instead
72
+ */
48
73
  apiUrl?: string;
74
+
75
+ /** API endpoint URL. Default: 'https://api.datalyr.com' */
49
76
  endpoint?: string;
50
- useServerTracking?: boolean; // Default: true for v1.0.0
77
+
78
+ /** Use server-side tracking. Default: true */
79
+ useServerTracking?: boolean;
80
+
81
+ /** Maximum retry attempts for failed requests. Default: 3 */
51
82
  maxRetries?: number;
83
+
84
+ /** Delay between retries in milliseconds. Default: 1000 */
52
85
  retryDelay?: number;
86
+
87
+ /** Request timeout in milliseconds. Default: 15000 */
53
88
  timeout?: number;
89
+
90
+ /** Number of events per batch. Default: 10 */
54
91
  batchSize?: number;
92
+
93
+ /** Interval between automatic flushes in milliseconds. Default: 30000 */
55
94
  flushInterval?: number;
95
+
96
+ /** Maximum events to store in queue. Default: 100 */
56
97
  maxQueueSize?: number;
98
+
99
+ /**
100
+ * Maximum events to store in queue. Default: 100
101
+ * @deprecated Use `maxQueueSize` instead
102
+ */
57
103
  maxEventQueueSize?: number;
104
+
105
+ /** Respect browser Do Not Track setting. Default: true */
58
106
  respectDoNotTrack?: boolean;
107
+
108
+ /** Enable automatic event tracking (sessions, app lifecycle). Default: false */
59
109
  enableAutoEvents?: boolean;
110
+
111
+ /** Enable attribution tracking (deep links, install referrer). Default: false */
60
112
  enableAttribution?: boolean;
113
+
114
+ /** Enable web-to-app attribution matching via email. Default: true */
61
115
  enableWebToAppAttribution?: boolean;
116
+
117
+ /**
118
+ * Auto-events configuration
119
+ * @deprecated Use `autoEventConfig` instead
120
+ */
62
121
  autoEvents?: AutoEventConfig;
122
+
123
+ /** Auto-events configuration */
63
124
  autoEventConfig?: AutoEventConfig;
125
+
126
+ /**
127
+ * Retry configuration
128
+ * @deprecated Use `maxRetries` and `retryDelay` instead
129
+ */
64
130
  retryConfig?: {
65
131
  maxRetries: number;
66
132
  retryDelay: number;
67
133
  };
134
+
135
+ /** SKAdNetwork template for automatic conversion value encoding (iOS only) */
68
136
  skadTemplate?: 'ecommerce' | 'gaming' | 'subscription';
69
137
 
70
- // Meta (Facebook) SDK Configuration
138
+ /** Meta (Facebook) SDK Configuration */
71
139
  meta?: MetaConfig;
72
140
 
73
- // TikTok SDK Configuration
141
+ /** TikTok SDK Configuration */
74
142
  tiktok?: TikTokConfig;
75
143
  }
76
144
  // Event Types
package/src/utils.ts CHANGED
@@ -124,12 +124,50 @@ export const getOrCreateSessionId = async (): Promise<string> => {
124
124
  }
125
125
  };
126
126
 
127
+ // Cached device info to avoid repeated async calls
128
+ let cachedDeviceInfo: DeviceInfoType | null = null;
129
+ let deviceInfoPromise: Promise<DeviceInfoType> | null = null;
130
+
127
131
  /**
128
- * Collect comprehensive device information
132
+ * Collect comprehensive device information (cached after first call)
133
+ * Device info is cached because it rarely changes during app session
129
134
  */
130
135
  export const getDeviceInfo = async (): Promise<DeviceInfoType> => {
136
+ // Return cached info if available
137
+ if (cachedDeviceInfo) {
138
+ return cachedDeviceInfo;
139
+ }
140
+
141
+ // If a fetch is already in progress, wait for it (prevents concurrent fetches)
142
+ if (deviceInfoPromise) {
143
+ return deviceInfoPromise;
144
+ }
145
+
146
+ // Start fetching and cache the promise
147
+ deviceInfoPromise = fetchDeviceInfoInternal();
148
+
149
+ try {
150
+ cachedDeviceInfo = await deviceInfoPromise;
151
+ return cachedDeviceInfo;
152
+ } finally {
153
+ deviceInfoPromise = null;
154
+ }
155
+ };
156
+
157
+ /**
158
+ * Clear the cached device info (useful for testing or after app update)
159
+ */
160
+ export const clearDeviceInfoCache = (): void => {
161
+ cachedDeviceInfo = null;
162
+ deviceInfoPromise = null;
163
+ };
164
+
165
+ /**
166
+ * Internal function to fetch device info
167
+ */
168
+ const fetchDeviceInfoInternal = async (): Promise<DeviceInfoType> => {
131
169
  const { width, height } = Dimensions.get('window');
132
-
170
+
133
171
  // If DeviceInfo is not available (like in Expo Go), use fallback
134
172
  if (!DeviceInfo) {
135
173
  return {
@@ -147,7 +185,7 @@ export const getDeviceInfo = async (): Promise<DeviceInfoType> => {
147
185
  isEmulator: false,
148
186
  };
149
187
  }
150
-
188
+
151
189
  try {
152
190
  const [
153
191
  deviceId,
@@ -174,7 +212,7 @@ export const getDeviceInfo = async (): Promise<DeviceInfoType> => {
174
212
  DeviceInfo.getCarrier().catch(() => undefined),
175
213
  DeviceInfo.isEmulator(),
176
214
  ]);
177
-
215
+
178
216
  return {
179
217
  deviceId,
180
218
  model,
@@ -192,7 +230,7 @@ export const getDeviceInfo = async (): Promise<DeviceInfoType> => {
192
230
  };
193
231
  } catch (error) {
194
232
  console.warn('Failed to collect device info:', error);
195
-
233
+
196
234
  // Fallback device info
197
235
  return {
198
236
  deviceId: generateUUID(),
@@ -232,11 +270,29 @@ export const createFingerprintData = async (): Promise<FingerprintData> => {
232
270
  }
233
271
  };
234
272
 
273
+ // Import network status manager for network type detection
274
+ let networkStatusManagerRef: any = null;
275
+
276
+ // Lazy load to avoid circular dependencies
277
+ const getNetworkStatusManager = () => {
278
+ if (!networkStatusManagerRef) {
279
+ try {
280
+ networkStatusManagerRef = require('./network-status').networkStatusManager;
281
+ } catch {
282
+ // Module not loaded yet
283
+ }
284
+ }
285
+ return networkStatusManagerRef;
286
+ };
287
+
235
288
  /**
236
289
  * Get network connection type
237
290
  */
238
291
  export const getNetworkType = (): string => {
239
- // This will be enhanced with react-native-netinfo if needed
292
+ const manager = getNetworkStatusManager();
293
+ if (manager) {
294
+ return manager.getNetworkType();
295
+ }
240
296
  return 'unknown';
241
297
  };
242
298