@datalyr/react-native
Version:
Datalyr SDK for React Native & Expo - Server-side attribution tracking
338 lines (297 loc) • 9.71 kB
text/typescript
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
import * as Application from 'expo-application';
import * as Device from 'expo-device';
import * as Network from 'expo-network';
import 'react-native-get-random-values';
import { v4 as uuidv4 } from 'uuid';
// Storage keys
export const STORAGE_KEYS = {
VISITOR_ID: '@datalyr/visitor_id',
SESSION_ID: '@datalyr/session_id',
SESSION_START: '@datalyr/session_start',
USER_ID: '@datalyr/user_id',
USER_PROPERTIES: '@datalyr/user_properties',
ATTRIBUTION_DATA: '@datalyr/attribution_data',
INSTALL_TIME: '@datalyr/install_time',
LAST_APP_VERSION: '@datalyr/last_app_version',
DEVICE_ID: '@datalyr/device_id',
} as const;
// Debug logging
export const debugLog = (message: string, ...args: any[]) => {
if (__DEV__) {
console.log(`[Datalyr] ${message}`, ...args);
}
};
export const errorLog = (message: string, error?: Error) => {
console.error(`[Datalyr Error] ${message}`, error);
};
// UUID generation
export const generateUUID = (): string => {
return uuidv4();
};
// Storage utilities
export const Storage = {
async getItem<T>(key: string): Promise<T | null> {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
errorLog(`Failed to get storage item ${key}:`, error as Error);
return null;
}
},
async setItem<T>(key: string, value: T): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
errorLog(`Failed to set storage item ${key}:`, error as Error);
}
},
async removeItem(key: string): Promise<void> {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
errorLog(`Failed to remove storage item ${key}:`, error as Error);
}
},
};
// Device info using Expo APIs
export interface DeviceInfo {
deviceId: string;
model: string;
manufacturer: string;
osVersion: string;
appVersion: string;
buildNumber: string;
bundleId: string;
screenWidth: number;
screenHeight: number;
timezone: string;
locale: string;
carrier?: string;
isEmulator: boolean;
}
export const getDeviceInfo = async (): Promise<DeviceInfo> => {
try {
const deviceId = await getOrCreateDeviceId();
return {
deviceId,
model: Device.modelName || Device.deviceName || 'Unknown',
manufacturer: Device.manufacturer || (Platform.OS === 'ios' ? 'Apple' : 'Unknown'),
osVersion: Device.osVersion || 'Unknown',
appVersion: Application.nativeApplicationVersion || '1.0.0',
buildNumber: Application.nativeBuildVersion || '1',
bundleId: Application.applicationId || 'unknown.bundle.id',
screenWidth: 0, // Would need Dimensions from react-native
screenHeight: 0, // Would need Dimensions from react-native
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locale: 'en-US', // Would need expo-localization for full locale
carrier: undefined, // Not available in Expo managed workflow
isEmulator: !Device.isDevice,
};
} catch (error) {
errorLog('Error getting device info:', error as Error);
return {
deviceId: await getOrCreateDeviceId(),
model: 'Unknown',
manufacturer: Platform.OS === 'ios' ? 'Apple' : 'Unknown',
osVersion: 'Unknown',
appVersion: '1.0.0',
buildNumber: '1',
bundleId: 'unknown.bundle.id',
screenWidth: 0,
screenHeight: 0,
timezone: 'UTC',
locale: 'en-US',
isEmulator: true,
};
}
};
// Device ID management
const getOrCreateDeviceId = async (): Promise<string> => {
try {
let deviceId = await Storage.getItem<string>(STORAGE_KEYS.DEVICE_ID);
if (!deviceId) {
deviceId = generateUUID();
await Storage.setItem(STORAGE_KEYS.DEVICE_ID, deviceId);
}
return deviceId;
} catch (error) {
errorLog('Error managing device ID:', error as Error);
return generateUUID();
}
};
// Visitor ID management
export const getOrCreateVisitorId = async (): Promise<string> => {
try {
let visitorId = await Storage.getItem<string>(STORAGE_KEYS.VISITOR_ID);
if (!visitorId) {
visitorId = generateUUID();
await Storage.setItem(STORAGE_KEYS.VISITOR_ID, visitorId);
debugLog('Created new visitor ID:', visitorId);
}
return visitorId;
} catch (error) {
errorLog('Error managing visitor ID:', error as Error);
return generateUUID();
}
};
// Session management
export const getOrCreateSessionId = async (): Promise<string> => {
try {
const sessionTimeout = 30 * 60 * 1000; // 30 minutes
const now = Date.now();
const [sessionId, sessionStart] = await Promise.all([
Storage.getItem<string>(STORAGE_KEYS.SESSION_ID),
Storage.getItem<number>(STORAGE_KEYS.SESSION_START),
]);
// Check if session is still valid
if (sessionId && sessionStart && (now - sessionStart) < sessionTimeout) {
return sessionId;
}
// Create new session
const newSessionId = generateUUID();
await Promise.all([
Storage.setItem(STORAGE_KEYS.SESSION_ID, newSessionId),
Storage.setItem(STORAGE_KEYS.SESSION_START, now),
]);
debugLog('Created new session:', newSessionId);
return newSessionId;
} catch (error) {
errorLog('Error managing session ID:', error as Error);
return generateUUID();
}
};
// Fingerprint data creation using Expo APIs
export const createFingerprintData = async () => {
try {
const deviceInfo = await getDeviceInfo();
return {
deviceId: deviceInfo.deviceId,
deviceInfo: {
model: deviceInfo.model,
manufacturer: deviceInfo.manufacturer,
osVersion: deviceInfo.osVersion,
screenSize: `${deviceInfo.screenWidth}x${deviceInfo.screenHeight}`,
timezone: deviceInfo.timezone,
locale: deviceInfo.locale,
carrier: deviceInfo.carrier,
isEmulator: deviceInfo.isEmulator,
},
};
} catch (error) {
errorLog('Error creating fingerprint data:', error as Error);
const deviceInfo = await getDeviceInfo();
return {
deviceId: deviceInfo.deviceId,
deviceInfo: {
model: deviceInfo.model,
manufacturer: deviceInfo.manufacturer,
osVersion: deviceInfo.osVersion,
screenSize: '0x0',
timezone: deviceInfo.timezone,
locale: deviceInfo.locale,
isEmulator: deviceInfo.isEmulator,
},
};
}
};
// IDFA/GAID collection has been removed for privacy compliance
// Modern attribution tracking relies on privacy-safe methods:
// Network type detection using Expo Network
export const getNetworkType = async (): Promise<string> => {
try {
const networkState = await Network.getNetworkStateAsync();
if (!networkState.isConnected) {
return 'none';
}
switch (networkState.type) {
case Network.NetworkStateType.WIFI:
return 'wifi';
case Network.NetworkStateType.CELLULAR:
return 'cellular';
case Network.NetworkStateType.ETHERNET:
return 'ethernet';
case Network.NetworkStateType.BLUETOOTH:
return 'bluetooth';
default:
return 'unknown';
}
} catch (error) {
debugLog('Error getting network type:', error);
return 'unknown';
}
};
// Event validation
export const validateEventName = (eventName: string): boolean => {
if (!eventName || typeof eventName !== 'string') {
return false;
}
if (eventName.length > 100) {
return false;
}
// Allow letters, numbers, underscores, and hyphens
const validPattern = /^[a-zA-Z0-9_-]+$/;
return validPattern.test(eventName);
};
export const validateEventData = (eventData?: Record<string, any>): boolean => {
if (!eventData) {
return true;
}
if (typeof eventData !== 'object' || Array.isArray(eventData)) {
return false;
}
try {
// Check if data can be serialized
JSON.stringify(eventData);
return true;
} catch {
return false;
}
};
// Install detection
export const isFirstLaunch = async (): Promise<boolean> => {
try {
const installTime = await Storage.getItem<string>(STORAGE_KEYS.INSTALL_TIME);
if (!installTime) {
// First launch - record install time
await Storage.setItem(STORAGE_KEYS.INSTALL_TIME, new Date().toISOString());
return true;
}
return false;
} catch (error) {
errorLog('Error checking first launch:', error as Error);
return false;
}
};
// App version tracking
export const checkAppVersion = async (): Promise<{ isUpdate: boolean; previousVersion?: string; currentVersion: string }> => {
try {
const currentVersion = Application.nativeApplicationVersion || '1.0.0';
const currentBuild = Application.nativeBuildVersion || '1';
const versionString = `${currentVersion}-${currentBuild}`;
const lastVersion = await Storage.getItem<string>(STORAGE_KEYS.LAST_APP_VERSION);
if (lastVersion && lastVersion !== versionString) {
await Storage.setItem(STORAGE_KEYS.LAST_APP_VERSION, versionString);
return {
isUpdate: true,
previousVersion: lastVersion,
currentVersion: versionString,
};
}
if (!lastVersion) {
await Storage.setItem(STORAGE_KEYS.LAST_APP_VERSION, versionString);
}
return {
isUpdate: false,
currentVersion: versionString,
};
} catch (error) {
errorLog('Error checking app version:', error as Error);
return {
isUpdate: false,
currentVersion: '1.0.0-1',
};
}
};