koala 2.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +32 -0
  3. data/Gemfile +5 -3
  4. data/ISSUE_TEMPLATE +25 -0
  5. data/PULL_REQUEST_TEMPLATE +11 -0
  6. data/changelog.md +161 -4
  7. data/code_of_conduct.md +64 -12
  8. data/koala.gemspec +5 -1
  9. data/lib/koala/api/batch_operation.rb +3 -6
  10. data/lib/koala/api/{graph_api.rb → graph_api_methods.rb} +29 -104
  11. data/lib/koala/api/graph_batch_api.rb +112 -65
  12. data/lib/koala/api/graph_collection.rb +19 -12
  13. data/lib/koala/api/graph_error_checker.rb +4 -3
  14. data/lib/koala/api.rb +65 -26
  15. data/lib/koala/configuration.rb +56 -0
  16. data/lib/koala/errors.rb +22 -2
  17. data/lib/koala/http_service/request.rb +133 -0
  18. data/lib/koala/http_service/response.rb +6 -4
  19. data/lib/koala/http_service/uploadable_io.rb +0 -5
  20. data/lib/koala/http_service.rb +29 -76
  21. data/lib/koala/oauth.rb +8 -8
  22. data/lib/koala/realtime_updates.rb +26 -21
  23. data/lib/koala/test_users.rb +9 -8
  24. data/lib/koala/version.rb +1 -1
  25. data/lib/koala.rb +7 -9
  26. data/readme.md +83 -109
  27. data/spec/cases/api_spec.rb +176 -69
  28. data/spec/cases/configuration_spec.rb +11 -0
  29. data/spec/cases/error_spec.rb +16 -3
  30. data/spec/cases/graph_api_batch_spec.rb +75 -44
  31. data/spec/cases/graph_api_spec.rb +15 -29
  32. data/spec/cases/graph_collection_spec.rb +47 -34
  33. data/spec/cases/graph_error_checker_spec.rb +31 -2
  34. data/spec/cases/http_service/request_spec.rb +250 -0
  35. data/spec/cases/http_service/response_spec.rb +24 -0
  36. data/spec/cases/http_service_spec.rb +126 -286
  37. data/spec/cases/koala_spec.rb +7 -5
  38. data/spec/cases/oauth_spec.rb +41 -2
  39. data/spec/cases/realtime_updates_spec.rb +51 -13
  40. data/spec/cases/test_users_spec.rb +56 -2
  41. data/spec/cases/uploadable_io_spec.rb +31 -31
  42. data/spec/fixtures/cat.m4v +0 -0
  43. data/spec/fixtures/facebook_data.yml +4 -6
  44. data/spec/fixtures/mock_facebook_responses.yml +41 -78
  45. data/spec/fixtures/vcr_cassettes/app_test_accounts.yml +97 -0
  46. data/spec/integration/graph_collection_spec.rb +8 -5
  47. data/spec/spec_helper.rb +2 -2
  48. data/spec/support/graph_api_shared_examples.rb +152 -337
  49. data/spec/support/koala_test.rb +11 -13
  50. data/spec/support/mock_http_service.rb +11 -14
  51. data/spec/support/uploadable_io_shared_examples.rb +4 -4
  52. metadata +47 -48
  53. data/.autotest +0 -12
  54. data/.travis.yml +0 -17
  55. data/Guardfile +0 -6
  56. data/autotest/discover.rb +0 -1
  57. data/lib/koala/api/rest_api.rb +0 -135
  58. data/lib/koala/http_service/multipart_request.rb +0 -41
  59. data/spec/cases/multipart_request_spec.rb +0 -65
  60. data/spec/support/rest_api_shared_examples.rb +0 -168
@@ -1,16 +1,18 @@
1
- require 'koala/api'
2
- require 'koala/api/batch_operation'
1
+ require "koala/api"
2
+ require "koala/api/batch_operation"
3
3
 
4
4
  module Koala
5
5
  module Facebook
6
6
  # @private
7
- class GraphBatchAPI < API
7
+ class GraphBatchAPI
8
8
  # inside a batch call we can do anything a regular Graph API can do
9
9
  include GraphAPIMethods
10
10
 
11
+ # Limits from @see https://developers.facebook.com/docs/marketing-api/batch-requests/v2.8
12
+ MAX_CALLS = 50
13
+
11
14
  attr_reader :original_api
