astronomer 2.0.14

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,20 @@
1
+ module Segment
2
+ class Analytics
3
+ module Defaults
4
+ module Request
5
+ HOST = 'api.astronomer.io'
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 Segment
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 = 'Segment::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
@@ -0,0 +1,82 @@
1
+ require 'segment/analytics/defaults'
2
+ require 'segment/analytics/utils'
3
+ require 'segment/analytics/response'
4
+ require 'segment/analytics/logging'
5
+ require 'net/http'
6
+ require 'net/https'
7
+ require 'json'
8
+
9
+ module Segment
10
+ class Analytics
11
+ class Request
12
+ include Segment::Analytics::Defaults::Request
13
+ include Segment::Analytics::Utils
14
+ include Segment::Analytics::Logging
15
+
16
+ # public: Creates a new request object to send analytics batch
17
+ #
18
+ def initialize(options = {})
19
+ options[:host] ||= HOST
20
+ options[:port] ||= PORT
21
+ options[:ssl] ||= SSL
22
+ options[:headers] ||= HEADERS
23
+ @path = options[:path] || PATH
24
+ @retries = options[:retries] || RETRIES
25
+ @backoff = options[:backoff] || BACKOFF
26
+
27
+ http = Net::HTTP.new(options[:host], options[:port])
28
+ http.use_ssl = options[:ssl]
29
+ http.read_timeout = 8
30
+ http.open_timeout = 4
31
+
32
+ @http = http
33
+ end
34
+
35
+ # public: Posts the app_id and batch of messages to the API.
36
+ #
37
+ # returns - Response of the status and error if it exists
38
+ def post(app_id, batch)
39
+ status, error = nil, nil
40
+ remaining_retries = @retries
41
+ backoff = @backoff
42
+ headers = { 'Content-Type' => 'application/json', 'accept' => 'application/json' }
43
+ begin
44
+ payload = JSON.generate :sentAt => datetime_in_iso8601(Time.new), :batch => batch
45
+ request = Net::HTTP::Post.new(@path, headers)
46
+ request.basic_auth app_id, nil
47
+
48
+ if self.class.stub
49
+ status = 200
50
+ error = nil
51
+ logger.debug "stubbed request to #{@path}: app id = #{app_id}, payload = #{payload}"
52
+ else
53
+ res = @http.request(request, payload)
54
+ status = res.code.to_i
55
+ body = JSON.parse(res.body)
56
+ error = body["error"]
57
+ end
58
+ rescue Exception => e
59
+ unless (remaining_retries -=1).zero?
60
+ sleep(backoff)
61
+ retry
62
+ end
63
+
64
+ logger.error e.message
65
+ e.backtrace.each { |line| logger.error line }
66
+ status = -1
67
+ error = "Connection error: #{e}"
68
+ end
69
+
70
+ Response.new status, error
71
+ end
72
+
73
+ class << self
74
+ attr_accessor :stub
75
+
76
+ def stub
77
+ @stub || ENV['STUB']
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,16 @@
1
+ module Segment
2
+ class Analytics
3
+ class Response
4
+ attr_reader :status, :error
5
+
6
+ # public: Simple class to wrap responses from the API
7
+ #
8
+ #
9
+ def initialize(status = 200, error = nil)
10
+ @status = status
11
+ @error = error
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,88 @@
1
+ require 'securerandom'
2
+
3
+ module Segment
4
+ class Analytics
5
+ module Utils
6
+ extend self
7
+
8
+ # public: Return a new hash with keys converted from strings to symbols
9
+ #
10
+ def symbolize_keys(hash)
11
+ hash.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
12
+ end
13
+
14
+ # public: Convert hash keys from strings to symbols in place
15
+ #
16
+ def symbolize_keys!(hash)
17
+ hash.replace symbolize_keys hash
18
+ end
19
+
20
+ # public: Return a new hash with keys as strings
21
+ #
22
+ def stringify_keys(hash)
23
+ hash.inject({}) { |memo, (k,v)| memo[k.to_s] = v; memo }
24
+ end
25
+
26
+ # public: Returns a new hash with all the date values in the into iso8601
27
+ # strings
28
+ #
29
+ def isoify_dates(hash)
30
+ hash.inject({}) { |memo, (k, v)|
31
+ memo[k] = datetime_in_iso8601(v)
32
+ memo
33
+ }
34
+ end
35
+
36
+ # public: Converts all the date values in the into iso8601 strings in place
37
+ #
38
+ def isoify_dates!(hash)
39
+ hash.replace isoify_dates hash
40
+ end
41
+
42
+ # public: Returns a uid string
43
+ #
44
+ def uid
45
+ arr = SecureRandom.random_bytes(16).unpack("NnnnnN")
46
+ arr[2] = (arr[2] & 0x0fff) | 0x4000
47
+ arr[3] = (arr[3] & 0x3fff) | 0x8000
48
+ "%08x-%04x-%04x-%04x-%04x%08x" % arr
49
+ end
50
+
51
+ def datetime_in_iso8601 datetime
52
+ case datetime
53
+ when Time
54
+ time_in_iso8601 datetime
55
+ when DateTime
56
+ time_in_iso8601 datetime.to_time
57
+ when Date
58
+ date_in_iso8601 datetime
59
+ else
60
+ datetime
61
+ end
62
+ end
63
+
64
+ def time_in_iso8601 time, fraction_digits = 3
65
+ fraction = if fraction_digits > 0
66
+ (".%06i" % time.usec)[0, fraction_digits + 1]
67
+ end
68
+
69
+ "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(time, true, 'Z')}"
70
+ end
71
+
72
+ def date_in_iso8601 date
73
+ date.strftime("%F")
74
+ end
75
+
76
+ def formatted_offset time, colon = true, alternate_utc_string = nil
77
+ time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon)
78
+ end
79
+
80
+ def seconds_to_utc_offset(seconds, colon = true)
81
+ (colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON) % [(seconds < 0 ? '-' : '+'), (seconds.abs / 3600), ((seconds.abs % 3600) / 60)]
82
+ end
83
+
84
+ UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
85
+ UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,5 @@
1
+ module Segment
2
+ class Analytics
3
+ VERSION = '2.0.13'
4
+ end
5
+ end
@@ -0,0 +1,60 @@
1
+ require 'segment/analytics/defaults'
2
+ require 'segment/analytics/utils'
3
+ require 'segment/analytics/defaults'
4
+ require 'segment/analytics/request'
5
+
6
+ module Segment
7
+ class Analytics
8
+ class Worker
9
+ include Segment::Analytics::Utils
10
+ include Segment::Analytics::Defaults
11
+
12
+ # public: Creates a new worker
13
+ #
14
+ # The worker continuously takes messages off the queue
15
+ # and makes requests to the segment.io api
16
+ #
17
+ # queue - Queue synchronized between client and worker
18
+ # app_id - String of the project's app_id
19
+ # options - Hash of worker options
20
+ # batch_size - Fixnum of how many items to send in a batch
21
+ # on_error - Proc of what to do on an error
22
+ #
23
+ def initialize(queue, app_id, options = {})
24
+ symbolize_keys! options
25
+ @queue = queue
26
+ @app_id = app_id
27
+ @batch_size = options[:batch_size] || Queue::BATCH_SIZE
28
+ @on_error = options[:on_error] || Proc.new { |status, error| }
29
+ @batch = []
30
+ @lock = Mutex.new
31
+ end
32
+
33
+ # public: Continuously runs the loop to check for new events
34
+ #
35
+ def run
36
+ until Thread.current[:should_exit]
37
+ return if @queue.empty?
38
+
39
+ @lock.synchronize do
40
+ until @batch.length >= @batch_size || @queue.empty?
41
+ @batch << @queue.pop
42
+ end
43
+ end
44
+
45
+ res = Request.new.post @app_id, @batch
46
+
47
+ @lock.synchronize { @batch.clear }
48
+
49
+ @on_error.call res.status, res.error unless res.status == 200
50
+ end
51
+ end
52
+
53
+ # public: Check whether we have outstanding requests.
54
+ #
55
+ def is_requesting?
56
+ @lock.synchronize { !@batch.empty? }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,302 @@
1
+ require 'spec_helper'
2
+
3
+ module Segment
4
+ class Analytics
5
+ describe Client do
6
+ let(:client) { Client.new :write_key => WRITE_KEY }
7
+ let(:queue) { client.instance_variable_get :@queue }
8
+
9
+ describe '#initialize' do
10
+ it 'errors if no write_key is supplied' do
11
+ expect { Client.new }.to raise_error(ArgumentError)
12
+ end
13
+
14
+ it 'does not error if a write_key is supplied' do
15
+ expect do
16
+ Client.new :write_key => WRITE_KEY
17
+ end.to_not raise_error
18
+ end
19
+
20
+ it 'does not error if a write_key is supplied as a string' do
21
+ expect do
22
+ Client.new 'write_key' => WRITE_KEY
23
+ end.to_not raise_error
24
+ end
25
+ end
26
+
27
+ describe '#track' do
28
+ it 'errors without an event' do
29
+ expect { client.track(:user_id => 'user') }.to raise_error(ArgumentError)
30
+ end
31
+
32
+ it 'errors without a user_id' do
33
+ expect { client.track(:event => 'Event') }.to raise_error(ArgumentError)
34
+ end
35
+
36
+ it 'errors if properties is not a hash' do
37
+ expect {
38
+ client.track({
39
+ :user_id => 'user',
40
+ :event => 'Event',
41
+ :properties => [1,2,3]
42
+ })
43
+ }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ it 'uses the timestamp given' do
47
+ time = Time.parse("1990-07-16 13:30:00.123 UTC")
48
+
49
+ client.track({
50
+ :event => 'testing the timestamp',
51
+ :user_id => 'joe',
52
+ :timestamp => time
53
+ })
54
+
55
+ msg = queue.pop
56
+
57
+ expect(Time.parse(msg[:timestamp])).to eq(time)
58
+ end
59
+
60
+ it 'does not error with the required options' do
61
+ expect do
62
+ client.track Queued::TRACK
63
+ queue.pop
64
+ end.to_not raise_error
65
+ end
66
+
67
+ it 'does not error when given string keys' do
68
+ expect do
69
+ client.track Utils.stringify_keys(Queued::TRACK)
70
+ queue.pop
71
+ end.to_not raise_error
72
+ end
73
+
74
+ it 'converts time and date traits into iso8601 format' do
75
+ client.track({
76
+ :user_id => 'user',
77
+ :event => 'Event',
78
+ :properties => {
79
+ :time => Time.utc(2013),
80
+ :time_with_zone => Time.zone.parse('2013-01-01'),
81
+ :date_time => DateTime.new(2013,1,1),
82
+ :date => Date.new(2013,1,1),
83
+ :nottime => 'x'
84
+ }
85
+ })
86
+ message = queue.pop
87
+
88
+ expect(message[:properties][:time]).to eq('2013-01-01T00:00:00.000Z')
89
+ expect(message[:properties][:time_with_zone]).to eq('2013-01-01T00:00:00.000Z')
90
+ expect(message[:properties][:date_time]).to eq('2013-01-01T00:00:00.000Z')
91
+ expect(message[:properties][:date]).to eq('2013-01-01')
92
+ expect(message[:properties][:nottime]).to eq('x')
93
+ end
94
+ end
95
+
96
+
97
+ describe '#identify' do
98
+ it 'errors without any user id' do
99
+ expect { client.identify({}) }.to raise_error(ArgumentError)
100
+ end
101
+
102
+ it 'does not error with the required options' do
103
+ expect do
104
+ client.identify Queued::IDENTIFY
105
+ queue.pop
106
+ end.to_not raise_error
107
+ end
108
+
109
+ it 'does not error with the required options as strings' do
110
+ expect do
111
+ client.identify Utils.stringify_keys(Queued::IDENTIFY)
112
+ queue.pop
113
+ end.to_not raise_error
114
+ end
115
+
116
+ it 'converts time and date traits into iso8601 format' do
117
+ client.identify({
118
+ :user_id => 'user',
119
+ :traits => {
120
+ :time => Time.utc(2013),
121
+ :time_with_zone => Time.zone.parse('2013-01-01'),
122
+ :date_time => DateTime.new(2013,1,1),
123
+ :date => Date.new(2013,1,1),
124
+ :nottime => 'x'
125
+ }
126
+ })
127
+
128
+ message = queue.pop
129
+
130
+ expect(message[:traits][:time]).to eq('2013-01-01T00:00:00.000Z')
131
+ expect(message[:traits][:time_with_zone]).to eq('2013-01-01T00:00:00.000Z')
132
+ expect(message[:traits][:date_time]).to eq('2013-01-01T00:00:00.000Z')
133
+ expect(message[:traits][:date]).to eq('2013-01-01')
134
+ expect(message[:traits][:nottime]).to eq('x')
135
+ end
136
+ end
137
+
138
+ describe '#alias' do
139
+ it 'errors without from' do
140
+ expect { client.alias :user_id => 1234 }.to raise_error(ArgumentError)
141
+ end
142
+
143
+ it 'errors without to' do
144
+ expect { client.alias :previous_id => 1234 }.to raise_error(ArgumentError)
145
+ end
146
+
147
+ it 'does not error with the required options' do
148
+ expect { client.alias ALIAS }.to_not raise_error
149
+ end
150
+
151
+ it 'does not error with the required options as strings' do
152
+ expect do
153
+ client.alias Utils.stringify_keys(ALIAS)
154
+ end.to_not raise_error
155
+ end
156
+ end
157
+
158
+ describe '#group' do
159
+ after do
160
+ client.flush
161
+ end
162
+
163
+ it 'errors without group_id' do
164
+ expect { client.group :user_id => 'foo' }.to raise_error(ArgumentError)
165
+ end
166
+
167
+ it 'errors without user_id' do
168
+ expect { client.group :group_id => 'foo' }.to raise_error(ArgumentError)
169
+ end
170
+
171
+ it 'does not error with the required options' do
172
+ client.group Queued::GROUP
173
+ end
174
+
175
+ it 'does not error with the required options as strings' do
176
+ client.group Utils.stringify_keys(Queued::GROUP)
177
+ end
178
+
179
+ it 'converts time and date traits into iso8601 format' do
180
+ client.identify({
181
+ :user_id => 'user',
182
+ :group_id => 'group',
183
+ :traits => {
184
+ :time => Time.utc(2013),
185
+ :time_with_zone => Time.zone.parse('2013-01-01'),
186
+ :date_time => DateTime.new(2013,1,1),
187
+ :date => Date.new(2013,1,1),
188
+ :nottime => 'x'
189
+ }
190
+ })
191
+
192
+ message = queue.pop
193
+
194
+ expect(message[:traits][:time]).to eq('2013-01-01T00:00:00.000Z')
195
+ expect(message[:traits][:time_with_zone]).to eq('2013-01-01T00:00:00.000Z')
196
+ expect(message[:traits][:date_time]).to eq('2013-01-01T00:00:00.000Z')
197
+ expect(message[:traits][:date]).to eq('2013-01-01')
198
+ expect(message[:traits][:nottime]).to eq('x')
199
+ end
200
+ end
201
+
202
+ describe '#page' do
203
+ it 'errors without user_id' do
204
+ expect { client.page :name => 'foo' }.to raise_error(ArgumentError)
205
+ end
206
+
207
+ it 'does not error with the required options' do
208
+ expect { client.page Queued::PAGE }.to_not raise_error
209
+ end
210
+
211
+ it 'does not error with the required options as strings' do
212
+ expect do
213
+ client.page Utils.stringify_keys(Queued::PAGE)
214
+ end.to_not raise_error
215
+ end
216
+ end
217
+
218
+ describe '#screen' do
219
+ it 'errors without user_id' do
220
+ expect { client.screen :name => 'foo' }.to raise_error(ArgumentError)
221
+ end
222
+
223
+ it 'does not error with the required options' do
224
+ expect { client.screen Queued::SCREEN }.to_not raise_error
225
+ end
226
+
227
+ it 'does not error with the required options as strings' do
228
+ expect do
229
+ client.screen Utils.stringify_keys(Queued::SCREEN)
230
+ end.to_not raise_error
231
+ end
232
+ end
233
+
234
+ describe '#flush' do
235
+ it 'waits for the queue to finish on a flush' do
236
+ client.identify Queued::IDENTIFY
237
+ client.track Queued::TRACK
238
+ client.flush
239
+
240
+ expect(client.queued_messages).to eq(0)
241
+ end
242
+
243
+ it 'completes when the process forks' do
244
+ client.identify Queued::IDENTIFY
245
+
246
+ Process.fork do
247
+ client.track Queued::TRACK
248
+ client.flush
249
+ expect(client.queued_messages).to eq(0)
250
+ end
251
+
252
+ Process.wait
253
+ end unless defined? JRUBY_VERSION
254
+ end
255
+
256
+ context 'common' do
257
+ check_property = proc { |msg, k, v| msg[k] && msg[k] == v }
258
+
259
+ let(:data) { { :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event => "coco barked", :name => "coco" } }
260
+
261
+ it 'does not convert ids given as fixnums to strings' do
262
+ [:track, :screen, :page, :identify].each do |s|
263
+ client.send(s, data)
264
+ message = queue.pop(true)
265
+
266
+ expect(check_property.call(message, :userId, 1)).to eq(true)
267
+ expect(check_property.call(message, :anonymousId, 4)).to eq(true)
268
+ end
269
+ end
270
+
271
+ context 'group' do
272
+ it 'does not convert ids given as fixnums to strings' do
273
+ client.group(data)
274
+ message = queue.pop(true)
275
+
276
+ expect(check_property.call(message, :userId, 1)).to eq(true)
277
+ expect(check_property.call(message, :groupId, 2)).to eq(true)
278
+ end
279
+ end
280
+
281
+ context 'alias' do
282
+ it 'does not convert ids given as fixnums to strings' do
283
+ client.alias(data)
284
+ message = queue.pop(true)
285
+
286
+ expect(check_property.call(message, :userId, 1)).to eq(true)
287
+ expect(check_property.call(message, :previousId, 3)).to eq(true)
288
+ end
289
+ end
290
+
291
+ it 'sends integrations' do
292
+ [:track, :screen, :page, :group, :identify, :alias].each do |s|
293
+ client.send s, :integrations => { :All => true, :Salesforce => false }, :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event => "coco barked", :name => "coco"
294
+ message = queue.pop(true)
295
+ expect(message[:integrations][:All]).to eq(true)
296
+ expect(message[:integrations][:Salesforce]).to eq(false)
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end