sensors_analytics_sdk 1.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/sensors_analytics_sdk.rb +395 -0
- 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: []
|