This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-06 14:16:09 +01:00
parent c2d12c556d
commit 058c3621df
6 changed files with 30 additions and 21 deletions

View File

@@ -74,7 +74,6 @@ const extraReferrers = {
'rytr.me': { type: 'ai', name: 'Rytr' }, 'rytr.me': { type: 'ai', name: 'Rytr' },
'notion.ai': { type: 'ai', name: 'Notion AI' }, 'notion.ai': { type: 'ai', name: 'Notion AI' },
'grammarly.com': { type: 'ai', name: 'Grammarly' }, 'grammarly.com': { type: 'ai', name: 'Grammarly' },
'bing.com': { type: 'ai', name: 'Bing AI' },
'grok.com': { type: 'ai', name: 'Grok' }, 'grok.com': { type: 'ai', name: 'Grok' },
'x.ai': { type: 'ai', name: 'xAI' }, 'x.ai': { type: 'ai', name: 'xAI' },
'aistudio.google.com': { type: 'ai', name: 'Google AI Studio' }, 'aistudio.google.com': { type: 'ai', name: 'Google AI Studio' },

View File

@@ -167,8 +167,12 @@ export function getChartSql({
const anyBreakdownOnGroup = breakdowns.some((breakdown) => const anyBreakdownOnGroup = breakdowns.some((breakdown) =>
breakdown.name.startsWith('group.') breakdown.name.startsWith('group.')
); );
const anyMetricOnGroup = !!event.property?.startsWith('group.');
const needsGroupArrayJoin = const needsGroupArrayJoin =
anyFilterOnGroup || anyBreakdownOnGroup || event.segment === 'group'; anyFilterOnGroup ||
anyBreakdownOnGroup ||
anyMetricOnGroup ||
event.segment === 'group';
if (needsGroupArrayJoin) { if (needsGroupArrayJoin) {
addCte( addCte(
@@ -453,8 +457,12 @@ export function getAggregateChartSql({
const anyBreakdownOnGroup = breakdowns.some((breakdown) => const anyBreakdownOnGroup = breakdowns.some((breakdown) =>
breakdown.name.startsWith('group.') breakdown.name.startsWith('group.')
); );
const anyMetricOnGroup = !!event.property?.startsWith('group.');
const needsGroupArrayJoin = const needsGroupArrayJoin =
anyFilterOnGroup || anyBreakdownOnGroup || event.segment === 'group'; anyFilterOnGroup ||
anyBreakdownOnGroup ||
anyMetricOnGroup ||
event.segment === 'group';
if (needsGroupArrayJoin) { if (needsGroupArrayJoin) {
addCte( addCte(

View File

@@ -676,6 +676,7 @@ export async function getEventList(options: GetEventListOptions) {
export async function getEventsCount({ export async function getEventsCount({
projectId, projectId,
profileId, profileId,
groupId,
events, events,
filters, filters,
startDate, startDate,
@@ -687,6 +688,10 @@ export async function getEventsCount({
sb.where.profileId = `profile_id = ${sqlstring.escape(profileId)}`; sb.where.profileId = `profile_id = ${sqlstring.escape(profileId)}`;
} }
if (groupId) {
sb.where.groupId = `has(groups, ${sqlstring.escape(groupId)})`;
}
if (startDate && endDate) { if (startDate && endDate) {
sb.where.created_at = `toDate(created_at) BETWEEN toDate('${formatClickhouseDate(startDate)}') AND toDate('${formatClickhouseDate(endDate)}')`; sb.where.created_at = `toDate(created_at) BETWEEN toDate('${formatClickhouseDate(startDate)}') AND toDate('${formatClickhouseDate(endDate)}')`;
} }

View File

@@ -137,7 +137,7 @@ export async function getGroupList({
WHERE ${conditions.join(' AND ')} WHERE ${conditions.join(' AND ')}
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ${take} LIMIT ${take}
OFFSET ${cursor ?? 0} OFFSET ${Math.max(0, (cursor ?? 0) * take)}
`); `);
return rows.map(transformGroup); return rows.map(transformGroup);
} }
@@ -194,15 +194,19 @@ export async function updateGroup(
if (!existing) { if (!existing) {
throw new Error(`Group ${id} not found`); throw new Error(`Group ${id} not found`);
} }
const mergedProperties = {
...(existing.properties ?? {}),
...(data.properties ?? {}),
};
const normalizedProperties = toDots(
mergedProperties as Record<string, unknown>
);
const updated = { const updated = {
id, id,
projectId, projectId,
type: data.type ?? existing.type, type: data.type ?? existing.type,
name: data.name ?? existing.name, name: data.name ?? existing.name,
properties: (data.properties ?? existing.properties) as Record< properties: normalizedProperties,
string,
string
>,
createdAt: existing.createdAt, createdAt: existing.createdAt,
}; };
await writeGroupToCh(updated); await writeGroupToCh(updated);
@@ -314,7 +318,7 @@ export async function getGroupMemberProfiles({
take: number; take: number;
search?: string; search?: string;
}): Promise<{ data: IServiceProfile[]; count: number }> { }): Promise<{ data: IServiceProfile[]; count: number }> {
const offset = Math.max(0, cursor ?? 0); const offset = Math.max(0, (cursor ?? 0) * take);
const searchCondition = search?.trim() const searchCondition = search?.trim()
? `AND (email ILIKE ${sqlstring.escape(`%${search.trim()}%`)} OR first_name ILIKE ${sqlstring.escape(`%${search.trim()}%`)} OR last_name ILIKE ${sqlstring.escape(`%${search.trim()}%`)})` ? `AND (email ILIKE ${sqlstring.escape(`%${search.trim()}%`)} OR first_name ILIKE ${sqlstring.escape(`%${search.trim()}%`)} OR last_name ILIKE ${sqlstring.escape(`%${search.trim()}%`)})`
: ''; : '';

View File

@@ -277,10 +277,7 @@ export class OpenPanel {
const mergedGroups = [...new Set([...this.groups, ...queuedGroups])]; const mergedGroups = [...new Set([...this.groups, ...queuedGroups])];
return { return {
...item.payload, ...item.payload,
profileId: profileId: item.payload.profileId ?? this.profileId,
'profileId' in item.payload
? (item.payload.profileId ?? this.profileId)
: this.profileId,
groups: mergedGroups.length > 0 ? mergedGroups : undefined, groups: mergedGroups.length > 0 ? mergedGroups : undefined,
}; };
} }
@@ -291,11 +288,7 @@ export class OpenPanel {
) { ) {
return { return {
...item.payload, ...item.payload,
profileId: String( profileId: item.payload.profileId ?? this.profileId,
'profileId' in item.payload
? (item.payload.profileId ?? this.profileId)
: (this.profileId ?? '')
),
} as TrackHandlerPayload['payload']; } as TrackHandlerPayload['payload'];
} }
if (item.type === 'group') { if (item.type === 'group') {

View File

@@ -7,15 +7,15 @@ 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(),
profileId: z.string().optional(), profileId: z.union([z.string().min(1), z.number()]).optional(),
}); });
export const zTrackPayload = z export const zTrackPayload = z
.object({ .object({
name: z.string().min(1), name: z.string().min(1),
properties: z.record(z.string(), z.unknown()).optional(), properties: z.record(z.string(), z.unknown()).optional(),
profileId: z.string().or(z.number()).optional(), profileId: z.union([z.string().min(1), z.number()]).optional(),
groups: z.array(z.string()).optional(), groups: z.array(z.string().min(1)).optional(),
}) })
.refine((data) => !RESERVED_EVENT_NAMES.includes(data.name as any), { .refine((data) => !RESERVED_EVENT_NAMES.includes(data.name as any), {
message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(', ')}`, message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(', ')}`,