diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..fcdf80c
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,26 @@
+{
+ "presets": [
+ "stage-0",
+ "es2015",
+ "env"
+ ],
+ "plugins": [
+ [
+ "transform-es3-property-literals",
+ "transform-es3-member-expression-literals",
+ [
+ "transform-es2015-modules-commonjs",
+ {
+ "loose": true
+ }
+ ],
+ "component",
+ [
+ {
+ "libraryName": "element-ui",
+ "styleLibraryName": "~./src/editor/theme/element-ui"
+ }
+ ]
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..8dd9905
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,123 @@
+{
+ "env": {
+ "browser": true,
+ "commonjs": true,
+ "es6": true,
+ "node": true
+ },
+ "globals": {
+ "XDomainRequest": true,
+ "window": true,
+ "wx": true
+ },
+ "extends": "eslint:recommended",
+ "parser": "babel-eslint",
+ "parserOptions": {
+ "ecmaVersion": 7,
+ "ecmaFeatures": {
+ // lambda表达式
+ "arrowFunctions": true,
+ // 解构赋值
+ "destructuring": true,
+ // class
+ "classes": true,
+ // http://es6.ruanyifeng.com/#docs/function#函数参数的默认值
+ "defaultParams": true,
+ // 块级作用域,允许使用let const
+ "blockBindings": true,
+ // 允许使用模块,模块内默认严格模式
+ "modules": true,
+ // 允许字面量定义对象时,用表达式做属性名
+ // http://es6.ruanyifeng.com/#docs/object#属性名表达式
+ "objectLiteralComputedProperties": true,
+ // 允许对象字面量方法名简写
+ /*var o = {
+ method() {
+ return "Hello!";
+ }
+ };
+
+ 等同于
+
+ var o = {
+ method: function() {
+ return "Hello!";
+ }
+ };
+ */
+ "objectLiteralShorthandMethods": true,
+ /*
+ 对象字面量属性名简写
+ var foo = 'bar';
+ var baz = {foo};
+ baz // {foo: "bar"}
+
+ // 等同于
+ var baz = {foo: foo};
+ */
+ "objectLiteralShorthandProperties": true,
+ // http://es6.ruanyifeng.com/#docs/function#rest参数
+ "restParams": true,
+ // http://es6.ruanyifeng.com/#docs/function#扩展运算符
+ "spread": true,
+ // http://es6.ruanyifeng.com/#docs/iterator#for---of循环
+ "forOf": true,
+ // http://es6.ruanyifeng.com/#docs/generator
+ "generators": true,
+ // http://es6.ruanyifeng.com/#docs/string#模板字符串
+ "templateStrings": true,
+ "superInFunctions": true,
+ // http://es6.ruanyifeng.com/#docs/object#对象的扩展运算符
+ "experimentalObjectRestSpread": true,
+ "experimentalDecorators": true
+ },
+ "sourceType": "module"
+ },
+ "rules": {
+ "indent": [ 1, 2, {
+ "SwitchCase": 1
+ }
+ ],
+ // 文件末尾强制换行
+ "eol-last": 1,
+ // 使用 === 替代 ==
+ "eqeqeq": [
+ 1,
+ "allow-null"
+ ],
+ // 控制逗号在行尾出现还是在行首出现
+ // http://eslint.org/docs/rules/comma-style
+ "comma-style": [
+ 1,
+ "last"
+ ],
+ "linebreak-style": [
+ 1,
+ "unix"
+ ],
+ "quotes": [
+ 1,
+ "single"
+ ],
+ "semi": [
+ 1,
+ "never"
+ ],
+ "no-extra-semi": 0,
+ "semi-spacing": 0,
+ "no-alert": 0,
+ "no-array-constructor": 1,
+ "no-caller": 1,
+ "no-catch-shadow": 0,
+ "no-cond-assign": 1,
+ "no-console": 0,
+ "no-constant-condition": 0,
+ "no-continue": 0,
+ "no-control-regex": 1,
+ "no-debugger": 1,
+ "no-delete-var": 1,
+ "no-mixed-spaces-and-tabs": 1,
+ "no-unused-vars": 1,
+ "comma-dangle": 1
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..acacf04
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+/node_modules/
+/build/
+/libs/
+/lib/
+!/lib/.gitkeep
+/temp/
+/private/
+# Logs
+*.log
+.DS_Store
+*.rdb
+
+# config etc
+.idea/
+config.js
+.vscode/
+!src/config.js
+
+
+npm-debug.log*
+/package-lock.json
+/yarn-error.log
+/yarn.lock
\ No newline at end of file
diff --git a/.jshintignore b/.jshintignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..997b3f7
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,10 @@
+{
+ "node": true,
+
+ "curly": true,
+ "latedef": true,
+ "quotmark": true,
+ "undef": true,
+ "unused": true,
+ "trailing": true
+}
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..f9a980a
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,25 @@
+/tests/
+/src/
+/vender/
+/vendor/
+/build/
+/temp/
+/bin/
+/private/
+/.*
+/*.sh
+
+# === vscode ====
+/.vscode/
+
+/.babelrc
+
+# === WebStorm ===
+/.idea/
+/testServer.js
+/webpack.config.js
+/yarn-error.log
+/yarn.lock
+/config.js
+/sugoio-jslib-snippet.js
+compile
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ab8d66c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,120 @@
+# Sugo JavaScript Library
+The Sugo JavaScript Library is a set of methods attached to a global `sugoio` object
+intended to be used by websites wishing to send data to Sugo projects. A full reference
+is available
+
+
+```sh
+npm install
+
+npm run build
+```
+
+You can then require the lib like a standard Node.js module:
+
+```javascript
+var sugoio = require('sugo-sdk-js');
+
+sugoio.init("YOUR_TOKEN");
+sugoio.track("An event");
+```
+
+## Building bundles for release
+- Install development dependencies: `npm install`
+- Build: `npm run deploy`
+- publish `npm publish`
+
+## custom config
+```
+cp config.default.js config.js
+//then edit config.js
+```
+## Running tests
+- Install development dependencies: `npm install`
+- Start test server: `npm test`
+- Browse to [http://localhost:4000/tests/](http://localhost:4000/tests/) and choose a scenario to run
+
+In the future we plan to automate the last step with a headless browser to streamline development (although
+Sugo production releases are tested against a large matrix of browsers and operating systems).
+
+```
+sugoio.init('TOKEN(APPID)', {
+ project_id: '项目ID',
+ loaded: function(lib) {
+ sugoio.time_event('页面停留时间')
+ sugoio._.register_event(window, 'beforeunload', function(){
+ sugoio.track('页面停留时间',{page: location.pathname})
+ }, false, true)
+ }
+});
+//给每条上报记录添加自定义维度
+sugoio.register({CS1:'userid',CS2: 'xxxx'});
+
+//手动埋点上报数据
+sugoio.track("事件名称", {
+ properties //附带属性
+})
+```
+
+## SDK上报维度
+|维度名|维度标题|类型|iOS|Android|Web|维度描述|
+|:---|:---|:---:|:---:|:---:|:---:|:---|
+|__time | 服务端时间 | date | √ | √ | √ | 服务端时间 |
+|sugo_nation | 国家 | string | √ | √ | √ | 用户所在国家(ip反解析) |
+|sugo_province | 省份 | string | √ | √ | √ | 用户所在省份(ip反解析) |
+|sugo_city | 城市 | string | √ | √ | √ | 用户所在城市(ip反解析) |
+|sugo_district | 地区 | string | √ | √ | √ | 用户所在地区(ip反解析) |
+|sugo_area | 区域 | string | √ | √ | √ | 用户所在区域(ip反解析) |
+|sugo_latitude | 纬度 | string | √ | √ | √ | 纬度(ip反解析) |
+|sugo_longitude | 经度 | string | √ | √ | √ | 经度(ip反解析) |
+|sugo_city_timezone | 城市时区 | string | √ | √ | √ | 所在时区代表城市(ip反解析) |
+|sugo_timezone | 时区 | string | √ | √ | √ | 所在时区(ip反解析) |
+|sugo_phone_code | 国际区号 | string | √ | √ | √ | 国际区号(ip反解析) |
+|sugo_nation_code | 国家代码 | string | √ | √ | √ | 国家代码(ip反解析) |
+|sugo_continent | 所在大洲代码 | string | √ | √ | √ | 所在大洲(ip反解析) |
+|sugo_administrative | 行政区划代码 | string | √ | √ | √ | 中国行政区划代码(ip反解析) |
+|sugo_operator | 宽带运营商 | string | √ | √ | √ | 用户所在运营商(ip反解析) |
+|sugo_ip | 客户端IP | string | √ | √ | √ | 客户端IP(nginx)Header的X-Real-IP,或者 remote_addr |
+|browser | 浏览器名称 | string | x | x | √ | 浏览器名称 |
+|browser_version | 浏览器版本 | string | x | x | √ | 浏览器版本 |
+|app_name | 应用名称 | string | √ | √ | x | 系统或app的系统名称(应用安装后的名字) |
+|app_version | 应用版本 | string | √ | √ | x | 系统或app的系统版本 |
+|app_build_number | 应用编译版本 | string | √ | √ | x | 应用编译版本 |
+|session_id | 会话ID | string | √ | √ | √ | 会话id(app每次打开自动生成,web每次打开浏览器自动生成,如果不关闭浏览器,一天后自动生成新的session_id) |
+|network | 网络类型 | string | √ | √ | x | 用户使用的网络类型(wifi,2g,3g,4g) |
+|device_id | 设备ID | string | √ | √ | x | Android: IMEI > mac > android_id/iOS: IDFA > IDFV |
+|bluetooth_version | 蓝牙版本 | string | x | √ | x | 用户蓝牙版本 |
+|has_bluetooth | 蓝牙功能 | string | x | √ | x | 用户是否有蓝牙 |
+|device_brand | 品牌 | string | √ | √ | √ | 用户电脑、平板、或手机牌子 |
+|device_model | 品牌型号 | string | √ | √ | √ | 用户电脑、平板、或手机型号 |
+|system_name | 操作系统名称 | string | √ | √ | √ | 客户端操作系统名称(Android,iOS, Windows, macOS 等) |
+|system_version | 操作系统版本 | string | √ | √ | √ | 客户端操作系统版本 |
+|radio | 通信协议 | string | √ | √ | x | 通信协议(gsm,cdma,sip等) |
+|carrier | 手机运营商 | string | √ | √ | x | 运营商(中国移动,中国联通等) |
+|screen_dpi | 屏幕DPI | int | x | √ | √ | 客户端分辨率每平方英寸的点数 |
+|screen_pixel | 屏幕分辨率 | string | √ | √ | √ | 客户端屏幕分辨率(格式:屏幕宽度*屏幕高度) |
+|event_time | 客户端事件时间 | date | √ | √ | √ | 客户端事件发生时间(unix毫秒数) |
+|current_url | 当前请求地址 | string | x | x | √ | 客户端当前请求地址 |
+|referring_domain | 客户引荐域名 | string | x | x | √ | 客户引荐域名(上一访问页面地址栏域名) |
+|host | 客户端域名 | string | x | x | √ | 浏览器地址栏域名 |
+|distinct_id | 用户唯一ID | string | √ | √ | √ | web首次访问生成用户唯一id并存在cookies,清除cookies算一个新的用户,app首次打开时候会生成一个用户唯一id,重装算另一个用户 |
+|has_nfc | NFC功能 | string | x | √ | x | 是否有NFC功能 |
+|has_telephone | 电话功能 | string | x | √ | x | 是否有电话功能 |
+|has_wifi | WIFI功能 | string | √ | √ | x | 是否有wifi功能 |
+|manufacturer | 设备制造商 | string | √ | √ | x | 设备制造商(meizu,huawei等) |
+|duration | 停留时间 | float | √ | √ | √ | 页面停留时间(停留事件才有) |
+|sdk_version | SDK版本 | string | √ | √ | √ | sdk版本 |
+|page_name | 页面名称 | string | √ | √ | √ | 页面名称或窗口名称,可在可视化埋点界面设置,默认取title |
+|path_name | 页面路径 | string | √ | √ | √ | 页面路径(web取域名之后的路径) |
+|event_id | 事件ID | string | √ | √ | √ | 事件ID,可视化埋点绑定的事件才上报 |
+|event_name | 事件名称 | string | √ | √ | √ | 事件名称 |
+|event_type | 事件类型 | string | √ | √ | √ | 事件类型click、focus、submit、change |
+|event_label | 事件源文本 | string | x | √ | √ | 事件源文本(如按钮上的文字) |
+|sugo_lib | sdk类型 | string | √ | √ | √ | sdk类型: web/Objective-C/Swift/Android/Wx-mini |
+|token | 应用ID | string | √ | √ | √ | 应用ID |
+|from_binding | 是否绑定事件 | string | √ | √ | x | 是否绑定事件(null或者true)(用于区分是绑定的,还是系统自动上报的,比如浏览、启动为自动上报,绑定取值1,自动上报取值0) |
+|google_play_service | GooglePlay服务 | string | x | √ | x | 是否有GooglePlay服务 |
+|page_category | 页面分类 | string | √ | √ | √ | 页面分类 |
+|first_visit_time | 首次访问时间 | date | √ | √ | √ | 首次访问时间(用户唯一ID的首次访问时间) |
+|first_login_time | 首次登陆时间 | date | √ | √ | √ | 首次登陆时间(业务ID的首次登录时间,需要把业务ID 传给sdk) |
+
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..a0480fc
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,198 @@
+# v1.5.0
+
+- 每次打开页面设置根据当前url重新匹配页面
+- 增加 editor-lite.js打包(不含vuejs),根据环境动态加载editor
+- 将原来同类对象的深度监听改为浅监听,点击同类勾选重新计算同类path(解决某些环境同类堆栈溢出bug)
+- 支持根据enable_hash监听hashchange事件,可视化埋点重新渲染埋点事件
+- 支持根据enable_hash监听hashchange事件,sdk上报动态加载新配置并绑定事件
+
+# v1.4.1
+
+- add encodeURIComponent for decide
+- add `event_type`, `path_name`, `page_category`, default value for track
+
+# v1.4.0
+
+- sugoio.init支持enable_hash参数, 主要针对vue等单页应用页面路径规则添加hash(pathname=location.pathname + location.hash)规则
+
+```js
+ sugoio.init('ee7469aca94d082748418945eccd13eb', {
+ project_id: 'com_HyoaKhQMl_project_ry_2vnt1M',
+ enable_hash: true,
+ ...
+ });
+```
+# v1.3.26
+
+- 添加sugoio.updateSessionId()方法,更新cookie中的session_id值
+- 小程序增加接口
+ - sugoio.time_event 停留记时函数
+ - sugoio.register 超级属性设置(存在覆盖)
+ - sugoio.register_once 超级属性设置(存在则不添加)
+ - sugoio.unregister 删除超级属性
+
+# v1.3.25(2018-06-19)
+
+ - 支持简单的关联元素同类埋点取值
+
+# v1.3.23,24(2018-06-19)
+
+- 完善小程序配置
+- 压缩打包微信小程序
+
+# v1.3.22(2018-06-19)
+
+- 增加微信小程序代码埋点sdk(libs/sugo-wx-mini.program.min.js)
+
+# v1.3.21(2018-06-19)
+
+- 修复埋点与iscroll兼容问题
+
+# v1.3.20(2018-06-11)
+
+- 修复cdn初始化参数未覆盖问题
+- 修复使用配置app_host拉取页面配置
+
+# v1.3.18(2018-06-07)
+
+- 修复sdk ie8下userAgent报错的问题
+
+# v1.3.17(2018-04-27)
+
+- 修复sdk的install函数, 确保每次都正确写入app_host等自定义配置
+
+# v1.3.16(2017-11-21)
++ 增加`首次访问`,`首次登录`事件类型
++ 上报数据增加`first_visit_time`, `first_login_time`字段,
+ 分别对应`首次访问时间`,`首次登录时间`
++ 增加`sugoio.track_first_time`, `sugoio.clear_first_login`接口
+
+### 接口定义
+```typescript
+interface SugoIO {
+ // 上报用户首次登录状态,调用该接口,执行步骤如下:
+ // 1. 如果user_id与持久化存储空间中的user_id一致,直接调用callback, 返回 SugoIO
+ // 2. 查询该用户是否为首次登录
+ // 2.1 请求成功
+ // 2.1.1 首次登录
+ // 2.1.2 将user_id,首次登录时间与user_real_dimension(如果提供了)写入存储空间
+ // 2.1.3 发送首次登录事件
+ // 2.2.4 执行callback(null)
+ // 2.2 请求错误
+ // 2.2.1 执行callback(err)
+ // 3. 返回SugoIO
+ track_first_time(
+ // 用户真实id
+ user_id: string,
+ // 用户真实id所属维度名,如果没有提供,上报数据中不会包含user_id
+ user_real_dimension?: string | ((err: Error |null) => any),
+ // 回调函数,如果出错,err为Error类型,正常完成时err为null
+ callback?: (err: Error | null)=> any
+ ): SugoIO
+
+ // 清除用户登录状态,返回SugoIO
+ clear_first_login(user_id: string): SugoIO
+}
+```
+### 示例
+```js
+sugoio.track_first_time('test_user_id', 'real_user_id', function (err) { console.log(err) });
+sugoio.track_first_time('test_user_id', 'real_user_id');
+sugoio.track_first_time('test_user_id', function (err) { console.log(err) });
+sugoio.clear_first_login('test_user_id')
+```
+
+# v1.3.0 (2017-11-16)
++ 上报数据代码支持ie8+
+ 如果要支持ie8及ie9,必须使用ie __标准模式__,需要在html中添加如下代码:
+ ```html
+
+
+
+
+
+
+ ```
+ 该内容定义文档使用ie标准模式渲染,这是上报数据能在ie8\9下运行的必要条件。
+ 该设置是兼容ie网站的渲染与js引擎的最优设置方式。
+ 当然你也可以使用其他设置使得你的网站采用标准模式渲染,详细设置参考:
+
+ + [合法DOCTYPE列表](https://www.w3.org/QA/2002/04/valid-dtd-list.html)
+ + https://msdn.microsoft.com/en-us/library/ff955275(v=vs.85).aspx
+ + https://hsivonen.fi/doctype/
+ + https://hsivonen.fi/doctype/#ie8modes
+ + https://hsivonen.fi/doctype/ie8-mode.png
+
++ 默认关闭高级选项,删除开启高级选项switch,由用户输入的code与关联元素决定是否开启高级选项
++ 用户从高级选项切换到其他编辑项时,自动切换页面状态由关联元素选择状态变为编辑状态
++ 修复埋点事件页面路径属性展示错误
+
+__ie8\9兼容模式下上报数据的支持视需求强烈而定__
+
+# v1.2.33 (2017-11-13)
++ 修复部署代码多次写入配置的bug。
++ UI优化:埋点圈选按钮增加生效与无效的状态。
+
+# v1.2.23 (2017-11-11)
++ 重构部署代码逻辑,修复隐患
+
+# v1.2.8 (2017-11-02)
++ UI升级
++ 默认开启事件高级选项
++ 关联元素同类有效配置默认变为false
++ 非编辑状态下,鼠标移动时不再圈选页面元素
+
+# v1.2.7(2017-11-01)
++ 开放Debug工具函数
++ 修复部署代码bug
+
+# v1.2.6 (2017-10-16)
++ 兼容旧页面配置
++ 兼容旧页面事件
++ 优化拖动元素,所有的元素都只能拖动标题部份,以提升输入内容时体验。
+
+# v1.2.5 (2017-09-26)
++ 删除重复内容字段:referrer、sugo_user_agent、sugo_args
+
+# v1.2.4 (2017-09-23)
++ 同类元素增加可选限制配置,包括id、class与自定义
++ 页面路径最大长度由50变为2048
+
+# v1.2.2 (2017-09-20)
++ 修复app_host配置:如果app_host中没有 protocol,则会加上当前页面的 protocol。
+ 该配置会影响sugoio-last.min.js加载以及上报与请求数据的接口。
+
+# v1.2.1 (2017-09-18)
++ 优化路径生成规则:
+ + 如果生成的最优路径对应多个元素,生成全量路径来代替之前的报错
+ + 同类元素使用全量路径匹配
+
++ 增强同类元素选择
+ + 同类元素路径与元素本身路径分离
+ + 同类元素各个元素的父级影响范围可任意定制
+
++ 增加忽略编辑器操作:按住`ctrl`(windows\linux)或`command`(OSX)键
++ UI与交互优化、统一
+
+# v1.0.0 (2017-08-29)
+### 新增功能
++ 新增页面元素关联维度功能
++ 新增页面路径通配符功能
++ 新增页面分类名称匹配功能
+### 优化
++ UI全部重构
++ 代码使用vue重构
++ 性能优化
++ 其他bug修复
+
+# v0.2.6 (2017-06-07)
++ 修复多次调用`sugoio.init`产生的数据被覆盖的bug。
++ 点击编辑器之外的元素时,`已创建事件`面板会被隐藏。
++ 修复删除已埋点事件时UI有时不能同步的问题。
++ 删除此前es5\6混合代码,统一转为es5,增加可读性及性能。
++ 修复一些功能函数中的bug。
++ 更详细的日志,以便排查错误。
+
+# v0.2.5 (2017-05-25)
++ 添加`system_name`字段
+
diff --git a/compile b/compile
new file mode 100644
index 0000000..06a02a2
--- /dev/null
+++ b/compile
@@ -0,0 +1,23 @@
+#!/bin/bash -eu
+echo 'rm build/*'
+rm -rf ./build/
+mkdir -p ./build
+
+if [ $@ = 'build' ]; then
+ echo 'rm libs/*'
+ rm -rf ./libs/
+ mkdir -p ./libs
+fi
+
+# sugoio-core
+echo $@ 'Bundling globals and editor files'
+npm run $@
+
+if [ $? -ne 0 ]; then
+ echo "Bundling was get error and exit"
+ exit 1
+fi
+
+echo "Copy to libs"
+cp ./build/sugo-sdk.js ./libs/sugoio-latest.min.buf.js
+cp ./build/sugo-sdk.js ./libs/sugoio-latest.min.js
diff --git a/config.default.js b/config.default.js
new file mode 100644
index 0000000..d8a09c0
--- /dev/null
+++ b/config.default.js
@@ -0,0 +1,30 @@
+/**
+ * @typedef {Object} SugoIOSDKJSConfig
+ * @property {boolean} debug
+ * @property {string} encode_type
+ * @property {string} api_host
+ * @property {string} app_host
+ * @property {string} decide_host
+ * @property {string} namespace
+ * @property {loaded} dimensions
+ */
+module.exports = {
+
+ debug: false,
+
+ encode_type: 'plain',// 默认json以前的方式 plain=新的数据包模式
+
+ //for /track /engage 数据上报
+ api_host: 'collect.sugo.io',
+
+ //for media 静态资源
+ app_host: 'localhost:8080',
+
+ enable_hash: false,
+
+ //可视化埋点
+ decide_host: 'localhost:8080',
+
+ // 插件所属命名空间,在install的时候会传入
+ namespace: ''
+}
diff --git a/constants.js b/constants.js
new file mode 100644
index 0000000..20be3c6
--- /dev/null
+++ b/constants.js
@@ -0,0 +1,42 @@
+/**
+ * @Author sugo.io
+ * @Date 17-10-30
+ * @desc 常量配置,如果没有把握,不要修改,可以新增
+ */
+
+module.exports = {
+ // sugoio 实例挂载到window对象上的属性
+ PRIMARY_INSTANCE_NAME: 'sugoio',
+ // 全局匹配挂荎sugoio的属性
+ INJECT_CONFIG_PROP_KEY: '__SUGO__INJECT__CONFIG__',
+
+ /* sugoio-core */
+ INIT_MODULE: 0,
+ INIT_SNIPPET: 1,
+
+ SET_QUEUE_KEY: '__sgs',
+ SET_ONCE_QUEUE_KEY: '__sgso',
+ ADD_QUEUE_KEY: '__sga',
+ APPEND_QUEUE_KEY: '__sgap',
+ UNION_QUEUE_KEY: '__sgu',
+ SET_ACTION: 'set',
+ SET_ONCE_ACTION: 'set_once',
+ ADD_ACTION: 'add',
+ APPEND_ACTION: 'append',
+ UNION_ACTION: 'union',
+ PEOPLE_DISTINCT_ID_KEY: 'people_distinct_id',
+ ALIAS_ID_KEY: '__alias',
+ CAMPAIGN_IDS_KEY: '__cmpns',
+ EVENT_TIMERS_KEY: '__timers',
+
+ /* 首次访问与首次登录 */
+ // 用户真实id存储维度名前缀
+ // 生成内部命名key,以免用户通过register方法覆盖
+ PEOPLE_REAL_USER_ID_DIMENSION_PREFIX: '__rpx',
+ FIRST_VISIT_TIME: 'first_visit_time',
+ FIRST_LOGIN_TIME: 'first_login_time',
+
+ /* standard event name */
+ FIRST_VISIT_EVENT_NAME: '首次访问',
+ FIRST_LOGIN_EVENT_NAME: '首次登录'
+}
diff --git a/install.js b/install.js
new file mode 100644
index 0000000..cdcdd5f
--- /dev/null
+++ b/install.js
@@ -0,0 +1,91 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-10
+ */
+
+const CONSTANTS = require('./constants')
+const DefConf = require('./config.default')
+const fs = require('fs')
+const path = require('path')
+
+const namespace = 'sugo-sdk-js'
+
+/**
+ * 需要扩展到应用的资源
+ * @typedef {Object} SugoIOExtensionExtract
+ * @property {Array<{path:string, file:string}>} [assets] - 静态资源文件或目录
+ */
+
+/**
+ * @typedef {function} SugoIOExtension
+ * @param {Koa} app
+ * @param {Router} router
+ * @param {Object} database
+ * @param {Object} config
+ * @param {Object} dependencies
+ * @return {Promise}
+ */
+
+/**
+ * @function {SugoIOExtension}
+ * 安装过程是将 SugoIO/config.js 中的`websdk_api_host`, `websdk_app_host`以及`websdk_decide_host`
+ * 写入到已发布的 ./libs/sugoio-latest.min.js 中。
+ * 由于url的复制性造成替换正则难以书写,且为全局多次替换,极易出错。
+ * 所以直接将配置写入到源代码最前面,以保证在sdk所有代码之前写入配置。
+ */
+function install(app, router, database, config, dependencies) {
+ return new Promise((resolve, reject) => {
+ let conf
+ try {
+ conf = require(path.join(__dirname, './config.js'))
+ } catch (e) {
+ conf = {}
+ console.log(`${namespace} no custom config`)
+ } finally {
+ // 合并 config,优先级为 config.default < config.js < config
+ conf = Object.assign(DefConf, conf, { namespace })
+ }
+
+ // 提取sugoio配置,写入到本地config中
+ const hostsKeys = ['api_host', 'app_host', 'decide_host']
+
+ hostsKeys.forEach((key) => {
+ const ck = 'websdk_' + key
+ if (config[ck]) {
+ conf[key] = config[ck]
+ }
+ })
+
+ const injectScript = `
+ (function(){
+ var INJECT_CONFIG = ${JSON.stringify(conf)};
+ var SugoIO = window['${CONSTANTS.PRIMARY_INSTANCE_NAME}'] || (window['${CONSTANTS.PRIMARY_INSTANCE_NAME}'] = {});
+ SugoIO['${CONSTANTS.INJECT_CONFIG_PROP_KEY}'] = INJECT_CONFIG;
+ })();
+ `
+ const rBuf = path.join(__dirname, './libs/sugoio-latest.min.buf.js')
+ const wBuf = path.join(__dirname, './libs/sugoio-latest.min.js')
+ let reader = fs.createReadStream(rBuf)
+ let writer = fs.createWriteStream(wBuf)
+
+ writer.write(injectScript)
+ reader.pipe(writer)
+
+ writer.on('finish', () => {
+ console.log(`--------------${namespace} installed---------------`)
+ // 静态资源可能会放在CDN,所以不能使用动态的 namespace
+ // 这会造成每次服务重器时url都发生变化
+ // 进而造成CDN缓存失效
+ resolve({ assets: [] })
+ })
+
+ writer.on('error', (err) => {
+ reject(err.message)
+ })
+ })
+}
+
+module.exports = {
+ install,
+ namespace
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..57681b7
--- /dev/null
+++ b/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "sugo-sdk-js",
+ "version": "1.5.0",
+ "description": "web可视化埋点",
+ "main": "install.js",
+ "scripts": {
+ "start": "pushd examples",
+ "server": "echo 'Browse to localhost:4000/tests' && node testServer.js",
+ "lint": "./node_modules/eslint/bin/eslint.js ./src",
+ "theme:conf": "./node_modules/.bin/et -i ./src/editor/theme/element-variables.css",
+ "dev": "./compile build:dev",
+ "deploy": "./compile build",
+ "theme": "./node_modules/.bin/et -c ./src/editor/theme/element-variables.css -o ./src/editor/theme/element-ui",
+ "build:dev": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
+ "build": "NODE_ENV=production ./node_modules/.bin/webpack -p --progress",
+ "snippet": "./node_modules/.bin/uglifyjs ./sugoio-jslib-snippet.js -m -c --ie8 --comments '/^/*!/' --timings -o ./sugoio-jslib-snippet.min.js",
+ "validate": "npm ls"
+ },
+ "dependencies": {
+ "css-selector-parser": "0.0.5",
+ "element-ui": "2.4.6",
+ "sugo-css-path": "^1.0.0",
+ "sugo-sdk-js-utils": "0.0.15",
+ "ua-parser-js": "^0.7.18",
+ "vue": "2.5.17"
+ },
+ "devDependencies": {
+ "babel-core": "^6.26.3",
+ "babel-eslint": "^7.2.3",
+ "babel-loader": "^7.1.5",
+ "babel-plugin-component": "^1.1.1",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
+ "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
+ "babel-plugin-transform-es3-property-literals": "^6.22.0",
+ "babel-preset-env": "^1.7.0",
+ "babel-preset-es2015": "^6.24.1",
+ "babel-preset-stage-0": "^6.24.1",
+ "babel-preset-vue-app": "^2.0.0",
+ "cookie-parser": "^1.4.3",
+ "css-loader": "^1.0.0",
+ "element-theme": "^2.0.1",
+ "element-theme-default": "^1.4.13",
+ "eslint": "^5.4.0",
+ "express": "^4.16.3",
+ "file-loader": "^0.8.5",
+ "lodash": "^4.17.11",
+ "lz-string": "^1.4.4",
+ "morgan": "^1.9.0",
+ "node-sass": "^4.9.3",
+ "precommit-hook": "^3.0.0",
+ "sass-loader": "^7.1.0",
+ "style-loader": "^0.22.1",
+ "sugo-analytics-common-tools": "^1.1.1",
+ "uglify-js": "^3.4.8",
+ "uglifyjs-webpack-plugin": "1.3.0",
+ "url-loader": "^1.1.1",
+ "vue-loader": "^12.1.0",
+ "vue-template-compiler": "2.5.17",
+ "webpack": "^2.7.0"
+ },
+ "author": "WuQic",
+ "license": "ISC",
+ "pre-commit": [
+ "lint",
+ "validate",
+ "test"
+ ]
+}
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..e2ef64d
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,13 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-11
+ * @description 获取服务器配置
+ */
+
+var CONSTANTS = require('../constants')
+var PRIMARY_INSTANCE_NAME = CONSTANTS.PRIMARY_INSTANCE_NAME
+var INJECT_CONFIG_PROP_KEY = CONSTANTS.INJECT_CONFIG_PROP_KEY
+var SugoIO = window[PRIMARY_INSTANCE_NAME] || (window[PRIMARY_INSTANCE_NAME] = {})
+
+/** @type {SugoIOSDKJSConfig} */
+module.exports = SugoIO[INJECT_CONFIG_PROP_KEY] || (SugoIO[INJECT_CONFIG_PROP_KEY] = {})
diff --git a/src/css-path/base.js b/src/css-path/base.js
new file mode 100644
index 0000000..49d459e
--- /dev/null
+++ b/src/css-path/base.js
@@ -0,0 +1,111 @@
+/**
+ * @Author sugo.io
+ * @Date 17-9-13
+ */
+
+var _ = require('sugo-sdk-js-utils')['default']
+
+/**
+ * @param {string} value
+ * @param {boolean} optimized
+ * @constructor
+ */
+function DOMNodePathStep (value, optimized) {
+ this.value = value
+ this.optimized = optimized || false
+}
+
+DOMNodePathStep.prototype.toString = function () {
+ return this.value
+}
+
+/**
+ * 将string转为16进制计数
+ * @param {string} c
+ * @return {string}
+ */
+function toHexByte (c) {
+ var hexByte = c.charCodeAt(0).toString(16)
+ if (hexByte.length === 1) {
+ hexByte = '0' + hexByte
+ }
+ return hexByte
+}
+
+/**
+ * 验证是否为合法的css标识字符
+ * 有效字符为[a-zA-Z0-9-]
+ * @param {string} c
+ * @return {boolean}
+ */
+function isCSSIdentChar (c) {
+ if (/[a-zA-Z0-9_-]/.test(c)) return true
+ return c.charCodeAt(0) >= 0xA0
+}
+
+/**
+ * css标识是否合法
+ * 以-或[a-zA-Z-]开头,后接[z-zA-Z0-9-]
+ * @param {string} value
+ * @return {boolean}
+ */
+function isCSSIdentifier (value) {
+ return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value)
+}
+
+/**
+ * 转义单个css标识字符
+ * @param {string} c
+ * @param {boolean} isLast
+ * @return {string}
+ */
+function escapeAsciiChar (c, isLast) {
+ return '\\' + toHexByte(c) + (isLast ? '' : ' ')
+}
+
+/**
+ * 转义css标识
+ * @param {string} ident
+ * @return {string}
+ */
+function escapeIdentifierIfNeeded (ident) {
+ if (isCSSIdentifier(ident)) return ident
+ var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident)
+ var lastIndex = ident.length - 1
+ return ident.replace(/./g, function (c, i) {
+ return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c))
+ ? escapeAsciiChar(c, i === lastIndex)
+ : c
+ })
+}
+
+/**
+ * 生成id标识符
+ * @param {string} id
+ * @return {string}
+ */
+function idSelector (id) {
+ return '#' + escapeIdentifierIfNeeded(id)
+}
+
+/**
+ * 获取节点class属性并转成数组,并加上$前缀
+ * @param {Element} node
+ * @return {Array}
+ */
+function prefixedElementClassNames (node) {
+ var classAttribute = node.getAttribute('class')
+ if (!classAttribute) return []
+ var arr = classAttribute.split(/\s+/g)
+ arr = _.filter(arr, Boolean)
+ return _.map(arr, function (name) {return '$' + name})
+}
+
+module.exports.DOMNodePathStep = DOMNodePathStep
+module.exports.toHexByte = toHexByte
+module.exports.isCSSIdentChar = isCSSIdentChar
+module.exports.isCSSIdentifier = isCSSIdentifier
+module.exports.escapeAsciiChar = escapeAsciiChar
+module.exports.escapeIdentifierIfNeeded = escapeIdentifierIfNeeded
+module.exports.idSelector = idSelector
+module.exports.prefixedElementClassNames = prefixedElementClassNames
diff --git a/src/css-path/index.bak.js b/src/css-path/index.bak.js
new file mode 100644
index 0000000..7c44653
--- /dev/null
+++ b/src/css-path/index.bak.js
@@ -0,0 +1,218 @@
+/**
+ * @Author sugo.io
+ * @Date 17-9-13
+ */
+
+var base = require('./base')
+var _ = require('sugo-sdk-js-utils')['default']
+
+var SHADOW_NODE_CONS = {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12,
+ DOCUMENT_POSITION_DISCONNECTED: 1,
+ DOCUMENT_POSITION_PRECEDING: 2,
+ DOCUMENT_POSITION_FOLLOWING: 4,
+ DOCUMENT_POSITION_CONTAINS: 8,
+ DOCUMENT_POSITION_CONTAINED_BY: 16,
+ DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32
+}
+var NodeConstructor = _.isBrowser() ? window.Node || SHADOW_NODE_CONS : SHADOW_NODE_CONS
+var DOMNodePathStep = base.DOMNodePathStep
+
+/**
+ * 生成节点的选择器
+ * @param {Element} node
+ * @param {boolean} optimized - 是否生成最优路径
+ * @param {boolean} isTargetNode
+ * @return {?DOMNodePathStep}
+ */
+function selector (node, optimized, isTargetNode) {
+
+ if (node.nodeType !== NodeConstructor.ELEMENT_NODE) {
+ return null
+ }
+
+ var id = node.getAttribute('id')
+ if (optimized && id) {
+ return new DOMNodePathStep(node.nodeName.toLowerCase() + base.idSelector(id), true)
+ }
+
+ // 如果该元素为Document节点,返回nodeName
+ var parent = node.parentNode
+ var nodeName = node.nodeName.toLowerCase()
+
+ if (!parent || parent.nodeType === NodeConstructor.DOCUMENT_NODE) {
+ return new DOMNodePathStep(nodeName.toLowerCase(), true)
+ }
+ // 根据兄弟元素确定selector
+ var prefixedOwnClassNamesArray = base.prefixedElementClassNames(node)
+ var needsClassNames = false
+ var needsNthChild = false
+ var ownIndex = -1
+ var siblings = parent.children
+ var i = 0
+
+ // 如果需要生成最优路径,遍历所有的兄弟元素,规则如下
+ // 1. 若兄弟元素与节点名称相同,则需要用class来区分,标识needsClassNames=true
+ // 2. 如果节点没有class,则需要用元素位置来区分,标识needsNthChild=true
+ // 3. 如果节点有class,遍历兄弟元素的class,如果节点class与兄弟元素class重复
+ // 依然需要元素位置来区分,标识needsNthChild=true
+ if (optimized) {
+ for (i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
+ var sibling = siblings[i]
+ if (sibling === node) {
+ ownIndex = i
+ continue
+ }
+ if (needsNthChild) {
+ continue
+ }
+ if (sibling.nodeName.toLowerCase() !== nodeName.toLowerCase()) {
+ continue
+ }
+
+ needsClassNames = true
+ var ownClassNames = prefixedOwnClassNamesArray
+ var ownClassNameCount = 0
+ for (var cn_idx = 0; cn_idx < ownClassNames.length; cn_idx++) {
+ ++ownClassNameCount
+ }
+ if (ownClassNameCount === 0) {
+ needsNthChild = true
+ continue
+ }
+ var siblingClassNamesArray = base.prefixedElementClassNames(sibling)
+ for (var j = 0; j < siblingClassNamesArray.length; ++j) {
+ var siblingClass = siblingClassNamesArray[j]
+ var o_idx = _.indexOf(ownClassNames, siblingClass)
+ if (o_idx === -1) {
+ continue
+ }
+ ownClassNames.splice(o_idx, 1)
+ if (!--ownClassNameCount) {
+ needsNthChild = true
+ break
+ }
+ }
+ }
+ } else if (siblings.length > 1) {
+ // 如果不需要生成最优路径,则全部使用nth-child描述
+ for (i = 0; i < siblings.length; i++) {
+ if (siblings[i] === node) {
+ ownIndex = i
+ needsNthChild = true
+ break
+ }
+ }
+ }
+
+ // 生成selector
+ // 1. 如果节点为input,并带有type属性、节点没有id、没有class,则需要标识节点的type类型
+ // 2. 如果needsNthChild为true,则需要生成:nth-child
+ // 3. 如果需要class标识,则需要生成class
+ var result = nodeName.toLowerCase()
+ if (isTargetNode && nodeName.toLowerCase() === 'input' &&
+ node.getAttribute('type') &&
+ !node.getAttribute('id') &&
+ !node.getAttribute('class')) {
+ result += '[type=\"' + node.getAttribute('type') + '\"]'
+ }
+
+ if (needsNthChild) {
+ result += ':nth-child(' + (ownIndex + 1) + ')'
+ } else if (needsClassNames) {
+ for (var idx = 0; idx < ownClassNames.length; idx++) {
+ result += '.' + base.escapeIdentifierIfNeeded(ownClassNames[idx].substr(1))
+ }
+ }
+
+ return new DOMNodePathStep(result, false)
+}
+
+/**
+ * 返回一个节点的 css selector
+ * @param {Element} node
+ * @param {boolean} optimized
+ * @param {boolean} isTargetNode
+ * @param {boolean} compatible 兼容原来的方式
+ * @return {DOMNodePathStep}
+ */
+function step (node, optimized, isTargetNode, compatible) {
+ if (node.nodeType !== NodeConstructor.ELEMENT_NODE) {
+ return null
+ }
+
+ var id = node.getAttribute('id')
+ // 最优解析
+ // 如果该元素有id,直接返回id
+
+ if (optimized && id && !compatible) {
+ return new DOMNodePathStep(base.idSelector(id), true)
+ }
+
+ // 如果元素全局唯一,直接返回元素nodeName
+ var nodeNameLower = node.nodeName.toLowerCase()
+ if (nodeNameLower === 'body' || nodeNameLower === 'head' || nodeNameLower === 'html') {
+ return new DOMNodePathStep(node.nodeName.toLowerCase(), true)
+ }
+
+ // 生成选择器
+ return selector(node, optimized, isTargetNode)
+}
+
+/**
+ * 生成节点 selector
+ * @param {Element} node
+ * @param {boolean} optimized - 是否生成最优selector
+ * @param {boolean} compatible
+ * @return {string}
+ */
+function path (node, optimized, compatible) {
+ if (node.nodeType !== NodeConstructor.ELEMENT_NODE) {
+ return ''
+ }
+
+ var steps = []
+ var contextNode = node
+
+ while (contextNode) {
+ var token = step(contextNode, optimized, contextNode === node, compatible)
+ if (!token) break
+ steps.push(token)
+ if (token.optimized) break
+ contextNode = contextNode.parentNode
+ }
+
+ return steps.reverse().join(' > ')
+}
+
+/**
+ * 生成节点最优 selector
+ * @param node
+ * @return {string}
+ */
+function optimized (node) {
+ return path(node, true, true)
+}
+
+/**
+ * 生成节点完整 selector
+ * @param {Element} node
+ * @return {string}
+ */
+function entire (node) {
+ return path(node, false, false)
+}
+
+module.exports.optimized = optimized
+module.exports.entire = entire
diff --git a/src/css-path/index.js b/src/css-path/index.js
new file mode 100644
index 0000000..7331257
--- /dev/null
+++ b/src/css-path/index.js
@@ -0,0 +1,5 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-28
+ */
+module.exports = require('sugo-css-path')
\ No newline at end of file
diff --git a/src/debugger.js b/src/debugger.js
new file mode 100644
index 0000000..8976533
--- /dev/null
+++ b/src/debugger.js
@@ -0,0 +1,76 @@
+/**
+ * @Author sugo.io
+ * @Date 17-10-30
+ */
+
+var _ = require('./utils')._
+var Regulation = require('./regulation-parser')
+var Selector = require('./css-path')
+var Logger = require('./logger').get('Sugoio.Debugger')
+
+/**
+ * @typedef {Object} SDKBuffer
+ * @property {Array} PageConfigs - 页面配置列表
+ * @property {Array} PageEvents - 页面事件列表
+ * @property {Array} PageCategories - 页面分类列表
+ */
+
+/** @type {SDKBuffer} */
+var Buffer = {
+ PageConfigs: [],
+ PageEvents: [],
+ PageCategories: []
+}
+
+var Debugger = {
+ /**
+ * css path apis
+ */
+ Selector: Selector,
+
+ /**
+ * 'PageConfigs'|'PageEvents'|'PageCategories'
+ * @param {string} key
+ * @param {*} value
+ * @return {exports}
+ */
+ addBuffer: function (key, value) {
+ if (!Buffer.hasOwnProperty(key)) {
+ Logger.warn('Not allow key: [%s]', key)
+ return this
+ }
+ Buffer[key] = value
+ return this
+ },
+
+ /**
+ * @param {function} filter
+ * @return {EventDesc|null}
+ */
+ findEvent: function (filter) {
+ return Buffer.PageEvents.find(filter) || null
+ },
+
+ /**
+ * @param {function} filter
+ * @return {PageConfigs|null}
+ */
+ findPageConfig: function (filter) {
+ return Buffer.PageConfigs.find(filter) || null
+ },
+
+ /**
+ * @param {function} filter
+ * @return {PageCategories|null}
+ */
+ findCategories: function (filter) {
+ return Buffer.PageCategories.find(filter) || null
+ },
+
+ // Regulation
+ Regulation: Regulation,
+ // utils
+ _: _
+}
+
+module.exports = Debugger
diff --git a/src/global.js b/src/global.js
new file mode 100644
index 0000000..826af19
--- /dev/null
+++ b/src/global.js
@@ -0,0 +1,100 @@
+/**
+ * @Author sugo.io
+ * @Date 17-10-28
+ * @description sdk全局对像,挂载sdk所有数据
+ */
+
+/**
+ * 定义SDKGlobal对象上的属性
+ * @typedef {Object} SDKGlobal
+ * @property {string} version
+ * @property {Logger} Logger
+ *
+ * @property {SDKEditor} Editor - editor
+ *
+ * @property {Debugger} Debugger
+ * @property {Regulations} Regulation
+ */
+
+var CONSTANTS = require('../constants')
+var logger = require('./logger')
+var Debugger = require('./debugger')
+var Regulation = require('./regulation-parser')
+
+var PRIMARY_INSTANCE_NAME = CONSTANTS.PRIMARY_INSTANCE_NAME
+var INJECT_CONFIG_PROP_KEY = CONSTANTS.INJECT_CONFIG_PROP_KEY
+var Store = {}
+
+/**
+ * @return {SugoIORawMaster|Sugoio}
+ */
+function get () {
+ return window[PRIMARY_INSTANCE_NAME] || (window[PRIMARY_INSTANCE_NAME] = [])
+}
+
+/**
+ * @param ins
+ * @return {SugoIORawMaster|Sugoio}
+ */
+function set (ins) {
+ var prev = get()
+ ins.Global = Global
+ ins[INJECT_CONFIG_PROP_KEY] = prev[INJECT_CONFIG_PROP_KEY]
+ window[PRIMARY_INSTANCE_NAME] = ins
+ return ins
+}
+
+/**
+ * 全局内存存储器
+ * @param {string} key
+ * @param {*} value
+ * @return {*}
+ */
+function store (key, value) {
+ Store[key] = value
+ return value
+}
+
+/**
+ * @param {string} key
+ * @return {*}
+ */
+function remove (key) {
+ var v
+ if (Store.hasOwnProperty(key)) {
+ v = Store[key]
+ delete Store[key]
+ }
+ return v
+}
+
+/**
+ * @param {string} key
+ * @return {*}
+ */
+function take (key) {
+ return Store[key]
+}
+
+var SDKGlobal = get()
+/** @type {SDKGlobal} */
+var Global = SDKGlobal.Global || (SDKGlobal.Global = {})
+
+Global.Logger = logger
+Global.Debugger = Debugger
+Global.version = process.env.SDK_VERSION
+Global.Regulation = Regulation
+
+// initialize
+set(get())
+
+module.exports = {
+ Global: Global,
+ get: get,
+ set: set,
+ store: store,
+ remove: remove,
+ take: take
+}
+
+
diff --git a/src/images/cancel-x.png b/src/images/cancel-x.png
new file mode 100644
index 0000000..d20b34b
Binary files /dev/null and b/src/images/cancel-x.png differ
diff --git a/src/images/close-x-dark.png b/src/images/close-x-dark.png
new file mode 100644
index 0000000..abd5551
Binary files /dev/null and b/src/images/close-x-dark.png differ
diff --git a/src/images/close-x-light.png b/src/images/close-x-light.png
new file mode 100644
index 0000000..6b15e2b
Binary files /dev/null and b/src/images/close-x-light.png differ
diff --git a/src/images/mini-news-dark.png b/src/images/mini-news-dark.png
new file mode 100644
index 0000000..82ae4a7
Binary files /dev/null and b/src/images/mini-news-dark.png differ
diff --git a/src/images/play-dark-small.png b/src/images/play-dark-small.png
new file mode 100644
index 0000000..86c27e2
Binary files /dev/null and b/src/images/play-dark-small.png differ
diff --git a/src/loader-globals.js b/src/loader-globals.js
new file mode 100644
index 0000000..cffbd49
--- /dev/null
+++ b/src/loader-globals.js
@@ -0,0 +1 @@
+require('./sugoio/entry').init_from_snippet()
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000..5e7bc48
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,60 @@
+/**
+ * @Author sugo.io
+ * @Date 17-10-30
+ */
+
+/** @type {SugoIOSDKJSConfig} */
+var Conf = require('./config')
+var Utils = require('sugo-sdk-js-utils')['default']
+var LoggerMap = {}
+var DEFAULT_NAMESPACE = 'Sugoio.SDK'
+// 开放一个debug隐藏参数,以便调试
+// 因为search以?开头,所以indexOf一定大于0
+var URLQuery = window.location.search.indexOf('sugoio_debug=true') > 0
+
+module.exports = {
+ /**
+ * @param {string} [namespace]
+ * @return {Utils.Logger}
+ */
+ get: function (namespace) {
+ namespace = namespace || DEFAULT_NAMESPACE
+
+ if (LoggerMap.hasOwnProperty(namespace)) {
+ return LoggerMap[namespace]
+ }
+
+ var logger = new (Utils.Logger)(namespace)
+
+ // 如果开启了debug或者开发环境下,输出全部日志信息
+ if (Conf.debug || URLQuery || process.env.NODE_ENV === 'development') {
+ logger.setLevel(Utils.LoggerLevel.DEBUG)
+ } else {
+ // 生产环境只输出error级别信息
+ logger.setLevel(Utils.LoggerLevel.ERROR)
+ }
+
+ LoggerMap[namespace] = logger
+ return logger
+ },
+
+ /**
+ * @param {Utils.LoggerLevel} level
+ * @param {string} [namespace]
+ */
+ setLevel: function setLevel (level, namespace) {
+ if (namespace) {
+ if (LoggerMap[namespace]) {
+ LoggerMap[namespace].setLevel(level)
+ }
+ }
+
+ for (var propKey in LoggerMap) {
+ LoggerMap[propKey].setLevel(level)
+ }
+ }
+}
+
+// 创建默认Logger
+module.exports.get(DEFAULT_NAMESPACE)
+
diff --git a/src/match-page.js b/src/match-page.js
new file mode 100644
index 0000000..5bf4ae0
--- /dev/null
+++ b/src/match-page.js
@@ -0,0 +1,38 @@
+/**
+ * @Author sugo.io
+ * @Date 17-8-28
+ * @description 确定页面信息
+ * 如果当前页面匹配多条页面信息记录
+ * 1. 设url = location.origin + location.pathname
+ * 2. 优先匹配page === url的记录,如果没有匹配到,则最最后一次更新的记录
+ */
+
+var _ = require('./utils')._
+var Logger = require('./logger').get()
+
+/**
+ * @param {String} url
+ * @param {Array} list
+ * @return {?PageConfDesc}
+ */
+module.exports = function (url, list) {
+ const conf = _.find(list, r => {
+ return r.page === url
+ })
+
+ Logger.info('Matched page config %o', conf)
+ return conf
+ // if (conf) {
+ // Logger.info('Matched page config %o', conf)
+ // return conf
+ // }
+
+ // conf = list.sort(function (a, b) {
+ // return new Date(a.changed_on) - new Date(b.changed_on)
+ // })
+
+ // var result = conf[0] || null
+ // Logger.info('Matched page config %o', result)
+ // return result
+}
+
diff --git a/src/match-pathname.js b/src/match-pathname.js
new file mode 100644
index 0000000..5302c38
--- /dev/null
+++ b/src/match-pathname.js
@@ -0,0 +1,93 @@
+/**
+ * @Author sugo.io
+ * @Date 17-8-19
+ * @description
+ * 改版后页面事件规则变得复杂,具体规则如下
+ * 设:
+ * `url = location.origin + location.pathname`
+ *
+ * ## 页面配置信息变化
+ * 1. `page` 存的值由`location.pathname`变为`url`。
+ * 2. 新增`category`字段
+ *
+ * ## 事件来源变化
+ * 1. 查询`appid`下的所有`PageConfDesc`,得到集合`S(p)`
+ * 2. 建立集合`P`,使用`S(p)`的`category`去匹配`url`,匹配成功,则将记录`page`属性添加到`P`。
+ * 兼容旧数据:
+ * 旧数据中没有`category`,使`category = '*' + page`
+ * 3. 使用页面`P`集合查询所有事件,匹配条件为`EventDesc.page in P`
+ *
+ * @see {PageConfDesc}
+ * @see {EventDesc}
+ *
+ */
+
+var _ = require('./utils')._
+var API = require('./sugo-event-api')
+var Regulation = require('./regulation-parser')
+var Logger = require('./logger').get()
+
+/**
+ * @param {Array} list
+ * @param {string} url
+ * @param {string} path
+ * @return {Array}
+ */
+function matcher (list, url, path) {
+
+ list = _.map(list, function (r) {
+ if (!r.category) {
+ r.category = '*' + r.page
+ }
+ return r
+ })
+
+ // 2.匹配page
+ // 兼容旧版数据:
+ // 旧的数据中没有category字段,默认使用'*' + page为其category
+
+ // 获取与当前页面兼容的页面配置
+ var regulations = Regulation.match(url, _.map(list, function (r) {
+ return r.category
+ }))
+
+ // 事件来源:
+ // 1. url下的所有事件
+ // 2. 页面category能匹配当前url的所有页面
+ var path_name = [url, path]
+
+ _.each(list, function (r) {
+ if (_.indexOf(regulations, r.category) !== -1) {
+ path_name.push(r.page)
+ }
+ })
+
+ return _.uniq(path_name)
+}
+
+/**
+ * @param {String} host
+ * @param {String} token
+ * @param {String} url
+ * @param {String} path
+ * @param {boolean} deployed
+ * @param {Function} callback
+ */
+module.exports = function (host, token, url, path, deployed, callback) {
+
+ // 配置 host
+ API.host = host
+
+ // 查询 token 下的所有PageConfDesc
+ API.getAllPageInfo(token, '0', deployed).success(function (res) {
+
+ if (!res.success) {
+ Logger.error('get all page info error: %s', res.message)
+ callback(res.message)
+ }
+
+ callback(null, matcher(res.result || [], url, path), res.result)
+ })
+}
+
+module.exports.matcher = matcher
diff --git a/src/regulation-parser.js b/src/regulation-parser.js
new file mode 100644
index 0000000..ab3c0f4
--- /dev/null
+++ b/src/regulation-parser.js
@@ -0,0 +1,22 @@
+/**
+ * @Author sugo.io
+ * @Date 17-8-17
+ * @description 通配符正则匹配
+ */
+
+var Regulation = require('css-selector-parser').Regulation
+
+/**
+ * @typedef {object} Regulations
+ * @property {function} test
+ * @property {function} creator
+ * @property {function} exec
+ * @property {function} match
+ */
+
+module.exports = {
+ test: Regulation.test,
+ creator: Regulation.creator,
+ exec: Regulation.exec,
+ match: Regulation.match
+}
diff --git a/src/request.js b/src/request.js
new file mode 100644
index 0000000..b26df52
--- /dev/null
+++ b/src/request.js
@@ -0,0 +1,150 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-14
+ * @description 兼容跨域方案
+ */
+
+var _ = require('./utils')._
+var logger = require('./logger').get()
+
+var XMLHttpRequestCORS = typeof XMLHttpRequest !== void 0 && 'withCredentials' in new XMLHttpRequest()
+// XDomainRequest only work in ie8+
+// https://hsivonen.fi/doctype/#ie8modes
+// ie默认使用的是兼容模式,即ie7,所以在ie8与ie9下userAgent均为MSIE7
+var XDomainRequestCORS = typeof XDomainRequest !== void 0 && navigator.userAgent.indexOf('MSIE') > 0 && parseInt(navigator.userAgent.match(/MSIE ([\d.]+)/)[1], 10) >= 7
+
+if (XMLHttpRequestCORS) {
+ logger.info('Support XMLHttpRequestCORS')
+}
+
+if (XDomainRequestCORS) {
+ logger.info('Support XDomainRequestCORS')
+}
+
+if (!XMLHttpRequestCORS && !XDomainRequestCORS) {
+ logger.warn('Not Support CORS. Sugoio will not work if your domain diff with config.api_host')
+}
+
+function parse (resp) {
+ var result
+ try {
+ result = _.JSONDecode(resp)
+ } catch (e) {
+ result = resp
+ }
+ return result
+}
+
+/**
+ * @typedef {Object} RequestHooksStruct
+ * @property {function(method:string, url:string, data:*): RequestHooksStruct} send
+ * @property {function(callback:function): RequestHooksStruct} success
+ * @property {function(callback:function): RequestHooksStruct} error
+ */
+
+/**
+ * @return {RequestHooksStruct}
+ */
+function createXMLHttpRequestCORS () {
+ var xhr = new XMLHttpRequest()
+ var callbacks = {
+ success: null,
+ error: null
+ }
+ var hooks = {
+ send: function (method, url, data) {
+ xhr.open(method, url)
+ xhr.withCredentials = false
+ xhr.onreadystatechange = function () {
+ var res
+ if (xhr.readyState === 4) {
+ res = parse(xhr.responseText)
+ if (xhr.status >= 200 && xhr.status < 300) {
+ if (callbacks.success) {
+ callbacks.success(res)
+ }
+ } else {
+ if (callbacks.error) {
+ callbacks.error(res)
+ }
+ }
+ }
+ }
+ xhr.send(data || null)
+ return hooks
+ },
+ success: function (callback) {
+ callbacks.success = callback
+ return hooks
+ },
+ error: function (callback) {
+ callbacks.error = callback
+ return hooks
+ }
+ }
+ return hooks
+}
+
+/**
+ * @return {RequestHooksStruct}
+ */
+function createXDomainRequestCORS () {
+ var xdr = new XDomainRequest()
+ var callbacks = {
+ success: null,
+ error: null
+ }
+ var hooks = {
+ send: function (method, url, data) {
+ xdr.open(method, url)
+ xdr.onload = function () {
+ if (callbacks.success) {
+ callbacks.success(parse(xdr.responseText))
+ }
+ }
+ xdr.onerror = function (err) {
+ if (callbacks.error) {
+ callbacks.error(err)
+ }
+ }
+ xdr.ontimeout = function () {}
+ xdr.onprogress = function () {}
+ setTimeout(function () {
+ xdr.send(data || null)
+ }, 0)
+ return hooks
+ },
+ success: function (callback) {
+ callbacks.success = callback
+ },
+ error: function (callback) {
+ callbacks.error = callback
+ }
+ }
+ return hooks
+
+}
+
+/** @type {RequestHooksStruct} */
+var Fake = {
+ send: function () { return Fake },
+ success: function () { return Fake },
+ error: function () { return Fake }
+}
+
+/**
+ * @return {RequestHooksStruct}
+ */
+module.exports = function () {
+ if (XMLHttpRequestCORS) {
+ return createXMLHttpRequestCORS()
+ }
+
+ if (XDomainRequestCORS) {
+ return createXDomainRequestCORS()
+ }
+
+ // not support CORS
+ return Fake
+}
+
diff --git a/src/sugo-event-api.js b/src/sugo-event-api.js
new file mode 100644
index 0000000..56b37fe
--- /dev/null
+++ b/src/sugo-event-api.js
@@ -0,0 +1,132 @@
+var Request = require('./sugoio-request')
+var _ = require('./utils')._
+var CONFIG = require('./config')
+var Logger = require('./logger').get()
+var request = require('./request')
+
+function concat () {
+ var arr = _.toArray(arguments)
+ return arr.join('')
+}
+
+function set_host (host) {
+ return /^https?/.test(host) ? host : document.location.protocol + '//' + host
+}
+
+module.exports = {
+
+ // 如果app_host中没有配置协议,则默认使用当前页面的协议
+ host: set_host(CONFIG.app_host),
+
+ /**
+ * 更新host
+ * @param {string} host
+ */
+ set_host: function (host) {
+ this.host = set_host(host)
+ },
+
+ serialize: function (params) {
+ Logger.debug('Before serialize params: %o', params)
+ return JSON.stringify({ q: _.compressUrlQuery(params) })
+ },
+
+ queryString: function (params) {
+ Logger.debug('Before compress params: %o', params)
+ return 'q=' + _.compressUrlQuery(params)
+ },
+
+ list: function (params) {
+ return request().send('GET', concat(this.host, '/api/sdk/desktop/vtrack-events-draft?', this.queryString(params)))
+ },
+
+ create: function (params) {
+ return Request.post(concat(this.host, '/api/sdk/desktop/vtrack-events-draft/create'), this.serialize(params))
+ },
+
+ delete: function (params) {
+ return Request['delete'](concat(this.host, '/api/sdk/desktop/vtrack-events-draft/delete?', this.queryString(params)))
+ },
+
+ //部署可视化配置(将草稿表记录发布到正式表)
+ deploy: function (params) {
+ return Request.post(concat(this.host, '/api/sdk/desktop/vtrack-events/deploy'), this.serialize(params))
+ },
+ // 保存页面参数设置
+ savePageInfo: function (params) {
+ return Request.post(concat(this.host, '/api/sdk/desktop/save-page-info'), this.serialize(params))
+ },
+
+ /**
+ * 项目项目所有维度
+ * @param {String} token
+ * @return {Object}
+ */
+ dimensions: function (token) {
+ return Request.post(concat(this.host, '/api/sdk/desktop/dimensions'), JSON.stringify({ token: token }))
+ },
+
+ /**
+ * 保存、更新页面分类记录
+ * @param {Array} models
+ * @param {String} token
+ * @param {String} app_version
+ */
+ savePageCategories: function (models, token, app_version) {
+ return Request.post(concat(this.host, '/api/sdk/desktop/page-categories/save'), JSON.stringify({
+ models: models,
+ token: token,
+ app_version: app_version
+ }))
+ },
+
+ /**
+ * 获取所有页面分类
+ * @param {String} appid
+ * @param {String} app_version
+ */
+ getPageCategories: function (appid, app_version) {
+ return Request.get(concat(this.host, '/api/sdk/desktop/page-categories?', this.queryString({ appid: appid, app_version: app_version })))
+ },
+
+ /**
+ * 获取已部署所有页面分类
+ * @param {String} appid
+ * @param {String} app_version
+ */
+ getDeployedPageCategories: function (appid, app_version) {
+ return request().send('GET', concat(this.host, '/api/sdk/desktop/page-categories-deployed?', this.queryString({ appid: appid, app_version: app_version })))
+ },
+
+ /**
+ * 查询appid所属的所有页面信息记录
+ * @param {string} appid
+ * @param {string} app_version
+ * @param {boolean} deployed - 是否获取已部署的页面分类
+ */
+ getAllPageInfo: function (appid, app_version, deployed) {
+ return request().send('GET', concat(
+ this.host,
+ '/api/sdk/desktop/page-info-list' + (deployed ? '-deployed?' : '?'),
+ this.queryString({ appid: appid, app_version: app_version }))
+ )
+ },
+
+ /**
+ * @typedef {Object} GetFirstLoginTimeResponseStruct
+ * @property {boolean} success
+ * @property {{firstLoginTime:number, isFirstLogin: boolean}} result
+ * @property {string} message
+ * @property {number} code
+ * @property {string} type
+ */
+ /**
+ * 查询用户登录状态
+ * @param user_id
+ * @param token
+ * @return {RequestHooksStruct}
+ */
+ getFirstLoginTime: function (user_id, token) {
+ return request().send('GET', concat(this.host, '/api/sdk/desktop/get-first-login-time?userId=', user_id + '&token=' + token))
+ }
+}
diff --git a/src/sugoio-request.js b/src/sugoio-request.js
new file mode 100644
index 0000000..f731465
--- /dev/null
+++ b/src/sugoio-request.js
@@ -0,0 +1,89 @@
+var _ = require('./utils')._
+var sgRequest = module.exports
+var config = {
+ withCredentials: true,
+ contentType: 'application/json',
+ headers: []
+}
+
+var parse = function (req) {
+ var result
+ try {
+ result = JSON.parse(req.responseText)
+ } catch (e) {
+ result = req.responseText
+ }
+ return [result, req]
+}
+
+var xhr = function (type, url, data) {
+ var methods = {
+ success: function () {},
+ error: function () {},
+ always: function () {}
+ }
+ var XHR = window.XMLHttpRequest || window.ActiveXObject
+ var request = new XHR('MSXML2.XMLHTTP.3.0')
+ request.open(type, url, true)
+ request.withCredentials = true
+ request.setRequestHeader('content-type', config.contentType)
+ if (config.headers.length > 0) {
+ _.each(config.headers, function (o) {
+ request.setRequestHeader(o.key, o.value)
+ })
+ }
+ request.onreadystatechange = function () {
+ var req
+ if (request.readyState === 4) {
+ req = parse(request)
+ if (request.status >= 200 && request.status < 300) {
+ methods
+ .success
+ .apply(methods, req)
+ } else {
+ methods
+ .error
+ .apply(methods, req)
+ }
+ methods
+ .always
+ .apply(methods, req)
+ }
+ }
+ request.send(data)
+ var resXHR = {
+ success: function (callback) {
+ methods.success = callback
+ return resXHR
+ },
+ error: function (callback) {
+ methods.error = callback
+ return resXHR
+ },
+ always: function (callback) {
+ methods.always = callback
+ return resXHR
+ }
+ }
+ return resXHR
+}
+sgRequest.get = function (src) {
+ return xhr('GET', src)
+}
+sgRequest.put = function (url, data) {
+ return xhr('PUT', url, data)
+}
+sgRequest.post = function (url, data) {
+ return xhr('POST', url, data)
+}
+sgRequest['delete'] = function (url) {
+ return xhr('DELETE', url)
+}
+sgRequest.setContentType = function (value) {
+ config.contentType = value
+}
+sgRequest.setRequestHeader = function (key, value) {
+ config.headers.push({ key: key, value: value })
+}
+
+
diff --git a/src/sugoio/DomTracker.js b/src/sugoio/DomTracker.js
new file mode 100644
index 0000000..a9f5567
--- /dev/null
+++ b/src/sugoio/DomTracker.js
@@ -0,0 +1,96 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-17
+ */
+var _ = require('sugo-sdk-js-utils')['default']
+var Logger = require('../logger').get('DOMTracker')
+
+/**
+ * @constructor
+ */
+function DomTracker () {
+ this.mp = null
+}
+
+// interface
+DomTracker.prototype.create_properties = function () {}
+DomTracker.prototype.event_handler = function () {}
+DomTracker.prototype.after_track_handler = function () {}
+
+DomTracker.prototype.init = function (sugoio_instance) {
+ this.mp = sugoio_instance
+ return this
+}
+
+/**
+ * @param {Object|string} query
+ * @param {string} event_name
+ * @param {Object=} properties
+ * @param {function(...[*])=} user_callback
+ */
+DomTracker.prototype.track = function (query, event_name, properties, user_callback) {
+ var that = this
+ var elements = _.querySelectorAll(query)
+
+ if (elements.length === 0) {
+ Logger.error('The DOM query (' + query + ') returned 0 elements')
+ return
+ }
+ _.each(elements, function (element) {
+ _.register_event(element, this.override_event, function (e) {
+ var options = {}
+ var props = that.create_properties(properties, this)
+ var timeout = that.mp.get_config('track_links_timeout')
+
+ that.event_handler(e, this, options)
+
+ // in case the sugoio servers don't get back to us in time
+ window.setTimeout(that.track_callback(user_callback, props, options, true), timeout)
+
+ // fire the tracking event
+ that.mp.track(event_name, props, that.track_callback(user_callback, props, options))
+ })
+ }, this)
+
+ return true
+}
+
+/**
+ * @param {function(...[*])} user_callback
+ * @param {Object} props
+ * @param {object} options
+ * @param {boolean} [timeout_occured]
+ */
+DomTracker.prototype.track_callback = function (user_callback, props, options, timeout_occured) {
+ timeout_occured = timeout_occured || false
+ var that = this
+
+ return function () {
+ // options is referenced from both callbacks, so we can have
+ // a 'lock' of sorts to ensure only one fires
+ if (options.callback_fired) { return }
+ options.callback_fired = true
+
+ if (user_callback && user_callback(timeout_occured, props) === false) {
+ // user can prevent the default functionality by
+ // returning false from their callback
+ return
+ }
+
+ that.after_track_handler(props, options, timeout_occured)
+ }
+}
+
+DomTracker.prototype.create_properties = function (properties, element) {
+ var props
+
+ if (typeof(properties) === 'function') {
+ props = properties(element)
+ } else {
+ props = _.extend({}, properties)
+ }
+
+ return props
+}
+
+module.exports = DomTracker
\ No newline at end of file
diff --git a/src/sugoio/FormTracker.js b/src/sugoio/FormTracker.js
new file mode 100644
index 0000000..30b1a96
--- /dev/null
+++ b/src/sugoio/FormTracker.js
@@ -0,0 +1,30 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-17
+ */
+
+var DomTracker = require('./DomTracker')
+var _ = require('sugo-sdk-js-utils')['default']
+
+/**
+ * FormTracker Object
+ * @varructor
+ * @extends DomTracker
+ */
+var FormTracker = function () {
+ this.override_event = 'submit'
+}
+_.inherit(FormTracker, DomTracker)
+
+FormTracker.prototype.event_handler = function (evt, element, options) {
+ options.element = element
+ evt.preventDefault()
+}
+
+FormTracker.prototype.after_track_handler = function (props, options) {
+ setTimeout(function () {
+ options.element.submit()
+ }, 0)
+}
+
+module.exports = FormTracker
\ No newline at end of file
diff --git a/src/sugoio/LinkTracker.js b/src/sugoio/LinkTracker.js
new file mode 100644
index 0000000..60aee27
--- /dev/null
+++ b/src/sugoio/LinkTracker.js
@@ -0,0 +1,51 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-17
+ */
+
+var DomTracker = require('./DomTracker')
+var _ = require('sugo-sdk-js-utils')['default']
+
+
+/**
+ * LinkTracker Object
+ * @varructor
+ * @extends DomTracker
+ */
+function LinkTracker () {
+ this.override_event = 'click'
+}
+
+_.inherit(LinkTracker, DomTracker)
+
+LinkTracker.prototype.create_properties = function (properties, element) {
+ var props = LinkTracker.superclass.create_properties.apply(this, arguments)
+
+ if (element.href) { props.url = element.href }
+
+ return props
+}
+
+LinkTracker.prototype.event_handler = function (evt, element, options) {
+ options.new_tab = (
+ evt.which === 2 ||
+ evt.metaKey ||
+ evt.ctrlKey ||
+ element.target === '_blank'
+ )
+ options.href = element.href
+
+ if (!options.new_tab) {
+ evt.preventDefault()
+ }
+}
+
+LinkTracker.prototype.after_track_handler = function (props, options) {
+ if (options.new_tab) { return }
+
+ setTimeout(function () {
+ window.location = options.href
+ }, 0)
+}
+
+module.exports = LinkTracker
\ No newline at end of file
diff --git a/src/sugoio/People.js b/src/sugoio/People.js
new file mode 100644
index 0000000..a62c22a
--- /dev/null
+++ b/src/sugoio/People.js
@@ -0,0 +1,495 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-18
+ */
+
+var _ = require('sugo-sdk-js-utils')['default']
+var CONSTANTS = require('../../constants')
+var Logger = require('../logger').get('People')
+
+function People () {
+ this._sugoio = null
+}
+
+People.prototype._init = function (sugoio_instance) {
+ this._sugoio = sugoio_instance
+}
+
+/**
+ * Set properties on a user record.
+ *
+ * ### Usage:
+ *
+ * sugoio.people.set('gender', 'm');
+ *
+ * // or set multiple properties at once
+ * sugoio.people.set({
+ * 'Company': 'Acme',
+ * 'Plan': 'Premium',
+ * 'Upgrade date': new Date()
+ * });
+ * // properties can be strings, integers, dates, or lists
+ *
+ * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
+ * @param {*} [to] A value to set on the given property name
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
+ */
+People.prototype.set = function (prop, to, callback) {
+ var data = {}
+ var $set = {}
+ if (_.isObject(prop)) {
+ _.each(prop, function (v, k) {
+ if (!this._is_reserved_property(k)) {
+ $set[k] = v
+ }
+ }, this)
+ callback = to
+ } else {
+ $set[prop] = to
+ }
+
+ // make sure that the referrer info has been updated and saved
+ if (this._get_config('save_referrer')) {
+ this._sugoio.persistence.update_referrer_info(document.referrer)
+ }
+
+ // update $set object with default people properties
+ $set = _.extend(
+ {},
+ _.info.people_properties(),
+ this._sugoio.persistence.get_referrer_info(),
+ $set
+ )
+
+ data[CONSTANTS.SET_ACTION] = $set
+
+ return this._send_request(data, callback)
+}
+
+/**
+ * Set properties on a user record, only if they do not yet exist.
+ * This will not overwrite previous people property values, unlike
+ * people.set().
+ *
+ * ### Usage:
+ *
+ * sugoio.people.set_once('First Login Date', new Date());
+ *
+ * // or set multiple properties at once
+ * sugoio.people.set_once({
+ * 'First Login Date': new Date(),
+ * 'Starting Plan': 'Premium'
+ * });
+ *
+ * // properties can be strings, integers or dates
+ *
+ * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
+ * @param {*} [to] A value to set on the given property name
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
+ */
+People.prototype.set_once = function (prop, to, callback) {
+ var data = {}
+ var $set_once = {}
+ if (_.isObject(prop)) {
+ _.each(prop, function (v, k) {
+ if (!this._is_reserved_property(k)) {
+ $set_once[k] = v
+ }
+ }, this)
+ callback = to
+ } else {
+ $set_once[prop] = to
+ }
+ data[CONSTANTS.SET_ONCE_ACTION] = $set_once
+ return this._send_request(data, callback)
+}
+
+/**
+ * Increment/decrement numeric people analytics properties.
+ *
+ * ### Usage:
+ *
+ * sugoio.people.increment('page_views', 1);
+ *
+ * // or, for convenience, if you're just incrementing a counter by
+ * // 1, you can simply do
+ * sugoio.people.increment('page_views');
+ *
+ * // to decrement a counter, pass a negative number
+ * sugoio.people.increment('credits_left', -1);
+ *
+ * // like sugoio.people.set(), you can increment multiple
+ * // properties at once:
+ * sugoio.people.increment({
+ * counter1: 1,
+ * counter2: 6
+ * });
+ *
+ * @param {object|string} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values.
+ * @param {number|function} [by] An amount to increment the given property
+ * @param {function} [callback] If provided, the callback will be called after the tracking event
+ */
+People.prototype.increment = function (prop, by, callback) {
+ var data = {}
+ var $add = {}
+ if (_.isObject(prop)) {
+ _.each(prop, function (v, k) {
+ if (!this._is_reserved_property(k)) {
+ if (isNaN(parseFloat(v))) {
+ return Logger.error('Invalid increment value passed to sugoio.people.increment - must be a number')
+ } else {
+ $add[k] = v
+ }
+ }
+ }, this)
+ callback = by
+ } else {
+ // convenience: sugoio.people.increment('property'); will
+ // increment 'property' by 1
+ if (_.isUndefined(by)) {
+ by = 1
+ }
+ $add[prop] = by
+ }
+ data[CONSTANTS.ADD_ACTION] = $add
+
+ return this._send_request(data, callback)
+}
+
+/**
+ * Append a value to a list-valued people analytics property.
+ *
+ * ### Usage:
+ *
+ * // append a value to a list, creating it if needed
+ * sugoio.people.append('pages_visited', 'homepage');
+ *
+ * // like sugoio.people.set(), you can append multiple
+ * // properties at once:
+ * sugoio.people.append({
+ * list1: 'bob',
+ * list2: 123
+ * });
+ *
+ * @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
+ * @param {*} [value] An item to append to the list
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
+ */
+People.prototype.append = function (list_name, value, callback) {
+ var data = {}
+ var $append = {}
+ if (_.isObject(list_name)) {
+ _.each(list_name, function (v, k) {
+ if (!this._is_reserved_property(k)) {
+ $append[k] = v
+ }
+ }, this)
+ callback = value
+ } else {
+ $append[list_name] = value
+ }
+ data[CONSTANTS.APPEND_ACTION] = $append
+
+ return this._send_request(data, callback)
+}
+
+/**
+ * Merge a given list with a list-valued people analytics property,
+ * excluding duplicate values.
+ *
+ * ### Usage:
+ *
+ * // merge a value to a list, creating it if needed
+ * sugoio.people.union('pages_visited', 'homepage');
+ *
+ * // like sugoio.people.set(), you can append multiple
+ * // properties at once:
+ * sugoio.people.union({
+ * list1: 'bob',
+ * list2: 123
+ * });
+ *
+ * // like sugoio.people.append(), you can append multiple
+ * // values to the same list:
+ * sugoio.people.union({
+ * list1: ['bob', 'billy']
+ * });
+ *
+ * @param {object|string} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
+ * @param {*} [values] Value / values to merge with the given property
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
+ */
+People.prototype.union = function (list_name, values, callback) {
+ var data = {}
+ var $union = {}
+ if (_.isObject(list_name)) {
+ _.each(list_name, function (v, k) {
+ if (!this._is_reserved_property(k)) {
+ $union[k] = _.isArray(v) ? v : [v]
+ }
+ }, this)
+ callback = values
+ } else {
+ $union[list_name] = _.isArray(values) ? values : [values]
+ }
+ data[CONSTANTS.UNION_ACTION] = $union
+
+ return this._send_request(data, callback)
+}
+
+/**
+ * Record that you have charged the current user a certain amount
+ * of money. Charges recorded with track_charge() will appear in the
+ * Sugoio revenue report.
+ *
+ * ### Usage:
+ *
+ * // charge a user $50
+ * sugoio.people.track_charge(50);
+ *
+ * // charge a user $30.50 on the 2nd of january
+ * sugoio.people.track_charge(30.50, {
+ * 'time': new Date('jan 1 2012')
+ * });
+ *
+ * @param {Number} amount The amount of money charged to the current user
+ * @param {Object} [properties] An associative array of properties associated with the charge
+ * @param {Function} [callback] If provided, the callback will be called when the server responds
+ */
+People.prototype.track_charge = function (amount, properties, callback) {
+ if (!_.isNumber(amount)) {
+ amount = parseFloat(amount)
+ if (isNaN(amount)) {
+ Logger.error('Invalid value passed to sugoio.people.track_charge - must be a number')
+ return
+ }
+ }
+
+ return this.append('transactions', _.extend({ 'amount': amount }, properties), callback)
+}
+
+/**
+ * Permanently clear all revenue report transactions from the
+ * current user's people analytics profile.
+ *
+ * ### Usage:
+ *
+ * sugoio.people.clear_charges();
+ *
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
+ */
+People.prototype.clear_charges = function (callback) {
+ return this.set('transactions', [], callback)
+}
+
+/**
+ * Permanently deletes the current people analytics profile from
+ * Sugoio (using the current client_distinct_id).
+ *
+ * ### Usage:
+ *
+ * // remove the all data you have stored about the current user
+ * sugoio.people.delete_user();
+ *
+ */
+People.prototype.delete_user = function () {
+ if (!this._identify_called()) {
+ Logger.error('sugoio.people.delete_user() requires you to call identify() first')
+ return
+ }
+ var data = {}
+ data['delete'] = this._sugoio.get_distinct_id()
+ return this._send_request(data)
+}
+
+/**
+ * toString
+ * @return {string}
+ */
+People.prototype.toString = function () {
+ return this._sugoio.toString() + '.people'
+}
+
+/**
+ * @param {object} data
+ * @param {function} callback
+ * @return {object}
+ * @private
+ */
+People.prototype._send_request = function (data, callback) {
+ data.token = this._get_config('token')
+ data.distinct_id = this._sugoio.get_distinct_id()
+
+ var date_encoded_data = _.encodeDates(data)
+ var truncated_data = _.truncate(date_encoded_data, 255)
+ var json_data = _.JSONEncode(date_encoded_data)
+ var encoded_data = _.base64Encode(json_data)
+
+ if (!this._identify_called()) {
+ this._enqueue(data)
+ if (!_.isUndefined(callback)) {
+ if (this._get_config('verbose')) {
+ callback({ status: -1, error: null })
+ } else {
+ callback(-1)
+ }
+ }
+ return truncated_data
+ }
+
+ Logger.log('SUGOIO PEOPLE REQUEST:')
+ Logger.log(truncated_data)
+
+ this._sugoio._send_request(
+ this._get_config('api_host') + '/post',
+ {
+ 'data': true,
+ 'locate': this.get_config('project_id'),
+ 'token': this.get_config('token')
+ },
+ this._sugoio._prepare_callback(callback, truncated_data),
+ encoded_data
+ )
+
+ return truncated_data
+}
+
+/**
+ * @param conf_var
+ * @return {*}
+ * @private
+ */
+People.prototype._get_config = function (conf_var) {
+ return this._sugoio.get_config(conf_var)
+}
+
+/**
+ * @return {boolean}
+ * @private
+ */
+People.prototype._identify_called = function () {
+ return this._sugoio._flags.identify_called === true
+}
+
+/**
+ * Queue up engage operations if identify hasn't been called yet.
+ * @param {object} data
+ * @private
+ */
+People.prototype._enqueue = function (data) {
+ if (CONSTANTS.SET_ACTION in data) {
+ this._sugoio.persistence._add_to_people_queue(CONSTANTS.SET_ACTION, data)
+ } else if (CONSTANTS.SET_ONCE_ACTION in data) {
+ this._sugoio.persistence._add_to_people_queue(CONSTANTS.SET_ONCE_ACTION, data)
+ } else if (CONSTANTS.ADD_ACTION in data) {
+ this._sugoio.persistence._add_to_people_queue(CONSTANTS.ADD_ACTION, data)
+ } else if (CONSTANTS.APPEND_ACTION in data) {
+ this._sugoio.persistence._add_to_people_queue(CONSTANTS.APPEND_ACTION, data)
+ } else if (CONSTANTS.UNION_ACTION in data) {
+ this._sugoio.persistence._add_to_people_queue(CONSTANTS.UNION_ACTION, data)
+ } else {
+ Logger.error('Invalid call to _enqueue():', data)
+ }
+}
+
+/**
+ * Flush queued engage operations - order does not matter,
+ * and there are network level race conditions anyway
+ * @param _set_callback
+ * @param _add_callback
+ * @param _append_callback
+ * @param _set_once_callback
+ * @param _union_callback
+ * @private
+ */
+People.prototype._flush = function (_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback) {
+ var _this = this
+ var $set_queue = _.extend({}, this._sugoio.persistence._get_queue(CONSTANTS.SET_ACTION))
+ var $set_once_queue = _.extend({}, this._sugoio.persistence._get_queue(CONSTANTS.SET_ONCE_ACTION))
+ var $add_queue = _.extend({}, this._sugoio.persistence._get_queue(CONSTANTS.ADD_ACTION))
+ var $append_queue = this._sugoio.persistence._get_queue(CONSTANTS.APPEND_ACTION)
+ var $union_queue = _.extend({}, this._sugoio.persistence._get_queue(CONSTANTS.UNION_ACTION))
+
+ if (!_.isUndefined($set_queue) && _.isObject($set_queue) && !_.isEmptyObject($set_queue)) {
+ _this._sugoio.persistence._pop_from_people_queue(CONSTANTS.SET_ACTION, $set_queue)
+ this.set($set_queue, function (response, data) {
+ // on bad response, we want to add it back to the queue
+ if (response === 0) {
+ _this._sugoio.persistence._add_to_people_queue(CONSTANTS.SET_ACTION, $set_queue)
+ }
+ if (!_.isUndefined(_set_callback)) {
+ _set_callback(response, data)
+ }
+ })
+ }
+
+ if (!_.isUndefined($set_once_queue) && _.isObject($set_once_queue) && !_.isEmptyObject($set_once_queue)) {
+ _this._sugoio.persistence._pop_from_people_queue(CONSTANTS.SET_ONCE_ACTION, $set_once_queue)
+ this.set_once($set_once_queue, function (response, data) {
+ // on bad response, we want to add it back to the queue
+ if (response === 0) {
+ _this._sugoio.persistence._add_to_people_queue(CONSTANTS.SET_ONCE_ACTION, $set_once_queue)
+ }
+ if (!_.isUndefined(_set_once_callback)) {
+ _set_once_callback(response, data)
+ }
+ })
+ }
+
+ if (!_.isUndefined($add_queue) && _.isObject($add_queue) && !_.isEmptyObject($add_queue)) {
+ _this._sugoio.persistence._pop_from_people_queue(CONSTANTS.ADD_ACTION, $add_queue)
+ this.increment($add_queue, function (response, data) {
+ // on bad response, we want to add it back to the queue
+ if (response === 0) {
+ _this._sugoio.persistence._add_to_people_queue(CONSTANTS.ADD_ACTION, $add_queue)
+ }
+ if (!_.isUndefined(_add_callback)) {
+ _add_callback(response, data)
+ }
+ })
+ }
+
+ if (!_.isUndefined($union_queue) && _.isObject($union_queue) && !_.isEmptyObject($union_queue)) {
+ _this._sugoio.persistence._pop_from_people_queue(CONSTANTS.UNION_ACTION, $union_queue)
+ this.union($union_queue, function (response, data) {
+ // on bad response, we want to add it back to the queue
+ if (response === 0) {
+ _this._sugoio.persistence._add_to_people_queue(CONSTANTS.UNION_ACTION, $union_queue)
+ }
+ if (!_.isUndefined(_union_callback)) {
+ _union_callback(response, data)
+ }
+ })
+ }
+
+ // we have to fire off each $append individually since there is
+ // no concat method server side
+ if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
+ var $append_item
+ var callback = function (response, data) {
+ if (response === 0) {
+ _this._sugoio.persistence._add_to_people_queue(CONSTANTS.APPEND_ACTION, $append_item)
+ }
+ if (!_.isUndefined(_append_callback)) {
+ _append_callback(response, data)
+ }
+ }
+ for (var i = $append_queue.length - 1; i >= 0; i--) {
+ $append_item = $append_queue.pop()
+ _this.append($append_item, callback)
+ }
+ // Save the shortened append queue
+ _this._sugoio.persistence.save()
+ }
+}
+
+/**
+ * @param {*} prop
+ * @return {boolean}
+ * @private
+ */
+People.prototype._is_reserved_property = function (prop) {
+ return prop === 'distinct_id' || prop === 'token'
+}
+
+module.exports = People
diff --git a/src/sugoio/Persistence.js b/src/sugoio/Persistence.js
new file mode 100644
index 0000000..b33d654
--- /dev/null
+++ b/src/sugoio/Persistence.js
@@ -0,0 +1,536 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-17
+ */
+
+var _ = require('sugo-sdk-js-utils')['default']
+var Config = require('../config')
+var CONSTANTS = require('../../constants')
+var Logger = require('../logger').get('Persistence')
+
+var RESERVED_PROPERTIES = [
+ CONSTANTS.SET_QUEUE_KEY,
+ CONSTANTS.SET_ONCE_QUEUE_KEY,
+ CONSTANTS.ADD_QUEUE_KEY,
+ CONSTANTS.APPEND_QUEUE_KEY,
+ CONSTANTS.UNION_QUEUE_KEY,
+ CONSTANTS.PEOPLE_DISTINCT_ID_KEY,
+ CONSTANTS.ALIAS_ID_KEY,
+ CONSTANTS.CAMPAIGN_IDS_KEY,
+ CONSTANTS.EVENT_TIMERS_KEY
+]
+
+/**
+ * 初始化时确定storage
+ * Sugoio Persistence Object
+ * @param {SugoIOConfigInterface} config
+ */
+function Persistence (config) {
+ this.props = {}
+ this.name = null
+ this.storage = null
+ this.disabled = false
+ this.campaign_params_saved = false
+ this.default_expiry = null
+ this.expire_days = null
+ this.cross_subdomain = false
+ this.secure = false
+
+ if (config.persistence_name) {
+ this.name = 'sg_' + config.persistence_name
+ } else {
+ this.name = 'sg_' + config.token + '_sugoio'
+ }
+
+ var storage_type = config.persistence
+ if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
+ Logger.error('Unknown persistence type ' + storage_type + '; falling back to cookie')
+ storage_type = config.persistence = 'cookie'
+ }
+
+ function localStorage_supported () {
+ var supported = true
+ try {
+ var key = '__sglssupport__', val = 'xyz'
+ _.localStorage.set(key, val)
+ if (_.localStorage.get(key) !== val) {
+ supported = false
+ }
+ _.localStorage.remove(key)
+ } catch (err) {
+ supported = false
+ }
+ if (!supported) {
+ Logger.warn('localStorage unsupported; falling back to cookie store')
+ }
+ return supported
+ }
+
+ if (storage_type === 'localStorage' && localStorage_supported()) {
+ this.storage = _.localStorage
+ } else {
+ this.storage = _.cookie
+ }
+
+ this.load()
+ this.update_config(config)
+ this.upgrade(config)
+ this.save()
+}
+
+/**
+ * 返回props上的所有字段,排除 RESERVED_PROPERTIES 中的 key
+ * @return {object}
+ */
+Persistence.prototype.properties = function () {
+ var p = {}
+ // Filter out reserved properties
+ _.each(this.props, function (v, k) {
+ if (_.include(RESERVED_PROPERTIES, k) || k.indexOf(CONSTANTS.PEOPLE_REAL_USER_ID_DIMENSION_PREFIX) === 0) {
+ return
+ }
+ p[k] = v
+ })
+ return p
+}
+
+/**
+ * 读取已存的数据
+ * @return {object}
+ */
+Persistence.prototype.load = function () {
+ if (this.disabled) { return this.props }
+
+ var entry = this.storage.parse(this.name)
+
+ if (entry) {
+ this.props = _.extend({}, entry)
+ }
+
+ return this.props
+}
+
+/**
+ * 更新属性
+ * @param {SugoIOConfigInterface} config
+ * @return {Persistence}
+ */
+Persistence.prototype.upgrade = function (config) {
+ var upgrade_from_old_lib = config.upgrade, old_cookie_name, old_cookie
+
+ if (upgrade_from_old_lib) {
+ old_cookie_name = 'sg_super_properties'
+ // Case where they had a custom cookie name before.
+ if (typeof(upgrade_from_old_lib) === 'string') {
+ old_cookie_name = upgrade_from_old_lib
+ }
+
+ old_cookie = this.storage.parse(old_cookie_name)
+
+ // remove the cookie
+ this.storage.remove(old_cookie_name)
+ this.storage.remove(old_cookie_name, true)
+
+ if (old_cookie) {
+ this.props = _.extend(
+ this.props,
+ old_cookie.all,
+ old_cookie.events
+ )
+ }
+ }
+
+ if (!config.cookie_name && config.name !== CONSTANTS.PRIMARY_INSTANCE_NAME) {
+ // special case to handle people with cookies of the form
+ // mp_TOKEN_INSTANCENAME from the first release of this library
+ old_cookie_name = 'sg_' + config.token + '_' + config.name
+ old_cookie = this.storage.parse(old_cookie_name)
+
+ if (old_cookie) {
+ this.storage.remove(old_cookie_name)
+ this.storage.remove(old_cookie_name, true)
+
+ // Save the prop values that were in the cookie from before -
+ // this should only happen once as we delete the old one.
+ this.register_once(old_cookie)
+ }
+ }
+
+ if (this.storage === _.localStorage) {
+ old_cookie = _.cookie.parse(this.name)
+
+ _.cookie.remove(this.name)
+ _.cookie.remove(this.name, true)
+
+ if (old_cookie) {
+ this.register_once(old_cookie)
+ }
+ }
+
+ return this
+}
+
+/**
+ * 将属性写入到持久化存储中
+ * @return {Persistence}
+ */
+Persistence.prototype.save = function () {
+ if (this.disabled) { return this}
+ this._expire_notification_campaigns()
+ this.storage.set(
+ this.name,
+ _.JSONEncode(this.props),
+ this.expire_days,
+ this.cross_subdomain,
+ this.secure
+ )
+ return this
+}
+
+/**
+ * 删除存储中的记录
+ * @return {Persistence}
+ */
+Persistence.prototype.remove = function () {
+ // remove both domain and subdomain cookies
+ this.storage.remove(this.name, false)
+ this.storage.remove(this.name, true)
+ return this
+}
+
+// removes the storage entry and deletes all loaded data
+// forced name for tests
+/**
+ * 删除存储中的记录,并清除props对象上的记录
+ * @return {Persistence}
+ */
+Persistence.prototype.clear = function () {
+ this.remove()
+ this.props = {}
+ return this
+}
+
+/**
+ * @param {Object} props
+ * @param {*=} default_value
+ * @param {number=} days
+ */
+Persistence.prototype.register_once = function (props, default_value, days) {
+ if (_.isObject(props)) {
+ if (typeof(default_value) === 'undefined') {
+ default_value = 'None'
+ }
+
+ this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days
+
+ _.each(props, function (val, prop) {
+ if (!this.props[prop] || this.props[prop] === default_value) {
+ this.props[prop] = val
+ }
+ }, this)
+
+ this.save()
+
+ return true
+ }
+ return false
+}
+
+/**
+ * @param {Object} props
+ * @param {number} [days]
+ * @return {boolean}
+ */
+Persistence.prototype.register = function (props, days) {
+ if (_.isObject(props)) {
+ this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days
+
+ _.extend(this.props, props)
+
+ this.save()
+
+ return true
+ }
+ return false
+}
+
+/**
+ * 清除单个记录
+ * @param prop
+ * @return {Persistence}
+ */
+Persistence.prototype.unregister = function (prop) {
+ if (prop in this.props) {
+ delete this.props[prop]
+ this.save()
+ }
+ return this
+}
+
+/**
+ * 检测属性是否过期,如果过期,删除记录
+ * @return {Persistence}
+ * @private
+ */
+Persistence.prototype._expire_notification_campaigns = function () {
+ var campaigns_shown = this.props[CONSTANTS.CAMPAIGN_IDS_KEY]
+ // 1 minute (Config.DEBUG) / 1 hour (PDXN)
+ var EXPIRY_TIME = Config.debug ? 60 * 1000 : 60 * 60 * 1000
+
+ if (!campaigns_shown) {
+ return this
+ }
+
+ for (var campaign_id in campaigns_shown) {
+ if (!campaigns_shown.hasOwnProperty(campaign_id)) continue
+ if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) {
+ delete campaigns_shown[campaign_id]
+ }
+ }
+
+ if (_.isEmptyObject(campaigns_shown)) {
+ delete this.props[CONSTANTS.CAMPAIGN_IDS_KEY]
+ }
+ return this
+}
+
+/**
+ * 写入有过期状态的属性
+ * TODO 该属性值由_.info.campaignParams函数提供,真是太TM扯淡了,业务和库高度耦合
+ * @return {Persistence}
+ */
+Persistence.prototype.update_campaign_params = function () {
+ if (!this.campaign_params_saved) {
+ this.register_once(_.info.campaignParams())
+ this.campaign_params_saved = true
+ }
+ return this
+}
+
+/**
+ * 解析页面referrer是否为搜索引擎,写入持久存储
+ * @param referrer
+ * @return {Persistence}
+ */
+Persistence.prototype.update_search_keyword = function (referrer) {
+ this.register(_.info.searchInfo(referrer))
+ return this
+}
+
+// EXPORTED METHOD, we test this directly.
+Persistence.prototype.update_referrer_info = function (referrer) {
+ // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
+ this.register_once({ 'referring_domain': _.info.referringDomain(referrer) || 'direct' }, '')
+}
+
+/**
+ * 解析referrer domain
+ * @return {object}
+ */
+Persistence.prototype.get_referrer_info = function () {
+ return _.strip_empty_properties({ 'referring_domain': this.props.referring_domain })
+}
+
+// safely fills the passed in object with stored properties,
+// does not override any properties defined in both
+// returns the passed in object
+/**
+ * 合并this.props与props并返回结果
+ * 只有props上没有的属性,才会从this.props取
+ * @param {object} props
+ * @return {object}
+ */
+Persistence.prototype.safe_merge = function (props) {
+ _.each(this.props, function (val, prop) {
+ if (!(prop in props)) {
+ props[prop] = val
+ }
+ })
+
+ return props
+}
+
+/**
+ * 设置属性
+ * @param config
+ * @return {Persistence}
+ */
+Persistence.prototype.update_config = function (config) {
+ this.default_expiry = this.expire_days = config.cookie_expiration
+ this.set_disabled(config.disable_persistence)
+ this.set_cross_subdomain(config.cross_subdomain_cookie)
+ this.set_secure(config.secure_cookie)
+ return this
+}
+
+/**
+ * 更新disabled属性,如果disable为true,删除记录
+ * @param {boolean} disabled
+ * @return {Persistence}
+ */
+Persistence.prototype.set_disabled = function (disabled) {
+ this.disabled = disabled
+ if (this.disabled) {
+ this.remove()
+ }
+ return this
+}
+
+/**
+ * 更新cross_subdomain,如果cross_subdomain与当前保存的不一致,则更新记录
+ * @param cross_subdomain
+ * @return {Persistence}
+ */
+Persistence.prototype.set_cross_subdomain = function (cross_subdomain) {
+ if (cross_subdomain !== this.cross_subdomain) {
+ this.cross_subdomain = cross_subdomain
+ this.remove()
+ this.save()
+ }
+ return this
+}
+
+/**
+ * 返回cross_subdomain
+ * @return {boolean}
+ */
+Persistence.prototype.get_cross_subdomain = function () {
+ return this.cross_subdomain
+}
+
+/**
+ * 更新secure,如果与之前的记录不同,更新记录
+ * @param {boolean} secure
+ * @return {Persistence}
+ */
+Persistence.prototype.set_secure = function (secure) {
+ if (secure !== this.secure) {
+ this.secure = !!secure
+ this.remove()
+ this.save()
+ }
+}
+
+/**
+ * TODO 一堆people相关的操作,目前好像没用上,待查
+ * @param queue
+ * @param data
+ * @private
+ */
+Persistence.prototype._add_to_people_queue = function (queue, data) {
+ var
+ q_key = this._get_queue_key(queue),
+ q_data = data[queue],
+ set_q = this._get_or_create_queue(CONSTANTS.SET_ACTION),
+ set_once_q = this._get_or_create_queue(CONSTANTS.SET_ONCE_ACTION),
+ add_q = this._get_or_create_queue(CONSTANTS.ADD_ACTION),
+ union_q = this._get_or_create_queue(CONSTANTS.UNION_ACTION),
+ append_q = this._get_or_create_queue(CONSTANTS.APPEND_ACTION, [])
+
+ if (q_key === CONSTANTS.SET_QUEUE_KEY) {
+ // Update the set queue - we can override any existing values
+ _.extend(set_q, q_data)
+ // if there was a pending increment, override it
+ // with the set.
+ this._pop_from_people_queue(CONSTANTS.ADD_ACTION, q_data)
+ // if there was a pending union, override it
+ // with the set.
+ this._pop_from_people_queue(CONSTANTS.UNION_ACTION, q_data)
+ } else if (q_key === CONSTANTS.SET_ONCE_QUEUE_KEY) {
+ // only queue the data if there is not already a set_once call for it.
+ _.each(q_data, function (v, k) {
+ if (!(k in set_once_q)) {
+ set_once_q[k] = v
+ }
+ })
+ } else if (q_key === CONSTANTS.ADD_QUEUE_KEY) {
+ _.each(q_data, function (v, k) {
+ // If it exists in the set queue, increment
+ // the value
+ if (k in set_q) {
+ set_q[k] += v
+ } else {
+ // If it doesn't exist, update the add
+ // queue
+ if (!(k in add_q)) {
+ add_q[k] = 0
+ }
+ add_q[k] += v
+ }
+ }, this)
+ } else if (q_key === CONSTANTS.UNION_QUEUE_KEY) {
+ _.each(q_data, function (v, k) {
+ if (_.isArray(v)) {
+ if (!(k in union_q)) {
+ union_q[k] = []
+ }
+ // We may send duplicates, the server will dedup them.
+ union_q[k] = union_q[k].concat(v)
+ }
+ })
+ } else if (q_key === CONSTANTS.APPEND_QUEUE_KEY) {
+ append_q.push(q_data)
+ }
+
+ Logger.log('SUGOIO PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):')
+ Logger.log(data)
+
+ this.save()
+}
+
+Persistence.prototype._pop_from_people_queue = function (queue, data) {
+ var q = this._get_queue(queue)
+ if (!_.isUndefined(q)) {
+ _.each(data, function (v, k) {
+ delete q[k]
+ }, this)
+
+ this.save()
+ }
+}
+
+Persistence.prototype._get_queue_key = function (queue) {
+ if (queue === CONSTANTS.SET_ACTION) {
+ return CONSTANTS.SET_QUEUE_KEY
+ } else if (queue === CONSTANTS.SET_ONCE_ACTION) {
+ return CONSTANTS.SET_ONCE_QUEUE_KEY
+ } else if (queue === CONSTANTS.ADD_ACTION) {
+ return CONSTANTS.ADD_QUEUE_KEY
+ } else if (queue === CONSTANTS.APPEND_ACTION) {
+ return CONSTANTS.APPEND_QUEUE_KEY
+ } else if (queue === CONSTANTS.UNION_ACTION) {
+ return CONSTANTS.UNION_QUEUE_KEY
+ } else {
+ Logger.error('Invalid queue:', queue)
+ }
+}
+
+Persistence.prototype._get_queue = function (queue) {
+ return this.props[this._get_queue_key(queue)]
+}
+
+Persistence.prototype._get_or_create_queue = function (queue, default_val) {
+ var key = this._get_queue_key(queue)
+ default_val = _.isUndefined(default_val) ? {} : default_val
+
+ return this.props[key] || (this.props[key] = default_val)
+}
+
+Persistence.prototype.set_event_timer = function (event_name, timestamp) {
+ var timers = this.props[CONSTANTS.EVENT_TIMERS_KEY] || {}
+ timers[event_name] = timestamp
+ this.props[CONSTANTS.EVENT_TIMERS_KEY] = timers
+ this.save()
+}
+
+Persistence.prototype.remove_event_timer = function (event_name) {
+ var timers = this.props[CONSTANTS.EVENT_TIMERS_KEY] || {}
+ var timestamp = timers[event_name]
+ if (!_.isUndefined(timestamp)) {
+ delete this.props[CONSTANTS.EVENT_TIMERS_KEY][event_name]
+ this.save()
+ }
+ return timestamp
+}
+
+module.exports = Persistence
diff --git a/src/sugoio/Sugoio.js b/src/sugoio/Sugoio.js
new file mode 100644
index 0000000..56635cf
--- /dev/null
+++ b/src/sugoio/Sugoio.js
@@ -0,0 +1,1133 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-18
+ */
+
+var Global = require('../global')
+var Config = require('../config')
+var _ = require('sugo-sdk-js-utils')['default']
+var CONSTANTS = require('../../constants')
+var Track = require('./Track')
+
+var DEFAULT_CONFIG = require('./default-config')
+var request = require('../request')
+var API = require('../sugo-event-api')
+var onDOMContentLoaded = require('./on-dom-content-loaded')
+
+/* ## Tracker */
+var LinkTracker = require('./LinkTracker')
+var FormTracker = require('./FormTracker')
+var People = require('./People')
+var Persistence = require('./Persistence')
+var Logger = require('../logger').get('Sugoio')
+
+var userAgent = navigator.userAgent
+// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
+// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials
+var USE_XHR = (window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest())
+
+// IE<10 does not support cross-origin XHR's but script tags
+// with defer won't block window.onload; ENQUEUE_REQUESTS
+// should only be true for Opera<12
+var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1)
+
+var DOM_LOADED = false
+onDOMContentLoaded(function () {
+ DOM_LOADED = true
+})
+
+function Sugoio () {
+ this.name = null
+ this.people = null
+ this.proxy = null
+ this.config = null
+ this.persistence = null
+ this.cookie = null
+
+ this.__autotrack_enabled = null
+ this.__loaded = null
+ this._jsc = null
+ this.__dom_loaded_queue = null
+ this.__request_queue = null
+ this.__disabled_events = null
+ this._flags = null
+}
+
+/**
+ * create_sugoio(token:string, config:object, name:string)
+ *
+ * This function is used by the init method of Sugoio objects
+ * as well as the main initializer at the end of the JSLib (that
+ * initializes document.sugoio as well as any additional instances
+ * declared before this file has loaded).
+ *
+ * @param {string} token
+ * @param {object} config
+ * @param {string} name
+ * @return {Sugoio}
+ */
+Sugoio.create_sugoio = function (token, config, name) {
+ var master = Global.get()
+ var instance
+ var target = (name === CONSTANTS.PRIMARY_INSTANCE_NAME) ? master : master[name]
+
+ // TODO 以是否为Array来判断太草率了些,改为属性值
+ if (target && !_.isArray(target)) {
+ Logger.error('You have already initialized ' + name)
+ return target
+ }
+
+ instance = new Sugoio()
+ instance._init(token, config, name)
+ instance.people = new People()
+ instance.people._init(instance)
+ instance.__autotrack_enabled = instance.get_config('autotrack')
+
+ // if any instance on the page has debug = true, we set the
+ // global debug to be true
+ Config.debug = Config.debug || instance.get_config('debug')
+
+ var dimensions = instance.get_config('dimensions')
+ if (dimensions) {
+ Config.dimensions = dimensions
+ }
+
+ if (instance.get_config('autotrack')) {
+ var num_buckets = 100
+ var num_enabled_buckets = 100
+ if (!Track.track.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
+ instance.__autotrack_enabled = false
+ Logger.error('Not in active bucket: disabling Automatic Event Collection.')
+ } else if (!Track.track.isBrowserSupported()) {
+ instance.__autotrack_enabled = false
+ Logger.error('Disabling Automatic Event Collection because this browser is not supported')
+ } else {
+ Track.track.init(instance)
+ }
+ }
+
+ // if target is not defined, we called init after the lib already
+ // loaded, so there won't be an array of things to execute
+ if (!_.isUndefined(target) && _.isArray(target)) {
+ // Crunch through the people queue first - we queue this data up &
+ // flush on identify, so it's better to do all these operations first
+ instance._execute_array.call(instance.people, target.people)
+ instance._execute_array(target)
+ }
+
+ instance.proxy = target.proxy
+
+ return instance
+}
+
+Sugoio.prototype.version = process.env.SDK_VERSION
+
+/**
+ * 该方法在entry中会被覆写
+ * @param {string} token
+ * @param {object} config
+ * @param {string} name
+ * @return {Sugoio}
+ */
+Sugoio.prototype.init = function (token, config, name) {
+ var master = Global.get()
+ if (_.isUndefined(name)) {
+ Logger.error('You must name your new library: init(token, config, name)')
+ return null
+ }
+
+ if (name === CONSTANTS.PRIMARY_INSTANCE_NAME) {
+ Logger.error('You must initialize the main sugoio object right after you include the Sugoio js snippet')
+ return null
+ }
+
+ var instance = Sugoio.create_sugoio(token, config, name)
+ master[name] = instance
+ instance._loaded()
+
+ return instance
+}
+
+/* ## Initialization methods */
+
+/**
+ * This function initializes a new instance of the Sugoio tracking object.
+ * All new instances are added to the main sugoio object as sub properties (such as
+ * sugoio.library_name) and also returned by this function. To define a
+ * second instance on the page, you would call:
+ *
+ * sugoio.init('new token', { your: 'config' }, 'library_name');
+ *
+ * and use it like so:
+ *
+ * sugoio.library_name.track(...);
+ *
+ * @param {String} token Your Sugoio API token
+ * @param {Object} [config] A dictionary of config options to override
+ * @param {String} [name] The name for the new sugoio instance that you want created
+ */
+
+// sugoio._init(token:string, config:object, name:string)
+//
+// This function sets up the current instance of the sugoio
+// library. The difference between this method and the init(...)
+// method is this one initializes the actual instance, whereas the
+// init(...) method sets up a new library and calls _init on it.
+//
+Sugoio.prototype._init = function (token, config, name) {
+ this.__loaded = true
+ this.name = name
+ this.config = {}
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
+ name: name,
+ token: token,
+ projectId: config.project_id,
+ cdn: config.app_host,
+ 'callback_fn': ((name === CONSTANTS.PRIMARY_INSTANCE_NAME) ? name : CONSTANTS.PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
+ }))
+
+ this._jsc = function () {}
+
+ this.__dom_loaded_queue = []
+ this.__request_queue = []
+ this.__disabled_events = []
+ this._flags = {
+ disable_all_events: false,
+ identify_called: false
+ }
+
+ this.persistence = this.cookie = new Persistence(this.config)
+ this.register_once({ distinct_id: _.UUID() }, '')
+}
+
+/* ## Private methods */
+
+Sugoio.prototype._loaded = function () {
+ this.get_config('loaded')(this)
+
+ // this happens after so a user can call identify/name_tag in
+ // the loaded callback
+ if (this.get_config('track_pageview')) {
+ this.track_pageview()
+ }
+}
+
+Sugoio.prototype._dom_loaded = function () {
+ _.each(this.__dom_loaded_queue, function (item) {
+ this._track_dom.apply(this, item)
+ }, this)
+ _.each(this.__request_queue, function (item) {
+ this._send_request.apply(this, item)
+ }, this)
+ delete this.__dom_loaded_queue
+ delete this.__request_queue
+}
+
+/**
+ * @param {DomTracker} DomClass
+ * @param args
+ * @return {*}
+ * @private
+ */
+Sugoio.prototype._track_dom = function (DomClass, args) {
+ if (this.get_config('img')) {
+ Logger.error('You can\'t use DOM tracking functions with img = true.')
+ return false
+ }
+
+ if (!DOM_LOADED) {
+ this.__dom_loaded_queue.push([DomClass, args])
+ return false
+ }
+
+ var dt = new DomClass().init(this)
+ return dt.track.apply(dt, args)
+}
+
+/**
+ * _prepare_callback() should be called by callers of _send_request for use
+ * as the callback argument.
+ *
+ * If there is no callback, this returns null.
+ * If we are going to make XHR/XDR requests, this returns a function.
+ * If we are going to use script tags, this returns a string to use as the
+ * callback GET param.
+ */
+Sugoio.prototype._prepare_callback = function (callback, data) {
+ if (_.isUndefined(callback)) {
+ return null
+ }
+
+ if (USE_XHR) {
+ return function (response) {
+ callback(response, data)
+ }
+ } else {
+ // if the user gives us a callback, we store as a random
+ // property on this instances jsc function and update our
+ // callback string to reflect that.
+ var jsc = this._jsc
+ var randomized_cb = '' + Math.floor(Math.random() * 100000000)
+ var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'
+ jsc[randomized_cb] = function (response) {
+ delete jsc[randomized_cb]
+ callback(response, data)
+ }
+ return callback_string
+ }
+}
+
+Sugoio.prototype._send_request = function (url, data, callback, encoded_data) {
+ if (ENQUEUE_REQUESTS) {
+ this.__request_queue.push(arguments)
+ return
+ }
+
+ // needed to correctly format responses
+ var verbose_mode = this.get_config('verbose')
+ if (data.verbose) { verbose_mode = true }
+ if (this.get_config('test')) { data.test = 1 }
+ if (verbose_mode) { data.verbose = 1 }
+ if (this.get_config('img')) { data.img = 1 }
+ if (!USE_XHR) {
+ if (callback) {
+ data.callback = callback
+ } else if (verbose_mode || this.get_config('test')) {
+ // Verbose output (from verbose mode, or an error in test mode) is a json blob,
+ // which by itself is not valid javascript. Without a callback, this verbose output will
+ // cause an error when returned via jsonp, so we force a no-op callback param.
+ // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4
+ data.callback = '(function(){})'
+ }
+ }
+
+ data.ip = this.get_config('ip') ? 1 : 0
+ data._ = new Date().getTime().toString()
+ url += '?' + _.HTTPBuildQuery(data)
+
+ if ('img' in data) {
+ var img = document.createElement('img')
+ img.src = url
+ document.body.appendChild(img)
+ } else if (USE_XHR && !data.data) {
+ try {
+ var req = new XMLHttpRequest()
+ req.open('GET', url, true)
+ // send the mp_optout cookie
+ // withCredentials cannot be modified until after calling .open on Android and Mobile Safari
+ req.withCredentials = false
+ req.onreadystatechange = function () {
+ if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4
+ if (url.indexOf('api.sugoio.com/track') !== -1) {
+ Track.track.checkForBackoff(req)
+ }
+ if (req.status === 200) {
+ if (callback) {
+ if (verbose_mode) {
+ callback(_.JSONDecode(req.responseText))
+ } else {
+ callback(Number(req.responseText))
+ }
+ }
+ } else {
+ var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText
+ Logger.error(error)
+ if (callback) {
+ if (verbose_mode) {
+ callback({ status: 0, error: error })
+ } else {
+ callback(0)
+ }
+ }
+ }
+ }
+ }
+ req.send(null)
+ } catch (e) {
+ Logger.error(e)
+ }
+ } else if (USE_XHR && data.data) { // post上报信息到网关
+ try {
+
+ var reqPost = new XMLHttpRequest()
+ reqPost.open('POST', url, true)
+ // send the mp_optout cookie
+ // withCredentials cannot be modified until after calling .open on Android and Mobile Safari
+ reqPost.withCredentials = false
+ // reqPost.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ reqPost.onreadystatechange = function () {
+ if (reqPost.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4
+ if (url.indexOf('api.sugoio.com/track') !== -1) {
+ Track.track.checkForBackoff(reqPost)
+ }
+ if (reqPost.status === 200) {
+ if (callback) {
+ if (verbose_mode) {
+ callback(_.JSONDecode(reqPost.responseText))
+ } else {
+ callback(Number(reqPost.responseText))
+ }
+ }
+ } else {
+ var error = 'Bad HTTP status: ' + reqPost.status + ' ' + reqPost.statusText
+ Logger.error(error)
+ if (callback) {
+ if (verbose_mode) {
+ callback({ status: 0, error: error })
+ } else {
+ callback(0)
+ }
+ }
+ }
+ }
+ }
+ reqPost.send(encoded_data)
+ } catch (e) {
+ Logger.error(e)
+ }
+ } else {
+ var script = document.createElement('script')
+ script.type = 'text/javascript'
+ script.async = true
+ script.defer = true
+ script.src = url
+ var s = document.getElementsByTagName('script')[0]
+ s.parentNode.insertBefore(script, s)
+ }
+}
+
+/**
+ * _execute_array() deals with processing any sugoio function
+ * calls that were called before the Sugoio library were loaded
+ * (and are thus stored in an array so they can be called later)
+ *
+ * Note: we fire off all the sugoio function calls && user defined
+ * functions BEFORE we fire off sugoio tracking calls. This is so
+ * identify/register/set_config calls can properly modify early
+ * tracking calls.
+ *
+ * @param {Array} array
+ */
+Sugoio.prototype._execute_array = function (array) {
+ var fn_name, alias_calls = [], other_calls = [], tracking_calls = []
+ _.each(array, function (item) {
+ if (item) {
+ fn_name = item[0]
+ if (typeof(item) === 'function') {
+ item.call(this)
+ } else if (_.isArray(item) && fn_name === 'alias') {
+ alias_calls.push(item)
+ } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') {
+ tracking_calls.push(item)
+ } else {
+ other_calls.push(item)
+ }
+ }
+ }, this)
+
+ var execute = function (calls, context) {
+ _.each(calls, function (item) {
+ this[item[0]].apply(this, item.slice(1))
+ }, context)
+ }
+
+ execute(alias_calls, this)
+ execute(other_calls, this)
+ execute(tracking_calls, this)
+}
+
+/**
+ * push() keeps the standard async-array-push
+ * behavior around after the lib is loaded.
+ * This is only useful for external integrations that
+ * do not wish to rely on our convenience methods
+ * (created in the snippet).
+ *
+ * ### Usage:
+ * sugoio.push(['register', { a: 'b' }]);
+ *
+ * @param {Array} item A [function_name, args...] array to be executed
+ */
+Sugoio.prototype.push = function (item) {
+ this._execute_array([item])
+}
+
+/**
+ * Disable events on the Sugoio object. If passed no arguments,
+ * this function disables tracking of any event. If passed an
+ * array of event names, those events will be disabled, but other
+ * events will continue to be tracked.
+ *
+ * Note: this function does not stop other sugoio functions from
+ * firing, such as register() or people.set().
+ *
+ * @param {Array} [events] An array of event names to disable
+ */
+Sugoio.prototype.disable = function (events) {
+ if (typeof(events) === 'undefined') {
+ this._flags.disable_all_events = true
+ } else {
+ this.__disabled_events = this.__disabled_events.concat(events)
+ }
+}
+
+/**
+ * Track an event. This is the most important and
+ * frequently used Sugoio function.
+ *
+ * ### Usage:
+ *
+ * // track an event named 'Registered'
+ * sugoio.track('Registered', {'Gender': 'Male', 'Age': 21});
+ *
+ * To track link clicks or form submissions, see track_links() or track_forms().
+ *
+ * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
+ * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself.
+ * @param {Function} [callback] If provided, the callback function will be called after tracking the event.
+ */
+Sugoio.prototype.track = function (event_name, properties, callback) {
+ var master = Global.get()
+ if (!_.isFunction(callback)) {
+ callback = _.noop
+ }
+
+ if (_.isUndefined(event_name)) {
+ Logger.error('No event name provided to sugoio.track')
+ return
+ }
+
+ if (this._event_is_disabled(event_name)) {
+ callback(0)
+ return
+ }
+
+ // set defaults
+ properties = properties || {}
+ properties.token = this.get_config('token')
+
+ // set duration if time_event was previously called for this event
+ var start_timestamp = this.persistence.remove_event_timer(event_name)
+ if (!_.isUndefined(start_timestamp)) {
+ var duration_in_ms = new Date().getTime() - start_timestamp
+ properties.duration = parseFloat((duration_in_ms / 1000).toFixed(2))
+ properties.event_type = 'duration' //增加停留事件类型
+ properties.host = location.host
+ }
+
+ // update persistence
+ this.persistence.update_search_keyword(document.referrer)
+
+ if (this.get_config('store_google')) {
+ this.persistence.update_campaign_params()
+ }
+
+ if (this.get_config('save_referrer')) {
+ this.persistence.update_referrer_info(document.referrer)
+ }
+
+ // note: extend writes to the first object, so lets make sure we
+ // don't write to the persistence properties object and info
+ // properties object by passing in a new object
+ // update properties with pageview info and super-properties
+ const enable_hash = this.get_config('enable_hash') || false
+ const hash = enable_hash ? location.hash : ''
+ properties = _.extend(
+ {
+ event_type: 'click', // default event_type
+ path_name: location.pathname + hash,
+ page_category: master.page_category
+ },
+ _.info.properties(),
+ // referrer may be in cookie
+ _.omit(this.persistence.properties(), ['referrer']),
+ { sdk_version: this.version || Global.Global.version },
+ properties
+ )
+
+ var realPeople = this._getRealPeopleJson()
+ properties = _.extend({}, properties, realPeople)
+
+ var property_blacklist = this.get_config('property_blacklist')
+ if (_.isArray(property_blacklist)) {
+ _.each(property_blacklist, function (blacklisted_prop) {
+ delete properties[blacklisted_prop]
+ })
+ } else {
+ Logger.error('Invalid value for property_blacklist config: ' + property_blacklist)
+ }
+
+ var encode_type = this.get_config('encode_type')
+ var isJSON = encode_type && encode_type === 'json'
+ var data,
+ serverDimensions = Global.take('serverDimensions'),
+ dimensions = this.get_config('dimensions')
+
+ var defaultProps = { 'event_name': event_name }
+ if (properties.event_id) {
+ defaultProps.event_id = properties.event_id
+ }
+
+ // 如果代码埋点设置了page_name则不覆盖,以代码埋点优先
+ if (!properties.page_name) {
+ // 设置自定义页面名称
+ // 默认当前页面title
+ var pageInfo = Global.take('pageInfo')
+ if (pageInfo && pageInfo.page_name) {
+ properties.page_name = pageInfo.page_name
+ } else {
+ properties.page_name = document.title
+ }
+ }
+
+ var session_id = this.get_property('session_id')
+ if (!session_id) {
+ session_id = _.shortUUID()
+ var expire_days = 1 //session_id expire_days
+ this.register_once({ 'session_id': session_id }, '', expire_days)
+ }
+ properties.session_id = session_id
+
+ if (isJSON) { //json数据包需要数值包裹
+ data = [_.extend(defaultProps, properties)]
+ } else {
+ data = _.extend(defaultProps, properties)
+ }
+ var truncated_data = _.truncate(data, 255)
+ var json_data = isJSON
+ ? _.JSONEncode(truncated_data)
+ : _.PlainEncode(truncated_data, dimensions, serverDimensions)
+
+ if (!json_data) {
+ Logger.error('Empty content. event name is => '.concat(event_name))
+ Logger.log('Please check the length of dimensions that from server.')
+ }
+
+ var encoded_data = _.base64Encode([json_data])
+ Logger.log('json_data:', json_data)
+ Logger.log('SUGOIO REQUEST: %o', truncated_data)
+
+ var uri = this.get_config('api_host')
+ + '/post?'
+ + 'locate=' + this.get_config('project_id')
+ + '&token=' + this.get_config('token')
+
+ request().send('POST', uri, encoded_data).success(function () {
+ callback(truncated_data)
+ })
+
+ return truncated_data
+}
+
+/**
+ * Track a page view event, which is currently ignored by the server.
+ * This function is called by default on page load unless the
+ * track_pageview configuration variable is false.
+ *
+ * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
+ * @api private
+ */
+Sugoio.prototype.track_pageview = function (page) {
+ const enable_hash = this.get_config('enable_hash') || false
+ const hash = enable_hash ? location.hash : ''
+ if (_.isUndefined(page)) {
+ page = location.pathname + hash
+ }
+ this.track('页面加载', _.info.pageviewInfo(page))
+}
+
+/**
+ * Track clicks on a set of document elements. Selector must be a
+ * valid query. Elements must exist on the page at the time track_links is called.
+ *
+ * ### Usage:
+ *
+ * // track click for link id #nav
+ * sugoio.track_links('#nav', 'Clicked Nav Link');
+ *
+ * ### Notes:
+ *
+ * This function will wait up to 300 ms for the Sugoio
+ * servers to respond. If they have not responded by that time
+ * it will head to the link without ensuring that your event
+ * has been tracked. To configure this timeout please see the
+ * set_config() documentation below.
+ *
+ * If you pass a function in as the properties argument, the
+ * function will receive the DOMElement that triggered the
+ * event as an argument. You are expected to return an object
+ * from the function; any properties defined on this object
+ * will be sent to sugoio as event properties.
+ *
+ * @type {Function}
+ * @param {Object|String} query A valid DOM query, element or jQuery-esque list
+ * @param {String} event_name The name of the event to track
+ * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement
+ */
+Sugoio.prototype.track_links = function (query, event_name, properties) {
+ return this._track_dom.call(this, LinkTracker, arguments)
+}
+
+/**
+ * Track form submissions. Selector must be a valid query.
+ *
+ * ### Usage:
+ *
+ * // track submission for form id 'register'
+ * sugoio.track_forms('#register', 'Created Account');
+ *
+ * ### Notes:
+ *
+ * This function will wait up to 300 ms for the sugoio
+ * servers to respond, if they have not responded by that time
+ * it will head to the link without ensuring that your event
+ * has been tracked. To configure this timeout please see the
+ * set_config() documentation below.
+ *
+ * If you pass a function in as the properties argument, the
+ * function will receive the DOMElement that triggered the
+ * event as an argument. You are expected to return an object
+ * from the function; any properties defined on this object
+ * will be sent to sugoio as event properties.
+ *
+ * @type {Function}
+ * @param {Object|String} query A valid DOM query, element or jQuery-esque list
+ * @param {String} event_name The name of the event to track
+ * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement
+ */
+Sugoio.prototype.track_forms = function (query, event_name, properties) {
+ return this._track_dom.call(this, FormTracker, arguments)
+}
+
+/**
+ * Time an event by including the time between this call and a
+ * later 'track' call for the same event in the properties sent
+ * with the event.
+ *
+ * ### Usage:
+ *
+ * // time an event named 'Registered'
+ * sugoio.time_event('Registered');
+ * sugoio.track('Registered', {'Gender': 'Male', 'Age': 21});
+ *
+ * When called for a particular event name, the next track call for that event
+ * name will include the elapsed time between the 'time_event' and 'track'
+ * calls. This value is stored as seconds in the 'duration' property.
+ *
+ * @param {String} event_name The name of the event.
+ * @return {Sugoio}
+ */
+Sugoio.prototype.time_event = function (event_name) {
+ if (_.isUndefined(event_name)) {
+ Logger.error('No event name provided to sugoio.time_event')
+ return this
+ }
+
+ if (this._event_is_disabled(event_name)) {
+ return this
+ }
+
+ this.persistence.set_event_timer(event_name, new Date().getTime())
+ return this
+}
+
+/**
+ * Register a set of super properties, which are included with all
+ * events. This will overwrite previous super property values.
+ *
+ * ### Usage:
+ *
+ * // register 'Gender' as a super property
+ * sugoio.register({'Gender': 'Female'});
+ *
+ * // register several super properties when a user signs up
+ * sugoio.register({
+ * 'Email': 'jdoe@example.com',
+ * 'Account Type': 'Free'
+ * });
+ *
+ * @param {Object} props An associative array of properties to store about the user
+ * @param {Number} [days] How many days since the user's last visit to store the super properties
+ */
+Sugoio.prototype.register = function (props, days) {
+ this.persistence.register(props, days)
+}
+
+/**
+ * Register a set of super properties only once. This will not
+ * overwrite previous super property values, unlike register().
+ *
+ * ### Usage:
+ *
+ * // register a super property for the first time only
+ * sugoio.register_once({
+ * 'First Login Date': new Date().toISOString()
+ * });
+ *
+ * ### Notes:
+ *
+ * If default_value is specified, current super properties
+ * with that value will be overwritten.
+ *
+ * @param {Object} props An associative array of properties to store about the user
+ * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
+ * @param {Number} [days] How many days since the users last visit to store the super properties
+ */
+Sugoio.prototype.register_once = function (props, default_value, days) {
+ this.persistence.register_once(props, default_value, days)
+}
+
+/**
+ * Delete a super property stored with the current user.
+ *
+ * @param {String} property The name of the super property to remove
+ */
+Sugoio.prototype.unregister = function (property) {
+ this.persistence.unregister(property)
+}
+
+Sugoio.prototype._register_single = function (prop, value) {
+ var props = {}
+ props[prop] = value
+ this.register(props)
+}
+
+/**
+ * Clears super properties and generates a new random client_distinct_id for this instance.
+ * Useful for clearing data when a user logs out.
+ */
+Sugoio.prototype.reset = function () {
+ this.persistence.clear()
+ this._flags.identify_called = false
+ this.register_once({ 'distinct_id': _.UUID() }, '')
+}
+
+/**
+ * Returns the current distinct id of the user. This is either the id automatically
+ * generated by the library or the id that has been passed by a call to identify().
+ *
+ * ### Notes:
+ *
+ * get_distinct_id() can only be called after the Sugoio library has finished loading.
+ * init() has a loaded function available to handle this automatically. For example:
+ *
+ * // set client_distinct_id after the sugoio library has loaded
+ * sugoio.init('YOUR PROJECT TOKEN', {
+ * loaded: function(sugoio) {
+ * client_distinct_id = sugoio.get_distinct_id();
+ * }
+ * });
+ */
+Sugoio.prototype.get_distinct_id = function () {
+ return this.get_property('distinct_id')
+}
+
+/**
+ * Provide a string to recognize the user by. The string passed to
+ * this method will appear in the Sugoio Streams product rather
+ * than an automatically generated name. Name tags do not have to
+ * be unique.
+ *
+ * This value will only be included in Streams data.
+ *
+ * @param {String} name_tag A human readable name for the user
+ * @api private
+ */
+Sugoio.prototype.name_tag = function (name_tag) {
+ this._register_single('sg_name_tag', name_tag)
+}
+
+/**
+ * Update the configuration of a sugoio library instance.
+ *
+ * The default config is:
+ *
+ * {
+ * // super properties cookie expiration (in days)
+ * cookie_expiration: 365
+ *
+ * // super properties span subdomains
+ * cross_subdomain_cookie: true
+ *
+ * // if this is true, the sugoio cookie or localStorage entry
+ * // will be deleted, and no user persistence will take place
+ * disable_persistence: false
+ *
+ * // type of persistent store for super properties (cookie/
+ * // localStorage) if set to 'localStorage', any existing
+ * // sugoio cookie value with the same persistence_name
+ * // will be transferred to localStorage and deleted
+ * persistence: 'cookie'
+ *
+ * // name for super properties persistent store
+ * persistence_name: ''
+ *
+ * // names of properties/superproperties which should never
+ * // be sent with track() calls
+ * property_blacklist: []
+ *
+ * // if this is true, sugoio cookies will be marked as
+ * // secure, meaning they will only be transmitted over https
+ * secure_cookie: false
+ *
+ * // the amount of time track_links will
+ * // wait for Sugoio's servers to respond
+ * track_links_timeout: 300
+ *
+ * // should we track a page view on page load
+ * track_pageview: true
+ *
+ * // if you set upgrade to be true, the library will check for
+ * // a cookie from our old js library and import super
+ * // properties from it, then the old cookie is deleted
+ * // The upgrade config option only works in the initialization,
+ * // so make sure you set it when you create the library.
+ * upgrade: false
+ * }
+ *
+ *
+ * @param {Object} config A dictionary of new configuration values to update
+ */
+Sugoio.prototype.set_config = function (config) {
+ if (_.isObject(config)) {
+ _.extend(this.config, config)
+
+ if (!this.get_config('persistence_name')) {
+ this.config.persistence_name = this.config.cookie_name
+ }
+ if (!this.get_config('disable_persistence')) {
+ this.config.disable_persistence = this.config.disable_cookie
+ }
+
+ if (this.persistence) {
+ this.persistence.update_config(this.config)
+ }
+ Config.debug = Config.debug || this.get_config('debug')
+ }
+}
+
+/**
+ * returns the current config object for the library.
+ */
+Sugoio.prototype.get_config = function (prop_name) {
+ return this.config[prop_name]
+}
+
+/**
+ * Returns the value of the super property named property_name. If no such
+ * property is set, get_property() will return the undefined value.
+ *
+ * ### Notes:
+ *
+ * get_property() can only be called after the Sugoio library has finished loading.
+ * init() has a loaded function available to handle this automatically. For example:
+ *
+ * // grab value for 'user_id' after the sugoio library has loaded
+ * sugoio.init('YOUR PROJECT TOKEN', {
+ * loaded: function(sugoio) {
+ * user_id = sugoio.get_property('user_id');
+ * }
+ * });
+ *
+ * @param {String} property_name The name of the super property you want to retrieve
+ */
+Sugoio.prototype.get_property = function (property_name) {
+ return this.persistence.props[property_name]
+}
+
+/**
+ * 生成用户真实id维度名
+ * @param {string} dimension
+ * @return {?string}
+ */
+Sugoio.createPeopleDimension = function (dimension) {
+ if (!_.isString(dimension) || dimension === '') {
+ return null
+ }
+
+ return CONSTANTS.PEOPLE_REAL_USER_ID_DIMENSION_PREFIX + dimension
+}
+
+/**
+ * 提取用户真实id维度
+ * @param {string} dimension
+ * @return {?string}
+ */
+Sugoio.extractPeopleDimension = function (dimension) {
+ if (!_.isString(dimension) || dimension === '') {
+ return null
+ }
+
+ var i = dimension.indexOf(CONSTANTS.PEOPLE_REAL_USER_ID_DIMENSION_PREFIX)
+ if (i === 0) {
+ return dimension.replace(CONSTANTS.PEOPLE_REAL_USER_ID_DIMENSION_PREFIX, '')
+ }
+ return dimension
+}
+
+/**
+ * @param {string} dimension
+ * @return {?string}
+ * @private
+ */
+Sugoio.prototype._getRealPeople = function (dimension) {
+ dimension = Sugoio.createPeopleDimension(dimension)
+ return dimension ? this.persistence.props[dimension] : null
+}
+
+/**
+ * 生成用户真实ID维度json
+ * @return {{}}
+ * @private
+ */
+Sugoio.prototype._getRealPeopleJson = function () {
+ var props = this.persistence.props
+ var key = null
+
+ for (var prop in props) {
+ if (!props.hasOwnProperty(prop)) continue
+ if (prop.indexOf(CONSTANTS.PEOPLE_REAL_USER_ID_DIMENSION_PREFIX) === 0) {
+ key = prop
+ break
+ }
+ }
+
+ var result = {}
+ if (!key) return result
+
+ var dimension = Sugoio.extractPeopleDimension(key)
+ if (!dimension) return result
+
+ result[dimension] = props[key]
+
+ return result
+}
+
+/**
+ * 录用户Id
+ * 函数功能实现如下:
+ * 先判断cookie(pc, 移动端存储本地)里是否已存在此登录id的首次登陆时间,
+ * 如果不存在则发起请求向服务端获取首次登陆时间,然后上报一条首次访问的时间,
+ * 并存储到cookie(pc, 移动端存储本地)
+ * 后台接口采用redis存储用户登录ID映射表
+ * @param {string} user_id
+ * @param {string|function} [user_real_dimension]
+ * @param {function(err: Error|null)} [callback]
+ * @return {Sugoio}
+ */
+Sugoio.prototype.track_first_time = function (user_id, user_real_dimension, callback) {
+ if (_.isUndefined(user_id) || user_id === '') {
+ Logger.error('Track first time param error: user_id required')
+ return this
+ }
+
+ // track_first_time(user_id, callback)
+ if (_.isFunction(user_real_dimension)) {
+ callback = user_real_dimension
+ user_real_dimension = void 0
+ }
+
+ // track_first_time(user_id, user_real_dimension)
+ if (_.isString(user_real_dimension) && _.isUndefined(callback)) {
+ callback = _.noop
+ }
+
+ var user = this._getRealPeople(user_real_dimension)
+ var self = this
+
+ if (user === user_id) {
+ callback(null)
+ return this
+ }
+
+ // 向服务端注册,更新cookie存储
+ API.getFirstLoginTime(user_id, this.get_config('token'))
+ .success(function (res) {
+ /** @type {GetFirstLoginTimeResponseStruct} */
+ var proxy = res
+ if (!proxy.success) {
+ return Logger.error('Fetch first login error: %s', proxy.message)
+ }
+ // 首次登录
+ // 将 user_id, first_login_time 写入cookie
+ // 并发送首次登录事件
+
+ // 由于用户可能手动清理了cookie,所以每次验证的时候都写一次
+ var obj = {}
+ obj[CONSTANTS.FIRST_LOGIN_TIME] = proxy.result.firstLoginTime
+
+ var dimension = Sugoio.createPeopleDimension(user_real_dimension)
+ if (dimension) {
+ obj[dimension] = user_id
+ }
+
+ self.register(obj)
+
+ if (proxy.result.isFirstLogin) {
+ // 发送首次登录事件
+ Logger.info('Store user first login information to cookie: %o', obj)
+ self.track(CONSTANTS.FIRST_LOGIN_EVENT_NAME, Track.track._getDefaultProperties('first_login'))
+ Logger.info('Track first login event: %s', CONSTANTS.FIRST_LOGIN_EVENT_NAME)
+ }
+
+ callback(null)
+ })
+ .error(function (err) {
+ Logger.error('Fetch first login error: %s', err.message)
+ callback(err || new Error('查询首次登录时间出错'))
+ })
+
+ return this
+}
+
+/**
+ * 清除真实用户id
+ * @param {string} user_id
+ * @param {string} user_real_dimension
+ * @return {Sugoio}
+ */
+Sugoio.prototype.clear_first_login = function (user_id, user_real_dimension) {
+
+ if (!_.isString(user_id) || user_id === '') {
+ Logger.error('Track clear first login param error: user_id required and must be string')
+ return this
+ }
+
+ if (!_.isString(user_real_dimension) || user_real_dimension === '') {
+ Logger.error('Track clear first login param error: user_real_dimension required and must be string')
+ return this
+ }
+
+ var dimension = Sugoio.createPeopleDimension(user_real_dimension)
+ var user = this._getRealPeople(user_real_dimension)
+
+ if (user !== user_id) return this
+
+ // 清除真实user_id,首次登录时间,维度名
+ if (dimension) {
+ this.unregister(dimension)
+ }
+
+ this.unregister(CONSTANTS.FIRST_LOGIN_TIME)
+ return this
+}
+
+Sugoio.prototype.toString = function () {
+ var name = this.get_config('name')
+ if (name !== CONSTANTS.PRIMARY_INSTANCE_NAME) {
+ name = CONSTANTS.PRIMARY_INSTANCE_NAME + '.' + name
+ }
+ return name
+}
+
+Sugoio.prototype._event_is_disabled = function (event_name) {
+ return _.isBlockedUA(userAgent) || this._flags.disable_all_events || _.include(this.__disabled_events, event_name)
+}
+
+Sugoio.prototype.updateSessionId = function(){
+ var session_id = _.shortUUID()
+ var expire_days = 1 //session_id expire_days
+ this.register({ 'session_id': session_id }, expire_days)
+}
+
+module.exports = Sugoio
diff --git a/src/sugoio/Track.js b/src/sugoio/Track.js
new file mode 100644
index 0000000..9c356b3
--- /dev/null
+++ b/src/sugoio/Track.js
@@ -0,0 +1,815 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-18
+ */
+
+var Config = require('../config')
+var _ = require('sugo-sdk-js-utils')['default']
+var Events = require('./events')
+var match_pathname = require('../match-pathname')
+var match_page = require('../match-page')
+var Regulation = require('../regulation-parser')
+var Selector = require('../css-path')
+var API = require('../sugo-event-api')
+var SugoIO = require('../global')
+var Debugger = require('../debugger')
+var request = require('../request')
+var CONSTANTS = require('../../constants')
+var Logger = require('../logger').get('Track')
+
+var DISABLE_COOKIE = '__sgced'
+
+// specifying these locally here since some websites override the global Node var
+// ex: https://www.codingame.com/
+var ELEMENT_NODE = 1
+var TEXT_NODE = 3
+
+var track = {
+ _initializedTokens: [],
+
+ _previousElementSibling: function (el) {
+ if (el.previousElementSibling) {
+ return el.previousElementSibling
+ } else {
+ do {
+ el = el.previousSibling
+ } while (el && el.nodeType !== ELEMENT_NODE)
+ return el
+ }
+ },
+
+ _loadScript: function (scriptUrlToLoad, callback) {
+ var scriptTag = document.createElement('script')
+ scriptTag.type = 'text/javascript'
+ scriptTag.src = scriptUrlToLoad
+ scriptTag.onload = callback
+
+ var scripts = document.getElementsByTagName('script')
+ if (scripts.length > 0) {
+ scripts[0].parentNode.insertBefore(scriptTag, scripts[0])
+ } else {
+ document.body.appendChild(scriptTag)
+ }
+ },
+
+ _loadCss: function (url, callback) {
+ var stylesheet = document.createElement('link')
+ stylesheet.href = url
+ stylesheet.rel = 'stylesheet'
+ stylesheet.type = 'text/css'
+ // temporarily set media to something inapplicable to ensure it'll fetch without blocking render
+ stylesheet.media = 'only x'
+ // set the media back when the stylesheet loads
+ stylesheet.onload = function () {
+ stylesheet.media = 'all'
+ callback()
+ }
+ document.getElementsByTagName('head')[0].appendChild(stylesheet)
+ },
+
+ _getClassName: function (elem) {
+ switch (typeof elem.className) {
+ case 'string':
+ return elem.className
+ case 'object': // handle cases where className might be SVGAnimatedString or some other type
+ return elem.className.baseVal || elem.getAttribute('class') || ''
+ default: // future proof
+ return ''
+ }
+ },
+
+ _getPropertiesFromElement: function (elem) {
+ var props = {
+ 'classes': this._getClassName(elem).split(' '),
+ 'tag_name': elem.tagName.toLowerCase()
+ }
+
+ if (_.include(['input', 'select', 'textarea'], elem.tagName.toLowerCase())) {
+ var formFieldValue = this._getFormFieldValue(elem)
+ if (this._includeProperty(elem, formFieldValue)) {
+ props.value = formFieldValue
+ }
+ }
+
+ _.each(elem.attributes, function (attr) {
+ props['attr__' + attr.name] = attr.value
+ })
+
+ var nthChild = 1
+ var nthOfType = 1
+ var currentElem = elem
+ while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
+ nthChild++
+ if (currentElem.tagName === elem.tagName) {
+ nthOfType++
+ }
+ }
+ props.nth_child = nthChild
+ props.nth_of_type = nthOfType
+
+ return props
+ },
+
+ /*
+ * Due to potential reference discrepancies (such as the webcomponents.js polyfill)
+ * We want to match tagNames instead of specific reference because something like element === document.body
+ * won't always work because element might not be a native element.
+ */
+ _isTag: function (el, tag) {
+ return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase()
+ },
+
+ _shouldTrackDomEvent: function (element, event) {
+ if (!element || this._isTag(element, 'html') || element.nodeType !== ELEMENT_NODE) {
+ return false
+ }
+ var tag = element.tagName.toLowerCase()
+ switch (tag) {
+ case 'html':
+ return false
+ case 'form':
+ return event.type === 'submit'
+ case 'input':
+ if (_.indexOf(['button', 'submit'], element.getAttribute('type')) === -1) {
+ return event.type === 'change' || event.type === 'focus'
+ } else {
+ return event.type === 'click'
+ }
+ case 'select':
+ case 'textarea':
+ return event.type === 'change'
+ default:
+ return event.type === 'click'
+ }
+ },
+
+ _getDefaultProperties: function (eventType) {
+ const ins = SugoIO.get()
+ const enable_hash = ins.get_config('enable_hash') || false
+ const hash = enable_hash ? location.hash : ''
+ return _.extend(_.info.properties(), {
+ 'event_type': eventType,
+ 'host': window.location.host,
+ 'path_name': window.location.pathname + hash,
+ 'sdk_version': SugoIO.Global.version
+ })
+ },
+
+ _getInputValue: function (input) {
+ var value = null
+ var type = input.type.toLowerCase()
+ switch (type) {
+ case 'checkbox':
+ if (input.checked) {
+ value = [input.value]
+ }
+ break
+ case 'radio':
+ if (input.checked) {
+ value = input.value
+ }
+ break
+ default:
+ value = input.value
+ break
+ }
+ return value
+ },
+
+ _getSelectValue: function (select) {
+ var value
+ if (select.multiple) {
+ var values = []
+ _.each(_.querySelectorAll('[selected]', select), function (option) {
+ values.push(option.value)
+ })
+ value = values
+ } else {
+ value = select.value
+ }
+ return value
+ },
+
+ _includeProperty: function (input, value) {
+ for (var curEl = input; curEl.parentNode && !this._isTag(curEl, 'body'); curEl = curEl.parentNode) {
+ var classes = this._getClassName(curEl).split(' ')
+ if (_.include(classes, 'sugoio-sensitive') || _.include(classes, 'sugoio-no-track')) {
+ return false
+ }
+ }
+
+ if (_.include(this._getClassName(input).split(' '), 'sugoio-include')) {
+ return true
+ }
+
+ if (value === null) {
+ return false
+ }
+
+ // don't include hidden or password fields
+ var type = input.type || ''
+ switch (type.toLowerCase()) {
+ case 'hidden':
+ return false
+ case 'password':
+ return false
+ }
+
+ // filter out data from fields that look like sensitive fields
+ var name = input.name || input.id || ''
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|seccode|securitycode|securitynum|socialsec|socsec|ssn/i
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
+ return false
+ }
+
+ if (typeof value === 'string') {
+ // check to see if input value looks like a credit card number
+ // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
+ var ccRegex = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/
+ if (ccRegex.test((value || '').replace(/[\- ]/g, ''))) {
+ return false
+ }
+
+ // check to see if input value looks like a social security number
+ var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/
+ if (ssnRegex.test(value)) {
+ return false
+ }
+ }
+
+ return true
+ },
+
+ _getFormFieldValue: function (field) {
+ var val
+ switch (field.tagName.toLowerCase()) {
+ case 'input':
+ val = this._getInputValue(field)
+ break
+ case 'select':
+ val = this._getSelectValue(field)
+ break
+ default:
+ val = field.value || field.textContent
+ break
+ }
+ return this._includeProperty(field, val) ? val : null
+ },
+
+ _getFormFieldProperties: function (form) {
+ var formFieldProps = {}
+ _.each(form.elements, function (field) {
+ var name = field.getAttribute('name') || field.getAttribute('id')
+ if (name !== null) {
+ name = 'form_field__' + name
+ var val = this._getFormFieldValue(field)
+ if (this._includeProperty(field, val)) {
+ var prevFieldVal = formFieldProps[name]
+ if (prevFieldVal !== undefined) { // combine values for inputs of same name
+ formFieldProps[name] = [].concat(prevFieldVal, val)
+ } else {
+ formFieldProps[name] = val
+ }
+ }
+ }
+ }, this)
+ return formFieldProps
+ },
+
+ _extractCustomPropertyValue: function (customProperty) {
+ return this._extractElementsValue(customProperty.css_selector).join(',')
+ },
+
+ /**
+ * @param {String} selector
+ * @return {Array}
+ * @private
+ */
+ _extractElementsValue: function (selector) {
+ var values = []
+
+ if (!_.isString(selector)) {
+ return values
+ }
+
+ _.each(_.querySelectorAll(selector), function (elem) {
+ if (_.indexOf(['input', 'select'], elem.tagName.toLowerCase()) > -1) {
+ values.push(elem.value)
+ } else if (elem.textContent) {
+ values.push(elem.textContent)
+ }
+ })
+
+ return values
+ },
+
+ /**
+ * 获取关联元素内容
+ * @param {Array} binds
+ * @param {string} parentSimilarPath
+ * @return {Object}
+ * @private
+ */
+ _getAssociatesElementsProps: function (binds, parentSimilarPath) {
+
+ // 上报相关的维度开启同类
+ const getChildSimilarPath = (p_path, c_path) => {
+ // 获取完整层级path进行比较
+ c_path = Selector.entire(document.querySelector(c_path))
+ let sameIdx = 0
+ // 对比找出控件 和上报维度相同内容
+ let pathLength = p_path.length
+ for (let i = 0; i < pathLength; i++) {
+ if (p_path.charAt(i) !== c_path.charAt(i)) {
+ sameIdx = i
+ break
+ }
+ }
+ // 生成新的path 获上报控件
+ var similarIdex = p_path.substr(sameIdx).substring(0, p_path.substr(sameIdx).indexOf(')'))
+ var similarEndPath = c_path.substr(sameIdx).substring(c_path.substr(sameIdx).indexOf(')'))
+ return c_path.substring(0, sameIdx) + similarIdex + similarEndPath
+ }
+
+ return (_.isArray(binds) ? binds : []).reduce(function (p, c) {
+ let path = c.path
+ // 上报相关的维度开启同类
+ if (c.similar) { // 根据父同类元素path获取关联元素对应同类path
+ path = getChildSimilarPath(parentSimilarPath, path)
+ }
+ p[c.dimension] = this._extractElementsValue(path).join(',')
+ return p
+ }.bind(this), {})
+ },
+
+ _getCustomProperties: function (targetElementList) {
+ var props = {}
+ _.each(this._customProperties.events, function (customProperty) {
+ // _.each(customProperty.event_selectors, function(eventSelector) {
+ var selectors = customProperty.event_path
+ if (!_.isArray(selectors)) {
+ selectors = [selectors]
+ }
+ _.each(selectors, function (eventSelector) {
+ var eventElements = _.querySelectorAll(eventSelector)
+ _.each(eventElements, function (eventElement) {
+ if (_.include(targetElementList, eventElement)) {
+ props[customProperty.event_name] = this._extractCustomPropertyValue(customProperty)
+ }
+ }, this)
+ }, this)
+ }, this)
+ return props
+ },
+
+ checkForBackoff: function (resp) {
+ // temporarily stop CE for X seconds if the 'X-MP-CE-Backoff' header says to
+ var secondsToDisable = parseInt(resp.getResponseHeader('X-MP-CE-Backoff'))
+ if (!isNaN(secondsToDisable) && secondsToDisable > 0) {
+ var disableUntil = _.timestamp() + (secondsToDisable * 1000)
+ Logger.log('disabling CE for ' + secondsToDisable + ' seconds (from ' + _.timestamp() + ' until ' + disableUntil + ')')
+ _.cookie.set_seconds(DISABLE_COOKIE, true, secondsToDisable, true)
+ }
+ },
+
+ _getEventTarget: function (e) {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes
+ if (typeof e.target === 'undefined') {
+ return e.srcElement
+ } else {
+ return e.target
+ }
+ },
+
+ _trackEvent: function (e, instance, events, pageInfo) {
+ var eventType = e.type
+ /*** Don't mess with this code without running IE8 tests on it ***/
+ var target = this._getEventTarget(e)
+ if (target.nodeType === TEXT_NODE) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
+ target = target.parentNode
+ }
+
+ if (this._shouldTrackDomEvent(target, e)) {
+ var targetElementList = [target]
+ var curEl = target
+ while (curEl.parentNode && !this._isTag(curEl, 'body')) {
+ targetElementList.push(curEl.parentNode)
+ curEl = curEl.parentNode
+ }
+
+ // var elementsJson = []
+ var href, elementText = '', form, explicitNoTrack = false
+ _.each(targetElementList, function (el, idx) {
+ // if the element or a parent element is an anchor tag
+ // include the href as a property
+ if (el.tagName.toLowerCase() === 'a') {
+ href = el.getAttribute('href')
+ } else if (el.tagName.toLowerCase() === 'form') {
+ form = el
+ }
+ // crawl up to max of 5 nodes to populate text content
+ if (!elementText && idx < 5 && el.textContent) {
+ var textContent = _.trim(el.textContent)
+ if (textContent) {
+ elementText = textContent.replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ').substring(0, 255)
+ }
+ }
+
+ // allow users to programatically prevent tracking of elements by adding class 'sugoio-no-track'
+ var classes = this._getClassName(el).split(' ')
+ if (_.include(classes, 'sugoio-no-track')) {
+ explicitNoTrack = true
+ }
+
+ // elementsJson.push(this._getPropertiesFromElement(el))
+ }, this)
+
+ if (explicitNoTrack) {
+ return false
+ }
+ var props = _.extend(
+ this._getDefaultProperties(eventType), {
+ 'event_label': elementText
+ })
+ if (form && (eventType === 'submit' || eventType === 'click')) {
+ _.extend(props, this._getFormFieldProperties(form))
+ }
+
+ _.each(events, _.bind(function (obj) {
+ // 过滤事件类型
+ if (obj.event_type !== eventType) return
+
+ // 关联维度
+ var binds = obj.binds || {}
+ var bindsArray = _.map(_.keys(binds), function (dimension) {
+ var record = binds[dimension]
+ return {
+ dimension: dimension,
+ path: record.path,
+ similar: record.similar
+ }
+ })
+
+ // 如果是同类元素,也上报数据
+ // TODO 性能优化 使用similar_path匹配同类元素
+
+ var path = obj.similar === true ? (obj.similar_path || _.similar(obj.event_path)) : obj.event_path
+ var elems = _.querySelectorAll(path)
+
+ _.each(elems, function (elem) {
+
+ if (Events.contains(elem, target)) {
+ var event_name = obj.event_name || 'web_event'
+ var code = _.trim(obj.code || '')
+ var parentSimilarPath = Selector.entire(elem)
+
+ // 确认有效关联元素
+ _.extend(props, this._getAssociatesElementsProps(bindsArray, parentSimilarPath))
+
+ if (code) {
+ try {
+ // web注入代码埋点
+ // 代码注入失败不影响整个上报记录
+ var sugo_props = new Function('e', 'element', 'conf', 'instance', code)
+ var custom_props = sugo_props(e, elem, obj, instance) || {}
+ custom_props.from_binding = true
+
+ if (_.isObject(custom_props)) {
+ _.extend(props, custom_props)
+ }
+ } catch (e) {
+ Logger.log('sugoio track code err => ' + e.message)
+ }
+ }
+
+ props.event_id = obj.event_id
+ instance.track(event_name, props)
+ }
+ }, this)
+
+ }, this))
+
+ return true
+ }
+ },
+
+ // only reason is to stub for unit tests
+ // since you can't override window.location props
+ _navigate: function (href) {
+ window.location.href = href
+ },
+
+ _addDomEventHandlers: function (instance) {
+ Logger.log('_addDomEventHandlers', instance)
+ var that = this
+ //根据可视化配置绑定上报事件
+ if (this._customProperties && this._customProperties.events.length > 0) {
+ // 改为代理事件以保证动态渲染的dom事件绑定有效
+ var handler = _.bind(function (e) {
+ if (_.cookie.parse(DISABLE_COOKIE) !== true) {
+ e = e || window.event
+ this._trackEvent(e, instance, this._customProperties.events, this._customProperties.pageInfo)
+ }
+ }, this)
+ _.register_event(document, 'focus', handler, false, true)
+ // _.register_event(document, 'submit', handler, false, true)
+ _.register_event(document, 'change', handler, false, true)
+ _.register_event(document, 'click', handler, false, true)
+ }
+ },
+ _customProperties: {},
+ init: function (instance) {
+ if (!(document && document.body)) {
+ Logger.log('document not ready yet, trying again in 500 milliseconds...')
+ setTimeout(() => {
+ this.init(instance)
+ }, 500)
+ return
+ }
+
+ var token = instance.get_config('token')
+ if (_.indexOf(this._initializedTokens, token) > -1) {
+ return Logger.log('autotrack already initialized for token "' + token + '"')
+ }
+ this._initializedTokens.push(token)
+ if (!this._maybeLoadEditor(instance)) {
+ const enable_hash = instance.get_config('enable_hash') || false
+ // decide callbak
+ var parseDecideResponse = _.bind(function (response) {
+ if (!response.success) {
+ return Logger.error(response.message)
+ }
+
+ var res = null
+
+ // 捕获服务端返回异常空数据引发的bug
+ try {
+ res = _.JSONDecode(_.decompressUrlQuery(response.result))
+ } catch (e) {
+ Logger.error(e.message)
+ Logger.error(e.stack)
+ }
+
+ if (!res) return
+
+ Logger.debug('Page decide: %o', res)
+
+ if (res.config.enable_collect_everything === true) {
+ let url = this._getCurrentUrl(enable_hash)
+ var pageInfo = match_page(url, res.page_info) || {}
+ var serverDimensions = res.dimensions
+ if (!serverDimensions || serverDimensions.length < 1) {
+ return Logger.error('获取服务端预设维度错误')
+ }
+
+ // instance.serverDimensions = serverDimensions
+ SugoIO.store('serverDimensions', serverDimensions)
+
+ this._customProperties.events = res.web_event_bindings || {}
+ this._customProperties.pageInfo = pageInfo
+
+ // Write in Debugger
+ Debugger.addBuffer('PageEvents', res.web_event_bindings)
+ Debugger.addBuffer('PageConfigs', res.page_info)
+
+ var props = this._getDefaultProperties('view')
+ if (pageInfo.code) {
+ // 浏览参数设置代码注入
+ try {
+ // 代码注入失败不影响整个上报记录
+ var sugo_props = new Function('conf', 'instance', pageInfo.code)
+ var custom_props = sugo_props(pageInfo, instance) || {}
+ if (_.isObject(custom_props)) {
+ _.extend(props, custom_props)
+ }
+ } catch (e) {
+ Logger.error('sugoio track code err => %s', e.message)
+ }
+ }
+ if (pageInfo.page_name) {
+ // 写入全局 storage
+ SugoIO.store('pageInfo', pageInfo)
+ }
+ var page_name = (pageInfo.page_name || document.title)
+ var properties = _.extend({ page_name: page_name }, props)
+
+ // 次访问时间记录
+ if (!instance.persistence.props.hasOwnProperty(CONSTANTS.FIRST_VISIT_TIME)) {
+ var timestamp = _.timestamp()
+ var tmp = {}
+
+ tmp[CONSTANTS.FIRST_VISIT_TIME] = timestamp
+ properties = _.extend({}, properties, tmp)
+ instance.register_once(tmp)
+
+ Logger.info('Track first visit event: %s', timestamp)
+
+ // 上报首次访问之后再上报浏览事件
+ var p = _.extend({}, properties, { event_type: 'first_visit' })
+ instance.track(CONSTANTS.FIRST_VISIT_EVENT_NAME, p, function () {
+ instance._loaded()
+ instance.track('浏览', properties)
+ })
+ } else {
+ instance._loaded()
+ instance.track('浏览', properties)
+ }
+
+ this._addDomEventHandlers(instance)
+
+ } else {
+ instance.__autotrack_enabled = false
+ }
+ }, this)
+
+ const loadSdkDecide = () => {
+ let url = this._getCurrentUrl(enable_hash)
+ let path = location.pathname + enable_hash ? location.hash : ''
+ API.set_host(instance.get_config('app_host'))
+ // 获取页面分类
+ API.getDeployedPageCategories(token, '0').success(_.bind(function (res) {
+ var categories = res.success ? res.result : []
+ Logger.debug('Page categories: %s', categories)
+ Debugger.addBuffer('PageCategories', categories)
+
+ var regulations = _.map(categories, function (r) {
+ return r.regulation
+ })
+ var matched = Regulation.exec(url, regulations)
+ if (matched) {
+ var record = _.find(categories, function (r) {return r.regulation === matched}) || {}
+ instance.page_category = record.name
+ }
+
+ // 加载获取pathname
+ var separator = '____'
+ match_pathname(instance.get_config('decide_host'), token, url, path, true, function (err, pathname) {
+ // 加载事件
+ if (err) {
+ return Logger.error(err)
+ }
+ var uri = instance.get_config('decide_host') + '/api/sdk/desktop/decide?'
+ + 'verbose=true'
+ + '&version=0'
+ + '&lib=web'
+ + '&projectId=' + instance.get_config('project_id')
+ + '&path_name=' + encodeURIComponent(pathname.join(separator))
+ + '&token=' + token
+ + '&separator=' + separator
+
+ request().send('GET', uri).success(parseDecideResponse)
+ })
+ }, this))
+ }
+
+ // vue单页应用hash变化即页面切换,重新加载配置
+ if (enable_hash) {
+ window.addEventListener('hashchange', () => {
+ setTimeout(() => {
+ // load server events config
+ loadSdkDecide()
+ }, 200)
+ }, false)
+ } else {
+ // load server events config
+ loadSdkDecide()
+ }
+ }
+ },
+
+ _getCurrentUrl: function(enable_hash) {
+ const hash = enable_hash === true ? location.hash : ''
+ return (location.origin || (location.protocol + '//' + location.host)) + location.pathname + hash
+ },
+
+ _editorParamsFromHash: function (instance, state) {
+ var editorParams
+ try {
+ if (_.isString(state)) {
+ state = JSON.parse(state)
+ }
+ var expiresInSeconds = state.expires_in
+ editorParams = {
+ 'accessToken': state.access_token,
+ 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000),
+ 'projectToken': state.token,
+ 'projectId': state.project_id,
+ 'userId': state.user_id,
+ 'choosePage': state.choose_page
+ }
+ window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams))
+
+ if (state.desiredHash) {
+ window.location.hash = state.desiredHash
+ } else if (window.history) {
+ history.replaceState('', document.title, window.location.pathname + window.location.search) // completely remove hash
+ } else {
+ window.location.hash = '' // clear hash (but leaves # unfortunately)
+ }
+ } catch (e) {
+ Logger.error('Unable to parse data from hash', e)
+ }
+ return editorParams
+ },
+
+ /**
+ * To load the visual editor, we need an access token and other state. That state comes from one of three places:
+ * 1. In the URL hash params if the customer is using an old snippet
+ * 2. From session storage under the key `_sugocehash` if the snippet already parsed the hash
+ * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page
+ */
+ _maybeLoadEditor: function (instance) {
+ var win = window
+ var storage = win.sessionStorage
+ var hash = ''
+ var fromHash = false
+ var fromStorage = storage.getItem('_sugocehash')
+ var params
+
+ try {
+ hash = window.atob(win.location.hash.replace('#', ''))
+ if (hash && _.includes(hash, 'state')) {
+ fromHash = JSON.parse(hash).state
+ }
+ } catch (e) {
+ // console.log(e)
+ }
+
+ if (fromHash) {
+ // happens if they are initializing the editor using an old snippet
+ params = this._editorParamsFromHash(instance, fromHash)
+ } else if (fromStorage) {
+ // happens if they are initialized the editor and using the new snippet
+ params = this._editorParamsFromHash(instance, fromStorage)
+ storage.removeItem('_sugocehash')
+ } else {
+ // get credentials from sessionStorage from a previous initialzation
+ params = JSON.parse(storage.getItem('editorParams') || '{}')
+ }
+
+ if (!params.app_host) {
+ params.app_host = instance.get_config('app_host')
+ }
+
+ if (params.projectToken && instance.get_config('token') === params.projectToken) {
+ this._loadEditor(instance, params)
+ return true
+ }
+
+ return false
+ },
+
+ // only load the codeless event editor once, even if there are multiple instances of SugoioLib
+ _editorLoaded: false,
+ _loadEditor: function (instance, editorParams) {
+ // TODO css与js一起打包,避免其中某一个加载失败造成的造成的异常
+ if (!this._editorLoaded) {
+ this._editorLoaded = true
+ var editorUrl
+ var cacheBuster = '?_ts=' + (new Date()).getTime()
+ var siteMedia = instance.get_config('app_host') + '/_bc/sugo-sdk-js/libs'
+ var filename = typeof window.Vue === 'function' ? 'sugo-editor-lite.min.js' : 'sugo-editor.min.js'
+ if (Config.debug) {
+ editorUrl = siteMedia + '/' + filename + cacheBuster
+ } else {
+ editorUrl = siteMedia + '/' + filename + cacheBuster
+ }
+ var that = this
+ that._loadScript(editorUrl, function () {
+ setTimeout(function () {
+ SugoIO.Global.Editor.run(instance, editorParams)
+ }, 400)
+ })
+ return true
+ }
+ return false
+ },
+
+ // this is a mechanism to ramp up CE with no server-side interaction.
+ // when CE is active, every page load results in a decide request. we
+ // need to gently ramp this up so we don't overload decide. this decides
+ // deterministically if CE is enabled for this project by modding the char
+ // value of the project token.
+ enabledForProject: function (token, numBuckets, numEnabledBuckets) {
+ numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10
+ numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10
+ var charCodeSum = 0
+ for (var i = 0; i < token.length; i++) {
+ charCodeSum += token.charCodeAt(i)
+ }
+ return (charCodeSum % numBuckets) < numEnabledBuckets
+ },
+
+ isBrowserSupported: function () {
+ // TODO IE8+ 判断
+ return typeof document.querySelectorAll !== void 0
+ }
+}
+
+_.bind_instance_methods(track)
+_.safewrap_instance_methods(track)
+
+module.exports = {
+ DISABLE_COOKIE: DISABLE_COOKIE,
+ track: track
+}
diff --git a/src/sugoio/default-config.js b/src/sugoio/default-config.js
new file mode 100644
index 0000000..202b0f5
--- /dev/null
+++ b/src/sugoio/default-config.js
@@ -0,0 +1,42 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-18
+ */
+
+/** @type {SugoIOSDKJSConfig} */
+var Config = require('../config')
+var HTTP_PROTOCOL = (('https:' === document.location.protocol) ? 'https://' : 'http://')
+
+/** @type {SugoIOConfigInterface} */
+var DEFAULT_CONFIG = {
+ api_host: HTTP_PROTOCOL + Config.api_host, //HTTP_PROTOCOL + 'api.mixpanel.com',
+ app_host: HTTP_PROTOCOL + Config.app_host, //HTTP_PROTOCOL + 'mixpanel.com',
+ decide_host: HTTP_PROTOCOL + Config.decide_host,
+ autotrack: true,
+ cdn: HTTP_PROTOCOL + Config.app_host,
+ encode_type: Config.encode_type || 'plain', // json/plain
+ dimensions: Config.dimensions || {},
+ cross_subdomain_cookie: true,
+ persistence: 'cookie',
+ persistence_name: '__persistence__sugoio',
+ cookie_name: '__cookie__',
+ loaded: function () {},
+ store_google: true,
+ save_referrer: true,
+ test: false,
+ verbose: false,
+ img: false,
+ track_pageview: false,
+ debug: false,
+ track_links_timeout: 300,
+ cookie_expiration: 365,
+ upgrade: false,
+ disable_persistence: false,
+ disable_cookie: false,
+ secure_cookie: false,
+ ip: true,
+ property_blacklist: [],
+ enable_hash: false // 单页应用页面设置开启hash配置
+}
+
+module.exports = DEFAULT_CONFIG
diff --git a/src/sugoio/dom.js b/src/sugoio/dom.js
new file mode 100644
index 0000000..e99a0c3
--- /dev/null
+++ b/src/sugoio/dom.js
@@ -0,0 +1,304 @@
+/**
+ * Created by coin on 30/12/2016.
+ */
+
+var exports = module.exports
+var _ = require('../utils')._
+
+var tryCatch = _.tryCatch
+var doc = document
+var body = doc.body
+var Node = _.isBrowser() ? window.Node : {}
+
+function $ (selector, context) {
+ return (context || body).querySelector(selector)
+}
+
+function $$ (selector, context) {
+ return (context || body).querySelectorAll(selector)
+}
+
+function display (elem, show) {
+ return tryCatch(function () {
+ elem.style.display = show ? 'block' : 'none'
+ return elem
+ })()
+}
+
+function show (elem) {
+ return display(elem, true)
+}
+
+function hide (elem) {
+ return display(elem, false)
+}
+
+function html (elem, html) {
+ var length = arguments.length
+ return tryCatch(function () {
+ if (length === 1) {
+ return elem.innerHTML
+ } else if (length === 2) {
+ elem.innerHTML = html
+ return elem
+ }
+ return elem
+ })()
+}
+
+function text (elem, text) {
+ var length = arguments.length
+ return tryCatch(function () {
+ if (length === 1) {
+ return elem.innerText
+ } else if (length === 2) {
+ elem.innerText = text
+ return elem
+ }
+ return elem
+ })()
+}
+
+function style (elem, style) {
+ return tryCatch(function () {
+ _.each(style, function (val, name) {
+ elem.style[name] = _.isNumber(val) ? val + 'px' : val
+ })
+ })()
+}
+
+function addClass (elem, name) {
+ return tryCatch(function () {
+ if (elem.classList) {
+ elem.classList.add(name)
+ } else {
+ var classNames = elem.className
+ if (classNames === '') {
+ elem.className = name
+ } else {
+ var list = classNames.split(/\u0020/)
+ if (list.indexOf(name) === -1) {
+ list.push(name)
+ elem.className = list.join(' ')
+ }
+ }
+ }
+ return elem
+ })()
+}
+
+function removeClass (elem, name) {
+ return tryCatch(function () {
+ if (elem.classList) {
+ elem.classList.remove(name)
+ } else {
+ var classNames = elem.className
+ if (classNames !== '') {
+ var list = classNames.split(/\u0020/)
+ var i = list.indexOf(name)
+ if (i !== -1) {
+ list.splice(i, 1)
+ elem.className = list.join(' ')
+ }
+ }
+ }
+ return elem
+ })()
+}
+
+function getBoundingClientRect (elem) {
+ return tryCatch(function () {
+ var attr = ['bottom', 'height', 'left', 'right', 'top', 'width']
+ var rect = elem.getBoundingClientRect() || {}
+ var result = {}
+ attr.forEach(function (name) {
+ result[name] = rect[name] === void 0 ? -99999 : rect[name]
+ })
+ return result
+ })()
+}
+
+function _getElementBounds (el) {
+ var bounds
+
+ // a lot of people use anchor tags to wrap children and make them all linkable
+ // (for an example look at the video tiles on youtube.com home page)
+ // since anchor tags are display inline by default, we want to calculate the bounds
+ // of all of its children. however, we only want to do that when all of the direct
+ // children are element nodes (and not text nodes + other children like we do often
+ // for icons + text)
+ var allChildrenAreElements = el.children.length === el.childNodes.length
+ if (el.tagName.toLowerCase() === 'a' && el.children.length > 0 && allChildrenAreElements) {
+ Array.from(el.children)
+ .filter(function (child) { // filter out elements that are hidden or inline
+ var styles = getComputedStyle(child)
+ return !(child.tagName === 'a' || styles.display === 'none')
+ })
+ .forEach(function (child) {
+ var b = child.getBoundingClientRect()
+ if (!bounds) {
+ bounds = {
+ top: b.top,
+ right: b.right,
+ bottom: b.bottom,
+ left: b.left
+ }
+ }
+ if (b.top < bounds.top) {
+ bounds.top = b.top
+ }
+ if (b.right > bounds.right) {
+ bounds.right = b.right
+ }
+ if (b.bottom > bounds.bottom) {
+ bounds.bottom = b.bottom
+ }
+ if (b.left < bounds.left) {
+ bounds.left = b.left
+ }
+ })
+ } else {
+ bounds = el.getBoundingClientRect()
+ }
+
+ if (!bounds) {
+ bounds = {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ }
+ }
+ return bounds
+}
+
+/**
+ * @param el
+ * @return {{height: number, left: number, top: number, width: number, right: number, bottom: number}}
+ */
+function getAbsoluteBoundingClientRect (el) {
+ var bb = {
+ height: 0,
+ left: 0,
+ top: 0,
+ width: 0,
+ right: 0,
+ bottom: 0
+ }
+ var BORDER_WIDTH = 2
+ if (el) {
+ var bounds = _getElementBounds(el)
+ Object.assign(bb, {
+ height: bounds.bottom - bounds.top + (BORDER_WIDTH * 2),
+ left: (bounds.left + (window.scrollX || window.pageXOffset) - BORDER_WIDTH),
+ top: (bounds.top + (window.scrollY || window.pageYOffset) - BORDER_WIDTH),
+ width: bounds.right - bounds.left + (BORDER_WIDTH * 2),
+ bottom: bounds.bottom + (window.scrollY || window.pageYOffset) - BORDER_WIDTH,
+ right: bounds.right + (window.scrollX || window.pageXOffset) - BORDER_WIDTH
+ })
+ }
+
+ return bb
+}
+
+function remove (elem, child) {
+ return tryCatch(function () {
+ elem.removeChild(child)
+ return elem
+ })()
+}
+
+function append (elem, child) {
+ return tryCatch(function () {
+ elem.appendChild(child)
+ return elem
+ })()
+}
+
+function attr (elem, name, value) {
+ return tryCatch(function () {
+ if (value === void 0)
+ return elem.getAttribute(name)
+ return elem.setAttribute(name, value)
+ })()
+}
+
+function isNode (any) {
+ var result = false
+ tryCatch(function () {
+ result = any.nodeType === Node.ELEMENT_NODE
+ })()
+ return result
+}
+
+function shiftHostPageDown (hostElements) {
+ var HEADER_HEIGHT = 65
+ // The goal is to move the entire page down by the height of the control bar
+ // so that it doesn't cover anything. For pages with no absolute or fixed positioning,
+ // padding works fine. However, we also need to adjust for absolutely positioned elements
+ //
+ // NOTE: This should be executed before we load the editor since it iterates all elements on the page
+ // update body padding to allow room for the header
+ // var bodyCss = getComputedStyle(document.body)
+ // var newPaddingTop = (parseInt(bodyCss['padding-top'], 10) || 0) + HEADER_HEIGHT
+ // document.body.style.cssText += '; padding-top:' + newPaddingTop + 'px !important; transition: padding 300ms cubic-bezier(0, 0, 0, 0.97);'
+
+ var hasAncestor = function hasAncestor (el, cssProp, vals) {
+ for (var curEl = el.parentNode; curEl.parentNode; curEl = curEl.parentNode) {
+ var parentCss = getComputedStyle(curEl)
+ if ((vals.includes && vals.includes(parentCss[cssProp])) || _.includes(vals, parentCss[cssProp])) {
+ return true
+ }
+ }
+ return false
+ }
+
+ Array.from(hostElements).forEach(function (el) {
+ var css = getComputedStyle(el)
+ if (css.position === 'fixed' || css.position === 'absolute' && !hasAncestor(el, 'position', ['absolute', 'fixed', 'relative'])) {
+ var origBodyStyles = document.body.style.cssText
+ var origElBounds = el.getBoundingClientRect()
+ document.body.style['padding-top'] = (parseInt(document.body.style['padding-top'], 10) || 0) + 1 + 'px'
+ var newElBounds = el.getBoundingClientRect()
+ document.body.style.cssText = origBodyStyles
+ if (origElBounds.top === newElBounds.top) {
+ var newTop = (parseInt(css.top, 10) || 0) + HEADER_HEIGHT
+ el.style.cssText += '; top:' + newTop + 'px !important; transition: top 500ms ease-out;'
+ }
+ }
+ })
+}
+
+/**
+ * @param {number} position
+ * @return {number}
+ * @see https://dom.spec.whatwg.org/#dom-document-compatmode
+ * @see https://drafts.csswg.org/cssom-view/#dom-element-scrolltop
+ */
+function scrollToTop (position) {
+ if (document.compatMode === 'CSS1Compat') {
+ return document.documentElement.scrollTop = position
+ }
+ return document.body.scrollTop = position
+}
+
+exports.doc = doc
+exports.body = body
+exports.$ = $
+exports.$$ = $$
+exports.display = display
+exports.show = show
+exports.hide = hide
+exports.html = html
+exports.text = text
+exports.style = style
+exports.addClass = addClass
+exports.removeClass = removeClass
+exports.getBoundingClientRect = getBoundingClientRect
+exports.getAbsoluteBoundingClientRect = getAbsoluteBoundingClientRect
+exports.remove = remove
+exports.append = append
+exports.attr = attr
+exports.isNode = isNode
+exports.shiftHostPageDown = shiftHostPageDown
+exports.scrollToTop = scrollToTop
diff --git a/src/sugoio/entry.js b/src/sugoio/entry.js
new file mode 100644
index 0000000..185d2ed
--- /dev/null
+++ b/src/sugoio/entry.js
@@ -0,0 +1,104 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-18
+ */
+
+var _ = require('sugo-sdk-js-utils')['default']
+var CONSTANTS = require('../../constants')
+var Sugoio = require('./Sugoio')
+var Global = require('../global')
+var onDOMContentLoaded = require('./on-dom-content-loaded')
+var Logger = require('../logger').get('Sugoio')
+
+
+/** @typedef {object} */
+var instances = {}
+
+function override_mp_init_func () {
+ /**
+ * @param {string} [token]
+ * @param {object} [config]
+ * @param {string} [name]
+ * @return {Sugoio}
+ */
+ Global.get().init = function (token, config, name) {
+ var master = Global.get()
+ if (name) {
+ // 如果传入name,则表示初始化一个子实例
+ // 并将其挂在主实例上
+ if (!master[name]) {
+ master[name] = instances[name] = Sugoio.create_sugoio(token, config, name)
+ }
+ return master[name]
+ } else {
+ var instance
+ // 没有name则初始化主实例
+ if (instances[CONSTANTS.PRIMARY_INSTANCE_NAME]) {
+ // 已经初始化直接返回
+ instance = instances[CONSTANTS.PRIMARY_INSTANCE_NAME]
+ } else if (token) {
+ // 初始化主实例
+ instance = Sugoio.create_sugoio(token, config, CONSTANTS.PRIMARY_INSTANCE_NAME)
+ instances[CONSTANTS.PRIMARY_INSTANCE_NAME] = instance
+ }
+
+ instance._ = _
+ Global.set(instance)
+ Logger.info('%s initialized', instance.name)
+ return instance
+ }
+ }
+}
+
+function init_from_snippet () {
+ var master = Global.get()
+
+ // Initialization
+ if (_.isUndefined(master)) {
+ // sugoio wasn't initialized properly, report error and quit
+ return Logger.error('"sugoio" object not initialized. Ensure you are using the latest version of the Sugoio JS Library along with the snippet we provide.')
+ }
+
+ if (master.__loaded || (master.config && master.persistence)) {
+ // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
+ return Logger.error('Sugoio library has already been downloaded at least once.')
+ }
+
+ var snippet_version = master.__SV || 0
+ if (snippet_version < 1.1) {
+ // sugoio wasn't initialized properly, report error and quit
+ return Logger.error('Version mismatch; please ensure you\'re using the latest version of the Sugoio code snippet.')
+ }
+
+ // Load instances of the Sugoio Library
+ _.each(master._i, function (item) {
+ if (item && _.isArray(item)) {
+ var name = item[item.length - 1]
+ if (instances.hasOwnProperty(name)) {
+ return Logger.error('Project '.concat(name).concat(' was initialized!'))
+ }
+ instances[name] = Sugoio.create_sugoio.apply(this, item)
+ }
+ })
+
+ override_mp_init_func()
+ master.init()
+
+ // Fire loaded events after updating the window's sugoio object
+ // 此时并没有loaded,只有在接口数据完全返回后才是loaded状态
+ // 所以需要放在track中调用
+ // _.each(instances, function (instance) {
+ // instance._loaded()
+ // })
+
+ // 在DOMContentLoaded之后触发所有实例的 _dom_loaded 方法
+ onDOMContentLoaded(function () {
+ _.each(instances, function (ins) {
+ ins._dom_loaded()
+ })
+ })
+}
+
+module.exports = {
+ init_from_snippet: init_from_snippet
+}
diff --git a/src/sugoio/events.js b/src/sugoio/events.js
new file mode 100644
index 0000000..c7a6bef
--- /dev/null
+++ b/src/sugoio/events.js
@@ -0,0 +1,166 @@
+/**
+ * Created by coin on 30/12/2016.
+ */
+
+var exports = module.exports
+var _ = require('../utils')._
+var doc = require('./dom').doc
+
+function uuid () {
+ return 'sugo_' + _.UUID().replace(/-/g, '_')
+}
+
+var MARK = { LISTENER: uuid() }
+var CssSelectorType = {
+ ID: /(?:^#\w+$)/,
+ // .a 或 .a.b..
+ CLASS: /(?:\.\w+)+/,
+ // tagName
+ TAG: /(?:[a-zA-z]+)/
+}
+
+var Hooks = {
+ type: function (selector) {
+ var type = null
+ _.each(CssSelectorType, function (r, t) {
+ return r.test(selector) && (type = t)
+ })
+ return type
+ },
+
+ isSelector: function (node, selector) {
+ if (!selector) {
+ return true
+ }
+ var handle = Hooks.hooks[this.type(selector)]
+ return _.isFunction(handle) ? handle(node, selector) : true
+ },
+
+ hooks: {
+ /**
+ * @private
+ * @return {boolean}
+ */
+ ID: function (node, str) {
+ return node.getAttribute('id') === str
+ },
+
+ /**
+ * @private
+ * @return {boolean}
+ */
+ CLASS: function (node, str) {
+ var classList = node.classList
+ if (!classList) {
+ classList = node.getAttribute('class')
+ classList = classList ? classList.split(' ') : []
+ }
+
+ var matchClassNames = str.split('.').splice(1)
+
+ return matchClassNames.some(function (className) {
+ return classList.indexOf(className) !== -1
+ })
+ },
+
+ /**
+ * @private
+ * @return {boolean}
+ */
+ TAG: function (node, str) {
+ return node.tagName.toLocaleLowerCase() === str.toLocaleLowerCase()
+ }
+ }
+}
+
+function on_base (node, type, handle, capture) {
+ node.addEventListener(type, handle, !!capture)
+ return node
+}
+
+function off_base (node, type, handle, capture) {
+ node.removeEventListener(type, handle, !!capture)
+ return node
+}
+
+function on (node, type, selector, handle, data) {
+ const listener_attr = node[MARK.LISTENER] || (node[MARK.LISTENER] = {})
+ const listener = listener_attr[type] || (listener_attr[type] = [])
+
+ listener.push(handle)
+
+ on_base(node, type, function (e) {
+ var target = closest(e.target, selector)
+ if (target) {
+ handle.call(target, e, target, data)
+ }
+ }, false)
+}
+
+function off (node, type, handle) {
+ const listener_attr = node[MARK.LISTENER] || (node[MARK.LISTENER] = {})
+
+ // off()
+ if (arguments.length === 0) {
+ type = handle = null
+ }
+ // off('click')
+ else if (arguments.length === 1) {
+ handle = null
+ }
+
+ if (type === null) {
+ _.each(listener_attr, function (_type, handles) {
+ _.each(handles, function (_handle) {
+ off(node, _type, _handle, false)
+ })
+ })
+ } else {
+ if (handle === null) {
+ handle = listener_attr[type]
+ } else {
+ handle = _.isArray(handle) ? handle : [handle]
+ }
+
+ _.each(handle, function (handle) {
+ off_base(node, type, handle, false)
+ })
+ }
+}
+
+function closest (node, selector) {
+
+ if (!selector) {
+ return node
+ }
+
+ if (node.closest) {
+ return node.closest(selector)
+ }
+
+ while (node !== doc && !Hooks.isSelector(node, selector)) {
+ node = node.parentNode
+ }
+
+ return node === doc ? null : node
+}
+
+/**
+ * from jquery-1.9.1
+ */
+function contains (a, b) {
+ var c = a.nodeType === 9 ? a.documentElement : a
+
+ return a === b || !!(b && b.nodeType === 1 && (
+ c.contains
+ ? c.contains(b)
+ : a.compareDocumentPosition && a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_CONTAINED_BY
+ ))
+}
+
+exports.on_base = on_base
+exports.off_base = off_base
+exports.on = on
+exports.off = off
+exports.closest = closest
+exports.contains = contains
diff --git a/src/sugoio/interfaces.js b/src/sugoio/interfaces.js
new file mode 100644
index 0000000..504077e
--- /dev/null
+++ b/src/sugoio/interfaces.js
@@ -0,0 +1,89 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-17
+ */
+
+/**
+ * @typedef {object} SugoIOConfigInterface
+ * @property {string} [api_host]
+ * @property {string} [app_host]
+ * @property {string} [decide_host]
+ * @property {boolean} [autotrack]
+ * @property {string} [cdn]
+ * @property {string} [encode_type] // enum { json, plain }
+ * @property {object} [dimensions]
+ * @property {boolean} [cross_subdomain_cookie]
+ * @property {string} [persistence] // enum { cookie, localStorage }
+ * @property {string} [persistence_name] // 持久化数据key
+ * @property {string} [cookie_name]
+ * @property {function} [loaded]
+ * @property {boolean} [store_google]
+ * @property {boolean} [save_referrer]
+ * @property {boolean} [test]
+ * @property {boolean} [verbose]
+ * @property {boolean} [img]
+ * @property {boolean} [track_pageview]
+ * @property {boolean} [debug]
+ * @property {number} [track_links_timeout]
+ * @property {number} [cookie_expiration]
+ * @property {boolean} [upgrade]
+ * @property {boolean} [disable_persistence]
+ * @property {boolean} [disable_cookie]
+ * @property {boolean} [secure_cookie]
+ * @property {boolean} [ip]
+ * @property {string[]} [property_blacklist]
+ *
+ * @property {string} [token]
+ * @property {string} [project_id]
+ * @property {string} [name]
+ */
+
+
+/**
+ * @typedef {object} SugoIOInstanceParams
+ * @property {string} token
+ * @property {string} name
+ * @property {SugoIOConfigInterface} config
+ */
+
+/**
+ * @typedef {Array} SugoIORawMasterPeople
+ * @property {function} toString
+ */
+
+/**
+ * People shallow methods
+ * @typedef {object} SugoIOPeopleShallow
+ * @property {function} set
+ * @property {function} set_once
+ * @property {function} increment
+ * @property {function} append
+ * @property {function} union
+ * @property {function} track_charge
+ * @property {function} clear_charges
+ * @property {function} delete_user
+ */
+
+/**
+ * 未初始化的SugoIO主对象
+ * @typedef {Array} SugoIORawMaster
+ * @property {Array} _i
+ * @property {number} _SV
+ * @property {SugoIORawMasterPeople} people
+ * @property {function(no_stub:boolean)} toString
+ *
+ *
+ * shallow methods for Sugoio & People
+ * @property {function} time_event
+ * @property {function} track
+ * @property {function} track_pageview
+ * @property {function} track_links
+ * @property {function} track_forms
+ * @property {function} register
+ * @property {function} register_once
+ * @property {function} unregister
+ * @property {function} name_tag
+ * @property {function} set_config
+ * @property {function} reset
+ * @property {SugoIOPeopleShallow} people
+ */
diff --git a/src/sugoio/on-dom-content-loaded.js b/src/sugoio/on-dom-content-loaded.js
new file mode 100644
index 0000000..9876219
--- /dev/null
+++ b/src/sugoio/on-dom-content-loaded.js
@@ -0,0 +1,57 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-18
+ */
+
+var _ = require('sugo-sdk-js-utils')['default']
+
+function doScrollCheck (callback) {
+ try {
+ document.documentElement.doScroll('left')
+ } catch (e) {
+ return setTimeout(function () {
+ doScrollCheck(callback)
+ }, 100)
+ }
+}
+
+module.exports = function (callback) {
+
+ function handle () {
+ if (handle.done) {
+ return
+ }
+ handle.done = true
+ callback()
+ }
+
+ if (document.addEventListener) {
+ if (document.readyState === 'complete') {
+ // safari 4 can fire the DOMContentLoaded event before loading all
+ // external JS (including this file). you will see some copypasta
+ // on the internet that checks for 'complete' and 'loaded', but
+ // 'loaded' is an IE thing
+ handle()
+ } else {
+ document.addEventListener('DOMContentLoaded', handle, false)
+ }
+ } else if (document.attachEvent) {
+ // IE
+ document.attachEvent('onreadystatechange', handle)
+
+ // check to make sure we arn't in a frame
+ var toplevel = false
+ try {
+ toplevel = window.frameElement === null
+ } catch (e) {
+ // noop
+ }
+
+ if (document.documentElement.doScroll && toplevel) {
+ doScrollCheck(handle)
+ }
+ }
+
+ // fallback handler, always will work
+ _.register_event(window, 'load', handle, true)
+}
\ No newline at end of file
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..2c65447
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,20 @@
+/**
+ * @Author sugo.io
+ * @Date 17-11-14
+ */
+const utils = require('sugo-sdk-js-utils')
+
+/**
+ * 获取当前页面路径
+ * @param {*} enable_hash
+ */
+const getCurrentUrl = (enable_hash) => {
+ const hash = enable_hash === true ? location.hash : ''
+ return (location.origin || (location.protocol + '//' + location.host)) + location.pathname + hash
+}
+
+module.exports = {
+ _: utils['default'],
+ getCurrentUrl,
+ userAgent: window.navigator.userAgent
+}
diff --git a/src/wx-mini.program.js b/src/wx-mini.program.js
new file mode 100644
index 0000000..d2da42e
--- /dev/null
+++ b/src/wx-mini.program.js
@@ -0,0 +1,489 @@
+// 微信小程序sdk上报
+
+let GlOBAL_CONFIG = {
+ project_id: '', // 项目ID
+ token: '', // 项目token
+ api_host: '', // 拉取维度服务器地址
+ gateway_host: '', // 数据上报网关地址
+ prefix: '_sugoio_',
+ dimPrefix: '_sugoio_dimensions_',
+ version: '1.0.0',
+ track_share_app: false, // 转发
+ track_pull_down_fresh: false, // 下拉
+ track_reach_bottom: false, // 上拉
+ track_auto_duration: true,
+ debug: false
+}
+
+const SUGO_EVENT_TIME_KEY = 'sugo_event_time'
+const SUGO_REGISTER_KEY = 'sugo_register'
+
+function getNetworkType(callbak) {
+ wx.getNetworkType({
+ success: function (res) {
+ callbak(res.networkType)
+ }
+ })
+}
+
+function getSystemInfo() {
+ const info = wx.getSystemInfoSync()
+ return {
+ device_brand: encodeURIComponent(info.brand),
+ device_model: encodeURIComponent(info.model),
+ system_name: encodeURIComponent(info.platform),
+ system_version: info.system,
+ app_name: 'wx-mini-program',
+ app_version: info.version,
+ screen_dpi: info.pixelRatio,
+ // screen_height: info.screenHeight,
+ // screen_width: info.screenWidth
+ screen_pixel: `${info.screenWidth}*${info.screenHeight}`
+ }
+}
+
+function getUID() {
+ try {
+ return wx.getStorageSync(GlOBAL_CONFIG.prefix + 'auid')
+ } catch (e) {
+ // console.log(e)
+ }
+}
+
+function setUID() {
+ try {
+ const uid = sugoio._.shortid()
+ wx.setStorageSync(GlOBAL_CONFIG.prefix + 'auid', uid)
+ return uid
+ } catch (e) {
+ // console.log(e)
+ }
+}
+
+function getPagePath() {
+ try {
+ let pages = getCurrentPages(), path = '/'
+ 0 < pages.length && (path = pages.pop().__route__)
+ return path
+ } catch (e) {
+ console.log('get current page path error:' + e)
+ }
+}
+
+function timestamp() {
+ Date.now = Date.now || function () {
+ return +new Date()
+ }
+ return Date.now()
+}
+
+function getMainInfo() {
+ let info = {
+ //page_name
+ //path_name
+ //event_id
+ //event_name
+ //event_type
+ //session_id
+ event_time: timestamp(),
+ sugo_lib: 'wechat.mini',
+ // current_url: getPagePath(),
+ path_name: getPagePath()
+ }
+ info.distinct_id = function () {
+ let uid = getUID()
+ uid || (uid = setUID())
+ return uid
+ }()
+ return info
+}
+
+function getBasicInfo() {
+ const info = getSystemInfo()
+ getNetworkType(function (network) {
+ try {
+ wx.setStorageSync(GlOBAL_CONFIG.prefix + 'network', network)
+ } catch (e) {
+ // console.log(e)
+ }
+ })
+ info.network = wx.getStorageSync(GlOBAL_CONFIG.prefix + 'network') || '4g'
+ return info
+}
+
+function getExtentInfo() {
+ let userInfo = sugoio.Data.userInfo
+ let infos = [], key
+ for (key in userInfo) userInfo.hasOwnProperty(key) && infos.push(key + '=' + userInfo[key])
+ userInfo = infos.join(';')
+ return {
+ app_name: 'wx' //,
+ // ext: 'v=' + DEF_CONFIG.version + (null !== userInfo && '' !== userInfo ? ';ui=' + encodeURIComponent(userInfo) : '')
+ }
+}
+
+function setServerDimensions() {
+ let uri = GlOBAL_CONFIG.api_host + '/api/sdk-wx-mini/dimensions?'
+ + 'app_version=' + GlOBAL_CONFIG.app_version
+ + '&project_id=' + GlOBAL_CONFIG.project_id
+ + '&token=' + GlOBAL_CONFIG.token
+ wx.request({
+ url: uri,
+ success: function (res) {
+ try {
+ wx.setStorageSync(GlOBAL_CONFIG.dimPrefix + GlOBAL_CONFIG.token, res.data.result)
+ } catch (e) {
+ // console.log(e)
+ }
+ }
+ })
+}
+
+function getServerDimensions() {
+ try {
+ return wx.getStorageSync(GlOBAL_CONFIG.dimPrefix + GlOBAL_CONFIG.token)
+ } catch (e) {
+ // console.log(e)
+ }
+}
+
+function removeEventTime(eventName) {
+ try {
+ let eventTimeObj = wx.getStorageSync(SUGO_EVENT_TIME_KEY)
+ const res = eventTimeObj ? eventTimeObj[eventName] : 0
+ if (res) {
+ delete eventTimeObj[eventName]
+ wx.setStorageSync(SUGO_EVENT_TIME_KEY, eventTimeObj)
+ }
+ return res || ''
+ } catch (e) {
+ // console.log(e)
+ }
+}
+
+function setRegisterObj(obj, once) {
+ let oldRegisterObj = wx.getStorageSync(SUGO_REGISTER_KEY)
+ let newRegisterObj = {}
+ if (once) {
+ newRegisterObj = {
+ ...obj,
+ ...oldRegisterObj
+ }
+ } else {
+ newRegisterObj = {
+ ...oldRegisterObj,
+ ...obj
+ }
+ }
+ try {
+ wx.setStorageSync(SUGO_REGISTER_KEY, newRegisterObj)
+ } catch (e) {
+ // console.log(e)
+ }
+}
+
+function getRegisterObj() {
+ try {
+ return wx.getStorageSync(SUGO_REGISTER_KEY)
+ } catch (e) {
+ // console.log(e)
+ }
+}
+
+function removeRegister(key) {
+ let oldRegisterObj = wx.getStorageSync(SUGO_REGISTER_KEY)
+ if (oldRegisterObj[key]) {
+ delete oldRegisterObj[key]
+ try {
+ wx.setStorageSync(SUGO_REGISTER_KEY, oldRegisterObj)
+ } catch (e) {
+ // console.log(e)
+ }
+ }
+}
+
+const sugoio = {
+ _: {
+ isArray: Array.isArray || function (obj) {
+ return Object.prototype.toString.call(obj) === '[object Array]'
+ },
+ isObject: (obj) => {
+ return (Object.prototype.toString.call(obj) === '[object Object]') && (obj != null)
+ },
+ has: (obj, key) => {
+ // obj 不能为 null 或者 undefined
+ return obj !== null && hasOwnProperty.call(obj, key)
+ },
+ shortid: () => {
+ let d = new Date().getTime()
+ const result = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+ const r = (d + Math.random() * 16) % 16 | 0
+ d = Math.floor(d / 16)
+ return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16)
+ })
+ return result
+ },
+ encode: (dimensions, serverDimensions) => {
+ if (!serverDimensions || serverDimensions.length < 1) {
+ return null
+ }
+ const EVENTS_MAPS = {
+ click: '点击',
+ change: '改变',
+ // submit: '提交',
+ focus: '对焦',
+ view: '浏览',
+ duration: '停留',
+ pageloading: '加载',
+ first_visit: '首次访问',
+ first_login: '首次登录'
+ }
+ const HEADER_SPLIT = '\x02' //\002
+ const CONTENT_SPLIT = '\x01' //\001
+ const DRUID_COLUMN_TYPE = { 0: 'l', 1: 'f', 2: 's', 3: 's', 4: 'd', 5: 'i', 6: 's', 7: 'f', 8: 'f' }
+ let keys = [], vals = [], removeDimensions = []
+ for (const dim in dimensions) {
+ const exists = serverDimensions.find(function (ser) { return ser.name === dim })
+ if (exists) {
+ keys.push(DRUID_COLUMN_TYPE[exists.type] + '|' + dim)
+ const val = dim === 'event_type' ? EVENTS_MAPS[dimensions[dim]] : dimensions[dim]
+ vals.push(val)
+ } else {
+ removeDimensions.push(dim)
+ }
+ }
+ return {
+ data: keys.join(',') + HEADER_SPLIT + vals.join(CONTENT_SPLIT),
+ removeDimensions
+ }
+ },
+ utf8Encode: (string) => {
+ string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
+ var utftext = '', start, end
+ var stringl = 0, n
+ start = end = 0
+ stringl = string.length
+ for (n = 0; n < stringl; n++) {
+ var c1 = string.charCodeAt(n)
+ var enc = null
+ if (c1 < 128) {
+ end++
+ } else if ((c1 > 127) && (c1 < 2048)) {
+ enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128)
+ } else {
+ enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128)
+ }
+ if (enc !== null) {
+ if (end > start) {
+ utftext += string.substring(start, end)
+ }
+ utftext += enc
+ start = end = n + 1
+ }
+ }
+ if (end > start) {
+ utftext += string.substring(start, string.length)
+ }
+ return utftext
+ },
+ base64Encode: (data) => {
+ var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
+ var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = '', tmp_arr = []
+ if (!data) {
+ return data
+ }
+ data = sugoio._.utf8Encode(data)
+ do { // pack three octets into four hexets
+ o1 = data.charCodeAt(i++)
+ o2 = data.charCodeAt(i++)
+ o3 = data.charCodeAt(i++)
+ bits = o1 << 16 | o2 << 8 | o3
+ h1 = bits >> 18 & 0x3f
+ h2 = bits >> 12 & 0x3f
+ h3 = bits >> 6 & 0x3f
+ h4 = bits & 0x3f
+ // use hexets to index into b64, and append result to encoded string
+ tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4)
+ } while (i < data.length)
+ enc = tmp_arr.join('')
+ switch (data.length % 3) {
+ case 1:
+ enc = enc.slice(0, -2) + '=='
+ break
+ case 2:
+ enc = enc.slice(0, -1) + '='
+ break
+ }
+ return enc
+ }
+ },
+ App: {
+ init: (opts) => {
+ if (!('api_host' in opts) || !('gateway_host' in opts) || !opts.project_id || !opts.token) {
+ throw new Error('project_id, token, api_host, gateway_host为必设参数.')
+ }
+ const systemInfo = getSystemInfo()
+ GlOBAL_CONFIG = {
+ ...GlOBAL_CONFIG,
+ ...opts,
+ app_version: systemInfo.app_version
+ }
+ setServerDimensions()
+ }
+ },
+ Page: {
+ init: () => {
+ setServerDimensions()
+ const pages = getCurrentPages()
+ const currentPage = pages[pages.length - 1]
+ currentPage.onShow && !function () {
+ var func = currentPage.onShow
+ currentPage.onShow = function () {
+ sugoio.track('浏览', {
+ // current_url: currentPage.__route__,
+ event_type: 'view'
+ })
+ if (GlOBAL_CONFIG.track_auto_duration) {
+ sugoio.time_event('停留')
+ }
+ func.call(this, arguments)
+ }
+ }()
+
+ GlOBAL_CONFIG.track_auto_duration && currentPage.onHide && !function () {
+ const func = currentPage.onHide
+ currentPage.onHide = function () {
+ sugoio.track('停留', {})
+ func.call(this, arguments)
+ }
+ }()
+
+ GlOBAL_CONFIG.track_pull_down_fresh && currentPage.onPullDownRefresh && ! function () {
+ const func = currentPage.onPullDownRefresh
+ currentPage.onPullDownRefresh = function () {
+ sugoio.track(GlOBAL_CONFIG.prefix + 'pulldownfresh', {
+ // current_url: currentPage.__route__,
+ event_type: 'onPullDownRefresh'
+ })
+ func.call(this, arguments)
+ }
+ }()
+ GlOBAL_CONFIG.track_reach_bottom && currentPage.onReachBottom && ! function () {
+ const func = currentPage.onReachBottom
+ currentPage.onReachBottom = function () {
+ sugoio.track(GlOBAL_CONFIG.prefix + 'reachbottom', {
+ // current_url: currentPage.__route__,
+ event_type: 'onReachBottom'
+ })
+ func.call(this, arguments)
+ }
+ }()
+ GlOBAL_CONFIG.track_share_app && currentPage.onShareAppMessage && ! function () {
+ const func = currentPage.onShareAppMessage
+ currentPage.onShareAppMessage = function () {
+ sugoio.track(GlOBAL_CONFIG.prefix + 'shareapp', {
+ // current_url: currentPage.__route__,
+ event_type: 'onShareAppMessage'
+ })
+ return func.call(this, arguments)
+ }
+ }()
+ }
+ },
+ track: (event_name, properties, callback) => {
+ let results = [], mainInfo = getMainInfo(), extInfo = getExtentInfo(), basicInfo = getBasicInfo()
+ mainInfo.event_type = properties.event_type || 'click'
+ mainInfo.event_name = event_name
+ mainInfo.event_id = GlOBAL_CONFIG.event_id || sugoio._.shortid()
+ mainInfo.token = GlOBAL_CONFIG.token
+ let props = 'undefined' === typeof properties ? {} : properties
+ for (const k in props) {
+ mainInfo[k] = props[k]
+ }
+ for (const k in extInfo) {
+ mainInfo[k] = extInfo[k]
+ }
+ for (const k in basicInfo) {
+ mainInfo[k] = basicInfo[k]
+ }
+
+ // 处理停留时间
+ var start_timestamp = removeEventTime(event_name)
+ if (start_timestamp) {
+ const duration_in_ms = new Date().getTime() - start_timestamp
+ mainInfo.duration = parseFloat((duration_in_ms / 1000).toFixed(2))
+ mainInfo.event_type = 'duration' //增加停留事件类型
+ }
+
+ // 处理register
+ const registerObj = getRegisterObj()
+ if (registerObj) {
+ mainInfo = {
+ ...mainInfo,
+ ...registerObj
+ }
+ }
+
+ results = [
+ 'locate=' + GlOBAL_CONFIG.project_id,
+ 'token=' + GlOBAL_CONFIG.token
+ ]
+ const serverDimensions = getServerDimensions()
+ if (!serverDimensions) {
+ console.log('track faild: no server dimenions')
+ return
+ }
+ const res = sugoio._.encode(mainInfo, serverDimensions)
+ if (!res) {
+ console.log('track failed: empty encode data')
+ return
+ }
+
+ if (GlOBAL_CONFIG.debug) {
+ console.log('reportData:', mainInfo)
+ }
+
+ const data = sugoio._.base64Encode(res.data)
+ if (res.removeDimensions.length) {
+ console.log('warn: excluded dimensions =>', res.removeDimensions)
+ }
+
+ wx.request({
+ url: `${GlOBAL_CONFIG.gateway_host}/post?${results.join('&')}`,
+ method: 'post',
+ data,
+ header: {
+ 'content-type': 'text/plain;charset=UTF-8'
+ },
+ complete: function () {
+ if (callback) {
+ callback()
+ }
+ }
+ })
+ },
+ Data: {
+ userInfo: null,
+ lanchInfo: null
+ },
+ time_event: (eventName) => {
+ try {
+ return wx.setStorageSync(SUGO_EVENT_TIME_KEY, {
+ [eventName]: new Date().getTime()
+ })
+ } catch (e) {
+ // console.log(e)
+ }
+ },
+ register_once: (obj) => {
+ setRegisterObj(obj, true)
+ },
+ register: (obj) => {
+ setRegisterObj(obj, false)
+ },
+ unregister: (key) => {
+ removeRegister(key)
+ }
+}
+
+module.exports = sugoio
diff --git a/sugoio-jslib-snippet.js b/sugoio-jslib-snippet.js
new file mode 100644
index 0000000..93a2a87
--- /dev/null
+++ b/sugoio-jslib-snippet.js
@@ -0,0 +1,225 @@
+/*!
+ * sugo-sdk-javascript JavaScript Lib v1.3.13
+ * http://sugo.io
+ * Date: 2017-11-28
+ */
+
+var SUGOIO_LIB_URL = '//localhost:8000/_bc/sugo-sdk-js/libs/sugoio-latest.min.js';
+
+(function (document, sugoio) {
+ // Only stub out if this is the first time running the snippet.
+ if (sugoio.__SV) return
+
+ var win = window
+ var script
+ var first_script
+ var functions
+ var lib_name = 'sugoio'
+
+ win[lib_name] = sugoio
+
+ try {
+ store(win.location.hash.replace('#', ''))
+ } catch (e) {
+ } finally {
+ capture(sugoio)
+ }
+
+ /**
+ * 保存参数到session
+ * 此处代码不必支持ie8
+ * @param {string} base64
+ */
+ function store (base64) {
+ var str = win.atob(base64)
+ var json = JSON.parse(str)
+ var state = json.state
+ var params = {
+ accessToken: state.access_token,
+ accessTokenExpiresAt: Date.now() + Number(state.expires_in) * 1000,
+ projectToken: state.token,
+ projectId: state.project_id,
+ userId: state.user_id,
+ choosePage: state.choose_page
+ }
+ // 存入参数
+ win.sessionStorage.setItem('editorParams', JSON.stringify(params))
+
+ // 还原用户原有hash
+ if (state['hash']) {
+ win.location.hash = state['hash']
+ } else if (win.history) {
+ win.history.replaceState('', document.title, win.location.pathname + win.location.search) // completely remove hash
+ } else {
+ win.location.hash = '' // clear hash (but leaves # unfortunately)
+ }
+ }
+
+ /**
+ * 兼容ie事件绑定
+ * @param {Element} target
+ * @param {string} type
+ * @param {Function} handle
+ */
+ function listen (target, type, handle) {
+ if (typeof target.addEventListener === 'function') {
+ return target.addEventListener(type, handle, true)
+ }
+ return target.attachEvent('on' + type, handle)
+ }
+
+ /**
+ * @param {Array} arr
+ * @param {Function} iterator
+ */
+ function forEach (arr, iterator) {
+ for (var i = 0, len = arr.length; i < len; i++) {
+ iterator(arr[i], i, arr)
+ }
+ }
+
+ /**
+ * 代理所有事件到document上,以便埋点代码控制
+ * 埋点代碼可以重寫hooks[name]
+ * 为了优先级,一律使用捕获模型
+ * @param {Object} target
+ * @example
+ * sugoio.proxy('focus', function(event){
+ * if (event.target is SDK children) {
+ * event.stopPropagation()
+ * }
+ * })
+ *
+ * sugoio.off('focus', function reference)
+ */
+ function capture (target) {
+ var hooks = {}
+ var doc = document
+ var events = ('blur focus focusin focusout load resize scroll unload click dblclick ' +
+ 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' +
+ 'change select submit keydown keypress keyup error contextmenu').split(' ')
+
+ target.proxy = {
+ proxy: proxy,
+ off: off
+ }
+
+ forEach(events, function (type) {
+ hooks[type] = []
+ listen(doc, type, function (event) {
+ try {
+ forEach(hooks[type], function (handle) {
+ handle(event)
+ })
+ } catch (e) {
+ if (console && typeof console.error === 'function') {
+ console.error(e.stack)
+ }
+ }
+ })
+ })
+
+ /**
+ * 代理事件
+ * @param {string} type
+ * @param {Function} handle
+ */
+ function proxy (type, handle) {
+ var listeners = hooks[type] || (hooks[type] = [])
+ listeners.push(handle)
+ return target.proxy
+ }
+
+ /**
+ *
+ * @param type
+ * @param handle
+ * @return {proxy}
+ */
+ function off (type, handle) {
+ var listeners = hooks[type] || []
+ var next = []
+
+ forEach(listeners, function (fn) {
+ if (fn !== handle) {
+ next.push(fn)
+ }
+ })
+
+ listeners[type] = next
+
+ return target.proxy
+ }
+ }
+
+ // initialize
+ sugoio._i = []
+ sugoio.init = function (token, config, name) {
+ // support multiple sugoio instances
+ var target = sugoio
+ if (typeof(name) !== 'undefined') {
+ target = sugoio[name] = []
+ } else {
+ name = lib_name
+ }
+
+ // Pass in current people object if it exists
+ target.people = target.people || []
+ target.toString = function (no_stub) {
+ var str = lib_name
+ if (name !== lib_name) {
+ str += '.' + name
+ }
+ if (!no_stub) {
+ str += ' (stub)'
+ }
+ return str
+ }
+
+ target.people.toString = function () {
+ // 1 instead of true for minifying
+ return target.toString(1) + '.people (stub)'
+ }
+
+ function _set_and_defer (target, fn) {
+ var split = fn.split('.')
+ if (split.length === 2) {
+ target = target[split[0]]
+ fn = split[1]
+ }
+ target[fn] = function () {
+ target.push([fn].concat(Array.prototype.slice.call(arguments, 0)))
+ }
+ }
+
+ // 删除无用的shallow
+ // create shallow clone of the public sugoio interface
+ functions = "time_event track track_pageview register register_once unregister set_config".split(' ')
+
+ forEach(functions, function (fn) {
+ _set_and_defer(target, fn)
+ })
+
+ // register sugoio instance
+ sugoio._i.push([token, config, name])
+ }
+
+ // Snippet version, used to fail on new features w/ old snippet
+ sugoio.__SV = 1.2
+
+ script = document.createElement("script")
+ script.type = "text/javascript"
+ script.async = true
+
+ if (typeof SUGOIO_CUSTOM_LIB_URL !== 'undefined') {
+ script.src = SUGOIO_CUSTOM_LIB_URL
+ } else if (win.location.protocol === 'file:' && SUGOIO_LIB_URL.match(/^\/\//)) {
+ script.src = 'https:' + SUGOIO_LIB_URL
+ } else {
+ script.src = SUGOIO_LIB_URL
+ }
+
+ first_script = document.getElementsByTagName("script")[0]
+ first_script.parentNode.insertBefore(script, first_script)
+
+})(document, window.sugoio || [])
diff --git a/sugoio-jslib-snippet.min.js b/sugoio-jslib-snippet.min.js
new file mode 100644
index 0000000..86114b5
--- /dev/null
+++ b/sugoio-jslib-snippet.min.js
@@ -0,0 +1,6 @@
+/*!
+ * sugo-sdk-javascript JavaScript Lib v1.3.13
+ * http://sugo.io
+ * Date: 2017-11-28
+ */
+var SUGOIO_LIB_URL="//localhost:8000/_bc/sugo-sdk-js/libs/sugoio-latest.min.js";!function(e,o){function t(e,o){for(var t=0,n=e.length;t
+
+
+ sugoio | Clients
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Last modification: 2:10 pm - 12.06.2014
+
Clients
+
+ All clients need to be verified before you can send email and
+ set a project.
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tellus Institute
+
+
+ Finibus Bonorum et Malorum" (The Extremes of Good and
+ Evil) by Cicero,written in 45 BC. This book is a treatise
+ on.
+
+
+
Active project completion with: 48%
+
+
+
+
+
+
+
+
Penatibus Consulting
+
+
+ There are many variations of passages of Lorem Ipsum
+ available, but the majority have suffered alteration in
+ some.
+
+
+
Active project completion with: 22%
+
+
+
+
+
+
+
+
Ultrices Incorporated
+
+
+ Many desktop publishing packages and web page editors now
+ use Lorem Ipsum as their default model text.
+
+
+
Active project completion with: 72%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..069ec7e
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,135 @@
+/**
+ * @Author sugo.io
+ * @Date 17-9-15
+ */
+const path = require('path')
+const webpack = require('webpack')
+const pkg = require('./package.json')
+const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
+const _ = require('lodash')
+
+console.log('============building version', pkg.version, '============')
+const isProduction = process.env.NODE_ENV === 'production'
+
+const output = {
+ path: path.resolve(__dirname, './build'),
+ filename: '[name].js'
+}
+
+// web-sdk编译
+let webSDKCompile = {
+ entry: {
+ 'sugo-sdk': './src/loader-globals.js'
+ },
+ output,
+ module: {
+ loaders: [
+ {
+ test: /\.vue$/,
+ loader: 'vue-loader',
+ exclude: /node_modules/
+ },
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ exclude: /node_modules/
+ },
+ {
+ test: /\.css$/,
+ loader: 'style-loader!css-loader'
+ },
+ {
+ test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 1024 * 20,
+ name: '[name].[ext]?[hash]'
+ }
+ }
+ ]
+ },
+ {
+ test: /\.(png|jpe?g|gif|svg)(\?\S*)?$/,
+ loader: 'file-loader',
+ options: {
+ limit: 1024 * 20,
+ name: '[name].[ext]?[hash]'
+ }
+ }
+ ]
+ },
+ resolve: {
+ alias: {
+ 'vue$': 'vue/dist/vue.common.js'
+ }
+ },
+ devtool: isProduction ? '' : 'source-map',
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env': {
+ 'NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+ 'SDK_VERSION': JSON.stringify(pkg.version)
+ }
+ })
+ ]
+}
+
+// 微信小程序编译
+let wxMiniSDKCompile = {
+ entry: {
+ 'wx-mini': './src/wx-mini.program.js'
+ },
+ output: {
+ ...output,
+ libraryTarget: 'commonjs2'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ exclude: /node_modules/
+ }
+ ]
+ }
+}
+
+if (isProduction) {
+ // webSDKCompile.devtool = void 0
+ // http://vue-loader.vuejs.org/en/workflow/production.html
+ webSDKCompile.plugins = (webSDKCompile.plugins || []).concat(
+ new UglifyJSPlugin({
+ uglifyOptions: {
+ compress: {
+ warnings: false,
+ properties: false,
+ computed_props: false
+ },
+ ie8: true,
+ keep_fnames: true,
+ comments: /^\/\*!/
+ }
+ })
+ )
+
+ // wxMiniSDKCompile.devtool = void 0
+ wxMiniSDKCompile.plugins = (wxMiniSDKCompile.plugins || []).concat(
+ new UglifyJSPlugin({
+ uglifyOptions: {
+ compress: {
+ warnings: false
+ },
+ output: {
+ comments: false
+ }
+ }
+ })
+ )
+}
+
+module.exports = [
+ webSDKCompile,
+ wxMiniSDKCompile
+]