@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 +1 -1
- package/src/lib/booking-api.ts +181 -1
package/package.json
CHANGED
package/src/lib/booking-api.ts
CHANGED
|
@@ -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
|
|
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
|
}
|