This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-19 15:13:44 +01:00
parent 47adf46625
commit 41993d3463
35 changed files with 1098 additions and 233 deletions

View File

@@ -38,9 +38,10 @@
"@openpanel/integrations": "workspace:^",
"@openpanel/json": "workspace:*",
"@openpanel/payments": "workspace:*",
"@openpanel/sdk": "^1.0.8",
"@openpanel/sdk-info": "workspace:^",
"@openpanel/validation": "workspace:^",
"@openpanel/web": "^1.0.1",
"@openpanel/web": "^1.0.12",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -116,7 +117,6 @@
"pushmodal": "^1.0.3",
"ramda": "^0.29.1",
"random-animal-name": "^0.1.1",
"rrweb-player": "2.0.0-alpha.20",
"rc-virtual-list": "^3.14.5",
"react": "catalog:",
"react-animate-height": "^3.2.3",
@@ -142,6 +142,7 @@
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"rrweb-player": "2.0.0-alpha.20",
"short-unique-id": "^5.0.3",
"slugify": "^1.6.6",
"sonner": "^1.4.0",

View File

@@ -69,11 +69,11 @@ export function ReplayEventFeed({ events }: { events: IServiceEvent[] }) {
className="h-full"
>
<ScrollArea className="flex-1 min-h-0" ref={viewportRef}>
<div className="flex flex-col">
<div className="flex w-full flex-col">
{visibleEvents.map(({ event, offsetMs }) => (
<div
key={event.id}
className="animate-in fade-in-0 slide-in-from-bottom-3 duration-300 fill-mode-both"
className="animate-in fade-in-0 slide-in-from-bottom-3 min-w-0 duration-300 fill-mode-both"
>
<ReplayEventItem
event={event}

View File

@@ -1,21 +1,17 @@
'use client';
import { EventIcon } from '@/components/events/event-icon';
import { cn } from '@/lib/utils';
import type { IServiceEvent } from '@openpanel/db';
function formatOffset(ms: number): string {
const sign = ms < 0 ? '-' : '+';
const abs = Math.abs(ms);
const totalSeconds = Math.floor(abs / 1000);
const m = Math.floor(totalSeconds / 60);
const s = totalSeconds % 60;
return `${sign}${m}:${s.toString().padStart(2, '0')}`;
function formatTime(date: Date | string): string {
const d = date instanceof Date ? date : new Date(date);
const h = d.getHours().toString().padStart(2, '0');
const m = d.getMinutes().toString().padStart(2, '0');
const s = d.getSeconds().toString().padStart(2, '0');
return `${h}:${m}:${s}`;
}
export function ReplayEventItem({
event,
offsetMs,
isCurrent,
onClick,
}: {
@@ -48,7 +44,7 @@ export function ReplayEventItem({
</div>
</div>
<span className="flex-shrink-0 text-xs tabular-nums text-muted-foreground">
{formatOffset(offsetMs)}
{formatTime(event.createdAt)}
</span>
</div>
</button>

View File

@@ -48,7 +48,7 @@ function ScrollArea({
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:!block"
ref={ref}
>
{children}

View File

@@ -2,7 +2,8 @@ import { useLocation } from '@tanstack/react-router';
export function usePageTabs(tabs: { id: string; label: string }[]) {
const location = useLocation();
const tab = location.pathname.split('/').pop();
const segments = location.pathname.split('/').filter(Boolean);
const tab = segments[segments.length - 1];
if (!tab) {
return {

View File

@@ -83,6 +83,7 @@ import { Route as AppOrganizationIdProjectIdEventsTabsStatsRouteImport } from '.
import { Route as AppOrganizationIdProjectIdEventsTabsEventsRouteImport } from './routes/_app.$organizationId.$projectId.events._tabs.events'
import { Route as AppOrganizationIdProjectIdEventsTabsConversionsRouteImport } from './routes/_app.$organizationId.$projectId.events._tabs.conversions'
import { Route as AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRouteImport } from './routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.index'
import { Route as AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRouteImport } from './routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.sessions'
import { Route as AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRouteImport } from './routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events'
const AppOrganizationIdProfileRouteImport = createFileRoute(
@@ -557,6 +558,12 @@ const AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute =
path: '/',
getParentRoute: () => AppOrganizationIdProjectIdProfilesProfileIdTabsRoute,
} as any)
const AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute =
AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRouteImport.update({
id: '/sessions',
path: '/sessions',
getParentRoute: () => AppOrganizationIdProjectIdProfilesProfileIdTabsRoute,
} as any)
const AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute =
AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRouteImport.update({
id: '/events',
@@ -633,6 +640,7 @@ export interface FileRoutesByFullPath {
'/$organizationId/$projectId/profiles/': typeof AppOrganizationIdProjectIdProfilesTabsIndexRoute
'/$organizationId/$projectId/settings/': typeof AppOrganizationIdProjectIdSettingsTabsIndexRoute
'/$organizationId/$projectId/profiles/$profileId/events': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute
'/$organizationId/$projectId/profiles/$profileId/sessions': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute
'/$organizationId/$projectId/profiles/$profileId/': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute
}
export interface FileRoutesByTo {
@@ -695,6 +703,7 @@ export interface FileRoutesByTo {
'/$organizationId/$projectId/settings/imports': typeof AppOrganizationIdProjectIdSettingsTabsImportsRoute
'/$organizationId/$projectId/settings/widgets': typeof AppOrganizationIdProjectIdSettingsTabsWidgetsRoute
'/$organizationId/$projectId/profiles/$profileId/events': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute
'/$organizationId/$projectId/profiles/$profileId/sessions': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -778,6 +787,7 @@ export interface FileRoutesById {
'/_app/$organizationId/$projectId/profiles/_tabs/': typeof AppOrganizationIdProjectIdProfilesTabsIndexRoute
'/_app/$organizationId/$projectId/settings/_tabs/': typeof AppOrganizationIdProjectIdSettingsTabsIndexRoute
'/_app/$organizationId/$projectId/profiles/$profileId/_tabs/events': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute
'/_app/$organizationId/$projectId/profiles/$profileId/_tabs/sessions': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute
'/_app/$organizationId/$projectId/profiles/$profileId/_tabs/': typeof AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute
}
export interface FileRouteTypes {
@@ -851,6 +861,7 @@ export interface FileRouteTypes {
| '/$organizationId/$projectId/profiles/'
| '/$organizationId/$projectId/settings/'
| '/$organizationId/$projectId/profiles/$profileId/events'
| '/$organizationId/$projectId/profiles/$profileId/sessions'
| '/$organizationId/$projectId/profiles/$profileId/'
fileRoutesByTo: FileRoutesByTo
to:
@@ -913,6 +924,7 @@ export interface FileRouteTypes {
| '/$organizationId/$projectId/settings/imports'
| '/$organizationId/$projectId/settings/widgets'
| '/$organizationId/$projectId/profiles/$profileId/events'
| '/$organizationId/$projectId/profiles/$profileId/sessions'
id:
| '__root__'
| '/'
@@ -995,6 +1007,7 @@ export interface FileRouteTypes {
| '/_app/$organizationId/$projectId/profiles/_tabs/'
| '/_app/$organizationId/$projectId/settings/_tabs/'
| '/_app/$organizationId/$projectId/profiles/$profileId/_tabs/events'
| '/_app/$organizationId/$projectId/profiles/$profileId/_tabs/sessions'
| '/_app/$organizationId/$projectId/profiles/$profileId/_tabs/'
fileRoutesById: FileRoutesById
}
@@ -1578,6 +1591,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRouteImport
parentRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsRoute
}
'/_app/$organizationId/$projectId/profiles/$profileId/_tabs/sessions': {
id: '/_app/$organizationId/$projectId/profiles/$profileId/_tabs/sessions'
path: '/sessions'
fullPath: '/$organizationId/$projectId/profiles/$profileId/sessions'
preLoaderRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRouteImport
parentRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsRoute
}
'/_app/$organizationId/$projectId/profiles/$profileId/_tabs/events': {
id: '/_app/$organizationId/$projectId/profiles/$profileId/_tabs/events'
path: '/events'
@@ -1689,6 +1709,7 @@ const AppOrganizationIdProjectIdProfilesTabsRouteWithChildren =
interface AppOrganizationIdProjectIdProfilesProfileIdTabsRouteChildren {
AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute
AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute
AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute: typeof AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute
}
@@ -1696,6 +1717,8 @@ const AppOrganizationIdProjectIdProfilesProfileIdTabsRouteChildren: AppOrganizat
{
AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute:
AppOrganizationIdProjectIdProfilesProfileIdTabsEventsRoute,
AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute:
AppOrganizationIdProjectIdProfilesProfileIdTabsSessionsRoute,
AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute:
AppOrganizationIdProjectIdProfilesProfileIdTabsIndexRoute,
}

View File

@@ -0,0 +1,33 @@
import { SessionsTable } from '@/components/sessions/table';
import { useSearchQueryState } from '@/hooks/use-search-query-state';
import { useTRPC } from '@/integrations/trpc/react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute(
'/_app/$organizationId/$projectId/profiles/$profileId/_tabs/sessions',
)({
component: Component,
});
function Component() {
const { projectId, profileId } = Route.useParams();
const trpc = useTRPC();
const { debouncedSearch } = useSearchQueryState();
const query = useInfiniteQuery(
trpc.session.list.infiniteQueryOptions(
{
projectId,
profileId,
take: 50,
search: debouncedSearch,
},
{
getNextPageParam: (lastPage) => lastPage.meta.next,
},
),
);
return <SessionsTable query={query} />;
}

View File

@@ -43,6 +43,7 @@ function Component() {
label: 'Overview',
},
{ id: 'events', label: 'Events' },
{ id: 'sessions', label: 'Sessions' },
]);
const handleTabChange = (tabId: string) => {

View File

@@ -1,19 +1,20 @@
import { EventsTable } from '@/components/events/table';
import { EventIcon } from '@/components/events/event-icon';
import FullPageLoadingState from '@/components/full-page-loading-state';
import { PageContainer } from '@/components/page-container';
import { PageHeader } from '@/components/page-header';
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
import { ReplayShell } from '@/components/sessions/replay';
import { useReadColumnVisibility } from '@/components/ui/data-table/data-table-hooks';
import {
useEventQueryFilters,
useEventQueryNamesFilter,
} from '@/hooks/use-event-query-filters';
import { KeyValueGrid } from '@/components/ui/key-value-grid';
import { Widget, WidgetBody, WidgetHead, WidgetTitle } from '@/components/widget';
import { useTRPC } from '@/integrations/trpc/react';
import { formatDateTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters';
import { useNumber } from '@/hooks/use-numer-formatter';
import { createProjectTitle } from '@/utils/title';
import { useInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { parseAsIsoDateTime, useQueryState } from 'nuqs';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Link, createFileRoute } from '@tanstack/react-router';
import type { IServiceEvent, IServiceSession } from '@openpanel/db';
export const Route = createFileRoute(
'/_app/$organizationId/$projectId/sessions_/$sessionId',
@@ -27,101 +28,353 @@ export const Route = createFileRoute(
projectId: params.projectId,
}),
),
context.queryClient.prefetchQuery(
context.trpc.event.events.queryOptions({
projectId: params.projectId,
sessionId: params.sessionId,
filters: [],
columnVisibility: {},
}),
),
]);
},
head: () => {
return {
meta: [
{
title: createProjectTitle('Sessions'),
},
],
};
},
head: () => ({
meta: [{ title: createProjectTitle('Session') }],
}),
pendingComponent: FullPageLoadingState,
});
function Component() {
const { projectId, sessionId } = Route.useParams();
const trpc = useTRPC();
function sessionToFakeEvent(session: IServiceSession): IServiceEvent {
return session as unknown as IServiceEvent;
}
const LIMIT = 50;
function VisitedRoutes({ paths }: { paths: string[] }) {
const counted = paths.reduce<Record<string, number>>((acc, p) => {
acc[p] = (acc[p] ?? 0) + 1;
return acc;
}, {});
const sorted = Object.entries(counted).sort((a, b) => b[1] - a[1]);
const max = sorted[0]?.[1] ?? 1;
if (sorted.length === 0) return null;
return (
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Visited pages</WidgetTitle>
</WidgetHead>
<div className="flex flex-col gap-1 p-1">
{sorted.map(([path, count]) => (
<div key={path} className="relative px-3 py-2 group">
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200 group-hover:bg-def-300"
style={{ width: `${(count / max) * 100}%` }}
/>
<div className="relative flex justify-between gap-2 min-w-0">
<span className="truncate text-sm">{path}</span>
<span className="shrink-0 text-sm font-medium">{count}</span>
</div>
</div>
))}
</div>
</Widget>
);
}
function EventDistribution({ events }: { events: IServiceEvent[] }) {
const counted = events.reduce<Record<string, number>>((acc, e) => {
acc[e.name] = (acc[e.name] ?? 0) + 1;
return acc;
}, {});
const sorted = Object.entries(counted).sort((a, b) => b[1] - a[1]);
const max = sorted[0]?.[1] ?? 1;
if (sorted.length === 0) return null;
return (
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Event distribution</WidgetTitle>
</WidgetHead>
<div className="flex flex-col gap-1 p-1">
{sorted.map(([name, count]) => (
<div key={name} className="relative px-3 py-2 group">
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200 group-hover:bg-def-300"
style={{ width: `${(count / max) * 100}%` }}
/>
<div className="relative flex justify-between gap-2">
<span className="text-sm">{name.replace(/_/g, ' ')}</span>
<span className="shrink-0 text-sm font-medium">{count}</span>
</div>
</div>
))}
</div>
</Widget>
);
}
function Component() {
const { projectId, sessionId, organizationId } = Route.useParams();
const trpc = useTRPC();
const number = useNumber()
const { data: session } = useSuspenseQuery(
trpc.session.byId.queryOptions({
trpc.session.byId.queryOptions({ sessionId, projectId }),
);
const { data: eventsData } = useSuspenseQuery(
trpc.event.events.queryOptions({
projectId,
sessionId,
filters: [],
columnVisibility: {},
}),
);
const events = eventsData?.data ?? [];
const isIdentified =
session.profileId && session.profileId !== session.deviceId;
const { data: profile } = useSuspenseQuery(
trpc.profile.byId.queryOptions({
profileId: session.profileId,
projectId,
}),
);
const [filters] = useEventQueryFilters();
const [startDate] = useQueryState('startDate', parseAsIsoDateTime);
const [endDate] = useQueryState('endDate', parseAsIsoDateTime);
const [eventNames] = useEventQueryNamesFilter();
const columnVisibility = useReadColumnVisibility('events');
const query = useInfiniteQuery(
trpc.event.events.infiniteQueryOptions(
{
projectId,
sessionId,
filters,
events: eventNames,
startDate: startDate || undefined,
endDate: endDate || undefined,
columnVisibility: columnVisibility ?? {},
},
{
enabled: columnVisibility !== null,
getNextPageParam: (lastPage) => lastPage.meta.next,
},
),
);
const fakeEvent = sessionToFakeEvent(session);
return (
<PageContainer>
<PageContainer className="col gap-8">
<PageHeader
title={`Session: ${session.id.slice(0, 4)}...${session.id.slice(-4)}`}
title={`Session: ${session.id}`}
>
<div className="row gap-4 mb-6">
{session.country && (
<div className="row gap-2 items-center">
<SerieIcon name={session.country} />
<span>
{session.country}
{session.city && ` / ${session.city}`}
</span>
</div>
)}
{session.device && (
<div className="row gap-2 items-center">
<SerieIcon name={session.device} />
<span className="capitalize">{session.device}</span>
</div>
)}
{session.os && (
<div className="row gap-2 items-center">
<SerieIcon name={session.os} />
<span>{session.os}</span>
</div>
)}
{session.model && (
<div className="row gap-2 items-center">
<SerieIcon name={session.model} />
<span>{session.model}</span>
</div>
)}
{session.country && (
<div className="row gap-2 items-center">
<SerieIcon name={session.country} />
<span>
{session.country}
{session.city && ` / ${session.city}`}
</span>
</div>
)}
{session.device && (
<div className="row gap-2 items-center">
<SerieIcon name={session.device} />
<span className="capitalize">{session.device}</span>
</div>
)}
{session.os && (
<div className="row gap-2 items-center">
<SerieIcon name={session.os} />
<span>{session.os}</span>
</div>
)}
{session.model && (
<div className="row gap-2 items-center">
<SerieIcon name={session.model} />
<span>{session.model}</span>
</div>
)}
{session.browser && (
<div className="row gap-2 items-center">
<SerieIcon name={session.browser} />
<span>{session.browser}</span>
</div>
)}
</div>
</div>
</PageHeader>
<div className="mb-6">
<ReplayShell sessionId={sessionId} projectId={projectId} />
{session.hasReplay && <ReplayShell sessionId={sessionId} projectId={projectId} />}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[340px_minmax(0,1fr)]">
{/* Left column */}
<div className="col gap-6">
{/* Session info */}
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Session info</WidgetTitle>
</WidgetHead>
<KeyValueGrid
className="border-0"
columns={1}
copyable
data={[
{ name: 'duration', value: number.formatWithUnit(session.duration/1000, 'min') },
{ name: 'createdAt', value: session.createdAt },
{ name: 'endedAt', value: session.endedAt },
{ name: 'screenViews', value: session.screenViewCount },
{ name: 'events', value: session.eventCount },
{ name: 'bounce', value: session.isBounce ? 'Yes' : 'No' },
...(session.entryPath
? [{ name: 'entryPath', value: session.entryPath }]
: []),
...(session.exitPath
? [{ name: 'exitPath', value: session.exitPath }]
: []),
...(session.referrerName
? [{ name: 'referrerName', value: session.referrerName }]
: []),
...(session.referrer
? [{ name: 'referrer', value: session.referrer }]
: []),
...(session.utmSource
? [{ name: 'utmSource', value: session.utmSource }]
: []),
...(session.utmMedium
? [{ name: 'utmMedium', value: session.utmMedium }]
: []),
...(session.utmCampaign
? [{ name: 'utmCampaign', value: session.utmCampaign }]
: []),
...(session.revenue > 0
? [{ name: 'revenue', value: `$${session.revenue}` }]
: []),
{ name: 'country', value: session.country, event: fakeEvent },
...(session.city
? [{ name: 'city', value: session.city, event: fakeEvent }]
: []),
...(session.os
? [{ name: 'os', value: session.os, event: fakeEvent }]
: []),
...(session.browser
? [
{
name: 'browser',
value: session.browser,
event: fakeEvent,
},
]
: []),
...(session.device
? [
{
name: 'device',
value: session.device,
event: fakeEvent,
},
]
: []),
...(session.brand
? [{ name: 'brand', value: session.brand, event: fakeEvent }]
: []),
...(session.model
? [{ name: 'model', value: session.model, event: fakeEvent }]
: []),
]}
/>
</Widget>
{/* Profile card */}
{isIdentified && profile && (
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Profile</WidgetTitle>
</WidgetHead>
<WidgetBody className="p-0">
<Link
to="/$organizationId/$projectId/profiles/$profileId"
params={{
organizationId,
projectId,
profileId: session.profileId,
}}
className="row items-center gap-3 p-4 transition-colors hover:bg-accent"
>
<ProfileAvatar {...profile} size="lg" />
<div className="col min-w-0 gap-0.5">
<span className="truncate font-medium">
{getProfileName(profile, false) ?? session.profileId}
</span>
{profile.email && (
<span className="truncate text-sm text-muted-foreground">
{profile.email}
</span>
)}
</div>
</Link>
</WidgetBody>
</Widget>
)}
{/* Visited pages */}
<VisitedRoutes
paths={events
.filter((e) => e.name === 'screen_view' && e.path)
.map((e) => e.path)}
/>
{/* Event distribution */}
<EventDistribution events={events} />
</div>
{/* Right column */}
<div className="col gap-6">
{/* Events list */}
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Events</WidgetTitle>
</WidgetHead>
<div className="divide-y">
{events.map((event) => (
<div
key={event.id}
className="row items-center gap-3 px-4 py-2"
>
<EventIcon name={event.name} meta={event.meta} size="sm" />
<div className="col min-w-0 flex-1">
<span className="truncate text-sm font-medium">
{event.name === 'screen_view' && event.path
? event.path
: event.name.replace(/_/g, ' ')}
</span>
</div>
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
{formatDateTime(event.createdAt)}
</span>
</div>
))}
{events.length === 0 && (
<div className="py-8 text-center text-sm text-muted-foreground">
No events found
</div>
)}
</div>
</Widget>
</div>
</div>
<EventsTable query={query} />
</PageContainer>
);
}

View File

@@ -2,19 +2,14 @@ import { OpenPanel } from '@openpanel/web';
const clientId = import.meta.env.VITE_OP_CLIENT_ID;
const createOpInstance = () => {
if (!clientId || clientId === 'undefined') {
return new Proxy({} as OpenPanel, {
get: () => () => {},
});
}
return new OpenPanel({
clientId,
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
};
export const op = createOpInstance();
export const op = new OpenPanel({
clientId,
disabled: clientId === 'undefined' || !clientId,
// apiUrl: 'http://localhost:3333',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
// sessionReplay: {
// enabled: true,
// }
});