From 84176fd7bcb70cbeaf334ce01dd58dd9bafb6e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Thu, 24 Apr 2025 15:42:37 +0900 Subject: [PATCH 1/5] feat(console-query): create tanstack query debugger Signed-off-by: piggggggggy --- apps/web/src/App.vue | 10 +++ .../src/_dev-tools/vue-query-console-debug.ts | 87 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 apps/web/src/_dev-tools/vue-query-console-debug.ts diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index 95d5dde6b7..e60950e368 100755 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -6,6 +6,8 @@ import { import type { Location } from 'vue-router'; import { useRoute, useRouter } from 'vue-router/composables'; +import { useQueryClient } from '@tanstack/vue-query'; + import { LocalStorageAccessor } from '@cloudforet/core-lib/local-storage-accessor'; import { PNoticeAlert, PToastAlert, PIconModal, PSidebar, PDataLoader, @@ -37,6 +39,14 @@ import MobileGuideModal from '@/services/auth/components/MobileGuideModal.vue'; import { AUTH_ROUTE } from '@/services/auth/routes/route-constant'; import { LANDING_ROUTE } from '@/services/landing/routes/route-constant'; + + +if (import.meta.env.DEV) { + const queryClient = useQueryClient(); + import('@/_dev-tools/vue-query-console-debug').then((mod) => mod.initVueQueryConsoleDebug(queryClient)); +} + + const router = useRouter(); const route = useRoute(); diff --git a/apps/web/src/_dev-tools/vue-query-console-debug.ts b/apps/web/src/_dev-tools/vue-query-console-debug.ts new file mode 100644 index 0000000000..536b18a86e --- /dev/null +++ b/apps/web/src/_dev-tools/vue-query-console-debug.ts @@ -0,0 +1,87 @@ +import type { QueryClient, QueryKey } from '@tanstack/vue-query'; + +type QuerySummary = { + index: number; + queryKey: QueryKey; + gcTime: number; + observers: number; + data: unknown; +}; + +export function initVueQueryConsoleDebug(client: QueryClient) { + const _summarizeQueries = (): QuerySummary[] => client.getQueryCache().getAll().map((query, index) => ({ + index, + queryKey: query.queryKey, + gcTime: query.gcTime, + observers: query.getObserversCount?.() ?? query.observers.length, + data: query.state.data, + })); + + const debugObject = { + getSummary: _summarizeQueries, + dump: () => console.table(_summarizeQueries().map((q) => ({ + appContext: _extractAppContext(q.queryKey), + resourceIdentifier: _extractResourceIdentifier(q.queryKey), + contextKey: _extractContextKey(q.queryKey), + params: _extractParams(q.queryKey), + observers: q.observers, + }))), + log: () => { + _summarizeQueries().forEach((q, i) => { + console.group(`Query #${i + 1}`); + console.log('queryKey:', q.queryKey); + console.log('gcTime:', q.gcTime); + console.log('observers:', q.observers); + console.log('data:', q.data); + console.groupEnd(); + }); + }, + inspect: (index: number) => { + const q = _summarizeQueries()[index]; + if (!q) { + console.warn('Query index not found'); + return; + } + console.group(`Inspecting Query #${index}`); + console.log('queryKey:', q.queryKey); + console.log('gcTime:', q.gcTime); + console.log('observers:', q.observers); + console.log('data:', q.data); + console.groupEnd(); + }, + }; + + (window as any).__QUERY_DEBUG__ = debugObject; +} + +const _extractAppContext = (key: QueryKey): string => { + if (!Array.isArray(key) || key.length === 0) return ''; + if (key[0] === 'admin') return 'admin'; + if (key.length < 2) return ''; + if (key[0] === 'workspace') return `${key[1]}`; + return ''; +}; + +const _extractResourceIdentifier = (key: QueryKey): string => { + if (!Array.isArray(key) || key.length < 4) return ''; + if (key[0] === 'admin') return `${key[1]}/${key[2]}/${key[3]}`; + if (key.length < 5) return ''; + if (key[0] === 'workspace') return `${key[2]}/${key[3]}/${key[4]}`; + return ''; +}; + +const _extractContextKey = (key: QueryKey): string => { + if (!Array.isArray(key) || key.length < 5) return ''; + if (key[0] === 'admin' && typeof key[4] === 'string') return key[4]; + if (key.length < 6) return ''; + if (key[0] === 'workspace' && typeof key[5] === 'string') return key[5]; + return ''; +}; + +const _extractParams = (key: QueryKey): string => { + if (!Array.isArray(key) || key.length < 6) return ''; + if (key[0] === 'admin') return JSON.stringify(typeof key[5] === 'string' ? key[6] : key[5]); + if (key.length < 7) return ''; + if (key[0] === 'workspace') return JSON.stringify(typeof key[6] === 'string' ? key[7] : key[6]); + return ''; +}; From bac5204e8702ede8686edd088d087036c4b15858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Thu, 24 Apr 2025 17:35:39 +0900 Subject: [PATCH 2/5] fix(query-debugger): improve debugger Signed-off-by: piggggggggy --- .../src/_dev-tools/vue-query-console-debug.ts | 131 ++++++++++++++---- 1 file changed, 105 insertions(+), 26 deletions(-) diff --git a/apps/web/src/_dev-tools/vue-query-console-debug.ts b/apps/web/src/_dev-tools/vue-query-console-debug.ts index 536b18a86e..984ca88489 100644 --- a/apps/web/src/_dev-tools/vue-query-console-debug.ts +++ b/apps/web/src/_dev-tools/vue-query-console-debug.ts @@ -1,4 +1,4 @@ -import type { QueryClient, QueryKey } from '@tanstack/vue-query'; +import type { QueryClient, QueryKey, Query } from '@tanstack/vue-query'; type QuerySummary = { index: number; @@ -6,29 +6,85 @@ type QuerySummary = { gcTime: number; observers: number; data: unknown; + query: Query; + isActive: boolean; }; +interface DumpOptions { + appContext?: string; + resourceIdentifier?: string; + activeOnly?: boolean; + // TODO: Add sortBy, limit, showParamsSize, intentLabel support +} + export function initVueQueryConsoleDebug(client: QueryClient) { - const _summarizeQueries = (): QuerySummary[] => client.getQueryCache().getAll().map((query, index) => ({ - index, - queryKey: query.queryKey, - gcTime: query.gcTime, - observers: query.getObserversCount?.() ?? query.observers.length, - data: query.state.data, - })); + const _summarizeQueries = (): QuerySummary[] => client.getQueryCache().getAll() + .sort((a, b) => { + const isActiveA = _isActive(a); + const isActiveB = _isActive(b); + return Number(isActiveB) - Number(isActiveA); + }) + .map((query, index) => { + const data = query.state.data; + const observers = query.getObserversCount?.() ?? query.observers.length; + + return { + index: index + 1, + queryKey: query.queryKey, + gcTime: query.gcTime, + observers, + data, + query, + isActive: _isActive(query), + }; + }); const debugObject = { + /** + * Returns a raw summary of all queries in the cache. + */ getSummary: _summarizeQueries, - dump: () => console.table(_summarizeQueries().map((q) => ({ - appContext: _extractAppContext(q.queryKey), - resourceIdentifier: _extractResourceIdentifier(q.queryKey), - contextKey: _extractContextKey(q.queryKey), - params: _extractParams(q.queryKey), - observers: q.observers, - }))), + /** + * Logs a table summary of all queries. + * Includes appContext, resource path, context keys, and params. + */ + dump: (options?: DumpOptions) => { + let summary = _summarizeQueries().map((q) => ({ + index: q.index, + appContext: _extractAppContext(q.queryKey), + resourceIdentifier: _extractResourceIdentifier(q.queryKey), + contextKey: _extractContextKey(q.queryKey), + params: _extractParams(q.queryKey), + observers: q.observers, + isActive: q.isActive, + })); + if (options?.appContext) { + summary = summary.filter((q) => q.appContext.startsWith(options?.appContext ?? '')); + } + if (options?.resourceIdentifier) { + summary = summary.filter((q) => q.resourceIdentifier.startsWith(options?.resourceIdentifier ?? '')); + } + if (options?.activeOnly) { + summary = summary.filter((q) => q.isActive); + } + const result = Object.fromEntries(summary.map((q) => [`${q.index}`, { + appContext: q.appContext, + resourceIdentifier: q.resourceIdentifier, + contextKey: q.contextKey, + params: q.params, + observers: q.observers, + isActive: q.isActive, + }])); + + console.table(result); + }, + /** + * Logs full details for every query in the cache. + * Grouped and expanded in the browser console. + */ log: () => { - _summarizeQueries().forEach((q, i) => { - console.group(`Query #${i + 1}`); + _summarizeQueries().forEach((q) => { + console.group(`Query #${q.index}`); console.log('queryKey:', q.queryKey); console.log('gcTime:', q.gcTime); console.log('observers:', q.observers); @@ -36,10 +92,14 @@ export function initVueQueryConsoleDebug(client: QueryClient) { console.groupEnd(); }); }, + /** + * Logs details for a specific query by its index in the cache. + */ inspect: (index: number) => { - const q = _summarizeQueries()[index]; + const q = _summarizeQueries().find((query) => query.index === index); if (!q) { console.warn('Query index not found'); + (window as any).__QUERY_DEBUG__.dump(); return; } console.group(`Inspecting Query #${index}`); @@ -47,6 +107,7 @@ export function initVueQueryConsoleDebug(client: QueryClient) { console.log('gcTime:', q.gcTime); console.log('observers:', q.observers); console.log('data:', q.data); + console.log('query:', q.query); console.groupEnd(); }, }; @@ -54,6 +115,14 @@ export function initVueQueryConsoleDebug(client: QueryClient) { (window as any).__QUERY_DEBUG__ = debugObject; } + +const _isActive = (query: Query) => { + const data = query.state.data; + return (data !== undefined && query.state.dataUpdatedAt > 0) || query.options?.enabled === true; +}; + +/* --------------------- Extractors --------------------- */ + const _extractAppContext = (key: QueryKey): string => { if (!Array.isArray(key) || key.length === 0) return ''; if (key[0] === 'admin') return 'admin'; @@ -72,16 +141,26 @@ const _extractResourceIdentifier = (key: QueryKey): string => { const _extractContextKey = (key: QueryKey): string => { if (!Array.isArray(key) || key.length < 5) return ''; - if (key[0] === 'admin' && typeof key[4] === 'string') return key[4]; - if (key.length < 6) return ''; - if (key[0] === 'workspace' && typeof key[5] === 'string') return key[5]; - return ''; + const startIndex = key[0] === 'admin' ? 4 : 5; + const lastIndex = key.length - 1; + const hasParams = typeof key[lastIndex] === 'object' && !Array.isArray(key[lastIndex]) && key[lastIndex] !== null; + + const contextPart = hasParams ? key.slice(startIndex, lastIndex) : key.slice(startIndex); + const parts = contextPart.filter((k) => typeof k === 'string' || typeof k === 'number'); + + return parts.join(', '); }; const _extractParams = (key: QueryKey): string => { - if (!Array.isArray(key) || key.length < 6) return ''; - if (key[0] === 'admin') return JSON.stringify(typeof key[5] === 'string' ? key[6] : key[5]); - if (key.length < 7) return ''; - if (key[0] === 'workspace') return JSON.stringify(typeof key[6] === 'string' ? key[7] : key[6]); + if (!Array.isArray(key) || key.length < 5) return ''; + if (key[0] === 'admin') { + const lastNamespace = key[key.length - 1]; + return typeof lastNamespace === 'object' ? JSON.stringify(lastNamespace) : ''; + } + if (key.length < 6) return ''; + if (key[0] === 'workspace') { + const lastNamespace = key[key.length - 1]; + return typeof lastNamespace === 'object' ? JSON.stringify(lastNamespace) : ''; + } return ''; }; From 0730f18bc0af64bc8477ef3219ffdb38e0332c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Thu, 24 Apr 2025 17:50:46 +0900 Subject: [PATCH 3/5] chore: apply copilot review Signed-off-by: piggggggggy --- apps/web/src/App.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index e60950e368..f108ffe136 100755 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -43,7 +43,11 @@ import { LANDING_ROUTE } from '@/services/landing/routes/route-constant'; if (import.meta.env.DEV) { const queryClient = useQueryClient(); - import('@/_dev-tools/vue-query-console-debug').then((mod) => mod.initVueQueryConsoleDebug(queryClient)); + import('@/_dev-tools/vue-query-console-debug').then((mod) => mod.initVueQueryConsoleDebug(queryClient)) + .catch((error) => { + console.error('Failed to load vue-query-console-debug module:', error); + console.error('Ensure the module exists and the path is correct.'); + }); } From 31e12ca44718409c1622b7f906ad55d580711422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Thu, 24 Apr 2025 17:55:55 +0900 Subject: [PATCH 4/5] chore: small fix Signed-off-by: piggggggggy --- apps/web/src/_dev-tools/vue-query-console-debug.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/_dev-tools/vue-query-console-debug.ts b/apps/web/src/_dev-tools/vue-query-console-debug.ts index 984ca88489..1882f84b4b 100644 --- a/apps/web/src/_dev-tools/vue-query-console-debug.ts +++ b/apps/web/src/_dev-tools/vue-query-console-debug.ts @@ -17,7 +17,7 @@ interface DumpOptions { } -export function initVueQueryConsoleDebug(client: QueryClient) { +export const initVueQueryConsoleDebug = (client: QueryClient) => { const _summarizeQueries = (): QuerySummary[] => client.getQueryCache().getAll() .sort((a, b) => { const isActiveA = _isActive(a); @@ -113,7 +113,7 @@ export function initVueQueryConsoleDebug(client: QueryClient) { }; (window as any).__QUERY_DEBUG__ = debugObject; -} +}; const _isActive = (query: Query) => { From 2778dcf1c2e5f14e8886e802d4e176c7e0f1aff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Thu, 24 Apr 2025 18:01:28 +0900 Subject: [PATCH 5/5] chore: solve lint error Signed-off-by: piggggggggy --- apps/web/src/_dev-tools/vue-query-console-debug.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/_dev-tools/vue-query-console-debug.ts b/apps/web/src/_dev-tools/vue-query-console-debug.ts index 1882f84b4b..b973872650 100644 --- a/apps/web/src/_dev-tools/vue-query-console-debug.ts +++ b/apps/web/src/_dev-tools/vue-query-console-debug.ts @@ -118,7 +118,7 @@ export const initVueQueryConsoleDebug = (client: QueryClient) => { const _isActive = (query: Query) => { const data = query.state.data; - return (data !== undefined && query.state.dataUpdatedAt > 0) || query.options?.enabled === true; + return (data !== undefined && query.state.dataUpdatedAt > 0) || (query.options as { enabled?: boolean })?.enabled === true; }; /* --------------------- Extractors --------------------- */