supergood 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 665ca96dbcbfec2b928ff9dd1828ad464d63b23048346ee0324cbeb2b2f22977
4
- data.tar.gz: e29262ab2a84f91991bb912223acf3d23e2e9332cbd0a33d6ec03db6b8388be0
3
+ metadata.gz: '04808e64355a5f8aba52cf3c3bcda9e16489ad70b4013a3d2eaa08bb673c8d76'
4
+ data.tar.gz: b9dbf12918f60e5760a3bc3fb27107025da236ef70e2fcdfa4ce610c40c830ee
5
5
  SHA512:
6
- metadata.gz: a1b3f9b9f987bc19cf9e596126830576374cd75d3eb5179d1a9615f137191440bba6999a376a207847626ad2ee1041662fd2ca45be5c9d8ce88aff1c093cb28d
7
- data.tar.gz: e96124b5501e72db95ac4e6c9e76c69896f6c8d0542b33c085d060d43e73d9da16f998fcd0a880dfc5d847f39b8d30c27b81963442965d26e3a86649a629063c
6
+ metadata.gz: 98a2fdd4c0fc38d74288abc6e8e9360995d8d1cd171142d15e12750b29c03310a2d6a7dc3bf4dbde0548db1fedd4e73b421ee2885068cffc3447c21d5aac037a
7
+ data.tar.gz: 2611eccbf1fd3f712b2cbbb67504151a581ea589947beb7f2d2cfb38388597940c3f1647f7040d948f1d9cb1010fef2d59c290a6348de1323f8527956e3bd0ee
data/lib/supergood/api.rb CHANGED
@@ -1,19 +1,13 @@
1
1
  require 'faraday'
2
+ require 'dotenv'
3
+
4
+ Dotenv.load
2
5
 
3
6
  module Supergood
4
7
  class Api
5
- DEFAULT_SUPERGOOD_BASE_URL = 'https://dashboard.supergood.ai'
6
- DEFAULT_SUPERGOOD_CONFIG = {
7
- flush_interval: 1,
8
- event_sink_endpoint: DEFAULT_SUPERGOOD_BASE_URL + '/api/events',
9
- error_sink_endpoint: DEFAULT_SUPERGOOD_BASE_URL + '/api/errors',
10
- keys_to_hash: ['request.body', 'response.body'],
11
- ignored_domains: []
12
- }
13
-
14
8
  def initialize(header_options, base_url)
9
+ @base_url = base_url
15
10
  @header_options = header_options
16
- @config_fetch_url = base_url + '/api/config'
17
11
  end
18
12
 
19
13
  def log
@@ -24,25 +18,46 @@ module Supergood
24
18
  @log = logger
25
19
  end
26
20
 
27
- def set_event_sink_url(url)
28
- @event_sink_url = url
21
+ def set_event_sink_endpoint(endpoint)
22
+ @event_sink_endpoint = endpoint
29
23
  end
30
24
 
31
- def set_error_sink_url(url)
32
- @error_sink_url = url
25
+ def set_error_sink_endpoint(endpoint)
26
+ @error_sink_endpoint = endpoint
33
27
  end
34
28
 
35
29
  def post_events(payload)
36
- Faraday.post(@event_sink_url, payload, @header_options)
30
+ conn = Faraday.new(url: @base_url, headers: @header_options)
31
+ response = conn.post(@event_sink_endpoint, body = payload.to_json, headers = @header_options)
32
+ if response.status == 200
33
+ return JSON.parse(response.body, symbolize_names: true)
34
+ elsif response.status == 401
35
+ raise SupergoodException.new ERRORS[:UNAUTHORIZED]
36
+ elsif response.status != 200 && response.status != 201
37
+ raise SupergoodException.new ERRORS[:POSTING_EVENTS]
38
+ end
37
39
  end
38
40
 
39
41
  def post_errors(payload)
