readme-metrics 0.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66f7b071458adb5af02621626935bc6d9520bad8e246369a5f500f9037c3ea3b
4
- data.tar.gz: f3d313877ae15673722a1c749a5529e971ec17491a7eef639924cf3991419c49
3
+ metadata.gz: b2495abfb80b4e0d481c89ce9108594a0e46b2526f9548cd5ce8e4be638fd776
4
+ data.tar.gz: 3da58b3ec731a500dabcba7ac7e86cb72e71c01d093a531e93e060925137f7fd
5
5
  SHA512:
6
- metadata.gz: be4ec7898257dbd67adf3e27070ea4cb1ee37b0d54d80de7ca2418356bfe3330a0fcdfa8ea3b16fed15afaa03a65ae0c58882f634385f37e76c275a2209f681d
7
- data.tar.gz: 76323ca324090a903898b7bd0ff818097272a6270466b4ed9eb58e06897c9becc6cc47264a0e64ee81e4a5a577aa7241ebd09f4d2bda9c3027e914b512b946e9
6
+ metadata.gz: a341373a18f63d3e5b6736fd5ddd8b17e13db3175474c6ade839129569becbbbe1ec4af9445c0c8bbbe17b53ccbd4b810b0bb4b11e4a1077a725d939fa7146fe
7
+ data.tar.gz: 5b28d3a9b0000a4642fc9847dab4d5674f1afd1fe888727f0d4dbe910b9f0ed45f757993bb334b026310ddebdee6632c82e9ace8d1e65b9cf7f2124209ece7ab
data/Gemfile CHANGED
@@ -11,3 +11,5 @@ gem "rake", "~> 12.0"
11
11
  gem "rspec", "~> 3.0"
12
12
  gem "standard"
13
13
  gem "webmock"
14
+ gem "os"
15
+ gem "uuid"
data/Gemfile.lock CHANGED
@@ -1,32 +1,35 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- readme-metrics (0.1.0)
4
+ readme-metrics (1.1.1)
5
5
  httparty (~> 0.18)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- addressable (2.7.0)
10
+ addressable (2.8.0)
11
11
  public_suffix (>= 2.0.2, < 5.0)
12
12
  ast (2.4.1)
13
13
  crack (0.4.3)
14
14
  safe_yaml (~> 1.0.0)
15
15
  diff-lcs (1.4.4)
16
16
  hashdiff (1.0.1)
17
- httparty (0.18.1)
17
+ httparty (0.20.0)
18
18
  mime-types (~> 3.0)
19
19
  multi_xml (>= 0.5.2)
20
20
  json-schema (2.8.1)
21
21
  addressable (>= 2.4)
22
- mime-types (3.3.1)
22
+ macaddr (1.7.2)
23
+ systemu (~> 2.6.5)
24
+ mime-types (3.4.1)
23
25
  mime-types-data (~> 3.2015)
24
- mime-types-data (3.2020.0512)
26
+ mime-types-data (3.2021.1115)
25
27
  multi_xml (0.6.0)
28
+ os (1.1.4)
26
29
  parallel (1.19.2)
27
30
  parser (2.7.1.4)
28
31
  ast (~> 2.4.1)
29
- public_suffix (4.0.5)
32
+ public_suffix (4.0.6)
30
33
  rack (2.2.3)
31
34
  rack-test (1.1.0)
32
35
  rack (>= 1.0, < 3)
@@ -65,7 +68,10 @@ GEM
65
68
  standard (0.4.7)
66
69
  rubocop (~> 0.85.0)
67
70
  rubocop-performance (~> 1.6.0)
71
+ systemu (2.6.5)
68
72
  unicode-display_width (1.7.0)
73
+ uuid (2.3.9)
74
+ macaddr (~> 1.0)
69
75
  webmock (3.8.3)
70
76
  addressable (>= 2.3.6)
71
77
  crack (>= 0.3.2)
@@ -76,11 +82,13 @@ PLATFORMS
76
82
 
77
83
  DEPENDENCIES
78
84
  json-schema
85
+ os
79
86
  rack-test
80
87
  rake (~> 12.0)
