Class: Shipping::RoadrunnerTracker
- Inherits:
-
ShipengineLtlTracker
- Object
- ShipengineLtlTracker
- Shipping::RoadrunnerTracker
- Defined in:
- app/services/shipping/roadrunner_tracker.rb
Overview
Reads Roadrunner LTL scans from the carrier's public tracking page, because
ShipEngine's LTL endpoint returns nothing for them — 0 scans across every
Roadrunner delivery, while Saia / XPO / R&L on the identical code path all
return full trails. Roadrunner is frequently the cheapest LTL quote, so
without this those loads are a tracking black hole: invisible to the
delivery status icon and unflaggable by ProblematicDeliverySweep, whose
freight rules need at least one scan to establish carrier custody.
Subclasses ShipengineLtlTracker and overrides only the two carrier-facing
steps — where the payload comes from (#fetch_tracking) and how it maps to
rows (#event_rows). Candidate selection, the delivered / age-backstop stop
conditions, idempotent upsert, and SCAC stamping are all inherited, so these
events are indistinguishable downstream from ShipEngine-sourced ones.
This is a scrape, and it will break
There is no Roadrunner tracking API available to us. The endpoint below is
the plain GET the carrier's own tracking page issues to fill its content
tab: no authentication, no anti-forgery token, no cookie, no JavaScript —
it returns a rendered HTML fragment containing a "Travel History" table.
That makes it cheap, not stable. Every failure path here is deliberately
silent-but-reported (log + ErrorReporting.warning, return nil) so a markup
change degrades to "no new scans" rather than breaking the hourly sweep for
every other LTL carrier.
ShipEngine have said they are replacing their LTL backend with WWEX group's
and deprecating the current one; if that lands and returns Roadrunner scans,
delete this class and drop the ShipengineLtlTracker::TRACKER_BY_CARRIER
entry — nothing else references it.
Constant Summary collapse
- TRACKING_URL =
The fragment endpoint the tracking page itself calls. The visible page at
tools.rrts.com/LTLTrack/?searchValues=<pro>renders an empty shell and
fetches this for the actual content, so going straight here skips the
JavaScript entirely. 'https://tools.rrts.com/LTLTrack/TrackingShipment/ShipmentDataTab/%<pro>s'- TIMEOUTS =
Short: this runs inside the hourly sweep's per-delivery loop, and a
hanging carrier page must not stall the other candidates. { open_timeout: 5, timeout: 15 }.freeze
- TIME_FORMAT =
Roadrunner renders "07/24/2026" and "11:24 AM" in separate cells, with no
offset — US Central, matching ShipengineLtlTracker::TRACKING_TZ. '%m/%d/%Y %I:%M %p'- TRAVEL_HISTORY_MARKER =
A carrier page that answers 200 with an error body would otherwise parse
to zero rows and look like "no scans yet" forever. Requiring the table
header keeps a broken fetch distinguishable from a genuinely empty one. 'Travel History'- PRE_PICKUP_PATTERN =
Roadrunner's only pre-custody rows. Matched before the shared inference
because "Pickup Request Received." / "Pickup rescheduled from … to …"
describe a booking, not a scan — treating them as movement would let
ProblematicDeliverySweep read a load that was never collected as being
in carrier custody, which is MissedFreightPickupSweep's job. /\bpickup\s+(?:request|rescheduled)\b/i- DEFAULT_STATUS_CODE =
Every other row on this page is a physical handling scan — "Trailer
Closed - ready for dispatch", "Trailer unloaded at Detroit terminal.",
"Attempting to schedule a delivery appointment." Six of ten rows on a real
trail match none of ShipmentEvent::STATUS_INFERENCE_PATTERNS, and
leaving those nil would show the delivery's status icon as unknown for
most of the move. They are in-transit by construction: the carrier only
lists an event once it has the freight.Deliberately a local default rather than widening the shared IT catch-all,
which every parcel carrier also runs through. 'IT'
Constants inherited from ShipengineLtlTracker
ShipengineLtlTracker::POLL_MAX_AGE, ShipengineLtlTracker::STATUS_CODE_BY_LABEL, ShipengineLtlTracker::TRACKER_BY_CARRIER, ShipengineLtlTracker::TRACKING_TZ
Instance Method Summary collapse
-
#event_rows(result) ⇒ Array<Hash>
Project the Travel History table into ShipmentEvent attribute hashes.
-
#fetch_tracking ⇒ String?
The tracking fragment HTML, or nil on any failure.
Methods inherited from ShipengineLtlTracker
candidates, #delivered?, for, #initialize, #past_backstop?, #poll!, #pollable?
Constructor Details
This class inherits a constructor from Shipping::ShipengineLtlTracker
Instance Method Details
#event_rows(result) ⇒ Array<Hash>
Project the Travel History table into ShipmentEvent attribute hashes.
The status strings are carrier prose, so the code comes from
#status_code_for — the shared ShipmentEvent.infer_status_code matcher
the parcel path uses for carriers that return nil codes, bracketed by two
Roadrunner-local rules (PRE_PICKUP_PATTERN, DEFAULT_STATUS_CODE).
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'app/services/shipping/roadrunner_tracker.rb', line 99 def event_rows(result) travel_history_rows(result).filter_map do |date, time, status| occurred_at = parse_scan_time(date, time) next if occurred_at.blank? || status.blank? status_code = status_code_for(status) { tracking_number: pro, carrier_code: tracking_scac, occurred_at: occurred_at, status_code: status_code, status_description: ShipmentEvent::STATUS_CODE_LABELS[status_code], carrier_status_description: status, description: status, payload: { source: 'roadrunner_ltltrack', date: date, time: time, status: status } } end end |
#fetch_tracking ⇒ String?
Returns the tracking fragment HTML, or nil on any failure.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'app/services/shipping/roadrunner_tracker.rb', line 75 def fetch_tracking response = connection.get(format(TRACKING_URL, pro: CGI.escape(pro)), appIDEncoded: '') body = response.body.to_s return body if body.include?(TRAVEL_HISTORY_MARKER) Rails.logger.warn("[RoadrunnerTracker] delivery=#{delivery.id} pro=#{pro} " \ "response carried no #{TRAVEL_HISTORY_MARKER.inspect} table (#{body.bytesize} bytes)") nil rescue StandardError => e Rails.logger.error("[RoadrunnerTracker] delivery=#{delivery.id} pro=#{pro} fetch failed: #{e.class}: #{e.}") ErrorReporting.warning('RoadrunnerTracker tracking fetch failed', error: e., delivery_id: delivery.id, pro_number: pro) nil end |