Class: Facebook::AdvertiserApiClient

Inherits:
BaseService show all
Defined in:
app/services/facebook/advertiser_api_client.rb

Overview

Service object: advertiser API client.

Constant Summary collapse

API_VERSION =

Marketing Graph API version. Meta versions the URL path; bump this
in lockstep when migrating to a newer API version (versions stay
supported for ~24 months, then auto-upgrade with possible breaking
changes — keep an eye on the changelog).

'v25.0'
BASE_URL =

URL for base.

"https://graph.facebook.com/#{API_VERSION}".freeze
PAGE_LIMIT =

Page size for the campaigns list endpoint. Facebook caps limit at
100 for campaign listings; well above the realistic active-campaign
count for one ad account.

100
CAMPAIGN_FIELDS =

Fields to request on each campaign — keeps the payload focused and
avoids Meta's "Please reduce the amount of data you're asking for"
errors when a large account has many campaigns.

%w[
  id
  name
  status
  effective_status
  objective
  daily_budget
  lifetime_budget
  created_time
  updated_time
].freeze

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #process, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Instance Method Details

#campaigns_with_spend_since(ad_account_id:, token:, since:) ⇒ Set<String>

Campaign ids that recorded any spend on or after since. Drives
FacebookCampaignSyncWorker Source visibility — a campaign that
hasn't spent within the window is mirrored as an archived Source.

Walks the same cursor pagination as #list_campaigns. The insights
endpoint only returns rows for campaigns with delivery in the range,
so campaigns that never spent simply don't appear.

Parameters:

  • ad_account_id (String)

    numeric ad account id (no act_ prefix).

  • token (String)

    Facebook System User access token (bearer).

  • since (Date)

    inclusive start of the spend window.

Returns:

  • (Set<String>)

    campaign ids with spend > 0 in [since, today].

Raises:

  • (RuntimeError)

    on a non-200 response.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/services/facebook/advertiser_api_client.rb', line 110

def campaigns_with_spend_since(ad_account_id:, token:, since:)
  spenders = Set.new
  cursor   = nil

  loop do
    response = connection(token).get("act_#{}/insights") do |req|
      req.params['level']      = 'campaign'
      req.params['fields']     = 'campaign_id,spend'
      req.params['time_range'] = { since: since.iso8601, until: Date.current.iso8601 }.to_json
      req.params['limit']      = PAGE_LIMIT
      req.params['after']      = cursor if cursor
    end

    unless response.status == 200
      body = safe_parse(response.body)
      err  = body.dig('error', 'message') || body['message'] || "HTTP #{response.status}"
      raise "Facebook::AdvertiserApiClient: campaign insights failed (HTTP #{response.status}): #{err}"
    end

    body = safe_parse(response.body)
    Array(body['data']).each do |row|
      spenders << row['campaign_id'].to_s if row['spend'].to_f.positive?
    end

    next_cursor = body.dig('paging', 'cursors', 'after')
    break if next_cursor.blank? || body.dig('paging', 'next').blank?
    break if next_cursor == cursor # defensive — same cursor twice would loop forever

    cursor = next_cursor
  end

  spenders
end

#list_campaigns(ad_account_id:, token:) ⇒ Array<Hash>

List all campaigns on the ad account. Walks Meta's paging.cursors.after
cursor pagination until paging.next stops being present and returns
the concatenated list.

Parameters:

  • ad_account_id (String)

    Facebook ad account ID (numeric — the
    act_ prefix is added here, do NOT include it in the parameter).

  • token (String)

    Facebook System User access token (bearer)
    with ads_read scope.

Returns:

  • (Array<Hash>)

    campaign objects per Meta's spec —
    id, name, status ("ACTIVE"|"PAUSED"|"ARCHIVED"|"DELETED"),
    effective_status, objective, daily_budget/lifetime_budget
    (in account currency minor units), created_time/updated_time
    (ISO 8601 strings).

Raises:

  • (RuntimeError)

    on non-200 responses (Sidekiq retry surface).



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'app/services/facebook/advertiser_api_client.rb', line 66

def list_campaigns(ad_account_id:, token:)
  campaigns = []
  cursor    = nil

  loop do
    response = connection(token).get("act_#{}/campaigns") do |req|
      req.params['limit']  = PAGE_LIMIT
      req.params['fields'] = CAMPAIGN_FIELDS.join(',')
      req.params['after']  = cursor if cursor
    end

    unless response.status == 200
      body = safe_parse(response.body)
      err  = body.dig('error', 'message') || body['message'] || "HTTP #{response.status}"
      raise "Facebook::AdvertiserApiClient: list_campaigns failed (HTTP #{response.status}): #{err}"
    end

    body = safe_parse(response.body)
    page = Array(body['data'])
    campaigns.concat(page)

    next_cursor = body.dig('paging', 'cursors', 'after')
    break if next_cursor.blank? || body.dig('paging', 'next').blank?
    break if next_cursor == cursor # defensive — same cursor twice would loop forever

    cursor = next_cursor
  end

  campaigns
end