@datalyr/react-native 1.2.0 → 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/CHANGELOG.md +8 -0
- package/README.md +30 -1
- 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/DatalyrNative.m +4 -0
- package/ios/DatalyrNative.swift +58 -1
- 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 +34 -2
- package/lib/datalyr-sdk.js +90 -8
- package/lib/index.d.ts +4 -1
- package/lib/index.js +2 -1
- package/lib/integrations/apple-search-ads-integration.d.ts +43 -0
- package/lib/integrations/apple-search-ads-integration.js +106 -0
- package/lib/integrations/index.d.ts +4 -1
- package/lib/integrations/index.js +3 -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 +67 -2
- package/lib/native/DatalyrNativeBridge.js +80 -7
- package/lib/native/SKAdNetworkBridge.d.ts +21 -0
- package/lib/native/SKAdNetworkBridge.js +54 -0
- package/package.json +9 -3
- package/src/ConversionValueEncoder.ts +67 -26
- package/src/datalyr-sdk-expo.ts +98 -9
- package/src/datalyr-sdk.ts +109 -14
- package/src/expo.ts +8 -0
- package/src/index.ts +6 -1
- package/src/integrations/apple-search-ads-integration.ts +119 -0
- package/src/integrations/index.ts +4 -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 +137 -9
- package/src/native/SKAdNetworkBridge.ts +86 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalyr/react-native",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking with bundled Meta and TikTok SDKs",
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"lib/",
|
|
16
16
|
"src/",
|
|
17
17
|
"ios/",
|
|
18
|
+
"android/",
|
|
18
19
|
"README.md",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"LICENSE",
|
|
@@ -38,10 +39,15 @@
|
|
|
38
39
|
"tiktok-attribution",
|
|
39
40
|
"google-attribution",
|
|
40
41
|
"ios-attribution",
|
|
42
|
+
"android-attribution",
|
|
43
|
+
"apple-search-ads",
|
|
44
|
+
"play-install-referrer",
|
|
45
|
+
"gclid",
|
|
41
46
|
"conversion-tracking",
|
|
42
47
|
"revenue-optimization",
|
|
43
48
|
"deferred-deep-linking",
|
|
44
|
-
"fbclid"
|
|
49
|
+
"fbclid",
|
|
50
|
+
"ttclid"
|
|
45
51
|
],
|
|
46
52
|
"author": "Datalyr",
|
|
47
53
|
"license": "MIT",
|
|
@@ -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,11 +30,13 @@ 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';
|
|
36
|
-
import { metaIntegration, tiktokIntegration } from './integrations';
|
|
37
|
+
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
|
|
37
38
|
import { DeferredDeepLinkResult } from './types';
|
|
39
|
+
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
38
40
|
|
|
39
41
|
export class DatalyrSDKExpo {
|
|
40
42
|
private state: SDKState;
|
|
@@ -114,6 +116,24 @@ export class DatalyrSDKExpo {
|
|
|
114
116
|
await attributionManager.initialize();
|
|
115
117
|
}
|
|
116
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
|
+
|
|
117
137
|
if (this.state.config.enableAutoEvents) {
|
|
118
138
|
this.autoEventsManager = new AutoEventsManager(
|
|
119
139
|
this.track.bind(this),
|
|
@@ -176,6 +196,14 @@ export class DatalyrSDKExpo {
|
|
|
176
196
|
}
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
// Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
|
|
200
|
+
await appleSearchAdsIntegration.initialize(config.debug);
|
|
201
|
+
debugLog('Platform integrations initialized', {
|
|
202
|
+
meta: metaIntegration.isAvailable(),
|
|
203
|
+
tiktok: tiktokIntegration.isAvailable(),
|
|
204
|
+
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
205
|
+
});
|
|
206
|
+
|
|
179
207
|
this.state.initialized = true;
|
|
180
208
|
|
|
181
209
|
if (attributionManager.isInstall()) {
|
|
@@ -432,8 +460,32 @@ export class DatalyrSDKExpo {
|
|
|
432
460
|
return this.state.anonymousId;
|
|
433
461
|
}
|
|
434
462
|
|
|
435
|
-
|
|
436
|
-
|
|
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();
|
|
437
489
|
}
|
|
438
490
|
|
|
439
491
|
async setAttributionData(data: Partial<AttributionData>): Promise<void> {
|
|
@@ -468,6 +520,10 @@ export class DatalyrSDKExpo {
|
|
|
468
520
|
}
|
|
469
521
|
}
|
|
470
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
|
+
*/
|
|
471
527
|
async trackWithSKAdNetwork(event: string, properties?: EventData): Promise<void> {
|
|
472
528
|
await this.track(event, properties);
|
|
473
529
|
|
|
@@ -478,13 +534,15 @@ export class DatalyrSDKExpo {
|
|
|
478
534
|
return;
|
|
479
535
|
}
|
|
480
536
|
|
|
481
|
-
|
|
537
|
+
// Use SKAN 4.0 encoding (includes coarse value and lock window)
|
|
538
|
+
const result = DatalyrSDKExpo.conversionEncoder.encodeWithSKAN4(event, properties);
|
|
482
539
|
|
|
483
|
-
if (
|
|
484
|
-
|
|
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);
|
|
485
543
|
|
|
486
544
|
if (DatalyrSDKExpo.debugEnabled) {
|
|
487
|
-
debugLog(`
|
|
545
|
+
debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
|
|
488
546
|
}
|
|
489
547
|
}
|
|
490
548
|
}
|
|
@@ -636,13 +694,22 @@ export class DatalyrSDKExpo {
|
|
|
636
694
|
return null;
|
|
637
695
|
}
|
|
638
696
|
|
|
639
|
-
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
|
|
697
|
+
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
640
698
|
return {
|
|
641
699
|
meta: metaIntegration.isAvailable(),
|
|
642
700
|
tiktok: tiktokIntegration.isAvailable(),
|
|
701
|
+
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
643
702
|
};
|
|
644
703
|
}
|
|
645
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Get Apple Search Ads attribution data
|
|
707
|
+
* Returns attribution if user installed via Apple Search Ads, null otherwise
|
|
708
|
+
*/
|
|
709
|
+
getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
|
|
710
|
+
return appleSearchAdsIntegration.getAttributionData();
|
|
711
|
+
}
|
|
712
|
+
|
|
646
713
|
async updateTrackingAuthorization(authorized: boolean): Promise<void> {
|
|
647
714
|
if (metaIntegration.isAvailable()) {
|
|
648
715
|
metaIntegration.updateTrackingAuthorization(authorized);
|
|
@@ -694,6 +761,22 @@ export class DatalyrSDKExpo {
|
|
|
694
761
|
const attributionData = attributionManager.getAttributionData();
|
|
695
762
|
const networkType = await getNetworkType();
|
|
696
763
|
|
|
764
|
+
// Get Apple Search Ads attribution if available
|
|
765
|
+
const asaAttribution = appleSearchAdsIntegration.getAttributionData();
|
|
766
|
+
const asaData = asaAttribution?.attribution ? {
|
|
767
|
+
asa_campaign_id: asaAttribution.campaignId,
|
|
768
|
+
asa_campaign_name: asaAttribution.campaignName,
|
|
769
|
+
asa_ad_group_id: asaAttribution.adGroupId,
|
|
770
|
+
asa_ad_group_name: asaAttribution.adGroupName,
|
|
771
|
+
asa_keyword_id: asaAttribution.keywordId,
|
|
772
|
+
asa_keyword: asaAttribution.keyword,
|
|
773
|
+
asa_org_id: asaAttribution.orgId,
|
|
774
|
+
asa_org_name: asaAttribution.orgName,
|
|
775
|
+
asa_click_date: asaAttribution.clickDate,
|
|
776
|
+
asa_conversion_type: asaAttribution.conversionType,
|
|
777
|
+
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
778
|
+
} : {};
|
|
779
|
+
|
|
697
780
|
const payload: EventPayload = {
|
|
698
781
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
699
782
|
visitorId: this.state.visitorId,
|
|
@@ -713,6 +796,8 @@ export class DatalyrSDKExpo {
|
|
|
713
796
|
timestamp: Date.now(),
|
|
714
797
|
sdk_variant: 'expo',
|
|
715
798
|
...attributionData,
|
|
799
|
+
// Apple Search Ads attribution
|
|
800
|
+
...asaData,
|
|
716
801
|
},
|
|
717
802
|
fingerprintData,
|
|
718
803
|
source: 'mobile_app',
|
|
@@ -945,10 +1030,14 @@ export class DatalyrExpo {
|
|
|
945
1030
|
return datalyrExpo.getDeferredAttributionData();
|
|
946
1031
|
}
|
|
947
1032
|
|
|
948
|
-
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
|
|
1033
|
+
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
949
1034
|
return datalyrExpo.getPlatformIntegrationStatus();
|
|
950
1035
|
}
|
|
951
1036
|
|
|
1037
|
+
static getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
|
|
1038
|
+
return datalyrExpo.getAppleSearchAdsAttribution();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
952
1041
|
static async updateTrackingAuthorization(authorized: boolean): Promise<void> {
|
|
953
1042
|
await datalyrExpo.updateTrackingAuthorization(authorized);
|
|
954
1043
|
}
|
package/src/datalyr-sdk.ts
CHANGED
|
@@ -27,10 +27,12 @@ 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 } from './integrations';
|
|
34
|
+
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
35
|
+
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
34
36
|
|
|
35
37
|
export class DatalyrSDK {
|
|
36
38
|
private state: SDKState;
|
|
@@ -123,6 +125,24 @@ export class DatalyrSDK {
|
|
|
123
125
|
await attributionManager.initialize();
|
|
124
126
|
}
|
|
125
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
|
+
|
|
126
146
|
// Initialize auto-events manager (asynchronously to avoid blocking)
|
|
127
147
|
if (this.state.config.enableAutoEvents) {
|
|
128
148
|
this.autoEventsManager = new AutoEventsManager(
|
|
@@ -181,9 +201,17 @@ export class DatalyrSDK {
|
|
|
181
201
|
await tiktokIntegration.initialize(config.tiktok, config.debug);
|
|
182
202
|
}
|
|
183
203
|
|
|
204
|
+
// Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
|
|
205
|
+
await appleSearchAdsIntegration.initialize(config.debug);
|
|
206
|
+
|
|
207
|
+
// Initialize Google Play Install Referrer (Android only)
|
|
208
|
+
await playInstallReferrerIntegration.initialize();
|
|
209
|
+
|
|
184
210
|
debugLog('Platform integrations initialized', {
|
|
185
211
|
meta: metaIntegration.isAvailable(),
|
|
186
212
|
tiktok: tiktokIntegration.isAvailable(),
|
|
213
|
+
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
214
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
187
215
|
});
|
|
188
216
|
|
|
189
217
|
// SDK initialized successfully - set state before tracking install event
|
|
@@ -485,6 +513,7 @@ export class DatalyrSDK {
|
|
|
485
513
|
currentUserId?: string;
|
|
486
514
|
queueStats: any;
|
|
487
515
|
attribution: any;
|
|
516
|
+
journey: any;
|
|
488
517
|
} {
|
|
489
518
|
return {
|
|
490
519
|
initialized: this.state.initialized,
|
|
@@ -495,6 +524,7 @@ export class DatalyrSDK {
|
|
|
495
524
|
currentUserId: this.state.currentUserId,
|
|
496
525
|
queueStats: this.eventQueue.getStats(),
|
|
497
526
|
attribution: attributionManager.getAttributionSummary(),
|
|
527
|
+
journey: journeyManager.getJourneySummary(),
|
|
498
528
|
};
|
|
499
529
|
}
|
|
500
530
|
|
|
@@ -506,10 +536,31 @@ export class DatalyrSDK {
|
|
|
506
536
|
}
|
|
507
537
|
|
|
508
538
|
/**
|
|
509
|
-
* 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
|
|
554
|
+
*/
|
|
555
|
+
getJourneySummary() {
|
|
556
|
+
return journeyManager.getJourneySummary();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get full customer journey (all touchpoints)
|
|
510
561
|
*/
|
|
511
|
-
|
|
512
|
-
return
|
|
562
|
+
getJourney() {
|
|
563
|
+
return journeyManager.getJourney();
|
|
513
564
|
}
|
|
514
565
|
|
|
515
566
|
/**
|
|
@@ -566,15 +617,16 @@ export class DatalyrSDK {
|
|
|
566
617
|
|
|
567
618
|
/**
|
|
568
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
|
|
569
621
|
*/
|
|
570
622
|
async trackWithSKAdNetwork(
|
|
571
|
-
event: string,
|
|
623
|
+
event: string,
|
|
572
624
|
properties?: EventData
|
|
573
625
|
): Promise<void> {
|
|
574
626
|
// Existing tracking (keep exactly as-is)
|
|
575
627
|
await this.track(event, properties);
|
|
576
628
|
|
|
577
|
-
//
|
|
629
|
+
// Automatic SKAdNetwork encoding with SKAN 4.0 support
|
|
578
630
|
if (!DatalyrSDK.conversionEncoder) {
|
|
579
631
|
if (DatalyrSDK.debugEnabled) {
|
|
580
632
|
errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
|
|
@@ -582,13 +634,15 @@ export class DatalyrSDK {
|
|
|
582
634
|
return;
|
|
583
635
|
}
|
|
584
636
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
+
|
|
590
644
|
if (DatalyrSDK.debugEnabled) {
|
|
591
|
-
debugLog(`
|
|
645
|
+
debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
|
|
592
646
|
}
|
|
593
647
|
} else if (DatalyrSDK.debugEnabled) {
|
|
594
648
|
debugLog(`No conversion value generated for event: ${event}`);
|
|
@@ -832,13 +886,32 @@ export class DatalyrSDK {
|
|
|
832
886
|
/**
|
|
833
887
|
* Get platform integration status
|
|
834
888
|
*/
|
|
835
|
-
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
|
|
889
|
+
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
|
|
836
890
|
return {
|
|
837
891
|
meta: metaIntegration.isAvailable(),
|
|
838
892
|
tiktok: tiktokIntegration.isAvailable(),
|
|
893
|
+
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
894
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
839
895
|
};
|
|
840
896
|
}
|
|
841
897
|
|
|
898
|
+
/**
|
|
899
|
+
* Get Apple Search Ads attribution data
|
|
900
|
+
* Returns attribution if user installed via Apple Search Ads, null otherwise
|
|
901
|
+
*/
|
|
902
|
+
getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
|
|
903
|
+
return appleSearchAdsIntegration.getAttributionData();
|
|
904
|
+
}
|
|
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
|
+
|
|
842
915
|
/**
|
|
843
916
|
* Update tracking authorization status on all platform SDKs
|
|
844
917
|
* Call this AFTER the user responds to the ATT permission dialog
|
|
@@ -919,6 +992,22 @@ export class DatalyrSDK {
|
|
|
919
992
|
const fingerprintData = await createFingerprintData();
|
|
920
993
|
const attributionData = attributionManager.getAttributionData();
|
|
921
994
|
|
|
995
|
+
// Get Apple Search Ads attribution if available
|
|
996
|
+
const asaAttribution = appleSearchAdsIntegration.getAttributionData();
|
|
997
|
+
const asaData = asaAttribution?.attribution ? {
|
|
998
|
+
asa_campaign_id: asaAttribution.campaignId,
|
|
999
|
+
asa_campaign_name: asaAttribution.campaignName,
|
|
1000
|
+
asa_ad_group_id: asaAttribution.adGroupId,
|
|
1001
|
+
asa_ad_group_name: asaAttribution.adGroupName,
|
|
1002
|
+
asa_keyword_id: asaAttribution.keywordId,
|
|
1003
|
+
asa_keyword: asaAttribution.keyword,
|
|
1004
|
+
asa_org_id: asaAttribution.orgId,
|
|
1005
|
+
asa_org_name: asaAttribution.orgName,
|
|
1006
|
+
asa_click_date: asaAttribution.clickDate,
|
|
1007
|
+
asa_conversion_type: asaAttribution.conversionType,
|
|
1008
|
+
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
1009
|
+
} : {};
|
|
1010
|
+
|
|
922
1011
|
const payload: EventPayload = {
|
|
923
1012
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
924
1013
|
visitorId: this.state.visitorId,
|
|
@@ -940,6 +1029,8 @@ export class DatalyrSDK {
|
|
|
940
1029
|
timestamp: Date.now(),
|
|
941
1030
|
// Attribution data
|
|
942
1031
|
...attributionData,
|
|
1032
|
+
// Apple Search Ads attribution
|
|
1033
|
+
...asaData,
|
|
943
1034
|
},
|
|
944
1035
|
fingerprintData,
|
|
945
1036
|
source: 'mobile_app',
|
|
@@ -1239,10 +1330,14 @@ export class Datalyr {
|
|
|
1239
1330
|
return datalyr.getDeferredAttributionData();
|
|
1240
1331
|
}
|
|
1241
1332
|
|
|
1242
|
-
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
|
|
1333
|
+
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
1243
1334
|
return datalyr.getPlatformIntegrationStatus();
|
|
1244
1335
|
}
|
|
1245
1336
|
|
|
1337
|
+
static getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
|
|
1338
|
+
return datalyr.getAppleSearchAdsAttribution();
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1246
1341
|
static async updateTrackingAuthorization(enabled: boolean): Promise<void> {
|
|
1247
1342
|
await datalyr.updateTrackingAuthorization(enabled);
|
|
1248
1343
|
}
|
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
|
|
|
@@ -52,5 +56,9 @@ export * from './event-queue';
|
|
|
52
56
|
export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
53
57
|
export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
54
58
|
|
|
59
|
+
// Export platform integrations
|
|
60
|
+
export { appleSearchAdsIntegration } from './integrations';
|
|
61
|
+
export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
62
|
+
|
|
55
63
|
// Default export
|
|
56
64
|
export default datalyrExpo;
|
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
|
|
@@ -27,7 +29,10 @@ export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEn
|
|
|
27
29
|
export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
28
30
|
|
|
29
31
|
// Export platform integrations
|
|
30
|
-
export { metaIntegration, tiktokIntegration } from './integrations';
|
|
32
|
+
export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
|
|
33
|
+
|
|
34
|
+
// Export native bridge types
|
|
35
|
+
export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
31
36
|
|
|
32
37
|
// Default export for compatibility
|
|
33
38
|
export default DatalyrSDK;
|