40
- Faraday.post(@event_sink_url, payload, @header_options)
42
+ conn = Faraday.new(url: @base_url, headers: @header_options)
43
+ response = conn.post(@error_sink_endpoint, body = payload.to_json, headers = @header_options)
44
+ if response.status == 200
45
+ return JSON.parse(response.body, symbolize_names: true)
46
+ else
47
+ @log.warn(ERRORS[:POSTING_ERRORS])
48
+ end
41
49
  end
42
50
 
43
51
  def fetch_config
44
- # Faraday.get(config_fetch_url, @header_options)
45
- DEFAULT_SUPERGOOD_CONFIG
52
+ conn = Faraday.new(url: @base_url, headers: @header_options)
53
+ response = conn.get('/api/config')
54
+ if response.status == 200
55
+ return JSON.parse(response.body, symbolize_names: true)
56
+ elsif response.status == 401
57
+ raise SupergoodException.new ERRORS[:UNAUTHORIZED]
58
+ elsif response.status != 200 && response.status != 201
59
+ raise SupergoodException.new ERRORS[:FETCHING_CONFIG]
60
+ end
46
61
  end
47
62
  end
48
63
  end
@@ -1,10 +1,11 @@
1
- require 'webmock'
2
1
  require 'json'
3
- require 'net/http'
4
- require 'uri'
5
2
  require 'securerandom'
6
3
  require 'dotenv'
7
4
  require 'base64'
5
+ require 'uri'
6
+
7
+ require_relative 'vendors/http'
8
+ require_relative 'vendors/net-http'
8
9
 
9
10
  Dotenv.load
10
11
 
@@ -14,34 +15,38 @@ module Supergood
14
15
 
15
16
  class << self
16
17
  def init(supergood_client_id=nil, supergood_client_secret=nil, base_url=nil)
17
-
18
18
  supergood_client_id = supergood_client_id || ENV['SUPERGOOD_CLIENT_ID']
19
19
  supergood_client_secret = supergood_client_secret || ENV['SUPERGOOD_CLIENT_SECRET']
20
- base_url = base_url || ENV['SUPERGOOD_BASE_URL'] || DEFAULT_SUPERGOOD_BASE_URL
21
- puts "Initializing Supergood with client_id: #{supergood_client_id}, client_secret: #{supergood_client_secret}, base_url: #{base_url}"
22
- header_options = {
23
- 'headers' => {
24
- 'Content-Type' => 'application/json',
25
- 'Authorization' => 'Basic ' + Base64.encode64(supergood_client_id + ':' + supergood_client_secret)
26
- }
27
- }
28
20
 
29
- @api = Supergood::Api.new(header_options, base_url)
30
- @logger = Supergood::Logger.new(@api, header_options, base_url)
21
+ if !supergood_client_id
22
+ raise SupergoodException.new ERRORS[:NO_CLIENT_ID]
23
+ end
24
+
25
+ if !supergood_client_secret
26
+ raise SupergoodException.new ERRORS[:NO_CLIENT_SECRET]
27
+ end
31
28
 
32
- config = api.fetch_config()
29
+ @base_url = base_url || ENV['SUPERGOOD_BASE_URL'] || DEFAULT_SUPERGOOD_BASE_URL
30
+ header_options = {
31
+ 'Content-Type' => 'application/json',
32
+ 'Authorization' => 'Basic ' + Base64.encode64(supergood_client_id + ':' + supergood_client_secret).gsub(/\n/, '')
33
+ }
33
34
 
34
- @ignored_domains = config[:ignored_domains]
35
- @keys_to_hash = config[:keys_to_hash]
35
+ @api = Supergood::Api.new(header_options, @base_url)
36
+ @config = @api.fetch_config
37
+ @ignored_domains = @config[:ignoredDomains]
38
+ @keys_to_hash = @config[:keysToHash]
39
+ @logger = Supergood::Logger.new(@api, @config, header_options)
36
40
 
37
- api.set_error_sink_url(base_url + config[:error_sink_endpoint])
38
- api.set_event_sink_url(base_url + config[:event_sink_endpoint])
39
- api.set_logger(@logger)
41
+ @api.set_error_sink_endpoint(@config[:errorSinkEndpoint])
42
+ @api.set_event_sink_endpoint(@config[:eventSinkEndpoint])
43
+ @api.set_logger(@logger)
40
44
 
