liveqa 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +40 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +12 -0
  9. data/README.md +40 -0
  10. data/Rakefile +4 -0
  11. data/lib/liveqa/api_operation/save.rb +57 -0
  12. data/lib/liveqa/api_resource.rb +123 -0
  13. data/lib/liveqa/async_handlers/base.rb +17 -0
  14. data/lib/liveqa/async_handlers/sidekiq.rb +33 -0
  15. data/lib/liveqa/config.rb +116 -0
  16. data/lib/liveqa/errors.rb +41 -0
  17. data/lib/liveqa/event.rb +22 -0
  18. data/lib/liveqa/library_name.rb +3 -0
  19. data/lib/liveqa/liveqa_object.rb +139 -0
  20. data/lib/liveqa/message.rb +50 -0
  21. data/lib/liveqa/plugins/rack/middleware.rb +121 -0
  22. data/lib/liveqa/plugins/rails/middleware_data.rb +44 -0
  23. data/lib/liveqa/plugins/rails/railtie.rb +23 -0
  24. data/lib/liveqa/plugins/sidekiq/client_middleware.rb +19 -0
  25. data/lib/liveqa/plugins/sidekiq/load.rb +14 -0
  26. data/lib/liveqa/plugins/sidekiq/server_middleware.rb +47 -0
  27. data/lib/liveqa/plugins.rb +16 -0
  28. data/lib/liveqa/request.rb +132 -0
  29. data/lib/liveqa/store.rb +49 -0
  30. data/lib/liveqa/util.rb +148 -0
  31. data/lib/liveqa/version.rb +3 -0
  32. data/lib/liveqa.rb +105 -0
  33. data/liveqa.gemspec +26 -0
  34. data/spec/lib/liveqa/async_handlers/base_spec.rb +19 -0
  35. data/spec/lib/liveqa/async_handlers/sidekiq_spec.rb +40 -0
  36. data/spec/lib/liveqa/config_spec.rb +40 -0
  37. data/spec/lib/liveqa/event_spec.rb +36 -0
  38. data/spec/lib/liveqa/liveqa_object_spec.rb +72 -0
  39. data/spec/lib/liveqa/message_spec.rb +101 -0
  40. data/spec/lib/liveqa/plugins/rack/middleware_spec.rb +25 -0
  41. data/spec/lib/liveqa/plugins/rails/middleware_data_spec.rb +67 -0
  42. data/spec/lib/liveqa/plugins/sidekiq/client_middleware_spec.rb +15 -0
  43. data/spec/lib/liveqa/plugins/sidekiq/server_middleware_spec.rb +63 -0
  44. data/spec/lib/liveqa/store_spec.rb +82 -0
  45. data/spec/lib/liveqa/util_spec.rb +123 -0
  46. data/spec/lib/liveqa_spec.rb +78 -0
  47. data/spec/spec_helper.rb +31 -0
  48. data/spec/support/rack_app.rb +12 -0
  49. metadata +176 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d6664a0eec986c9adae1bacead1983dc3d739162
