twitter-ads 5.2.0 → 6.0.0

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 (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
@@ -20,7 +20,14 @@ module TwitterAds
20
20
  # @return [self] A fully hydrated instance of the current class.
21
21
  #
22
22
  # @since 0.1.0
23
- def from_response(object)
23
+ def from_response(object, headers = nil)
24
+ if !headers.nil?
25
+ TwitterAds::Utils.extract_response_headers(headers).each { |key, value|
26
+ singleton_class.class_eval { attr_accessor key }
27
+ instance_variable_set("@#{key}", value)
28
+ }
29
+ end
30
+
24
31
  self.class.properties.each do |name, type|
25
32
  value = nil
26
33
  if type == :time && object[name] && !object[name].empty?
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2019 Twitter, Inc.
3
+
4
+ module TwitterRestApi
5
+ class UserIdLookup
6
+ include TwitterAds::DSL
7
+ include TwitterAds::Resource
8
+
9
+ attr_reader :account
10
+
11
+ property :id, read_only: true
12
+ property :id_str, read_only: true
13
+ property :screen_name, read_only: true
14
+
15
+ DOMAIN = 'https://api.twitter.com'
16
+ RESOURCE = '/1.1/users/show.json'
17
+
18
+ def self.load(account, opts = {})
19
+ response = TwitterAds::Request.new(
20
+ account.client,
21
+ :get,
22
+ RESOURCE,
23
+ params: opts,
24
+ domain: DOMAIN
25
+ ).perform
26
+ new.from_response(response.body, response.headers)
27
+ end
28
+ end
29
+ end
@@ -41,12 +41,24 @@ module TwitterAds
41
41
  property :to_delete, type: :bool
42
42
 
43
43
  RESOURCE = "/#{TwitterAds::API_VERSION}/" \
44
- 'accounts/%{account_id}/user_settings/%{id}' # @api private
44
+ 'accounts/%{account_id}/tax_settings' # @api private
45
45
 
46
46
  def initialize(account)
47
47
  @account = account
48
48
  self
49
49
  end
50
50
 
51
+ def self.load(account)
52
+ resource = RESOURCE % { account_id: account.id }
53
+ response = Request.new(account.client, :get, resource).perform
54
+ new(account).from_response(response.body[:data])
55
+ end
56
+
57
+ def save
58
+ resource = RESOURCE % { account_id: account.id }
59
+ params = to_params
60
+ response = Request.new(account.client, :put, resource, params: params).perform
61
+ from_response(response.body[:data])
62
+ end
51
63
  end
52
64
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2019 Twitter, Inc.
3
+
4
+ module TwitterAds
5
+ class Conversation
6
+
7
+ include TwitterAds::DSL
8
+ include TwitterAds::Resource
9
+
10
+ property :name, read_only: true
11
+ property :targeting_type, read_only: true
12
+ property :targeting_value, read_only: true
13
+ property :conversation_type, read_only: true
14
+
15
+ RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \
16
+ 'targeting_criteria/conversations' # @api private
17
+
18
+ def initialize(account)
19
+ @account = account
20
+ self
21
+ end
22
+ end
23
+ end
@@ -70,6 +70,29 @@ module TwitterAds
70
70
  warn message
71
71
  end
72
72
 
73
+ def extract_response_headers(headers)
74
+ values = {}
75
+ # only get "X-${name}" custom response headers
76
+ headers.each { |key, value|
77
+ if key =~ /^x-/
78
+ values[key.gsub(/^x-/, '').tr('-', '_')] = \
79
+ value.first =~ /^[0-9]*$/ ? value.first.to_i : value.first
80
+ end
81
+ }
82
+ values
83
+ end
84
+
85
+ def flatten_params(args)
86
+ params = args
87
+ params.each { |key, value|
88
+ if value.is_a?(Array)
89
+ next if value.empty?
90
+ params[key] = value.join(',')
91
+ end
92
+ }
93
+ params
94
+ end
95
+
73
96
  end
74
97
 
75
98
  end
@@ -2,5 +2,5 @@
2
2
  # Copyright (C) 2019 Twitter, Inc.
3
3
 
4
4
  module TwitterAds
5
- VERSION = '5.2.0'
5
+ VERSION = '6.0.0'
6
6
  end
@@ -0,0 +1,23 @@
1
+ {
2
+ "data_type": "tweet_previews",
3
+ "request": {
4
+ "params": {
5
+ "tweet_ids": [
6
+ "1130942781109596160",
7
+ "1101254234031370240"
8
+ ],
9
+ "tweet_type": "PUBLISHED",
10
+ "account_id": "2iqph"
11
+ }
12
+ },
13
+ "data": [
14
+ {
15
+ "tweet_id": "1130942781109596160",
16
+ "preview": "<iframe class='tweet-preview' src='https://ton.smf1.twitter.com/ads-manager/tweet-preview/index.html?data=c29tZSByYW5kb20gYmFzZTY0IHN0cmluZ3MgaGVyZS4uLg=='>"
17
+ },
18
+ {
19
+ "tweet_id": "1101254234031370240",
20
+ "preview": "<iframe class='tweet-preview' src='https://ton.smf1.twitter.com/ads-manager/tweet-preview/index.html?data=c29tZSByYW5kb20gYmFzZTY0IHN0cmluZ3MgaGVyZS4uLg=='>"
21
+ }
22
+ ]
23
+ }
@@ -31,7 +31,6 @@ describe TwitterAds::TargetingCriteria do
31
31
  targeting_type
32
32
  targeting_value
33
33
  tailored_audience_expansion
34
- tailored_audience_type
35
34
  )
