diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 01d8888..2f94459 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 1.使用 npm 方式 install 神策 SDK 模块 +# sensorsdata-analytics-react-native -对于 React Native 开发的应用,可以使用 npm 方式集成神策 SDK RN 模块。 +# 1.安装 React Native 模块 ## 1.1 npm 安装 sensorsdata-analytics-react-native 模块 @@ -8,16 +8,27 @@ npm install sensorsdata-analytics-react-native ``` -## 1.2 `link` sensorsdata-analytics-react-native 模块 +## 1.2 `link` sensorsdata-analytics-react-native 模块(React Native 0.60 以下版本) -注意:React Native 0.60 及以上版本会 autolinking,不需要执行下边的 react-native link 命令 ```sh react-native link sensorsdata-analytics-react-native ``` +## 1.3 配置 package.json +在 package.json 文件增加如下配置: +```sh +"scripts": { + "postinstall": "node node_modules/sensorsdata-analytics-react-native/SensorsDataRNHook.js -run" +} +``` +## 1.4 执行 npm install 命令 + ```sh + npm install + ``` ### 详细文档请参考:[Android & iOS SDK 在 React Native 中使用说明](https://www.sensorsdata.cn/manual/sdk_reactnative.html) + ## License Copyright 2015-2020 Sensors Data Inc. @@ -33,3 +44,5 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +**同时,我们禁止一切基于神策数据开源 SDK 的商业活动!** diff --git a/RNSensorsAnalyticsModule.podspec b/RNSensorsAnalyticsModule.podspec index a88b958..03be3fe 100644 --- a/RNSensorsAnalyticsModule.podspec +++ b/RNSensorsAnalyticsModule.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "RNSensorsAnalyticsModule" - s.version = "1.1.8" + s.version = "2.0.0" s.summary = "The official React Native SDK of Sensors Analytics." s.description = <<-DESC 神策分析 RN 组件 @@ -18,4 +18,3 @@ Pod::Spec.new do |s| end - \ No newline at end of file diff --git a/SensorsDataRNHook.js b/SensorsDataRNHook.js new file mode 100644 index 0000000..f52426b --- /dev/null +++ b/SensorsDataRNHook.js @@ -0,0 +1,323 @@ +#! node option +// 系统变量 +var path = require("path"), + fs = require("fs"), + dir = path.resolve(__dirname, ".."); +var reactNavigationPath = dir + '/react-navigation', + reactNavigationPath3X = dir + '/@react-navigation/native/src', + reactNavigationPath4X = dir + '/@react-navigation/native/lib/module'; +// 自定义变量 +// RN 控制点击事件 Touchable.js 源码文件 +var RNClickFilePath = dir + '/react-native/Libraries/Components/Touchable/Touchable.js'; + +// click 需 hook 的自执行代码 +var sensorsdataClickHookCode = "(function(thatThis){ try {var ReactNative = require('react-native');thatThis.props.onPress && ReactNative.NativeModules.RNSensorsDataModule.trackViewClick(ReactNative.findNodeHandle(thatThis))} catch (error) { throw new Error('SensorsData RN Hook Code 调用异常: ' + error);}})(this); /* SENSORSDATA HOOK */ "; + +// hook 代码实现点击事件采集 +sensorsdataHookClickRN = function () { + // 读取文件内容 + var fileContent = fs.readFileSync(RNClickFilePath, 'utf8'); + // 已经 hook 过了,不需要再次 hook + if (fileContent.indexOf('SENSORSDATA HOOK') > -1) { + return; + } + // 获取 hook 的代码插入的位置 + var hookIndex = fileContent.indexOf("this.touchableHandlePress("); + // 判断文件是否异常,不存在 touchableHandlePress 方法,导致无法 hook 点击事件 + if (hookIndex == -1) { + throw "Can't not find touchableHandlePress function"; + }; + // 插入 hook 代码 + var hookedContent = `${fileContent.substring(0, hookIndex)}\n${sensorsdataClickHookCode}\n${fileContent.substring(hookIndex)}`; + // 备份 Touchable.js 源文件 + fs.renameSync(RNClickFilePath, `${RNClickFilePath}_sensorsdata_backup`); + // 重写 Touchable.js 文件 + fs.writeFileSync(RNClickFilePath, hookedContent, 'utf8'); +}; +// 恢复被 hook 过的代码 +sensorsdataResetRN = function (resetFilePath) { + // 读取文件内容 + var fileContent = fs.readFileSync(resetFilePath, "utf8"); + // 未被 hook 过代码,不需要处理 + if (fileContent.indexOf('SENSORSDATA HOOK') == -1) { + return; + } + // 检查备份文件是否存在 + var backFilePath = `${resetFilePath}_sensorsdata_backup`; + if (!fs.existsSync(backFilePath)) { + throw `File: ${backFilePath} not found, Please rm -rf node_modules and npm install again`; + } + // 将备份文件重命名恢复 + 自动覆盖被 hook 过的同名 Touchable.js 文件 + fs.renameSync(backFilePath, resetFilePath); +}; + + + +addTryCatch = function (functionBody) { + functionBody = functionBody.replace(/this/g, 'thatThis'); + return "(function(thatThis){\n" + + " try{\n " + functionBody + + " \n } catch (error) { throw new Error('SensorsData RN Hook Code 调用异常: ' + error);}\n" + + "})(this); /* SENSORSDATA HOOK */"; +} + + +// hook 代码实现 PageView 事件采集; + + +navigationString3 = function (prevStateVarName, currentStateVarName, actionName) { + var script = `function $$$getActivePageName$$$(navigationState){ + if(!navigationState) + return null; + const route = navigationState.routes[navigationState.index]; + if(route.routes){ + return $$$getActivePageName$$$(route); + }else{ + if(route.params) { + if(!route.params["sensorsdataurl"]){ + route.params.sensorsdataurl = route.routeName; + } + return route.params; + } else { + route.params = {sensorsdataurl:route.routeName}; + } + return route.params; + } + } + `; + + if (actionName) { + script = `${script} + var type = ${actionName}.type; + var iosOnPageShow = false; + + if (require('react-native').Platform.OS === 'android') { + if(type == 'Navigation/SET_PARAMS' || type == 'Navigation/COMPLETE_TRANSITION') { + return; + } + } else if (require('react-native').Platform.OS === 'ios') { + if(type == 'Navigation/BACK' && (${currentStateVarName} && !${currentStateVarName}.isTransitioning)) { + iosOnPageShow = true; + } else if (!(type == 'Navigation/SET_PARAMS' || type == 'Navigation/COMPLETE_TRANSITION')) { + iosOnPageShow = true; + } + if (!iosOnPageShow) { + return; + } + } + + + `; + } + + script = `${script} var params = $$$getActivePageName$$$(${currentStateVarName}); + if (require('react-native').Platform.OS === 'android') { + if (${prevStateVarName}){ + var prevParams = $$$getActivePageName$$$(${prevStateVarName}); + if (params.sensorsdataurl == prevParams.sensorsdataurl){ + return; + } + } + require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(params); + } else if (require('react-native').Platform.OS === 'ios') { + if (!${actionName} || iosOnPageShow) { + require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(params); + } + }`; + return script; +}; +navigationEventString = function () { + var script = `if(require('react-native').Platform.OS !== 'ios') { + return; + } + if(payload && payload.state && payload.state.key && payload.state.routeName && payload.state.key != payload.state.routeName) { + if(payload.state.params) { + if(!payload.state.params.sensorsdataurl){ + payload.state.params.sensorsdataurl = payload.state.routeName; + } + }else{ + payload.state.params = {sensorsdataurl:payload.state.routeName}; + } + if(type == 'didFocus') { + require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(payload.state.params); + } + } + `; + return script; +}; +navigationString = function (currentStateVarName, actionName) { + var script = `function $$$getActivePageName$$$(navigationState){ + if(!navigationState) + return null; + const route = navigationState.routes[navigationState.index]; + if(route.routes){ + return $$$getActivePageName$$$(route); + }else{ + if(route.params) { + if(!route.params["sensorsdataurl"]){ + route.params.sensorsdataurl = route.routeName; + } + return route.params; + } else { + route.params = {sensorsdataurl:route.routeName}; + } + return route.params; + } + } + `; + + if (actionName) { + script = `${script} + var type = ${actionName}.type; + if(type == 'Navigation/SET_PARAMS' || type == 'Navigation/COMPLETE_TRANSITION') { + return; + } + `; + } + + script = `${script} var params = $$$getActivePageName$$$(${currentStateVarName}); + if (require('react-native').Platform.OS === 'android') { + require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(params);}`; + return script; +}; + + +/** + * hook react navigation + * type: 1\2\3 对应的三个不同的兼容模式的 RN 文件 + * reset 判断是否是重置还是 hook,true 为重置 +*/ +injectReactNavigation = function (dirPath, type, reset = false) { + if (!dirPath.endsWith('/')) { + dirPath += '/'; + } + if (type == 1) { + var createNavigationContainerJsFilePath = `${dirPath}src/createNavigationContainer.js`; + var getChildEventSubscriberJsFilePath = `${dirPath}src/getChildEventSubscriber.js`; + if (!fs.existsSync(createNavigationContainerJsFilePath)) { + return + } + if (!fs.existsSync(getChildEventSubscriberJsFilePath)) { + return; + } + // common.modifyFile(createNavigationContainerJsFilePath, onNavigationStateChangeTransformer); + if (reset) { + sensorsdataResetRN(createNavigationContainerJsFilePath); + sensorsdataResetRN(getChildEventSubscriberJsFilePath); + } else { + // 读取文件内容 + var content = fs.readFileSync(createNavigationContainerJsFilePath, 'utf8'); + // 已经 hook 过了,不需要再次 hook + if (content.indexOf('SENSORSDATA HOOK') > -1) { + return; + } + // 获取 hook 的代码插入的位置 + var index = content.indexOf("if (typeof this.props.onNavigationStateChange === 'function') {"); + if (index == -1) + throw "index is -1"; + content = content.substring(0, index) + addTryCatch(navigationString('nav', 'action')) + '\n' + content.substring(index) + var didMountIndex = content.indexOf('componentDidMount() {'); + if (didMountIndex == -1) + throw "didMountIndex is -1"; + var forEachIndex = content.indexOf('this._actionEventSubscribers.forEach(subscriber =>', didMountIndex); + var clojureEnd = content.indexOf(';', forEachIndex); + // 插入 hook 代码 + content = content.substring(0, forEachIndex) + '{' + + addTryCatch(navigationString('this.state.nav', null)) + '\n' + + content.substring(forEachIndex, clojureEnd + 1) + + '}' + content.substring(clojureEnd + 1); + // 备份 navigation 源文件 + fs.renameSync(createNavigationContainerJsFilePath, `${createNavigationContainerJsFilePath}_sensorsdata_backup`); + // 重写文件 + fs.writeFileSync(createNavigationContainerJsFilePath, content, 'utf8'); + + // common.modifyFile(getChildEventSubscriberJsFilePath, onEventSubscriberTransformer); + var content = fs.readFileSync(getChildEventSubscriberJsFilePath, 'utf8'); + // 已经 hook 过了,不需要再次 hook + if (content.indexOf('SENSORSDATA HOOK') > -1) { + return; + } + // 获取 hook 的代码插入的位置 + var script = "const emit = (type, payload) => {"; + var index = content.indexOf(script); + if (index == -1) + throw "index is -1"; + content = content.substring(0, index + script.length) + addTryCatch(navigationEventString()) + '\n' + content.substring(index + script.length); + // 备份 navigation 源文件 + fs.renameSync(getChildEventSubscriberJsFilePath, `${getChildEventSubscriberJsFilePath}_sensorsdata_backup`); + // 重写文件 + fs.writeFileSync(getChildEventSubscriberJsFilePath, content, 'utf8'); + } + + } else if (type == 2) { + const createAppContainerJsFilePath = `${dirPath}/createAppContainer.js`; + if (!fs.existsSync(createAppContainerJsFilePath)) { + return; + } + if (reset) { + sensorsdataResetRN(createAppContainerJsFilePath); + } else { + // common.modifyFile(createAppContainerJsFilePath, onNavigationStateChangeTransformer3); + // 读取文件内容 + var content = fs.readFileSync(createAppContainerJsFilePath, 'utf8'); + // 已经 hook 过了,不需要再次 hook + if (content.indexOf('SENSORSDATA HOOK') > -1) { + return; + } + var index = content.indexOf("if (typeof this.props.onNavigationStateChange === 'function') {"); + if (index == -1) + throw "index is -1"; + content = content.substring(0, index) + addTryCatch(navigationString3('prevNav', 'nav', 'action')) + '\n' + content.substring(index) + var didMountIndex = content.indexOf('componentDidMount() {'); + if (didMountIndex == -1) + throw "didMountIndex is -1"; + var forEachIndex = content.indexOf('this._actionEventSubscribers.forEach(subscriber =>', didMountIndex); + if (forEachIndex == -1) { + forEachIndex = content.indexOf( + 'this._actionEventSubscribers.forEach((subscriber) =>', + didMountIndex, + ); + } + var clojureEnd = content.indexOf(';', forEachIndex); + content = content.substring(0, forEachIndex) + '{' + + addTryCatch(navigationString3(null, 'this.state.nav', null)) + '\n' + + content.substring(forEachIndex, clojureEnd + 1) + + '}' + content.substring(clojureEnd + 1); + // 备份 navigation 源文件 + fs.renameSync(createAppContainerJsFilePath, `${createAppContainerJsFilePath}_sensorsdata_backup`); + // 重写文件 + fs.writeFileSync(createAppContainerJsFilePath, content, 'utf8'); + } + } +} +sensorsdataHookViewRN = function () { + injectReactNavigation(reactNavigationPath, 1); + injectReactNavigation(reactNavigationPath3X, 2); + injectReactNavigation(reactNavigationPath4X, 2) +}; + +// 恢复被 hook 的 view 文件 +sensorsdataResetViewRN = function () { + injectReactNavigation(reactNavigationPath, 1, true); + injectReactNavigation(reactNavigationPath3X, 2, true); + injectReactNavigation(reactNavigationPath4X, 2, true) +}; + +// 全部 hook 文件恢复 +resetAllSensorsdataHookRN = function () { + sensorsdataResetRN(RNClickFilePath); + sensorsdataResetViewRN(); +}; +// 命令行 +switch (process.argv[2]) { + case '-run': + sensorsdataHookClickRN(RNClickFilePath); + sensorsdataHookViewRN(); + break; + case '-reset': + resetAllSensorsdataHookRN(); + break; + default: + console.log('can not find this options: ' + process.argv[2]); +} + diff --git a/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsModule.java b/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsModule.java index 8d5d179..dd1cf70 100755 --- a/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsModule.java +++ b/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsModule.java @@ -30,6 +30,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeMap; import com.sensorsdata.analytics.android.sdk.SensorsDataAPI; +import com.sensorsdata.analytics.utils.RNUtils; import org.json.JSONObject; @@ -67,30 +68,6 @@ public String getName() { return MODULE_NAME; } - /** - * ReadableMap 转换成 JSONObject - */ - private JSONObject convertToJSONObject(ReadableMap properties) { - if (properties == null) { - return null; - } - - JSONObject json = null; - ReadableNativeMap nativeMap = null; - try { - nativeMap = (ReadableNativeMap) properties; - json = new JSONObject(properties.toString()).getJSONObject("NativeMap"); - } catch (Exception e) { - Log.e(LOGTAG, "" + e.getMessage()); - String superName = nativeMap.getClass().getSuperclass().getSimpleName(); - try { - json = new JSONObject(properties.toString()).getJSONObject(superName); - } catch (Exception e1) { - Log.e(LOGTAG, "" + e1.getMessage()); - } - } - return json; - } /** * 参数类型在@ReactMethod注明的方法中,会被直接映射到它们对应的JavaScript类型 @@ -118,7 +95,7 @@ private JSONObject convertToJSONObject(ReadableMap properties) { @ReactMethod public void track(String eventName, ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().track(eventName, convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().track(eventName, RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -190,7 +167,7 @@ public void trackTimerBegin(String eventName) { @ReactMethod public void trackTimerEnd(String eventName, ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().trackTimerEnd(eventName, convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().trackTimerEnd(eventName, RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -279,7 +256,7 @@ public void logout() { @ReactMethod public void trackInstallation(String eventName, ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().trackInstallation(eventName, convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().trackInstallation(eventName, RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -289,25 +266,25 @@ public void trackInstallation(String eventName, ReadableMap properties) { /** * 导出 trackViewScreen 方法给 RN 使用. *

- * 此方法用于 RN 中 Tab 切换页面的时候调用,用于记录 $AppViewScreen 事件. + * 此方法用于 RN 中切换页面的时候调用,用于记录 $AppViewScreen 事件. * - * @param url 页面的 url 记录到 $url 字段中(如果不需要此属性,可以传 null ). + * @param url 页面的 url 记录到 $url 字段中. * @param properties 页面的属性. *

* 注:为保证记录到的 $AppViewScreen 事件和 Auto Track 采集的一致, - * 需要传入 $title(页面的title) 、$screen_name (页面的名称,即 包名.类名)字段. + * 需要传入 $title(页面的标题) 、$screen_name (页面的名称,即 包名.类名)字段. *

* RN 中使用示例: * */ @ReactMethod public void trackViewScreen(String url, ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().trackViewScreen(url, convertToJSONObject(properties)); + RNAgent.trackViewScreen(url, RNUtils.convertToJSONObject(properties), false); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -329,7 +306,7 @@ public void trackViewScreen(String url, ReadableMap properties) { @ReactMethod public void profileSet(ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().profileSet(convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().profileSet(RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -354,7 +331,7 @@ public void profileSet(ReadableMap properties) { @ReactMethod public void profileSetOnce(ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().profileSetOnce(convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().profileSetOnce(RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -561,7 +538,7 @@ public void getAnonymousIdPromise(Promise promise) { @ReactMethod public void registerSuperProperties(ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().registerSuperProperties(convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().registerSuperProperties(RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -663,7 +640,7 @@ public void deleteAll() { @ReactMethod public void trackChannelEvent(String eventName, ReadableMap properties) { try { - SensorsDataAPI.sharedInstance().trackChannelEvent(eventName, convertToJSONObject(properties)); + SensorsDataAPI.sharedInstance().trackChannelEvent(eventName, RNUtils.convertToJSONObject(properties)); } catch (Exception e) { e.printStackTrace(); Log.e(LOGTAG, e.toString() + ""); @@ -687,5 +664,4 @@ public void identify(String distinctId) { Log.e(LOGTAG, e.toString() + ""); } } - } diff --git a/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsPackage.java b/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsPackage.java index ac319c2..237d863 100644 --- a/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsPackage.java +++ b/android/src/main/java/com/sensorsdata/analytics/RNSensorsAnalyticsPackage.java @@ -15,11 +15,13 @@ */ public class RNSensorsAnalyticsPackage implements ReactPackage { + public static final String VERSION = "2.0.0"; @Override public List createNativeModules(ReactApplicationContext reactContext) { List modules = new ArrayList<>(); //在你们的Package中 添加神策原生模块 modules.add(new RNSensorsAnalyticsModule(reactContext)); + modules.add(new RNSensorsDataModule(reactContext)); return modules; } diff --git a/ios/RNSensorsAnalyticsModule.h b/ios/RNSensorsAnalyticsModule.h index a33e67d..969424d 100644 --- a/ios/RNSensorsAnalyticsModule.h +++ b/ios/RNSensorsAnalyticsModule.h @@ -7,7 +7,11 @@ // #import +#if __has_include("RCTBridgeModule.h") +#import "RCTBridgeModule.h" +#else #import +#endif @interface RNSensorsAnalyticsModule : NSObject diff --git a/ios/RNSensorsAnalyticsModule.m b/ios/RNSensorsAnalyticsModule.m index 60326eb..449c6a9 100644 --- a/ios/RNSensorsAnalyticsModule.m +++ b/ios/RNSensorsAnalyticsModule.m @@ -7,9 +7,14 @@ // #import "RNSensorsAnalyticsModule.h" -#import -#import +#import "SAReactNativeManager.h" + +#if __has_include("SensorsAnalyticsSDK.h") +#import "SensorsAnalyticsSDK.h" +#else #import +#endif + @implementation RNSensorsAnalyticsModule @@ -210,11 +215,10 @@ @implementation RNSensorsAnalyticsModule */ RCT_EXPORT_METHOD(trackViewScreen:(NSString *)url withProperties:(NSDictionary *)properties){ @try { - [[SensorsAnalyticsSDK sharedInstance] trackViewScreen:url withProperties:properties]; + [[SAReactNativeManager sharedInstance] trackViewScreen:url properties:properties autoTrack:NO]; } @catch (NSException *exception) { NSLog(@"[RNSensorsAnalytics] error:%@",exception); } - } /** * 导出 set 方法给 RN 使用. diff --git a/ios/RNSensorsAnalyticsModule.xcodeproj/project.pbxproj b/ios/RNSensorsAnalyticsModule.xcodeproj/project.pbxproj index 658c273..52f814f 100644 --- a/ios/RNSensorsAnalyticsModule.xcodeproj/project.pbxproj +++ b/ios/RNSensorsAnalyticsModule.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ A18E9FB421A6AEDD00A66E41 /* RNSensorsAnalyticsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A18E9FB221A6AEDD00A66E41 /* RNSensorsAnalyticsModule.m */; }; + FC39004C244715AE00F486A7 /* SAReactNativeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FC39004B244715AE00F486A7 /* SAReactNativeManager.m */; }; + FC9EE671243731C000C45D16 /* RNSensorsDataModule.m in Sources */ = {isa = PBXBuildFile; fileRef = FC9EE670243731C000C45D16 /* RNSensorsDataModule.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -26,6 +28,10 @@ A18E9FA621A6AB4300A66E41 /* libRNSensorsAnalyticsModule.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNSensorsAnalyticsModule.a; sourceTree = BUILT_PRODUCTS_DIR; }; A18E9FB221A6AEDD00A66E41 /* RNSensorsAnalyticsModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSensorsAnalyticsModule.m; sourceTree = ""; }; A18E9FB321A6AEDD00A66E41 /* RNSensorsAnalyticsModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSensorsAnalyticsModule.h; sourceTree = ""; }; + FC39004A244715AE00F486A7 /* SAReactNativeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SAReactNativeManager.h; sourceTree = ""; }; + FC39004B244715AE00F486A7 /* SAReactNativeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SAReactNativeManager.m; sourceTree = ""; }; + FC9EE66F243731C000C45D16 /* RNSensorsDataModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSensorsDataModule.h; sourceTree = ""; }; + FC9EE670243731C000C45D16 /* RNSensorsDataModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSensorsDataModule.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -44,6 +50,10 @@ children = ( A18E9FB321A6AEDD00A66E41 /* RNSensorsAnalyticsModule.h */, A18E9FB221A6AEDD00A66E41 /* RNSensorsAnalyticsModule.m */, + FC9EE66F243731C000C45D16 /* RNSensorsDataModule.h */, + FC9EE670243731C000C45D16 /* RNSensorsDataModule.m */, + FC39004A244715AE00F486A7 /* SAReactNativeManager.h */, + FC39004B244715AE00F486A7 /* SAReactNativeManager.m */, A18E9FA721A6AB4300A66E41 /* Products */, ); sourceTree = ""; @@ -113,6 +123,8 @@ buildActionMask = 2147483647; files = ( A18E9FB421A6AEDD00A66E41 /* RNSensorsAnalyticsModule.m in Sources */, + FC39004C244715AE00F486A7 /* SAReactNativeManager.m in Sources */, + FC9EE671243731C000C45D16 /* RNSensorsDataModule.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/RNSensorsAnalyticsModule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/RNSensorsAnalyticsModule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/RNSensorsAnalyticsModule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/RNSensorsAnalyticsModule.xcodeproj/xcshareddata/xcschemes/RNSensorsAnalyticsModule.xcscheme b/ios/RNSensorsAnalyticsModule.xcodeproj/xcshareddata/xcschemes/RNSensorsAnalyticsModule.xcscheme new file mode 100644 index 0000000..f9f5053 --- /dev/null +++ b/ios/RNSensorsAnalyticsModule.xcodeproj/xcshareddata/xcschemes/RNSensorsAnalyticsModule.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/RNSensorsDataModule.h b/ios/RNSensorsDataModule.h new file mode 100644 index 0000000..ebd206c --- /dev/null +++ b/ios/RNSensorsDataModule.h @@ -0,0 +1,23 @@ +// +// RNSensorsDataModule.h +// RNSensorsAnalyticsModule +// +// Created by 彭远洋 on 2020/4/3. +// Copyright © 2020 ziven.mac. All rights reserved. +// + +#import + +#if __has_include("RCTBridgeModule.h") +#import "RCTBridgeModule.h" +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface RNSensorsDataModule : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RNSensorsDataModule.m b/ios/RNSensorsDataModule.m new file mode 100644 index 0000000..8078ece --- /dev/null +++ b/ios/RNSensorsDataModule.m @@ -0,0 +1,39 @@ +// +// RNSensorsDataModule.m +// RNSensorsAnalyticsModule +// +// Created by 彭远洋 on 2020/4/3. +// Copyright © 2020 ziven.mac. All rights reserved. +// + +#import "RNSensorsDataModule.h" +#import "SAReactNativeManager.h" + +@implementation RNSensorsDataModule + +RCT_EXPORT_MODULE(RNSensorsDataModule) + +/** + * React Native 自动采集点击事件 + * + * @param reactTag View 唯一标识符 + * +*/ +RCT_EXPORT_METHOD(trackViewClick:(NSInteger)reactTag) { + [[SAReactNativeManager sharedInstance] trackViewClick:@(reactTag)]; +} + +/** + * React Native 自动采集页面浏览事件 + * + * @param properties 页面相关消息 + * +*/ +RCT_EXPORT_METHOD(trackViewScreen:(NSDictionary *)params) { + // 自动采集页面浏览时 url 在 params + NSString *url = params[@"sensorsdataurl"]; + NSDictionary *properties = params[@"sensorsdataparams"]; + [[SAReactNativeManager sharedInstance] trackViewScreen:url properties:properties autoTrack:YES]; +} + +@end diff --git a/ios/SAReactNativeManager.h b/ios/SAReactNativeManager.h new file mode 100644 index 0000000..615bff7 --- /dev/null +++ b/ios/SAReactNativeManager.h @@ -0,0 +1,52 @@ +// +// SAReactNativeManager.h +// SensorsAnalyticsSDK +// +// Created by 彭远洋 on 2020/3/16. +// Copyright © 2020 Sensors Data Co., Ltd. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SAReactNativeManager : NSObject + +@property (nonatomic, copy, readonly) NSString *currentScreenName; + ++ (instancetype)sharedInstance; + +/** + @abstract + 触发 React Native 点击事件 + + @param reactTag React Native 分配的唯一标识符 + */ +- (void)trackViewClick:(NSNumber *)reactTag; + +/** + @abstract + 触发 React Native 页面浏览事件 + + @param url 页面路径 + @param properties 自定义页面属性 + @param autoTrack 是否为自动埋点 + */ +- (void)trackViewScreen:(nullable NSString *)url properties:(nullable NSDictionary *)properties autoTrack:(BOOL)autoTrack; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/SAReactNativeManager.m b/ios/SAReactNativeManager.m new file mode 100644 index 0000000..012ba07 --- /dev/null +++ b/ios/SAReactNativeManager.m @@ -0,0 +1,148 @@ +// +// SAReactNativeManager.m +// SensorsAnalyticsSDK +// +// Created by 彭远洋 on 2020/3/16. +// Copyright © 2020 Sensors Data Co., Ltd. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Either turn on ARC for the project or use -fobjc-arc flag on this file. +#endif + +#import "SAReactNativeManager.h" +#import +#import +#import + +#if __has_include("SensorsAnalyticsSDK.h") +#import "SensorsAnalyticsSDK.h" +#else +#import +#endif + +@interface SAReactNativeManager () + +@property (nonatomic, copy) NSString *currentScreenName; +@property (nonatomic, copy) NSString *currentTitle; + +@end + +@implementation SAReactNativeManager + +#pragma mark - life cycle ++ (instancetype)sharedInstance { + static SAReactNativeManager *manager; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[SAReactNativeManager alloc] init]; + }); + return manager; +} + +#pragma mark - public +- (void)trackViewClick:(NSNumber *)reactTag { + if (![[SensorsAnalyticsSDK sharedInstance] isAutoTrackEnabled]) { + return; + } + // 忽略 $AppClick 事件 + if ([[SensorsAnalyticsSDK sharedInstance] isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppClick]) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + UIView *view = [[SAReactNativeManager sharedInstance] viewForTag:reactTag]; + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + NSDictionary *clickProperties = [self viewClickPorperties]; + [properties addEntriesFromDictionary:clickProperties]; + properties[@"$element_content"] = [view accessibilityLabel]; + + [[SensorsAnalyticsSDK sharedInstance] trackViewAppClick:view withProperties:[properties copy]]; + }); +} + +- (void)trackViewScreen:(nullable NSString *)url properties:(nullable NSDictionary *)properties autoTrack:(BOOL)autoTrack { + if (url && ![url isKindOfClass:NSString.class]) { + NSLog(@"[RNSensorsAnalytics] error: url {%@} is not String Class !!!", url); + return; + } + NSString *screenName = properties[@"$screen_name"] ?: url; + NSString *title = properties[@"$title"]; + NSDictionary *pageProps = [self viewScreenProperties:screenName title:title]; + + if (autoTrack && ![[SensorsAnalyticsSDK sharedInstance] isAutoTrackEnabled]) { + return; + } + // 忽略 $AppViewScreen 事件 + if (autoTrack && [[SensorsAnalyticsSDK sharedInstance] isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppViewScreen]) { + return; + } + NSMutableDictionary *eventProps = [NSMutableDictionary dictionary]; + [eventProps addEntriesFromDictionary:pageProps]; + [eventProps addEntriesFromDictionary:properties]; + + [[SensorsAnalyticsSDK sharedInstance] trackViewScreen:url withProperties:[eventProps copy]]; +} + +#pragma mark - SDK Method ++ (RCTRootView *)rootView { + // RCTRootView 只能是 UIViewController 的 view,不能作为其他 View 的 SubView 使用 + UIViewController *root = [[[UIApplication sharedApplication] keyWindow] rootViewController]; + UIView *view = [root view]; + // 不是混编 React Native 项目时直接获取 RootViewController 的 view + if ([view isKindOfClass:RCTRootView.class]) { + return (RCTRootView *)view; + } + Class utils = NSClassFromString(@"SAAutoTrackUtils"); + if (!utils) { + return nil; + } + SEL currentCallerSEL = NSSelectorFromString(@"currentViewController"); + if (![utils respondsToSelector:currentCallerSEL]) { + return nil; + } + + // 混编 React Native 项目时获取当前显示的 UIViewController 的 view +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + UIViewController *caller = [utils performSelector:currentCallerSEL]; +#pragma clang diagnostic pop + + if (![caller.view isKindOfClass:RCTRootView.class]) { + return nil; + } + return (RCTRootView *)caller.view; +} + +#pragma mark - private +- (UIView *)viewForTag:(NSNumber *)reactTag { + RCTRootView *rootView = [SAReactNativeManager rootView]; + RCTUIManager *manager = rootView.bridge.uiManager; + return [manager viewForReactTag:reactTag]; +} + +- (NSDictionary *)viewScreenProperties:(NSString *)screenName title:(NSString *)title { + _currentScreenName = screenName; + _currentTitle = title ?: screenName; + return [self viewClickPorperties]; +} + +- (NSDictionary *)viewClickPorperties { + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + properties[@"$screen_name"] = _currentScreenName; + properties[@"$title"] = _currentTitle; + return [properties copy]; +} + +@end diff --git a/package.json b/package.json index 70152cf..282eaef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sensorsdata-analytics-react-native", - "version": "1.1.8", + "version": "2.0.0", "private": false, "description": "神策分析 RN 组件", "main": "index.js",