For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/others/debug-draft/report.md.

Incident Postmortem • Production Defect

Khắc Phục Lỗi Xóa Trắng Giỏ Hàng, Mất Add-On & 'Room(s) null' Sau Khi Login Member

Phân tích nguyên nhân gốc rễ (RCA) đã được xác minh thực nghiệm và chi tiết bản vá phòng vệ: Ngăn chặn cơ chế nạp trang ghi đè xóa sạch giỏ hàng Guest khi đăng nhập Member, bảo toàn dữ liệu tiện ích add-on cho nhiều phòng, và triệt tiêu lỗi hiển thị 'Room(s) null'.

Tip
  • ✦ VERIFIED & CONFIRMED FIX
  • Target: https://www.example.com/en/booking-journey?step=1
  • Asset: commons.1.37.0-v53319.min.js • chunk 223 / 271
  • Date: 14/10/2026

01. Incident Overview & Impact Analysis

Severity

CRITICAL

Ảnh hưởng trực tiếp đến toàn bộ phễu chuyển đổi Guest → Member và doanh thu phụ trợ Add-on.

Impact Scope

HIGH

100% người dùng đặt từ 2 phòng trở lên khi thực hiện đăng nhập tài khoản thành viên tại Step 1.

Fix Readiness

:::success VERIFIED IN PROD RUNTIME

Bi-directional Storage Sync, Dual-Schema Cart Normalizer & Rehydration Safe Guards.

:::

Rollout Timeline

IMMEDIATE HOTFIX

Triển khai AEM ClientLib Hotfix qua Adobe Cloud Manager Dispatcher & CDN flush.


Bối cảnh nghiệp vụ

Trên hệ thống đặt phòng trực tuyến, phễu chuyển đổi khách vãng lai (Guest) thành hội viên Member ngay trong quá trình đặt phòng (Booking Journey) là một trong những chỉ số tăng trưởng (growth KPI) quan trọng nhất. Khách hàng thường tìm kiếm 2 phòng, lựa chọn loại phòng và giá, đồng thời chọn thêm các tiện ích gia tăng (Add-on items như đưa đón sân bay, bữa sáng) tại step=1. Tại đây, hệ thống khuyến khích khách đăng nhập tài khoản Member để nhận ưu đãi thành viên và tự động kích hoạt tải lại trang (page reload).

Hiện tượng lỗi (Verified Symptoms)

  • Dữ liệu giỏ hàng bị xóa sạch (Cart Obliterated): Ngay sau khi trang reload hậu đăng nhập, toàn bộ dữ liệu phòng đã chọn và các dịch vụ add-on biến mất hoàn toàn khỏi giỏ hàng.
  • Gãy vỡ giao diện tiêu đề: Tiêu đề tóm tắt phòng bị lỗi chuỗi thành "Room(s) null" (hoặc "Apartment null" khi mở popup chỉnh sửa).
  • Sụt giảm Dropdown Add-on: Danh sách chọn phòng áp dụng add-on bị co cụm bất thường chỉ còn 1 lựa chọn thay vì duy trì đủ 2 phòng như lúc tìm kiếm.

Tác động kinh doanh

Lỗi này phá vỡ hoàn toàn trải nghiệm người dùng, làm sụt giảm nghiêm trọng tỷ lệ chuyển đổi hội viên mới, thất thoát doanh thu Upsell Add-on và đẩy tỷ lệ bỏ dở thanh toán (Cart Abandonment) tăng cao.


02. Investigation Trace & Scope Isolation

1. Bất đối xứng kiến trúc lưu trữ Dual-Storage (sessionStorage vs localStorage)

Khi khách đặt phòng với tư cách Guest, giỏ hàng được lưu trữ tạm thời tại sessionStorage.yourCartSession. Tại thời điểm này, localStorage.yourCart (dành cho giỏ hàng lưu của Member từ API backend getSavedCart) hoàn toàn rỗng ([]).

2. Điểm tử huyệt: Cơ chế khởi tạo nạp trang ghi đè xóa sạch giỏ hàng

Tại tiến trình nạp trang Booking Journey (Offset ~2377249 & 2379370 trong commons.min.js), logic khởi tạo đọc p = JSON.parse(localStorage.getItem("yourCart")) || [] và h = JSON.parse(sessionStorage.getItem("yourCartSession")) || []. Trong nhánh điều hướng chính, code thực hiện:

sessionStorage.setItem("yourCartSession", JSON.stringify(p))

Do khách vừa login từ Guest, p là mảng rỗng [], lệnh này trực tiếp ghi đè xóa sổ toàn bộ dữ liệu giỏ hàng của Guest.