36
35
 
37
36
  include_examples 'object property check', read, write
@@ -21,63 +21,4 @@ describe TwitterAds::Tweet do
21
21
 
22
22
  let(:account) { client.accounts.first }
23
23
 
24
- describe '#tweet_preview' do
25
-
26
- let!(:resource_collection) { "#{ADS_API}/accounts/#{account.id}/tweet/preview" }
27
-
28
- before(:each) do
29
- stub_fixture(:get, :tweet_preview, /#{resource_collection}.*/)
30
- end
31
-
32
- context 'with an existing tweet id' do
33
-
34
- it 'successfully returns a preview of the specified tweet' do
35
- params = { id: 634798319504617472 }
36
- result = subject.preview(account, params)
37
- expect(result.size).not_to be_nil
38
- expect(result).to all(include(:platform, :preview))
39
- end
40
-
41
- end
42
-
43
- context 'when previewing a new tweet' do
44
-
45
- it 'url encodes the status content' do
46
- params = { text: 'Hello World!', card_id: '19v69' }
47
- expect(URI).to receive(:escape).at_least(:once).and_call_original
48
- result = subject.preview(account, params)
49
- expect(result.size).not_to be_nil
50
- expect(result).to all(include(:platform, :preview))
51
- end
52
-
53
- it 'allows a single value for the media_ids param' do
54
- resource = "/#{TwitterAds::API_VERSION}/accounts/#{account.id}/tweet/preview"
55
- expected = { text: 'Hello%20World!', media_ids: 634458428836962304 }
56
-
57
- expect(TwitterAds::Request).to receive(:new).with(
58
- account.client, :get, resource, params: expected).and_call_original
59
-
60
- params = { text: 'Hello World!', media_ids: 634458428836962304 }
61
- result = subject.preview(account, params)
62
- expect(result.size).not_to be_nil
63
- expect(result).to all(include(:platform, :preview))
64
- end
65
-
66
- it 'allows an array of values for the media_ids param' do
67
- resource = "/#{TwitterAds::API_VERSION}/accounts/#{account.id}/tweet/preview"
68
- expected = { text: 'Hello%20World!', media_ids: '634458428836962304,634458428836962305' }
69
-
70
- expect(TwitterAds::Request).to receive(:new).with(
71
- account.client, :get, resource, params: expected).and_call_original
72
-
73
- params = { text: 'Hello World!', media_ids: [634458428836962304, 634458428836962305] }
74
- result = subject.preview(account, params)
75
- expect(result.size).not_to be_nil
76
- expect(result).to all(include(:platform, :preview))
77
- end
78
-
79
- end
80
-
81
- end
82
-
83
24
  end
@@ -55,10 +55,26 @@ describe TwitterAds::Client do
55
55
 
56
56
  it 'allows additional options' do
57
57
  expect {
58
- Client.new(consumer_key, consumer_secret, access_token, {})
58
+ Client.new(consumer_key, consumer_secret, access_token, access_token_secret, options: {})
59
59
  }.not_to raise_error
60
60
  end
61
61
 
62
+ it 'test client options' do
63
+ client = Client.new(
64
+ consumer_key,
65
+ consumer_secret,
66
+ access_token,
67
+ access_token_secret,
68
+ options: {
69
+ handle_rate_limit: true,
70
+ retry_max: 1,
71
+ retry_delay: 3000,
72
+ retry_on_status: [404, 500, 503]
73
+ }
74
+ )
75
+ expect(client.options.length).to eq 4
76
+ end
77
+
62
78
  end
63
79
 
64
80
  describe '#inspect' do
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2019 Twitter, Inc.
3
+
4
+ require 'spec_helper'
5
+
6
+ include TwitterAds::Enum
7
+
8
+ describe TwitterAds::Creative::TweetPreview do
9
+
10
+ let!(:resource) { "#{ADS_API}/accounts/2iqph/tweet_previews" }
11
+
12
+ before(:each) do
13
+ stub_fixture(:get, :accounts_load, "#{ADS_API}/accounts/2iqph")
14
+ stub_fixture(:get, :tweet_previews, /#{resource}\?.*/)
15
+ end
16
+
17
+ let(:client) do
18
+ Client.new(
19
+ Faker::Lorem.characters(40),
20
+ Faker::Lorem.characters(40),
21
+ Faker::Lorem.characters(40),
22
+ Faker::Lorem.characters(40)
23
+ )
24
+ end
25
+
26
+ let(:account) { client.accounts('2iqph') }
27
+ let(:instance) { described_class.new(account) }
28
+
29
+ it 'inspect TweetPreview.load() response' do
30
+ preview = instance.load(
31
+ account,
32
+ tweet_ids: %w(1130942781109596160 1101254234031370240),
33
+ tweet_type: TweetType::PUBLISHED)
34
+
35
+ expect(preview).to be_instance_of(Cursor)
36
+ expect(preview.count).to eq 2
37
+ tweet = preview.first
38
+ expect(tweet.tweet_id).to eq '1130942781109596160'
39
+ end
40
+
41
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2019 Twitter, Inc.
3
+
4
+ require 'spec_helper'
5
+
6
+ describe TwitterAds::Campaign do
7
+
8
+ before(:each) do
9
+ allow_any_instance_of(Object).to receive(:sleep)
10
+ stub_fixture(:get, :accounts_load, "#{ADS_API}/accounts/2iqph")
11
+ end
12
+
13
+ let(:client) do
14
+ Client.new(
15
+ Faker::Lorem.characters(40),
16
+ Faker::Lorem.characters(40),
17
+ Faker::Lorem.characters(40),
18
+ Faker::Lorem.characters(40)
19
+ )
20
+ end
21
+ let(:account) { client_simple.accounts('2iqph') }
22
+
23
+ let(:client_simple) do
24
+ Client.new(
25
+ Faker::Lorem.characters(40),
26
+ Faker::Lorem.characters(40),
27
+ Faker::Lorem.characters(40),
28
+ Faker::Lorem.characters(40),
29
+ options: {
30
+ handle_rate_limit: true
31
+ }
32
+ )
33
+ end
34
+ let(:account1) { client_simple.accounts('2iqph') }
35
+
36
+ let(:client_combine) do
37
+ Client.new(
38
+ Faker::Lorem.characters(40),
39
+ Faker::Lorem.characters(40),
40
+ Faker::Lorem.characters(40),
41
+ Faker::Lorem.characters(40),
42
+ options: {
43
+ handle_rate_limit: true,
44
+ retry_max: 1,
45
+ retry_delay: 3000,
46
+ retry_on_status: [500]
47
+ }
48
+ )
49
+ end
50
+ let(:account2) { client_combine.accounts('2iqph') }
51
+
52
+ it 'test rate-limit handle - success' do
53
+ stub = stub_request(:get, "#{ADS_API}/accounts/2iqph/campaigns").to_return(
54
+ {
55
+ body: fixture(:campaigns_all),
56
+ status: 429,
57
+ headers: {
58
+ 'x-account-rate-limit-limit': 10000,
59
+ 'x-account-rate-limit-remaining': 9999,
60
+ 'x-account-rate-limit-reset': Time.now.to_i + 5
61
+ }
62
+ },
63
+ {
64
+ body: fixture(:campaigns_all),
65
+ status: 200
66
+ }
67
+ )
68
+ cusor = described_class.all(account1)
69
+ expect(cusor).to be_instance_of(Cursor)
70
+ expect(stub).to have_been_requested.times(2)
71
+ end
72
+
73
+ it 'test rate-limit handle - fail' do
74
+ stub = stub_request(:get, "#{ADS_API}/accounts/2iqph/campaigns").to_return(
75
+ {
76
+ body: fixture(:campaigns_all),
77
+ status: 429,
78
+ headers: {
79
+ 'x-account-rate-limit-limit': 10000,
80
+ 'x-account-rate-limit-remaining': 9999,
81
+ 'x-account-rate-limit-reset': Time.now.to_i + 5
82
+ }
83
+ },
84
+ {
85
+ body: fixture(:campaigns_all),
86
+ status: 429,
87
+ headers: {
88
+ 'x-account-rate-limit-limit': 10000,
89
+ 'x-account-rate-limit-remaining': 9999,
90
+ 'x-account-rate-limit-reset': 4102444800
91
+ }
92
+ }
93
+ )
94
+ begin
95
+ cusor = described_class.all(account1)
96
+ rescue RateLimit => e
97
+ cusor = e
98
+ end
99
+ expect(cusor).to be_instance_of(RateLimit)
100
+ expect(stub).to have_been_requested.times(2)
101
+ expect(cusor.reset_at).to eq 4102444800
102
+ end
103
+
104
+ it 'test rate-limit handle with retry - case 1' do
105
+ # scenario:
106
+ # - 500 (retry) -> 429 (handle rate limit) -> 200 (end)
107
+ stub = stub_request(:get, "#{ADS_API}/accounts/2iqph/campaigns").to_return(
108
+ {
109
+ body: fixture(:campaigns_all),
110
+ status: 500,
111
+ headers: {
112
+ 'x-account-rate-limit-limit': 10000,
113
+ 'x-account-rate-limit-remaining': 9999,
114
+ 'x-account-rate-limit-reset': 4102444800
115
+ }
116
+ },
117
+ {
118
+ body: fixture(:campaigns_all),
119
+ status: 429,
120
+ headers: {
121
+ 'x-account-rate-limit-limit': 10000,
122
+ 'x-account-rate-limit-remaining': 9999,
123
+ 'x-account-rate-limit-reset': Time.now.to_i + 5
124
+ }
125
+ },
126
+ {
127
+ body: fixture(:campaigns_all),
128
+ status: 200
129
+ }
130
+ )
131
+ cusor = described_class.all(account2)
132
+ expect(cusor).to be_instance_of(Cursor)
133
+ expect(stub).to have_been_requested.times(3)
134
+ end
135
+
136
+ it 'test rate-limit handle with retry - case 2' do
137
+ # scenario:
138
+ # - 429 (handle rate limit) -> 500 (retry) -> 200 (end)
139
+ stub = stub_request(:get, "#{ADS_API}/accounts/2iqph/campaigns").to_return(
140
+ {
141
+ body: fixture(:campaigns_all),
142
+ status: 429,
143
+ headers: {
144
+ 'x-account-rate-limit-limit': 10000,
145
+ 'x-account-rate-limit-remaining': 9999,
146
+ 'x-account-rate-limit-reset': Time.now.to_i + 5
147
+ }
148
+ },
149
+ {
150
+ body: fixture(:campaigns_all),
151
+ status: 500,
152
+ headers: {
153
+ 'x-account-rate-limit-limit': 10000,
154
+ 'x-account-rate-limit-remaining': 9999,
155
+ 'x-account-rate-limit-reset': 4102444800
156
+ }
157
+ },
158
+ {
159
+ body: fixture(:campaigns_all),
160
+ status: 200
161
+ }
162
+ )
163
+ cusor = described_class.all(account2)
164
+ expect(cusor).to be_instance_of(Cursor)
165
+ expect(stub).to have_been_requested.times(3)
166
+ end
167
+
168
+ it 'test rate-limit header access from cusor class' do
169
+ stub_request(:get, "#{ADS_API}/accounts/2iqph/campaigns").to_return(
170
+ {
171
+ body: fixture(:campaigns_all),
172
+ status: 200,
173
+ headers: {
174
+ 'x-account-rate-limit-limit': 10000,
175
+ 'x-account-rate-limit-remaining': 9999,
176
+ 'x-account-rate-limit-reset': 4102444800
177
+ }
178
+ }
179
+ )
180
+ cusor = described_class.all(account)
181
+ expect(cusor).to be_instance_of(Cursor)
182
+ expect(cusor.account_rate_limit_limit).to eq 10000
183
+ expect(cusor.account_rate_limit_remaining).to eq 9999
184
+ expect(cusor.account_rate_limit_reset).to eq 4102444800
185
+ end
186
+
187
+ it 'test rate-limit header access from resource class' do
188
+ stub_request(:any, /accounts\/2iqph\/campaigns\/2wap7/).to_return(
189
+ {
190
+ body: fixture(:campaigns_load),
191
+ status: 200,
192
+ headers: {
193
+ 'x-account-rate-limit-limit': 10000,
194
+ 'x-account-rate-limit-remaining': 9999,
195
+ 'x-account-rate-limit-reset': 4102444800
196
+ }
197
+ }
198
+ )
199
+
200
+ campaign = described_class.load(account, '2wap7')
201
+ resource = "/#{TwitterAds::API_VERSION}/accounts/2iqph/campaigns/2wap7"
202
+ params = {}
203
+ response = TwitterAds::Request.new(client, :get, resource, params: params).perform
204
+ data = campaign.from_response(response.body[:data], response.headers)
205
+ expect(data).to be_instance_of(Campaign)
206
+ expect(data.account_rate_limit_limit).to eq 10000
207
+ expect(data.account_rate_limit_remaining).to eq 9999
208
+ expect(data.account_rate_limit_reset).to eq 4102444800
209
+ end
210
+
211
+ it 'test rate-limit header access check instance variables not conflicting' do
212
+ stub_request(:get, "#{ADS_API}/accounts/2iqph/campaigns").to_return(
213
+ {
214
+ body: fixture(:campaigns_all),
215
+ status: 200,
216
+ headers: {
217
+ 'x-account-rate-limit-limit': 10000,
218
+ 'x-account-rate-limit-remaining': 9999,
219
+ 'x-account-rate-limit-reset': 4102444800
220
+ }
221
+ },
222
+ {
223
+ body: fixture(:campaigns_all),
224
+ status: 200,
225
+ headers: {
226
+ 'x-account-rate-limit-limit': 10000,
227
+ 'x-account-rate-limit-remaining': 9998,
228
+ 'x-account-rate-limit-reset': 4102444800
229
+ }
230
+ }
231
+ )
232
+
233
+ campaign_first = described_class.all(account)
234
+ campaign_second = described_class.all(account)
235
+
236
+ expect(campaign_first).to be_instance_of(Cursor)
237
+ expect(campaign_first.account_rate_limit_limit).to eq 10000
238
+ expect(campaign_first.account_rate_limit_remaining).to eq 9999
239
+ expect(campaign_first.account_rate_limit_reset).to eq 4102444800
240
+
241
+ expect(campaign_second).to be_instance_of(Cursor)
242
+ expect(campaign_second.account_rate_limit_limit).to eq 10000
243
+ expect(campaign_second.account_rate_limit_remaining).to eq 9998
244
+ expect(campaign_second.account_rate_limit_reset).to eq 4102444800
245
+ end
246
+
247
+ end