twitter-ads 5.2.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/twitter-ads.rb +4 -1
  3. data/lib/twitter-ads/account.rb +9 -8
  4. data/lib/twitter-ads/campaign/campaign.rb +1 -2
  5. data/lib/twitter-ads/campaign/funding_instrument.rb +1 -2
  6. data/lib/twitter-ads/campaign/line_item.rb +2 -3
  7. data/lib/twitter-ads/campaign/organic_tweet.rb +1 -3
  8. data/lib/twitter-ads/campaign/targeting_criteria.rb +0 -1
  9. data/lib/twitter-ads/campaign/tweet.rb +4 -49
  10. data/lib/twitter-ads/client.rb +2 -2
  11. data/lib/twitter-ads/creative/account_media.rb +4 -6
  12. data/lib/twitter-ads/creative/draft_tweet.rb +40 -0
  13. data/lib/twitter-ads/creative/image_app_download_card.rb +2 -2
  14. data/lib/twitter-ads/creative/image_conversation_card.rb +3 -2
  15. data/lib/twitter-ads/creative/media_creative.rb +1 -2
  16. data/lib/twitter-ads/creative/media_library.rb +2 -4
  17. data/lib/twitter-ads/creative/promoted_account.rb +1 -2
  18. data/lib/twitter-ads/creative/promoted_tweet.rb +1 -2
  19. data/lib/twitter-ads/creative/scheduled_tweet.rb +1 -12
  20. data/lib/twitter-ads/creative/tweets.rb +52 -0
  21. data/lib/twitter-ads/creative/video_app_download_card.rb +4 -6
  22. data/lib/twitter-ads/creative/video_conversation_card.rb +6 -6
  23. data/lib/twitter-ads/creative/video_website_card.rb +3 -5
  24. data/lib/twitter-ads/creative/website_card.rb +2 -2
  25. data/lib/twitter-ads/cursor.rb +6 -0
  26. data/lib/twitter-ads/enum.rb +10 -5
  27. data/lib/twitter-ads/error.rb +5 -15
  28. data/lib/twitter-ads/http/request.rb +30 -1
  29. data/lib/twitter-ads/http/response.rb +1 -13
  30. data/lib/twitter-ads/resources/analytics.rb +99 -47
  31. data/lib/twitter-ads/resources/dsl.rb +8 -1
  32. data/lib/twitter-ads/restapi.rb +29 -0
  33. data/lib/twitter-ads/settings/tax.rb +13 -1
  34. data/lib/twitter-ads/targeting_criteria/conversation.rb +23 -0
  35. data/lib/twitter-ads/utils.rb +23 -0
  36. data/lib/twitter-ads/version.rb +1 -1
  37. data/spec/fixtures/tweet_previews.json +23 -0
  38. data/spec/twitter-ads/campaign/targeting_criteria_spec.rb +0 -1
  39. data/spec/twitter-ads/campaign/tweet_spec.rb +0 -59
  40. data/spec/twitter-ads/client_spec.rb +17 -1
  41. data/spec/twitter-ads/creative/tweet_previews_spec.rb +41 -0
  42. data/spec/twitter-ads/rate_limit_spec.rb +247 -0
  43. data/spec/twitter-ads/retry_count_spec.rb +61 -0
  44. metadata +14 -17
  45. data/lib/twitter-ads/audiences/audience_intelligence.rb +0 -68
  46. data/spec/fixtures/tweet_preview.json +0 -24
  47. data/spec/twitter-ads/creative/account_media_spec.rb +0 -32
  48. data/spec/twitter-ads/creative/image_app_download_card_spec.rb +0 -43
  49. data/spec/twitter-ads/creative/image_conversation_card_spec.rb +0 -40
  50. data/spec/twitter-ads/creative/video_app_download_card_spec.rb +0 -42
  51. data/spec/twitter-ads/creative/video_conversation_card_spec.rb +0 -51
  52. data/spec/twitter-ads/creative/website_card_spec.rb +0 -42
@@ -18,15 +18,13 @@ module TwitterAds
18
18
  property :deleted, type: :bool, read_only: true
19
19
  property :id, read_only: true
20
20
  property :updated_at, type: :time, read_only: true
21
- property :video_content_id, read_only: true
22
- property :video_hls_url, read_only: true
23
21
  property :video_owner_id, read_only: true
24
- property :video_poster_url, read_only: true
25
- property :video_url, read_only: true
22
+ property :poster_media_url, read_only: true
23
+ property :media_url, read_only: true
26
24
 