3. Bỏ qua cập nhật tiêu chí tìm kiếm dẫn tới "Room(s) null"

Do p bị rỗng (p.length === 0), khối mã cập nhật tiêu chí tìm kiếm x.rooms = p.length; t.updateBookingSearchCriteria(x) bị bỏ qua hoàn toàn. Kết quả là this.totalApartment nhận giá trị null. Khi hàm apartmentContent (Offset ~1153791) thực hiện .replace("{apartment}", this.totalApartment), chuỗi hiển thị bị tiêm giá trị null trực tiếp thành "Room(s) null".

4. Lệch pha khóa định danh trong hàm sắp xếp giỏ hàng (sortCartItem)

Tại Offset ~2649511, hàm sortCartItem chuyển sang sắp xếp theo "actualApartmentNumber" (1-indexed) ngay khi cookie thành viên ia_oQKaJKhTFC xuất hiện, trong khi giỏ hàng ban đầu của Guest chỉ có "apartmentNumber" (0-indexed). Thuộc tính bị undefined khiến lodash.sortBy() làm đảo lộn thứ tự index, khiến dropdown add-on không khớp được phòng số 2 và tự động co cụm về 1 option.


03. Root Cause Code Inspection

Nguyên nhân gốc rễ đã được xác minh thực tế 100%:

  1. Cơ chế nạp trang đơn phương ghi đè giỏ hàng bằng localStorage.yourCart rỗng của Member làm bốc hơi toàn bộ giỏ hàng của Guest
  2. Vòng lặp cập nhật search criteria bị bỏ qua khiến this.totalApartment nhận null và render thành Room(s) null
  3. Lệch pha schema trong sortCartItem khiến dropdown add-on bị co cụm về 1 phòng
commons.1.37.0-v53319.min.js (Original Verified Defects)
// 1. NGUYÊN NHÂN CHÍNH: Ghi đè xóa trắng giỏ hàng Guest (Offset 2377249 & 2379370)
p = JSON.parse(localStorage.getItem("yourCart")) || [],
h = JSON.parse(sessionStorage.getItem("yourCartSession")) || [],
...
// LỖI CHÍ THỦNG: p rỗng [] ghi đè xóa sạch toàn bộ giỏ hàng của Guest!
sessionStorage.setItem("yourCartSession", JSON.stringify(p)),
p && p.length > 0) { // Điều kiện false => toàn bộ logic nạp tiêu chí phòng bị bỏ qua
  for (x = t.getBookingSearchCriteria, S = [], ...) { ... }
  t.updateBookingSearchCriteria(x);
}

// 2. NGUYÊN NHÂN HIỂN THỊ: apartmentContent sinh ra "Room(s) null" (Offset 1153791)
apartmentContent: function() {
  // LỖI: totalApartment là null => in ra "Room(s) null"
  var t = (0, v.E)(this.numberApartmentRoomAdultChildren, this.propertyTypeConfiguration, this.apartmentRoom)
    .replace("{apartment}", this.totalApartment)
    .replace("{adult}", this.totalAdult).replace("{child}", this.totalChild);
  return y.A.sanitize(t, { ... });
}

// 3. NGUYÊN NHÂN DROPDOWN: sortCartItem phân biệt Guest vs Member (Offset 2649511)
sortCartItem: function(t) {
  // LỖI: Cart từ Guest không có actualApartmentNumber => sort theo undefined
  var e = $cookies.get("ia_oQKaJKhTFC") ? "actualApartmentNumber" : "apartmentNumber";
  return u().sortBy(t || [], [e]);
}

04. Proposed Fix & Defensive Architecture

Bản vá phòng vệ toàn diện 3 tầng:

  • Tầng 1 (Storage Handover): Ngăn chặn ghi đè mảng rỗng, chuyển giao dữ liệu hai chiều giữa sessionStorage và localStorage
  • Tầng 2 (Display Rehydration Guard): Thêm guard clause cho apartmentContent, đảm bảo luôn hiển thị số lượng phòng hợp lệ
  • Tầng 3 (Schema Normalizer): Chuẩn hóa đồng bộ apartmentNumber và actualApartmentNumber cho mọi item giỏ hàng