81
88
  readme-metrics!
82
89
  rspec (~> 3.0)
83
90
  standard
91
+ uuid
84
92
  webmock
85
93
 
86
94
  BUNDLED WITH
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
- # readmeio
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,73 +25,88 @@ 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
- ### Batching requests
28
+ ### SDK Options
28
29
 
29
- By default, the middleware will batch requests to the ReadMe API in groups of
30
- 10. For every 10 requests made to your application, the middleware will make a
31
- single request to ReadMe. If you wish to override this, provide a
32
- `buffer_length` option when configuring the middleware.
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
33
+ 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.
34
+ `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.
35
+ `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.
36
+ `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
37
 
34
- ### Sensitive Data
38
+ ### Payload Data
35
39
 
36
- If you have sensitive data you'd like to prevent from being sent to the Metrics
37
- API via headers, query params or payload bodies, you can specify a list of keys
38
- to filter via the `reject_params` option. Key-value pairs matching these keys
39
- will not be included in the request to the Metrics API.
40
-
41
- You are also able to specify a set of `allow_only` which should only be sent through.
42
- Any header or body values not matching these keys will be filtered out and not
43
- send to the API.
44
-
45
- You may only specify either `reject_params` or `allow_only` keys, not both.
40
+ Option | Required? | Type | Description
41
+ --------------------|-----------|------------------|----------
42
+ `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.
43
+ `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.
44
+ `email` | no | string | Email of the user that is making the call.
45
+ `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}`.
46
+ `ignore` | no | bool | A flag that when set to `true` will suppress sending the log.
46
47
 
47
48
  ### Rails
48
49
 
49
50
  ```ruby
50
- # application.rb
51
+ # config/environments/development.rb or config/environments/production.rb
51
52
  require "readme/metrics"
52
53
 
