From 073ca8a6d8a347c18a824182e5824ed95d5dee04 Mon Sep 17 00:00:00 2001 From: Murali Date: Tue, 7 Jul 2026 00:37:48 +0530 Subject: [PATCH 1/2] feat: search profiles and sessions by profile id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile search only matched email/first/last name — useless for backend_userid-identified users with no email or name. Sessions search now also matches profile_id. Patterns escaped via sqlstring. Co-Authored-By: Claude Fable 5 --- packages/db/src/services/profile.service.ts | 6 ++++-- packages/db/src/services/session.service.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/db/src/services/profile.service.ts b/packages/db/src/services/profile.service.ts index 16b17780f..880e50115 100644 --- a/packages/db/src/services/profile.service.ts +++ b/packages/db/src/services/profile.service.ts @@ -202,7 +202,8 @@ export async function getProfileList({ sb.offset = Math.max(0, (cursor ?? 0) * take); sb.orderBy.created_at = 'created_at DESC'; if (search) { - sb.where.search = `(email ILIKE '%${search}%' OR first_name ILIKE '%${search}%' OR last_name ILIKE '%${search}%')`; + const pattern = sqlstring.escape(`%${search}%`); + sb.where.search = `(id ILIKE ${pattern} OR email ILIKE ${pattern} OR first_name ILIKE ${pattern} OR last_name ILIKE ${pattern})`; } if (isExternal !== undefined) { sb.where.external = `is_external = ${isExternal ? 'true' : 'false'}`; @@ -222,7 +223,8 @@ export async function getProfileListCount({ sb.where.project_id = `project_id = ${sqlstring.escape(projectId)}`; sb.groupBy.project_id = 'project_id'; if (search) { - sb.where.search = `(email ILIKE '%${search}%' OR first_name ILIKE '%${search}%' OR last_name ILIKE '%${search}%')`; + const pattern = sqlstring.escape(`%${search}%`); + sb.where.search = `(id ILIKE ${pattern} OR email ILIKE ${pattern} OR first_name ILIKE ${pattern} OR last_name ILIKE ${pattern})`; } if (isExternal !== undefined) { sb.where.external = `is_external = ${isExternal ? 'true' : 'false'}`; diff --git a/packages/db/src/services/session.service.ts b/packages/db/src/services/session.service.ts index 5d14fff74..55a59f00b 100644 --- a/packages/db/src/services/session.service.ts +++ b/packages/db/src/services/session.service.ts @@ -180,7 +180,7 @@ export async function getSessionList({ sb.where.profileId = `profile_id = ${sqlstring.escape(profileId)}`; if (search) { const s = sqlstring.escape(`%${search}%`); - sb.where.search = `(entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; + sb.where.search = `(profile_id ILIKE ${s} OR entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; } if (filters?.length) { Object.assign(sb.where, getEventFiltersWhereClause(filters)); @@ -304,7 +304,8 @@ export async function getSessionsCount({ } if (search) { - sb.where.search = `(entry_path ILIKE '%${search}%' OR exit_path ILIKE '%${search}%' OR referrer ILIKE '%${search}%' OR referrer_name ILIKE '%${search}%')`; + const s = sqlstring.escape(`%${search}%`); + sb.where.search = `(profile_id ILIKE ${s} OR entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; } if (filters && filters.length > 0) { From 3fb4a122aef061bb4650d58fa585a66074189ba0 Mon Sep 17 00:00:00 2001 From: Murali Date: Tue, 7 Jul 2026 01:42:37 +0530 Subject: [PATCH 2/2] address review: primary-key fast path for id-shaped searches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ILIKE '%…%' scans every row of the project — email/name search already times out on large projects, and adding id to the same disjunction inherits that. Instead, single-token searches (16+ chars, no @/spaces — uids/uuids/cuids) now short-circuit to a prefix LIKE on id, which prunes via the (project_id, id) primary key regardless of table size. Email and name search is unchanged from main. Co-Authored-By: Claude Fable 5 --- packages/db/src/services/profile.service.ts | 24 +++++++++++++++++---- packages/db/src/services/session.service.ts | 22 ++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/db/src/services/profile.service.ts b/packages/db/src/services/profile.service.ts index 880e50115..466f954be 100644 --- a/packages/db/src/services/profile.service.ts +++ b/packages/db/src/services/profile.service.ts @@ -154,6 +154,24 @@ interface GetProfileListOptions { isExternal?: boolean; } +// Single-token identifiers (firebase uids, uuids, cuids): no spaces or "@", +// 16+ chars. ID-shaped searches resolve via the (project_id, id) primary key +// instead of ILIKE-scanning email/name columns, which times out on projects +// with tens of millions of profiles. +export function looksLikeProfileId(search: string) { + return /^[A-Za-z0-9_.:-]{16,}$/.test(search); +} + +function getProfileSearchWhereClause(search: string) { + if (looksLikeProfileId(search)) { + // Prefix LIKE (no leading wildcard) is primary-key-prunable and also + // covers exact matches. The id regex excludes LIKE metacharacters. + return `id LIKE ${sqlstring.escape(`${search}%`)}`; + } + const pattern = sqlstring.escape(`%${search}%`); + return `(email ILIKE ${pattern} OR first_name ILIKE ${pattern} OR last_name ILIKE ${pattern})`; +} + export async function getProfiles(ids: string[], projectId: string) { const filteredIds = uniq(ids.filter((id) => id !== '')); @@ -202,8 +220,7 @@ export async function getProfileList({ sb.offset = Math.max(0, (cursor ?? 0) * take); sb.orderBy.created_at = 'created_at DESC'; if (search) { - const pattern = sqlstring.escape(`%${search}%`); - sb.where.search = `(id ILIKE ${pattern} OR email ILIKE ${pattern} OR first_name ILIKE ${pattern} OR last_name ILIKE ${pattern})`; + sb.where.search = getProfileSearchWhereClause(search); } if (isExternal !== undefined) { sb.where.external = `is_external = ${isExternal ? 'true' : 'false'}`; @@ -223,8 +240,7 @@ export async function getProfileListCount({ sb.where.project_id = `project_id = ${sqlstring.escape(projectId)}`; sb.groupBy.project_id = 'project_id'; if (search) { - const pattern = sqlstring.escape(`%${search}%`); - sb.where.search = `(id ILIKE ${pattern} OR email ILIKE ${pattern} OR first_name ILIKE ${pattern} OR last_name ILIKE ${pattern})`; + sb.where.search = getProfileSearchWhereClause(search); } if (isExternal !== undefined) { sb.where.external = `is_external = ${isExternal ? 'true' : 'false'}`; diff --git a/packages/db/src/services/session.service.ts b/packages/db/src/services/session.service.ts index 55a59f00b..e09d20212 100644 --- a/packages/db/src/services/session.service.ts +++ b/packages/db/src/services/session.service.ts @@ -11,7 +11,11 @@ import { clix } from '../clickhouse/query-builder'; import { createSqlBuilder } from '../sql-builder'; import { getEventFiltersWhereClause } from './chart.service'; import { getOrganizationByProjectIdCached } from './organization.service'; -import { type IServiceProfile, getProfilesCached } from './profile.service'; +import { + type IServiceProfile, + getProfilesCached, + looksLikeProfileId, +} from './profile.service'; export type IClickhouseSession = { id: string; @@ -179,8 +183,12 @@ export async function getSessionList({ if (profileId) sb.where.profileId = `profile_id = ${sqlstring.escape(profileId)}`; if (search) { - const s = sqlstring.escape(`%${search}%`); - sb.where.search = `(profile_id ILIKE ${s} OR entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; + if (looksLikeProfileId(search)) { + sb.where.search = `profile_id LIKE ${sqlstring.escape(`${search}%`)}`; + } else { + const s = sqlstring.escape(`%${search}%`); + sb.where.search = `(entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; + } } if (filters?.length) { Object.assign(sb.where, getEventFiltersWhereClause(filters)); @@ -304,8 +312,12 @@ export async function getSessionsCount({ } if (search) { - const s = sqlstring.escape(`%${search}%`); - sb.where.search = `(profile_id ILIKE ${s} OR entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; + if (looksLikeProfileId(search)) { + sb.where.search = `profile_id LIKE ${sqlstring.escape(`${search}%`)}`; + } else { + const s = sqlstring.escape(`%${search}%`); + sb.where.search = `(entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`; + } } if (filters && filters.length > 0) {