supergood 0.0.1 → 0.0.2

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: '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