From 89c5b61740fa1c5d6722732d5e8f29ac5c1d17c2 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:53:03 +0300 Subject: [PATCH 001/129] fix(ai): resolve and validate trait filters centrally --- packages/ai/src/ai/mcp/agent-tools.ts | 4 +- .../ai/mcp/build-batch-query-requests.test.ts | 39 ++++++ packages/ai/src/ai/mcp/mcp-utils.ts | 10 +- packages/ai/src/ai/mcp/tools.ts | 2 +- packages/ai/src/ai/tools/get-data.ts | 91 +++++------- packages/ai/src/query/batch-executor.ts | 42 ++++-- packages/ai/src/query/builders/pages.ts | 8 ++ packages/ai/src/query/builders/sessions.ts | 34 ++++- packages/ai/src/query/builders/summary.ts | 1 + packages/ai/src/query/index.ts | 18 ++- packages/ai/src/query/simple-builder.ts | 6 +- packages/ai/src/query/trait-filters.test.ts | 130 ++++++++++++++++++ packages/ai/src/query/trait-filters.ts | 69 ++++++++++ 13 files changed, 371 insertions(+), 83 deletions(-) create mode 100644 packages/ai/src/query/trait-filters.test.ts create mode 100644 packages/ai/src/query/trait-filters.ts diff --git a/packages/ai/src/ai/mcp/agent-tools.ts b/packages/ai/src/ai/mcp/agent-tools.ts index 2e527a5f8..bda6e686a 100644 --- a/packages/ai/src/ai/mcp/agent-tools.ts +++ b/packages/ai/src/ai/mcp/agent-tools.ts @@ -189,7 +189,7 @@ Critical schema footguns: website id column is client_id (not website_id); times }), get_data: tool({ description: - "Run 1-10 pre-built analytics queries in one call. Preferred for explicit analytics requests. Covers traffic, pages, sessions, errors, performance, vitals, custom events, profiles, links, uptime, LLM, and revenue. Use preset (last_7d/last_30d/etc.) or from/to dates. Supports filters, groupBy, orderBy. If a type is invalid, the server returns valid options.", + "Run 1-10 pre-built analytics queries in one call. Preferred for explicit analytics requests. Covers traffic, pages, sessions, errors, performance, vitals, custom events, profiles, links, uptime, LLM, and revenue. Use preset (last_7d/last_30d/etc.) or from/to dates. Supports filters (column names or trait: to segment by identified-user traits, e.g. trait:plan), groupBy, orderBy. If a type or filter field is invalid, the error lists valid options.", strict: true, inputSchema: z.object({ websiteId: z.string(), @@ -244,7 +244,7 @@ Critical schema footguns: website id column is client_id (not website_id); times type: r.type, data: r.data, rowCount: r.data.length, - ...(r.error && { error: "Query failed" }), + ...(r.error && { error: r.error }), })), ...invalid.map((q) => ({ type: q.type, diff --git a/packages/ai/src/ai/mcp/build-batch-query-requests.test.ts b/packages/ai/src/ai/mcp/build-batch-query-requests.test.ts index 375afcae7..5c8fd94bb 100644 --- a/packages/ai/src/ai/mcp/build-batch-query-requests.test.ts +++ b/packages/ai/src/ai/mcp/build-batch-query-requests.test.ts @@ -45,4 +45,43 @@ describe("buildBatchQueryRequests", () => { "nonsense_two", ]); }); + + it("rejects filters that are not supported by the query type", () => { + const { requests, invalid } = buildBatchQueryRequests( + [ + { + type: "top_pages", + preset: "last_7d", + filters: [{ field: "secret_col", op: "eq", value: "x" }], + }, + ], + "website-1", + "UTC" + ); + + expect(requests).toHaveLength(0); + expect(invalid[0]?.type).toBe("top_pages"); + expect(invalid[0]?.error).toContain("secret_col"); + expect(invalid[0]?.error).toContain("Allowed fields"); + expect(invalid[0]?.error).toContain("trait:"); + }); + + it("keeps trait filters for the query execution layer to resolve", () => { + const { requests, invalid } = buildBatchQueryRequests( + [ + { + type: "top_pages", + preset: "last_7d", + filters: [{ field: "trait:plan", op: "eq", value: "pro" }], + }, + ], + "website-1", + "UTC" + ); + + expect(invalid).toHaveLength(0); + expect(requests[0]?.filters).toEqual([ + { field: "trait:plan", op: "eq", value: "pro" }, + ]); + }); }); diff --git a/packages/ai/src/ai/mcp/mcp-utils.ts b/packages/ai/src/ai/mcp/mcp-utils.ts index f03dc511e..3291f2bce 100644 --- a/packages/ai/src/ai/mcp/mcp-utils.ts +++ b/packages/ai/src/ai/mcp/mcp-utils.ts @@ -12,7 +12,7 @@ import { resolveDatePreset, } from "../../lib/date-presets"; import { QueryBuilders } from "../../query/builders"; -import { suggestQueryTypes } from "../../query"; +import { invalidFilterFieldError, suggestQueryTypes } from "../../query"; import type { Filter, QueryRequest } from "../../query/types"; import { z } from "zod"; @@ -125,6 +125,14 @@ export function buildBatchQueryRequests( }); continue; } + const filterError = invalidFilterFieldError( + resolvedType, + q.filters as Filter[] | undefined + ); + if (filterError) { + invalid.push({ error: filterError, type: q.type }); + continue; + } requests.push({ projectId: websiteId, type: q.type, diff --git a/packages/ai/src/ai/mcp/tools.ts b/packages/ai/src/ai/mcp/tools.ts index 73c823711..fcfd93912 100644 --- a/packages/ai/src/ai/mcp/tools.ts +++ b/packages/ai/src/ai/mcp/tools.ts @@ -365,7 +365,7 @@ const getDataTool = defineMcpTool( .array(FilterSchema) .optional() .describe( - "Filters [{field, op, value}]. ops: eq, ne, contains, not_contains, starts_with, in, not_in. 'field' is the ClickHouse column name — call get_schema if unsure. Rejected fields return the allowed list for this query." + "Filters [{field, op, value}]. ops: eq, ne, contains, not_contains, starts_with, in, not_in. 'field' is a ClickHouse column name — call get_schema if unsure — or trait: (e.g. trait:plan) to segment by an identified-user trait. Rejected fields return the allowed list for this query." ), groupBy: z.array(z.string()).optional().describe("Fields to group by."), orderBy: z.string().optional().describe("Field to order results by."), diff --git a/packages/ai/src/ai/tools/get-data.ts b/packages/ai/src/ai/tools/get-data.ts index 9ef2767e2..f6b376838 100644 --- a/packages/ai/src/ai/tools/get-data.ts +++ b/packages/ai/src/ai/tools/get-data.ts @@ -1,14 +1,9 @@ -import { - isTraitFilterField, - resolveTraitSegment, - type TraitFilter, - TraitFilterError, -} from "@databuddy/services/identity"; +import { TraitFilterError } from "@databuddy/services/identity"; import { captureError } from "../../lib/tracing"; import { tool } from "ai"; import { z } from "zod"; import { getWebsiteDomain } from "../../lib/website-utils"; -import { executeQuery, QueryBuilders } from "../../query"; +import { executeQuery, hasTraitFilters, QueryBuilders } from "../../query"; import { shiftDate, todayInTimeZone } from "../../query/date-utils"; import type { QueryRequest } from "../../query/types"; import { getAppContext, resolveToolWebsite } from "./utils"; @@ -105,27 +100,6 @@ function describeTraitFilterError(error: unknown): string { return "Trait filter failed"; } -async function resolveItemFilters( - websiteId: string, - filters: QueryItem["filters"] -): Promise { - if (!filters?.length) { - return filters; - } - const traitFilters = filters.filter((f) => isTraitFilterField(f.field)); - if (traitFilters.length === 0) { - return filters; - } - const segment = await resolveTraitSegment( - websiteId, - traitFilters as TraitFilter[] - ); - return [ - ...filters.filter((f) => !isTraitFilterField(f.field)), - { field: "profile_id", op: "in" as const, value: segment }, - ]; -} - const PRESET_DAYS = { last_7d: 7, last_14d: 14, @@ -203,20 +177,6 @@ export const getDataTool = tool({ }; } - let filters: QueryItem["filters"]; - try { - filters = await resolveItemFilters(websiteId, item.filters); - } catch (error) { - return { - type: item.type, - websiteId, - data: [], - rowCount: 0, - executionTime: Date.now() - queryStart, - error: describeTraitFilterError(error), - }; - } - const domain = resolvedDomain || (await getWebsiteDomain(websiteId)); const timezone = item.timezone ?? ctx.timezone ?? "UTC"; const { from, to } = resolveDates(item, timezone); @@ -226,28 +186,43 @@ export const getDataTool = tool({ from, to, timeUnit: item.timeUnit, - filters: filters as QueryRequest["filters"], + filters: item.filters as QueryRequest["filters"], groupBy: item.groupBy, orderBy: item.orderBy, limit: item.limit, timezone, }; - const data = await executeQuery(req, domain, timezone); - return { - type: item.type, - websiteId, - summary: buildResultSummary( - item.type, - from, - to, - item.filters, - item.groupBy - ), - data: data.slice(0, MAX_MODEL_ROWS), - rowCount: data.length, - executionTime: Date.now() - queryStart, - }; + try { + const data = await executeQuery(req, domain, timezone); + return { + type: item.type, + websiteId, + summary: buildResultSummary( + item.type, + from, + to, + item.filters, + item.groupBy + ), + data: data.slice(0, MAX_MODEL_ROWS), + rowCount: data.length, + executionTime: Date.now() - queryStart, + }; + } catch (error) { + return { + type: item.type, + websiteId, + data: [], + rowCount: 0, + executionTime: Date.now() - queryStart, + error: hasTraitFilters(req.filters) + ? describeTraitFilterError(error) + : error instanceof Error + ? error.message + : "Query failed", + }; + } }) ); diff --git a/packages/ai/src/query/batch-executor.ts b/packages/ai/src/query/batch-executor.ts index 4e80dd2a6..42b52ba8e 100644 --- a/packages/ai/src/query/batch-executor.ts +++ b/packages/ai/src/query/batch-executor.ts @@ -5,6 +5,7 @@ import { getClickHouseQuerySettings, SimpleQueryBuilder, } from "./simple-builder"; +import { resolveRequestTraitFilters } from "./trait-filters"; import type { QueryRequest, SimpleQueryConfig } from "./types"; import { applyPlugins } from "./utils"; @@ -353,16 +354,11 @@ async function runSingle( } function groupBySchema( - requests: BatchRequest[] + items: { index: number; req: BatchRequest }[] ): Map { const groups = new Map(); - for (let i = 0; i < requests.length; i++) { - const req = requests[i]; - if (!req) { - continue; - } - + for (const { index, req } of items) { const config = QueryBuilders[req.type]; if (!config) { continue; @@ -370,7 +366,7 @@ function groupBySchema( const sig = getSchemaSignature(req.type, config) || `__solo_${req.type}`; const list = groups.get(sig) || []; - list.push({ index: i, req }); + list.push({ index, req }); groups.set(sig, list); } @@ -454,12 +450,36 @@ export async function executeBatch( batch_types: requests.map((r) => r.type).join(","), }); - if (requests.length === 1 && requests[0]) { - return [await runSingle(requests[0], opts)]; + const traitFailures = new Map(); + const resolvedRequests = await Promise.all( + requests.map(async (req, index) => { + try { + return await resolveRequestTraitFilters(req); + } catch (e) { + const error = e instanceof Error ? e.message : "Trait filter failed"; + mergeWideEvent({ query_error: error }); + traitFailures.set(index, { type: req.type, data: [], error }); + return req; + } + }) + ); + + if ( + traitFailures.size === 0 && + resolvedRequests.length === 1 && + resolvedRequests[0] + ) { + return [await runSingle(resolvedRequests[0], opts)]; } - const groups = groupBySchema(requests); + const successfulRequests = resolvedRequests.flatMap((req, index) => + traitFailures.has(index) ? [] : [{ index, req }] + ); + const groups = groupBySchema(successfulRequests); const results: BatchResult[] = Array.from({ length: requests.length }); + for (const [index, failure] of traitFailures) { + results[index] = failure; + } async function runGroup( groupItems: { index: number; req: BatchRequest }[] diff --git a/packages/ai/src/query/builders/pages.ts b/packages/ai/src/query/builders/pages.ts index 709323256..a950c255e 100644 --- a/packages/ai/src/query/builders/pages.ts +++ b/packages/ai/src/query/builders/pages.ts @@ -29,6 +29,8 @@ export const PagesBuilders: Record = { "utm_source", "utm_medium", "utm_campaign", + "profile_id", + "anonymous_id", ], customizable: true, plugins: { @@ -99,6 +101,7 @@ export const PagesBuilders: Record = { "utm_source", "utm_medium", "utm_campaign", + "profile_id", ], customizable: true, plugins: { @@ -212,6 +215,7 @@ export const PagesBuilders: Record = { "utm_source", "utm_medium", "utm_campaign", + "profile_id", ], customizable: true, plugins: { @@ -336,6 +340,8 @@ export const PagesBuilders: Record = { "utm_source", "utm_medium", "utm_campaign", + "profile_id", + "anonymous_id", ], customizable: true, plugins: { @@ -355,6 +361,8 @@ export const PagesBuilders: Record = { "utm_source", "utm_medium", "utm_campaign", + "profile_id", + "anonymous_id", ], customizable: true, plugins: { diff --git a/packages/ai/src/query/builders/sessions.ts b/packages/ai/src/query/builders/sessions.ts index 47d9a5c9c..482bc7b83 100644 --- a/packages/ai/src/query/builders/sessions.ts +++ b/packages/ai/src/query/builders/sessions.ts @@ -17,7 +17,9 @@ export const SessionsBuilders: Record = { tags: ["sessions", "metrics", "overview"], }, customSql: (ctx) => { - const { websiteId, startDate, endDate } = ctx; + const { websiteId, startDate, endDate, filterConditions, filterParams } = + ctx; + const filterClause = appendFilterClause(filterConditions); return { sql: ` WITH session_rollup AS ( @@ -33,6 +35,7 @@ export const SessionsBuilders: Record = { AND time >= toDateTime({startDate:String}) AND time <= toDateTime(concat({endDate:String}, ' 23:59:59')) AND session_id != '' + ${filterClause} GROUP BY session_id ) SELECT @@ -42,10 +45,11 @@ export const SessionsBuilders: Record = { sum(total_events) as total_events FROM session_rollup `, - params: { websiteId, startDate, endDate }, + params: { websiteId, startDate, endDate, ...filterParams }, }; }, timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -72,6 +76,7 @@ export const SessionsBuilders: Record = { groupBy: ["duration_range"], orderBy: "sessions DESC", timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -92,6 +97,7 @@ export const SessionsBuilders: Record = { groupBy: ["device_type"], orderBy: "sessions DESC", timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -113,6 +119,7 @@ export const SessionsBuilders: Record = { orderBy: "sessions DESC", limit: 100, timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -133,6 +140,7 @@ export const SessionsBuilders: Record = { groupBy: ["toDate(time)"], orderBy: "date ASC", timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -144,7 +152,9 @@ export const SessionsBuilders: Record = { tags: ["sessions", "flow", "paths", "transitions"], }, customSql: (ctx) => { - const { websiteId, startDate, endDate } = ctx; + const { websiteId, startDate, endDate, filterConditions, filterParams } = + ctx; + const filterClause = appendFilterClause(filterConditions); return { sql: ` WITH page_events AS ( @@ -164,6 +174,7 @@ export const SessionsBuilders: Record = { AND event_name = 'screen_view' AND session_id != '' AND path != '' + ${filterClause} ) SELECT path as from_path, @@ -177,10 +188,16 @@ export const SessionsBuilders: Record = { ORDER BY transitions DESC LIMIT 100 `, - params: { websiteId, startDate, endDate: inclusiveEndDate(endDate) }, + params: { + websiteId, + startDate, + endDate: inclusiveEndDate(endDate), + ...filterParams, + }, }; }, timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -202,6 +219,7 @@ export const SessionsBuilders: Record = { orderBy: "sessions DESC", limit: 100, timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, } satisfies SimpleQueryConfig, @@ -213,9 +231,11 @@ export const SessionsBuilders: Record = { tags: ["sessions", "investigation", "product-usage"], }, customSql: (ctx) => { - const { websiteId, startDate, endDate } = ctx; + const { websiteId, startDate, endDate, filterConditions, filterParams } = + ctx; const limit = ctx.limit ?? 10; const offset = ctx.offset ?? 0; + const filterClause = appendFilterClause(filterConditions); return { sql: ` WITH base_sessions AS ( @@ -239,6 +259,7 @@ export const SessionsBuilders: Record = { AND time >= toDateTime({startDate:String}) AND time <= toDateTime({endDate:String}) AND session_id != '' + ${filterClause} GROUP BY session_id ), custom_counts AS ( @@ -346,9 +367,11 @@ export const SessionsBuilders: Record = { endDate: inclusiveEndDate(endDate), limit, offset, + ...filterParams, }, }; }, + allowedFilters: ["profile_id", "anonymous_id"], plugins: { normalizeGeo: true }, } satisfies SimpleQueryConfig, @@ -474,6 +497,7 @@ export const SessionsBuilders: Record = { }, }; }, + allowedFilters: ["profile_id", "anonymous_id"], plugins: { normalizeGeo: true, }, diff --git a/packages/ai/src/query/builders/summary.ts b/packages/ai/src/query/builders/summary.ts index 20cebae7e..fdaa94353 100644 --- a/packages/ai/src/query/builders/summary.ts +++ b/packages/ai/src/query/builders/summary.ts @@ -138,6 +138,7 @@ export const SummaryBuilders: Record = { }; }, timeField: "time", + allowedFilters: ["profile_id", "anonymous_id"], customizable: true, plugins: { sessionAttribution: true }, }, diff --git a/packages/ai/src/query/index.ts b/packages/ai/src/query/index.ts index 8e486a5c1..672cd5916 100644 --- a/packages/ai/src/query/index.ts +++ b/packages/ai/src/query/index.ts @@ -2,6 +2,10 @@ import { z } from "zod"; import { QueryBuilders, suggestQueryTypes } from "./builders"; import { SimpleQueryBuilder } from "./simple-builder"; +import { + invalidFilterFieldError, + resolveRequestTraitFilters, +} from "./trait-filters"; import type { FilterOperators, QueryRequest, TimeGranularity } from "./types"; const FILTER_OPS = [ @@ -85,7 +89,14 @@ export const executeQuery = async ( websiteDomain?: string | null, timezone?: string, abortSignal?: AbortSignal -) => createBuilder(request, websiteDomain, timezone).execute(abortSignal); +) => { + const filterError = invalidFilterFieldError(request.type, request.filters); + if (filterError) { + throw new Error(filterError); + } + const resolved = await resolveRequestTraitFilters(request); + return createBuilder(resolved, websiteDomain, timezone).execute(abortSignal); +}; export const compileQuery = ( request: QueryRequest, @@ -102,4 +113,9 @@ export { export * from "./builders"; export * from "./expressions"; export { allowedFilterFields, isFilterFieldAllowed } from "./simple-builder"; +export { + hasTraitFilters, + invalidFilterFieldError, + resolveRequestTraitFilters, +} from "./trait-filters"; export * from "./types"; diff --git a/packages/ai/src/query/simple-builder.ts b/packages/ai/src/query/simple-builder.ts index 03d85e8d8..d52eb6f26 100644 --- a/packages/ai/src/query/simple-builder.ts +++ b/packages/ai/src/query/simple-builder.ts @@ -1048,10 +1048,8 @@ export class SimpleQueryBuilder { if (!filter || filter.target || filter.having) { continue; } - // Skip filters for fields not supported by this query type rather - // than throwing — in a multi-query batch, a dimension specific to - // one query type (e.g. "href" for outbound_links) should simply be - // ignored by query types that don't know about it. + // Unsupported filters are skipped so one multi-query batch can share + // dimension-specific filters without breaking unrelated query types. if (!isFilterFieldAllowed(this.config, filter.field)) { continue; } diff --git a/packages/ai/src/query/trait-filters.test.ts b/packages/ai/src/query/trait-filters.test.ts new file mode 100644 index 000000000..8d9f17562 --- /dev/null +++ b/packages/ai/src/query/trait-filters.test.ts @@ -0,0 +1,130 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + invalidFilterFieldError, + resolveRequestTraitFilters, +} from "./trait-filters"; +import type { QueryRequest } from "./types"; + +const mockResolveTraitSegment = vi.fn(); + +vi.mock("@databuddy/services/identity", () => { + class TraitFilterError extends Error {} + + return { + TraitFilterError, + isTraitFilterField: (field: string) => + field.startsWith("trait:") && field.length > "trait:".length, + resolveTraitSegment: mockResolveTraitSegment, + }; +}); + +function makeRequest(overrides: Partial = {}): QueryRequest { + return { + projectId: "test-site", + type: "top_pages", + from: "2026-04-01", + to: "2026-04-11", + ...overrides, + }; +} + +describe("resolveRequestTraitFilters", () => { + beforeEach(() => { + mockResolveTraitSegment.mockReset(); + mockResolveTraitSegment.mockResolvedValue(["profile-a", "profile-b"]); + }); + + it("returns the original request when no trait filters are present", async () => { + const request = makeRequest({ + filters: [{ field: "path", op: "eq", value: "/" }], + }); + + await expect(resolveRequestTraitFilters(request)).resolves.toBe(request); + expect(mockResolveTraitSegment).not.toHaveBeenCalled(); + }); + + it("resolves trait filters to a profile_id segment filter", async () => { + const request = makeRequest({ + filters: [ + { field: "path", op: "eq", value: "/" }, + { field: "trait:plan", op: "eq", value: "pro" }, + { field: "trait:role", op: "in", value: ["admin", "owner"] }, + ], + }); + + const resolved = await resolveRequestTraitFilters(request); + + expect(mockResolveTraitSegment).toHaveBeenCalledWith("test-site", [ + { field: "trait:plan", op: "eq", value: "pro" }, + { field: "trait:role", op: "in", value: ["admin", "owner"] }, + ]); + expect(resolved.filters).toEqual([ + { field: "path", op: "eq", value: "/" }, + { field: "profile_id", op: "in", value: ["profile-a", "profile-b"] }, + ]); + }); + + it("rejects trait filters for organization-scoped queries", async () => { + await expect( + resolveRequestTraitFilters( + makeRequest({ + organizationWebsiteIds: ["site-a", "site-b"], + projectId: "org-id", + filters: [{ field: "trait:plan", op: "eq", value: "pro" }], + }) + ) + ).rejects.toThrow("website-scoped queries"); + expect(mockResolveTraitSegment).not.toHaveBeenCalled(); + }); + + it("rejects query types that cannot filter by profile_id", async () => { + await expect( + resolveRequestTraitFilters( + makeRequest({ + type: "session_events", + filters: [{ field: "trait:plan", op: "eq", value: "pro" }], + }) + ) + ).rejects.toThrow("Trait filters are not supported for session_events"); + expect(mockResolveTraitSegment).not.toHaveBeenCalled(); + }); +}); + +describe("invalidFilterFieldError", () => { + it("rejects fields outside the query type's allowlist with the allowed list", () => { + const error = invalidFilterFieldError("session_metrics", [ + { field: "definitely_not_a_column", op: "eq", value: "x" }, + ]); + expect(error).toContain("definitely_not_a_column"); + expect(error).toContain("session_metrics"); + expect(error).toContain("profile_id"); + }); + + it("accepts allowed, global, and trait filter fields", () => { + expect( + invalidFilterFieldError("session_metrics", [ + { field: "country", op: "eq", value: "US" }, + { field: "profile_id", op: "eq", value: "user-1" }, + { field: "trait:plan", op: "eq", value: "free" }, + ]) + ).toBeNull(); + }); + + it("ignores target and having scoped filters", () => { + expect( + invalidFilterFieldError("session_metrics", [ + { field: "session_count", op: "eq", value: 3, having: true }, + { field: "custom_field", op: "eq", value: "x", target: "my_cte" }, + ]) + ).toBeNull(); + }); + + it("returns null for unknown query types and empty filters", () => { + expect( + invalidFilterFieldError("nope", [ + { field: "custom", op: "eq", value: "x" }, + ]) + ).toBeNull(); + expect(invalidFilterFieldError("session_metrics", [])).toBeNull(); + }); +}); diff --git a/packages/ai/src/query/trait-filters.ts b/packages/ai/src/query/trait-filters.ts new file mode 100644 index 000000000..64d2e35d3 --- /dev/null +++ b/packages/ai/src/query/trait-filters.ts @@ -0,0 +1,69 @@ +import { + isTraitFilterField, + resolveTraitSegment, + type TraitFilter, + TraitFilterError, +} from "@databuddy/services/identity"; +import { QueryBuilders } from "./builders"; +import { allowedFilterFields, isFilterFieldAllowed } from "./simple-builder"; +import type { Filter, QueryRequest } from "./types"; + +export function hasTraitFilters(filters: Filter[] | undefined): boolean { + return Boolean(filters?.some((f) => isTraitFilterField(f.field))); +} + +export function invalidFilterFieldError( + type: string, + filters: Filter[] | undefined +): string | null { + const config = QueryBuilders[type]; + if (!(config && filters?.length)) { + return null; + } + const disallowed = filters.find( + (f) => + !( + f.target || + f.having || + isTraitFilterField(f.field) || + isFilterFieldAllowed(config, f.field) + ) + ); + if (!disallowed) { + return null; + } + const allowed = allowedFilterFields(config).sort().join(", "); + return `Filter on field '${disallowed.field}' is not permitted for ${type}. Allowed fields: ${allowed}. Identified-user traits can be filtered with trait: (e.g. trait:plan) on query types that accept profile_id.`; +} + +export async function resolveRequestTraitFilters( + request: QueryRequest +): Promise { + if (!hasTraitFilters(request.filters)) { + return request; + } + if (request.organizationWebsiteIds) { + throw new TraitFilterError( + "Trait filters are only supported for website-scoped queries." + ); + } + const config = QueryBuilders[request.type]; + if (config && !isFilterFieldAllowed(config, "profile_id")) { + throw new TraitFilterError( + `Trait filters are not supported for ${request.type}. Query types that support them accept a profile_id filter.` + ); + } + const filters = request.filters ?? []; + const traitFilters = filters.filter((f) => isTraitFilterField(f.field)); + const segment = await resolveTraitSegment( + request.projectId, + traitFilters as TraitFilter[] + ); + return { + ...request, + filters: [ + ...filters.filter((f) => !isTraitFilterField(f.field)), + { field: "profile_id", op: "in", value: segment }, + ], + }; +} From b9d5abe6beb550f5a27f2beab1fb6aa3ff94d677 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:53:34 +0300 Subject: [PATCH 002/129] fix(dashboard): stop session guard from revoking sessions on transient fetch failures --- apps/dashboard/components/providers/session-guard.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/dashboard/components/providers/session-guard.tsx b/apps/dashboard/components/providers/session-guard.tsx index db50d64e8..2eee6cc80 100644 --- a/apps/dashboard/components/providers/session-guard.tsx +++ b/apps/dashboard/components/providers/session-guard.tsx @@ -13,9 +13,7 @@ export function SessionGuard({ children }: { children: React.ReactNode }) { } clearPersistedQueryCache(); - authClient.signOut().finally(() => { - window.location.href = "/login"; - }); + window.location.href = "/login"; }, [isPending, session]); return <>{children}; From ed9e2d1818078766641c81262a7c9c443379114f Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:55:48 +0300 Subject: [PATCH 003/129] fix(tracker): assert the real vitals payload shape in e2e test --- packages/tracker/tests/vitals.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/tracker/tests/vitals.spec.ts b/packages/tracker/tests/vitals.spec.ts index ab9570b49..8dd86fd3a 100644 --- a/packages/tracker/tests/vitals.spec.ts +++ b/packages/tracker/tests/vitals.spec.ts @@ -1,7 +1,6 @@ import { expect, test } from "./test-utils"; type WebVitalEvent = { - name: "web_vital"; timestamp: number; path: string; metricName: string; @@ -43,9 +42,9 @@ test.describe("Web Vitals Tracking", () => { const validMetrics = ["FCP", "LCP", "CLS", "INP", "TTFB", "FPS"]; for (const vital of allVitals) { - expect(vital.name).toBe("web_vital"); expect(validMetrics).toContain(vital.metricName); expect(typeof vital.metricValue).toBe("number"); + expect(typeof vital.path).toBe("string"); } console.table( From 1f177bd256eecd1c77f35a8ee5dda0c150262b70 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 19:10:30 +0300 Subject: [PATCH 004/129] feat(dashboard): lifetime value column on users page --- .../app/(main)/websites/[id]/users/page.tsx | 20 +++++++++++++++++++ apps/dashboard/types/analytics.ts | 1 + packages/ai/src/query/builders/profiles.ts | 11 ++++++++++ 3 files changed, 32 insertions(+) diff --git a/apps/dashboard/app/(main)/websites/[id]/users/page.tsx b/apps/dashboard/app/(main)/websites/[id]/users/page.tsx index 19ca07931..6666193a3 100644 --- a/apps/dashboard/app/(main)/websites/[id]/users/page.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/users/page.tsx @@ -40,6 +40,7 @@ import { useAtomValue } from "jotai"; import Image from "next/image"; import { notFound, useParams, useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { formatCurrency } from "@/lib/formatters"; import { generateProfileName } from "./[userId]/_components/generate-profile-name"; import { type ProfileSort, useProfilesData } from "./use-users"; import { useEventNames } from "./use-event-names"; @@ -169,6 +170,9 @@ function SkeletonRow() { + + + @@ -507,6 +511,22 @@ export default function UsersPage() { }, size: 70, }, + { + id: "ltv", + header: "LTV", + cell: ({ row }) => { + const ltv = row.original.ltv ?? 0; + if (ltv === 0) { + return ; + } + return ( + + {formatCurrency(ltv)} + + ); + }, + size: 90, + }, { id: "type", header: "Type", diff --git a/apps/dashboard/types/analytics.ts b/apps/dashboard/types/analytics.ts index fb0ba24b6..5adcc12f3 100644 --- a/apps/dashboard/types/analytics.ts +++ b/apps/dashboard/types/analytics.ts @@ -14,6 +14,7 @@ export interface ProfileData { email: string | null; first_visit: string; last_visit: string; + ltv: number; os_name: string; profile_id: string; referrer: string; diff --git a/packages/ai/src/query/builders/profiles.ts b/packages/ai/src/query/builders/profiles.ts index 5f2cb5a1c..44f7ab46f 100644 --- a/packages/ai/src/query/builders/profiles.ts +++ b/packages/ai/src/query/builders/profiles.ts @@ -333,6 +333,15 @@ export const ProfilesBuilders: Record = { AND ${CUSTOM_EVENTS_VISITOR_KEY} IN (SELECT visitor_id FROM visitor_profiles) GROUP BY visitor_id ), + visitor_revenue AS ( + SELECT + ${CUSTOM_EVENTS_VISITOR_KEY} as visitor_id, + toFloat64(sumIf(amount, type != 'refund') - sumIf(amount, type = 'refund')) as ltv + FROM ${Analytics.revenue} + WHERE (owner_id = {websiteId:String} OR website_id = {websiteId:String}) + AND ${CUSTOM_EVENTS_VISITOR_KEY} IN (SELECT visitor_id FROM visitor_profiles) + GROUP BY visitor_id + ), visitor_sessions AS ( SELECT ${EVENTS_VISITOR_KEY} as visitor_id, @@ -390,6 +399,7 @@ export const ProfilesBuilders: Record = { vp.referrer AS referrer, COALESCE(vce.custom_event_count, 0) as custom_event_count, COALESCE(vce.unique_event_names, 0) as unique_event_names, + COALESCE(vr.ltv, 0) as ltv, COALESCE(vs.session_id, '') as session_id, COALESCE(vs.session_start, '') as session_start, COALESCE(vs.session_end, '') as session_end, @@ -406,6 +416,7 @@ export const ProfilesBuilders: Record = { COALESCE(vs.events, []) as events FROM visitor_profiles vp LEFT JOIN visitor_custom_events vce ON vp.visitor_id = vce.visitor_id + LEFT JOIN visitor_revenue vr ON vp.visitor_id = vr.visitor_id LEFT JOIN visitor_sessions vs ON vp.visitor_id = vs.visitor_id ORDER BY vp.${profileSort}, vs.session_start DESC `, From cd7a4e1932783c85827b3ef1576946d5d9c32760 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 19:12:52 +0300 Subject: [PATCH 005/129] feat(dashboard): find users by email via deterministic hash lookup --- .../src/integration/profile-handlers.test.ts | 27 +++++++++++++ .../app/(main)/websites/[id]/users/page.tsx | 34 +++++++++++++++++ packages/rpc/src/routers/profiles.ts | 38 ++++++++++++++++++- 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/apps/api/src/integration/profile-handlers.test.ts b/apps/api/src/integration/profile-handlers.test.ts index 0cbe3ca74..042fad296 100644 --- a/apps/api/src/integration/profile-handlers.test.ts +++ b/apps/api/src/integration/profile-handlers.test.ts @@ -251,6 +251,33 @@ describe("upsertProfile trait history", () => { }); }); +describe("profiles.findByEmail", () => { + iit("finds a profile by exact email via the lookup hash", async () => { + const user = await signUp(); + const org = await insertOrganization(); + await addToOrganization(user.id, org.id, "member"); + const website = await insertWebsite({ organizationId: org.id }); + await upsertProfile( + website.id, + "user_1", + splitTraits({ email: "Jo@Acme.com", name: "Jo" }) + ); + const ctx = userContext(user, org.id); + + const found = await call(appRouter.profiles.findByEmail, ctx)({ + websiteId: website.id, + email: "jo@acme.com", + }); + expect(found).toEqual({ profileId: "user_1" }); + + const missing = await call(appRouter.profiles.findByEmail, ctx)({ + websiteId: website.id, + email: "nobody@acme.com", + }); + expect(missing).toBeNull(); + }); +}); + describe("profiles.traitKeys / traitValues", () => { iit("lists distinct keys and values for the website only", async () => { const user = await signUp(); diff --git a/apps/dashboard/app/(main)/websites/[id]/users/page.tsx b/apps/dashboard/app/(main)/websites/[id]/users/page.tsx index 6666193a3..679df65f7 100644 --- a/apps/dashboard/app/(main)/websites/[id]/users/page.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/users/page.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/table"; import { useDateFilters } from "@/hooks/use-date-filters"; import { useTraitKeys, useTraitValues } from "@/hooks/use-profiles"; +import { orpc } from "@/lib/orpc"; import { getDeviceIcon } from "@/components/device-icon"; import { dynamicQueryFiltersAtom } from "@/stores/jotai/filterAtoms"; import type { DynamicQueryFilter } from "@/stores/jotai/filterAtoms"; @@ -27,6 +28,7 @@ import { ArrowUpIcon, GlobeIcon, LightningIcon, + MagnifyingGlassIcon, TagIcon, UsersIcon, } from "@databuddy/ui/icons"; @@ -48,10 +50,12 @@ import { Badge, Button, EmptyState, + Input, Skeleton, Tooltip, dayjs, } from "@databuddy/ui"; +import { toast } from "sonner"; import { DropdownMenu } from "@databuddy/ui/client"; const wwwRegex = /^www\./; @@ -201,6 +205,7 @@ export default function UsersPage() { order: "desc", }); const [eventFilter, setEventFilter] = useState(null); + const [emailQuery, setEmailQuery] = useState(""); const [traitFilter, setTraitFilter] = useState<{ key: string; value: string | null; @@ -724,6 +729,35 @@ export default function UsersPage() { const refreshAction = ( +
{ + e.preventDefault(); + const email = emailQuery.trim().toLowerCase(); + if (!email) { + return; + } + const found = await orpc.profiles.findByEmail + .call({ websiteId, email }) + .catch(() => null); + if (found) { + router.push(`/websites/${websiteId}/users/${found.profileId}`); + } else { + toast.info("No user found with that email"); + } + }} + > +
+ + setEmailQuery(e.target.value)} + placeholder="Find by email" + type="email" + value={emailQuery} + /> +
+
+ + + )} + + + ); +} diff --git a/apps/dashboard/lib/ai-components/schemas.ts b/apps/dashboard/lib/ai-components/schemas.ts index 713924128..39fa6777b 100644 --- a/apps/dashboard/lib/ai-components/schemas.ts +++ b/apps/dashboard/lib/ai-components/schemas.ts @@ -116,6 +116,21 @@ export const linkPreviewSchema = z }) .passthrough(); +export const feedbackPreviewSchema = z + .object({ + type: z.literal("feedback-preview"), + mode: z.enum(["offer", "sent"]), + feedback: z + .object({ + title: z.string(), + category: z.string().optional(), + description: z.string(), + }) + .passthrough(), + message: z.string().optional(), + }) + .passthrough(); + const funnelStepSchema = z .object({ type: z.enum(["PAGE_VIEW", "EVENT", "CUSTOM"]), @@ -302,6 +317,7 @@ export const componentSchemaMap: Record = { "mini-map": miniMapSchema, "links-list": linksListSchema, "link-preview": linkPreviewSchema, + "feedback-preview": feedbackPreviewSchema, "funnels-list": funnelsListSchema, "funnel-preview": funnelPreviewSchema, "dashboard-actions": dashboardActionsSchema, diff --git a/apps/dashboard/lib/ai-components/types.ts b/apps/dashboard/lib/ai-components/types.ts index e49ea0e3c..cd0bfe46f 100644 --- a/apps/dashboard/lib/ai-components/types.ts +++ b/apps/dashboard/lib/ai-components/types.ts @@ -91,6 +91,17 @@ export interface LinkPreviewInput { type: "link-preview"; } +export interface FeedbackPreviewInput { + feedback: { + title: string; + category?: string; + description: string; + }; + message?: string; + mode: "offer" | "sent"; + type: "feedback-preview"; +} + export interface FunnelStepInput { name: string; target: string; From 63397080d0f11cff36970e68d301b5e786571cef Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:44:12 +0300 Subject: [PATCH 033/129] feat(ai): vague-report gate and routing for feedback requests --- packages/ai/src/ai/mcp/run-agent.test.ts | 6 ++++++ packages/ai/src/ai/mcp/run-agent.ts | 2 +- packages/ai/src/ai/prompts/analytics.ts | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/ai/mcp/run-agent.test.ts b/packages/ai/src/ai/mcp/run-agent.test.ts index 8969ad520..277a9f9e8 100644 --- a/packages/ai/src/ai/mcp/run-agent.test.ts +++ b/packages/ai/src/ai/mcp/run-agent.test.ts @@ -29,6 +29,12 @@ describe("MCP agent active tool selection", () => { source: "slack", }) ).toBeUndefined(); + expect( + selectActiveToolsForQuestion({ + question: "this agent sucks, tell the team", + source: "dashboard", + }) + ).toBeUndefined(); }); it("keeps all tools available when the user reports something broken", () => { diff --git a/packages/ai/src/ai/mcp/run-agent.ts b/packages/ai/src/ai/mcp/run-agent.ts index ddd0e0676..29539a391 100644 --- a/packages/ai/src/ai/mcp/run-agent.ts +++ b/packages/ai/src/ai/mcp/run-agent.ts @@ -400,7 +400,7 @@ const INVESTIGATION_REQUEST_PATTERN = /\b(why|what caused|root cause|because|reason|investigate|investigation|diagnose|debug|deploy|deployed|deployment|commit|commits|merged|pull request|pr|branch|release|rollback|regression|search console|google search|keyword|seo|impressions|ranking|scrape|crawl)\b/i; const COPY_ONLY_PATTERN = /\b(exact copy|copy only)\b/i; const FEEDBACK_REQUEST_PATTERN = - /\b(feedback|feature request|report (this|that|it|a bug)|file a bug|send (this|that|it) to)\b/i; + /\b(feedback|feature request|report (this|that|it|a bug)|file a bug|send (this|that|it) to|tell the (databuddy )?team|complain)\b/i; const SLACK_FOLLOW_UP_OPEN_TAG = " Date: Sat, 4 Jul 2026 23:44:12 +0300 Subject: [PATCH 034/129] feat(evals): vague complaint feedback eval case --- packages/evals/src/cases/feedback.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/evals/src/cases/feedback.ts b/packages/evals/src/cases/feedback.ts index f75ac700a..386c394f8 100644 --- a/packages/evals/src/cases/feedback.ts +++ b/packages/evals/src/cases/feedback.ts @@ -51,6 +51,24 @@ export const feedbackCases: EvalCase[] = [ ], }, }, + { + id: "feedback-vague-complaint-gets-clarified", + category: "behavioral", + name: "Asks for specifics instead of submitting a vague complaint", + query: "this agent sucks, tell the team", + websiteId: WS, + surfaces: ["agent", "slack"], + tags: ["feedback"], + expect: { + toolsNotCalled: ["submit_feedback"], + responseMatches: [ + { + pattern: "\\?", + description: "asks a clarifying question about what went wrong", + }, + ], + }, + }, { id: "feedback-own-website-errors-stay-analytics", category: "behavioral", From 7c9b0d055d814eef266e0006dab62400a1c71aaa Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:44:37 +0300 Subject: [PATCH 035/129] feat(charts): server-side chart PNG rendering with dashboard theme --- bun.lock | 54 ++++- packages/charts/assets/fonts/bold.otf | Bin 0 -> 174024 bytes packages/charts/assets/fonts/medium.otf | Bin 0 -> 169344 bytes packages/charts/assets/fonts/regular.otf | Bin 0 -> 164276 bytes packages/charts/assets/fonts/semibold.otf | Bin 0 -> 170544 bytes packages/charts/package.json | 26 ++ packages/charts/src/index.test.ts | 86 +++++++ packages/charts/src/index.ts | 81 +++++++ packages/charts/src/options.ts | 275 ++++++++++++++++++++++ packages/charts/src/themes.ts | 26 ++ packages/charts/tsconfig.json | 5 + 11 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 packages/charts/assets/fonts/bold.otf create mode 100644 packages/charts/assets/fonts/medium.otf create mode 100644 packages/charts/assets/fonts/regular.otf create mode 100644 packages/charts/assets/fonts/semibold.otf create mode 100644 packages/charts/package.json create mode 100644 packages/charts/src/index.test.ts create mode 100644 packages/charts/src/index.ts create mode 100644 packages/charts/src/options.ts create mode 100644 packages/charts/src/themes.ts create mode 100644 packages/charts/tsconfig.json diff --git a/bun.lock b/bun.lock index 0ce7a02d2..b0712e945 100644 --- a/bun.lock +++ b/bun.lock @@ -509,6 +509,19 @@ "unbuild": "^3.6.1", }, }, + "packages/charts": { + "name": "@databuddy/charts", + "version": "0.1.0", + "dependencies": { + "@resvg/resvg-js": "^2.6.2", + "echarts": "^6.1.0", + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "^26.0.0", + "typescript": "catalog:", + }, + }, "packages/db": { "name": "@databuddy/db", "version": "1.0.0", @@ -678,6 +691,7 @@ "@types/jszip": "^3.4.1", "autumn-js": "catalog:", "dayjs": "^1.11.19", + "drizzle-orm": "catalog:", "evlog": "catalog:", "jszip": "^3.10.1", "keypal": "0.2.0", @@ -691,7 +705,7 @@ }, "packages/sdk": { "name": "@databuddy/sdk", - "version": "2.5.0", + "version": "2.6.0", "devDependencies": { "@playwright/test": "^1.58.2", "@types/bun": "catalog:", @@ -1132,6 +1146,8 @@ "@databuddy/cache": ["@databuddy/cache@workspace:packages/cache"], + "@databuddy/charts": ["@databuddy/charts@workspace:packages/charts"], + "@databuddy/dashboard": ["@databuddy/dashboard@workspace:apps/dashboard"], "@databuddy/db": ["@databuddy/db@workspace:packages/db"], @@ -1962,6 +1978,32 @@ "@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="], + "@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="], + + "@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="], + + "@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.6.2", "", { "os": "android", "cpu": "arm64" }, "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ=="], + + "@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A=="], + + "@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw=="], + + "@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.6.2", "", { "os": "linux", "cpu": "arm" }, "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw=="], + + "@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg=="], + + "@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg=="], + + "@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw=="], + + "@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ=="], + + "@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ=="], + + "@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.6.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w=="], + + "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="], @@ -2956,6 +2998,8 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "echarts": ["echarts@6.1.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.1.0" } }, "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "effect": ["effect@4.0.0-beta.92", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-et6Fy8X9cxhzV/w4U372lB7YCP/B9a8FYVxlx9UMzSgnmwWKxpldnaHz7mHpZHh+OU+ENhU9DO/rTAakmT0sEA=="], @@ -4808,6 +4852,8 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zrender": ["zrender@6.1.0", "", { "dependencies": { "tslib": "2.3.0" } }, "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ=="], + "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -4872,6 +4918,8 @@ "@databuddy/docs/resend": ["resend@6.12.3", "", { "dependencies": { "postal-mime": "2.7.4", "svix": "1.92.2" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-FkEi6YPnVL96/LvH8+QP7NaeaBy5brYXwlRqUCqZZeNL0/iyKij18IPmyPXYauT/2ODn1JG04qKz+qlJfzqzTw=="], + "@databuddy/rpc/drizzle-orm": ["drizzle-orm@1.0.0-rc.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-jGCqAgxpz+OSHP2jQGooUHBxnFMTYl0TTRSfULBl52VNf7CtyNRnazUi+VdbSxvJrDP2lnIsmUh5O+HhKeSJCg=="], + "@databuddy/test/drizzle-orm": ["drizzle-orm@1.0.0-rc.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-jGCqAgxpz+OSHP2jQGooUHBxnFMTYl0TTRSfULBl52VNf7CtyNRnazUi+VdbSxvJrDP2lnIsmUh5O+HhKeSJCg=="], "@databuddy/uptime/effect": ["effect@4.0.0-beta.66", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw=="], @@ -5348,6 +5396,8 @@ "ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "echarts/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], + "effect/uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], "engine.io/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], @@ -5632,6 +5682,8 @@ "zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], + "@anthropic-ai/tokenizer/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], diff --git a/packages/charts/assets/fonts/bold.otf b/packages/charts/assets/fonts/bold.otf new file mode 100644 index 0000000000000000000000000000000000000000..4db588de575ed924caa946ea1da039e999d5c4e3 GIT binary patch literal 174024 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