@datalyr/react-native 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +145 -9
- 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 +400 -1
- package/ios/PrivacyInfo.xcprivacy +48 -0
- package/lib/ConversionValueEncoder.d.ts +13 -1
- package/lib/ConversionValueEncoder.js +57 -23
- package/lib/datalyr-sdk.d.ts +31 -2
- package/lib/datalyr-sdk.js +138 -30
- package/lib/index.d.ts +5 -1
- package/lib/index.js +4 -1
- 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 +78 -0
- package/lib/integrations/play-install-referrer.js +166 -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 +142 -0
- package/lib/native/SKAdNetworkBridge.js +328 -0
- package/lib/network-status.d.ts +84 -0
- package/lib/network-status.js +281 -0
- package/lib/types.d.ts +51 -0
- package/lib/utils.d.ts +6 -1
- package/lib/utils.js +52 -2
- package/package.json +13 -4
- package/src/ConversionValueEncoder.ts +67 -26
- package/src/datalyr-sdk-expo.ts +55 -6
- package/src/datalyr-sdk.ts +161 -38
- package/src/expo.ts +4 -0
- package/src/index.ts +7 -1
- package/src/integrations/index.ts +3 -1
- package/src/integrations/meta-integration.ts +4 -3
- package/src/integrations/play-install-referrer.ts +218 -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 +481 -2
- package/src/network-status.ts +312 -0
- package/src/types.ts +74 -6
- package/src/utils.ts +62 -6
|
@@ -18,14 +18,15 @@ export class MetaIntegration {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Initialize Meta SDK with configuration
|
|
21
|
+
* Supported on both iOS and Android via native modules
|
|
21
22
|
*/
|
|
22
23
|
async initialize(config, debug = false) {
|
|
23
24
|
var _a;
|
|
24
25
|
this.debug = debug;
|
|
25
26
|
this.config = config;
|
|
26
|
-
// Only available on iOS via native
|
|
27
|
-
if (Platform.OS !== 'ios') {
|
|
28
|
-
this.log('Meta SDK only available on iOS');
|
|
27
|
+
// Only available on iOS and Android via native modules
|
|
28
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
29
|
+
this.log('Meta SDK only available on iOS and Android');
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
this.available = isNativeModuleAvailable();
|
|
@@ -0,0 +1,78 @@
|
|
|
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 (standard)
|
|
23
|
+
* - gbraid: Google Ads privacy-safe click ID (iOS App campaigns)
|
|
24
|
+
* - wbraid: Google Ads privacy-safe click ID (Web-to-App campaigns)
|
|
25
|
+
* - utm_source, utm_medium, utm_campaign, etc.
|
|
26
|
+
*/
|
|
27
|
+
export interface PlayInstallReferrer {
|
|
28
|
+
referrerUrl: string;
|
|
29
|
+
referrerClickTimestamp: number;
|
|
30
|
+
installBeginTimestamp: number;
|
|
31
|
+
installCompleteTimestamp?: number;
|
|
32
|
+
gclid?: string;
|
|
33
|
+
gbraid?: string;
|
|
34
|
+
wbraid?: string;
|
|
35
|
+
utmSource?: string;
|
|
36
|
+
utmMedium?: string;
|
|
37
|
+
utmCampaign?: string;
|
|
38
|
+
utmTerm?: string;
|
|
39
|
+
utmContent?: string;
|
|
40
|
+
[key: string]: any;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Google Play Install Referrer Integration
|
|
44
|
+
*
|
|
45
|
+
* Retrieves install attribution data from Google Play Store.
|
|
46
|
+
* Only available on Android.
|
|
47
|
+
*/
|
|
48
|
+
declare class PlayInstallReferrerIntegration {
|
|
49
|
+
private referrerData;
|
|
50
|
+
private initialized;
|
|
51
|
+
/**
|
|
52
|
+
* Check if Play Install Referrer is available
|
|
53
|
+
*/
|
|
54
|
+
isAvailable(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Initialize and fetch install referrer data
|
|
57
|
+
* Should be called once on first app launch
|
|
58
|
+
*/
|
|
59
|
+
initialize(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Fetch install referrer from Play Store
|
|
62
|
+
*/
|
|
63
|
+
fetchInstallReferrer(): Promise<PlayInstallReferrer | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Parse referrer URL to extract UTM parameters and click IDs
|
|
66
|
+
*/
|
|
67
|
+
private parseReferrerUrl;
|
|
68
|
+
/**
|
|
69
|
+
* Get cached install referrer data
|
|
70
|
+
*/
|
|
71
|
+
getReferrerData(): PlayInstallReferrer | null;
|
|
72
|
+
/**
|
|
73
|
+
* Get attribution data in standard format
|
|
74
|
+
*/
|
|
75
|
+
getAttributionData(): Record<string, any>;
|
|
76
|
+
}
|
|
77
|
+
export declare const playInstallReferrerIntegration: PlayInstallReferrerIntegration;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
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 (standard)
|
|
23
|
+
* - gbraid: Google Ads privacy-safe click ID (iOS App campaigns)
|
|
24
|
+
* - wbraid: Google Ads privacy-safe click ID (Web-to-App campaigns)
|
|
25
|
+
* - utm_source, utm_medium, utm_campaign, etc.
|
|
26
|
+
*/
|
|
27
|
+
import { Platform, NativeModules } from 'react-native';
|
|
28
|
+
import { debugLog, errorLog } from '../utils';
|
|
29
|
+
const { DatalyrPlayInstallReferrer } = NativeModules;
|
|
30
|
+
/**
|
|
31
|
+
* Google Play Install Referrer Integration
|
|
32
|
+
*
|
|
33
|
+
* Retrieves install attribution data from Google Play Store.
|
|
34
|
+
* Only available on Android.
|
|
35
|
+
*/
|
|
36
|
+
class PlayInstallReferrerIntegration {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.referrerData = null;
|
|
39
|
+
this.initialized = false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if Play Install Referrer is available
|
|
43
|
+
*/
|
|
44
|
+
isAvailable() {
|
|
45
|
+
return Platform.OS === 'android' && !!DatalyrPlayInstallReferrer;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Initialize and fetch install referrer data
|
|
49
|
+
* Should be called once on first app launch
|
|
50
|
+
*/
|
|
51
|
+
async initialize() {
|
|
52
|
+
if (this.initialized)
|
|
53
|
+
return;
|
|
54
|
+
if (!this.isAvailable()) {
|
|
55
|
+
debugLog('[PlayInstallReferrer] Not available (iOS or native module missing)');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
this.referrerData = await this.fetchInstallReferrer();
|
|
60
|
+
this.initialized = true;
|
|
61
|
+
if (this.referrerData) {
|
|
62
|
+
debugLog('[PlayInstallReferrer] Install referrer fetched:', {
|
|
63
|
+
utmSource: this.referrerData.utmSource,
|
|
64
|
+
utmMedium: this.referrerData.utmMedium,
|
|
65
|
+
hasGclid: !!this.referrerData.gclid,
|
|
66
|
+
hasGbraid: !!this.referrerData.gbraid,
|
|
67
|
+
hasWbraid: !!this.referrerData.wbraid,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
errorLog('[PlayInstallReferrer] Failed to initialize:', error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fetch install referrer from Play Store
|
|
77
|
+
*/
|
|
78
|
+
async fetchInstallReferrer() {
|
|
79
|
+
if (!this.isAvailable()) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const referrer = await DatalyrPlayInstallReferrer.getInstallReferrer();
|
|
84
|
+
if (!referrer) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Parse UTM parameters from referrer URL
|
|
88
|
+
const parsed = this.parseReferrerUrl(referrer.referrerUrl);
|
|
89
|
+
return {
|
|
90
|
+
...referrer,
|
|
91
|
+
...parsed,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
errorLog('[PlayInstallReferrer] Error fetching referrer:', error);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse referrer URL to extract UTM parameters and click IDs
|
|
101
|
+
*/
|
|
102
|
+
parseReferrerUrl(referrerUrl) {
|
|
103
|
+
const params = {};
|
|
104
|
+
if (!referrerUrl)
|
|
105
|
+
return params;
|
|
106
|
+
try {
|
|
107
|
+
// Referrer URL is URL-encoded, decode it first
|
|
108
|
+
const decoded = decodeURIComponent(referrerUrl);
|
|
109
|
+
const searchParams = new URLSearchParams(decoded);
|
|
110
|
+
// Extract UTM parameters
|
|
111
|
+
params.utmSource = searchParams.get('utm_source') || undefined;
|
|
112
|
+
params.utmMedium = searchParams.get('utm_medium') || undefined;
|
|
113
|
+
params.utmCampaign = searchParams.get('utm_campaign') || undefined;
|
|
114
|
+
params.utmTerm = searchParams.get('utm_term') || undefined;
|
|
115
|
+
params.utmContent = searchParams.get('utm_content') || undefined;
|
|
116
|
+
// Extract click IDs (gclid, gbraid, wbraid)
|
|
117
|
+
params.gclid = searchParams.get('gclid') || undefined;
|
|
118
|
+
params.gbraid = searchParams.get('gbraid') || undefined;
|
|
119
|
+
params.wbraid = searchParams.get('wbraid') || undefined;
|
|
120
|
+
// Store any additional parameters
|
|
121
|
+
const knownParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'gbraid', 'wbraid'];
|
|
122
|
+
searchParams.forEach((value, key) => {
|
|
123
|
+
if (!knownParams.includes(key) && !key.startsWith('utm_')) {
|
|
124
|
+
params[key] = value;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
debugLog('[PlayInstallReferrer] Parsed referrer URL:', params);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
errorLog('[PlayInstallReferrer] Error parsing referrer URL:', error);
|
|
131
|
+
}
|
|
132
|
+
return params;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get cached install referrer data
|
|
136
|
+
*/
|
|
137
|
+
getReferrerData() {
|
|
138
|
+
return this.referrerData;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get attribution data in standard format
|
|
142
|
+
*/
|
|
143
|
+
getAttributionData() {
|
|
144
|
+
if (!this.referrerData)
|
|
145
|
+
return {};
|
|
146
|
+
return {
|
|
147
|
+
// Install referrer specific
|
|
148
|
+
install_referrer_url: this.referrerData.referrerUrl,
|
|
149
|
+
referrer_click_timestamp: this.referrerData.referrerClickTimestamp,
|
|
150
|
+
install_begin_timestamp: this.referrerData.installBeginTimestamp,
|
|
151
|
+
// Google Ads click IDs (gclid is standard, gbraid/wbraid are privacy-safe alternatives)
|
|
152
|
+
gclid: this.referrerData.gclid,
|
|
153
|
+
gbraid: this.referrerData.gbraid,
|
|
154
|
+
wbraid: this.referrerData.wbraid,
|
|
155
|
+
// UTM parameters
|
|
156
|
+
utm_source: this.referrerData.utmSource,
|
|
157
|
+
utm_medium: this.referrerData.utmMedium,
|
|
158
|
+
utm_campaign: this.referrerData.utmCampaign,
|
|
159
|
+
utm_term: this.referrerData.utmTerm,
|
|
160
|
+
utm_content: this.referrerData.utmContent,
|
|
161
|
+
// Source indicators
|
|
162
|
+
attribution_source: 'play_install_referrer',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export const playInstallReferrerIntegration = new PlayInstallReferrerIntegration();
|
|
@@ -41,13 +41,14 @@ export class TikTokIntegration {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Initialize TikTok SDK with configuration
|
|
44
|
+
* Supported on both iOS and Android via native modules
|
|
44
45
|
*/
|
|
45
46
|
async initialize(config, debug = false) {
|
|
46
47
|
this.debug = debug;
|
|
47
48
|
this.config = config;
|
|
48
|
-
// Only available on iOS via native
|
|
49
|
-
if (Platform.OS !== 'ios') {
|
|
50
|
-
this.log('TikTok SDK only available on iOS');
|
|
49
|
+
// Only available on iOS and Android via native modules
|
|
50
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
51
|
+
this.log('TikTok SDK only available on iOS and Android');
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
this.available = isNativeModuleAvailable();
|
package/lib/journey.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journey Tracking Module for React Native
|
|
3
|
+
* Mirrors the Web SDK's journey tracking capabilities:
|
|
4
|
+
* - First-touch attribution with 90-day expiration
|
|
5
|
+
* - Last-touch attribution with 90-day expiration
|
|
6
|
+
* - Up to 30 touchpoints stored
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Attribution data for a touch
|
|
10
|
+
*/
|
|
11
|
+
export interface TouchAttribution {
|
|
12
|
+
timestamp: number;
|
|
13
|
+
expires_at: number;
|
|
14
|
+
captured_at: number;
|
|
15
|
+
source?: string;
|
|
16
|
+
medium?: string;
|
|
17
|
+
campaign?: string;
|
|
18
|
+
term?: string;
|
|
19
|
+
content?: string;
|
|
20
|
+
clickId?: string;
|
|
21
|
+
clickIdType?: string;
|
|
22
|
+
fbclid?: string;
|
|
23
|
+
gclid?: string;
|
|
24
|
+
ttclid?: string;
|
|
25
|
+
gbraid?: string;
|
|
26
|
+
wbraid?: string;
|
|
27
|
+
lyr?: string;
|
|
28
|
+
landingPage?: string;
|
|
29
|
+
referrer?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A single touchpoint in the customer journey
|
|
33
|
+
*/
|
|
34
|
+
export interface TouchPoint {
|
|
35
|
+
timestamp: number;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
source?: string;
|
|
38
|
+
medium?: string;
|
|
39
|
+
campaign?: string;
|
|
40
|
+
clickIdType?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Journey manager for tracking customer touchpoints
|
|
44
|
+
*/
|
|
45
|
+
export declare class JourneyManager {
|
|
46
|
+
private firstTouch;
|
|
47
|
+
private lastTouch;
|
|
48
|
+
private journey;
|
|
49
|
+
private initialized;
|
|
50
|
+
/**
|
|
51
|
+
* Initialize journey tracking by loading persisted data
|
|
52
|
+
*/
|
|
53
|
+
initialize(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Check if attribution has expired
|
|
56
|
+
*/
|
|
57
|
+
private isExpired;
|
|
58
|
+
/**
|
|
59
|
+
* Store first touch attribution (only if not already set or expired)
|
|
60
|
+
*/
|
|
61
|
+
storeFirstTouch(attribution: Partial<TouchAttribution>): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Get first touch attribution (null if expired)
|
|
64
|
+
*/
|
|
65
|
+
getFirstTouch(): TouchAttribution | null;
|
|
66
|
+
/**
|
|
67
|
+
* Store last touch attribution (always updates)
|
|
68
|
+
*/
|
|
69
|
+
storeLastTouch(attribution: Partial<TouchAttribution>): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Get last touch attribution (null if expired)
|
|
72
|
+
*/
|
|
73
|
+
getLastTouch(): TouchAttribution | null;
|
|
74
|
+
/**
|
|
75
|
+
* Add a touchpoint to the customer journey
|
|
76
|
+
*/
|
|
77
|
+
addTouchpoint(sessionId: string, attribution: Partial<TouchAttribution>): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Get customer journey (all touchpoints)
|
|
80
|
+
*/
|
|
81
|
+
getJourney(): TouchPoint[];
|
|
82
|
+
/**
|
|
83
|
+
* Record attribution from a deep link or install
|
|
84
|
+
* Updates first-touch (if not set), last-touch, and adds touchpoint
|
|
85
|
+
*/
|
|
86
|
+
recordAttribution(sessionId: string, attribution: Partial<TouchAttribution>): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Get attribution data for events (mirrors Web SDK format)
|
|
89
|
+
*/
|
|
90
|
+
getAttributionData(): Record<string, any>;
|
|
91
|
+
/**
|
|
92
|
+
* Clear all journey data (for testing/reset)
|
|
93
|
+
*/
|
|
94
|
+
clearJourney(): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Get journey summary for debugging
|
|
97
|
+
*/
|
|
98
|
+
getJourneySummary(): {
|
|
99
|
+
hasFirstTouch: boolean;
|
|
100
|
+
hasLastTouch: boolean;
|
|
101
|
+
touchpointCount: number;
|
|
102
|
+
daysSinceFirstTouch: number;
|
|
103
|
+
sources: string[];
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export declare const journeyManager: JourneyManager;
|
package/lib/journey.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journey Tracking Module for React Native
|
|
3
|
+
* Mirrors the Web SDK's journey tracking capabilities:
|
|
4
|
+
* - First-touch attribution with 90-day expiration
|
|
5
|
+
* - Last-touch attribution with 90-day expiration
|
|
6
|
+
* - Up to 30 touchpoints stored
|
|
7
|
+
*/
|
|
8
|
+
import { Storage, debugLog, errorLog } from './utils';
|
|
9
|
+
// Storage keys for journey data
|
|
10
|
+
const JOURNEY_STORAGE_KEYS = {
|
|
11
|
+
FIRST_TOUCH: '@datalyr/first_touch',
|
|
12
|
+
LAST_TOUCH: '@datalyr/last_touch',
|
|
13
|
+
JOURNEY: '@datalyr/journey',
|
|
14
|
+
};
|
|
15
|
+
// 90-day attribution window (matching web SDK)
|
|
16
|
+
const ATTRIBUTION_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
|
|
17
|
+
// Maximum touchpoints to store
|
|
18
|
+
const MAX_TOUCHPOINTS = 30;
|
|
19
|
+
/**
|
|
20
|
+
* Journey manager for tracking customer touchpoints
|
|
21
|
+
*/
|
|
22
|
+
export class JourneyManager {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.firstTouch = null;
|
|
25
|
+
this.lastTouch = null;
|
|
26
|
+
this.journey = [];
|
|
27
|
+
this.initialized = false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Initialize journey tracking by loading persisted data
|
|
31
|
+
*/
|
|
32
|
+
async initialize() {
|
|
33
|
+
if (this.initialized)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
debugLog('Initializing journey manager...');
|
|
37
|
+
// Load first touch
|
|
38
|
+
const savedFirstTouch = await Storage.getItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH);
|
|
39
|
+
if (savedFirstTouch && !this.isExpired(savedFirstTouch)) {
|
|
40
|
+
this.firstTouch = savedFirstTouch;
|
|
41
|
+
}
|
|
42
|
+
else if (savedFirstTouch) {
|
|
43
|
+
// Expired, clear it
|
|
44
|
+
await Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH);
|
|
45
|
+
}
|
|
46
|
+
// Load last touch
|
|
47
|
+
const savedLastTouch = await Storage.getItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH);
|
|
48
|
+
if (savedLastTouch && !this.isExpired(savedLastTouch)) {
|
|
49
|
+
this.lastTouch = savedLastTouch;
|
|
50
|
+
}
|
|
51
|
+
else if (savedLastTouch) {
|
|
52
|
+
await Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH);
|
|
53
|
+
}
|
|
54
|
+
// Load journey
|
|
55
|
+
const savedJourney = await Storage.getItem(JOURNEY_STORAGE_KEYS.JOURNEY);
|
|
56
|
+
if (savedJourney) {
|
|
57
|
+
this.journey = savedJourney;
|
|
58
|
+
}
|
|
59
|
+
this.initialized = true;
|
|
60
|
+
debugLog('Journey manager initialized', {
|
|
61
|
+
hasFirstTouch: !!this.firstTouch,
|
|
62
|
+
hasLastTouch: !!this.lastTouch,
|
|
63
|
+
touchpointCount: this.journey.length,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
errorLog('Failed to initialize journey manager:', error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if attribution has expired
|
|
72
|
+
*/
|
|
73
|
+
isExpired(attribution) {
|
|
74
|
+
return Date.now() >= attribution.expires_at;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Store first touch attribution (only if not already set or expired)
|
|
78
|
+
*/
|
|
79
|
+
async storeFirstTouch(attribution) {
|
|
80
|
+
try {
|
|
81
|
+
// Only store if no valid first touch exists
|
|
82
|
+
if (this.firstTouch && !this.isExpired(this.firstTouch)) {
|
|
83
|
+
debugLog('First touch already exists, not overwriting');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
this.firstTouch = {
|
|
88
|
+
...attribution,
|
|
89
|
+
timestamp: attribution.timestamp || now,
|
|
90
|
+
captured_at: now,
|
|
91
|
+
expires_at: now + ATTRIBUTION_WINDOW_MS,
|
|
92
|
+
};
|
|
93
|
+
await Storage.setItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH, this.firstTouch);
|
|
94
|
+
debugLog('First touch stored:', this.firstTouch);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
errorLog('Failed to store first touch:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get first touch attribution (null if expired)
|
|
102
|
+
*/
|
|
103
|
+
getFirstTouch() {
|
|
104
|
+
if (this.firstTouch && this.isExpired(this.firstTouch)) {
|
|
105
|
+
this.firstTouch = null;
|
|
106
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH).catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
return this.firstTouch;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Store last touch attribution (always updates)
|
|
112
|
+
*/
|
|
113
|
+
async storeLastTouch(attribution) {
|
|
114
|
+
try {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
this.lastTouch = {
|
|
117
|
+
...attribution,
|
|
118
|
+
timestamp: attribution.timestamp || now,
|
|
119
|
+
captured_at: now,
|
|
120
|
+
expires_at: now + ATTRIBUTION_WINDOW_MS,
|
|
121
|
+
};
|
|
122
|
+
await Storage.setItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH, this.lastTouch);
|
|
123
|
+
debugLog('Last touch stored:', this.lastTouch);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
errorLog('Failed to store last touch:', error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get last touch attribution (null if expired)
|
|
131
|
+
*/
|
|
132
|
+
getLastTouch() {
|
|
133
|
+
if (this.lastTouch && this.isExpired(this.lastTouch)) {
|
|
134
|
+
this.lastTouch = null;
|
|
135
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH).catch(() => { });
|
|
136
|
+
}
|
|
137
|
+
return this.lastTouch;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Add a touchpoint to the customer journey
|
|
141
|
+
*/
|
|
142
|
+
async addTouchpoint(sessionId, attribution) {
|
|
143
|
+
try {
|
|
144
|
+
const touchpoint = {
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
sessionId,
|
|
147
|
+
source: attribution.source,
|
|
148
|
+
medium: attribution.medium,
|
|
149
|
+
campaign: attribution.campaign,
|
|
150
|
+
clickIdType: attribution.clickIdType,
|
|
151
|
+
};
|
|
152
|
+
this.journey.push(touchpoint);
|
|
153
|
+
// Keep only last MAX_TOUCHPOINTS
|
|
154
|
+
if (this.journey.length > MAX_TOUCHPOINTS) {
|
|
155
|
+
this.journey = this.journey.slice(-MAX_TOUCHPOINTS);
|
|
156
|
+
}
|
|
157
|
+
await Storage.setItem(JOURNEY_STORAGE_KEYS.JOURNEY, this.journey);
|
|
158
|
+
debugLog('Touchpoint added, total:', this.journey.length);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
errorLog('Failed to add touchpoint:', error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get customer journey (all touchpoints)
|
|
166
|
+
*/
|
|
167
|
+
getJourney() {
|
|
168
|
+
return [...this.journey];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Record attribution from a deep link or install
|
|
172
|
+
* Updates first-touch (if not set), last-touch, and adds touchpoint
|
|
173
|
+
*/
|
|
174
|
+
async recordAttribution(sessionId, attribution) {
|
|
175
|
+
// Only process if we have meaningful attribution data
|
|
176
|
+
const hasAttribution = attribution.source || attribution.clickId || attribution.campaign || attribution.lyr;
|
|
177
|
+
if (!hasAttribution) {
|
|
178
|
+
debugLog('No attribution data to record');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Store first touch if not set
|
|
182
|
+
if (!this.getFirstTouch()) {
|
|
183
|
+
await this.storeFirstTouch(attribution);
|
|
184
|
+
}
|
|
185
|
+
// Always update last touch
|
|
186
|
+
await this.storeLastTouch(attribution);
|
|
187
|
+
// Add touchpoint
|
|
188
|
+
await this.addTouchpoint(sessionId, attribution);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get attribution data for events (mirrors Web SDK format)
|
|
192
|
+
*/
|
|
193
|
+
getAttributionData() {
|
|
194
|
+
const firstTouch = this.getFirstTouch();
|
|
195
|
+
const lastTouch = this.getLastTouch();
|
|
196
|
+
const journey = this.getJourney();
|
|
197
|
+
return {
|
|
198
|
+
// First touch (with snake_case and camelCase aliases)
|
|
199
|
+
first_touch_source: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.source,
|
|
200
|
+
first_touch_medium: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.medium,
|
|
201
|
+
first_touch_campaign: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.campaign,
|
|
202
|
+
first_touch_timestamp: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp,
|
|
203
|
+
firstTouchSource: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.source,
|
|
204
|
+
firstTouchMedium: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.medium,
|
|
205
|
+
firstTouchCampaign: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.campaign,
|
|
206
|
+
// Last touch
|
|
207
|
+
last_touch_source: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.source,
|
|
208
|
+
last_touch_medium: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.medium,
|
|
209
|
+
last_touch_campaign: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.campaign,
|
|
210
|
+
last_touch_timestamp: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.timestamp,
|
|
211
|
+
lastTouchSource: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.source,
|
|
212
|
+
lastTouchMedium: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.medium,
|
|
213
|
+
lastTouchCampaign: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.campaign,
|
|
214
|
+
// Journey metrics
|
|
215
|
+
touchpoint_count: journey.length,
|
|
216
|
+
touchpointCount: journey.length,
|
|
217
|
+
days_since_first_touch: (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp)
|
|
218
|
+
? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
|
|
219
|
+
: 0,
|
|
220
|
+
daysSinceFirstTouch: (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp)
|
|
221
|
+
? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
|
|
222
|
+
: 0,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Clear all journey data (for testing/reset)
|
|
227
|
+
*/
|
|
228
|
+
async clearJourney() {
|
|
229
|
+
this.firstTouch = null;
|
|
230
|
+
this.lastTouch = null;
|
|
231
|
+
this.journey = [];
|
|
232
|
+
await Promise.all([
|
|
233
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH),
|
|
234
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH),
|
|
235
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.JOURNEY),
|
|
236
|
+
]);
|
|
237
|
+
debugLog('Journey data cleared');
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get journey summary for debugging
|
|
241
|
+
*/
|
|
242
|
+
getJourneySummary() {
|
|
243
|
+
const firstTouch = this.getFirstTouch();
|
|
244
|
+
const journey = this.getJourney();
|
|
245
|
+
const sources = [...new Set(journey.map(t => t.source).filter(Boolean))];
|
|
246
|
+
return {
|
|
247
|
+
hasFirstTouch: !!firstTouch,
|
|
248
|
+
hasLastTouch: !!this.getLastTouch(),
|
|
249
|
+
touchpointCount: journey.length,
|
|
250
|
+
daysSinceFirstTouch: (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp)
|
|
251
|
+
? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
|
|
252
|
+
: 0,
|
|
253
|
+
sources,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Export singleton instance
|
|
258
|
+
export const journeyManager = new JourneyManager();
|