12
15
  def initialize(api)
13
- super(api.access_token, api.app_secret)
14
16
  @original_api = api
15
17
  end
16
18
 
@@ -18,7 +20,9 @@ module Koala
18
20
  @batch_calls ||= []
19
21
  end
20
22
 
21
- def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
23
+ # Enqueue a call into the batch for later processing.
24
+ # See API#graph_call
25
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
22
26
  # normalize options for consistency
23
27
  options = Koala::Utils.symbolize_hash(options)
24
28
 
@@ -34,74 +38,117 @@ module Koala
34
38
  nil # batch operations return nothing immediately
35
39
  end
36
40
 
37
- # redefine the graph_call method so we can use this API inside the batch block
38
- # just like any regular Graph API
39
- alias_method :graph_call_outside_batch, :graph_call
40
- alias_method :graph_call, :graph_call_in_batch
41
-
42
- # execute the queued batch calls
41
+ # execute the queued batch calls. limits it to 50 requests per call.
42
+ # NOTE: if you use `name` and JsonPath references, you should ensure to call `execute` for each
43
+ # co-reference group and that the group size is not greater than the above limits.
43
44
  def execute(http_options = {})
44
- return [] unless batch_calls.length > 0
45
- # Turn the call args collected into what facebook expects
46
- args = {}
47
- args["batch"] = MultiJson.dump(batch_calls.map { |batch_op|
48
- args.merge!(batch_op.files) if batch_op.files
49
- batch_op.to_batch_params(access_token, app_secret)
50
- })
45
+ return [] if batch_calls.empty?
46
+
47
+ batch_results = []
48
+ batch_calls.each_slice(MAX_CALLS) do |batch|
49
+ # Turn the call args collected into what facebook expects
50
+ args = {"batch" => batch_args(batch)}
51
+ batch.each do |call|
52
+ args.merge!(call.files || {})
53
+ end
51
54
 
52
- batch_result = graph_call_outside_batch('/', args, 'post', http_options) do |response|
53
- unless response
54
- # Facebook sometimes reportedly returns an empty body at times
55
- # see https://github.com/arsduo/koala/issues/184
56
- raise BadFacebookResponse.new(200, '', "Facebook returned an empty body")
55
+ original_api.graph_call("/", args, "post", http_options) do |response|
56
+ raise bad_response if response.nil?
57
+
58
+ batch_results += generate_results(response, batch)
57
59
  end
60
+ end
61
+
62
+ batch_results
63
+ end
64
+
65
+ def generate_results(response, batch)
66
+ index = 0
67
+ response.map do |call_result|
68
+ batch_op = batch[index]
69
+ index += 1
70
+ post_process = batch_op.post_processing
58
71
 
59
- # map the results with post-processing included
60
- index = 0 # keep compat with ruby 1.8 - no with_index for map
61
- response.map do |call_result|
62
- # Get the options hash
63
- batch_op = batch_calls[index]
64
- index += 1
65
-
66
- raw_result = nil
67
- if call_result
68
- parsed_headers = if call_result.has_key?('headers')
69
- call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
70
- else
71
- {}
72
- end
73
-
74
- if (error = check_response(call_result['code'], call_result['body'].to_s, parsed_headers))
75
- raw_result = error
76
- else
77
- # (see note in regular api method about JSON parsing)
78
- body = MultiJson.load("[#{call_result['body'].to_s}]")[0]
79
-
80
- # Get the HTTP component they want
81
- raw_result = case batch_op.http_options[:http_component]
82
- when :status
83
- call_result["code"].to_i
84
- when :headers
85
- # facebook returns the headers as an array of k/v pairs, but we want a regular hash
86
- parsed_headers
87
- else
88
- body
89
- end
90
- end
91
- end
92
-
93
- # turn any results that are pageable into GraphCollections
94
- # and pass to post-processing callback if given
95
- result = GraphCollection.evaluate(raw_result, @original_api)
96
- if batch_op.post_processing
97
- batch_op.post_processing.call(result)
98
- else
99
- result
100
- end
72
+ # turn any results that are pageable into GraphCollections
73
+ result = result_from_response(call_result, batch_op)
74
+
75
+ # and pass to post-processing callback if given
76
+ if post_process
77
+ post_process.call(result)
78
+ else
79
+ result
101
80
  end
102
81
  end
103
82
  end
104
83
 
