facebook_ads 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +18 -5
  4. data/facebook_ads.gemspec +2 -2
  5. data/lib/facebook_ads.rb +10 -1
  6. data/lib/facebook_ads/ad.rb +11 -0
  7. data/lib/facebook_ads/ad_account.rb +44 -2
  8. data/lib/facebook_ads/ad_campaign.rb +12 -3
  9. data/lib/facebook_ads/ad_creative.rb +17 -5
  10. data/lib/facebook_ads/ad_set.rb +2 -2
  11. data/lib/facebook_ads/ad_targeting.rb +16 -5
  12. data/spec/facebook_ads/ad_account_spec.rb +20 -0
  13. data/spec/facebook_ads/ad_campaign_spec.rb +42 -2
  14. data/spec/facebook_ads/ad_targeting_spec.rb +20 -0
  15. data/spec/facebook_ads/facebook_ads_spec.rb +16 -5
  16. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_ad_campaigns/lists_campaigns.yml +3 -3
  17. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_ad_creatives/lists_creatives.yml +3 -3
  18. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_ad_images/lists_images.yml +3 -3
  19. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_ad_sets/lists_ad_sets.yml +3 -3
  20. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_ads/lists_ads.yml +3 -3
  21. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_all/lists_all_accounts.yml +3 -3
  22. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_create_ad_campaign/creates_a_new_ad_campaign.yml +6 -6
  23. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_create_ad_creative/creates_carousel_ad_creative.yml +7301 -0
  24. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_create_ad_images/creates_an_image.yml +7 -7
  25. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_find_by/finds_a_specific_account.yml +3 -3
  26. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_reach_estimate/estimates_the_reach_of_a_targeting_spec.yml +3 -3
  27. data/spec/fixtures/cassettes/FacebookAds_AdCampaign/_create_ad_set/creates_valid_ad_set.yml +114 -0
  28. data/spec/fixtures/cassettes/FacebookAds_AdCampaign/_destroy/{creates_a_new_ad_campaign.yml → sets_effective_status_to_deleted.yml} +9 -9
  29. data/spec/support/facebook_ads.rb +1 -0
  30. data/spec/support/vcr.rb +3 -0
  31. metadata +8 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7450bd7b5cbf0923d6fd31c81789dbb4a41c6a6e
4
- data.tar.gz: 4a96fdf4cd1940418b414a4b35f1d9dd16b7faf5
3
+ metadata.gz: a57756116ad7b5abac5c48ce3ac5d6f249f069e1
4
+ data.tar.gz: 5f4577a436b3e3b3099802a05eb4cfe1f2ec6280
5
5
  SHA512:
6
- metadata.gz: 9aea51760cc15a61efcb50b2ba823162b56d6f5a7426efb803627913d586cc050405dede1c737e2f47e34ed34431bde4bb1d81cd0f26f4d4d8701aa6c859cd9d
7
- data.tar.gz: a4835643fae536365837cf1071b5d6df228317aca5dbc28e15d1d82a51ec65f1ae733dcca4bfee00b86d5ba6fbe83c327516a35e13e39feb8e32b3c5bd9511bb
6
+ metadata.gz: c1bfa9b506e7321f4647f9ed61d7e1031a0f72543e13386ab21c1715edd098574f8c466394dba4e5a52338ef7f87330cc25052b3c7d8010970efa9a2a2db264b
7
+ data.tar.gz: 27f49b9e0bd2c6f09138a43f984b9b766b771320aa3ec155f933d9c67e3fd13e4d758c620eacf19e3a922ac9d1463dc801a19cc888fa881ee2ae393101304b68
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
 
8
8
  /test_access_token
9
9
  /test_business_id
10
+ .idea
data/README.md CHANGED
@@ -36,10 +36,10 @@ FacebookAds.app_secret = '[YOUR_APP_SECRET]'
36
36
 
37
37
  ## API Version
38
38
 
39
- This gem currently uses v2.8 of the Marketing API (soon to be updated to 2.9). You can change the version as desired with the following:
39
+ This gem currently uses v2.9 of the Marketing API (2.10 is released as of 7/18/2017). You can change the version as desired with the following:
40
40
 
