facebook_ads 0.4 → 0.5

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 (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