41
45
  @request_cache = {}
42
46
  @response_cache = {}
43
47
 
44
- set_interval(config[:flush_interval]) { flush_cache }
48
+ @interval_thread = set_interval(@config[:flushInterval]) { flush_cache }
49
+ self
45
50
  end
46
51
 
47
52
  def log
@@ -67,12 +72,9 @@ module Supergood
67
72
  end
68
73
 
69
74
  begin
70
- log.debug(data)
71
- # api.post_events(data)
75
+ api.post_events(data)
72
76
  rescue => e
73
- #TODO Add error posting to Supergood
74
- puts "Error posting events: #{e}"
75
- api.post_errors(e)
77
+ log.error(data, e, e.message)
76
78
  ensure
77
79
  @response_cache.clear
78
80
  @request_cache.clear if force
@@ -80,10 +82,16 @@ module Supergood
80
82
 
81
83
  end
82
84
 
85
+ def close(force = true)
86
+ log.debug('Cleaning up, flushing cache gracefully.')
87
+ @interval_thread.kill
88
+ flush_cache(force)
89
+ end
90
+
83
91
  def set_interval(delay)
84
92
  Thread.new do
85
93
  loop do
86
- sleep delay
94
+ sleep delay / 1000.0
87
95
  yield # call passed block
88
96
  end
89
97
  end
@@ -97,109 +105,77 @@ module Supergood
97
105
  @instance ||= Supergood.new
98
106
  end
99
107
 
100
- def intercept(http, request, request_body)
108
+ def intercept(request)
101
109
  request_id = SecureRandom.uuid
102
- start_time = Time.now
103
- response = yield
104
- ensure
105
- if log_event?(http, request)
106
- time = ((Time.now - start_time) * 1000)
107
- url_payload = parse_url(http, request)
108
- @request_cache[request_id] = {
109
- request: {
110
- id: request_id,
111
- method: request.method,
112
- url: url_payload[:url],
113
- protocol: url_payload[:protocol],
114
- domain: url_payload[:domain],
115
- path: url_payload[:path],
116
- search: url_payload[:search],
117
- body: hash_specified_keys(request.body),
118
- header: hash_specified_keys(get_header(request)),
119
- }
120
- }
121
- if defined?(response) && response
122
- request_payload = @request_cache[request_id]
123
- @response_cache[request_id] = {
124
- request: request_payload,
125
- response: {
126
- status: response.code,
127
- status_text: response.message,
128
- header: hash_specified_keys(get_header(response)),
129
- body: hash_specified_keys(response.body),
130
- }
131
- }
132
- @request_cache.delete(request_id)
133
- end
110
+ requested_at = Time.now
111
+ if !ignored?(request[:domain])
112
+ cache_request(request_id, requested_at, request)
134
113
  end
135
- end
136
-
137
- def log_event?(http, request)
138
- !ignored?(http, request) && (http.started? || webmock?(http, request))
139
- end
140
114
 
141
- def ignored?(http, request)
142
- url = request_url(http, request)
143
- @ignored_domains.any? do |pattern|
144
- url =~ pattern
115
+ response = yield
116
+ if !ignored?(request[:domain]) && defined?(response)
117
+ cache_response(request_id, requested_at, response)
145
118
  end
146
- end
147
119
 
148
- def webmock?(http, request)
149
- return false unless defined?(::WebMock)
150
- uri = request_uri_as_string(http, request)
151
- method = request.method.downcase.to_sym
152
- signature = WebMock::RequestSignature.new(method, uri)
153
- ::WebMock.registered_request?(signature)
120
+ return response[:original_response]
154
121
  end
155
122
 
