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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17dad1b40b974d05ae3d57a664e6f95c393cbaf8e807071631c11800b2ee118c
4
- data.tar.gz: 2d3595d321dea5bac6b5577180f608a2f9045e333592e5c33cca4c52a3e997ea
3
+ metadata.gz: 8f8fe458fadd08b945bcd32ba4d64a4ba2ba6a609dde94f3a501c50bc505967d
4
+ data.tar.gz: bfa8ff8c37330c3487d8572e2d9ec207147780334295810dc3d0aecfe529e951
5
5
  SHA512:
6
- metadata.gz: a586f8ae1273269473451ea60bfeccb5e9fd01b6fa4ddacfd99b5918dfed24aa340a9e61aba6036a96b572c1eef50c4d2e1a9cd87162442197a02a4ff2456b2c
7
- data.tar.gz: a7d894c039bdc676164f1dcd083650ae8ce026152baad7d83ea790a3ee4850e4de59246e922a641d6446943389c456557c1712b8b34c2085d4398e226c323c86
6
+ metadata.gz: 0b849b923ddafe6073b039ebedd9387b1f17fb93e990c3ffa7bcd8c407b5349a387001a6f46684fe7202d955826c705892d46dadc8da20db503546cf2a0d7e83
7
+ data.tar.gz: c728319d845cb1ae8d368c0f995a71f0f1451ad03a8f94064aaa782b4d6cc094cf2269c46c0e5c2d86dbde5ea3527edab4371b4e617df5ad24599558bb29fd3c
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,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- readme-metrics (0.2.0)
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.7.0)
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.18.1)
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
- mime-types (3.3.1)
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.2020.0512)
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.5)
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
- # 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,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
- ### 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 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
- ### Sensitive Data
37
+ ### Payload Data
35
38
 
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.
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
- options = {
54
- api_key: "YOUR_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, options do |env|
60
+ config.middleware.use Readme::Metrics, sdk_options do |env|
61
61
  current_user = env['warden'].authenticate
62
62
 
63
- if current_user.present?
64
- {
65
- id: current_user.id,
66
- label: current_user.name,
67
- email: current_user.email
68
- }
69
- else
70
- {
71
- id: "guest",
72
- label: "Guest User",
73
- email: "guest@example.com"
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
- options = {
84
- api_key: "YOUR_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, options do |env|
87
+ use Readme::Metrics, sdk_options do |env|
90
88
  {
91
- id: "my_application_id"
92
- label: "My Application",
93
- email: "my.application@example.com"
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/master/packages/ruby/LICENSE)
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
- { 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,58 +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.map(&:downcase)
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.downcase) }
21
- end
22
+ class AllowOnly
23
+ def initialize(filter_fields)
24
+ @allowed_fields = filter_fields
25
+ end
22
26
 
23
- def pass_through?
24
- false
25
- end
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
- class RejectParams
29
- def initialize(filter_values)
30
- @filter_values = filter_values.map(&:downcase)
31
- end
31
+ allowed_params.merge(Filter.redact(rejected_params))
32
+ end
32
33
 
33
- def filter(hash)
34
- hash.reject { |key, _value| @filter_values.include?(key.downcase) }
34
+ def pass_through?
35
+ false
36
+ end
35
37
  end
36
38
 
37
- def pass_through?
38
- false
39
- end
40
- end
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
- class None
43
- def filter(hash)
44
- hash
51
+ def pass_through?
52
+ false
53
+ end
45
54
  end
46
55
 
47
- def pass_through?
48
- true
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
- class FilterArgsError < StandardError
53
- def initialize
54
- msg = "Can only supply either reject_params or allow_only, not both."
55
- 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
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: @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
@@ -48,18 +59,34 @@ module Readme
48
59
  def request_body
49
60
  if @filter.pass_through?
50
61
  pass_through_body
51
- else
52
- # Only JSON allowed for non-pass-through situations. It will raise
53
- # if the body can't be parsed as JSON, aborting the request.
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
@@ -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 = "1.0.0"
3
+ VERSION = "2.0.1"
4
4
  end
5
5
  end
@@ -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
- DEFAULT_BUFFER_LENGTH = 10
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 user_info_invalid?(user_info)
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 user_info_invalid?(user_info)
136
- user_info.nil? ||
137
- user_info.values.any?(&:nil?) ||
138
- 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)
139
152
  end
140
153
  end
141
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.
@@ -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: 1.0.0
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: 2020-08-28 00:00:00.000000000 Z
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/master/packages/ruby
63
- changelog_uri: https://github.com/readmeio/metrics-sdks/blob/master/packages/ruby/CHANGELOG.md
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.2
108
+ rubygems_version: 3.1.4
80
109
  signing_key:
81
110
  specification_version: 4
82
111
  summary: SDK for Readme's metrics API
@@ -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