@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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
package com.datalyr.reactnative;
|
|
2
|
+
|
|
3
|
+
import android.os.RemoteException;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.bridge.Arguments;
|
|
7
|
+
import com.facebook.react.bridge.Promise;
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
9
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
10
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
11
|
+
import com.facebook.react.bridge.WritableMap;
|
|
12
|
+
|
|
13
|
+
import com.android.installreferrer.api.InstallReferrerClient;
|
|
14
|
+
import com.android.installreferrer.api.InstallReferrerStateListener;
|
|
15
|
+
import com.android.installreferrer.api.ReferrerDetails;
|
|
16
|
+
|
|
17
|
+
import java.io.UnsupportedEncodingException;
|
|
18
|
+
import java.net.URLDecoder;
|
|
19
|
+
import java.util.HashMap;
|
|
20
|
+
import java.util.Map;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Google Play Install Referrer Module for Android
|
|
24
|
+
*
|
|
25
|
+
* Captures install attribution data from Google Play Store:
|
|
26
|
+
* - UTM parameters (utm_source, utm_medium, utm_campaign, etc.)
|
|
27
|
+
* - Google Ads click ID (gclid)
|
|
28
|
+
* - Referrer timestamps
|
|
29
|
+
*
|
|
30
|
+
* This data is critical for attributing installs to marketing campaigns.
|
|
31
|
+
*/
|
|
32
|
+
public class DatalyrPlayInstallReferrerModule extends ReactContextBaseJavaModule {
|
|
33
|
+
private static final String TAG = "DatalyrPlayReferrer";
|
|
34
|
+
private static final String MODULE_NAME = "DatalyrPlayInstallReferrer";
|
|
35
|
+
|
|
36
|
+
private final ReactApplicationContext reactContext;
|
|
37
|
+
private InstallReferrerClient referrerClient;
|
|
38
|
+
|
|
39
|
+
public DatalyrPlayInstallReferrerModule(ReactApplicationContext context) {
|
|
40
|
+
super(context);
|
|
41
|
+
this.reactContext = context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Override
|
|
45
|
+
public String getName() {
|
|
46
|
+
return MODULE_NAME;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if Play Install Referrer is available
|
|
51
|
+
*/
|
|
52
|
+
@ReactMethod
|
|
53
|
+
public void isAvailable(Promise promise) {
|
|
54
|
+
try {
|
|
55
|
+
// Check if the Play Install Referrer library is available
|
|
56
|
+
Class.forName("com.android.installreferrer.api.InstallReferrerClient");
|
|
57
|
+
promise.resolve(true);
|
|
58
|
+
} catch (ClassNotFoundException e) {
|
|
59
|
+
promise.resolve(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get install referrer data from Google Play
|
|
65
|
+
*
|
|
66
|
+
* Returns an object with:
|
|
67
|
+
* - referrerUrl: The full referrer URL
|
|
68
|
+
* - referrerClickTimestamp: When the referrer link was clicked (ms)
|
|
69
|
+
* - installBeginTimestamp: When the install began (ms)
|
|
70
|
+
* - installCompleteTimestamp: When install was completed (ms) - Android 10+
|
|
71
|
+
* - gclid: Google Ads click ID (if present)
|
|
72
|
+
* - utmSource, utmMedium, utmCampaign, etc.
|
|
73
|
+
*/
|
|
74
|
+
@ReactMethod
|
|
75
|
+
public void getInstallReferrer(final Promise promise) {
|
|
76
|
+
try {
|
|
77
|
+
referrerClient = InstallReferrerClient.newBuilder(reactContext.getApplicationContext()).build();
|
|
78
|
+
|
|
79
|
+
referrerClient.startConnection(new InstallReferrerStateListener() {
|
|
80
|
+
@Override
|
|
81
|
+
public void onInstallReferrerSetupFinished(int responseCode) {
|
|
82
|
+
switch (responseCode) {
|
|
83
|
+
case InstallReferrerClient.InstallReferrerResponse.OK:
|
|
84
|
+
try {
|
|
85
|
+
ReferrerDetails details = referrerClient.getInstallReferrer();
|
|
86
|
+
WritableMap result = parseReferrerDetails(details);
|
|
87
|
+
promise.resolve(result);
|
|
88
|
+
} catch (RemoteException e) {
|
|
89
|
+
Log.e(TAG, "Failed to get install referrer", e);
|
|
90
|
+
promise.resolve(null);
|
|
91
|
+
} finally {
|
|
92
|
+
referrerClient.endConnection();
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED:
|
|
97
|
+
Log.d(TAG, "Install referrer not supported on this device");
|
|
98
|
+
promise.resolve(null);
|
|
99
|
+
referrerClient.endConnection();
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE:
|
|
103
|
+
Log.d(TAG, "Install referrer service unavailable");
|
|
104
|
+
promise.resolve(null);
|
|
105
|
+
referrerClient.endConnection();
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
default:
|
|
109
|
+
Log.d(TAG, "Install referrer unknown response: " + responseCode);
|
|
110
|
+
promise.resolve(null);
|
|
111
|
+
referrerClient.endConnection();
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Override
|
|
117
|
+
public void onInstallReferrerServiceDisconnected() {
|
|
118
|
+
Log.d(TAG, "Install referrer service disconnected");
|
|
119
|
+
// Connection lost - can try to reconnect if needed
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
} catch (Exception e) {
|
|
123
|
+
Log.e(TAG, "Failed to start install referrer client", e);
|
|
124
|
+
promise.resolve(null);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse ReferrerDetails into a WritableMap with UTM parameters extracted
|
|
130
|
+
*/
|
|
131
|
+
private WritableMap parseReferrerDetails(ReferrerDetails details) {
|
|
132
|
+
WritableMap result = Arguments.createMap();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
String referrerUrl = details.getInstallReferrer();
|
|
136
|
+
result.putString("referrerUrl", referrerUrl);
|
|
137
|
+
result.putDouble("referrerClickTimestamp", details.getReferrerClickTimestampSeconds() * 1000.0);
|
|
138
|
+
result.putDouble("installBeginTimestamp", details.getInstallBeginTimestampSeconds() * 1000.0);
|
|
139
|
+
|
|
140
|
+
// Android 10+ has install complete timestamp
|
|
141
|
+
try {
|
|
142
|
+
long installCompleteTs = details.getInstallBeginTimestampServerSeconds();
|
|
143
|
+
if (installCompleteTs > 0) {
|
|
144
|
+
result.putDouble("installCompleteTimestamp", installCompleteTs * 1000.0);
|
|
145
|
+
}
|
|
146
|
+
} catch (NoSuchMethodError e) {
|
|
147
|
+
// Method not available on older Android versions
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Parse UTM parameters from referrer URL
|
|
151
|
+
if (referrerUrl != null && !referrerUrl.isEmpty()) {
|
|
152
|
+
Map<String, String> params = parseReferrerUrl(referrerUrl);
|
|
153
|
+
|
|
154
|
+
// UTM parameters
|
|
155
|
+
if (params.containsKey("utm_source")) {
|
|
156
|
+
result.putString("utmSource", params.get("utm_source"));
|
|
157
|
+
}
|
|
158
|
+
if (params.containsKey("utm_medium")) {
|
|
159
|
+
result.putString("utmMedium", params.get("utm_medium"));
|
|
160
|
+
}
|
|
161
|
+
if (params.containsKey("utm_campaign")) {
|
|
162
|
+
result.putString("utmCampaign", params.get("utm_campaign"));
|
|
163
|
+
}
|
|
164
|
+
if (params.containsKey("utm_term")) {
|
|
165
|
+
result.putString("utmTerm", params.get("utm_term"));
|
|
166
|
+
}
|
|
167
|
+
if (params.containsKey("utm_content")) {
|
|
168
|
+
result.putString("utmContent", params.get("utm_content"));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Google Ads click ID
|
|
172
|
+
if (params.containsKey("gclid")) {
|
|
173
|
+
result.putString("gclid", params.get("gclid"));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Other potential click IDs
|
|
177
|
+
if (params.containsKey("fbclid")) {
|
|
178
|
+
result.putString("fbclid", params.get("fbclid"));
|
|
179
|
+
}
|
|
180
|
+
if (params.containsKey("ttclid")) {
|
|
181
|
+
result.putString("ttclid", params.get("ttclid"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// App referrer (used by some attribution providers)
|
|
185
|
+
if (params.containsKey("referrer")) {
|
|
186
|
+
result.putString("referrer", params.get("referrer"));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Log.d(TAG, "Install referrer parsed successfully");
|
|
191
|
+
|
|
192
|
+
} catch (Exception e) {
|
|
193
|
+
Log.e(TAG, "Failed to parse referrer details", e);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse URL-encoded referrer string into key-value pairs
|
|
201
|
+
*/
|
|
202
|
+
private Map<String, String> parseReferrerUrl(String referrerUrl) {
|
|
203
|
+
Map<String, String> params = new HashMap<>();
|
|
204
|
+
|
|
205
|
+
if (referrerUrl == null || referrerUrl.isEmpty()) {
|
|
206
|
+
return params;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Decode the URL-encoded referrer
|
|
211
|
+
String decoded = URLDecoder.decode(referrerUrl, "UTF-8");
|
|
212
|
+
|
|
213
|
+
// Split by & to get key=value pairs
|
|
214
|
+
String[] pairs = decoded.split("&");
|
|
215
|
+
for (String pair : pairs) {
|
|
216
|
+
int idx = pair.indexOf('=');
|
|
217
|
+
if (idx > 0 && idx < pair.length() - 1) {
|
|
218
|
+
String key = pair.substring(0, idx);
|
|
219
|
+
String value = pair.substring(idx + 1);
|
|
220
|
+
params.put(key, value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (UnsupportedEncodingException e) {
|
|
224
|
+
Log.e(TAG, "Failed to decode referrer URL", e);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return params;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -20,8 +20,8 @@ Pod::Spec.new do |s|
|
|
|
20
20
|
s.swift_version = "5.0"
|
|
21
21
|
|
|
22
22
|
s.dependency "React-Core"
|
|
23
|
-
s.dependency "FBSDKCoreKit", "~>
|
|
24
|
-
s.dependency "TikTokBusinessSDK", "~> 1.
|
|
23
|
+
s.dependency "FBSDKCoreKit", "~> 18.0"
|
|
24
|
+
s.dependency "TikTokBusinessSDK", "~> 1.6"
|
|
25
25
|
|
|
26
26
|
# Disable bitcode (required for TikTok SDK)
|
|
27
27
|
s.pod_target_xcconfig = {
|
package/ios/DatalyrSKAdNetwork.m
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
RCT_EXPORT_MODULE();
|
|
10
10
|
|
|
11
|
+
// SKAN 3.0 - Legacy method for iOS 14.0-16.0
|
|
11
12
|
RCT_EXPORT_METHOD(updateConversionValue:(NSInteger)value
|
|
12
13
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
13
14
|
reject:(RCTPromiseRejectBlock)reject) {
|
|
@@ -23,4 +24,54 @@ RCT_EXPORT_METHOD(updateConversionValue:(NSInteger)value
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
// SKAN 4.0 - New method for iOS 16.1+ with coarse value and lock window support
|
|
28
|
+
RCT_EXPORT_METHOD(updatePostbackConversionValue:(NSInteger)fineValue
|
|
29
|
+
coarseValue:(NSString *)coarseValue
|
|
30
|
+
lockWindow:(BOOL)lockWindow
|
|
31
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
32
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
33
|
+
if (@available(iOS 16.1, *)) {
|
|
34
|
+
// Convert string to SKAdNetwork.CoarseConversionValue
|
|
35
|
+
SKAdNetworkCoarseConversionValue coarse;
|
|
36
|
+
if ([coarseValue isEqualToString:@"high"]) {
|
|
37
|
+
coarse = SKAdNetworkCoarseConversionValueHigh;
|
|
38
|
+
} else if ([coarseValue isEqualToString:@"medium"]) {
|
|
39
|
+
coarse = SKAdNetworkCoarseConversionValueMedium;
|
|
40
|
+
} else {
|
|
41
|
+
coarse = SKAdNetworkCoarseConversionValueLow;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[SKAdNetwork updatePostbackConversionValue:fineValue
|
|
45
|
+
coarseValue:coarse
|
|
46
|
+
lockWindow:lockWindow
|
|
47
|
+
completionHandler:^(NSError * _Nullable error) {
|
|
48
|
+
if (error) {
|
|
49
|
+
reject(@"skadnetwork_error", error.localizedDescription, error);
|
|
50
|
+
} else {
|
|
51
|
+
resolve(@(YES));
|
|
52
|
+
}
|
|
53
|
+
}];
|
|
54
|
+
} else if (@available(iOS 14.0, *)) {
|
|
55
|
+
// Fallback to SKAN 3.0 for iOS 14.0-16.0
|
|
56
|
+
@try {
|
|
57
|
+
[SKAdNetwork updateConversionValue:fineValue];
|
|
58
|
+
resolve(@(YES));
|
|
59
|
+
} @catch (NSException *exception) {
|
|
60
|
+
reject(@"skadnetwork_error", exception.reason, nil);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
reject(@"ios_version_error", @"SKAdNetwork requires iOS 14.0+", nil);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if SKAN 4.0 is available (iOS 16.1+)
|
|
68
|
+
RCT_EXPORT_METHOD(isSKAN4Available:(RCTPromiseResolveBlock)resolve
|
|
69
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
70
|
+
if (@available(iOS 16.1, *)) {
|
|
71
|
+
resolve(@(YES));
|
|
72
|
+
} else {
|
|
73
|
+
resolve(@(NO));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { SKANCoarseValue, SKANConversionResult } from './native/SKAdNetworkBridge';
|
|
1
2
|
interface EventMapping {
|
|
2
3
|
bits: number[];
|
|
3
4
|
revenueBits?: number[];
|
|
4
5
|
priority: number;
|
|
6
|
+
coarseValue?: SKANCoarseValue;
|
|
7
|
+
lockWindow?: boolean;
|
|
5
8
|
}
|
|
6
9
|
interface ConversionTemplate {
|
|
7
10
|
name: string;
|
|
@@ -11,13 +14,22 @@ export declare class ConversionValueEncoder {
|
|
|
11
14
|
private template;
|
|
12
15
|
constructor(template: ConversionTemplate);
|
|
13
16
|
/**
|
|
14
|
-
* Encode an event into Apple's 0-63 conversion value format
|
|
17
|
+
* Encode an event into Apple's 0-63 conversion value format (SKAN 3.0 compatible)
|
|
18
|
+
* @deprecated Use encodeWithSKAN4 for iOS 16.1+
|
|
15
19
|
*/
|
|
16
20
|
encode(event: string, properties?: Record<string, any>): number;
|
|
21
|
+
/**
|
|
22
|
+
* Encode an event with full SKAN 4.0 support (fine value, coarse value, lock window)
|
|
23
|
+
*/
|
|
24
|
+
encodeWithSKAN4(event: string, properties?: Record<string, any>): SKANConversionResult;
|
|
17
25
|
/**
|
|
18
26
|
* Map revenue amount to 3-bit tier (0-7)
|
|
19
27
|
*/
|
|
20
28
|
private getRevenueTier;
|
|
29
|
+
/**
|
|
30
|
+
* Map revenue to SKAN 4.0 coarse value
|
|
31
|
+
*/
|
|
32
|
+
private getCoarseValueForRevenue;
|
|
21
33
|
}
|
|
22
34
|
export declare const ConversionTemplates: {
|
|
23
35
|
ecommerce: ConversionTemplate;
|
|
@@ -3,18 +3,27 @@ export class ConversionValueEncoder {
|
|
|
3
3
|
this.template = template;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Encode an event into Apple's 0-63 conversion value format
|
|
6
|
+
* Encode an event into Apple's 0-63 conversion value format (SKAN 3.0 compatible)
|
|
7
|
+
* @deprecated Use encodeWithSKAN4 for iOS 16.1+
|
|
7
8
|
*/
|
|
8
9
|
encode(event, properties) {
|
|
10
|
+
return this.encodeWithSKAN4(event, properties).fineValue;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Encode an event with full SKAN 4.0 support (fine value, coarse value, lock window)
|
|
14
|
+
*/
|
|
15
|
+
encodeWithSKAN4(event, properties) {
|
|
9
16
|
const mapping = this.template.events[event];
|
|
10
|
-
if (!mapping)
|
|
11
|
-
return 0;
|
|
17
|
+
if (!mapping) {
|
|
18
|
+
return { fineValue: 0, coarseValue: 'low', lockWindow: false, priority: 0 };
|
|
19
|
+
}
|
|
12
20
|
let conversionValue = 0;
|
|
13
21
|
// Set event bits
|
|
14
22
|
for (const bit of mapping.bits) {
|
|
15
23
|
conversionValue |= (1 << bit);
|
|
16
24
|
}
|
|
17
25
|
// Set revenue bits if revenue is provided
|
|
26
|
+
let coarseValue = mapping.coarseValue || 'medium';
|
|
18
27
|
if (mapping.revenueBits && properties) {
|
|
19
28
|
const revenue = properties.revenue || properties.value || 0;
|
|
20
29
|
const revenueTier = this.getRevenueTier(revenue);
|
|
@@ -23,8 +32,17 @@ export class ConversionValueEncoder {
|
|
|
23
32
|
conversionValue |= (1 << mapping.revenueBits[i]);
|
|
24
33
|
}
|
|
25
34
|
}
|
|
35
|
+
// Upgrade coarse value based on revenue
|
|
36
|
+
coarseValue = this.getCoarseValueForRevenue(revenue);
|
|
26
37
|
}
|
|
27
|
-
|
|
38
|
+
// Ensure value is within 0-63 range
|
|
39
|
+
const fineValue = Math.min(conversionValue, 63);
|
|
40
|
+
return {
|
|
41
|
+
fineValue,
|
|
42
|
+
coarseValue,
|
|
43
|
+
lockWindow: mapping.lockWindow || false,
|
|
44
|
+
priority: mapping.priority
|
|
45
|
+
};
|
|
28
46
|
}
|
|
29
47
|
/**
|
|
30
48
|
* Map revenue amount to 3-bit tier (0-7)
|
|
@@ -46,40 +64,56 @@ export class ConversionValueEncoder {
|
|
|
46
64
|
return 6; // $100-250
|
|
47
65
|
return 7; // $250+
|
|
48
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Map revenue to SKAN 4.0 coarse value
|
|
69
|
+
*/
|
|
70
|
+
getCoarseValueForRevenue(revenue) {
|
|
71
|
+
if (revenue < 10)
|
|
72
|
+
return 'low'; // $0-10 = low value
|
|
73
|
+
if (revenue < 50)
|
|
74
|
+
return 'medium'; // $10-50 = medium value
|
|
75
|
+
return 'high'; // $50+ = high value
|
|
76
|
+
}
|
|
49
77
|
}
|
|
50
|
-
// Industry templates
|
|
78
|
+
// Industry templates with SKAN 4.0 support
|
|
51
79
|
export const ConversionTemplates = {
|
|
80
|
+
// E-commerce template - optimized for online stores
|
|
81
|
+
// SKAN 4.0: purchase locks window, high-value events get "high" coarse value
|
|
52
82
|
ecommerce: {
|
|
53
83
|
name: 'ecommerce',
|
|
54
84
|
events: {
|
|
55
|
-
purchase: { bits: [0], revenueBits: [1, 2, 3], priority: 100 },
|
|
56
|
-
add_to_cart: { bits: [4], priority: 30 },
|
|
57
|
-
begin_checkout: { bits: [5], priority: 50 },
|
|
58
|
-
signup: { bits: [6], priority: 20 },
|
|
59
|
-
subscribe: { bits: [0, 1], revenueBits: [2, 3, 4], priority: 90 },
|
|
60
|
-
view_item: { bits: [7], priority: 10 }
|
|
85
|
+
purchase: { bits: [0], revenueBits: [1, 2, 3], priority: 100, coarseValue: 'high', lockWindow: true },
|
|
86
|
+
add_to_cart: { bits: [4], priority: 30, coarseValue: 'low' },
|
|
87
|
+
begin_checkout: { bits: [5], priority: 50, coarseValue: 'medium' },
|
|
88
|
+
signup: { bits: [6], priority: 20, coarseValue: 'low' },
|
|
89
|
+
subscribe: { bits: [0, 1], revenueBits: [2, 3, 4], priority: 90, coarseValue: 'high', lockWindow: true },
|
|
90
|
+
view_item: { bits: [7], priority: 10, coarseValue: 'low' }
|
|
61
91
|
}
|
|
62
92
|
},
|
|
93
|
+
// Gaming template - optimized for mobile games
|
|
94
|
+
// SKAN 4.0: purchase locks window, tutorial completion is medium value
|
|
63
95
|
gaming: {
|
|
64
96
|
name: 'gaming',
|
|
65
97
|
events: {
|
|
66
|
-
level_complete: { bits: [0], priority: 40 },
|
|
67
|
-
tutorial_complete: { bits: [1], priority: 60 },
|
|
68
|
-
purchase: { bits: [2], revenueBits: [3, 4, 5], priority: 100 },
|
|
69
|
-
achievement_unlocked: { bits: [6], priority: 30 },
|
|
70
|
-
session_start: { bits: [7], priority: 10 },
|
|
71
|
-
ad_watched: { bits: [0, 6], priority: 20 }
|
|
98
|
+
level_complete: { bits: [0], priority: 40, coarseValue: 'medium' },
|
|
99
|
+
tutorial_complete: { bits: [1], priority: 60, coarseValue: 'medium' },
|
|
100
|
+
purchase: { bits: [2], revenueBits: [3, 4, 5], priority: 100, coarseValue: 'high', lockWindow: true },
|
|
101
|
+
achievement_unlocked: { bits: [6], priority: 30, coarseValue: 'low' },
|
|
102
|
+
session_start: { bits: [7], priority: 10, coarseValue: 'low' },
|
|
103
|
+
ad_watched: { bits: [0, 6], priority: 20, coarseValue: 'low' }
|
|
72
104
|
}
|
|
73
105
|
},
|
|
106
|
+
// Subscription template - optimized for subscription apps
|
|
107
|
+
// SKAN 4.0: subscribe/upgrade lock window, trial is medium value
|
|
74
108
|
subscription: {
|
|
75
109
|
name: 'subscription',
|
|
76
110
|
events: {
|
|
77
|
-
trial_start: { bits: [0], priority: 70 },
|
|
78
|
-
subscribe: { bits: [1], revenueBits: [2, 3, 4], priority: 100 },
|
|
79
|
-
upgrade: { bits: [1, 5], revenueBits: [2, 3, 4], priority: 90 },
|
|
80
|
-
cancel: { bits: [6], priority: 20 },
|
|
81
|
-
signup: { bits: [7], priority: 30 },
|
|
82
|
-
payment_method_added: { bits: [0, 7], priority: 50 }
|
|
111
|
+
trial_start: { bits: [0], priority: 70, coarseValue: 'medium' },
|
|
112
|
+
subscribe: { bits: [1], revenueBits: [2, 3, 4], priority: 100, coarseValue: 'high', lockWindow: true },
|
|
113
|
+
upgrade: { bits: [1, 5], revenueBits: [2, 3, 4], priority: 90, coarseValue: 'high', lockWindow: true },
|
|
114
|
+
cancel: { bits: [6], priority: 20, coarseValue: 'low' },
|
|
115
|
+
signup: { bits: [7], priority: 30, coarseValue: 'low' },
|
|
116
|
+
payment_method_added: { bits: [0, 7], priority: 50, coarseValue: 'medium' }
|
|
83
117
|
}
|
|
84
118
|
}
|
|
85
119
|
};
|
package/lib/datalyr-sdk.d.ts
CHANGED
|
@@ -56,15 +56,30 @@ export declare class DatalyrSDK {
|
|
|
56
56
|
currentUserId?: string;
|
|
57
57
|
queueStats: any;
|
|
58
58
|
attribution: any;
|
|
59
|
+
journey: any;
|
|
59
60
|
};
|
|
60
61
|
/**
|
|
61
62
|
* Get the persistent anonymous ID
|
|
62
63
|
*/
|
|
63
64
|
getAnonymousId(): string;
|
|
64
65
|
/**
|
|
65
|
-
* Get detailed attribution data
|
|
66
|
+
* Get detailed attribution data (includes journey tracking data)
|
|
66
67
|
*/
|
|
67
|
-
getAttributionData(): AttributionData
|
|
68
|
+
getAttributionData(): AttributionData & Record<string, any>;
|
|
69
|
+
/**
|
|
70
|
+
* Get journey tracking summary
|
|
71
|
+
*/
|
|
72
|
+
getJourneySummary(): {
|
|
73
|
+
hasFirstTouch: boolean;
|
|
74
|
+
hasLastTouch: boolean;
|
|
75
|
+
touchpointCount: number;
|
|
76
|
+
daysSinceFirstTouch: number;
|
|
77
|
+
sources: string[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Get full customer journey (all touchpoints)
|
|
81
|
+
*/
|
|
82
|
+
getJourney(): import("./journey").TouchPoint[];
|
|
68
83
|
/**
|
|
69
84
|
* Set custom attribution data (for testing or manual attribution)
|
|
70
85
|
*/
|
|
@@ -91,6 +106,7 @@ export declare class DatalyrSDK {
|
|
|
91
106
|
updateAutoEventsConfig(config: Partial<AutoEventConfig>): void;
|
|
92
107
|
/**
|
|
93
108
|
* Track event with automatic SKAdNetwork conversion value encoding
|
|
109
|
+
* Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
|
|
94
110
|
*/
|
|
95
111
|
trackWithSKAdNetwork(event: string, properties?: EventData): Promise<void>;
|
|
96
112
|
/**
|
|
@@ -140,12 +156,18 @@ export declare class DatalyrSDK {
|
|
|
140
156
|
meta: boolean;
|
|
141
157
|
tiktok: boolean;
|
|
142
158
|
appleSearchAds: boolean;
|
|
159
|
+
playInstallReferrer: boolean;
|
|
143
160
|
};
|
|
144
161
|
/**
|
|
145
162
|
* Get Apple Search Ads attribution data
|
|
146
163
|
* Returns attribution if user installed via Apple Search Ads, null otherwise
|
|
147
164
|
*/
|
|
148
165
|
getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null;
|
|
166
|
+
/**
|
|
167
|
+
* Get Google Play Install Referrer attribution data (Android only)
|
|
168
|
+
* Returns referrer data if available, null otherwise
|
|
169
|
+
*/
|
|
170
|
+
getPlayInstallReferrer(): Record<string, any> | null;
|
|
149
171
|
/**
|
|
150
172
|
* Update tracking authorization status on all platform SDKs
|
|
151
173
|
* Call this AFTER the user responds to the ATT permission dialog
|
|
@@ -221,6 +243,7 @@ export declare class Datalyr {
|
|
|
221
243
|
currentUserId?: string;
|
|
222
244
|
queueStats: any;
|
|
223
245
|
attribution: any;
|
|
246
|
+
journey: any;
|
|
224
247
|
};
|
|
225
248
|
static getAnonymousId(): string;
|
|
226
249
|
static getAttributionData(): AttributionData;
|
package/lib/datalyr-sdk.js
CHANGED
|
@@ -3,10 +3,11 @@ import { getOrCreateVisitorId, getOrCreateAnonymousId, getOrCreateSessionId, cre
|
|
|
3
3
|
import { createHttpClient, HttpClient } from './http-client';
|
|
4
4
|
import { createEventQueue, EventQueue } from './event-queue';
|
|
5
5
|
import { attributionManager } from './attribution';
|
|
6
|
+
import { journeyManager } from './journey';
|
|
6
7
|
import { AutoEventsManager } from './auto-events';
|
|
7
8
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
8
9
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
9
|
-
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
|
|
10
|
+
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
10
11
|
export class DatalyrSDK {
|
|
11
12
|
constructor() {
|
|
12
13
|
this.autoEventsManager = null;
|
|
@@ -82,6 +83,22 @@ export class DatalyrSDK {
|
|
|
82
83
|
if (this.state.config.enableAttribution) {
|
|
83
84
|
await attributionManager.initialize();
|
|
84
85
|
}
|
|
86
|
+
// Initialize journey tracking (for first-touch, last-touch, touchpoints)
|
|
87
|
+
await journeyManager.initialize();
|
|
88
|
+
// Record initial attribution to journey if this is a new session with attribution
|
|
89
|
+
const initialAttribution = attributionManager.getAttributionData();
|
|
90
|
+
if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
|
|
91
|
+
await journeyManager.recordAttribution(this.state.sessionId, {
|
|
92
|
+
source: initialAttribution.utm_source || initialAttribution.campaign_source,
|
|
93
|
+
medium: initialAttribution.utm_medium || initialAttribution.campaign_medium,
|
|
94
|
+
campaign: initialAttribution.utm_campaign || initialAttribution.campaign_name,
|
|
95
|
+
fbclid: initialAttribution.fbclid,
|
|
96
|
+
gclid: initialAttribution.gclid,
|
|
97
|
+
ttclid: initialAttribution.ttclid,
|
|
98
|
+
clickIdType: initialAttribution.fbclid ? 'fbclid' : initialAttribution.gclid ? 'gclid' : initialAttribution.ttclid ? 'ttclid' : undefined,
|
|
99
|
+
lyr: initialAttribution.lyr,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
85
102
|
// Initialize auto-events manager (asynchronously to avoid blocking)
|
|
86
103
|
if (this.state.config.enableAutoEvents) {
|
|
87
104
|
this.autoEventsManager = new AutoEventsManager(this.track.bind(this), this.state.config.autoEventConfig);
|
|
@@ -134,10 +151,13 @@ export class DatalyrSDK {
|
|
|
134
151
|
}
|
|
135
152
|
// Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
|
|
136
153
|
await appleSearchAdsIntegration.initialize(config.debug);
|
|
154
|
+
// Initialize Google Play Install Referrer (Android only)
|
|
155
|
+
await playInstallReferrerIntegration.initialize();
|
|
137
156
|
debugLog('Platform integrations initialized', {
|
|
138
157
|
meta: metaIntegration.isAvailable(),
|
|
139
158
|
tiktok: tiktokIntegration.isAvailable(),
|
|
140
159
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
160
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
141
161
|
});
|
|
142
162
|
// SDK initialized successfully - set state before tracking install event
|
|
143
163
|
this.state.initialized = true;
|
|
@@ -398,6 +418,7 @@ export class DatalyrSDK {
|
|
|
398
418
|
currentUserId: this.state.currentUserId,
|
|
399
419
|
queueStats: this.eventQueue.getStats(),
|
|
400
420
|
attribution: attributionManager.getAttributionSummary(),
|
|
421
|
+
journey: journeyManager.getJourneySummary(),
|
|
401
422
|
};
|
|
402
423
|
}
|
|
403
424
|
/**
|
|
@@ -407,10 +428,28 @@ export class DatalyrSDK {
|
|
|
407
428
|
return this.state.anonymousId;
|
|
408
429
|
}
|
|
409
430
|
/**
|
|
410
|
-
* Get detailed attribution data
|
|
431
|
+
* Get detailed attribution data (includes journey tracking data)
|
|
411
432
|
*/
|
|
412
433
|
getAttributionData() {
|
|
413
|
-
|
|
434
|
+
const attribution = attributionManager.getAttributionData();
|
|
435
|
+
const journeyData = journeyManager.getAttributionData();
|
|
436
|
+
// Merge attribution with journey data
|
|
437
|
+
return {
|
|
438
|
+
...attribution,
|
|
439
|
+
...journeyData,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get journey tracking summary
|
|
444
|
+
*/
|
|
445
|
+
getJourneySummary() {
|
|
446
|
+
return journeyManager.getJourneySummary();
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get full customer journey (all touchpoints)
|
|
450
|
+
*/
|
|
451
|
+
getJourney() {
|
|
452
|
+
return journeyManager.getJourney();
|
|
414
453
|
}
|
|
415
454
|
/**
|
|
416
455
|
* Set custom attribution data (for testing or manual attribution)
|
|
@@ -460,22 +499,25 @@ export class DatalyrSDK {
|
|
|
460
499
|
// MARK: - SKAdNetwork Enhanced Methods
|
|
461
500
|
/**
|
|
462
501
|
* Track event with automatic SKAdNetwork conversion value encoding
|
|
502
|
+
* Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
|
|
463
503
|
*/
|
|
464
504
|
async trackWithSKAdNetwork(event, properties) {
|
|
465
505
|
// Existing tracking (keep exactly as-is)
|
|
466
506
|
await this.track(event, properties);
|
|
467
|
-
//
|
|
507
|
+
// Automatic SKAdNetwork encoding with SKAN 4.0 support
|
|
468
508
|
if (!DatalyrSDK.conversionEncoder) {
|
|
469
509
|
if (DatalyrSDK.debugEnabled) {
|
|
470
510
|
errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
|
|
471
511
|
}
|
|
472
512
|
return;
|
|
473
513
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
514
|
+
// Use SKAN 4.0 encoding (includes coarse value and lock window)
|
|
515
|
+
const result = DatalyrSDK.conversionEncoder.encodeWithSKAN4(event, properties);
|
|
516
|
+
if (result.fineValue > 0 || result.priority > 0) {
|
|
517
|
+
// Use SKAN 4.0 method (automatically falls back to SKAN 3.0 on older iOS)
|
|
518
|
+
const success = await SKAdNetworkBridge.updatePostbackConversionValue(result);
|
|
477
519
|
if (DatalyrSDK.debugEnabled) {
|
|
478
|
-
debugLog(`
|
|
520
|
+
debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
|
|
479
521
|
}
|
|
480
522
|
}
|
|
481
523
|
else if (DatalyrSDK.debugEnabled) {
|
|
@@ -676,6 +718,7 @@ export class DatalyrSDK {
|
|
|
676
718
|
meta: metaIntegration.isAvailable(),
|
|
677
719
|
tiktok: tiktokIntegration.isAvailable(),
|
|
678
720
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
721
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
679
722
|
};
|
|
680
723
|
}
|
|
681
724
|
/**
|
|
@@ -685,6 +728,14 @@ export class DatalyrSDK {
|
|
|
685
728
|
getAppleSearchAdsAttribution() {
|
|
686
729
|
return appleSearchAdsIntegration.getAttributionData();
|
|
687
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Get Google Play Install Referrer attribution data (Android only)
|
|
733
|
+
* Returns referrer data if available, null otherwise
|
|
734
|
+
*/
|
|
735
|
+
getPlayInstallReferrer() {
|
|
736
|
+
const data = playInstallReferrerIntegration.getReferrerData();
|
|
737
|
+
return data ? playInstallReferrerIntegration.getAttributionData() : null;
|
|
738
|
+
}
|
|
688
739
|
/**
|
|
689
740
|
* Update tracking authorization status on all platform SDKs
|
|
690
741
|
* Call this AFTER the user responds to the ATT permission dialog
|