Bỏ qua để đến nội dung

Debugging & điều tra bug

Đi từ ngoài vào trong — mỗi bước loại trừ một tầng nguyên nhân:

  1. Password có thực sự lưu chưa? “Test connection” (POST /api/helpdesk/connections/test-imap) là test-only cho credential — không tự lưu imap_host/username/password vào hd_connections. Nếu bạn chỉ bấm Test rồi rời trang mà chưa bấm Save, connection thật vẫn rỗng. Ngoại lệ: nếu test thành công và trả về danh sách folder, nó persist side-effect available_folders (để Folder tab hiển thị) — không phải toàn bộ credential:

    connections.controller.ts:84-93
    if (result.ok && Array.isArray(result.folders) && b.id) {
    try {
    const svc = await createItemsService(ctx, 'hd_connections');
    await svc.updateOne(Number(b.id), { available_folders: result.folders.map((p: string) => ({ path: p })) }, { emitEvents: false });
    } catch (err: any) {
    ctx.log?.warn?.({ err: err?.message, id: b.id }, 'helpdesk persist available_folders failed');
    }
    }
  2. Kiểm tra qua APIGET /api/helpdesk/connections (BFF có sẵn, cùng route dùng cho UI):

    Terminal window
    curl -s 'http://localhost:8888/api/helpdesk/connections' \
    -H 'Cookie: <copy cookie từ DevTools sau khi login>' | jq

    Xem đúng field: inbound_provider (phải là imap, không phải rỗng/null), status (không được là error/disconnected/disabled), needs_reauth (phải null hoặc false) — cron poll loại bỏ connection không thoả:

    imap-source.ts:26-29
    const conns: HdConnection[] = await ctx.database('hd_connections')
    .where({ inbound_provider: 'imap' })
    .whereNotIn('status', ['error', 'disconnected', 'disabled'])
    .where((qb: any) => qb.whereNull('needs_reauth').orWhere('needs_reauth', false));
  3. First-sync watermark có “nuốt mất” mail cũ không? Lần đầu bật connection (hoặc imap_uidvalidity đổi vì mailbox bị rebuild), pipeline không kéo mail cũ — nó set watermark = UID mới nhất rồi return ngay, chỉ mail đến sau thời điểm bật mới được ingest:

    imap-source.ts:60-71
    let lastUid = Number(conn.imap_last_uid ?? 0);
    const validityChanged = uidValidity !== Number(conn.imap_uidvalidity ?? 0);
    if (!lastUid || validityChanged) {
    lastUid = Math.max(0, uidNext - 1);
    await updateWatermark(ctx, conn.id, { imap_uidvalidity: uidValidity, imap_last_uid: lastUid });
    await touchSynced(ctx, conn.id);
    return;
    }

    Nếu bạn gửi mail test trước khi bật connection lần đầu, mail đó vĩnh viễn không được ingest — gửi mail mới sau khi connection đã ở trạng thái polling ổn định.

  4. Folder đúng chưa? Folder IMAP polled lấy từ cấu hình inbox — nếu mailbox có nhiều folder (Inbox, Spam, Promotions…) và cấu hình trỏ sai folder, mail vẫn nằm nguyên trên server, không lỗi gì cả.

  5. Sender filter có chặn không? Mỗi inbox có inbound_filters (allow/deny + rule email/domain/pattern) trong channel_config. Người gửi test có nằm trong deny hoặc ngoài allow — mail bị drop trước khi tạo contact/conversation, không để lại log lỗi ở UI.

  6. Plus-address có khớp inbox không? Nếu một connection dùng chung cho nhiều inbox, routing dựa vào địa chỉ nhận khớp receiving_address (support+project1@…). Không khớp inbox nào → drop hoàn toàn (dropped: no-inbox-match), không rơi nhầm sang inbox khác.

  7. Message-ID trùng? Dedup theo external_message_id — gửi lại đúng 1 email (test bằng cách forward lại chính nó) sẽ bị skip là duplicate, không phải bug.

Xem chi tiết đầy đủ pipeline 8 bước ở Inbound: IMAP → ticket.

  • SLA không realtimerecomputeSlaStatus() chạy qua cron * * * * * (mỗi phút), không phải mỗi lần đọc. Sau khi đổi SLA policy hoặc working hours, đợi tối đa ~60s rồi mới thấy badge cập nhật.
  • Counts sidebar đến từ endpoint GET /helpdesk/conversations/counts (Knex aggregate — COUNT GROUP BY), không tính client-side nữa. Client chỉ refresh counts thủ công (sidebar mount) hoặc mỗi 60s qua SLA ticker — nếu vừa đổi filter/label mà counts chưa cập nhật, đó là do chưa tới tick refresh, không phải bug tính sai.
  • Đối chiếu bằng cách gọi trực tiếp GET /api/helpdesk/conversations/counts và so với SELECT COUNT(*) ... GROUP BY status thủ công trên DB nếu nghi ngờ sai số.

