@ticketboothapp/booking 1.2.100 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.100",
3
+ "version": "1.2.101",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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
  }