wip
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@openpanel/queue';
|
} from '@openpanel/queue';
|
||||||
import { getRedisCache } from '@openpanel/redis';
|
import { getRedisCache } from '@openpanel/redis';
|
||||||
import {
|
import {
|
||||||
|
type IAssignGroupPayload,
|
||||||
type IDecrementPayload,
|
type IDecrementPayload,
|
||||||
type IGroupPayload,
|
type IGroupPayload,
|
||||||
type IIdentifyPayload,
|
type IIdentifyPayload,
|
||||||
@@ -341,31 +342,29 @@ async function handleGroup(
|
|||||||
context: TrackContext
|
context: TrackContext
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { id, type, name, properties = {} } = payload;
|
const { id, type, name, properties = {} } = payload;
|
||||||
const profileId = payload.profileId ?? context.deviceId;
|
await groupBuffer.add({
|
||||||
|
|
||||||
const promises: Promise<unknown>[] = [];
|
|
||||||
promises.push(
|
|
||||||
groupBuffer.add({
|
|
||||||
id,
|
id,
|
||||||
projectId: context.projectId,
|
projectId: context.projectId,
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
properties,
|
properties,
|
||||||
})
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
if (profileId) {
|
async function handleAssignGroup(
|
||||||
promises.push(
|
payload: IAssignGroupPayload,
|
||||||
upsertProfile({
|
context: TrackContext
|
||||||
|
): Promise<void> {
|
||||||
|
const profileId = payload.profileId ?? context.deviceId;
|
||||||
|
if (!profileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await upsertProfile({
|
||||||
id: String(profileId),
|
id: String(profileId),
|
||||||
projectId: context.projectId,
|
projectId: context.projectId,
|
||||||
isExternal: !!(payload.profileId ?? context.identity?.profileId),
|
isExternal: !!payload.profileId,
|
||||||
groups: [id],
|
groups: payload.groupIds,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handler(
|
export async function handler(
|
||||||
@@ -419,6 +418,9 @@ export async function handler(
|
|||||||
case 'group':
|
case 'group':
|
||||||
await handleGroup(validatedBody.payload, context);
|
await handleGroup(validatedBody.payload, context);
|
||||||
break;
|
break;
|
||||||
|
case 'assign_group':
|
||||||
|
await handleAssignGroup(validatedBody.payload, context);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return reply.status(400).send({
|
return reply.status(400).send({
|
||||||
status: 400,
|
status: 400,
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
import {
|
|
||||||
useXAxisProps,
|
|
||||||
useYAxisProps,
|
|
||||||
} from '@/components/report-chart/common/axis';
|
|
||||||
import { Widget, WidgetBody } from '@/components/widget';
|
|
||||||
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
|
|
||||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
|
||||||
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
|
|
||||||
import { TrendingUpIcon } from 'lucide-react';
|
import { TrendingUpIcon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
@@ -16,6 +8,14 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
|
||||||
|
import {
|
||||||
|
useXAxisProps,
|
||||||
|
useYAxisProps,
|
||||||
|
} from '@/components/report-chart/common/axis';
|
||||||
|
import { Widget, WidgetBody } from '@/components/widget';
|
||||||
|
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
|
||||||
|
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -37,10 +37,16 @@ function Tooltip(props: any) {
|
|||||||
{formatDate(new Date(payload.timestamp))}
|
{formatDate(new Date(payload.timestamp))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-10 w-1 rounded-full" style={{ background: getChartColor(0) }} />
|
<div
|
||||||
|
className="h-10 w-1 rounded-full"
|
||||||
|
style={{ background: getChartColor(0) }}
|
||||||
|
/>
|
||||||
<div className="col gap-1">
|
<div className="col gap-1">
|
||||||
<div className="text-muted-foreground text-sm">Total members</div>
|
<div className="text-muted-foreground text-sm">Total members</div>
|
||||||
<div className="font-semibold text-lg" style={{ color: getChartColor(0) }}>
|
<div
|
||||||
|
className="font-semibold text-lg"
|
||||||
|
style={{ color: getChartColor(0) }}
|
||||||
|
>
|
||||||
{number.format(payload.cumulative)}
|
{number.format(payload.cumulative)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +93,7 @@ export function GroupMemberGrowth({ data }: Props) {
|
|||||||
<ResponsiveContainer>
|
<ResponsiveContainer>
|
||||||
<AreaChart data={chartData}>
|
<AreaChart data={chartData}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
|
||||||
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
|
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor={color} stopOpacity={0.02} />
|
<stop offset="95%" stopColor={color} stopOpacity={0.02} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
@@ -97,17 +103,18 @@ export function GroupMemberGrowth({ data }: Props) {
|
|||||||
cursor={{ stroke: color, strokeOpacity: 0.3 }}
|
cursor={{ stroke: color, strokeOpacity: 0.3 }}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
|
||||||
dataKey="cumulative"
|
dataKey="cumulative"
|
||||||
|
dot={false}
|
||||||
|
fill={`url(#${gradientId})`}
|
||||||
|
isAnimationActive={false}
|
||||||
stroke={color}
|
stroke={color}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
fill={`url(#${gradientId})`}
|
type="monotone"
|
||||||
dot={false}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
/>
|
||||||
<XAxis {...xAxisProps} />
|
<XAxis {...xAxisProps} />
|
||||||
<YAxis {...yAxisProps} domain={[0, 'dataMax']} />
|
<YAxis {...yAxisProps} />
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
|
className="stroke-border"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
strokeOpacity={0.5}
|
strokeOpacity={0.5}
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export default function BillingUsage({ organization }: Props) {
|
|||||||
<XAxis {...xAxisProps} dataKey="date" />
|
<XAxis {...xAxisProps} dataKey="date" />
|
||||||
<YAxis {...yAxisProps} domain={[0, 'dataMax']} />
|
<YAxis {...yAxisProps} domain={[0, 'dataMax']} />
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
|
className="stroke-border"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
strokeOpacity={0.5}
|
strokeOpacity={0.5}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Widget } from '@/components/widget';
|
import { ZapIcon } from 'lucide-react';
|
||||||
|
import { Widget, WidgetEmptyState } from '@/components/widget';
|
||||||
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
|
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -6,12 +7,15 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MostEvents = ({ data }: Props) => {
|
export const MostEvents = ({ data }: Props) => {
|
||||||
const max = Math.max(...data.map((item) => item.count));
|
const max = data.length > 0 ? Math.max(...data.map((item) => item.count)) : 0;
|
||||||
return (
|
return (
|
||||||
<Widget className="w-full">
|
<Widget className="w-full">
|
||||||
<WidgetHead>
|
<WidgetHead>
|
||||||
<WidgetTitle>Popular events</WidgetTitle>
|
<WidgetTitle>Popular events</WidgetTitle>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
|
{data.length === 0 ? (
|
||||||
|
<WidgetEmptyState icon={ZapIcon} text="No events yet" />
|
||||||
|
) : (
|
||||||
<div className="flex flex-col gap-1 p-1">
|
<div className="flex flex-col gap-1 p-1">
|
||||||
{data.slice(0, 5).map((item) => (
|
{data.slice(0, 5).map((item) => (
|
||||||
<div key={item.name} className="relative px-3 py-2">
|
<div key={item.name} className="relative px-3 py-2">
|
||||||
@@ -28,6 +32,7 @@ export const MostEvents = ({ data }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Widget } from '@/components/widget';
|
import { RouteIcon } from 'lucide-react';
|
||||||
|
import { Widget, WidgetEmptyState } from '@/components/widget';
|
||||||
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
|
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -6,12 +7,15 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PopularRoutes = ({ data }: Props) => {
|
export const PopularRoutes = ({ data }: Props) => {
|
||||||
const max = Math.max(...data.map((item) => item.count));
|
const max = data.length > 0 ? Math.max(...data.map((item) => item.count)) : 0;
|
||||||
return (
|
return (
|
||||||
<Widget className="w-full">
|
<Widget className="w-full">
|
||||||
<WidgetHead>
|
<WidgetHead>
|
||||||
<WidgetTitle>Most visted pages</WidgetTitle>
|
<WidgetTitle>Most visted pages</WidgetTitle>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
|
{data.length === 0 ? (
|
||||||
|
<WidgetEmptyState icon={RouteIcon} text="No pages visited yet" />
|
||||||
|
) : (
|
||||||
<div className="flex flex-col gap-1 p-1">
|
<div className="flex flex-col gap-1 p-1">
|
||||||
{data.slice(0, 5).map((item) => (
|
{data.slice(0, 5).map((item) => (
|
||||||
<div key={item.path} className="relative px-3 py-2">
|
<div key={item.path} className="relative px-3 py-2">
|
||||||
@@ -28,6 +32,7 @@ export const PopularRoutes = ({ data }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,6 +54,19 @@ export function WidgetBody({ children, className }: WidgetBodyProps) {
|
|||||||
return <div className={cn('p-4', className)}>{children}</div>;
|
return <div className={cn('p-4', className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WidgetEmptyStateProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
export function WidgetEmptyState({ icon: Icon, text }: WidgetEmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2 py-8 text-muted-foreground">
|
||||||
|
<Icon size={28} strokeWidth={1.5} />
|
||||||
|
<p className="text-sm">{text}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface WidgetProps {
|
export interface WidgetProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { MostEvents } from '@/components/profiles/most-events';
|
|||||||
import { PopularRoutes } from '@/components/profiles/popular-routes';
|
import { PopularRoutes } from '@/components/profiles/popular-routes';
|
||||||
import { ProfileActivity } from '@/components/profiles/profile-activity';
|
import { ProfileActivity } from '@/components/profiles/profile-activity';
|
||||||
import { KeyValueGrid } from '@/components/ui/key-value-grid';
|
import { KeyValueGrid } from '@/components/ui/key-value-grid';
|
||||||
import { Widget, WidgetBody } from '@/components/widget';
|
import { Widget, WidgetBody, WidgetEmptyState } from '@/components/widget';
|
||||||
import { WidgetTable } from '@/components/widget-table';
|
import { WidgetTable } from '@/components/widget-table';
|
||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { formatDateTime, formatTimeAgoOrDateTime } from '@/utils/date';
|
import { formatDateTime, formatTimeAgoOrDateTime } from '@/utils/date';
|
||||||
@@ -64,7 +64,7 @@ function Component() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const g = group.data;
|
const g = group.data;
|
||||||
const m = metrics.data?.[0];
|
const m = metrics.data;
|
||||||
|
|
||||||
if (!g) {
|
if (!g) {
|
||||||
return null;
|
return null;
|
||||||
@@ -177,9 +177,7 @@ function Component() {
|
|||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody className="p-0">
|
<WidgetBody className="p-0">
|
||||||
{members.data.length === 0 ? (
|
{members.data.length === 0 ? (
|
||||||
<p className="py-4 text-center text-muted-foreground text-sm">
|
<WidgetEmptyState icon={UsersIcon} text="No members yet" />
|
||||||
No members found
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<WidgetTable
|
<WidgetTable
|
||||||
columnClassName="px-2"
|
columnClassName="px-2"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function App() {
|
|||||||
for (const id of u.groupIds) {
|
for (const id of u.groupIds) {
|
||||||
const meta = PRESET_GROUPS.find((g) => g.id === id);
|
const meta = PRESET_GROUPS.find((g) => g.id === id);
|
||||||
if (meta) {
|
if (meta) {
|
||||||
op.setGroup(id, meta);
|
op.upsertGroup({ id, ...meta });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,3 +299,11 @@ const ROLLUP_DATE_PREFIX = '1970-01-01';
|
|||||||
export function isClickhouseDefaultMinDate(date: string): boolean {
|
export function isClickhouseDefaultMinDate(date: string): boolean {
|
||||||
return date.startsWith(ROLLUP_DATE_PREFIX) || date.startsWith('1969-12-31');
|
return date.startsWith(ROLLUP_DATE_PREFIX) || date.startsWith('1969-12-31');
|
||||||
}
|
}
|
||||||
|
export function toNullIfDefaultMinDate(date?: string | null): Date | null {
|
||||||
|
if (!date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return isClickhouseDefaultMinDate(date)
|
||||||
|
? null
|
||||||
|
: convertClickhouseDateToJs(date);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
IAliasPayload as AliasPayload,
|
IAliasPayload as AliasPayload,
|
||||||
|
IAssignGroupPayload as AssignGroupPayload,
|
||||||
IDecrementPayload as DecrementPayload,
|
IDecrementPayload as DecrementPayload,
|
||||||
IGroupPayload as GroupPayload,
|
IGroupPayload as GroupPayload,
|
||||||
IIdentifyPayload as IdentifyPayload,
|
IIdentifyPayload as IdentifyPayload,
|
||||||
@@ -13,6 +14,7 @@ import { Api } from './api';
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
AliasPayload,
|
AliasPayload,
|
||||||
|
AssignGroupPayload,
|
||||||
DecrementPayload,
|
DecrementPayload,
|
||||||
GroupPayload,
|
GroupPayload,
|
||||||
IdentifyPayload,
|
IdentifyPayload,
|
||||||
@@ -27,7 +29,7 @@ export interface TrackProperties {
|
|||||||
groups?: string[];
|
groups?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupMetadata = Omit<GroupPayload, 'id'>;
|
export type UpsertGroupPayload = GroupPayload;
|
||||||
|
|
||||||
export interface OpenPanelOptions {
|
export interface OpenPanelOptions {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@@ -187,26 +189,38 @@ export class OpenPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setGroups(groupIds: string[]) {
|
upsertGroup(payload: UpsertGroupPayload) {
|
||||||
this.log('set groups', groupIds);
|
this.log('upsert group', payload);
|
||||||
this.groups = groupIds;
|
return this.send({
|
||||||
|
type: 'group',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setGroup(groupId: string, metadata?: GroupMetadata) {
|
setGroup(groupId: string) {
|
||||||
this.log('set group', groupId, metadata);
|
this.log('set group', groupId);
|
||||||
if (!this.groups.includes(groupId)) {
|
if (!this.groups.includes(groupId)) {
|
||||||
this.groups = [...this.groups, groupId];
|
this.groups = [...this.groups, groupId];
|
||||||
}
|
}
|
||||||
if (metadata) {
|
|
||||||
return this.send({
|
return this.send({
|
||||||
type: 'group',
|
type: 'assign_group',
|
||||||
payload: {
|
payload: {
|
||||||
id: groupId,
|
groupIds: [groupId],
|
||||||
...metadata,
|
|
||||||
profileId: this.profileId,
|
profileId: this.profileId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGroups(groupIds: string[]) {
|
||||||
|
this.log('set groups', groupIds);
|
||||||
|
this.groups = [...new Set([...this.groups, ...groupIds])];
|
||||||
|
return this.send({
|
||||||
|
type: 'assign_group',
|
||||||
|
payload: {
|
||||||
|
groupIds,
|
||||||
|
profileId: this.profileId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,7 +305,7 @@ export class OpenPanel {
|
|||||||
profileId: item.payload.profileId ?? this.profileId,
|
profileId: item.payload.profileId ?? this.profileId,
|
||||||
} as TrackHandlerPayload['payload'];
|
} as TrackHandlerPayload['payload'];
|
||||||
}
|
}
|
||||||
if (item.type === 'group') {
|
if (item.type === 'assign_group') {
|
||||||
return {
|
return {
|
||||||
...item.payload,
|
...item.payload,
|
||||||
profileId: item.payload.profileId ?? this.profileId,
|
profileId: item.payload.profileId ?? this.profileId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getGroupsByIds,
|
getGroupsByIds,
|
||||||
getGroupTypes,
|
getGroupTypes,
|
||||||
TABLE_NAMES,
|
TABLE_NAMES,
|
||||||
|
toNullIfDefaultMinDate,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import sqlstring from 'sqlstring';
|
import sqlstring from 'sqlstring';
|
||||||
@@ -49,7 +50,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
byId: protectedProcedure
|
byId: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.query(async ({ input: { id, projectId } }) => {
|
.query(({ input: { id, projectId } }) => {
|
||||||
return getGroupById(id, projectId);
|
return getGroupById(id, projectId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
properties: z.record(z.string()).default({}),
|
properties: z.record(z.string()).default({}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(({ input }) => {
|
||||||
return createGroup(input);
|
return createGroup(input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -77,26 +78,26 @@ export const groupRouter = createTRPCRouter({
|
|||||||
properties: z.record(z.string()).optional(),
|
properties: z.record(z.string()).optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input: { id, projectId, ...data } }) => {
|
.mutation(({ input: { id, projectId, ...data } }) => {
|
||||||
return updateGroup(id, projectId, data);
|
return updateGroup(id, projectId, data);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.mutation(async ({ input: { id, projectId } }) => {
|
.mutation(({ input: { id, projectId } }) => {
|
||||||
return deleteGroup(id, projectId);
|
return deleteGroup(id, projectId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
types: protectedProcedure
|
types: protectedProcedure
|
||||||
.input(z.object({ projectId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.query(async ({ input: { projectId } }) => {
|
.query(({ input: { projectId } }) => {
|
||||||
return getGroupTypes(projectId);
|
return getGroupTypes(projectId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
metrics: protectedProcedure
|
metrics: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.query(async ({ input: { id, projectId } }) => {
|
.query(async ({ input: { id, projectId } }) => {
|
||||||
return chQuery<{
|
const data = await chQuery<{
|
||||||
totalEvents: number;
|
totalEvents: number;
|
||||||
uniqueProfiles: number;
|
uniqueProfiles: number;
|
||||||
firstSeen: string;
|
firstSeen: string;
|
||||||
@@ -111,11 +112,18 @@ export const groupRouter = createTRPCRouter({
|
|||||||
WHERE project_id = ${sqlstring.escape(projectId)}
|
WHERE project_id = ${sqlstring.escape(projectId)}
|
||||||
AND has(groups, ${sqlstring.escape(id)})
|
AND has(groups, ${sqlstring.escape(id)})
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalEvents: data[0]?.totalEvents ?? 0,
|
||||||
|
uniqueProfiles: data[0]?.uniqueProfiles ?? 0,
|
||||||
|
firstSeen: toNullIfDefaultMinDate(data[0]?.firstSeen),
|
||||||
|
lastSeen: toNullIfDefaultMinDate(data[0]?.lastSeen),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
activity: protectedProcedure
|
activity: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.query(async ({ input: { id, projectId } }) => {
|
.query(({ input: { id, projectId } }) => {
|
||||||
return chQuery<{ count: number; date: string }>(`
|
return chQuery<{ count: number; date: string }>(`
|
||||||
SELECT count() AS count, toStartOfDay(created_at) AS date
|
SELECT count() AS count, toStartOfDay(created_at) AS date
|
||||||
FROM ${TABLE_NAMES.events}
|
FROM ${TABLE_NAMES.events}
|
||||||
@@ -174,7 +182,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
mostEvents: protectedProcedure
|
mostEvents: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.query(async ({ input: { id, projectId } }) => {
|
.query(({ input: { id, projectId } }) => {
|
||||||
return chQuery<{ count: number; name: string }>(`
|
return chQuery<{ count: number; name: string }>(`
|
||||||
SELECT count() as count, name
|
SELECT count() as count, name
|
||||||
FROM ${TABLE_NAMES.events}
|
FROM ${TABLE_NAMES.events}
|
||||||
@@ -189,7 +197,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
popularRoutes: protectedProcedure
|
popularRoutes: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.query(async ({ input: { id, projectId } }) => {
|
.query(({ input: { id, projectId } }) => {
|
||||||
return chQuery<{ count: number; path: string }>(`
|
return chQuery<{ count: number; path: string }>(`
|
||||||
SELECT count() as count, path
|
SELECT count() as count, path
|
||||||
FROM ${TABLE_NAMES.events}
|
FROM ${TABLE_NAMES.events}
|
||||||
@@ -204,7 +212,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
memberGrowth: protectedProcedure
|
memberGrowth: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), projectId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.query(async ({ input: { id, projectId } }) => {
|
.query(({ input: { id, projectId } }) => {
|
||||||
return chQuery<{ date: string; count: number }>(`
|
return chQuery<{ date: string; count: number }>(`
|
||||||
SELECT
|
SELECT
|
||||||
toDate(toStartOfDay(min_date)) AS date,
|
toDate(toStartOfDay(min_date)) AS date,
|
||||||
@@ -228,13 +236,13 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
properties: protectedProcedure
|
properties: protectedProcedure
|
||||||
.input(z.object({ projectId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.query(async ({ input: { projectId } }) => {
|
.query(({ input: { projectId } }) => {
|
||||||
return getGroupPropertyKeys(projectId);
|
return getGroupPropertyKeys(projectId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
listByIds: protectedProcedure
|
listByIds: protectedProcedure
|
||||||
.input(z.object({ projectId: z.string(), ids: z.array(z.string()) }))
|
.input(z.object({ projectId: z.string(), ids: z.array(z.string()) }))
|
||||||
.query(async ({ input: { projectId, ids } }) => {
|
.query(({ input: { projectId, ids } }) => {
|
||||||
return getGroupsByIds(projectId, ids);
|
return getGroupsByIds(projectId, ids);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export const zGroupPayload = z.object({
|
|||||||
type: z.string().min(1),
|
type: z.string().min(1),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
properties: z.record(z.unknown()).optional(),
|
properties: z.record(z.unknown()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zAssignGroupPayload = z.object({
|
||||||
|
groupIds: z.array(z.string().min(1)),
|
||||||
profileId: z.union([z.string().min(1), z.number()]).optional(),
|
profileId: z.union([z.string().min(1), z.number()]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,6 +114,10 @@ export const zTrackHandlerPayload = z.discriminatedUnion('type', [
|
|||||||
type: z.literal('group'),
|
type: z.literal('group'),
|
||||||
payload: zGroupPayload,
|
payload: zGroupPayload,
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('assign_group'),
|
||||||
|
payload: zAssignGroupPayload,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type ITrackPayload = z.infer<typeof zTrackPayload>;
|
export type ITrackPayload = z.infer<typeof zTrackPayload>;
|
||||||
@@ -119,6 +127,7 @@ export type IDecrementPayload = z.infer<typeof zDecrementPayload>;
|
|||||||
export type IAliasPayload = z.infer<typeof zAliasPayload>;
|
export type IAliasPayload = z.infer<typeof zAliasPayload>;
|
||||||
export type IReplayPayload = z.infer<typeof zReplayPayload>;
|
export type IReplayPayload = z.infer<typeof zReplayPayload>;
|
||||||
export type IGroupPayload = z.infer<typeof zGroupPayload>;
|
export type IGroupPayload = z.infer<typeof zGroupPayload>;
|
||||||
|
export type IAssignGroupPayload = z.infer<typeof zAssignGroupPayload>;
|
||||||
export type ITrackHandlerPayload = z.infer<typeof zTrackHandlerPayload>;
|
export type ITrackHandlerPayload = z.infer<typeof zTrackHandlerPayload>;
|
||||||
|
|
||||||
// Deprecated types for beta version of the SDKs
|
// Deprecated types for beta version of the SDKs
|
||||||
|
|||||||
Reference in New Issue
Block a user