Permission cache 60s

app-access.ts cache quyền theo policy set trong Map in-memory, TTL 60s, key = policy id đã sort. Vừa đổi quyền cho user mà vẫn 403 → đợi tối đa 60s hoặc restart process (cache không invalidate theo event, chỉ hết hạn theo TTL).

BFF thiếu token

Mọi route BFF assertPermission trước, userApi.request sau — 401 ở đây thường là session cookie hết hạn hoặc chưa login, không phải lỗi logic helpdesk.

SSR $fetch không cookie

use-helpdesk-counts.ts cố tình không chạy khi SSR$fetch phía server không forward cookie auth → 401 giả, rồi state rỗng bị serialize xuống client khiến client tưởng đã load xong.

app-access.ts — cache 60s per-process
const cache = new Map<string, CachedPermissions>();
const CACHE_TTL_MS = 60_000;
const cacheKey = getCacheKey(policyIds);
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
if (hasMatch(cached.records, module, action)) return;
throw forbidden();
}
use-helpdesk-counts.ts:13-18 — SSR guard đúng cách
async function refresh() {
// Client-only: during SSR the server-side $fetch doesn't forward the auth
// cookie → 401, and the zeroed state + loaded flag would serialize down and
// stop the client from ever fetching.
if (import.meta.server) return
// ...
}

Nếu bạn tự viết composable mới gọi BFF và thấy 401 chỉ xảy ra ở lần render đầu (SSR) rồi tự hết sau khi client hydrate — thêm guard if (import.meta.server) return tương tự, đừng cố “fix” bằng cách forward cookie thủ công.

Phần tiêu đề “(d) Deep-link hoặc assignee “mất” sau khi update”

Triệu chứng: mở conversation qua URL trực tiếp (không phải click từ list), hoặc vừa đổi assignee/status xong quay lại thấy conversation “biến mất” khỏi view hiện tại.

Nguyên nhân: useHelpdeskConversations giữ items (mọi thứ store biết) tách khỏi pageIds (thứ tự trang server đang hiển thị). Nếu id không nằm trong trang đã fetch, getById trả undefined — nhìn như “mất dữ liệu” trong khi thực ra dữ liệu chưa từng được kéo về.

use-helpdesk-conversations.ts:91-99 — cách fix đúng: gọi ensureOne
async function ensureOne(id: string): Promise<Conversation | undefined> {
const found = getById(id)
if (found) return found
const conv = await api.getConversation(id).catch(() => null)
if (conv) _merge([conv])
return getById(id)
}

Khi thêm 1 trang mới cần mở conversation theo id từ URL (route param), luôn gọi ensureOne(id) trong onMounted/setup thay vì chỉ đọc getById(id) rồi coi undefined là lỗi.

(e) Playwright login chết sau khi restart backend

Phần tiêu đề “(e) Playwright login chết sau khi restart backend”

global-setup.ts luôn login lại từ đầu mỗi lần chạy pnpm exec playwright test — nó không tái sử dụng tests/.auth/state.json cũ, luôn ghi đè sau khi login thành công. Vì vậy chạy lại toàn bộ suite thường tự phục hồi.

Nếu vẫn gặp lỗi auth kỳ lạ giữa chừng (v.d. chạy 1 spec lẻ bằng --grep mà không qua globalSetup đường thường, hoặc nghi ngờ cookie cũ), xoá thủ công state để buộc login mới hoàn toàn:

Terminal window
rm -f admin/packages/helpdesk/tests/.auth/state.json
cd admin && pnpm exec playwright test

(f) Sửa backend extension không thấy hiệu lực

Phần tiêu đề “(f) Sửa backend extension không thấy hiệu lực”

Extension load một lần lúc boot — không có hot-reload. Sửa file trong backend/extensions/helpdesk/src/** mà không thấy thay đổi khi test là vì bạn quên 1 trong 2 bước:

Terminal window
# 1. Rebuild dist của extension
cd backend/extensions/helpdesk && node ../../node_modules/@codihaus/odp-extensions-sdk/dist/cli.js build
# 2. Restart backend process (không phải reload trang admin)
# → tuỳ setup: pm2 restart / kill process cũ rồi chạy lại lệnh start backend

Reload trang Admin (:8888) chỉ áp dụng thay đổi frontend (Nuxt dev server có HMR thật). Thay đổi backend luôn cần build + restart process.

Khi không chắc bug là do code mới hay do state môi trường (DB đã bị sửa tay, connection cũ bị hỏng…), dùng git stash để loại trừ nhanh:

Terminal window
git stash # tạm cất thay đổi đang làm dở
# rebuild + restart backend, test lại hành vi cũ
git stash pop # khôi phục thay đổi, so sánh

Nếu bug vẫn tái hiện sau git stash (tức là quay về code cũ mà vẫn lỗi) → nguyên nhân nằm ở môi trường/data, không phải thay đổi code đang làm — đỡ mất thời gian soi sai chỗ.