84
+ def bad_response
85
+ # Facebook sometimes reportedly returns an empty body at times
86
+ BadFacebookResponse.new(200, "", "Facebook returned an empty body")
87
+ end
88
+
89
+ def result_from_response(response, options)
90
+ return nil if response.nil?
91
+
92
+ headers = headers_from_response(response)
93
+ error = error_from_response(response, headers)
94
+ component = options.http_options[:http_component]
95
+
96
+ error || desired_component(
97
+ component: component,
98
+ response: response,
99
+ headers: headers
100
+ )
101
+ end
102
+
103
+ def headers_from_response(response)
104
+ headers = response.fetch("headers", [])
105
+
106
+ headers.inject({}) do |compiled_headers, header|
107
+ compiled_headers.merge(header.fetch("name") => header.fetch("value"))
108
+ end
109
+ end
110
+
111
+ def error_from_response(response, headers)
112
+ code = response["code"]
113
+ body = response["body"].to_s
114
+
115
+ GraphErrorChecker.new(code, body, headers).error_if_appropriate
116
+ end
117
+
118
+ def batch_args(calls_for_batch)
119
+ calls = calls_for_batch.map do |batch_op|
120
+ batch_op.to_batch_params(access_token, app_secret)
121
+ end
122
+
123
+ JSON.dump calls
124
+ end
125
+
126
+ def json_body(response)
127
+ # quirks_mode is needed because Facebook sometimes returns a raw true or false value --
128
+ # in Ruby 2.4 we can drop that.
129
+ JSON.parse(response.fetch("body"), quirks_mode: true)
130
+ end
131
+
132
+ def desired_component(component:, response:, headers:)
133
+ result = Koala::HTTPService::Response.new(response['status'], response['body'], headers)
134
+
135
+ # Get the HTTP component they want
136
+ case component
137
+ when :status then response["code"].to_i
138
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
139
+ when :headers then headers
140
+ # (see note in regular api method about JSON parsing)
141
+ else GraphCollection.evaluate(result, original_api)
142
+ end
143
+ end
144
+
145
+ def access_token
146
+ original_api.access_token
147
+ end
148
+
149
+ def app_secret
150
+ original_api.app_secret
151
+ end
105
152
  end
106
153
  end
107
154
  end
@@ -16,30 +16,41 @@ module Koala
16
16
  attr_reader :api
17
17
  # The entire raw response from Facebook.
18
18
  attr_reader :raw_response
19
+ # The headers from the Facebook response
20
+ attr_reader :headers
19
21
 
20
22
  # Initialize the array of results and store various additional paging-related information.
21
23
  #
22
- # @param response the response from Facebook (a hash whose "data" key is an array)
24
+ # @param [Koala::HTTPService::Response] response object wrapping the raw Facebook response
23
25
  # @param api the Graph {Koala::Facebook::API API} instance to use to make calls
24
26
  # (usually the API that made the original call).
25
27
  #
26
- # @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
28
+ # @return [Koala::Facebook::API::GraphCollection] an initialized GraphCollection
27
29
  # whose paging, summary, raw_response, and api attributes are populated.
28
30
  def initialize(response, api)
29
- super response["data"]
30
- @paging = response["paging"]
31
- @summary = response["summary"]
32
- @raw_response = response
31
+ super response.data["data"]
32
+ @paging = response.data["paging"]
33
+ @summary = response.data["summary"]
34
+ @raw_response = response.data
33
35
  @api = api
36
+ @headers = response.headers
34
37
  end
35
38
 
36
39
  # @private
37
40
  # Turn the response into a GraphCollection if they're pageable;
38
- # if not, return the original response.
41
+ # if not, return the data of the original response.
39
42
  # The Ads API (uniquely so far) returns a hash rather than an array when queried
40
43
  # with get_connections.
41
44
  def self.evaluate(response, api)
42
- response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
45
+ return nil if response.nil?
46
+
47
+ is_pageable?(response) ? self.new(response, api) : response.data
48
+ end
49
+
50
+ # response will always be an instance of Koala::HTTPService::Response
51
+ # since that is what we get from Koala::Facebook::API#api
52
+ def self.is_pageable?(response)
53
+ response.data.is_a?(Hash) && response.data["data"].is_a?(Array)
43
54
  end
44
55
 
45
56
  # Retrieve the next page of results.
@@ -113,9 +124,5 @@ module Koala
113
124
  end
