Skip to content

Release: pricing overhaul, insights pipeline improvements#463

Open
izadoesdev wants to merge 5 commits into
mainfrom
staging
Open

Release: pricing overhaul, insights pipeline improvements#463
izadoesdev wants to merge 5 commits into
mainfrom
staging

Conversation

@izadoesdev
Copy link
Copy Markdown
Member

@izadoesdev izadoesdev commented May 29, 2026

Summary

  • Pricing table overhaul: Unhide Error Tracking, add Platform section (Uptime, Short Links, Revenue, Alerts, Team Members, Websites, API, Slack, SDKs), link features to dedicated pages, update copy
  • Insights pipeline: Investigation overhaul with tools, actions, and model routing; full insight context in history; dedup fix with title fallback
  • Cleanup: Remove unused Wordmark component from footer

Commits

  • feat(docs): overhaul pricing table and remove unused wordmark (#462)
  • fix(insights): history dedup uses title fallback, limits after dedup (#461)
  • feat(insights): replace flat title history with full insight context (#460)
  • feat(insights): investigation pipeline overhaul with tools, actions, and model routing

Summary by cubic

Overhauls pricing and insights, and turns the agent into an organization workspace with multi-site tools. Insights now gate on credits and track usage; auth, webhooks, and billing logs are more robust.

  • New Features

    • Pricing: Unhide Error Tracking, add Platform section (Uptime, Short Links, Revenue, Alerts, Team Members, Websites, API, Slack, SDKs), link features to their pages, and update copy.
    • Insights: Add funnel/goal conversion signal detection; gate generation on available credits and bill usage; keep richer history with root cause, evidence, depth, and actions.
    • Agent: Organization-wide workspace with a new /agent route, org-scoped chats, website “@mentions”, and a list_websites tool for multi-site queries; no‑websites empty state.
    • Slack: Start stream with an immediate “thinking” indicator; sturdier streaming and abort handling.
    • SQL: Per-table schema validation catches alias.column mistakes before hitting ClickHouse; new tests included.
  • Refactors

    • Auth/Billing/Webhooks: Require verified local email for account linking; purge reset tokens and revoke sessions on password reset/change; disable session cookie cache; forward Better‑Auth logs to evlog; sanitize Autumn requests and parse webhooks with Zod; log quota‑exceeded events with usage/grant context.
    • OAuth + tokens: Add shared helpers getOAuthToken/createCachedTokenFn with TTL and org scoping; used by GitHub and Search Console; AgentContext and app context now include organizationId and accessible websites.
    • Tooling: Introduce createToolkit to assemble analytics/investigation/mutation tools; add search-console, list_websites, investigation-tools; extract prompts; bump ai to ^6.0.188.
    • Data contracts/UI: Scope agent chats to organizations (nullable websiteId, survives site deletion); API serves workspace with accessible websites; dashboard adds global agent provider/workspace and renders insight action pills; minor CSP and number-format fixes.

Written for commit 6e47c76. Summary will update on new commits.

Review in cubic

…and model routing

* refactor(ai): extract shared OAuth token helpers, clean up GSC integration

- Extract getOAuthToken + createCachedTokenFn into shared oauth-token.ts,
  replacing identical copy-pasted token-fetch logic in search-console.ts,
  github-tools.ts, and generation.ts
- Extract getUserProviderToken in RPC integrations router, deduplicating
  the token lookup in checkSearchConsoleAccess and listGitHubRepos
- Extract useOAuthConnect hook in dashboard integrations UI, deduplicating
  the linkSocial mutation between GSC and GitHub rows
- Extract querySearchAnalytics as a standalone testable function
- Move ALWAYS_ON_TOOLS set to module scope in generation.ts
- Add 8 unit tests for search console query mapping, error handling, and
  request shape

* feat(slack): show thinking indicator immediately on stream start

Start the Slack stream eagerly with a task_update chunk (status:
in_progress) before the agent produces output, so users see the
animated thinking card instead of just a reaction emoji. Resolves
thinking to "complete" on first text flush and "error" on failures.

Also desloppified respond.ts: removed dead lazy-start logic
(tryStartStream, shouldStream, streamStartAttempted), extracted
flushAndStop/recoverFromError/logStreamError helpers, added early
bail-out for pre-aborted signals, and added a streaming-unavailable
fallback test.

* chore(deps): align ai sdk versions

* feat(insights): broaden investigation signals

* feat(dashboard): show insight investigation context

* chore(repo): tidy cleanup leftovers

* feat(db): per-table column validation in SQL validator

Replace BAD_EVENTS_COLUMN_REPLACEMENTS blocklist with proper
AGENT_TABLE_COLUMNS schema map. Validates alias.column patterns
against the actual table schema — catches cross-table misuse like
es.browser_name on error_spans before it hits ClickHouse.

* refactor(insights): extract prompts, deduplicate detection, improve readability

- Extract prompt builders and data fetchers into prompts.ts (266 lines)
- generation.ts: 1076 → 804 lines
- detection.ts: replace if/else filter chain with METRIC_FILTERS lookup
- detection.ts: deduplicate 5 signals.push() blocks with makeWowSignal()
- detection.ts: 584 → 510 lines
- validate.ts: replace inline array with SENTIMENT_DIVERGENCE_TYPES Set
- Share OrgWebsiteRow interface between generation and prompts
- Extract isEnabled() for tool filter readability

* feat(ai): shared investigation tools, actions pipeline, chat agent upgrade

- createInvestigationTools() shared across insights pipeline and chat agent
- Chat agent now has scrape, GSC, and GitHub tools (was scrape only)
- AgentContext gains organizationId for OAuth token lookups
- Add actions schema to insights (fix_goal, add_custom_event, etc.)
- Add actions column to analytics_insights table
- Chat agent prompt updated with investigation tool guidance

* feat(dashboard): render insight actions as clickable pills

Add InsightAction type and render action buttons on insight cards.
Actions include fix_goal, add_custom_event, create_funnel, etc.
Currently shows a toast on click — wire to actual mutations next.

* refactor(ai): unified toolkit, lint fixes, insights gets mutation tools

- createToolkit() assembles tools from capabilities (analytics,
  investigation, mutations, memory, dashboard)
- Chat agent simplified from 12 imports to one createToolkit() call
- Insights pipeline gains mutation tools (create_annotation, fix_goal,
  create_funnel) for executing actions during investigation
- Fix all lint errors (block statements, formatting)

* feat(insights): session investigation tools, mutation prompt, richer actions

- Add session query types to web_metrics description (interesting_sessions,
  session_list, session_flow, session_pages)
- Investigation prompt now guides agent to use session-level queries for
  user behavior analysis (step 6 in strategy)
- Agent told it can execute mutations directly (create_annotation with
  confirmed=true, update_goal for target mismatches)
- Verified: agent generates specific add_custom_event actions with exact
  event names, elements, and pages

* feat(insights): code_fix actions for cursor/claude-code integration

- Add code_fix action type with prompt, file_hint, error_message params
- Dashboard copies code_fix prompt to clipboard on click
- investigate_further also copies prompt to clipboard
- Agent generates cursor-ready prompts with exact files, changes, and
  error context from GitHub search results

* refactor(insights): extract queryPeriodPair helper in enrichment

Deduplicate period-pair query boilerplate in enrichSegments and
enrichErrors. Both functions called queryFn 4 times with nearly
identical params differing only in date range. The helper creates
a curried function that handles the current/previous split.

* fix(insights): address PR review feedback

- OAuth token: scope preferUserId lookup to org membership (P1)
- OAuth token: add 45-min TTL to cached tokens (P2)
- Error detection: catch 0→N error spikes (P2)
- Prompts: use ?? instead of || for maxInsights (P2)
- Prompts: order dismissed patterns by recency (P2)
- Validate: fix truncation overflow (P2)
- Dashboard: handle clipboard write failures (P2)

* fix(insights): address remaining review comments

- Vitals enrichment: check previous-period sample size too (P2)
- OAuth cache: negative-result caching with 5-min TTL to avoid
  repeated DB lookups when no account connected (P2)
…460)

Previous insights are now included with description, rootCause,
severity, change percent, and recurrence count. The agent can
compare current findings against previous runs — noting what
resolved, worsened, or persists. Deduplicates by subjectKey so
recurring issues show "(reported 3x)" instead of appearing as
separate entries.
…461)

- Empty subjectKey no longer collapses unrelated insights (P1)
- Fetch 50 rows then dedup to 12 unique subjects (was LIMIT 12 before dedup)
- Remove unused runId from select
* feat(insights): inject site capabilities into investigation prompt

Query custom events, errors, vitals, funnels, and goals upfront
and include them in the prompt so the agent knows what data sources
exist before making tool calls. Prevents wasted queries on unconfigured
features (revenue, custom events on sites without them).

* chore(docs): remove unused Wordmark component from footer

The decorative SVG wordmark at the bottom of the footer was removed.
Drops the component file and its import.

* feat(docs): overhaul pricing table to reflect actual product features

The comparison table was underselling the product. This adds all
shipping features that were missing, organizes the table into sections,
and links feature names to their dedicated pages.

Changes:
- Unhide Error Tracking from pricing (it ships on Hobby+)
- Add "Analytics features" section with per-plan limits and unlimited
  features shown as checkmarks
- Add "Platform" section: Uptime Monitoring, Short Links, Revenue
  Tracking, Alerts, Team Members, Websites, API Access, Slack, SDKs
- Add "Enterprise" section header for SSO, Audit Logs, Support
- Link feature names to /feature-flags, /web-vitals, /errors, /uptime,
  /links where dedicated pages exist
- Update page subtitle and footer notes to reflect full product surface
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

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

Project Deployment Actions Updated (UTC)
dashboard (staging) Ready Ready Preview, Comment May 30, 2026 9:09pm
databuddy-status Ready Ready Preview, Comment May 30, 2026 9:09pm
documentation (staging) Ready Ready Preview, Comment May 30, 2026 9:09pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

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: fedfa2bb-f2aa-41ed-ac47-790968c0faf2

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 and usage tips.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 29, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​ai-sdk/​react@​3.0.186 ⏵ 3.0.193991007498100

View full report

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR overhauls the insights pipeline — introducing WoW signal detection for errors, revenue, and Web Vitals, restructuring the investigation agent with dedicated tools (Search Console, GitHub, scrape), adding machine-readable actions to insight cards, and tightening the copy validation to truncate rather than drop verbose insights. The docs pricing table is also updated with a new Platform section.

  • Insights pipeline: Detection now covers errors, revenue, vitals, and custom events via WoW comparison; enrichment adds vitals context per signal and widens the query window to full lookbackDays; prompt helpers are extracted to prompts.ts; model routing escalates balanced → deep when critical signals are present.
  • New tools: search-console.ts and investigation-tools.ts are added; OAuth token fetching is unified via oauth-token.ts with TTL-based in-memory caching and role-priority fallback.
  • Schema & API: actions JSONB column is added to analyticsInsights, and rootCause/evidence/investigationDepth are now returned from both endpoints — but actions is selected from the DB yet absent from the mapped return value and history endpoint, so it never reaches clients.

Confidence Score: 4/5

Safe to merge with one fix: the actions field must be added to the getInsightsFromDb return mapping and the history endpoint select before the feature is usable.

The insights pipeline refactor and new tooling are well-structured. The one concrete defect is that the new machine-readable actions field is stored correctly in the DB and fetched in the query, but silently dropped before the API response is built, so it never reaches clients. Everything else — signal detection, enrichment, model routing, OAuth caching, SQL column validation — looks correct and is covered by tests.

packages/rpc/src/routers/insights.ts — the getInsightsFromDb return mapping and the history endpoint select/mapping both need the actions field added to match what is stored and queried.

Important Files Changed

Filename Overview
packages/rpc/src/routers/insights.ts Adds rootCause, evidence, investigationDepth, and actions to DB selects; actions is selected in getInsightsFromDb but dropped from the returned object, and is absent entirely from the history endpoint select and mapping.
packages/db/src/clickhouse/sql-validation.ts Adds per-table column allowlist (AGENT_TABLE_COLUMNS) and qualified-alias column validation; may produce false positives when a CTE name collides with a real table's implied alias.
apps/insights/src/detection.ts Adds WoW signal detection for errors, revenue, Web Vitals, and custom events alongside the existing z-score path; signal filters, collapsing, and severity assignments look correct.
apps/insights/src/enrichment.ts Refactors enrichment queries into a reusable queryPeriodPair helper, widens the enrichment window to full lookbackDays, and adds vitals context per signal.
apps/insights/src/generation.ts Moves prompt helpers to prompts.ts, introduces model routing based on critical signals, adds toolkit-based tool selection, and persists the new actions field correctly.
packages/ai/src/ai/tools/utils/oauth-token.ts New shared OAuth token fetcher with role-priority fallback and TTL-based in-memory caching; replaces the per-tool ad-hoc patterns.
packages/ai/src/ai/tools/search-console.ts New Google Search Console tool that queries search analytics via OAuth token; error handling and token caching look correct.
packages/ai/src/ai/insights/validate.ts Tightens description/suggestion limits and switches from dropping verbose insights to truncating them at sentence boundaries.
packages/db/src/drizzle/schema/analytics.ts Adds actions JSONB column to analyticsInsights table with properly typed AnalyticsInsightAction interface.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Scheduled insights run] --> B[detectSignals z-score + WoW]
    B --> C[enrichSignals segments, errors, vitals, annotations]
    C --> D[buildInvestigationPrompt]
    D --> E{modelForTier}
    E -->|fast| F[gpt-5.4-mini]
    E -->|balanced + critical| G[claude-opus-4.7]
    E -->|balanced| H[claude-sonnet-4.6]
    F & G & H --> I[ToolLoopAgent emit_insight]
    I --> J[validateInsight truncate or drop]
    J --> K[persistWebsiteInsights dedupe + upsert]
    K --> L[(analyticsInsights DB actions JSONB)]
    L -->|getInsightsFromDb| M[API response actions dropped]
    L -->|history endpoint| N[History response actions never selected]
