@datalyr/react-native 1.4.7 → 1.4.9
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 +19 -2
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +44 -0
- package/ios/DatalyrNativeModule.swift +224 -80
- package/ios/DatalyrObjCExceptionCatcher.h +14 -0
- package/ios/DatalyrObjCExceptionCatcher.m +30 -0
- package/lib/datalyr-sdk.d.ts +1 -0
- package/lib/datalyr-sdk.js +38 -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 +39 -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/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.4.
|
|
3
|
+
"version": "1.4.9",
|
|
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(),
|
|
@@ -236,7 +246,7 @@ export class DatalyrSDK {
|
|
|
236
246
|
const installData = await attributionManager.trackInstall();
|
|
237
247
|
await this.track('app_install', {
|
|
238
248
|
platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
|
|
239
|
-
sdk_version: '1.
|
|
249
|
+
sdk_version: '1.4.9',
|
|
240
250
|
...installData,
|
|
241
251
|
});
|
|
242
252
|
}
|
|
@@ -939,6 +949,13 @@ export class DatalyrSDK {
|
|
|
939
949
|
metaIntegration.updateTrackingAuthorization(enabled);
|
|
940
950
|
tiktokIntegration.updateTrackingAuthorization(enabled);
|
|
941
951
|
|
|
952
|
+
// Refresh cached advertiser info after ATT status change
|
|
953
|
+
try {
|
|
954
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
955
|
+
} catch (error) {
|
|
956
|
+
errorLog('Failed to refresh advertiser info:', error as Error);
|
|
957
|
+
}
|
|
958
|
+
|
|
942
959
|
// Track ATT status event
|
|
943
960
|
await this.track('$att_status', {
|
|
944
961
|
authorized: enabled,
|
|
@@ -1022,6 +1039,9 @@ export class DatalyrSDK {
|
|
|
1022
1039
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
1023
1040
|
} : {};
|
|
1024
1041
|
|
|
1042
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
1043
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
1044
|
+
|
|
1025
1045
|
const payload: EventPayload = {
|
|
1026
1046
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
1027
1047
|
visitorId: this.state.visitorId,
|
|
@@ -1039,8 +1059,24 @@ export class DatalyrSDK {
|
|
|
1039
1059
|
device_model: deviceInfo.model,
|
|
1040
1060
|
app_version: deviceInfo.appVersion,
|
|
1041
1061
|
app_build: deviceInfo.buildNumber,
|
|
1062
|
+
app_name: deviceInfo.bundleId, // Best available app name
|
|
1063
|
+
app_namespace: deviceInfo.bundleId,
|
|
1064
|
+
screen_width: deviceInfo.screenWidth,
|
|
1065
|
+
screen_height: deviceInfo.screenHeight,
|
|
1066
|
+
locale: deviceInfo.locale,
|
|
1067
|
+
timezone: deviceInfo.timezone,
|
|
1068
|
+
carrier: deviceInfo.carrier,
|
|
1042
1069
|
network_type: getNetworkType(),
|
|
1043
1070
|
timestamp: Date.now(),
|
|
1071
|
+
sdk_version: '1.4.9',
|
|
1072
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
1073
|
+
...(advertiserInfo ? {
|
|
1074
|
+
idfa: advertiserInfo.idfa,
|
|
1075
|
+
idfv: advertiserInfo.idfv,
|
|
1076
|
+
gaid: advertiserInfo.gaid,
|
|
1077
|
+
att_status: advertiserInfo.att_status,
|
|
1078
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
1079
|
+
} : {}),
|
|
1044
1080
|
// Attribution data
|
|
1045
1081
|
...attributionData,
|
|
1046
1082
|
// 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
|
|