sensors_analytics_sdk 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []