Skip to content

Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images#541

Open
izadoesdev wants to merge 86 commits into
mainfrom
staging
Open

Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images#541
izadoesdev wants to merge 86 commits into
mainfrom
staging

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Jul 4, 2026

Copy link
Copy Markdown
Member

What's in this release

Users page

  • LTV column: lifetime revenue per visitor (refunds netted), computed via a revenue join in profile_list
  • Email search: exact-match lookup via the deterministic email hash, jumps straight to the profile; works against encrypted PII; pending state and error-vs-miss handling hardened
  • Trait filter value picker distinguishes loading from empty; filter chip styling deduped; trait distribution ranked per key

Revenue attribution

  • invoice.paid / invoice.payment_succeeded handling: recurring subscription payments now attribute to sessions and identified users. Metadata is read from the invoice, subscription_details, and parent.subscription_details (covers Autumn and newer Stripe API versions where payment intents carry no metadata)
  • Duplicate prevention: invoice rows reuse the matching payment-intent row's transaction id (same rule as the query-time dedup, applied at ingestion), so one payment stays one row
  • Attribution carry-forward across event ordering: succeeded/failed/refund events no longer replace an attributed row with an unattributed version; refunds inherit attribution from the original payment
  • Failed invoices stay at zero amount to preserve the revenue sum invariant (attribution still carried through failed and refund handlers)
  • Five duplicated ClickHouse insert blocks collapsed into one helper (net -54 lines)
  • Setup UI and docs list invoice.paid as a required webhook event

Feedback pipeline

  • Feedback rows now carry source, website, conversation id, and metadata; website_id fkey indexed
  • Shared submission service in @databuddy/services with source-aware Slack alerts
  • submit_feedback agent tool for dashboard chat and the Slack bot, with consent-aware prompting and a single source of truth for the prompt rules
  • Vague-report gate: vague complaints route to clarification instead of silent submission
  • feedback-preview chat component with send and receipt modes
  • Eval cases for tool behavior and vague-complaint routing

Slack agent

  • Deploy-safe shutdown: active runs are aborted with a user-facing notice and run handlers settle before the telemetry drain flushes. Fixes runs dying with an unexplained "Something went wrong" and their agent_run events never reaching Axiom when a deploy landed mid-answer
  • 5-minute run timeout: hung runs fail visibly with slack_run_timed_out telemetry instead of sitting on "Thinking..." until the next deploy kills them
  • Chart images: new @databuddy/charts package 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 the files:write scope to the Slack app

Agent & AI

  • list_profile_traits tool: trait key/value distribution with profile counts, guiding agents to trait:<key> segmentation before querying
  • Public query errors sanitized behind an allowlist across get_data, MCP tools, and raw SQL paths; telemetry gated on the exported sanitized constant; error messaging keyed on error type, not input shape
  • Trait filters resolved and validated centrally; unknown query types reject explicitly
  • Trait segmentation eval cases

Dashboard & misc

  • has_active_subscription trait dropped (redundant with plan)
  • OpenAI Ads signup attribution improved and the route hardened
  • Session guard no longer revokes sessions on transient fetch failures; owner-scoped persisted-cache storage refinements
  • Vitals e2e test asserts the real payload shape (was vacuously passing)
  • Prose style cleanup in tool descriptions and payments docs

@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
dashboard (staging) Ready Ready Preview, Comment Jul 9, 2026 10:47pm
databuddy-status Ready Ready Preview, Comment Jul 9, 2026 10:47pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
documentation (staging) Skipped Skipped Jul 9, 2026 10:47pm

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

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

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 461e44d8-2b28-4f8d-9fc6-2409a659d340

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging

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

❤️ Share

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

@unkey-deploy

unkey-deploy Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Unkey Deploy

Name Status Preview Inspect Updated (UTC)
api (preview) Ready Visit Preview Inspect Jul 9, 2026 10:46pm

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR centralizes AI query trait-filter resolution into a new trait-filters.ts module, adds filter-field validation for MCP batch requests, and removes the authClient.signOut() call in the session guard to prevent spurious sign-outs on transient fetch failures.

  • Trait filter centralization: resolveRequestTraitFilters and invalidFilterFieldError now live in one place and are re-used by executeQuery, executeBatch, and the MCP batch-request builder, replacing per-caller ad-hoc logic.
  • Session guard fix: The guard now redirects directly to /login without calling signOut(); this stops a transient null-session response from permanently revoking a still-valid session on the server.
  • Error surfacing: MCP batch results now return the real error string (r.error) instead of the redacted "Query failed", giving the AI agent actionable context for self-correction.

Confidence Score: 4/5

The 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

Filename Overview
packages/ai/src/query/trait-filters.ts New module centralizing trait-filter resolution and field validation; logic is clean, but unknown query types bypass the profile_id guard and trigger an unnecessary identity service call.
packages/ai/src/query/batch-executor.ts Trait resolution moved into executeBatch; index bookkeeping for traitFailures is correct and the fast-path single-query shortcut is properly gated on zero failures.
packages/ai/src/ai/mcp/agent-tools.ts Error surfacing upgraded from a static "Query failed" string to the real error; could leak internal ClickHouse error details to the LLM context.
apps/dashboard/components/providers/session-guard.tsx Removes signOut() before redirect to /login, preventing irreversible session revocation on transient network failures; intentional and correct.
packages/ai/src/ai/tools/get-data.ts Per-item trait resolution removed; now delegates to executeQuery which handles it centrally; error branch now differentiates trait vs. generic failures correctly.
packages/ai/src/ai/mcp/mcp-utils.ts Adds invalidFilterFieldError() call between type resolution and request push, giving the MCP layer early rejection before execution.
packages/ai/src/query/index.ts executeQuery now validates filter fields and resolves trait filters before building; validation runs twice for executeBatch callers (once at MCP layer, once here) but no functional impact.
packages/ai/src/query/trait-filters.test.ts New test file with good coverage of happy path, org-scoped rejection, unknown-type rejection, and field allowlist logic.
packages/ai/src/query/builders/sessions.ts Adds profile_id/anonymous_id to allowedFilters and threads filterConditions/filterParams into custom SQL queries for session builders.

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)
Loading
%%{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)
Loading

Reviews (1): Last reviewed commit: "chore(staging): merge main" | Re-trigger Greptile

Comment on lines +50 to +55
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.`
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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.`
);
}

Comment thread packages/ai/src/ai/mcp/agent-tools.ts Outdated
data: r.data,
rowCount: r.data.length,
...(r.error && { error: "Query failed" }),
...(r.error && { error: r.error }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

izadoesdev added 16 commits July 9, 2026 01:43
…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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread lefthook.yml
check-types:
glob: "**/*.{ts,tsx}"
run: bun run check-types
ch-types:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread lefthook.yml
run: bun run check-types
ch-types:
glob: "packages/db/src/clickhouse/schema/**/*.sql"
run: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)}`)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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}')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant