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