readme-metrics 0.1.0
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 +7 -0
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +87 -0
- data/LICENSE +13 -0
- data/README.md +94 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/http_request.rb +84 -0
- data/lib/readme/errors.rb +33 -0
- data/lib/readme/filter.rb +46 -0
- data/lib/readme/har/collection.rb +24 -0
- data/lib/readme/har/request_serializer.rb +56 -0
- data/lib/readme/har/response_serializer.rb +80 -0
- data/lib/readme/har/serializer.rb +74 -0
- data/lib/readme/metrics.rb +86 -0
- data/lib/readme/metrics/version.rb +5 -0
- data/lib/readme/payload.rb +18 -0
- data/lib/readme/request_queue.rb +37 -0
- data/readme-metrics.gemspec +29 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 00a1c3d34d8c61c9fbcaacc3914ddb3e03837ce22bd3325dbabd30ba5b4c373e
|
4
|
+
data.tar.gz: 34665a455715eaf423370cba89b31575dc87d53169c9bb8680f8b87a847e1704
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '05980a5c1bbdb0b23d438f18c8fcb27f931e3781e2e41ce938dff13611bfc6b70d8c61c141f644d4eeb3b1381019a3eff38e52da2adeb223cd1761ba8b6e53e7'
|
7
|
+
data.tar.gz: 0a084c22580cc59868089773b85dec83ad295c1984ac616f1ad8a947a8914130f35c19c78bf05b8945188cddbf49882391a8375581c5f5f50d0af0531e9db2ea
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in readme-metrics.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "json-schema"
|
9
|
+
gem "rack-test"
|
10
|
+
gem "rake", "~> 12.0"
|
11
|
+
gem "rspec", "~> 3.0"
|
12
|
+
gem "standard"
|
13
|
+
gem "webmock"
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
readme-metrics (0.1.0)
|
5
|
+
httparty (~> 0.18)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
addressable (2.7.0)
|
11
|
+
public_suffix (>= 2.0.2, < 5.0)
|
12
|
+
ast (2.4.1)
|
13
|
+
crack (0.4.3)
|
14
|
+
safe_yaml (~> 1.0.0)
|
15
|
+
diff-lcs (1.4.4)
|
16
|
+
hashdiff (1.0.1)
|
17
|
+
httparty (0.18.1)
|
18
|
+
mime-types (~> 3.0)
|
19
|
+
multi_xml (>= 0.5.2)
|
20
|
+
json-schema (2.8.1)
|
21
|
+
addressable (>= 2.4)
|
22
|
+
mime-types (3.3.1)
|
23
|
+
mime-types-data (~> 3.2015)
|
24
|
+
mime-types-data (3.2020.0512)
|
25
|
+
multi_xml (0.6.0)
|
26
|
+
parallel (1.19.2)
|
27
|
+
parser (2.7.1.4)
|
28
|
+
ast (~> 2.4.1)
|
29
|
+
public_suffix (4.0.5)
|
30
|
+
rack (2.2.3)
|
31
|
+
rack-test (1.1.0)
|
32
|
+
rack (>= 1.0, < 3)
|
33
|
+
rainbow (3.0.0)
|
34
|
+
rake (12.3.3)
|
35
|
+
regexp_parser (1.7.1)
|
36
|
+
rexml (3.2.4)
|
37
|
+
rspec (3.9.0)
|
38
|
+
rspec-core (~> 3.9.0)
|
39
|
+
rspec-expectations (~> 3.9.0)
|
40
|
+
rspec-mocks (~> 3.9.0)
|
41
|
+
rspec-core (3.9.2)
|
42
|
+
rspec-support (~> 3.9.3)
|
43
|
+
rspec-expectations (3.9.2)
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
45
|
+
rspec-support (~> 3.9.0)
|
46
|
+
rspec-mocks (3.9.1)
|
47
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
48
|
+
rspec-support (~> 3.9.0)
|
49
|
+
rspec-support (3.9.3)
|
50
|
+
rubocop (0.85.1)
|
51
|
+
parallel (~> 1.10)
|
52
|
+
parser (>= 2.7.0.1)
|
53
|
+
rainbow (>= 2.2.2, < 4.0)
|
54
|
+
regexp_parser (>= 1.7)
|
55
|
+
rexml
|
56
|
+
rubocop-ast (>= 0.0.3)
|
57
|
+
ruby-progressbar (~> 1.7)
|
58
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
59
|
+
rubocop-ast (0.3.0)
|
60
|
+
parser (>= 2.7.1.4)
|
61
|
+
rubocop-performance (1.6.1)
|
62
|
+
rubocop (>= 0.71.0)
|
63
|
+
ruby-progressbar (1.10.1)
|
64
|
+
safe_yaml (1.0.5)
|
65
|
+
standard (0.4.7)
|
66
|
+
rubocop (~> 0.85.0)
|
67
|
+
rubocop-performance (~> 1.6.0)
|
68
|
+
unicode-display_width (1.7.0)
|
69
|
+
webmock (3.8.3)
|
70
|
+
addressable (>= 2.3.6)
|
71
|
+
crack (>= 0.3.2)
|
72
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
73
|
+
|
74
|
+
PLATFORMS
|
75
|
+
ruby
|
76
|
+
|
77
|
+
DEPENDENCIES
|
78
|
+
json-schema
|
79
|
+
rack-test
|
80
|
+
rake (~> 12.0)
|
81
|
+
readme-metrics!
|
82
|
+
rspec (~> 3.0)
|
83
|
+
standard
|
84
|
+
webmock
|
85
|
+
|
86
|
+
BUNDLED WITH
|
87
|
+
2.1.4
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2020, ReadMe
|
2
|
+
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose
|
4
|
+
with or without fee is hereby granted, provided that the above copyright notice
|
5
|
+
and this permission notice appear in all copies.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
9
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
11
|
+
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
12
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
13
|
+
THIS SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# readmeio
|
2
|
+
|
3
|
+
Track your API metrics within ReadMe.
|
4
|
+
|
5
|
+
[](https://readme.io)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add it to your Gemfile
|
10
|
+
|
11
|
+
`gem "readme-metrics"`
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
`Readme::Metrics` is a Rack middleware and is compatible with all Rack-based
|
16
|
+
apps, including Rails.
|
17
|
+
|
18
|
+
When configuring the middleware, you must provide a block to tell the
|
19
|
+
middleware how to get values for the current user. These may be values taken
|
20
|
+
from the environment, or you may hardcode them.
|
21
|
+
|
22
|
+
If you're using Warden-based authentication like Devise, you may fetch the
|
23
|
+
current_user for a given request from the environment.
|
24
|
+
|
25
|
+
### Batching requests
|
26
|
+
|
27
|
+
By default, the middleware will batch requests to the ReadMe API in groups of
|
28
|
+
10. For every 10 requests made to your application, the middleware will make a
|
29
|
+
single request to ReadMe. If you wish to override this, provide a
|
30
|
+
`buffer_length` option when configuring the middleware.
|
31
|
+
|
32
|
+
### Sensitive Data
|
33
|
+
|
34
|
+
If you have sensitive data you'd like to prevent from being sent to the Metrics
|
35
|
+
API via headers, query params or payload bodies, you can specify a list of keys
|
36
|
+
to filter via the `reject_params` option. Key-value pairs matching these keys
|
37
|
+
will not be included in the request to the Metrics API.
|
38
|
+
|
39
|
+
You are also able to specify a set of `allow_only` which should only be sent through.
|
40
|
+
Any header or body values not matching these keys will be filtered out and not
|
41
|
+
send to the API.
|
42
|
+
|
43
|
+
You may only specify either `reject_params` or `allow_only` keys, not both.
|
44
|
+
|
45
|
+
### Rails
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# application.rb
|
49
|
+
require "readme/metrics"
|
50
|
+
|
51
|
+
options = {
|
52
|
+
api_key: "YOUR_API_KEY",
|
53
|
+
development: false,
|
54
|
+
reject_params: ["not_included", "dont_send"],
|
55
|
+
buffer_length: 5,
|
56
|
+
}
|
57
|
+
|
58
|
+
config.middleware.use Readme::Metrics, options do |env|
|
59
|
+
current_user = env['warden'].authenticate(scope: :current_user)
|
60
|
+
|
61
|
+
{
|
62
|
+
id: current_user.id
|
63
|
+
label: current_user.full_name,
|
64
|
+
email: current_user.email
|
65
|
+
}
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Rack::Builder
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
Rack::Builder.new do |builder|
|
73
|
+
options = {
|
74
|
+
api_key: "YOUR_API_KEY",
|
75
|
+
development: false,
|
76
|
+
reject_params: ["not_included", "dont_send"]
|
77
|
+
}
|
78
|
+
|
79
|
+
builder.use Readme::Metrics, options do |env|
|
80
|
+
{
|
81
|
+
id: "my_application_id"
|
82
|
+
label: "My Application",
|
83
|
+
email: "my.application@example.com"
|
84
|
+
}
|
85
|
+
end
|
86
|
+
builder.run your_app
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
## License
|
91
|
+
|
92
|
+
[View our license here](https://github.com/readmeio/metrics-sdks/tree/master/packages/ruby/LICENSE)
|
93
|
+
|
94
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "readme/metrics"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/http_request.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/request"
|
3
|
+
|
4
|
+
class HttpRequest
|
5
|
+
HTTP_NON_HEADERS = [
|
6
|
+
Rack::HTTP_COOKIE,
|
7
|
+
Rack::HTTP_VERSION,
|
8
|
+
Rack::HTTP_HOST,
|
9
|
+
Rack::HTTP_PORT
|
10
|
+
]
|
11
|
+
|
12
|
+
def initialize(env)
|
13
|
+
@request = Rack::Request.new(env)
|
14
|
+
end
|
15
|
+
|
16
|
+
def url
|
17
|
+
@request.url
|
18
|
+
end
|
19
|
+
|
20
|
+
def query_params
|
21
|
+
@request.GET
|
22
|
+
end
|
23
|
+
|
24
|
+
def cookies
|
25
|
+
@request.cookies
|
26
|
+
end
|
27
|
+
|
28
|
+
def http_version
|
29
|
+
@request.get_header(Rack::HTTP_VERSION)
|
30
|
+
end
|
31
|
+
|
32
|
+
def request_method
|
33
|
+
@request.request_method
|
34
|
+
end
|
35
|
+
|
36
|
+
def content_type
|
37
|
+
@request.content_type
|
38
|
+
end
|
39
|
+
|
40
|
+
def form_data?
|
41
|
+
@request.form_data?
|
42
|
+
end
|
43
|
+
|
44
|
+
def content_length
|
45
|
+
@request.content_length.to_i
|
46
|
+
end
|
47
|
+
|
48
|
+
def headers
|
49
|
+
@request
|
50
|
+
.each_header
|
51
|
+
.select { |key, _| http_header?(key) }
|
52
|
+
.to_h
|
53
|
+
.transform_keys { |header| normalize_header_name(header) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def body
|
57
|
+
@request.body.rewind
|
58
|
+
content = @request.body.read
|
59
|
+
@request.body.rewind
|
60
|
+
|
61
|
+
content
|
62
|
+
end
|
63
|
+
|
64
|
+
def parsed_form_data
|
65
|
+
@request.POST
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# "headers" in Rack::Request just means any key in the env. The HTTP headers
|
71
|
+
# are all the headers prefixed with `HTTP_` as per the spec:
|
72
|
+
# https://github.com/rack/rack/blob/master/SPEC.rdoc#the-environment-
|
73
|
+
# Other "headers" like version and host are prefixed with `HTTP_` by Rack but
|
74
|
+
# don't seem to be considered legit HTTP headers.
|
75
|
+
def http_header?(name)
|
76
|
+
name.start_with?("HTTP") && !HTTP_NON_HEADERS.include?(name)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Headers like `Content-Type: application/json` come into rack like
|
80
|
+
# `"HTTP_CONTENT_TYPE" => "application/json"`.
|
81
|
+
def normalize_header_name(header)
|
82
|
+
header.delete_prefix("HTTP_").split("_").map(&:capitalize).join("-")
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Readme
|
2
|
+
class Errors
|
3
|
+
API_KEY_ERROR = "Missing API Key"
|
4
|
+
REJECT_PARAMS_ERROR = "The `reject_params` option must be an array of strings"
|
5
|
+
ALLOW_ONLY_ERROR = "The `allow_only` option must be an array of strings"
|
6
|
+
BUFFER_LENGTH_ERROR = "The `buffer_length` must be an Integer"
|
7
|
+
DEVELOPMENT_ERROR = "The `development` option must be a boolean"
|
8
|
+
|
9
|
+
MISSING_BLOCK_ERROR = <<~MESSAGE
|
10
|
+
Missing block argument. You must provide a block when configuring the
|
11
|
+
middleware. The block must return a hash containing user info:
|
12
|
+
use Readme::Metrics, options do |env|
|
13
|
+
{ id: "unique_id", label: "Your user label", email: "Your user email" }
|
14
|
+
end
|
15
|
+
MESSAGE
|
16
|
+
|
17
|
+
def self.bad_block_message(result)
|
18
|
+
<<~MESSAGE
|
19
|
+
The request could not be sent to Readme. Something went wrong when
|
20
|
+
setting user info. Double check the block configured on the ReadMe
|
21
|
+
middleware.
|
22
|
+
|
23
|
+
Expected a hash with the shape:
|
24
|
+
{ id: "unique_id", label: "Your user label", email: "Your user email" }
|
25
|
+
|
26
|
+
Received value:
|
27
|
+
#{result}
|
28
|
+
MESSAGE
|
29
|
+
end
|
30
|
+
|
31
|
+
class ConfigurationError < StandardError; end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Filter
|
2
|
+
def self.for(reject: nil, allow_only: nil)
|
3
|
+
if !reject.nil? && !allow_only.nil?
|
4
|
+
raise FilterArgsError
|
5
|
+
elsif !reject.nil?
|
6
|
+
RejectParams.new(reject)
|
7
|
+
elsif !allow_only.nil?
|
8
|
+
AllowOnly.new(allow_only)
|
9
|
+
else
|
10
|
+
None.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class AllowOnly
|
15
|
+
def initialize(filter_values)
|
16
|
+
@filter_values = filter_values
|
17
|
+
end
|
18
|
+
|
19
|
+
def filter(hash)
|
20
|
+
hash.select { |key, _value| @filter_values.include?(key) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class RejectParams
|
25
|
+
def initialize(filter_values)
|
26
|
+
@filter_values = filter_values
|
27
|
+
end
|
28
|
+
|
29
|
+
def filter(hash)
|
30
|
+
hash.reject { |key, _value| @filter_values.include?(key) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class None
|
35
|
+
def filter(hash)
|
36
|
+
hash
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class FilterArgsError < StandardError
|
41
|
+
def initialize
|
42
|
+
msg = "Can only supply either reject_params or allow_only, not both."
|
43
|
+
super(msg)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Readme
|
2
|
+
module Har
|
3
|
+
class Collection
|
4
|
+
def initialize(filter, hash)
|
5
|
+
@filter = filter
|
6
|
+
@hash = hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_h
|
10
|
+
filtered_hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_a
|
14
|
+
filtered_hash.map { |name, value| {name: name, value: value} }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def filtered_hash
|
20
|
+
@filter.filter(@hash)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "readme/har/collection"
|
2
|
+
require "readme/filter"
|
3
|
+
|
4
|
+
module Readme
|
5
|
+
module Har
|
6
|
+
class RequestSerializer
|
7
|
+
def initialize(request, filter = Filter::None.new)
|
8
|
+
@request = request
|
9
|
+
@filter = filter
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_json
|
13
|
+
{
|
14
|
+
method: @request.request_method,
|
15
|
+
queryString: Har::Collection.new(@filter, @request.query_params).to_a,
|
16
|
+
url: @request.url,
|
17
|
+
httpVersion: @request.http_version,
|
18
|
+
headers: Har::Collection.new(@filter, @request.headers).to_a,
|
19
|
+
cookies: Har::Collection.new(@filter, @request.cookies).to_a,
|
20
|
+
postData: postData,
|
21
|
+
headersSize: -1,
|
22
|
+
bodySize: @request.content_length
|
23
|
+
}.compact
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def postData
|
29
|
+
if @request.content_type.nil?
|
30
|
+
nil
|
31
|
+
elsif @request.form_data?
|
32
|
+
{
|
33
|
+
params: form_encoded_body,
|
34
|
+
mimeType: @request.content_type
|
35
|
+
}
|
36
|
+
else
|
37
|
+
{
|
38
|
+
text: request_body,
|
39
|
+
mimeType: @request.content_type
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def form_encoded_body
|
45
|
+
Har::Collection.new(@filter, @request.parsed_form_data).to_a
|
46
|
+
end
|
47
|
+
|
48
|
+
def request_body
|
49
|
+
parsed_body = JSON.parse(@request.body)
|
50
|
+
Har::Collection.new(@filter, parsed_body).to_h.to_json
|
51
|
+
rescue
|
52
|
+
@request.body
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "rack/utils"
|
2
|
+
require "readme/har/collection"
|
3
|
+
|
4
|
+
module Readme
|
5
|
+
module Har
|
6
|
+
class ResponseSerializer
|
7
|
+
JSON_MIME_TYPES = ["application/json", "application/x-json", "text/json", "text/x-json", "+json"]
|
8
|
+
|
9
|
+
def initialize(request, response, filter)
|
10
|
+
@request = request
|
11
|
+
@response = response
|
12
|
+
@filter = filter
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json
|
16
|
+
{
|
17
|
+
status: @response.status,
|
18
|
+
statusText: Rack::Utils::HTTP_STATUS_CODES[@response.status],
|
19
|
+
httpVersion: @request.http_version,
|
20
|
+
headers: Har::Collection.new(@filter, @response.headers).to_a,
|
21
|
+
content: content,
|
22
|
+
redirectURL: @response.location.to_s,
|
23
|
+
headersSize: -1,
|
24
|
+
bodySize: @response.content_length,
|
25
|
+
cookies: Har::Collection.new(@filter, @request.cookies).to_a
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def content
|
32
|
+
if response_body.nil?
|
33
|
+
empty_content
|
34
|
+
elsif content_type_is_json?
|
35
|
+
json_content
|
36
|
+
else
|
37
|
+
pass_through_content
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty_content
|
42
|
+
{mimeType: "", size: 0}
|
43
|
+
end
|
44
|
+
|
45
|
+
def json_content
|
46
|
+
parsed_body = JSON.parse(response_body)
|
47
|
+
|
48
|
+
{mimeType: @response.content_type,
|
49
|
+
size: @response.content_length,
|
50
|
+
text: Har::Collection.new(@filter, parsed_body).to_h.to_json}
|
51
|
+
rescue
|
52
|
+
pass_through_content
|
53
|
+
end
|
54
|
+
|
55
|
+
def pass_through_content
|
56
|
+
{mimeType: @response.content_type,
|
57
|
+
size: @response.content_length,
|
58
|
+
text: response_body}
|
59
|
+
end
|
60
|
+
|
61
|
+
def response_body
|
62
|
+
if @response.body.nil?
|
63
|
+
nil
|
64
|
+
elsif @response.body.respond_to?(:rewind)
|
65
|
+
@response.body.rewind
|
66
|
+
body = @response.body.each.reduce(:+)
|
67
|
+
@response.body.rewind
|
68
|
+
|
69
|
+
body
|
70
|
+
else
|
71
|
+
@response.body.each.reduce(:+)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def content_type_is_json?
|
76
|
+
JSON_MIME_TYPES.include? @response.content_type
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "http_request"
|
3
|
+
require "readme/metrics"
|
4
|
+
require "readme/har/request_serializer"
|
5
|
+
require "readme/har/response_serializer"
|
6
|
+
require "readme/har/collection"
|
7
|
+
|
8
|
+
module Readme
|
9
|
+
module Har
|
10
|
+
class Serializer
|
11
|
+
HAR_VERSION = "1.2"
|
12
|
+
|
13
|
+
def initialize(env, response, start_time, end_time, filter)
|
14
|
+
@http_request = HttpRequest.new(env)
|
15
|
+
@response = response
|
16
|
+
@start_time = start_time
|
17
|
+
@end_time = end_time
|
18
|
+
@filter = filter
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_json
|
22
|
+
{
|
23
|
+
log: {
|
24
|
+
version: HAR_VERSION,
|
25
|
+
creator: creator,
|
26
|
+
entries: entries
|
27
|
+
}
|
28
|
+
}.to_json
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def creator
|
34
|
+
{
|
35
|
+
name: Readme::Metrics::SDK_NAME,
|
36
|
+
version: Readme::Metrics::VERSION
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def entries
|
41
|
+
[
|
42
|
+
{
|
43
|
+
cache: {},
|
44
|
+
timings: timings,
|
45
|
+
request: request,
|
46
|
+
response: response,
|
47
|
+
startedDateTime: @start_time.iso8601,
|
48
|
+
time: elapsed_time
|
49
|
+
}
|
50
|
+
]
|
51
|
+
end
|
52
|
+
|
53
|
+
def timings
|
54
|
+
{
|
55
|
+
send: 0,
|
56
|
+
receive: 0,
|
57
|
+
wait: elapsed_time
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def elapsed_time
|
62
|
+
((@end_time - @start_time) * 1000).to_i
|
63
|
+
end
|
64
|
+
|
65
|
+
def request
|
66
|
+
Har::RequestSerializer.new(@http_request, @filter).as_json
|
67
|
+
end
|
68
|
+
|
69
|
+
def response
|
70
|
+
Har::ResponseSerializer.new(@http_request, @response, @filter).as_json
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "readme/metrics/version"
|
2
|
+
require "readme/har/serializer"
|
3
|
+
require "readme/filter"
|
4
|
+
require "readme/payload"
|
5
|
+
require "readme/request_queue"
|
6
|
+
require "readme/errors"
|
7
|
+
require "httparty"
|
8
|
+
|
9
|
+
module Readme
|
10
|
+
class Metrics
|
11
|
+
SDK_NAME = "Readme.io Ruby SDK"
|
12
|
+
DEFAULT_BUFFER_LENGTH = 10
|
13
|
+
ENDPOINT = "https://metrics.readme.io/v1/request"
|
14
|
+
USER_INFO_KEYS = [:id, :label, :email]
|
15
|
+
|
16
|
+
def initialize(app, options, &get_user_info)
|
17
|
+
validate_options(options)
|
18
|
+
raise Errors::ConfigurationError, Errors::MISSING_BLOCK_ERROR if get_user_info.nil?
|
19
|
+
|
20
|
+
@app = app
|
21
|
+
@development = options[:development] || false
|
22
|
+
@filter = Filter.for(
|
23
|
+
reject: options[:reject_params],
|
24
|
+
allow_only: options[:allow_only]
|
25
|
+
)
|
26
|
+
@get_user_info = get_user_info
|
27
|
+
|
28
|
+
buffer_length = options[:buffer_length] || DEFAULT_BUFFER_LENGTH
|
29
|
+
@@request_queue = Readme::RequestQueue.new(options[:api_key], buffer_length)
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
start_time = Time.now
|
34
|
+
status, headers, body = @app.call(env)
|
35
|
+
end_time = Time.now
|
36
|
+
|
37
|
+
response = Rack::Response.new(body, status, headers)
|
38
|
+
|
39
|
+
har = Har::Serializer.new(env, response, start_time, end_time, @filter)
|
40
|
+
|
41
|
+
user_info = @get_user_info.call(env)
|
42
|
+
|
43
|
+
if user_info_invalid?(user_info)
|
44
|
+
puts Errors.bad_block_message(user_info)
|
45
|
+
return [status, headers, body]
|
46
|
+
else
|
47
|
+
payload = Payload.new(har, user_info, development: @development)
|
48
|
+
@@request_queue.push(payload.to_json)
|
49
|
+
end
|
50
|
+
|
51
|
+
[status, headers, body]
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def validate_options(options)
|
57
|
+
raise Errors::ConfigurationError, Errors::API_KEY_ERROR if options[:api_key].nil?
|
58
|
+
|
59
|
+
if options[:reject_params]&.any? { |param| !param.is_a? String }
|
60
|
+
raise Errors::ConfigurationError, Errors::REJECT_PARAMS_ERROR
|
61
|
+
end
|
62
|
+
|
63
|
+
if options[:allow_only]&.any? { |param| !param.is_a? String }
|
64
|
+
raise Errors::ConfigurationError, Errors::ALLOW_ONLY_ERROR
|
65
|
+
end
|
66
|
+
|
67
|
+
if options[:buffer_length] && !options[:buffer_length].is_a?(Integer)
|
68
|
+
raise Errors::ConfigurationError, Errors::BUFFER_LENGTH_ERROR
|
69
|
+
end
|
70
|
+
|
71
|
+
if options[:development] && !is_a_boolean?(options[:development])
|
72
|
+
raise Errors::ConfigurationError, Errors::DEVELOPMENT_ERROR
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def is_a_boolean?(arg)
|
77
|
+
arg == true || arg == false
|
78
|
+
end
|
79
|
+
|
80
|
+
def user_info_invalid?(user_info)
|
81
|
+
user_info.nil? ||
|
82
|
+
user_info.values.any?(&:nil?) ||
|
83
|
+
USER_INFO_KEYS.sort != user_info.keys.sort
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Readme
|
2
|
+
class Payload
|
3
|
+
def initialize(har, user_info, development:)
|
4
|
+
@har = har
|
5
|
+
@user_info = user_info
|
6
|
+
@development = development
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_json
|
10
|
+
{
|
11
|
+
group: @user_info,
|
12
|
+
clientIPAddress: "1.1.1.1",
|
13
|
+
development: @development,
|
14
|
+
request: JSON.parse(@har.to_json)
|
15
|
+
}.to_json
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "readme/metrics"
|
2
|
+
|
3
|
+
module Readme
|
4
|
+
class RequestQueue
|
5
|
+
attr_reader :queue
|
6
|
+
|
7
|
+
def initialize(api_key, buffer_length)
|
8
|
+
@queue = []
|
9
|
+
@buffer_length = buffer_length
|
10
|
+
@api_key = api_key
|
11
|
+
end
|
12
|
+
|
13
|
+
def push(request)
|
14
|
+
@queue << request
|
15
|
+
|
16
|
+
if ready_to_send?
|
17
|
+
payloads = @queue.slice!(0, @buffer_length)
|
18
|
+
HTTParty.post(
|
19
|
+
Readme::Metrics::ENDPOINT,
|
20
|
+
basic_auth: {username: @api_key, password: ""},
|
21
|
+
headers: {"Content-Type" => "application/json"},
|
22
|
+
body: to_json(payloads)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def ready_to_send?
|
30
|
+
@queue.length >= @buffer_length
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_json(payloads)
|
34
|
+
"[#{payloads.join(", ")}]"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "lib/readme/metrics/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "readme-metrics"
|
5
|
+
spec.version = Readme::Metrics::VERSION
|
6
|
+
spec.authors = ["ReadMe"]
|
7
|
+
spec.email = ["support@readme.io"]
|
8
|
+
spec.license = "ISC"
|
9
|
+
|
10
|
+
spec.summary = "SDK for Readme's metrics API"
|
11
|
+
spec.description = "Middleware for logging requests to Readme's metrics API"
|
12
|
+
spec.homepage = "https://docs.readme.com/metrics/docs/getting-started-with-api-metrics"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/readmeio/metrics-sdks/blob/master/packages/ruby"
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/readmeio/metrics-sdks/blob/master/packages/ruby/CHANGELOG.md"
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
# spec.bindir = "exe"
|
25
|
+
# spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_runtime_dependency "httparty", "~> 0.18"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: readme-metrics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ReadMe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-08-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httparty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.18'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.18'
|
27
|
+
description: Middleware for logging requests to Readme's metrics API
|
28
|
+
email:
|
29
|
+
- support@readme.io
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- ".rspec"
|
36
|
+
- Gemfile
|
37
|
+
- Gemfile.lock
|
38
|
+
- LICENSE
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- bin/console
|
42
|
+
- bin/setup
|
43
|
+
- lib/http_request.rb
|
44
|
+
- lib/readme/errors.rb
|
45
|
+
- lib/readme/filter.rb
|
46
|
+
- lib/readme/har/collection.rb
|
47
|
+
- lib/readme/har/request_serializer.rb
|
48
|
+
- lib/readme/har/response_serializer.rb
|
49
|
+
- lib/readme/har/serializer.rb
|
50
|
+
- lib/readme/metrics.rb
|
51
|
+
- lib/readme/metrics/version.rb
|
52
|
+
- lib/readme/payload.rb
|
53
|
+
- lib/readme/request_queue.rb
|
54
|
+
- readme-metrics.gemspec
|
55
|
+
homepage: https://docs.readme.com/metrics/docs/getting-started-with-api-metrics
|
56
|
+
licenses:
|
57
|
+
- ISC
|
58
|
+
metadata:
|
59
|
+
homepage_uri: https://docs.readme.com/metrics/docs/getting-started-with-api-metrics
|
60
|
+
source_code_uri: https://github.com/readmeio/metrics-sdks/blob/master/packages/ruby
|
61
|
+
changelog_uri: https://github.com/readmeio/metrics-sdks/blob/master/packages/ruby/CHANGELOG.md
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.7.0
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubygems_version: 3.1.2
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: SDK for Readme's metrics API
|
81
|
+
test_files: []
|