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.
- checksums.yaml +4 -4
- data/.gitignore +36 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +201 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/changeslog.md +6 -0
- data/examples/demo.rb +129 -0
- data/lib/sensors_analytics/client.rb +267 -0
- data/lib/sensors_analytics/consumers.rb +129 -0
- data/lib/sensors_analytics/errors.rb +22 -0
- data/lib/sensors_analytics/http.rb +59 -0
- data/lib/sensors_analytics/version.rb +4 -0
- data/lib/sensors_analytics_sdk.rb +6 -440
- data/sensors_analytics_sdk.gemspec +24 -0
- metadata +77 -5
@@ -1,444 +1,10 @@
|
|
1
|
-
require 'base64'
|
2
|
-
require 'json'
|
3
|
-
require 'net/http'
|
4
|
-
require 'zlib'
|
5
1
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|