facebook_ads 0.6.3 → 0.6.4

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