sensors_analytics_sdk 1.5.2 → 1.6.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,319 @@
1
+ module SensorsAnalytics
2
+ 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})$/
3
+
4
+ # Sensors Analytics SDK
5
+ #
6
+ # 通过 Sensors Analytics SDK 的构造方法及参数 Consumer 初始化对象,并通过 track, trackSignUp, profileSet 等方法向 Sensors Analytics 发送数据,例如
7
+ #
8
+ # consumer = SensorsAnalytics::DefaultConsumer.new(SENSORS_ANALYTICS_URL)
9
+ # sa = SensorsAnalytics::SensorsAnalytics.new(consumer)
10
+ # sa.track("abcdefg", "ServerStart", {"sex" => "female"})
11
+ #
12
+ # SENSORS_ANALYTICS_URL 是 Sensors Analytics 收集数据的 URI,可以从配置界面中获得。
13
+ #
14
+ # Sensors Analytics SDK 通过 Consumer 向 Sensors Analytics 发送数据,提供三种 Consumer:
15
+ #
16
+ # DefaultConsumer - 逐条同步发送数据
17
+ # BatchConsumer - 批量同步发送数据
18
+ # DebugModeConsumer - Debug 模式,用于校验数据导入是否正确
19
+ #
20
+ # Consumer 的具体信息请参看对应注释
21
+ class SensorsAnalytics
22
+
23
+ # Sensors Analytics SDK 构造函数,传入 Consumer 对象
24
+ def initialize(consumer)
25
+ @consumer = consumer
26
+ # 初始化事件公共属性
27
+ clear_super_properties
28
+ end
29
+
30
+ # 设置每个事件都带有的一些公共属性
31
+ #
32
+ # 当 track 的 Properties 和 Super Properties 有相同的 key 时,将采用 track 的
33
+ def register_super_properties(properties)
34
+ properties.each do |key, value|
35
+ @super_properties[key] = value
36
+ end
37
+ end
38
+
39
+ # 删除所有已设置的事件公共属性
40
+ def clear_super_properties
41
+ @super_properties = {
42
+ '$lib' => 'Ruby',
43
+ '$lib_version' => VERSION,
44
+ }
45
+ end
46
+
47
+ # 记录一个的事件,其中 distinct_id 为触发事件的用户ID,event_name 标示事件名称,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
48
+ def track(distinct_id, event_name, properties={})
49
+ _track_event(:track, distinct_id, distinct_id, event_name, properties)
50
+ end
51
+
52
+ # 记录注册行为,其中 distinct_id 为注册后的用户ID,origin_distinct_id 为注册前的临时ID,properties 是一个哈希表,其中每对元素描述事件的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、TrueClass 和 FalseClass 类型
53
+ #
54
+ # 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:
55
+ #
56
+ # http://www.sensorsdata.cn/manual/track_signup.html
57
+ #
58
+ # 并在必要时联系我们的技术支持人员。
59
+ def track_signup(distinct_id, origin_distinct_id, properties={})
60
+ _track_event(:track_signup, distinct_id, origin_distinct_id, :$SignUp, properties)
61
+ end
62
+
63
+ # 设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
64
+ #
65
+ # 无论用户该属性值是否存在,都将用 properties 中的属性覆盖原有设置
66
+ def profile_set(distinct_id, properties)
67
+ _track_event(:profile_set, distinct_id, distinct_id, nil, properties)
68
+ end
69
+
70
+ # 尝试设置用户的一个或多个属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,哈希表的 Value 可以为 Integer、Float、String、Time、TrueClass 和 FalseClass 类型
71
+ #
72
+ # 若用户不存在该属性,则设置用户的属性,否则放弃
73
+ def profile_set_once(distinct_id, properties)
74
+ _track_event(:profile_set_once, distinct_id, distinct_id, nil, properties)
75
+ end
76
+
77
+ # 为用户的一个或多个属性累加一个数值,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为 Integer 类型
78
+ #
79
+ # 若该属性不存在,则创建它并设置默认值为0
80
+ def profile_increment(distinct_id, properties)
81
+ _track_event(:profile_increment, distinct_id, distinct_id, nil, properties)
82
+ end
83
+
84
+ # 追加数据至用户的一个或多个列表类型的属性,properties 是一个哈希表,其中每对元素描述用户的一个属性,哈希表的 Key 必须为 String 类型,Value 必须为元素是 String 类型的数组
85
+ #
86
+ # 若该属性不存在,则创建一个空数组,并插入 properties 中的属性值
87
+ def profile_append(distinct_id, properties)
88
+ _track_event(:profile_append, distinct_id, distinct_id, nil, properties)
89
+ end
90
+
91
+ # 删除用户一个或多个属性,properties 是一个数组,其中每个元素描述一个需要删除的属性的 Key
92
+ def profile_unset(distinct_id, properties)
93
+ unless properties.is_a?(Array)
94
+ IllegalDataError.new("Properties of PROFILE UNSET must be an instance of Array<String>.")
95
+ end
96
+ property_hash = {}
97
+ properties.each do |key|
98
+ property_hash[key] = true
99
+ end
100
+ _track_event(:profile_unset, distinct_id, distinct_id, nil, property_hash)
101
+ end
102
+
103
+ # 上报 item 数据
104
+ def item_set(item_type, item_id, properties)
105
+ _track_item(:item_set, item_type, item_id, properties)
106
+ end
107
+
108
+ # 删除 item 数据
109
+ def item_delete(item_type, item_id)
110
+ _track_item(:item_delete, item_type, item_id, {})
111
+ end
112
+
113
+ private
114
+
115
+ def _track_event(event_type, distinct_id, origin_distinct_id, event_name, properties)
116
+ _assert_key(:DistinctId, distinct_id)
117
+ _assert_key(:OriginalDistinctId, origin_distinct_id)
118
+ if event_type == :track
119
+ _assert_key_with_regex(:EventName, event_name)
120
+ end
121
+ _assert_properties(event_type, properties)
122
+
123
+ # 从事件属性中获取时间配置
124
+ event_time = _extract_time_from_properties(properties)
125
+ properties.delete(:$time)
126
+ properties.delete("$time")
127
+
128
+ event_properties = {}
129
+ if event_type == :track || event_type == :track_signup
130
+ event_properties = @super_properties.dup
131
+ end
132
+
133
+ properties.each do |key, value|
134
+ if value.is_a?(Time)
135
+ event_properties[key] = value.strftime("%Y-%m-%d %H:%M:%S.#{(value.to_f * 1000.0).to_i % 1000}")
136
+ else
137
+ event_properties[key] = value
138
+ end
139
+ end
140
+
141
+ lib_properties = _get_lib_properties
142
+
143
+ # Track / TrackSignup / ProfileSet / ProfileSetOne / ProfileIncrement / ProfileAppend / ProfileUnset
144
+ event = {
145
+ type: event_type,
146
+ time: event_time,
147
+ distinct_id: distinct_id,
148
+ properties: event_properties,
149
+ lib: lib_properties,
150
+ }
151
+
152
+ if event_type == :track
153
+ # Track
154
+ event[:event] = event_name
155
+ elsif event_type == :track_signup
156
+ # TrackSignUp
157
+ event[:event] = event_name
158
+ event[:original_id] = origin_distinct_id
159
+ end
160
+
161
+ @consumer.send(event)
162
+ end
163
+
164
+ def _track_item(event_type, item_type, item_id, properties)
165
+ _assert_key_with_regex(:item_type, item_type)
166
+ _assert_key(:item_id, item_id)
167
+
168
+ if event_type == :item_set
169
+ _assert_properties(event_type, properties)
170
+ end
171
+
172
+ # 从事件属性中获取时间配置
173
+ event_time = _extract_time_from_properties(properties)
174
+ properties.delete(:$time)
175
+ properties.delete("$time")
176
+
177
+ lib_properties = _get_lib_properties
178
+
179
+ # event_type 有 item_set 和 item_delete 两种
180
+ event = {
181
+ type: event_type,
182
+ time: event_time,
183
+ item_id: item_id,
184
+ item_type: item_type,
185
+ lib: lib_properties,
186
+ }
187
+
188
+ if event_type == :item_set
189
+ # item_set 有 properties 属性
190
+ item_properties = {}
191
+
192
+ properties.each do |key, value|
193
+ if value.is_a?(Time)
194
+ item_properties[key] = value.strftime("%Y-%m-%d %H:%M:%S.#{(value.to_f * 1000.0).to_i % 1000}")
195
+ else
196
+ item_properties[key] = value
197
+ end
198
+ end
199
+
200
+ event[:properties] = item_properties
201
+ end
202
+
203
+ @consumer.send(event)
204
+ end
205
+
206
+ def _extract_time_from_properties(properties)
207
+ properties.each do |key, value|
208
+ if (key == :$time || key == "$time") && value.is_a?(Time)
209
+ return (value.to_f * 1000).to_i
210
+ end
211
+ end
212
+ return (Time.now.to_f * 1000).to_i
213
+ end
214
+
215
+ def _get_lib_properties
216
+ @lib_properties ||= begin
217
+ lib_properties = {
218
+ '$lib' => 'Ruby',
219
+ '$lib_version' => VERSION,
220
+ '$lib_method' => 'code',
221
+ }
222
+
223
+ @super_properties.each do |key, value|
224
+ if key == :$app_version || key == "$app_version"
225
+ lib_properties[:$app_version] = value
226
+ end
227
+ end
228
+
229
+ #lib_properties[:$lib_detail] = _get_lib_detail
230
+
231
+ lib_properties
232
+ end
233
+ end
234
+
235
+ def _get_lib_detail
236
+ begin
237
+ raise Exception
238
+ rescue Exception => e
239
+ trace = e.backtrace[3].split(':')
240
+ file = trace[0]
241
+ line = trace[1]
242
+ function = trace[2].split('`')[1][0..-2]
243
+ return "###{function}###{file}###{line}"
244
+ end
245
+ end
246
+
247
+ def _assert_key(type, key)
248
+ unless key.instance_of?(String) || key.instance_of?(Symbol)
249
+ raise IllegalDataError.new("#{type} must be an instance of String / Symbol.")
250
+ end
251
+ unless key.length >= 1
252
+ raise IllegalDataError.new("#{type} is empty.")
253
+ end
254
+ unless key.length <= 255
255
+ raise IllegalDataError.new("#{type} is too long, max length is 255.")
256
+ end
257
+ end
258
+
259
+ def _assert_key_with_regex(type, key)
260
+ _assert_key(type, key)
261
+ unless key =~ KEY_PATTERN
262
+ raise IllegalDataError.new("#{type} '#{key}' is invalid.")
263
+ end
264
+ end
265
+
266
+ def _assert_properties(event_type, properties)
267
+ unless properties.instance_of?(Hash)
268
+ raise IllegalDataError.new("Properties must be an instance of Hash.")
269
+ end
270
+ properties.each do |key, value|
271
+ _assert_key_with_regex(:PropertyKey, key)
272
+
273
+ 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)
274
+ raise IllegalDataError.new("The properties value must be an instance of Integer/Float/String/Array.")
275
+ end
276
+
277
+ # 属性为 Array 时,元素必须为 String 或 Symbol 类型
278
+ if value.is_a?(Array)
279
+ value.each do |element|
280
+ unless element.is_a?(String) || element.is_a?(Symbol)
281
+ raise IllegalDataError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
282
+ end
283
+ # 元素的长度不能超过8192
284
+ unless element.length <= 8192
285
+ raise IllegalDataError.new("The properties value is too long.")
286
+ end
287
+ end
288
+ end
289
+
290
+ # 属性为 String 或 Symbol 时,长度不能超过8191
291
+ if value.is_a?(String) || value.is_a?(Symbol)
292
+ unless value.length <= 8192
293
+ raise IllegalDataError.new("The properties value is too long.")
294
+ end
295
+ end
296
+
297
+ # profile_increment 的属性必须为数值类型
298
+ if event_type == :profile_increment
299
+ unless value.is_a?(Integer)
300
+ raise IllegalDataError.new("The properties value of PROFILE INCREMENT must be an instance of Integer.")
301
+ end
302
+ end
303
+
304
+ # profile_append 的属性必须为数组类型,且数组元素必须为字符串
305
+ if event_type == :profile_append
306
+ unless value.is_a?(Array)
307
+ raise IllegalDataError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
308
+ end
309
+ value.each do |element|
310
+ unless element.is_a?(String) || element.is_a?(Symbol)
311
+ raise IllegalDataError.new("The properties value of PROFILE APPEND must be an instance of Array[String].")
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ end
319
+ end
@@ -0,0 +1,129 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'zlib'
4
+
5
+ module SensorsAnalytics
6
+ class SensorsAnalyticsConsumer
7
+ def initialize(server_url)
8
+ @http_client = Http.new(server_url, keep_alive: true)
9
+ end
10
+
11
+ def request!(event_list, headers = {})
12
+ unless event_list.is_a?(Array) && headers.is_a?(Hash)
13
+ raise IllegalDataError.new("The argument of 'request!' should be a Array.")
14
+ end
15
+
16
+ # GZip && Base64 encode
17
+ wio = StringIO.new("w")
18
+ gzip_io = Zlib::GzipWriter.new(wio)
19
+ gzip_io.write(event_list.to_json)
20
+ gzip_io.close
21
+ data = Base64.encode64(wio.string).gsub("\n", '')
22
+ form_data = {"data_list" => data, "gzip" => 1}
23
+
24
+ @http_client.request(form_data, headers)
25
+ end
26
+ end
27
+
28
+ # 实现逐条、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI
29
+ class DefaultConsumer < SensorsAnalyticsConsumer
30
+
31
+ def initialize(server_url)
32
+ super(server_url)
33
+ end
34
+
35
+ def send(event)
36
+ event_list = [event]
37
+
38
+ begin
39
+ response_code, response_body = request!(event_list)
40
+ rescue => e
41
+ raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
42
+ end
43
+
44
+ unless response_code.to_i == 200
45
+ raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ # 实现批量、同步发送的 Consumer,初始化参数为 Sensors Analytics 收集数据的 URI 和批量发送的缓存大小
52
+ class BatchConsumer < SensorsAnalyticsConsumer
53
+
54
+ MAX_FLUSH_BULK = 50
55
+
56
+ def initialize(server_url, flush_bulk = MAX_FLUSH_BULK)
57
+ @event_buffer = []
58
+ @flush_bulk = [flush_bulk, MAX_FLUSH_BULK].min
59
+ super(server_url)
60
+ end
61
+
62
+ def send(event)
63
+ @event_buffer << event
64
+ flush if @event_buffer.length >= @flush_bulk
65
+ end
66
+
67
+ def flush
68
+ @event_buffer.each_slice(@flush_bulk) do |event_list|
69
+ begin
70
+ response_code, response_body = request!(event_list)
71
+ rescue => e
72
+ raise ConnectionError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
73
+ end
74
+
75
+ unless response_code.to_i == 200
76
+ raise ServerError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
77
+ end
78
+ end
79
+ @event_buffer = []
80
+ end
81
+
82
+ end
83
+
84
+ # Debug 模式的 Consumer,Debug 模式的具体信息请参考文档
85
+ #
86
+ # http://www.sensorsdata.cn/manual/debug_mode.html
87
+ #
88
+ # write_data 参数为 true,则 Debug 模式下导入的数据会导入 Sensors Analytics;否则,Debug 模式下导入的数据将只进行格式校验,不会导入 Sensors Analytics 中
89
+ class DebugConsumer < SensorsAnalyticsConsumer
90
+
91
+ def initialize(server_url, write_data)
92
+ uri = URI.parse(server_url)
93
+ # 将 URL Path 替换成 Debug 模式的 '/debug'
94
+ uri.path = '/debug'
95
+
96
+ @headers = {}
97
+ unless write_data
98
+ @headers['Dry-Run'] = 'true'
99
+ end
100
+
101
+ super(uri.to_s)
102
+ end
103
+
104
+ def send(event)
105
+ event_list = [event]
106
+
107
+ begin
108
+ response_code, response_body = request!(event_list, @headers)
109
+ rescue => e
110
+ raise DebugModeError.new("Could not connect to Sensors Analytics, with error \"#{e.message}\".")
111
+ end
112
+
113
+ puts "=========================================================================="
114
+
115
+ if response_code.to_i == 200
116
+ puts "valid message: #{event_list.to_json}"
117
+ else
118
+ puts "invalid message: #{event_list.to_json}"
119
+ puts "response code: #{response_code}"
120
+ puts "response body: #{response_body}"
121
+ end
122
+
123
+ if response_code.to_i >= 300
124
+ raise DebugModeError.new("Could not write to Sensors Analytics, server responded with #{response_code} returning: '#{response_body}'")
125
+ end
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,22 @@
1
+ module SensorsAnalytics
2
+ # Sensors Analytics SDK 的异常
3
+ # 使用 Sensors Analytics SDK 时,应该捕获 IllegalDataError、ConnectionError 和 ServerError。Debug 模式下,会抛出 DebugModeError 用于校验数据导入是否正确,线上运行时不需要捕获 DebugModeError
4
+ class SensorsAnalyticsError < StandardError
5
+ end
6
+
7
+ # 输入数据格式错误,如 Distinct Id、Event Name、Property Keys 不符合命名规范,或 Property Values 不符合数据类型要求
8
+ class IllegalDataError < SensorsAnalyticsError
9
+ end
10
+
11
+ # 网络连接错误
12
+ class ConnectionError < SensorsAnalyticsError
13
+ end
14
+
15
+ # 服务器返回导入失败
16
+ class ServerError < SensorsAnalyticsError
17
+ end
18
+
19
+ # Debug模式下各种异常
20
+ class DebugModeError < SensorsAnalyticsError
21
+ end
22
+ end