4
+ data.tar.gz: 7036cb5e2e9b76d770b9ac7a9a9a5902b09aa33c
5
+ SHA512:
6
+ metadata.gz: b83f428273d7e13842210e74ae6676368f57da02a758ad6ef7d905bd391eb110556af77e3b9a59d36490702a5fd03f7f8212d477b1a63b5240223fe59bb5a140
7
+ data.tar.gz: 4b4424ca15f6043a65fcf1651eb66f9a06b409692a31ba14e284bcff9810635c42234b31eecd40074bf4ca0c8c6e5343a2884448ab2b9ba6122e0c043efac02c
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ doc
2
+ *.gem
3
+ Gemfile.lock
4
+ todo.md
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,40 @@
1
+ Metrics/AbcSize:
2
+ Max: 24
3
+ Metrics/LineLength:
4
+ Max: 120
5
+ Metrics/ClassLength:
6
+ CountComments: false
7
+ Max: 120
8
+ Metrics/MethodLength:
9
+ CountComments: false
10
+ Max: 20
11
+ Metrics/CyclomaticComplexity:
12
+ Max: 7
13
+
14
+ Layout/EmptyLinesAroundClassBody:
15
+ Enabled: false
16
+ Layout/EmptyLinesAroundModuleBody:
17
+ Enabled: false
18
+ Layout/EmptyLinesAroundBlockBody:
19
+ Enabled: false
20
+
21
+ Style/FrozenStringLiteralComment:
22
+ Enabled: false
23
+ Style/MethodMissing:
24
+ Enabled: false
25
+
26
+ Documentation:
27
+ Enabled: false
28
+
29
+ Bundler/DuplicatedGem:
30
+ Enabled: false
31
+
32
+ AllCops:
33
+ TargetRubyVersion: 2.3
34
+ Exclude:
35
+ - 'spec/**/*'
36
+ Exclude:
37
+ - bin/**/*
38
+ - vendor/**/*
39
+ - tmp/**/*
40
+ - spec/**/*
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.3
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ cache: bundler
2
+ language: ruby
3
+ rvm:
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3
7
+ - 2.4
8
+ script:
9
+ - bundle exec rspec spec
10
+ - bundle exec rubocop .
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1
4
+
5
+ - Production ready
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ if RUBY_VERSION > '2.1.0' && RUBY_VERSION < '2.2.2'
4
+ gem 'sidekiq', '>= 2.13.0', '< 5.0'
5
+ else
6
+ gem 'sidekiq', '>= 2.13.0'
7
+ end
8
+
9
+ gem 'rack'
10
+ gem 'rails'
11
+
12
+ gemspec
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # LiveQA
2
+
3
+ [![Build Status](https://travis-ci.org/arkes/liveqa-ruby.svg?branch=master)](https://travis-ci.org/arkes/liveqa-ruby)
4
+
5
+ LiveQA ruby integration for [LiveQA](https://www.liveqa.io)
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ gem install liveqa
11
+ ```
12
+
13
+ For Rails in your Gemfile
14
+
15
+ ```ruby
16
+ gem 'liveqa'
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ ```ruby
22
+ LiveQA.configure do |config|
23
+ config.api_key = 'your-api-key'
24
+ end
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ LiveQA.track('my event', {
31
+ user_id: 42,
32
+ properties: {
33
+ order_id: 84
34
+ }
35
+ });
36
+ ```
37
+
38
+ ## Issues
39
+
40
+ If you have any issue you can report them on github, or contact support@liveqa.io
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+
4
+ task default: :spec
@@ -0,0 +1,57 @@
1
+ module LiveQA
2
+ class APIOperation
3
+ ##
4
+ # == Save (create or update) a resource for the API
5
+ #
6
+ module Save
7
+ module ClassMethods
8
+ ##
9
+ # Create an API Resource and validate the params for the API.
10
+ #
11
+ # @param [Hash] params for the request
12
+ # @param [Hash] Additional options for the request
13
+ #
14
+ # @return [LiveQA::Object] response from the API
15
+ def create(params = {}, options = {})
16
+ response = request(:post, resource_path, params, options)
17
+ initialize_from(response)
18
+ end
19
+
20
+ ##
21
+ # Update an API Resource and validate params for the API
22
+ #
23
+ # @param [String] id for the request
24
+ # @param [Hash] params for the request
25
+ # @param [Hash] Additional options for the request
26
+ #
27
+ # @return [LiveQA::Object] response from the API
28
+ def update(id, params = {}, options = {})
29
+ response = request(:put, "#{resource_path}/#{id}", params, options)
30
+ initialize_from(response)
31
+ end
32
+
33
+ end
34
+
35
+ ##
36
+ # Create or Update an API Resource
37
+ #
38
+ # @param [Hash] params for the request
39
+ # @param [Hash] Additional options for the request
40
+ #
41
+ # @return [LiveQA::Object] response from the API
42
+ def save(params = {}, options = {})
43
+ path = singleton_methods.include?(:id) ? "#{resource_path}/#{id}" : resource_path
44
+ method = singleton_methods.include?(:id) ? :put : :post
45
+
46
+ response = request(method, path, to_hash.merge(params), options)
47
+ update_from(response)
48
+ end
49
+ alias update save
50
+
51
+ def self.included(base)
52
+ base.extend(ClassMethods)
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,123 @@
1
+ module LiveQA
2
+ ##
3
+ # == LiveQA \API \Resource
4
+ #
5
+ # Define the API requests methods
6
+ class APIResource < LiveQAObject
7
+ ##
8
+ # @return [String] resource name
9
+ def resource_name
10
+ self.class.resource_name
11
+ end
12
+
13
+ ##
14
+ # @return [String] resource path
15
+ def resource_path(path = nil)
16
+ self.class.resource_path(path)
17
+ end
18
+
19
+ ##
20
+ # Send a request to the API
21
+ #
22
+ # @param [Symbol] method for the request
23
+ # @param [String] endpoint for the request
24
+ # @param [Hash] options
25
+ # @param [Hash] Payload
26
+ #
27
+ # @return [Hash] response
28
+ # @raise [LiveQA::RequestError] if the request is invalid
29
+ def request(method, resource_path, payload = {}, options = {})
30
+ self.class.request(method, resource_path, payload, options)
31
+ end
32
+
33
+ class << self
34
+
35
+ ##
36
+ # @return [String] resource path
37
+ def resource_path(path = nil)
38
+ "/api/#{configurations.api_version}/#{path || @resource_name || CGI.escape(resource_name)}"
39
+ end
40
+
41
+ ##
42
+ # @return [String] resource name
43
+ def resource_name
44
+ Util.underscore("#{class_name}s")
45
+ end
46
+
47
+ ##
48
+ # @return [String] the class name
49
+ def class_name
50
+ name.split('::')[-1]
51
+ end
52
+
53
+ ##
54
+ # Send a request to the API
55
+ #
56
+ # @param [Symbol] method for the request
57
+ # @param [String] endpoint for the request
58
+ # @param [Hash] options
59
+ # @param [Hash] Payload
60
+ #
61
+ # @return [Hash] response
62
+ # @raise [LiveQA::RequestError] if the request is invalid
63
+ def request(method, path, payload = {}, options = {})
64
+ payload = Util.deep_obfuscate_value(payload, configurations.obfuscated_fields)
65
+ url_params = Util.encode_parameters(payload) if method == :get
66
+ uri = build_endpoint_url(path, url_params)
67
+
68
+ request_options = Util.compact(
69
+ method: method,
70
+ url: uri.to_s,
71
+ payload: payload.to_json,
72
+ proxy: configurations.proxy_url,
73
+ use_ssl: configurations.http_secure
74
+ ).merge(headers).merge(options)
75
+
76
+ Request.execute(request_options)
77
+ rescue LiveQA::RequestError => error
78
+ return error.http_body if error.http_status == 422
79
+ raise
80
+ end
81
+
82
+ private
83
+
84
+ def build_endpoint_url(path, params = nil)
85
+ host, port = configurations.api_host.split(':')
86
+ port = port.to_i if port
87
+
88
+ url_params = Util.compact(
89
+ host: host,
90
+ path: path,
91
+ port: port,
92
+ query: params
93
+ )
94
+
95
+ return URI::HTTPS.build(url_params) if configurations.http_secure
96
+ URI::HTTP.build(url_params)
97
+ end
98
+
99
+ def ssl_options
100
+ return {} unless configurations.http_secure
101
+ {
102
+ ca_file: File.expand_path(File.dirname(__FILE__) + '/../../vendor/cacert.pem'),
103
+ verify_mode: OpenSSL::SSL::VERIFY_PEER
104
+ }
105
+ end
106
+
107
+ def headers
108
+ {
109
+ headers: {
110
+ accept: 'application/json',
111
+ content_type: 'application/json',
112
+ x_request_key: configurations.api_key
113
+ }
114
+ }
115
+ end
116
+
117
+ def configurations
118
+ LiveQA.configurations
119
+ end
120
+
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,17 @@
1
+ module LiveQA
2
+ module AsyncHandlers
3
+ class Base
4
+
5
+ def enqueue(*)
6
+ raise LiveQA::MissingImplementation, 'Method \'enqueue\' need to be implemented in a subclass'
7
+ end
8
+
9
+ def execute(args)
10
+ klass_name, method, payload, request_options = args
11
+
12
+ Object.const_get(klass_name).send(method.to_sym, payload, request_options)
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ require 'sidekiq'
2
+
3
+ module LiveQA
4
+ module AsyncHandlers
5
+ class Sidekiq < Base
6
+ include ::Sidekiq::Worker
7
+
8
+ OPTIONS = {
9
+ 'queue' => 'liveqa',
10
+ 'class' => self
11
+ }.freeze
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(options = {})
16
+ @options = OPTIONS.merge(
17
+ Util.deep_stringify_key(options)
18
+ )
19
+ end
20
+
21
+ def enqueue(*args)
22
+ ::Sidekiq::Client.push(
23
+ options.merge('args' => args)
24
+ )
25
+ end
26
+
27
+ def perform(*args)
28
+ execute(args)
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,116 @@
1
+ module LiveQA
2
+ ##
3
+ # == LiveQA \Config
4
+ #
5
+ # Represent the LiveQA configuration for the API
6
+ class Config
7
+
8
+ ASYNC_HANDLERS = {
9
+ sidekiq: {
10
+ class: 'LiveQA::AsyncHandlers::Sidekiq',
11
+ require: 'liveqa/async_handlers/sidekiq'
12
+ }
13
+ }.freeze
14
+
15
+ DEFAULT_OBFUSCATED_FIELDS = %w[
16
+ password
17
+ password_confirmation
18
+ secret
19
+ secret_token
20
+ authenticity_token
21
+ token
22
+ api_key
23
+ access_token
24
+ credit_card_number
25
+ cvv
26
+ ccv
27
+ ].freeze
28
+
29
+ ##
30
+ # @return [String] API key
31
+ attr_accessor :api_key
32
+
33
+ ##
34
+ # @return [String] API host
35
+ attr_accessor :api_host
36
+
37
+ ##
38
+ # @return [String] API version
39
+ attr_accessor :api_version
40
+
41
+ ##
42
+ # @return [String] proxy url
43
+ attr_accessor :proxy_url
44
+
45
+ ##
46
+ # @return [Boolean] http secure
47
+ attr_accessor :http_secure
48
+
49
+ ##
50
+ # @return [Boolean] service is enable
51
+ attr_accessor :enabled
52
+
53
+ ##
54
+ # @return [Array[String]] fields to obfuscate
55
+ attr_accessor :obfuscated_fields
56
+
57
+ ##
58
+ # @return [Null|Symbol|Proc] asynchronous handler
59
+ attr_accessor :async_handler
60
+
61
+ ##
62
+ # @return [Hash] options for asynchronous handler
63
+ attr_accessor :async_options
64
+
65
+ ##
66
+ # @param [Hash{Symbol=>Object}]
67
+ # Initialize and validate the configuration
68
+ def initialize(options = {})
69
+ self.api_key = options[:api_key]
70
+ self.api_host = options[:api_host] || 'api.liveqa.io'
71
+ self.api_version = options[:api_version] || 'v1'
72
+ self.proxy_url = options[:proxy_url]
73
+ self.http_secure = options[:http_secure] || true
74
+ self.enabled = options[:enabled] || true
75
+ self.obfuscated_fields = options[:obfuscated_fields] || []
76
+ self.async_handler = options[:async_handler]
77
+ self.async_options = options[:async_options] || {}
78
+ end
79
+
80
+ ##
81
+ # validate the configuration
82
+ # Raise when configuration are not valid
83
+ # @return [Boolean] true
84
+ def valid!
85
+ format!
86
+
87
+ %i[api_key api_host api_version].each do |field|
88
+ validate_presence(field)
89
+ end
90
+
91
+ true
92
+ end
93
+
94
+ private
95
+
96
+ ##
97
+ # Format configuration fields
98
+ #
99
+ # * Set obfuscated_fields to string
100
+ # * Change to the class for async handler
101
+ #
102
+ def format!
103
+ self.obfuscated_fields = (obfuscated_fields.map(&:to_s) + DEFAULT_OBFUSCATED_FIELDS).uniq
104
+
105
+ return unless ASYNC_HANDLERS[async_handler]
106
+
107
+ require ASYNC_HANDLERS[async_handler][:require]
108
+ self.async_handler = Object.const_get(ASYNC_HANDLERS[async_handler][:class]).new(async_options)
109
+ end
110
+
111
+ def validate_presence(field)
112
+ raise LiveQA::ConfigurationError, "#{field} can't be blank" if send(field).nil? || send(field).empty?
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,41 @@
1
+ module LiveQA
2
+
3
+ ##
4
+ # Configuration Error
5
+ class ConfigurationError < StandardError; end
6
+
7
+ ##
8
+ # Missing Implementation Error
9
+ class MissingImplementation < StandardError; end
10
+
11
+ ##
12
+ # Request to API Error
13
+ class RequestError < StandardError; end
14
+
15
+ ##
16
+ # Method unknown for the request
17
+ class UnknownRequestMethod < StandardError; end
18
+
19
+ ##
20
+ # Request to API Error
21
+ class RequestError < StandardError
22
+
23
+ attr_reader :http_body
24
+ attr_reader :http_status
25
+ attr_reader :http_status_type
26
+ attr_reader :http_message
27
+
28
+ def initialize(response, message: nil)
29
+ @http_status = response.code.to_i
30
+ @http_status_type = response.code_type
31
+ @http_body = response.body
32
+ @http_message = message || response.message
33
+ end
34
+
35
+ def to_s
36
+ "Status #{http_status}: #{http_message}"
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,22 @@
1
+ module LiveQA
2
+ ##
3
+ # == LiveQA \Event
4
+ #
5
+ # @example: Usage
6
+ #
7
+ # request = LiveQA::Event.create('Event Name') #=> #<LiveQA::Response...>
8
+ #
9
+ class Event < APIResource
10
+ include LiveQA::APIOperation::Save
11
+
12
+ class << self
13
+
14
+ def build_payload(payload)
15
+ Message
16
+ .to_h
17
+ .merge(payload)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module LiveQA # :nodoc:
2
+ LIBRARY_NAME = 'liveqa'.freeze
3
+ end
@@ -0,0 +1,139 @@
1
+ module LiveQA
2
+ ##
3
+ # == LiveQA \Object
4
+ #
5
+ # Define the API objects
6
+ class LiveQAObject
7
+ ##
8
+ # @return [Hash] JSON parsed response
9
+ attr_reader :raw
10
+
11
+ ##
12
+ # @return [Hash] JSON parsed response
13
+ attr_reader :data
14
+
15
+ attr_reader :accepted
16
+ alias accepted? accepted
17
+
18
+ attr_reader :errors
19
+
20
+ class << self
21
+
22
+ ##
23
+ # Initialize from the API response
24
+ #
25
+ # @return [LiveQA::LiveQAObject]
26
+ def initialize_from(_response = '')
27
+ object = new
28
+
29
+ object.successful = true
30
+
31
+ object
32
+ end
33
+
34
+ end
35
+
36
+ ##
37
+ # Initialize and create accessor for values
38
+ #
39
+ # @params [Hash] values
40
+ def initialize(values = {})
41
+ @data = []
42
+ @values = {}
43
+
44
+ update_attributes(values)
45
+ end
46
+
47
+ ##
48
+ # get attribute value
49
+ def [](key)
50
+ @values[key.to_sym]
51
+ end
52
+
53
+ ##
54
+ # set attribute value
55
+ def []=(key, value)
56
+ send(:"#{key}=", value)
57
+ end
58
+
59
+ ##
60
+ # @return [Array] all the keys
61
+ def keys
62
+ @values.keys
63
+ end
64
+
65
+ ##
66
+ # @return [Hash] values to hash
67
+ def to_hash
68
+ @values
69
+ end
70
+
71
+ ##
72
+ # @return [JSON] values to JSON
73
+ def to_json
74
+ JSON.generate(@values)
75
+ end
76
+
77
+ ##
78
+ # Update the attribute and add accessor for new attributes
79
+ #
80
+ # @param [Hash] values
81
+ def update_attributes(attributes)
82
+ attributes.each do |(key, value)|
83
+ add_accessor(key, value)
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Add data for sub-object
89
+ #
90
+ # @param [Object] data
91
+ def add_data(data)
92
+ @data << data
93
+ end
94
+
95
+ protected
96
+
97
+ def inspect
98
+ id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : ''
99
+ "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
100
+ end
101
+
102
+ def method_missing(name, *args)
103
+ super unless name.to_s.end_with?('=')
104
+
105
+ attribute = name.to_s[0...-1].to_sym
106
+ value = args.first
107
+
108
+ add_accessor(attribute, value)
109
+ end
110
+
111
+ private
112
+
113
+ def add_accessors(keys, payload = raw)
114
+ keys.each do |key|
115
+ add_accessor(key, payload[key])
116
+ end
117
+ end
118
+
119
+ def add_accessor(name, value)
120
+ @values[name] = value
121
+
122
+ define_singleton_method(name) { @values[name] }
123
+ define_singleton_method(:"#{name}=") do |v|
124
+ @values[name] = v
125
+ end
126
+
127
+ define_singleton_method(:"#{name}?") { value } if [FalseClass, TrueClass].include?(value.class)
128
+ end
129
+
130
+ def remove_accessor(name)
131
+ @values.delete(name)
132
+
133
+ singleton_class.class_eval { remove_method name.to_sym } if singleton_methods.include?(name.to_sym)
134
+ singleton_class.class_eval { remove_method "#{name}=".to_sym } if singleton_methods.include?("#{name}=".to_sym)
135
+ singleton_class.class_eval { remove_method "#{name}?".to_sym } if singleton_methods.include?("#{name}?".to_sym)
136
+ end
137
+
138
+ end
139
+ end