Skip to content

perf(ai): stage identity key sets for the profile queries - #764

Merged
izadoesdev merged 5 commits into
mainfrom
staging
Sep 8, 2026
Merged

perf(ai): stage identity key sets for the profile queries#764
izadoesdev merged 5 commits into
mainfrom
staging

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Sep 8, 2026

Copy link
Copy Markdown
Member

Extends the prepareSql hook from #761 to the two remaining profile queries, and records where the technique stops paying.

builder before after
profile_sessions 1467ms 619ms 2.37x
profile_detail 574ms 627ms 0.92x — not adopted

profile_sessions

It read 4,474,325 rows and 440 MiB to return 4 sessions. target_identity_rows ran 58 times across 173 base-table scans, because target_anonymous_ids, target_session_ids and visitor_ids were each referenced as subqueries at ten sites, and ClickHouse inlines a CTE per reference.

All three are tiny per-profile key sets — 2 to 3 values on the site measured. They now resolve concurrently in a prepare pass and the sites bind parameters. Bound directly, the query reads 623,095 rows and 50.7 MiB.

Three of those sites live inside profileActivityCte itself, which is why this lands at 2.37x rather than the 2.22x from binding only the outer ones.

profile_detail is deliberately not staged

It shares the same helper, so enabling it was a one-line change. Measured, it came out slower: it returns a single row from a comparatively cheap query, so three prepare round trips cost more than the repeated subqueries they remove.

That is the boundary on this technique — staging pays when the repeated subquery is expensive relative to a round trip, and loses when it is not. profile_detail keeps the subquery fallback and is unchanged.

Verification

Output identical for both staged builders across three visitors and date ranges returning 28, 3, 3 and 1, 1, 1 rows, with plugins applied to both sides.

That last detail matters: an earlier comparison looked like a mismatch purely because execute() enriches geo through applyPlugins while raw chQuery does not, so the staged side had more data. Comparing raw SQL output against execute() is not a valid check for any builder with plugins.

673 tests pass, typecheck and lint clean. Also carries #762 and #763, already reviewed.


Summary by cubic

Stages the identity key sets for the profile queries so profile_sessions resolves them once up front instead of re-running subqueries 58 times — reads drop from 4.47M rows / 440 MiB to 623K rows / 50.7 MiB, 2.37x faster; profile_detail keeps the subquery fallback because staging measured slower there. This also carries the organization business context feature: an editable, AI-generated brief per organization that the insights agent consumes as an organization_profile source.

New Features

  • Adds a Business Context settings page where owners can edit the brief manually or generate one from the organization website.
  • Generation runs as a background insights job and stores the draft with source URLs, revision history, and source locking.
  • The insights agent treats the brief as attributed evidence alongside team replies and website pages.

Refactors

  • Organization metadata is now server-managed; Better Auth rejects client-supplied metadata on organization create/update.

Written for commit 05737fa. Summary will update on new commits.

Review in cubic

The hook resolved a single key set, which covered profile_list but not
profile_sessions, where three separate sets feed the query. It now returns
an array of stages, each naming the parameter it binds, and they run
concurrently.

profile_list becomes a one element array and still produces byte identical
output against the single-query form.
profile_sessions read 4,474,325 rows and 440 MiB to return 4. The identity
chain is the cause: target_identity_rows ran 58 times and the query made
173 base table scans, because target_anonymous_ids, target_session_ids and
visitor_ids were each referenced as subqueries at seven sites and ClickHouse
inlines a CTE per reference.

All three are tiny per-profile key sets, 2 to 3 values here, so prepareSql
now resolves them concurrently and the seven sites bind parameters instead.
profileActivityCte takes the resolved keys and falls back to the original
subqueries when they are absent, so profile_detail, which shares that
helper, is untouched.

Measured on the busiest website, three runs each: 1290ms to 580ms, 2.22x.
Bound directly the query reads 623,095 rows and 50.7 MiB instead of 4.5M
and 440 MiB.

