Class: MicrosoftAds::ConversionsClient
- Inherits:
-
Object
- Object
- MicrosoftAds::ConversionsClient
- Defined in:
- app/services/microsoft_ads/conversions_client.rb
Overview
REST client for the Microsoft Advertising (Bing Ads) Campaign Management
v13 ApplyOfflineConversions operation — uploads offline conversions keyed
by Microsoft Click Id (msclkid). The direct parallel to
OpenaiAds::ApiClient, and the conversion-side sibling of the SOAP
CampaignManagementClient.
Why REST (the campaign read path is still SOAP): Microsoft is
deprecating the SOAP API on 2027-01-31 and asks integrators to move to REST
before 2026-10-01, so the net-new conversion path is built REST-first. Auth
uses the same four credentials as the SOAP client, but REST passes the OAuth
token as a standard Authorization: Bearer header (the SOAP
AuthenticationToken envelope element has no REST equivalent); the developer
token, CustomerId and CustomerAccountId remain custom headers.
200-with-PartialErrors: the operation returns HTTP 200 even when
individual conversions are rejected — per-item failures arrive in the
PartialErrors array of BatchError objects. A 200 with a non-empty
PartialErrors is therefore a failure for our (single) conversion, and a
terminal one: the same payload re-sent gets the same rejection.
Constant Summary collapse
- SERVICE_URL =
Production Campaign Management v13 REST endpoint for ApplyOfflineConversions.
(Sandbox swapscampaign.api→campaign.api.sandbox.) 'https://campaign.api.bingads.microsoft.com/CampaignManagement/v13/OfflineConversions/Apply'
Instance Method Summary collapse
-
#apply_offline_conversions(conversions:) ⇒ Hash
Upload one or more offline conversions.
-
#initialize(developer_token:, access_token:, customer_id:, account_id:) ⇒ ConversionsClient
constructor
A new instance of ConversionsClient.
Constructor Details
#initialize(developer_token:, access_token:, customer_id:, account_id:) ⇒ ConversionsClient
Returns a new instance of ConversionsClient.
36 37 38 39 40 41 |
# File 'app/services/microsoft_ads/conversions_client.rb', line 36 def initialize(developer_token:, access_token:, customer_id:, account_id:) @developer_token = developer_token @access_token = access_token @customer_id = customer_id @account_id = account_id end |
Instance Method Details
#apply_offline_conversions(conversions:) ⇒ Hash
Upload one or more offline conversions.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 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 96 97 98 99 |
# File 'app/services/microsoft_ads/conversions_client.rb', line 52 def apply_offline_conversions(conversions:) response = connection.post('') do |req| req.body = { OfflineConversions: conversions }.to_json end case response.status when 200 partial_errors = extract_partial_errors(response.body) if partial_errors.empty? Rails.logger.info "MicrosoftAds::ConversionsClient: #{conversions.size} offline conversion(s) accepted (HTTP 200)" { status: :reported, http_status: 200 } else # 200 + PartialErrors = the conversion data itself was rejected (bad # click id, conversion time outside the goal's window, unknown goal # name). Terminal — a retry sends the identical payload and earns the # identical rejection. msg = partial_errors.map { |e| "#{e['ErrorCode'].presence || e['Code']}: #{e['Message']}" }.join('; ') Rails.logger.error "MicrosoftAds::ConversionsClient: PartialErrors -- #{msg}" { status: :rejected, http_status: 200, error: msg, partial_errors: partial_errors } end when 401 # Token lapsed mid-run. The reporter fetches a fresh OAuth token per run # via OauthService#access_token!, so this is transient — keep :failed so # the sweep retries with a newly-minted token. { status: :failed, http_status: 401, error: 'Unauthorized (HTTP 401)' } when 429 { status: :rate_limited, http_status: 429, error: 'Rate limited (HTTP 429)' } when 400, 403, 404 # Persistent client errors (malformed payload, wrong account, unknown # goal) never succeed on retry — terminal :rejected so the sweep doesn't # churn them daily for 7 days. (401 stays :failed: a lapsed token is # transient; the reporter mints a fresh one on the next run.) msg = (response.body) || "HTTP #{response.status}" Rails.logger.error "MicrosoftAds::ConversionsClient: Rejected (terminal HTTP #{response.status}) -- #{msg}" { status: :rejected, http_status: response.status, error: msg } else msg = (response.body) || "HTTP #{response.status}" Rails.logger.error "MicrosoftAds::ConversionsClient: Failed -- #{msg}" { status: :failed, http_status: response.status, error: msg } end rescue Faraday::TimeoutError => e # Keep status :failed so ConversionRetrySweepWorker still re-enqueues it # (it selects result == 'failed'); the `timeout` flag lets the reporter # skip AppSignal noise for these transient, already-retried failures. { status: :failed, http_status: nil, error: "Timeout: #{e.}", timeout: true } rescue Faraday::Error => e { status: :failed, http_status: nil, error: e. } end |