@datalyr/react-native 1.4.8 → 1.5.0
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 +37 -2
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +44 -0
- package/ios/DatalyrNativeModule.swift +40 -0
- package/lib/datalyr-sdk.d.ts +8 -0
- package/lib/datalyr-sdk.js +114 -2
- package/lib/event-queue.js +1 -1
- package/lib/http-client.js +3 -3
- package/lib/integrations/tiktok-integration.js +4 -1
- package/lib/native/DatalyrNativeBridge.d.ts +15 -0
- package/lib/native/DatalyrNativeBridge.js +18 -0
- package/lib/native/index.d.ts +2 -1
- package/lib/native/index.js +1 -1
- package/lib/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/datalyr-sdk-expo.ts +61 -7
- package/src/datalyr-sdk.ts +127 -3
- package/src/event-queue.ts +1 -1
- package/src/http-client.ts +3 -3
- package/src/integrations/tiktok-integration.ts +3 -1
- package/src/native/DatalyrNativeBridge.ts +37 -0
- package/src/native/index.ts +2 -0
- package/src/types.ts +2 -2
- package/src/utils-expo.ts +55 -19
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Mobile analytics and attribution SDK for React Native and Expo. Track events, id
|
|
|
19
19
|
- [Attribution](#attribution)
|
|
20
20
|
- [Automatic Capture](#automatic-capture)
|
|
21
21
|
- [Deferred Deep Links](#deferred-deep-links)
|
|
22
|
+
- [Web-to-App Attribution](#web-to-app-attribution)
|
|
22
23
|
- [Event Queue](#event-queue)
|
|
23
24
|
- [Auto Events](#auto-events)
|
|
24
25
|
- [SKAdNetwork](#skadnetwork)
|
|
@@ -356,6 +357,23 @@ if (deferred) {
|
|
|
356
357
|
}
|
|
357
358
|
```
|
|
358
359
|
|
|
360
|
+
### Web-to-App Attribution
|
|
361
|
+
|
|
362
|
+
Automatically recover attribution from a web prelander when users install the app from an ad.
|
|
363
|
+
|
|
364
|
+
**How it works:**
|
|
365
|
+
- **Android**: Attribution params are passed through the Play Store `referrer` URL parameter (set by the web SDK's `trackAppDownloadClick()`). The mobile SDK reads these via the Play Install Referrer API — deterministic, ~95% accuracy.
|
|
366
|
+
- **iOS**: On first install, the SDK calls the Datalyr API to match the device's IP against recent `$app_download_click` web events within 24 hours — ~90%+ accuracy for immediate installs.
|
|
367
|
+
|
|
368
|
+
No additional mobile code is needed. Attribution is recovered automatically during `initialize()` on first install, before the `app_install` event fires.
|
|
369
|
+
|
|
370
|
+
After a match, the SDK:
|
|
371
|
+
1. Merges web attribution (click IDs, UTMs, cookies) into the mobile session
|
|
372
|
+
2. Tracks a `$web_attribution_matched` event for analytics
|
|
373
|
+
3. All subsequent events (including purchases) carry the matched attribution
|
|
374
|
+
|
|
375
|
+
**Fallback:** If IP matching misses (e.g., VPN toggle during install), email-based attribution is still recovered when `identify()` is called with the user's email.
|
|
376
|
+
|
|
359
377
|
### Manual Attribution
|
|
360
378
|
|
|
361
379
|
Set attribution programmatically:
|
|
@@ -478,13 +496,25 @@ await Datalyr.initialize({
|
|
|
478
496
|
await Datalyr.initialize({
|
|
479
497
|
apiKey: 'dk_your_api_key',
|
|
480
498
|
tiktok: {
|
|
481
|
-
appId: 'your_app_id',
|
|
482
|
-
tiktokAppId: '7123456789',
|
|
499
|
+
appId: 'your_app_id', // Events API App ID
|
|
500
|
+
tiktokAppId: '7123456789', // TikTok App ID (Developer Portal)
|
|
501
|
+
accessToken: 'your_access_token', // Events API Access Token
|
|
483
502
|
enableAppEvents: true,
|
|
484
503
|
},
|
|
485
504
|
});
|
|
486
505
|
```
|
|
487
506
|
|
|
507
|
+
**Where to find your TikTok credentials:**
|
|
508
|
+
|
|
509
|
+
| Credential | Where to get it |
|
|
510
|
+
|------------|----------------|
|
|
511
|
+
| `tiktokAppId` | [TikTok Developer Portal](https://developers.tiktok.com) → Your App → App ID |
|
|
512
|
+
| `appId` | TikTok Business Center → Assets → Events → Your App → App ID |
|
|
513
|
+
| `accessToken` | TikTok Business Center → Assets → Events → Your App → Settings → Access Token |
|
|
514
|
+
|
|
515
|
+
> **Note:** The `accessToken` enables client-side TikTok SDK features (enhanced attribution, real-time event forwarding). Without it, events are still tracked server-side via Datalyr postbacks — you'll see a warning in debug mode.
|
|
516
|
+
```
|
|
517
|
+
|
|
488
518
|
### Apple Search Ads
|
|
489
519
|
|
|
490
520
|
Attribution for users who install from Apple Search Ads (iOS 14.3+). Automatically fetched on initialization.
|
|
@@ -667,12 +697,17 @@ Check status: `Datalyr.getPlatformIntegrationStatus()`
|
|
|
667
697
|
|
|
668
698
|
### TikTok SDK Not Working
|
|
669
699
|
|
|
700
|
+
1. Make sure you have all three TikTok credentials (see [TikTok setup](#tiktok))
|
|
701
|
+
2. The `accessToken` is required for client-side SDK — without it, you'll see a warning but server-side tracking still works
|
|
702
|
+
3. Check status: `Datalyr.getPlatformIntegrationStatus()`
|
|
703
|
+
|
|
670
704
|
```typescript
|
|
671
705
|
await Datalyr.initialize({
|
|
672
706
|
apiKey: 'dk_your_api_key',
|
|
673
707
|
tiktok: {
|
|
674
708
|
appId: 'your_app_id',
|
|
675
709
|
tiktokAppId: '7123456789012345',
|
|
710
|
+
accessToken: 'your_access_token',
|
|
676
711
|
},
|
|
677
712
|
});
|
|
678
713
|
```
|
package/android/build.gradle
CHANGED
|
@@ -26,6 +26,9 @@ import com.tiktok.TikTokBusinessSdk.TTConfig;
|
|
|
26
26
|
import com.tiktok.appevents.TikTokAppEvent;
|
|
27
27
|
import com.tiktok.appevents.TikTokAppEventLogger;
|
|
28
28
|
|
|
29
|
+
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
|
|
30
|
+
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
|
|
31
|
+
|
|
29
32
|
import java.math.BigDecimal;
|
|
30
33
|
import java.util.Currency;
|
|
31
34
|
import java.util.HashMap;
|
|
@@ -391,6 +394,47 @@ public class DatalyrNativeModule extends ReactContextBaseJavaModule {
|
|
|
391
394
|
promise.resolve(result);
|
|
392
395
|
}
|
|
393
396
|
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Advertiser Info (GAID on Android)
|
|
399
|
+
// ============================================================================
|
|
400
|
+
|
|
401
|
+
@ReactMethod
|
|
402
|
+
public void getAdvertiserInfo(Promise promise) {
|
|
403
|
+
// GAID must be fetched on a background thread
|
|
404
|
+
new Thread(() -> {
|
|
405
|
+
try {
|
|
406
|
+
WritableMap result = Arguments.createMap();
|
|
407
|
+
|
|
408
|
+
// Fetch Google Advertising ID
|
|
409
|
+
try {
|
|
410
|
+
AdvertisingIdClient.Info adInfo = AdvertisingIdClient.getAdvertisingIdInfo(reactContext.getApplicationContext());
|
|
411
|
+
boolean limitAdTracking = adInfo.isLimitAdTrackingEnabled();
|
|
412
|
+
result.putBoolean("advertiser_tracking_enabled", !limitAdTracking);
|
|
413
|
+
result.putInt("att_status", limitAdTracking ? 2 : 3); // 2=denied, 3=authorized
|
|
414
|
+
|
|
415
|
+
if (!limitAdTracking && adInfo.getId() != null) {
|
|
416
|
+
result.putString("gaid", adInfo.getId());
|
|
417
|
+
}
|
|
418
|
+
} catch (GooglePlayServicesNotAvailableException e) {
|
|
419
|
+
// Google Play Services not available (e.g., Huawei devices)
|
|
420
|
+
result.putInt("att_status", 3);
|
|
421
|
+
result.putBoolean("advertiser_tracking_enabled", true);
|
|
422
|
+
Log.d(TAG, "Google Play Services not available for GAID");
|
|
423
|
+
} catch (Exception e) {
|
|
424
|
+
// Fallback — GAID not available but not blocking
|
|
425
|
+
result.putInt("att_status", 3);
|
|
426
|
+
result.putBoolean("advertiser_tracking_enabled", true);
|
|
427
|
+
Log.d(TAG, "GAID not available: " + e.getMessage());
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
promise.resolve(result);
|
|
431
|
+
} catch (Exception e) {
|
|
432
|
+
Log.e(TAG, "Failed to get advertiser info: " + e.getMessage());
|
|
433
|
+
promise.resolve(null);
|
|
434
|
+
}
|
|
435
|
+
}).start();
|
|
436
|
+
}
|
|
437
|
+
|
|
394
438
|
// ============================================================================
|
|
395
439
|
// Helper Methods
|
|
396
440
|
// ============================================================================
|
|
@@ -2,6 +2,8 @@ import ExpoModulesCore
|
|
|
2
2
|
import FBSDKCoreKit
|
|
3
3
|
import TikTokBusinessSDK
|
|
4
4
|
import AdServices
|
|
5
|
+
import AppTrackingTransparency
|
|
6
|
+
import AdSupport
|
|
5
7
|
|
|
6
8
|
public class DatalyrNativeModule: Module {
|
|
7
9
|
private var tiktokInitialized = false
|
|
@@ -280,6 +282,44 @@ public class DatalyrNativeModule: Module {
|
|
|
280
282
|
])
|
|
281
283
|
}
|
|
282
284
|
|
|
285
|
+
// MARK: - Advertiser Info (IDFA, IDFV, ATT Status)
|
|
286
|
+
|
|
287
|
+
AsyncFunction("getAdvertiserInfo") { (promise: Promise) in
|
|
288
|
+
var result: [String: Any] = [:]
|
|
289
|
+
|
|
290
|
+
// IDFV is always available
|
|
291
|
+
if let idfv = UIDevice.current.identifierForVendor?.uuidString {
|
|
292
|
+
result["idfv"] = idfv
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ATT status
|
|
296
|
+
if #available(iOS 14, *) {
|
|
297
|
+
let status = ATTrackingManager.trackingAuthorizationStatus
|
|
298
|
+
result["att_status"] = status.rawValue
|
|
299
|
+
result["advertiser_tracking_enabled"] = status == .authorized
|
|
300
|
+
|
|
301
|
+
// IDFA only if ATT authorized
|
|
302
|
+
if status == .authorized {
|
|
303
|
+
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
|
|
304
|
+
let zeroUUID = "00000000-0000-0000-0000-000000000000"
|
|
305
|
+
if idfa != zeroUUID {
|
|
306
|
+
result["idfa"] = idfa
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// Pre-iOS 14, tracking allowed by default
|
|
311
|
+
result["att_status"] = 3 // .authorized equivalent
|
|
312
|
+
result["advertiser_tracking_enabled"] = true
|
|
313
|
+
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
|
|
314
|
+
let zeroUUID = "00000000-0000-0000-0000-000000000000"
|
|
315
|
+
if idfa != zeroUUID {
|
|
316
|
+
result["idfa"] = idfa
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
promise.resolve(result)
|
|
321
|
+
}
|
|
322
|
+
|
|
283
323
|
// MARK: - Apple Search Ads Attribution
|
|
284
324
|
|
|
285
325
|
AsyncFunction("getAppleSearchAdsAttribution") { (promise: Promise) in
|
package/lib/datalyr-sdk.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class DatalyrSDK {
|
|
|
9
9
|
private autoEventsManager;
|
|
10
10
|
private appStateSubscription;
|
|
11
11
|
private networkStatusUnsubscribe;
|
|
12
|
+
private cachedAdvertiserInfo;
|
|
12
13
|
private static conversionEncoder?;
|
|
13
14
|
private static debugEnabled;
|
|
14
15
|
constructor();
|
|
@@ -33,6 +34,13 @@ export declare class DatalyrSDK {
|
|
|
33
34
|
* Called automatically during identify() if email is provided
|
|
34
35
|
*/
|
|
35
36
|
private fetchAndMergeWebAttribution;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch deferred web attribution on first app install.
|
|
39
|
+
* Uses IP-based matching (iOS) or Play Store referrer (Android) to recover
|
|
40
|
+
* attribution data (fbclid, utm_*, etc.) from a prelander web visit.
|
|
41
|
+
* Called automatically during initialize() when a fresh install is detected.
|
|
42
|
+
*/
|
|
43
|
+
private fetchDeferredWebAttribution;
|
|
36
44
|
/**
|
|
37
45
|
* Alias a user (connect anonymous user to known user)
|
|
38
46
|
*/
|
package/lib/datalyr-sdk.js
CHANGED
|
@@ -8,12 +8,14 @@ import { AutoEventsManager } from './auto-events';
|
|
|
8
8
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
9
9
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
10
10
|
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
11
|
+
import { AdvertiserInfoBridge } from './native/DatalyrNativeBridge';
|
|
11
12
|
import { networkStatusManager } from './network-status';
|
|
12
13
|
export class DatalyrSDK {
|
|
13
14
|
constructor() {
|
|
14
15
|
this.autoEventsManager = null;
|
|
15
16
|
this.appStateSubscription = null;
|
|
16
17
|
this.networkStatusUnsubscribe = null;
|
|
18
|
+
this.cachedAdvertiserInfo = null;
|
|
17
19
|
// Initialize state with defaults
|
|
18
20
|
this.state = {
|
|
19
21
|
initialized: false,
|
|
@@ -26,9 +28,11 @@ export class DatalyrSDK {
|
|
|
26
28
|
maxRetries: 3,
|
|
27
29
|
retryDelay: 1000,
|
|
28
30
|
batchSize: 10,
|
|
29
|
-
flushInterval:
|
|
31
|
+
flushInterval: 30000,
|
|
30
32
|
maxQueueSize: 100,
|
|
31
33
|
respectDoNotTrack: true,
|
|
34
|
+
enableAutoEvents: true,
|
|
35
|
+
enableAttribution: true,
|
|
32
36
|
},
|
|
33
37
|
visitorId: '',
|
|
34
38
|
anonymousId: '', // Persistent anonymous identifier
|
|
@@ -166,6 +170,13 @@ export class DatalyrSDK {
|
|
|
166
170
|
}
|
|
167
171
|
// Wait for all platform integrations to complete
|
|
168
172
|
await Promise.all(platformInitPromises);
|
|
173
|
+
// Cache advertiser info (IDFA/GAID, ATT status) once at init to avoid per-event native bridge calls
|
|
174
|
+
try {
|
|
175
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
errorLog('Failed to cache advertiser info:', error);
|
|
179
|
+
}
|
|
169
180
|
debugLog('Platform integrations initialized', {
|
|
170
181
|
meta: metaIntegration.isAvailable(),
|
|
171
182
|
tiktok: tiktokIntegration.isAvailable(),
|
|
@@ -176,10 +187,15 @@ export class DatalyrSDK {
|
|
|
176
187
|
this.state.initialized = true;
|
|
177
188
|
// Check for app install (after SDK is marked as initialized)
|
|
178
189
|
if (attributionManager.isInstall()) {
|
|
190
|
+
// iOS: Attempt deferred web-to-app attribution via IP matching before tracking install
|
|
191
|
+
// Android: Play Store referrer is handled by playInstallReferrerIntegration
|
|
192
|
+
if (Platform.OS === 'ios') {
|
|
193
|
+
await this.fetchDeferredWebAttribution();
|
|
194
|
+
}
|
|
179
195
|
const installData = await attributionManager.trackInstall();
|
|
180
196
|
await this.track('app_install', {
|
|
181
197
|
platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
|
|
182
|
-
sdk_version: '1.
|
|
198
|
+
sdk_version: '1.4.9',
|
|
183
199
|
...installData,
|
|
184
200
|
});
|
|
185
201
|
}
|
|
@@ -357,6 +373,77 @@ export class DatalyrSDK {
|
|
|
357
373
|
// Non-blocking - continue even if attribution fetch fails
|
|
358
374
|
}
|
|
359
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Fetch deferred web attribution on first app install.
|
|
378
|
+
* Uses IP-based matching (iOS) or Play Store referrer (Android) to recover
|
|
379
|
+
* attribution data (fbclid, utm_*, etc.) from a prelander web visit.
|
|
380
|
+
* Called automatically during initialize() when a fresh install is detected.
|
|
381
|
+
*/
|
|
382
|
+
async fetchDeferredWebAttribution() {
|
|
383
|
+
var _a;
|
|
384
|
+
if (!((_a = this.state.config) === null || _a === void 0 ? void 0 : _a.apiKey)) {
|
|
385
|
+
debugLog('API key not available for deferred attribution fetch');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
debugLog('Fetching deferred web attribution via IP matching...');
|
|
390
|
+
const baseUrl = this.state.config.endpoint || 'https://api.datalyr.com';
|
|
391
|
+
const controller = new AbortController();
|
|
392
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
393
|
+
const response = await fetch(`${baseUrl}/attribution/deferred-lookup`, {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
headers: {
|
|
396
|
+
'Content-Type': 'application/json',
|
|
397
|
+
'X-Datalyr-API-Key': this.state.config.apiKey,
|
|
398
|
+
},
|
|
399
|
+
body: JSON.stringify({ platform: Platform.OS }),
|
|
400
|
+
signal: controller.signal,
|
|
401
|
+
});
|
|
402
|
+
clearTimeout(timeout);
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
debugLog('Deferred attribution lookup failed:', response.status);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const result = await response.json();
|
|
408
|
+
if (!result.found || !result.attribution) {
|
|
409
|
+
debugLog('No deferred web attribution found for this IP');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const webAttribution = result.attribution;
|
|
413
|
+
debugLog('Deferred web attribution found:', {
|
|
414
|
+
visitor_id: webAttribution.visitor_id,
|
|
415
|
+
has_fbclid: !!webAttribution.fbclid,
|
|
416
|
+
has_gclid: !!webAttribution.gclid,
|
|
417
|
+
utm_source: webAttribution.utm_source,
|
|
418
|
+
});
|
|
419
|
+
// Merge web attribution into current session
|
|
420
|
+
attributionManager.mergeWebAttribution(webAttribution);
|
|
421
|
+
// Track match event for analytics
|
|
422
|
+
await this.track('$web_attribution_matched', {
|
|
423
|
+
web_visitor_id: webAttribution.visitor_id,
|
|
424
|
+
web_user_id: webAttribution.user_id,
|
|
425
|
+
fbclid: webAttribution.fbclid,
|
|
426
|
+
gclid: webAttribution.gclid,
|
|
427
|
+
ttclid: webAttribution.ttclid,
|
|
428
|
+
gbraid: webAttribution.gbraid,
|
|
429
|
+
wbraid: webAttribution.wbraid,
|
|
430
|
+
fbp: webAttribution.fbp,
|
|
431
|
+
fbc: webAttribution.fbc,
|
|
432
|
+
utm_source: webAttribution.utm_source,
|
|
433
|
+
utm_medium: webAttribution.utm_medium,
|
|
434
|
+
utm_campaign: webAttribution.utm_campaign,
|
|
435
|
+
utm_content: webAttribution.utm_content,
|
|
436
|
+
utm_term: webAttribution.utm_term,
|
|
437
|
+
web_timestamp: webAttribution.timestamp,
|
|
438
|
+
match_method: 'ip',
|
|
439
|
+
});
|
|
440
|
+
debugLog('Successfully merged deferred web attribution');
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
errorLog('Error fetching deferred web attribution:', error);
|
|
444
|
+
// Non-blocking - email-based fallback will catch this on identify()
|
|
445
|
+
}
|
|
446
|
+
}
|
|
360
447
|
/**
|
|
361
448
|
* Alias a user (connect anonymous user to known user)
|
|
362
449
|
*/
|
|
@@ -760,6 +847,13 @@ export class DatalyrSDK {
|
|
|
760
847
|
}
|
|
761
848
|
metaIntegration.updateTrackingAuthorization(enabled);
|
|
762
849
|
tiktokIntegration.updateTrackingAuthorization(enabled);
|
|
850
|
+
// Refresh cached advertiser info after ATT status change
|
|
851
|
+
try {
|
|
852
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
errorLog('Failed to refresh advertiser info:', error);
|
|
856
|
+
}
|
|
763
857
|
// Track ATT status event
|
|
764
858
|
await this.track('$att_status', {
|
|
765
859
|
authorized: enabled,
|
|
@@ -835,6 +929,8 @@ export class DatalyrSDK {
|
|
|
835
929
|
asa_conversion_type: asaAttribution.conversionType,
|
|
836
930
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
837
931
|
} : {};
|
|
932
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
933
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
838
934
|
const payload = {
|
|
839
935
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
840
936
|
visitorId: this.state.visitorId,
|
|
@@ -852,8 +948,24 @@ export class DatalyrSDK {
|
|
|
852
948
|
device_model: deviceInfo.model,
|
|
853
949
|
app_version: deviceInfo.appVersion,
|
|
854
950
|
app_build: deviceInfo.buildNumber,
|
|
951
|
+
app_name: deviceInfo.bundleId, // Best available app name
|
|
952
|
+
app_namespace: deviceInfo.bundleId,
|
|
953
|
+
screen_width: deviceInfo.screenWidth,
|
|
954
|
+
screen_height: deviceInfo.screenHeight,
|
|
955
|
+
locale: deviceInfo.locale,
|
|
956
|
+
timezone: deviceInfo.timezone,
|
|
957
|
+
carrier: deviceInfo.carrier,
|
|
855
958
|
network_type: getNetworkType(),
|
|
856
959
|
timestamp: Date.now(),
|
|
960
|
+
sdk_version: '1.4.9',
|
|
961
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
962
|
+
...(advertiserInfo ? {
|
|
963
|
+
idfa: advertiserInfo.idfa,
|
|
964
|
+
idfv: advertiserInfo.idfv,
|
|
965
|
+
gaid: advertiserInfo.gaid,
|
|
966
|
+
att_status: advertiserInfo.att_status,
|
|
967
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
968
|
+
} : {}),
|
|
857
969
|
// Attribution data
|
|
858
970
|
...attributionData,
|
|
859
971
|
// Apple Search Ads attribution
|
package/lib/event-queue.js
CHANGED
|
@@ -216,7 +216,7 @@ export const createEventQueue = (httpClient, config) => {
|
|
|
216
216
|
const defaultConfig = {
|
|
217
217
|
maxQueueSize: 100,
|
|
218
218
|
batchSize: 10,
|
|
219
|
-
flushInterval:
|
|
219
|
+
flushInterval: 30000, // 30 seconds — matches SDK constructor defaults and docs
|
|
220
220
|
maxRetryCount: 3,
|
|
221
221
|
};
|
|
222
222
|
return new EventQueue(httpClient, { ...defaultConfig, ...config });
|
package/lib/http-client.js
CHANGED
|
@@ -45,7 +45,7 @@ export class HttpClient {
|
|
|
45
45
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
46
46
|
const headers = {
|
|
47
47
|
'Content-Type': 'application/json',
|
|
48
|
-
'User-Agent': `@datalyr/react-native/1.
|
|
48
|
+
'User-Agent': `@datalyr/react-native/1.4.8`,
|
|
49
49
|
};
|
|
50
50
|
// Server-side tracking uses X-API-Key header
|
|
51
51
|
if (this.config.useServerTracking !== false) {
|
|
@@ -143,7 +143,7 @@ export class HttpClient {
|
|
|
143
143
|
return {
|
|
144
144
|
event: payload.eventName,
|
|
145
145
|
userId: payload.userId || payload.visitorId,
|
|
146
|
-
anonymousId: payload.visitorId,
|
|
146
|
+
anonymousId: payload.anonymousId || payload.visitorId,
|
|
147
147
|
properties: {
|
|
148
148
|
...payload.eventData,
|
|
149
149
|
sessionId: payload.sessionId,
|
|
@@ -152,7 +152,7 @@ export class HttpClient {
|
|
|
152
152
|
},
|
|
153
153
|
context: {
|
|
154
154
|
library: '@datalyr/react-native',
|
|
155
|
-
version: '1.
|
|
155
|
+
version: '1.4.8',
|
|
156
156
|
source: 'mobile_app', // Explicitly set source for mobile
|
|
157
157
|
userProperties: payload.userProperties,
|
|
158
158
|
},
|
|
@@ -62,9 +62,12 @@ export class TikTokIntegration {
|
|
|
62
62
|
this.initialized = true;
|
|
63
63
|
this.log(`TikTok SDK initialized with App ID: ${config.tiktokAppId}`);
|
|
64
64
|
}
|
|
65
|
+
else {
|
|
66
|
+
console.warn('[Datalyr/TikTok] TikTok SDK not initialized (accessToken may be missing). Events will still be sent server-side via Datalyr postbacks.');
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
|
-
|
|
70
|
+
console.warn('[Datalyr/TikTok] TikTok SDK init failed. Events will still be sent server-side via Datalyr postbacks.', error);
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
@@ -78,6 +78,21 @@ export declare const AppleSearchAdsNativeBridge: {
|
|
|
78
78
|
*/
|
|
79
79
|
getAttribution(): Promise<AppleSearchAdsAttribution | null>;
|
|
80
80
|
};
|
|
81
|
+
export interface AdvertiserInfo {
|
|
82
|
+
idfa?: string;
|
|
83
|
+
idfv?: string;
|
|
84
|
+
gaid?: string;
|
|
85
|
+
att_status: number;
|
|
86
|
+
advertiser_tracking_enabled: boolean;
|
|
87
|
+
}
|
|
88
|
+
export declare const AdvertiserInfoBridge: {
|
|
89
|
+
/**
|
|
90
|
+
* Get advertiser info (IDFA, IDFV, ATT status)
|
|
91
|
+
* IDFA is only available when ATT is authorized (iOS 14+)
|
|
92
|
+
* IDFV is always available on iOS
|
|
93
|
+
*/
|
|
94
|
+
getAdvertiserInfo(): Promise<AdvertiserInfo | null>;
|
|
95
|
+
};
|
|
81
96
|
export declare const PlayInstallReferrerNativeBridge: {
|
|
82
97
|
/**
|
|
83
98
|
* Check if Play Install Referrer is available
|
|
@@ -219,6 +219,24 @@ export const AppleSearchAdsNativeBridge = {
|
|
|
219
219
|
}
|
|
220
220
|
},
|
|
221
221
|
};
|
|
222
|
+
export const AdvertiserInfoBridge = {
|
|
223
|
+
/**
|
|
224
|
+
* Get advertiser info (IDFA, IDFV, ATT status)
|
|
225
|
+
* IDFA is only available when ATT is authorized (iOS 14+)
|
|
226
|
+
* IDFV is always available on iOS
|
|
227
|
+
*/
|
|
228
|
+
async getAdvertiserInfo() {
|
|
229
|
+
if (!DatalyrNative)
|
|
230
|
+
return null;
|
|
231
|
+
try {
|
|
232
|
+
return await DatalyrNative.getAdvertiserInfo();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('[Datalyr/AdvertiserInfo] Get advertiser info failed:', error);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
222
240
|
// MARK: - Play Install Referrer Bridge (Android only)
|
|
223
241
|
export const PlayInstallReferrerNativeBridge = {
|
|
224
242
|
/**
|
package/lib/native/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
* Native Module Exports
|
|
3
3
|
*/
|
|
4
4
|
export { SKAdNetworkBridge } from './SKAdNetworkBridge';
|
|
5
|
-
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, } from './DatalyrNativeBridge';
|
|
5
|
+
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, AdvertiserInfoBridge, } from './DatalyrNativeBridge';
|
|
6
|
+
export type { AdvertiserInfo } from './DatalyrNativeBridge';
|
package/lib/native/index.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Native Module Exports
|
|
3
3
|
*/
|
|
4
4
|
export { SKAdNetworkBridge } from './SKAdNetworkBridge';
|
|
5
|
-
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, } from './DatalyrNativeBridge';
|
|
5
|
+
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, AdvertiserInfoBridge, } from './DatalyrNativeBridge';
|
package/lib/types.d.ts
CHANGED
|
@@ -83,9 +83,9 @@ export interface DatalyrConfig {
|
|
|
83
83
|
maxEventQueueSize?: number;
|
|
84
84
|
/** Respect browser Do Not Track setting. Default: true */
|
|
85
85
|
respectDoNotTrack?: boolean;
|
|
86
|
-
/** Enable automatic event tracking (sessions, app lifecycle). Default:
|
|
86
|
+
/** Enable automatic event tracking (sessions, app lifecycle). Default: true */
|
|
87
87
|
enableAutoEvents?: boolean;
|
|
88
|
-
/** Enable attribution tracking (deep links, install referrer). Default:
|
|
88
|
+
/** Enable attribution tracking (deep links, install referrer). Default: true */
|
|
89
89
|
enableAttribution?: boolean;
|
|
90
90
|
/** Enable web-to-app attribution matching via email. Default: true */
|
|
91
91
|
enableWebToAppAttribution?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalyr/react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking with bundled Meta and TikTok SDKs for iOS and Android",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
package/src/datalyr-sdk-expo.ts
CHANGED
|
@@ -34,9 +34,9 @@ import { journeyManager } from './journey';
|
|
|
34
34
|
import { createAutoEventsManager, AutoEventsManager } from './auto-events';
|
|
35
35
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
36
36
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
37
|
-
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
|
|
37
|
+
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
38
38
|
import { DeferredDeepLinkResult } from './types';
|
|
39
|
-
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
39
|
+
import { AppleSearchAdsAttribution, AdvertiserInfoBridge } from './native/DatalyrNativeBridge';
|
|
40
40
|
|
|
41
41
|
export class DatalyrSDKExpo {
|
|
42
42
|
private state: SDKState;
|
|
@@ -44,6 +44,7 @@ export class DatalyrSDKExpo {
|
|
|
44
44
|
private eventQueue: EventQueue;
|
|
45
45
|
private autoEventsManager: AutoEventsManager | null = null;
|
|
46
46
|
private appStateSubscription: any = null;
|
|
47
|
+
private cachedAdvertiserInfo: any = null;
|
|
47
48
|
private static conversionEncoder?: ConversionValueEncoder;
|
|
48
49
|
private static debugEnabled = false;
|
|
49
50
|
|
|
@@ -59,9 +60,11 @@ export class DatalyrSDKExpo {
|
|
|
59
60
|
maxRetries: 3,
|
|
60
61
|
retryDelay: 1000,
|
|
61
62
|
batchSize: 10,
|
|
62
|
-
flushInterval:
|
|
63
|
+
flushInterval: 30000,
|
|
63
64
|
maxQueueSize: 100,
|
|
64
65
|
respectDoNotTrack: true,
|
|
66
|
+
enableAutoEvents: true,
|
|
67
|
+
enableAttribution: true,
|
|
65
68
|
},
|
|
66
69
|
visitorId: '',
|
|
67
70
|
anonymousId: '',
|
|
@@ -198,10 +201,22 @@ export class DatalyrSDKExpo {
|
|
|
198
201
|
|
|
199
202
|
// Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
|
|
200
203
|
await appleSearchAdsIntegration.initialize(config.debug);
|
|
204
|
+
|
|
205
|
+
// Initialize Play Install Referrer (Android only)
|
|
206
|
+
await playInstallReferrerIntegration.initialize();
|
|
207
|
+
|
|
208
|
+
// Cache advertiser info (IDFA/GAID, ATT status) once at init to avoid per-event native bridge calls
|
|
209
|
+
try {
|
|
210
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
errorLog('Failed to cache advertiser info:', error as Error);
|
|
213
|
+
}
|
|
214
|
+
|
|
201
215
|
debugLog('Platform integrations initialized', {
|
|
202
216
|
meta: metaIntegration.isAvailable(),
|
|
203
217
|
tiktok: tiktokIntegration.isAvailable(),
|
|
204
218
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
219
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
205
220
|
});
|
|
206
221
|
|
|
207
222
|
this.state.initialized = true;
|
|
@@ -210,7 +225,7 @@ export class DatalyrSDKExpo {
|
|
|
210
225
|
const installData = await attributionManager.trackInstall();
|
|
211
226
|
await this.track('app_install', {
|
|
212
227
|
platform: Platform.OS,
|
|
213
|
-
sdk_version: '1.
|
|
228
|
+
sdk_version: '1.4.9',
|
|
214
229
|
sdk_variant: 'expo',
|
|
215
230
|
...installData,
|
|
216
231
|
});
|
|
@@ -694,14 +709,23 @@ export class DatalyrSDKExpo {
|
|
|
694
709
|
return null;
|
|
695
710
|
}
|
|
696
711
|
|
|
697
|
-
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
712
|
+
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
|
|
698
713
|
return {
|
|
699
714
|
meta: metaIntegration.isAvailable(),
|
|
700
715
|
tiktok: tiktokIntegration.isAvailable(),
|
|
701
716
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
717
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
702
718
|
};
|
|
703
719
|
}
|
|
704
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Get Play Install Referrer data (Android only)
|
|
723
|
+
*/
|
|
724
|
+
getPlayInstallReferrer(): Record<string, any> | null {
|
|
725
|
+
const data = playInstallReferrerIntegration.getReferrerData();
|
|
726
|
+
return data ? playInstallReferrerIntegration.getAttributionData() : null;
|
|
727
|
+
}
|
|
728
|
+
|
|
705
729
|
/**
|
|
706
730
|
* Get Apple Search Ads attribution data
|
|
707
731
|
* Returns attribution if user installed via Apple Search Ads, null otherwise
|
|
@@ -717,6 +741,13 @@ export class DatalyrSDKExpo {
|
|
|
717
741
|
if (tiktokIntegration.isAvailable()) {
|
|
718
742
|
tiktokIntegration.updateTrackingAuthorization(authorized);
|
|
719
743
|
}
|
|
744
|
+
|
|
745
|
+
// Refresh cached advertiser info after ATT status change
|
|
746
|
+
try {
|
|
747
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
748
|
+
} catch (error) {
|
|
749
|
+
errorLog('Failed to refresh advertiser info:', error as Error);
|
|
750
|
+
}
|
|
720
751
|
}
|
|
721
752
|
|
|
722
753
|
private async handleDeferredDeepLink(data: DeferredDeepLinkResult): Promise<void> {
|
|
@@ -759,7 +790,7 @@ export class DatalyrSDKExpo {
|
|
|
759
790
|
const deviceInfo = await getDeviceInfo();
|
|
760
791
|
const fingerprintData = await createFingerprintData();
|
|
761
792
|
const attributionData = attributionManager.getAttributionData();
|
|
762
|
-
const networkType =
|
|
793
|
+
const networkType = getNetworkType();
|
|
763
794
|
|
|
764
795
|
// Get Apple Search Ads attribution if available
|
|
765
796
|
const asaAttribution = appleSearchAdsIntegration.getAttributionData();
|
|
@@ -777,6 +808,9 @@ export class DatalyrSDKExpo {
|
|
|
777
808
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
778
809
|
} : {};
|
|
779
810
|
|
|
811
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
812
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
813
|
+
|
|
780
814
|
const payload: EventPayload = {
|
|
781
815
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
782
816
|
visitorId: this.state.visitorId,
|
|
@@ -792,9 +826,25 @@ export class DatalyrSDKExpo {
|
|
|
792
826
|
device_model: deviceInfo.model,
|
|
793
827
|
app_version: deviceInfo.appVersion,
|
|
794
828
|
app_build: deviceInfo.buildNumber,
|
|
829
|
+
app_name: deviceInfo.bundleId,
|
|
830
|
+
app_namespace: deviceInfo.bundleId,
|
|
831
|
+
screen_width: deviceInfo.screenWidth,
|
|
832
|
+
screen_height: deviceInfo.screenHeight,
|
|
833
|
+
locale: deviceInfo.locale,
|
|
834
|
+
timezone: deviceInfo.timezone,
|
|
835
|
+
carrier: deviceInfo.carrier,
|
|
795
836
|
network_type: networkType,
|
|
796
837
|
timestamp: Date.now(),
|
|
838
|
+
sdk_version: '1.4.9',
|
|
797
839
|
sdk_variant: 'expo',
|
|
840
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
841
|
+
...(advertiserInfo ? {
|
|
842
|
+
idfa: advertiserInfo.idfa,
|
|
843
|
+
idfv: advertiserInfo.idfv,
|
|
844
|
+
gaid: advertiserInfo.gaid,
|
|
845
|
+
att_status: advertiserInfo.att_status,
|
|
846
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
847
|
+
} : {}),
|
|
798
848
|
...attributionData,
|
|
799
849
|
// Apple Search Ads attribution
|
|
800
850
|
...asaData,
|
|
@@ -1030,10 +1080,14 @@ export class DatalyrExpo {
|
|
|
1030
1080
|
return datalyrExpo.getDeferredAttributionData();
|
|
1031
1081
|
}
|
|
1032
1082
|
|
|
1033
|
-
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
1083
|
+
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
|
|
1034
1084
|
return datalyrExpo.getPlatformIntegrationStatus();
|
|
1035
1085
|
}
|
|
1036
1086
|
|
|
1087
|
+
static getPlayInstallReferrer(): Record<string, any> | null {
|
|
1088
|
+
return datalyrExpo.getPlayInstallReferrer();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1037
1091
|
static getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
|
|
1038
1092
|
return datalyrExpo.getAppleSearchAdsAttribution();
|
|
1039
1093
|
}
|
package/src/datalyr-sdk.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { createAutoEventsManager, AutoEventsManager, SessionData } from './auto-
|
|
|
32
32
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
33
33
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
34
34
|
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
35
|
-
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
35
|
+
import { AppleSearchAdsAttribution, AdvertiserInfoBridge } from './native/DatalyrNativeBridge';
|
|
36
36
|
import { networkStatusManager } from './network-status';
|
|
37
37
|
|
|
38
38
|
export class DatalyrSDK {
|
|
@@ -42,6 +42,7 @@ export class DatalyrSDK {
|
|
|
42
42
|
private autoEventsManager: AutoEventsManager | null = null;
|
|
43
43
|
private appStateSubscription: any = null;
|
|
44
44
|
private networkStatusUnsubscribe: (() => void) | null = null;
|
|
45
|
+
private cachedAdvertiserInfo: any = null;
|
|
45
46
|
private static conversionEncoder?: ConversionValueEncoder;
|
|
46
47
|
private static debugEnabled = false;
|
|
47
48
|
|
|
@@ -58,9 +59,11 @@ export class DatalyrSDK {
|
|
|
58
59
|
maxRetries: 3,
|
|
59
60
|
retryDelay: 1000,
|
|
60
61
|
batchSize: 10,
|
|
61
|
-
flushInterval:
|
|
62
|
+
flushInterval: 30000,
|
|
62
63
|
maxQueueSize: 100,
|
|
63
64
|
respectDoNotTrack: true,
|
|
65
|
+
enableAutoEvents: true,
|
|
66
|
+
enableAttribution: true,
|
|
64
67
|
},
|
|
65
68
|
visitorId: '',
|
|
66
69
|
anonymousId: '', // Persistent anonymous identifier
|
|
@@ -221,6 +224,13 @@ export class DatalyrSDK {
|
|
|
221
224
|
// Wait for all platform integrations to complete
|
|
222
225
|
await Promise.all(platformInitPromises);
|
|
223
226
|
|
|
227
|
+
// Cache advertiser info (IDFA/GAID, ATT status) once at init to avoid per-event native bridge calls
|
|
228
|
+
try {
|
|
229
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
errorLog('Failed to cache advertiser info:', error as Error);
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
debugLog('Platform integrations initialized', {
|
|
225
235
|
meta: metaIntegration.isAvailable(),
|
|
226
236
|
tiktok: tiktokIntegration.isAvailable(),
|
|
@@ -233,10 +243,16 @@ export class DatalyrSDK {
|
|
|
233
243
|
|
|
234
244
|
// Check for app install (after SDK is marked as initialized)
|
|
235
245
|
if (attributionManager.isInstall()) {
|
|
246
|
+
// iOS: Attempt deferred web-to-app attribution via IP matching before tracking install
|
|
247
|
+
// Android: Play Store referrer is handled by playInstallReferrerIntegration
|
|
248
|
+
if (Platform.OS === 'ios') {
|
|
249
|
+
await this.fetchDeferredWebAttribution();
|
|
250
|
+
}
|
|
251
|
+
|
|
236
252
|
const installData = await attributionManager.trackInstall();
|
|
237
253
|
await this.track('app_install', {
|
|
238
254
|
platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
|
|
239
|
-
sdk_version: '1.
|
|
255
|
+
sdk_version: '1.4.9',
|
|
240
256
|
...installData,
|
|
241
257
|
});
|
|
242
258
|
}
|
|
@@ -443,6 +459,88 @@ export class DatalyrSDK {
|
|
|
443
459
|
}
|
|
444
460
|
}
|
|
445
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Fetch deferred web attribution on first app install.
|
|
464
|
+
* Uses IP-based matching (iOS) or Play Store referrer (Android) to recover
|
|
465
|
+
* attribution data (fbclid, utm_*, etc.) from a prelander web visit.
|
|
466
|
+
* Called automatically during initialize() when a fresh install is detected.
|
|
467
|
+
*/
|
|
468
|
+
private async fetchDeferredWebAttribution(): Promise<void> {
|
|
469
|
+
if (!this.state.config?.apiKey) {
|
|
470
|
+
debugLog('API key not available for deferred attribution fetch');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
debugLog('Fetching deferred web attribution via IP matching...');
|
|
476
|
+
|
|
477
|
+
const baseUrl = this.state.config.endpoint || 'https://api.datalyr.com';
|
|
478
|
+
const controller = new AbortController();
|
|
479
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
480
|
+
|
|
481
|
+
const response = await fetch(`${baseUrl}/attribution/deferred-lookup`, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: {
|
|
484
|
+
'Content-Type': 'application/json',
|
|
485
|
+
'X-Datalyr-API-Key': this.state.config.apiKey,
|
|
486
|
+
},
|
|
487
|
+
body: JSON.stringify({ platform: Platform.OS }),
|
|
488
|
+
signal: controller.signal,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
clearTimeout(timeout);
|
|
492
|
+
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
debugLog('Deferred attribution lookup failed:', response.status);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const result = await response.json() as { found: boolean; attribution?: any };
|
|
499
|
+
|
|
500
|
+
if (!result.found || !result.attribution) {
|
|
501
|
+
debugLog('No deferred web attribution found for this IP');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const webAttribution = result.attribution;
|
|
506
|
+
debugLog('Deferred web attribution found:', {
|
|
507
|
+
visitor_id: webAttribution.visitor_id,
|
|
508
|
+
has_fbclid: !!webAttribution.fbclid,
|
|
509
|
+
has_gclid: !!webAttribution.gclid,
|
|
510
|
+
utm_source: webAttribution.utm_source,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Merge web attribution into current session
|
|
514
|
+
attributionManager.mergeWebAttribution(webAttribution);
|
|
515
|
+
|
|
516
|
+
// Track match event for analytics
|
|
517
|
+
await this.track('$web_attribution_matched', {
|
|
518
|
+
web_visitor_id: webAttribution.visitor_id,
|
|
519
|
+
web_user_id: webAttribution.user_id,
|
|
520
|
+
fbclid: webAttribution.fbclid,
|
|
521
|
+
gclid: webAttribution.gclid,
|
|
522
|
+
ttclid: webAttribution.ttclid,
|
|
523
|
+
gbraid: webAttribution.gbraid,
|
|
524
|
+
wbraid: webAttribution.wbraid,
|
|
525
|
+
fbp: webAttribution.fbp,
|
|
526
|
+
fbc: webAttribution.fbc,
|
|
527
|
+
utm_source: webAttribution.utm_source,
|
|
528
|
+
utm_medium: webAttribution.utm_medium,
|
|
529
|
+
utm_campaign: webAttribution.utm_campaign,
|
|
530
|
+
utm_content: webAttribution.utm_content,
|
|
531
|
+
utm_term: webAttribution.utm_term,
|
|
532
|
+
web_timestamp: webAttribution.timestamp,
|
|
533
|
+
match_method: 'ip',
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
debugLog('Successfully merged deferred web attribution');
|
|
537
|
+
|
|
538
|
+
} catch (error) {
|
|
539
|
+
errorLog('Error fetching deferred web attribution:', error as Error);
|
|
540
|
+
// Non-blocking - email-based fallback will catch this on identify()
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
446
544
|
/**
|
|
447
545
|
* Alias a user (connect anonymous user to known user)
|
|
448
546
|
*/
|
|
@@ -939,6 +1037,13 @@ export class DatalyrSDK {
|
|
|
939
1037
|
metaIntegration.updateTrackingAuthorization(enabled);
|
|
940
1038
|
tiktokIntegration.updateTrackingAuthorization(enabled);
|
|
941
1039
|
|
|
1040
|
+
// Refresh cached advertiser info after ATT status change
|
|
1041
|
+
try {
|
|
1042
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
errorLog('Failed to refresh advertiser info:', error as Error);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
942
1047
|
// Track ATT status event
|
|
943
1048
|
await this.track('$att_status', {
|
|
944
1049
|
authorized: enabled,
|
|
@@ -1022,6 +1127,9 @@ export class DatalyrSDK {
|
|
|
1022
1127
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
1023
1128
|
} : {};
|
|
1024
1129
|
|
|
1130
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
1131
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
1132
|
+
|
|
1025
1133
|
const payload: EventPayload = {
|
|
1026
1134
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
1027
1135
|
visitorId: this.state.visitorId,
|
|
@@ -1039,8 +1147,24 @@ export class DatalyrSDK {
|
|
|
1039
1147
|
device_model: deviceInfo.model,
|
|
1040
1148
|
app_version: deviceInfo.appVersion,
|
|
1041
1149
|
app_build: deviceInfo.buildNumber,
|
|
1150
|
+
app_name: deviceInfo.bundleId, // Best available app name
|
|
1151
|
+
app_namespace: deviceInfo.bundleId,
|
|
1152
|
+
screen_width: deviceInfo.screenWidth,
|
|
1153
|
+
screen_height: deviceInfo.screenHeight,
|
|
1154
|
+
locale: deviceInfo.locale,
|
|
1155
|
+
timezone: deviceInfo.timezone,
|
|
1156
|
+
carrier: deviceInfo.carrier,
|
|
1042
1157
|
network_type: getNetworkType(),
|
|
1043
1158
|
timestamp: Date.now(),
|
|
1159
|
+
sdk_version: '1.4.9',
|
|
1160
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
1161
|
+
...(advertiserInfo ? {
|
|
1162
|
+
idfa: advertiserInfo.idfa,
|
|
1163
|
+
idfv: advertiserInfo.idfv,
|
|
1164
|
+
gaid: advertiserInfo.gaid,
|
|
1165
|
+
att_status: advertiserInfo.att_status,
|
|
1166
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
1167
|
+
} : {}),
|
|
1044
1168
|
// Attribution data
|
|
1045
1169
|
...attributionData,
|
|
1046
1170
|
// Apple Search Ads attribution
|
package/src/event-queue.ts
CHANGED
|
@@ -262,7 +262,7 @@ export const createEventQueue = (httpClient: HttpClient, config?: Partial<QueueC
|
|
|
262
262
|
const defaultConfig: QueueConfig = {
|
|
263
263
|
maxQueueSize: 100,
|
|
264
264
|
batchSize: 10,
|
|
265
|
-
flushInterval:
|
|
265
|
+
flushInterval: 30000, // 30 seconds — matches SDK constructor defaults and docs
|
|
266
266
|
maxRetryCount: 3,
|
|
267
267
|
};
|
|
268
268
|
|
package/src/http-client.ts
CHANGED
|
@@ -72,7 +72,7 @@ export class HttpClient {
|
|
|
72
72
|
|
|
73
73
|
const headers: Record<string, string> = {
|
|
74
74
|
'Content-Type': 'application/json',
|
|
75
|
-
'User-Agent': `@datalyr/react-native/1.
|
|
75
|
+
'User-Agent': `@datalyr/react-native/1.4.8`,
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
// Server-side tracking uses X-API-Key header
|
|
@@ -188,7 +188,7 @@ export class HttpClient {
|
|
|
188
188
|
return {
|
|
189
189
|
event: payload.eventName,
|
|
190
190
|
userId: payload.userId || payload.visitorId,
|
|
191
|
-
anonymousId: payload.visitorId,
|
|
191
|
+
anonymousId: payload.anonymousId || payload.visitorId,
|
|
192
192
|
properties: {
|
|
193
193
|
...payload.eventData,
|
|
194
194
|
sessionId: payload.sessionId,
|
|
@@ -197,7 +197,7 @@ export class HttpClient {
|
|
|
197
197
|
},
|
|
198
198
|
context: {
|
|
199
199
|
library: '@datalyr/react-native',
|
|
200
|
-
version: '1.
|
|
200
|
+
version: '1.4.8',
|
|
201
201
|
source: 'mobile_app', // Explicitly set source for mobile
|
|
202
202
|
userProperties: payload.userProperties,
|
|
203
203
|
},
|
|
@@ -76,9 +76,11 @@ export class TikTokIntegration {
|
|
|
76
76
|
if (success) {
|
|
77
77
|
this.initialized = true;
|
|
78
78
|
this.log(`TikTok SDK initialized with App ID: ${config.tiktokAppId}`);
|
|
79
|
+
} else {
|
|
80
|
+
console.warn('[Datalyr/TikTok] TikTok SDK not initialized (accessToken may be missing). Events will still be sent server-side via Datalyr postbacks.');
|
|
79
81
|
}
|
|
80
82
|
} catch (error) {
|
|
81
|
-
|
|
83
|
+
console.warn('[Datalyr/TikTok] TikTok SDK init failed. Events will still be sent server-side via Datalyr postbacks.', error);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -90,6 +90,15 @@ interface DatalyrNativeModule {
|
|
|
90
90
|
logoutTikTok(): Promise<boolean>;
|
|
91
91
|
updateTikTokTrackingAuthorization(enabled: boolean): Promise<boolean>;
|
|
92
92
|
|
|
93
|
+
// Advertiser Info (IDFA, IDFV, GAID, ATT Status)
|
|
94
|
+
getAdvertiserInfo(): Promise<{
|
|
95
|
+
idfa?: string;
|
|
96
|
+
idfv?: string;
|
|
97
|
+
gaid?: string;
|
|
98
|
+
att_status: number;
|
|
99
|
+
advertiser_tracking_enabled: boolean;
|
|
100
|
+
} | null>;
|
|
101
|
+
|
|
93
102
|
// Apple Search Ads Methods (iOS only)
|
|
94
103
|
getAppleSearchAdsAttribution(): Promise<AppleSearchAdsAttribution | null>;
|
|
95
104
|
|
|
@@ -374,6 +383,34 @@ export const AppleSearchAdsNativeBridge = {
|
|
|
374
383
|
},
|
|
375
384
|
};
|
|
376
385
|
|
|
386
|
+
// MARK: - Advertiser Info Bridge
|
|
387
|
+
|
|
388
|
+
export interface AdvertiserInfo {
|
|
389
|
+
idfa?: string;
|
|
390
|
+
idfv?: string;
|
|
391
|
+
gaid?: string;
|
|
392
|
+
att_status: number;
|
|
393
|
+
advertiser_tracking_enabled: boolean;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const AdvertiserInfoBridge = {
|
|
397
|
+
/**
|
|
398
|
+
* Get advertiser info (IDFA, IDFV, ATT status)
|
|
399
|
+
* IDFA is only available when ATT is authorized (iOS 14+)
|
|
400
|
+
* IDFV is always available on iOS
|
|
401
|
+
*/
|
|
402
|
+
async getAdvertiserInfo(): Promise<AdvertiserInfo | null> {
|
|
403
|
+
if (!DatalyrNative) return null;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
return await DatalyrNative.getAdvertiserInfo();
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error('[Datalyr/AdvertiserInfo] Get advertiser info failed:', error);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
377
414
|
// MARK: - Play Install Referrer Bridge (Android only)
|
|
378
415
|
|
|
379
416
|
export const PlayInstallReferrerNativeBridge = {
|
package/src/native/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -105,10 +105,10 @@ export interface DatalyrConfig {
|
|
|
105
105
|
/** Respect browser Do Not Track setting. Default: true */
|
|
106
106
|
respectDoNotTrack?: boolean;
|
|
107
107
|
|
|
108
|
-
/** Enable automatic event tracking (sessions, app lifecycle). Default:
|
|
108
|
+
/** Enable automatic event tracking (sessions, app lifecycle). Default: true */
|
|
109
109
|
enableAutoEvents?: boolean;
|
|
110
110
|
|
|
111
|
-
/** Enable attribution tracking (deep links, install referrer). Default:
|
|
111
|
+
/** Enable attribution tracking (deep links, install referrer). Default: true */
|
|
112
112
|
enableAttribution?: boolean;
|
|
113
113
|
|
|
114
114
|
/** Enable web-to-app attribution matching via email. Default: true */
|
package/src/utils-expo.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
-
import { Platform } from 'react-native';
|
|
2
|
+
import { Platform, Dimensions } from 'react-native';
|
|
3
3
|
import * as Application from 'expo-application';
|
|
4
4
|
import * as Device from 'expo-device';
|
|
5
5
|
import * as Network from 'expo-network';
|
|
@@ -84,10 +84,14 @@ export interface DeviceInfo {
|
|
|
84
84
|
isEmulator: boolean;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// Cached device info to avoid repeated async calls (matches utils.ts pattern)
|
|
88
|
+
let cachedDeviceInfo: DeviceInfo | null = null;
|
|
89
|
+
let deviceInfoPromise: Promise<DeviceInfo> | null = null;
|
|
90
|
+
|
|
91
|
+
const fetchDeviceInfoInternal = async (): Promise<DeviceInfo> => {
|
|
88
92
|
try {
|
|
89
93
|
const deviceId = await getOrCreateDeviceId();
|
|
90
|
-
|
|
94
|
+
|
|
91
95
|
return {
|
|
92
96
|
deviceId,
|
|
93
97
|
model: Device.modelName || Device.deviceName || 'Unknown',
|
|
@@ -96,10 +100,10 @@ export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
|
96
100
|
appVersion: Application.nativeApplicationVersion || '1.0.0',
|
|
97
101
|
buildNumber: Application.nativeBuildVersion || '1',
|
|
98
102
|
bundleId: Application.applicationId || 'unknown.bundle.id',
|
|
99
|
-
screenWidth:
|
|
100
|
-
screenHeight:
|
|
103
|
+
screenWidth: Dimensions.get('window').width,
|
|
104
|
+
screenHeight: Dimensions.get('window').height,
|
|
101
105
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
102
|
-
locale: 'en-US',
|
|
106
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale || 'en-US',
|
|
103
107
|
carrier: undefined, // Not available in Expo managed workflow
|
|
104
108
|
isEmulator: !Device.isDevice,
|
|
105
109
|
};
|
|
@@ -113,8 +117,8 @@ export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
|
113
117
|
appVersion: '1.0.0',
|
|
114
118
|
buildNumber: '1',
|
|
115
119
|
bundleId: 'unknown.bundle.id',
|
|
116
|
-
screenWidth:
|
|
117
|
-
screenHeight:
|
|
120
|
+
screenWidth: Dimensions.get('window').width,
|
|
121
|
+
screenHeight: Dimensions.get('window').height,
|
|
118
122
|
timezone: 'UTC',
|
|
119
123
|
locale: 'en-US',
|
|
120
124
|
isEmulator: true,
|
|
@@ -122,6 +126,19 @@ export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
|
122
126
|
}
|
|
123
127
|
};
|
|
124
128
|
|
|
129
|
+
export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
130
|
+
if (cachedDeviceInfo) return cachedDeviceInfo;
|
|
131
|
+
if (deviceInfoPromise) return deviceInfoPromise;
|
|
132
|
+
|
|
133
|
+
deviceInfoPromise = fetchDeviceInfoInternal();
|
|
134
|
+
try {
|
|
135
|
+
cachedDeviceInfo = await deviceInfoPromise;
|
|
136
|
+
return cachedDeviceInfo;
|
|
137
|
+
} finally {
|
|
138
|
+
deviceInfoPromise = null;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
125
142
|
// Device ID management
|
|
126
143
|
const getOrCreateDeviceId = async (): Promise<string> => {
|
|
127
144
|
try {
|
|
@@ -246,30 +263,49 @@ export const createFingerprintData = async () => {
|
|
|
246
263
|
// IDFA/GAID collection has been removed for privacy compliance
|
|
247
264
|
// Modern attribution tracking relies on privacy-safe methods:
|
|
248
265
|
|
|
249
|
-
//
|
|
250
|
-
|
|
266
|
+
// Cached network type to avoid per-event native bridge calls
|
|
267
|
+
let cachedNetworkType = 'unknown';
|
|
268
|
+
let networkTypeLastFetched = 0;
|
|
269
|
+
const NETWORK_TYPE_CACHE_MS = 30000; // Refresh every 30s
|
|
270
|
+
|
|
271
|
+
// Network type detection using Expo Network — cached to avoid per-event async calls
|
|
272
|
+
export const getNetworkType = (): string => {
|
|
273
|
+
// Trigger background refresh if stale, but always return cached value synchronously
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
if (now - networkTypeLastFetched > NETWORK_TYPE_CACHE_MS) {
|
|
276
|
+
networkTypeLastFetched = now;
|
|
277
|
+
refreshNetworkType();
|
|
278
|
+
}
|
|
279
|
+
return cachedNetworkType;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const refreshNetworkType = async (): Promise<void> => {
|
|
251
283
|
try {
|
|
252
284
|
const networkState = await Network.getNetworkStateAsync();
|
|
253
|
-
|
|
285
|
+
|
|
254
286
|
if (!networkState.isConnected) {
|
|
255
|
-
|
|
287
|
+
cachedNetworkType = 'none';
|
|
288
|
+
return;
|
|
256
289
|
}
|
|
257
|
-
|
|
290
|
+
|
|
258
291
|
switch (networkState.type) {
|
|
259
292
|
case Network.NetworkStateType.WIFI:
|
|
260
|
-
|
|
293
|
+
cachedNetworkType = 'wifi';
|
|
294
|
+
break;
|
|
261
295
|
case Network.NetworkStateType.CELLULAR:
|
|
262
|
-
|
|
296
|
+
cachedNetworkType = 'cellular';
|
|
297
|
+
break;
|
|
263
298
|
case Network.NetworkStateType.ETHERNET:
|
|
264
|
-
|
|
299
|
+
cachedNetworkType = 'ethernet';
|
|
300
|
+
break;
|
|
265
301
|
case Network.NetworkStateType.BLUETOOTH:
|
|
266
|
-
|
|
302
|
+
cachedNetworkType = 'bluetooth';
|
|
303
|
+
break;
|
|
267
304
|
default:
|
|
268
|
-
|
|
305
|
+
cachedNetworkType = 'unknown';
|
|
269
306
|
}
|
|
270
307
|
} catch (error) {
|
|
271
308
|
debugLog('Error getting network type:', error);
|
|
272
|
-
return 'unknown';
|
|
273
309
|
}
|
|
274
310
|
};
|
|
275
311
|
|