sensors_analytics_sdk 1.5.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,267 @@
1
+ module SensorsAnalytics
2
+ KEY_PATTERN = /^((?!^distinct_id$|^original_id$|^time$|^properties$|^id$|^first_id$|^second_id$|^users$|^events$|^event$|^user_id$|^date$|^datetime$)[a-zA-Z_$][a-zA-Z\d_$]{0,99})$/
3
+
4
+ # Sensors Analytics SDK
5
+ #
6
+ # 通过 Sensors Analytics SDK 的构造方法及参数 Consumer 初始化对象,并通过 track, trackSignUp, profileSet 等方法向 Sensors Analytics 发送数据,例如
7
+ #
8
+ # consumer = SensorsAnalytics::DefaultConsumer.new(SENSORS_ANALYTICS_URL)
9
+ # sa = SensorsAnalytics::SensorsAnalytics.new(consumer)
10
+ # sa.track("abcdefg", "ServerStart", {"sex" => "female"})
11
+ #
12
+ # SENSORS_ANALYTICS_URL 是 Sensors Analytics 收集数据的 URI,可以从配置界面中获得。
13
+ #
14
+ # Sensors Analytics SDK 通过 Consumer 向 Sensors Analytics 发送数据,提供三种 Consumer:
15
+ #
16
+ # DefaultConsumer - 逐条同步发送数据
17
+ # BatchConsumer - 批量同步发送数据
18
+ # DebugModeConsumer - Debug 模式,用于校验数据导入是否正确
19
+ #
20
+ # Consumer 的具体信息请参看对应注释
21
+ class SensorsAnalytics
22
+
23
+ # Sensors Analytics SDK 构造函数,传入 Consumer 对象
24
+ def initialize(consumer)
25
+ @consumer = consumer
26
+ # 初始化事件公共属性
27
+ clear_super_properties
28
+ end
29
+
30
+ # 设置每个事件都带有的一些公共属性
31
+ #
32
+ # 当 track 的 Properties 和 Super Properties 有相同的 key 时,将采用 track 的
33
+ def register_super_properties(properties)
34
+ properties.each do |key, value|
35
+ @super_properties[key] = value
36
+ end
37
+ end
38
+
39
+ # 删除所有已设置的事件公共属性
40
+ def clear_super_properties
41
+ @super_properties = {
42
+ '$lib' => 'Ruby',
43
+ '$lib_version' => VERSION,
44
+ }
45
+ end
46
+
47
+ # 记录一个的事件,其中 distinct_id 为触发事件的用户ID,event_name 标示事件名称,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
48
+ def track(distinct_id, event_name, properties={})
49
+ _track_event(:track, distinct_id, distinct_id, event_name, properties)
50
+ end
51
+
52
+ # 记录注册行为,其中 distinct_id 为注册后的用户ID,origin_distinct_id 为注册前的临时ID,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
53
+ #
54
+ # 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:
55
+ #
56
+ # http://www.sensorsdata.cn/manual/track_signup.html
57
+ #
58
+ # 并在必要时联系我们的技术支持人员。
59
+ def track_signup(distinct_id, origin_distinct_id, properties={})
60
+ _track_event(:track_signup, distinct_id, origin_distinct_id, :$SignUp, properties)
61
+ end
62
+
63
+ # 设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
64
+ #
65
+ # 无论用户该属性值是否存在,都将用 properties 中的属性覆盖原有设置
66
+ def profile_set(distinct_id, properties)
67
+ _track_event(:profile_set, distinct_id, distinct_id, nil, properties)
68
+ end
69
+
70
+ # 尝试设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
71
+ #
72
+ # 若用户不存在该属性,则设置用户的属性,否则放弃
73
+ def profile_set_once(distinct_id, properties)
74
+ _track_event(:profile_set_once, distinct_id, distinct_id, nil, properties)
75
+ end
76
+
77
+ # 为用户的一个或多个属性累加一个数值,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为 Integer 类型
78
+ #
79
+ # 若该属性不存在,则创建它并设置默认值为0
80
+ def profile_increment(distinct_id, properties)
81
+ _track_event(:profile_increment, distinct_id, distinct_id, nil, properties)
82
+ end
83
+
84
+ # 追加数据至用户的一个或多个列表类型的属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为元素是 String 类型的数组
85
+ #
86
+ # 若该属性不存在,则创建一个空数组,并插入 properties 中的属性值
87
+ def profile_append(distinct_id, properties)
88
+ _track_event(:profile_append, distinct_id, distinct_id, nil, properties)
89
+ end
90
+
91
+ # 删除用户一个或多个属性,properties 是一个数组,其中每个元素描述一个需要删除的属性的 Key
92
+ def profile_unset(distinct_id, properties)
93
+ unless properties.is_a?(Array)
94
+ IllegalDataError.new("Properties of PROFILE UNSET must be an instance of Array<String>.")
95
+ end
96
+ property_hash = {}
97
+ properties.each do |key|
98
+ property_hash[key] = true
99
+ end
100
+ _track_event(:profile_unset, distinct_id, distinct_id, nil, property_hash)
101
+ end
102
+
103
+ private
104
+
105
+ def _track_event(event_type, distinct_id, origin_distinct_id, event_name, properties)
106
+ _assert_key(:DistinctId, distinct_id)
107
+ _assert_key(:OriginalDistinctId, origin_distinct_id)
108
+ if event_type == :track
109
+ _assert_key_with_regex(:EventName, event_name)
110
+ end
111
+ _assert_properties(event_type, properties)
112
+
113
+ # 从事件属性中获取时间配置
114
+ event_time = _extract_time_from_properties(properties)
115
+ properties.delete(:$time)
116
+ properties.delete("$time")
117
+
118
+ event_properties = {}
119
+ if event_type == :track || event_type == :track_signup
120
+ event_properties = @super_properties.dup
121
+ end
122
+
123
+ properties.each do |key, value|
124
+ if value.is_a?(Time)
125
+ event_properties[key] = value.strftime("%Y-%m-%d %H:%M:%S.#{(value.to_f * 1000.0).to_i % 1000}")
126
+ else
127
+ event_properties[key] = value
128
+ end
129
+ end
130
+
131
+ lib_properties = _get_lib_properties
132
+
133
+ # Track / TrackSignup / ProfileSet / ProfileSetOne / ProfileIncrement / ProfileAppend / ProfileUnset
134
+ event = {
135
+ type: event_type,
136
+ time: event_time,
137
+ distinct_id: distinct_id,
138
+ properties: event_properties,
139
+ lib: lib_properties,
140
+ }
141
+
142
+ if event_type == :track
143
+ # Track
144
+ event[:event] = event_name
145
+ elsif event_type == :track_signup
146
+ # TrackSignUp
147
+ event[:event] = event_name
148
+ event[:original_id] = origin_distinct_id
149
+ end
150
+
151
+ @consumer.send(event)
152
+ end
153
+
154
+ def _extract_time_from_properties(properties)
155
+ properties.each do |key, value|
156
+ if (key == :$time || key == "$time") && value.is_a?(Time)
157
+ return (value.to_f * 1000).to_i
158
+ end
159
+ end
160
+ return (Time.now.to_f * 1000).to_i
161
+ end
162
+
163
+ def _get_lib_properties
164
+ @lib_properties ||= begin
165
+ lib_properties = {
166
+ '$lib' => 'Ruby',
167
+ '$lib_version' => VERSION,
168
+ '$lib_method' => 'code',
169
+ }
170
+
171
+ @super_properties.each do |key, value|
172
+ if key == :$app_version || key == "$app_version"
173
+ lib_properties[:$app_version] = value
174
+ end
175
+ end
176
+
177
+ #lib_properties[:$lib_detail] = _get_lib_detail
178
+
179
+ lib_properties
180
+ end
181
+ end
182
+
183
+ def _get_lib_detail
184
+ begin
185
+ raise Exception
186
+ rescue Exception => e
187
+ trace = e.backtrace[3].split(':')
188
+ file = trace[0]
189
+ line = trace[1]
190
+ function = trace[2].split('`')[1][0..-2]
191
+ return "###{function}###{file}###{line}"
192
+ end
193
+ end
194
+
195
+ def _assert_key(type, key)
196
+ unless key.instance_of?(String) || key.instance_of?(Symbol)
197
+ raise IllegalDataError.new("#{type} must be an instance of String / Symbol.")
198
+ end
199
+ unless key.length >= 1
200
+ raise IllegalDataError.new("#{type} is empty.")
201
+ end
202
+ unless key.length <= 255
203
+ raise IllegalDataError.new("#{type} is too long, max length is 255.")
204
+ end
205
+ end
206
+
207
+ def _assert_key_with_regex(type, key)
208
+ _assert_key(type, key)
209
+ unless key =~ KEY_PATTERN
210
+ raise IllegalDataError.new("#{type} '#{key}' is invalid.")
211
+ end
212
+ end
213
+
214
+ def _assert_properties(event_type, properties)
215
+ unless properties.instance_of?(Hash)
216
+ raise IllegalDataError.new("Properties must be an instance of Hash.")
217
+ end
218
+ properties.each do |key, value|
219
+ _assert_key_with_regex(:PropertyKey, key)
220
+
221
+ unless value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Array) || value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(Time)
222
+ raise IllegalDataError.new("The properties value must be an instance of Integer/Float/String/Array.")
223
+ end
224
+
225
+ # 属性为 Array 时,元素必须为 String 或 Symbol 类型
226
+ if value.is_a?(Array)
227
+ value.each do |element|
228
+ unless element.is_a?(String) || element.is_a?(Symbol)
229
+ raise IllegalDataError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
230
+ end
231
+ # 元素的长度不能超过8192
232
+ unless element.length <= 8192
233
+ raise IllegalDataError.new("The properties value is too long.")
234
+ end
235
+ end
236
+ end
237
+
238
+ # 属性为 String 或 Symbol 时,长度不能超过8191
239
+ if value.is_a?(String) || value.is_a?(Symbol)
240
+ unless value.length <= 8192
241
+ raise IllegalDataError.new("The properties value is too long.")
242
+ end
243
+ end
244
+
245
+ # profile_increment 的属性必须为数值类型
246
+ if event_type == :profile_increment
247
+ unless value.is_a?(Integer)
248
+ raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Integer.")
249
+ end
250
+ end
251
+
252
+ # profile_append 的属性必须为数组类型,且数组元素必须为字符串
253
+ if event_type == :profile_append
254
+ unless value.is_a?(Array)
255
+ raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
256
+ end
257
+ value.each do |element|
258
+ unless element.is_a?(String) || element.is_a?(Symbol)
259
+ raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ end
267
+ end
@@ -0,0 +1,129 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'zlib'
4
+
5
+ module SensorsAnalytics
6
+ class SensorsAnalyticsConsumer
7
+ def initialize(server_url)
8
+ @http_client = Http.new(server_url, keep_alive: true)
9
+ end
10
+
11
+ def request!(event_list, headers = {})
12
+ unless event_list.is_a?(Array) && headers.is_a?(Hash)
13
+ raise IllegalDataError.new("The argument of 'request!' should be a Array.")
14
+ end
15
+
16
+ # GZip && Base64 encode
17
+ wio = StringIO.new("w")
18
+ gzip_io = Zlib::GzipWriter.new(wio)
19
+ gzip_io.write(event_list.to_json)
20
+ gzip_io.close
21
+ data = Base64.encode64(wio.string).gsub("\n", '')
22
+ form_data = {"data_list" => data, "gzip" => 1}
23
+
24
+ @http_client.request(form_data, headers)
25
+ end
26
+ end
27
+
28
+ # 实现逐条、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI
29
+ class DefaultConsumer < SensorsAnalyticsConsumer
30
+
31
+ def initialize(server_url)
32
+ super(server_url)
33
+ end
34
+
35
+ def send(event)
36
+ event_list = [event]
37
+
38
+ begin
39
+ response_code, response_body = request!(event_list)
40
+ rescue => e
41
+ raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
42
+ end
43
+
44
+ unless response_code.to_i == 200
45
+ raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ # 实现批量、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI 和批量发送的缓存大小
52
+ class BatchConsumer < SensorsAnalyticsConsumer
53
+
54
+ MAX_FLUSH_BULK = 50
55
+
56
+ def initialize(server_url, flush_bulk = MAX_FLUSH_BULK)
57
+ @event_buffer = []
58
+ @flush_bulk = [flush_bulk, MAX_FLUSH_BULK].min
59
+ super(server_url)
60
+ end
61
+
62
+ def send(event)
63
+ @event_buffer << event
64
+ flush if @event_buffer.length >= @flush_bulk
65
+ end
66
+
67
+ def flush
68
+ @event_buffer.each_slice(@flush_bulk) do |event_list|
69
+ begin
70
+ response_code, response_body = request!(event_list)
71
+ rescue => e
72
+ raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
73
+ end
74
+
75
+ unless response_code.to_i == 200
76
+ raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
77
+ end
78
+ end
79
+ @event_buffer = []
80
+ end
81
+
82
+ end
83
+
84
+ # Debug 模式的 Consumer,Debug 模式的具体信息请参考文档
85
+ #
86
+ # http://www.sensorsdata.cn/manual/debug_mode.html
87
+ #
88
+ # write_data 参数为 true,则 Debug 模式下导入的数据会导入 Sensors Analytics;否则,Debug 模式下导入的数据将只进行格式校验,不会导入 Sensors Analytics 中
89
+ class DebugConsumer < SensorsAnalyticsConsumer
90
+
91
+ def initialize(server_url, write_data)
92
+ uri = URI.parse(server_url)
93
+ # 将 URL Path 替换成 Debug 模式的 '/debug'
94
+ uri.path = '/debug'
95
+
96
+ @headers = {}
97
+ unless write_data
98
+ @headers['Dry-Run'] = 'true'
99
+ end
100
+
101
+ super(uri.to_s)
102
+ end
103
+
104
+ def send(event)
105
+ event_list = [event]
106
+
107
+ begin
108
+ response_code, response_body = request!(event_list, @headers)
109
+ rescue => e
110
+ raise DebugModeError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
111
+ end
112
+
113
+ puts "=========================================================================="
114
+
115
+ if response_code.to_i == 200
116
+ puts "valid message: #{event_list.to_json}"
117
+ else
118
+ puts "invalid message: #{event_list.to_json}"
119
+ puts "response code: #{response_code}"
120
+ puts "response body: #{response_body}"
121
+ end
122
+
123
+ if response_code.to_i >= 300
124
+ raise DebugModeError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
125
+ end
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,22 @@
1
+ module SensorsAnalytics
2
+ # Sensors Analytics SDK 的异常
3
+ # 使用 Sensors Analytics SDK 时,应该捕获 IllegalDataError、ConnectionError 和 ServerError。Debug 模式下,会抛出 DebugModeError 用于校验数据导入是否正确,线上运行时不需要捕获 DebugModeError
4
+ class SensorsAnalyticsError < StandardError
5
+ end
6
+
7
+ # 输入数据格式错误,如 Distinct Id、Event Name、Property Keys 不符合命名规范,或 Property Values 不符合数据类型要求
8
+ class IllegalDataError < SensorsAnalyticsError
9
+ end
10
+
11
+ # 网络连接错误
12
+ class ConnectionError < SensorsAnalyticsError
13
+ end
14
+
15
+ # 服务器返回导入失败
16
+ class ServerError < SensorsAnalyticsError
17
+ end
18
+
19
+ # Debug模式下各种异常
20
+ class DebugModeError < SensorsAnalyticsError
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ require 'net/http'
2
+
3
+ begin
4
+ require 'net/http/persistent'
5
+ rescue LoadError
6
+ # do nothing
7
+ end
8
+
9
+ module SensorsAnalytics
10
+ class Http
11
+ def initialize(server_url, options = {})
12
+ @uri = _get_uri(server_url)
13
+ @keep_alive = options[:keep_alive] && defined?(Net::HTTP::Persistent)
14
+ end
15
+
16
+ def request(form_data, headers = {})
17
+ init_header = {"User-Agent" => "SensorsAnalytics Ruby SDK"}
18
+ headers.each do |key, value|
19
+ init_header[key] = value
20
+ end
21
+
22
+ request = Net::HTTP::Post.new(@uri.request_uri, init_header)
23
+ request.set_form_data(form_data)
24
+
25
+ response = do_request(request)
26
+ [response.code, response.body]
27
+ end
28
+
29
+ private
30
+
31
+ def do_request(request)
32
+ if @keep_alive
33
+ @client ||= begin
34
+ client = Net::HTTP::Persistent.new name: "sa_sdk"
35
+ client.open_timeout = 10
36
+ client
37
+ end
38
+ @client.request(@uri, request)
39
+ else
40
+ client = Net::HTTP.new(@uri.host, @uri.port)
41
+ client.open_timeout = 10
42
+ client.continue_timeout = 10
43
+ client.read_timeout = 10
44
+ client.request(request)
45
+ end
46
+ end
47
+
48
+ def _get_uri(url)
49
+ begin
50
+ URI.parse(url)
51
+ rescue URI::InvalidURIError
52
+ host = url.match(".+\:\/\/([^\/]+)")[1]
53
+ uri = URI.parse(url.sub(host, 'dummy-host'))
54
+ uri.instance_variable_set('@host', host)
55
+ uri
56
+ end
57
+ end
58
+ end
59
+ end