@datalyr/react-native 1.2.1 → 1.3.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/android/build.gradle +54 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +423 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrPackage.java +30 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrPlayInstallReferrerModule.java +229 -0
- package/datalyr-react-native.podspec +2 -2
- package/ios/DatalyrSKAdNetwork.m +52 -1
- package/lib/ConversionValueEncoder.d.ts +13 -1
- package/lib/ConversionValueEncoder.js +57 -23
- package/lib/datalyr-sdk.d.ts +25 -2
- package/lib/datalyr-sdk.js +59 -8
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/integrations/index.d.ts +3 -1
- package/lib/integrations/index.js +2 -1
- package/lib/integrations/meta-integration.d.ts +1 -0
- package/lib/integrations/meta-integration.js +4 -3
- package/lib/integrations/play-install-referrer.d.ts +74 -0
- package/lib/integrations/play-install-referrer.js +156 -0
- package/lib/integrations/tiktok-integration.d.ts +1 -0
- package/lib/integrations/tiktok-integration.js +4 -3
- package/lib/journey.d.ts +106 -0
- package/lib/journey.js +258 -0
- package/lib/native/DatalyrNativeBridge.d.ts +42 -3
- package/lib/native/DatalyrNativeBridge.js +63 -9
- package/lib/native/SKAdNetworkBridge.d.ts +21 -0
- package/lib/native/SKAdNetworkBridge.js +54 -0
- package/package.json +8 -3
- package/src/ConversionValueEncoder.ts +67 -26
- package/src/datalyr-sdk-expo.ts +55 -6
- package/src/datalyr-sdk.ts +72 -13
- package/src/expo.ts +4 -0
- package/src/index.ts +2 -0
- package/src/integrations/index.ts +3 -1
- package/src/integrations/meta-integration.ts +4 -3
- package/src/integrations/play-install-referrer.ts +203 -0
- package/src/integrations/tiktok-integration.ts +4 -3
- package/src/journey.ts +338 -0
- package/src/native/DatalyrNativeBridge.ts +99 -13
- package/src/native/SKAdNetworkBridge.ts +86 -2
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { SKANCoarseValue, SKANConversionResult } from './native/SKAdNetworkBridge';
|
|
2
|
+
|
|
3
|
+
// SKAN 4.0 compatible event mapping
|
|
2
4
|
interface EventMapping {
|
|
3
5
|
bits: number[];
|
|
4
6
|
revenueBits?: number[];
|
|
5
7
|
priority: number;
|
|
8
|
+
coarseValue?: SKANCoarseValue; // SKAN 4.0: low, medium, high
|
|
9
|
+
lockWindow?: boolean; // SKAN 4.0: lock the conversion window after this event
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
interface ConversionTemplate {
|
|
@@ -18,11 +22,21 @@ export class ConversionValueEncoder {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
* Encode an event into Apple's 0-63 conversion value format
|
|
25
|
+
* Encode an event into Apple's 0-63 conversion value format (SKAN 3.0 compatible)
|
|
26
|
+
* @deprecated Use encodeWithSKAN4 for iOS 16.1+
|
|
22
27
|
*/
|
|
23
28
|
encode(event: string, properties?: Record<string, any>): number {
|
|
29
|
+
return this.encodeWithSKAN4(event, properties).fineValue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encode an event with full SKAN 4.0 support (fine value, coarse value, lock window)
|
|
34
|
+
*/
|
|
35
|
+
encodeWithSKAN4(event: string, properties?: Record<string, any>): SKANConversionResult {
|
|
24
36
|
const mapping = this.template.events[event];
|
|
25
|
-
if (!mapping)
|
|
37
|
+
if (!mapping) {
|
|
38
|
+
return { fineValue: 0, coarseValue: 'low', lockWindow: false, priority: 0 };
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
let conversionValue = 0;
|
|
28
42
|
|
|
@@ -32,18 +46,30 @@ export class ConversionValueEncoder {
|
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
// Set revenue bits if revenue is provided
|
|
49
|
+
let coarseValue: SKANCoarseValue = mapping.coarseValue || 'medium';
|
|
35
50
|
if (mapping.revenueBits && properties) {
|
|
36
51
|
const revenue = properties.revenue || properties.value || 0;
|
|
37
52
|
const revenueTier = this.getRevenueTier(revenue);
|
|
38
|
-
|
|
53
|
+
|
|
39
54
|
for (let i = 0; i < Math.min(mapping.revenueBits.length, 3); i++) {
|
|
40
55
|
if ((revenueTier >> i) & 1) {
|
|
41
56
|
conversionValue |= (1 << mapping.revenueBits[i]);
|
|
42
57
|
}
|
|
43
58
|
}
|
|
59
|
+
|
|
60
|
+
// Upgrade coarse value based on revenue
|
|
61
|
+
coarseValue = this.getCoarseValueForRevenue(revenue);
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
// Ensure value is within 0-63 range
|
|
65
|
+
const fineValue = Math.min(conversionValue, 63);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
fineValue,
|
|
69
|
+
coarseValue,
|
|
70
|
+
lockWindow: mapping.lockWindow || false,
|
|
71
|
+
priority: mapping.priority
|
|
72
|
+
};
|
|
47
73
|
}
|
|
48
74
|
|
|
49
75
|
/**
|
|
@@ -59,43 +85,58 @@ export class ConversionValueEncoder {
|
|
|
59
85
|
if (revenue < 250) return 6; // $100-250
|
|
60
86
|
return 7; // $250+
|
|
61
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map revenue to SKAN 4.0 coarse value
|
|
91
|
+
*/
|
|
92
|
+
private getCoarseValueForRevenue(revenue: number): SKANCoarseValue {
|
|
93
|
+
if (revenue < 10) return 'low'; // $0-10 = low value
|
|
94
|
+
if (revenue < 50) return 'medium'; // $10-50 = medium value
|
|
95
|
+
return 'high'; // $50+ = high value
|
|
96
|
+
}
|
|
62
97
|
}
|
|
63
98
|
|
|
64
|
-
// Industry templates
|
|
99
|
+
// Industry templates with SKAN 4.0 support
|
|
65
100
|
export const ConversionTemplates = {
|
|
101
|
+
// E-commerce template - optimized for online stores
|
|
102
|
+
// SKAN 4.0: purchase locks window, high-value events get "high" coarse value
|
|
66
103
|
ecommerce: {
|
|
67
104
|
name: 'ecommerce',
|
|
68
105
|
events: {
|
|
69
|
-
purchase: { bits: [0], revenueBits: [1, 2, 3], priority: 100 },
|
|
70
|
-
add_to_cart: { bits: [4], priority: 30 },
|
|
71
|
-
begin_checkout: { bits: [5], priority: 50 },
|
|
72
|
-
signup: { bits: [6], priority: 20 },
|
|
73
|
-
subscribe: { bits: [0, 1], revenueBits: [2, 3, 4], priority: 90 },
|
|
74
|
-
view_item: { bits: [7], priority: 10 }
|
|
106
|
+
purchase: { bits: [0], revenueBits: [1, 2, 3], priority: 100, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
|
|
107
|
+
add_to_cart: { bits: [4], priority: 30, coarseValue: 'low' as SKANCoarseValue },
|
|
108
|
+
begin_checkout: { bits: [5], priority: 50, coarseValue: 'medium' as SKANCoarseValue },
|
|
109
|
+
signup: { bits: [6], priority: 20, coarseValue: 'low' as SKANCoarseValue },
|
|
110
|
+
subscribe: { bits: [0, 1], revenueBits: [2, 3, 4], priority: 90, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
|
|
111
|
+
view_item: { bits: [7], priority: 10, coarseValue: 'low' as SKANCoarseValue }
|
|
75
112
|
}
|
|
76
113
|
} as ConversionTemplate,
|
|
77
|
-
|
|
114
|
+
|
|
115
|
+
// Gaming template - optimized for mobile games
|
|
116
|
+
// SKAN 4.0: purchase locks window, tutorial completion is medium value
|
|
78
117
|
gaming: {
|
|
79
118
|
name: 'gaming',
|
|
80
119
|
events: {
|
|
81
|
-
level_complete: { bits: [0], priority: 40 },
|
|
82
|
-
tutorial_complete: { bits: [1], priority: 60 },
|
|
83
|
-
purchase: { bits: [2], revenueBits: [3, 4, 5], priority: 100 },
|
|
84
|
-
achievement_unlocked: { bits: [6], priority: 30 },
|
|
85
|
-
session_start: { bits: [7], priority: 10 },
|
|
86
|
-
ad_watched: { bits: [0, 6], priority: 20 }
|
|
120
|
+
level_complete: { bits: [0], priority: 40, coarseValue: 'medium' as SKANCoarseValue },
|
|
121
|
+
tutorial_complete: { bits: [1], priority: 60, coarseValue: 'medium' as SKANCoarseValue },
|
|
122
|
+
purchase: { bits: [2], revenueBits: [3, 4, 5], priority: 100, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
|
|
123
|
+
achievement_unlocked: { bits: [6], priority: 30, coarseValue: 'low' as SKANCoarseValue },
|
|
124
|
+
session_start: { bits: [7], priority: 10, coarseValue: 'low' as SKANCoarseValue },
|
|
125
|
+
ad_watched: { bits: [0, 6], priority: 20, coarseValue: 'low' as SKANCoarseValue }
|
|
87
126
|
}
|
|
88
127
|
} as ConversionTemplate,
|
|
89
|
-
|
|
128
|
+
|
|
129
|
+
// Subscription template - optimized for subscription apps
|
|
130
|
+
// SKAN 4.0: subscribe/upgrade lock window, trial is medium value
|
|
90
131
|
subscription: {
|
|
91
132
|
name: 'subscription',
|
|
92
133
|
events: {
|
|
93
|
-
trial_start: { bits: [0], priority: 70 },
|
|
94
|
-
subscribe: { bits: [1], revenueBits: [2, 3, 4], priority: 100 },
|
|
95
|
-
upgrade: { bits: [1, 5], revenueBits: [2, 3, 4], priority: 90 },
|
|
96
|
-
cancel: { bits: [6], priority: 20 },
|
|
97
|
-
signup: { bits: [7], priority: 30 },
|
|
98
|
-
payment_method_added: { bits: [0, 7], priority: 50 }
|
|
134
|
+
trial_start: { bits: [0], priority: 70, coarseValue: 'medium' as SKANCoarseValue },
|
|
135
|
+
subscribe: { bits: [1], revenueBits: [2, 3, 4], priority: 100, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
|
|
136
|
+
upgrade: { bits: [1, 5], revenueBits: [2, 3, 4], priority: 90, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
|
|
137
|
+
cancel: { bits: [6], priority: 20, coarseValue: 'low' as SKANCoarseValue },
|
|
138
|
+
signup: { bits: [7], priority: 30, coarseValue: 'low' as SKANCoarseValue },
|
|
139
|
+
payment_method_added: { bits: [0, 7], priority: 50, coarseValue: 'medium' as SKANCoarseValue }
|
|
99
140
|
}
|
|
100
141
|
} as ConversionTemplate
|
|
101
142
|
};
|
package/src/datalyr-sdk-expo.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { createHttpClient, HttpClient } from './http-client';
|
|
31
31
|
import { createEventQueue, EventQueue } from './event-queue';
|
|
32
32
|
import { attributionManager, AttributionData } from './attribution';
|
|
33
|
+
import { journeyManager } from './journey';
|
|
33
34
|
import { createAutoEventsManager, AutoEventsManager } from './auto-events';
|
|
34
35
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
35
36
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
@@ -115,6 +116,24 @@ export class DatalyrSDKExpo {
|
|
|
115
116
|
await attributionManager.initialize();
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// Initialize journey tracking (for first-touch, last-touch, touchpoints)
|
|
120
|
+
await journeyManager.initialize();
|
|
121
|
+
|
|
122
|
+
// Record initial attribution to journey if this is a new session with attribution
|
|
123
|
+
const initialAttribution = attributionManager.getAttributionData();
|
|
124
|
+
if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
|
|
125
|
+
await journeyManager.recordAttribution(this.state.sessionId, {
|
|
126
|
+
source: initialAttribution.utm_source || initialAttribution.campaign_source,
|
|
127
|
+
medium: initialAttribution.utm_medium || initialAttribution.campaign_medium,
|
|
128
|
+
campaign: initialAttribution.utm_campaign || initialAttribution.campaign_name,
|
|
129
|
+
fbclid: initialAttribution.fbclid,
|
|
130
|
+
gclid: initialAttribution.gclid,
|
|
131
|
+
ttclid: initialAttribution.ttclid,
|
|
132
|
+
clickIdType: initialAttribution.fbclid ? 'fbclid' : initialAttribution.gclid ? 'gclid' : initialAttribution.ttclid ? 'ttclid' : undefined,
|
|
133
|
+
lyr: initialAttribution.lyr,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
if (this.state.config.enableAutoEvents) {
|
|
119
138
|
this.autoEventsManager = new AutoEventsManager(
|
|
120
139
|
this.track.bind(this),
|
|
@@ -441,8 +460,32 @@ export class DatalyrSDKExpo {
|
|
|
441
460
|
return this.state.anonymousId;
|
|
442
461
|
}
|
|
443
462
|
|
|
444
|
-
|
|
445
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Get detailed attribution data (includes journey tracking data)
|
|
465
|
+
*/
|
|
466
|
+
getAttributionData(): AttributionData & Record<string, any> {
|
|
467
|
+
const attribution = attributionManager.getAttributionData();
|
|
468
|
+
const journeyData = journeyManager.getAttributionData();
|
|
469
|
+
|
|
470
|
+
// Merge attribution with journey data
|
|
471
|
+
return {
|
|
472
|
+
...attribution,
|
|
473
|
+
...journeyData,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get journey tracking summary
|
|
479
|
+
*/
|
|
480
|
+
getJourneySummary() {
|
|
481
|
+
return journeyManager.getJourneySummary();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get full customer journey (all touchpoints)
|
|
486
|
+
*/
|
|
487
|
+
getJourney() {
|
|
488
|
+
return journeyManager.getJourney();
|
|
446
489
|
}
|
|
447
490
|
|
|
448
491
|
async setAttributionData(data: Partial<AttributionData>): Promise<void> {
|
|
@@ -477,6 +520,10 @@ export class DatalyrSDKExpo {
|
|
|
477
520
|
}
|
|
478
521
|
}
|
|
479
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Track event with automatic SKAdNetwork conversion value encoding
|
|
525
|
+
* Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
|
|
526
|
+
*/
|
|
480
527
|
async trackWithSKAdNetwork(event: string, properties?: EventData): Promise<void> {
|
|
481
528
|
await this.track(event, properties);
|
|
482
529
|
|
|
@@ -487,13 +534,15 @@ export class DatalyrSDKExpo {
|
|
|
487
534
|
return;
|
|
488
535
|
}
|
|
489
536
|
|
|
490
|
-
|
|
537
|
+
// Use SKAN 4.0 encoding (includes coarse value and lock window)
|
|
538
|
+
const result = DatalyrSDKExpo.conversionEncoder.encodeWithSKAN4(event, properties);
|
|
491
539
|
|
|
492
|
-
if (
|
|
493
|
-
|
|
540
|
+
if (result.fineValue > 0 || result.priority > 0) {
|
|
541
|
+
// Use SKAN 4.0 method (automatically falls back to SKAN 3.0 on older iOS)
|
|
542
|
+
const success = await SKAdNetworkBridge.updatePostbackConversionValue(result);
|
|
494
543
|
|
|
495
544
|
if (DatalyrSDKExpo.debugEnabled) {
|
|
496
|
-
debugLog(`
|
|
545
|
+
debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
|
|
497
546
|
}
|
|
498
547
|
}
|
|
499
548
|
}
|
package/src/datalyr-sdk.ts
CHANGED
|
@@ -27,10 +27,11 @@ import {
|
|
|
27
27
|
import { createHttpClient, HttpClient } from './http-client';
|
|
28
28
|
import { createEventQueue, EventQueue } from './event-queue';
|
|
29
29
|
import { attributionManager, AttributionData } from './attribution';
|
|
30
|
+
import { journeyManager } from './journey';
|
|
30
31
|
import { createAutoEventsManager, AutoEventsManager, SessionData } from './auto-events';
|
|
31
32
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
32
33
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
33
|
-
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
|
|
34
|
+
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
34
35
|
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
35
36
|
|
|
36
37
|
export class DatalyrSDK {
|
|
@@ -124,6 +125,24 @@ export class DatalyrSDK {
|
|
|
124
125
|
await attributionManager.initialize();
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// Initialize journey tracking (for first-touch, last-touch, touchpoints)
|
|
129
|
+
await journeyManager.initialize();
|
|
130
|
+
|
|
131
|
+
// Record initial attribution to journey if this is a new session with attribution
|
|
132
|
+
const initialAttribution = attributionManager.getAttributionData();
|
|
133
|
+
if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
|
|
134
|
+
await journeyManager.recordAttribution(this.state.sessionId, {
|
|
135
|
+
source: initialAttribution.utm_source || initialAttribution.campaign_source,
|
|
136
|
+
medium: initialAttribution.utm_medium || initialAttribution.campaign_medium,
|
|
137
|
+
campaign: initialAttribution.utm_campaign || initialAttribution.campaign_name,
|
|
138
|
+
fbclid: initialAttribution.fbclid,
|
|
139
|
+
gclid: initialAttribution.gclid,
|
|
140
|
+
ttclid: initialAttribution.ttclid,
|
|
141
|
+
clickIdType: initialAttribution.fbclid ? 'fbclid' : initialAttribution.gclid ? 'gclid' : initialAttribution.ttclid ? 'ttclid' : undefined,
|
|
142
|
+
lyr: initialAttribution.lyr,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
// Initialize auto-events manager (asynchronously to avoid blocking)
|
|
128
147
|
if (this.state.config.enableAutoEvents) {
|
|
129
148
|
this.autoEventsManager = new AutoEventsManager(
|
|
@@ -185,10 +204,14 @@ export class DatalyrSDK {
|
|
|
185
204
|
// Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
|
|
186
205
|
await appleSearchAdsIntegration.initialize(config.debug);
|
|
187
206
|
|
|
207
|
+
// Initialize Google Play Install Referrer (Android only)
|
|
208
|
+
await playInstallReferrerIntegration.initialize();
|
|
209
|
+
|
|
188
210
|
debugLog('Platform integrations initialized', {
|
|
189
211
|
meta: metaIntegration.isAvailable(),
|
|
190
212
|
tiktok: tiktokIntegration.isAvailable(),
|
|
191
213
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
214
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
192
215
|
});
|
|
193
216
|
|
|
194
217
|
// SDK initialized successfully - set state before tracking install event
|
|
@@ -490,6 +513,7 @@ export class DatalyrSDK {
|
|
|
490
513
|
currentUserId?: string;
|
|
491
514
|
queueStats: any;
|
|
492
515
|
attribution: any;
|
|
516
|
+
journey: any;
|
|
493
517
|
} {
|
|
494
518
|
return {
|
|
495
519
|
initialized: this.state.initialized,
|
|
@@ -500,6 +524,7 @@ export class DatalyrSDK {
|
|
|
500
524
|
currentUserId: this.state.currentUserId,
|
|
501
525
|
queueStats: this.eventQueue.getStats(),
|
|
502
526
|
attribution: attributionManager.getAttributionSummary(),
|
|
527
|
+
journey: journeyManager.getJourneySummary(),
|
|
503
528
|
};
|
|
504
529
|
}
|
|
505
530
|
|
|
@@ -511,10 +536,31 @@ export class DatalyrSDK {
|
|
|
511
536
|
}
|
|
512
537
|
|
|
513
538
|
/**
|
|
514
|
-
* Get detailed attribution data
|
|
539
|
+
* Get detailed attribution data (includes journey tracking data)
|
|
540
|
+
*/
|
|
541
|
+
getAttributionData(): AttributionData & Record<string, any> {
|
|
542
|
+
const attribution = attributionManager.getAttributionData();
|
|
543
|
+
const journeyData = journeyManager.getAttributionData();
|
|
544
|
+
|
|
545
|
+
// Merge attribution with journey data
|
|
546
|
+
return {
|
|
547
|
+
...attribution,
|
|
548
|
+
...journeyData,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get journey tracking summary
|
|
515
554
|
*/
|
|
516
|
-
|
|
517
|
-
return
|
|
555
|
+
getJourneySummary() {
|
|
556
|
+
return journeyManager.getJourneySummary();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get full customer journey (all touchpoints)
|
|
561
|
+
*/
|
|
562
|
+
getJourney() {
|
|
563
|
+
return journeyManager.getJourney();
|
|
518
564
|
}
|
|
519
565
|
|
|
520
566
|
/**
|
|
@@ -571,15 +617,16 @@ export class DatalyrSDK {
|
|
|
571
617
|
|
|
572
618
|
/**
|
|
573
619
|
* Track event with automatic SKAdNetwork conversion value encoding
|
|
620
|
+
* Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
|
|
574
621
|
*/
|
|
575
622
|
async trackWithSKAdNetwork(
|
|
576
|
-
event: string,
|
|
623
|
+
event: string,
|
|
577
624
|
properties?: EventData
|
|
578
625
|
): Promise<void> {
|
|
579
626
|
// Existing tracking (keep exactly as-is)
|
|
580
627
|
await this.track(event, properties);
|
|
581
628
|
|
|
582
|
-
//
|
|
629
|
+
// Automatic SKAdNetwork encoding with SKAN 4.0 support
|
|
583
630
|
if (!DatalyrSDK.conversionEncoder) {
|
|
584
631
|
if (DatalyrSDK.debugEnabled) {
|
|
585
632
|
errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
|
|
@@ -587,13 +634,15 @@ export class DatalyrSDK {
|
|
|
587
634
|
return;
|
|
588
635
|
}
|
|
589
636
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
637
|
+
// Use SKAN 4.0 encoding (includes coarse value and lock window)
|
|
638
|
+
const result = DatalyrSDK.conversionEncoder.encodeWithSKAN4(event, properties);
|
|
639
|
+
|
|
640
|
+
if (result.fineValue > 0 || result.priority > 0) {
|
|
641
|
+
// Use SKAN 4.0 method (automatically falls back to SKAN 3.0 on older iOS)
|
|
642
|
+
const success = await SKAdNetworkBridge.updatePostbackConversionValue(result);
|
|
643
|
+
|
|
595
644
|
if (DatalyrSDK.debugEnabled) {
|
|
596
|
-
debugLog(`
|
|
645
|
+
debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
|
|
597
646
|
}
|
|
598
647
|
} else if (DatalyrSDK.debugEnabled) {
|
|
599
648
|
debugLog(`No conversion value generated for event: ${event}`);
|
|
@@ -837,11 +886,12 @@ export class DatalyrSDK {
|
|
|
837
886
|
/**
|
|
838
887
|
* Get platform integration status
|
|
839
888
|
*/
|
|
840
|
-
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
889
|
+
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
|
|
841
890
|
return {
|
|
842
891
|
meta: metaIntegration.isAvailable(),
|
|
843
892
|
tiktok: tiktokIntegration.isAvailable(),
|
|
844
893
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
894
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
845
895
|
};
|
|
846
896
|
}
|
|
847
897
|
|
|
@@ -853,6 +903,15 @@ export class DatalyrSDK {
|
|
|
853
903
|
return appleSearchAdsIntegration.getAttributionData();
|
|
854
904
|
}
|
|
855
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Get Google Play Install Referrer attribution data (Android only)
|
|
908
|
+
* Returns referrer data if available, null otherwise
|
|
909
|
+
*/
|
|
910
|
+
getPlayInstallReferrer(): Record<string, any> | null {
|
|
911
|
+
const data = playInstallReferrerIntegration.getReferrerData();
|
|
912
|
+
return data ? playInstallReferrerIntegration.getAttributionData() : null;
|
|
913
|
+
}
|
|
914
|
+
|
|
856
915
|
/**
|
|
857
916
|
* Update tracking authorization status on all platform SDKs
|
|
858
917
|
* Call this AFTER the user responds to the ATT permission dialog
|
package/src/expo.ts
CHANGED
|
@@ -22,6 +22,10 @@ export * from './types';
|
|
|
22
22
|
// Export attribution manager
|
|
23
23
|
export { attributionManager } from './attribution';
|
|
24
24
|
|
|
25
|
+
// Export journey tracking
|
|
26
|
+
export { journeyManager } from './journey';
|
|
27
|
+
export type { TouchAttribution, TouchPoint } from './journey';
|
|
28
|
+
|
|
25
29
|
// Export auto-events
|
|
26
30
|
export { createAutoEventsManager, AutoEventsManager } from './auto-events';
|
|
27
31
|
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { Datalyr };
|
|
|
12
12
|
// Export types and utilities
|
|
13
13
|
export * from './types';
|
|
14
14
|
export { attributionManager } from './attribution';
|
|
15
|
+
export { journeyManager } from './journey';
|
|
16
|
+
export type { TouchAttribution, TouchPoint } from './journey';
|
|
15
17
|
export { createAutoEventsManager, AutoEventsManager } from './auto-events';
|
|
16
18
|
|
|
17
19
|
// Re-export utilities for advanced usage
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Platform SDK Integrations
|
|
3
|
-
* Meta (Facebook), TikTok,
|
|
3
|
+
* Meta (Facebook), TikTok, Apple Search Ads, and Google Play Install Referrer SDK wrappers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export { MetaIntegration, metaIntegration } from './meta-integration';
|
|
7
7
|
export { TikTokIntegration, tiktokIntegration } from './tiktok-integration';
|
|
8
8
|
export { AppleSearchAdsIntegration, appleSearchAdsIntegration } from './apple-search-ads-integration';
|
|
9
|
+
export { playInstallReferrerIntegration } from './play-install-referrer';
|
|
10
|
+
export type { PlayInstallReferrer } from './play-install-referrer';
|
|
@@ -20,14 +20,15 @@ export class MetaIntegration {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Initialize Meta SDK with configuration
|
|
23
|
+
* Supported on both iOS and Android via native modules
|
|
23
24
|
*/
|
|
24
25
|
async initialize(config: MetaConfig, debug: boolean = false): Promise<void> {
|
|
25
26
|
this.debug = debug;
|
|
26
27
|
this.config = config;
|
|
27
28
|
|
|
28
|
-
// Only available on iOS via native
|
|
29
|
-
if (Platform.OS !== 'ios') {
|
|
30
|
-
this.log('Meta SDK only available on iOS');
|
|
29
|
+
// Only available on iOS and Android via native modules
|
|
30
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
31
|
+
this.log('Meta SDK only available on iOS and Android');
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Play Install Referrer Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides Android install attribution via the Google Play Install Referrer API.
|
|
5
|
+
* This captures UTM parameters and click IDs passed through Google Play Store.
|
|
6
|
+
*
|
|
7
|
+
* How it works:
|
|
8
|
+
* 1. User clicks ad/link with UTM parameters
|
|
9
|
+
* 2. Google Play Store stores the referrer URL
|
|
10
|
+
* 3. On first app launch, SDK retrieves the referrer
|
|
11
|
+
* 4. Attribution data (utm_source, utm_medium, gclid, etc.) is extracted
|
|
12
|
+
*
|
|
13
|
+
* Requirements:
|
|
14
|
+
* - Android only (returns null on iOS)
|
|
15
|
+
* - Requires Google Play Install Referrer Library in build.gradle:
|
|
16
|
+
* implementation 'com.android.installreferrer:installreferrer:2.2'
|
|
17
|
+
*
|
|
18
|
+
* Attribution data captured:
|
|
19
|
+
* - referrer_url: Full referrer URL from Play Store
|
|
20
|
+
* - referrer_click_timestamp: When the referrer link was clicked
|
|
21
|
+
* - install_begin_timestamp: When the install began
|
|
22
|
+
* - gclid: Google Ads click ID (if present)
|
|
23
|
+
* - utm_source, utm_medium, utm_campaign, etc.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Platform, NativeModules } from 'react-native';
|
|
27
|
+
import { debugLog, errorLog } from '../utils';
|
|
28
|
+
|
|
29
|
+
export interface PlayInstallReferrer {
|
|
30
|
+
// Raw referrer URL from Play Store
|
|
31
|
+
referrerUrl: string;
|
|
32
|
+
// Timestamp when the referrer link was clicked (ms)
|
|
33
|
+
referrerClickTimestamp: number;
|
|
34
|
+
// Timestamp when the install began (ms)
|
|
35
|
+
installBeginTimestamp: number;
|
|
36
|
+
// Timestamp when install was completed (ms)
|
|
37
|
+
installCompleteTimestamp?: number;
|
|
38
|
+
// Google Ads click ID
|
|
39
|
+
gclid?: string;
|
|
40
|
+
// UTM Parameters
|
|
41
|
+
utmSource?: string;
|
|
42
|
+
utmMedium?: string;
|
|
43
|
+
utmCampaign?: string;
|
|
44
|
+
utmTerm?: string;
|
|
45
|
+
utmContent?: string;
|
|
46
|
+
// Additional parameters
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface PlayInstallReferrerModule {
|
|
51
|
+
getInstallReferrer(): Promise<PlayInstallReferrer | null>;
|
|
52
|
+
isAvailable(): Promise<boolean>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { DatalyrPlayInstallReferrer } = NativeModules as {
|
|
56
|
+
DatalyrPlayInstallReferrer?: PlayInstallReferrerModule;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Google Play Install Referrer Integration
|
|
61
|
+
*
|
|
62
|
+
* Retrieves install attribution data from Google Play Store.
|
|
63
|
+
* Only available on Android.
|
|
64
|
+
*/
|
|
65
|
+
class PlayInstallReferrerIntegration {
|
|
66
|
+
private referrerData: PlayInstallReferrer | null = null;
|
|
67
|
+
private initialized = false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if Play Install Referrer is available
|
|
71
|
+
*/
|
|
72
|
+
isAvailable(): boolean {
|
|
73
|
+
return Platform.OS === 'android' && !!DatalyrPlayInstallReferrer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize and fetch install referrer data
|
|
78
|
+
* Should be called once on first app launch
|
|
79
|
+
*/
|
|
80
|
+
async initialize(): Promise<void> {
|
|
81
|
+
if (this.initialized) return;
|
|
82
|
+
|
|
83
|
+
if (!this.isAvailable()) {
|
|
84
|
+
debugLog('[PlayInstallReferrer] Not available (iOS or native module missing)');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
this.referrerData = await this.fetchInstallReferrer();
|
|
90
|
+
this.initialized = true;
|
|
91
|
+
|
|
92
|
+
if (this.referrerData) {
|
|
93
|
+
debugLog('[PlayInstallReferrer] Install referrer fetched:', {
|
|
94
|
+
utmSource: this.referrerData.utmSource,
|
|
95
|
+
utmMedium: this.referrerData.utmMedium,
|
|
96
|
+
hasGclid: !!this.referrerData.gclid,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
errorLog('[PlayInstallReferrer] Failed to initialize:', error as Error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch install referrer from Play Store
|
|
106
|
+
*/
|
|
107
|
+
async fetchInstallReferrer(): Promise<PlayInstallReferrer | null> {
|
|
108
|
+
if (!this.isAvailable()) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const referrer = await DatalyrPlayInstallReferrer!.getInstallReferrer();
|
|
114
|
+
|
|
115
|
+
if (!referrer) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parse UTM parameters from referrer URL
|
|
120
|
+
const parsed = this.parseReferrerUrl(referrer.referrerUrl);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...referrer,
|
|
124
|
+
...parsed,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
errorLog('[PlayInstallReferrer] Error fetching referrer:', error as Error);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse referrer URL to extract UTM parameters and click IDs
|
|
134
|
+
*/
|
|
135
|
+
private parseReferrerUrl(referrerUrl: string): Partial<PlayInstallReferrer> {
|
|
136
|
+
const params: Partial<PlayInstallReferrer> = {};
|
|
137
|
+
|
|
138
|
+
if (!referrerUrl) return params;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Referrer URL is URL-encoded, decode it first
|
|
142
|
+
const decoded = decodeURIComponent(referrerUrl);
|
|
143
|
+
const searchParams = new URLSearchParams(decoded);
|
|
144
|
+
|
|
145
|
+
// Extract UTM parameters
|
|
146
|
+
params.utmSource = searchParams.get('utm_source') || undefined;
|
|
147
|
+
params.utmMedium = searchParams.get('utm_medium') || undefined;
|
|
148
|
+
params.utmCampaign = searchParams.get('utm_campaign') || undefined;
|
|
149
|
+
params.utmTerm = searchParams.get('utm_term') || undefined;
|
|
150
|
+
params.utmContent = searchParams.get('utm_content') || undefined;
|
|
151
|
+
|
|
152
|
+
// Extract click IDs
|
|
153
|
+
params.gclid = searchParams.get('gclid') || undefined;
|
|
154
|
+
|
|
155
|
+
// Store any additional parameters
|
|
156
|
+
searchParams.forEach((value, key) => {
|
|
157
|
+
if (!key.startsWith('utm_') && key !== 'gclid') {
|
|
158
|
+
params[key] = value;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
debugLog('[PlayInstallReferrer] Parsed referrer URL:', params);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
errorLog('[PlayInstallReferrer] Error parsing referrer URL:', error as Error);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return params;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get cached install referrer data
|
|
172
|
+
*/
|
|
173
|
+
getReferrerData(): PlayInstallReferrer | null {
|
|
174
|
+
return this.referrerData;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get attribution data in standard format
|
|
179
|
+
*/
|
|
180
|
+
getAttributionData(): Record<string, any> {
|
|
181
|
+
if (!this.referrerData) return {};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
// Install referrer specific
|
|
185
|
+
install_referrer_url: this.referrerData.referrerUrl,
|
|
186
|
+
referrer_click_timestamp: this.referrerData.referrerClickTimestamp,
|
|
187
|
+
install_begin_timestamp: this.referrerData.installBeginTimestamp,
|
|
188
|
+
|
|
189
|
+
// Standard attribution fields
|
|
190
|
+
gclid: this.referrerData.gclid,
|
|
191
|
+
utm_source: this.referrerData.utmSource,
|
|
192
|
+
utm_medium: this.referrerData.utmMedium,
|
|
193
|
+
utm_campaign: this.referrerData.utmCampaign,
|
|
194
|
+
utm_term: this.referrerData.utmTerm,
|
|
195
|
+
utm_content: this.referrerData.utmContent,
|
|
196
|
+
|
|
197
|
+
// Source indicators
|
|
198
|
+
attribution_source: 'play_install_referrer',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const playInstallReferrerIntegration = new PlayInstallReferrerIntegration();
|