156
- # TODO: Hash keys, move to Utils?
157
- def hash_specified_keys(body_or_header)
158
- body_or_header
159
- end
160
-
161
- def get_header(request_or_response)
162
- header = {}
163
- request_or_response.each_header do |k,v|
164
- header[k] = v
123
+ def cache_request(request_id, requested_at, request)
124
+ begin
125
+ request_payload = {
126
+ id: request_id,
127
+ headers: request[:headers],
128
+ method: request[:method],
129
+ url: request[:url],
130
+ path: request[:path],
131
+ search: request[:search] || '',
132
+ body: Supergood::Utils.safe_parse_json(request[:body]),
133
+ requestedAt: requested_at
134
+ }
135
+ @request_cache[request_id] = {
136
+ request: request_payload
137
+ }
138
+ rescue => e
139
+ log.error({ request: request }, e, ERRORS[:CACHING_REQUEST])
165
140
  end
166
- header
167
141
  end
168
142
 
169
- def parse_url(http, request)
170
- url = request_url(http, request)
171
- uri = URI.parse(url)
172
- {
173
- url: url,
174
- protocol: uri.scheme,
175
- domain: uri.host,
176
- path: uri.path,
177
- search: uri.query,
178
- }
179
- end
180
-
181
- def request_url(http, request)
182
- URI::DEFAULT_PARSER.unescape("http#{"s" if http.use_ssl?}://#{http.address}:#{http.port}#{request.path}")
143
+ def cache_response(request_id, requested_at, response)
144
+ begin
145
+ responded_at = Time.now
146
+ duration = (responded_at - requested_at) * 1000
147
+ request_payload = @request_cache[request_id]
148
+ response_payload = {
149
+ headers: response[:headers],
150
+ status: response[:status],
151
+ statusText: response[:statusText],
152
+ body: Supergood::Utils.safe_parse_json(response[:body]),
153
+ respondedAt: responded_at,
154
+ duration: duration.round,
155
+ }
156
+ @response_cache[request_id] = Supergood::Utils.hash_values_from_keys(request_payload.merge({
157
+ response: response_payload
158
+ }), @keys_to_hash)
159
+ @request_cache.delete(request_id)
160
+ rescue => e
161
+ puts e
162
+ log.error(
163
+ { request: request_payload, response: response_payload },
164
+ e, ERRORS[:CACHING_RESPONSE]
165
+ )
166
+ end
183
167
  end
184
- end
185
- end
186
-
187
168
 
188
- # Attach library to Net::HTTP and WebMock
189
- block = lambda do |a|
190
- # raise instance_methods.inspect
191
- alias request_without_net_http_logger request
192
- def request(request, body = nil, &block)
193
- Supergood.intercept(self, request, body) do
194
- request_without_net_http_logger(request, body, &block)
169
+ def ignored?(domain)
170
+ base_domain = URI.parse(@base_url).hostname
171
+ if domain == base_domain
172
+ return true
173
+ else
174
+ @ignored_domains.any? do |ignored_domain|
175
+ pattern = URI.parse(ignored_domain).hostname
176
+ domain =~ pattern
177
+ end
178
+ end
195
179
  end
196
180
  end
197
181
  end
198
-
199
- if defined?(::WebMock)
200
- klass = WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_get("@webMockNetHTTP")
201
- # raise klass.instance_methods.inspect
202
- klass.class_eval(&block)
203
- end
204
-
205
- Net::HTTP.class_eval(&block)
@@ -0,0 +1,25 @@
1
+ ERRORS = {
2
+ CACHING_RESPONSE: 'Error Caching Response',
3
+ CACHING_REQUEST: 'Error Caching Request',
4
+ DUMPING_DATA_TO_DISK: 'Error Dumping Data to Disk',
5
+ POSTING_EVENTS: 'Error Posting Events',
6
+ POSTING_ERRORS: 'Error Posting Errors',
7
+ FETCHING_CONFIG: 'Error Fetching Config',
8
+ WRITING_TO_DISK: 'Error writing to disk',
9
+ TEST_ERROR: 'Test Error for Testing Purposes',
10
+ UNAUTHORIZED: 'Unauthorized: Invalid Client ID or Secret. Exiting.',
11
+ NO_CLIENT_ID:
12
+ 'No Client ID Provided, set SUPERGOOD_CLIENT_ID or pass it as an argument',
13
+ NO_CLIENT_SECRET:
14
+ 'No Client Secret Provided, set SUPERGOOD_CLIENT_SECRET or pass it as an argument'
15
+ };
16
+
17
+ DEFAULT_SUPERGOOD_BYTE_LIMIT = 500000
18
+
19
+ # GZIP_START_BYTES = b'\x1f\x8b'
20
+
21
+ class SupergoodException < StandardError
22
+ def initialize(msg = message)
23
+ super(msg)
24
+ end
25
+ end
@@ -5,20 +5,27 @@ Dotenv.load
5
5
 