Loading

Comments Outside Diff (1)

  1. packages/rpc/src/routers/insights.ts, line 221-237 (link)

    P1 actions selected but silently dropped from return value

    actions is fetched from the DB at line 208 (actions: analyticsInsights.actions) but is never added to the mapped object returned here — so every caller gets undefined for that field. parseInsightShape also doesn't include it. The history endpoint (around line 524) has the same gap: actions is neither selected nor mapped, meaning the new machine-readable actions feature is fully written to the DB but never surfaced to clients.

Reviews (1): Last reviewed commit: "feat(docs): overhaul pricing table and r..." | Re-trigger Greptile

Comment on lines 448 to 480
}
}

const aliasToTable = new Map<string, string>();
for (const ref of refs) {
if (!cteNames.has(ref.name) && ref.name in AGENT_TABLE_COLUMNS) {
aliasToTable.set(ref.alias, ref.name);
}
}

const QUALIFIED_COLUMN =
/\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
QUALIFIED_COLUMN.lastIndex = 0;
let qm = QUALIFIED_COLUMN.exec(sanitized);
while (qm) {
const alias = qm[1].toLowerCase();
const col = qm[2].toLowerCase();
const table = aliasToTable.get(alias);
if (table) {
const validCols = AGENT_TABLE_COLUMNS[table];
if (validCols && !validCols.has(col)) {
return {
valid: false,
reason: `Column "${qm[2]}" does not exist on ${table}. Valid columns: ${[...validCols].join(", ")}.`,
};
}
}
qm = QUALIFIED_COLUMN.exec(sanitized);
}

