Compare commits
3 Commits
ace231b93f
...
85f29d8b49
| Author | SHA1 | Date | |
|---|---|---|---|
| 85f29d8b49 | |||
| b8cd3716c4 | |||
| 48d5bdafa4 |
@@ -292,6 +292,8 @@ function AnalyticsContent() {
|
|||||||
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
|
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
|
||||||
// 添加标签名称状态 - 用于在UI中显示和API请求
|
// 添加标签名称状态 - 用于在UI中显示和API请求
|
||||||
const [selectedTagNames, setSelectedTagNames] = useState<string[]>([]);
|
const [selectedTagNames, setSelectedTagNames] = useState<string[]>([]);
|
||||||
|
// 添加子路径筛选状态
|
||||||
|
const [selectedSubpath, setSelectedSubpath] = useState<string>('');
|
||||||
// 添加分页状态
|
// 添加分页状态
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [pageSize, setPageSize] = useState<number>(10);
|
const [pageSize, setPageSize] = useState<number>(10);
|
||||||
@@ -377,6 +379,11 @@ function AnalyticsContent() {
|
|||||||
params.append('tagName', tagName);
|
params.append('tagName', tagName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加子路径筛选参数
|
||||||
|
if (selectedSubpath) {
|
||||||
|
params.append('subpath', selectedSubpath);
|
||||||
|
}
|
||||||
|
|
||||||
// 记录构建的 URL,以确保参数正确包含
|
// 记录构建的 URL,以确保参数正确包含
|
||||||
const summaryUrl = `${baseUrl}/summary?${params.toString()}`;
|
const summaryUrl = `${baseUrl}/summary?${params.toString()}`;
|
||||||
@@ -446,7 +453,7 @@ function AnalyticsContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize, selectedShortUrl, shouldFetchData]);
|
}, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, selectedSubpath, currentPage, pageSize, selectedShortUrl, shouldFetchData]);
|
||||||
|
|
||||||
// Function to clear the shorturl filter
|
// Function to clear the shorturl filter
|
||||||
const handleClearShortUrlFilter = () => {
|
const handleClearShortUrlFilter = () => {
|
||||||
@@ -467,6 +474,31 @@ function AnalyticsContent() {
|
|||||||
window.location.href = newUrl.toString();
|
window.location.href = newUrl.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清除子路径筛选
|
||||||
|
const handleClearSubpathFilter = () => {
|
||||||
|
setSelectedSubpath('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理子路径点击
|
||||||
|
const handlePathClick = (path: string) => {
|
||||||
|
console.log('====== ANALYTICS PAGE PATH DEBUG ======');
|
||||||
|
console.log('Original path:', path);
|
||||||
|
|
||||||
|
// 从路径中提取 subpath 部分,移除前导斜杠
|
||||||
|
// 示例:如果路径是 "/slug/subpath",我们需要提取 "subpath" 部分
|
||||||
|
// 或者直接使用原始路径,取决于您的路径结构
|
||||||
|
const pathParts = path.split('/').filter(Boolean);
|
||||||
|
// 如果路径包含多个部分,获取第二部分(subpath)
|
||||||
|
const subpath = pathParts.length > 1 ? pathParts[1] : path;
|
||||||
|
|
||||||
|
console.log('Extracted subpath:', subpath);
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
|
setSelectedSubpath(subpath);
|
||||||
|
// 重置到第一页
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
@@ -553,6 +585,23 @@ function AnalyticsContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 如果有选定的 subpath,显示提示 */}
|
||||||
|
{selectedSubpath && (
|
||||||
|
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-md text-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span>Filtered by Channel:</span>
|
||||||
|
<span className="ml-2 font-medium">{selectedSubpath}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleClearSubpathFilter}
|
||||||
|
className="ml-3 text-blue-700 hover:text-blue-900 p-1 rounded-full hover:bg-blue-200"
|
||||||
|
aria-label="Clear subpath filter"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 只在没有选中 shorturl 时显示筛选选择器 */}
|
{/* 只在没有选中 shorturl 时显示筛选选择器 */}
|
||||||
{!selectedShortUrl && (
|
{!selectedShortUrl && (
|
||||||
<>
|
<>
|
||||||
@@ -670,6 +719,7 @@ function AnalyticsContent() {
|
|||||||
projectIds={selectedProjectIds}
|
projectIds={selectedProjectIds}
|
||||||
tagIds={selectedTagNames}
|
tagIds={selectedTagNames}
|
||||||
linkId={selectedShortUrl?.externalId}
|
linkId={selectedShortUrl?.externalId}
|
||||||
|
subpath={selectedSubpath}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -681,6 +731,7 @@ function AnalyticsContent() {
|
|||||||
startTime={format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'")}
|
startTime={format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'")}
|
||||||
endTime={format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'")}
|
endTime={format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'")}
|
||||||
linkId={selectedShortUrl.externalId}
|
linkId={selectedShortUrl.externalId}
|
||||||
|
onPathClick={handlePathClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// 添加团队、项目和标签筛选
|
// 添加团队、项目和标签筛选
|
||||||
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
|
// 添加子路径筛选
|
||||||
|
subpath: searchParams.get('subpath') || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<typeof data> = {
|
const response: ApiResponse<typeof data> = {
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// 添加团队、项目和标签筛选
|
// 添加团队、项目和标签筛选
|
||||||
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
|
// 添加子路径筛选
|
||||||
|
subpath: searchParams.get('subpath') || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<typeof data> = {
|
const response: ApiResponse<typeof data> = {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const linkId = searchParams.get('linkId') || undefined;
|
const linkId = searchParams.get('linkId') || undefined;
|
||||||
const linkSlug = searchParams.get('linkSlug') || undefined;
|
const linkSlug = searchParams.get('linkSlug') || undefined;
|
||||||
const userId = searchParams.get('userId') || undefined;
|
const userId = searchParams.get('userId') || undefined;
|
||||||
|
const subpath = searchParams.get('subpath') || undefined;
|
||||||
|
|
||||||
// 获取可能存在的多个团队、项目和标签ID
|
// 获取可能存在的多个团队、项目和标签ID
|
||||||
const teamIds = searchParams.getAll('teamId');
|
const teamIds = searchParams.getAll('teamId');
|
||||||
@@ -26,6 +27,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const sortOrder = (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined;
|
const sortOrder = (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined;
|
||||||
|
|
||||||
console.log("API接收到的tagIds:", tagIds); // 添加日志便于调试
|
console.log("API接收到的tagIds:", tagIds); // 添加日志便于调试
|
||||||
|
console.log("API接收到的subpath:", subpath); // 添加日志便于调试
|
||||||
|
|
||||||
// 获取事件列表
|
// 获取事件列表
|
||||||
const params: EventsQueryParams = {
|
const params: EventsQueryParams = {
|
||||||
@@ -35,6 +37,7 @@ export async function GET(request: NextRequest) {
|
|||||||
linkId,
|
linkId,
|
||||||
linkSlug,
|
linkSlug,
|
||||||
userId,
|
userId,
|
||||||
|
subpath,
|
||||||
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
@@ -44,6 +47,9 @@ export async function GET(request: NextRequest) {
|
|||||||
sortOrder
|
sortOrder
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 记录完整的参数用于调试
|
||||||
|
console.log("完整请求参数:", JSON.stringify(params));
|
||||||
|
|
||||||
const result = await getEvents(params);
|
const result = await getEvents(params);
|
||||||
|
|
||||||
const response: ApiResponse<typeof result.events> = {
|
const response: ApiResponse<typeof result.events> = {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// Add debug log to check if linkId is being received
|
// Add debug log to check if linkId is being received
|
||||||
const linkId = searchParams.get('linkId');
|
const linkId = searchParams.get('linkId');
|
||||||
|
const subpath = searchParams.get('subpath');
|
||||||
console.log('Summary API received linkId:', linkId);
|
console.log('Summary API received linkId:', linkId);
|
||||||
|
console.log('Summary API received subpath:', subpath);
|
||||||
console.log('Summary API full parameters:', Object.fromEntries(searchParams.entries()));
|
console.log('Summary API full parameters:', Object.fromEntries(searchParams.entries()));
|
||||||
console.log('Summary API URL:', request.url);
|
console.log('Summary API URL:', request.url);
|
||||||
|
|
||||||
@@ -23,7 +25,8 @@ export async function GET(request: NextRequest) {
|
|||||||
linkId: searchParams.get('linkId') || undefined,
|
linkId: searchParams.get('linkId') || undefined,
|
||||||
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
|
subpath: searchParams.get('subpath') || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<typeof summary> = {
|
const response: ApiResponse<typeof summary> = {
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// 添加团队、项目和标签筛选
|
// 添加团队、项目和标签筛选
|
||||||
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
|
// 添加子路径筛选
|
||||||
|
subpath: searchParams.get('subpath') || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<typeof data> = {
|
const response: ApiResponse<typeof data> = {
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ interface UtmData {
|
|||||||
conversions: number;
|
conversions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期时间字符串为ClickHouse支持的格式
|
// 辅助函数,将日期格式化为标准格式
|
||||||
const formatDateTime = (dateStr: string): string => {
|
function formatDateTime(dateString: string): string {
|
||||||
return dateStr.replace('T', ' ').replace('Z', '');
|
const date = new Date(dateString);
|
||||||
};
|
return date.toISOString().split('.')[0];
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -24,6 +25,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const startTime = searchParams.get('startTime');
|
const startTime = searchParams.get('startTime');
|
||||||
const endTime = searchParams.get('endTime');
|
const endTime = searchParams.get('endTime');
|
||||||
const linkId = searchParams.get('linkId');
|
const linkId = searchParams.get('linkId');
|
||||||
|
const subpath = searchParams.get('subpath');
|
||||||
|
|
||||||
// 获取团队、项目和标签筛选参数
|
// 获取团队、项目和标签筛选参数
|
||||||
const teamIds = searchParams.getAll('teamId');
|
const teamIds = searchParams.getAll('teamId');
|
||||||
@@ -39,6 +41,7 @@ export async function GET(request: NextRequest) {
|
|||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
linkId,
|
linkId,
|
||||||
|
subpath,
|
||||||
teamIds,
|
teamIds,
|
||||||
projectIds,
|
projectIds,
|
||||||
tagIds,
|
tagIds,
|
||||||
@@ -63,6 +66,34 @@ export async function GET(request: NextRequest) {
|
|||||||
conditions.push(`link_id = '${linkId}'`);
|
conditions.push(`link_id = '${linkId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加子路径筛选 - 使用更精确的匹配方式
|
||||||
|
if (subpath && subpath.trim() !== '') {
|
||||||
|
console.log('====== UTM API SUBPATH DEBUG ======');
|
||||||
|
console.log('Raw subpath param:', subpath);
|
||||||
|
|
||||||
|
// 清理并准备subpath值
|
||||||
|
let cleanSubpath = subpath.trim();
|
||||||
|
// 移除开头的斜杠以便匹配
|
||||||
|
if (cleanSubpath.startsWith('/')) {
|
||||||
|
cleanSubpath = cleanSubpath.substring(1);
|
||||||
|
}
|
||||||
|
// 移除结尾的斜杠以便匹配
|
||||||
|
if (cleanSubpath.endsWith('/')) {
|
||||||
|
cleanSubpath = cleanSubpath.substring(0, cleanSubpath.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cleaned subpath:', cleanSubpath);
|
||||||
|
|
||||||
|
// 使用正则表达式匹配URL中的第二个路径部分
|
||||||
|
// 示例: 在 "https://abc.com/slug/subpath/" 中匹配 "subpath"
|
||||||
|
const condition = `match(JSONExtractString(event_attributes, 'full_url'), '/[^/]+/${cleanSubpath}(/|\\\\?|$)')`;
|
||||||
|
|
||||||
|
console.log('Final SQL condition:', condition);
|
||||||
|
console.log('==================================');
|
||||||
|
|
||||||
|
conditions.push(condition);
|
||||||
|
}
|
||||||
|
|
||||||
// 添加团队筛选
|
// 添加团队筛选
|
||||||
if (teamIds && teamIds.length > 0) {
|
if (teamIds && teamIds.length > 0) {
|
||||||
// 如果只有一个团队ID
|
// 如果只有一个团队ID
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface PathAnalyticsProps {
|
|||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
linkId?: string;
|
linkId?: string;
|
||||||
|
onPathClick?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PathData {
|
interface PathData {
|
||||||
@@ -12,7 +13,7 @@ interface PathData {
|
|||||||
percentage: number;
|
percentage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkId }) => {
|
const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkId, onPathClick }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [pathData, setPathData] = useState<PathData[]>([]);
|
const [pathData, setPathData] = useState<PathData[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -83,6 +84,19 @@ const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkI
|
|||||||
fetchPathData();
|
fetchPathData();
|
||||||
}, [startTime, endTime, linkId]);
|
}, [startTime, endTime, linkId]);
|
||||||
|
|
||||||
|
const handlePathClick = (path: string, e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('====== PATH CLICK DEBUG ======');
|
||||||
|
console.log('Path value:', path);
|
||||||
|
console.log('Path type:', typeof path);
|
||||||
|
console.log('Path length:', path.length);
|
||||||
|
console.log('Path chars:', Array.from(path).map(c => c.charCodeAt(0)));
|
||||||
|
console.log('==============================');
|
||||||
|
if (onPathClick) {
|
||||||
|
onPathClick(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="py-8 flex justify-center">
|
return <div className="py-8 flex justify-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" />
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" />
|
||||||
@@ -118,7 +132,15 @@ const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkI
|
|||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{pathData.map((item, index) => (
|
{pathData.map((item, index) => (
|
||||||
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{item.path}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="hover:text-blue-600 hover:underline cursor-pointer"
|
||||||
|
onClick={(e) => handlePathClick(item.path, e)}
|
||||||
|
>
|
||||||
|
{item.path}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{item.count}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{item.count}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ interface UtmAnalyticsProps {
|
|||||||
teamIds?: string[];
|
teamIds?: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
subpath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, projectIds, tagIds }: UtmAnalyticsProps) {
|
export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath }: UtmAnalyticsProps) {
|
||||||
const [activeTab, setActiveTab] = useState<string>('source');
|
const [activeTab, setActiveTab] = useState<string>('source');
|
||||||
const [utmData, setUtmData] = useState<UtmData[]>([]);
|
const [utmData, setUtmData] = useState<UtmData[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
@@ -38,6 +39,7 @@ export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, proj
|
|||||||
if (startTime) params.append('startTime', startTime);
|
if (startTime) params.append('startTime', startTime);
|
||||||
if (endTime) params.append('endTime', endTime);
|
if (endTime) params.append('endTime', endTime);
|
||||||
if (linkId) params.append('linkId', linkId);
|
if (linkId) params.append('linkId', linkId);
|
||||||
|
if (subpath) params.append('subpath', subpath);
|
||||||
params.append('utmType', activeTab);
|
params.append('utmType', activeTab);
|
||||||
|
|
||||||
// 添加团队ID参数
|
// 添加团队ID参数
|
||||||
@@ -78,7 +80,7 @@ export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, proj
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUtmData();
|
fetchUtmData();
|
||||||
}, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds]);
|
}, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath]);
|
||||||
|
|
||||||
// 安全地格式化数字
|
// 安全地格式化数字
|
||||||
const formatNumber = (value: number | undefined | null): string => {
|
const formatNumber = (value: number | undefined | null): string => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface EventsQueryParams {
|
|||||||
teamIds?: string[];
|
teamIds?: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
subpath?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
@@ -66,6 +67,7 @@ export async function getEventsSummary(params: {
|
|||||||
teamIds?: string[];
|
teamIds?: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
subpath?: string;
|
||||||
}): Promise<EventsSummary> {
|
}): Promise<EventsSummary> {
|
||||||
console.log('getEventsSummary received params:', params);
|
console.log('getEventsSummary received params:', params);
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
@@ -186,6 +188,7 @@ export async function getTimeSeriesData(params: {
|
|||||||
teamIds?: string[];
|
teamIds?: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
subpath?: string;
|
||||||
}): Promise<TimeSeriesData[]> {
|
}): Promise<TimeSeriesData[]> {
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
|
|
||||||
@@ -221,6 +224,7 @@ export async function getGeoAnalytics(params: {
|
|||||||
teamIds?: string[];
|
teamIds?: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
subpath?: string;
|
||||||
}): Promise<GeoData[]> {
|
}): Promise<GeoData[]> {
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
|
|
||||||
@@ -257,6 +261,7 @@ export async function getDeviceAnalytics(params: {
|
|||||||
teamIds?: string[];
|
teamIds?: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
subpath?: string;
|
||||||
}): Promise<DeviceAnalytics> {
|
}): Promise<DeviceAnalytics> {
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createClient } from '@clickhouse/client';
|
import { createClient } from '@clickhouse/client';
|
||||||
import type { EventsQueryParams } from './types';
|
import { EventsQueryParams } from './analytics';
|
||||||
|
|
||||||
// ClickHouse 客户端配置
|
// ClickHouse 客户端配置
|
||||||
const clickhouse = createClient({
|
const clickhouse = createClient({
|
||||||
@@ -58,6 +58,34 @@ export function buildFilter(params: Partial<EventsQueryParams>): string {
|
|||||||
filters.push(`user_id = '${params.userId}'`);
|
filters.push(`user_id = '${params.userId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加子路径过滤条件 - 使用更精确的匹配方式
|
||||||
|
if (params.subpath && params.subpath.trim() !== '') {
|
||||||
|
console.log('====== SUBPATH DEBUG ======');
|
||||||
|
console.log('Raw subpath param:', params.subpath);
|
||||||
|
|
||||||
|
// 清理并准备subpath值
|
||||||
|
let cleanSubpath = params.subpath.trim();
|
||||||
|
// 移除开头的斜杠以便匹配
|
||||||
|
if (cleanSubpath.startsWith('/')) {
|
||||||
|
cleanSubpath = cleanSubpath.substring(1);
|
||||||
|
}
|
||||||
|
// 移除结尾的斜杠以便匹配
|
||||||
|
if (cleanSubpath.endsWith('/')) {
|
||||||
|
cleanSubpath = cleanSubpath.substring(0, cleanSubpath.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cleaned subpath:', cleanSubpath);
|
||||||
|
|
||||||
|
// 使用正则表达式匹配URL中的第二个路径部分
|
||||||
|
// 示例: 在 "https://abc.com/slug/subpath/" 中匹配 "subpath"
|
||||||
|
const condition = `match(JSONExtractString(event_attributes, 'full_url'), '/[^/]+/${cleanSubpath}(/|\\\\?|$)')`;
|
||||||
|
|
||||||
|
console.log('Final SQL condition:', condition);
|
||||||
|
console.log('==========================');
|
||||||
|
|
||||||
|
filters.push(condition);
|
||||||
|
}
|
||||||
|
|
||||||
// 添加团队ID过滤条件
|
// 添加团队ID过滤条件
|
||||||
if (params.teamId) {
|
if (params.teamId) {
|
||||||
filters.push(`team_id = '${params.teamId}'`);
|
filters.push(`team_id = '${params.teamId}'`);
|
||||||
|
|||||||
Reference in New Issue
Block a user