sensors_analytics_sdk 1.5.0 → 1.6.2

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.
@@ -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