@ga-components/logger 2.0.25 → 2.0.27

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/dist/index.js CHANGED
@@ -222,6 +222,62 @@ var parseQueryParams = (url) => {
222
222
  return null;
223
223
  }
224
224
  };
225
+ var ALWAYS_LOG_ENDPOINTS = [
226
+ { method: "POST", pattern: "/mytrip/seat/payment" },
227
+ { method: "POST", pattern: "/seat/remove/{bookingCode}" },
228
+ { method: "POST", pattern: "/seat/map/store/{bookingCode}" },
229
+ { method: "GET", pattern: "/seat/summary/{bookingCode}" },
230
+ { method: "POST", pattern: "/mytrip/baggage/save/{bookingCode}" },
231
+ { method: "POST", pattern: "/mytrip/baggage-wtp/save/{bookingCode}" },
232
+ { method: "GET", pattern: "/mytrip/baggage/list/{trx}" },
233
+ { method: "GET", pattern: "/mytrip/baggage/list/{trx}" },
234
+ { method: "POST", pattern: "/reschedule/list" },
235
+ { method: "POST", pattern: "/reschedule/confirmation/{bookingCode}" },
236
+ { method: "POST", pattern: "/reschedule/cancel" },
237
+ { method: "GET", pattern: "/reschedule/resend-otp/{bookingCode}" },
238
+ { method: "GET", pattern: "/reschedule/generate-otp/{bookingCode}" },
239
+ { method: "POST", pattern: "/refund/upload-file" },
240
+ { method: "POST", pattern: "/refund/submit-voluntary" },
241
+ { method: "POST", pattern: "/refund/reupload-file" },
242
+ { method: "POST", pattern: "/refund/price-different/confirmation" },
243
+ { method: "POST", pattern: "/refund/list" },
244
+ { method: "POST", pattern: "/refund/list-chatbot" },
245
+ { method: "POST", pattern: "/refund/detail" },
246
+ { method: "POST", pattern: "/refund/confirmation" },
247
+ { method: "POST", pattern: "/refund/calculate" },
248
+ { method: "GET", pattern: "/refund/resend-otp/{bookingCode}" },
249
+ { method: "GET", pattern: "/refund/generate-otp/{bookingCode}" },
250
+ { method: "GET", pattern: "/refund/checkEligibility/{orderId}" },
251
+ { method: "GET", pattern: "/refund/bank-list" },
252
+ { method: "POST", pattern: "/dapi/booking/insurance/remove" },
253
+ { method: "POST", pattern: "/dapi/booking/insurance/acceptance" },
254
+ { method: "POST", pattern: "/addon/service-insurance/{bookingCode}" },
255
+ { method: "POST", pattern: "/specialRequest/gps/products/{bookingCode}" },
256
+ { method: "POST", pattern: "/ancill/specialRequest/gps/products/{bookingCode}" },
257
+ { method: "POST", pattern: "/ancill/specialRequest/payment/v1/ancillary_gps/pay" },
258
+ { method: "POST", pattern: "/ancill/specialRequest/ancillary/save_gps/{bookingCode}" },
259
+ { method: "POST", pattern: "/specialRequest/deprecated/service/{bookingCode}" },
260
+ { method: "POST", pattern: "/ancill/specialRequest/payment/v1/ancillary_lounge/pay" },
261
+ { method: "POST", pattern: "/ancill/specialRequest/ancillary/save_lounge/{bookingCode}" }
262
+ ];
263
+ var ALWAYS_LOG_MATCHERS = ALWAYS_LOG_ENDPOINTS.map((entry) => ({
264
+ method: entry.method.toUpperCase(),
265
+ regex: new RegExp(
266
+ "^" + entry.pattern.split("/").map(
267
+ (seg) => seg.startsWith("{") && seg.endsWith("}") ? "[^/]+" : seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
268
+ ).join("/") + "/?$"
269
+ )
270
+ }));
271
+ function shouldAlwaysLogEndpoint(url, method) {
272
+ let pathname;
273
+ try {
274
+ pathname = new URL(url, window.location.origin).pathname;
275
+ } catch {
276
+ pathname = url;
277
+ }
278
+ const upperMethod = method.toUpperCase();
279
+ return ALWAYS_LOG_MATCHERS.some((m) => m.method === upperMethod && m.regex.test(pathname));
280
+ }
225
281
  var Logger = class {
226
282
  constructor() {
227
283
  this.config = null;
@@ -410,6 +466,7 @@ var Logger = class {
410
466
  const key = this.buildErrorKey("onerror", finalMessage);
411
467
  if (this.loggedErrorKeys.has(key)) return false;
412
468
  this.loggedErrorKeys.add(key);
469
+ this.addBreadcrumb("error", finalMessage, { source: trimmedSource, lineno, colno });
413
470
  void this.error({
414
471
  fnName: "window.onerror",
415
472
  messageError: new Error(finalMessage),
@@ -423,6 +480,7 @@ var Logger = class {
423
480
  const key = this.buildErrorKey("unhandledrejection", msg);
424
481
  if (this.loggedErrorKeys.has(key)) return;
425
482
  this.loggedErrorKeys.add(key);
483
+ this.addBreadcrumb("error", msg);
426
484
  void this.error({
427
485
  fnName: "window.unhandledrejection",
428
486
  messageError: new Error(msg)
@@ -437,6 +495,7 @@ var Logger = class {
437
495
  const key = this.buildErrorKey("console.error", trimmedMsg);
438
496
  if (this.loggedErrorKeys.has(key)) return;
439
497
  this.loggedErrorKeys.add(key);
498
+ this.addBreadcrumb("error", trimmedMsg);
440
499
  const originalError = args.find((a) => a instanceof Error);
441
500
  const errorToSend = originalError ?? new Error(trimmedMsg);
442
501
  void this.error({
@@ -543,7 +602,8 @@ var Logger = class {
543
602
  status: response.status,
544
603
  duration: `${duration}ms`
545
604
  });
546
- if (response.status >= 400) {
605
+ const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
606
+ if (!isLocalhost && response.status >= 400) {
547
607
  const dedupKey = this.buildErrorKey("interceptFetch", `${method} ${url} ${response.status}`);
548
608
  if (!this.loggedErrorKeys.has(dedupKey)) {
549
609
  this.loggedErrorKeys.add(dedupKey);
@@ -555,8 +615,8 @@ var Logger = class {
555
615
  } catch {
556
616
  responseBody = "[unable to read response body]";
557
617
  }
558
- const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
559
- if (!isLocalhost) {
618
+ const isLocalhost2 = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
619
+ if (!isLocalhost2) {
560
620
  this.error({
561
621
  fnName: "interceptFetch",
562
622
  messageError: new Error(`HTTP ${response.status} ${response.statusText} \u2014 ${url}`),
@@ -573,6 +633,32 @@ var Logger = class {
573
633
  });
574
634
  }
575
635
  }
636
+ } else if (!isLocalhost && response.status < 400 && shouldAlwaysLogEndpoint(url, method)) {
637
+ const dedupKey = this.buildErrorKey("interceptFetch", `${method} ${url} ${response.status}`);
638
+ if (!this.loggedErrorKeys.has(dedupKey)) {
639
+ this.loggedErrorKeys.add(dedupKey);
640
+ let responseBody = null;
641
+ try {
642
+ const cloned = response.clone();
643
+ const contentType = cloned.headers.get("content-type") ?? "";
644
+ responseBody = contentType.includes("application/json") ? await cloned.json() : await cloned.text();
645
+ } catch {
646
+ responseBody = "[unable to read response body]";
647
+ }
648
+ this.info({
649
+ fnName: "interceptFetch",
650
+ endpointName: url,
651
+ payload: {
652
+ method,
653
+ status: response.status,
654
+ statusText: response.statusText,
655
+ duration: `${duration}ms`,
656
+ query: parseQueryParams(url),
657
+ request: parseRequestBody(init?.body),
658
+ response: responseBody
659
+ }
660
+ });
661
+ }
576
662
  }
577
663
  return response;
578
664
  } catch (networkError) {
@@ -642,7 +728,8 @@ var Logger = class {
642
728
  status,
643
729
  duration: `${duration}ms`
644
730
  });
645
- if (status >= 400) {
731
+ const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
732
+ if (!isLocalhost && status >= 400) {
646
733
  const dedupKey = self.buildErrorKey("interceptXHR", `${method} ${url} ${status}`);
647
734
  if (self.loggedErrorKeys.has(dedupKey)) return;
648
735
  self.loggedErrorKeys.add(dedupKey);
@@ -651,8 +738,8 @@ var Logger = class {
651
738
  responseBody = JSON.parse(this.responseText);
652
739
  } catch {
653
740
  }
654
- const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
655
- if (!isLocalhost) {
741
+ const isLocalhost2 = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
742
+ if (!isLocalhost2) {
656
743
  void self.error({
657
744
  fnName: "interceptXHR",
658
745
  messageError: new Error(`HTTP ${status} ${this.statusText} \u2014 ${url}`),
@@ -668,6 +755,28 @@ var Logger = class {
668
755
  }
669
756
  });
670
757
  }
758
+ } else if (!isLocalhost && status < 400 && shouldAlwaysLogEndpoint(url, method)) {
759
+ const dedupKey = self.buildErrorKey("interceptXHR", `${method} ${url} ${status}`);
760
+ if (self.loggedErrorKeys.has(dedupKey)) return;
761
+ self.loggedErrorKeys.add(dedupKey);
762
+ let responseBody = this.responseText;
763
+ try {
764
+ responseBody = JSON.parse(this.responseText);
765
+ } catch {
766
+ }
767
+ void self.info({
768
+ fnName: "interceptXHR",
769
+ endpointName: url,
770
+ payload: {
771
+ method,
772
+ status,
773
+ statusText: this.statusText,
774
+ duration: `${duration}ms`,
775
+ query: parseQueryParams(url),
776
+ request: requestBody,
777
+ response: responseBody
778
+ }
779
+ });
671
780
  }
672
781
  });
673
782
  this.addEventListener("error", () => {
@@ -716,6 +825,13 @@ var Logger = class {
716
825
  });
717
826
  const disabledAtClick = this.isElementDisabled(target);
718
827
  const invalidFieldAtClick = this.hasInvalidFieldNearby(target);
828
+ const ariaDisabledAtClick = target.getAttribute("aria-disabled") === "true";
829
+ const pointerEventsBlockedAtClick = window.getComputedStyle(target).pointerEvents === "none";
830
+ const overlayInterceptedAtClick = (() => {
831
+ const elAtPoint = document.elementFromPoint(event.clientX, event.clientY);
832
+ if (!elAtPoint || elAtPoint === target || target.contains(elAtPoint)) return null;
833
+ return elAtPoint;
834
+ })();
719
835
  const clickTime = Date.now();
720
836
  const domActivityAtClick = this.lastDomActivity;
721
837
  const DEAD_CLICK_DELAY_MS = 4e3;
@@ -784,7 +900,25 @@ var Logger = class {
784
900
  const recentFetch = this.breadcrumbs.filter(
785
901
  (b) => b.type === "fetch" && clickTime - new Date(b.timestamp).getTime() <= RAGE_CLICK_WINDOW_MS
786
902
  ).slice(-1)[0];
787
- const possibleReason = disabledAtClick ? { type: "disabled", detail: "Button dalam kondisi disabled saat diklik" } : recentFetch ? { type: "api_error", detail: recentFetch.message } : invalidFieldAtClick ? { type: "validation_error", detail: "Field invalid terdeteksi di form terdekat" } : this.recentDomChanges.length ? { type: "dom_feedback", detail: this.recentDomChanges.slice(-1)[0] } : { type: "no_feedback", detail: "Tidak ada feedback UI terdeteksi setelah klik" };
903
+ const recentError = this.breadcrumbs.filter(
904
+ (b) => b.type === "error" && clickTime - new Date(b.timestamp).getTime() <= RAGE_CLICK_WINDOW_MS
905
+ ).slice(-1)[0];
906
+ const possibleReason = disabledAtClick ? { type: "disabled", detail: "Button dalam kondisi disabled saat diklik" } : ariaDisabledAtClick ? {
907
+ type: "aria_disabled",
908
+ detail: 'Element memiliki aria-disabled="true" (disabled secara visual/aksesibilitas, bukan native disabled attribute)'
909
+ } : pointerEventsBlockedAtClick ? {
910
+ type: "pointer_events_blocked",
911
+ detail: "Element memiliki CSS pointer-events: none saat diklik (tidak bisa menerima event klik sama sekali)"
912
+ } : overlayInterceptedAtClick ? {
913
+ type: "overlay_intercepted",
914
+ detail: `Element lain (${getElementSelector(overlayInterceptedAtClick)}) menutupi target saat klik terjadi, klik tidak sampai ke target aslinya`
915
+ } : recentFetch ? { type: "api_error", detail: recentFetch.message } : invalidFieldAtClick ? {
916
+ type: "validation_error",
917
+ detail: "Field invalid terdeteksi di form terdekat"
918
+ } : recentError ? { type: "js_error", detail: recentError.message } : this.recentDomChanges.length ? { type: "dom_feedback", detail: this.recentDomChanges.slice(-1)[0] } : {
919
+ type: "no_feedback",
920
+ detail: "Tidak ada feedback UI terdeteksi setelah klik"
921
+ };
788
922
  const formattedLog = formatClickLog({
789
923
  fnName: "interceptClick.rageClick",
790
924
  tagName,
@@ -911,6 +1045,7 @@ var Logger = class {
911
1045
  y: window.scrollY,
912
1046
  width: window.innerWidth,
913
1047
  height: window.innerHeight,
1048
+ imageTimeout: 3e3,
914
1049
  onclone: (clonedDoc) => {
915
1050
  if (!canApplyHighlight) return;
916
1051
  const clonedTarget = clonedDoc.querySelector(`[data-ga-highlight="${HIGHLIGHT_MARKER}"]`);