@ticketboothapp/booking 1.2.99 → 1.2.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -355,26 +355,186 @@ function getAuthHeaders(): Record<string, string> {
355
355
 
356
356
  /** Idempotent GET retries: transient network errors and 502/503/504 (new trace span each attempt). */
357
357
  const BOOKING_GET_MAX_RETRIES = 2;
358
+ const BOOKING_ATTEMPT_HEADER = 'X-Booking-Attempt-Id';
359
+ const SLOW_REQUEST_THRESHOLD_MS = 2500;
360
+ const SUCCESS_TIMING_SAMPLE_RATE = 0.02;
358
361
 
359
362
  function sleep(ms: number): Promise<void> {
360
363
  return new Promise((resolve) => setTimeout(resolve, ms));
361
364
  }
362
365
 
366
+ function newBookingAttemptId(): string {
367
+ try {
368
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
369
+ return crypto.randomUUID();
370
+ }
371
+ } catch {
372
+ /* fall through */
373
+ }
374
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
375
+ }
376
+
377
+ function getEndpointFromUrl(url: string): string {
378
+ try {
379
+ const parsed = new URL(url, typeof window !== 'undefined' ? window.location.href : API_BASE);
380
+ return parsed.pathname;
381
+ } catch {
382
+ return '';
383
+ }
384
+ }
385
+
386
+ function isBookingCriticalEndpoint(endpoint: string): boolean {
387
+ return (
388
+ endpoint === '/1/get-availabilities' ||
389
+ endpoint === '/1/products' ||
390
+ endpoint === '/1/reserve' ||
391
+ endpoint.startsWith('/1/companies/')
392
+ );
393
+ }
394
+
395
+ function getConnectionDiagnostics(): Record<string, number | string | boolean> | null {
396
+ if (typeof navigator === 'undefined') return null;
397
+ const connection = (
398
+ navigator as Navigator & {
399
+ connection?: {
400
+ effectiveType?: string;
401
+ rtt?: number;
402
+ downlink?: number;
403
+ saveData?: boolean;
404
+ };
405
+ }
406
+ ).connection;
407
+ if (!connection) return null;
408
+ return {
409
+ effectiveType: connection.effectiveType ?? 'unknown',
410
+ rtt: typeof connection.rtt === 'number' ? connection.rtt : -1,
411
+ downlink: typeof connection.downlink === 'number' ? connection.downlink : -1,
412
+ saveData: Boolean(connection.saveData),
413
+ };
414
+ }
415
+
416
+ function getAvailabilityQueryShape(url: string): Record<string, string | boolean | string[]> | null {
417
+ try {
418
+ const parsed = new URL(url, typeof window !== 'undefined' ? window.location.href : API_BASE);
419
+ if (parsed.pathname !== '/1/get-availabilities') return null;
420
+ const params = parsed.searchParams;
421
+ return {
422
+ queryKeys: Array.from(params.keys()).sort(),
423
+ productId: params.get('productId') ?? '',
424
+ productOptionId: params.get('productOptionId') ?? '',
425
+ startDate: params.get('startDate') ?? '',
426
+ endDate: params.get('endDate') ?? '',
427
+ fromDateTime: params.get('fromDateTime') ?? '',
428
+ toDateTime: params.get('toDateTime') ?? '',
429
+ allOptions: params.get('allOptions') === 'true',
430
+ summary: params.get('summary') === 'true',
431
+ };
432
+ } catch {
433
+ return null;
434
+ }
435
+ }
436
+
437
+ function latestResourceTiming(url: string): Record<string, number | string | boolean> | null {
438
+ if (typeof performance === 'undefined') return null;
439
+ const entry = performance
440
+ .getEntriesByName(url)
441
+ .filter((item): item is PerformanceResourceTiming => item.entryType === 'resource')
442
+ .at(-1);
443
+ if (!entry) return null;
444
+ return {
445
+ name: entry.name,
446
+ durationMs: Math.round(entry.duration),
447
+ startTimeMs: Math.round(entry.startTime),
448
+ fetchStartMs: Math.round(entry.fetchStart),
449
+ domainLookupStartMs: Math.round(entry.domainLookupStart),
450
+ domainLookupEndMs: Math.round(entry.domainLookupEnd),
451
+ connectStartMs: Math.round(entry.connectStart),
452
+ connectEndMs: Math.round(entry.connectEnd),
453
+ secureConnectionStartMs: Math.round(entry.secureConnectionStart),
454
+ requestStartMs: Math.round(entry.requestStart),
455
+ responseStartMs: Math.round(entry.responseStart),
456
+ responseEndMs: Math.round(entry.responseEnd),
457
+ transferSize: entry.transferSize,
458
+ encodedBodySize: entry.encodedBodySize,
459
+ decodedBodySize: entry.decodedBodySize,
460
+ timingRestricted: entry.responseStart === 0 && entry.responseEnd === 0,
461
+ };
462
+ }
463
+
464
+ function bookingRequestTelemetryContext(
465
+ url: string,
466
+ endpoint: string,
467
+ headers: Record<string, string>,
468
+ requestAttemptId: string,
469
+ attemptNumber: number,
470
+ elapsedMs?: number,
471
+ ): Record<string, unknown> {
472
+ const traceparent = headers.traceparent;
473
+ const traceId = traceparent ? traceIdFromTraceparent(traceparent) : undefined;
474
+ return {
475
+ endpoint,
476
+ method: 'GET',
477
+ correlationId: headers[BOOKING_CORRELATION_HEADER],
478
+ traceparent,
479
+ ...(traceId ? { traceId } : {}),
480
+ requestAttemptId,
481
+ attemptNumber,
482
+ maxRetries: BOOKING_GET_MAX_RETRIES,
483
+ requestUrlPath: endpoint,
484
+ requestUrlSearchLength: (() => {
485
+ try {
486
+ return new URL(url, window.location.href).search.length;
487
+ } catch {
488
+ return 0;
489
+ }
490
+ })(),
491
+ availabilityQueryShape: getAvailabilityQueryShape(url),
492
+ visibilityState: typeof document !== 'undefined' ? document.visibilityState : undefined,
493
+ pageAgeMs: typeof performance !== 'undefined' ? Math.round(performance.now()) : undefined,
494
+ connection: getConnectionDiagnostics(),
495
+ ...(elapsedMs != null ? { elapsedMs } : {}),
496
+ };
497
+ }
498
+
363
499
  async function fetchBookingGetWithRetry(
364
500
  url: string,
365
501
  extra?: Pick<RequestInit, 'cache' | 'signal'>,
366
502
  ): Promise<Response> {
367
503
  let lastErr: unknown;
504
+ const endpoint = getEndpointFromUrl(url);
368
505
  for (let attempt = 0; attempt <= BOOKING_GET_MAX_RETRIES; attempt++) {
369
506
  if (attempt > 0) {
370
507
  await sleep(250 * attempt);
371
508
  }
509
+ const requestAttemptId = newBookingAttemptId();
510
+ const headers = {
511
+ ...getAuthHeaders(),
512
+ [BOOKING_ATTEMPT_HEADER]: requestAttemptId,
513
+ };
514
+ const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
515
+ if (isBookingCriticalEndpoint(endpoint)) {
516
+ reportBookingClientTelemetryEvent('BOOKING_DIALOG_REQUEST_STARTED', {
517
+ ...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1),
518
+ });
519
+ }
372
520
  try {
373
521
  const res = await fetch(url, {
374
522
  ...extra,
375
523
  method: 'GET',
376
- headers: getAuthHeaders(),
524
+ headers,
377
525
  });
526
+ const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt);
527
+ if (
528
+ elapsedMs >= SLOW_REQUEST_THRESHOLD_MS ||
529
+ (isBookingCriticalEndpoint(endpoint) && Math.random() < SUCCESS_TIMING_SAMPLE_RATE)
530
+ ) {
531
+ reportBookingClientTelemetryEvent('BOOKING_DIALOG_REQUEST_TIMING', {
532
+ ...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1, elapsedMs),
533
+ httpStatus: res.status,
534
+ ok: res.ok,
535
+ resourceTiming: latestResourceTiming(url),
536
+ });
537
+ }
378
538
  if (res.ok) {
379
539
  return res;
380
540
  }
