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,4 @@
1
+
2
+ module SensorsAnalytics
3
+ VERSION = '1.6.0'
4
+ end
@@ -1,444 +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.3'
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
- properties.delete("$time")
146
-
147
- event_properties = {}
148
- if event_type == :track || event_type == :track_signup
149
- event_properties = @super_properties.dup
150
- end
151
-
152
- properties.each do |key, value|
153
- if value.is_a?(Time)
154
- event_properties[key] = value.strftime("%Y-%m-%d %H:%M:%S.#{(value.to_f * 1000.0).to_i % 1000}")
155
- else
156
- event_properties[key] = value
157
- end
158
- end
159
-
160
- lib_properties = _get_lib_properties()
161
-
162
- # Track / TrackSignup / ProfileSet / ProfileSetOne / ProfileIncrement / ProfileAppend / ProfileUnset
163
- event = {
164
- :type => event_type,
165
- :time => event_time,
166
- :distinct_id => distinct_id,
167
- :properties => event_properties,
168
- :lib => lib_properties,
169
- }
170
-
171
- if event_type == :track
172
- # Track
173
- event[:event] = event_name
174
- elsif event_type == :track_signup
175
- # TrackSignUp
176
- event[:event] = event_name
177
- event[:original_id] = origin_distinct_id
178
- end
179
-
180
- @consumer.send(event)
181
- end
182
-
183
- def _extract_time_from_properties(properties)
184
- properties.each do |key, value|
185
- if (key == :$time || key == "$time") && value.is_a?(Time)
186
- return (value.to_f * 1000).to_i
187
- end
188
- end
189
- return (Time.now().to_f * 1000).to_i
190
- end
191
-
192
- def _get_lib_properties()
193
- lib_properties = {
194
- '$lib' => 'Ruby',
195
- '$lib_version' => VERSION,
196
- '$lib_method' => 'code',
197
- }
198
-
199
- @super_properties.each do |key, value|
200
- if key == :$app_version || key == "$app_version"
201
- lib_properties[:$app_version] = value
202
- end
203
- end
204
-
205
- begin
206
- raise Exception
207
- rescue Exception => e
208
- trace = e.backtrace[3].split(':')
209
- file = trace[0]
210
- line = trace[1]
211
- function = trace[2].split('`')[1][0..-2]
212
- lib_properties[:$lib_detail] = "###{function}###{file}###{line}"
213
- end
214
-
215
- return lib_properties
216
- end
217
-
218
- def _assert_key(type, key)
219
- unless key.instance_of?(String) || key.instance_of?(Symbol)
220
- raise IllegalDataError.new("#{type} must be an instance of String / Symbol.")
221
- end
222
- unless key.length >= 1
223
- raise IllegalDataError.new("#{type} is empty.")
224
- end
225
- unless key.length <= 255
226
- raise IllegalDataError.new("#{type} is too long, max length is 255.")
227
- end
228
- end
229
-
230
- def _assert_key_with_regex(type, key)
231
- _assert_key(type, key)
232
- unless key =~ KEY_PATTERN
233
- raise IllegalDataError.new("#{type} '#{key}' is invalid.")
234
- end
235
- end
236
-
237
- def _assert_properties(event_type, properties)
238
- unless properties.instance_of?(Hash)
239
- raise IllegalDataError.new("Properties must be an instance of Hash.")
240
- end
241
- properties.each do |key, value|
242
- _assert_key_with_regex(:PropertyKey, key)
243
-
244
- 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)
245
- raise IllegalDataError.new("The properties value must be an instance of Integer/Float/String/Array.")
246
- end
247
-
248
- # 属性为 Array 时,元素必须为 String 或 Symbol 类型
249
- if value.is_a?(Array)
250
- value.each do |element|
251
- unless element.is_a?(String) || element.is_a?(Symbol)
252
- raise IllegalDataError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
253
- end
254
- # 元素的长度不能超过8192
255
- unless element.length <= 8192
256
- raise IllegalDataError.new("The properties value is too long.")
257
- end
258
- end
259
- end
260
-
261
- # 属性为 String 或 Symbol 时,长度不能超过8191
262
- if value.is_a?(String) || value.is_a?(Symbol)
263
- unless value.length <= 8192
264
- raise IllegalDataError.new("The properties value is too long.")
265
- end
266
- end
267
-
268
- # profile_increment 的属性必须为数值类型
269
- if event_type == :profile_increment
270
- unless value.is_a?(Integer)
271
- raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Integer.")
272
- end
273
- end
274
-
275
- # profile_append 的属性必须为数组类型,且数组元素必须为字符串
276
- if event_type == :profile_append
277
- unless value.is_a?(Array)
278
- raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
279
- end
280
- value.each do |element|
281
- unless element.is_a?(String) || element.is_a?(Symbol)
282
- raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
283
- end
284
- end
285
- end
286
- end
287
- end
288
-
289
- end
290
-
291
- class SensorsAnalyticsConsumer
292
-
293
- def initialize(server_url)
294
- @server_url = server_url
295
- end
296
-
297
- def request!(event_list, headers = {})
298
- unless event_list.is_a?(Array) && headers.is_a?(Hash)
299
- raise IllegalDataError.new("The argument of 'request!' should be a Array.")
300
- end
301
-
302
- # GZip && Base64 encode
303
- wio = StringIO.new("w")
304
- gzip_io = Zlib::GzipWriter.new(wio)
305
- gzip_io.write(event_list.to_json)
306
- gzip_io.close()
307
- data = Base64.encode64(wio.string).gsub("\n", '')
308
-
309
- form_data = {"data_list" => data, "gzip" => 1}
310
-
311
- init_header = {"User-Agent" => "SensorsAnalytics Ruby SDK"}
312
- headers.each do |key, value|
313
- init_header[key] = value
314
- end
315
-
316
- uri = _get_uri(@server_url)
317
- request = Net::HTTP::Post.new(uri.request_uri, initheader = init_header)
318
- request.set_form_data(form_data)
319
-
320
- client = Net::HTTP.new(uri.host, uri.port)
321
- client.open_timeout = 10
322
- client.continue_timeout = 10
323
- client.read_timeout = 10
324
-
325
- response = client.request(request)
326
- return [response.code, response.body]
327
- end
328
-
329
- def _get_uri(url)
330
- begin
331
- URI.parse(url)
332
- rescue URI::InvalidURIError
333
- host = url.match(".+\:\/\/([^\/]+)")[1]
334
- uri = URI.parse(url.sub(host, 'dummy-host'))
335
- uri.instance_variable_set('@host', host)
336
- uri
337
- end
338
- end
339
-
340
- end
341
-
342
- # 实现逐条、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI
343
- class DefaultConsumer < SensorsAnalyticsConsumer
344
-
345
- def initialize(server_url)
346
- super(server_url)
347
- end
348
-
349
- def send(event)
350
- event_list = [event]
351
-
352
- begin
353
- response_code, response_body = request!(event_list)
354
- rescue => e
355
- raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
356
- end
357
-
358
- unless response_code.to_i == 200
359
- raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
360
- end
361
- end
362
-
363
- end
364
-
365
- # 实现批量、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI 和批量发送的缓存大小
366
- class BatchConsumer < SensorsAnalyticsConsumer
367
-
368
- MAX_FLUSH_BULK = 50
369
-
370
- def initialize(server_url, flush_bulk = MAX_FLUSH_BULK)
371
- @event_buffer = []
372
- @flush_bulk = [flush_bulk, MAX_FLUSH_BULK].min
373
- super(server_url)
374
- end
375
-
376
- def send(event)
377
- @event_buffer << event
378
- flush if @event_buffer.length >= @flush_bulk
379
- end
380
-
381
- def flush()
382
- @event_buffer.each_slice(@flush_bulk) do |event_list|
383
- begin
384
- response_code, response_body = request!(event_list)
385
- rescue => e
386
- raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
387
- end
388
-
389
- unless response_code.to_i == 200
390
- raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
391
- end
392
- end
393
- @event_buffer = []
394
- end
395
-
396
- end
397
-
398
- # Debug 模式的 Consumer,Debug 模式的具体信息请参考文档
399
- #
400
- # http://www.sensorsdata.cn/manual/debug_mode.html
401
- #
402
- # write_data 参数为 true,则 Debug 模式下导入的数据会导入 Sensors Analytics;否则,Debug 模式下导入的数据将只进行格式校验,不会导入 Sensors Analytics 中
403
- class DebugConsumer < SensorsAnalyticsConsumer
404
-
405
- def initialize(server_url, write_data)
406
- uri = _get_uri(server_url)
407
- # 将 URL Path 替换成 Debug 模式的 '/debug'
408
- uri.path = '/debug'
409
-
410
- @headers = {}
411
- unless write_data
412
- @headers['Dry-Run'] = 'true'
413
- end
414
-
415
- super(uri.to_s)
416
- end
417
-
418
- def send(event)
419
- event_list = [event]
420
-
421
- begin
422
- response_code, response_body = request!(event_list, @headers)
423
- rescue => e
424
- raise DebugModeError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
425
- end
426
-
427
- puts "=========================================================================="
428
-
429
- if response_code.to_i == 200
430
- puts "valid message: #{event_list.to_json}"
431
- else
432
- puts "invalid message: #{event_list.to_json}"
433
- puts "response code: #{response_code}"
434
- puts "response body: #{response_body}"
435
- end
436
-
437
- if response_code.to_i >= 300
438
- raise DebugModeError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
439
- end
440
- end
441
-
442
- end
8
+ module SensorsAnalytics
443
9
 
444
10
  end