@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 +30 -0
- package/lib/attribution.d.ts +5 -0
- package/lib/attribution.js +39 -0
- package/lib/datalyr-sdk.d.ts +12 -0
- package/lib/datalyr-sdk.js +88 -4
- package/lib/http-client.js +1 -0
- package/lib/types.d.ts +3 -0
- package/lib/utils.d.ts +6 -0
- package/lib/utils.js +20 -0
- package/package.json +2 -1
- package/src/attribution.ts +45 -0
- package/src/datalyr-sdk.ts +104 -6
- package/src/http-client.ts +1 -0
- package/src/types.ts +3 -1
- package/src/utils.ts +20 -0
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.
|
package/lib/attribution.d.ts
CHANGED
|
@@ -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
|
*/
|
package/lib/attribution.js
CHANGED
|
@@ -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
|
*/
|
package/lib/datalyr-sdk.d.ts
CHANGED
|
@@ -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;
|
package/lib/datalyr-sdk.js
CHANGED
|
@@ -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
|
}
|
package/lib/http-client.js
CHANGED
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.
|
|
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",
|
package/src/attribution.ts
CHANGED
|
@@ -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
|
*/
|
package/src/datalyr-sdk.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/http-client.ts
CHANGED
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
|
*/
|