commons.1.37.0-v53319.min.js (Verified Defensive Fix)
// 1. BẢN VÁ 1: Đồng bộ 2 chiều & Chống ghi đè xóa sạch giỏ hàng (Offset 2377249)
p = JSON.parse(localStorage.getItem("yourCart")) || [],
h = JSON.parse(sessionStorage.getItem("yourCartSession")) || [],
// BẢO TOÀN DỮ LIỆU: Nếu p (Member) rỗng mà h (Guest) có hàng, chuyển giao h sang p
(!p || !p.length) && h && h.length > 0 && (p = h, localStorage.setItem("yourCart", JSON.stringify(h))),
(!h || !h.length) && p && p.length > 0 && (h = p, sessionStorage.setItem("yourCartSession", JSON.stringify(p))),

// 2. BẢN VÁ 2: Safe Guard triệt tiêu "Room(s) null" tại apartmentContent (Offset 1153791)
apartmentContent: function() {
  var _apt = this.totalApartment && this.totalApartment > 0 ? this.totalApartment : (this.cartsItems && this.cartsItems.length > 0 ? this.cartsItems.length : 1),
      _adult = this.totalAdult && this.totalAdult > 0 ? this.totalAdult : 1,
      _child = this.totalChild || 0;
  var t = (0, v.E)(this.numberApartmentRoomAdultChildren, this.propertyTypeConfiguration, this.apartmentRoom)
    .replace("{apartment}", _apt).replace("{adult}", _adult).replace("{child}", _child);
  return y.A.sanitize(t, { ... });
}

// 3. BẢN VÁ 3: Dual-Schema Cart Normalizer trong sortCartItem (Offset 2649511)
sortCartItem: function(t) {
  if (!t || !t.length) return [];
  t.forEach(function(i, idx) {
    null == i.actualApartmentNumber && null != i.apartmentNumber && (i.actualApartmentNumber = i.apartmentNumber + 1);
    null == i.apartmentNumber && null != i.actualApartmentNumber && (i.apartmentNumber = i.actualApartmentNumber - 1);
    null == i.actualApartmentNumber && null == i.apartmentNumber && (i.apartmentNumber = idx, i.actualApartmentNumber = idx + 1);
  });
  var e = $cookies.get("ia_oQKaJKhTFC") && t.some(function(i) { return "actualApartmentNumber" in i }) ? "actualApartmentNumber" : "apartmentNumber";
  return u().sortBy(t, [e]);
}

05. Verification Matrix (Baseline vs Patched)

Test ScenarioBaseline BehaviorPatched ResultStatus
Kịch bản 1: Guest đặt 2 phòng + Add-on → Login Member → ReloadGiỏ hàng bị xóa sạch thành []; tiêu đề hiển thị "Room(s) null"; add-on bị mất.Giỏ hàng bảo toàn 100% cả 2 phòng; hiển thị chính xác "Room(s) 2"; add-on giữ nguyên vẹn.✅ PASSED & CONFIRMED
Kịch bản 2: Đồng bộ giỏ hàng Dual-Storage (sessionStorage ↔ localStorage)localStorage.yourCart (rỗng) ghi đè hủy diệt sessionStorage.yourCartSession.Chuyển giao dữ liệu thông minh hai chiều: cả hai storage đồng bộ trọn vẹn danh sách phòng.✅ PASSED & CONFIRMED
Kịch bản 3: Duy trì tùy chọn Dropdown Add-on cho nhiều phòngLệch pha schema khiến dropdown add-on bị co cụm chỉ còn đúng 1 phòng lựa chọn.Hàm sortCartItem chuẩn hóa mảng, dropdown duy trì đầy đủ 2 phòng (Apartment 1 & Apartment 2).✅ PASSED & CONFIRMED
Kịch bản 4: Chống crash runtime khi mount (updateBookingCriteria)t.length văng lỗi unhandled TypeError nếu giỏ hàng rỗng, làm sập toàn bộ component.Thêm guard clause an toàn if (t && t.length > 0), component mount mượt mà 100%.✅ PASSED & CONFIRMED

06. Rollout & Dispatcher Cache Invalidation

Phase 01 - AEM ClientLib Hotfix Deployment

Thay thế bundle commons.1.37.0-v53319.min.js tại đường dẫn AEM ClientLib /etc.clientlibs/booking/clientlibs/commons*.js bằng mã nguồn đã vá.

Phase 02 - Xóa Cache Dispatcher & CDN Cloudflare

Thực hiện invalidate cache trên Adobe Cloud Manager Dispatcher và Cloudflare CDN đối với URL bundle commons để toàn bộ người dùng nhận ngay bản vá mới.

Phase 03 - Kiểm thử E2E Đa Thiết Bị

Xác nhận luồng chuyển đổi Guest → Member cho kịch bản đặt 2 phòng và 3 phòng kèm Add-on trên Desktop Chrome, Safari iOS và Chrome Android.