114
125
  end
115
126
  end
116
-
117
- # @private
118
- # legacy support for when GraphCollection lived directly under Koala::Facebook
119
- GraphCollection = API::GraphCollection
120
127
  end
121
128
  end
@@ -17,7 +17,7 @@ module Koala
17
17
 
18
18
  # Facebook can return debug information in the response headers -- see
19
19
  # https://developers.facebook.com/docs/graph-api/using-graph-api#bugdebug
20
- DEBUG_HEADERS = ["x-fb-debug", "x-fb-rev", "x-fb-trace-id"]
20
+ DEBUG_HEADERS = %w[x-fb-debug x-fb-rev x-fb-trace-id x-business-use-case-usage x-ad-account-usage x-app-usage]
21
21
 
22
22
  def error_if_appropriate
23
23
  if http_status >= 400
@@ -61,8 +61,9 @@ module Koala
61
61
  # Normally, we start with the response body. If it isn't valid JSON, we start with an empty
62
62
  # hash and fill it with error data.
63
63
  @response_hash ||= begin
64
- MultiJson.load(body)
65
- rescue MultiJson::DecodeError
64
+ parsed_body = JSON.parse(body)
65
+ parsed_body.is_a?(Hash) ? parsed_body : {}
66
+ rescue JSON::ParserError
66
67
  {}
67
68
  end
68
69
  end
data/lib/koala/api.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # graph_batch_api and legacy are required at the bottom, since they depend on API being defined
2
- require 'koala/api/graph_api'
3
- require 'koala/api/rest_api'
2
+ require 'koala/api/graph_api_methods'
3
+ require 'koala/api/graph_collection'
4
4
  require 'openssl'
5
5
 
6
6
  module Koala
@@ -13,23 +13,74 @@ module Koala
13
13
  # signed by default, unless you pass appsecret_proof:
14
14
  # false as an option to the API call. (See
15
15
  # https://developers.facebook.com/docs/graph-api/securing-requests/)
16
+ # @param [Block] rate_limit_hook block called with limits received in facebook response headers
16
17
  # @note If no access token is provided, you can only access some public information.
17
18
  # @return [Koala::Facebook::API] the API client
18
- def initialize(access_token = nil, app_secret = nil)
19
+ def initialize(access_token = Koala.config.access_token, app_secret = Koala.config.app_secret, rate_limit_hook = Koala.config.rate_limit_hook)
19
20
  @access_token = access_token
20
21
  @app_secret = app_secret
22
+ @rate_limit_hook = rate_limit_hook
21
23
  end
22
24
 
23
- attr_reader :access_token, :app_secret
25
+ attr_reader :access_token, :app_secret, :rate_limit_hook
24
26
 
25
27
  include GraphAPIMethods
26
- include RestAPIMethods
28
+
29
+ # Make a call directly to the Graph API.
30
+ # (See any of the other methods for example invocations.)
31
+ #
32
+ # @param path the Graph API path to query (no leading / needed)
33
+ # @param args (see #get_object)
34
+ # @param verb the type of HTTP request to make (get, post, delete, etc.)
35
+ # @options (see #get_object)
36
+ #
37
+ # @yield response when making a batch API call, you can pass in a block
38
+ # that parses the results, allowing for cleaner code.
39
+ # The block's return value is returned in the batch results.
40
+ # See the code for {#get_picture} for examples.
41
+ # (Not needed in regular calls; you'll probably rarely use this.)
42
+ #
43
+ # @raise [Koala::Facebook::APIError] if Facebook returns an error
44
+ #
45
+ # @return the result from Facebook
46
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
47
+ # enable appsecret_proof by default
48
+ options = {:appsecret_proof => true}.merge(options) if @app_secret
49
+ response = api(path, args, verb, options)
50
+
51
+ error = GraphErrorChecker.new(response.status, response.body, response.headers).error_if_appropriate
52
+ raise error if error
53
+
54
+ # if we want a component other than the body (e.g. redirect header for images), provide that
55
+ http_component = options[:http_component]
56
+ desired_data = if options[:http_component]
57
+ http_component == :response ? response : response.send(http_component)
58
+ else
59
+ # turn this into a GraphCollection if it's pageable
60
+ API::GraphCollection.evaluate(response, self)
61
+ end
62
+
63
+ if rate_limit_hook
64
+ limits = %w(x-business-use-case-usage x-ad-account-usage x-app-usage).each_with_object({}) do |key, hash|
65
+ value = response.headers.fetch(key, nil)
66
+ next unless value
67
+ hash[key] = JSON.parse(response.headers[key])
68
+ rescue JSON::ParserError => e
69
+ Koala::Utils.logger.error("#{e.class}: #{e.message} while parsing #{key} = #{value}")
70
+ end
71
+
72
+ rate_limit_hook.call(limits) if limits.keys.any?
73
+ end
74
+
75
+ # now process as appropriate for the given call (get picture header, etc.)
76
+ post_processing ? post_processing.call(desired_data) : desired_data
77
+ end
78
+
27
79
 
