sensors_analytics_sdk 1.5.0 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
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
+ if @uri.scheme == "https"
37
+ client.use_ssl = true
38
+ end
39
+ client
40
+ end
41
+ @client.request(@uri, request)
42
+ else
43
+ client = Net::HTTP.new(@uri.host, @uri.port)
44
+ client.open_timeout = 10
45
+ client.continue_timeout = 10
46
+ client.read_timeout = 10
47
+ if @uri.scheme == "https"
48
+ client.use_ssl = true
49
+ end
50
+ client.request(request)
51
+ end
52
+ end
53
+
54
+ def _get_uri(url)
55
+ begin
56
+ URI.parse(url)
57
+ rescue URI::InvalidURIError
58
+ host = url.match(".+\:\/\/([^\/]+)")[1]
59
+ uri = URI.parse(url.sub(host, 'dummy-host'))
60
+ uri.instance_variable_set('@host', host)
61
+ uri
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module SensorsAnalytics
3
+ VERSION = '1.6.2'
4
+ end
@@ -1,443 +1,10 @@
1
- require 'base64'
2
- require 'json'
3
- require 'net/http'
4
- require 'zlib'
5
1
 
6
- module SensorsAnalytics
7
-
8
- VERSION = '1.5.0'
2
+ require 'sensors_analytics/version'
3
+ require 'sensors_analytics/errors'
4
+ require 'sensors_analytics/http'
5
+ require 'sensors_analytics/consumers'
6
+ require 'sensors_analytics/client'
9
7
 
