Skip to content

Commit d2355b3

Browse files
author
chenru
committed
1.新增针对 navigation 控件的页面浏览全埋点采集;
2.修复触摸会触发点击事件的缺陷。
1 parent 8f83f35 commit d2355b3

File tree

15 files changed

+710
-56
lines changed

15 files changed

+710
-56
lines changed

README.md

100644100755
Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
# 1.使用 npm 方式 install 神策 SDK 模块
1+
# sensorsdata-analytics-react-native
22

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

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

77
```sh
88
npm install sensorsdata-analytics-react-native
99
```
1010

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

13-
<span style="color:red">注意:React Native 0.60 及以上版本会 autolinking,不需要执行下边的 react-native link 命令</span>
1413
```sh
1514
react-native link sensorsdata-analytics-react-native
1615
```
16+
## 1.3 配置 package.json
17+
在 package.json 文件增加如下配置:
18+
```sh
19+
"scripts": {
20+
"postinstall": "node node_modules/sensorsdata-analytics-react-native/SensorsDataRNHook.js -run"
21+
}
22+
```
1723

24+
## 1.4 执行 npm install 命令
25+
```sh
26+
npm install
27+
```
1828

1929
### 详细文档请参考:[Android & iOS SDK 在 React Native 中使用说明](https://www.sensorsdata.cn/manual/sdk_reactnative.html)
2030

31+
2132
## License
2233

2334
Copyright 2015-2020 Sensors Data Inc.
@@ -33,3 +44,5 @@ distributed under the License is distributed on an "AS IS" BASIS,
3344
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3445
See the License for the specific language governing permissions and
3546
limitations under the License.
47+
48+
**同时,我们禁止一切基于神策数据开源 SDK 的商业活动!**

RNSensorsAnalyticsModule.podspec

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Pod::Spec.new do |s|
33
s.name = "RNSensorsAnalyticsModule"
4-
s.version = "1.1.8"
4+
s.version = "2.0.0"
55
s.summary = "The official React Native SDK of Sensors Analytics."
66
s.description = <<-DESC
77
神策分析 RN 组件
@@ -18,4 +18,3 @@ Pod::Spec.new do |s|
1818

1919
end
2020

21-

