Skip to content

Commit

Permalink
1.新增针对 navigation 控件的页面浏览全埋点采集;
Browse files Browse the repository at this point in the history
2.修复触摸会触发点击事件的缺陷。
  • Loading branch information
chenru committed Apr 29, 2020
1 parent 8f83f35 commit d2355b3
Show file tree
Hide file tree
Showing 15 changed files with 710 additions and 56 deletions.
21 changes: 17 additions & 4 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# 1.使用 npm 方式 install 神策 SDK 模块
# sensorsdata-analytics-react-native

对于 React Native 开发的应用,可以使用 npm 方式集成神策 SDK RN 模块。
# 1.安装 React Native 模块

## 1.1 npm 安装 sensorsdata-analytics-react-native 模块

```sh
npm install sensorsdata-analytics-react-native
```

## 1.2 `link` sensorsdata-analytics-react-native 模块
## 1.2 `link` sensorsdata-analytics-react-native 模块(React Native 0.60 以下版本)

<span style="color:red">注意:React Native 0.60 及以上版本会 autolinking,不需要执行下边的 react-native link 命令</span>
```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.
Expand All @@ -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 的商业活动!**
3 changes: 1 addition & 2 deletions RNSensorsAnalyticsModule.podspec
Original file line number Diff line number Diff line change
@@ -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 组件
Expand All @@ -18,4 +18,3 @@ Pod::Spec.new do |s|

end


323 changes: 323 additions & 0 deletions SensorsDataRNHook.js
Original file line number Diff line number Diff line change
@@ -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]);
}

Loading

0 comments on commit d2355b3

Please sign in to comment.