10
- 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})$/
11
-
12
- # Sensors Analytics SDK 的异常
13
- # 使用 Sensors Analytics SDK 时,应该捕获 IllegalDataError、ConnectionError 和 ServerError。Debug 模式下,会抛出 DebugModeError 用于校验数据导入是否正确,线上运行时不需要捕获 DebugModeError
14
- class SensorsAnalyticsError < StandardError
15
- end
16
-
17
- # 输入数据格式错误,如 Distinct Id、Event Name、Property Keys 不符合命名规范,或 Property Values 不符合数据类型要求
18
- class IllegalDataError < SensorsAnalyticsError
19
- end
20
-
21
- # 网络连接错误
22
- class ConnectionError < SensorsAnalyticsError
23
- end
24
-
25
- # 服务器返回导入失败
26
- class ServerError < SensorsAnalyticsError
27
- end
28
-
29
- # Debug模式下各种异常
30
- class DebugModeError < SensorsAnalyticsError
31
- end
32
-
33
- # Sensors Analytics SDK
34
- #
35
- # 通过 Sensors Analytics SDK 的构造方法及参数 Consumer 初始化对象,并通过 track, trackSignUp, profileSet 等方法向 Sensors Analytics 发送数据,例如
36
- #
37
- # consumer = SensorsAnalytics::DefaultConsumer.new(SENSORS_ANALYTICS_URL)
38
- # sa = SensorsAnalytics::SensorsAnalytics.new(consumer)
39
- # sa.track("abcdefg", "ServerStart", {"sex" => "female"})
40
- #
41
- # SENSORS_ANALYTICS_URL 是 Sensors Analytics 收集数据的 URI,可以从配置界面中获得。
42
- #
43
- # Sensors Analytics SDK 通过 Consumer 向 Sensors Analytics 发送数据,提供三种 Consumer:
44
- #
45
- # DefaultConsumer - 逐条同步发送数据
46
- # BatchConsumer - 批量同步发送数据
47
- # DebugModeConsumer - Debug 模式,用于校验数据导入是否正确
48
- #
49
- # Consumer 的具体信息请参看对应注释
50
- class SensorsAnalytics
51
-
52
- # Sensors Analytics SDK 构造函数,传入 Consumer 对象
53
- def initialize(consumer)
54
- @consumer = consumer
55
- # 初始化事件公共属性
56
- clear_super_properties()
57
- end
58
-
59
- # 设置每个事件都带有的一些公共属性
60
- #
61
- # 当 track 的 Properties 和 Super Properties 有相同的 key 时,将采用 track 的
62
- def register_super_properties(properties)
63
- properties.each do |key, value|
64
- @super_properties[key] = value
65
- end
66
- end
67
-
68
- # 删除所有已设置的事件公共属性
69
- def clear_super_properties()
70
- @super_properties = {
71
- '$lib' => 'Ruby',
72
- '$lib_version' => VERSION,
73
- }
74
- end
75
-
76
- # 记录一个的事件,其中 distinct_id 为触发事件的用户ID,event_name 标示事件名称,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
77
- def track(distinct_id, event_name, properties={})
78
- _track_event(:track, distinct_id, distinct_id, event_name, properties)
79
- end
80
-
81
- # 记录注册行为,其中 distinct_id 为注册后的用户ID,origin_distinct_id 为注册前的临时ID,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
82
- #
83
- # 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:
84
- #
85
- # http://www.sensorsdata.cn/manual/track_signup.html
86
- #
87
- # 并在必要时联系我们的技术支持人员。
88
- def track_signup(distinct_id, origin_distinct_id, properties={})
89
- _track_event(:track_signup, distinct_id, origin_distinct_id, :$SignUp, properties)
90
- end
91
-
92
- # 设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
93
- #
94
- # 无论用户该属性值是否存在,都将用 properties 中的属性覆盖原有设置
95
- def profile_set(distinct_id, properties)
96
- _track_event(:profile_set, distinct_id, distinct_id, nil, properties)
97
- end
98
-
99
- # 尝试设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
100
- #
101
- # 若用户不存在该属性,则设置用户的属性,否则放弃
102
- def profile_set_once(distinct_id, properties)
103
- _track_event(:profile_set_once, distinct_id, distinct_id, nil, properties)
104
- end
105
-
106
- # 为用户的一个或多个属性累加一个数值,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为 Integer 类型
107
- #
108
- # 若该属性不存在,则创建它并设置默认值为0
109
- def profile_increment(distinct_id, properties)
110
- _track_event(:profile_increment, distinct_id, distinct_id, nil, properties)
111
- end
112
-
113
- # 追加数据至用户的一个或多个列表类型的属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为元素是 String 类型的数组
114
- #
115
- # 若该属性不存在,则创建一个空数组,并插入 properties 中的属性值
116
- def profile_append(distinct_id, properties)
117
- _track_event(:profile_append, distinct_id, distinct_id, nil, properties)
118
- end
119
-
120
- # 删除用户一个或多个属性,properties 是一个数组,其中每个元素描述一个需要删除的属性的 Key
121
- def profile_unset(distinct_id, properties)
122
- unless properties.is_a?(Array)
123
- IllegalDataError.new("Properties of PROFILE UNSET must be an instance of Array<String>.")
124
- end
125
- property_hash = {}
126
- properties.each do |key|
127
- property_hash[key] = true
128
- end
129
- _track_event(:profile_unset, distinct_id, distinct_id, nil, property_hash)
130
- end
131
-
132
- private
133
-
134
- def _track_event(event_type, distinct_id, origin_distinct_id, event_name, properties)
135
- _assert_key(:DistinctId, distinct_id)
136
- _assert_key(:OriginalDistinctId, origin_distinct_id)
137
- if event_type == :track
138
- _assert_key_with_regex(:EventName, event_name)
139
- end
140
- _assert_properties(event_type, properties)
141
-
142
- # 从事件属性中获取时间配置
143
- event_time = _extract_time_from_properties(properties)
144
- properties.delete(:$time)
145
-
146
- event_properties = {}
147
- if event_type == :track || event_type == :track_signup
148
- event_properties = @super_properties.dup
149
- end
150
-
151
- properties.each do |key, value|
152
- if value.is_a?(Time)
153
- event_properties[key] = value.strftime("%Y-%m-%d %H:%M:%S.#{(value.to_f * 1000.0).to_i % 1000}")
154
- else
155
- event_properties[key] = value
156
- end
157
- end
158
-
159
- lib_properties = _get_lib_properties()
160
-
161
- # Track / TrackSignup / ProfileSet / ProfileSetOne / ProfileIncrement / ProfileAppend / ProfileUnset
162
- event = {
163
- :type => event_type,
164
- :time => event_time,
165
- :distinct_id => distinct_id,
166
- :properties => event_properties,
167
- :lib => lib_properties,
168
- }
169
-
170
- if event_type == :track
171
- # Track
172
- event[:event] = event_name
173
- elsif event_type == :track_signup
174
- # TrackSignUp
175
- event[:event] = event_name
176
- event[:original_id] = origin_distinct_id
177
- end
178
-
179
- @consumer.send(event)
180
- end
181
-
182
- def _extract_time_from_properties(properties)
183
- properties.each do |key, value|
184
- if (key == :$time || key == "$time") && value.is_a?(Time)
185
- return (value.to_f * 1000).to_i
186
- end
187
- end
188
- return (Time.now().to_f * 1000).to_i
189
- end
190
-
191
- def _get_lib_properties()
192
- lib_properties = {
193
- '$lib' => 'Ruby',
194
- '$lib_version' => VERSION,
195
- '$lib_method' => 'code',
196
- }
197
-
198
- @super_properties.each do |key, value|
199
- if key == :$app_version || key == "$app_version"
200
- lib_properties[:$app_version] = value
201
- end
202
- end
203
-
204
- begin
205
- raise Exception
206
- rescue Exception => e
207
- trace = e.backtrace[3].split(':')
208
- file = trace[0]
209
- line = trace[1]
210
- function = trace[2].split('`')[1][0..-2]
211
- lib_properties[:$lib_detail] = "###{function}###{file}###{line}"
212
- end
213
-
214
- return lib_properties
215
- end
216
-
217
- def _assert_key(type, key)
218
- unless key.instance_of?(String) || key.instance_of?(Symbol)
219
- raise IllegalDataError.new("#{type} must be an instance of String / Symbol.")
220
- end
221
- unless key.length >= 1
222
- raise IllegalDataError.new("#{type} is empty.")
223
- end
224
- unless key.length <= 255
225
- raise IllegalDataError.new("#{type} is too long, max length is 255.")
226
- end
227
- end
228
-
229
- def _assert_key_with_regex(type, key)
230
- _assert_key(type, key)
231
- unless key =~ KEY_PATTERN
232
- raise IllegalDataError.new("#{type} '#{key}' is invalid.")
233
- end
234
- end
235
-
236
- def _assert_properties(event_type, properties)
237
- unless properties.instance_of?(Hash)
238
- raise IllegalDataError.new("Properties must be an instance of Hash.")
239
- end
240
- properties.each do |key, value|
241
- _assert_key_with_regex(:PropertyKey, key)
242
-
243
- 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)
244
- raise IllegalDateError.new("The properties value must be an instance of Integer/Float/String/Array.")
245
- end
246
-
247
- # 属性为 Array 时,元素必须为 String 或 Symbol 类型
248
- if value.is_a?(Array)
249
- value.each do |element|
250
- unless element.is_a?(String) || element.is_a?(Symbol)
251
- raise IllegalDateError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
252
- end
253
- # 元素的长度不能超过8192
254
- unless element.length <= 8192
255
- raise IllegalDateError.new("The properties value is too long.")
256
- end
257
- end
258
- end
259
-
260
- # 属性为 String 或 Symbol 时,长度不能超过8191
261
- if value.is_a?(String) || value.is_a?(Symbol)
262
- unless value.length <= 8192
263
- raise IllegalDateError.new("The properties value is too long.")
264
- end
265
- end
266
-
267
- # profile_increment 的属性必须为数值类型
268
- if event_type == :profile_increment
269
- unless value.is_a?(Integer)
270
- raise IllegalDateError.new("The properties value of PROFILE INCREMENT must be an instance of Integer.")
271
- end
272
- end
273
-
274
- # profile_append 的属性必须为数组类型,且数组元素必须为字符串
275
- if event_type == :profile_append
276
- unless value.is_a?(Array)
277
- raise IllegalDateError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
278
- end
279
- value.each do |element|
280
- unless element.is_a?(String) || element.is_a?(Symbol)
281
- raise IllegalDateError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
282
- end
283
- end
284
- end
285
- end
286
- end
287
-
288
- end
289
-
290
- class SensorsAnalyticsConsumer
291
-
292
- def initialize(server_url)
293
- @server_url = server_url
294
- end
295
-
296
- def request!(event_list, headers = {})
297
- unless event_list.is_a?(Array) && headers.is_a?(Hash)
298
- raise IllegalDateError.new("The argument of 'request!' should be a Array.")
299
- end
300
-
301
- # GZip && Base64 encode
302
- wio = StringIO.new("w")
303
- gzip_io = Zlib::GzipWriter.new(wio)
304
- gzip_io.write(event_list.to_json)
305
- gzip_io.close()
306
- data = Base64.encode64(wio.string).gsub("\n", '')
307
-
308
- form_data = {"data_list" => data, "gzip" => 1}
309
-
310
- init_header = {"User-Agent" => "SensorsAnalytics Ruby SDK"}
311
- headers.each do |key, value|
312
- init_header[key] = value
313
- end
314
-
315
- uri = _get_uri(@server_url)
316
- request = Net::HTTP::Post.new(uri.request_uri, initheader = init_header)
317
- request.set_form_data(form_data)
318
-
319
- client = Net::HTTP.new(uri.host, uri.port)
320
- client.open_timeout = 10
321
- client.continue_timeout = 10
322
- client.read_timeout = 10
323
-
324
- response = client.request(request)
325
- return [response.code, response.body]
326
- end
327
-
328
- def _get_uri(url)
329
- begin
330
- URI.parse(url)
331
- rescue URI::InvalidURIError
332
- host = url.match(".+\:\/\/([^\/]+)")[1]
333
- uri = URI.parse(url.sub(host, 'dummy-host'))
334
- uri.instance_variable_set('@host', host)
335
- uri
336
- end
337
- end
338
-
339
- end
340
-
341
- # 实现逐条、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI
342
- class DefaultConsumer < SensorsAnalyticsConsumer
343
-
344
- def initialize(server_url)
345
- super(server_url)
346
- end
347
-
348
- def send(event)
349
- event_list = [event]
350
-
351
- begin
352
- response_code, response_body = request!(event_list)
353
- rescue => e
354
- raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
355
- end
356
-
357
- unless response_code.to_i == 200
358
- raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
359
- end
360
- end
361
-
362
- end
363
-
364
- # 实现批量、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI 和批量发送的缓存大小
365
- class BatchConsumer < SensorsAnalyticsConsumer
366
-
367
- MAX_FLUSH_BULK = 50
368
-
369
- def initialize(server_url, flush_bulk = MAX_FLUSH_BULK)
370
- @event_buffer = []
371
- @flush_bulk = [flush_bulk, MAX_FLUSH_BULK].min
372
- super(server_url)
373
- end
374
-
375
- def send(event)
376
- @event_buffer << event
377
- flush if @event_buffer.length >= @flush_bulk
378
- end
379
-
380
- def flush()
381
- @event_buffer.each_slice(@flush_bulk) do |event_list|
382
- begin
383
- response_code, response_body = request!(event_list)
384
- rescue => e
385
- raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
386
- end
387
-
388
- unless response_code.to_i == 200
389
- raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
390
- end
391
- end
392
- @event_buffer = []
393
- end
394
-
395
- end
396
-
397
- # Debug 模式的 Consumer,Debug 模式的具体信息请参考文档
398
- #
399
- # http://www.sensorsdata.cn/manual/debug_mode.html
400
- #
401
- # write_data 参数为 true,则 Debug 模式下导入的数据会导入 Sensors Analytics;否则,Debug 模式下导入的数据将只进行格式校验,不会导入 Sensors Analytics 中
402
- class DebugConsumer < SensorsAnalyticsConsumer
403
-
404
- def initialize(server_url, write_data)
405
- uri = _get_uri(server_url)
406
- # 将 URL Path 替换成 Debug 模式的 '/debug'
407
- uri.path = '/debug'
408
-
409
- @headers = {}
410
- unless write_data
411
- @headers['Dry-Run'] = 'true'
412
- end
413
-
414
- super(uri.to_s)
415
- end
416
-
417
- def send(event)
418
- event_list = [event]
419
-
420
- begin
421
- response_code, response_body = request!(event_list, @headers)
422
- rescue => e
423
- raise DebugModeError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
424
- end
425
-
426
- puts "=========================================================================="
427
-
428
- if response_code.to_i == 200
429
- puts "valid message: #{event_list.to_json}"
430
- else
431
- puts "invalid message: #{event_list.to_json}"
432
- puts "response code: #{response_code}"
433
- puts "response body: #{response_body}"
434
- end
435
-
436
- if response_code.to_i >= 300
437
- raise DebugModeError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
438
- end
439
- end
440
-
441
- end
8
+ module SensorsAnalytics
442
9
 
443
10
  end