supergood 0.0.1 → 0.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 665ca96dbcbfec2b928ff9dd1828ad464d63b23048346ee0324cbeb2b2f22977
4
- data.tar.gz: e29262ab2a84f91991bb912223acf3d23e2e9332cbd0a33d6ec03db6b8388be0
3
+ metadata.gz: 6a68d9434b0aa7e8bca7075c037ad289028c4391e9701996a07af8cfc576157a
4
+ data.tar.gz: 57d60fb1d8e8d1402191934ab02779e2bbb68fce2ddf69254273a2247d275e61
5
5
  SHA512:
6
- metadata.gz: a1b3f9b9f987bc19cf9e596126830576374cd75d3eb5179d1a9615f137191440bba6999a376a207847626ad2ee1041662fd2ca45be5c9d8ce88aff1c093cb28d
7
- data.tar.gz: e96124b5501e72db95ac4e6c9e76c69896f6c8d0542b33c085d060d43e73d9da16f998fcd0a880dfc5d847f39b8d30c27b81963442965d26e3a86649a629063c
6
+ metadata.gz: 93a92fe1e55fed593c7a8039bb4a5dd1fd05874b51178fced4cbf84780cd8dfb21bd89950c27dad37396de2979fd841445a67d30c4ed0b2bacf3ade2858c5b39
7
+ data.tar.gz: fb32a7b166621bd629952bf2fa414c3daa28e4e9b6b0fc2d44da1cef5e23a3f8a4ea5daf5a397ebab883dbd17461495f9e4fa229741550224afe0578a20ffd8b
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,39 @@ 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
+ log.debug("Using config %s" % @config.inspect)
50
+ self
45
51
  end
46
52
 
47
53
  def log
@@ -67,12 +73,9 @@ module Supergood
67
73
  end
68
74
 
69
75
  begin
70
- log.debug(data)
71
- # api.post_events(data)
76
+ api.post_events(data)
72
77
  rescue => e
73
- #TODO Add error posting to Supergood
74
- puts "Error posting events: #{e}"
75
- api.post_errors(e)
78
+ log.error(data, e, e.message)
76
79
  ensure
77
80
  @response_cache.clear
78
81
  @request_cache.clear if force
@@ -80,10 +83,16 @@ module Supergood
80
83
 
81
84
  end
82
85
 
86
+ def close(force = true)
87
+ log.debug('Cleaning up, flushing cache gracefully.')
88
+ @interval_thread.kill
89
+ flush_cache(force)
90
+ end
91
+
83
92
  def set_interval(delay)
84
93
  Thread.new do
85
94
  loop do
86
- sleep delay
95
+ sleep delay / 1000.0
87
96
  yield # call passed block
88
97
  end
89
98
  end
@@ -97,109 +106,77 @@ module Supergood
97
106
  @instance ||= Supergood.new
98
107
  end
99
108
 
100
- def intercept(http, request, request_body)
109
+ def intercept(request)
101
110
  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
111
+ requested_at = Time.now
112
+ if !ignored?(request[:domain])
113
+ cache_request(request_id, requested_at, request)
134
114
  end
135
- end
136
-
137
- def log_event?(http, request)
138
- !ignored?(http, request) && (http.started? || webmock?(http, request))
139
- end
140
115
 
141
- def ignored?(http, request)
142
- url = request_url(http, request)
143
- @ignored_domains.any? do |pattern|
144
- url =~ pattern
116
+ response = yield
117
+ if !ignored?(request[:domain]) && defined?(response)
118
+ cache_response(request_id, requested_at, response)
145
119
  end
146
- end
147
120
 
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)
121
+ return response[:original_response]
154
122
  end
155
123
 
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
124
+ def cache_request(request_id, requested_at, request)
125
+ begin
126
+ request_payload = {
127
+ id: request_id,
128
+ headers: request[:headers],
129
+ method: request[:method],
130
+ url: request[:url],
131
+ path: request[:path],
132
+ search: request[:search] || '',
133
+ body: Supergood::Utils.safe_parse_json(request[:body]),
134
+ requestedAt: requested_at
135
+ }
136
+ @request_cache[request_id] = {
137
+ request: request_payload
138
+ }
139
+ rescue => e
140
+ log.error({ request: request }, e, ERRORS[:CACHING_REQUEST])
165
141
  end
166
- header
167
142
  end
168
143
 
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}")
144
+ def cache_response(request_id, requested_at, response)
145
+ begin
146
+ responded_at = Time.now
147
+ duration = (responded_at - requested_at) * 1000
148
+ request_payload = @request_cache[request_id]
149
+ response_payload = {
150
+ headers: response[:headers],
151
+ status: response[:status],
152
+ statusText: response[:statusText],
153
+ body: Supergood::Utils.safe_parse_json(response[:body]),
154
+ respondedAt: responded_at,
155
+ duration: duration.round,
156
+ }
157
+ @response_cache[request_id] = Supergood::Utils.hash_values_from_keys(request_payload.merge({
158
+ response: response_payload
159
+ }), @keys_to_hash)
160
+ @request_cache.delete(request_id)
161
+ rescue => e
162
+ puts e
163
+ log.error(
164
+ { request: request_payload, response: response_payload },
165
+ e, ERRORS[:CACHING_RESPONSE]
166
+ )
167
+ end
183
168
  end
184
- end
185
- end
186
-
187
169
 
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)
170
+ def ignored?(domain)
171
+ base_domain = URI.parse(@base_url).hostname
172
+ if domain == base_domain
173
+ return true
174
+ else
175
+ @ignored_domains.any? do |ignored_domain|
176
+ pattern = URI.parse(ignored_domain).hostname
177
+ domain =~ pattern
178
+ end
179
+ end
195
180
  end
196
181
  end
197
182
  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.3'.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.3
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