feat(bounce-rate): Add bounce rate tracking service, API controller, and visualization page for analytics dashboard

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-19 21:04:43 -07:00
parent a532e43e31
commit 210aed3df6
3 changed files with 8 additions and 42 deletions

View file

@ -26,7 +26,10 @@ export class BounceRateController {
? new Date(query.startDate)
: new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000);
const result = await this.bounceRateService.getBounceRateMetrics(startDate, endDate);
const [result, avgSessionDuration] = await Promise.all([
this.bounceRateService.getBounceRateMetrics(startDate, endDate),
this.bounceRateService.calculateAvgSessionDuration(startDate, endDate),
]);
return {
overall: result.bounceRate,
@ -35,7 +38,7 @@ export class BounceRateController {
mobile: result.bounceRate,
tablet: result.bounceRate,
},
avgSessionDuration: 0,
avgSessionDuration: Math.round(avgSessionDuration),
pagesPerSession: result.avgPagesPerSession,
exitRate: result.bounceRate,
change: result.comparison.changePercent,

View file

@ -248,7 +248,7 @@ export class BounceRateService {
/**
* Calculate average session duration from engagement timestamps
*/
private async calculateAvgSessionDuration(
async calculateAvgSessionDuration(
startDate: Date,
endDate: Date,
): Promise<number> {

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useMemo } from 'react';
import { MetricCard, DashboardLayout, DashboardWidget } from '@lilith/ui-analytics';
import { DataTable } from '@lilith/ui-data';
@ -47,29 +47,6 @@ const Subtitle = styled.p`
margin: ${(props) => props.theme.spacing.xs} 0 0 0;
`;
const DateFilterContainer = styled.div`
display: flex;
gap: ${(props) => props.theme.spacing.sm};
padding: ${(props) => props.theme.spacing.xs};
background: ${(props) => props.theme.colors.surface};
border-radius: ${(props) => props.theme.borderRadius.md};
`;
const FilterButton = styled.button<{ $isActive: boolean }>`
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border: none;
border-radius: ${(props) => props.theme.borderRadius.sm};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.medium};
cursor: pointer;
transition: all ${(props) => props.theme.transitions.fast};
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : 'transparent')};
color: ${(props) => (props.$isActive ? '#fff' : props.theme.colors.text.secondary)};
&:hover {
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : props.theme.colors.hover.surface)};
}
`;
const AlertsContainer = styled.div`
display: flex;
@ -153,8 +130,6 @@ const formatDuration = (seconds: number): string => {
// ============================================================================
export const BounceRatePage = () => {
const [dateRange, setDateRange] = useState('30d');
const { data: metrics, isLoading } = useBounceRateMetrics();
const { data: byPage } = useBounceRateByPage();
const { data: history } = useBounceRateHistory();
@ -216,18 +191,6 @@ export const BounceRatePage = () => {
<Subtitle>Analyze visitor engagement and page effectiveness</Subtitle>
</div>
<DateFilterContainer data-testid="date-filter">
{['7d', '30d', '90d'].map((range) => (
<FilterButton
key={range}
$isActive={dateRange === range}
onClick={() => setDateRange(range)}
data-testid={`filter-${range}`}
>
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : '90 Days'}
</FilterButton>
))}
</DateFilterContainer>
</PageHeader>
{/* High Bounce Alert */}
@ -276,7 +239,7 @@ export const BounceRatePage = () => {
<DashboardWidget>
<MetricCard
label="Exit Rate"
value={metricsData?.byDevice?.desktop ?? 0}
value={metricsData?.exitRate ?? 0}
format="percentage"
icon={<LogOutIcon size={20} />}
data-testid="exit-rate-card"