28
80
  # Makes a request to the appropriate Facebook API.
29
81
  # @note You'll rarely need to call this method directly.
30
82
  #
31
83
  # @see GraphAPIMethods#graph_call
32
- # @see RestAPIMethods#rest_call
33
84
  #
34
85
  # @param path the server path for this request (leading / is prepended if not present)
35
86
  # @param args arguments to be sent to Facebook
@@ -44,14 +95,10 @@ module Koala
44
95
  # @option options [Boolean] :beta use Facebook's beta tier
45
96
  # @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless.
46
97
  # (All API requests with access tokens use SSL.)
47
- # @param error_checking_block a block to evaluate the response status for additional JSON-encoded errors
48
- #
49
- # @yield The response for evaluation
50
- #
51
98
  # @raise [Koala::Facebook::ServerError] if Facebook returns an error (response status >= 500)
52
99
  #
53
- # @return the body of the response from Facebook (unless another http_component is requested)
54
- def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
100
+ # @return a Koala::HTTPService::Response object representing the returned Facebook data
101
+ def api(path, args = {}, verb = "get", options = {})
55
102
  # we make a copy of args so the modifications (added access_token & appsecret_proof)
56
103
  # do not affect the received argument
57
104
  args = args.dup
@@ -60,6 +107,7 @@ module Koala
60
107
  # This is explicitly needed in batch requests so GraphCollection
61
108
  # results preserve any specific access tokens provided
62
109
  args["access_token"] ||= @access_token || @app_access_token if @access_token || @app_access_token
110
+
63
111
  if options.delete(:appsecret_proof) && args["access_token"] && @app_secret
64
112
  args["appsecret_proof"] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @app_secret, args["access_token"])
65
113
  end
@@ -68,7 +116,7 @@ module Koala
68
116
  args = sanitize_request_parameters(args) unless preserve_form_arguments?(options)
69
117
 
70
118
  # add a leading / if needed...
71
- path = "/#{path}" unless path =~ /^\//
119
+ path = "/#{path}" unless path.to_s =~ /^\//
72
120
 
73
121
  # make the request via the provided service
74
122
  result = Koala.make_request(path, args, verb, options)
@@ -77,17 +125,7 @@ module Koala
77
125
  raise Koala::Facebook::ServerError.new(result.status.to_i, result.body)
78
126
  end
79
127
 
80
- yield result if error_checking_block
81
-
82
- # if we want a component other than the body (e.g. redirect header for images), return that
83
- if component = options[:http_component]
84
- component == :response ? result : result.send(options[:http_component])
85
- else
86
- # parse the body as JSON and run it through the error checker (if provided)
87
- # Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
88
- # and cause MultiJson.load to fail -- so we account for that by wrapping the result in []
89
- MultiJson.load("[#{result.body.to_s}]")[0]
90
- end
128
+ result
91
129
  end
92
130
 
93
131
  private
@@ -113,8 +151,9 @@ module Koala
113
151
  def preserve_form_arguments?(options)
114
152
  options[:format] == :json || options[:preserve_form_arguments] || Koala.config.preserve_form_arguments
115
153
  end
154
+
155
+ def check_response(http_status, body, headers)
156
+ end
116
157
  end
117
158
  end
118
159
  end