53
- options = {
54
- api_key: "YOUR_API_KEY",
54
+ sdk_options = {
55
+ api_key: "<<apiKey>>",
55
56
  development: false,
56
57
  reject_params: ["not_included", "dont_send"],
57
58
  buffer_length: 5,
58
59
  }
59
60
 
60
- config.middleware.use Readme::Metrics, options do |env|
61
- current_user = env['warden'].authenticate(scope: :current_user)
61
+ config.middleware.use Readme::Metrics, sdk_options do |env|
62
+ current_user = env['warden'].authenticate
62
63
 
63
- {
64
- id: current_user.id
65
- label: current_user.full_name,
64
+ payload_data = current_user.present? ? {
65
+ api_key: current_user.api_key, # Not the same as the ReadMe API Key
66
+ label: current_user.name,
66
67
  email: current_user.email
68
+ } : {
69
+ api_key: "guest",
70
+ label: "Guest User",
71
+ email: "guest@example.com"
67
72
  }
73
+
74
+ payload_data
68
75
  end
69
76
  ```
70
77
 
71
- ### Rack::Builder
78
+ ### Rack
72
79
 
73
80
  ```ruby
74
- Rack::Builder.new do |builder|
75
- options = {
76
- api_key: "YOUR_API_KEY",
77
- development: false,
78
- reject_params: ["not_included", "dont_send"]
79
- }
81
+ # config.ru
82
+ sdk_options = {
83
+ api_key: "<<apiKey>>",
84
+ development: false,
85
+ reject_params: ["not_included", "dont_send"]
86
+ }
80
87
 
81
- builder.use Readme::Metrics, options do |env|
88
+ use Readme::Metrics, sdk_options do |env|
82
89
  {
83
- id: "my_application_id"
84
- label: "My Application",
85
- email: "my.application@example.com"
90
+ api_key: "owlbert_api_key"
91
+ label: "Owlbert",
92
+ email: "owlbert@example.com",
93
+ log_id: SecureRandom.uuid
86
94
  }
87
- end
88
- builder.run your_app
89
95
  end
96
+
97
+ run YourApp.new
90
98
  ```
91
99
 
92
- ## License
100
+ ### Sample Applications
101
+
102
+ - [Rails](https://github.com/readmeio/metrics-sdk-rails-sample)
103
+ - [Rack](https://github.com/readmeio/metrics-sdk-racks-sample)
104
+ - [Sinatra](https://github.com/readmeio/metrics-sdk-sinatra-example)
93
105
 
94
- [View our license here](https://github.com/readmeio/metrics-sdks/tree/master/packages/ruby/LICENSE)
106
+ ### Contributing
95
107
 
108
+ 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.
109
+
110
+ ## License
96
111
 
112
+ [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
- { id: "unique_id", label: "Your user label", email: "Your user email" }
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,46 +1,73 @@
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
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
- class AllowOnly
15
- def initialize(filter_values)
16
- @filter_values = filter_values
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
- def filter(hash)
20
- hash.select { |key, _value| @filter_values.include?(key) }
21
- end
22
- end
22
+ class AllowOnly
23
+ def initialize(filter_fields)
24
+ @allowed_fields = filter_fields
25
+ end
23
26
 
24
- class RejectParams
25
- def initialize(filter_values)
26
- @filter_values = filter_values
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)
30
+
31
+ allowed_params.merge(Filter.redact(rejected_params))
32
+ end
33
+
34
+ def pass_through?
35
+ false
36
+ end
27
37
  end
28
38
 
29
- def filter(hash)
30
- hash.reject { |key, _value| @filter_values.include?(key) }
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
50
+
51
+ def pass_through?
52
+ false
53
+ end
31
54
  end
32
- end
33
55
 
34
- class None
35
- def filter(hash)
36
- hash
56
+ class None
57
+ def filter(hash)
58
+ hash
59
+ end
60
+
61
+ def pass_through?
62
+ true
63
+ end
37
64
  end
38
- end
39
65
 
40
- class FilterArgsError < StandardError
41
- def initialize
42
- msg = "Can only supply either reject_params or allow_only, not both."
43
- super(msg)
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
44
71
  end
45
72
  end
46
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: @request.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
@@ -46,9 +57,37 @@ module Readme
46
57
  end
47
58
 
48
59
  def request_body
60
+ if @filter.pass_through?
61
+ pass_through_body
62
+ elsif is_form_urlencoded?
63
+ form_urlencoded_body
64
+ elsif is_json?
65
+ json_body
66
+ else
67
+ @request.body
68
+ end
69
+ end
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
+
80
+ def json_body
49
81
  parsed_body = JSON.parse(@request.body)
50
82
  Har::Collection.new(@filter, parsed_body).to_h.to_json
51
- rescue
83
+ end
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
+
90
+ def pass_through_body
52
91
  @request.body
53
92
  end
54
93
  end
@@ -4,8 +4,6 @@ require "readme/har/collection"
4
4
  module Readme
5
5
  module Har
6
6
  class ResponseSerializer
7
- JSON_MIME_TYPES = ["application/json", "application/x-json", "text/json", "text/x-json", "+json"]
8
-
9
7
  def initialize(request, response, filter)
10
8
  @request = request
11
9
  @response = response
@@ -29,9 +27,9 @@ module Readme
29
27
  private
30
28
 
31
29
  def content
32
- if response_body.nil?
30
+ if @response.body.empty?
33
31
  empty_content
34
- elsif content_type_is_json?
32
+ elsif @response.json?
35
33
  json_content
36
34
  else
37
35
  pass_through_content
@@ -43,37 +41,23 @@ module Readme
43
41
  end
44
42
 
45
43
  def json_content
46
- parsed_body = JSON.parse(response_body)
44
+ parsed_body = JSON.parse(@response.body)
47
45
 
48
- {mimeType: @response.content_type,
49
- size: @response.content_length,
50
- text: Har::Collection.new(@filter, parsed_body).to_h.to_json}
46
+ {
47
+ mimeType: @response.content_type,
48
+ size: @response.content_length,
49
+ text: Har::Collection.new(@filter, parsed_body).to_h.to_json
50
+ }
51
51
  rescue
52
52
  pass_through_content
53
53
  end
54
54
 
55
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
56
+ {
57
+ mimeType: @response.content_type,
58
+ size: @response.content_length,
59
+ text: @response.body
60
+ }
77
61
  end
78
62
  end
79
63
  end
@@ -32,7 +32,8 @@ module Readme
32
32
  def creator
33
33
  {
34
34
  name: Readme::Metrics::SDK_NAME,
35
- version: Readme::Metrics::VERSION
35
+ version: Readme::Metrics::VERSION,
36
+ comment: "#{Readme::Metrics::PLATFORM}/#{RUBY_VERSION}"
36
37
  }
37
38
  end
38
39
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Readme
2
2
  class Metrics
3
- VERSION = "0.2.0"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
@@ -4,16 +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"
7
+ require "readme/http_request"
8
+ require "readme/http_response"
8
9
  require "httparty"
9
10
  require "logger"
11
+ require "os"
10
12
 
11
13
  module Readme
12
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
+
13
27
  SDK_NAME = "Readme.io Ruby SDK"
14
- DEFAULT_BUFFER_LENGTH = 10
28
+ PLATFORM = platform
29
+ DEFAULT_BUFFER_LENGTH = 1
15
30
  ENDPOINT = "https://metrics.readme.io/v1/request"
16
- USER_INFO_KEYS = [:id, :label, :email]
17
31
 
18
32
  def self.logger
19
33
  @@logger
@@ -32,7 +46,7 @@ module Readme
32
46
  @get_user_info = get_user_info
33
47
 
34
48
  buffer_length = options[:buffer_length] || DEFAULT_BUFFER_LENGTH
35
- @@request_queue = Readme::RequestQueue.new(options[:api_key], buffer_length)
49
+ @@request_queue = options[:request_queue] || Readme::RequestQueue.new(options[:api_key], buffer_length)
36
50
  @@logger = options[:logger] || Logger.new($stdout)
37
51
  end
38
52
 
@@ -40,16 +54,17 @@ module Readme
40
54
  start_time = Time.now
41
55
  status, headers, body = @app.call(env)
42
56
  end_time = Time.now
43
- response = Rack::Response.new(body, status, headers)
44
57
 
45
58
  begin
59
+ response = HttpResponse.from_parts(status, headers, body)
46
60
  process_response(
47
61
  response: response,
48
62
  env: env,
49
63
  start_time: start_time,
50
64
  end_time: end_time
51
65
  )
52
- rescue
66
+ rescue => e
67
+ Readme::Metrics.logger.warn "The following error occured when trying to log to the ReadMe metrics API: #{e.message}. Request not logged."
53
68
  [status, headers, body]
54
69
  end
55
70
 
@@ -63,16 +78,34 @@ module Readme
63
78
  har = Har::Serializer.new(request, response, start_time, end_time, @filter)
64
79
  user_info = @get_user_info.call(env)
65
80
 
66
- if user_info_invalid?(user_info)
67
- Readme::Metrics.logger.error Errors.bad_block_message(user_info)
81
+ if !user_info_valid?(user_info)
82
+ Readme::Metrics.logger.warn Errors.bad_block_message(user_info)
68
83
  elsif request.options?
69
84
  Readme::Metrics.logger.info "OPTIONS request omitted from ReadMe API logging"
85
+ elsif !can_filter? request, response
86
+ Readme::Metrics.logger.warn "Request or response body MIME type isn't supported for filtering. Omitting request from ReadMe API logging"
70
87
  else
71
88
  payload = Payload.new(har, user_info, development: @development)
72
- @@request_queue.push(payload.to_json)
89
+ @@request_queue.push(payload.to_json) unless payload.ignore
73
90
  end
74
91
  end
75
92
 
93
+ def can_filter?(request, response)
94
+ @filter.pass_through? || can_parse_bodies?(request, response)
95
+ end
96
+
97
+ def can_parse_bodies?(request, response)
98
+ parseable_request?(request) && parseable_response?(response)
99
+ end
100
+
101
+ def parseable_response?(response)
102
+ response.body.empty? || response.json?
103
+ end
104
+
105
+ def parseable_request?(request)
106
+ request.body.empty? || request.json? || request.form_data?
107
+ end
108
+
76
109
  def validate_options(options)
77
110
  raise Errors::ConfigurationError, Errors::API_KEY_ERROR if options[:api_key].nil?
78
111
 
@@ -112,10 +145,10 @@ module Readme
112
145
  arg == true || arg == false
113
146
  end
114
147
 
115
- def user_info_invalid?(user_info)
116
- user_info.nil? ||
117
- user_info.values.any?(&:nil?) ||
118
- USER_INFO_KEYS.sort != user_info.keys.sort
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)
119
152
  end
120
153
  end
121
154
  end
@@ -1,13 +1,22 @@
1
+ require "uuid"
2
+
1
3
  module Readme
2
4
  class Payload
3
- def initialize(har, user_info, development:)
5
+ attr_reader :ignore
6
+
7
+ def initialize(har, info, development:)
4
8
  @har = har
5
- @user_info = 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,
@@ -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/master/packages/ruby"
17
- spec.metadata["changelog_uri"] = "https://github.com/readmeio/metrics-sdks/blob/master/packages/ruby/CHANGELOG.md"
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.
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: 0.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ReadMe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-26 00:00:00.000000000 Z
11
+ date: 2022-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -38,15 +38,18 @@ files:
38
38
  - LICENSE
39
39
  - README.md
40
40
  - Rakefile
41
+ - SECURITY.md
41
42
  - bin/console
42
43
  - bin/setup
43
- - lib/http_request.rb
44
+ - lib/readme/content_type_helper.rb
44
45
  - lib/readme/errors.rb
45
46
  - lib/readme/filter.rb
46
47
  - lib/readme/har/collection.rb
47
48
  - lib/readme/har/request_serializer.rb
48
49
  - lib/readme/har/response_serializer.rb
49
50
  - lib/readme/har/serializer.rb
51
+ - lib/readme/http_request.rb
52
+ - lib/readme/http_response.rb
50
53
  - lib/readme/metrics.rb
51
54
  - lib/readme/metrics/version.rb
52
55
  - lib/readme/payload.rb
@@ -57,8 +60,8 @@ licenses:
57
60
  - ISC
58
61
  metadata:
59
62
  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
63
+ source_code_uri: https://github.com/readmeio/metrics-sdks/blob/main/packages/ruby
64
+ changelog_uri: https://github.com/readmeio/metrics-sdks/blob/main/packages/ruby/CHANGELOG.md
62
65
  post_install_message:
63
66
  rdoc_options: []
64
67
  require_paths:
@@ -74,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
77
  - !ruby/object:Gem::Version
75
78
  version: '0'
76
79
  requirements: []
77
- rubygems_version: 3.1.2
80
+ rubygems_version: 3.1.4
78
81
  signing_key:
79
82
  specification_version: 4
80
83
  summary: SDK for Readme's metrics API
data/lib/http_request.rb DELETED
@@ -1,88 +0,0 @@
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 options?
49
- @request.request_method == "OPTIONS"
50
- end
51
-
52
- def headers
53
- @request
54
- .each_header
55
- .select { |key, _| http_header?(key) }
56
- .to_h
57
- .transform_keys { |header| normalize_header_name(header) }
58
- end
59
-
60
- def body
61
- @request.body.rewind
62
- content = @request.body.read
63
- @request.body.rewind
64
-
65
- content
66
- end
67
-
68
- def parsed_form_data
69
- @request.POST
70
- end
71
-
72
- private
73
-
74
- # "headers" in Rack::Request just means any key in the env. The HTTP headers
75
- # are all the headers prefixed with `HTTP_` as per the spec:
76
- # https://github.com/rack/rack/blob/master/SPEC.rdoc#the-environment-
77
- # Other "headers" like version and host are prefixed with `HTTP_` by Rack but
78
- # don't seem to be considered legit HTTP headers.
79
- def http_header?(name)
80
- name.start_with?("HTTP") && !HTTP_NON_HEADERS.include?(name)
81
- end
82
-
83
- # Headers like `Content-Type: application/json` come into rack like
84
- # `"HTTP_CONTENT_TYPE" => "application/json"`.
85
- def normalize_header_name(header)
86
- header.delete_prefix("HTTP_").split("_").map(&:capitalize).join("-")
87
- end
88
- end