Class: Pinterest::ConversionReporter
- Inherits:
-
BaseService
- Object
- BaseService
- Pinterest::ConversionReporter
- Defined in:
- app/services/pinterest/conversion_reporter.rb
Overview
Pinterest Conversions API v5 -- server-to-server event reporting.
Sends conversion events (checkout, lead) to Pinterest so they can be used
for campaign optimization, targeting, and conversion reporting.
Mirrors the existing Invoicing::GoogleConversionReporter pattern:
- Called from PinterestConversionWorker
- Persists result metadata to the record's
pinterest_conversion_metaJSONB column - Uses
event_idfor deduplication with client-side Pinterest Tag
API reference:
POST https://api.pinterest.com/v5/ad_accounts/#ad_account_id/events
https://developers.pinterest.com/docs/api/v5/#operation/events/create
Constant Summary collapse
- API_BASE =
Api base.
"https://api.pinterest.com/v5"- EVENT_CHECKOUT =
Pinterest standard event names
"checkout"- EVENT_LEAD =
Event lead.
"lead"- PRIMARY_USER_DATA_KEYS =
Pinterest CAPI matches a conversion to a user via
user_data. It needs a
primary identifier — a hashed email (em) or anepikclick_id; an
IP + user-agent pair also qualifies.external_idalone is supplementary
and Pinterest rejects such events with HTTP 422 (AppSignal #5223 —
CRM-rep-created records with no email and no visit). %i[em click_id].freeze
Instance Attribute Summary
Attributes inherited from BaseService
Instance Method Summary collapse
-
#send_events(events) ⇒ Object
── API Communication ────────────────────────────────────────────────.
-
#send_opportunity_conversion(opportunity) ⇒ Object
── Opportunity Conversions ──────────────────────────────────────────.
-
#send_order_conversion(order) ⇒ Object
── Order Conversions ────────────────────────────────────────────────.
-
#sibling_opportunity_already_reported(opportunity) ⇒ Opportunity?
When a rep creates a CRM opp for a customer who already had an open quote-builder / online opp, both opps can transition to qualified and both would fire send_opportunity_conversion against the same Pinterest session — phantom lead volume that biases Pinterest smart bidding.
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
#send_events(events) ⇒ Object
── API Communication ────────────────────────────────────────────────
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'app/services/pinterest/conversion_reporter.rb', line 166 def send_events(events) result = Pinterest::ApiClient.new.send_events( ad_account_id: ad_account_id, token: conversion_token, events: events ) if result[:status] == :failed if result[:timeout] # Transient timeout to an intermittently-slow endpoint. The record # keeps result: 'failed', so ConversionRetrySweepWorker re-enqueues it # within its 7-day window — don't page AppSignal for each attempt # (mirror of #5214; the sweep's contract is to keep this noise out). Rails.logger.warn("Pinterest::ConversionReporter: #{result[:error]} (will retry via sweep)") else ErrorReporting.error("Pinterest::ConversionReporter: #{result[:error]}", { http_status: result[:http_status], event_count: events.size }) end end result end |
#send_opportunity_conversion(opportunity) ⇒ Object
── Opportunity Conversions ──────────────────────────────────────────
107 108 109 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'app/services/pinterest/conversion_reporter.rb', line 107 def send_opportunity_conversion(opportunity) return { success: false, reason: :not_a_sales_opportunity } unless opportunity.sales_opportunity? if opportunity..present? Rails.logger.warn "Pinterest::ConversionReporter: Opportunity #{opportunity.reference_number} already reported" return { success: false, reason: :already_reported, meta: opportunity. } end if (sibling = sibling_opportunity_already_reported(opportunity)) Rails.logger.info "Pinterest::ConversionReporter: Sibling opp #{sibling.reference_number} already reported; skipping #{opportunity.reference_number}" return { success: false, reason: :sibling_already_reported, sibling_opportunity_id: sibling.id } end conversion_date_time = opportunity.updated_at || opportunity.created_at fractional_value = (opportunity.value || 0) * 0.2 # Same AVG_LEAD_CONVERSION_RATE as Google email = opportunity.emails.pick(:detail) || opportunity.customer&.email visit = find_visit(opportunity) # Shared id with the browser pixel — see send_order_conversion for rationale. event_id = opportunity.tracking_event_id.presence || SecureRandom.uuid event = build_event( event_name: EVENT_LEAD, event_time: conversion_date_time, event_id: event_id, email: email, external_id: opportunity.customer&.id || opportunity.id, ip: visit&.ip, user_agent: visit&.user_agent, landing_page: visit&.landing_page.presence || fallback_event_source_url(opportunity), click_id: visit&.&.dig('epik'), custom_data: { currency: "USD", value: fractional_value.to_f.round(2).to_s, order_id: opportunity.reference_number } ) return persist_skipped(opportunity, event_id, EVENT_LEAD, conversion_date_time, email) unless matchable?(event) result = send_events([event]) = { result: result[:status], event_id: event_id, event_name: EVENT_LEAD, attempted_at: Time.current, reported_at: (Time.current if result[:status] == :reported), conversion_date_time: conversion_date_time, conversion_email: email, http_status: result[:http_status], error: result[:error] }.compact opportunity.update_column(:pinterest_conversion_meta, ) { success: result[:status] == :reported, meta: } end |
#send_order_conversion(order) ⇒ Object
── Order Conversions ────────────────────────────────────────────────
43 44 45 46 47 48 49 50 51 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 100 101 102 103 |
# File 'app/services/pinterest/conversion_reporter.rb', line 43 def send_order_conversion(order) return { success: false, reason: :not_a_sales_order } unless order.is_sales_order? if order..present? Rails.logger.warn "Pinterest::ConversionReporter: Order #{order.reference_number} already reported" return { success: false, reason: :already_reported, meta: order. } end conversion_date_time = order.invoices.first&.created_at unless conversion_date_time Rails.logger.warn "Pinterest::ConversionReporter: No invoice for order #{order.reference_number}" return { success: false, reason: :no_invoice } end total = order.invoices.sum(&:revenue_consolidated) email = order.tracking_email&.first || order.order_emails.first visit = find_visit(order) # Shared id with the browser pixel — the Order's `tracking_event_id` is # injected into the confirmation page via `<meta name="tracking:event-id">`, # so Pinterest dedup matches browser ↔ server events. Fall back to a fresh # UUID for legacy orders that predate the column (those events were # already reported with their own ids, no dedup loss). event_id = order.tracking_event_id.presence || SecureRandom.uuid event = build_event( event_name: EVENT_CHECKOUT, event_time: conversion_date_time, event_id: event_id, email: email, external_id: order.customer&.id || order.id, ip: visit&.ip, user_agent: visit&.user_agent, landing_page: visit&.landing_page.presence || fallback_event_source_url(order), click_id: visit&.&.dig('epik'), custom_data: { currency: "USD", value: total.to_f.round(2).to_s, order_id: order.reference_number, num_items: order.line_items.size } ) return persist_skipped(order, event_id, EVENT_CHECKOUT, conversion_date_time, email) unless matchable?(event) result = send_events([event]) = { result: result[:status], event_id: event_id, event_name: EVENT_CHECKOUT, attempted_at: Time.current, reported_at: (Time.current if result[:status] == :reported), conversion_date_time: conversion_date_time, conversion_email: email, http_status: result[:http_status], error: result[:error] }.compact order.update_column(:pinterest_conversion_meta, ) { success: result[:status] == :reported, meta: } end |
#sibling_opportunity_already_reported(opportunity) ⇒ Opportunity?
When a rep creates a CRM opp for a customer who already had an open
quote-builder / online opp, both opps can transition to qualified and
both would fire send_opportunity_conversion against the same Pinterest
session — phantom lead volume that biases Pinterest smart bidding.
Mirrors Invoicing::GoogleConversionReporter#sibling_opportunity_already_reported.
Public so tests (and external auditing) can probe it without .send.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'app/services/pinterest/conversion_reporter.rb', line 201 def sibling_opportunity_already_reported(opportunity) customer_id = opportunity.customer_id return nil if customer_id.nil? cluster_ids = [opportunity.parent_id, opportunity.merged_into_id, opportunity.id].compact Opportunity .where(customer_id: customer_id) .where.not(id: opportunity.id) .where("pinterest_conversion_meta->>'reported_at' IS NOT NULL") .where( 'opportunities.id IN (?) OR opportunities.parent_id IN (?) OR opportunities.merged_into_id IN (?)', cluster_ids, cluster_ids, cluster_ids ) .first end |