27
25
  property :country_code
28
26
  property :app_cta
29
- property :image_media_id
27
+ property :poster_media_key
30
28
  property :ipad_app_id
31
29
  property :ipad_deep_link
32
30
  property :iphone_app_id
@@ -34,7 +32,7 @@ module TwitterAds
34
32
  property :googleplay_app_id
35
33
  property :googleplay_deep_link
36
34
  property :name
37
- property :video_id
35
+ property :media_key
38
36
 
39
37
  RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \
40
38
  'accounts/%{account_id}/cards/video_app_download' # @api private
@@ -18,14 +18,14 @@ module TwitterAds
18
18
  property :deleted, type: :bool, read_only: true
19
19
  property :id, read_only: true
20
20
  property :updated_at, type: :time, read_only: true
21
- property :video_url, read_only: true
22
- property :video_poster_url, read_only: true
21
+ property :media_url, read_only: true
22
+ property :poster_media_url, read_only: true
23
23
 
24
- property :cover_image_id
25
- property :cover_video_id
24
+ property :unlocked_image_media_key
25
+ property :unlocked_video_media_key
26
26
  property :fourth_cta
27
27
  property :fourth_cta_tweet
28
- property :image_media_id
28
+ property :poster_media_key
29
29
  property :first_cta
30
30
  property :first_cta_tweet
31
31
  property :name
@@ -36,7 +36,7 @@ module TwitterAds
36
36
  property :third_cta
37
37
  property :third_cta_tweet
38
38
  property :title
39
- property :video_id
39
+ property :media_key
40
40
 
41
41
  RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \
42
42
  'accounts/%{account_id}/cards/video_conversation' # @api private
@@ -19,21 +19,19 @@ module TwitterAds
19
19
  property :deleted, type: :bool, read_only: true
20
20
  property :id, read_only: true
21
21
  property :updated_at, type: :time, read_only: true
22
- property :video_content_id, read_only: true
23
22
  property :video_height, read_only: true
24
- property :video_hls_url, read_only: true
25
23
  property :video_owner_id, read_only: true
26
24
  property :video_poster_height, read_only: true
27
- property :video_poster_url, read_only: true
25
+ property :poster_media_url, read_only: true
28
26
  property :video_poster_width, read_only: true
29
- property :video_url, read_only: true
27
+ property :media_url, read_only: true
30
28
  property :video_width, read_only: true
31
29
  property :website_display_url, read_only: true
32
30
  property :website_dest_url, read_only: true
33
31
 
34
32
  property :name
35
33
  property :title
36
- property :video_id
34
+ property :media_key
37
35
  property :website_url
38
36
 
39
37
  RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/accounts/%{account_id}/cards/video_website"
@@ -17,14 +17,14 @@ module TwitterAds
17
17
  property :created_at, type: :time, read_only: true
18
18
  property :deleted, type: :bool, read_only: true
19
19
  property :id, read_only: true
20
- property :image, read_only: true
20
+ property :media_url, read_only: true
21
21
  property :image_display_height, read_only: true
22
22
  property :image_display_width, read_only: true
23
23
  property :updated_at, type: :time, read_only: true
24
24
  property :website_dest_url, read_only: true
25
25
  property :website_display_url, read_only: true
26
26
 
27
- property :image_media_id
27
+ property :media_key
28
28
  property :name
29
29
  property :website_title
30
30
  property :website_url
@@ -112,6 +112,12 @@ module TwitterAds
112
112
  def from_response(response)
113
113
  @next_cursor = response.body[:next_cursor]
114
114
  @total_count = response.body[:total_count].to_i if response.body.key?(:total_count)
115
+
116
+ TwitterAds::Utils.extract_response_headers(response.headers).each { |key, value|
117
+ singleton_class.class_eval { attr_accessor key }
118
+ instance_variable_set("@#{key}", value)
119
+ }
120
+
115
121
  response.body.fetch(:data, []).each do |object|
116
122
  @collection << if @klass&.method_defined?(:from_response)