const selectCount = sanitized.match(SELECT_KEYWORD_PATTERN)?.length ?? 0;
if (selectCount > 1 + cteNames.size) {
return {
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 CTE-name collision with implied table alias causes false positives

When a CTE has the same name as a real table's implied alias, the column validator incorrectly treats the CTE's columns as belonging to the real table. For example, a query like WITH events AS (SELECT client_id, COUNT(*) as cnt FROM analytics.events ...) SELECT events.cnt FROM events would be rejected because cnt is not in AGENT_TABLE_COLUMNS["analytics.events"]. The check !cteNames.has(ref.name) guards against ref.name ("analytics.events") being a CTE, but the implied alias "events" is still inserted into aliasToTable, shadowing the CTE. Valid AI-generated queries using computed CTE columns will be falsely rejected if they choose CTE names that match a table's leaf name.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

9 issues found across 49 files

Confidence score: 2/5

  • Merge risk is high because apps/insights/src/prompts.ts combines untrusted annotation/history text with prompt context that can authorize mutation tools, creating a credible prompt-injection path to unintended writes.
  • There are multiple concrete correctness regressions in core flows: token selection can return null despite valid tokens in packages/ai/src/ai/tools/utils/oauth-token.ts, and schema constraints in packages/ai/src/ai/schemas/smart-insights-output.ts can reject valid structured action payloads.
  • packages/db/src/clickhouse/sql-validation.ts has several validation gaps/collision risks (legacy unqualified columns and alias/CTE handling) that can allow invalid references or wrongly reject valid queries, increasing runtime/query reliability risk.
  • Pay close attention to apps/insights/src/prompts.ts, packages/ai/src/ai/tools/utils/oauth-token.ts, packages/ai/src/ai/schemas/smart-insights-output.ts, and packages/db/src/clickhouse/sql-validation.ts - they contain the highest-impact security and regression concerns.
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/ai/src/ai/tools/utils/oauth-token.ts">

<violation number="1" location="packages/ai/src/ai/tools/utils/oauth-token.ts:31">
P1: Fallback query should filter out NULL access tokens. Without this, a higher-priority member with a null token shadows valid tokens from lower-priority members, causing the function to return null when a valid token exists.</violation>
</file>

<file name="apps/insights/src/prompts.ts">

<violation number="1" location="apps/insights/src/prompts.ts:345">
P1: Untrusted annotation/history text is fed into the same prompt that authorizes confirmed mutation tools, creating a prompt-injection path for unintended writes.</violation>
</file>

<file name="apps/slack/src/slack/respond.ts">

<violation number="1" location="apps/slack/src/slack/respond.ts:360">
P2: `flushAndStop` only sends the first 3500 characters of `pending`, silently dropping the rest. The previous `flush(true)` looped (`do...while (force && pending)`) to drain all buffered content. Add a similar loop here to avoid truncating the user's partial response on error/abort.</violation>
</file>

<file name="apps/insights/src/enrichment.ts">

<violation number="1" location="apps/insights/src/enrichment.ts:380">
P2: `enrichVitals` treats `0` p75 values as missing data, which can suppress valid vitals changes from being reported.</violation>
</file>

<file name="packages/ai/src/ai/schemas/smart-insights-output.ts">

<violation number="1" location="packages/ai/src/ai/schemas/smart-insights-output.ts:148">
P1: `actions.params` is typed as string-only values, which conflicts with action payloads that require structured data (e.g. funnel `steps` arrays) and can cause valid emitted insights to fail schema validation.</violation>
</file>

<file name="packages/ai/src/ai/agents/analytics.ts">

<violation number="1" location="packages/ai/src/ai/agents/analytics.ts:69">
P2: Investigation tools are now gated on `organizationId`, which can silently remove `scrape_page` in org-less agent contexts compared to previous behavior.</violation>
</file>

<file name="packages/db/src/clickhouse/sql-validation.ts">

<violation number="1" location="packages/db/src/clickhouse/sql-validation.ts:21">
P2: Add a sync-breadcrumb comment on `AGENT_TABLE_COLUMNS` noting it must list the same tables as `AGENT_TENANT_COLUMN_BY_TABLE`. Without it, a future addition to one map but not the other will silently skip column validation for the new table.

(Based on your team's feedback about preserving cross-reference comments.) [FEEDBACK_USED]</violation>

<violation number="2" location="packages/db/src/clickhouse/sql-validation.ts:453">
P2: Also guard against CTE-name collisions on the alias key when populating `aliasToTable`; otherwise valid CTE-qualified columns can be validated against the wrong base table and rejected.</violation>

<violation number="3" location="packages/db/src/clickhouse/sql-validation.ts:458">
P1: Restore a check for bare legacy `analytics.events` column names; the new qualified-only validation misses unqualified invalid columns and lets old schema names through.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant U as User
    participant D as Dashboard (Next.js)
    participant I as Insights Service
    participant DB as Database (Postgres)
    participant CH as ClickHouse
    participant AI as AI Model (Claude/GPT)
    participant GH as GitHub API
    participant SC as Google Search Console
    participant Slack as Slack Client

    Note over U,Slack: NEW: Investigation Pipeline with Tools & Actions

    D->>I: POST /v1/agent (userId, organizationId, websiteId)
    I->>I: createAgentConfig (includes organizationId)
    I->>I: createToolkit(capabilities, domain, organizationId)
    I->>I: createInvestigationTools(domain, orgId, userId)
    I->>GH: githubFetch (via OAuth token)
    I->>SC: querySearchAnalytics (via OAuth token)
    I->>I: getCachedSiteContext(domain)
    I->>I: scrapePage(domain, path) - fallback context

    I->>CH: detectSignals(websiteId, timezone)
    Note right of I: Queries events_by_date,<br/>summary_metrics, error_summary,<br/>revenue_overview, vitals_overview,<br/>custom_events_discovery

    CH-->>I: DetectedSignal[]

    I->>CH: enrichSignals(DetectedSignal[])
    Note right of I: Queries segments, errors,<br/>vitals, annotations per signal
    CH-->>I: EnrichedSignal[] (with vitalsContext, errorContext, segments)

    I->>AI: modelForTier(tier, hasCriticalSignals)
    alt hasCriticalSignals AND tier == "balanced"
        I->>AI: Route to deep model (Claude Opus)
    else
        I->>AI: Route to configured tier model
    end

    I->>DB: fetchRecentAnnotations(websiteId)
    I->>DB: fetchInsightHistory(orgId, websiteId)
    I->>DB: fetchDismissedPatterns(orgId, websiteId)
    I->>DB: fetchSiteCapabilities(websiteId) - funnels, goals, events
    DB-->>I: Context for AI prompt

    I->>I: buildInvestigationPrompt(...)
    I->>I: buildSystemPrompt(config, { investigationMode: true })
    Note left of I: NEW: Actions (fix_goal,<br/>code_fix, etc.), evidence,<br/>investigationDepth in schema

    I->>AI: Generate insights (insightSchema with actions)
    AI-->>I: ParsedInsight[] (with rootCause, evidence, actions)

    I->>I: validateInsight (NEW: truncateAtSentence, MAX_DESCRIPTION=300)
    I->>DB: storeAnalyticsSummary (stores actions JSONB)
    DB-->>I: GeneratedWebsiteInsight[]

    D->>I: GET /api/insights (RPC)
    I->>DB: SELECT (including rootCause, evidence, investigationDepth, actions)
    DB-->>I: Insight[] with new fields
    I-->>D: InsightCardViewModel

    Note over D: NEW: rootCause, investigationEvidence,<br/>actions[] in view model
    D->>D: toInsightCardViewModel(insight)

    alt action type is code_fix or investigate_further
        D->>U: Show click-to-copy prompt button
        U->>D: Click action pill
        D->>U: navigator.clipboard.writeText(prompt)
    else other action types
        D->>U: Show toast.info(action.label)
    end

    Note over D,Slack: NEW: Slack streaming with thinking indicator

    Slack->>D: app_mention with text
    D->>D: startThinkingStream(...)
    D->>Slack: chat.startStream with task_update "in_progress"
    D->>D: Stream agent response
    D->>Slack: chat.appendStream (resolve thinking, add text)
    alt error occurs
        D->>Slack: chat.appendStream with "error" task_update
        Slack-->>D: stopStream with user-facing error message
    else success
        D->>Slack: chat.appendStream with "complete" task_update
        D->>Slack: chat.stopStream
    end

    Note over U,DB: NEW: Pricing Page - Platform Section

    D->>U: Show Pricing Table
    Note right of U: NEW: Unhide Error Tracking<br/>NEW: Platform section with 9 features<br/>NEW: Feature labels link to pages<br/>NEW: Check icons for unlimited
Loading

Shadow auto-approve: would not auto-approve because issues were found.
Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment on lines +31 to +43
const [fallback] = await db
.select({ accessToken: account.accessToken })
.from(account)
.innerJoin(member, eq(member.userId, account.userId))
.where(
and(
eq(member.organizationId, organizationId),
eq(account.providerId, providerId)
)
)
.orderBy(ROLE_PRIORITY)
.limit(1);

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P1: Fallback query should filter out NULL access tokens. Without this, a higher-priority member with a null token shadows valid tokens from lower-priority members, causing the function to return null when a valid token exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/tools/utils/oauth-token.ts, line 31:

<comment>Fallback query should filter out NULL access tokens. Without this, a higher-priority member with a null token shadows valid tokens from lower-priority members, causing the function to return null when a valid token exists.</comment>

<file context>
@@ -0,0 +1,66 @@
+		}
+	}
+
+	const [fallback] = await db
+		.select({ accessToken: account.accessToken })
+		.from(account)
</file context>
Suggested change
const [fallback] = await db
.select({ accessToken: account.accessToken })
.from(account)
.innerJoin(member, eq(member.userId, account.userId))
.where(
and(
eq(member.organizationId, organizationId),
eq(account.providerId, providerId)
)
)
.orderBy(ROLE_PRIORITY)
.limit(1);
const [fallback] = await db
.select({ accessToken: account.accessToken })
.from(account)
.innerJoin(member, eq(member.userId, account.userId))
.where(
and(
eq(member.organizationId, organizationId),
eq(account.providerId, providerId),
sql`${account.accessToken} IS NOT NULL`
)
)
.orderBy(ROLE_PRIORITY)
.limit(1);
Fix with Cubic

- Confidence > 0.7 requires segment isolation or temporal correlation.
- Actions: include when fixable (fix_goal, add_custom_event, create_annotation, create_funnel, add_tracking, investigate_further, code_fix).
- code_fix: when you find a bug with a clear fix, emit a code_fix action with params {prompt, file_hint, error_message}. The prompt should be paste-ready for Cursor or Claude Code — include the exact file to change, what to change, and why.
- You have mutation tools: call create_annotation directly to mark deploys or incidents on the timeline. Call update_goal to fix goal target mismatches. Use confirmed=true to execute.${
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P1: Untrusted annotation/history text is fed into the same prompt that authorizes confirmed mutation tools, creating a prompt-injection path for unintended writes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/prompts.ts, line 345:

<comment>Untrusted annotation/history text is fed into the same prompt that authorizes confirmed mutation tools, creating a prompt-injection path for unintended writes.</comment>

<file context>
@@ -0,0 +1,454 @@
+- Confidence > 0.7 requires segment isolation or temporal correlation.
+- Actions: include when fixable (fix_goal, add_custom_event, create_annotation, create_funnel, add_tracking, investigate_further, code_fix).
+- code_fix: when you find a bug with a clear fix, emit a code_fix action with params {prompt, file_hint, error_message}. The prompt should be paste-ready for Cursor or Claude Code — include the exact file to change, what to change, and why.
+- You have mutation tools: call create_annotation directly to mark deploys or incidents on the timeline. Call update_goal to fix goal target mismatches. Use confirmed=true to execute.${
+		options?.investigationMode
+			? "\n- Investigate detected signals using tools. Call emit_insight for each finding. Drop noise."
</file context>
Fix with Cubic

]),
label: z.string().describe("Button label (e.g. 'Fix goal target')"),
params: z
.record(z.string(), z.string())
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P1: actions.params is typed as string-only values, which conflicts with action payloads that require structured data (e.g. funnel steps arrays) and can cause valid emitted insights to fail schema validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/schemas/smart-insights-output.ts, line 148:

<comment>`actions.params` is typed as string-only values, which conflicts with action payloads that require structured data (e.g. funnel `steps` arrays) and can cause valid emitted insights to fail schema validation.</comment>

<file context>
@@ -121,21 +123,41 @@ export const insightSchema = z.object({
+				]),
+				label: z.string().describe("Button label (e.g. 'Fix goal target')"),
+				params: z
+					.record(z.string(), z.string())
+					.describe(
+						"Action-specific parameters. code_fix: {prompt, file_hint, error_message} — generates a cursor/claude-code-ready prompt."
</file context>
Fix with Cubic

}
}

