clearbit 0.2.7 → 0.3.3

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,49 @@
1
+ require 'clearbit/analytics/defaults'
2
+
3
+ module Clearbit
4
+ class Analytics
5
+ class BackoffPolicy
6
+ include Clearbit::Analytics::Defaults::BackoffPolicy
7
+
8
+ # @param [Hash] opts
9
+ # @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
10
+ # @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
11
+ # @option opts [Numeric] :multiplier The value to multiply the current
12
+ # interval with for each retry attempt
13
+ # @option opts [Numeric] :randomization_factor The randomization factor
14
+ # to use to create a range around the retry interval
15
+ def initialize(opts = {})
16
+ @min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
17
+ @max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
18
+ @multiplier = opts[:multiplier] || MULTIPLIER
19
+ @randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR
20
+
21
+ @attempts = 0
22
+ end
23
+
24
+ # @return [Numeric] the next backoff interval, in milliseconds.
25
+ def next_interval
26
+ interval = @min_timeout_ms * (@multiplier**@attempts)
27
+ interval = add_jitter(interval, @randomization_factor)
28
+
29
+ @attempts += 1
30
+
31
+ [interval, @max_timeout_ms].min
32
+ end
33
+
34
+ private
35
+
36
+ def add_jitter(base, randomization_factor)
37
+ random_number = rand
38
+ max_deviation = base * randomization_factor
39
+ deviation = random_number * max_deviation
40
+
41
+ if random_number < 0.5
42
+ base - deviation
43
+ else
44
+ base + deviation
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,189 @@
1
+ require 'thread'
2
+ require 'time'
3
+
4
+ require 'clearbit/analytics/defaults'
5
+ require 'clearbit/analytics/logging'
6
+ require 'clearbit/analytics/utils'
7
+ require 'clearbit/analytics/worker'
8
+
9
+ module Clearbit
10
+ class Analytics
11
+ class Client
12
+ include Clearbit::Analytics::Utils
13
+ include Clearbit::Analytics::Logging
14
+
15
+ # @param [Hash] opts
16
+ # @option opts [String] :write_key Your project's write_key
17
+ # @option opts [FixNum] :max_queue_size Maximum number of calls to be
18
+ # remain queued.
19
+ # @option opts [Proc] :on_error Handles error calls from the API.
20
+ def initialize(opts = {})
21
+ symbolize_keys!(opts)
22
+
23
+ @queue = Queue.new
24
+ @write_key = opts[:write_key]
25
+ @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
26
+ @worker_mutex = Mutex.new
27
+ @worker = Worker.new(@queue, @write_key, opts)
28
+
29
+ check_write_key!
30
+
31
+ at_exit { @worker_thread && @worker_thread[:should_exit] = true }
32
+ end
33
+
34
+ # Synchronously waits until the worker has flushed the queue.
35
+ #
36
+ # Use only for scripts which are not long-running, and will specifically
37
+ # exit
38
+ def flush
39
+ while !@queue.empty? || @worker.is_requesting?
40
+ ensure_worker_running
41
+ sleep(0.1)
42
+ end
43
+ end
44
+
45
+ # @!macro common_attrs
46
+ # @option attrs [String] :anonymous_id ID for a user when you don't know
47
+ # who they are yet. (optional but you must provide either an
48
+ # `anonymous_id` or `user_id`)
49
+ # @option attrs [Hash] :context ({})
50
+ # @option attrs [Hash] :integrations What integrations this event
51
+ # goes to (optional)
52
+ # @option attrs [String] :message_id ID that uniquely
53
+ # identifies a message across the API. (optional)
54
+ # @option attrs [Time] :timestamp When the event occurred (optional)
55
+ # @option attrs [String] :user_id The ID for this user in your database
56
+ # (optional but you must provide either an `anonymous_id` or `user_id`)
57
+ # @option attrs [Hash] :options Options such as user traits (optional)
58
+
59
+ # Tracks an event
60
+ #
61
+ # @see https://segment.com/docs/sources/server/ruby/#track
62
+ #
63
+ # @param [Hash] attrs
64
+ #
65
+ # @option attrs [String] :event Event name
66
+ # @option attrs [Hash] :properties Event properties (optional)
67
+ # @macro common_attrs
68
+ def track(attrs)
69
+ symbolize_keys! attrs
70
+ enqueue(FieldParser.parse_for_track(attrs))
71
+ end
72
+
73
+ # Identifies a user
74
+ #
75
+ # @see https://segment.com/docs/sources/server/ruby/#identify
76
+ #
77
+ # @param [Hash] attrs
78
+ #
79
+ # @option attrs [Hash] :traits User traits (optional)
80
+ # @macro common_attrs
81
+ def identify(attrs)
82
+ symbolize_keys! attrs
83
+ enqueue(FieldParser.parse_for_identify(attrs))
84
+ end
85
+
86
+ # Aliases a user from one id to another
87
+ #
88
+ # @see https://segment.com/docs/sources/server/ruby/#alias
89
+ #
90
+ # @param [Hash] attrs
91
+ #
92
+ # @option attrs [String] :previous_id The ID to alias from
93
+ # @macro common_attrs
94
+ def alias(attrs)
95
+ symbolize_keys! attrs
96
+ enqueue(FieldParser.parse_for_alias(attrs))
97
+ end
98
+
99
+ # Associates a user identity with a group.
100
+ #
101
+ # @see https://segment.com/docs/sources/server/ruby/#group
102
+ #
103
+ # @param [Hash] attrs
104
+ #
105
+ # @option attrs [String] :group_id The ID of the group
106
+ # @option attrs [Hash] :traits User traits (optional)
107
+ # @macro common_attrs
108
+ def group(attrs)
109
+ symbolize_keys! attrs
110
+ enqueue(FieldParser.parse_for_group(attrs))
111
+ end
112
+
113
+ # Records a page view
114
+ #
115
+ # @see https://segment.com/docs/sources/server/ruby/#page
116
+ #
117
+ # @param [Hash] attrs
118
+ #
119
+ # @option attrs [String] :name Name of the page
120
+ # @option attrs [Hash] :properties Page properties (optional)
121
+ # @macro common_attrs
122
+ def page(attrs)
123
+ symbolize_keys! attrs
124
+ enqueue(FieldParser.parse_for_page(attrs))
125
+ end
126
+
127
+ # Records a screen view (for a mobile app)
128
+ #
129
+ # @param [Hash] attrs
130
+ #
131
+ # @option attrs [String] :name Name of the screen
132
+ # @option attrs [Hash] :properties Screen properties (optional)
133
+ # @option attrs [String] :category The screen category (optional)
134
+ # @macro common_attrs
135
+ def screen(attrs)
136
+ symbolize_keys! attrs
137
+ enqueue(FieldParser.parse_for_screen(attrs))
138
+ end
139
+
140
+ # @return [Fixnum] number of messages in the queue
141
+ def queued_messages
142
+ @queue.length
143
+ end
144
+
145
+ private
146
+
147
+ # private: Enqueues the action.
148
+ #
149
+ # returns Boolean of whether the item was added to the queue.
150
+ def enqueue(action)
151
+ # add our request id for tracing purposes
152
+ action[:messageId] ||= uid
153
+
154
+ if @queue.length < @max_queue_size
155
+ @queue << action
156
+ ensure_worker_running
157
+
158
+ true
159
+ else
160
+ logger.warn(
161
+ 'Queue is full, dropping events. The :max_queue_size ' \
162
+ 'configuration parameter can be increased to prevent this from ' \
163
+ 'happening.'
164
+ )
165
+ false
166
+ end
167
+ end
168
+
169
+ # private: Checks that the write_key is properly initialized
170
+ def check_write_key!
171
+ raise ArgumentError, 'Write key must be initialized' if @write_key.nil?
172
+ end
173
+
174
+ def ensure_worker_running
175
+ return if worker_running?
176
+ @worker_mutex.synchronize do
177
+ return if worker_running?
178
+ @worker_thread = Thread.new do
179
+ @worker.run
180
+ end
181
+ end
182
+ end
183
+
184
+ def worker_running?
185
+ @worker_thread && @worker_thread.alive?
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,36 @@
1
+ module Clearbit
2
+ class Analytics
3
+ module Defaults
4
+ module Request
5
+ HOST = 'x.clearbit.com'
6
+ PORT = 443
7
+ PATH = '/v1/import'
8
+ SSL = true
9
+ HEADERS = { 'Accept' => 'application/json',
10
+ 'Content-Type' => 'application/json',
11
+ 'User-Agent' => "clearbit-ruby/#{Clearbit::VERSION}" }
12
+ RETRIES = 10
13
+ end
14
+
15
+ module Queue
16
+ MAX_SIZE = 10000
17
+ end
18
+
19
+ module Message
20
+ MAX_BYTES = 32768 # 32Kb
21
+ end
22
+
23
+ module MessageBatch
24
+ MAX_BYTES = 512_000 # 500Kb
25
+ MAX_SIZE = 100
26
+ end
27
+
28
+ module BackoffPolicy
29
+ MIN_TIMEOUT_MS = 100
30
+ MAX_TIMEOUT_MS = 10000
31
+ MULTIPLIER = 1.5
32
+ RANDOMIZATION_FACTOR = 0.5
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,192 @@
1
+ module Clearbit
2
+ class Analytics
3
+ # Handles parsing fields according to the Segment Spec
4
+ #
5
+ # @see https://segment.com/docs/spec/
6
+ class FieldParser
7
+ class << self
8
+ include Clearbit::Analytics::Utils
9
+
10
+ # In addition to the common fields, track accepts:
11
+ #
12
+ # - "event"
13
+ # - "properties"
14
+ def parse_for_track(fields)
15
+ common = parse_common_fields(fields)
16
+
17
+ event = fields[:event]
18
+ properties = fields[:properties] || {}
19
+
20
+ check_presence!(event, 'event')
21
+ check_is_hash!(properties, 'properties')
22
+
23
+ isoify_dates! properties
24
+
25
+ common.merge({
26
+ :type => 'track',
27
+ :event => event.to_s,
28
+ :properties => properties
29
+ })
30
+ end
31
+
32
+ # In addition to the common fields, identify accepts:
33
+ #
34
+ # - "traits"
35
+ def parse_for_identify(fields)
36
+ common = parse_common_fields(fields)
37
+
38
+ traits = fields[:traits] || {}
39
+ check_is_hash!(traits, 'traits')
40
+ isoify_dates! traits
41
+
42
+ common.merge({
43
+ :type => 'identify',
44
+ :traits => traits
45
+ })
46
+ end
47
+
48
+ # In addition to the common fields, alias accepts:
49
+ #
50
+ # - "previous_id"
51
+ def parse_for_alias(fields)
52
+ common = parse_common_fields(fields)
53
+
54
+ previous_id = fields[:previous_id]
55
+ check_presence!(previous_id, 'previous_id')
56
+
57
+ common.merge({
58
+ :type => 'alias',
59
+ :previousId => previous_id
60
+ })
61
+ end
62
+
63
+ # In addition to the common fields, group accepts:
64
+ #
65
+ # - "group_id"
66
+ # - "traits"
67
+ def parse_for_group(fields)
68
+ common = parse_common_fields(fields)
69
+
70
+ group_id = fields[:group_id]
71
+ traits = fields[:traits] || {}
72
+
73
+ check_presence!(group_id, 'group_id')
74
+ check_is_hash!(traits, 'traits')
75
+
76
+ isoify_dates! traits
77
+
78
+ common.merge({
79
+ :type => 'group',
80
+ :groupId => group_id,
81
+ :traits => traits
82
+ })
83
+ end
84
+
85
+ # In addition to the common fields, page accepts:
86
+ #
87
+ # - "name"
88
+ # - "properties"
89
+ def parse_for_page(fields)
90
+ common = parse_common_fields(fields)
91
+
92
+ name = fields[:name] || ''
93
+ properties = fields[:properties] || {}
94
+
95
+ check_is_hash!(properties, 'properties')
96
+
97
+ isoify_dates! properties
98
+
99
+ common.merge({
100
+ :type => 'page',
101
+ :name => name.to_s,
102
+ :properties => properties
103
+ })
104
+ end
105
+
106
+ # In addition to the common fields, screen accepts:
107
+ #
108
+ # - "name"
109
+ # - "properties"
110
+ # - "category" (Not in spec, retained for backward compatibility"
111
+ def parse_for_screen(fields)
112
+ common = parse_common_fields(fields)
113
+
114
+ name = fields[:name]
115
+ properties = fields[:properties] || {}
116
+ category = fields[:category]
117
+
118
+ check_presence!(name, 'name')
119
+ check_is_hash!(properties, 'properties')
120
+
121
+ isoify_dates! properties
122
+
123
+ parsed = common.merge({
124
+ :type => 'screen',
125
+ :name => name,
126
+ :properties => properties
127
+ })
128
+
129
+ parsed[:category] = category if category
130
+
131
+ parsed
132
+ end
133
+
134
+ private
135
+
136
+ def parse_common_fields(fields)
137
+ timestamp = fields[:timestamp] || Time.new
138
+ message_id = fields[:message_id].to_s if fields[:message_id]
139
+ context = fields[:context] || {}
140
+
141
+ check_user_id! fields
142
+ check_timestamp! timestamp
143
+
144
+ add_context! context
145
+
146
+ parsed = {
147
+ :context => context,
148
+ :messageId => message_id,
149
+ :timestamp => datetime_in_iso8601(timestamp)
150
+ }
151
+
152
+ parsed[:userId] = fields[:user_id] if fields[:user_id]
153
+ parsed[:anonymousId] = fields[:anonymous_id] if fields[:anonymous_id]
154
+ parsed[:integrations] = fields[:integrations] if fields[:integrations]
155
+
156
+ # Not in spec, retained for backward compatibility
157
+ parsed[:options] = fields[:options] if fields[:options]
158
+
159
+ parsed
160
+ end
161
+
162
+ def check_user_id!(fields)
163
+ unless fields[:user_id] || fields[:anonymous_id]
164
+ raise ArgumentError, 'Must supply either user_id or anonymous_id'
165
+ end
166
+ end
167
+
168
+ def check_timestamp!(timestamp)
169
+ raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
170
+ end
171
+
172
+ def add_context!(context)
173
+ context[:library] = { :name => 'clearbit-ruby', :version => Clearbit::VERSION.to_s }
174
+ end
175
+
176
+ # private: Ensures that a string is non-empty
177
+ #
178
+ # obj - String|Number that must be non-blank
179
+ # name - Name of the validated value
180
+ def check_presence!(obj, name)
181
+ if obj.nil? || (obj.is_a?(String) && obj.empty?)
182
+ raise ArgumentError, "#{name} must be given"
183
+ end
184
+ end
185
+
186
+ def check_is_hash!(obj, name)
187
+ raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end