├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.md ├── debug.py ├── dev.sh ├── image ├── fail.png ├── mainpage.png └── success.png ├── lyrebird_tracking ├── __init__.py ├── context.py ├── data │ └── base.json ├── report_template │ ├── components │ │ ├── filter-tag.js │ │ └── tracking-detail.js │ ├── data │ │ └── report-data.js │ ├── index.html │ ├── main.js │ ├── report.html │ └── style │ │ └── report.css ├── server │ ├── __init__.py │ ├── base_handler.py │ ├── data_manager.py │ ├── search_handler.py │ └── validator.py ├── static │ ├── component │ │ ├── banner.vue │ │ ├── filter-tag.vue │ │ ├── main.vue │ │ ├── tracking-detail.vue │ │ └── tracking-list.vue │ └── js │ │ └── main.js ├── templates │ └── index.html ├── tracking.py └── webui.py ├── requirements.txt ├── setup.py └── tests ├── test_filter_error.py └── test_select.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | *.DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Python template 30 | # Byte-compiled / optimized / DLL files 31 | __pycache__/ 32 | *.py[cod] 33 | *$py.class 34 | 35 | # C extensions 36 | *.so 37 | 38 | # Distribution / packaging 39 | .Python 40 | env/ 41 | build/ 42 | develop-eggs/ 43 | dist/ 44 | downloads/ 45 | eggs/ 46 | .eggs/ 47 | lib/ 48 | lib64/ 49 | parts/ 50 | sdist/ 51 | var/ 52 | wheels/ 53 | *.egg-info/ 54 | .installed.cfg 55 | *.egg 56 | 57 | # PyInstaller 58 | # Usually these files are written by a python script from a template 59 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 60 | *.manifest 61 | *.spec 62 | 63 | # Installer logs 64 | pip-log.txt 65 | pip-delete-this-directory.txt 66 | 67 | # Unit test / coverage reports 68 | htmlcov/ 69 | .tox/ 70 | .coverage 71 | .coverage.* 72 | .cache 73 | nosetests.xml 74 | coverage.xml 75 | *,cover 76 | .hypothesis/ 77 | 78 | # Translations 79 | *.mo 80 | *.pot 81 | 82 | # Django stuff: 83 | *.log 84 | local_settings.py 85 | 86 | # Flask stuff: 87 | instance/ 88 | .webassets-cache 89 | 90 | # Scrapy stuff: 91 | .scrapy 92 | 93 | # Sphinx documentation 94 | docs/_build/ 95 | 96 | # PyBuilder 97 | target/ 98 | 99 | # Jupyter Notebook 100 | .ipynb_checkpoints 101 | 102 | # pyenv 103 | .python-version 104 | 105 | # celery beat schedule file 106 | celerybeat-schedule 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # dotenv 112 | .env 113 | 114 | # virtualenv 115 | .venv 116 | venv/ 117 | ENV/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | .idea/ 126 | .statistics_result.xls 127 | 128 | .vscode/ 129 | 130 | .pytest_cache/ 131 | 132 | data/ 133 | __pycache__ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | include: 3 | - stage: unit test 4 | language: python 5 | catch: pip 6 | python: 7 | - "3.6" 8 | # command to install dependencies 9 | install: 10 | - pip install -r requirements.txt 11 | - pip install . 12 | # command to run tests 13 | script: pytest 14 | 15 | - stage: release to pypi 16 | if: type = push AND tag IS present 17 | language: python 18 | catch: pip 19 | python: 20 | - "3.6" 21 | script: 22 | - echo "Skipping tests" 23 | deploy: 24 | provider: pypi 25 | user: meituanqa 26 | password: 27 | secure: "B8ua64zQJyydVVVWIMG3lCn3yjca4ZFtxYiThQXKIAh/dkDr40B6Em7zCgjb5n1d6A2MhkQKwBcAaEbhX7W2zEjQv6PAeV42QOYgB/W9pdBGbUh31ZXQSwE0mpQdAH0WCCdLOpB0F0e95JDDtVhjQlhAYbrVSpqYAhIeq79rBtJNvxRRrHjdzHYW+qbwFjbWxlO3c06TEpU2dTwHPdBx9iup4utmEmA51HIQkItDzNdHcRCPLyZvr+P27xp9ikrYgg2He/Usyq/zC6V7NhtMaPga8sf4jMPoYEvW6V/fFyRssvlytpdTAeSyZRAyHRl6cw6fyehqfbiTdhFNzUMRWkg/Oijd0+S4EbpmY5DX4+mVDuM0EU6eizchvT9sjwZcSrlEBK74oyWtQOfLy0yl5+T/Eny3pqLaF1wBGARyMw3Nkviijc1llh/wHgsT7eYUDAoTm7XdEPmtv22FVL7OXCrUbuPRdkyzy0x8WSit9gJfj4IJ0L5kdiNoSBtGw1XASgIyJJUwc3ke+sGp4WmrveYK+Lrp/DCgMH/VtSdSvWkuKAJ1c9Eck8NhD2ZJvgQaKGPgmE3Q+3fMbZcI2U3gJeqkGXgNhSGGDau5694vTFdbs3bbX/dS72ahcCu9hkTPEV+oFWXDmXD6I/q8/hEJ2OhxOmdS7EeV36b47h8pWLA=" 28 | on: 29 | tags: true 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft lyrebird_tracking 2 | recursive-exclude * *.pyc *.pyo *.swo *.swp *.map *.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lyrebird-tracking 2 | 3 | ## 简介 4 | 5 | ***Tracking 是基于[Lyrebird](https://github.com/meituan/lyrebird)的插件,为移动端应用提供服务请求的数据分析及验证的功能*** 6 | 7 | 8 | ## 环境要求 9 | 10 | * macOS 11 | 12 | * Python3.6及以上 13 | 14 | 15 | ## 安装 16 | 17 | ``` bash 18 | pip3 install lyrebird-tracking 19 | ``` 20 | 21 | ## 简单示例 22 | 23 | 请求数据分析的应用场景比较广泛,以其中一种典型应用场景如客户端[埋点](https://baike.baidu.com/item/%E5%9F%8B%E7%82%B9)的分析校验来进行示例 24 | 25 | ### 分析待测对象 26 | 27 | 1. 假设埋点上报的host为 ***abctest.com*** 28 | 2. 埋点上报内容如下所述,key2字段为索引值,action与page字段是待校验项 29 | 30 | ``` json 31 | [ 32 | { 33 | "key1": "val1", 34 | "property1" : [ 35 | { 36 | "key2": "val2", 37 | "action": "view", 38 | "page": "detail_page" 39 | } 40 | ] 41 | } 42 | ] 43 | ``` 44 | 45 | ### 编写配置文件 46 | 47 | 1. selector:以JSONPath描述过滤筛选JSON的逻辑,详细语法示例见[附录](#配置文件数据格式) 48 | 2. assert:校验逻辑,校验action与page两个字段,以JSONSchema语法描述预期 49 | 50 | ``` json 51 | { 52 | "target": [ 53 | "abctest.com" 54 | ], 55 | "cases": [{ 56 | "name": "test case 1st", 57 | "selector": "$[?key1='val1'].property1[?key2='val2']", 58 | "asserts": [{ 59 | "field": "action", 60 | "schema": { 61 | "type": "string", 62 | "pattern": "view" 63 | } 64 | }, 65 | { 66 | "field": "page", 67 | "schema": { 68 | "type": "string", 69 | "pattern": "detail_page" 70 | } 71 | } 72 | ] 73 | }] 74 | } 75 | ``` 76 | 77 | ### 校验功能 78 | 79 | 若移动端发出符合预期的Request Data,如下 80 | 81 | ``` json 82 | [ 83 | { 84 | "key1": "val1", 85 | "property1" : [ 86 | { 87 | "action": "view", 88 | "key2": "val2", 89 | "lab": { 90 | "good_id": 10001, 91 | "index": 5, 92 | "page_name": "detail_page101" 93 | }, 94 | "page": "detail_page" 95 | } 96 | ] 97 | } 98 | ] 99 | ``` 100 | 101 | Tracking会自动分析和校验,如图所示 102 | 103 | 104 | 105 | 若移动端发出不符合预期的Request Data,如其中action字段的值不符合预期"view",如下 106 | 107 | ``` json 108 | [ 109 | { 110 | "key1": "val1", 111 | "property1" : [ 112 | { 113 | "action": "click", 114 | "key2": "val2", 115 | "lab": { 116 | "good_id": 10001, 117 | "index": 5, 118 | "page_name": "detail_page101" 119 | }, 120 | "page": "detail_page" 121 | } 122 | ] 123 | } 124 | ] 125 | ``` 126 | 127 | Tracking会自动分析和校验,并将错误信息高亮标红展示,如图所示 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | ## 详细功能介绍 136 | 137 | 138 | 如上图所示: 139 | 140 | 1. 工具栏:清空测试缓存 141 | 142 | 2. 分组标签:可以分组筛选查看case的结果 143 | 144 | 3. case列表:以时间倒序排列展示的case记录 145 | 146 | 1. case通过,即为所有校验字段全部通过,展示绿色 Pass Button 147 | 2. case失败,即为只要有一个校验字段失败,展示红色 Fail Button 148 | 3. case没有触发,展示为N/A状态,点击可以查看具体预期值等信息 149 | 4. 单行点击,可以唤起对应的case详情,查看具体校验字段和原数据等信息 150 | 151 | 4. 校验字段详情展示:点击左侧一行,右侧会展示对应的校验详情,展开状态 152 | 153 | 1. Field:校验字段 154 | 2. Expect Schema:校验字段对应的预期JSONSchema 155 | 3. Actual vaule:实际校验字段的值 156 | 157 | 5. 校验字段名和结果展示:校验详情的收起状态, 收起状态的校验结果有三种: 158 | 159 | 1. 字段校验通过,校验字段展示绿色 160 | 2. 字段校验失败,校验字段展示红色 161 | 3. 字段校验逻辑为空,校验字段展示蓝色 162 | 163 | 6. 匹配数据展示区 164 | 165 | 1. 展示匹配查询规则的数据 166 | 2. 字段校验失败,红色高亮提示 167 | 2. 未配置校验/断言逻辑的,蓝色高亮提示 168 | 3. 校验结果错误展示红色高亮提示,鼠标悬停在字段上,会有详细的错误提示 169 | 170 | 171 | 172 | ## 使用流程 173 | 174 | 1. 准备基准数据文件,数据格式见[附录](#JSONPath-简明介绍) 175 | 176 | 2. 将基准数据文件放入指定路径下: ***~.lyrebird/plugins/lyrebird_tracking/base.json*** 177 | 178 | 3. 启动[Lyrebird](https://github.com/meituan/lyrebird)工具,[手机链接代理](https://github.com/meituan/lyrebird#%E8%BF%9E%E6%8E%A5%E7%A7%BB%E5%8A%A8%E8%AE%BE%E5%A4%87),操作过程中观测case校验等信息展示 179 | 180 | 181 | ## 开发者指南 182 | 183 | ``` shell 184 | # clone 代码 185 | git clone https://github.com/meituan/lyrebird-tracking.git 186 | 187 | # 进入工程目录 188 | cd lyrebird-tracking 189 | 190 | # 创建虚拟环境 191 | python3 -m venv venv 192 | 193 | # 安装依赖 194 | source venv/bin/activate 195 | pip3 install -r requirements.txt 196 | 197 | # 使用IDE打开工程(推荐Pycharm或vscode) 198 | 199 | # 在IDE中执行debug.py即可开始调试 200 | ``` 201 | 202 | 203 | ## 附录 204 | 205 | ### 配置文件数据格式 206 | 207 | #### 字段说明 208 | 209 | * target:待校验的host集合 210 | 211 | * cases:测试用例集合 212 | 213 | * name:测试用例名 214 | 215 | * selector:JSONPath语法描述的查询条件 216 | 217 | * asserts:校验条件 218 | 219 | * field:需要校验的字段 220 | 221 | * schema:JSONSchema语法描述的校验条件,不校验可填为{},前端会高亮显示该字段 222 | 223 | * groupname(可选):case对应分组的组名 224 | 225 | * groupid(可选):case对应分组的组id 226 | 227 | 228 | 229 | ``` json 230 | { 231 | "target": [ 232 | "abctest.com" 233 | ], 234 | "cases": [{ 235 | "name": "test case 1st", 236 | "selector": "$[?key1='val1'].property1[?key2='val2']", 237 | "asserts": [{ 238 | "field": "action", 239 | "schema": { 240 | "type": "string", 241 | "pattern": "view" 242 | } 243 | }, 244 | { 245 | "field": "page", 246 | "schema": { 247 | "type": "string", 248 | "pattern": "detail_page" 249 | } 250 | } 251 | ], 252 | "groupname": "group1", 253 | "groupid": 1 254 | }, { 255 | "name": "test case 2nd", 256 | "selector": "$[*].property2[?key3='val3']", 257 | "asserts": [{ 258 | "field": "action", 259 | "schema": { 260 | "type": "string", 261 | "pattern": "click" 262 | } 263 | }, 264 | { 265 | "field": "page", 266 | "schema": { 267 | "type": "string", 268 | "pattern": "home_page" 269 | } 270 | } 271 | ], 272 | "groupname": "group2", 273 | "groupid": 2 274 | }] 275 | } 276 | ``` 277 | 278 | ### JSONPath 简明介绍 279 | 280 | 用于查询逻辑的selector配置基于[JSONPath](https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html)的语法。类似于XPath在xml文档中的定位,JSONPath表达式通常是用来路径检索或设置Json的。目前仅支持一部分JSONPath语法,如下所述。 281 | 282 | #### 支持语法 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 |
JSONPath描述
$根对象。例如$name
[num]数组访问,其中num是数字。例如$[0].leader.departments[1].name
[*]数组访问,访问所有数组的元素。例如$[*].leader.departments[2].name
[key='test']字符串类型对象属性判断相等的过滤,例如$departs[name = 'test']
.属性访问,例如$name.a.b
293 | 294 | 295 | #### 语法示例 296 | 297 | ``` json 298 | [ 299 | { 300 | "name":"king", 301 | "property":123, 302 | "house":120 303 | }, 304 | { 305 | "name":"wang", 306 | "property":456, 307 | "house":240 308 | }, 309 | { 310 | "name":"king", 311 | "car":"audi", 312 | "house":120 313 | }, 314 | { 315 | "name":"king", 316 | "property":123, 317 | "house":789 318 | }, 319 | { 320 | "name":"king", 321 | "property":666, 322 | "house":666 323 | } 324 | ] 325 | ``` 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 |
JSONPath语义
$根对象
$[1]第1个元素
$[*]全部元素
$[name='king']list中name属性为'king'的元素
$[name='king'].propertylist中name属性为'king'的元素并且取该元素的property属性的值
336 | 337 | ### JSONSchema 介绍 338 | 339 | 用于校验逻辑的schema配置基于[JSONSchema](https://json-schema.org/understanding-json-schema/basics.html)的语法。JSON Schema 用以标注和验证JSON文档的元数据的文档,可以类比于XML Schema。相对于JSON Schema,一个JSON文档就是JSON Schema的一个instance,可以校验数据结构、数据类型、和详细的判断等。 340 | 341 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | import lyrebird 2 | import pip 3 | 4 | if __name__ == '__main__': 5 | version_num = pip.__version__[:pip.__version__.find('.')] 6 | if int(version_num) >= 10: 7 | from pip import __main__ 8 | __main__._main(['install', '.', '--upgrade']) 9 | else: 10 | pip.main(['install', '.', '--upgrade']) 11 | lyrebird.debug() -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "***************************" 4 | echo " Lyrebird-tracking setup start " 5 | echo "***************************" 6 | 7 | # 如果已经有venv目录,删除此目录 8 | if [ -e "./venv/" ]; then 9 | rm -rf ./venv/ 10 | fi 11 | 12 | mkdir venv 13 | python3 -m venv ./venv 14 | 15 | # 有些设备上虚拟环境中没有pip,需要通过easy_install安装 16 | if [ ! -e "./venv/bin/pip" ] ;then 17 | echo "pip no exist, install pip with easy_install" 18 | ./venv/bin/easy_install pip 19 | fi 20 | 21 | source ./venv/bin/activate 22 | pip3 install -r ./requirements.txt 23 | 24 | 25 | echo "***************************" 26 | echo " Lyrebird-tracking setup finish " 27 | echo "***************************" 28 | -------------------------------------------------------------------------------- /image/fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/image/fail.png -------------------------------------------------------------------------------- /image/mainpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/image/mainpage.png -------------------------------------------------------------------------------- /image/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/image/success.png -------------------------------------------------------------------------------- /lyrebird_tracking/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/lyrebird_tracking/__init__.py -------------------------------------------------------------------------------- /lyrebird_tracking/context.py: -------------------------------------------------------------------------------- 1 | """ 2 | 应用上下文类 3 | 4 | """ 5 | class Context: 6 | def __init__(self): 7 | # config 8 | self.config = {} 9 | # result list : name and result for list show 10 | self.result_list = [] 11 | # content : all data 12 | self.content = [] 13 | # error messages list 14 | self.error_list = [] 15 | # selected group list 16 | self.select_groups = [] 17 | 18 | 19 | # 单例模式 20 | app_context = Context() 21 | -------------------------------------------------------------------------------- /lyrebird_tracking/data/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": [ 3 | "abctest.com" 4 | ], 5 | "cases": [{ 6 | "name": "test case 1st", 7 | "selector": "$[?key1='val1'].property1[?key2='val2']", 8 | "asserts": [{ 9 | "field": "action", 10 | "schema": { 11 | "type": "string", 12 | "pattern": "view" 13 | } 14 | }, 15 | { 16 | "field": "page", 17 | "schema": { 18 | "type": "string", 19 | "pattern": "detail_page" 20 | } 21 | }, 22 | { 23 | "field": "lab", 24 | "schema": { 25 | "type": "object", 26 | "properties": { 27 | "page_name": { 28 | "type": "string" 29 | }, 30 | "good_id": { 31 | "type": "integer" 32 | }, 33 | "index": { 34 | "type": "integer" 35 | } 36 | }, 37 | "required": [ 38 | "page_name", 39 | "good_id", 40 | "index" 41 | ] 42 | } 43 | } 44 | ], 45 | "groupname": "group1", 46 | "groupid": 1 47 | }, { 48 | "name": "test case 2nd", 49 | "selector": "$[*].property2[?key3='val3']", 50 | "asserts": [{ 51 | "field": "action", 52 | "schema": { 53 | "type": "string", 54 | "pattern": "click" 55 | } 56 | }, 57 | { 58 | "field": "page", 59 | "schema": { 60 | "type": "string", 61 | "pattern": "home_page" 62 | } 63 | }, { 64 | "field": "clickcount", 65 | "schema": { 66 | "type": "number", 67 | "minimum": 0, 68 | "maximum": 100 69 | } 70 | }, { 71 | "field": "goodid_list", 72 | "schema": { 73 | "type": "array", 74 | "minItems": 2, 75 | "maxItems": 3, 76 | "items": { 77 | "type": "number" 78 | } 79 | } 80 | } 81 | ], 82 | "groupname": "group2", 83 | "groupid": 2 84 | }, { 85 | "name": "test case 3rd", 86 | "selector": "$property3[2].property4[?key6='val7'].property5", 87 | "asserts": [{ 88 | "field": "email", 89 | "schema": { 90 | "type": "string", 91 | "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$", 92 | "minLength": 2, 93 | "maxLength": 3, 94 | "format": "date-time|email|hostname|ipv4|ipv6|uri" 95 | } 96 | }], 97 | "groupname": "group3", 98 | "groupid": 3 99 | }] 100 | } -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/components/filter-tag.js: -------------------------------------------------------------------------------- 1 | Vue.component( 2 | 'filter-tag', { 3 | template: '#filter-tag', 4 | props: [], 5 | data: function() { 6 | return { 7 | grouplist: [], 8 | showModal: false, 9 | changeGroupCache: [], 10 | allGroup: [] 11 | } 12 | }, 13 | mounted: function() { 14 | this.tagData(); 15 | }, 16 | methods: { 17 | tagData: function() { 18 | let filterdata = null; 19 | filterdata = baseData; 20 | for (let i = 0; i < filterdata.cases.length; i++) { 21 | let name = filterdata.cases[i].groupname; 22 | if (typeof name != "undefined") { 23 | // 如果grouplist里面不包含当前groupname返回-1,包含返回index值 24 | if (this.grouplist == 0 || this.grouplist.indexOf(name) == -1) { 25 | this.grouplist.push(name); 26 | } 27 | } 28 | } 29 | // 初始化,展示list赋值展示全部,赋值给AllGroup 30 | this.allGroup = [].concat(this.grouplist); 31 | }, 32 | handleClose: function(event, name) { 33 | let index = this.grouplist.indexOf(name); 34 | if (index > -1) { 35 | this.grouplist.splice(index, 1); 36 | } 37 | this.$emit("filterchange", this.grouplist); 38 | }, 39 | changeOk: function() { 40 | this.grouplist = this.changeGroupCache; 41 | this.$emit("filterchange", this.grouplist); 42 | this.$Notice.success({ 43 | title: "Change Filter Success" 44 | }); 45 | }, 46 | activatedDataChange: function(val) { 47 | console.log("Selected Groups Change", val); 48 | this.changeGroupCache = val; 49 | } 50 | } 51 | 52 | } 53 | ) -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/components/tracking-detail.js: -------------------------------------------------------------------------------- 1 | Vue.component( 2 | 'tracking-detail', { 3 | template: '#tracking-detail-tpl', 4 | props: ["currentcontent", "codedetail"], 5 | computed: { 6 | infoContaint: function() { 7 | infoStr = JSON.stringify(this.currentcontent.content, null, 4); 8 | return '
' + infoStr + "
"; 9 | } 10 | }, 11 | updated: function() { 12 | Prism.highlightAll(); 13 | }, 14 | mounted: function() { 15 | Prism.highlightAll(); 16 | //create monaco editor 17 | editorContainer = this.$el.querySelector("#container"); 18 | option = { 19 | value: JSON.stringify(this.codedetail, null, 4), 20 | language: "json", 21 | theme: "vs", 22 | glyphMargin: true, 23 | readOnly: true 24 | }; 25 | this.editor = window.monaco.editor.create(editorContainer, option); 26 | //hover register 27 | showhint = this.showhint 28 | monaco.languages.register({ id: 'json' }); 29 | monaco.languages.registerHoverProvider('json', { 30 | provideHover: function(model, position) { 31 | return showhint(model, position) 32 | } 33 | }); 34 | }, 35 | data: function() { 36 | return { 37 | editor: null, 38 | activeNames: ["1"], 39 | match_array: [] 40 | }; 41 | }, 42 | methods: { 43 | jsoninfo: function(content) { 44 | infoStr = JSON.stringify(content, null, 4); 45 | return '
' + infoStr + "
"; 46 | }, 47 | showhint: function(model, position) { 48 | let line = position.lineNumber; 49 | let field = null; 50 | let hint = null; 51 | for (var i = 0; i < this.match_array.length; i++) { 52 | if (this.match_array[i].linenumber === line) { 53 | field = this.match_array[i].linenumber; 54 | hint = JSON.stringify(this.match_array[i].hint); 55 | break; 56 | } 57 | } 58 | if (field) { 59 | return { 60 | range: new monaco.Range(1, 1, line, model.getLineMaxColumn(1)), 61 | contents: [{ value: '**Schema check**' }, { value: hint }] 62 | } 63 | } else { 64 | return { 65 | range: new monaco.Range(0, 0, 0, 0), 66 | contents: [] 67 | } 68 | } 69 | } 70 | }, 71 | components: {}, 72 | watch: { 73 | codedetail: function() { 74 | //每次切换后,都需要清空保存hint提示的array 75 | this.match_array = [] 76 | console.log("Code editor: content change"); 77 | this.editor.setValue(JSON.stringify(this.codedetail, null, 4)); 78 | this.editor.trigger(this.editor.getValue(), "editor.action.formatDocument"); 79 | 80 | //如果没有数据展示,不需要做后续的显示处理 81 | if (this.codedetail == null) { 82 | console.log('haha'); 83 | return null 84 | } 85 | 86 | for (let i = 0; i < this.currentcontent.asserts.length; i++) { 87 | let matches = this.editor.getModel().findMatches('"' + this.currentcontent.asserts[i].field + '":', false, true, false, false); 88 | if (matches == 0) { 89 | return 90 | } 91 | let match_start_linenumber = matches[0].range.startLineNumber; 92 | 93 | //如果含错误提示,才放入hint提示列表中 94 | if (this.currentcontent.asserts[i].flag === false) { 95 | let match_obj = { 96 | fieldname: this.currentcontent.asserts[i].field, 97 | linenumber: match_start_linenumber, 98 | hint: this.currentcontent.asserts[i].hint 99 | } 100 | this.match_array.push(match_obj); 101 | } 102 | 103 | let fieldname = this.currentcontent.asserts[i].field; 104 | let fvalue = this.codedetail[fieldname]; 105 | let fstr = JSON.stringify(fvalue, null, 4); 106 | //获取块大小,涂色用,根据换行符的个数 107 | let detail_length = fstr.split('\n').length; 108 | 109 | 110 | //如果断言的字段有问题,就高亮出来 111 | if (this.currentcontent.asserts[i].flag === false) { 112 | this.editor.deltaDecorations([], [{ 113 | range: new monaco.Range(match_start_linenumber, 1, match_start_linenumber + detail_length - 1, 1), 114 | options: { isWholeLine: true, className: "myContentClass" } 115 | }]); 116 | } 117 | // 无断言预期,高亮展示蓝色 118 | else if (this.currentcontent.asserts[i].flag === 2) { 119 | this.editor.deltaDecorations([], [{ 120 | range: new monaco.Range(match_start_linenumber, 1, match_start_linenumber + detail_length - 1, 1), 121 | options: { isWholeLine: true, className: "infoContentClass" } 122 | }]); 123 | } 124 | 125 | } 126 | } 127 | } 128 | } 129 | 130 | ) -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/data/report-data.js: -------------------------------------------------------------------------------- 1 | var reportCaseData = null 2 | var baseData = null 3 | var detailCollection = null -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tracking Report 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Tracking Report

