liveqa 1.4.6

Sign up to get free protection for your applications and to get access to all the features.
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