diff --git a/apps/api/src/controllers/profile.controller.ts b/apps/api/src/controllers/profile.controller.ts index 5bb787c8..1971b2a4 100644 --- a/apps/api/src/controllers/profile.controller.ts +++ b/apps/api/src/controllers/profile.controller.ts @@ -24,6 +24,7 @@ export async function updateProfile( await upsertProfile({ id: profileId, + isExternal: true, projectId, properties: { ...(properties ?? {}), @@ -45,7 +46,7 @@ export async function incrementProfileProperty( const { profileId, property, value } = request.body; const projectId = request.projectId; - const profile = await getProfileById(profileId); + const profile = await getProfileById(profileId, projectId); if (!profile) { return reply.status(404).send('Not found'); } @@ -69,6 +70,7 @@ export async function incrementProfileProperty( id: profile.id, projectId, properties: profile.properties, + isExternal: true, }); reply.status(202).send(profile.id); @@ -83,7 +85,7 @@ export async function decrementProfileProperty( const { profileId, property, value } = request.body; const projectId = request.projectId; - const profile = await getProfileById(profileId); + const profile = await getProfileById(profileId, projectId); if (!profile) { return reply.status(404).send('Not found'); } @@ -107,6 +109,7 @@ export async function decrementProfileProperty( id: profile.id, projectId, properties: profile.properties, + isExternal: true, }); reply.status(202).send(profile.id); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list-item.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list-item.tsx index 6dab0d1d..ce1b7dd7 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list-item.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list-item.tsx @@ -6,6 +6,7 @@ import { Tooltiper } from '@/components/ui/tooltip'; import { useAppParams } from '@/hooks/useAppParams'; import { useNumber } from '@/hooks/useNumerFormatter'; import { cn } from '@/utils/cn'; +import { getProfileName } from '@/utils/getters'; import Link from 'next/link'; import type { @@ -96,10 +97,7 @@ export function EventListItem(props: EventListItemProps) {
- + { @@ -108,7 +106,7 @@ export function EventListItem(props: EventListItemProps) { href={`/${organizationSlug}/${projectId}/profiles/${profile?.id}`} className="max-w-[80px] overflow-hidden text-ellipsis whitespace-nowrap text-sm text-muted-foreground hover:underline" > - {profile?.firstName} {profile?.lastName} + {getProfileName(profile)} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx index d11218dc..ff4db3dc 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx @@ -58,7 +58,7 @@ export default async function Page({ const startDate = parseAsString.parseServerSide(searchParams.startDate); const endDate = parseAsString.parseServerSide(searchParams.endDate); const [profile, events, count, conversions] = await Promise.all([ - getProfileById(profileId), + getProfileById(profileId, projectId), getEventList(eventListOptions), getEventsCount(eventListOptions), getConversionEventNames(projectId), diff --git a/apps/dashboard/src/utils/getters.ts b/apps/dashboard/src/utils/getters.ts index 1b5eae29..f8e2afe4 100644 --- a/apps/dashboard/src/utils/getters.ts +++ b/apps/dashboard/src/utils/getters.ts @@ -1,6 +1,15 @@ import type { IServiceProfile } from '@openpanel/db'; export function getProfileName(profile: IServiceProfile | undefined | null) { - if (!profile) return 'No name'; - return [profile.firstName, profile.lastName].filter(Boolean).join(' '); + if (!profile) return 'Unknown'; + + if (!profile.isExternal) { + return profile.id; + } + + return ( + [profile.firstName, profile.lastName].filter(Boolean).join(' ') || + profile.email || + profile.id + ); } diff --git a/packages/db/clickhouse_tables.sql b/packages/db/clickhouse_tables.sql index 7300bb8e..daa4443c 100644 --- a/packages/db/clickhouse_tables.sql +++ b/packages/db/clickhouse_tables.sql @@ -43,7 +43,6 @@ ORDER BY CREATE TABLE openpanel.profiles ( `id` String, - `external_id` String, `first_name` String, `last_name` String, `email` String, @@ -90,4 +89,23 @@ FROM events GROUP BY date, - project_id; \ No newline at end of file + project_id; + +-- DROP external_id and add is_external column +ALTER TABLE + profiles DROP COLUMN external_id; + +ALTER TABLE + profiles +ADD + COLUMN is_external Boolean +AFTER + id; + +ALTER TABLE + profiles +UPDATE + is_external = length(id) != 32 +WHERE + true + and length(id) != 32; \ No newline at end of file diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 7aff8e42..825b0ab5 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -3,7 +3,7 @@ import { escape } from 'sqlstring'; import superjson from 'superjson'; import { v4 as uuid } from 'uuid'; -import { randomSplitName, toDots } from '@openpanel/common'; +import { toDots } from '@openpanel/common'; import { redis, redisPub } from '@openpanel/redis'; import type { IChartEventFilter } from '@openpanel/validation'; @@ -215,14 +215,14 @@ export async function createEvent( `create event ${payload.name} for deviceId: ${payload.deviceId} profileId ${payload.profileId}` ); - const exists = await getProfileById(payload.profileId); + const exists = await getProfileById(payload.profileId, payload.projectId); if (!exists && payload.profileId !== '') { - const { firstName, lastName } = randomSplitName(); await upsertProfile({ id: payload.profileId, + isExternal: false, projectId: payload.projectId, - firstName, - lastName, + firstName: '', + lastName: '', properties: { path: payload.path, country: payload.country, diff --git a/packages/db/src/services/profile.service.ts b/packages/db/src/services/profile.service.ts index 050eb9d6..799efdee 100644 --- a/packages/db/src/services/profile.service.ts +++ b/packages/db/src/services/profile.service.ts @@ -7,13 +7,13 @@ import { ch, chQuery } from '../clickhouse-client'; import { createSqlBuilder } from '../sql-builder'; import { getEventFiltersWhereClause } from './chart.service'; -export async function getProfileById(id: string) { - if (id === '') { +export async function getProfileById(id: string, projectId: string) { + if (id === '' || projectId === '') { return null; } const [profile] = await chQuery( - `SELECT *, created_at as max_created_at FROM profiles WHERE id = ${escape(id)} ORDER BY created_at DESC LIMIT 1` + `SELECT *, created_at as max_created_at FROM profiles WHERE id = ${escape(id)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1` ); if (!profile) { @@ -39,6 +39,7 @@ function getProfileSelectFields() { 'argMax(avatar, created_at) as avatar', 'argMax(properties, created_at) as properties', 'argMax(project_id, created_at) as project_id', + 'argMax(is_external, created_at) as is_external', 'max(created_at) as max_created_at', ].join(', '); } @@ -109,32 +110,14 @@ export async function getProfileListCount({ return data?.count ?? 0; } -export async function getProfilesByExternalId( - externalId: string | null, - projectId: string -) { - if (externalId === null) { - return []; - } - - const data = await chQuery( - `SELECT - ${getProfileSelectFields()} - FROM profiles - GROUP BY id - HAVING project_id = ${escape(projectId)} AND external_id = ${escape(externalId)}` - ); - - return data.map(transformProfile); -} - export type IServiceProfile = Omit< IClickhouseProfile, - 'max_created_at' | 'properties' | 'first_name' | 'last_name' + 'max_created_at' | 'properties' | 'first_name' | 'last_name' | 'is_external' > & { firstName: string; lastName: string; createdAt: Date; + isExternal: boolean; properties: Record & { country?: string; city?: string; @@ -155,6 +138,7 @@ export interface IClickhouseProfile { avatar: string; properties: Record; project_id: string; + is_external: boolean; max_created_at: string; } @@ -166,6 +150,7 @@ export interface IServiceUpsertProfile { email?: string; avatar?: string; properties?: Record; + isExternal: boolean; } export function transformProfile({ @@ -178,6 +163,7 @@ export function transformProfile({ ...profile, firstName: first_name, lastName: last_name, + isExternal: profile.is_external, properties: toObject(profile.properties), createdAt: new Date(max_created_at), }; diff --git a/packages/trpc/src/routers/profile.ts b/packages/trpc/src/routers/profile.ts index 902e4d41..7776c132 100644 --- a/packages/trpc/src/routers/profile.ts +++ b/packages/trpc/src/routers/profile.ts @@ -20,7 +20,7 @@ export const profileRouter = createTRPCRouter({ .map((item) => item.replace(/\.([0-9]+)/g, '[*]')) .map((item) => `properties.${item}`); - properties.push('external_id', 'first_name', 'last_name', 'email'); + properties.push('id', 'first_name', 'last_name', 'email'); return pipe( sort((a, b) => a.length - b.length),