@datalyr/react-native 1.0.5 → 1.1.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/README.md CHANGED
@@ -14,6 +14,7 @@ Official Datalyr SDK for React Native & Expo - Mobile attribution tracking and a
14
14
  - 💾 **Offline Support** - Events saved and retried when reconnected
15
15
  - 🔒 **Privacy First** - GDPR/CCPA compliant
16
16
  - ⚡ **Lightweight** - < 100KB, minimal battery impact
17
+ - 🆔 **Identity Resolution** - Persistent anonymous ID links web → mobile → server events
17
18
 
18
19
  ## Installation
19
20
 
@@ -164,6 +165,35 @@ await Datalyr.setAttributionData({
164
165
  });
165
166
  ```
166
167
 
168
+ ## Identity Resolution (New in v1.1.0)
169
+
170
+ The SDK now includes persistent anonymous IDs for complete user journey tracking:
171
+
172
+ ```typescript
173
+ // Get anonymous ID (persists across app sessions)
174
+ const anonymousId = Datalyr.getAnonymousId();
175
+
176
+ // Pass to your backend for attribution preservation
177
+ await fetch('/api/purchase', {
178
+ method: 'POST',
179
+ body: JSON.stringify({
180
+ items: cart,
181
+ anonymous_id: anonymousId // Links server events to mobile events
182
+ })
183
+ });
184
+
185
+ // Identity is automatically linked when you identify a user
186
+ await Datalyr.identify('user_123', {
187
+ email: 'user@example.com'
188
+ });
189
+ // This creates a $identify event that links anonymous_id to user_id
190
+ ```
191
+
192
+ ### Key Benefits:
193
+ - **Attribution Preservation**: Never lose fbclid, gclid, ttclid, or lyr tracking
194
+ - **Complete Journey**: Track users from web → app → server
195
+ - **Automatic Linking**: Identity resolution happens automatically
196
+
167
197
  ## Session Management
168
198
 
169
199
  Sessions are tracked automatically with a 30-minute timeout.
@@ -95,6 +95,11 @@ export declare class AttributionManager {
95
95
  * Track install event with attribution data
96
96
  */
97
97
  trackInstall(): Promise<AttributionData>;
98
+ /**
99
+ * Merge web attribution data into mobile session
100
+ * Called when web-to-app attribution is resolved via email
101
+ */
102
+ mergeWebAttribution(webAttribution: any): void;
98
103
  /**
99
104
  * Set custom attribution data
100
105
  */
@@ -335,6 +335,45 @@ export class AttributionManager {
335
335
  }
336
336
  return this.attributionData;
337
337
  }
338
+ /**
339
+ * Merge web attribution data into mobile session
340
+ * Called when web-to-app attribution is resolved via email
341
+ */
342
+ mergeWebAttribution(webAttribution) {
343
+ debugLog('Merging web attribution data:', webAttribution);
344
+ // Only merge if we don't already have attribution data
345
+ // Web attribution takes precedence for first-touch
346
+ if (!this.attributionData.fbclid && webAttribution.fbclid) {
347
+ this.attributionData.fbclid = webAttribution.fbclid;
348
+ }
349
+ if (!this.attributionData.gclid && webAttribution.gclid) {
350
+ this.attributionData.gclid = webAttribution.gclid;
351
+ }
352
+ if (!this.attributionData.ttclid && webAttribution.ttclid) {
353
+ this.attributionData.ttclid = webAttribution.ttclid;
354
+ }
355
+ // Merge UTM parameters
356
+ if (!this.attributionData.utm_source && webAttribution.utm_source) {
357
+ this.attributionData.utm_source = webAttribution.utm_source;
358
+ }
359
+ if (!this.attributionData.utm_medium && webAttribution.utm_medium) {
360
+ this.attributionData.utm_medium = webAttribution.utm_medium;
361
+ }
362
+ if (!this.attributionData.utm_campaign && webAttribution.utm_campaign) {
363
+ this.attributionData.utm_campaign = webAttribution.utm_campaign;
364
+ }
365
+ if (!this.attributionData.utm_content && webAttribution.utm_content) {
366
+ this.attributionData.utm_content = webAttribution.utm_content;
367
+ }
368
+ if (!this.attributionData.utm_term && webAttribution.utm_term) {
369
+ this.attributionData.utm_term = webAttribution.utm_term;
370
+ }
371
+ // Store web visitor ID for cross-device tracking
372
+ this.attributionData.web_visitor_id = webAttribution.visitor_id;
373
+ // Save merged attribution data
374
+ this.saveAttributionData();
375
+ debugLog('Web attribution merged successfully');
376
+ }
338
377
  /**
339
378
  * Set custom attribution data
340
379
  */
@@ -26,6 +26,11 @@ export declare class DatalyrSDK {
26
26
  * Identify a user
27
27
  */
28
28
  identify(userId: string, properties?: UserProperties): Promise<void>;
29
+ /**
30
+ * Fetch web attribution data for user and merge into mobile session
31
+ * Called automatically during identify() if email is provided
32
+ */
33
+ private fetchAndMergeWebAttribution;
29
34
  /**
30
35
  * Alias a user (connect anonymous user to known user)
31
36
  */
@@ -45,11 +50,16 @@ export declare class DatalyrSDK {
45
50
  initialized: boolean;
46
51
  workspaceId: string;
47
52
  visitorId: string;
53
+ anonymousId: string;
48
54
  sessionId: string;
49
55
  currentUserId?: string;
50
56
  queueStats: any;
51
57
  attribution: any;
52
58
  };
59
+ /**
60
+ * Get the persistent anonymous ID
61
+ */
62
+ getAnonymousId(): string;
53
63
  /**
54
64
  * Get detailed attribution data
55
65
  */
@@ -151,11 +161,13 @@ export declare class Datalyr {
151
161
  initialized: boolean;
152
162
  workspaceId: string;
153
163
  visitorId: string;
164
+ anonymousId: string;
154
165
  sessionId: string;
155
166
  currentUserId?: string;
156
167
  queueStats: any;
157
168
  attribution: any;
158
169
  };
170
+ static getAnonymousId(): string;
159
171
  static getAttributionData(): AttributionData;
160
172
  static setAttributionData(data: Partial<AttributionData>): Promise<void>;
161
173
  static getCurrentSession(): SessionData | null;
@@ -1,5 +1,5 @@
1
1
  import { Platform, AppState } from 'react-native';
2
- import { getOrCreateVisitorId, getOrCreateSessionId, createFingerprintData, generateUUID, getDeviceInfo, getNetworkType, validateEventName, validateEventData, debugLog, errorLog, Storage, STORAGE_KEYS, } from './utils';
2
+ import { getOrCreateVisitorId, getOrCreateAnonymousId, getOrCreateSessionId, createFingerprintData, generateUUID, getDeviceInfo, getNetworkType, validateEventName, validateEventData, debugLog, errorLog, Storage, STORAGE_KEYS, } from './utils';
3
3
  import { createHttpClient, HttpClient } from './http-client';
4
4
  import { createEventQueue, EventQueue } from './event-queue';
5
5
  import { attributionManager } from './attribution';
@@ -27,6 +27,7 @@ export class DatalyrSDK {
27
27
  respectDoNotTrack: true,
28
28
  },
29
29
  visitorId: '',
30
+ anonymousId: '', // Persistent anonymous identifier
30
31
  sessionId: '',
31
32
  userProperties: {},
32
33
  eventQueue: [],
@@ -70,8 +71,9 @@ export class DatalyrSDK {
70
71
  flushInterval: this.state.config.flushInterval || 30000,
71
72
  maxRetryCount: this.state.config.maxRetries || 3,
72
73
  });
73
- // Initialize visitor ID and session
74
+ // Initialize visitor ID, anonymous ID and session
74
75
  this.state.visitorId = await getOrCreateVisitorId();
76
+ this.state.anonymousId = await getOrCreateAnonymousId();
75
77
  this.state.sessionId = await getOrCreateSessionId();
76
78
  // Load persisted user data
77
79
  await this.loadPersistedUserData();
@@ -128,6 +130,7 @@ export class DatalyrSDK {
128
130
  debugLog('Datalyr SDK initialized successfully', {
129
131
  workspaceId: this.state.config.workspaceId,
130
132
  visitorId: this.state.visitorId,
133
+ anonymousId: this.state.anonymousId,
131
134
  sessionId: this.state.sessionId,
132
135
  });
133
136
  }
@@ -191,16 +194,83 @@ export class DatalyrSDK {
191
194
  this.state.userProperties = { ...this.state.userProperties, ...properties };
192
195
  // Persist user data
193
196
  await this.persistUserData();
194
- // Track identify event
195
- await this.track('identify', {
197
+ // Track $identify event for identity resolution
198
+ await this.track('$identify', {
196
199
  userId,
200
+ anonymous_id: this.state.anonymousId,
197
201
  ...properties
198
202
  });
203
+ // Fetch and merge web attribution if email is provided
204
+ if (this.state.config.enableWebToAppAttribution !== false) {
205
+ const email = (properties === null || properties === void 0 ? void 0 : properties.email) || (typeof userId === 'string' && userId.includes('@') ? userId : null);
206
+ if (email) {
207
+ await this.fetchAndMergeWebAttribution(email);
208
+ }
209
+ }
199
210
  }
200
211
  catch (error) {
201
212
  errorLog('Error identifying user:', error);
202
213
  }
203
214
  }
215
+ /**
216
+ * Fetch web attribution data for user and merge into mobile session
217
+ * Called automatically during identify() if email is provided
218
+ */
219
+ async fetchAndMergeWebAttribution(email) {
220
+ try {
221
+ debugLog('Fetching web attribution for email:', email);
222
+ // Call API endpoint to get web attribution
223
+ const response = await fetch('https://api.datalyr.com/attribution/lookup', {
224
+ method: 'POST',
225
+ headers: {
226
+ 'Content-Type': 'application/json',
227
+ 'X-Datalyr-API-Key': this.state.config.apiKey,
228
+ },
229
+ body: JSON.stringify({ email }),
230
+ });
231
+ if (!response.ok) {
232
+ debugLog('Failed to fetch web attribution:', response.status);
233
+ return;
234
+ }
235
+ const result = await response.json();
236
+ if (!result.found || !result.attribution) {
237
+ debugLog('No web attribution found for user');
238
+ return;
239
+ }
240
+ const webAttribution = result.attribution;
241
+ debugLog('Web attribution found:', {
242
+ visitor_id: webAttribution.visitor_id,
243
+ has_fbclid: !!webAttribution.fbclid,
244
+ has_gclid: !!webAttribution.gclid,
245
+ utm_source: webAttribution.utm_source,
246
+ });
247
+ // Merge web attribution into current session
248
+ await this.track('$web_attribution_merged', {
249
+ web_visitor_id: webAttribution.visitor_id,
250
+ web_user_id: webAttribution.user_id,
251
+ fbclid: webAttribution.fbclid,
252
+ gclid: webAttribution.gclid,
253
+ ttclid: webAttribution.ttclid,
254
+ gbraid: webAttribution.gbraid,
255
+ wbraid: webAttribution.wbraid,
256
+ fbp: webAttribution.fbp,
257
+ fbc: webAttribution.fbc,
258
+ utm_source: webAttribution.utm_source,
259
+ utm_medium: webAttribution.utm_medium,
260
+ utm_campaign: webAttribution.utm_campaign,
261
+ utm_content: webAttribution.utm_content,
262
+ utm_term: webAttribution.utm_term,
263
+ web_timestamp: webAttribution.timestamp,
264
+ });
265
+ // Update attribution manager with web data
266
+ attributionManager.mergeWebAttribution(webAttribution);
267
+ debugLog('Successfully merged web attribution into mobile session');
268
+ }
269
+ catch (error) {
270
+ errorLog('Error fetching web attribution:', error);
271
+ // Non-blocking - continue even if attribution fetch fails
272
+ }
273
+ }
204
274
  /**
205
275
  * Alias a user (connect anonymous user to known user)
206
276
  */
@@ -214,6 +284,7 @@ export class DatalyrSDK {
214
284
  newUserId,
215
285
  previousId: previousId || this.state.visitorId,
216
286
  visitorId: this.state.visitorId,
287
+ anonymousId: this.state.anonymousId, // Include for identity resolution
217
288
  };
218
289
  debugLog('Aliasing user:', aliasData);
219
290
  // Track alias event
@@ -265,12 +336,19 @@ export class DatalyrSDK {
265
336
  initialized: this.state.initialized,
266
337
  workspaceId: this.state.config.workspaceId || '',
267
338
  visitorId: this.state.visitorId,
339
+ anonymousId: this.state.anonymousId,
268
340
  sessionId: this.state.sessionId,
269
341
  currentUserId: this.state.currentUserId,
270
342
  queueStats: this.eventQueue.getStats(),
271
343
  attribution: attributionManager.getAttributionSummary(),
272
344
  };
273
345
  }
346
+ /**
347
+ * Get the persistent anonymous ID
348
+ */
349
+ getAnonymousId() {
350
+ return this.state.anonymousId;
351
+ }
274
352
  /**
275
353
  * Get detailed attribution data
276
354
  */
@@ -383,11 +461,14 @@ export class DatalyrSDK {
383
461
  const payload = {
384
462
  workspaceId: this.state.config.workspaceId || 'mobile_sdk',
385
463
  visitorId: this.state.visitorId,
464
+ anonymousId: this.state.anonymousId, // Include persistent anonymous ID
386
465
  sessionId: this.state.sessionId,
387
466
  eventId: generateUUID(),
388
467
  eventName,
389
468
  eventData: {
390
469
  ...eventData,
470
+ // Include anonymous_id in event data for attribution
471
+ anonymous_id: this.state.anonymousId,
391
472
  // Auto-captured mobile data
392
473
  platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
393
474
  os_version: deviceInfo.osVersion,
@@ -578,6 +659,9 @@ export class Datalyr {
578
659
  static getStatus() {
579
660
  return datalyr.getStatus();
580
661
  }
662
+ static getAnonymousId() {
663
+ return datalyr.getAnonymousId();
664
+ }
581
665
  static getAttributionData() {
582
666
  return datalyr.getAttributionData();
583
667
  }
@@ -167,6 +167,7 @@ export class HttpClient {
167
167
  const testPayload = {
168
168
  workspaceId: 'test',
169
169
  visitorId: 'test',
170
+ anonymousId: 'test',
170
171
  sessionId: 'test',
171
172
  eventId: 'test',
172
173
  eventName: 'connection_test',
package/lib/types.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface DatalyrConfig {
22
22
  respectDoNotTrack?: boolean;
23
23
  enableAutoEvents?: boolean;
24
24
  enableAttribution?: boolean;
25
+ enableWebToAppAttribution?: boolean;
25
26
  autoEvents?: AutoEventConfig;
26
27
  autoEventConfig?: AutoEventConfig;
27
28
  retryConfig?: {
@@ -59,6 +60,7 @@ export interface FingerprintData {
59
60
  export interface EventPayload {
60
61
  workspaceId: string;
61
62
  visitorId: string;
63
+ anonymousId: string;
62
64
  sessionId: string;
63
65
  eventId: string;
64
66
  eventName: string;
@@ -81,6 +83,7 @@ export interface SDKState {
81
83
  initialized: boolean;
82
84
  config: DatalyrConfig;
83
85
  visitorId: string;
86
+ anonymousId: string;
84
87
  sessionId: string;
85
88
  currentUserId?: string;
86
89
  userProperties: UserProperties;
package/lib/utils.d.ts CHANGED
@@ -2,6 +2,7 @@ import 'react-native-get-random-values';
2
2
  import { DeviceInfo as DeviceInfoType, FingerprintData } from './types';
3
3
  export declare const STORAGE_KEYS: {
4
4
  VISITOR_ID: string;
5
+ ANONYMOUS_ID: string;
5
6
  SESSION_ID: string;
6
7
  USER_ID: string;
7
8
  USER_PROPERTIES: string;
@@ -25,6 +26,11 @@ export declare const hashString: (str: string) => string;
25
26
  * Get or create a persistent visitor ID
26
27
  */
27
28
  export declare const getOrCreateVisitorId: () => Promise<string>;
29
+ /**
30
+ * Get or create a persistent anonymous ID
31
+ * This ID persists across app reinstalls and never changes
32
+ */
33
+ export declare const getOrCreateAnonymousId: () => Promise<string>;
28
34
  /**
29
35
  * Get or create a session ID (with session timeout logic)
30
36
  */
package/lib/utils.js CHANGED
@@ -13,6 +13,7 @@ import 'react-native-get-random-values'; // Required for uuid
13
13
  // Storage Keys
14
14
  export const STORAGE_KEYS = {
15
15
  VISITOR_ID: '@datalyr/visitor_id',
16
+ ANONYMOUS_ID: '@datalyr/anonymous_id', // Persistent anonymous identifier
16
17
  SESSION_ID: '@datalyr/session_id',
17
18
  USER_ID: '@datalyr/user_id',
18
19
  USER_PROPERTIES: '@datalyr/user_properties',
@@ -63,6 +64,25 @@ export const getOrCreateVisitorId = async () => {
63
64
  return generateUUID(); // Fallback to memory-only ID
64
65
  }
65
66
  };
67
+ /**
68
+ * Get or create a persistent anonymous ID
69
+ * This ID persists across app reinstalls and never changes
70
+ */
71
+ export const getOrCreateAnonymousId = async () => {
72
+ try {
73
+ let anonymousId = await AsyncStorage.getItem(STORAGE_KEYS.ANONYMOUS_ID);
74
+ if (!anonymousId) {
75
+ // Generate anonymous_id with anon_ prefix to match web SDK
76
+ anonymousId = `anon_${generateUUID()}`;
77
+ await AsyncStorage.setItem(STORAGE_KEYS.ANONYMOUS_ID, anonymousId);
78
+ }
79
+ return anonymousId;
80
+ }
81
+ catch (error) {
82
+ console.warn('Failed to get/create anonymous ID:', error);
83
+ return `anon_${generateUUID()}`; // Fallback to memory-only ID
84
+ }
85
+ };
66
86
  /**
67
87
  * Get or create a session ID (with session timeout logic)
68
88
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalyr/react-native",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -58,6 +58,7 @@
58
58
  "devDependencies": {
59
59
  "@types/jest": "^30.0.0",
60
60
  "@types/react": "^18.2.0",
61
+ "@types/react-native": "^0.72.8",
61
62
  "@types/uuid": "^10.0.0",
62
63
  "@typescript-eslint/eslint-plugin": "^8.0.0",
63
64
  "@typescript-eslint/parser": "^8.0.0",
@@ -441,6 +441,51 @@ export class AttributionManager {
441
441
  return this.attributionData;
442
442
  }
443
443
 
444
+ /**
445
+ * Merge web attribution data into mobile session
446
+ * Called when web-to-app attribution is resolved via email
447
+ */
448
+ mergeWebAttribution(webAttribution: any): void {
449
+ debugLog('Merging web attribution data:', webAttribution);
450
+
451
+ // Only merge if we don't already have attribution data
452
+ // Web attribution takes precedence for first-touch
453
+ if (!this.attributionData.fbclid && webAttribution.fbclid) {
454
+ this.attributionData.fbclid = webAttribution.fbclid;
455
+ }
456
+ if (!this.attributionData.gclid && webAttribution.gclid) {
457
+ this.attributionData.gclid = webAttribution.gclid;
458
+ }
459
+ if (!this.attributionData.ttclid && webAttribution.ttclid) {
460
+ this.attributionData.ttclid = webAttribution.ttclid;
461
+ }
462
+
463
+ // Merge UTM parameters
464
+ if (!this.attributionData.utm_source && webAttribution.utm_source) {
465
+ this.attributionData.utm_source = webAttribution.utm_source;
466
+ }
467
+ if (!this.attributionData.utm_medium && webAttribution.utm_medium) {
468
+ this.attributionData.utm_medium = webAttribution.utm_medium;
469
+ }
470
+ if (!this.attributionData.utm_campaign && webAttribution.utm_campaign) {
471
+ this.attributionData.utm_campaign = webAttribution.utm_campaign;
472
+ }
473
+ if (!this.attributionData.utm_content && webAttribution.utm_content) {
474
+ this.attributionData.utm_content = webAttribution.utm_content;
475
+ }
476
+ if (!this.attributionData.utm_term && webAttribution.utm_term) {
477
+ this.attributionData.utm_term = webAttribution.utm_term;
478
+ }
479
+
480
+ // Store web visitor ID for cross-device tracking
481
+ this.attributionData.web_visitor_id = webAttribution.visitor_id;
482
+
483
+ // Save merged attribution data
484
+ this.saveAttributionData();
485
+
486
+ debugLog('Web attribution merged successfully');
487
+ }
488
+
444
489
  /**
445
490
  * Set custom attribution data
446
491
  */
@@ -10,6 +10,7 @@ import {
10
10
  } from './types';
11
11
  import {
12
12
  getOrCreateVisitorId,
13
+ getOrCreateAnonymousId,
13
14
  getOrCreateSessionId,
14
15
  createFingerprintData,
15
16
  generateUUID,
@@ -56,6 +57,7 @@ export class DatalyrSDK {
56
57
  respectDoNotTrack: true,
57
58
  },
58
59
  visitorId: '',
60
+ anonymousId: '', // Persistent anonymous identifier
59
61
  sessionId: '',
60
62
  userProperties: {},
61
63
  eventQueue: [],
@@ -106,8 +108,9 @@ export class DatalyrSDK {
106
108
  maxRetryCount: this.state.config.maxRetries || 3,
107
109
  });
108
110
 
109
- // Initialize visitor ID and session
111
+ // Initialize visitor ID, anonymous ID and session
110
112
  this.state.visitorId = await getOrCreateVisitorId();
113
+ this.state.anonymousId = await getOrCreateAnonymousId();
111
114
  this.state.sessionId = await getOrCreateSessionId();
112
115
 
113
116
  // Load persisted user data
@@ -174,6 +177,7 @@ export class DatalyrSDK {
174
177
  debugLog('Datalyr SDK initialized successfully', {
175
178
  workspaceId: this.state.config.workspaceId,
176
179
  visitorId: this.state.visitorId,
180
+ anonymousId: this.state.anonymousId,
177
181
  sessionId: this.state.sessionId,
178
182
  });
179
183
 
@@ -244,24 +248,101 @@ export class DatalyrSDK {
244
248
 
245
249
  // Update current user ID
246
250
  this.state.currentUserId = userId;
247
-
251
+
248
252
  // Merge user properties
249
253
  this.state.userProperties = { ...this.state.userProperties, ...properties };
250
254
 
251
255
  // Persist user data
252
256
  await this.persistUserData();
253
257
 
254
- // Track identify event
255
- await this.track('identify', {
256
- userId,
257
- ...properties
258
+ // Track $identify event for identity resolution
259
+ await this.track('$identify', {
260
+ userId,
261
+ anonymous_id: this.state.anonymousId,
262
+ ...properties
258
263
  });
259
264
 
265
+ // Fetch and merge web attribution if email is provided
266
+ if (this.state.config.enableWebToAppAttribution !== false) {
267
+ const email = properties?.email || (typeof userId === 'string' && userId.includes('@') ? userId : null);
268
+ if (email) {
269
+ await this.fetchAndMergeWebAttribution(email);
270
+ }
271
+ }
272
+
260
273
  } catch (error) {
261
274
  errorLog('Error identifying user:', error as Error);
262
275
  }
263
276
  }
264
277
 
278
+ /**
279
+ * Fetch web attribution data for user and merge into mobile session
280
+ * Called automatically during identify() if email is provided
281
+ */
282
+ private async fetchAndMergeWebAttribution(email: string): Promise<void> {
283
+ try {
284
+ debugLog('Fetching web attribution for email:', email);
285
+
286
+ // Call API endpoint to get web attribution
287
+ const response = await fetch('https://api.datalyr.com/attribution/lookup', {
288
+ method: 'POST',
289
+ headers: {
290
+ 'Content-Type': 'application/json',
291
+ 'X-Datalyr-API-Key': this.state.config.apiKey!,
292
+ },
293
+ body: JSON.stringify({ email }),
294
+ });
295
+
296
+ if (!response.ok) {
297
+ debugLog('Failed to fetch web attribution:', response.status);
298
+ return;
299
+ }
300
+
301
+ const result = await response.json() as { found: boolean; attribution?: any };
302
+
303
+ if (!result.found || !result.attribution) {
304
+ debugLog('No web attribution found for user');
305
+ return;
306
+ }
307
+
308
+ const webAttribution = result.attribution;
309
+ debugLog('Web attribution found:', {
310
+ visitor_id: webAttribution.visitor_id,
311
+ has_fbclid: !!webAttribution.fbclid,
312
+ has_gclid: !!webAttribution.gclid,
313
+ utm_source: webAttribution.utm_source,
314
+ });
315
+
316
+ // Merge web attribution into current session
317
+ await this.track('$web_attribution_merged', {
318
+ web_visitor_id: webAttribution.visitor_id,
319
+ web_user_id: webAttribution.user_id,
320
+ fbclid: webAttribution.fbclid,
321
+ gclid: webAttribution.gclid,
322
+ ttclid: webAttribution.ttclid,
323
+ gbraid: webAttribution.gbraid,
324
+ wbraid: webAttribution.wbraid,
325
+ fbp: webAttribution.fbp,
326
+ fbc: webAttribution.fbc,
327
+ utm_source: webAttribution.utm_source,
328
+ utm_medium: webAttribution.utm_medium,
329
+ utm_campaign: webAttribution.utm_campaign,
330
+ utm_content: webAttribution.utm_content,
331
+ utm_term: webAttribution.utm_term,
332
+ web_timestamp: webAttribution.timestamp,
333
+ });
334
+
335
+ // Update attribution manager with web data
336
+ attributionManager.mergeWebAttribution(webAttribution);
337
+
338
+ debugLog('Successfully merged web attribution into mobile session');
339
+
340
+ } catch (error) {
341
+ errorLog('Error fetching web attribution:', error as Error);
342
+ // Non-blocking - continue even if attribution fetch fails
343
+ }
344
+ }
345
+
265
346
  /**
266
347
  * Alias a user (connect anonymous user to known user)
267
348
  */
@@ -276,6 +357,7 @@ export class DatalyrSDK {
276
357
  newUserId,
277
358
  previousId: previousId || this.state.visitorId,
278
359
  visitorId: this.state.visitorId,
360
+ anonymousId: this.state.anonymousId, // Include for identity resolution
279
361
  };
280
362
 
281
363
  debugLog('Aliasing user:', aliasData);
@@ -335,6 +417,7 @@ export class DatalyrSDK {
335
417
  initialized: boolean;
336
418
  workspaceId: string;
337
419
  visitorId: string;
420
+ anonymousId: string;
338
421
  sessionId: string;
339
422
  currentUserId?: string;
340
423
  queueStats: any;
@@ -344,6 +427,7 @@ export class DatalyrSDK {
344
427
  initialized: this.state.initialized,
345
428
  workspaceId: this.state.config.workspaceId || '',
346
429
  visitorId: this.state.visitorId,
430
+ anonymousId: this.state.anonymousId,
347
431
  sessionId: this.state.sessionId,
348
432
  currentUserId: this.state.currentUserId,
349
433
  queueStats: this.eventQueue.getStats(),
@@ -351,6 +435,13 @@ export class DatalyrSDK {
351
435
  };
352
436
  }
353
437
 
438
+ /**
439
+ * Get the persistent anonymous ID
440
+ */
441
+ getAnonymousId(): string {
442
+ return this.state.anonymousId;
443
+ }
444
+
354
445
  /**
355
446
  * Get detailed attribution data
356
447
  */
@@ -489,11 +580,14 @@ export class DatalyrSDK {
489
580
  const payload: EventPayload = {
490
581
  workspaceId: this.state.config.workspaceId || 'mobile_sdk',
491
582
  visitorId: this.state.visitorId,
583
+ anonymousId: this.state.anonymousId, // Include persistent anonymous ID
492
584
  sessionId: this.state.sessionId,
493
585
  eventId: generateUUID(),
494
586
  eventName,
495
587
  eventData: {
496
588
  ...eventData,
589
+ // Include anonymous_id in event data for attribution
590
+ anonymous_id: this.state.anonymousId,
497
591
  // Auto-captured mobile data
498
592
  platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
499
593
  os_version: deviceInfo.osVersion,
@@ -721,6 +815,10 @@ export class Datalyr {
721
815
  return datalyr.getStatus();
722
816
  }
723
817
 
818
+ static getAnonymousId(): string {
819
+ return datalyr.getAnonymousId();
820
+ }
821
+
724
822
  static getAttributionData(): AttributionData {
725
823
  return datalyr.getAttributionData();
726
824
  }
@@ -213,6 +213,7 @@ export class HttpClient {
213
213
  const testPayload: EventPayload = {
214
214
  workspaceId: 'test',
215
215
  visitorId: 'test',
216
+ anonymousId: 'test',
216
217
  sessionId: 'test',
217
218
  eventId: 'test',
218
219
  eventName: 'connection_test',
package/src/types.ts CHANGED
@@ -25,6 +25,7 @@ export interface DatalyrConfig {
25
25
  respectDoNotTrack?: boolean;
26
26
  enableAutoEvents?: boolean;
27
27
  enableAttribution?: boolean;
28
+ enableWebToAppAttribution?: boolean;
28
29
  autoEvents?: AutoEventConfig;
29
30
  autoEventConfig?: AutoEventConfig;
30
31
  retryConfig?: {
@@ -33,7 +34,6 @@ export interface DatalyrConfig {
33
34
  };
34
35
  skadTemplate?: 'ecommerce' | 'gaming' | 'subscription';
35
36
  }
36
-
37
37
  // Event Types
38
38
  export interface EventData {
39
39
  [key: string]: any;
@@ -66,6 +66,7 @@ export interface FingerprintData {
66
66
  export interface EventPayload {
67
67
  workspaceId: string;
68
68
  visitorId: string;
69
+ anonymousId: string; // Persistent anonymous identifier
69
70
  sessionId: string;
70
71
  eventId: string;
71
72
  eventName: string;
@@ -92,6 +93,7 @@ export interface SDKState {
92
93
  initialized: boolean;
93
94
  config: DatalyrConfig;
94
95
  visitorId: string;
96
+ anonymousId: string; // Persistent anonymous identifier
95
97
  sessionId: string;
96
98
  currentUserId?: string;
97
99
  userProperties: UserProperties;
package/src/utils.ts CHANGED
@@ -16,6 +16,7 @@ import { DeviceInfo as DeviceInfoType, FingerprintData } from './types';
16
16
  // Storage Keys
17
17
  export const STORAGE_KEYS = {
18
18
  VISITOR_ID: '@datalyr/visitor_id',
19
+ ANONYMOUS_ID: '@datalyr/anonymous_id', // Persistent anonymous identifier
19
20
  SESSION_ID: '@datalyr/session_id',
20
21
  USER_ID: '@datalyr/user_id',
21
22
  USER_PROPERTIES: '@datalyr/user_properties',
@@ -71,6 +72,25 @@ export const getOrCreateVisitorId = async (): Promise<string> => {
71
72
  }
72
73
  };
73
74
 
75
+ /**
76
+ * Get or create a persistent anonymous ID
77
+ * This ID persists across app reinstalls and never changes
78
+ */
79
+ export const getOrCreateAnonymousId = async (): Promise<string> => {
80
+ try {
81
+ let anonymousId = await AsyncStorage.getItem(STORAGE_KEYS.ANONYMOUS_ID);
82
+ if (!anonymousId) {
83
+ // Generate anonymous_id with anon_ prefix to match web SDK
84
+ anonymousId = `anon_${generateUUID()}`;
85
+ await AsyncStorage.setItem(STORAGE_KEYS.ANONYMOUS_ID, anonymousId);
86
+ }
87
+ return anonymousId;
88
+ } catch (error) {
89
+ console.warn('Failed to get/create anonymous ID:', error);
90
+ return `anon_${generateUUID()}`; // Fallback to memory-only ID
91
+ }
92
+ };
93
+
74
94
  /**
75
95
  * Get or create a session ID (with session timeout logic)
76
96
  */