Release: pricing overhaul, insights pipeline improvements#463
Release: pricing overhaul, insights pipeline improvements#463izadoesdev wants to merge 5 commits into
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Greptile SummaryThis 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
Confidence Score: 4/5Safe 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
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]
|
| } | ||
| } | ||
|
|
||
| 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
9 issues found across 49 files
Confidence score: 2/5
- Merge risk is high because
apps/insights/src/prompts.tscombines 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 inpackages/ai/src/ai/schemas/smart-insights-output.tscan reject valid structured action payloads. packages/db/src/clickhouse/sql-validation.tshas 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, andpackages/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
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
| 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); | ||
|
|
There was a problem hiding this comment.
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>
| 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); |
| - 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.${ |
There was a problem hiding this comment.
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>
| ]), | ||
| label: z.string().describe("Button label (e.g. 'Fix goal target')"), | ||
| params: z | ||
| .record(z.string(), z.string()) |
There was a problem hiding this comment.
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>
| } | ||
| } | ||
|
|
||
| const QUALIFIED_COLUMN = |
There was a problem hiding this comment.
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>
| } | ||
| stopText?: string | ||
| ): Promise<void> { | ||
| if (pending.trim()) { |
There was a problem hiding this comment.
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>
| const curVal = cur?.p75 ?? 0; | ||
| const prevVal = prev?.p75 ?? 0; | ||
| if ( | ||
| curVal === 0 || | ||
| prevVal === 0 || | ||
| (cur?.samples ?? 0) < 5 || | ||
| (prev?.samples ?? 0) < 5 | ||
| ) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
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>
| 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; | |
| } |
| "dashboard", | ||
| ], | ||
| domain: context.websiteDomain, | ||
| organizationId: context.organizationId, |
There was a problem hiding this comment.
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>
| "analytics.link_visits": "client_id", | ||
| }; | ||
|
|
||
| export const AGENT_TABLE_COLUMNS: Readonly< |
There was a problem hiding this comment.
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.)
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>
| 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< |
|
|
||
| const aliasToTable = new Map<string, string>(); | ||
| for (const ref of refs) { | ||
| if (!cteNames.has(ref.name) && ref.name in AGENT_TABLE_COLUMNS) { |
There was a problem hiding this comment.
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>
| 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(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.
|
The latest updates on your projects. Learn more about Unkey Deploy
|
| userId, | ||
| websiteId: body.websiteId, | ||
| websiteDomain: domain, | ||
| organizationId: organizationId ?? undefined, |
There was a problem hiding this comment.
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
| const orgMember = await db.query.member.findFirst({ | ||
| where: { organizationId, role: "owner" }, | ||
| columns: { userId: true }, | ||
| }); | ||
| return orgMember?.userId ?? null; |
There was a problem hiding this comment.
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>
| 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; | |
| } |
| limit: number | ||
| ) { | ||
| const { start, end } = getRangeBounds(range, appContext.timezone); | ||
| const websiteId = requireWebsiteId(appContext); |
There was a problem hiding this comment.
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>
| autumn: "identify", | ||
| autumn_stage: "getSession", | ||
| }); | ||
| throw err; |
There was a problem hiding this comment.
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>
| throw err; | |
| return null; |
| await db | ||
| .insert(analyticsInsights) | ||
| .values(toInsert) | ||
| .onConflictDoUpdate({ |
There was a problem hiding this comment.
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>
| export function useAgentChatTransport( | ||
| chatId: string, | ||
| organizationId: string | null, | ||
| defaultWebsiteId?: string |
There was a problem hiding this comment.
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>
| return executeQuery( | ||
| { | ||
| projectId: appContext.websiteId, | ||
| projectId: requireWebsiteId(appContext), |
There was a problem hiding this comment.
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>
| name: w.name, | ||
| domain: w.domain, | ||
| })), | ||
| defaultWebsiteId: ctx.defaultWebsiteId ?? ctx.websiteId ?? null, |
There was a problem hiding this comment.
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>
| websiteId: context.websiteId, | ||
| websiteDomain: context.websiteDomain, | ||
| defaultWebsiteId: context.defaultWebsiteId ?? context.websiteId, | ||
| accessibleWebsites: context.accessibleWebsites, |
There was a problem hiding this comment.
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>
| accessibleWebsites: context.accessibleWebsites, | |
| accessibleWebsites: context.accessibleWebsites?.slice(0, 50), |
| const [metricSignals, funnelGoalSignals] = await Promise.all([ | ||
| detectSignals(detectParams), | ||
| detectFunnelGoalSignals(detectParams), | ||
| ]); |
There was a problem hiding this comment.
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>
| 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), | |
| }); | |
| } | |
|
|
||
| let defaultWebsiteId: string | null = null; | ||
| let defaultDomain: string | undefined; | ||
| if (body.websiteId) { |
There was a problem hiding this comment.
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>
Summary
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 routingSummary 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
/agentroute, org-scoped chats, website “@mentions”, and alist_websitestool for multi-site queries; no‑websites empty state.Refactors
getOAuthToken/createCachedTokenFnwith TTL and org scoping; used by GitHub and Search Console;AgentContextand app context now includeorganizationIdand accessible websites.createToolkitto assemble analytics/investigation/mutation tools; addsearch-console,list_websites,investigation-tools; extract prompts; bumpaito^6.0.188.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.