@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.mjs CHANGED
@@ -185,6 +185,62 @@ var parseQueryParams = (url) => {
185
185
  return null;
186
186
  }
187
187
  };
188
+ var ALWAYS_LOG_ENDPOINTS = [
189
+ { method: "POST", pattern: "/mytrip/seat/payment" },
190
+ { method: "POST", pattern: "/seat/remove/{bookingCode}" },
191
+ { method: "POST", pattern: "/seat/map/store/{bookingCode}" },
192
+ { method: "GET", pattern: "/seat/summary/{bookingCode}" },
193
+ { method: "POST", pattern: "/mytrip/baggage/save/{bookingCode}" },
194
+ { method: "POST", pattern: "/mytrip/baggage-wtp/save/{bookingCode}" },
195
+ { method: "GET", pattern: "/mytrip/baggage/list/{trx}" },
196
+ { method: "GET", pattern: "/mytrip/baggage/list/{trx}" },
197
+ { method: "POST", pattern: "/reschedule/list" },
198
+ { method: "POST", pattern: "/reschedule/confirmation/{bookingCode}" },
199
+ { method: "POST", pattern: "/reschedule/cancel" },
200
+ { method: "GET", pattern: "/reschedule/resend-otp/{bookingCode}" },
201
+ { method: "GET", pattern: "/reschedule/generate-otp/{bookingCode}" },
202
+ { method: "POST", pattern: "/refund/upload-file" },
203
+ { method: "POST", pattern: "/refund/submit-voluntary" },
204
+ { method: "POST", pattern: "/refund/reupload-file" },
205
+ { method: "POST", pattern: "/refund/price-different/confirmation" },
206
+ { method: "POST", pattern: "/refund/list" },
207
+ { method: "POST", pattern: "/refund/list-chatbot" },
208
+ { method: "POST", pattern: "/refund/detail" },
209
+ { method: "POST", pattern: "/refund/confirmation" },
210
+ { method: "POST", pattern: "/refund/calculate" },
211
+ { method: "GET", pattern: "/refund/resend-otp/{bookingCode}" },
212
+ { method: "GET", pattern: "/refund/generate-otp/{bookingCode}" },
213
+ { method: "GET", pattern: "/refund/checkEligibility/{orderId}" },
214
+ { method: "GET", pattern: "/refund/bank-list" },
215
+ { method: "POST", pattern: "/dapi/booking/insurance/remove" },
216
+ { method: "POST", pattern: "/dapi/booking/insurance/acceptance" },
217
+ { method: "POST", pattern: "/addon/service-insurance/{bookingCode}" },
218
+ { method: "POST", pattern: "/specialRequest/gps/products/{bookingCode}" },
219
+ { method: "POST", pattern: "/ancill/specialRequest/gps/products/{bookingCode}" },
220
+ { method: "POST", pattern: "/ancill/specialRequest/payment/v1/ancillary_gps/pay" },
221
+ { method: "POST", pattern: "/ancill/specialRequest/ancillary/save_gps/{bookingCode}" },
222
+ { method: "POST", pattern: "/specialRequest/deprecated/service/{bookingCode}" },
223
+ { method: "POST", pattern: "/ancill/specialRequest/payment/v1/ancillary_lounge/pay" },
224
+ { method: "POST", pattern: "/ancill/specialRequest/ancillary/save_lounge/{bookingCode}" }
225
+ ];
226
+ var ALWAYS_LOG_MATCHERS = ALWAYS_LOG_ENDPOINTS.map((entry) => ({
227
+ method: entry.method.toUpperCase(),
228
+ regex: new RegExp(
229
+ "^" + entry.pattern.split("/").map(
230
+ (seg) => seg.startsWith("{") && seg.endsWith("}") ? "[^/]+" : seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
231
+ ).join("/") + "/?$"
232
+ )
233
+ }));
234
+ function shouldAlwaysLogEndpoint(url, method) {
235
+ let pathname;
236
+ try {
237
+ pathname = new URL(url, window.location.origin).pathname;
238
+ } catch {
239
+ pathname = url;
240
+ }
241
+ const upperMethod = method.toUpperCase();
242
+ return ALWAYS_LOG_MATCHERS.some((m) => m.method === upperMethod && m.regex.test(pathname));
243
+ }
188
244
  var Logger = class {
189
245
  constructor() {
190
246
  this.config = null;
@@ -373,6 +429,7 @@ var Logger = class {
373
429
  const key = this.buildErrorKey("onerror", finalMessage);
374
430
  if (this.loggedErrorKeys.has(key)) return false;
375
431
  this.loggedErrorKeys.add(key);
432
+ this.addBreadcrumb("error", finalMessage, { source: trimmedSource, lineno, colno });
376
433
  void this.error({
377
434
  fnName: "window.onerror",
378
435
  messageError: new Error(finalMessage),
@@ -386,6 +443,7 @@ var Logger = class {
386
443
  const key = this.buildErrorKey("unhandledrejection", msg);
387
444
  if (this.loggedErrorKeys.has(key)) return;
388
445
  this.loggedErrorKeys.add(key);
446
+ this.addBreadcrumb("error", msg);
389
447
  void this.error({
390
448
  fnName: "window.unhandledrejection",
391
449
  messageError: new Error(msg)
@@ -400,6 +458,7 @@ var Logger = class {
400
458
  const key = this.buildErrorKey("console.error", trimmedMsg);
401
459
  if (this.loggedErrorKeys.has(key)) return;
402
460
  this.loggedErrorKeys.add(key);
461
+ this.addBreadcrumb("error", trimmedMsg);
403
462
  const originalError = args.find((a) => a instanceof Error);
404
463
  const errorToSend = originalError ?? new Error(trimmedMsg);
405
464
  void this.error({
@@ -506,7 +565,8 @@ var Logger = class {
506
565
  status: response.status,
507
566
  duration: `${duration}ms`
508
567
  });
509
- if (response.status >= 400) {
568
+ const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
569
+ if (!isLocalhost && response.status >= 400) {
510
570
  const dedupKey = this.buildErrorKey("interceptFetch", `${method} ${url} ${response.status}`);
511
571
  if (!this.loggedErrorKeys.has(dedupKey)) {
512
572
  this.loggedErrorKeys.add(dedupKey);
@@ -518,8 +578,8 @@ var Logger = class {
518
578
  } catch {
519
579
  responseBody = "[unable to read response body]";
520
580
  }
521
- const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
522
- if (!isLocalhost) {
581
+ const isLocalhost2 = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
582
+ if (!isLocalhost2) {
523
583
  this.error({
524
584
  fnName: "interceptFetch",
525
585
  messageError: new Error(`HTTP ${response.status} ${response.statusText} \u2014 ${url}`),
@@ -536,6 +596,32 @@ var Logger = class {
536
596
  });
537
597
  }
538
598
  }
599
+ } else if (!isLocalhost && response.status < 400 && shouldAlwaysLogEndpoint(url, method)) {
600
+ const dedupKey = this.buildErrorKey("interceptFetch", `${method} ${url} ${response.status}`);
601
+ if (!this.loggedErrorKeys.has(dedupKey)) {
602
+ this.loggedErrorKeys.add(dedupKey);
603
+ let responseBody = null;
604
+ try {
605
+ const cloned = response.clone();
606
+ const contentType = cloned.headers.get("content-type") ?? "";
607
+ responseBody = contentType.includes("application/json") ? await cloned.json() : await cloned.text();
608
+ } catch {
609
+ responseBody = "[unable to read response body]";
610
+ }
611
+ this.info({
612
+ fnName: "interceptFetch",
613
+ endpointName: url,
614
+ payload: {
615
+ method,
616
+ status: response.status,
617
+ statusText: response.statusText,
618
+ duration: `${duration}ms`,
619
+ query: parseQueryParams(url),
620
+ request: parseRequestBody(init?.body),
621
+ response: responseBody
622
+ }
623
+ });
624
+ }
539
625
  }
540
626
  return response;
541
627
  } catch (networkError) {
@@ -605,7 +691,8 @@ var Logger = class {
605
691
  status,
606
692
  duration: `${duration}ms`
607
693
  });
608
- if (status >= 400) {
694
+ const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
695
+ if (!isLocalhost && status >= 400) {
609
696
  const dedupKey = self.buildErrorKey("interceptXHR", `${method} ${url} ${status}`);
610
697
  if (self.loggedErrorKeys.has(dedupKey)) return;
611
698
  self.loggedErrorKeys.add(dedupKey);
@@ -614,8 +701,8 @@ var Logger = class {
614
701
  responseBody = JSON.parse(this.responseText);
615
702
  } catch {
616
703
  }
617
- const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
618
- if (!isLocalhost) {
704
+ const isLocalhost2 = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
705
+ if (!isLocalhost2) {
619
706
  void self.error({
620
707
  fnName: "interceptXHR",
621
708
  messageError: new Error(`HTTP ${status} ${this.statusText} \u2014 ${url}`),
@@ -631,6 +718,28 @@ var Logger = class {
631
718
  }
632
719
  });
633
720
  }
721
+ } else if (!isLocalhost && status < 400 && shouldAlwaysLogEndpoint(url, method)) {
722
+ const dedupKey = self.buildErrorKey("interceptXHR", `${method} ${url} ${status}`);
723
+ if (self.loggedErrorKeys.has(dedupKey)) return;
724
+ self.loggedErrorKeys.add(dedupKey);
725
+ let responseBody = this.responseText;
726
+ try {
727
+ responseBody = JSON.parse(this.responseText);
728
+ } catch {
729
+ }
730
+ void self.info({
731
+ fnName: "interceptXHR",
732
+ endpointName: url,
733
+ payload: {
734
+ method,
735
+ status,
736
+ statusText: this.statusText,
737
+ duration: `${duration}ms`,
738
+ query: parseQueryParams(url),
739
+ request: requestBody,
740
+ response: responseBody
741
+ }
742
+ });
634
743
  }
635
744
  });
636
745
  this.addEventListener("error", () => {
@@ -679,6 +788,13 @@ var Logger = class {
679
788
  });
680
789
  const disabledAtClick = this.isElementDisabled(target);
681
790
  const invalidFieldAtClick = this.hasInvalidFieldNearby(target);
791
+ const ariaDisabledAtClick = target.getAttribute("aria-disabled") === "true";
792
+ const pointerEventsBlockedAtClick = window.getComputedStyle(target).pointerEvents === "none";
793
+ const overlayInterceptedAtClick = (() => {
794
+ const elAtPoint = document.elementFromPoint(event.clientX, event.clientY);
795
+ if (!elAtPoint || elAtPoint === target || target.contains(elAtPoint)) return null;
796
+ return elAtPoint;
797
+ })();
682
798
  const clickTime = Date.now();
683
799
  const domActivityAtClick = this.lastDomActivity;
684
800
  const DEAD_CLICK_DELAY_MS = 4e3;
@@ -747,7 +863,25 @@ var Logger = class {
747
863
  const recentFetch = this.breadcrumbs.filter(
748
864
  (b) => b.type === "fetch" && clickTime - new Date(b.timestamp).getTime() <= RAGE_CLICK_WINDOW_MS
749
865
  ).slice(-1)[0];
750
- 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" };
866
+ const recentError = this.breadcrumbs.filter(
867
+ (b) => b.type === "error" && clickTime - new Date(b.timestamp).getTime() <= RAGE_CLICK_WINDOW_MS
868
+ ).slice(-1)[0];
869
+ const possibleReason = disabledAtClick ? { type: "disabled", detail: "Button dalam kondisi disabled saat diklik" } : ariaDisabledAtClick ? {
870
+ type: "aria_disabled",
871
+ detail: 'Element memiliki aria-disabled="true" (disabled secara visual/aksesibilitas, bukan native disabled attribute)'
872
+ } : pointerEventsBlockedAtClick ? {
873
+ type: "pointer_events_blocked",
874
+ detail: "Element memiliki CSS pointer-events: none saat diklik (tidak bisa menerima event klik sama sekali)"
875
+ } : overlayInterceptedAtClick ? {
876
+ type: "overlay_intercepted",
877
+ detail: `Element lain (${getElementSelector(overlayInterceptedAtClick)}) menutupi target saat klik terjadi, klik tidak sampai ke target aslinya`
878
+ } : recentFetch ? { type: "api_error", detail: recentFetch.message } : invalidFieldAtClick ? {
879
+ type: "validation_error",
880
+ detail: "Field invalid terdeteksi di form terdekat"
881
+ } : recentError ? { type: "js_error", detail: recentError.message } : this.recentDomChanges.length ? { type: "dom_feedback", detail: this.recentDomChanges.slice(-1)[0] } : {
882
+ type: "no_feedback",
883
+ detail: "Tidak ada feedback UI terdeteksi setelah klik"
884
+ };
751
885
  const formattedLog = formatClickLog({
752
886
  fnName: "interceptClick.rageClick",
753
887
  tagName,
@@ -874,6 +1008,7 @@ var Logger = class {
874
1008
  y: window.scrollY,
875
1009
  width: window.innerWidth,
876
1010
  height: window.innerHeight,
1011
+ imageTimeout: 3e3,
877
1012
  onclone: (clonedDoc) => {
878
1013
  if (!canApplyHighlight) return;
879
1014
  const clonedTarget = clonedDoc.querySelector(`[data-ga-highlight="${HIGHLIGHT_MARKER}"]`);