SensorsDataRNHook.js

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
#! node option
2+
// 系统变量
3+
var path = require("path"),
4+
fs = require("fs"),
5+
dir = path.resolve(__dirname, "..");
6+
var reactNavigationPath = dir + '/react-navigation',
7+
reactNavigationPath3X = dir + '/@react-navigation/native/src',
8+
reactNavigationPath4X = dir + '/@react-navigation/native/lib/module';
9+
// 自定义变量
10+
// RN 控制点击事件 Touchable.js 源码文件
11+
var RNClickFilePath = dir + '/react-native/Libraries/Components/Touchable/Touchable.js';
12+
13+
// click 需 hook 的自执行代码
14+
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 */ ";
15+
16+
// hook 代码实现点击事件采集
17+
sensorsdataHookClickRN = function () {
18+
// 读取文件内容
19+
var fileContent = fs.readFileSync(RNClickFilePath, 'utf8');
20+
// 已经 hook 过了,不需要再次 hook
21+
if (fileContent.indexOf('SENSORSDATA HOOK') > -1) {
22+
return;
23+
}
24+
// 获取 hook 的代码插入的位置
25+
var hookIndex = fileContent.indexOf("this.touchableHandlePress(");
26+
// 判断文件是否异常,不存在 touchableHandlePress 方法,导致无法 hook 点击事件
27+
if (hookIndex == -1) {
28+
throw "Can't not find touchableHandlePress function";
29+
};
30+
// 插入 hook 代码
31+
var hookedContent = `${fileContent.substring(0, hookIndex)}\n${sensorsdataClickHookCode}\n${fileContent.substring(hookIndex)}`;
32+
// 备份 Touchable.js 源文件
33+
fs.renameSync(RNClickFilePath, `${RNClickFilePath}_sensorsdata_backup`);
34+
// 重写 Touchable.js 文件
35+
fs.writeFileSync(RNClickFilePath, hookedContent, 'utf8');
36+
};
37+
// 恢复被 hook 过的代码
38+
sensorsdataResetRN = function (resetFilePath) {
39+
// 读取文件内容
40+
var fileContent = fs.readFileSync(resetFilePath, "utf8");
41+
// 未被 hook 过代码,不需要处理
42+
if (fileContent.indexOf('SENSORSDATA HOOK') == -1) {
43+
return;
44+
}
45+
// 检查备份文件是否存在
46+
var backFilePath = `${resetFilePath}_sensorsdata_backup`;
47+
if (!fs.existsSync(backFilePath)) {
48+
throw `File: ${backFilePath} not found, Please rm -rf node_modules and npm install again`;
49+
}
50+
// 将备份文件重命名恢复 + 自动覆盖被 hook 过的同名 Touchable.js 文件
51+
fs.renameSync(backFilePath, resetFilePath);
52+
};
53+
54+
55+
56+
addTryCatch = function (functionBody) {
57+
functionBody = functionBody.replace(/this/g, 'thatThis');
58+
return "(function(thatThis){\n" +
59+
" try{\n " + functionBody +
60+
" \n } catch (error) { throw new Error('SensorsData RN Hook Code 调用异常: ' + error);}\n" +
61+
"})(this); /* SENSORSDATA HOOK */";
62+
}
63+
64+
65+
// hook 代码实现 PageView 事件采集;
66+
67+
68+
navigationString3 = function (prevStateVarName, currentStateVarName, actionName) {
69+
var script = `function $$$getActivePageName$$$(navigationState){
70+
if(!navigationState)
71+
return null;
72+
const route = navigationState.routes[navigationState.index];
73+
if(route.routes){
74+
return $$$getActivePageName$$$(route);
75+
}else{
76+
if(route.params) {
77+
if(!route.params["sensorsdataurl"]){
78+
route.params.sensorsdataurl = route.routeName;
79+
}
80+
return route.params;
81+
} else {
82+
route.params = {sensorsdataurl:route.routeName};
83+
}
84+
return route.params;
85+
}
86+
}
87+
`;
88+
89+
if (actionName) {
90+
script = `${script}
91+
var type = ${actionName}.type;
92+
var iosOnPageShow = false;
93+
94+
if (require('react-native').Platform.OS === 'android') {
95+
if(type == 'Navigation/SET_PARAMS' || type == 'Navigation/COMPLETE_TRANSITION') {
96+
return;
97+
}
98+
} else if (require('react-native').Platform.OS === 'ios') {
99+
if(type == 'Navigation/BACK' && (${currentStateVarName} && !${currentStateVarName}.isTransitioning)) {
100+
iosOnPageShow = true;
101+
} else if (!(type == 'Navigation/SET_PARAMS' || type == 'Navigation/COMPLETE_TRANSITION')) {
102+
iosOnPageShow = true;
103+
}
104+
if (!iosOnPageShow) {
105+
return;
106+
}
107+
}
108+
109+
110+
`;
111+
}
112+
113+
script = `${script} var params = $$$getActivePageName$$$(${currentStateVarName});
114+
if (require('react-native').Platform.OS === 'android') {
115+
if (${prevStateVarName}){
116+
var prevParams = $$$getActivePageName$$$(${prevStateVarName});
117+
if (params.sensorsdataurl == prevParams.sensorsdataurl){
118+
return;
119+
}
120+
}
121+
require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(params);
122+
} else if (require('react-native').Platform.OS === 'ios') {
123+
if (!${actionName} || iosOnPageShow) {
124+
require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(params);
125+
}
126+
}`;
127+
return script;
128+
};
129+
navigationEventString = function () {
130+
var script = `if(require('react-native').Platform.OS !== 'ios') {
131+
return;
132+
}
133+
if(payload && payload.state && payload.state.key && payload.state.routeName && payload.state.key != payload.state.routeName) {
134+
if(payload.state.params) {
135+
if(!payload.state.params.sensorsdataurl){
136+
payload.state.params.sensorsdataurl = payload.state.routeName;
137+
}
138+
}else{
139+
payload.state.params = {sensorsdataurl:payload.state.routeName};
140+
}
141+
if(type == 'didFocus') {
142+
require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(payload.state.params);
143+
}
144+
}
145+
`;
146+
return script;
147+
};
148+
navigationString = function (currentStateVarName, actionName) {
149+
var script = `function $$$getActivePageName$$$(navigationState){
150+
if(!navigationState)
151+
return null;
152+
const route = navigationState.routes[navigationState.index];
153+
if(route.routes){
154+
return $$$getActivePageName$$$(route);
155+
}else{
156+
if(route.params) {
157+
if(!route.params["sensorsdataurl"]){
158+
route.params.sensorsdataurl = route.routeName;
159+
}
160+
return route.params;
161+
} else {
162+
route.params = {sensorsdataurl:route.routeName};
163+
}
164+
return route.params;
165+
}
166+
}
167+
`;
168+
169+
if (actionName) {
170+
script = `${script}
171+
var type = ${actionName}.type;
172+
if(type == 'Navigation/SET_PARAMS' || type == 'Navigation/COMPLETE_TRANSITION') {
173+
return;
174+
}
175+
`;
176+
}
177+
178+
script = `${script} var params = $$$getActivePageName$$$(${currentStateVarName});
179+
if (require('react-native').Platform.OS === 'android') {
180+
require('react-native').NativeModules.RNSensorsDataModule.trackViewScreen(params);}`;
181+
return script;
182+
};
183+
184+
185+
/**
186+
* hook react navigation
187+
* type: 1\2\3 对应的三个不同的兼容模式的 RN 文件
188+
* reset 判断是否是重置还是 hook,true 为重置
189+
*/
190+
injectReactNavigation = function (dirPath, type, reset = false) {
191+
if (!dirPath.endsWith('/')) {
192+
dirPath += '/';
193+
}
194+
if (type == 1) {
195+
var createNavigationContainerJsFilePath = `${dirPath}src/createNavigationContainer.js`;
196+
var getChildEventSubscriberJsFilePath = `${dirPath}src/getChildEventSubscriber.js`;
197+
if (!fs.existsSync(createNavigationContainerJsFilePath)) {
198+
return
199+
}
200+
if (!fs.existsSync(getChildEventSubscriberJsFilePath)) {
201+
return;
202+
}
203+
// common.modifyFile(createNavigationContainerJsFilePath, onNavigationStateChangeTransformer);
204+
if (reset) {
205+
sensorsdataResetRN(createNavigationContainerJsFilePath);
206+
sensorsdataResetRN(getChildEventSubscriberJsFilePath);
207+
} else {
208+
// 读取文件内容
209+
var content = fs.readFileSync(createNavigationContainerJsFilePath, 'utf8');
210+
// 已经 hook 过了,不需要再次 hook
211+
if (content.indexOf('SENSORSDATA HOOK') > -1) {
212+
return;
213+
}
214+
// 获取 hook 的代码插入的位置
215+
var index = content.indexOf("if (typeof this.props.onNavigationStateChange === 'function') {");
216+
if (index == -1)
217+
throw "index is -1";
218+
content = content.substring(0, index) + addTryCatch(navigationString('nav', 'action')) + '\n' + content.substring(index)
219+
var didMountIndex = content.indexOf('componentDidMount() {');
220+
if (didMountIndex == -1)
221+
throw "didMountIndex is -1";
222+
var forEachIndex = content.indexOf('this._actionEventSubscribers.forEach(subscriber =>', didMountIndex);
223+
var clojureEnd = content.indexOf(';', forEachIndex);
224+
// 插入 hook 代码
225+
content = content.substring(0, forEachIndex) + '{' +
226+
addTryCatch(navigationString('this.state.nav', null)) + '\n' +
227+
content.substring(forEachIndex, clojureEnd + 1) +
228+
'}' + content.substring(clojureEnd + 1);
229+
// 备份 navigation 源文件
230+
fs.renameSync(createNavigationContainerJsFilePath, `${createNavigationContainerJsFilePath}_sensorsdata_backup`);
231+
// 重写文件
232+
fs.writeFileSync(createNavigationContainerJsFilePath, content, 'utf8');
233+
234+
// common.modifyFile(getChildEventSubscriberJsFilePath, onEventSubscriberTransformer);
235+
var content = fs.readFileSync(getChildEventSubscriberJsFilePath, 'utf8');
236+
// 已经 hook 过了,不需要再次 hook
237+
if (content.indexOf('SENSORSDATA HOOK') > -1) {
238+
return;
239+
}
240+
// 获取 hook 的代码插入的位置
241+
var script = "const emit = (type, payload) => {";
242+
var index = content.indexOf(script);
243+
if (index == -1)
244+
throw "index is -1";
245+
content = content.substring(0, index + script.length) + addTryCatch(navigationEventString()) + '\n' + content.substring(index + script.length);
246+
// 备份 navigation 源文件
247+
fs.renameSync(getChildEventSubscriberJsFilePath, `${getChildEventSubscriberJsFilePath}_sensorsdata_backup`);
248+
// 重写文件
249+
fs.writeFileSync(getChildEventSubscriberJsFilePath, content, 'utf8');
250+
}
251+
252+
} else if (type == 2) {
253+
const createAppContainerJsFilePath = `${dirPath}/createAppContainer.js`;
254+
if (!fs.existsSync(createAppContainerJsFilePath)) {
255+
return;
256+
}
257+
if (reset) {
258+
sensorsdataResetRN(createAppContainerJsFilePath);
259+
} else {
260+
// common.modifyFile(createAppContainerJsFilePath, onNavigationStateChangeTransformer3);
261+
// 读取文件内容
262+
var content = fs.readFileSync(createAppContainerJsFilePath, 'utf8');
263+
// 已经 hook 过了,不需要再次 hook
264+
if (content.indexOf('SENSORSDATA HOOK') > -1) {
265+
return;
266+
}
267+
var index = content.indexOf("if (typeof this.props.onNavigationStateChange === 'function') {");
268+
if (index == -1)
269+
throw "index is -1";
270+
content = content.substring(0, index) + addTryCatch(navigationString3('prevNav', 'nav', 'action')) + '\n' + content.substring(index)
271+
var didMountIndex = content.indexOf('componentDidMount() {');
272+
if (didMountIndex == -1)
273+
throw "didMountIndex is -1";
274+
var forEachIndex = content.indexOf('this._actionEventSubscribers.forEach(subscriber =>', didMountIndex);
275+
if (forEachIndex == -1) {
276+
forEachIndex = content.indexOf(
277+
'this._actionEventSubscribers.forEach((subscriber) =>',
278+
didMountIndex,
279+
);
280+
}
281+
var clojureEnd = content.indexOf(';', forEachIndex);
282+
content = content.substring(0, forEachIndex) + '{' +
283+
addTryCatch(navigationString3(null, 'this.state.nav', null)) + '\n' +
284+
content.substring(forEachIndex, clojureEnd + 1) +
285+
'}' + content.substring(clojureEnd + 1);
286+
// 备份 navigation 源文件
287+
fs.renameSync(createAppContainerJsFilePath, `${createAppContainerJsFilePath}_sensorsdata_backup`);
288+
// 重写文件
289+
fs.writeFileSync(createAppContainerJsFilePath, content, 'utf8');
290+
}
291+
}
292+
}
293+
sensorsdataHookViewRN = function () {
294+
injectReactNavigation(reactNavigationPath, 1);
295+
injectReactNavigation(reactNavigationPath3X, 2);
296+
injectReactNavigation(reactNavigationPath4X, 2)
297+
};
298+
299+
// 恢复被 hook 的 view 文件
300+
sensorsdataResetViewRN = function () {
301+
injectReactNavigation(reactNavigationPath, 1, true);
302+
injectReactNavigation(reactNavigationPath3X, 2, true);
303+
injectReactNavigation(reactNavigationPath4X, 2, true)
304+
};
305+
306+
// 全部 hook 文件恢复
307+
resetAllSensorsdataHookRN = function () {
308+
sensorsdataResetRN(RNClickFilePath);
309+
sensorsdataResetViewRN();
310+
};
311+
// 命令行
312+
switch (process.argv[2]) {
313+
case '-run':
314+
sensorsdataHookClickRN(RNClickFilePath);
315+
sensorsdataHookViewRN();
316+
break;
317+
case '-reset':
318+
resetAllSensorsdataHookRN();
319+
break;
320+
default:
321+
console.log('can not find this options: ' + process.argv[2]);
322+
}
323+

0 commit comments

Comments
 (0)