117
123
  @klass.new(
@@ -106,6 +106,7 @@ module TwitterAds
106
106
  FUNDING_INSTRUMENT = 'FUNDING_INSTRUMENT'
107
107
  CAMPAIGN = 'CAMPAIGN'
108
108
  LINE_ITEM = 'LINE_ITEM'
109
+ PROMOTED_ACCOUNT = 'PROMOTED_ACCOUNT'
109
110
  PROMOTED_TWEET = 'PROMOTED_TWEET'
110
111
  ORGANIC_TWEET = 'ORGANIC_TWEET'
111
112
  MEDIA_CREATIVE = 'MEDIA_CREATIVE'
@@ -143,7 +144,10 @@ module TwitterAds
143
144
  end
144
145
 
145
146
  module Optimizations
147
+ APP_CLICKS = 'APP_CLICKS'
148
+ APP_INSTALLS = 'APP_INSTALLS'
146
149
  DEFAULT = 'DEFAULT'
150
+ ENGAGEMENTS = 'ENGAGEMENTS'
147
151
  WEBSITE_CONVERSIONS = 'WEBSITE_CONVERSIONS'
148
152
  end
149
153
 
@@ -218,11 +222,6 @@ module TwitterAds
218
222
  HASHTAG = 'HASHTAG'
219
223
  end
220
224
 
221
- module AudienceDefinition
222
- TARGETING_CRITERIA = 'TARGETING_CRITERIA'
223
- KEYWORD_AUDIENCE = 'KEYWORD_AUDIENCE'
224
- end
225
-
226
225
  module LookalikeExpansion
227
226
  DEFINED = 'DEFINED'
228
227
  EXPANDED = 'EXPANDED'
@@ -233,5 +232,11 @@ module TwitterAds
233
232
  PUBLISHED = 'PUBLISHED'
234
233
  SCHEDULED = 'SCHEDULED'
235
234
  end
235
+
236
+ module TimelineType
237
+ ALL = 'ALL'
238
+ NULLCAST = 'NULLCAST'
239
+ ORGANIC = 'ORGANIC'
240
+ end
236
241
  end
237
242
  end
@@ -59,18 +59,7 @@ module TwitterAds
59
59
 
60
60
  # Server Errors (5XX)
61
61
  class ServerError < Error; end
62
-
63
- class ServiceUnavailable < ServerError
64
- attr_reader :retry_after
65
-
66
- def initialize(object)
67
- super object
68
- if object.headers['retry-after']
69
- @retry_after = object.headers['retry-after']
70
- end
71
- self
72
- end
73
- end
62
+ class ServiceUnavailable < ServerError; end
74
63
 
75
64
  # Client Errors (4XX)
76
65
  class ClientError < Error; end
@@ -80,12 +69,13 @@ module TwitterAds
80
69
  class BadRequest < ClientError; end
81
70
 
82
71
  class RateLimit < ClientError
83
- attr_reader :reset_at, :retry_after
72
+ attr_reader :reset_at
84
73
 
85
74
  def initialize(object)
86
75
  super object
87
- @retry_after = object.headers['retry-after']
88
- @reset_at = object.headers['rate_limit_reset']
76
+ header = object.headers.fetch('x-account-rate-limit-reset', nil) ||
77
+ object.headers.fetch('x-rate-limit-reset', nil)
78
+ @reset_at = header.first.to_i
89
79
  self
90
80
  end
91
81
  end
@@ -71,8 +71,37 @@ module TwitterAds
71
71
  token = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)
72
72
  request.oauth!(consumer.http, consumer, token)
73
73
 
74
+ handle_rate_limit = @client.options.fetch(:handle_rate_limit, false)
75
+ retry_max = @client.options.fetch(:retry_max, 0)
76
+ retry_delay = @client.options.fetch(:retry_delay, 1500)
77
+ retry_on_status = @client.options.fetch(:retry_on_status, [500, 503])
78
+ retry_count = 0
79
+ retry_after = nil
80
+
74
81
  write_log(request) if @client.options[:trace]
75
- response = consumer.http.request(request)
82
+ while retry_count <= retry_max
83
+ response = consumer.http.request(request)
84
+ status_code = response.code.to_i
85
+ break if status_code >= 200 && status_code < 300
86
+
87
+ if handle_rate_limit && retry_after.nil?
88
+ rate_limit_reset = response.fetch('x-account-rate-limit-reset', nil) ||
89
+ response.fetch('x-rate-limit-reset', nil)
90
+ if status_code == 429
91
+ retry_after = rate_limit_reset.to_i - Time.now.to_i
92
+ @client.logger.warn('Request reached Rate Limit: resume in %d seconds' % retry_after)
93
+ sleep(retry_after + 5)
94
+ next
95
+ end
96
+ end
97
+
98
+ if retry_max.positive?
99
+ break unless retry_on_status.include?(status_code)
100
+ sleep(retry_delay / 1000)
101
+ end
102
+
103
+ retry_count += 1
104
+ end
76
105
  write_log(response) if @client.options[:trace]
