From 6d013e53fe3ff07bf2d6b24390d569630b32903e Mon Sep 17 00:00:00 2001 From: xiaoweii Date: Wed, 27 Mar 2024 22:03:12 +0800 Subject: [PATCH] feat: integrate android and swift sdk --- android/build.gradle | 97 +++--- android/gradle.properties | 6 +- android/src/main/AndroidManifest.xml | 3 +- .../ClickstreamReactNativeModule.kt | 148 ---------- .../ClickstreamReactNativeModule.kt | 235 +++++++++++++++ .../ClickstreamReactNativePackage.kt | 14 +- .../ClickstreamReactNativeExample/Info.plist | 3 - example/ios/Podfile | 2 +- example/ios/Podfile.lock | 4 +- example/package.json | 2 +- example/src/App.tsx | 180 +++++++++--- ios/Clickstream | 2 +- ios/ClickstreamReactNative.mm | 31 +- ios/ClickstreamReactNative.swift | 136 +++++++-- package.json | 208 ++++++------- src/ClickstreamAnalytics.ts | 64 +++- src/__tests__/ClickstreamAnalytics.test.ts | 275 ++++++++++++++++++ ...{index.test.ts => ModuleNotLinked.test.ts} | 18 +- src/types/Analytics.ts | 11 +- 19 files changed, 1021 insertions(+), 418 deletions(-) delete mode 100644 android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativeModule.kt create mode 100644 android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativeModule.kt rename android/src/main/java/{com => software/aws/solution}/clickstreamreactnative/ClickstreamReactNativePackage.kt (69%) create mode 100644 src/__tests__/ClickstreamAnalytics.test.ts rename src/__tests__/{index.test.ts => ModuleNotLinked.test.ts} (53%) diff --git a/android/build.gradle b/android/build.gradle index 60e0a72..af476af 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,95 +1,74 @@ buildscript { - // Buildscript is evaluated before everything else so we can't use getExtOrDefault - def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ClickstreamReactNative_kotlinVersion"] + // Buildscript is evaluated before everything else so we can't use getExtOrDefault + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ClickstreamReactNative_kotlinVersion"] - repositories { - google() - mavenCentral() - } + repositories { + google() + mavenCentral() + } - dependencies { - classpath "com.android.tools.build:gradle:7.2.1" - // noinspection DifferentKotlinGradleVersion - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } + dependencies { + classpath "com.android.tools.build:gradle:7.2.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } } def isNewArchitectureEnabled() { - return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" } apply plugin: "com.android.library" apply plugin: "kotlin-android" if (isNewArchitectureEnabled()) { - apply plugin: "com.facebook.react" + apply plugin: "com.facebook.react" } def getExtOrDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ClickstreamReactNative_" + name] + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ClickstreamReactNative_" + name] } def getExtOrIntegerDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ClickstreamReactNative_" + name]).toInteger() -} - -def supportsNamespace() { - def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') - def major = parsed[0].toInteger() - def minor = parsed[1].toInteger() - - // Namespace support was added in 7.3.0 - return (major == 7 && minor >= 3) || major >= 8 + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ClickstreamReactNative_" + name]).toInteger() } android { - if (supportsNamespace()) { - namespace "com.clickstreamreactnative" - - sourceSets { - main { - manifest.srcFile "src/main/AndroidManifestNew.xml" - } + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") } - } - compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") - - defaultConfig { - minSdkVersion getExtOrIntegerDefault("minSdkVersion") - targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") - - } - - buildTypes { - release { - minifyEnabled false + buildTypes { + release { + minifyEnabled false + } } - } - lintOptions { - disable "GradleCompatible" - } + lintOptions { + disable "GradleCompatible" + } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } repositories { - mavenCentral() - google() + mavenCentral() + google() } def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { - // For < 0.71, this will be from the local maven repo - // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin - //noinspection GradleDynamicVersion - implementation "com.facebook.react:react-native:+" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "software.aws.solution:clickstream:0.11.1" + // For < 0.71, this will be from the local maven repo + // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "software.aws.solution:clickstream:0.12.0" } diff --git a/android/gradle.properties b/android/gradle.properties index 908136f..f44d713 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ ClickstreamReactNative_kotlinVersion=1.7.0 -ClickstreamReactNative_minSdkVersion=21 -ClickstreamReactNative_targetSdkVersion=31 -ClickstreamReactNative_compileSdkVersion=31 +ClickstreamReactNative_minSdkVersion=16 +ClickstreamReactNative_targetSdkVersion=30 +ClickstreamReactNative_compileSdkVersion=30 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 5e4e43f..7428d3e 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + package="software.aws.solution.clickstreamreactnative"/> diff --git a/android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativeModule.kt b/android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativeModule.kt deleted file mode 100644 index 378d706..0000000 --- a/android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativeModule.kt +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance - * with the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES - * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ -package com.clickstreamreactnative - -import com.amazonaws.logging.LogFactory -import com.amazonaws.logging.Log -import com.amplifyframework.AmplifyException -import com.amplifyframework.core.AmplifyConfiguration -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReadableMap -import com.amplifyframework.core.Amplify -import org.json.JSONObject -import software.aws.solution.clickstream.AWSClickstreamPlugin -import software.aws.solution.clickstream.ClickstreamAnalytics -import software.aws.solution.clickstream.ClickstreamEvent -import software.aws.solution.clickstream.ClickstreamItem -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - - -class ClickstreamReactNativeModule(reactContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactContext) { - private var isInitialized = false - private val cachedThreadPool: ExecutorService by lazy { Executors.newCachedThreadPool() } - private val log: Log = LogFactory.getLog( - ClickstreamReactNativeModule::class.java - ) - - override fun getName(): String { - return NAME - } - - // Example method - @ReactMethod - fun multiply(a: Double, b: Double, promise: Promise) { - promise.resolve(a * b) - } - - @ReactMethod - fun configure(map: ReadableMap, promise: Promise) { - if (isInitialized) { - promise.resolve(false) - } -// if (ThreadUtil.notInMainThread()) { -// log.error("Clickstream SDK initialization failed, please initialize in the main thread") -// promise.resolve(false) -// } - val context = reactApplicationContext - val initConfig = map.toHashMap() - val amplifyObject = JSONObject() - val analyticsObject = JSONObject() - val pluginsObject = JSONObject() - val awsClickstreamPluginObject = JSONObject() - awsClickstreamPluginObject.put("appId", initConfig["appId"]) - awsClickstreamPluginObject.put("endpoint", initConfig["endpoint"]) - pluginsObject.put("awsClickstreamPlugin", awsClickstreamPluginObject) - analyticsObject.put("plugins", pluginsObject) - amplifyObject.put("analytics", analyticsObject) - val configure = AmplifyConfiguration.fromJson(amplifyObject) - try { - Amplify.addPlugin(AWSClickstreamPlugin(context)) - Amplify.configure(configure, context) - } catch (exception: AmplifyException) { - log.error("Clickstream SDK initialization failed with error: " + exception.message) - promise.resolve(false) - } - val sessionTimeoutDuration = (initConfig["sessionTimeoutDuration"] as Double).toLong() - val sendEventsInterval = (initConfig["sendEventsInterval"] as Double).toLong() - ClickstreamAnalytics.getClickStreamConfiguration() - .withLogEvents(initConfig["isLogEvents"] as Boolean) - .withTrackScreenViewEvents(initConfig["isTrackScreenViewEvents"] as Boolean) - .withTrackUserEngagementEvents(initConfig["isTrackUserEngagementEvents"] as Boolean) - .withTrackAppExceptionEvents(initConfig["isTrackAppExceptionEvents"] as Boolean) - .withSendEventsInterval(sendEventsInterval) - .withSessionTimeoutDuration(sessionTimeoutDuration) - .withCompressEvents(initConfig["isCompressEvents"] as Boolean) - .withAuthCookie(initConfig["authCookie"] as String) - promise.resolve(true) - isInitialized = true - } - - @ReactMethod - private fun record(map: ReadableMap) { - cachedThreadPool.execute { - val event = map.toHashMap() - val eventName = event["name"] as String - val eventBuilder = ClickstreamEvent.builder().name(eventName) - if (event["items"] != null) { - val items = event["items"] as ArrayList<*> - if (items.size > 0) { - val clickstreamItems = arrayOfNulls(items.size) - for (index in 0 until items.size) { - val builder = ClickstreamItem.builder() - for ((key, value) in (items[index] as HashMap<*, *>)) { - if (value is String) { - builder.add(key.toString(), value) - } else if (value is Double) { - builder.add(key.toString(), value) - } else if (value is Boolean) { - builder.add(key.toString(), value) - } else if (value is Int) { - builder.add(key.toString(), value) - } else if (value is Long) { - builder.add(key.toString(), value) - } - } - clickstreamItems[index] = builder.build() - } - eventBuilder.setItems(clickstreamItems) - } - } - if (event["attributes"] != null) { - val attributes = event["attributes"] as HashMap<*, *> - for ((key, value) in attributes) { - if (value is String) { - eventBuilder.add(key.toString(), value) - } else if (value is Double) { - eventBuilder.add(key.toString(), value) - } else if (value is Boolean) { - eventBuilder.add(key.toString(), value) - } else if (value is Int) { - eventBuilder.add(key.toString(), value) - } else if (value is Long) { - eventBuilder.add(key.toString(), value) - } - } - } - ClickstreamAnalytics.recordEvent(eventBuilder.build()) - } - } - - companion object { - const val NAME = "ClickstreamReactNative" - } -} diff --git a/android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativeModule.kt b/android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativeModule.kt new file mode 100644 index 0000000..3699814 --- /dev/null +++ b/android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativeModule.kt @@ -0,0 +1,235 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package software.aws.solution.clickstreamreactnative + +import com.amazonaws.logging.Log +import com.amazonaws.logging.LogFactory +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import software.aws.solution.clickstream.ClickstreamAnalytics +import software.aws.solution.clickstream.ClickstreamAttribute +import software.aws.solution.clickstream.ClickstreamConfiguration +import software.aws.solution.clickstream.ClickstreamEvent +import software.aws.solution.clickstream.ClickstreamItem +import software.aws.solution.clickstream.ClickstreamUserAttribute +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + + +class ClickstreamReactNativeModule(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + private var isInitialized = false + private val cachedThreadPool: ExecutorService by lazy { Executors.newCachedThreadPool() } + private val log: Log = LogFactory.getLog( + ClickstreamReactNativeModule::class.java + ) + + override fun getName(): String { + return NAME + } + + @ReactMethod + fun init(map: ReadableMap, promise: Promise) { + if (isInitialized) { + promise.resolve(false) + return + } + val context = reactApplicationContext.applicationContext + val initConfig = map.toHashMap() + val sessionTimeoutDuration = (initConfig["sessionTimeoutDuration"] as Double).toLong() + val sendEventsInterval = (initConfig["sendEventsInterval"] as Double).toLong() + val configuration = ClickstreamConfiguration() + .withAppId(initConfig["appId"] as String) + .withEndpoint(initConfig["endpoint"] as String) + .withLogEvents(initConfig["isLogEvents"] as Boolean) + .withTrackScreenViewEvents(initConfig["isTrackScreenViewEvents"] as Boolean) + .withTrackUserEngagementEvents(initConfig["isTrackUserEngagementEvents"] as Boolean) + .withTrackAppExceptionEvents(initConfig["isTrackAppExceptionEvents"] as Boolean) + .withSendEventsInterval(sendEventsInterval) + .withSessionTimeoutDuration(sessionTimeoutDuration) + .withCompressEvents(initConfig["isCompressEvents"] as Boolean) + .withAuthCookie(initConfig["authCookie"] as String) + + (initConfig["globalAttributes"] as? HashMap<*, *>)?.takeIf { it.isNotEmpty() } + ?.let { attributes -> + val globalAttributes = ClickstreamAttribute.builder() + for ((key, value) in attributes) { + when (value) { + is String -> globalAttributes.add(key.toString(), value) + is Double -> globalAttributes.add(key.toString(), value) + is Boolean -> globalAttributes.add(key.toString(), value) + is Int -> globalAttributes.add(key.toString(), value) + is Long -> globalAttributes.add(key.toString(), value) + } + } + configuration.withInitialGlobalAttributes(globalAttributes.build()) + } + val latch = CountDownLatch(1); + try { + reactApplicationContext.runOnUiQueueThread { + ClickstreamAnalytics.init(context, configuration) + latch.countDown() + } + latch.await() + promise.resolve(true) + isInitialized = true + } catch (exception: Exception) { + promise.resolve(false) + log.error("Clickstream SDK initialization failed with error: " + exception.message) + } + } + + @ReactMethod + private fun record(map: ReadableMap) { + cachedThreadPool.execute { + val event = map.toHashMap() + val eventName = event["name"] as String + val eventBuilder = ClickstreamEvent.builder().name(eventName) + (event["items"] as? List<*>)?.takeIf { it.isNotEmpty() }?.let { items -> + val clickstreamItems = arrayOfNulls(items.size) + for (index in items.indices) { + val builder = ClickstreamItem.builder() + for ((key, value) in (items[index] as HashMap<*, *>)) { + when (value) { + is String -> builder.add(key.toString(), value) + is Double -> builder.add(key.toString(), value) + is Boolean -> builder.add(key.toString(), value) + is Int -> builder.add(key.toString(), value) + is Long -> builder.add(key.toString(), value) + } + } + clickstreamItems[index] = builder.build() + } + eventBuilder.setItems(clickstreamItems) + } + (event["attributes"] as? Map<*, *>)?.forEach { (key, value) -> + when (value) { + is String -> eventBuilder.add(key.toString(), value) + is Double -> eventBuilder.add(key.toString(), value) + is Boolean -> eventBuilder.add(key.toString(), value) + is Int -> eventBuilder.add(key.toString(), value) + is Long -> eventBuilder.add(key.toString(), value) + } + } + ClickstreamAnalytics.recordEvent(eventBuilder.build()) + } + } + + @ReactMethod + private fun setUserId(userId: String?) { + ClickstreamAnalytics.setUserId(userId) + } + + @ReactMethod + private fun setUserAttributes(map: ReadableMap) { + val attributes = map.toHashMap() + val builder = ClickstreamUserAttribute.Builder() + for ((key, value) in attributes) { + when (value) { + is String -> builder.add(key, value) + is Double -> builder.add(key, value) + is Boolean -> builder.add(key, value) + is Int -> builder.add(key, value) + is Long -> builder.add(key, value) + } + } + builder.build().takeIf { it.userAttributes.size() > 0 }?.let { userAttributes -> + ClickstreamAnalytics.addUserAttributes(userAttributes) + } + } + + @ReactMethod + private fun setGlobalAttributes(map: ReadableMap) { + val attributes = map.toHashMap() + val builder = ClickstreamAttribute.Builder() + attributes.forEach { (key, value) -> + when (value) { + is String -> builder.add(key, value) + is Double -> builder.add(key, value) + is Boolean -> builder.add(key, value) + is Int -> builder.add(key, value) + is Long -> builder.add(key, value) + } + } + builder.build().takeIf { it.attributes.size() > 0 }?.let { globalAttributes -> + ClickstreamAnalytics.addGlobalAttributes(globalAttributes) + } + } + + @ReactMethod + private fun deleteGlobalAttributes(array: ReadableArray) { + val stringList = array.toArrayList().filterIsInstance() + if (stringList.isNotEmpty()) { + ClickstreamAnalytics.deleteGlobalAttributes(*stringList.toTypedArray()) + } + } + + @ReactMethod + private fun updateConfigure(map: ReadableMap?) { + map?.toHashMap()?.takeIf { it.isNotEmpty() }?.also { arguments -> + val configure = ClickstreamAnalytics.getClickStreamConfiguration() + arguments["appId"]?.let { + configure.withAppId(it as String) + } + arguments["endpoint"]?.let { + configure.withEndpoint(it as String) + } + arguments["isLogEvents"]?.let { + configure.withLogEvents(it as Boolean) + } + arguments["isTrackScreenViewEvents"]?.let { + configure.withTrackScreenViewEvents(it as Boolean) + } + arguments["isTrackUserEngagementEvents"]?.let { + configure.withTrackUserEngagementEvents(it as Boolean) + } + arguments["isTrackAppExceptionEvents"]?.let { + configure.withTrackAppExceptionEvents(it as Boolean) + } + arguments["isCompressEvents"]?.let { + configure.withCompressEvents(it as Boolean) + } + arguments["authCookie"]?.let { + configure.withAuthCookie(it as String) + } + } + } + + @ReactMethod + private fun flushEvents() { + ClickstreamAnalytics.flushEvents() + } + + @ReactMethod + private fun disable() { + reactApplicationContext.runOnUiQueueThread { + ClickstreamAnalytics.disable() + } + } + + @ReactMethod + private fun enable() { + reactApplicationContext.runOnUiQueueThread { + ClickstreamAnalytics.enable() + } + } + + companion object { + const val NAME = "ClickstreamReactNative" + } +} diff --git a/android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativePackage.kt b/android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativePackage.kt similarity index 69% rename from android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativePackage.kt rename to android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativePackage.kt index 54f5330..def3c2e 100644 --- a/android/src/main/java/com/clickstreamreactnative/ClickstreamReactNativePackage.kt +++ b/android/src/main/java/software/aws/solution/clickstreamreactnative/ClickstreamReactNativePackage.kt @@ -10,7 +10,7 @@ * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * and limitations under the License. */ -package com.clickstreamreactnative +package software.aws.solution.clickstreamreactnative import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule @@ -19,11 +19,11 @@ import com.facebook.react.uimanager.ViewManager class ClickstreamReactNativePackage : ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - return listOf(ClickstreamReactNativeModule(reactContext)) - } + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(ClickstreamReactNativeModule(reactContext)) + } - override fun createViewManagers(reactContext: ReactApplicationContext): List> { - return emptyList() - } + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } } diff --git a/example/ios/ClickstreamReactNativeExample/Info.plist b/example/ios/ClickstreamReactNativeExample/Info.plist index 18e41c8..3d0e8b5 100644 --- a/example/ios/ClickstreamReactNativeExample/Info.plist +++ b/example/ios/ClickstreamReactNativeExample/Info.plist @@ -26,10 +26,7 @@ NSAppTransportSecurity - NSAllowsArbitraryLoads - - NSAllowsLocalNetworking NSLocationWhenInUseUsageDescription diff --git a/example/ios/Podfile b/example/ios/Podfile index 0d3bad7..83546bf 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -5,7 +5,7 @@ require Pod::Executable.execute_command('node', ['-p', {paths: [process.argv[1]]}, )', __dir__]).strip -platform :ios, 13.0 +platform :ios, 13.4 prepare_react_native_project! # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ddbe4cf..0b7f1c9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1332,7 +1332,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Amplify: 516e5da5f256f62841b6bc659e1644bc999d7b6e boost: d3f49c53809116a5d38da093a8aa78bf551aed09 - clickstream-react-native: 7269f748bcd33ceab502782f18798320764e3ee6 + clickstream-react-native: ff710a895a3d523452c6850666062b489e9e035f CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FBLazyVector: 56e0e498dbb513b96c40bac6284729ba4e62672d @@ -1396,6 +1396,6 @@ SPEC CHECKSUMS: SQLite.swift: 4fc2be46c36392e3b87afe6fe7f1801c1daa07ef Yoga: a716eea57d0d3430219c0a5a233e1e93ee931eb7 -PODFILE CHECKSUM: 133fa8278bdce9cfb4f45b4658fd278b67278ede +PODFILE CHECKSUM: daadd1904a2d3fd60284a76fa1afe9c56098c16f COCOAPODS: 1.13.0 diff --git a/example/package.json b/example/package.json index 9f209c5..4608606 100644 --- a/example/package.json +++ b/example/package.json @@ -6,7 +6,7 @@ "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start", - "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", + "build:android": "cd android && ./gradlew assembleDebug", "build:ios": "cd ios && xcodebuild -workspace ClickstreamReactNativeExample.xcworkspace -scheme ClickstreamReactNativeExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { diff --git a/example/src/App.tsx b/example/src/App.tsx index 00997a8..f786371 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,70 +1,180 @@ import * as React from 'react'; -import { StyleSheet, View, Text } from 'react-native'; +import { + StyleSheet, + Text, + TouchableOpacity, + ScrollView, + SafeAreaView, +} from 'react-native'; import { ClickstreamAnalytics } from 'clickstream-react-native'; export default function App() { - const [result, setResult] = React.useState(); - const [initResult, setInitResult] = React.useState(); - - React.useEffect(() => { - ClickstreamAnalytics.multiply(3, 7).then(setResult); - ClickstreamAnalytics.configure({ - appId: '123', - endpoint: 'https://example.com/collect', + const initSDK = async () => { + const res = await ClickstreamAnalytics.init({ + appId: 'shopping', + endpoint: + 'http://Clicks-Inges-GMCZD4cV3Xyp-634383170.us-east-1.elb.amazonaws.com/collect', isLogEvents: true, - sendEventsInterval: 8000, - isTrackScreenViewEvents: false, + sendEventsInterval: 10000, + isTrackScreenViewEvents: true, isCompressEvents: false, - sessionTimeoutDuration: 20000, - }).then(setInitResult); - + sessionTimeoutDuration: 30000, + globalAttributes: { + channel: 'Samsung', + Class: 5, + isTrue: true, + Score: 24.32, + }, + }); + console.log('init result is:' + res); + }; + const recordEventWithName = () => { + ClickstreamAnalytics.record({ + name: 'testEventWithName', + }); + }; + const recordEventWithAttributes = () => { ClickstreamAnalytics.record({ - name: 'button_click', + name: 'testEventWithAttributes', attributes: { category: 'shoes', - currency: 'CNY', intValue: 13, - longValue: 99999999139919, - doubleValue: 11.1234567890121213, boolValue: true, value: 279.9, }, + }); + }; + const recordEventWithItems = () => { + ClickstreamAnalytics.record({ + name: 'product_view', + attributes: { + category: 'shoes', + currency: 'CNY', + price: 279.9, + }, items: [ { id: '1', - name: 'testName1', - brand: 'Google', + name: 'boy shoes', + brand: 'Nike', currency: 'CNY', - category: 'book', + category: 'shoes', locationId: '1', - intValue: 13, - longValue: 99999999139919, - doubleValue: 11.1234567890121213, - boolValue: true, - value: 279.9, }, ], }); + }; + const setUserId = () => { + ClickstreamAnalytics.setUserId('123'); + }; + + const setUserIdNull = () => { + ClickstreamAnalytics.setUserId(null); + }; + + const setUserAttributes = () => { + ClickstreamAnalytics.setUserAttributes({ + category: 'shoes', + currency: 'CNY', + value: 279.9, + }); + ClickstreamAnalytics.setUserAttributes({}); + }; + const setGlobalAttributes = () => { + ClickstreamAnalytics.setGlobalAttributes({}); + ClickstreamAnalytics.setGlobalAttributes({ + channel: 'Samsung', + Class: 5, + isTrue: true, + Score: 24.32, + }); + }; + + const deleteGlobalAttributes = () => { + ClickstreamAnalytics.deleteGlobalAttributes(['Class', 'isTrue', 'Score']); + ClickstreamAnalytics.deleteGlobalAttributes(['']); + }; + const updateConfigure = () => { + ClickstreamAnalytics.updateConfigure({ + appId: 'shopping1', + endpoint: 'https://example.com/collect', + isLogEvents: true, + isCompressEvents: false, + isTrackUserEngagementEvents: false, + isTrackAppExceptionEvents: false, + authCookie: 'test cookie', + isTrackScreenViewEvents: false, + }); + }; + + const flushEvents = () => { + ClickstreamAnalytics.flushEvents(); + }; + + const disable = () => { + ClickstreamAnalytics.disable(); + }; + + const enable = () => { + ClickstreamAnalytics.enable(); + }; + + React.useEffect(() => { + initSDK().then(() => {}); }, []); return ( - - Result: {result} - Init SDK Result: {initResult ? 'success' : 'false'} - + + + + + + + + + + + + + + + + + ); } +interface ListItemProps { + title: string; + onPress: () => void; +} + +const ListItem: React.FC = ({ title, onPress }) => ( + + {title} + +); + const styles = StyleSheet.create({ container: { flex: 1, + marginTop: 20, + }, + listItem: { + flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', + padding: 12, + borderBottomWidth: 1, + borderBottomColor: '#ddd', }, - box: { - width: 60, - height: 60, - marginVertical: 20, + title: { + fontSize: 16, }, }); diff --git a/ios/Clickstream b/ios/Clickstream index 534d1f5..3fd0573 160000 --- a/ios/Clickstream +++ b/ios/Clickstream @@ -1 +1 @@ -Subproject commit 534d1f51b2733b665e71c6557ee462b4c33196d7 +Subproject commit 3fd05730311702b82eaf5108b60f73a400667901 diff --git a/ios/ClickstreamReactNative.mm b/ios/ClickstreamReactNative.mm index e1ac2a3..9063935 100644 --- a/ios/ClickstreamReactNative.mm +++ b/ios/ClickstreamReactNative.mm @@ -8,18 +8,43 @@ @interface RCT_EXTERN_MODULE(ClickstreamReactNative, NSObject) -RCT_EXTERN_METHOD(multiply:(float)a:(float)b +RCT_EXTERN_METHOD(init:(NSDictionary *)arguments :(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(configure:(NSDictionary *)arguments +RCT_EXTERN_METHOD(record:(NSDictionary *)arguments :(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(record:(NSDictionary *)arguments +RCT_EXTERN_METHOD(setUserId:(NSString *)userId + :(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(setUserAttributes:(NSDictionary *)arguments + :(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(setGlobalAttributes:(NSDictionary *)arguments + :(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(deleteGlobalAttributes:(NSArray *)arguments :(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(updateConfigure:(NSDictionary *)arguments + :(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(flushEvents:(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(disable:(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(enable:(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject) + + (BOOL)requiresMainQueueSetup { return NO; diff --git a/ios/ClickstreamReactNative.swift b/ios/ClickstreamReactNative.swift index 75d0eaf..6ea2114 100644 --- a/ios/ClickstreamReactNative.swift +++ b/ios/ClickstreamReactNative.swift @@ -8,33 +8,35 @@ import Amplify @objc(ClickstreamReactNative) class ClickstreamReactNative: NSObject { - @objc(multiply::::) - func multiply(a: Float, b: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - resolve(a * b) - } + var isInitialized = false - @objc(configure:::) - func configure(arguments: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + @objc(init:::) + func `init`(arguments: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + if isInitialized { + resolve(false) + return + } do { - let plugins: [String: JSONValue] = [ - "awsClickstreamPlugin": [ - "appId": JSONValue.string(arguments["appId"] as! String), - "endpoint": JSONValue.string(arguments["endpoint"] as! String), - "isCompressEvents": JSONValue.boolean(arguments["isCompressEvents"] as! Bool), - "autoFlushEventsInterval": JSONValue.number(arguments["sendEventsInterval"] as! Double), - "isTrackAppExceptionEvents": JSONValue.boolean(arguments["isTrackAppExceptionEvents"] as! Bool) - ] - ] - let analyticsConfiguration = AnalyticsCategoryConfiguration(plugins: plugins) - let config = AmplifyConfiguration(analytics: analyticsConfiguration) - try Amplify.add(plugin: AWSClickstreamPlugin()) - try Amplify.configure(config) - let configure = try ClickstreamAnalytics.getClickstreamConfiguration() - configure.isLogEvents = arguments["isLogEvents"] as! Bool - configure.isTrackScreenViewEvents = arguments["isTrackScreenViewEvents"] as! Bool - configure.isTrackUserEngagementEvents = arguments["isTrackUserEngagementEvents"] as! Bool - configure.sessionTimeoutDuration = arguments["sessionTimeoutDuration"] as! Int64 - configure.authCookie = arguments["authCookie"] as? String + let configuration = ClickstreamConfiguration() + .withAppId(arguments["appId"] as! String) + .withEndpoint(arguments["endpoint"] as! String) + .withLogEvents(arguments["isLogEvents"] as! Bool) + .withTrackScreenViewEvents(arguments["isTrackScreenViewEvents"] as! Bool) + .withTrackUserEngagementEvents(arguments["isTrackUserEngagementEvents"] as! Bool) + .withTrackAppExceptionEvents(arguments["isTrackAppExceptionEvents"] as! Bool) + .withSendEventInterval(arguments["sendEventsInterval"] as! Int) + .withSessionTimeoutDuration(arguments["sessionTimeoutDuration"] as! Int64) + .withCompressEvents(arguments["isCompressEvents"] as! Bool) + .withAuthCookie(arguments["authCookie"] as! String) + if arguments["globalAttributes"] != nil { + let attributes = arguments["globalAttributes"] as! [String: Any] + if attributes.count > 0 { + let globalAttributes = getClickstreamAttributes(attributes) + _ = configuration.withInitialGlobalAttributes(globalAttributes) + } + } + try ClickstreamAnalytics.initSDK(configuration) + isInitialized = true resolve(true) } catch { log.error("Fail to initialize ClickstreamAnalytics: \(error)") @@ -45,12 +47,12 @@ class ClickstreamReactNative: NSObject { @objc(record:::) func record(arguments: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { let eventName = arguments["name"] as! String - let attributes = arguments["attributes"] as! [String: Any] - let items = arguments["items"] as! [[String: Any]] + let attributes = arguments["attributes"] as? [String: Any] ?? [:] + let items = arguments["items"] as? [[String: Any]] if attributes.count > 0 { - if items.count > 0 { + if items != nil, items!.count > 0 { var clickstreamItems: [ClickstreamAttribute] = [] - for itemObject in items { + for itemObject in items! { clickstreamItems.append(getClickstreamAttributes(itemObject)) } ClickstreamAnalytics.recordEvent(eventName, getClickstreamAttributes(attributes), clickstreamItems) @@ -62,6 +64,82 @@ class ClickstreamReactNative: NSObject { } } + @objc(setUserId:::) + func setUserId(userId: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + ClickstreamAnalytics.setUserId(userId) + } + + @objc(setUserAttributes:::) + func setUserAttributes(arguments: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let userAttributes = getClickstreamAttributes(arguments) + if userAttributes.count > 0 { + ClickstreamAnalytics.addUserAttributes(getClickstreamAttributes(arguments)) + } + } + + @objc(setGlobalAttributes:::) + func setGlobalAttributes(arguments: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let globalAttributes = getClickstreamAttributes(arguments) + if globalAttributes.count > 0 { + ClickstreamAnalytics.addGlobalAttributes(getClickstreamAttributes(arguments)) + } + } + + @objc(deleteGlobalAttributes:::) + func deleteGlobalAttributes(arguments: [String], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + for attribute in arguments { + ClickstreamAnalytics.deleteGlobalAttributes(attribute) + } + } + + @objc(updateConfigure:::) + func updateConfigure(arguments: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + do { + let configuration = try ClickstreamAnalytics.getClickstreamConfiguration() + if let appId = arguments["appId"] as? String { + configuration.appId = appId + } + if let endpoint = arguments["endpoint"] as? String { + configuration.endpoint = endpoint + } + if let isLogEvents = arguments["isLogEvents"] as? Bool { + configuration.isLogEvents = isLogEvents + } + if let isTrackScreenViewEvents = arguments["isTrackScreenViewEvents"] as? Bool { + configuration.isTrackScreenViewEvents = isTrackScreenViewEvents + } + if let isTrackUserEngagementEvents = arguments["isTrackUserEngagementEvents"] as? Bool { + configuration.isTrackUserEngagementEvents = isTrackUserEngagementEvents + } + if let isTrackAppExceptionEvents = arguments["isTrackAppExceptionEvents"] as? Bool { + configuration.isTrackAppExceptionEvents = isTrackAppExceptionEvents + } + if let isCompressEvents = arguments["isCompressEvents"] as? Bool { + configuration.isCompressEvents = isCompressEvents + } + if let authCookie = arguments["authCookie"] as? String { + configuration.authCookie = authCookie + } + } catch { + log.error("Failed to config ClickstreamAnalytics: \(error)") + } + } + + @objc(flushEvents::) + func flushEvents(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + ClickstreamAnalytics.flushEvents() + } + + @objc(disable::) + func disable(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + ClickstreamAnalytics.disable() + } + + @objc(enable::) + func enable(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + ClickstreamAnalytics.enable() + } + func getClickstreamAttributes(_ attrs: [String: Any]) -> ClickstreamAttribute { var attributes: ClickstreamAttribute = [:] for (key, value) in attrs { diff --git a/package.json b/package.json index 6787f7c..79fcee4 100644 --- a/package.json +++ b/package.json @@ -1,106 +1,106 @@ { - "name": "clickstream-react-native", - "version": "0.1.0", - "description": "ClickstreamAnalytics React Native SDK", - "main": ".lib/index.ts", - "module": "./lib-esm/index.js", - "typings": "./lib-esm/index.d.ts", - "react-native": ".src/index.ts", - "source": "src/index", - "scripts": { - "example": "yarn workspace clickstream-react-native-example start", - "test": "npx jest -w 1 --coverage", - "lint": "npx eslint 'src/*.{js,ts,tsx}'", - "format": "npx prettier --check 'src/**/*.{js,ts}'", - "clean-all": "rimraf android/build example/android/build example/android/app/build example/ios/build lib lib-esm", - "clean-js": "rimraf lib lib-esm", - "build:cjs": "npx tsc --module commonjs", - "build:esm": "npx tsc --module esnext --outDir lib-esm", - "build": "npm run clean-js && npm run build:esm && npm run build:cjs", - "pack": "npm run build && npm pack" - }, - "repository": { - "type": "git", - "url": "https://github.com/awslabs/clickstream-react-native.git" - }, - "author": "AWS GCR Solutions Team", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/awslabs/clickstream-react-native.git/issues" - }, - "homepage": "https://awslabs.github.io/clickstream-analytics-on-aws", - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@react-native/eslint-config": "0.73.2", - "@types/jest": "^29.5.5", - "@types/react": "^18.2.44", - "eslint": "^8.51.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.1", - "jest": "^29.7.0", - "prettier": "^3.0.3", - "react-native": "0.73.5", - "typescript": "^4.9.5" - }, - "peerDependencies": { - "react-native": "*" - }, - "workspaces": [ - "example" - ], - "packageManager": "yarn@3.6.1", - "eslintConfig": { - "root": true, - "extends": [ - "@react-native", - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - } - ] - } - }, - "eslintIgnore": [ - "node_modules/", - "lib/", - "lib-esm/" - ], - "prettier": { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - }, - "jest": { - "preset": "react-native", - "modulePathIgnorePatterns": [ - "/example/node_modules", - "/lib/", - "/lib-esm/" - ] - }, - "files": [ - "src", - "lib", - "lib-esm", - "android/src", - "android/build.gradle", - "android/gradle.properties", - "ios/Clickstream/Sources", - "ios/ClickstreamReactNative*", - "*.podspec", - "!**/__tests__", - "!**/.*" - ] + "name": "clickstream-react-native", + "version": "0.1.0", + "description": "ClickstreamAnalytics React Native SDK", + "main": ".lib/index.js", + "module": "./lib-esm/index.js", + "typings": "./lib-esm/index.d.ts", + "react-native": ".src/index.ts", + "source": "src/index", + "scripts": { + "example": "yarn workspace clickstream-react-native-example start", + "test": "npx jest -w 1 --coverage", + "lint": "npx eslint 'src/*.{js,ts,tsx}'", + "format": "npx prettier --check 'src/**/*.{js,ts}'", + "clean-all": "rimraf android/build example/android/build example/android/app/build example/ios/build lib lib-esm", + "clean-js": "rimraf lib lib-esm", + "build:cjs": "npx tsc --module commonjs", + "build:esm": "npx tsc --module esnext --outDir lib-esm", + "build": "npm run clean-js && npm run build:esm && npm run build:cjs", + "pack": "npm run build && npm pack" + }, + "repository": { + "type": "git", + "url": "https://github.com/awslabs/clickstream-react-native.git" + }, + "author": "AWS GCR Solutions Team", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/awslabs/clickstream-react-native.git/issues" + }, + "homepage": "https://awslabs.github.io/clickstream-analytics-on-aws", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@react-native/eslint-config": "0.73.2", + "@types/jest": "^29.5.5", + "@types/react": "^18.2.44", + "eslint": "^8.51.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "react-native": "0.73.5", + "typescript": "^4.9.5" + }, + "peerDependencies": { + "react-native": "*" + }, + "workspaces": [ + "example" + ], + "packageManager": "yarn@3.6.1", + "eslintConfig": { + "root": true, + "extends": [ + "@react-native", + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } + ] + } + }, + "eslintIgnore": [ + "node_modules/", + "lib/", + "lib-esm/" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/", + "/lib-esm/" + ] + }, + "files": [ + "src", + "lib", + "lib-esm", + "android/src", + "android/build.gradle", + "android/gradle.properties", + "ios/Clickstream/Sources", + "ios/ClickstreamReactNative*", + "*.podspec", + "!**/__tests__", + "!**/.*" + ] } diff --git a/src/ClickstreamAnalytics.ts b/src/ClickstreamAnalytics.ts index 33c2eb8..0b83a6d 100644 --- a/src/ClickstreamAnalytics.ts +++ b/src/ClickstreamAnalytics.ts @@ -12,6 +12,7 @@ */ import { NativeModules, Platform } from 'react-native'; import type { ClickstreamConfiguration, ClickstreamEvent } from './types'; +import { ClickstreamAttribute, Configuration } from './types'; const LINKING_ERROR = `The package 'clickstream-react-native' doesn't seem to be linked. Make sure: \n\n` + @@ -31,13 +32,7 @@ const ClickstreamReactNative = NativeModules.ClickstreamReactNative ); export class ClickstreamAnalytics { - public static multiply(a: number, b: number): Promise { - return ClickstreamReactNative.multiply(a, b); - } - - public static configure( - configuration: ClickstreamConfiguration - ): Promise { + public static init(configure: ClickstreamConfiguration): Promise { let initConfiguration: ClickstreamConfiguration = { appId: '', endpoint: '', @@ -50,20 +45,63 @@ export class ClickstreamAnalytics { sessionTimeoutDuration: 1800000, authCookie: '', }; - Object.assign(initConfiguration, configuration); + Object.assign(initConfiguration, configure); if (initConfiguration.appId === '' || initConfiguration.endpoint === '') { console.log('Please configure your appId and endpoint'); - return new Promise(() => { - return false; - }); + return Promise.resolve(false); } - return ClickstreamReactNative.configure(initConfiguration); + return ClickstreamReactNative.init(initConfiguration); } public static record(event: ClickstreamEvent) { - if (event.name === null || event.name === '') { + if (event.name === undefined || event.name === null || event.name === '') { console.log('Please set your event name'); + return; } ClickstreamReactNative.record(event); } + + public static setUserId(userId: string | null) { + ClickstreamReactNative.setUserId(userId); + } + + public static setUserAttributes(userAttributes: ClickstreamAttribute) { + if (this.isNotEmpty(userAttributes)) { + ClickstreamReactNative.setUserAttributes(userAttributes); + } + } + + public static setGlobalAttributes(globalAttributes: ClickstreamAttribute) { + if (this.isNotEmpty(globalAttributes)) { + ClickstreamReactNative.setGlobalAttributes(globalAttributes); + } + } + + public static deleteGlobalAttributes(globalAttributes: string[]) { + if (globalAttributes.length > 0) { + ClickstreamReactNative.deleteGlobalAttributes(globalAttributes); + } + } + + public static updateConfigure(configure: Configuration) { + if (this.isNotEmpty(configure)) { + ClickstreamReactNative.updateConfigure(configure); + } + } + + public static flushEvents() { + ClickstreamReactNative.flushEvents(); + } + + public static disable() { + ClickstreamReactNative.disable(); + } + + public static enable() { + ClickstreamReactNative.enable(); + } + + static isNotEmpty(obj: any): boolean { + return obj !== undefined && obj !== null && Object.keys(obj).length > 0; + } } diff --git a/src/__tests__/ClickstreamAnalytics.test.ts b/src/__tests__/ClickstreamAnalytics.test.ts new file mode 100644 index 0000000..d23b3c9 --- /dev/null +++ b/src/__tests__/ClickstreamAnalytics.test.ts @@ -0,0 +1,275 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { ClickstreamAnalytics } from '../index'; +import { NativeModules } from 'react-native'; + +jest.mock('react-native', () => { + const actualNativeModules = jest.requireActual('react-native').NativeModules; + return { + NativeModules: { + ...actualNativeModules, + ClickstreamReactNative: { + init: jest.fn().mockImplementation(() => { + return Promise.resolve(true); + }), + record: jest.fn(), + setUserId: jest.fn(), + setUserAttributes: jest.fn(), + setGlobalAttributes: jest.fn(), + deleteGlobalAttributes: jest.fn(), + updateConfigure: jest.fn(), + flushEvents: jest.fn(), + disable: jest.fn(), + enable: jest.fn(), + }, + }, + Platform: { + ...actualNativeModules.Platform, + select: jest.fn((obj) => obj.ios || obj.default), + }, + }; +}); +describe('ClickstreamAnalytics test', () => { + test('test init SDK with default configuration', async () => { + const res = await ClickstreamAnalytics.init({ + appId: 'testAppId', + endpoint: 'https://example.com/collect', + }); + expect(res).toBeTruthy(); + expect(NativeModules.ClickstreamReactNative.init).toHaveBeenCalledWith( + expect.objectContaining({ + appId: 'testAppId', + endpoint: 'https://example.com/collect', + sendEventsInterval: 10000, + isTrackScreenViewEvents: true, + isTrackUserEngagementEvents: true, + isTrackAppExceptionEvents: false, + isLogEvents: false, + isCompressEvents: true, + sessionTimeoutDuration: 1800000, + authCookie: '', + }) + ); + }); + + test('test init SDK with custom configuration', async () => { + await ClickstreamAnalytics.init({ + appId: 'testAppId', + endpoint: 'https://example.com/collect', + isLogEvents: true, + sendEventsInterval: 10000, + isTrackScreenViewEvents: true, + isCompressEvents: false, + sessionTimeoutDuration: 30000, + }); + expect(NativeModules.ClickstreamReactNative.init).toHaveBeenCalledWith( + expect.objectContaining({ + appId: 'testAppId', + endpoint: 'https://example.com/collect', + isLogEvents: true, + sendEventsInterval: 10000, + isTrackScreenViewEvents: true, + isCompressEvents: false, + sessionTimeoutDuration: 30000, + isTrackUserEngagementEvents: true, + isTrackAppExceptionEvents: false, + authCookie: '', + }) + ); + }); + + test('test init SDK with empty appId', async () => { + const res = await ClickstreamAnalytics.init({ + appId: '', + endpoint: 'https://example.com/collect', + }); + expect(res).toBeFalsy(); + expect(NativeModules.ClickstreamReactNative.init).not.toHaveBeenCalled(); + }); + + test('test init SDK with empty endpoint', async () => { + const res = await ClickstreamAnalytics.init({ + appId: 'testAppId', + endpoint: '', + }); + expect(res).toBeFalsy(); + expect(NativeModules.ClickstreamReactNative.init).not.toHaveBeenCalled(); + }); + + test('test record event with valid event name', () => { + ClickstreamAnalytics.record({ + name: 'product_view', + attributes: { + category: 'shoes', + currency: 'CNY', + price: 279.9, + }, + }); + expect(NativeModules.ClickstreamReactNative.record).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'product_view', + attributes: { + category: 'shoes', + currency: 'CNY', + price: 279.9, + }, + }) + ); + }); + + test('test record event with empty event name', () => { + ClickstreamAnalytics.record({ + name: '', + }); + expect(NativeModules.ClickstreamReactNative.record).not.toHaveBeenCalled(); + }); + + test('test set userId', () => { + ClickstreamAnalytics.setUserId('123'); + expect(NativeModules.ClickstreamReactNative.setUserId).toHaveBeenCalledWith( + '123' + ); + }); + + test('test set userId null', () => { + ClickstreamAnalytics.setUserId(null); + expect(NativeModules.ClickstreamReactNative.setUserId).toHaveBeenCalledWith( + null + ); + }); + + test('test set user attributes', () => { + ClickstreamAnalytics.setUserAttributes({ + user_age: 21, + user_name: 'carl', + }); + expect( + NativeModules.ClickstreamReactNative.setUserAttributes + ).toHaveBeenCalledWith( + expect.objectContaining({ + user_age: 21, + user_name: 'carl', + }) + ); + }); + test('test set empty user attribute', () => { + ClickstreamAnalytics.setUserAttributes({}); + expect( + NativeModules.ClickstreamReactNative.setUserAttributes + ).not.toHaveBeenCalled(); + }); + + test('test set global attributes', () => { + ClickstreamAnalytics.setGlobalAttributes({ + channel: 'Samsung', + Class: 5, + isTrue: true, + Score: 24.32, + }); + expect( + NativeModules.ClickstreamReactNative.setGlobalAttributes + ).toHaveBeenCalledWith( + expect.objectContaining({ + channel: 'Samsung', + Class: 5, + isTrue: true, + Score: 24.32, + }) + ); + }); + + test('test set empty global attribute', () => { + ClickstreamAnalytics.setGlobalAttributes({}); + expect( + NativeModules.ClickstreamReactNative.setGlobalAttributes + ).not.toHaveBeenCalled(); + }); + + test('test delete global attributes', () => { + ClickstreamAnalytics.deleteGlobalAttributes(['Class', 'isTrue', 'Score']); + expect( + NativeModules.ClickstreamReactNative.deleteGlobalAttributes + ).toHaveBeenCalledWith(['Class', 'isTrue', 'Score']); + }); + + test('test delete empty global attribute', () => { + ClickstreamAnalytics.deleteGlobalAttributes([]); + expect( + NativeModules.ClickstreamReactNative.deleteGlobalAttributes + ).not.toHaveBeenCalled(); + }); + + test('test update configure', () => { + ClickstreamAnalytics.updateConfigure({ + appId: 'shopping', + endpoint: 'https://example.com/collect', + isLogEvents: true, + isCompressEvents: false, + isTrackUserEngagementEvents: false, + isTrackAppExceptionEvents: false, + authCookie: 'test cookie', + isTrackScreenViewEvents: false, + }); + expect( + NativeModules.ClickstreamReactNative.updateConfigure + ).toHaveBeenCalledWith( + expect.objectContaining({ + appId: 'shopping', + endpoint: 'https://example.com/collect', + isLogEvents: true, + isCompressEvents: false, + isTrackUserEngagementEvents: false, + isTrackAppExceptionEvents: false, + authCookie: 'test cookie', + isTrackScreenViewEvents: false, + }) + ); + }); + + test('test update empty configure', () => { + ClickstreamAnalytics.updateConfigure({}); + expect( + NativeModules.ClickstreamReactNative.updateConfigure + ).not.toHaveBeenCalled(); + }); + + test('test flush events', () => { + ClickstreamAnalytics.flushEvents(); + expect(NativeModules.ClickstreamReactNative.flushEvents).toHaveBeenCalled(); + }); + + test('test enable SDK', () => { + ClickstreamAnalytics.enable(); + expect(NativeModules.ClickstreamReactNative.enable).toHaveBeenCalled(); + }); + + test('test disable SDK', () => { + ClickstreamAnalytics.disable(); + expect(NativeModules.ClickstreamReactNative.disable).toHaveBeenCalled(); + }); + + test('test isNotEmpty', async () => { + expect( + ClickstreamAnalytics.isNotEmpty({ + testKey: '123', + }) + ).toBe(true); + expect(ClickstreamAnalytics.isNotEmpty(undefined)).toBe(false); + expect(ClickstreamAnalytics.isNotEmpty(null)).toBe(false); + expect(ClickstreamAnalytics.isNotEmpty({})).toBe(false); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); +}); diff --git a/src/__tests__/index.test.ts b/src/__tests__/ModuleNotLinked.test.ts similarity index 53% rename from src/__tests__/index.test.ts rename to src/__tests__/ModuleNotLinked.test.ts index 8ee965a..bdfdf12 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/ModuleNotLinked.test.ts @@ -10,8 +10,20 @@ * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * and limitations under the License. */ -describe('ClickstreamAnalytics test', () => { - test('test add', () => { - expect(3 * 7).toBe(21); +import { ClickstreamAnalytics } from '../index'; + +describe('ModuleNotLinked test', () => { + test('test init SDK when native module unlinked', async () => { + try { + await ClickstreamAnalytics.init({ + appId: 'testAppId', + endpoint: 'https://example.com/collect', + }); + fail('test failed, should throw linking error'); + } catch (error) { + expect((error as any).message).toContain( + "The package 'clickstream-react-native' doesn't seem to be linked" + ); + } }); }); diff --git a/src/types/Analytics.ts b/src/types/Analytics.ts index 24a46d1..80eaac4 100644 --- a/src/types/Analytics.ts +++ b/src/types/Analytics.ts @@ -12,13 +12,16 @@ */ export interface ClickstreamConfiguration extends Configuration { - readonly appId: string; - readonly endpoint: string; + appId: string; + endpoint: string; readonly sendEventsInterval?: number; readonly sessionTimeoutDuration?: number; + readonly globalAttributes?: ClickstreamAttribute; } export interface Configuration { + appId?: string; + endpoint?: string; isLogEvents?: boolean; isCompressEvents?: boolean; authCookie?: string; @@ -28,7 +31,7 @@ export interface Configuration { } export interface ClickstreamAttribute { - [key: string]: string | number | boolean | null; + [key: string]: string | number | boolean; } export interface Item { @@ -47,7 +50,7 @@ export interface Item { category4?: string; category5?: string; - [key: string]: string | number | boolean | null | undefined; + [key: string]: string | number | boolean | undefined; } export interface ClickstreamEvent {