Output verified identical across three visitors and date ranges returning
28, 3 and 3 sessions, comparing the single-query form with plugins applied
to both sides. An earlier comparison looked like a mismatch only because
execute() enriches geo through applyPlugins and raw chQuery does not.
Three more visitor_ids subqueries live inside profileActivityCte itself, so
profile_sessions was still re-running that chain for them. Binding them the
same way takes it from 2.22x to 2.37x, 1467ms to 619ms on the busiest site.

The prepare stages are now a shared profileIdentityPrepareStages helper
rather than being spelled out inside profile_sessions.

profile_detail deliberately does not opt in. It shares the helper and was
the obvious next candidate, but measured at 574ms to 627ms, 0.92x: it
returns a single row from a comparatively cheap query, so three prepare
round trips cost more than the repeated subqueries they remove. It keeps
the subquery fallback and is unchanged.

Output verified identical for both builders across three visitors and date
ranges, with plugins applied to both sides.
* feat(dashboard): add editable organization business context

* fix(dashboard): preserve context provenance and organization setup
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
dashboard (staging) Ready Ready Preview Sep 8, 2026 8:53pm UTC
databuddy-status Ready Ready Preview Sep 8, 2026 8:53pm UTC
documentation (staging) Ready Ready Preview Sep 8, 2026 8:53pm UTC

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 131c3208-6545-4c17-bbcb-359bad410a9e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds multi-stage ClickHouse identity-key preparation for profile sessions and carries a broader organization business-context feature through dashboard, RPC, services, auth, and the Insights worker. The staged profile path improves query cost, while the business-context flow adds editable and AI-generated organization briefs with provenance and concurrency handling.

  • Stages profile identity keys before executing profile_sessions.
  • Adds organization business-context storage, generation, editing, authorization, and integration tests.
  • Feeds saved organization context into investigation selection and agent prompts.
  • Protects server-managed organization metadata from native auth updates.
  • Includes two correctness concerns around staged identity consistency and generated-profile provenance, plus lifecycle, coverage, and repository-rule follow-ups.

Confidence Score: 3/5

The PR is not yet safe to merge because staged identity reads can return an inconsistent profile history and generated public-source content can be mislabeled as team-authored context.

Two correctness defects affect the semantic integrity of profile-session results and the provenance supplied to investigations; the worker lifecycle, missing staged-path coverage, and explicit typing rule also need follow-up.

Files Needing Attention: packages/ai/src/query/builders/profiles.ts, packages/services/src/organization-business-context.ts, apps/insights/src/organization-business-context.ts

Important Files Changed

Filename Overview
packages/ai/src/query/builders/profiles.ts Adds three staged identity-key reads for profile sessions, with a consistency risk across independent ClickHouse snapshots and no execution-level regression test.
packages/ai/src/query/simple-builder.ts Generalizes preparation to named concurrent stages and binds their results into the final custom query.
packages/services/src/organization-business-context.ts Implements transactional metadata-backed business-context state, but can incorrectly inherit team provenance for a rewritten AI draft.
apps/insights/src/organization-business-context.ts Generates and bills AI business briefs from validated same-site pages; failure checkpointing can leave generation temporarily stranded.
packages/rpc/src/routers/business-context.ts Adds permission-scoped read, save, and generation endpoints with rate limiting and queue dispatch.
apps/dashboard/app/(main)/organizations/components/business-context-editor.tsx Adds an editor with generation review, optimistic-concurrency conflict handling, source display, and shared UI primitives.
apps/insights/src/business-context.ts Merges saved organization profiles into bounded website investigation context while rechecking tenant scope.
packages/auth/src/auth.ts Prevents native organization create/update APIs from replacing server-managed metadata.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    ProfileRequest[Profile sessions request] --> Prepare[Resolve identity key stages]
    Prepare --> Anon[Anonymous IDs]
    Prepare --> Sessions[Session IDs]
    Prepare --> Visitors[Visitor IDs]
    Anon --> ProfileQuery[Execute profile sessions query]
    Sessions --> ProfileQuery
    Visitors --> ProfileQuery
    ProfileQuery --> Plugins[Apply result plugins]
    Plugins --> ProfileRows[Session history]

    Dashboard[Organization settings] --> RPC[Business-context RPC]
    RPC --> Store[Organization metadata service]
    RPC --> Queue[Insights queue]
    Queue --> Worker[Business-context generator]
    Worker --> PublicPages[Validated website pages]
    Worker --> Store
    Store --> ContextLoader[Investigation context loader]
    ContextLoader --> Agent[Signal selection and investigation agent]
