Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/scripts/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async function main() {
imported_at: null,
sdk_name: 'test-script',
sdk_version: '1.0.0',
groups: [],
});
}

Expand Down
40 changes: 40 additions & 0 deletions apps/api/src/controllers/track.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { generateDeviceId, parseUserAgent } from '@openpanel/common/server';
import {
getProfileById,
getSalts,
groupBuffer,
replayBuffer,
upsertProfile,
} from '@openpanel/db';
import { type GeoLocation, getGeoLocation } from '@openpanel/geo';
import { getEventsGroupQueueShard } from '@openpanel/queue';
import { getRedisCache } from '@openpanel/redis';
import {
type IAssignGroupPayload,
type IDecrementPayload,
type IGroupPayload,
type IIdentifyPayload,
type IIncrementPayload,
type IReplayPayload,
Expand Down Expand Up @@ -210,6 +213,7 @@ async function handleTrack(
headers,
event: {
...payload,
groups: payload.groups ?? [],
timestamp: timestamp.value,
isTimestampFromThePast: timestamp.isFromPast,
},
Expand Down Expand Up @@ -324,6 +328,36 @@ async function handleReplay(
await replayBuffer.add(row);
}

async function handleGroup(
payload: IGroupPayload,
context: TrackContext
): Promise<void> {
const { id, type, name, properties = {} } = payload;
await groupBuffer.add({
id,
projectId: context.projectId,
type,
name,
properties,
});
}

async function handleAssignGroup(
payload: IAssignGroupPayload,
context: TrackContext
): Promise<void> {
const profileId = payload.profileId ?? context.deviceId;
if (!profileId) {
return;
}
await upsertProfile({
id: String(profileId),
projectId: context.projectId,
isExternal: !!payload.profileId,
groups: payload.groupIds,
});
}

export async function handler(
request: FastifyRequest<{
Body: ITrackHandlerPayload;
Expand Down Expand Up @@ -372,6 +406,12 @@ export async function handler(
case 'replay':
await handleReplay(validatedBody.payload, context);
break;
case 'group':
await handleGroup(validatedBody.payload, context);
break;
case 'assign_group':
await handleAssignGroup(validatedBody.payload, context);
break;
default:
return reply.status(400).send({
status: 400,
Expand Down
11 changes: 10 additions & 1 deletion apps/public/content/docs/dashboard/understand-the-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ The trailing edge of the line (the current, incomplete interval) is shown as a d

## Insights

If you have configured insights for your project, a scrollable row of insight cards appears below the chart. Each card shows a pre-configured metric with its current value and trend. Clicking a card applies that insight's filter to the entire overview page. Insights are optional—this section is hidden when none have been configured.
A scrollable row of insight cards appears below the chart once your project has at least 30 days of data. OpenPanel automatically detects significant trends across pageviews, entry pages, referrers, and countries—no configuration needed.

Each card shows:
- **Share**: The percentage of total traffic that property represents (e.g., "United States: 42% of all sessions")
- **Absolute change**: The raw increase or decrease in sessions compared to the previous period
- **Percentage change**: How much that property grew or declined relative to its own previous value

For example, if the US had 1,000 sessions last week and 1,200 this week, the card shows "+200 sessions (+20%)".

Clicking any insight card filters the entire overview page to show only data matching that property—letting you drill into what's driving the trend.

---

Expand Down
69 changes: 47 additions & 22 deletions apps/start/src/components/events/table/columns.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -65,31 +64,31 @@ export function useColumns() {
return (
<div className="flex items-center gap-2">
<button
type="button"
className="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">
<button
type="button"
className="font-medium hover:underline"
onClick={() => {
pushModal('EventDetails', {
id: row.original.id,
createdAt: row.original.createdAt,
projectId: row.original.projectId,
});
}}
className="font-medium hover:underline"
type="button"
>
{renderName()}
</button>
Expand All @@ -107,8 +106,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)}
Expand All @@ -119,8 +118,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>
Expand All @@ -130,8 +129,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>
Expand All @@ -152,10 +151,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>
);
},
Expand All @@ -175,7 +174,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>
Expand All @@ -189,7 +188,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>
Expand All @@ -203,13 +202,39 @@ 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>
);
},
},
{
accessorKey: 'groups',
header: 'Groups',
size: 200,
meta: {
hidden: true,
},
cell({ row }) {
const { groups } = row.original;
if (!groups?.length) {
return null;
}
return (
<div className="flex flex-wrap gap-1">
{groups.map((g) => (
<span
className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs"
key={g}
>
{g}
</span>
))}
</div>
);
},
},
{
accessorKey: 'properties',
header: 'Properties',
Expand All @@ -221,14 +246,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({
Expand Down
Loading
Loading