119
-
120
- require 'koala/api/graph_batch_api'
@@ -0,0 +1,56 @@
1
+ # Global configuration for Koala.
2
+ class Koala::Configuration
3
+ # The default access token to be used if none is otherwise supplied.
4
+ attr_accessor :access_token
5
+
6
+ # The default app secret value to be used if none is otherwise supplied.
7
+ attr_accessor :app_secret
8
+
9
+ # The default application ID to use if none is otherwise supplied.
10
+ attr_accessor :app_id
11
+
12
+ # The default app access token to be used if none is otherwise supplied.
13
+ attr_accessor :app_access_token
14
+
15
+ # The default API version to use if none is otherwise specified.
16
+ attr_accessor :api_version
17
+
18
+ # The default value to use for the oauth_callback_url if no other is provided.
19
+ attr_accessor :oauth_callback_url
20
+
21
+ # Whether to preserve arrays in arguments, which are expected by certain FB APIs (see the ads API
22
+ # in particular, https://developers.facebook.com/docs/marketing-api/adgroup/v2.4)
23
+ attr_accessor :preserve_form_arguments
24
+
25
+ # The server to use for Graph API requests
26
+ attr_accessor :graph_server
27
+
28
+ # The server to use when constructing dialog URLs.
29
+ attr_accessor :dialog_host
30
+
31
+ # Whether or not to mask tokens
32
+ attr_accessor :mask_tokens
33
+
34
+ # Called with the info for the rate limits in the response header
35
+ attr_accessor :rate_limit_hook
36
+
37
+ # Certain Facebook services (beta, video) require you to access different
38
+ # servers. If you're using your own servers, for instance, for a proxy,
39
+ # you can change both the matcher (what value to change when updating the URL) and the
40
+ # replacement values (what to add).
41
+ #
42
+ # So, for instance, to use the beta stack, we match on .facebook and change it to .beta.facebook.
43
+ # If you're talking to fbproxy.mycompany.com, you could set up beta.fbproxy.mycompany.com for
44
+ # FB's beta tier, and set the matcher to /\.fbproxy/ and the beta_replace to '.beta.fbproxy'.
45
+ attr_accessor :host_path_matcher
46
+ attr_accessor :video_replace
47
+ attr_accessor :beta_replace
48
+
49
+ def initialize
50
+ # Default to our default values.
51
+ Koala::HTTPService::DEFAULT_SERVERS.each_pair do |key, value|
52
+ self.public_send("#{key}=", value)
53
+ end
54
+ self.mask_tokens = true
55
+ end
56
+ end
data/lib/koala/errors.rb CHANGED
@@ -23,7 +23,10 @@ module Koala
23
23
  :fb_error_user_title,
24
24
  :fb_error_trace_id,
25
25
  :fb_error_debug,
26
- :fb_error_rev
26
+ :fb_error_rev,
27
+ :fb_buc_usage,
28
+ :fb_ada_usage,
29
+ :fb_app_usage
27
30
 
28
31
  # Create a new API Error
29
32
  #
@@ -50,7 +53,7 @@ module Koala
50
53
  else
51
54
  unless error_info
52
55
  begin
53
- error_info = MultiJson.load(response_body)['error'] if response_body
56
+ error_info = JSON.parse(response_body)['error'] if response_body
54
57
  rescue
55
58
  end
56
59
  error_info ||= {}
@@ -66,6 +69,9 @@ module Koala
66
69
  self.fb_error_trace_id = error_info["x-fb-trace-id"]
67
70
  self.fb_error_debug = error_info["x-fb-debug"]
68
71
  self.fb_error_rev = error_info["x-fb-rev"]
72
+ self.fb_buc_usage = json_parse_for(error_info, "x-business-use-case-usage")
73
+ self.fb_ada_usage = json_parse_for(error_info, "x-ad-account-usage")
74
+ self.fb_app_usage = json_parse_for(error_info, "x-app-usage")
69
75
 
70
76
  error_array = []
71
77
  %w(type code error_subcode message error_user_title error_user_msg x-fb-trace-id).each do |key|
@@ -82,6 +88,20 @@ module Koala
82
88
 
83
89
  super(message)
84
90
  end
91
+
92
+ private
93
+
94
+ # refs: https://developers.facebook.com/docs/graph-api/overview/rate-limiting/#headers
95
+ # NOTE: The header will contain a JSON-formatted string that describes current application rate limit usage.
96
+ def json_parse_for(error_info, key)
97
+ string = error_info[key]
98
+ return if string.nil?
99
+
100
+ JSON.parse(string)
101
+ rescue JSON::ParserError => e
102
+ Koala::Utils.logger.error("#{e.class}: #{e.message} while parsing #{key} = #{string}")
103
+ nil
104
+ end
85
105
  end
86
106
 
87
107
  # Facebook returned an invalid response body