sensors_analytics_sdk 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/sensors_analytics_sdk.rb +395 -0
  3. metadata +43 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d1cd00d1b6166cb408c62a339b4b4d698140f361
4
+ data.tar.gz: fcf52c303dce0aab8e94b5b9b9de7956da472c37
5
+ SHA512:
6
+ metadata.gz: 16e6736ac422888a62e58958ba3a06c4fe5cd7d1be7a1808e29c4eab87c34686865cafac45ac5df12183271e7695c9ead103a6ec3169ad3147591ebc940217fa
7
+ data.tar.gz: 68760a63e5ae1a9ad0be1b5f07438752f6b5edd0af6883cdfd03b393a99f9f3424762cd7e374949d8757bef278f80dd0d85586a97498ee2b336c7d6a41cd27e4
@@ -0,0 +1,395 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'zlib'
5
+
6
+ module SensorsAnalytics
7
+
8
+ VERSION = '1.3.6'
9
+
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
+ # raise IllegalDataError
64
+ _assert_properties(properties)
65
+
66
+ properties.each do |key, value|
67
+ @super_properties[key] = value
68
+ end
69
+ end
70
+
71
+ # 删除所有已设置的事件公共属性
72
+ def clear_super_properties()
73
+ @super_properties = {
74
+ '$lib' => 'Ruby',
75
+ '$lib_version' => VERSION,
76
+ }
77
+ end
78
+
79
+ # 记录一个的事件,其中 distinct_id 为触发事件的用户ID,event_name 标示事件名称,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
80
+ def track(distinct_id, event_name, properties={})
81
+ _track_event(:track, distinct_id, distinct_id, event_name, properties)
82
+ end
83
+
84
+ # 记录注册行为,其中 distinct_id 为注册后的用户ID,origin_distinct_id 为注册前的临时ID,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
85
+ #
86
+ # 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:
87
+ #
88
+ # http://www.sensorsdata.cn/manual/track_signup.html
89
+ #
90
+ # 并在必要时联系我们的技术支持人员。
91
+ def track_signup(distinct_id, origin_distinct_id, properties={})
92
+ _track_event(:track_signup, distinct_id, origin_distinct_id, :$SignUp, properties)
93
+ end
94
+
95
+ # 设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
96
+ #
97
+ # 无论用户该属性值是否存在,都将用 properties 中的属性覆盖原有设置
98
+ def profile_set(distinct_id, properties)
99
+ _track_event(:profile_set, distinct_id, distinct_id, nil, properties)
100
+ end
101
+
102
+ # 尝试设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
103
+ #
104
+ # 若用户不存在该属性,则设置用户的属性,否则放弃
105
+ def profile_set_once(distinct_id, properties)
106
+ _track_event(:profile_set_once, distinct_id, distinct_id, nil, properties)
107
+ end
108
+
109
+ # 为用户的一个或多个属性累加一个数值,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为 Integer 类型
110
+ #
111
+ # 若该属性不存在,则创建它并设置默认值为0
112
+ def profile_increment(distinct_id, properties)
113
+ _track_event(:profile_increment, distinct_id, distinct_id, nil, properties)
114
+ end
115
+
116
+ # 追加数据至用户的一个或多个列表类型的属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为元素是 String 类型的数组
117
+ #
118
+ # 若该属性不存在,则创建一个空数组,并插入 properties 中的属性值
119
+ def profile_append(distinct_id, properties)
120
+ _track_event(:profile_append, distinct_id, distinct_id, nil, properties)
121
+ end
122
+
123
+ # 删除用户一个或多个属性,properties 是一个数组,其中每个元素描述一个需要删除的属性的 Key
124
+ def profile_unset(distinct_id, properties)
125
+ unless properties.is_a?(Array)
126
+ IllegalDataError.new("Properties of PROFILE UNSET must be an instance of Array<String>.")
127
+ end
128
+ property_hash = {}
129
+ properties.each do |key|
130
+ property_hash[key] = true
131
+ end
132
+ _track_event(:profile_unset, distinct_id, distinct_id, nil, property_hash)
133
+ end
134
+
135
+ private
136
+
137
+ def _track_event(event_type, distinct_id, origin_distinct_id, event_name, properties)
138
+ _assert_key(:DistinctId, distinct_id)
139
+ _assert_key(:OriginalDistinctId, origin_distinct_id)
140
+ if event_type == :track
141
+ _assert_key_with_regex(:EventName, event_name)
142
+ end
143
+ _assert_properties(event_type, properties)
144
+
145
+ # 从事件属性中获取时间配置
146
+ event_time = _extract_time_from_properties(properties)
147
+ properties.delete("$time")
148
+
149
+ event_properties = @super_properties.dup
150
+ properties.each do |key, value|
151
+ event_properties[key] = value
152
+ end
153
+
154
+ # Track / TrackSignup / ProfileSet / ProfileSetOne / ProfileIncrement / ProfileAppend / ProfileUnset
155
+ event = {
156
+ "type" => event_type,
157
+ "time" => event_time,
158
+ "distinct_id" => distinct_id,
159
+ "properties" => event_properties,
160
+ }
161
+
162
+ if event_type == :track
163
+ # Track
164
+ event["event"] = event_name
165
+ elsif event_type == :track_signup
166
+ # TrackSignUp
167
+ event["event"] = event_name
168
+ event["original_id"] = origin_distinct_id
169
+ end
170
+
171
+ @consumer.send(event)
172
+ end
173
+
174
+ def _extract_time_from_properties(properties)
175
+ properties.each do |key, value|
176
+ if key == "$time" && value.is_a?(Time)
177
+ return (value.to_f * 1000).to_i
178
+ end
179
+ end
180
+ return (Time.now().to_f * 1000).to_i
181
+ end
182
+
183
+ def _assert_key(type, key)
184
+ unless key.instance_of?(String) || key.instance_of?(Symbol)
185
+ raise IllegalDataError.new("#{type} must be an instance of String.")
186
+ end
187
+ unless key.length >= 1
188
+ raise IllegalDataError.new("#{type} is empty.")
189
+ end
190
+ unless key.length <= 255
191
+ raise IllegalDataError.new("#{type} is too long, max length is 255.")
192
+ end
193
+ end
194
+
195
+ def _assert_key_with_regex(type, key)
196
+ _assert_key(type, key)
197
+ unless key =~ KEY_PATTERN
198
+ raise IllegalDataError.new("#{type} '#{key}' is invalid.")
199
+ end
200
+ end
201
+
202
+ def _assert_properties(event_type, properties)
203
+ unless properties.instance_of?(Hash)
204
+ raise IllegalDataError.new("Properties must be an instance of Hash.")
205
+ end
206
+ properties.each do |key, value|
207
+ _assert_key_with_regex(:PropertyKey, key)
208
+
209
+ 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)
210
+ raise IllegalDateError.new("The properties value must be an instance of Integer/Float/String/Array.")
211
+ end
212
+
213
+ # 属性为 Array 时,元素必须为 String 或 Symbol 类型
214
+ if value.is_a?(Array)
215
+ value.each do |element|
216
+ unless element.is_a?(String) || element.is_a?(Symbol)
217
+ raise IllegalDateError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
218
+ end
219
+ # 元素的长度不能超过255
220
+ unless element.length <= 255
221
+ raise IllegalDateError.new("The properties value is too long.")
222
+ end
223
+ end
224
+ end
225
+
226
+ # 属性为 String 或 Symbol 时,长度不能超过255
227
+ if value.is_a?(String) || value.is_a?(Symbol)
228
+ unless value.length <= 255
229
+ raise IllegalDateError.new("The properties value is too long.")
230
+ end
231
+ end
232
+
233
+ # profile_increment 的属性必须为数值类型
234
+ if event_type == :profile_increment
235
+ unless value.is_a?(Integer)
236
+ raise IllegalDateError.new("The properties value of PROFILE INCREMENT must be an instance of Integer.")
237
+ end
238
+ end
239
+
240
+ # profile_append 的属性必须为数组类型,且数组元素必须为字符串
241
+ if event_type == :profile_append
242
+ unless value.is_a?(Array)
243
+ raise IllegalDateError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
244
+ end
245
+ value.each do |element|
246
+ unless element.is_a?(String) || element.is_a?(Symbol)
247
+ raise IllegalDateError.new("The properties value of PROFILE INCREMENT must be an instance of Array[String].")
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ end
255
+
256
+ class SensorsAnalyticsConsumer
257
+
258
+ def initialize(server_url)
259
+ @server_url = server_url
260
+ end
261
+
262
+ def request!(event_list, headers = {})
263
+ unless event_list.is_a?(Array) && headers.is_a?(Hash)
264
+ raise IllegalDateError.new("The argument of 'request!' should be a Array.")
265
+ end
266
+
267
+ # GZip && Base64 encode
268
+ wio = StringIO.new("w")
269
+ gzip_io = Zlib::GzipWriter.new(wio)
270
+ gzip_io.write(event_list.to_json)
271
+ gzip_io.close()
272
+ data = Base64.encode64(wio.string).gsub("\n", '')
273
+
274
+ form_data = {"data_list" => data, "gzip" => 1}
275
+
276
+ init_header = {"User-Agent" => "SensorsAnalytics Ruby SDK"}
277
+ headers.each do |key, value|
278
+ init_header[key] = value
279
+ end
280
+
281
+ uri = URI(@server_url)
282
+ request = Net::HTTP::Post.new(uri.request_uri, initheader = init_header)
283
+ request.set_form_data(form_data)
284
+
285
+ client = Net::HTTP.new(uri.host, uri.port)
286
+ client.open_timeout = 10
287
+ client.continue_timeout = 10
288
+ client.read_timeout = 10
289
+
290
+ response = client.request(request)
291
+ return [response.code, response.body]
292
+ end
293
+
294
+ end
295
+
296
+ # 实现逐条、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI
297
+ class DefaultConsumer < SensorsAnalyticsConsumer
298
+
299
+ def initialize(server_url)
300
+ super(server_url)
301
+ end
302
+
303
+ def send(event)
304
+ event_list = [event]
305
+
306
+ begin
307
+ response_code, response_body = request!(event_list)
308
+ rescue => e
309
+ raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
310
+ end
311
+
312
+ unless response_code.to_i == 200
313
+ raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
314
+ end
315
+ end
316
+
317
+ end
318
+
319
+ # 实现批量、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI 和批量发送的缓存大小
320
+ class BatchConsumer < SensorsAnalyticsConsumer
321
+
322
+ MAX_FLUSH_BULK = 50
323
+
324
+ def initialize(server_url, flush_bulk = MAX_FLUSH_BULK)
325
+ @event_buffer = []
326
+ @flush_bulk = [flush_bulk, MAX_FLUSH_BULK].min
327
+ super(server_url)
328
+ end
329
+
330
+ def send(event)
331
+ @event_buffer << event
332
+ flush if @event_buffer.length >= @flush_bulk
333
+ end
334
+
335
+ def flush()
336
+ @event_buffer.each_slice(@flush_bulk) do |event_list|
337
+ begin
338
+ response_code, response_body = request!(event_list)
339
+ rescue => e
340
+ raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
341
+ end
342
+
343
+ unless response_code.to_i == 200
344
+ raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
345
+ end
346
+ end
347
+ @event_buffer = []
348
+ end
349
+
350
+ end
351
+
352
+ # Debug 模式的 Consumer,Debug 模式的具体信息请参考文档
353
+ #
354
+ # http://www.sensorsdata.cn/manual/debug_mode.html
355
+ #
356
+ # write_data 参数为 true,则 Debug 模式下导入的数据会导入 Sensors Analytics;否则,Debug 模式下导入的数据将只进行格式校验,不会导入 Sensors Analytics 中
357
+ class DebugConsumer < SensorsAnalyticsConsumer
358
+
359
+ def initialize(server_url, write_data)
360
+ uri = URI(server_url)
361
+ # 将 URL Path 替换成 Debug 模式的 '/debug'
362
+ uri.path = '/debug'
363
+
364
+ @headers = {}
365
+ unless write_data
366
+ @headers['Dry-Run'] = 'true'
367
+ end
368
+
369
+ super(uri.to_s)
370
+ end
371
+
372
+ def send(event)
373
+ event_list = [event]
374
+
375
+ begin
376
+ response_code, response_body = request!(event_list, @headers)
377
+ rescue => e
378
+ raise DebugModeError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
379
+ end
380
+
381
+ puts "=========================================================================="
382
+
383
+ if response_code.to_i == 200
384
+ puts "valid message: #{event_list.to_json}"
385
+ else
386
+ puts "invalid message: #{event_list.to_json}"
387
+ puts "response code: #{response_code}"
388
+ puts "response body: #{response_body}"
389
+ raise DebugModeError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
390
+ end
391
+ end
392
+
393
+ end
394
+
395
+ end
metadata ADDED
@@ -0,0 +1,43 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sensors_analytics_sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.6
5
+ platform: ruby
6
+ authors:
7
+ - Yuhan ZOU
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This is the official Ruby SDK for Sensors Analytics.
14
+ email: zouyuhan@sensorsdata.cn
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/sensors_analytics_sdk.rb
20
+ homepage: http://www.sensorsdata.cn
21
+ licenses: []
22
+ metadata: {}
23
+ post_install_message:
24
+ rdoc_options: []
25
+ require_paths:
26
+ - lib
27
+ required_ruby_version: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
32
+ required_rubygems_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - '>='
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ requirements: []
38
+ rubyforge_project:
39
+ rubygems_version: 2.0.14
40
+ signing_key:
41
+ specification_version: 4
42
+ summary: SensorsAnalyticsSDK
43
+ test_files: []