77
106
 
78
107
  Response.new(response.code, response.each {}, response.body)
@@ -9,9 +9,7 @@ module TwitterAds
9
9
  attr_reader :code,
10
10
  :headers,
11
11
  :raw_body,
12
- :body,
13
- :rate_limit_remaining,
14
- :rate_limit_reset
12
+ :body
15
13
 
16
14
  # Creates a new Response object instance.
17
15
  #
@@ -37,16 +35,6 @@ module TwitterAds
37
35
  @body = raw_body
38
36
  end
39
37
 
40
- if headers.key?('x-rate-limit-reset')
41
- @rate_limit = headers['x-rate-limit-limit'].first
42
- @rate_limit_remaining = headers['x-rate-limit-remaining'].first
43
- @rate_limit_reset = headers['x-rate-limit-reset'].first.to_i
44
- elsif headers.key?('x-cost-rate-limit-reset')
45
- @rate_limit = headers['x-cost-rate-limit-limit'].first
46
- @rate_limit_remaining = headers['x-cost-rate-limit-remaining'].first
47
- @rate_limit_reset = Time.at(headers['x-cost-rate-limit-reset'].first.to_i)
48
- end
49
-
50
38
  self
51
39
  end
52
40
 
@@ -5,15 +5,35 @@ require 'zlib'
5
5
  require 'open-uri'
6
6
 
7
7
  module TwitterAds
8
- module Analytics
8
+ class Analytics
9
9
 
10
+ include TwitterAds::DSL
11
+ include TwitterAds::Resource
10
12
  include TwitterAds::Enum
11
13
 
14
+ attr_reader :account
15
+
16
+ property :id, read_only: true
17
+ property :id_str, read_only: true
18
+ property :status, read_only: true
19
+ property :url, read_only: true
20
+ property :created_at, type: :time, read_only: true
21
+ property :expires_at, type: :time, read_only: true
22
+ property :updated_at, type: :time, read_only: true
23
+ property :start_time, type: :time, read_only: true
24
+ property :end_time, type: :time, read_only: true
25
+
26
+ property :entity, read_only: true
27
+ property :entity_ids, read_only: true
28
+ property :placement, read_only: true
29
+ property :granularity, read_only: true
30
+ property :metric_groups, read_only: true
31
+
12
32
  ANALYTICS_MAP = {
13
33
  'TwitterAds::Campaign' => Entity::CAMPAIGN,
14
34
  'TwitterAds::LineItem' => Entity::LINE_ITEM,
15
35
  'TwitterAds::OrganicTweet' => Entity::ORGANIC_TWEET,
16
- 'TwitterAds::Creative::PromotedAccount' => Entity::ACCOUNT,
36
+ 'TwitterAds::Creative::PromotedAccount' => Entity::PROMOTED_ACCOUNT,
17
37
  'TwitterAds::Creative::PromotedTweet' => Entity::PROMOTED_TWEET,
18
38
  'TwitterAds::Creative::MediaCreative' => Entity::MEDIA_CREATIVE
19
39
  }.freeze
@@ -25,42 +45,38 @@ module TwitterAds
25
45
  RESOURCE_ACTIVE_ENTITIES = "/#{TwitterAds::API_VERSION}/" +
26
46
  'stats/accounts/%{account_id}/active_entities' # @api private
27
47
 
28
- def self.included(klass)
29
- klass.send :include, InstanceMethods
30
- klass.extend ClassMethods
48
+ def initialize(account)
49
+ @account = account
50
+ self
31
51
  end
32
52
 
33
- module InstanceMethods
34
-
35
- # Pulls a list of metrics for the current object instance.
36
- #
37
- # @example
38
- # metric_groups = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
39
- # object.stats(metrics)
40
- #
41
- # @param metric_groups [Array] A collection of metric groups to fetch.
42
- # @param opts [Hash] An optional Hash of extended options.
43
- # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
44
- # @option opts [Time] :end_time The end time to use (default: now).
45
- # @option opts [Symbol] :granularity The granularity to use (default: :hour).
46
- #
47
- # @return [Array] The collection of stats requested.
48
- #
49
- # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
50
- # @since 1.0.0
51
- def stats(metric_groups, opts = {})
52
- self.class.stats(account, [id], metric_groups, opts)
53
- end
54
-
53
+ # Pulls a list of metrics for the current object instance.
54
+ #
55
+ # @example
56
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
57
+ # object.stats(metrics)
58
+ #
59
+ # @param metric_groups [Array] A collection of metric groups to fetch.
60
+ # @param opts [Hash] An optional Hash of extended options.
61
+ # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
62
+ # @option opts [Time] :end_time The end time to use (default: now).
63
+ # @option opts [Symbol] :granularity The granularity to use (default: :hour).
64
+ #
65
+ # @return [Array] The collection of stats requested.
66
+ #
67
+ # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
68
+ # @since 1.0.0
69
+ def stats(metric_groups, opts = {})
70
+ self.class.stats(account, [id], metric_groups, opts)
55
71
  end
