react-native-test-adx 0.1.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/LICENSE +20 -0
- package/README.md +333 -0
- package/lib/module/AdaxonSdk.js +120 -0
- package/lib/module/components/AdaxonProvider.js +46 -0
- package/lib/module/config/ApiConstants.js +18 -0
- package/lib/module/config/StorageKeys.js +12 -0
- package/lib/module/core/AdaxonClient.js +88 -0
- package/lib/module/errors/AdaxonSdkError.js +23 -0
- package/lib/module/hooks/useAdaxon.js +35 -0
- package/lib/module/index.js +10 -0
- package/lib/module/package.json +1 -0
- package/lib/module/platform/android.js +10 -0
- package/lib/module/platform/app.js +18 -0
- package/lib/module/platform/ios.js +11 -0
- package/lib/module/platform/network.js +61 -0
- package/lib/module/platform/reactNative.js +78 -0
- package/lib/module/platform/types.js +3 -0
- package/lib/module/services/app/AppInfoService.js +25 -0
- package/lib/module/services/authentication/AuthenticationService.js +26 -0
- package/lib/module/services/device/DeviceInfoService.js +22 -0
- package/lib/module/services/event/BatchProcessor.js +64 -0
- package/lib/module/services/event/EventBuilder.js +43 -0
- package/lib/module/services/event/EventQueueService.js +35 -0
- package/lib/module/services/event/EventService.js +42 -0
- package/lib/module/services/event/normalizeProperties.js +42 -0
- package/lib/module/services/event/predefined.js +13 -0
- package/lib/module/services/network/ApiClient.js +62 -0
- package/lib/module/services/network/NetworkInfoService.js +69 -0
- package/lib/module/state/AdaxonContext.js +7 -0
- package/lib/module/storage/AnonymousIdService.js +31 -0
- package/lib/module/storage/EventQueueStorage.js +143 -0
- package/lib/module/storage/memoryStorage.js +16 -0
- package/lib/module/types/AdaxonConfig.js +3 -0
- package/lib/module/types/ApiTypes.js +1 -0
- package/lib/module/types/EventTypes.js +1 -0
- package/lib/module/types/QueuedEvent.js +3 -0
- package/lib/module/utils/locale.js +15 -0
- package/lib/module/utils/strings.js +17 -0
- package/lib/module/utils/uuid.js +13 -0
- package/lib/module/validation/EventValidator.js +63 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AdaxonSdk.d.ts +28 -0
- package/lib/typescript/src/components/AdaxonProvider.d.ts +11 -0
- package/lib/typescript/src/config/ApiConstants.d.ts +17 -0
- package/lib/typescript/src/config/StorageKeys.d.ts +5 -0
- package/lib/typescript/src/core/AdaxonClient.d.ts +29 -0
- package/lib/typescript/src/errors/AdaxonSdkError.d.ts +15 -0
- package/lib/typescript/src/hooks/useAdaxon.d.ts +19 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/platform/android.d.ts +10 -0
- package/lib/typescript/src/platform/app.d.ts +14 -0
- package/lib/typescript/src/platform/ios.d.ts +8 -0
- package/lib/typescript/src/platform/network.d.ts +6 -0
- package/lib/typescript/src/platform/reactNative.d.ts +8 -0
- package/lib/typescript/src/platform/types.d.ts +15 -0
- package/lib/typescript/src/services/app/AppInfoService.d.ts +11 -0
- package/lib/typescript/src/services/authentication/AuthenticationService.d.ts +10 -0
- package/lib/typescript/src/services/device/DeviceInfoService.d.ts +10 -0
- package/lib/typescript/src/services/event/BatchProcessor.d.ts +19 -0
- package/lib/typescript/src/services/event/EventBuilder.d.ts +25 -0
- package/lib/typescript/src/services/event/EventQueueService.d.ts +13 -0
- package/lib/typescript/src/services/event/EventService.d.ts +23 -0
- package/lib/typescript/src/services/event/normalizeProperties.d.ts +26 -0
- package/lib/typescript/src/services/event/predefined.d.ts +14 -0
- package/lib/typescript/src/services/network/ApiClient.d.ts +14 -0
- package/lib/typescript/src/services/network/NetworkInfoService.d.ts +14 -0
- package/lib/typescript/src/state/AdaxonContext.d.ts +9 -0
- package/lib/typescript/src/storage/AnonymousIdService.d.ts +11 -0
- package/lib/typescript/src/storage/EventQueueStorage.d.ts +21 -0
- package/lib/typescript/src/storage/memoryStorage.d.ts +3 -0
- package/lib/typescript/src/types/AdaxonConfig.d.ts +30 -0
- package/lib/typescript/src/types/ApiTypes.d.ts +31 -0
- package/lib/typescript/src/types/EventTypes.d.ts +65 -0
- package/lib/typescript/src/types/QueuedEvent.d.ts +16 -0
- package/lib/typescript/src/utils/locale.d.ts +3 -0
- package/lib/typescript/src/utils/strings.d.ts +3 -0
- package/lib/typescript/src/utils/uuid.d.ts +3 -0
- package/lib/typescript/src/validation/EventValidator.d.ts +13 -0
- package/package.json +107 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useContext, useMemo } from 'react';
|
|
4
|
+
import { AdaxonSdkError } from "../errors/AdaxonSdkError.js";
|
|
5
|
+
import { AdaxonContext } from "../state/AdaxonContext.js";
|
|
6
|
+
export function useAdaxon() {
|
|
7
|
+
const value = useContext(AdaxonContext);
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new AdaxonSdkError('INVALID_CONFIG', 'useAdaxon must be used within AdaxonProvider.');
|
|
10
|
+
}
|
|
11
|
+
const {
|
|
12
|
+
sdk,
|
|
13
|
+
isInitialized,
|
|
14
|
+
initializationError
|
|
15
|
+
} = value;
|
|
16
|
+
const methods = useMemo(() => ({
|
|
17
|
+
track: sdk.track.bind(sdk),
|
|
18
|
+
trackBatch: sdk.trackBatch.bind(sdk),
|
|
19
|
+
enqueueEvent: sdk.enqueueEvent.bind(sdk),
|
|
20
|
+
processPendingEvents: sdk.processPendingEvents.bind(sdk),
|
|
21
|
+
logEvent: sdk.logEvent.bind(sdk),
|
|
22
|
+
trackPurchase: sdk.trackPurchase.bind(sdk),
|
|
23
|
+
trackAddToCart: sdk.trackAddToCart.bind(sdk),
|
|
24
|
+
trackLogin: sdk.trackLogin.bind(sdk),
|
|
25
|
+
trackCompleteRegistration: sdk.trackCompleteRegistration.bind(sdk),
|
|
26
|
+
loadRecommendations: sdk.loadRecommendations.bind(sdk),
|
|
27
|
+
showOffer: sdk.showOffer.bind(sdk),
|
|
28
|
+
setExternalUserId: sdk.setExternalUserId.bind(sdk)
|
|
29
|
+
}), [sdk]);
|
|
30
|
+
return {
|
|
31
|
+
isInitialized,
|
|
32
|
+
initializationError,
|
|
33
|
+
...methods
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { AdaxonSdk } from "./AdaxonSdk.js";
|
|
4
|
+
export { AdaxonProvider } from "./components/AdaxonProvider.js";
|
|
5
|
+
export { useAdaxon } from "./hooks/useAdaxon.js";
|
|
6
|
+
export { AdaxonSdk };
|
|
7
|
+
export { AdaxonSdkError } from "./errors/AdaxonSdkError.js";
|
|
8
|
+
export { AdxEvent, AdxParam } from "./services/event/predefined.js";
|
|
9
|
+
export const Adaxon = AdaxonSdk.shared;
|
|
10
|
+
export default AdaxonSdk;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { nonEmptyString, setIfPresent } from "../utils/strings.js";
|
|
4
|
+
export function deviceFieldsFromAndroid(constants) {
|
|
5
|
+
const fields = {};
|
|
6
|
+
setIfPresent(fields, 'os_version', nonEmptyString(constants.Release));
|
|
7
|
+
setIfPresent(fields, 'brand', nonEmptyString(constants.Brand));
|
|
8
|
+
setIfPresent(fields, 'model', nonEmptyString(constants.Model));
|
|
9
|
+
return fields;
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { nonEmptyString, setIfPresent } from "../utils/strings.js";
|
|
4
|
+
export function appFieldsFromNative(constants) {
|
|
5
|
+
const fields = {};
|
|
6
|
+
setIfPresent(fields, 'package_name', firstNonEmpty(constants.applicationId, constants.bundleId, constants.androidPackage, constants.iosBundleIdentifier));
|
|
7
|
+
setIfPresent(fields, 'app_version', firstNonEmpty(constants.nativeApplicationVersion, constants.appVersion, constants.nativeAppVersion));
|
|
8
|
+
return fields;
|
|
9
|
+
}
|
|
10
|
+
function firstNonEmpty(...values) {
|
|
11
|
+
for (const value of values) {
|
|
12
|
+
const next = nonEmptyString(value);
|
|
13
|
+
if (next) {
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { nonEmptyString, setIfPresent } from "../utils/strings.js";
|
|
4
|
+
export function deviceFieldsFromIos(constants) {
|
|
5
|
+
const fields = {
|
|
6
|
+
brand: 'Apple'
|
|
7
|
+
};
|
|
8
|
+
setIfPresent(fields, 'os_version', nonEmptyString(constants.osVersion) ?? nonEmptyString(constants.Version));
|
|
9
|
+
setIfPresent(fields, 'model', nonEmptyString(constants.model));
|
|
10
|
+
return fields;
|
|
11
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { nonEmptyString } from "../utils/strings.js";
|
|
4
|
+
const CONNECTION_TYPES = new Set(['wifi', 'cellular', 'ethernet', 'bluetooth', 'vpn', 'wimax', 'none', 'unknown', 'other']);
|
|
5
|
+
const IPV4_PATTERN = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
|
|
6
|
+
function asNativeNetworkState(state) {
|
|
7
|
+
if (state && typeof state === 'object') {
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function publicIpAddress(value) {
|
|
13
|
+
const ip = usableIpAddress(value);
|
|
14
|
+
return ip && !isPrivateIpAddress(ip) ? ip : undefined;
|
|
15
|
+
}
|
|
16
|
+
export function connectionTypeFromNativeState(state) {
|
|
17
|
+
const type = nonEmptyString(asNativeNetworkState(state)?.type)?.toLowerCase();
|
|
18
|
+
return type && CONNECTION_TYPES.has(type) ? type : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function ipAddressFromNativeState(state) {
|
|
21
|
+
const details = asNativeNetworkState(state)?.details;
|
|
22
|
+
return usableIpAddress(nonEmptyString(details?.ipAddress) ?? nonEmptyString(details?.ip_address));
|
|
23
|
+
}
|
|
24
|
+
export function usableIpAddress(value) {
|
|
25
|
+
if (!value) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const ip = value.trim();
|
|
29
|
+
if (IPV4_PATTERN.test(ip)) {
|
|
30
|
+
return ip;
|
|
31
|
+
}
|
|
32
|
+
if (ip.includes(':') && ip.length <= 45 && /^[0-9a-fA-F:.]+$/.test(ip)) {
|
|
33
|
+
return ip;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
export function isPrivateIpAddress(ip) {
|
|
38
|
+
const value = ip.trim().toLowerCase();
|
|
39
|
+
if (value === '::1' || value.startsWith('fe80:') || value.startsWith('fc') || value.startsWith('fd')) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (!IPV4_PATTERN.test(value)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const octets = value.split('.').map(Number);
|
|
46
|
+
const first = octets[0] ?? 0;
|
|
47
|
+
const second = octets[1] ?? 0;
|
|
48
|
+
return first === 10 || first === 127 || first === 0 || first === 192 && second === 168 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31;
|
|
49
|
+
}
|
|
50
|
+
export function parsePublicIpResponse(raw) {
|
|
51
|
+
const trimmed = raw.trim();
|
|
52
|
+
if (trimmed.startsWith('{')) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(trimmed);
|
|
55
|
+
return publicIpAddress(nonEmptyString(parsed.ip));
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return publicIpAddress(trimmed);
|
|
61
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Linking, NativeModules, Platform } from 'react-native';
|
|
4
|
+
import { nonEmptyString, setIfPresent } from "../utils/strings.js";
|
|
5
|
+
import { readLocaleFields } from "../utils/locale.js";
|
|
6
|
+
import { appFieldsFromNative } from "./app.js";
|
|
7
|
+
import { deviceFieldsFromAndroid } from "./android.js";
|
|
8
|
+
import { deviceFieldsFromIos } from "./ios.js";
|
|
9
|
+
import { connectionTypeFromNativeState, ipAddressFromNativeState, usableIpAddress } from "./network.js";
|
|
10
|
+
export function detectReactNativePlatform() {
|
|
11
|
+
return Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'web';
|
|
12
|
+
}
|
|
13
|
+
function collectFromReactNative(platform) {
|
|
14
|
+
const device = {
|
|
15
|
+
platform
|
|
16
|
+
};
|
|
17
|
+
if (Platform.OS === 'android') {
|
|
18
|
+
Object.assign(device, deviceFieldsFromAndroid(Platform.constants));
|
|
19
|
+
} else if (Platform.OS === 'ios') {
|
|
20
|
+
Object.assign(device, deviceFieldsFromIos({
|
|
21
|
+
...Platform.constants,
|
|
22
|
+
Version: Platform.Version
|
|
23
|
+
}));
|
|
24
|
+
} else {
|
|
25
|
+
setIfPresent(device, 'os_version', nonEmptyString(Platform.Version));
|
|
26
|
+
}
|
|
27
|
+
Object.assign(device, readLocaleFields());
|
|
28
|
+
return device;
|
|
29
|
+
}
|
|
30
|
+
export const reactNativeDeviceReader = {
|
|
31
|
+
readDeviceInfo: collectFromReactNative
|
|
32
|
+
};
|
|
33
|
+
export const reactNativeAppInfoReader = {
|
|
34
|
+
readAppInfo: () => appFieldsFromNative(readNativeAppConstants())
|
|
35
|
+
};
|
|
36
|
+
export const reactNativeNetworkInfoReader = {
|
|
37
|
+
readNetworkInfo: collectFromNativeNetwork
|
|
38
|
+
};
|
|
39
|
+
export const reactNativeUrlOpener = Linking;
|
|
40
|
+
function readNativeAppConstants() {
|
|
41
|
+
const modules = NativeModules ?? {};
|
|
42
|
+
const expoApp = modules.ExpoApplication;
|
|
43
|
+
const expoConstants = modules.ExponentConstants;
|
|
44
|
+
const deviceInfo = modules.RNDeviceInfo;
|
|
45
|
+
return {
|
|
46
|
+
...deviceInfo,
|
|
47
|
+
...expoConstants,
|
|
48
|
+
...expoApp
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function readOptionalNative(read) {
|
|
52
|
+
try {
|
|
53
|
+
return await read();
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function collectFromNativeNetwork() {
|
|
59
|
+
const network = {};
|
|
60
|
+
const modules = NativeModules ?? {};
|
|
61
|
+
const netInfo = modules.RNCNetInfo;
|
|
62
|
+
const expoNetwork = modules.ExpoNetwork;
|
|
63
|
+
const getCurrentState = netInfo?.getCurrentState;
|
|
64
|
+
if (typeof getCurrentState === 'function') {
|
|
65
|
+
const state = await readOptionalNative(() => getCurrentState(null));
|
|
66
|
+
setIfPresent(network, 'connection_type', connectionTypeFromNativeState(state));
|
|
67
|
+
setIfPresent(network, 'ip_address', ipAddressFromNativeState(state));
|
|
68
|
+
}
|
|
69
|
+
const getNetworkState = expoNetwork?.getNetworkStateAsync;
|
|
70
|
+
if (!network.connection_type && typeof getNetworkState === 'function') {
|
|
71
|
+
setIfPresent(network, 'connection_type', connectionTypeFromNativeState(await readOptionalNative(getNetworkState)));
|
|
72
|
+
}
|
|
73
|
+
const getIpAddress = expoNetwork?.getIpAddressAsync;
|
|
74
|
+
if (!network.ip_address && typeof getIpAddress === 'function') {
|
|
75
|
+
setIfPresent(network, 'ip_address', usableIpAddress(nonEmptyString(await readOptionalNative(getIpAddress))));
|
|
76
|
+
}
|
|
77
|
+
return network;
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { SDK_VERSION } from "../../config/ApiConstants.js";
|
|
4
|
+
export class AppInfoService {
|
|
5
|
+
constructor(testMode, reader) {
|
|
6
|
+
this.testMode = testMode;
|
|
7
|
+
this.reader = reader;
|
|
8
|
+
}
|
|
9
|
+
getAppInfo() {
|
|
10
|
+
if (!this.cached) {
|
|
11
|
+
this.cached = {
|
|
12
|
+
...this.readOptionalFields(),
|
|
13
|
+
sdk_version: this.testMode ? `${SDK_VERSION}-test` : SDK_VERSION
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return this.cached;
|
|
17
|
+
}
|
|
18
|
+
readOptionalFields() {
|
|
19
|
+
try {
|
|
20
|
+
return this.reader?.readAppInfo() ?? {};
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_TIMEOUT_MS } from "../../config/ApiConstants.js";
|
|
4
|
+
import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
|
|
5
|
+
export class AuthenticationService {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.validateConfig(config);
|
|
8
|
+
this.appId = config.appId;
|
|
9
|
+
this.sdkKey = config.sdkKey;
|
|
10
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11
|
+
}
|
|
12
|
+
validateConfig(config) {
|
|
13
|
+
if (!config.appId?.trim()) {
|
|
14
|
+
this.fail('appId is required.');
|
|
15
|
+
}
|
|
16
|
+
if (!config.sdkKey?.trim()) {
|
|
17
|
+
this.fail('sdkKey is required.');
|
|
18
|
+
}
|
|
19
|
+
if (config.timeoutMs !== undefined && (!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0)) {
|
|
20
|
+
this.fail('timeoutMs must be a positive number.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
fail(message) {
|
|
24
|
+
throw new AdaxonSdkError('INVALID_CONFIG', message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export class DeviceInfoService {
|
|
4
|
+
constructor(platform, reader) {
|
|
5
|
+
this.platform = platform;
|
|
6
|
+
this.reader = reader;
|
|
7
|
+
}
|
|
8
|
+
getDeviceInfo() {
|
|
9
|
+
if (!this.cached) {
|
|
10
|
+
try {
|
|
11
|
+
this.cached = this.reader?.readDeviceInfo(this.platform) ?? {
|
|
12
|
+
platform: this.platform
|
|
13
|
+
};
|
|
14
|
+
} catch {
|
|
15
|
+
this.cached = {
|
|
16
|
+
platform: this.platform
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return this.cached;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { MAX_BATCH_SIZE } from "../../config/ApiConstants.js";
|
|
4
|
+
import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
|
|
5
|
+
const TEMPORARY_FAILURE_CODES = new Set(['NETWORK_ERROR', 'TIMEOUT', 'HTTP_ERROR', 'AUTHENTICATION_ERROR']);
|
|
6
|
+
export class BatchProcessor {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.queue = config.queue;
|
|
9
|
+
this.sendBatch = config.sendBatch;
|
|
10
|
+
}
|
|
11
|
+
async process() {
|
|
12
|
+
if (!this.queue.tryAcquireProcessorLock()) {
|
|
13
|
+
return this.result('already_processing');
|
|
14
|
+
}
|
|
15
|
+
let batchesSent = 0;
|
|
16
|
+
let eventsProcessed = 0;
|
|
17
|
+
try {
|
|
18
|
+
while (true) {
|
|
19
|
+
const events = await this.queue.getNextBatch(MAX_BATCH_SIZE);
|
|
20
|
+
if (events.length === 0) {
|
|
21
|
+
return this.result('completed', {
|
|
22
|
+
batchesSent,
|
|
23
|
+
eventsProcessed
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.sendBatch(events.map(event => event.payload));
|
|
28
|
+
if (response.success !== true) {
|
|
29
|
+
return this.stopped(batchesSent, eventsProcessed, events);
|
|
30
|
+
}
|
|
31
|
+
await this.queue.remove(events);
|
|
32
|
+
batchesSent += 1;
|
|
33
|
+
eventsProcessed += events.length;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return this.stopped(batchesSent, eventsProcessed, events, error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return this.stopped(batchesSent, eventsProcessed, undefined, error);
|
|
40
|
+
} finally {
|
|
41
|
+
this.queue.releaseProcessorLock();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
stopped(batchesSent, eventsProcessed, events, error) {
|
|
45
|
+
if (error !== undefined && !(error instanceof AdaxonSdkError && TEMPORARY_FAILURE_CODES.has(error.code))) {
|
|
46
|
+
console.error('[Adaxon] Batch processor failed unexpectedly', error);
|
|
47
|
+
}
|
|
48
|
+
return this.result('stopped', {
|
|
49
|
+
batchesSent,
|
|
50
|
+
eventsProcessed,
|
|
51
|
+
failedQueueIds: events?.map(event => event.queue_id) ?? [],
|
|
52
|
+
error
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
result(status, extra = {}) {
|
|
56
|
+
return {
|
|
57
|
+
status,
|
|
58
|
+
batchesSent: 0,
|
|
59
|
+
eventsProcessed: 0,
|
|
60
|
+
failedQueueIds: [],
|
|
61
|
+
...extra
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { createUuid } from "../../utils/uuid.js";
|
|
4
|
+
import { nonEmptyString, setIfPresent } from "../../utils/strings.js";
|
|
5
|
+
export class EventBuilder {
|
|
6
|
+
sessionId = createUuid();
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.externalUserId = config.externalUserId;
|
|
10
|
+
}
|
|
11
|
+
setExternalUserId(externalUserId) {
|
|
12
|
+
this.externalUserId = externalUserId;
|
|
13
|
+
}
|
|
14
|
+
async build(input) {
|
|
15
|
+
const {
|
|
16
|
+
validator,
|
|
17
|
+
appId,
|
|
18
|
+
anonymousId,
|
|
19
|
+
deviceInfo,
|
|
20
|
+
appInfo,
|
|
21
|
+
networkInfo
|
|
22
|
+
} = this.config;
|
|
23
|
+
validator.validateTrackInput(input);
|
|
24
|
+
const user = {
|
|
25
|
+
anonymous_id: await anonymousId.getAnonymousId()
|
|
26
|
+
};
|
|
27
|
+
setIfPresent(user, 'external_user_id', nonEmptyString(this.externalUserId));
|
|
28
|
+
return {
|
|
29
|
+
event_id: input.eventId ?? createUuid(),
|
|
30
|
+
event_name: input.eventName,
|
|
31
|
+
event_time: input.eventTime ?? new Date().toISOString(),
|
|
32
|
+
app_id: appId,
|
|
33
|
+
properties: input.properties ?? {},
|
|
34
|
+
user,
|
|
35
|
+
device: deviceInfo.getDeviceInfo(),
|
|
36
|
+
app: appInfo.getAppInfo(),
|
|
37
|
+
session: {
|
|
38
|
+
session_id: this.sessionId
|
|
39
|
+
},
|
|
40
|
+
network: await networkInfo.getNetworkInfo()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { MAX_BATCH_SIZE } from "../../config/ApiConstants.js";
|
|
4
|
+
import { createUuid } from "../../utils/uuid.js";
|
|
5
|
+
const activeProcessors = new Set();
|
|
6
|
+
export class EventQueueService {
|
|
7
|
+
constructor(storage) {
|
|
8
|
+
this.storage = storage;
|
|
9
|
+
}
|
|
10
|
+
add(payload) {
|
|
11
|
+
return this.storage.append({
|
|
12
|
+
queue_id: createUuid(),
|
|
13
|
+
created_at: Date.now(),
|
|
14
|
+
payload
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
getNextBatch(limit = MAX_BATCH_SIZE) {
|
|
18
|
+
const size = Math.min(Math.max(0, Math.floor(limit)), MAX_BATCH_SIZE);
|
|
19
|
+
return this.storage.getNext(size);
|
|
20
|
+
}
|
|
21
|
+
remove(events) {
|
|
22
|
+
return this.storage.remove(events);
|
|
23
|
+
}
|
|
24
|
+
tryAcquireProcessorLock() {
|
|
25
|
+
const lockId = this.storage.lockId;
|
|
26
|
+
if (activeProcessors.has(lockId)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
activeProcessors.add(lockId);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
releaseProcessorLock() {
|
|
33
|
+
activeProcessors.delete(this.storage.lockId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { EVENT_TRACKING_PATH, RECOMMENDATIONS_PATH } from "../../config/ApiConstants.js";
|
|
4
|
+
import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
|
|
5
|
+
export class EventService {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.api = config.api;
|
|
8
|
+
this.builder = config.builder;
|
|
9
|
+
this.validator = config.validator;
|
|
10
|
+
}
|
|
11
|
+
track(eventName, properties = {}, options = {}) {
|
|
12
|
+
return this.trackBatch([{
|
|
13
|
+
eventName,
|
|
14
|
+
properties,
|
|
15
|
+
...options
|
|
16
|
+
}]);
|
|
17
|
+
}
|
|
18
|
+
async trackBatch(events) {
|
|
19
|
+
this.validator.validateBatch(events);
|
|
20
|
+
const payloads = await Promise.all(events.map(event => this.builder.build(event)));
|
|
21
|
+
return this.postEvents(payloads);
|
|
22
|
+
}
|
|
23
|
+
sendPayloads(payloads) {
|
|
24
|
+
this.validator.validateBatch(payloads);
|
|
25
|
+
return this.postEvents(payloads);
|
|
26
|
+
}
|
|
27
|
+
async getRecommendations(input) {
|
|
28
|
+
const eventPayload = await this.builder.build(input);
|
|
29
|
+
const payload = {
|
|
30
|
+
...eventPayload,
|
|
31
|
+
event_type: 'recommendation'
|
|
32
|
+
};
|
|
33
|
+
const response = await this.api.post(RECOMMENDATIONS_PATH, payload);
|
|
34
|
+
if (!Array.isArray(response.offers)) {
|
|
35
|
+
throw new AdaxonSdkError('HTTP_ERROR', 'Adaxon recommendation response is missing the offers array.');
|
|
36
|
+
}
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
postEvents(payloads) {
|
|
40
|
+
return this.api.post(EVENT_TRACKING_PATH, payloads);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
|
|
4
|
+
import { AdxParam } from "./predefined.js";
|
|
5
|
+
export function normalizeEventValues(values) {
|
|
6
|
+
if (values === undefined || values === null) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
if (!isPlainObject(values)) {
|
|
10
|
+
throw new AdaxonSdkError('VALIDATION_ERROR', 'Event values must be a plain object.');
|
|
11
|
+
}
|
|
12
|
+
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
13
|
+
}
|
|
14
|
+
export function buildPurchaseProperties(params) {
|
|
15
|
+
return buildCommerceProperties(params, params.amount);
|
|
16
|
+
}
|
|
17
|
+
export function buildAddToCartProperties(params) {
|
|
18
|
+
return buildCommerceProperties(params, params.price);
|
|
19
|
+
}
|
|
20
|
+
function buildCommerceProperties(params, revenue) {
|
|
21
|
+
const properties = {
|
|
22
|
+
...params
|
|
23
|
+
};
|
|
24
|
+
const contentId = params.content_id ?? params.product_id;
|
|
25
|
+
if (typeof revenue === 'number') {
|
|
26
|
+
properties[AdxParam.REVENUE] = revenue;
|
|
27
|
+
}
|
|
28
|
+
if (typeof params.currency === 'string') {
|
|
29
|
+
properties[AdxParam.CURRENCY] = params.currency;
|
|
30
|
+
}
|
|
31
|
+
if (typeof contentId === 'string') {
|
|
32
|
+
properties[AdxParam.CONTENT_ID] = contentId;
|
|
33
|
+
}
|
|
34
|
+
return properties;
|
|
35
|
+
}
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const prototype = Object.getPrototypeOf(value);
|
|
41
|
+
return prototype === Object.prototype || prototype === null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export const AdxEvent = {
|
|
4
|
+
PURCHASE: 'adx_purchase',
|
|
5
|
+
ADD_TO_CART: 'adx_add_to_cart',
|
|
6
|
+
LOGIN: 'adx_login',
|
|
7
|
+
COMPLETE_REGISTRATION: 'adx_complete_registration'
|
|
8
|
+
};
|
|
9
|
+
export const AdxParam = {
|
|
10
|
+
REVENUE: 'adx_revenue',
|
|
11
|
+
CURRENCY: 'adx_currency',
|
|
12
|
+
CONTENT_ID: 'adx_content_id'
|
|
13
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { BASE_URL } from "../../config/ApiConstants.js";
|
|
4
|
+
import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
|
|
5
|
+
export class ApiClient {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
const fetchImplementation = config.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
9
|
+
if (typeof fetchImplementation !== 'function') {
|
|
10
|
+
throw new AdaxonSdkError('INVALID_CONFIG', 'A fetch implementation is required.');
|
|
11
|
+
}
|
|
12
|
+
this.fetchImplementation = fetchImplementation;
|
|
13
|
+
}
|
|
14
|
+
async post(path, body) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
const response = await this.fetchImplementation(`${BASE_URL}${path}`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Accept': 'application/json',
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'x-sdk-key': this.config.sdkKey
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
signal: controller.signal
|
|
27
|
+
});
|
|
28
|
+
const parsedBody = this.parseJson(await response.text());
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw AdaxonSdkError.fromHttp(response.status, parsedBody);
|
|
31
|
+
}
|
|
32
|
+
if (parsedBody === null) {
|
|
33
|
+
throw new AdaxonSdkError('HTTP_ERROR', 'Adaxon returned an empty or invalid JSON response.', {
|
|
34
|
+
status: response.status
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return parsedBody;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof AdaxonSdkError) {
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
43
|
+
throw new AdaxonSdkError('TIMEOUT', 'Adaxon request timed out.', {
|
|
44
|
+
cause: error
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const detail = AdaxonSdkError.messageOf(error, '');
|
|
48
|
+
throw new AdaxonSdkError('NETWORK_ERROR', `Unable to reach Adaxon at ${BASE_URL}.${detail ? ` ${detail}` : ''}`, {
|
|
49
|
+
cause: error
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
parseJson(rawBody) {
|
|
56
|
+
try {
|
|
57
|
+
return rawBody ? JSON.parse(rawBody) : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|