fix: optimize event buffer (#278)
* fix: how we fetch profiles in the buffer * perf: optimize event buffer * remove unused file * fix * wip * wip: try groupmq 2 * try simplified event buffer with duration calculation on the fly instead
This commit is contained in:
committed by
GitHub
parent
4736f8509d
commit
4483e464d1
@@ -1,25 +1,20 @@
|
||||
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { SerieIcon } from '../report-chart/common/serie-icon';
|
||||
import { EventIcon } from './event-icon';
|
||||
import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import { useAppParams } from '@/hooks/use-app-params';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { pushModal } from '@/modals';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
|
||||
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
|
||||
|
||||
import { SerieIcon } from '../report-chart/common/serie-icon';
|
||||
import { EventIcon } from './event-icon';
|
||||
|
||||
type EventListItemProps = IServiceEventMinimal | IServiceEvent;
|
||||
|
||||
export function EventListItem(props: EventListItemProps) {
|
||||
const { organizationId, projectId } = useAppParams();
|
||||
const { createdAt, name, path, duration, meta } = props;
|
||||
const { createdAt, name, path, meta } = props;
|
||||
const profile = 'profile' in props ? props.profile : null;
|
||||
|
||||
const number = useNumber();
|
||||
|
||||
const renderName = () => {
|
||||
if (name === 'screen_view') {
|
||||
if (path.includes('/')) {
|
||||
@@ -32,83 +27,65 @@ export function EventListItem(props: EventListItemProps) {
|
||||
return name.replace(/_/g, ' ');
|
||||
};
|
||||
|
||||
const renderDuration = () => {
|
||||
if (name === 'screen_view') {
|
||||
return (
|
||||
<span className="text-muted-foreground">
|
||||
{number.shortWithUnit(duration / 1000, 'min')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isMinimal = 'minimal' in props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isMinimal) {
|
||||
pushModal('EventDetails', {
|
||||
id: props.id,
|
||||
projectId,
|
||||
createdAt,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'card hover:bg-light-background flex w-full items-center justify-between rounded-lg p-4 transition-colors',
|
||||
meta?.conversion &&
|
||||
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`,
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-4 text-left ">
|
||||
<EventIcon size="sm" name={name} meta={meta} />
|
||||
<span>
|
||||
<span className="font-medium">{renderName()}</span>
|
||||
{' '}
|
||||
{renderDuration()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pl-10">
|
||||
<div className="flex origin-left scale-75 gap-1">
|
||||
<SerieIcon name={props.country} />
|
||||
<SerieIcon name={props.os} />
|
||||
<SerieIcon name={props.browser} />
|
||||
</div>
|
||||
<button
|
||||
className={cn(
|
||||
'card flex w-full items-center justify-between rounded-lg p-4 transition-colors hover:bg-light-background',
|
||||
meta?.conversion &&
|
||||
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isMinimal) {
|
||||
pushModal('EventDetails', {
|
||||
id: props.id,
|
||||
projectId,
|
||||
createdAt,
|
||||
});
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-4 text-left">
|
||||
<EventIcon meta={meta} name={name} size="sm" />
|
||||
<span className="font-medium">{renderName()}</span>
|
||||
</div>
|
||||
<div className="pl-10">
|
||||
<div className="flex origin-left scale-75 gap-1">
|
||||
<SerieIcon name={props.country} />
|
||||
<SerieIcon name={props.os} />
|
||||
<SerieIcon name={props.browser} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
{profile && (
|
||||
<Tooltiper asChild content={getProfileName(profile)}>
|
||||
<Link
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
to={'/$organizationId/$projectId/profiles/$profileId'}
|
||||
params={{
|
||||
organizationId,
|
||||
projectId,
|
||||
profileId: profile.id,
|
||||
}}
|
||||
className="max-w-[80px] overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground hover:underline"
|
||||
>
|
||||
{getProfileName(profile)}
|
||||
</Link>
|
||||
</Tooltiper>
|
||||
)}
|
||||
|
||||
<Tooltiper asChild content={createdAt.toLocaleString()}>
|
||||
<div className=" text-muted-foreground">
|
||||
{createdAt.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
{profile && (
|
||||
<Tooltiper asChild content={getProfileName(profile)}>
|
||||
<Link
|
||||
className="max-w-[80px] overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
params={{
|
||||
organizationId,
|
||||
projectId,
|
||||
profileId: profile.id,
|
||||
}}
|
||||
to={'/$organizationId/$projectId/profiles/$profileId'}
|
||||
>
|
||||
{getProfileName(profile)}
|
||||
</Link>
|
||||
</Tooltiper>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltiper asChild content={createdAt.toLocaleString()}>
|
||||
<div className="text-muted-foreground">
|
||||
{createdAt.toLocaleTimeString()}
|
||||
</div>
|
||||
</Tooltiper>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AnimatedNumber } from '../animated-number';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -8,71 +9,53 @@ import { useDebounceState } from '@/hooks/use-debounce-state';
|
||||
import useWS from '@/hooks/use-ws';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
|
||||
import { useParams } from '@tanstack/react-router';
|
||||
import { AnimatedNumber } from '../animated-number';
|
||||
|
||||
export default function EventListener({
|
||||
onRefresh,
|
||||
}: {
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const params = useParams({
|
||||
strict: false,
|
||||
});
|
||||
const { projectId } = useAppParams();
|
||||
const counter = useDebounceState(0, 1000);
|
||||
useWS<IServiceEventMinimal | IServiceEvent>(
|
||||
useWS<{ count: number }>(
|
||||
`/live/events/${projectId}`,
|
||||
(event) => {
|
||||
if (event) {
|
||||
const isProfilePage = !!params?.profileId;
|
||||
if (isProfilePage) {
|
||||
const profile = 'profile' in event ? event.profile : null;
|
||||
if (profile?.id === params?.profileId) {
|
||||
counter.set((prev) => prev + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
counter.set((prev) => prev + 1);
|
||||
}
|
||||
({ count }) => {
|
||||
counter.set((prev) => prev + count);
|
||||
},
|
||||
{
|
||||
debounce: {
|
||||
delay: 1000,
|
||||
maxWait: 5000,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 items-center gap-2 rounded-md border border-border bg-card px-3 font-medium leading-none"
|
||||
onClick={() => {
|
||||
counter.set(0);
|
||||
onRefresh();
|
||||
}}
|
||||
className="flex h-8 items-center gap-2 rounded-md border border-border bg-card px-3 font-medium leading-none"
|
||||
type="button"
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all'
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
'absolute top-0 left-0 h-3 w-3 rounded-full bg-emerald-500 transition-all'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{counter.debounced === 0 ? (
|
||||
'Listening'
|
||||
) : (
|
||||
<AnimatedNumber value={counter.debounced} suffix=" new events" />
|
||||
<AnimatedNumber suffix=" new events" value={counter.debounced} />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { EventIcon } from '@/components/events/event-icon';
|
||||
import { ProjectLink } from '@/components/links';
|
||||
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
|
||||
import { KeyValueGrid } from '@/components/ui/key-value-grid';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { pushModal } from '@/modals';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||
import { KeyValueGrid } from '@/components/ui/key-value-grid';
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
|
||||
export function useColumns() {
|
||||
const number = useNumber();
|
||||
@@ -28,17 +27,24 @@ export function useColumns() {
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
cell({ row }) {
|
||||
const { name, path, duration, properties, revenue } = row.original;
|
||||
const { name, path, revenue } = row.original;
|
||||
const fullTitle =
|
||||
name === 'screen_view'
|
||||
? path
|
||||
: name === 'revenue' && revenue
|
||||
? `${name} (${number.currency(revenue / 100)})`
|
||||
: name.replace(/_/g, ' ');
|
||||
|
||||
const renderName = () => {
|
||||
if (name === 'screen_view') {
|
||||
if (path.includes('/')) {
|
||||
return <span className="max-w-md truncate">{path}</span>;
|
||||
return path;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="text-muted-foreground">Screen: </span>
|
||||
<span className="max-w-md truncate">{path}</span>
|
||||
{path}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -50,38 +56,27 @@ export function useColumns() {
|
||||
return name.replace(/_/g, ' ');
|
||||
};
|
||||
|
||||
const renderDuration = () => {
|
||||
if (name === 'screen_view') {
|
||||
return (
|
||||
<span className="text-muted-foreground">
|
||||
{number.shortWithUnit(duration / 1000, 'min')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="transition-transform hover:scale-105"
|
||||
className="shrink-0 transition-transform hover:scale-105"
|
||||
onClick={() => {
|
||||
pushModal('EditEvent', {
|
||||
id: row.original.id,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<EventIcon
|
||||
size="sm"
|
||||
name={row.original.name}
|
||||
meta={row.original.meta}
|
||||
name={row.original.name}
|
||||
size="sm"
|
||||
/>
|
||||
</button>
|
||||
<span className="flex gap-2">
|
||||
<span className="flex min-w-0 flex-1 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-0 max-w-full truncate text-left font-medium hover:underline"
|
||||
title={fullTitle}
|
||||
onClick={() => {
|
||||
pushModal('EventDetails', {
|
||||
id: row.original.id,
|
||||
@@ -89,11 +84,10 @@ export function useColumns() {
|
||||
projectId: row.original.projectId,
|
||||
});
|
||||
}}
|
||||
className="font-medium hover:underline"
|
||||
type="button"
|
||||
>
|
||||
{renderName()}
|
||||
<span className="block truncate">{renderName()}</span>
|
||||
</button>
|
||||
{renderDuration()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -107,8 +101,8 @@ export function useColumns() {
|
||||
if (profile) {
|
||||
return (
|
||||
<ProjectLink
|
||||
className="group row items-center gap-2 whitespace-nowrap font-medium hover:underline"
|
||||
href={`/profiles/${encodeURIComponent(profile.id)}`}
|
||||
className="group whitespace-nowrap font-medium hover:underline row items-center gap-2"
|
||||
>
|
||||
<ProfileAvatar size="sm" {...profile} />
|
||||
{getProfileName(profile)}
|
||||
@@ -119,8 +113,8 @@ export function useColumns() {
|
||||
if (profileId && profileId !== deviceId) {
|
||||
return (
|
||||
<ProjectLink
|
||||
href={`/profiles/${encodeURIComponent(profileId)}`}
|
||||
className="whitespace-nowrap font-medium hover:underline"
|
||||
href={`/profiles/${encodeURIComponent(profileId)}`}
|
||||
>
|
||||
Unknown
|
||||
</ProjectLink>
|
||||
@@ -130,8 +124,8 @@ export function useColumns() {
|
||||
if (deviceId) {
|
||||
return (
|
||||
<ProjectLink
|
||||
href={`/profiles/${encodeURIComponent(deviceId)}`}
|
||||
className="whitespace-nowrap font-medium hover:underline"
|
||||
href={`/profiles/${encodeURIComponent(deviceId)}`}
|
||||
>
|
||||
Anonymous
|
||||
</ProjectLink>
|
||||
@@ -152,10 +146,10 @@ export function useColumns() {
|
||||
const { sessionId } = row.original;
|
||||
return (
|
||||
<ProjectLink
|
||||
href={`/sessions/${encodeURIComponent(sessionId)}`}
|
||||
className="whitespace-nowrap font-medium hover:underline"
|
||||
href={`/sessions/${encodeURIComponent(sessionId)}`}
|
||||
>
|
||||
{sessionId.slice(0,6)}
|
||||
{sessionId.slice(0, 6)}
|
||||
</ProjectLink>
|
||||
);
|
||||
},
|
||||
@@ -175,7 +169,7 @@ export function useColumns() {
|
||||
cell({ row }) {
|
||||
const { country, city } = row.original;
|
||||
return (
|
||||
<div className="row items-center gap-2 min-w-0">
|
||||
<div className="row min-w-0 items-center gap-2">
|
||||
<SerieIcon name={country} />
|
||||
<span className="truncate">{city}</span>
|
||||
</div>
|
||||
@@ -189,7 +183,7 @@ export function useColumns() {
|
||||
cell({ row }) {
|
||||
const { os } = row.original;
|
||||
return (
|
||||
<div className="row items-center gap-2 min-w-0">
|
||||
<div className="row min-w-0 items-center gap-2">
|
||||
<SerieIcon name={os} />
|
||||
<span className="truncate">{os}</span>
|
||||
</div>
|
||||
@@ -203,7 +197,7 @@ export function useColumns() {
|
||||
cell({ row }) {
|
||||
const { browser } = row.original;
|
||||
return (
|
||||
<div className="row items-center gap-2 min-w-0">
|
||||
<div className="row min-w-0 items-center gap-2">
|
||||
<SerieIcon name={browser} />
|
||||
<span className="truncate">{browser}</span>
|
||||
</div>
|
||||
@@ -221,14 +215,14 @@ export function useColumns() {
|
||||
const { properties } = row.original;
|
||||
const filteredProperties = Object.fromEntries(
|
||||
Object.entries(properties || {}).filter(
|
||||
([key]) => !key.startsWith('__'),
|
||||
),
|
||||
([key]) => !key.startsWith('__')
|
||||
)
|
||||
);
|
||||
const items = Object.entries(filteredProperties);
|
||||
const limit = 2;
|
||||
const data = items.slice(0, limit).map(([key, value]) => ({
|
||||
name: key,
|
||||
value: value,
|
||||
value,
|
||||
}));
|
||||
if (items.length > limit) {
|
||||
data.push({
|
||||
|
||||
@@ -35,6 +35,7 @@ type Props = {
|
||||
>,
|
||||
unknown
|
||||
>;
|
||||
showEventListener?: boolean;
|
||||
};
|
||||
|
||||
const LOADING_DATA = [{}, {}, {}, {}, {}, {}, {}, {}, {}] as IServiceEvent[];
|
||||
@@ -215,7 +216,7 @@ const VirtualizedEventsTable = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const EventsTable = ({ query }: Props) => {
|
||||
export const EventsTable = ({ query, showEventListener = false }: Props) => {
|
||||
const { isLoading } = query;
|
||||
const columns = useColumns();
|
||||
|
||||
@@ -272,7 +273,7 @@ export const EventsTable = ({ query }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<EventsTableToolbar query={query} table={table} />
|
||||
<EventsTableToolbar query={query} table={table} showEventListener={showEventListener} />
|
||||
<VirtualizedEventsTable table={table} data={data} isLoading={isLoading} />
|
||||
<div className="w-full h-10 center-center pt-4" ref={inViewportRef}>
|
||||
<div
|
||||
@@ -291,9 +292,11 @@ export const EventsTable = ({ query }: Props) => {
|
||||
function EventsTableToolbar({
|
||||
query,
|
||||
table,
|
||||
showEventListener,
|
||||
}: {
|
||||
query: Props['query'];
|
||||
table: Table<IServiceEvent>;
|
||||
showEventListener: boolean;
|
||||
}) {
|
||||
const { projectId } = useAppParams();
|
||||
const [startDate, setStartDate] = useQueryState(
|
||||
@@ -305,7 +308,7 @@ function EventsTableToolbar({
|
||||
return (
|
||||
<DataTableToolbarContainer>
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
||||
<EventListener onRefresh={() => query.refetch()} />
|
||||
{showEventListener && <EventListener onRefresh={() => query.refetch()} />}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user