56
72
 
57
- module ClassMethods
73
+ class << self
58
74
 
59
75
  # Pulls a list of metrics for a specified set of object IDs.
60
76
  #
61
77
  # @example
62
78
  # ids = ['7o4em', 'oc9ce', '1c5lji']
63
- # metric_groups = [MetricGroups.MOBILE_CONVERSION, MetricGroups.ENGAGEMENT]
79
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
64
80
  # object.stats(account, ids, metric_groups)
65
81
  #
66
82
  # @param account [Account] The Account object instance.
@@ -70,7 +86,7 @@ module TwitterAds
70
86
  # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
71
87
  # @option opts [Time] :end_time The end time to use (default: now).
72
88
  # @option opts [Symbol] :granularity The granularity to use (default: :hour).
73
- # @option opts [Symbol] :placement The placement of entity (default: ALL_ON_TWITTER).
89
+ # @option opts [String] :placement The placement of entity (default: ALL_ON_TWITTER).
74
90
  #
75
91
  # @return [Array] The collection of stats requested.
76
92
  #
@@ -108,7 +124,7 @@ module TwitterAds
108
124
  #
109
125
  # @example
110
126
  # ids = ['7o4em', 'oc9ce', '1c5lji']
111
- # metric_groups = [MetricGroups.MOBILE_CONVERSION, MetricGroups.ENGAGEMENT]
127
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
112
128
  # object.create_async_job(account, ids, metric_groups)
113
129
  #
114
130
  # @param account [Account] The Account object instance.
@@ -118,16 +134,17 @@ module TwitterAds
118
134
  # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
119
135
  # @option opts [Time] :end_time The end time to use (default: now).
120
136
  # @option opts [Symbol] :granularity The granularity to use (default: :hour).
121
- # @option opts [Symbol] :placement The placement of entity (default: ALL_ON_TWITTER).
122
- # @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
137
+ # @option opts [String] :placement The placement of entity (default: ALL_ON_TWITTER).
138
+ # @option opts [String] :segmentation_type The segmentation type to use (default: none).
123
139
  #
124
140
  # @return The response of creating job
125
141
  #
126
142
  # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
127
- # @sync 1.0.0
143
+ # @since 1.0.0
128
144
 
129
145
  def create_async_job(account, ids, metric_groups, opts = {})
130
146
  # set default metric values
147
+ entity = opts.fetch(:entity, name)
131
148
  end_time = opts.fetch(:end_time, (Time.now - Time.now.sec - (60 * Time.now.min)))
132
149
  start_time = opts.fetch(:start_time, end_time - 604_800) # 7 days ago
133
150
  granularity = opts.fetch(:granularity, :hour)
@@ -143,7 +160,7 @@ module TwitterAds
143
160
  start_time: TwitterAds::Utils.to_time(start_time, granularity, start_utc_offset),
144
161
  end_time: TwitterAds::Utils.to_time(end_time, granularity, end_utc_offset),
145
162
  granularity: granularity.to_s.upcase,
146
- entity: ANALYTICS_MAP[name],
163
+ entity: ANALYTICS_MAP[entity],
147
164
  placement: placement,
148
165
  country: country,
149
166
  platform: platform
@@ -153,19 +170,18 @@ module TwitterAds
153
170
  params['entity_ids'] = ids.join(',')
154
171
 
155
172
  resource = self::RESOURCE_ASYNC_STATS % { account_id: account.id }
156
- puts 'my resource is ' + resource
157
173
  response = Request.new(account.client, :post, resource, params: params).perform
158
- response.body[:data]
174
+ TwitterAds::Analytics.new(account).from_response(response.body[:data], response.headers)
159
175
  end
160
176
 
161
177
  # Check async job status.
162
178
  # GET /#{TwitterAds::API_VERSION}/stats/jobs/accounts/:account_id
163
179
  #
164
180
  # @example