@@ -388,8 +548,28 @@ async function fetchBookingGetWithRetry(
388
548
  } catch (e) {
389
549
  lastErr = e;
390
550
  if (attempt < BOOKING_GET_MAX_RETRIES) {
551
+ const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt);
552
+ reportBookingClientTelemetryEvent('BOOKING_DIALOG_REQUEST_RETRYING', {
553
+ ...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1, elapsedMs),
554
+ errorName: e instanceof Error ? e.name : undefined,
555
+ errorMessage: e instanceof Error ? e.message : String(e),
556
+ requestSignalAborted: Boolean(extra?.signal?.aborted),
557
+ });
391
558
  continue;
392
559
  }
560
+ const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt);
561
+ reportBookingClientTelemetryEvent('BOOKING_DIALOG_NETWORK_DIAGNOSTIC', {
562
+ ...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1, elapsedMs),
563
+ errorName: e instanceof Error ? e.name : undefined,
564
+ errorMessage: e instanceof Error ? e.message : String(e),
565
+ errorCause: e instanceof Error && e.cause ? String(e.cause) : undefined,
566
+ requestSignalAborted: Boolean(extra?.signal?.aborted),
567
+ abortReason:
568
+ extra?.signal && 'reason' in extra.signal
569
+ ? String(extra.signal.reason ?? '')
570
+ : undefined,
571
+ resourceTiming: latestResourceTiming(url),
572
+ });
393
573
  throw e;
