diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.module.css
new file mode 100644
index 00000000..140ad0bb
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.module.css
@@ -0,0 +1,5 @@
+.link {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
index d4cf827a..73148b83 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
@@ -1,21 +1,27 @@
import Link from 'next/link';
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
-import { useFormat, useMessages } from 'components/hooks';
+import { useFormat, useLocale, useMessages } from 'components/hooks';
import Profile from 'components/common/Profile';
+import styles from './SessionsTable.module.css';
+import { formatDate } from 'lib/date';
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
+ const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
const breakpoint = useBreakpoint();
const { formatValue } = useFormat();
return (
-
- {row => }
-
-
- {row => {row.id}}
+
+ {row => (
+
+
+ {row.id}
+
+ )}
+
{row => formatValue(row.country, 'country')}
@@ -28,7 +34,7 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
{row => formatValue(row.device, 'device')}
- {row => row.lastAt}
+ {row => formatDate(new Date(row.lastAt), 'PPPpp', locale)}
);
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx
index c644fb89..0a15430c 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx
@@ -23,7 +23,6 @@ export function SessionActivity({
return (
-
Activity log
{data.map(({ eventId, createdAt, urlPath, eventName, visitId }) => {
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
lastDay = createdAt;
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.module.css b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.module.css
new file mode 100644
index 00000000..2057622b
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.module.css
@@ -0,0 +1,39 @@
+.data {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ width: 200px;
+ position: relative;
+}
+
+.header {
+ font-weight: bold;
+}
+
+.empty {
+ color: var(--font-color300);
+ text-align: center;
+}
+
+.label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.type {
+ font-size: 10px;
+ text-transform: uppercase;
+ padding: 0 6px;
+ border-radius: 4px;
+ border: 1px solid var(--base300);
+}
+
+.name {
+ color: var(--font-color200);
+ font-weight: bold;
+}
+
+.value {
+ margin: 5px 0;
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx
new file mode 100644
index 00000000..2d99f8b3
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx
@@ -0,0 +1,34 @@
+import { Loading, TextOverflow } from 'react-basics';
+import { useMessages, useSessionData } from 'components/hooks';
+import Empty from 'components/common/Empty';
+import styles from './SessionData.module.css';
+import { DATA_TYPES } from 'lib/constants';
+
+export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading } = useSessionData(websiteId, sessionId);
+
+ if (isLoading) {
+ return
;
+ }
+
+ return (
+
+
{formatMessage(labels.properties)}
+ {!data?.length &&
}
+ {data?.map(({ dataKey, dataType, stringValue }) => {
+ return (
+
+
+
+ {dataKey}
+
+
{DATA_TYPES[dataType]}
+
+
{stringValue}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css
index af5ed201..8e2667f4 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css
+++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css
@@ -2,6 +2,7 @@
display: grid;
grid-template-columns: 300px 1fr max-content;
margin-bottom: 40px;
+ position: relative;
}
.sidebar {
@@ -12,16 +13,32 @@
gap: 20px;
padding-right: 20px;
border-right: 1px solid var(--base300);
+ position: relative;
}
.content {
- padding: 0 20px;
-}
-
-.stats {
display: flex;
flex-direction: column;
- gap: 20px;
+ gap: 30px;
+ padding: 0 20px;
+ position: relative;
+}
+
+.data {
border-left: 1px solid var(--base300);
padding-left: 20px;
+ position: relative;
+ transition: width 200ms ease-in-out;
+}
+
+@media screen and (max-width: 992px) {
+ .page {
+ grid-template-columns: 1fr;
+ gap: 30px;
+ }
+
+ .sidebar,
+ .data {
+ border: 0;
+ }
}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx
index 723e6ff7..d4ed503e 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx
@@ -4,9 +4,10 @@ import SessionInfo from './SessionInfo';
import { useWebsiteSession } from 'components/hooks';
import { Loading } from 'react-basics';
import Profile from 'components/common/Profile';
-import styles from './SessionDetailsPage.module.css';
import { SessionActivity } from './SessionActivity';
-import SessionStats from './SessionStats';
+import { SessionStats } from './SessionStats';
+import { SessionData } from './SessionData';
+import styles from './SessionDetailsPage.module.css';
export default function SessionDetailsPage({
websiteId,
@@ -30,10 +31,11 @@ export default function SessionDetailsPage({
+
-
>
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx
index 574152ec..a25f7af3 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx
@@ -1,14 +1,15 @@
import MetricCard from 'components/metrics/MetricCard';
import { useMessages } from 'components/hooks';
+import MetricsBar from 'components/metrics/MetricsBar';
-export default function SessionStats({ data }) {
+export function SessionStats({ data }) {
const { formatMessage, labels } = useMessages();
return (
- <>
+
- >
+
);
}
diff --git a/src/components/common/Profile.tsx b/src/components/common/Profile.tsx
index 63a59c16..40544994 100644
--- a/src/components/common/Profile.tsx
+++ b/src/components/common/Profile.tsx
@@ -10,9 +10,9 @@ function convertToPastel(hexColor: string, pastelFactor: number = 0.5) {
hexColor = hexColor.replace(/^#/, '');
// Convert hex to RGB
- let r = parseInt(hexColor.substr(0, 2), 16);
- let g = parseInt(hexColor.substr(2, 2), 16);
- let b = parseInt(hexColor.substr(4, 2), 16);
+ let r = parseInt(hexColor.substring(0, 2), 16);
+ let g = parseInt(hexColor.substring(2, 4), 16);
+ let b = parseInt(hexColor.substring(4, 6), 16);
// Calculate pastel version (mix with white)
//const pastelFactor = 0.5; // Adjust this value to control pastel intensity
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
index 4ef89251..e06a0414 100644
--- a/src/components/hooks/index.ts
+++ b/src/components/hooks/index.ts
@@ -6,6 +6,7 @@ export * from './queries/useRealtime';
export * from './queries/useReport';
export * from './queries/useReports';
export * from './queries/useSessionActivity';
+export * from './queries/useSessionData';
export * from './queries/useWebsiteSession';
export * from './queries/useWebsiteSessions';
export * from './queries/useShareToken';
diff --git a/src/components/hooks/queries/useSessionData.ts b/src/components/hooks/queries/useSessionData.ts
new file mode 100644
index 00000000..f2e8f524
--- /dev/null
+++ b/src/components/hooks/queries/useSessionData.ts
@@ -0,0 +1,12 @@
+import { useApi } from './useApi';
+
+export function useSessionData(websiteId: string, sessionId: string) {
+ const { get, useQuery } = useApi();
+
+ return useQuery({
+ queryKey: ['session:data', { websiteId, sessionId }],
+ queryFn: () => {
+ return get(`/sessions/${sessionId}/data`, { websiteId });
+ },
+ });
+}
diff --git a/src/components/messages.ts b/src/components/messages.ts
index 764348fa..2dfe61ee 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -274,6 +274,7 @@ export const labels = defineMessages({
previousYear: { id: 'label.previous-year', defaultMessage: 'Previous year' },
lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' },
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
+ properties: { id: 'label.properties', defaultMessage: 'Properties' },
});
export const messages = defineMessages({
diff --git a/src/pages/api/sessions/[sessionId]/data.ts b/src/pages/api/sessions/[sessionId]/data.ts
new file mode 100644
index 00000000..c0c20064
--- /dev/null
+++ b/src/pages/api/sessions/[sessionId]/data.ts
@@ -0,0 +1,42 @@
+import { canViewWebsite } from 'lib/auth';
+import { useAuth, useCors, useValidate } from 'lib/middleware';
+import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiResponse } from 'next';
+import { methodNotAllowed, ok, unauthorized } from 'next-basics';
+import { getSessionData } from 'queries';
+import * as yup from 'yup';
+
+export interface SessionDataRequestQuery {
+ sessionId: string;
+ websiteId: string;
+}
+
+const schema = {
+ GET: yup.object().shape({
+ sessionId: yup.string().uuid().required(),
+ websiteId: yup.string().uuid().required(),
+ }),
+};
+
+export default async (
+ req: NextApiRequestQueryBody,
+ res: NextApiResponse,
+) => {
+ await useCors(req, res);
+ await useAuth(req, res);
+ await useValidate(schema, req, res);
+
+ if (req.method === 'GET') {
+ const { websiteId, sessionId } = req.query;
+
+ if (!(await canViewWebsite(req.auth, websiteId))) {
+ return unauthorized(res);
+ }
+
+ const data = await getSessionData(websiteId, sessionId);
+
+ return ok(res, data);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/src/queries/analytics/eventData/getEventDataEvents.ts b/src/queries/analytics/events/getEventDataEvents.ts
similarity index 100%
rename from src/queries/analytics/eventData/getEventDataEvents.ts
rename to src/queries/analytics/events/getEventDataEvents.ts
diff --git a/src/queries/analytics/eventData/getEventDataFields.ts b/src/queries/analytics/events/getEventDataFields.ts
similarity index 100%
rename from src/queries/analytics/eventData/getEventDataFields.ts
rename to src/queries/analytics/events/getEventDataFields.ts
diff --git a/src/queries/analytics/eventData/getEventDataStats.ts b/src/queries/analytics/events/getEventDataStats.ts
similarity index 100%
rename from src/queries/analytics/eventData/getEventDataStats.ts
rename to src/queries/analytics/events/getEventDataStats.ts
diff --git a/src/queries/analytics/eventData/getEventDataUsage.ts b/src/queries/analytics/events/getEventDataUsage.ts
similarity index 100%
rename from src/queries/analytics/eventData/getEventDataUsage.ts
rename to src/queries/analytics/events/getEventDataUsage.ts
diff --git a/src/queries/analytics/events/saveEvent.ts b/src/queries/analytics/events/saveEvent.ts
index cd41b7a3..fa4805df 100644
--- a/src/queries/analytics/events/saveEvent.ts
+++ b/src/queries/analytics/events/saveEvent.ts
@@ -4,7 +4,7 @@ import clickhouse from 'lib/clickhouse';
import kafka from 'lib/kafka';
import prisma from 'lib/prisma';
import { uuid } from 'lib/crypto';
-import { saveEventData } from 'queries/analytics/eventData/saveEventData';
+import { saveEventData } from './saveEventData';
export async function saveEvent(args: {
websiteId: string;
diff --git a/src/queries/analytics/eventData/saveEventData.ts b/src/queries/analytics/events/saveEventData.ts
similarity index 100%
rename from src/queries/analytics/eventData/saveEventData.ts
rename to src/queries/analytics/events/saveEventData.ts
diff --git a/src/queries/analytics/sessions/getSessionData.ts b/src/queries/analytics/sessions/getSessionData.ts
new file mode 100644
index 00000000..e3106b27
--- /dev/null
+++ b/src/queries/analytics/sessions/getSessionData.ts
@@ -0,0 +1,41 @@
+import prisma from 'lib/prisma';
+import clickhouse from 'lib/clickhouse';
+import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
+
+export async function getSessionData(...args: [websiteId: string, sessionId: string]) {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(websiteId: string, sessionId: string) {
+ return prisma.client.sessionData.findMany({
+ where: {
+ id: sessionId,
+ websiteId,
+ },
+ });
+}
+
+async function clickhouseQuery(websiteId: string, sessionId: string) {
+ const { rawQuery } = clickhouse;
+
+ return rawQuery(
+ `
+ select
+ website_id as websiteId,
+ session_id as sessionId,
+ data_key as dataKey,
+ data_type as dataType,
+ string_value as stringValue,
+ number_value as numberValue,
+ date_value as dateValue,
+ created_at as createdAt
+ from session_data
+ where website_id = {websiteId:UUID}
+ and session_id = {sessionId:UUID}
+ `,
+ { websiteId, sessionId },
+ );
+}
diff --git a/src/queries/analytics/sessions/getWebsiteSession.ts b/src/queries/analytics/sessions/getWebsiteSession.ts
index 2e6e8eca..df3e8a04 100644
--- a/src/queries/analytics/sessions/getWebsiteSession.ts
+++ b/src/queries/analytics/sessions/getWebsiteSession.ts
@@ -37,9 +37,9 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
city,
min(created_at) as firstAt,
max(created_at) as lastAt,
- uniq(visit_id) as "visits",
- sumIf(1, event_type = 1) as "views",
- sumIf(1, event_type = 2) as "events"
+ uniq(visit_id) as visits,
+ sumIf(1, event_type = 1) as views,
+ sumIf(1, event_type = 2) as events
from website_event
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
diff --git a/src/queries/analytics/sessions/getWebsiteSessions.ts b/src/queries/analytics/sessions/getWebsiteSessions.ts
index 2094c8a4..c5fe6cec 100644
--- a/src/queries/analytics/sessions/getWebsiteSessions.ts
+++ b/src/queries/analytics/sessions/getWebsiteSessions.ts
@@ -42,7 +42,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
subdivision1,
city,
min(created_at) as firstAt,
- max(created_at) as lastAt
+ max(created_at) as lastAt,
+ uniq(visit_id) as visits
from website_event
where website_id = {websiteId:UUID}
${dateQuery}
@@ -52,5 +53,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
`,
params,
pageParams,
- );
+ ).then((result: any) => ({
+ ...result,
+ visits: Number(result.visits),
+ }));
}
diff --git a/src/queries/index.ts b/src/queries/index.ts
index cd7ea280..82d40ee5 100644
--- a/src/queries/index.ts
+++ b/src/queries/index.ts
@@ -3,13 +3,13 @@ export * from 'queries/prisma/team';
export * from 'queries/prisma/teamUser';
export * from 'queries/prisma/user';
export * from 'queries/prisma/website';
+export * from './analytics/events/getEventDataEvents';
+export * from './analytics/events/getEventDataFields';
+export * from './analytics/events/getEventDataStats';
+export * from './analytics/events/getEventDataUsage';
export * from './analytics/events/getEventMetrics';
-export * from './analytics/events/getEventUsage';
export * from './analytics/events/getEvents';
-export * from './analytics/eventData/getEventDataEvents';
-export * from './analytics/eventData/getEventDataFields';
-export * from './analytics/eventData/getEventDataStats';
-export * from './analytics/eventData/getEventDataUsage';
+export * from './analytics/events/getEventUsage';
export * from './analytics/events/saveEvent';
export * from './analytics/reports/getFunnel';
export * from './analytics/reports/getJourney';
@@ -20,6 +20,7 @@ export * from './analytics/pageviews/getPageviewMetrics';
export * from './analytics/pageviews/getPageviewStats';
export * from './analytics/sessions/createSession';
export * from './analytics/sessions/getWebsiteSession';
+export * from './analytics/sessions/getSessionData';
export * from './analytics/sessions/getSessionMetrics';
export * from './analytics/sessions/getWebsiteSessions';
export * from './analytics/sessions/getSessionActivity';