41
41
  ```ruby
42
- FacebookAds.base_uri = 'https://graph.facebook.com/v2.9'
42
+ FacebookAds.base_uri = 'https://graph.facebook.com/v2.10'
43
43
  ```
44
44
 
45
45
  ## Console
@@ -210,7 +210,7 @@ carousel_ad_creative = account.create_ad_creative({
210
210
  call_to_action_type: 'SHOP_NOW',
211
211
  multi_share_optimized: true,
212
212
  multi_share_end_card: false
213
- }, carousel: true)
213
+ }, creative_type: 'carousel')
214
214
  ```
215
215
  See FacebookAds::AdCreative::CALL_TO_ACTION_TYPES for a list of all call to action types.
216
216
 
@@ -224,8 +224,20 @@ image_ad_creative = account.create_ad_creative({
224
224
  link_title: 'A link title.',
225
225
  image_hash: ad_images.first.hash,
226
226
  call_to_action_type: 'SHOP_NOW'
227
- }, carousel: false)
227
+ }, creative_type: 'image')
228
228
  ```
229
+
230
+ Create a single creative for a web link:
231
+ ```ruby
232
+ image_ad_creative = account.create_ad_creative({
233
+ title: 'Test Link Title',
234
+ body: 'Link Description Text',
235
+ object_url: 'www.example.com/my-ad-link',
236
+ link_url: 'www.example.com/my-ad-link',
237
+ image_hash: ad_images.first.hash,
238
+ }, creative_type: 'link')
239
+ ```
240
+
229
241
  The options will be different depending on the thing being advertised (Android app, iOS app or website).
230
242
 
231
243
  Find a creative by ID:
@@ -300,7 +312,8 @@ ad_set = campaign.create_ad_set(
300
312
  optimization_goal: 'OFFSITE_CONVERSIONS',
301
313
  daily_budget: 500, # This is in cents, so the daily budget here is $5.
302
314
  billing_event: 'IMPRESSIONS',
303
- status: 'PAUSED'
315
+ status: 'PAUSED',
316
+ is_autobid: true
304
317
  )