394
574
  }
395
575
  }
@@ -1298,6 +1478,7 @@ export interface Availability {
1298
1478
  pricesByCategory?: {
1299
1479
  retailPrices: Array<{ category: string; price: number }>;
1300
1480
  };
1481
+ isSummary?: boolean;
1301
1482
  }
1302
1483
 
1303
1484
  export type PrecomputedPricesByCategory = Record<string, Record<string, number>>;
@@ -1306,13 +1487,16 @@ export interface GetAvailabilitiesResponse {
1306
1487
  availabilities: Availability[];
1307
1488
  pricingConfig?: PricingConfig;
1308
1489
  precomputedPrices?: PrecomputedPricesByCategory;
1490
+ precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory>;
1309
1491
  resourcePriceByCurrency?: Record<string, number>;
1310
1492
  resourcePriceByOption?: Record<string, Record<string, number>>;
1493
+ responseMode?: 'summary' | string;
1311
1494
  }
1312
1495
 
1313
1496
  export interface GetAvailabilitiesOptions {
1314
1497
  promoCode?: string | null;
1315
1498
  allOptions?: boolean;
1499
+ summary?: boolean;
1316
1500
  /**
1317
1501
  * When set, TicketBooth should return rates/precomputed prices for this partner pricing profile.
1318
1502
  * Must match the profile linked on the partner (e.g. capabilities.pricingProfileId).
@@ -1359,6 +1543,7 @@ export async function getAvailabilities(
1359
1543
  });
1360
1544
  if (options?.promoCode?.trim()) params.set('promoCode', options.promoCode.trim());
1361
1545
  if (options?.allOptions === true) params.set('allOptions', 'true');
1546
+ if (options?.summary === true) params.set('summary', 'true');
1362
1547
  const pricingProfileId = options?.pricingProfileId?.trim();
1363
1548
  if (pricingProfileId) params.set('pricingProfileId', pricingProfileId);
1364
1549
  const cancellationPolicyProfileId = options?.cancellationPolicyProfileId?.trim();
@@ -1407,12 +1592,17 @@ export async function getAvailabilities(
1407
1592
  }
1408
1593
 
1409
1594
  const bookingData = (data as { data?: GetAvailabilitiesResponse } | null)?.data;
1595
+ const isSummary = options?.summary === true || bookingData?.responseMode === 'summary';
1410
1596
  return {
1411
- availabilities: bookingData?.availabilities ?? [],
1597
+ availabilities: (bookingData?.availabilities ?? []).map((availability) =>
1598
+ isSummary ? { ...availability, isSummary: true } : availability
1599
+ ),
1412
1600
  pricingConfig: bookingData?.pricingConfig,
1413
1601
  precomputedPrices: bookingData?.precomputedPrices,
1602
+ precomputedPricesByOption: bookingData?.precomputedPricesByOption,
1414
1603
  resourcePriceByCurrency: bookingData?.resourcePriceByCurrency,
1415
1604
  resourcePriceByOption: bookingData?.resourcePriceByOption,
1605
+ responseMode: bookingData?.responseMode,
1416
1606
  };
1417
1607
  }
1418
1608
 
@@ -1649,6 +1839,16 @@ export interface CheckoutBreakdown {
1649
1839
  currency: string;
1650
1840
  }
1651
1841
 
1842
+ export interface CheckoutDependentAddOnSelection {
1843
+ dependentAddOnProductId: string;
1844
+ dependentAddOnProductOptionId?: string;
1845
+ offeringId: string;
1846
+ slotStart: string;
1847
+ slotEnd: string;
1848
+ quantity: number;
1849
+ checkoutAnswers?: Record<string, string>;
1850
+ }
1851
+
1652
1852
  export interface CreatePaymentIntentRequest {
1653
1853
  productId: string;
1654
1854
  optionId: string;
@@ -1667,6 +1867,7 @@ export interface CreatePaymentIntentRequest {
1667
1867
  cancellationPolicyId?: string;
1668
1868
  termsAcceptedAt?: string;
1669
1869
  checkoutBreakdown?: CheckoutBreakdown;
1870
+ dependentAddOnSelection?: CheckoutDependentAddOnSelection;
1670
1871
  itineraryDisplay?: ItineraryDisplayStep[];
1671
1872
  skipConfirmationCommunications?: boolean;
1672
1873
  disableAutoCommunications?: boolean;
@@ -1765,6 +1966,7 @@ export interface ConfirmFreeBookingRequest {
1765
1966
  pickupLocationId?: string;
1766
1967
  termsAcceptedAt?: string;
1767
1968
  itineraryDisplay?: ItineraryDisplayStep[];
1969
+ dependentAddOnSelection?: CheckoutDependentAddOnSelection;
1768
1970
  skipConfirmationCommunications?: boolean;
1769
1971
  disableAutoCommunications?: boolean;
1770
1972
  source?: string;
@@ -1846,6 +2048,7 @@ export interface ConfirmBookingWithoutPaymentRequest {
1846
2048
  skipConfirmationCommunications?: boolean;
1847
2049
  disableAutoCommunications?: boolean;
1848
2050
  checkoutBreakdown: CheckoutBreakdown;
2051
+ dependentAddOnSelection?: CheckoutDependentAddOnSelection;
1849
2052
  depositAmount: number;
1850
2053
  balanceAmount: number;
1851
2054
  totalAmount: number;
@@ -74,6 +74,8 @@ export interface BookingRuntimeCatalog {
74
74
  getProductDescription: (...args: any[]) => any;
75
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
76
  buildMinimalProductFromConfig?: (...args: any[]) => any;
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ getStaticProductByIdOrSlug?: (...args: any[]) => any;
77
79
  /** Image URL helper from the host catalog (e.g. Via Via `constants/images`). */
78
80
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
81
  getImageUrl?: (...args: any[]) => any;