const QUALIFIED_COLUMN =
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P1: Restore a check for bare legacy analytics.events column names; the new qualified-only validation misses unqualified invalid columns and lets old schema names through.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/sql-validation.ts, line 458:

<comment>Restore a check for bare legacy `analytics.events` column names; the new qualified-only validation misses unqualified invalid columns and lets old schema names through.</comment>

<file context>
@@ -371,6 +448,33 @@ export function validateAgentSQL(sql: string): {
+		}
+	}
+
+	const QUALIFIED_COLUMN =
+		/\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
+	QUALIFIED_COLUMN.lastIndex = 0;
</file context>
Fix with Cubic

}
stopText?: string
): Promise<void> {
if (pending.trim()) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P2: flushAndStop only sends the first 3500 characters of pending, silently dropping the rest. The previous flush(true) looped (do...while (force && pending)) to drain all buffered content. Add a similar loop here to avoid truncating the user's partial response on error/abort.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/slack/src/slack/respond.ts, line 360:

<comment>`flushAndStop` only sends the first 3500 characters of `pending`, silently dropping the rest. The previous `flush(true)` looped (`do...while (force && pending)`) to drain all buffered content. Add a similar loop here to avoid truncating the user's partial response on error/abort.</comment>

<file context>
@@ -358,85 +318,175 @@ async function finishStreamedResponse(
-		}
+	stopText?: string
+): Promise<void> {
+	if (pending.trim()) {
+		await client.chat
+			.appendStream({
</file context>
Fix with Cubic

Comment on lines +380 to +389
const curVal = cur?.p75 ?? 0;
const prevVal = prev?.p75 ?? 0;
if (
curVal === 0 ||
prevVal === 0 ||
(cur?.samples ?? 0) < 5 ||
(prev?.samples ?? 0) < 5
) {
continue;
}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P2: enrichVitals treats 0 p75 values as missing data, which can suppress valid vitals changes from being reported.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/enrichment.ts, line 380:

<comment>`enrichVitals` treats `0` p75 values as missing data, which can suppress valid vitals changes from being reported.</comment>

<file context>
@@ -373,24 +352,72 @@ async function enrichAnnotations(
+	for (const name of ["LCP", "INP", "CLS", "FCP", "TTFB"]) {
+		const cur = currentMap.get(name);
+		const prev = previousMap.get(name);
+		const curVal = cur?.p75 ?? 0;
+		const prevVal = prev?.p75 ?? 0;
+		if (
</file context>
Suggested change
const curVal = cur?.p75 ?? 0;
const prevVal = prev?.p75 ?? 0;
if (
curVal === 0 ||
prevVal === 0 ||
(cur?.samples ?? 0) < 5 ||
(prev?.samples ?? 0) < 5
) {
continue;
}
const curVal = cur?.p75;
const prevVal = prev?.p75;
if (
curVal == null ||
prevVal == null ||
(cur?.samples ?? 0) < 5 ||
(prev?.samples ?? 0) < 5
) {
continue;
}
Fix with Cubic

"dashboard",
],
domain: context.websiteDomain,
organizationId: context.organizationId,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P2: Investigation tools are now gated on organizationId, which can silently remove scrape_page in org-less agent contexts compared to previous behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/agents/analytics.ts, line 69:

<comment>Investigation tools are now gated on `organizationId`, which can silently remove `scrape_page` in org-less agent contexts compared to previous behavior.</comment>

<file context>
@@ -80,12 +57,18 @@ export function createConfig(
+				"dashboard",
+			],
+			domain: context.websiteDomain,
+			organizationId: context.organizationId,
+			userId: context.userId,
+		}),
</file context>
Fix with Cubic

"analytics.link_visits": "client_id",
};

export const AGENT_TABLE_COLUMNS: Readonly<
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P2: Add a sync-breadcrumb comment on AGENT_TABLE_COLUMNS noting it must list the same tables as AGENT_TENANT_COLUMN_BY_TABLE. Without it, a future addition to one map but not the other will silently skip column validation for the new table.

(Based on your team's feedback about preserving cross-reference comments.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/sql-validation.ts, line 21:

<comment>Add a sync-breadcrumb comment on `AGENT_TABLE_COLUMNS` noting it must list the same tables as `AGENT_TENANT_COLUMN_BY_TABLE`. Without it, a future addition to one map but not the other will silently skip column validation for the new table.

(Based on your team's feedback about preserving cross-reference comments.) </comment>

<file context>
@@ -18,6 +18,100 @@ export const AGENT_TENANT_COLUMN_BY_TABLE: Readonly<Record<string, string>> = {
 	"analytics.link_visits": "client_id",
 };
 
+export const AGENT_TABLE_COLUMNS: Readonly<
+	Record<string, ReadonlySet<string>>
+> = {
</file context>
Suggested change
export const AGENT_TABLE_COLUMNS: Readonly<
/**
* Column allowlist for each table in AGENT_TENANT_COLUMN_BY_TABLE.
* Keep this map in sync every table listed above must also appear here.
*/
export const AGENT_TABLE_COLUMNS: Readonly<
Fix with Cubic


const aliasToTable = new Map<string, string>();
for (const ref of refs) {
if (!cteNames.has(ref.name) && ref.name in AGENT_TABLE_COLUMNS) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

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

P2: Also guard against CTE-name collisions on the alias key when populating aliasToTable; otherwise valid CTE-qualified columns can be validated against the wrong base table and rejected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/clickhouse/sql-validation.ts, line 453:

<comment>Also guard against CTE-name collisions on the alias key when populating `aliasToTable`; otherwise valid CTE-qualified columns can be validated against the wrong base table and rejected.</comment>

<file context>
@@ -371,6 +448,33 @@ export function validateAgentSQL(sql: string): {
 
+	const aliasToTable = new Map<string, string>();
+	for (const ref of refs) {
+		if (!cteNames.has(ref.name) && ref.name in AGENT_TABLE_COLUMNS) {
+			aliasToTable.set(ref.alias, ref.name);
+		}
</file context>
Suggested change
if (!cteNames.has(ref.name) && ref.name in AGENT_TABLE_COLUMNS) {
if (!cteNames.has(ref.name) && !cteNames.has(ref.alias) && ref.name in AGENT_TABLE_COLUMNS) {
Fix with Cubic

* fix(auth): make local-email verification explicit and raise password ceiling

Set accountLinking.requireLocalEmailVerified: true explicitly so future
config changes can't silently regress the OAuth pre-account-takeover
protection. Bump maxPasswordLength from 32 to 128 to align with
NIST 800-63B and unblock password manager output.

* fix(auth): purge reset tokens and revoke sessions on password change

Outstanding reset-password verification rows previously survived both
a successful reset and a settings-driven password change, leaving the
old link usable until its 1-hour TTL. Add an onPasswordReset hook and
a databaseHooks.account.update.after hook on the credential account so
both flows purge the user's outstanding reset tokens. Also enable
revokeSessionsOnPasswordReset so existing sessions die when the email
reset flow completes.

* fix(auth): disable session cookie cache so revocation is immediate

With cookieCache enabled, Better-Auth validates requests against the
signed sessionData cookie payload for up to maxAge seconds without
hitting Redis. That left a 5-minute window where a stolen cookie kept
working after logout, password reset, ban-user, or organization
member removal. secondaryStorage already points at Redis, so dropping
the cache costs one fast round-trip per request and closes the
post-revocation replay window entirely.

* feat(basket): log quota-exceeded events with billing context

Emit an explicit warn at the point of detection so blocked-ingest rows
carry customerId, featureId, properties, and usage/grant fields. The
wide-event context was previously lost by the time the error reached
Axiom, leaving 114k quota-exceeded rows with no attribution.

* refactor(basket): drop redundant JSDoc and centralize event input types

Remove restating JSDoc across the basket lib/utils modules (one comment
on validateRequest was also stale, describing a return shape the function
no longer has). Promote AnalyticsEventInput and OutgoingLinkInput to
exported z.infer types in the validation package and reuse them in the
route handlers instead of re-deriving locally.

* refactor(api): sanitize Autumn request body and split out session loading

Sanitize the parsed body in a single pass and reuse one autumn handler
instance. Extract loadSession so a getSession failure is logged and
rethrown instead of being swallowed into a null identity, and drop the
manual activeOrganizationId cast now that the session type carries it.

* refactor(rpc): dedupe billing-control handlers via shared upsert helper

Collapse the near-identical auto-topup, usage-alert, and spend-limit
mutation handlers into one generic upsertBillingControl that handles the
owner check, getOrCreate/merge/update, and error logging. Replaces the
three parallel entry interfaces with a single keyed BillingControlEntries.

* feat(insights): add funnel and goal conversion signal detection

Detect week-over-week conversion drops on funnel definitions and goals
via a new funnel-detection module, reusing the shared wowWindow helper
and makeWowSignal (both lifted out of detection.ts) and enrichment's
window logic. Extract insight persistence and dedup out of generation.ts
into persistence.ts, and expose the rpc analytics-utils entrypoint that
funnel detection needs for funnel/goal analytics processing.

* fix(rpc): stop swallowing DB errors in workspace lookups

Transient DB failures in getWebsiteById and getOrganizationRole were
caught and degraded to null, surfacing as misleading 404/403 responses
that hid the real incident. Let errors propagate to the ORPC handler.

* feat(auth): forward Better-Auth internal logs to evlog

Better-Auth wraps internal failures (e.g. session-refresh DB writes) as
a generic "Failed to get session" and logs the real cause only to its
default console logger, so the underlying error never reached Axiom.
Wire a logger.log forwarder to evlog to surface the actual error.

* fix(rpc): stop swallowing DB errors when resolving billing owner

A transient DB failure in _getOrganizationOwnerId was caught and
degraded to null, which fell back to billing the member instead of the
org owner. Let the error propagate so identity is never silently wrong.

* refactor(rpc): drop redundant JSDoc from billing plan helpers

Names and signatures already document these pure helpers; the JSDoc only
restated them and the example blocks duplicated the same call pattern.

* refactor(api): parse Autumn webhook payloads with Zod at the boundary

Replace unchecked `as` casts of untrusted webhook data with Zod schemas,
deriving the payload types via z.infer. Malformed payloads now return a
logged 400 instead of crashing deep inside a handler.

* feat(insights): bill agent usage and gate generation on credits

Add a resolveInsightsBilling DI seam that resolves the billing customer and
checks agent credits before running the website agent, skipping generation
when credits are exhausted. Track usage against Autumn after each agent run.
Align getComparisonPeriod with the detection wowWindow so the agent and
signal detection share the same week-over-week window.

* fix(ai): allow "insights" as an agent usage source

The insights generation pipeline bills agent usage via trackAgentUsageAndBill;
widen the source union so it can identify itself as the insights source.

* style(docs): sort pricing table interface members and attributes

Satisfy the lint assist rules (sorted interface members and JSX attributes)
that were failing CI on the pricing table components.

* fix(api): use namespace import for zod in Autumn webhook

* feat(db): scope agent chats to organization

Make agent_chats.website_id nullable, index by organization instead of
website, and switch the website FK to ON DELETE SET NULL so chats survive
website deletion. Agent chats are now an organization-level workspace.

* feat(rpc): list agent chats by organization

List and authorize agent chats against the organization workspace rather
than a single website, and tolerate chats with a null website.

* feat(ai): give the agent multi-website workspace context

Add a list-websites tool and thread accessible-website context (accessible
websites + default website id) through the toolkit so the agent can query
across an organization's sites instead of a single hard-coded website.

* feat(api): serve the agent over an organization workspace

Resolve the agent's accessible websites and default website from the
organization context so a single chat can operate across the workspace's
sites instead of one fixed website.

* feat(dashboard): rebuild the agent as an organization workspace

Move the agent out from under a single website into a top-level workspace:
add a global /agent route with chat history, mentions for referencing
specific websites, and a no-websites empty state. Chats are now scoped to
the organization, so switching websites no longer loses the conversation.

* style(api): format agent route

* fix(ai): stop the agent from emitting emojis

Add a global no-emoji rule to the shared behavior rules so it applies
across the dashboard, MCP, and Slack variants, and drop the warning
emoji from the link-delete confirmation the model could otherwise mirror.

* fix(dashboard): smooth out the website mention menu

Memoize each mention row and stabilize its callbacks so navigating with
arrow keys or the pointer only re-renders the rows whose selection
changed, switch hover from mouseEnter to mouseMove so a keyboard-driven
scroll under a still cursor no longer hijacks the selection, and scroll
the active row into view. Also lift the input toolbar into its own
memoized component and the input surface into a named element so the
menu wiring reads flat.

* refactor(dashboard): drive agent chat navigation through callbacks

Make AgentWorkspace's new/select/delete handlers required props and
remove the hidden route-param fallbacks from ChatHistory and
NewChatButton, so the workspace no longer reads the route. Each page
now owns its own navigation: the org page routes to /agent/* and the
per-website page to /websites/[id]/agent/*, both clearing the stored
last-chat key on delete-to-empty.

* fix(ai): enforce website membership in resolveToolWebsite

Require an explicit websiteId to be in the accessible set or the
single-site context, instead of skipping the check when the accessible
list is empty. Closes a fail-open path where any websiteId would be
accepted.

* fix(ai): harden agent tool context and result keying

Escape &, <, > in context XML attributes (not just quotes) and escape
website ids. Honor an explicit websiteId in search_console instead of
short-circuiting on the default domain. Make get_data result keys
collision-proof so repeated type+websiteId queries no longer overwrite.

* fix(insights): bump createdAt on insight refresh to preserve dedupe cooldown

The refresh path updated content but left createdAt stale, so actively
refreshed insights eventually fell outside the cooldown window and were
re-inserted as duplicates. Bump createdAt on refresh to match the insert
path.

* fix(insights): make funnel/goal detection deterministic and type-safe

Order funnel and goal queries by createdAt so the MAX_DEFINITIONS limit
is stable, and map CUSTOM step/goal types explicitly to the EVENT branch
instead of an unsafe cast.

* fix(dashboard): allow WebAssembly in production CSP

Adds 'wasm-unsafe-eval' to script-src so Shiki's Oniguruma WASM can
instantiate. Without it, the flags page (which renders CodeBlock via
Shiki) hit a CSP CompileError in production for every visitor.

* fix(dashboard): use stable en-US locale in formatNumber

formatNumber called Intl.NumberFormat(undefined, …) so the server
rendered numbers with the Node default locale (en-US) while the
client used the browser's locale. For non-en-US visitors that
mismatch produced React #418 hydration errors and a blank screen
on /websites. Matches formatCurrency and lib/format-locale-number
which already pin en-US for SSR stability.

* fix(dashboard): gate per-website agent page on org load

Show a skeleton while the organization context resolves instead of
rendering the workspace with a null organizationId, which produced a
transient "No active workspace" flash on the per-website agent route.

* refactor(dashboard): extract shared agent gate guard

Dedupe loading/no-websites logic across the global agent page and
redirect, and only fire the redirect once the gate is ready.
@unkey-deploy
Copy link
Copy Markdown

unkey-deploy Bot commented May 30, 2026

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

Name Status Preview Inspect Updated (UTC)
api (preview) Ready Visit Preview Inspect May 30, 2026 9:08pm

userId,
websiteId: body.websiteId,
websiteDomain: domain,
organizationId: organizationId ?? undefined,
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

22 issues found across 80 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/utils/billing.ts">

<violation number="1" location="packages/rpc/src/utils/billing.ts:19">
P1: This change removes error isolation from organization owner lookup, so a transient DB failure now throws and can break billing resolution paths that previously fell back gracefully.</violation>
</file>

<file name="apps/dashboard/components/agent/hooks/use-agent-chat.ts">

<violation number="1" location="apps/dashboard/components/agent/hooks/use-agent-chat.ts:20">
P2: The new optional/nullable scope parameters allow sending chat requests without any tenant scope (`organizationId` null and `websiteId` undefined). Add a guard so at least one scope identifier is required before building the request.</violation>
</file>

<file name="packages/ai/src/ai/config/context.ts">

<violation number="1" location="packages/ai/src/ai/config/context.ts:63">
P2: Escape `defaultWebsiteId`/`websiteDomain` before inserting them into XML-like element text to avoid malformed prompt context.</violation>
</file>

<file name="packages/ai/src/ai/prompts/analytics.ts">

<violation number="1" location="packages/ai/src/ai/prompts/analytics.ts:195">
P2: Escape website id/domain before interpolating into `<website-scope>` guidance; raw DB/context values can break prompt structure and enable prompt-injection via crafted website metadata.</violation>
</file>

<file name="packages/ai/src/ai/tools/dashboard-actions.ts">

<violation number="1" location="packages/ai/src/ai/tools/dashboard-actions.ts:88">
P2: `defaultWebsiteId` is prioritized ahead of `context.websiteId`, which can override the currently scoped website and send dashboard actions to the wrong site.</violation>
</file>

<file name="apps/basket/src/lib/billing.ts">

<violation number="1" location="apps/basket/src/lib/billing.ts:45">
P2: Avoid logging raw `properties` in quota-exceeded warnings; this payload is unbounded and may contain sensitive data. Log only safe metadata (for example keys/count) or a sanitized subset.</violation>
</file>

<file name="apps/insights/src/funnel-detection.ts">

<violation number="1" location="apps/insights/src/funnel-detection.ts:145">
P2: `getTotalWebsiteUsers` is executed once per goal per period, causing redundant website-wide queries and avoidable slowdown as goal count grows. Cache/reuse totals per period instead of recomputing for each goal.</violation>
</file>

<file name="apps/dashboard/next.config.ts">

<violation number="1" location="apps/dashboard/next.config.ts:97">
P2: Adding `wasm-unsafe-eval` globally weakens the CSP `script-src` policy in production. Scope this to only the routes/environments that require runtime WebAssembly evaluation, otherwise keep the stricter default policy.</violation>
</file>

<file name="packages/ai/src/ai/tools/profiles.ts">

<violation number="1" location="packages/ai/src/ai/tools/profiles.ts:10">
P2: Reject empty `websiteId` values in the schema so invalid input does not silently fall back to the default website.</violation>
</file>

<file name="packages/ai/src/ai/tools/utils/context.ts">

<violation number="1" location="packages/ai/src/ai/tools/utils/context.ts:29">
P2: `websiteId` presence check is truthy-based, so an empty string is treated as "not provided" and incorrectly falls back to the default website.</violation>
</file>

<file name="packages/ai/src/ai/tools/search-console.ts">

<violation number="1" location="packages/ai/src/ai/tools/search-console.ts:114">
P2: Handle website-resolution errors before querying so the tool returns a structured error instead of throwing.</violation>
</file>

<file name="packages/ai/src/ai/insights/flag-context.ts">

<violation number="1" location="packages/ai/src/ai/insights/flag-context.ts:27">
P1: Requiring `websiteId` here can break organization-scoped flag change queries by throwing when no website is selected, instead of still returning org-level (`websiteId IS NULL`) changes.</violation>
</file>

<file name="packages/ai/src/ai/tools/execute-sql-query.ts">

<violation number="1" location="packages/ai/src/ai/tools/execute-sql-query.ts:99">
P2: Validate `websiteId` as non-empty. Right now `""` is accepted and treated as omitted, which can silently query the default website instead of the caller’s intended target.</violation>
</file>

<file name="apps/api/src/billing/autumn.ts">

<violation number="1" location="apps/api/src/billing/autumn.ts:83">
P1: `identifyAutumnCustomer` now propagates session lookup errors, which can fail the billing request instead of returning an anonymous/null identity on transient auth errors.</violation>
</file>

<file name="apps/dashboard/components/agent/agent-chat-surface.tsx">

<violation number="1" location="apps/dashboard/components/agent/agent-chat-surface.tsx:68">
P2: Last-chat persistence key is now inconsistent with the reader: this writes by `defaultWebsiteId` first, while chat restoration reads by `organizationId`, so recent chat state can be lost/restored incorrectly.</violation>
</file>

<file name="packages/ai/src/ai/agents/execution.ts">

<violation number="1" location="packages/ai/src/ai/agents/execution.ts:22">
P2: `"insights"` was added only in this local source union, but the upstream/shared source types still exclude it. That leaves the new value effectively unreachable from typed call paths and creates contract drift across agent source definitions.</violation>
</file>

<file name="packages/ai/src/ai/insights/business-context.ts">

<violation number="1" location="packages/ai/src/ai/insights/business-context.ts:31">
P2: This change introduces inconsistent website ID resolution: revenue queries now use `defaultWebsiteId ?? websiteId`, but revenue config lookup still uses only `websiteId`. This can produce mismatched `revenue_configured` vs queried data when only `defaultWebsiteId` is set (or when IDs differ).</violation>
</file>

<file name="packages/ai/src/ai/tools/list-websites.ts">

<violation number="1" location="packages/ai/src/ai/tools/list-websites.ts:18">
P2: Derive `defaultWebsiteId` from the single accessible website as a fallback to keep website selection behavior consistent.</violation>
</file>

<file name="packages/ai/src/ai/agents/analytics.ts">

<violation number="1" location="packages/ai/src/ai/agents/analytics.ts:45">
P2: Passing the full `accessibleWebsites` array into prompt context is unbounded and can significantly bloat system prompt tokens for large orgs.</violation>
</file>

<file name="apps/insights/src/persistence.ts">

<violation number="1" location="apps/insights/src/persistence.ts:226">
P1: The upsert conflict target makes dedupe effectively global, so post-cooldown repeats overwrite old insight rows instead of creating new history entries.</violation>
</file>

<file name="apps/insights/src/generation.ts">

<violation number="1" location="apps/insights/src/generation.ts:203">
P2: Avoid coupling the new funnel-goal detector to the existing metric detector. A failure in the added path now drops all signals instead of just the new ones.</violation>
</file>

<file name="apps/api/src/routes/agent.ts">

<violation number="1" location="apps/api/src/routes/agent.ts:680">
P2: Select the sole accessible website as the default when no websiteId is provided, otherwise single-site chats lose memory and enrichment.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.
Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment on lines +19 to +23
const orgMember = await db.query.member.findFirst({
where: { organizationId, role: "owner" },
columns: { userId: true },
});
return orgMember?.userId ?? null;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P1: This change removes error isolation from organization owner lookup, so a transient DB failure now throws and can break billing resolution paths that previously fell back gracefully.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/rpc/src/utils/billing.ts, line 19:

<comment>This change removes error isolation from organization owner lookup, so a transient DB failure now throws and can break billing resolution paths that previously fell back gracefully.</comment>

<file context>
@@ -16,16 +16,11 @@ const _getOrganizationOwnerId = async (
-		logger.error({ error }, "Error resolving organization owner");
-		return null;
-	}
+	const orgMember = await db.query.member.findFirst({
+		where: { organizationId, role: "owner" },
+		columns: { userId: true },
</file context>
Suggested change
const orgMember = await db.query.member.findFirst({
where: { organizationId, role: "owner" },
columns: { userId: true },
});
return orgMember?.userId ?? null;
try {
const orgMember = await db.query.member.findFirst({
where: { organizationId, role: "owner" },
columns: { userId: true },
});
return orgMember?.userId ?? null;
} catch (error) {
logger.error({ error }, "Error resolving organization owner");
return null;
}
Fix with Cubic

limit: number
) {
const { start, end } = getRangeBounds(range, appContext.timezone);
const websiteId = requireWebsiteId(appContext);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P1: Requiring websiteId here can break organization-scoped flag change queries by throwing when no website is selected, instead of still returning org-level (websiteId IS NULL) changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/insights/flag-context.ts, line 27:

<comment>Requiring `websiteId` here can break organization-scoped flag change queries by throwing when no website is selected, instead of still returning org-level (`websiteId IS NULL`) changes.</comment>

<file context>
@@ -24,16 +24,17 @@ export async function fetchFlagChangeContext(
 	limit: number
 ) {
 	const { start, end } = getRangeBounds(range, appContext.timezone);
+	const websiteId = requireWebsiteId(appContext);
 
 	const scopeCondition = appContext.organizationId
</file context>
Fix with Cubic

autumn: "identify",
autumn_stage: "getSession",
});
throw err;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P1: identifyAutumnCustomer now propagates session lookup errors, which can fail the billing request instead of returning an anonymous/null identity on transient auth errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/billing/autumn.ts, line 83:

<comment>`identifyAutumnCustomer` now propagates session lookup errors, which can fail the billing request instead of returning an anonymous/null identity on transient auth errors.</comment>

<file context>
@@ -48,69 +48,64 @@ async function stripPrivilegedBody(request: Request): Promise<Request> {
+			autumn: "identify",
+			autumn_stage: "getSession",
+		});
+		throw err;
+	}
+}
</file context>
Suggested change
throw err;
return null;
Fix with Cubic

await db
.insert(analyticsInsights)
.values(toInsert)
.onConflictDoUpdate({
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P1: The upsert conflict target makes dedupe effectively global, so post-cooldown repeats overwrite old insight rows instead of creating new history entries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/persistence.ts, line 226:

<comment>The upsert conflict target makes dedupe effectively global, so post-cooldown repeats overwrite old insight rows instead of creating new history entries.</comment>

<file context>
@@ -0,0 +1,295 @@
+		await db
+			.insert(analyticsInsights)
+			.values(toInsert)
+			.onConflictDoUpdate({
+				target: [analyticsInsights.organizationId, analyticsInsights.dedupeKey],
+				targetWhere: isNotNull(analyticsInsights.dedupeKey),
</file context>
Fix with Cubic

export function useAgentChatTransport(
chatId: string,
organizationId: string | null,
defaultWebsiteId?: string
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: The new optional/nullable scope parameters allow sending chat requests without any tenant scope (organizationId null and websiteId undefined). Add a guard so at least one scope identifier is required before building the request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/components/agent/hooks/use-agent-chat.ts, line 20:

<comment>The new optional/nullable scope parameters allow sending chat requests without any tenant scope (`organizationId` null and `websiteId` undefined). Add a guard so at least one scope identifier is required before building the request.</comment>

<file context>
@@ -6,17 +6,28 @@ import { DefaultChatTransport, type UIMessage } from "ai";
+export function useAgentChatTransport(
+	chatId: string,
+	organizationId: string | null,
+	defaultWebsiteId?: string
+) {
 	const thinking = useAtomValue(agentThinkingAtom);
</file context>
Fix with Cubic

return executeQuery(
{
projectId: appContext.websiteId,
projectId: requireWebsiteId(appContext),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: This change introduces inconsistent website ID resolution: revenue queries now use defaultWebsiteId ?? websiteId, but revenue config lookup still uses only websiteId. This can produce mismatched revenue_configured vs queried data when only defaultWebsiteId is set (or when IDs differ).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/insights/business-context.ts, line 31:

<comment>This change introduces inconsistent website ID resolution: revenue queries now use `defaultWebsiteId ?? websiteId`, but revenue config lookup still uses only `websiteId`. This can produce mismatched `revenue_configured` vs queried data when only `defaultWebsiteId` is set (or when IDs differ).</comment>

<file context>
@@ -28,7 +28,7 @@ function runQuery(
 	return executeQuery(
 		{
-			projectId: appContext.websiteId,
+			projectId: requireWebsiteId(appContext),
 			type,
 			from: range.from,
</file context>
Fix with Cubic

name: w.name,
domain: w.domain,
})),
defaultWebsiteId: ctx.defaultWebsiteId ?? ctx.websiteId ?? null,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: Derive defaultWebsiteId from the single accessible website as a fallback to keep website selection behavior consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/tools/list-websites.ts, line 18:

<comment>Derive `defaultWebsiteId` from the single accessible website as a fallback to keep website selection behavior consistent.</comment>

<file context>
@@ -0,0 +1,22 @@
+				name: w.name,
+				domain: w.domain,
+			})),
+			defaultWebsiteId: ctx.defaultWebsiteId ?? ctx.websiteId ?? null,
+			count: websites.length,
+		};
</file context>
Fix with Cubic

websiteId: context.websiteId,
websiteDomain: context.websiteDomain,
defaultWebsiteId: context.defaultWebsiteId ?? context.websiteId,
accessibleWebsites: context.accessibleWebsites,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: Passing the full accessibleWebsites array into prompt context is unbounded and can significantly bloat system prompt tokens for large orgs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/agents/analytics.ts, line 45:

<comment>Passing the full `accessibleWebsites` array into prompt context is unbounded and can significantly bloat system prompt tokens for large orgs.</comment>

<file context>
@@ -41,6 +41,9 @@ export function createConfig(
 		websiteId: context.websiteId,
 		websiteDomain: context.websiteDomain,
+		defaultWebsiteId: context.defaultWebsiteId ?? context.websiteId,
+		accessibleWebsites: context.accessibleWebsites,
+		organizationId: context.organizationId,
 		timezone: context.timezone,
</file context>
Suggested change
accessibleWebsites: context.accessibleWebsites,
accessibleWebsites: context.accessibleWebsites?.slice(0, 50),
Fix with Cubic

Comment on lines +203 to +206
const [metricSignals, funnelGoalSignals] = await Promise.all([
detectSignals(detectParams),
detectFunnelGoalSignals(detectParams),
]);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: Avoid coupling the new funnel-goal detector to the existing metric detector. A failure in the added path now drops all signals instead of just the new ones.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/generation.ts, line 203:

<comment>Avoid coupling the new funnel-goal detector to the existing metric detector. A failure in the added path now drops all signals instead of just the new ones.</comment>

<file context>
@@ -270,11 +195,18 @@ async function analyzeWebsite(params: {
 			timezone: params.config.timezone,
-		});
+		};
+		const [metricSignals, funnelGoalSignals] = await Promise.all([
+			detectSignals(detectParams),
+			detectFunnelGoalSignals(detectParams),
</file context>
Suggested change
const [metricSignals, funnelGoalSignals] = await Promise.all([
detectSignals(detectParams),
detectFunnelGoalSignals(detectParams),
]);
const metricSignals = await detectSignals(detectParams);
let funnelGoalSignals: Awaited<ReturnType<typeof detectFunnelGoalSignals>> = [];
try {
funnelGoalSignals = await detectFunnelGoalSignals(detectParams);
} catch (error) {
emitInsightsEvent("warn", "generation.funnel_goal_detection_failed", {
error: String(error),
});
}
Fix with Cubic


let defaultWebsiteId: string | null = null;
let defaultDomain: string | undefined;
if (body.websiteId) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: Select the sole accessible website as the default when no websiteId is provided, otherwise single-site chats lose memory and enrichment.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/routes/agent.ts, line 680:

<comment>Select the sole accessible website as the default when no websiteId is provided, otherwise single-site chats lose memory and enrichment.</comment>

<file context>
@@ -676,30 +667,53 @@ export const agent = new Elysia({ prefix: "/v1/agent" })
 
+					let defaultWebsiteId: string | null = null;
+					let defaultDomain: string | undefined;
+					if (body.websiteId) {
+						const match = accessibleWebsites.find(
+							(w) => w.id === body.websiteId
</file context>
Fix with Cubic

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