Error Tracking for Travel Booking Engines: When a Failed Search Costs the Sale
EngineeringJune 20, 202611 min read

Error Tracking for Travel Booking Engines: When a Failed Search Costs the Sale

A silent API timeout in your flight search can bleed bookings for hours before anyone notices. Here's how to catch it.

The PagerDuty alert came in at 2:34am on a Saturday. Peak booking season. Of course.

"Conversion rate dropped 41% in the last 2 hours. No deploys. No traffic anomalies."

I rolled out of bed, grabbed coffee, and started the familiar drill. Sentry first — error volume looked normal. Maybe a slight uptick in network timeouts, nothing screaming. GA4 next — funnel showed users searching, getting results, then vanishing before the passenger details page. But GA4 couldn't tell me why. It just knew they left.

So I did the thing I always do when tools don't talk to each other: I started matching timestamps across dashboards.

The Amadeus API was returning results, technically. But about 40% of those results had zero available fares. The GDS was responding — just with empty arrays. No exception. No error code. Just... nothing bookable.

Our frontend dutifully rendered "No flights found" and users bounced. Three hours of bleeding bookings before I traced it to an expired fare agreement that Amadeus had silently deprecated overnight.

That's when I stopped treating GDS monitoring and booking analytics as separate concerns.

What You'll Walk Away With

  • Search-step error tracking that catches API failures even when they return HTTP 200
  • Session-level correlation tying abandonment to specific error types
  • Real-time alerts for when Amadeus starts serving garbage
  • Revenue attribution — the exact dollar cost of each timeout

