Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images#541
Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images#541izadoesdev wants to merge 86 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
The latest updates on your projects. Learn more about Unkey Deploy
|
Greptile SummaryThis PR centralizes AI query trait-filter resolution into a new
Confidence Score: 4/5The core refactor is sound — trait resolution is cleanly centralized, index bookkeeping in executeBatch is correct, and the session guard fix prevents accidental sign-outs. The two flagged items are edge-case inefficiencies that do not break the happy path. The unknown-type guard in resolveRequestTraitFilters lets an unnecessary identity-service round-trip happen before an unknown query type fails downstream, and the raw ClickHouse error strings now reach the LLM context without any sanitization pass. Neither causes incorrect data or broken flows on valid inputs, but both deserve a follow-up before the code sees heavy use with novel query types or noisy ClickHouse errors. packages/ai/src/query/trait-filters.ts (unknown-type guard) and packages/ai/src/ai/mcp/agent-tools.ts (raw error forwarding) are worth a second look. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant AI as AI Agent / Tool
participant MCP as MCP buildBatchQueryRequests
participant EX as executeBatch
participant TF as resolveRequestTraitFilters
participant ID as Identity Service (resolveTraitSegment)
participant CH as ClickHouse
AI->>MCP: "queries[] with trait:<key> filters"
MCP->>MCP: invalidFilterFieldError() per query
MCP-->>AI: invalid[] (bad field) + requests[] (ok)
AI->>EX: executeBatch(requests)
loop per request (parallel)
EX->>TF: resolveRequestTraitFilters(req)
alt has trait filters
TF->>ID: resolveTraitSegment(projectId, traitFilters)
ID-->>TF: profile_id[]
TF-->>EX: req with profile_id filter
else no trait filters
TF-->>EX: original req (no-op)
end
end
EX->>CH: union query (successful requests)
CH-->>EX: rows
EX-->>AI: BatchResult[] (with errors for failures)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant AI as AI Agent / Tool
participant MCP as MCP buildBatchQueryRequests
participant EX as executeBatch
participant TF as resolveRequestTraitFilters
participant ID as Identity Service (resolveTraitSegment)
participant CH as ClickHouse
AI->>MCP: "queries[] with trait:<key> filters"
MCP->>MCP: invalidFilterFieldError() per query
MCP-->>AI: invalid[] (bad field) + requests[] (ok)
AI->>EX: executeBatch(requests)
loop per request (parallel)
EX->>TF: resolveRequestTraitFilters(req)
alt has trait filters
TF->>ID: resolveTraitSegment(projectId, traitFilters)
ID-->>TF: profile_id[]
TF-->>EX: req with profile_id filter
else no trait filters
TF-->>EX: original req (no-op)
end
end
EX->>CH: union query (successful requests)
CH-->>EX: rows
EX-->>AI: BatchResult[] (with errors for failures)
Reviews (1): Last reviewed commit: "chore(staging): merge main" | Re-trigger Greptile |
| 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.` | ||
| ); | ||
| } |
There was a problem hiding this comment.
Unnecessary identity service call for unknown query types. When
config is undefined (unknown query type), the guard if (config && !isFilterFieldAllowed(config, "profile_id")) is a no-op — config is falsy so the throw is skipped. The function then calls resolveTraitSegment (a network round-trip to the identity service) before the query ultimately fails downstream with an "unknown type" error. The guard should also reject when the config is missing entirely.
| 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 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.` | |
| ); | |
| } |
| data: r.data, | ||
| rowCount: r.data.length, | ||
| ...(r.error && { error: "Query failed" }), | ||
| ...(r.error && { error: r.error }), |
There was a problem hiding this comment.
Raw errors now forwarded to AI agent context. Switching from the static
"Query failed" to r.error means ClickHouse-level error messages (which can include table names, column names, and query fragments) are forwarded verbatim to the LLM context. For filter-validation errors this is intentional and useful, but a transient ClickHouse failure or a malformed query could expose internal schema details. Consider forwarding r.error as-is for known structured errors (e.g. TraitFilterError messages or the filter-field error format) and falling back to a sanitized string for raw database errors.
…axiom-telemetry fix(observability): stop local dev from polluting prod Axiom datasets
…-resvg-libgcc fix(slack): restore the crash-looping slack service (resvg needs libgcc in distroless)
After the libgcc fix, the slack binary got one step further and crashed with ENOENT on /app/packages/charts/assets/fonts: @databuddy/charts reads its .otf fonts from disk at module load, but only the compiled binary was copied into the runtime image. Copy the charts assets dir over too. Verified locally: a linux/amd64 build (matching Railway) now boots past both the libgcc dlopen and the fonts read, reaching env validation (fails only on unset secrets, which Railway provides).
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 11 unresolved issues from previous reviews.
Re-trigger cubic
Flag exposure telemetry ($flag_evaluated) fired only on the cache-miss fetch path in getFlag. Once flag results are served from the persisted cache (the SDK 2.6.0 change), nearly every evaluation is a cache hit and returned early without firing the event, so exposure telemetry collapsed ~90% (364->35) even though the flags themselves evaluate correctly. Fire onFlagEvaluated on the cache-hit path too; the browser manager's per-flag:value dedupe keeps it to one exposure per session as before. Verified: new test asserts a second (cache-hit) getFlag still fires evaluation.
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 11 unresolved issues from previous reviews.
Re-trigger cubic
…d filters Add links.paginated (offset paging, capped 50/page) with server-side search, sort, type, and folder filters. Cap links.list at 1000 rows so the unbounded array query can no longer OOM on large workspaces.
Replace the load-everything links page with an infinite, virtualized list (@tanstack/react-virtual) backed by links.paginated. Move search, sort, and type filtering server-side, and swap the folder accordions for a folder filter dropdown. Fixes freezes/crashes on high link counts. Removes the now-orphaned client-side filter hook and folder-accordion component, and the dead links.list array cache maintenance.
Add a /insights/[id] route with getById and related RPC procedures and an insight detail view. Wire card and cockpit navigation to the page and point Slack digest links at /insights/<id> instead of the #insight-<id> anchor.
- schema-parse: shared DDL parser used by codegen and verify - codegen: emit tables.generated.ts (one Row type per table + registry) from schema/*.sql - verify: semantic colored diff of repo .sql against the live cluster, exit 1 on drift - ch:check freshness guard; wire generate/verify into turbo, lefthook, and CI
- apply.ts reads schema/*.sql (tables before MVs), strips replication for single-node dev/test/self-host unless CLICKHOUSE_CLUSTER is set - setup.ts (clickhouse:init) now applies from the .sql source of truth - schema.ts keeps only the row interfaces; the stale CREATE strings and initClickHouseSchema (plain MergeTree, missing columns) are gone
Insert shape optionalizes DEFAULT/Nullable columns and omits MATERIALIZED/ALIAS, so insert sites and query sites each get the correct shape from the same DDL.
…le lacks buildTrackEvent set 12 fields (event_type, session_start_time, screen_resolution, connection_type, rtt, downlink, load_time, dom_interactive, connection_time, redirect_time, domain_lookup_time, event_id) that the prod events table has no columns for; ClickHouse silently dropped them (input_format_skip_unknown_fields=1), so removing them changes no stored data. Types now come from the generated EventsInsert; insert datetime columns accept number | string.
…le lacks Umami adapter set event_type/event_id/screen_resolution, dropped on insert like basket's. Adds a types-only export (@databuddy/db/clickhouse/tables) so consumers pull generated row/insert types without dragging in the whole clickhouse barrel.
…ire AnalyticsEvent api/ai TableFieldsMap now uses keyof <Table>Row from the generated types, so valid field names track the live columns. AnalyticsEvent had no remaining importers and is deleted; its phantom columns are gone from the field-name unions too.
…ted Insert types Pure type-annotation swaps (object shapes already match the columns), so ingestion behavior is unchanged.
…erated types ErrorSpanRow, WebVitalsSpan, WebVitalsHourlyAggregate, CustomOutgoingLink, CustomEvent, DailyPageviewsAggregate, AITrafficSpan, UptimeMonitor, RevenueTransaction, LinkVisit had no importers after consumers moved to the generated Row/Insert types. schema.ts now holds only WebVitalMetricName and BlockedTraffic.
…nsert Drops the phantom 'city' field (blocked_traffic has no such column; it was silently discarded on insert). BlockedTraffic hand type had no other importers and is deleted; schema.ts now holds only the WebVitalMetricName enum.
…ore feature cards Onboarding completion showed a dismal 6% because onboardingCompleted only fired from the secondary 'Go to Dashboard' button. The explore step's primary CTAs are the four feature cards, which are plain Links that navigate into the product without recording completion — so users who activated via the intended path were counted as drop-offs. Fire completion (once, ref-guarded) when a feature card is clicked too; the flow itself already worked, only the metric was undercounting.
There was a problem hiding this comment.
21 issues found across 65 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="turbo.json">
<violation number="1" location="turbo.json:90">
P2: The `generate` task's narrow `inputs` omits `$TURBO_DEFAULT$`, so changes to helper modules (like `./schema-parse.ts`, which `codegen.ts` imports for the actual SQL parsing logic) won't invalidate the Turborepo cache. If `schema-parse.ts` is updated, a stale `tables.generated.ts` may be silently restored from cache instead of regenerating.</violation>
</file>
<file name=".github/workflows/ci.yml">
<violation number="1" location=".github/workflows/ci.yml:46">
P2: The `CLICKHOUSE_READONLY_URL` secret (which includes ClickHouse cluster credentials) is defined at the job-level `env`, so it is injected into every step in the `typecheck` job — including `bun install`, docs postinstall, `check-types`, and `ch:check` — even though only the final `ch:verify` step connects to the live cluster. Scoping the secret to only the step that needs it reduces blast radius if any command or dependency accidentally logs environment variables.</violation>
</file>
<file name="apps/dashboard/app/(main)/links/_components/virtualized-links-list.tsx">
<violation number="1" location="apps/dashboard/app/(main)/links/_components/virtualized-links-list.tsx:43">
P2: The `scrollMargin` offset is calculated only once on mount and never recalculated when the surrounding layout changes. Since the `useLayoutEffect` only depends on the stable `scrollRef` object, window resizes, header height changes, or any content reflow above the list will leave the margin stale. This causes the virtualized rows' `translateY` transforms to be offset incorrectly, producing visibly misaligned rows. TanStack Virtual's docs explicitly recommend using `ResizeObserver` to keep `scrollMargin` up to date. Consider observing the scroll and list elements with a `ResizeObserver` and recalculating `scrollMargin` whenever their layout changes.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/analytics/traffic/ai_traffic_spans.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/analytics/traffic/ai_traffic_spans.sql:10">
P2: The `idx_client_id` bloom filter on `client_id` is likely redundant. `client_id` is already the leading column in `ORDER BY (client_id, bot_type, timestamp)`, so ClickHouse's sparse primary-key marks already handle granule-skipping for equality queries on this column. Keeping the index adds unnecessary write and storage overhead during inserts and merges without meaningful read benefit. Consider dropping `idx_client_id` and keeping `idx_bot_name` (which covers a non-ORDER BY column) and optionally `idx_bot_type`.</violation>
</file>
<file name="lefthook.yml">
<violation number="1" location="lefthook.yml:30">
P1: Running a generator that mutates `tables.generated.ts` concurrently with `check-types` risks race conditions because both commands read from and write to the same file. `check-types` may validate a stale or partially-written version of the generated file. With `parallel: true`, `priority` is ignored, so ordering cannot be enforced in the legacy commands format. One approach is to switch to the `jobs`/`group` configuration (available in lefthook 1.10+) to sequence generation before type-checking, or run these commands sequentially so the generated output is always validated before the commit proceeds.</violation>
<violation number="2" location="lefthook.yml:32">
P1: The `ch-types` pre-commit hook can mask schema-generation failures. Because the multiline `run` script does not use `set -e` or `&&`, a failure from `bun run generate-db` does not stop execution; `git add` still runs and returns success for an already-tracked file, so lefthook reports the hook as passing and stale `tables.generated.ts` can be committed.
Consider chaining the commands with `&&` or adding `set -e` at the top of the script so the hook fails fast when generation errors.</violation>
</file>
<file name="packages/rpc/src/routers/links.ts">
<violation number="1" location="packages/rpc/src/routers/links.ts:169">
P1: The legacy `/links/list` endpoint was previously unbounded and now silently truncates to 1000 rows, returning a plain array with no truncation signal. Existing callers like the AI `listLinks` helper (`packages/ai/src/ai/tools/link-catalog.ts:121`) still consume this endpoint to enumerate all workspace links and report counts. Once a workspace exceeds 1000 links, the AI silently receives an incomplete set and can report wrong totals or miss links entirely, with no discoverable error path.
Consider either migrating internal callers to the new paginated endpoint, or having the legacy endpoint emit a detectable truncation signal (e.g., a header or a wrapped response with `hasMore`) so consumers know the set is incomplete.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/analytics/web-vitals/web_vitals_hourly_mv.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/analytics/web-vitals/web_vitals_hourly_mv.sql:14">
P0: The materialized view writes finalized `Float64` aggregates (`p75`, `p50`, `avg_value`, `min_value`, `max_value`) into a `SummingMergeTree` destination table. ClickHouse's `SummingMergeTree` sums all numeric columns during merges when no explicit summing columns are listed, so percentile, average, min, and max values become silently incorrect whenever the same `(client_id, path, metric_name, hour)` key spans multiple insert blocks. To keep hourly metrics correct across partial inserts, the destination table should use `AggregatingMergeTree` with `AggregateFunction(...)` types (e.g. `AggregateFunction(quantile(0.75), Float64)` and `AggregateFunction(avg, Float64)`), and the materialized view should write state aggregates (e.g. `quantileState(0.75)(metric_value)`).</violation>
</file>
<file name="apps/dashboard/app/(main)/insights/_components/insight-detail-content.tsx">
<violation number="1" location="apps/dashboard/app/(main)/insights/_components/insight-detail-content.tsx:23">
P2: The `formatChange` helper suppresses display only when `value === 0`, but uses `toFixed(0)` afterward — so small non-zero values like `0.4` and `-0.4` become `+0%` and `-0%`. This contradicts the intent to hide zero changes and can show conflicting directional indicators for tiny movements. A post-round zero guard (e.g. using `Math.round` and checking for `0`) would keep the zero-suppression behavior consistent.</violation>
</file>
<file name="packages/rpc/src/routers/insights.ts">
<violation number="1" location="packages/rpc/src/routers/insights.ts:615">
P1: Both `getById` and `related` endpoints query the database by `insightId` before authorizing the caller against the insight's organization via `withWorkspace`. This ordering creates a cross-tenant existence oracle: a missing insight returns `{ success: true, insight: null }` (or an empty list for `related`), while an existing insight the caller cannot access causes `withWorkspace` to throw a `forbidden` or `unauthorized` error. An attacker can distinguish non-existent IDs from existent-but-forbidden ones, leaking whether insights exist in other organizations.
Prefer aligning with the auth-first pattern used by `list` and `orgNarrative` in this file. One option is to accept `organizationId` in the input, authorize first, and include `eq(analyticsInsights.organizationId, input.organizationId)` in the query so that both not-found and unauthorized cases produce the same `insight: null` (or empty list) response.</violation>
<violation number="2" location="packages/rpc/src/routers/insights.ts:672">
P2: The `getById` handler duplicates the same insight projection and object-shaping logic already present in `getHistory`. Both perform identical field mapping, `parseInsightShape` spreading, timestamp formatting, and null-coalescing for nullable columns. Extracting a shared `mapToHistoryInsight(row)` helper (similar to how `getInsightsFromDb` is used elsewhere in this file) would eliminate drift risk and keep the two response paths consistent.</violation>
</file>
<file name="packages/db/src/clickhouse/schema-parse.ts">
<violation number="1" location="packages/db/src/clickhouse/schema-parse.ts:84">
P2: The `COLUMN_MODIFIERS` regex is case-sensitive, but ClickHouse keywords are case-insensitive. If a DDL file uses lowercase modifiers (e.g., `default`, `codec`), the regex won't match, so the parser treats the trailing modifier clause as part of the column type string. That corrupts downstream nullable/type detection. Adding the `i` flag aligns it with the other regexes in this file.</violation>
<violation number="2" location="packages/db/src/clickhouse/schema-parse.ts:124">
P1: The `clause()` helper builds a regex whose lookahead relies on `stops.join("|")`. When `stops` is empty (as it is for `SETTINGS` in `parseTable`), the resulting regex contains `(?=(?:)\b|$)`. The empty group `(?:)` always matches, so the lookahead only checks for a word boundary; the non-greedy capture then resolves to empty string because the position after `SETTINGS\s+` is a word boundary. This means `parseTable(...).settings` will always be `""` even when the DDL contains a `SETTINGS` clause, breaking downstream schema verification.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/analytics/pageviews/daily_pageviews.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/analytics/pageviews/daily_pageviews.sql:6">
P2: The bloom-filter skip index on `client_id` is redundant because `client_id` is already the leading column of the ORDER BY key. ClickHouse's primary sparse index automatically prunes granules for equality/range predicates on leading sort-key columns, so a secondary `bloom_filter(0.01)` adds storage and insert/merge overhead with no query-time benefit. The existing `analytics.events` table in the same schema follows this convention by placing bloom filters on non-ORDER BY columns (`session_id`, `anonymous_id`, `path`, `profile_id`) but omitting one on the leading `client_id`. Removing this index keeps the schema aligned with the codebase's established pattern and avoids unnecessary write/storage cost.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/analytics/errors/error_spans.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/analytics/errors/error_spans.sql:19">
P2: The `ORDER BY` for `error_spans` places `timestamp` after `error_type` and `path`. This means the most common time-range error queries—like the `recent_errors` feed that only filters by `client_id` and `timestamp`—cannot use the primary-key index to skip old timestamp ranges within each client. Consider placing `timestamp` earlier in the sort key (for example `(client_id, timestamp, error_type, path)`) so time-range queries can prune granules efficiently, similar to how `analytics.events` uses `(client_id, time, id)`.</violation>
</file>
<file name="packages/db/src/clickhouse/verify.ts">
<violation number="1" location="packages/db/src/clickhouse/verify.ts:32">
P2: The Basic auth header is built with `btoa(...)` after URI-decoding the ClickHouse credentials. `btoa` rejects characters outside the Latin-1 range (0x00–0xFF), so any non-ASCII username or password causes the script to throw `InvalidCharacterError` and fail before querying the cluster. Because this runs in a Node environment, `Buffer.from(...).toString('base64')` is the safer replacement that handles arbitrary UTF-8 credentials correctly.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/analytics/revenue/revenue.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/analytics/revenue/revenue.sql:20">
P1: Using a second-precision `DateTime('UTC')` as the `ReplacingMergeTree` version column makes ties likely when multiple rows for the same transaction arrive within a second—e.g., during payment-intent/invoice deduplication or attribution carry-forward. When versions tie, ClickHouse falls back to part-selection order and can keep an arbitrary (e.g., unattributed or zero-amount) row instead of the intended one. Consider changing this to `DateTime64(3, 'UTC')` (or a monotonic counter) so the version increases reliably across same-second inserts and the attribution-carry-forward guarantee works predictably.</violation>
<violation number="2" location="packages/db/src/clickhouse/schema/analytics/revenue/revenue.sql:30">
P1: The `ReplicatedReplacingMergeTree` deduplication guarantee is scoped to individual partitions, and this table partitions by `toYYYYMM(created)`. If a refund, update, or attribution-carry-forward event for the same `(owner_id, transaction_id)` occurs in a different month than the original transaction, ClickHouse will never merge those rows together because merges do not cross partitions. Unless every insert preserves the original transaction's `created` timestamp, or every revenue query explicitly deduplicates with `FINAL`/`argMax`, revenue/LTV totals can silently double-count. Consider either ensuring insert logic freezes `created` for the same transaction across all lifecycle events, or mandate query-time deduplication on all revenue reads.</violation>
<violation number="3" location="packages/db/src/clickhouse/schema/analytics/revenue/revenue.sql:32">
P2: The `ORDER BY` key on `(owner_id, transaction_id)` could be too narrow for a multi-provider revenue table. Since `provider` and `website_id` are stored but excluded from the ReplacingMergeTree key, a payment from a second provider with a colliding transaction ID would be silently merged away during background merges. Consider widening the key to `(owner_id, provider, transaction_id)` (and possibly `website_id`) so deduplication only occurs within the provider/account namespace it is intended for.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/analytics/traffic/blocked_traffic.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/analytics/traffic/blocked_traffic.sql:30">
P2: The `ORDER BY` clause places `timestamp` before `client_id`, which is inconsistent with the established convention in this codebase for multi-tenant analytics tables (`analytics.events` uses `client_id, time, id`; `analytics.custom_events` uses `owner_id, event_name, timestamp`). In ClickHouse, sparse primary-key pruning works left-to-right, so tenant-scoped queries filtered by `client_id` will scan granules containing all clients' blocked traffic for the time window. Reordering to place `client_id` first aligns with existing tables and avoids unnecessary I/O for dashboard and API queries.</violation>
</file>
<file name="packages/db/src/clickhouse/schema/uptime/uptime_monitor.sql">
<violation number="1" location="packages/db/src/clickhouse/schema/uptime/uptime_monitor.sql:29">
P2: The `ReplicatedMergeTree` ZooKeeper path for this new table uses `uptime_monitor` without the database prefix, which breaks the codebase convention and risks replication-path collisions. Every other replicated table in the same schema directory already follows the `{database}_{table}` naming pattern for the Keeper path (e.g., `analytics_events`, `analytics_web_vitals_spans`, `analytics_blocked_traffic`). To stay consistent and avoid potential metadata conflicts, the path should include the database prefix so it becomes `uptime_uptime_monitor`.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| `min_value` Float64, | ||
| `max_value` Float64 | ||
| ) | ||
| AS SELECT client_id, path, metric_name, toStartOfHour(timestamp) AS hour, count() AS sample_count, quantile(0.75)(metric_value) AS p75, quantile(0.5)(metric_value) AS p50, avg(metric_value) AS avg_value, min(metric_value) AS min_value, max(metric_value) AS max_value |
There was a problem hiding this comment.
P0: The materialized view writes finalized Float64 aggregates (p75, p50, avg_value, min_value, max_value) into a SummingMergeTree destination table. ClickHouse's SummingMergeTree sums all numeric columns during merges when no explicit summing columns are listed, so percentile, average, min, and max values become silently incorrect whenever the same (client_id, path, metric_name, hour) key spans multiple insert blocks. To keep hourly metrics correct across partial inserts, the destination table should use AggregatingMergeTree with AggregateFunction(...) types (e.g. AggregateFunction(quantile(0.75), Float64) and AggregateFunction(avg, Float64)), and the materialized view should write state aggregates (e.g. quantileState(0.75)(metric_value)).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/schema/analytics/web-vitals/web_vitals_hourly_mv.sql, line 14:
<comment>The materialized view writes finalized `Float64` aggregates (`p75`, `p50`, `avg_value`, `min_value`, `max_value`) into a `SummingMergeTree` destination table. ClickHouse's `SummingMergeTree` sums all numeric columns during merges when no explicit summing columns are listed, so percentile, average, min, and max values become silently incorrect whenever the same `(client_id, path, metric_name, hour)` key spans multiple insert blocks. To keep hourly metrics correct across partial inserts, the destination table should use `AggregatingMergeTree` with `AggregateFunction(...)` types (e.g. `AggregateFunction(quantile(0.75), Float64)` and `AggregateFunction(avg, Float64)`), and the materialized view should write state aggregates (e.g. `quantileState(0.75)(metric_value)`).</comment>
<file context>
@@ -0,0 +1,16 @@
+ `min_value` Float64,
+ `max_value` Float64
+)
+AS SELECT client_id, path, metric_name, toStartOfHour(timestamp) AS hour, count() AS sample_count, quantile(0.75)(metric_value) AS p75, quantile(0.5)(metric_value) AS p50, avg(metric_value) AS avg_value, min(metric_value) AS min_value, max(metric_value) AS max_value
+FROM analytics.web_vitals_spans
+GROUP BY client_id, path, metric_name, hour
</file context>
| check-types: | ||
| glob: "**/*.{ts,tsx}" | ||
| run: bun run check-types | ||
| ch-types: |
There was a problem hiding this comment.
P1: Running a generator that mutates tables.generated.ts concurrently with check-types risks race conditions because both commands read from and write to the same file. check-types may validate a stale or partially-written version of the generated file. With parallel: true, priority is ignored, so ordering cannot be enforced in the legacy commands format. One approach is to switch to the jobs/group configuration (available in lefthook 1.10+) to sequence generation before type-checking, or run these commands sequentially so the generated output is always validated before the commit proceeds.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lefthook.yml, line 30:
<comment>Running a generator that mutates `tables.generated.ts` concurrently with `check-types` risks race conditions because both commands read from and write to the same file. `check-types` may validate a stale or partially-written version of the generated file. With `parallel: true`, `priority` is ignored, so ordering cannot be enforced in the legacy commands format. One approach is to switch to the `jobs`/`group` configuration (available in lefthook 1.10+) to sequence generation before type-checking, or run these commands sequentially so the generated output is always validated before the commit proceeds.</comment>
<file context>
@@ -27,6 +27,11 @@ pre-commit:
check-types:
glob: "**/*.{ts,tsx}"
run: bun run check-types
+ ch-types:
+ glob: "packages/db/src/clickhouse/schema/**/*.sql"
+ run: |
</file context>
| run: bun run check-types | ||
| ch-types: | ||
| glob: "packages/db/src/clickhouse/schema/**/*.sql" | ||
| run: | |
There was a problem hiding this comment.
P1: The ch-types pre-commit hook can mask schema-generation failures. Because the multiline run script does not use set -e or &&, a failure from bun run generate-db does not stop execution; git add still runs and returns success for an already-tracked file, so lefthook reports the hook as passing and stale tables.generated.ts can be committed.
Consider chaining the commands with && or adding set -e at the top of the script so the hook fails fast when generation errors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lefthook.yml, line 32:
<comment>The `ch-types` pre-commit hook can mask schema-generation failures. Because the multiline `run` script does not use `set -e` or `&&`, a failure from `bun run generate-db` does not stop execution; `git add` still runs and returns success for an already-tracked file, so lefthook reports the hook as passing and stale `tables.generated.ts` can be committed.
Consider chaining the commands with `&&` or adding `set -e` at the top of the script so the hook fails fast when generation errors.</comment>
<file context>
@@ -27,6 +27,11 @@ pre-commit:
run: bun run check-types
+ ch-types:
+ glob: "packages/db/src/clickhouse/schema/**/*.sql"
+ run: |
+ bun run generate-db
+ git add packages/db/src/clickhouse/schema/tables.generated.ts
</file context>
| }); | ||
| } | ||
|
|
||
| const LINKS_LIST_MAX = 1000; |
There was a problem hiding this comment.
P1: The legacy /links/list endpoint was previously unbounded and now silently truncates to 1000 rows, returning a plain array with no truncation signal. Existing callers like the AI listLinks helper (packages/ai/src/ai/tools/link-catalog.ts:121) still consume this endpoint to enumerate all workspace links and report counts. Once a workspace exceeds 1000 links, the AI silently receives an incomplete set and can report wrong totals or miss links entirely, with no discoverable error path.
Consider either migrating internal callers to the new paginated endpoint, or having the legacy endpoint emit a detectable truncation signal (e.g., a header or a wrapped response with hasMore) so consumers know the set is incomplete.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/rpc/src/routers/links.ts, line 169:
<comment>The legacy `/links/list` endpoint was previously unbounded and now silently truncates to 1000 rows, returning a plain array with no truncation signal. Existing callers like the AI `listLinks` helper (`packages/ai/src/ai/tools/link-catalog.ts:121`) still consume this endpoint to enumerate all workspace links and report counts. Once a workspace exceeds 1000 links, the AI silently receives an incomplete set and can report wrong totals or miss links entirely, with no discoverable error path.
Consider either migrating internal callers to the new paginated endpoint, or having the legacy endpoint emit a detectable truncation signal (e.g., a header or a wrapped response with `hasMore`) so consumers know the set is incomplete.</comment>
<file context>
@@ -154,6 +166,53 @@ function requireLinkAccess(
});
}
+const LINKS_LIST_MAX = 1000;
+
+function buildLinkListConditions(
</file context>
| success: z.literal(true), | ||
| }) | ||
| ) | ||
| .handler(async ({ context, input }) => { |
There was a problem hiding this comment.
P1: Both getById and related endpoints query the database by insightId before authorizing the caller against the insight's organization via withWorkspace. This ordering creates a cross-tenant existence oracle: a missing insight returns { success: true, insight: null } (or an empty list for related), while an existing insight the caller cannot access causes withWorkspace to throw a forbidden or unauthorized error. An attacker can distinguish non-existent IDs from existent-but-forbidden ones, leaking whether insights exist in other organizations.
Prefer aligning with the auth-first pattern used by list and orgNarrative in this file. One option is to accept organizationId in the input, authorize first, and include eq(analyticsInsights.organizationId, input.organizationId) in the query so that both not-found and unauthorized cases produce the same insight: null (or empty list) response.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/rpc/src/routers/insights.ts, line 615:
<comment>Both `getById` and `related` endpoints query the database by `insightId` before authorizing the caller against the insight's organization via `withWorkspace`. This ordering creates a cross-tenant existence oracle: a missing insight returns `{ success: true, insight: null }` (or an empty list for `related`), while an existing insight the caller cannot access causes `withWorkspace` to throw a `forbidden` or `unauthorized` error. An attacker can distinguish non-existent IDs from existent-but-forbidden ones, leaking whether insights exist in other organizations.
Prefer aligning with the auth-first pattern used by `list` and `orgNarrative` in this file. One option is to accept `organizationId` in the input, authorize first, and include `eq(analyticsInsights.organizationId, input.organizationId)` in the query so that both not-found and unauthorized cases produce the same `insight: null` (or empty list) response.</comment>
<file context>
@@ -586,6 +596,202 @@ export const insightsRouter = {
+ success: z.literal(true),
+ })
+ )
+ .handler(async ({ context, input }) => {
+ const [row] = await db
+ .select({
</file context>
| ) | ||
| ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/analytics_error_spans', '{replica}') | ||
| PARTITION BY toDate(timestamp) | ||
| ORDER BY (client_id, error_type, path, timestamp) |
There was a problem hiding this comment.
P2: The ORDER BY for error_spans places timestamp after error_type and path. This means the most common time-range error queries—like the recent_errors feed that only filters by client_id and timestamp—cannot use the primary-key index to skip old timestamp ranges within each client. Consider placing timestamp earlier in the sort key (for example (client_id, timestamp, error_type, path)) so time-range queries can prune granules efficiently, similar to how analytics.events uses (client_id, time, id).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/schema/analytics/errors/error_spans.sql, line 19:
<comment>The `ORDER BY` for `error_spans` places `timestamp` after `error_type` and `path`. This means the most common time-range error queries—like the `recent_errors` feed that only filters by `client_id` and `timestamp`—cannot use the primary-key index to skip old timestamp ranges within each client. Consider placing `timestamp` earlier in the sort key (for example `(client_id, timestamp, error_type, path)`) so time-range queries can prune granules efficiently, similar to how `analytics.events` uses `(client_id, time, id)`.</comment>
<file context>
@@ -0,0 +1,20 @@
+)
+ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/analytics_error_spans', '{replica}')
+PARTITION BY toDate(timestamp)
+ORDER BY (client_id, error_type, path, timestamp)
+SETTINGS index_granularity = 8192
</file context>
| const url = new URL(raw); | ||
| const headers: Record<string, string> = {}; | ||
| if (url.username) { | ||
| headers.Authorization = `Basic ${btoa(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`)}`; |
There was a problem hiding this comment.
P2: The Basic auth header is built with btoa(...) after URI-decoding the ClickHouse credentials. btoa rejects characters outside the Latin-1 range (0x00–0xFF), so any non-ASCII username or password causes the script to throw InvalidCharacterError and fail before querying the cluster. Because this runs in a Node environment, Buffer.from(...).toString('base64') is the safer replacement that handles arbitrary UTF-8 credentials correctly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/verify.ts, line 32:
<comment>The Basic auth header is built with `btoa(...)` after URI-decoding the ClickHouse credentials. `btoa` rejects characters outside the Latin-1 range (0x00–0xFF), so any non-ASCII username or password causes the script to throw `InvalidCharacterError` and fail before querying the cluster. Because this runs in a Node environment, `Buffer.from(...).toString('base64')` is the safer replacement that handles arbitrary UTF-8 credentials correctly.</comment>
<file context>
@@ -0,0 +1,136 @@
+ const url = new URL(raw);
+ const headers: Record<string, string> = {};
+ if (url.username) {
+ headers.Authorization = `Basic ${btoa(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`)}`;
+ url.username = "";
+ url.password = "";
</file context>
| ) | ||
| ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/analytics_revenue', '{replica}', synced_at) | ||
| PARTITION BY toYYYYMM(created) | ||
| ORDER BY (owner_id, transaction_id) |
There was a problem hiding this comment.
P2: The ORDER BY key on (owner_id, transaction_id) could be too narrow for a multi-provider revenue table. Since provider and website_id are stored but excluded from the ReplacingMergeTree key, a payment from a second provider with a colliding transaction ID would be silently merged away during background merges. Consider widening the key to (owner_id, provider, transaction_id) (and possibly website_id) so deduplication only occurs within the provider/account namespace it is intended for.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/schema/analytics/revenue/revenue.sql, line 32:
<comment>The `ORDER BY` key on `(owner_id, transaction_id)` could be too narrow for a multi-provider revenue table. Since `provider` and `website_id` are stored but excluded from the ReplacingMergeTree key, a payment from a second provider with a colliding transaction ID would be silently merged away during background merges. Consider widening the key to `(owner_id, provider, transaction_id)` (and possibly `website_id`) so deduplication only occurs within the provider/account namespace it is intended for.</comment>
<file context>
@@ -0,0 +1,33 @@
+)
+ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/analytics_revenue', '{replica}', synced_at)
+PARTITION BY toYYYYMM(created)
+ORDER BY (owner_id, transaction_id)
+SETTINGS index_granularity = 8192
</file context>
| ) | ||
| ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/analytics_blocked_traffic', '{replica}') | ||
| PARTITION BY toYYYYMM(timestamp) | ||
| ORDER BY (timestamp, client_id, id) |
There was a problem hiding this comment.
P2: The ORDER BY clause places timestamp before client_id, which is inconsistent with the established convention in this codebase for multi-tenant analytics tables (analytics.events uses client_id, time, id; analytics.custom_events uses owner_id, event_name, timestamp). In ClickHouse, sparse primary-key pruning works left-to-right, so tenant-scoped queries filtered by client_id will scan granules containing all clients' blocked traffic for the time window. Reordering to place client_id first aligns with existing tables and avoids unnecessary I/O for dashboard and API queries.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/schema/analytics/traffic/blocked_traffic.sql, line 30:
<comment>The `ORDER BY` clause places `timestamp` before `client_id`, which is inconsistent with the established convention in this codebase for multi-tenant analytics tables (`analytics.events` uses `client_id, time, id`; `analytics.custom_events` uses `owner_id, event_name, timestamp`). In ClickHouse, sparse primary-key pruning works left-to-right, so tenant-scoped queries filtered by `client_id` will scan granules containing all clients' blocked traffic for the time window. Reordering to place `client_id` first aligns with existing tables and avoids unnecessary I/O for dashboard and API queries.</comment>
<file context>
@@ -0,0 +1,31 @@
+)
+ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/analytics_blocked_traffic', '{replica}')
+PARTITION BY toYYYYMM(timestamp)
+ORDER BY (timestamp, client_id, id)
+SETTINGS index_granularity = 8192
</file context>
| ORDER BY (timestamp, client_id, id) | |
| ORDER BY (client_id, timestamp, id) |
| INDEX idx_status status TYPE minmax GRANULARITY 1, | ||
| INDEX idx_timestamp timestamp TYPE minmax GRANULARITY 1 | ||
| ) | ||
| ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/uptime_monitor', '{replica}') |
There was a problem hiding this comment.
P2: The ReplicatedMergeTree ZooKeeper path for this new table uses uptime_monitor without the database prefix, which breaks the codebase convention and risks replication-path collisions. Every other replicated table in the same schema directory already follows the {database}_{table} naming pattern for the Keeper path (e.g., analytics_events, analytics_web_vitals_spans, analytics_blocked_traffic). To stay consistent and avoid potential metadata conflicts, the path should include the database prefix so it becomes uptime_uptime_monitor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/schema/uptime/uptime_monitor.sql, line 29:
<comment>The `ReplicatedMergeTree` ZooKeeper path for this new table uses `uptime_monitor` without the database prefix, which breaks the codebase convention and risks replication-path collisions. Every other replicated table in the same schema directory already follows the `{database}_{table}` naming pattern for the Keeper path (e.g., `analytics_events`, `analytics_web_vitals_spans`, `analytics_blocked_traffic`). To stay consistent and avoid potential metadata conflicts, the path should include the database prefix so it becomes `uptime_uptime_monitor`.</comment>
<file context>
@@ -0,0 +1,32 @@
+ INDEX idx_status status TYPE minmax GRANULARITY 1,
+ INDEX idx_timestamp timestamp TYPE minmax GRANULARITY 1
+)
+ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/uptime_monitor', '{replica}')
+PARTITION BY toYYYYMM(timestamp)
+ORDER BY (site_id, timestamp)
</file context>
…inline DDL The identity test read schema.ts for 'profile_id String DEFAULT \'\'' and ADD COLUMN migrations, but the ClickHouse schema moved to .sql files (commit 0c42b73) and schema.ts no longer holds DDL, so the test failed on every run and blocked the pre-push hook. The profile_id columns are intact in the .sql files; parse each profile table via schema-parse and assert it defines a String profile_id column with a default. Drops the obsolete ADD COLUMN check (the .sql path is idempotent CREATE TABLE IF NOT EXISTS).
…sInsert shape The event mapper dropped columns the events table lacks (event_id, event_type, session_start_time, screen_resolution, connection_type, rtt, downlink, load_time, dom_interactive, connection_time, redirect_time, domain_lookup_time), but the test still asserted all of them and hardcoded the old field list. Update the field-mapping, defaults, passthrough, and completeness checks to the 42 fields buildTrackEvent actually returns.
…n't throw resolveAgentBillingCustomerId tests passed in isolation but failed under the full suite: a prior test loads the real @databuddy/db client, which throws 'DATABASE_URL is not set' at init before the mock.module override matters. Add a bun test preload setting dummy DB/Redis URLs (queries stay mocked, nothing connects). Full ai suite: 3565 pass, 0 fail.
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 32 unresolved issues from previous reviews.
Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/rpc/src/routers/insights.ts">
<violation number="1" location="packages/rpc/src/routers/insights.ts:618">
P2: The new rate-limit enforcement is duplicated identically across `getById`, `related`, and the pre-existing `orgNarrative` handlers. All three construct a key, call `ratelimit()`, then compute retry-after with the same `Math.max(1, Math.ceil((rl.reset - Date.now()) / 1000))` formula. This means any change to failure behavior or retry-after math must be updated in three places, and divergences are easy to miss. Consider extracting a small helper such as `enforceRateLimit(key, limit, window)` so the logic lives in one place.</violation>
</file>
<file name="apps/dashboard/app/(main)/insights/hooks/use-insights-local-state.ts">
<violation number="1" location="apps/dashboard/app/(main)/insights/hooks/use-insights-local-state.ts:66">
P1: Optimistic rollback passed as a per-call `onError` callback to `mutate` can be silently skipped when multiple mutations fire in quick succession. TanStack Query v5 removes and resubscribes the mutation observer on each `mutate()` call, so per-call callbacks like `onError` only run for the most recent invocation. If a user dismisses insight A, then quickly dismisses insight B before A's request fails, A's rollback never executes, leaving local state (and `localStorage`) inconsistent with the server even after a refresh.
Consider moving the rollback logic to the hook-level `onError` inside the `useMutation` configuration, or switching to `mutateAsync` with explicit try/catch so each request is handled independently.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| saveDismissedIds(organizationId, next); | ||
| return next; | ||
| }); | ||
| setDismissedMutation.mutate( |
There was a problem hiding this comment.
P1: Optimistic rollback passed as a per-call onError callback to mutate can be silently skipped when multiple mutations fire in quick succession. TanStack Query v5 removes and resubscribes the mutation observer on each mutate() call, so per-call callbacks like onError only run for the most recent invocation. If a user dismisses insight A, then quickly dismisses insight B before A's request fails, A's rollback never executes, leaving local state (and localStorage) inconsistent with the server even after a refresh.
Consider moving the rollback logic to the hook-level onError inside the useMutation configuration, or switching to mutateAsync with explicit try/catch so each request is handled independently.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/app/(main)/insights/hooks/use-insights-local-state.ts, line 66:
<comment>Optimistic rollback passed as a per-call `onError` callback to `mutate` can be silently skipped when multiple mutations fire in quick succession. TanStack Query v5 removes and resubscribes the mutation observer on each `mutate()` call, so per-call callbacks like `onError` only run for the most recent invocation. If a user dismisses insight A, then quickly dismisses insight B before A's request fails, A's rollback never executes, leaving local state (and `localStorage`) inconsistent with the server even after a refresh.
Consider moving the rollback logic to the hook-level `onError` inside the `useMutation` configuration, or switching to `mutateAsync` with explicit try/catch so each request is handled independently.</comment>
<file context>
@@ -61,7 +63,22 @@ export function useInsightsLocalState(
return next;
});
- setDismissedMutation.mutate({ insightId, dismissed: false });
+ setDismissedMutation.mutate(
+ { insightId, dismissed: false },
+ {
</file context>
| }) | ||
| ) | ||
| .handler(async ({ context, input }) => { | ||
| const rl = await ratelimit( |
There was a problem hiding this comment.
P2: The new rate-limit enforcement is duplicated identically across getById, related, and the pre-existing orgNarrative handlers. All three construct a key, call ratelimit(), then compute retry-after with the same Math.max(1, Math.ceil((rl.reset - Date.now()) / 1000)) formula. This means any change to failure behavior or retry-after math must be updated in three places, and divergences are easy to miss. Consider extracting a small helper such as enforceRateLimit(key, limit, window) so the logic lives in one place.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/rpc/src/routers/insights.ts, line 618:
<comment>The new rate-limit enforcement is duplicated identically across `getById`, `related`, and the pre-existing `orgNarrative` handlers. All three construct a key, call `ratelimit()`, then compute retry-after with the same `Math.max(1, Math.ceil((rl.reset - Date.now()) / 1000))` formula. This means any change to failure behavior or retry-after math must be updated in three places, and divergences are easy to miss. Consider extracting a small helper such as `enforceRateLimit(key, limit, window)` so the logic lives in one place.</comment>
<file context>
@@ -613,6 +615,17 @@ export const insightsRouter = {
})
)
.handler(async ({ context, input }) => {
+ const rl = await ratelimit(
+ `insights:getById:${context.user.id}`,
+ INSIGHT_READ_RATE_LIMIT,
</file context>
What's in this release
Users page
profile_listRevenue attribution
invoice.paid/invoice.payment_succeededhandling: recurring subscription payments now attribute to sessions and identified users. Metadata is read from the invoice,subscription_details, andparent.subscription_details(covers Autumn and newer Stripe API versions where payment intents carry no metadata)invoice.paidas a required webhook eventFeedback pipeline
website_idfkey indexed@databuddy/serviceswith source-aware Slack alertssubmit_feedbackagent tool for dashboard chat and the Slack bot, with consent-aware prompting and a single source of truth for the prompt rulesfeedback-previewchat component with send and receipt modesSlack agent
agent_runevents never reaching Axiom when a deploy landed mid-answerslack_run_timed_outtelemetry instead of sitting on "Thinking..." until the next deploy kills them@databuddy/chartspackage renders the agent's chart components (line, area, bar, stacked bar, pie, donut) to dashboard-themed PNGs server-side (ECharts SSR + resvg, LT Superior fonts, dark/light design tokens). Slack answers upload up to 3 charts into the thread and fall back to the existing text tables if rendering or upload fails. Activating uploads requires adding thefiles:writescope to the Slack appAgent & AI
list_profile_traitstool: trait key/value distribution with profile counts, guiding agents totrait:<key>segmentation before queryingget_data, MCP tools, and raw SQL paths; telemetry gated on the exported sanitized constant; error messaging keyed on error type, not input shapeDashboard & misc
has_active_subscriptiontrait dropped (redundant withplan)