18 |
19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/main.js: -------------------------------------------------------------------------------- 1 | var caseInfo = new Vue({ 2 | el: '#caseInfo', 3 | data: { 4 | columns1: [{ 5 | title: 'Case', 6 | key: 'name', 7 | className: 'i-table-caseName', 8 | sortable: true 9 | }, { 10 | title: 'Result', 11 | width: 100, 12 | key: 'result', 13 | className: 'i-table-caseName', 14 | sortable: true, 15 | render: (h, params) => { 16 | if (params.row.result === "pass") { 17 | return h( 18 | "i-button", { 19 | props: { size: "small", type: "success" }, 20 | style: { width: "50px" } 21 | }, 22 | "Pass" 23 | ); 24 | } else if (params.row.result === "fail") { 25 | return h( 26 | "i-button", { 27 | props: { size: "small", type: "error" }, 28 | style: { width: "50px" } 29 | }, 30 | "Fail" 31 | ); 32 | } else { 33 | return h( 34 | "i-button", { 35 | props: { size: "small", type: "default" }, 36 | style: { width: "50px" } 37 | }, 38 | "N/A" 39 | ); 40 | } 41 | } 42 | }], 43 | caseInfo: reportCaseData.result, 44 | filter_rules: [], 45 | currentTracking: null, 46 | currentData: null, 47 | codedetail: null 48 | }, 49 | methods: { 50 | filterList: function(grouplist) { 51 | this.filter_rules = grouplist; 52 | }, 53 | handleRowSelect: function(row, index) { 54 | this.currentTracking = index; 55 | for (let i = 0; i < detailCollection.length; i++) { 56 | let id = detailCollection[i].id 57 | if (row.id == id) { 58 | this.currentData = detailCollection[i]; 59 | this.codedetail = detailCollection[i].content; 60 | console.log(id); 61 | console.log(this.codedetail); 62 | console.log(this.currentData); 63 | } 64 | } 65 | } 66 | }, 67 | computed: { 68 | displayedData: function() { 69 | // filter出name 70 | let showdata = []; 71 | if (this.filter_rules.length == 0) { 72 | showdata = this.caseInfo; 73 | } else { 74 | for (let i = 0; i < this.filter_rules.length; i++) { 75 | let filter_rule = this.filter_rules[i]; 76 | let filtercells = this.caseInfo.filter(function(elem) { 77 | return elem.groupname == filter_rule; 78 | }); 79 | showdata = showdata.concat(filtercells); 80 | } 81 | } 82 | return showdata; 83 | } 84 | } 85 | }) -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 | 38 | 39 | 85 | 86 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /lyrebird_tracking/report_template/style/report.css: -------------------------------------------------------------------------------- 1 | .ivu-table { 2 | height: auto; 3 | } 4 | 5 | .i-table-caseName { 6 | font-size: 8px; 7 | } 8 | 9 | .tag-caseName { 10 | font-size: 3px; 11 | } 12 | 13 | .i-table-icon { 14 | font-size: 16px; 15 | text-align: center; 16 | } 17 | 18 | .myContentClass { 19 | background: lightpink; 20 | } 21 | 22 | .infoContentClass { 23 | background: lightblue; 24 | } 25 | 26 | .small { 27 | margin-right: 10px; 28 | font-size: 100%; 29 | } 30 | 31 | .content-header { 32 | margin-bottom: 10px; 33 | } 34 | 35 | #channel { 36 | margin-bottom: 10px; 37 | } 38 | 39 | #screenShot { 40 | background-color: #f5f5f5; 41 | border: 1px solid #ccc; 42 | } 43 | 44 | .picList { 45 | padding: 10px; 46 | float: left; 47 | } -------------------------------------------------------------------------------- /lyrebird_tracking/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_handler import load_base, init_data 2 | from .search_handler import SearchHandler 3 | from .data_manager import new_caseresult 4 | from .validator import Verify 5 | from lyrebird_tracking.context import app_context 6 | from lyrebird import context 7 | import lyrebird 8 | 9 | 10 | def tracking_init(): 11 | """ 12 | tracking初始化函数 13 | 1. 加载基准文件 14 | 2. 对基准文件进行初始化 15 | 16 | """ 17 | load_base() 18 | init_data() 19 | 20 | 21 | def search(jsonnode, jsonpath): 22 | """ 23 | 查询函数 24 | :param jsonnode: 待搜索的json数据源 25 | :param jsonpath: 搜索条件 26 | :return: 满足查询搜索条件的结果数组 27 | 28 | """ 29 | node = SearchHandler(jsonnode) 30 | targets_list = node.find(jsonpath).data 31 | return targets_list 32 | 33 | 34 | def validate(rule, targets_list): 35 | """ 36 | 验证函数 37 | :param rule: 校验规则 38 | :param targets_list: 目标查询的数组 39 | 40 | 校验失败会发消息总线 41 | 42 | """ 43 | verify = Verify() 44 | result_list = verify.check(targets_list, rule) 45 | for item in result_list: 46 | # handle data change 47 | new_caseresult(item) 48 | # emit socket io to FE 49 | context.application.socket_io.emit('update', namespace='/tracking-plugin') 50 | if item.get('result') == 'fail': 51 | error_message = dict((k, item[k]) for k in ('name', 'content') if k in item) 52 | error_message['error_msg'] = filter_error_msg(item) 53 | # Bug 54 | # 有埋点错误消息,发事件给消息总线 55 | # pubilsh_error_msg(error_message) 56 | 57 | 58 | def pubilsh_error_msg(msg): 59 | """ 60 | 将错误信息通过消息总线发送出去,订阅tracking频道的其他插件会监听到 61 | :param msg: 错误信息详情 62 | 63 | """ 64 | 65 | app_context.error_list.append(msg) 66 | lyrebird.publish('tracking.error', msg) 67 | lyrebird.publish('tracking.error', msg, state=True) 68 | lyrebird.publish('tracking.error_list', app_context.error_list) 69 | lyrebird.publish('tracking.error_list', app_context.error_list, state=True) 70 | 71 | 72 | def filter_error_msg(result_dict): 73 | """ 74 | 从测试结果中筛选出错误信息,转化为字符串 75 | :param result_dict: dict类型,待处理的结果信息 76 | :return error_str: str类型,最终的错误信息字符串 77 | 78 | """ 79 | # 筛选错误信息 80 | error_msg = dict((k, result_dict[k]) for k in ('groupname', 'name') if k in result_dict) 81 | error_list = [] 82 | for item in result_dict.get('asserts'): 83 | if item.get('flag') is False: 84 | error_detail = {'field': item.get('field'), 'error detail': item.get('hint')} 85 | error_list.append(error_detail) 86 | error_msg['error message'] = error_list 87 | 88 | # 转换为字符串 89 | error_str = '' 90 | for key in error_msg.keys(): 91 | if key == 'error message': 92 | temp_str = key + ':\n' 93 | for item in error_msg.get(key): 94 | temp_str = temp_str + 'field: ' + item.get('field') + '\n' \ 95 | + 'error detail: ' + item.get('error detail') + '\n' 96 | else: 97 | temp_str = key + ': ' + str(error_msg.get(key)) + '\n' 98 | error_str = error_str + temp_str 99 | 100 | return error_str 101 | -------------------------------------------------------------------------------- /lyrebird_tracking/server/base_handler.py: -------------------------------------------------------------------------------- 1 | import lyrebird 2 | import os 3 | import json 4 | import codecs 5 | from lyrebird_tracking.context import app_context 6 | from lyrebird.log import get_logger 7 | import uuid 8 | storage = lyrebird.get_plugin_storage() 9 | BASE_FILE = os.path.abspath(os.path.join(storage, 'base.json')) 10 | DEFAULT_BASE_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', './data/base.json')) 11 | 12 | 13 | def load_base(): 14 | """ 15 | 加载基准文件(Base文件) 16 | 读取文件路径:~/.lyrbeird/plugin/lyrebird_tracking/base.json 17 | 没有该文件,会加载为默认配置 DEFAULT_BASE_FILE 18 | 19 | """ 20 | lyrebird_conf = lyrebird.context.application.conf 21 | # 读取指定base文件,写入到base.json 22 | if lyrebird_conf.get('tracking.base'): 23 | base_path = lyrebird_conf.get('tracking.base') 24 | base = codecs.open(base_path, 'r', 'utf-8').read() 25 | f = codecs.open(BASE_FILE, 'w', 'utf-8') 26 | f.write(base) 27 | f.close() 28 | 29 | # 通过本地默认base文件获取base 30 | elif not os.path.exists(BASE_FILE): 31 | copy_file(BASE_FILE) 32 | 33 | with codecs.open(BASE_FILE, 'r', 'utf-8') as f: 34 | conf_data = json.load(f) 35 | 36 | # base文件内容放置到conf_data中 37 | app_context.config = conf_data 38 | 39 | 40 | def init_data(): 41 | """ 42 | 初始化基准文件 43 | 初始化解析为应用上下文的变量: 44 | app_context.result_list - case列表数据 45 | app_context.content - 校验结果详情数据 46 | 47 | """ 48 | for item in app_context.config.get('cases'): 49 | result_dict = { 50 | 'id': str(uuid.uuid4()), 51 | 'result': 'NA', 'name': item.get('name')} 52 | # 加入分组信息,用于前端筛选 53 | if item.get('groupid'): 54 | result_dict.update({'groupid': item.get('groupid')}) 55 | 56 | if item.get('groupname'): 57 | result_dict.update({'groupname': item.get('groupname')}) 58 | if not item.get('groupname') in app_context.select_groups: 59 | app_context.select_groups.append(item.get('groupname')) 60 | else: 61 | item.update({'groupname': 'unnamed'}) 62 | result_dict.update({'groupname': 'unnamed'}) 63 | if not 'unnamed' in app_context.select_groups: 64 | app_context.select_groups.append('unnamed') 65 | 66 | target_dict = { 67 | 'asserts': item.get('asserts'), 68 | 'content': None, 'selector': item.get('selector'), 69 | 'source': None, 'url': None} 70 | target_dict.update(result_dict) 71 | app_context.result_list.append(result_dict) 72 | app_context.content.append(target_dict) 73 | 74 | 75 | 76 | def copy_file(target_path): 77 | """ 78 | 复制文件内容,复制默认基准文件到 ~/.lyrbeird/plugin/lyrebird_tracking/base.json 79 | :param target_path: 目标路径 80 | 81 | """ 82 | f_from = codecs.open(DEFAULT_BASE_FILE, 'r', 'utf-8') 83 | f_to = codecs.open(target_path, 'w', 'utf-8') 84 | f_to.write(f_from.read()) 85 | f_to.close() 86 | f_from.close() 87 | -------------------------------------------------------------------------------- /lyrebird_tracking/server/data_manager.py: -------------------------------------------------------------------------------- 1 | from lyrebird_tracking.context import app_context 2 | 3 | 4 | def new_caseresult(result_content): 5 | """ 6 | 新增case内容,处理断言结果的原始内容,对应用上下文进行更新 7 | :param result_content: 断言结果的原始内容 8 | 9 | """ 10 | # 筛选出带目标key的子字典 11 | show_cell = dict((k, result_content[k]) 12 | for k in ('result', 'id', 'name', 'groupid', 'groupname') if k in result_content) 13 | # 筛选出未测试的case,标记为NA,name为唯一标识进行筛选 14 | untested_list = list(filter(lambda x: x.get('result') == 'NA' and x.get( 15 | 'name') == result_content.get('name'), app_context.content)) 16 | # 如果对应未测试的case存在 17 | if untested_list: 18 | untested_item = untested_list[0] 19 | untested_list_item = list(filter(lambda x: x.get('result') == 'NA' and x.get( 20 | 'name') == result_content.get('name'), app_context.result_list))[0] 21 | # 删除对应未测试case 22 | app_context.content.remove(untested_item) 23 | app_context.result_list.remove(untested_list_item) 24 | # 增加对应测试case,在列表展示数据内插入到列表index=0的位置 25 | app_context.result_list.insert(0, show_cell) 26 | app_context.content.append(result_content) 27 | -------------------------------------------------------------------------------- /lyrebird_tracking/server/search_handler.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class SearchHandler: 5 | """ 6 | 搜索逻辑类 7 | 主要用于判断数据源是否符合配置规则 8 | 9 | """ 10 | 11 | def __init__(self, *args): 12 | self.data = [] 13 | self.data += args 14 | self.search_list = [] 15 | self.subnodes = [] 16 | 17 | def find(self, jsonpath): 18 | """ 19 | 查询逻辑的入口函数,支持链式查询方式,如 SearchHandler(data).find(path1).find(path2) 20 | :param jsonpath: 用JSONPath描述的查询逻辑 21 | :return: SearchHandler对象,用于链式查询 22 | 23 | """ 24 | self.subnodes = self.data 25 | self.search_list = self.transfer_query(jsonpath) 26 | for query in self.search_list: 27 | self._query_parser(query) 28 | return SearchHandler(*self.subnodes) 29 | 30 | def transfer_query(self, jsonpath): 31 | """ 32 | 解析jsonpath语法函数 33 | 主要用正则进行拆分解析查询逻辑 34 | :param jsonpath: 用JSONPath描述的查询逻辑 35 | :return: 按查询顺序进行排列的查询语句数组 36 | 37 | """ 38 | if '$' in jsonpath and not jsonpath.startswith('$'): 39 | return 40 | # split $ means root 41 | if jsonpath.startswith('$'): 42 | jsonpath = jsonpath[1:] 43 | # 以[...]为条件进行正则字符串切割 44 | pattern = re.compile(r'(\[.+?\])') 45 | source_list = re.split(pattern, jsonpath) 46 | query_list = [] 47 | # 解析正则切割后的string list 48 | for item in source_list: 49 | # 如果是[...],直接append 50 | if item.startswith('['): 51 | query_list.append(item) 52 | # 如果是 property,以.切割成多个属性,extend到querylist里 53 | elif '.' in item: 54 | item_list = item.split('.') 55 | query_list.extend(item_list) 56 | else: 57 | query_list.append(item) 58 | # remove null item 59 | query_list = [x for x in query_list if x != ''] 60 | return query_list 61 | 62 | def _query_parser(self, query_str): 63 | """ 64 | 根据正则匹配,识别list或property 65 | :param query_str: 查询字符串 66 | 67 | """ 68 | # 匹配 [...] 69 | if re.match(r'(\[.+?\])', query_str): 70 | self._query_list(query_str) 71 | else: 72 | self._query_property(query_str) 73 | 74 | def _query_list(self, query_str): 75 | """ 76 | 根据正则匹配,识别三种类型的list模式 77 | :param query_str: 查询字符串 78 | 79 | """ 80 | node_list = self.subnodes 81 | self.subnodes = [] 82 | for obj in node_list: 83 | if not isinstance(obj, list): 84 | continue 85 | # match such as:[2] 86 | if re.match(r'(\[\d+?\])', query_str): 87 | self._query_list_by_index(obj, query_str) 88 | # match such as:[*] 89 | elif query_str == '[*]': 90 | self._query_list_no_index(obj, query_str) 91 | # match such as: [?key=val] 92 | elif re.match(r'(\[\?.+?\=.+?\])', query_str): 93 | self._query_list_kw(obj, query_str) 94 | 95 | def _query_list_by_index(self, node, query_str): 96 | # 从[333]取出index 333 97 | index_num = query_str.split('[')[1].split(']')[0] 98 | if len(node) - 1 >= int(index_num): 99 | self.subnodes.append(node[int(index_num)]) 100 | 101 | def _query_list_no_index(self, node, query_str): 102 | # 将整个list extend 至 subnodes里 103 | self.subnodes.extend(node) 104 | 105 | def _query_list_kw(self, node, query_str): 106 | # handle such as :[?key1=val1&key2=val2&key3=val3] 107 | sword = query_str.split('?')[1].split(']')[0] 108 | q_list = sword.split('&') 109 | 110 | for item in node: 111 | if not isinstance(item, dict): 112 | continue 113 | 114 | flag = 1 115 | for q in q_list: 116 | key = q.split('=')[0] 117 | val = q.split('=')[1].strip("'") 118 | # handle value of int type 119 | if val.isnumeric(): 120 | val = int(val) 121 | if item.get(key) != val: 122 | flag = 0 123 | break 124 | continue 125 | if flag == 1: 126 | self.subnodes.append(item) 127 | 128 | def _query_property(self, query_str): 129 | """ 130 | 处理property模式,默认返回对应property的val 131 | :param query_str: 查询字符串 132 | 133 | """ 134 | node_list = self.subnodes 135 | self.subnodes = [] 136 | for obj in node_list: 137 | if not isinstance(obj, dict): 138 | continue 139 | if query_str not in obj: 140 | continue 141 | self.subnodes.append(obj[query_str]) 142 | -------------------------------------------------------------------------------- /lyrebird_tracking/server/validator.py: -------------------------------------------------------------------------------- 1 | import jsonschema 2 | from jsonschema import Draft4Validator 3 | import uuid 4 | from lyrebird_tracking.server.search_handler import SearchHandler 5 | 6 | 7 | class Verify: 8 | """ 9 | 校验类 10 | 根据配置文件的校验规则进行校验 11 | 12 | """ 13 | 14 | def check(self, elements_list, rule): 15 | """ 16 | 校验函数 17 | 基于JSONSchea根据校验规则和输入的数据源进行校验,返回的数据会更新应用上下文,校验结果随之会更新在前端 18 | :param elements_list: list类型,待校验的数据列表 19 | :param rule: 校验规则 20 | 21 | """ 22 | result_list = [] 23 | checker = rule.get('asserts') 24 | for ele in elements_list: 25 | flag = True 26 | check_list = [] 27 | for item in checker: 28 | field = item.get('field') 29 | schema = item.get('schema') 30 | 31 | # 判断实际有没有对应的字段 32 | search_list = [field] 33 | node = SearchHandler(ele) 34 | for i in range(len(search_list)): 35 | node = node.find(search_list[i]) 36 | result = node.data 37 | if result: 38 | content = result[0] 39 | else: 40 | content = None 41 | 42 | # 错误提示标记hint 43 | hint = None 44 | # 如果schema为空,高亮,不校验,用特殊高亮区别标记出来 45 | if schema: 46 | result = Draft4Validator(schema).is_valid(content) 47 | try: 48 | jsonschema.validate(content, schema) 49 | except jsonschema.ValidationError as e: 50 | hint = e.message 51 | else: 52 | result = 2 53 | 54 | flag = flag * result 55 | 56 | if content: 57 | check_list.append( 58 | {'field': field, 'schema': schema, 'actual': content, 'flag': result, 'hint': hint}) 59 | else: 60 | check_list.append({'field': field, 'schema': schema, 'actual': 'error!exists-false', 61 | 'flag': result, 'hint': 'The field is not exists!'}) 62 | 63 | result_dict = dict((k, rule[k]) for k in ('name', 'groupid', 'groupname', 'selector') if k in rule) 64 | result_dict['asserts'] = check_list 65 | result_dict['id'] = str(uuid.uuid4()) 66 | result_dict['content'] = ele 67 | if flag: 68 | result_dict['result'] = 'pass' 69 | else: 70 | result_dict['result'] = 'fail' 71 | 72 | result_list.append(result_dict) 73 | 74 | return result_list 75 | -------------------------------------------------------------------------------- /lyrebird_tracking/static/component/banner.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /lyrebird_tracking/static/component/filter-tag.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 112 | 118 | -------------------------------------------------------------------------------- /lyrebird_tracking/static/component/main.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 79 | 80 | 94 | -------------------------------------------------------------------------------- /lyrebird_tracking/static/component/tracking-detail.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 184 | 185 | 197 | -------------------------------------------------------------------------------- /lyrebird_tracking/static/component/tracking-list.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 136 | 137 | 152 | -------------------------------------------------------------------------------- /lyrebird_tracking/static/js/main.js: -------------------------------------------------------------------------------- 1 | Vue.config.devtools = true; 2 | 3 | iview.lang('en-US'); 4 | 5 | new Vue({ 6 | el: '#app', 7 | data: {}, 8 | components: { 9 | 'tracking': httpVueLoader('/ui/plugin/tracking/static/component/main.vue') 10 | } 11 | }) -------------------------------------------------------------------------------- /lyrebird_tracking/templates/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lyrebird_tracking/tracking.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | import time 4 | 5 | import lyrebird 6 | from lyrebird import HandlerContext, context 7 | from urllib.parse import urlparse 8 | 9 | from lyrebird_tracking.context import app_context 10 | from lyrebird_tracking.server import search, validate 11 | 12 | 13 | class TrackingHandler: 14 | """ 15 | 处理Lyrebird数据流 16 | Trakcing校验数据来源,来自于Lyrebird的请求上下文 17 | """ 18 | def handle(self, handler_context: HandlerContext): 19 | """ 20 | 数据流处理函数,继承与Lyrebird的HandlerContext,会获取Lyrebird的请求上下文 21 | :param handler_context: 请求上下文,取request data进行筛选校验 22 | """ 23 | url = handler_context.get_origin_url() 24 | if url: 25 | hostname = urlparse(url).hostname 26 | else: 27 | hostname = urlparse(handler_context.request.url).hostname 28 | 29 | # 获取配置文件的目标host列表,取自于config中的target 30 | if hostname in app_context.config.get('target'): 31 | # 判断是否为gzip类型,若是进行解压缩处理 32 | if 'Content-Encoding' in handler_context.request.headers and handler_context.request.headers.get( 33 | 'Content-Encoding') == 'gzip': 34 | reqs_data = json.loads(gzip.decompress(handler_context.request.data).decode()) 35 | else: 36 | reqs_data = [] 37 | # 取出配置文件的cases内容,进行查询和校验 38 | rule_list = app_context.config.get('cases') 39 | for rule in rule_list: 40 | # 根据配置文件的selector选择器配置,进行查询 41 | targets_list = search(reqs_data, rule.get('selector')) 42 | # 如果有匹配的结果,进行进一步的校验 43 | if targets_list: 44 | validate(rule, targets_list) 45 | -------------------------------------------------------------------------------- /lyrebird_tracking/webui.py: -------------------------------------------------------------------------------- 1 | import json 2 | import lyrebird 3 | import os 4 | import codecs 5 | from flask import request, jsonify, Response, abort 6 | from lyrebird import context 7 | from lyrebird_tracking.server import tracking_init 8 | from lyrebird_tracking.context import app_context 9 | import shutil 10 | 11 | 12 | class Tracking(lyrebird.PluginView): 13 | """ 14 | tracking插件视图 15 | 16 | """ 17 | def index(self): 18 | """ 19 | 插件首页 20 | """ 21 | return self.render_template('index.html') 22 | 23 | def get_result(self): 24 | """ 25 | 获取case列表API 26 | :return: case列表数据 27 | """ 28 | return jsonify({'result': app_context.result_list}) 29 | 30 | def get_content(self, id=''): 31 | """ 32 | 获取校验详情API 33 | :param id: 对应uuid,每个case的唯一标识,根据id查询校验详情 34 | :return: 对应id的校验详情 35 | """ 36 | for item in app_context.content: 37 | if item['id'] == id: 38 | return jsonify(item) 39 | return abort(400, 'Request not found') 40 | 41 | def save_report(self): 42 | """ 43 | 保存测试报告API 44 | """ 45 | report_data_path = os.path.join(os.path.dirname(__file__), 'report_template/data/report-data.js') 46 | with codecs.open(report_data_path, 'w+', 'utf-8') as f: 47 | f.write('var reportCaseData='+json.dumps({'result': app_context.result_list}, ensure_ascii = False)) 48 | f.write('\n') 49 | f.write('var baseData='+json.dumps(app_context.config, ensure_ascii = False)) 50 | f.write('\n') 51 | f.write('var detailCollection='+json.dumps(app_context.content, ensure_ascii = False)) 52 | f.write('\n') 53 | report_path = os.path.join(os.path.dirname(__file__), 'report_template') 54 | target_path = os.path.abspath(os.path.join(lyrebird.get_plugin_storage(), 'report')) 55 | if os.path.exists(target_path): 56 | shutil.rmtree(target_path) 57 | shutil.copytree(report_path, target_path) 58 | 59 | return context.make_ok_response() 60 | 61 | def clear_result(self): 62 | """ 63 | 清空测试缓存API 64 | 需要进行初始化,并且发送socketio消息给前端重新load页面 65 | """ 66 | app_context.result_list = [] 67 | app_context.content = [] 68 | tracking_init() 69 | context.application.socket_io.emit('update', namespace='/tracking-plugin') 70 | return context.make_ok_response() 71 | 72 | def get_base_info(self): 73 | """ 74 | 获取基准文件信息API 75 | 主要用于分组筛选 76 | :return: 基准文件原始数据 77 | """ 78 | return jsonify(app_context.config) 79 | 80 | def groups(self): 81 | """ 82 | 获取选中的case分组;初始返回所有分组 83 | """ 84 | return jsonify(app_context.select_groups) 85 | 86 | def select(self): 87 | """ 88 | 更新选中的case分组 89 | """ 90 | grouplist = request.json.get('group') 91 | app_context.select_groups = grouplist 92 | return context.make_ok_response() 93 | 94 | 95 | def on_create(self): 96 | """ 97 | 插件初始化函数 98 | """ 99 | # tracking 初始化 100 | tracking_init() 101 | # 设置模板文件目录(可选,设置静态文件目录) 102 | self.set_template_root('lyrebird_tracking') 103 | self.add_url_rule('/', view_func=self.index) 104 | self.add_url_rule('/result', view_func=self.get_result) 105 | self.add_url_rule('/content/', view_func=self.get_content) 106 | self.add_url_rule('/report', view_func=self.save_report) 107 | self.add_url_rule('/clear', view_func=self.clear_result) 108 | self.add_url_rule('/base', view_func=self.get_base_info) 109 | self.add_url_rule('/group', view_func=self.groups) 110 | self.add_url_rule('/select', view_func=self.select, methods=['POST']) 111 | 112 | 113 | def get_icon(self): 114 | """ 115 | 设置展示在边栏的图标 116 | :return: 返回图标样式 117 | """ 118 | return 'fa fa-fw fa-line-chart' 119 | 120 | def default_conf(self): 121 | """ 122 | 设置默认的 conf.json 123 | :return: 返回 conf.json 内容 124 | """ 125 | # 读取插件 conf.json 返回 126 | conf_path = os.path.dirname(__file__) + '/conf.json' 127 | with codecs.open(conf_path, 'r', 'utf-8') as f: 128 | return json.load(f) 129 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lyrebird 2 | jsonschema -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name='lyrebird-tracking', 11 | version='0.11.2', 12 | packages=['lyrebird_tracking'], 13 | url='https://github.com/meituan/lyrebird-tracking', 14 | author='HBQA', 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | include_package_data=True, 18 | zip_safe=False, 19 | classifiers=( 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.6", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: MacOS", 24 | ), 25 | entry_points={ 26 | 'console_scripts': [ 27 | ], 28 | 'lyrebird_data_handler': [ 29 | 'tracking = lyrebird_tracking.tracking:TrackingHandler' 30 | ], 31 | 'lyrebird_web': [ 32 | 'tracking = lyrebird_tracking.webui:Tracking' 33 | ] 34 | }, 35 | install_requires=[ 36 | 'lyrebird==1.8.7', 37 | 'jsonschema' 38 | ] 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /tests/test_filter_error.py: -------------------------------------------------------------------------------- 1 | from lyrebird_tracking.server.__init__ import filter_error_msg 2 | import operator 3 | 4 | result_dic = { 5 | 'name': 'a', 6 | 'asserts': [{ 7 | 'field': 'a', 8 | 'flag': False, 9 | 'hint': "x" 10 | }, { 11 | 'field': 'b', 12 | 'flag': False, 13 | 'hint': 'y' 14 | }, { 15 | 'field': 'c', 16 | 'flag': True, 17 | 'hint': "z" 18 | }], 19 | 'content': { 20 | 'a': 1 21 | }, 22 | } 23 | 24 | excepted_error = { 25 | 'name': 'a', 26 | 'content': { 27 | 'a': 1 28 | }, 29 | 'error_msg': "name: a\nerror message:\nfield: a\nerror detail: x\nfield: b\n" 30 | "error detail: y\n" 31 | } 32 | 33 | 34 | def test_filter_error(): 35 | error_message = dict((k, result_dic[k]) for k in ('name', 'content') if k in result_dic) 36 | error_message['error_msg'] = filter_error_msg(result_dic) 37 | assert operator.eq(excepted_error, error_message) is True 38 | -------------------------------------------------------------------------------- /tests/test_select.py: -------------------------------------------------------------------------------- 1 | from lyrebird_tracking.server.search_handler import SearchHandler 2 | 3 | data_list = [ 4 | { 5 | "a": 3, 6 | "b": [ 7 | { 8 | "x": "s", 9 | "y": [{ 10 | "n": "test", 11 | "m": "haha" 12 | }, { 13 | "n": "notest", 14 | "m": "heihei" 15 | }] 16 | }, 17 | { 18 | "x": "t", 19 | "y": "ooo" 20 | }, { 21 | "x": "st", 22 | "m": 123 23 | } 24 | ] 25 | }, { 26 | "a": 3, 27 | "b": [{ 28 | "x": "st", 29 | "y": 456 30 | }, { 31 | "x": "st", 32 | "y": 789 33 | }, { 34 | "x": "s", 35 | "y": 123 36 | }] 37 | } 38 | ] 39 | 40 | raw_data = { 41 | "haha": [ 42 | { 43 | 'a': 'p1', 44 | 'b': 'pp', 45 | 'evs': 46 | [ 47 | { 48 | 'b': 'p2', 49 | 'x': { 50 | 'y': { 51 | 'z': [ 52 | { 53 | 'c': 'p3', 54 | 'core': 'gq' 55 | }, 56 | { 57 | 'c': 'p2', 58 | 'core': 'gq1' 59 | } 60 | ] 61 | } 62 | } 63 | }, 64 | { 65 | 'b': 'p2', 66 | 'x': { 67 | 'y': { 68 | 'z': [ 69 | { 70 | 'c': 'p33', 71 | 'core': 'gq3' 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | ] 78 | }, 79 | {'a': 'p1', 'b': 'p2','evsmock': []}, 80 | [1, 'a'], 81 | 1, 82 | 'b' 83 | ], 84 | "heihei": [{'a': 'p1', 'evs': []}], 85 | "lala": 123 86 | } 87 | 88 | 89 | def test_transfer_query(): 90 | jsonpath_str = "$[?ca='travel'].evs[?vb='yf']" 91 | search_list = SearchHandler().transfer_query(jsonpath_str) 92 | assert search_list == ["[?ca='travel']", "evs", "[?vb='yf']"] 93 | 94 | jsonpath_str = "$haha[?a='p1'].evs[?b='p2'].x.y.z[?c='p3']" 95 | search_list = SearchHandler().transfer_query(jsonpath_str) 96 | assert search_list == ["haha", "[?a='p1']", "evs", "[?b='p2']", "x", "y", "z", "[?c='p3']"] 97 | 98 | 99 | def test_query1(): 100 | jsonpath_str = "$haha[?a='p1'].evs[?b='p2'].x.y.z[?c='p3']" 101 | node = SearchHandler(raw_data) 102 | result = node.find(jsonpath_str).data 103 | assert len(result) == 1 104 | assert result[0]['core'] == 'gq' 105 | 106 | 107 | def test_query2(): 108 | jsonpath_str = "haha[?a='p1'].evs[?b='p2'].x.y.z[?c='p3']" 109 | node = SearchHandler(raw_data) 110 | result = node.find(jsonpath_str).data 111 | assert len(result) == 1 112 | assert result[0]['core'] == 'gq' 113 | 114 | 115 | def test_query3(): 116 | jsonpath_str = "$haha[?a='p1'].$evs[?b='p2'].x.y.z[?c='p3']" 117 | node = SearchHandler(raw_data) 118 | result = node.find(jsonpath_str).data 119 | assert len(result) == 0 120 | 121 | 122 | def test_query4(): 123 | jsonpath_str = "$[?a=3].b[?x='s']" 124 | node = SearchHandler(data_list) 125 | result = node.find(jsonpath_str).data 126 | assert len(result) == 2 127 | assert result[1]['y'] == 123 128 | assert result[0]['y'][0]['n'] == 'test' 129 | 130 | 131 | def test_query5(): 132 | jsonpath_str = "$[*].b[?x='st']" 133 | node = SearchHandler(data_list) 134 | result = node.find(jsonpath_str).data 135 | assert len(result) == 3 136 | assert result[0]['m'] == 123 137 | assert result[1]['y'] == 456 138 | assert result[2]['y'] == 789 139 | 140 | 141 | def test_query6(): 142 | jsonpath_str = "$[2].b[?x='st']" 143 | node = SearchHandler(data_list) 144 | result = node.find(jsonpath_str).data 145 | assert len(result) == 0 146 | 147 | 148 | def test_query7(): 149 | jsonpath_str = "$[1].b[?x='st']" 150 | node = SearchHandler(data_list) 151 | result = node.find(jsonpath_str).data 152 | assert len(result) == 2 153 | assert result[0]['y'] == 456 154 | assert result[1]['y'] == 789 155 | 156 | 157 | def test_query8(): 158 | jsonpath_str = "$haha[?a='p1'].evs[*].x.y.z[?c='p3']" 159 | node = SearchHandler(raw_data) 160 | result = node.find(jsonpath_str).data 161 | assert len(result) == 1 162 | assert result[0]['core'] == 'gq' 163 | 164 | 165 | def test_query9(): 166 | jsonpath_str = "$haha[?a='p1'].evs[*].x.y.z" 167 | node = SearchHandler(raw_data) 168 | result = node.find(jsonpath_str).data 169 | assert len(result) == 2 170 | assert result[1][0]['c'] == 'p33' 171 | 172 | 173 | def test_query10(): 174 | jsonpath_str = "$haha[?a='p1'&b='pp'].evs[*].x.y.z" 175 | node = SearchHandler(raw_data) 176 | result = node.find(jsonpath_str).data 177 | assert len(result) == 2 178 | assert result[1][0]['c'] == 'p33' --------------------------------------------------------------------------------