6
6
  module Supergood
7
7
  class Logger < Logger
8
- def initialize(post_errors, config, header_options)
8
+ def initialize(api, config, header_options)
9
9
  super(STDOUT)
10
- @post_errors = post_errors
10
+ @api = api
11
11
  @config = config
12
12
  @header_options = header_options
13
13
  end
14
14
 
15
- def post_errors
16
- @post_errors
17
- end
18
-
19
- def warn(payload, error, msg)
20
- super
21
- post_errors({ error: error, message: msg, payload: payload })
15
+ def error(data, error, msg)
16
+ super(error)
17
+ @api.post_errors(
18
+ {
19
+ error: error.backtrace.join('\n'),
20
+ message: msg,
21
+ payload: {
22
+ config: @config,
23
+ data: data,
24
+ packageName: 'supergood-rb',
25
+ packageVersion: Supergood::VERSION
26
+ }
27
+ }
28
+ )
22
29
  end
23
30
 
24
31
  def debug(payload)
@@ -1,8 +1,72 @@
1
1
  require 'rudash'
2
+ require 'digest'
2
3
 
3
4
  module Supergood
4
5
  module Utils
5
- def hash_specified_keys
6
+ def self.hash_value(input)
7
+ hash = Digest::SHA1.new
8
+ if input == nil
9
+ return ''
10
+ elsif input.class == Array
11
+ return [Base64.strict_encode64(hash.update(input.to_json).to_s)]
12
+ elsif input.class == Hash
13
+ return {'hashed': Base64.strict_encode64(hash.update(input.to_json).to_s)}
14
+ elsif input.class == String
15
+ return Base64.strict_encode64(hash.update(input).to_s)
16
+ end
17
+ end
18
+
19
+ # Hash values from specified keys, or hash if the bodies exceed a byte limit
20
+ def self.hash_values_from_keys(obj, keys_to_hash, byte_limit=DEFAULT_SUPERGOOD_BYTE_LIMIT)
21
+ _obj = obj
22
+
23
+ if !keys_to_hash.include?('response.body')
24
+ payload = R_.get(_obj, 'response.body')
25
+ payload_size = payload.to_s.length()
26
+ if(payload_size >= byte_limit)
27
+ R_.set(_obj, 'response.body', Supergood::Utils.hash_value(payload))
28
+ end
29
+ end
30
+
31
+ if !keys_to_hash.include?('request.body')
32
+ payload = R_.get(_obj, 'request.body')
33
+ payload_size = payload.to_s.length()
34
+ if(payload_size >= byte_limit)
35
+ R_.set(_obj, 'request.body', Supergood::Utils.hash_value(payload))
36
+ end
37
+ end
38
+
39
+ keys_to_hash.each { |key|
40
+ value = R_.get(_obj, key)
41
+ if !!value
42
+ R_.set(_obj, key, Supergood::Utils.hash_value(value))
43
+ end
44
+ }
45
+
46
+ return _obj
47
+ end
48
+
49
+ def self.safe_parse_json(input)
50
+ if !input || input == ''
51
+ return ''
52
+ end
53
+
54
+ begin
55
+ return JSON.parse(input)
56
+ rescue => e
57
+ input
58
+ end
59
+ end
60
+ def self.get_header(request_or_response)
61
+ header = {}
62
+ request_or_response.each_header do |k,v|
63
+ header[k] = v
64
+ end
65
+ header
66
+ end
67
+
68
+ def self.request_url(http, request)
69
+ URI::DEFAULT_PARSER.unescape("http#{"s" if http.use_ssl?}://#{http.address}#{request.path}")
6
70
  end