Works with Amadeus, Sabre, Travelport, custom aggregators. If you're on JustAnalytics, it's maybe 30 lines of wrapper code. If you're on Sentry plus GA4? Well. This shows you what you're missing (and honestly, it's kind of embarrassing how long I ran that setup before consolidating my observability stack).

Prerequisites

  • A JustAnalytics account (free tier handles 100K events/month)
  • Access to your booking engine's API integration layer
  • Basic familiarity with async JavaScript or your backend language of choice
  • Your GDS provider's API documentation handy — you'll need response schemas

Step 1: Instrument Your Booking Funnel Steps

Travel booking flows are messier than e-commerce. Way messier.

A typical flow chains: search → results → fare selection → passenger details → payment. Five steps. But here's the thing — each step can fail independently, and because travel sites aggregate multiple providers, any single step might hit 3-6 external APIs. I've seen a single "search" button trigger calls to Amadeus, Sabre, two hotel aggregators, and a car rental API. All in parallel. All with different timeout behaviors.

Start by tagging each step:

import { JA } from '@justanalytics/browser';

// On search initiation
function initiateFlightSearch(searchParams) {
  JA.track('booking_funnel', {
    step: 'search_initiated',
    origin: searchParams.origin,
    destination: searchParams.destination,
    departure_date: searchParams.departureDate,
    return_date: searchParams.returnDate,
    passengers: searchParams.passengers,
    cabin_class: searchParams.cabinClass
  });

  return fetchFlightResults(searchParams);
}

The search parameters matter. When you're debugging "why did JFK-LHR fail yesterday," you need to filter by route. Trust me — generic "search failed" events are useless at 2am.

Why this is different from e-commerce: Checkout has one product, one price. Flight search returns dozens of options from multiple providers with different fare rules. Context is everything. "We have a problem" vs. "Sabre is timing out on transatlantic routes departing after August 15" — night and day. We covered e-commerce error tracking separately if that's your vertical.

Step 2: Wrap Your GDS API Calls

Here's where most travel sites screw up. They catch exceptions but not degraded responses.

An Amadeus query that returns in 8 seconds with zero results? Not an exception. Valid HTTP 200. Kills your conversion anyway. Wrap every external API call:

async function fetchAmadeusFlights(searchParams, sessionId) {
  const startTime = Date.now();

  try {
    const response = await amadeus.shopping.flightOffers.get({
      originLocationCode: searchParams.origin,
      destinationLocationCode: searchParams.destination,
      departureDate: searchParams.departureDate,
      adults: searchParams.passengers.adults
    });

    const duration = Date.now() - startTime;
    const resultCount = response.data?.length || 0;

    // Track API performance even on success
    JA.track('gds_api_call', {
      provider: 'amadeus',
      route: `${searchParams.origin}-${searchParams.destination}`,
      duration_ms: duration,
      result_count: resultCount,
      status: resultCount > 0 ? 'success' : 'empty_results',
      session_id: sessionId
    });

    // Empty results are not exceptions, but they kill conversions
    if (resultCount === 0 && duration > 5000) {
      JA.track('gds_degraded_response', {
        provider: 'amadeus',
        route: `${searchParams.origin}-${searchParams.destination}`,
        duration_ms: duration,
        probable_cause: 'timeout_or_no_inventory',
        session_id: sessionId
      });
    }

    return response;

  } catch (error) {
    const duration = Date.now() - startTime;

    JA.track('gds_api_error', {
      provider: 'amadeus',
      route: `${searchParams.origin}-${searchParams.destination}`,
      duration_ms: duration,
      error_type: error.code || 'unknown',
      error_message: error.message,
      session_id: sessionId
    });

    throw error;
  }
}

The duration_ms field is critical. GDS APIs have wildly variable response times — Amadeus might return in 800ms for a domestic route, 7 seconds for a multi-city international with 4 passengers. If your frontend timeout is 5 seconds, you'll see "empty results" when the real problem is the response hadn't arrived yet.

Gotcha: Sabre and Travelport have different error shapes. Sabre returns errors in the response body with HTTP 200 (maddening). Travelport uses SOAP faults (even more maddening). Provider-specific parsing is non-negotiable. I spent a week thinking we had zero Sabre errors. We had hundreds. The dashboard just wasn't parsing them. For structuring your OpenTelemetry spans around these providers, see our tracing guide.

Step 3: Track the Fare Selection Cliff

This is the conversion killer nobody talks about. And honestly? It's the one that frustrates me most, because it's technically correct behavior.

User searches. Gets 47 results. Clicks a $412 fare. Goes to passenger details. Hits "Continue to Payment." Gets... "This fare is no longer available."

Fare expired between search and selection. Happens constantly on high-demand routes. Not an error — a valid business rule. But from the user's perspective? Broken site. Wasted 15 minutes. Gone to Kayak.

async function selectFare(fareOffer, sessionId) {
  JA.track('booking_funnel', {
    step: 'fare_selected',
    fare_id: fareOffer.id,
    price: fareOffer.price,
    currency: fareOffer.currency,
    provider: fareOffer.source,
    cabin_class: fareOffer.cabinClass,
    session_id: sessionId
  });

  try {
    const availability = await verifyFareAvailability(fareOffer);

    if (!availability.available) {
      JA.track('fare_expiration', {
        fare_id: fareOffer.id,
        original_price: fareOffer.price,
        time_since_search_ms: Date.now() - sessionStorage.getItem('search_timestamp'),
        reason: availability.reason || 'inventory_sold',
        session_id: sessionId
      });

      // This is where users bounce
      return { success: false, reason: 'fare_expired' };
    }

    return { success: true, fare: availability.confirmedFare };

  } catch (error) {
    JA.track('fare_verification_error', {
      fare_id: fareOffer.id,
      error_type: error.code,
      provider: fareOffer.source,
      session_id: sessionId
    });
    throw error;
  }
}

The time_since_search_ms metric reveals patterns. Fares expiring in under 60 seconds? GDS cache issue. Expiring after 10+ minutes? Users are just slow (or indecisive — no judgment, I've spent 45 minutes comparing legroom charts). Different problems, different fixes.

We've covered correlating errors with funnel drop-off if you want the conceptual framework.

Step 4: Payment Integration

Travel payments are their own beast. Multi-currency, split payments, airline-specific card restrictions — there's a lot that can break. Track booking_funnel with step: 'payment_initiated', include booking_value and route, capture both Stripe decline errors (in the response object) and thrown exceptions.

A $2,400 family vacation booking that fails at payment? That's a frustrated customer who's been clicking for 20 minutes. They don't retry. They go to Expedia. We've written about checkout error tracking patterns that apply here.

Step 5: Build Your Correlation Dashboard

Now you've got booking funnel events and API errors flowing into the same stream. Here's the query that finds your conversion killers:

SELECT
  properties->>'step' as booking_step,
  properties->>'provider' as gds_provider,
  COUNT(DISTINCT properties->>'session_id') as total_sessions,
  COUNT(DISTINCT CASE
    WHEN event IN ('gds_api_error', 'gds_degraded_response', 'fare_expiration', 'payment_error')
    THEN properties->>'session_id'
  END) as error_sessions,
  ROUND(100.0 * COUNT(DISTINCT CASE
    WHEN event IN ('gds_api_error', 'gds_degraded_response', 'fare_expiration', 'payment_error')
    THEN properties->>'session_id'
  END) / COUNT(DISTINCT properties->>'session_id'), 2) as error_rate,
  SUM(CASE
    WHEN event IN ('gds_api_error', 'gds_degraded_response', 'fare_expiration')
    THEN (properties->>'booking_value')::numeric
  END) as revenue_at_risk
FROM events
WHERE event IN ('booking_funnel', 'gds_api_error', 'gds_degraded_response', 'fare_expiration', 'payment_error')
  AND timestamp > NOW() - INTERVAL '24 hours'
GROUP BY booking_step, gds_provider
ORDER BY error_rate DESC

Example output (hypothetical):

Booking StepGDS ProviderTotal SessionsError SessionsError RateRevenue at Risk
search_initiatedamadeus8,2341,64720.0%
fare_selectedsabre2,89131210.8%$487,340
payment_initiated1,204675.6%$103,218

In a scenario like this, the Amadeus search error rate would demand immediate investigation. That 20% means one in five searches failing or returning garbage. At scale? That's real money walking out the door.

Step 6: Set Up Provider-Specific Alerts

Generic error alerts don't work for travel. I've tried. You get alert fatigue within a week. You need alerts that understand your GDS provider mix.

In JustAnalytics, create these alerts:

Alert 1: GDS timeout spike

  • Condition: event = 'gds_api_call' AND properties->>'duration_ms' > 5000
  • Threshold: More than 100 events in 10 minutes
  • Group by: properties->>'provider'

Alert 2: Empty results anomaly

  • Condition: event = 'gds_degraded_response' AND properties->>'status' = 'empty_results'
  • Threshold: Error rate exceeds 15% (versus 7-day baseline)
  • Group by: properties->>'route'

Alert 3: Fare expiration surge

  • Condition: event = 'fare_expiration'
  • Threshold: More than 50 events in 5 minutes
  • This usually means a provider is having inventory sync issues

Route-level grouping matters. JFK-LHR might be fine while SFO-CDG is completely broken. A global alert fires constantly; route-specific tells you exactly where to look.

Common Travel Booking Errors

These show up constantly. Start here when debugging conversion drops.

Empty results on valid routes — GDS returns HTTP 200 but with zero offers. Usually an expired fare agreement, blackout dates, or query parameters exceeding carrier limits (like 9 passengers when max is 8).

Session timeout mid-booking — User spends 12 minutes comparing fare rules. Session expires. They click "Continue" and get kicked back to search. Track session age at each funnel step.

Currency mismatch failures — User searches in USD, fare stored in EUR, payment processor expects GBP. If any conversion breaks, you get silent failures. Log currency at every step.

Multi-passenger validation — One invalid passport expiration date can fail the entire booking without clear feedback. Validate incrementally. Track exactly which field triggered rejection.

What This Won't Fix

Look, error tracking is not magic.

If searches are slow because you're querying 6 GDS providers sequentially instead of in parallel — that's architecture, and no amount of telemetry fixes bad concurrency. If users abandon because your prices are higher than Kayak — that's business. If your UI is confusing about baggage fees — session replay shows the frustration, but the fix is design work, not instrumentation.

Next Steps

Add session replay to fare selection. When users abandon mid-comparison, see exactly what they saw — unclear pricing, broken comparison tables, unresponsive "Select" buttons. JustAnalytics includes session replay with privacy masking.

Correlate errors with traffic source. Some paid channels drive users searching impossible routes (8 passengers, 4 stops, Christmas week — good luck). Segment by UTM. For ad fraud, ClickzProtect handles click fraud detection.

Tag by device. Mobile breaks differently. Tiny calendars. Keyboard covering submit buttons. If mobile error rate is 3x desktop? That's your priority.

Monitor GDS status pages. Amadeus, Sabre, Travelport all have status endpoints. Sometimes "your problem" is "their outage." For teams running VoIP alongside web booking, VeloCalls handles call-quality monitoring with similar provider-correlation. For AI-powered booking agents, DevOS ties agent execution to error patterns.

Frequently Asked Questions

Why do travel booking engines need specialized error tracking?

Travel booking flows chain multiple external APIs — GDS providers like Amadeus or Sabre for availability, payment gateways, third-party hotel or car inventory systems. A timeout from any single provider can silently break the booking without throwing a user-visible error. Standard error tracking catches exceptions but misses API-level failures that show up as empty results or stuck loading states.

What are the most common errors that cause search abandonment in travel sites?

GDS timeout errors top the list — Amadeus and Sabre queries can take 3-8 seconds on complex itineraries, and if your frontend timeout is shorter than the backend response time, users see a blank results page. Second is fare expiration race conditions, where a price shown in search results is no longer valid when the user clicks through. Third is session state loss during multi-step bookings, especially when users switch devices or let sessions expire mid-flow.

How do you correlate API errors with booking abandonment rates?

Tag each search request with a session ID and search context (origin, destination, dates, passenger count). When an API error occurs, log it with the same session ID. Query sessions where search was initiated but no booking completed, filtering for those with API errors. The difference in conversion rate between error sessions and clean sessions shows you exactly how much revenue each error type costs.

Can you track third-party GDS errors if they don't throw JavaScript exceptions?

Yes, but you need to instrument at the API response level, not just catch exceptions. Wrap your GDS API calls to track response status, timing, and result count. An empty results array from Amadeus is not an exception — it's a valid response that might indicate a timeout, inventory issue, or search parameter problem. Track these as custom events with error_type indicating the failure mode.


Try JustAnalytics

Analytics + errors + APM + replay + uptime in one under-5KB script. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier: 100K events/mo. Pro: $49/month ($39 billed annually).

Start free → · AI Command Center MCP

JP
JustAnalytics Platform TeamContributor

Author at JustAnalytics.

Related posts