readme-metrics 1.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +16 -6
- data/README.md +43 -40
- data/SECURITY.md +12 -0
- data/lib/readme/content_type_helper.rb +17 -0
- data/lib/readme/errors.rb +1 -1
- data/lib/readme/filter.rb +56 -41
- data/lib/readme/har/request_serializer.rb +32 -5
- data/lib/readme/har/serializer.rb +2 -1
- data/lib/readme/http_request.rb +101 -0
- data/lib/readme/http_response.rb +45 -0
- data/lib/readme/metrics/version.rb +1 -1
- data/lib/readme/metrics.rb +24 -11
- data/lib/readme/payload.rb +11 -2
- data/readme-metrics.gemspec +4 -2
- metadata +37 -8
- data/lib/content_type_helper.rb +0 -15
- data/lib/http_request.rb +0 -99
- data/lib/http_response.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f8fe458fadd08b945bcd32ba4d64a4ba2ba6a609dde94f3a501c50bc505967d
|
4
|
+
data.tar.gz: bfa8ff8c37330c3487d8572e2d9ec207147780334295810dc3d0aecfe529e951
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b849b923ddafe6073b039ebedd9387b1f17fb93e990c3ffa7bcd8c407b5349a387001a6f46684fe7202d955826c705892d46dadc8da20db503546cf2a0d7e83
|
7
|
+
data.tar.gz: c728319d845cb1ae8d368c0f995a71f0f1451ad03a8f94064aaa782b4d6cc094cf2269c46c0e5c2d86dbde5ea3527edab4371b4e617df5ad24599558bb29fd3c
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,32 +1,37 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
readme-metrics (
|
4
|
+
readme-metrics (2.0.1)
|
5
5
|
httparty (~> 0.18)
|
6
|
+
os (~> 1.1.4)
|
7
|
+
uuid (~> 2.3.8)
|
6
8
|
|
7
9
|
GEM
|
8
10
|
remote: https://rubygems.org/
|
9
11
|
specs:
|
10
|
-
addressable (2.
|
12
|
+
addressable (2.8.0)
|
11
13
|
public_suffix (>= 2.0.2, < 5.0)
|
12
14
|
ast (2.4.1)
|
13
15
|
crack (0.4.3)
|
14
16
|
safe_yaml (~> 1.0.0)
|
15
17
|
diff-lcs (1.4.4)
|
16
18
|
hashdiff (1.0.1)
|
17
|
-
httparty (0.
|
19
|
+
httparty (0.20.0)
|
18
20
|
mime-types (~> 3.0)
|
19
21
|
multi_xml (>= 0.5.2)
|
20
22
|
json-schema (2.8.1)
|
21
23
|
addressable (>= 2.4)
|
22
|
-
|
24
|
+
macaddr (1.7.2)
|
25
|
+
systemu (~> 2.6.5)
|
26
|
+
mime-types (3.4.1)
|
23
27
|
mime-types-data (~> 3.2015)
|
24
|
-
mime-types-data (3.
|
28
|
+
mime-types-data (3.2022.0105)
|
25
29
|
multi_xml (0.6.0)
|
30
|
+
os (1.1.4)
|
26
31
|
parallel (1.19.2)
|
27
32
|
parser (2.7.1.4)
|
28
33
|
ast (~> 2.4.1)
|
29
|
-
public_suffix (4.0.
|
34
|
+
public_suffix (4.0.6)
|
30
35
|
rack (2.2.3)
|
31
36
|
rack-test (1.1.0)
|
32
37
|
rack (>= 1.0, < 3)
|
@@ -65,7 +70,10 @@ GEM
|
|
65
70
|
standard (0.4.7)
|
66
71
|
rubocop (~> 0.85.0)
|
67
72
|
rubocop-performance (~> 1.6.0)
|
73
|
+
systemu (2.6.5)
|
68
74
|
unicode-display_width (1.7.0)
|
75
|
+
uuid (2.3.9)
|
76
|
+
macaddr (~> 1.0)
|
69
77
|
webmock (3.8.3)
|
70
78
|
addressable (>= 2.3.6)
|
71
79
|
crack (>= 0.3.2)
|
@@ -76,11 +84,13 @@ PLATFORMS
|
|
76
84
|
|
77
85
|
DEPENDENCIES
|
78
86
|
json-schema
|
87
|
+
os
|
79
88
|
rack-test
|
80
89
|
rake (~> 12.0)
|
81
90
|
readme-metrics!
|
82
91
|
rspec (~> 3.0)
|
83
92
|
standard
|
93
|
+
uuid
|
84
94
|
webmock
|
85
95
|
|
86
96
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
#
|
1
|
+
# readme-metrics
|
2
2
|
|
3
3
|
Track your API metrics within ReadMe.
|
4
4
|
|
5
|
+
[![RubyGems](https://img.shields.io/gem/v/readme-metrics)](https://rubygems.org/gems/readme-metrics)
|
5
6
|
[![Build](https://github.com/readmeio/metrics-sdks/workflows/ruby/badge.svg)](https://github.com/readmeio/metrics-sdks)
|
6
7
|
|
7
8
|
[![](https://d3vv6lp55qjaqc.cloudfront.net/items/1M3C3j0I0s0j3T362344/Untitled-2.png)](https://readme.io)
|
@@ -24,25 +25,24 @@ from the environment, or you may hardcode them.
|
|
24
25
|
If you're using Warden-based authentication like Devise, you may fetch the
|
25
26
|
current_user for a given request from the environment.
|
26
27
|
|
27
|
-
###
|
28
|
+
### SDK Options
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
`
|
30
|
+
Option | Type | Description
|
31
|
+
-----------------|------------------|---------
|
32
|
+
`reject_params` | Array of strings | If you have sensitive data you'd like to prevent from being sent to the Metrics API via headers, query params or payload bodies, you can specify a list of keys to filter via the `reject_params` option. NOTE: cannot be used in conjunction with `allow_only`. You may only specify either `reject_params` or `allow_only` keys, not both.
|
33
|
+
`allow_only` | Array of strings | The inverse of `reject_params`. If included all parameters but those in this list will be redacted. NOTE: cannot be used in conjunction with `reject_params`. You may only specify either `reject_params` or `allow_only` keys, not both.
|
34
|
+
`development` | bool | Defaults to `false`. When `true`, the log will be marked as a development log. This is great for separating staging or test data from data coming from customers.
|
35
|
+
`buffer_length` | number | Defaults to `1`. This value should be a number representing the amount of requests to group up before sending them over the network. Increasing this value may increase performance by batching, but will also delay the time until logs show up in the dashboard given the buffer size needs to be reached in order for the logs to be sent.
|
33
36
|
|
34
|
-
###
|
37
|
+
### Payload Data
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
to
|
39
|
-
will
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
send to the API.
|
44
|
-
|
45
|
-
You may only specify either `reject_params` or `allow_only` keys, not both.
|
39
|
+
Option | Required? | Type | Description
|
40
|
+
--------------------|-----------|------------------|----------
|
41
|
+
`api_key` | yes | string | API Key used to make the request. Note that this is different from the `readmeAPIKey` described above in the options data. This should be a value from your API that is unique to each of your users.
|
42
|
+
`label` | no | string | This will be the user's display name in the API Metrics Dashboard, since it's much easier to remember a name than an API key.
|
43
|
+
`email` | no | string | Email of the user that is making the call.
|
44
|
+
`log_id` | no | string | A UUIDv4 identifier. If not provided this will be automatically generated for you. Providing your own `log_id` is useful if you want to know the URL of the log in advance, i.e. `{your_base_url}/logs/{your_log_id}`.
|
45
|
+
`ignore` | no | bool | A flag that when set to `true` will suppress sending the log.
|
46
46
|
|
47
47
|
### Rails
|
48
48
|
|
@@ -50,29 +50,27 @@ You may only specify either `reject_params` or `allow_only` keys, not both.
|
|
50
50
|
# config/environments/development.rb or config/environments/production.rb
|
51
51
|
require "readme/metrics"
|
52
52
|
|
53
|
-
|
54
|
-
api_key: "
|
53
|
+
sdk_options = {
|
54
|
+
api_key: "<<apiKey>>",
|
55
55
|
development: false,
|
56
56
|
reject_params: ["not_included", "dont_send"],
|
57
57
|
buffer_length: 5,
|
58
58
|
}
|
59
59
|
|
60
|
-
config.middleware.use Readme::Metrics,
|
60
|
+
config.middleware.use Readme::Metrics, sdk_options do |env|
|
61
61
|
current_user = env['warden'].authenticate
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
}
|
75
|
-
end
|
63
|
+
payload_data = current_user.present? ? {
|
64
|
+
api_key: current_user.api_key, # Not the same as the ReadMe API Key
|
65
|
+
label: current_user.name,
|
66
|
+
email: current_user.email
|
67
|
+
} : {
|
68
|
+
api_key: "guest",
|
69
|
+
label: "Guest User",
|
70
|
+
email: "guest@example.com"
|
71
|
+
}
|
72
|
+
|
73
|
+
payload_data
|
76
74
|
end
|
77
75
|
```
|
78
76
|
|
@@ -80,17 +78,18 @@ end
|
|
80
78
|
|
81
79
|
```ruby
|
82
80
|
# config.ru
|
83
|
-
|
84
|
-
api_key: "
|
81
|
+
sdk_options = {
|
82
|
+
api_key: "<<apiKey>>",
|
85
83
|
development: false,
|
86
84
|
reject_params: ["not_included", "dont_send"]
|
87
85
|
}
|
88
86
|
|
89
|
-
use Readme::Metrics,
|
87
|
+
use Readme::Metrics, sdk_options do |env|
|
90
88
|
{
|
91
|
-
|
92
|
-
label: "
|
93
|
-
email: "
|
89
|
+
api_key: "owlbert_api_key"
|
90
|
+
label: "Owlbert",
|
91
|
+
email: "owlbert@example.com",
|
92
|
+
log_id: SecureRandom.uuid
|
94
93
|
}
|
95
94
|
end
|
96
95
|
|
@@ -103,6 +102,10 @@ run YourApp.new
|
|
103
102
|
- [Rack](https://github.com/readmeio/metrics-sdk-racks-sample)
|
104
103
|
- [Sinatra](https://github.com/readmeio/metrics-sdk-sinatra-example)
|
105
104
|
|
105
|
+
### Contributing
|
106
|
+
|
107
|
+
Ensure you are running the version of ruby specified in the `Gemfile.lock`; use `rvm` to easy manage ruby versions. Run `bundle` to install dependencies, `rake` or `rspec` to ensure tests pass, and `bundle exec standardrb` to lint the code.
|
108
|
+
|
106
109
|
## License
|
107
110
|
|
108
|
-
[View our license here](https://github.com/readmeio/metrics-sdks/tree/
|
111
|
+
[View our license here](https://github.com/readmeio/metrics-sdks/tree/main/packages/ruby/LICENSE)
|
data/SECURITY.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Reporting a Vulnerability
|
4
|
+
|
5
|
+
If there are any vulnerabilities in `readme-metrics`, don't hesitate to _report them_.
|
6
|
+
|
7
|
+
Please email security@readme.io and describe what you've found.
|
8
|
+
|
9
|
+
- If you have a fix, explain or attach it.
|
10
|
+
- In the near time, expect a reply with the required steps. Also, there may be a demand for a pull request which include the fixes.
|
11
|
+
|
12
|
+
> You should not disclose the vulnerability publicly if you haven't received an answer in some weeks. If the vulnerability is rejected, you may post it publicly within some hour of rejection, unless the rejection is withdrawn within that time period. After the vulnerability has been fixed, you may disclose the vulnerability details publicly over some days.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Readme
|
2
|
+
module ContentTypeHelper
|
3
|
+
# Assumes the includer has a `content_type` method defined.
|
4
|
+
|
5
|
+
JSON_MIME_TYPES = [
|
6
|
+
"application/json",
|
7
|
+
"application/x-json",
|
8
|
+
"text/json",
|
9
|
+
"text/x-json",
|
10
|
+
"+json"
|
11
|
+
]
|
12
|
+
|
13
|
+
def json?
|
14
|
+
JSON_MIME_TYPES.any? { |mime_type| content_type.include?(mime_type) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/readme/errors.rb
CHANGED
@@ -25,7 +25,7 @@ module Readme
|
|
25
25
|
middleware.
|
26
26
|
|
27
27
|
Expected a hash with the shape:
|
28
|
-
{
|
28
|
+
{ api_key: "Your user api key", label: "Your user label", email: "Your user email" }
|
29
29
|
|
30
30
|
Received value:
|
31
31
|
#{result}
|
data/lib/readme/filter.rb
CHANGED
@@ -1,58 +1,73 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
module Readme
|
2
|
+
class Filter
|
3
|
+
def self.for(reject: nil, allow_only: nil)
|
4
|
+
if !reject.nil? && !allow_only.nil?
|
5
|
+
raise FilterArgsError
|
6
|
+
elsif !reject.nil?
|
7
|
+
RejectParams.new(reject)
|
8
|
+
elsif !allow_only.nil?
|
9
|
+
AllowOnly.new(allow_only)
|
10
|
+
else
|
11
|
+
None.new
|
12
|
+
end
|
11
13
|
end
|
12
|
-
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def self.redact(rejected_params)
|
16
|
+
rejected_params.each_with_object({}) do |(k, v), hash|
|
17
|
+
# If it's a string then return the length of the redacted field
|
18
|
+
hash[k.to_str] = "[REDACTED#{v.is_a?(String) ? " #{v.length}" : ""}]"
|
19
|
+
end
|
17
20
|
end
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
class AllowOnly
|
23
|
+
def initialize(filter_fields)
|
24
|
+
@allowed_fields = filter_fields
|
25
|
+
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
+
def filter(hash)
|
28
|
+
allowed_fields = @allowed_fields.map(&:downcase)
|
29
|
+
allowed_params, rejected_params = hash.partition { |key, _value| allowed_fields.include?(key.downcase) }.map(&:to_h)
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
@filter_values = filter_values.map(&:downcase)
|
31
|
-
end
|
31
|
+
allowed_params.merge(Filter.redact(rejected_params))
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
34
|
+
def pass_through?
|
35
|
+
false
|
36
|
+
end
|
35
37
|
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
class RejectParams
|
40
|
+
def initialize(filter_fields)
|
41
|
+
@rejected_fields = filter_fields
|
42
|
+
end
|
43
|
+
|
44
|
+
def filter(hash)
|
45
|
+
rejected_fields = @rejected_fields.map(&:downcase)
|
46
|
+
rejected_params, allowed_params = hash.partition { |key, _value| rejected_fields.include?(key.downcase) }.map(&:to_h)
|
47
|
+
|
48
|
+
allowed_params.merge(Filter.redact(rejected_params))
|
49
|
+
end
|
41
50
|
|
42
|
-
|
43
|
-
|
44
|
-
|
51
|
+
def pass_through?
|
52
|
+
false
|
53
|
+
end
|
45
54
|
end
|
46
55
|
|
47
|
-
|
48
|
-
|
56
|
+
class None
|
57
|
+
def filter(hash)
|
58
|
+
hash
|
59
|
+
end
|
60
|
+
|
61
|
+
def pass_through?
|
62
|
+
true
|
63
|
+
end
|
49
64
|
end
|
50
|
-
end
|
51
65
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
66
|
+
class FilterArgsError < StandardError
|
67
|
+
def initialize
|
68
|
+
msg = "Can only supply either reject_params or allow_only, not both."
|
69
|
+
super(msg)
|
70
|
+
end
|
56
71
|
end
|
57
72
|
end
|
58
73
|
end
|
@@ -1,10 +1,11 @@
|
|
1
|
+
require "cgi"
|
1
2
|
require "readme/har/collection"
|
2
3
|
require "readme/filter"
|
3
4
|
|
4
5
|
module Readme
|
5
6
|
module Har
|
6
7
|
class RequestSerializer
|
7
|
-
def initialize(request, filter = Filter::None.new)
|
8
|
+
def initialize(request, filter = Readme::Filter::None.new)
|
8
9
|
@request = request
|
9
10
|
@filter = filter
|
10
11
|
end
|
@@ -13,7 +14,7 @@ module Readme
|
|
13
14
|
{
|
14
15
|
method: @request.request_method,
|
15
16
|
queryString: Har::Collection.new(@filter, @request.query_params).to_a,
|
16
|
-
url:
|
17
|
+
url: url,
|
17
18
|
httpVersion: @request.http_version,
|
18
19
|
headers: Har::Collection.new(@filter, @request.headers).to_a,
|
19
20
|
cookies: Har::Collection.new(@filter, @request.cookies).to_a,
|
@@ -25,6 +26,16 @@ module Readme
|
|
25
26
|
|
26
27
|
private
|
27
28
|
|
29
|
+
def url
|
30
|
+
url = URI(@request.url)
|
31
|
+
headers = @request.headers
|
32
|
+
forward_proto = headers["X-Forwarded-Proto"]
|
33
|
+
forward_host = headers["X-Forwarded-Host"]
|
34
|
+
url.host = forward_host if forward_host.is_a?(String)
|
35
|
+
url.scheme = forward_proto if forward_proto.is_a?(String)
|
36
|
+
url.to_s
|
37
|
+
end
|
38
|
+
|
28
39
|
def postData
|
29
40
|
if @request.content_type.nil?
|
30
41
|
nil
|
@@ -48,18 +59,34 @@ module Readme
|
|
48
59
|
def request_body
|
49
60
|
if @filter.pass_through?
|
50
61
|
pass_through_body
|
51
|
-
|
52
|
-
|
53
|
-
|
62
|
+
elsif is_form_urlencoded?
|
63
|
+
form_urlencoded_body
|
64
|
+
elsif is_json?
|
54
65
|
json_body
|
66
|
+
else
|
67
|
+
@request.body
|
55
68
|
end
|
56
69
|
end
|
57
70
|
|
71
|
+
def is_json?
|
72
|
+
["application/json", "application/x-json", "text/json", "text/x-json"]
|
73
|
+
.include?(@request.content_type) || @request.content_type.include?("+json")
|
74
|
+
end
|
75
|
+
|
76
|
+
def is_form_urlencoded?
|
77
|
+
@request.content_type == "application/x-www-form-urlencoded"
|
78
|
+
end
|
79
|
+
|
58
80
|
def json_body
|
59
81
|
parsed_body = JSON.parse(@request.body)
|
60
82
|
Har::Collection.new(@filter, parsed_body).to_h.to_json
|
61
83
|
end
|
62
84
|
|
85
|
+
def form_urlencoded_body
|
86
|
+
parsed_body = CGI.parse(@request.body).transform_values(&:first)
|
87
|
+
Har::Collection.new(@filter, parsed_body).to_h.to_json
|
88
|
+
end
|
89
|
+
|
63
90
|
def pass_through_body
|
64
91
|
@request.body
|
65
92
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/request"
|
3
|
+
require_relative "content_type_helper"
|
4
|
+
|
5
|
+
module Readme
|
6
|
+
class HttpRequest
|
7
|
+
include ContentTypeHelper
|
8
|
+
|
9
|
+
HTTP_NON_HEADERS = [
|
10
|
+
Rack::HTTP_COOKIE,
|
11
|
+
Rack::HTTP_VERSION,
|
12
|
+
Rack::HTTP_HOST,
|
13
|
+
Rack::HTTP_PORT
|
14
|
+
]
|
15
|
+
|
16
|
+
def initialize(env)
|
17
|
+
@request = Rack::Request.new(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
def url
|
21
|
+
@request.url
|
22
|
+
end
|
23
|
+
|
24
|
+
def query_params
|
25
|
+
@request.GET
|
26
|
+
end
|
27
|
+
|
28
|
+
def cookies
|
29
|
+
@request.cookies
|
30
|
+
end
|
31
|
+
|
32
|
+
def http_version
|
33
|
+
@request.get_header(Rack::HTTP_VERSION)
|
34
|
+
end
|
35
|
+
|
36
|
+
def request_method
|
37
|
+
@request.request_method
|
38
|
+
end
|
39
|
+
|
40
|
+
def content_type
|
41
|
+
@request.content_type
|
42
|
+
end
|
43
|
+
|
44
|
+
def form_data?
|
45
|
+
@request.form_data?
|
46
|
+
end
|
47
|
+
|
48
|
+
def content_length
|
49
|
+
@request.content_length.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def options?
|
53
|
+
@request.request_method == "OPTIONS"
|
54
|
+
end
|
55
|
+
|
56
|
+
def headers
|
57
|
+
@request
|
58
|
+
.each_header
|
59
|
+
.select { |key, _| http_header?(key) }
|
60
|
+
.to_h
|
61
|
+
.transform_keys { |header| normalize_header_name(header) }
|
62
|
+
.merge unprefixed_headers
|
63
|
+
end
|
64
|
+
|
65
|
+
def body
|
66
|
+
@request.body.rewind
|
67
|
+
content = @request.body.read
|
68
|
+
@request.body.rewind
|
69
|
+
|
70
|
+
content
|
71
|
+
end
|
72
|
+
|
73
|
+
def parsed_form_data
|
74
|
+
@request.POST
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# "headers" in Rack::Request just means any key in the env. The HTTP headers
|
80
|
+
# are all the headers prefixed with `HTTP_` as per the spec:
|
81
|
+
# https://github.com/rack/rack/blob/master/SPEC.rdoc#the-environment-
|
82
|
+
# Other "headers" like version and host are prefixed with `HTTP_` by Rack but
|
83
|
+
# don't seem to be considered legit HTTP headers.
|
84
|
+
def http_header?(name)
|
85
|
+
name.start_with?("HTTP") && !HTTP_NON_HEADERS.include?(name)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Headers like `Content-Type: application/json` come into rack like
|
89
|
+
# `"HTTP_CONTENT_TYPE" => "application/json"`.
|
90
|
+
def normalize_header_name(header)
|
91
|
+
header.delete_prefix("HTTP_").split("_").map(&:capitalize).join("-")
|
92
|
+
end
|
93
|
+
|
94
|
+
# These special headers are explicitly _not_ prefixed with HTTP_ in the Rack
|
95
|
+
# env so we need to add them in manually
|
96
|
+
def unprefixed_headers
|
97
|
+
{"Content-Type" => @request.content_type,
|
98
|
+
"Content-Length" => @request.content_length}.compact
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/response"
|
3
|
+
require_relative "content_type_helper"
|
4
|
+
|
5
|
+
module Readme
|
6
|
+
class HttpResponse < SimpleDelegator
|
7
|
+
include ContentTypeHelper
|
8
|
+
|
9
|
+
def self.from_parts(status, headers, body)
|
10
|
+
new(Rack::Response.new(body, status, headers))
|
11
|
+
end
|
12
|
+
|
13
|
+
def body
|
14
|
+
if raw_body.respond_to?(:rewind)
|
15
|
+
raw_body.rewind
|
16
|
+
content = raw_body.each.reduce("", :+)
|
17
|
+
raw_body.rewind
|
18
|
+
|
19
|
+
content
|
20
|
+
else
|
21
|
+
raw_body.each.reduce("", :+)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def content_length
|
26
|
+
if empty_body_status?
|
27
|
+
0
|
28
|
+
elsif !headers["Content-Length"]
|
29
|
+
body.bytesize
|
30
|
+
else
|
31
|
+
headers["Content-Length"].to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def raw_body
|
38
|
+
__getobj__.body
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty_body_status?
|
42
|
+
Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/readme/metrics.rb
CHANGED
@@ -4,17 +4,30 @@ require "readme/filter"
|
|
4
4
|
require "readme/payload"
|
5
5
|
require "readme/request_queue"
|
6
6
|
require "readme/errors"
|
7
|
-
require "http_request"
|
8
|
-
require "http_response"
|
7
|
+
require "readme/http_request"
|
8
|
+
require "readme/http_response"
|
9
9
|
require "httparty"
|
10
10
|
require "logger"
|
11
|
+
require "os"
|
11
12
|
|
12
13
|
module Readme
|
13
14
|
class Metrics
|
15
|
+
def self.platform
|
16
|
+
if OS.windows?
|
17
|
+
"windows"
|
18
|
+
elsif OS.mac?
|
19
|
+
"mac"
|
20
|
+
elsif OS.linux?
|
21
|
+
"linux"
|
22
|
+
else
|
23
|
+
"unknown"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
14
27
|
SDK_NAME = "Readme.io Ruby SDK"
|
15
|
-
|
28
|
+
PLATFORM = platform
|
29
|
+
DEFAULT_BUFFER_LENGTH = 1
|
16
30
|
ENDPOINT = "https://metrics.readme.io/v1/request"
|
17
|
-
USER_INFO_KEYS = [:id, :label, :email]
|
18
31
|
|
19
32
|
def self.logger
|
20
33
|
@@logger
|
@@ -33,7 +46,7 @@ module Readme
|
|
33
46
|
@get_user_info = get_user_info
|
34
47
|
|
35
48
|
buffer_length = options[:buffer_length] || DEFAULT_BUFFER_LENGTH
|
36
|
-
@@request_queue = Readme::RequestQueue.new(options[:api_key], buffer_length)
|
49
|
+
@@request_queue = options[:request_queue] || Readme::RequestQueue.new(options[:api_key], buffer_length)
|
37
50
|
@@logger = options[:logger] || Logger.new($stdout)
|
38
51
|
end
|
39
52
|
|
@@ -65,7 +78,7 @@ module Readme
|
|
65
78
|
har = Har::Serializer.new(request, response, start_time, end_time, @filter)
|
66
79
|
user_info = @get_user_info.call(env)
|
67
80
|
|
68
|
-
if
|
81
|
+
if !user_info_valid?(user_info)
|
69
82
|
Readme::Metrics.logger.warn Errors.bad_block_message(user_info)
|
70
83
|
elsif request.options?
|
71
84
|
Readme::Metrics.logger.info "OPTIONS request omitted from ReadMe API logging"
|
@@ -73,7 +86,7 @@ module Readme
|
|
73
86
|
Readme::Metrics.logger.warn "Request or response body MIME type isn't supported for filtering. Omitting request from ReadMe API logging"
|
74
87
|
else
|
75
88
|
payload = Payload.new(har, user_info, development: @development)
|
76
|
-
@@request_queue.push(payload.to_json)
|
89
|
+
@@request_queue.push(payload.to_json) unless payload.ignore
|
77
90
|
end
|
78
91
|
end
|
79
92
|
|
@@ -132,10 +145,10 @@ module Readme
|
|
132
145
|
arg == true || arg == false
|
133
146
|
end
|
134
147
|
|
135
|
-
def
|
136
|
-
user_info.nil?
|
137
|
-
user_info.values.any?(&:nil?)
|
138
|
-
|
148
|
+
def user_info_valid?(user_info)
|
149
|
+
!user_info.nil? &&
|
150
|
+
!user_info.values.any?(&:nil?) &&
|
151
|
+
user_info.has_key?(:api_key) || user_info.has_key?(:id)
|
139
152
|
end
|
140
153
|
end
|
141
154
|
end
|
data/lib/readme/payload.rb
CHANGED
@@ -1,13 +1,22 @@
|
|
1
|
+
require "uuid"
|
2
|
+
|
1
3
|
module Readme
|
2
4
|
class Payload
|
3
|
-
|
5
|
+
attr_reader :ignore
|
6
|
+
|
7
|
+
def initialize(har, info, development:)
|
4
8
|
@har = har
|
5
|
-
@user_info =
|
9
|
+
@user_info = info.slice(:id, :label, :email)
|
10
|
+
@user_info[:id] = info[:api_key] unless info[:api_key].nil? # swap api_key for id if api_key is present
|
11
|
+
@log_id = info[:log_id]
|
12
|
+
@ignore = info[:ignore]
|
6
13
|
@development = development
|
14
|
+
@uuid = UUID.new
|
7
15
|
end
|
8
16
|
|
9
17
|
def to_json
|
10
18
|
{
|
19
|
+
logId: UUID.validate(@log_id) ? @log_id : @uuid.generate,
|
11
20
|
group: @user_info,
|
12
21
|
clientIPAddress: "1.1.1.1",
|
13
22
|
development: @development,
|
data/readme-metrics.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
14
14
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
-
spec.metadata["source_code_uri"] = "https://github.com/readmeio/metrics-sdks/blob/
|
17
|
-
spec.metadata["changelog_uri"] = "https://github.com/readmeio/metrics-sdks/blob/
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/readmeio/metrics-sdks/blob/main/packages/ruby"
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/readmeio/metrics-sdks/blob/main/packages/ruby/CHANGELOG.md"
|
18
18
|
|
19
19
|
# Specify which files should be added to the gem when it is released.
|
20
20
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
@@ -26,4 +26,6 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.require_paths = ["lib"]
|
27
27
|
|
28
28
|
spec.add_runtime_dependency "httparty", "~> 0.18"
|
29
|
+
spec.add_runtime_dependency "uuid", "~> 2.3.8"
|
30
|
+
spec.add_runtime_dependency "os", "~> 1.1.4"
|
29
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: readme-metrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ReadMe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0.18'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: uuid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.8
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.3.8
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: os
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.1.4
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.1.4
|
27
55
|
description: Middleware for logging requests to Readme's metrics API
|
28
56
|
email:
|
29
57
|
- support@readme.io
|
@@ -38,17 +66,18 @@ files:
|
|
38
66
|
- LICENSE
|
39
67
|
- README.md
|
40
68
|
- Rakefile
|
69
|
+
- SECURITY.md
|
41
70
|
- bin/console
|
42
71
|
- bin/setup
|
43
|
-
- lib/content_type_helper.rb
|
44
|
-
- lib/http_request.rb
|
45
|
-
- lib/http_response.rb
|
72
|
+
- lib/readme/content_type_helper.rb
|
46
73
|
- lib/readme/errors.rb
|
47
74
|
- lib/readme/filter.rb
|
48
75
|
- lib/readme/har/collection.rb
|
49
76
|
- lib/readme/har/request_serializer.rb
|
50
77
|
- lib/readme/har/response_serializer.rb
|
51
78
|
- lib/readme/har/serializer.rb
|
79
|
+
- lib/readme/http_request.rb
|
80
|
+
- lib/readme/http_response.rb
|
52
81
|
- lib/readme/metrics.rb
|
53
82
|
- lib/readme/metrics/version.rb
|
54
83
|
- lib/readme/payload.rb
|
@@ -59,8 +88,8 @@ licenses:
|
|
59
88
|
- ISC
|
60
89
|
metadata:
|
61
90
|
homepage_uri: https://docs.readme.com/metrics/docs/getting-started-with-api-metrics
|
62
|
-
source_code_uri: https://github.com/readmeio/metrics-sdks/blob/
|
63
|
-
changelog_uri: https://github.com/readmeio/metrics-sdks/blob/
|
91
|
+
source_code_uri: https://github.com/readmeio/metrics-sdks/blob/main/packages/ruby
|
92
|
+
changelog_uri: https://github.com/readmeio/metrics-sdks/blob/main/packages/ruby/CHANGELOG.md
|
64
93
|
post_install_message:
|
65
94
|
rdoc_options: []
|
66
95
|
require_paths:
|
@@ -76,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
105
|
- !ruby/object:Gem::Version
|
77
106
|
version: '0'
|
78
107
|
requirements: []
|
79
|
-
rubygems_version: 3.1.
|
108
|
+
rubygems_version: 3.1.4
|
80
109
|
signing_key:
|
81
110
|
specification_version: 4
|
82
111
|
summary: SDK for Readme's metrics API
|
data/lib/content_type_helper.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
module ContentTypeHelper
|
2
|
-
# Assumes the includer has a `content_type` method defined.
|
3
|
-
|
4
|
-
JSON_MIME_TYPES = [
|
5
|
-
"application/json",
|
6
|
-
"application/x-json",
|
7
|
-
"text/json",
|
8
|
-
"text/x-json",
|
9
|
-
"+json"
|
10
|
-
]
|
11
|
-
|
12
|
-
def json?
|
13
|
-
JSON_MIME_TYPES.any? { |mime_type| content_type.include?(mime_type) }
|
14
|
-
end
|
15
|
-
end
|
data/lib/http_request.rb
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
require "rack"
|
2
|
-
require "rack/request"
|
3
|
-
require "content_type_helper"
|
4
|
-
|
5
|
-
class HttpRequest
|
6
|
-
include ContentTypeHelper
|
7
|
-
|
8
|
-
HTTP_NON_HEADERS = [
|
9
|
-
Rack::HTTP_COOKIE,
|
10
|
-
Rack::HTTP_VERSION,
|
11
|
-
Rack::HTTP_HOST,
|
12
|
-
Rack::HTTP_PORT
|
13
|
-
]
|
14
|
-
|
15
|
-
def initialize(env)
|
16
|
-
@request = Rack::Request.new(env)
|
17
|
-
end
|
18
|
-
|
19
|
-
def url
|
20
|
-
@request.url
|
21
|
-
end
|
22
|
-
|
23
|
-
def query_params
|
24
|
-
@request.GET
|
25
|
-
end
|
26
|
-
|
27
|
-
def cookies
|
28
|
-
@request.cookies
|
29
|
-
end
|
30
|
-
|
31
|
-
def http_version
|
32
|
-
@request.get_header(Rack::HTTP_VERSION)
|
33
|
-
end
|
34
|
-
|
35
|
-
def request_method
|
36
|
-
@request.request_method
|
37
|
-
end
|
38
|
-
|
39
|
-
def content_type
|
40
|
-
@request.content_type
|
41
|
-
end
|
42
|
-
|
43
|
-
def form_data?
|
44
|
-
@request.form_data?
|
45
|
-
end
|
46
|
-
|
47
|
-
def content_length
|
48
|
-
@request.content_length.to_i
|
49
|
-
end
|
50
|
-
|
51
|
-
def options?
|
52
|
-
@request.request_method == "OPTIONS"
|
53
|
-
end
|
54
|
-
|
55
|
-
def headers
|
56
|
-
@request
|
57
|
-
.each_header
|
58
|
-
.select { |key, _| http_header?(key) }
|
59
|
-
.to_h
|
60
|
-
.transform_keys { |header| normalize_header_name(header) }
|
61
|
-
.merge unprefixed_headers
|
62
|
-
end
|
63
|
-
|
64
|
-
def body
|
65
|
-
@request.body.rewind
|
66
|
-
content = @request.body.read
|
67
|
-
@request.body.rewind
|
68
|
-
|
69
|
-
content
|
70
|
-
end
|
71
|
-
|
72
|
-
def parsed_form_data
|
73
|
-
@request.POST
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
# "headers" in Rack::Request just means any key in the env. The HTTP headers
|
79
|
-
# are all the headers prefixed with `HTTP_` as per the spec:
|
80
|
-
# https://github.com/rack/rack/blob/master/SPEC.rdoc#the-environment-
|
81
|
-
# Other "headers" like version and host are prefixed with `HTTP_` by Rack but
|
82
|
-
# don't seem to be considered legit HTTP headers.
|
83
|
-
def http_header?(name)
|
84
|
-
name.start_with?("HTTP") && !HTTP_NON_HEADERS.include?(name)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Headers like `Content-Type: application/json` come into rack like
|
88
|
-
# `"HTTP_CONTENT_TYPE" => "application/json"`.
|
89
|
-
def normalize_header_name(header)
|
90
|
-
header.delete_prefix("HTTP_").split("_").map(&:capitalize).join("-")
|
91
|
-
end
|
92
|
-
|
93
|
-
# These special headers are explicitly _not_ prefixed with HTTP_ in the Rack
|
94
|
-
# env so we need to add them in manually
|
95
|
-
def unprefixed_headers
|
96
|
-
{"Content-Type" => @request.content_type,
|
97
|
-
"Content-Length" => @request.content_length}.compact
|
98
|
-
end
|
99
|
-
end
|
data/lib/http_response.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
require "rack"
|
2
|
-
require "rack/response"
|
3
|
-
require "content_type_helper"
|
4
|
-
|
5
|
-
class HttpResponse < SimpleDelegator
|
6
|
-
include ContentTypeHelper
|
7
|
-
|
8
|
-
def self.from_parts(status, headers, body)
|
9
|
-
new(Rack::Response.new(body, status, headers))
|
10
|
-
end
|
11
|
-
|
12
|
-
def body
|
13
|
-
if raw_body.respond_to?(:rewind)
|
14
|
-
raw_body.rewind
|
15
|
-
content = raw_body.each.reduce("", :+)
|
16
|
-
raw_body.rewind
|
17
|
-
|
18
|
-
content
|
19
|
-
else
|
20
|
-
raw_body.each.reduce("", :+)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def content_length
|
25
|
-
if empty_body_status?
|
26
|
-
0
|
27
|
-
elsif !headers["Content-Length"]
|
28
|
-
body.bytesize
|
29
|
-
else
|
30
|
-
headers["Content-Length"].to_i
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def raw_body
|
37
|
-
__getobj__.body
|
38
|
-
end
|
39
|
-
|
40
|
-
def empty_body_status?
|
41
|
-
Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i)
|
42
|
-
end
|
43
|
-
end
|