7
71
  end
8
72
  end
@@ -0,0 +1,34 @@
1
+ module Supergood
2
+ module Vendor
3
+ module HTTPrb
4
+ if defined?(::HTTP)
5
+ HTTP::Client.class_eval {
6
+ alias original_perform perform
7
+ def perform(original_request_payload, original_options)
8
+ request = {
9
+ headers: original_request_payload.headers.to_hash,
10
+ method: original_request_payload.verb.upcase.to_s,
11
+ body: Supergood::Utils.safe_parse_json(original_request_payload.body.source),
12
+ url: original_request_payload.uri.to_s,
13
+ path: original_request_payload.uri.path,
14
+ search: original_request_payload.uri.query,
15
+ domain: original_request_payload.uri.host
16
+ }
17
+ Supergood.intercept(request) do
18
+ original_response = original_perform(original_request_payload, original_options)
19
+ status, statusText = original_response.status.to_s.split(' ')
20
+ {
21
+ headers: original_response.headers.to_hash,
22
+ status: status,
23
+ statusText: statusText,
24
+ body: Supergood::Utils.safe_parse_json(original_response),
25
+ original_response: original_response
26
+ }
27
+ end
28
+ end
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,46 @@
1
+
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Supergood
6
+ module Vendor
7
+ module NetHTTP
8
+ block = lambda do |x|
9
+ alias original_request_method request
10
+ def request(original_request_payload, body = nil, &block)
11
+ http = self;
12
+ url = Supergood::Utils.request_url(http, original_request_payload)
13
+ uri = URI.parse(url)
14
+ request = {
15
+ headers: Supergood::Utils.get_header(original_request_payload),
16
+ method: original_request_payload.method,
17
+ body: original_request_payload.body,
18
+ url: url,
19
+ path: original_request_payload.path,
20
+ search: uri.query,
21
+ domain: uri.host,
22
+ }
23
+ Supergood.intercept(request) do
24
+ original_response = original_request_method(original_request_payload, body, &block)
25
+ {
26
+ headers: Supergood::Utils.get_header(original_response),
27
+ status: original_response.code,
28
+ statusText: original_response.message,
29
+ body: original_response.body,
30
+ original_response: original_response
31
+ }
32
+ end
33
+ end
34
+ end
35
+
36
+ if defined?(Net::HTTP)
37
+ Net::HTTP.class_eval(&block)
38
+ end
39
+
40
+ if defined?(::WebMock)
41
+ WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_get("@webMockNetHTTP").class_eval(&block)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,3 @@
1
+ module Supergood
2
+ VERSION = '0.0.2'.freeze
3
+ end
data/lib/supergood.rb CHANGED
@@ -3,4 +3,6 @@ module Supergood
3
3
  require_relative 'supergood/logger'
4
4
  require_relative 'supergood/client'
5
5
  require_relative 'supergood/utils'
6
+ require_relative 'supergood/constants'
7
+ require_relative 'supergood/version'
6
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: supergood
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Klarfeld
@@ -19,14 +19,18 @@ files:
19
19
  - lib/supergood.rb
20
20
  - lib/supergood/api.rb
21
21
  - lib/supergood/client.rb
22
+ - lib/supergood/constants.rb
22
23
  - lib/supergood/logger.rb
23
24
  - lib/supergood/utils.rb
24
- - lib/version.rb
25
+ - lib/supergood/vendors/http.rb
26
+ - lib/supergood/vendors/net-http.rb
27
+ - lib/supergood/version.rb
25
28
  homepage: https://supergood.ai
26
29
  licenses:
27
- - Business Source License 1.1
30
+ - Nonstandard
28
31
  metadata:
29
32
  source_code_uri: https://github.com/supergoodsystems/supergood-rb
33
+ license: BUSL-1.1
30
34
  post_install_message:
31
35
  rdoc_options: []
32
36
  require_paths:
data/lib/version.rb DELETED
@@ -1,3 +0,0 @@
1
- module Supergood
2
- VERSION = '0.0.1'.freeze
3
- end