Loading

Reviews (1): Last reviewed commit: "fix(dashboard): match business context t..." | Re-trigger Greptile

Comment on lines +333 to +354
return [
{
as: "targetAnonIds",
column: "anonymous_id",
params,
sql: `${identity} SELECT anonymous_id FROM target_anonymous_ids`,
},
{
as: "targetSessionIds",
column: "session_id",
params,
sql: `${identity} SELECT session_id FROM target_session_ids`,
},
{
as: "targetVisitorIds",
column: "anonymous_id",
params,
sql: `${identity}
SELECT anonymous_id FROM target_anonymous_ids
UNION DISTINCT
SELECT {visitorId:String} AS anonymous_id`,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Identity reads lose consistency

The three identity sets and the final profile query now run as four independent ClickHouse requests. If identity events are ingested between those requests, or cached prepare results have different ages, the final query can combine anonymous and session IDs from different data states. This can omit newly linked activity or return an inconsistent session history. Resolve the related sets from one staged query or otherwise preserve a consistent identity snapshot.

Knowledge Base Used:

Comment on lines +255 to +260
let origin: "team" | "website" = "team";
if (generated?.draft?.content === brief.content) {
origin =
current.profile?.origin === "team" && current.profile.content
? "team"
: "website";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Generated drafts inherit trust

When an unchanged generated draft replaces a nonempty team-authored profile, this code keeps origin: "team" solely because of the previous profile. Regeneration is not required to retain the earlier team assertions, so an AI rewrite based on public sources can be labeled as team-supplied context. Investigation prompts then give those generated claims stronger provenance than they have.

Knowledge Base Used: Analytics and intelligence workflows

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +434 to +444
const remaining = Math.max(1, 120_000 - (performance.now() - started));
await bounded(
markBusinessContextGeneration({
...input,
status: "failed",
error: signal.aborted
? "Generation took too long. Try again; your saved context is unchanged."
: failure,
}),
AbortSignal.timeout(Math.floor(remaining))
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failure state can strand

If this attempt to persist the failure state times out or encounters the service's four-second organization lock timeout, the new error escapes the catch. Because the job has only one attempt and no recovery sweep repairs these metadata-backed generations, the stored state remains running. The dashboard can therefore report generation in progress until the passive 180-second expiry is observed. Guard or retry this checkpoint so the original failure cannot strand the lifecycle.

Knowledge Base Used: Analytics and intelligence workflows

Comment on lines 994 to +996
allowedFilters: ["anonymous_id"],
requiredFilters: ["anonymous_id"],
prepareSql: profileIdentityPrepareStages,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Staged path lacks coverage

The production profile_sessions path now depends on execute() resolving three prepare stages, but the profile tests call customSql directly and exercise only the inline-subquery fallback. Add an execution-level test for prepared parameter binding, empty identity sets, and staged-builder batch routing. Otherwise, regressions in the new path introduced by this PR can pass while production results fail.

Knowledge Base Used: Analytics query engine

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +84 to +86
export async function generateOrganizationBusinessContext(
payload: unknown
): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Explicit typing rule violated

This worker boundary introduces payload: unknown, while the repository requires explicit types instead of any, unknown, or never. Define a concrete parsed job-payload type here. The same new pattern appears in the organization metadata helpers and contextError, so those instances must also be corrected before merging.

Context Used: Basic guidelines for the project so vibe coders do... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@izadoesdev
izadoesdev merged commit 7e8b5c5 into main Sep 8, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant