facebook_ads 0.6.3 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +1 -4
  4. data/CHANGELOG.md +3 -0
  5. data/Gemfile +3 -1
  6. data/README.md +4 -4
  7. data/Rakefile +2 -0
  8. data/bin/{console → facebook_ads_console} +1 -0
  9. data/facebook_ads.gemspec +3 -2
  10. data/lib/facebook_ads.rb +4 -6
  11. data/lib/facebook_ads/account.rb +13 -0
  12. data/lib/facebook_ads/ad.rb +2 -0
  13. data/lib/facebook_ads/ad_account.rb +45 -46
  14. data/lib/facebook_ads/ad_audience.rb +2 -0
  15. data/lib/facebook_ads/ad_campaign.rb +7 -4
  16. data/lib/facebook_ads/ad_creative.rb +2 -0
  17. data/lib/facebook_ads/ad_exception.rb +2 -0
  18. data/lib/facebook_ads/ad_image.rb +6 -5
  19. data/lib/facebook_ads/ad_insight.rb +2 -0
  20. data/lib/facebook_ads/ad_product.rb +2 -0
  21. data/lib/facebook_ads/ad_product_catalog.rb +2 -8
  22. data/lib/facebook_ads/ad_product_feed.rb +2 -0
  23. data/lib/facebook_ads/ad_product_set.rb +2 -0
  24. data/lib/facebook_ads/ad_set.rb +54 -30
  25. data/lib/facebook_ads/ad_targeting.rb +6 -4
  26. data/lib/facebook_ads/ad_user.rb +15 -0
  27. data/lib/facebook_ads/advertisable_application.rb +14 -0
  28. data/lib/facebook_ads/base.rb +37 -11
  29. data/spec/facebook_ads/ad_account_spec.rb +2 -0
  30. data/spec/facebook_ads/ad_campaign_spec.rb +2 -0
  31. data/spec/facebook_ads/ad_creative_spec.rb +2 -0
  32. data/spec/facebook_ads/ad_image_spec.rb +2 -0
  33. data/spec/facebook_ads/ad_insight_spec.rb +2 -0
  34. data/spec/facebook_ads/ad_product_catalog_spec.rb +2 -0
  35. data/spec/facebook_ads/ad_product_feed_spec.rb +2 -0
  36. data/spec/facebook_ads/ad_product_set_spec.rb +2 -0
  37. data/spec/facebook_ads/ad_product_spec.rb +2 -0
  38. data/spec/facebook_ads/ad_set_spec.rb +2 -0
  39. data/spec/facebook_ads/ad_spec.rb +2 -0
  40. data/spec/facebook_ads/ad_targeting_spec.rb +2 -0
  41. data/spec/facebook_ads/facebook_ads_spec.rb +2 -0
  42. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_ad_campaigns/lists_campaigns.yml +1 -1
  43. data/spec/fixtures/cassettes/FacebookAds_AdAccount/_create_ad_campaign/creates_a_new_ad_campaign.yml +1 -1
  44. data/spec/fixtures/cassettes/FacebookAds_AdCampaign/_destroy/sets_effective_status_to_deleted.yml +2 -2
  45. data/spec/spec_helper.rb +2 -0
  46. data/spec/support/approvals.rb +2 -0
  47. data/spec/support/facebook_ads.rb +2 -0
  48. data/spec/support/rest_client.rb +3 -5
  49. data/spec/support/vcr.rb +2 -0
  50. metadata +8 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 103ab340673c4ae87c97e894fa6f6c2dbe3a02c5
4
- data.tar.gz: 8db58b179cd4f38150e630b9e2edc87dd5d47890
3
+ metadata.gz: 35a61cb9e965397a7e50da136ce697ed6ea3437e
4
+ data.tar.gz: ce5c09cf7acd43f39b0aa8f82d87d3a21962d50e
5
5
  SHA512:
6
- metadata.gz: 12c9e8bd1be33b94c4cbd339dc149376d30daf1fd6731bfea6e2621343b2d168224c79194c9f59e42d3cb20ebe04889666e9846d2606266a62ffd54622f30460
7
- data.tar.gz: 3766749dcfa84ea4c87b47a36340f7095eb9b49660b23abc90d630230f9f8b6144d24e5823a90fec1a1a823d716240391311d3dbb2dc0cafa63f41d58c55afad
6
+ metadata.gz: 1ad4ac70297f77209589c34323fd9af60d5721e8824cd2b3380e9a320c89de7124b45dd7a1133be489a362a631d35a873e00f059e10c2b9835231eae7f41f9c1
7
+ data.tar.gz: 365dda2ba4b84cdc4d7d86ca95ba1ccfdfd399dbc34664c2aa800cbba44b2cd41d3e181989653b03dc7821a9071dda4ef96f1b25f947aa71b7fd3861e1d8c236
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.2
2
+ TargetRubyVersion: 2.3
3
3
 
4
4
  Metrics/AbcSize:
5
5
  Enabled: false
@@ -22,17 +22,12 @@ Metrics/ParameterLists:
22
22
  Lint/EndAlignment:
23
23
  Enabled: false
24
24
 
25
- Style/Documentation:
25
+ Layout/IndentationWidth:
26
26
  Enabled: false
27
- Style/IndentationWidth:
27
+ Layout/ElseAlignment:
28
28
  Enabled: false
29
- Style/ElseAlignment:
29
+
30
+ Style/Documentation:
30
31
  Enabled: false
31
32
  Style/GuardClause:
32
33
  Enabled: false
33
- Style/NumericLiterals:
34
- Enabled: false
35
-
36
- # This is temporary - it can be remove after updating rubocop to the latest.
37
- Style/PercentLiteralDelimiters:
38
- Enabled: false
@@ -1,9 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2
4
- # branches:
5
- # only:
6
- # - master
3
+ - 2.3
7
4
  script:
8
5
  - bundle exec rubocop
9
6
  - bundle exec rake
@@ -1,3 +1,6 @@
1
+ ## 0.6.4 (2018-10-03)
2
+ - Expose `budget_remaining`, `daily_budget`, `lifetime_budget` for Ad Campaigns. These are needed when we have budgets at the Ad Campaign level and not at the Ad Set level.
3
+
1
4
  ## 0.6.3 (2018-05-17)
2
5
  - Replaced deprecated `is_autobid` with new `bid_strategy` field
3
6
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
  gemspec
3
5
 
@@ -5,7 +7,7 @@ group :development, :test do
5
7
  gem 'awesome_print'
6
8
  gem 'pry'
7
9
  gem 'rake'
8
- gem 'rubocop', '0.49.0'
10
+ gem 'rubocop', '0.49.1'
9
11
  end
10
12
 
11
13
  group :test do
data/README.md CHANGED
@@ -49,7 +49,7 @@ It reads the Access Token from a file called test_access_token.
49
49
 
50
50
  ```bash
51
51
  echo [YOUR_ACCESS_TOKEN] > test_access_token
52
- bin/console
52
+ bin/facebook_ads_console
53
53
  ```
54
54
 
55
55
  ## Usage Examples
@@ -230,9 +230,9 @@ image_ad_creative = account.create_ad_creative({
230
230
  Create a single creative for a web link:
231
231
  ```ruby
232
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',
233
+ title: 'Test Link Title',
234
+ body: 'Link Description Text',
235
+ object_url: 'www.example.com/my-ad-link',
236
236
  link_url: 'www.example.com/my-ad-link',
237
237
  image_hash: ad_images.first.hash,
238
238
  }, creative_type: 'link')
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/core/rake_task'
2
4
  RSpec::Core::RakeTask.new(:spec)
3
5
  task default: :spec
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'facebook_ads'
@@ -1,11 +1,12 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  # To publish the next version:
4
5
  # gem build facebook_ads.gemspec
5
- # gem push facebook_ads-0.6.2.gem
6
+ # gem push facebook_ads-0.6.4.gem
6
7
  Gem::Specification.new do |s|
7
8
  s.name = 'facebook_ads'
8
- s.version = '0.6.3'
9
+ s.version = '0.6.4'
9
10
  s.platform = Gem::Platform::RUBY
10
11
  s.licenses = ['MIT']
11
12
  s.authors = ['Chris Estreich']
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # External requires.
2
4
  require 'json'
3
5
  require 'rest-client'
@@ -38,7 +40,7 @@ module FacebookAds
38
40
  end
39
41
 
40
42
  def self.api_version
41
- @api_version = '2.9' unless defined?(@api_version)
43
+ @api_version = '3.1' unless defined?(@api_version)
42
44
  @api_version
43
45
  end
44
46
 
@@ -59,11 +61,7 @@ module FacebookAds
59
61
  end
60
62
 
61
63
  def self.appsecret_proof
62
- OpenSSL::HMAC.hexdigest(
63
- OpenSSL::Digest.new('sha256'),
64
- @app_secret,
65
- @access_token
66
- )
64
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @app_secret, @access_token)
67
65
  end
68
66
 
69
67
  def self.business_id=(business_id)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FacebookAds
4
+ class Account < Base
5
+ FIELDS = %w[].freeze
6
+
7
+ class << self
8
+ def all(query = {})
9
+ get('/me/accounts', query: query, objectify: true)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # An ad belongs to an ad set. It is created using an ad creative.
3
5
  # https://developers.facebook.com/docs/marketing-api/reference/adgroup
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
- # An ad account has many ad campaigns, ad images, and ad creatives.
3
4
  # https://developers.facebook.com/docs/marketing-api/reference/ad-account
4
5
  class AdAccount < Base
5
6
  FIELDS = %w[id account_id account_status age created_time currency name].freeze
@@ -8,17 +9,21 @@ module FacebookAds
8
9
  def all(query = {})
9
10
  get('/me/adaccounts', query: query, objectify: true)
10
11
  end
12
+ end
11
13
 
12
- def find_by(conditions)
13
- all.detect do |object|
14
- conditions.all? do |key, value|
15
- object.send(key) == value
16
- end
17
- end
18
- end
14
+ # AdvertisableApplication
15
+
16
+ def advertisable_applications(limit: 100)
17
+ AdvertisableApplication.paginate("/#{id}/advertisable_applications", query: { limit: limit })
18
+ end
19
+
20
+ # AdUser
21
+
22
+ def ad_users(limit: 100)
23
+ AdCampaign.paginate("/#{id}/users", query: { limit: limit })
19
24
  end
20
25
 
21
- # has_many campaigns
26
+ # AdCampaign
22
27
 
23
28
  def ad_campaigns(effective_status: ['ACTIVE'], limit: 100)
24
29
  AdCampaign.paginate("/#{id}/campaigns", query: { effective_status: effective_status, limit: limit })
@@ -39,7 +44,19 @@ module FacebookAds
39
44
  AdCampaign.find(result['id'])
40
45
  end
41
46
 
42
- # has_many ad_images
47
+ # AdSet
48
+
49
+ def ad_sets(effective_status: ['ACTIVE'], limit: 100)
50
+ AdSet.paginate("/#{id}/adsets", query: { effective_status: effective_status, limit: limit })
51
+ end
52
+
53
+ # Ad
54
+
55
+ def ads(effective_status: ['ACTIVE'], limit: 100)
56
+ Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
57
+ end
58
+
59
+ # AdImage
43
60
 
44
61
  def ad_images(hashes: nil, limit: 100)
45
62
  if !hashes.nil?
@@ -60,7 +77,7 @@ module FacebookAds
60
77
  !response['images'].nil? ? ad_images(hashes: response['images'].map { |_key, hash| hash['hash'] }) : []
61
78
  end
62
79
 
63
- # has_many ad_creatives
80
+ # AdCreative
64
81
 
65
82
  def ad_creatives(limit: 100)
66
83
  AdCreative.paginate("/#{id}/adcreatives", query: { limit: limit })
@@ -81,25 +98,27 @@ module FacebookAds
81
98
  end
82
99
  end
83
100
 
84
- # has_many ad_sets
85
-
86
- def ad_sets(effective_status: ['ACTIVE'], limit: 100)
87
- AdSet.paginate("/#{id}/adsets", query: { effective_status: effective_status, limit: limit })
88
- end
89
-
90
- # has_many ads
101
+ # AdAudience
91
102
 
92
- def ads(effective_status: ['ACTIVE'], limit: 100)
93
- Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
103
+ def ad_audiences(limit: 100)
104
+ AdAudience.paginate("/#{id}/customaudiences", query: { limit: limit })
94
105
  end
95
106
 
96
- # has_many ad_audiences
107
+ def create_ad_audience_with_pixel(name:, pixel_id:, event_name:, subtype: 'WEBSITE', retention_days: 15)
108
+ query = {
109
+ name: name,
110
+ pixel_id: pixel_id,
111
+ subtype: subtype,
112
+ retention_days: retention_days,
113
+ rule: { event: { i_contains: event_name } }.to_json,
114
+ prefill: 1
115
+ }
97
116
 
98
- def ad_audiences
99
- AdAudience.paginate("/#{id}/customaudiences")
117
+ result = AdAudience.post("/#{id}/customaudiences", query: query)
118
+ AdAudience.find(result['id'])
100
119
  end
101
120
 
102
- # has_many ad_insights
121
+ # AdInsight
103
122
 
104
123
  def ad_insights(range: Date.today..Date.today, level: 'ad', time_increment: 1)
105
124
  ad_campaigns.map do |ad_campaign|
@@ -123,6 +142,7 @@ module FacebookAds
123
142
  optimize_for: optimization_goal,
124
143
  currency: currency
125
144
  }
145
+
126
146
  self.class.get("/#{id}/reachestimate", query: query, objectify: false)
127
147
  end
128
148
 
@@ -142,29 +162,8 @@ module FacebookAds
142
162
  optimization_goal: optimization_goal,
143
163
  currency: currency
144
164
  }
145
- self.class.get("/#{id}/delivery_estimate", query: query, objectify: false)
146
- end
147
-
148
- # has_many applications
149
-
150
- def applications
151
- self.class.get("/#{id}/advertisable_applications", objectify: false)
152
- end
153
-
154
- # hash_many ad_audiences
155
-
156
- def create_ad_audience_with_pixel(name:, pixel_id:, event_name:, subtype: 'WEBSITE', retention_days: 15)
157
- query = {
158
- name: name,
159
- pixel_id: pixel_id,
160
- subtype: subtype,
161
- retention_days: retention_days,
162
- rule: { event: { i_contains: event_name } }.to_json,
163
- prefill: 1
164
- }
165
165
 
166
- result = AdAudience.post("/#{id}/customaudiences", query: query)
167
- AdAudience.find(result['id'])
166
+ self.class.get("/#{id}/delivery_estimate", query: query, objectify: false)
168
167
  end
169
168
 
170
169
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # https://developers.facebook.com/docs/marketing-api/reference/custom-audience
3
5
  class AdAudience < Base
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
- # An ad campaign has many ad sets and belongs to an ad account.
3
4
  # https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group
4
5
  class AdCampaign < Base
5
6
  FIELDS = %w[
@@ -15,6 +16,7 @@ module FacebookAds
15
16
  start_time
16
17
  stop_time
17
18
  updated_time spend_cap
19
+ budget_remaining daily_budget lifetime_budget
18
20
  ].freeze
19
21
 
20
22
  STATUSES = %w[
@@ -45,13 +47,13 @@ module FacebookAds
45
47
  VIDEO_VIEWS
46
48
  ].freeze
47
49
 
48
- # belongs_to ad_account
50
+ # AdAccount
49
51
 
50
52
  def ad_account
51
53
  @ad_account ||= AdAccount.find("act_#{account_id}")
52
54
  end
53
55
 
54
- # has_many ad_sets
56
+ # AdSet
55
57
 
56
58
  def ad_sets(effective_status: ['ACTIVE'], limit: 100)
57
59
  AdSet.paginate("/#{id}/adsets", query: { effective_status: effective_status, limit: limit })
@@ -94,7 +96,7 @@ module FacebookAds
94
96
  AdSet.find(result['id'])
95
97
  end
96
98
 
97
- # has_many ad_insights
99
+ # AdInsight
98
100
 
99
101
  def ad_insights(range: Date.today..Date.today, level: 'ad', time_increment: 1)
100
102
  query = {
@@ -102,6 +104,7 @@ module FacebookAds
102
104
  time_increment: time_increment,
103
105
  time_range: { since: range.first.to_s, until: range.last.to_s }
104
106
  }
107
+
105
108
  AdInsight.paginate("/#{id}/insights", query: query)
106
109
  end
107
110
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # Ad ad creative has many ad images and belongs to an ad account.
3
5
  # https://developers.facebook.com/docs/marketing-api/reference/ad-creative
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  class AdException < StandardError
3
5
  attr_reader :code, :title, :message
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
- # An ad image belongs to an ad account.
3
4
  # An image will always produce the same hash.
4
5
  # https://developers.facebook.com/docs/marketing-api/reference/ad-image
5
6
  class AdImage < Base
@@ -11,10 +12,10 @@ module FacebookAds
11
12
  end
12
13
  end
13
14
 
14
- # @TODO:
15
- # You are setting a key that conflicts with a built-in method FacebookAds::AdImage#hash defined in Hash.
16
- # This can cause unexpected behavior when accessing the key via as a property.
17
- # You can still access the key via the #[] method.
15
+ # @FIXME: You are setting a key that conflicts with a built-in method
16
+ # FacebookAds::AdImage#hash defined in Hash.
17
+ disable_warnings
18
+
18
19
  def hash
19
20
  self[:hash]
20
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # Ad insights exist for ad accounts, ad campaigns, ad sets, and ads.
3
5
  # A lot more can be done here.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # https://developers.facebook.com/docs/marketing-api/reference/product-item
3
5
  class AdProduct < Base
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # https://developers.facebook.com/docs/marketing-api/reference/product-catalog
3
5
  class AdProductCatalog < Base
@@ -8,14 +10,6 @@ module FacebookAds
8
10
  get("/#{FacebookAds.business_id}/owned_product_catalogs", query: query, objectify: true)
9
11
  end
10
12
 
11
- def find_by(conditions)
12
- all.detect do |object|
13
- conditions.all? do |key, value|
14
- object.send(key) == value
15
- end
16
- end
17
- end
18
-
19
13
  def create(name:)
20
14
  query = { name: name }
21
15
  result = post("/#{FacebookAds.business_id}/owned_product_catalogs", query: query)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # https://developers.facebook.com/docs/marketing-api/reference/product-feed
3
5
  class AdProductFeed < Base
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # https://developers.facebook.com/docs/marketing-api/reference/product-set
3
5
  class AdProductSet < Base
@@ -1,38 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
- # An ad set belongs to a campaign and has many ads.
3
- # https://developers.facebook.com/docs/marketing-api/reference/ad-campaign
4
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-account/adsets
4
5
  class AdSet < Base
5
- # Full set of fields:
6
- # FIELDS = %w(
7
- # id account_id adlabels adset_schedule attribution_window_days
8
- # bid_amount bid_info billing_event budget_remaining
9
- # campaign campaign_id configured_status created_time
10
- # creative_sequence daily_budget effective_status end_time
11
- # frequency_cap frequency_cap_reset_period
12
- # frequency_control_specs instagram_actor_id
13
- # bid_strategy lifetime_budget
14
- # lifetime_frequency_cap lifetime_imps name optimization_goal
15
- # pacing_type promoted_object recommendations
16
- # recurring_budget_semantics rf_prediction_id rtb_flag
17
- # start_time status targeting time_based_ad_rotation_id_blocks
18
- # time_based_ad_rotation_intervals updated_time
19
- # use_new_app_click
20
- # ).freeze
21
-
22
- # Fields we might actually care about:
23
6
  FIELDS = %w[
24
- id account_id campaign_id
7
+ id
8
+ account_id
9
+ campaign_id
25
10
  name
26
- status configured_status effective_status
27
- bid_strategy bid_amount billing_event optimization_goal pacing_type
28
- daily_budget budget_remaining lifetime_budget
11
+ status
12
+ configured_status
13
+ effective_status
14
+ bid_strategy
15
+ bid_amount
16
+ billing_event
17
+ optimization_goal
18
+ pacing_type
19
+ daily_budget
20
+ budget_remaining
21
+ lifetime_budget
29
22
  promoted_object
30
23
  targeting
31
- created_time updated_time
24
+ created_time
25
+ updated_time
32
26
  ].freeze
33
27
 
34
- STATUSES = %w[ACTIVE PAUSED DELETED PENDING_REVIEW DISAPPROVED PREAPPROVED PENDING_BILLING_INFO CAMPAIGN_PAUSED ARCHIVED ADSET_PAUSED].freeze
35
- BILLING_EVENTS = %w[APP_INSTALLS IMPRESSIONS].freeze
28
+ STATUSES = %w[
29
+ ACTIVE
30
+ PAUSED
31
+ DELETED
32
+ PENDING_REVIEW
33
+ DISAPPROVED
34
+ PREAPPROVED
35
+ PENDING_BILLING_INFO
36
+ CAMPAIGN_PAUSED
37
+ ARCHIVED
38
+ ADSET_PAUSED
39
+ ].freeze
40
+
41
+ BILLING_EVENTS = %w[APP_INSTALLS IMPRESSIONS].freeze
42
+
36
43
  OPTIMIZATION_GOALS = %w[
37
44
  NONE
38
45
  APP_INSTALLS
@@ -55,25 +62,26 @@ module FacebookAds
55
62
  APP_DOWNLOADS
56
63
  LANDING_PAGE_VIEWS
57
64
  ].freeze
65
+
58
66
  BID_STRATEGIES = %w[
59
67
  LOWEST_COST_WITHOUT_CAP
60
68
  LOWEST_COST_WITH_BID_CAP
61
69
  TARGET_COST
62
70
  ].freeze
63
71
 
64
- # belongs_to ad_account
72
+ # AdAccount
65
73
 
66
74
  def ad_account
67
75
  @ad_account ||= AdAccount.find("act_#{account_id}")
68
76
  end
69
77
 
70
- # belongs_to ad_campaign
78
+ # AdCampaign
71
79
 
72
80
  def ad_campaign
73
81
  @campaign ||= AdCampaign.find(campaign_id)
74
82
  end
75
83
 
76
- # has_many ads
84
+ # Ad
77
85
 
78
86
  def ads(effective_status: ['ACTIVE'], limit: 100)
79
87
  Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
@@ -84,5 +92,21 @@ module FacebookAds
84
92
  result = Ad.post("/act_#{account_id}/ads", query: query)
85
93
  Ad.find(result['id'])
86
94
  end
95
+
96
+ # AdInsight
97
+
98
+ # levels = enum {ad, adset, campaign, account}
99
+ # breakdowns = list<enum {..., product_id, ...}>
100
+ def ad_insights(range: Date.today..Date.today, level: nil, breakdowns: [], fields: [], limit: 1_000)
101
+ query = {
102
+ time_range: { since: range.first.to_s, until: range.last.to_s },
103
+ level: level,
104
+ breakdowns: breakdowns&.join(','),
105
+ fields: fields&.join(','),
106
+ limit: limit
107
+ }.reject { |_key, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
108
+
109
+ AdInsight.paginate("/#{id}/insights", query: query)
110
+ end
87
111
  end
88
112
  end
@@ -1,17 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # https://developers.facebook.com/docs/marketing-api/targeting-specs
3
5
  class AdTargeting
4
6
  MEN = 1
5
7
  WOMEN = 2
6
8
  GENDERS = [MEN, WOMEN].freeze
7
- ANDROID_OS = 'Android'.freeze
8
- APPLE_OS = 'iOS'.freeze
9
+ ANDROID_OS = 'Android'
10
+ APPLE_OS = 'iOS'
9
11
  OSES = [ANDROID_OS, APPLE_OS].freeze
10
12
  ANDROID_DEVICES = %w[Android_Smartphone Android_Tablet].freeze
11
13
  APPLE_DEVICES = %w[iPhone iPad iPod].freeze
12
14
  DEVICES = ANDROID_DEVICES + APPLE_DEVICES
13
- INSTALLED = 'installed'.freeze
14
- NOT_INSTALLED = 'not_installed'.freeze
15
+ INSTALLED = 'installed'
16
+ NOT_INSTALLED = 'not_installed'
15
17
  APP_INSTALL_STATES = [INSTALLED, NOT_INSTALLED].freeze
16
18
 
17
19
  attr_accessor :genders, :age_min, :age_max, :countries, :user_os, :user_device, :app_install_state, :custom_locations, :income
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FacebookAds
4
+ # An ad user belongs to an ad account.
5
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group
6
+ class AdUser < Base
7
+ FIELDS = %w[].freeze
8
+
9
+ # belongs_to ad_account
10
+
11
+ def ad_account
12
+ @ad_account ||= AdAccount.find("act_#{account_id}")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FacebookAds
4
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-account/advertisable_applications
5
+ class AdvertisableApplication < Base
6
+ FIELDS = %w[].freeze
7
+
8
+ # belongs_to ad_account
9
+
10
+ def ad_account
11
+ @ad_account ||= AdAccount.find("act_#{account_id}")
12
+ end
13
+ end
14
+ end
@@ -1,20 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FacebookAds
2
4
  # The base class for all ads objects.
3
5
  class Base < Hashie::Mash
4
6
  class << self
5
- def find(id)
6
- get("/#{id}", objectify: true)
7
- end
7
+ # HTTP verbs.
8
8
 
9
9
  def get(path, query: {}, objectify:)
10
- query = pack(query, objectify: objectify) # Adds access token, fields, etc.
10
+ query = pack(query, objectify: objectify)
11
11
  uri = "#{FacebookAds.base_uri}#{path}?" + build_nested_query(query)
12
12
  FacebookAds.logger.debug "GET #{uri}"
13
+
13
14
  response = begin
14
15
  RestClient.get(uri, accept: :json, accept_encoding: :identity)
15
16
  rescue RestClient::Exception => e
16
17
  exception(:get, path, e)
17
18
  end
19
+
18
20
  unpack(response, objectify: objectify)
19
21
  end
20
22
 
@@ -22,11 +24,13 @@ module FacebookAds
22
24
  query = pack(query, objectify: false)
23
25
  uri = "#{FacebookAds.base_uri}#{path}"
24
26
  FacebookAds.logger.debug "POST #{uri} #{query}"
27
+
25
28
  response = begin
26
29
  RestClient.post(uri, query)
27
30
  rescue RestClient::Exception => e
28
31
  exception(:post, path, e)
29
32
  end
33
+
30
34
  unpack(response, objectify: false)
31
35
  end
32
36
 
@@ -34,28 +38,50 @@ module FacebookAds
34
38
  query = pack(query, objectify: false)
35
39
  uri = "#{FacebookAds.base_uri}#{path}?" + build_nested_query(query)
36
40
  FacebookAds.logger.debug "DELETE #{uri}"
41
+
37
42
  response = begin
38
43
  RestClient.delete(uri)
39
44
  rescue RestClient::Exception => e
40
45
  exception(:delete, path, e)
41
46
  end
47
+
42
48
  unpack(response, objectify: false)
43
49
  end
44
50
 
51
+ # Common idioms.
52
+
53
+ def all(_query = {})
54
+ raise StandardError, 'Subclass must implement `all`.'
55
+ end
56
+
57
+ def find(id)
58
+ get("/#{id}", objectify: true)
59
+ end
60
+
61
+ def find_by(conditions)
62
+ all.detect do |object|
63
+ conditions.all? do |key, value|
64
+ object.send(key) == value
65
+ end
66
+ end
67
+ end
68
+
45
69
  def paginate(path, query: {})
46
- query[:limit] ||= 100
47
- limit = query[:limit]
48
- response = get(path, query: query.merge(fields: self::FIELDS.join(',')), objectify: false)
49
- data = response['data'].nil? ? [] : response['data']
70
+ query = query.merge(limit: 100) unless query[:limit]
71
+ query = query.merge(fields: self::FIELDS.join(',')) unless query[:fields] || self::FIELDS.empty?
72
+ response = get(path, query: query, objectify: false)
73
+ data = response['data'].nil? ? [] : response['data']
50
74
 
51
- if data.length == limit
75
+ if data.length == query[:limit]
52
76
  while !(paging = response['paging']).nil? && !(url = paging['next']).nil?
53
77
  FacebookAds.logger.debug "GET #{url}"
78
+
54
79
  response = begin
55
80
  RestClient.get(url)
56
81
  rescue RestClient::Exception => e
57
82
  exception(:get, url, e)
58
83
  end
84
+
59
85
  response = unpack(response, objectify: false)
60
86
  data += response['data'] unless response['data'].nil?
61
87
  end
@@ -85,8 +111,8 @@ module FacebookAds
85
111
  def pack(hash, objectify:)
86
112
  hash = hash.merge(access_token: FacebookAds.access_token)
87
113
  hash = hash.merge(appsecret_proof: FacebookAds.appsecret_proof) if FacebookAds.app_secret
88
- hash = hash.merge(fields: self::FIELDS.join(',')) if objectify
89
- hash.delete_if { |_k, v| v.nil? }
114
+ hash = hash.merge(fields: self::FIELDS.join(',')) if objectify && !hash[:fields] && !self::FIELDS.empty?
115
+ hash.reject { |_key, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
90
116
  end
91
117
 
92
118
  def unpack(response, objectify:)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  # FACEBOOK_ACCESS_TOKEN=... rspec spec/facebook_ads/ad_account_spec.rb
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  # FACEBOOK_ACCESS_TOKEN=... rspec spec/facebook_ads/ad_campaign_spec.rb
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdCreative do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdImage do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdInsight do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdProductCatalog do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdProductFeed do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdProductSet do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdProduct do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdSet do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::Ad do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe FacebookAds::AdTargeting do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  # rspec spec/facebook_ads/facebook_ads_spec.rb
@@ -2,7 +2,7 @@
2
2
  http_interactions:
3
3
  - request:
4
4
  method: get
5
- uri: https://graph.facebook.com/v<api_version>/act_10152335766987003/campaigns?access_token=<access_token>&appsecret_proof=<appsecret_proof>&effective_status%5B%5D=ACTIVE&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap&limit=100
5
+ uri: https://graph.facebook.com/v<api_version>/act_10152335766987003/campaigns?access_token=<access_token>&appsecret_proof=<appsecret_proof>&effective_status%5B%5D=ACTIVE&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap,budget_remaining,daily_budget,lifetime_budget&limit=100
6
6
  body:
7
7
  encoding: US-ASCII
8
8
  string: ''
@@ -61,7 +61,7 @@ http_interactions:
61
61
  recorded_at: Wed, 26 Apr 2017 00:48:41 GMT
62
62
  - request:
63
63
  method: get
64
- uri: https://graph.facebook.com/v<api_version>/6076262393242?access_token=<access_token>&appsecret_proof=<appsecret_proof>&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap
64
+ uri: https://graph.facebook.com/v<api_version>/6076262393242?access_token=<access_token>&appsecret_proof=<appsecret_proof>&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap,budget_remaining,daily_budget,lifetime_budget
65
65
  body:
66
66
  encoding: US-ASCII
67
67
  string: ''
@@ -2,7 +2,7 @@
2
2
  http_interactions:
3
3
  - request:
4
4
  method: get
5
- uri: https://graph.facebook.com/v<api_version>/6076262142242?access_token=<access_token>&appsecret_proof=<appsecret_proof>&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap
5
+ uri: https://graph.facebook.com/v<api_version>/6076262142242?access_token=<access_token>&appsecret_proof=<appsecret_proof>&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap,budget_remaining,daily_budget,lifetime_budget
6
6
  body:
7
7
  encoding: US-ASCII
8
8
  string: ''
@@ -110,7 +110,7 @@ http_interactions:
110
110
  recorded_at: Wed, 26 Apr 2017 00:52:20 GMT
111
111
  - request:
112
112
  method: get
113
- uri: https://graph.facebook.com/v<api_version>/6076262142242?access_token=<access_token>&appsecret_proof=<appsecret_proof>&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap
113
+ uri: https://graph.facebook.com/v<api_version>/6076262142242?access_token=<access_token>&appsecret_proof=<appsecret_proof>&fields=id,account_id,buying_type,can_use_spend_cap,configured_status,created_time,effective_status,name,objective,start_time,stop_time,updated_time,spend_cap,budget_remaining,daily_budget,lifetime_budget
114
114
  body:
115
115
  encoding: US-ASCII
116
116
  string: ''
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'coveralls'
2
4
  Coveralls.wear!
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'approvals/rspec'
2
4
 
3
5
  Approvals.configure do |c|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  FacebookAds.business_id = '760977220612233'
2
4
  FacebookAds.access_token = ENV['FACEBOOK_ACCESS_TOKEN'] || 'fake-facebook-access-token'
3
5
  FacebookAds.app_secret = ENV['FACEBOOK_APP_SECRET'] || 'fake-facebook-app-secret'
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Force the request headers to only accept JSON, and disable gzip.
2
4
  # This makes our VCR cassettes human readable.
3
5
  module RestClient
4
6
  class Request
5
7
  def default_headers
6
- {
7
- accept: 'application/json',
8
- accept_encoding: 'identity',
9
- user_agent: RestClient::Platform.default_user_agent
10
- }
8
+ { accept: 'application/json', accept_encoding: 'identity' }
11
9
  end
12
10
  end
13
11
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'vcr'
2
4
 
3
5
  VCR.configure do |c|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: facebook_ads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Estreich
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-30 00:00:00.000000000 Z
11
+ date: 2018-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -42,7 +42,7 @@ description: This gem allows to easily manage your Facebook ads via Facebook's M
42
42
  API in ruby.
43
43
  email: chris@tophatter.com
44
44
  executables:
45
- - console
45
+ - facebook_ads_console
46
46
  extensions: []
47
47
  extra_rdoc_files:
48
48
  - README.md
@@ -57,9 +57,10 @@ files:
57
57
  - LICENSE.md
58
58
  - README.md
59
59
  - Rakefile
60
- - bin/console
60
+ - bin/facebook_ads_console
61
61
  - facebook_ads.gemspec
62
62
  - lib/facebook_ads.rb
63
+ - lib/facebook_ads/account.rb
63
64
  - lib/facebook_ads/ad.rb
64
65
  - lib/facebook_ads/ad_account.rb
65
66
  - lib/facebook_ads/ad_audience.rb
@@ -74,6 +75,8 @@ files:
74
75
  - lib/facebook_ads/ad_product_set.rb
75
76
  - lib/facebook_ads/ad_set.rb
76
77
  - lib/facebook_ads/ad_targeting.rb
78
+ - lib/facebook_ads/ad_user.rb
79
+ - lib/facebook_ads/advertisable_application.rb
77
80
  - lib/facebook_ads/base.rb
78
81
  - spec/facebook_ads/ad_account_spec.rb
79
82
  - spec/facebook_ads/ad_campaign_spec.rb
@@ -136,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
139
  version: '0'
137
140
  requirements: []
138
141
  rubyforge_project:
139
- rubygems_version: 2.6.14
142
+ rubygems_version: 2.6.12
140
143
  signing_key:
141
144
  specification_version: 4
142
145
  summary: Facebook Marketing API SDK for Ruby.