165
- # TwitterAds::LineItem.check_async_job_status(account, job_id: '1357343438724431305')
181
+ # TwitterAds::LineItem.check_async_job_status(account, job_ids: ['1357343438724431305'])
166
182
  #
167
183
  # @param account [Account] The Account object instance.
168
- # @option opts [String] :job_id The starting time to use (default: 7 days ago).
184
+ # @option opts [Array] :job_ids A collection of job IDs to fetch.
169
185
  #
170
186
  # @return A cursor of job statuses
171
187
 
@@ -173,18 +189,18 @@ module TwitterAds
173
189
  # set default values
174
190
  job_ids = opts.fetch(:job_ids, nil)
175
191
  params = {}
176
- params[:job_ids] = Array.wrap(job_ids).join(',') if job_ids
192
+ params[:job_ids] = job_ids.join(',') if job_ids
177
193
 
178
194
  resource = self::RESOURCE_ASYNC_STATS % { account_id: account.id }
179
195
  request = Request.new(account.client, :get, resource, params: params)
180
- Cursor.new(nil, request, init_with: [account])
196
+ Cursor.new(TwitterAds::Analytics, request, init_with: [account])
181
197
  end
182
198
 
183
199
  # Fetch async job data for a completed job.
184
200
  # Raises HTTP 404 exception, otherwise retries up to 5 times with exponential backoff.
185
201
  #
186
202
  # @example
187
- # response_data = TwitterAds::LineItem.fetch_async_job_data(account, file_url)
203
+ # response_data = TwitterAds::LineItem.fetch_async_job_data(account, data_url)
188
204
  #
189
205
  # @param data_url [String] The URL from the successful completion of an async job.
190
206
  #
@@ -209,21 +225,57 @@ module TwitterAds
209
225
  end
210
226
  end
211
227
 
228
+ # Retrieve details about which entities' analytics metrics
229
+ # have changed in a given time period.
230
+ #
231
+ # @example
232
+ # time = Time.now
233
+ # utc_offset = '+09:00'
234
+ # start_time = time - (60 * 60 * 24) # -1 day
235
+ # end_time = time
236
+ # active_entities = TwitterAds::LineItem.active_entities(
237
+ # account,
238
+ # line_item_ids: %w(exrfs),
239
+ # start_time: start_time,
240
+ # end_time: end_time,
241
+ # utc_offset: utc_offset,
242
+ # granularity: :day)
243
+ #
244
+ # @param account [Account] The Account object instance.
245
+ # @param entity [String] The entity type to retrieve data for.
246
+ # @param start_time [Time] Scopes the retrieved data to the specified start time.
247
+ # @param end_time [Time] Scopes the retrieved data to the specified end time.
248
+ # @option opts [Array] :campaign_ids A collection of IDs to be fetched.
249
+ # @option opts [Array] :funding_instrument_ids A collection of IDs to be fetched.
250
+ # @option opts [Array] :line_item_ids A collection of IDs to be fetched.
251
+ #
252
+ # @return A list of entity details.
253
+ #
254
+ # @see https://developer.twitter.com/en/docs/ads/analytics/api-reference/active-entities
255
+
212
256
  def active_entities(account, start_time:, end_time:, **opts)
213
- entity_type = name
257
+ entity = opts.fetch(:entity, name)
214
258
  granularity = opts.fetch(:granularity, nil)
215
259
  start_utc_offset = opts[:start_utc_offset] || opts[:utc_offset]
216
260
  end_utc_offset = opts[:end_utc_offset] || opts[:utc_offset]
217
261
 
218
- if entity_type == 'OrganicTweet'
219
- raise "'OrganicTweet' not support with 'active_entities'"
262
+ if entity == 'OrganicTweet'
263
+ raise "'OrganicTweet' is not supported with 'active_entities'"
220
264
  end
221
265
 
222
266
  params = {
223
- entity: ANALYTICS_MAP[entity_type],
267
+ entity: ANALYTICS_MAP[entity],
224
268
  start_time: TwitterAds::Utils.to_time(start_time, granularity, start_utc_offset),
225
269
  end_time: TwitterAds::Utils.to_time(end_time, granularity, end_utc_offset)
226
- }.merge!(opts)
270
+ }
271
+
272
+ opts.each { |k, v|
273
+ params[k] = if v.instance_of?(Array)
274
+ v.join(',')
275
+ else
276
+ v
277
+ end
278
+ }
227
279
 
228
280
  resource = self::RESOURCE_ACTIVE_ENTITIES % { account_id: account.id }
229
281
  response = Request.new(account.client, :get, resource, params: params).perform