305
318
  ```
306
319
  See FacebookAds::AdSet::OPTIMIZATION_GOALS for a list of all optimization goals.
data/facebook_ads.gemspec CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  # To publish the next version:
4
4
  # gem build facebook_ads.gemspec
5
- # gem push facebook_ads-0.4.gem
5
+ # gem push facebook_ads-0.5.gem
6
6
  Gem::Specification.new do |s|
7
7
  s.name = 'facebook_ads'
8
- s.version = '0.4'
8
+ s.version = '0.5'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.licenses = ['MIT']
11
11
  s.authors = ['Chris Estreich']
data/lib/facebook_ads.rb CHANGED
@@ -29,10 +29,19 @@ module FacebookAds
29
29
  end
30
30
 
31
31
  def self.base_uri
32
- @base_uri = 'https://graph.facebook.com/v2.8' unless defined?(@base_uri)
32
+ @base_uri = "https://graph.facebook.com/v#{api_version}" unless defined?(@base_uri)
33
33
  @base_uri
34
34
  end
35
35
 
36
+ def self.api_version=(api_version)
37
+ @api_version = api_version
38
+ end
39
+
40
+ def self.api_version
41
+ @api_version = '2.9' unless defined?(@api_version)
42
+ @api_version
43
+ end
44
+
36
45
  def self.access_token=(access_token)
37
46
  @access_token = access_token
38
47
  end
@@ -28,5 +28,16 @@ module FacebookAds
28
28
  def ad_creative
29
29
  @ad_creative ||= AdCreative.find(creative['id'])
30
30
  end
31
+
32
+ # has_many ad_insights
33
+
34
+ def ad_insights(range: Date.today..Date.today, level: 'ad', time_increment: 1)
35
+ query = {
36
+ level: level,
37
+ time_increment: time_increment,
38
+ time_range: { 'since': range.first.to_s, 'until': range.last.to_s }
39
+ }
40
+ AdInsight.paginate("/#{id}/insights", query: query)
41
+ end
31
42
  end
32
43
  end
@@ -66,8 +66,19 @@ module FacebookAds
66
66
  AdCreative.paginate("/#{id}/adcreatives", query: { limit: limit })
67
67
  end
68
68
 
69
- def create_ad_creative(creative, carousel: true)
70
- carousel ? create_carousel_ad_creative(creative) : create_image_ad_creative(creative)
69
+ def create_ad_creative(creative, creative_type: nil, carousel: false)
70
+ # Support old deprecated carousel param
71
+ return create_carousel_ad_creative(creative) if carousel
72
+ case creative_type
73
+ when 'carousel'
74
+ create_carousel_ad_creative(creative)
75
+ when 'link'
76
+ create_link_ad_creative(creative)
77
+ when 'image'
78
+ create_image_ad_creative(creative)
79
+ else
80
+ create_image_ad_creative(creative)
81
+ end
71
82
  end
72
83
 
73
84
  # has_many ad_sets
@@ -115,6 +126,25 @@ module FacebookAds
115
126
  self.class.get("/#{id}/reachestimate", query: query, objectify: false)
116
127
  end
117
128
 
129
+ def delivery_estimate(targeting:, optimization_goal:, currency: 'USD')
130
+ raise Exception, "Optimization goal must be one of: #{AdSet::OPTIMIZATION_GOALS.join(', ')}" unless AdSet::OPTIMIZATION_GOALS.include?(optimization_goal)
131
+
132
+ if targeting.is_a?(AdTargeting)
133
+ if targeting.validate!
134
+ targeting = targeting.to_hash
135
+ else
136
+ raise Exception, 'The provided targeting spec is not valid.'
137
+ end
138
+ end
139
+
140
+ query = {
141
+ targeting_spec: targeting.to_json,
142
+ optimization_goal: optimization_goal,
143
+ currency: currency
144
+ }
145
+ self.class.get("/#{id}/delivery_estimate", query: query, objectify: false)
146
+ end
147
+
118
148
  # has_many applications
119
149
 
120
150
  def applications
@@ -179,6 +209,18 @@ module FacebookAds
179
209
  AdCreative.find(result['id'])
180
210
  end
181
211
 
212
+ def create_link_ad_creative(creative)
213
+ required = %i[name title body object_url link_url image_hash]
214
+
215
+ unless (keys = required - creative.keys).length.zero?
216
+ raise Exception, "Creative is missing the following: #{keys.join(', ')}"
217
+ end
218
+
219
+ query = AdCreative.link(creative)
220
+ result = AdCreative.post("/#{id}/adcreatives", query: query)
221
+ AdCreative.find(result['id'])
222
+ end
223
+
182
224
  def download(url)
183
225
  pathname = Pathname.new(url)
184
226
  name = "#{pathname.dirname.basename}.jpg"
@@ -60,7 +60,7 @@ module FacebookAds
60
60
  AdSet.paginate("/#{id}/adsets", query: { effective_status: effective_status, limit: limit })
61
61
  end
62
62
 
63
- def create_ad_set(name:, promoted_object:, targeting:, daily_budget:, optimization_goal:, billing_event: 'IMPRESSIONS', status: 'ACTIVE', is_autobid: nil, bid_amount: nil)
63
+ def create_ad_set(name:, promoted_object: {}, targeting:, daily_budget: nil, lifetime_budget: nil, end_time: nil, optimization_goal:, billing_event: 'IMPRESSIONS', status: 'ACTIVE', is_autobid: nil, bid_amount: nil)
64
64
  raise Exception, "Optimization goal must be one of: #{AdSet::OPTIMIZATION_GOALS.join(', ')}" unless AdSet::OPTIMIZATION_GOALS.include?(optimization_goal)
65
65
  raise Exception, "Billing event must be one of: #{AdSet::BILLING_EVENTS.join(', ')}" unless AdSet::BILLING_EVENTS.include?(billing_event)
66
66
 
@@ -77,12 +77,21 @@ module FacebookAds
77
77
  targeting: targeting.to_json,
78
78
  promoted_object: promoted_object.to_json,
79
79
  optimization_goal: optimization_goal,
80
- daily_budget: daily_budget,
81
80
  billing_event: billing_event,
82
81
  status: status,
83
82
  is_autobid: is_autobid,
84
83
  bid_amount: bid_amount
85
84
  }
85
+
86
+ if daily_budget && lifetime_budget
87
+ raise Exception 'Only one budget may be set between daily_budget and life_budget'
88
+ elsif daily_budget
89
+ query[:daily_budget] = daily_budget
90
+ elsif lifetime_budget
91
+ query[:lifetime_budget] = lifetime_budget
92
+ query[:end_time] = end_time
93
+ end
94
+
86
95
  result = AdSet.post("/act_#{account_id}/adsets", query: query)
87
96
  AdSet.find(result['id'])
88
97
  end
@@ -93,7 +102,7 @@ module FacebookAds
93
102
  query = {
94
103
  level: level,
95
104
  time_increment: time_increment,
96
- time_range: { 'since': range.first.to_s, 'until': range.last.to_s }
105
+ time_range: { since: range.first.to_s, until: range.last.to_s }
97
106
  }
98
107
  AdInsight.paginate("/#{id}/insights", query: query)
99
108
  end
@@ -2,15 +2,17 @@ module FacebookAds
2
2
  # Ad ad creative has many ad images and belongs to an ad account.
3
3
  # https://developers.facebook.com/docs/marketing-api/reference/ad-creative
4
4
  class AdCreative < Base
5
- FIELDS = %w[id name object_story_id object_story_spec object_type thumbnail_url run_status].freeze
6
- CALL_TO_ACTION_TYPES = %w[SHOP_NOW INSTALL_MOBILE_APP USE_MOBILE_APP SIGN_UP DOWNLOAD BUY_NOW].freeze
5
+ FIELDS = %w[id name object_story_id object_story_spec object_type thumbnail_url].freeze
6
+ CALL_TO_ACTION_TYPES = %w[SHOP_NOW INSTALL_MOBILE_APP USE_MOBILE_APP SIGN_UP DOWNLOAD BUY_NOW NO_BUTTON].freeze
7
7
 
8
8
  class << self
9
- def photo(name:, page_id:, instagram_actor_id: nil, message:, link:, app_link: nil, link_title:, image_hash:, call_to_action_type:)
9
+ def photo(name:, page_id:, instagram_actor_id: nil, message:, link:, app_link: nil, link_title:, image_hash:, call_to_action_type:, link_description: nil)
10
10
  object_story_spec = {
11
11
  'page_id' => page_id, # 300664329976860
12
12
  'instagram_actor_id' => instagram_actor_id, # 503391023081924
13
13
  'link_data' => {
14
+ 'name' => link_title,
15
+ 'description' => link_description,
14
16
  'link' => link, # https://tophatter.com/, https://itunes.apple.com/app/id619460348, http://play.google.com/store/apps/details?id=com.tophatter
15
17
  'message' => message,
16
18
  'image_hash' => image_hash,
@@ -19,8 +21,7 @@ module FacebookAds
19
21
  'value' => {
20
22
  # 'application' =>,
21
23
  'link' => link,
22
- 'app_link' => app_link,
23
- 'link_title' => link_title
24
+ 'app_link' => app_link
24
25
  }
25
26
  }
26
27
  }
@@ -89,6 +90,17 @@ module FacebookAds
89
90
  product_set_id: product_set_id
90
91
  }
91
92
  end
93
+
94
+ def link(name:, title:, body:, object_url:, link_url:, image_hash:)
95
+ {
96
+ name: name,
97
+ title: title,
98
+ body: body,
99
+ object_url: object_url,
100
+ link_url: link_url,
101
+ image_hash: image_hash
102
+ }
103
+ end
92
104
  end
93
105
  end
94
106
  end
@@ -72,8 +72,8 @@ module FacebookAds
72
72
  Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
73
73
  end
74
74
 
75
- def create_ad(name:, creative_id:)
76
- query = { name: name, adset_id: id, creative: { creative_id: creative_id }.to_json }
75
+ def create_ad(name:, creative_id:, status: 'PAUSED')
76
+ query = { name: name, adset_id: id, creative: { creative_id: creative_id }.to_json, status: status }
77
77
  result = Ad.post("/act_#{account_id}/ads", query: query)
78
78
  Ad.find(result['id'])
79
79
  end
@@ -14,20 +14,30 @@ module FacebookAds
14
14
  NOT_INSTALLED = 'not_installed'.freeze
15
15
  APP_INSTALL_STATES = [INSTALLED, NOT_INSTALLED].freeze
16
16
 
17
- attr_accessor :genders, :age_min, :age_max, :countries, :user_os, :user_device, :app_install_state
17
+ attr_accessor :genders, :age_min, :age_max, :countries, :user_os, :user_device, :app_install_state, :custom_locations, :income
18
18
 
19
19
  def initialize
20
20
  # self.genders = [WOMEN] # If nil, defaults to all genders.
21
21
  # self.age_min = 18 # If nil, defaults to 18.
22
22
  # self.age_max = 65 # If nil, defaults to 65+.
23
- self.countries = ['US']
24
23
  # self.user_os = [ANDROID_OS]
25
24
  # self.user_device = ANDROID_DEVICES
26
25
  # self.app_install_state = NOT_INSTALLED
26
+ self.income = [] # An a rray of objects with 'id' and optional 'name'
27
+ end
28
+
29
+ def geo_locations
30
+ if custom_locations
31
+ { custom_locations: custom_locations }
32
+ elsif countries
33
+ { countries: countries }
34
+ else
35
+ { countries: ['US'] }
36
+ end
27
37
  end
28
38
 
29
39
  def validate!
30
- { gender: genders, countries: countries, user_os: user_os, user_device: user_device }.each_pair do |key, array|
40
+ { gender: genders, countries: countries, user_os: user_os, user_device: user_device, custom_locations: custom_locations }.each_pair do |key, array|
31
41
  if !array.nil? && !array.is_a?(Array)
32
42
  raise Exception, "#{self.class.name}: #{key} must be an array"
33
43
  end
@@ -49,10 +59,11 @@ module FacebookAds
49
59
  genders: genders,
50
60
  age_min: age_min,
51
61
  age_max: age_max,
52
- geo_locations: { countries: countries },
62
+ geo_locations: geo_locations,
53
63
  user_os: user_os,
54
64
  user_device: user_device,
55
- app_install_state: app_install_state
65
+ app_install_state: app_install_state,
66
+ income: income
56
67
  }.reject { |_k, v| v.nil? }
57
68
  end
58
69
  end
@@ -65,6 +65,26 @@ describe FacebookAds::AdAccount do
65
65
  end
66
66
  end
67
67
 
68
+ describe '.create_ad_creative' do
69
+ it 'creates carousel ad creative', :vcr do
70
+ ad_images = account.create_ad_images(%w[https://img0.etsystatic.com/108/1/13006112/il_570xN.1047856494_l2gp.jpg https://img1.etsystatic.com/143/0/13344107/il_570xN.1141249285_xciv.jpg])
71
+ carousel_ad_creative = account.create_ad_creative({
72
+ name: 'Test Carousel Creative',
73
+ page_id: '300664329976860', # Add your Facebook Page ID here.
74
+ link: 'http://play.google.com/store/apps/details?id=com.tophatter', # Add your Play Store ID here.
75
+ message: 'A message.',
76
+ assets: [
77
+ { hash: ad_images[0].hash, title: 'Image #1 Title' },
78
+ { hash: ad_images[1].hash, title: 'Image #2 Title' }
79
+ ],
80
+ call_to_action_type: 'SHOP_NOW',
81
+ multi_share_optimized: true,
82
+ multi_share_end_card: false
83
+ }, creative_type: 'carousel')
84
+ expect(carousel_ad_creative.id).to eq('120330000008134415')
85
+ end
86
+ end
87
+
68
88
  describe '.ad_sets' do
69
89
  it 'lists ad sets', :vcr do
70
90
  ad_sets = account.ad_sets
@@ -2,14 +2,54 @@ require 'spec_helper'
2
2
 
3
3
  # FACEBOOK_ACCESS_TOKEN=... rspec spec/facebook_ads/ad_campaign_spec.rb
4
4
  describe FacebookAds::AdCampaign do
5
+ let(:campaign) do
6
+ FacebookAds::AdCampaign.new(
7
+ id: '120330000008134915',
8
+ account_id: '10152335766987003',
9
+ buying_type: 'AUCTION',
10
+ can_use_spend_cap: true,
11
+ configured_status: 'PAUSED',
12
+ created_time: '2017-08-25T15:47:51-0700',
13
+ effective_status: 'PAUSED',
14
+ name: 'Test Campaign',
15
+ objective: 'CONVERSIONS',
16
+ start_time: '1969-12-31T15:59:59-0800',
17
+ updated_time: '2017-08-25T15:47:51-0700'
18
+ )
19
+ end
20
+
21
+ let(:targeting) do
22
+ targeting = FacebookAds::AdTargeting.new
23
+ targeting.genders = [FacebookAds::AdTargeting::WOMEN]
24
+ targeting.age_min = 29
25
+ targeting.age_max = 65
26
+ targeting.countries = ['US']
27
+ targeting.user_os = [FacebookAds::AdTargeting::ANDROID_OS]
28
+ targeting.user_device = FacebookAds::AdTargeting::ANDROID_DEVICES
29
+ targeting.app_install_state = FacebookAds::AdTargeting::NOT_INSTALLED
30
+ targeting
31
+ end
32
+
5
33
  xdescribe '.ad_sets' do
6
34
  end
7
35
 
8
- xdescribe '.create_ad_set' do
36
+ describe '.create_ad_set' do
37
+ it 'creates_valid_ad_set', :vcr do
38
+ ad_set = campaign.create_ad_set(
39
+ name: 'Test Ad Set',
40
+ targeting: targeting,
41
+ optimization_goal: 'IMPRESSIONS',
42
+ daily_budget: 500, # This is in cents, so the daily budget here is $5.
43
+ billing_event: 'IMPRESSIONS',
44
+ status: 'PAUSED',
45
+ is_autobid: true
46
+ )
47
+ expect(ad_set.id).to eq('120330000008135715')
48
+ end
9
49
  end
10
50
 
11
51
  describe '.destroy' do
12
- it 'creates a new ad campaign', :vcr do
52
+ it 'sets effective status to deleted', :vcr do
13
53
  ad_campaign = FacebookAds::AdCampaign.find('6076262142242')
14
54
  expect(ad_campaign.destroy).to be(true)
15
55
  ad_campaign = FacebookAds::AdCampaign.find(ad_campaign.id)
@@ -1,4 +1,24 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe FacebookAds::AdTargeting do
4
+ let(:targeting) { FacebookAds::AdTargeting.new }
5
+
6
+ describe '#geo_locations' do
7
+ let(:custom_locations) { [{ radius: 10, distance_unit: 'mile', address_string: '1601 Willow Road, Menlo Park, CA 94025' }] }
8
+ let(:countries) { ['JP'] }
9
+
10
+ it 'should return custom locations if specified' do
11
+ targeting.custom_locations = custom_locations
12
+ expect(targeting.geo_locations).to eq(custom_locations: custom_locations)
13
+ end
14
+
15
+ it 'should return countries if specified' do
16
+ targeting.countries = countries
17
+ expect(targeting.geo_locations).to eq(countries: countries)
18
+ end
19
+
20
+ it 'should default to US if nothing is specified' do
21
+ expect(targeting.geo_locations).to eq(countries: ['US'])
22
+ end
23
+ end
4
24
  end