plainflow 2.2.3.pre.p1

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,31 @@
1
+ require 'plainflow/analytics/defaults'
2
+ require 'plainflow/analytics/utils'
3
+ require 'plainflow/analytics/version'
4
+ require 'plainflow/analytics/client'
5
+ require 'plainflow/analytics/worker'
6
+ require 'plainflow/analytics/request'
7
+ require 'plainflow/analytics/response'
8
+ require 'plainflow/analytics/logging'
9
+
10
+ module Plainflow
11
+ class Analytics
12
+ def initialize options = {}
13
+ Request.stub = options[:stub] if options.has_key?(:stub)
14
+ @client = Plainflow::Analytics::Client.new options
15
+ end
16
+
17
+ def method_missing message, *args, &block
18
+ if @client.respond_to? message
19
+ @client.send message, *args, &block
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def respond_to_missing?(method_name, include_private = false)
26
+ @client.respond_to?(method_name) || super
27
+ end
28
+
29
+ include Logging
30
+ end
31
+ end
@@ -0,0 +1,376 @@
1
+ require 'thread'
2
+ require 'time'
3
+ require 'plainflow/analytics/utils'
4
+ require 'plainflow/analytics/worker'
5
+ require 'plainflow/analytics/defaults'
6
+
7
+ module Plainflow
8
+ class Analytics
9
+ class Client
10
+ include Plainflow::Analytics::Utils
11
+
12
+ # public: Creates a new client
13
+ #
14
+ # attrs - Hash
15
+ # :secret_key - String of your project's secret_key
16
+ # :max_queue_size - Fixnum of the max calls to remain queued (optional)
17
+ # :on_error - Proc which handles error calls from the API
18
+ def initialize attrs = {}
19
+ symbolize_keys! attrs
20
+
21
+ @queue = Queue.new
22
+ @secret_key = attrs[:secret_key]
23
+ @max_queue_size = attrs[:max_queue_size] || Defaults::Queue::MAX_SIZE
24
+ @options = attrs
25
+ @worker_mutex = Mutex.new
26
+ @worker = Worker.new @queue, @secret_key, @options
27
+
28
+ check_secret_key!
29
+
30
+ at_exit { @worker_thread && @worker_thread[:should_exit] = true }
31
+ end
32
+
33
+ # public: Synchronously waits until the worker has flushed the queue.
34
+ # Use only for scripts which are not long-running, and will
35
+ # specifically exit
36
+ #
37
+ def flush
38
+ while !@queue.empty? || @worker.is_requesting?
39
+ ensure_worker_running
40
+ sleep(0.1)
41
+ end
42
+ end
43
+
44
+ # public: Tracks an event
45
+ #
46
+ # attrs - Hash
47
+ # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
48
+ # :context - Hash of context. (optional)
49
+ # :event - String of event name.
50
+ # :integrations - Hash specifying what integrations this event goes to. (optional)
51
+ # :options - Hash specifying options such as user traits. (optional)
52
+ # :properties - Hash of event properties. (optional)
53
+ # :timestamp - Time of when the event occurred. (optional)
54
+ # :user_id - String of the user id.
55
+ # :message_id - String of the message id that uniquely identified a message across the API. (optional)
56
+ def track attrs
57
+ symbolize_keys! attrs
58
+ check_user_id! attrs
59
+
60
+ event = attrs[:event]
61
+ properties = attrs[:properties] || {}
62
+ timestamp = attrs[:timestamp] || Time.new
63
+ context = attrs[:context] || {}
64
+ message_id = attrs[:message_id].to_s if attrs[:message_id]
65
+
66
+ check_timestamp! timestamp
67
+
68
+ if event.nil? || event.empty?
69
+ fail ArgumentError, 'Must supply event as a non-empty string'
70
+ end
71
+
72
+ fail ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash
73
+ isoify_dates! properties
74
+
75
+ add_context context
76
+
77
+ enqueue({
78
+ :event => event,
79
+ :userId => attrs[:user_id],
80
+ :anonymousId => attrs[:anonymous_id],
81
+ :context => context,
82
+ :options => attrs[:options],
83
+ :integrations => attrs[:integrations],
84
+ :properties => properties,
85
+ :messageId => message_id,
86
+ :timestamp => datetime_in_iso8601(timestamp),
87
+ :type => 'track'
88
+ })
89
+ end
90
+
91
+ # public: Identifies a user
92
+ #
93
+ # attrs - Hash
94
+ # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
95
+ # :context - Hash of context. (optional)
96
+ # :integrations - Hash specifying what integrations this event goes to. (optional)
97
+ # :options - Hash specifying options such as user traits. (optional)
98
+ # :timestamp - Time of when the event occurred. (optional)
99
+ # :traits - Hash of user traits. (optional)
100
+ # :user_id - String of the user id
101
+ # :message_id - String of the message id that uniquely identified a message across the API. (optional)
102
+ def identify attrs
103
+ symbolize_keys! attrs
104
+ check_user_id! attrs
105
+
106
+ traits = attrs[:traits] || {}
107
+ timestamp = attrs[:timestamp] || Time.new
108
+ context = attrs[:context] || {}
109
+ message_id = attrs[:message_id].to_s if attrs[:message_id]
110
+
111
+ check_timestamp! timestamp
112
+
113
+ fail ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash
114
+ isoify_dates! traits
115
+
116
+ add_context context
117
+
118
+ enqueue({
119
+ :userId => attrs[:user_id],
120
+ :anonymousId => attrs[:anonymous_id],
121
+ :integrations => attrs[:integrations],
122
+ :context => context,
123
+ :traits => traits,
124
+ :options => attrs[:options],
125
+ :messageId => message_id,
126
+ :timestamp => datetime_in_iso8601(timestamp),
127
+ :type => 'identify'
128
+ })
129
+ end
130
+
131
+ # public: Aliases a user from one id to another
132
+ #
133
+ # attrs - Hash
134
+ # :context - Hash of context (optional)
135
+ # :integrations - Hash specifying what integrations this event goes to. (optional)
136
+ # :options - Hash specifying options such as user traits. (optional)
137
+ # :previous_id - String of the id to alias from
138
+ # :timestamp - Time of when the alias occured (optional)
139
+ # :user_id - String of the id to alias to
140
+ # :message_id - String of the message id that uniquely identified a message across the API. (optional)
141
+ def alias(attrs)
142
+ symbolize_keys! attrs
143
+
144
+ from = attrs[:previous_id]
145
+ to = attrs[:user_id]
146
+ timestamp = attrs[:timestamp] || Time.new
147
+ context = attrs[:context] || {}
148
+ message_id = attrs[:message_id].to_s if attrs[:message_id]
149
+
150
+ check_presence! from, 'previous_id'
151
+ check_presence! to, 'user_id'
152
+ check_timestamp! timestamp
153
+ add_context context
154
+
155
+ enqueue({
156
+ :previousId => from,
157
+ :userId => to,
158
+ :integrations => attrs[:integrations],
159
+ :context => context,
160
+ :options => attrs[:options],
161
+ :messageId => message_id,
162
+ :timestamp => datetime_in_iso8601(timestamp),
163
+ :type => 'alias'
164
+ })
165
+ end
166
+
167
+ # public: Associates a user identity with a group.
168
+ #
169
+ # attrs - Hash
170
+ # :context - Hash of context (optional)
171
+ # :integrations - Hash specifying what integrations this event goes to. (optional)
172
+ # :options - Hash specifying options such as user traits. (optional)
173
+ # :previous_id - String of the id to alias from
174
+ # :timestamp - Time of when the alias occured (optional)
175
+ # :user_id - String of the id to alias to
176
+ # :message_id - String of the message id that uniquely identified a message across the API. (optional)
177
+ def group(attrs)
178
+ symbolize_keys! attrs
179
+ check_user_id! attrs
180
+
181
+ group_id = attrs[:group_id]
182
+ user_id = attrs[:user_id]
183
+ traits = attrs[:traits] || {}
184
+ timestamp = attrs[:timestamp] || Time.new
185
+ context = attrs[:context] || {}
186
+ message_id = attrs[:message_id].to_s if attrs[:message_id]
187
+
188
+ fail ArgumentError, '.traits must be a hash' unless traits.is_a? Hash
189
+ isoify_dates! traits
190
+
191
+ check_presence! group_id, 'group_id'
192
+ check_timestamp! timestamp
193
+ add_context context
194
+
195
+ enqueue({
196
+ :groupId => group_id,
197
+ :userId => user_id,
198
+ :traits => traits,
199
+ :integrations => attrs[:integrations],
200
+ :options => attrs[:options],
201
+ :context => context,
202
+ :messageId => message_id,
203
+ :timestamp => datetime_in_iso8601(timestamp),
204
+ :type => 'group'
205
+ })
206
+ end
207
+
208
+ # public: Records a page view
209
+ #
210
+ # attrs - Hash
211
+ # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
212
+ # :category - String of the page category (optional)
213
+ # :context - Hash of context (optional)
214
+ # :integrations - Hash specifying what integrations this event goes to. (optional)
215
+ # :name - String name of the page
216
+ # :options - Hash specifying options such as user traits. (optional)
217
+ # :properties - Hash of page properties (optional)
218
+ # :timestamp - Time of when the pageview occured (optional)
219
+ # :user_id - String of the id to alias from
220
+ # :message_id - String of the message id that uniquely identified a message across the API. (optional)
221
+ def page(attrs)
222
+ symbolize_keys! attrs
223
+ check_user_id! attrs
224
+
225
+ name = attrs[:name].to_s
226
+ properties = attrs[:properties] || {}
227
+ timestamp = attrs[:timestamp] || Time.new
228
+ context = attrs[:context] || {}
229
+ message_id = attrs[:message_id].to_s if attrs[:message_id]
230
+
231
+ fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
232
+ isoify_dates! properties
233
+
234
+ check_timestamp! timestamp
235
+ add_context context
236
+
237
+ enqueue({
238
+ :userId => attrs[:user_id],
239
+ :anonymousId => attrs[:anonymous_id],
240
+ :name => name,
241
+ :category => attrs[:category],
242
+ :properties => properties,
243
+ :integrations => attrs[:integrations],
244
+ :options => attrs[:options],
245
+ :context => context,
246
+ :messageId => message_id,
247
+ :timestamp => datetime_in_iso8601(timestamp),
248
+ :type => 'page'
249
+ })
250
+ end
251
+ # public: Records a screen view (for a mobile app)
252
+ #
253
+ # attrs - Hash
254
+ # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
255
+ # :category - String screen category (optional)
256
+ # :context - Hash of context (optional)
257
+ # :integrations - Hash specifying what integrations this event goes to. (optional)
258
+ # :name - String name of the screen
259
+ # :options - Hash specifying options such as user traits. (optional)
260
+ # :properties - Hash of screen properties (optional)
261
+ # :timestamp - Time of when the screen occured (optional)
262
+ # :user_id - String of the id to alias from
263
+ def screen(attrs)
264
+ symbolize_keys! attrs
265
+ check_user_id! attrs
266
+
267
+ name = attrs[:name].to_s
268
+ properties = attrs[:properties] || {}
269
+ timestamp = attrs[:timestamp] || Time.new
270
+ context = attrs[:context] || {}
271
+ message_id = attrs[:message_id].to_s if attrs[:message_id]
272
+
273
+ fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
274
+ isoify_dates! properties
275
+
276
+ check_timestamp! timestamp
277
+ add_context context
278
+
279
+ enqueue({
280
+ :userId => attrs[:user_id],
281
+ :anonymousId => attrs[:anonymous_id],
282
+ :name => name,
283
+ :properties => properties,
284
+ :category => attrs[:category],
285
+ :options => attrs[:options],
286
+ :integrations => attrs[:integrations],
287
+ :context => context,
288
+ :messageId => message_id,
289
+ :timestamp => timestamp.iso8601,
290
+ :type => 'screen'
291
+ })
292
+ end
293
+
294
+ # public: Returns the number of queued messages
295
+ #
296
+ # returns Fixnum of messages in the queue
297
+ def queued_messages
298
+ @queue.length
299
+ end
300
+
301
+ private
302
+
303
+ # private: Enqueues the action.
304
+ #
305
+ # returns Boolean of whether the item was added to the queue.
306
+ def enqueue(action)
307
+ # add our request id for tracing purposes
308
+ action[:messageId] ||= uid
309
+ unless queue_full = @queue.length >= @max_queue_size
310
+ ensure_worker_running
311
+ @queue << action
312
+ end
313
+ !queue_full
314
+ end
315
+
316
+ # private: Ensures that a string is non-empty
317
+ #
318
+ # obj - String|Number that must be non-blank
319
+ # name - Name of the validated value
320
+ #
321
+ def check_presence!(obj, name)
322
+ if obj.nil? || (obj.is_a?(String) && obj.empty?)
323
+ fail ArgumentError, "#{name} must be given"
324
+ end
325
+ end
326
+
327
+ # private: Adds contextual information to the call
328
+ #
329
+ # context - Hash of call context
330
+ def add_context(context)
331
+ context[:library] = { :name => "analytics-ruby", :version => Plainflow::Analytics::VERSION.to_s }
332
+ end
333
+
334
+ # private: Checks that the secret_key is properly initialized
335
+ def check_secret_key!
336
+ fail ArgumentError, 'Write key must be initialized' if @secret_key.nil?
337
+ end
338
+
339
+ # private: Checks the timstamp option to make sure it is a Time.
340
+ def check_timestamp!(timestamp)
341
+ fail ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
342
+ end
343
+
344
+ def event attrs
345
+ symbolize_keys! attrs
346
+
347
+ {
348
+ :userId => user_id,
349
+ :name => name,
350
+ :properties => properties,
351
+ :context => context,
352
+ :timestamp => datetime_in_iso8601(timestamp),
353
+ :type => 'screen'
354
+ }
355
+ end
356
+
357
+ def check_user_id! attrs
358
+ fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id]
359
+ end
360
+
361
+ def ensure_worker_running
362
+ return if worker_running?
363
+ @worker_mutex.synchronize do
364
+ return if worker_running?
365
+ @worker_thread = Thread.new do
366
+ @worker.run
367
+ end
368
+ end
369
+ end
370
+
371
+ def worker_running?
372
+ @worker_thread && @worker_thread.alive?
373
+ end
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,20 @@
1
+ module Plainflow
2
+ class Analytics
3
+ module Defaults
4
+ module Request
5
+ HOST = 'pipe.plainflow.net'
6
+ PORT = 443
7
+ PATH = '/v1/import'
8
+ SSL = true
9
+ HEADERS = { :accept => 'application/json' }
10
+ RETRIES = 4
11
+ BACKOFF = 30.0
12
+ end
13
+
14
+ module Queue
15
+ BATCH_SIZE = 100
16
+ MAX_SIZE = 10000
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require 'logger'
2
+
3
+ module Plainflow
4
+ class Analytics
5
+ module Logging
6
+ class << self
7
+ def logger
8
+ @logger ||= if defined?(Rails)
9
+ Rails.logger
10
+ else
11
+ logger = Logger.new STDOUT
12
+ logger.progname = 'Plainflow::Analytics'
13
+ logger
14
+ end
15
+ end
16
+
17
+ def logger= logger
18
+ @logger = logger
19
+ end
20
+ end
21
+
22
+ def self.included base
23
+ class << base
24
+ def logger
25
+ Logging.logger
26
+ end
27
+ end
28
+ end
29
+
30
+ def logger
31
+ Logging.logger
32
+ end
33
+ end
34
+ end
35
+ end