Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/opencode/src/altimate/native/finops/bq-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,59 @@ export function interpolateBqRegion(sqlTemplate: string, bqRegion?: unknown): st
export function bqRegionFor(warehouse: string): unknown {
return Registry.getConfig(warehouse)?.location
}

/**
* Append a region-hint to a BigQuery error message when the error looks
* region-related (missing dataset, region-not-found, unknown INFORMATION_SCHEMA
* view). The hint tells the user which region the query ran against and how to
* change it — the root cause on non-US projects is almost always an unset or
* mis-typed `location` on the warehouse config.
*
* Returns the original error string unchanged when no region signal is present,
* so non-region failures (bad syntax, auth, quota) stay legible.
*/
export function augmentBqError(error: unknown, sanitizedRegion: string): string {
const msg = String(error)
// Signals we treat as region-related:
// 1. A backticked region-qualified INFORMATION_SCHEMA reference, which is
// what BigQuery emits when the dataset can't be resolved in the queried
// region: `region-eu.INFORMATION_SCHEMA.JOBS not found`.
// 2. A "Not found: ... INFORMATION_SCHEMA" line — covers cases where BQ
// mentions the view name but not the region prefix.
// 3. The literal "Not found: Dataset region-..." form some BQ surfaces use.
// Anchored to these specific shapes so unrelated text containing "region-"
// (e.g. "region-based routing denied", "multi-region-aware feature disabled")
// does NOT trigger the BigQuery-connection hint.
const hasRegionSignal =
/region-[a-z0-9]+[a-z0-9-]*\.INFORMATION_SCHEMA/i.test(msg) ||
/Not\s+found:.*INFORMATION_SCHEMA/i.test(msg) ||
/Not\s+found:\s*Dataset\s+region-[a-z0-9]+/i.test(msg)
if (!hasRegionSignal) return msg
// Idempotent — don't append twice if this helper runs in nested catches
if (msg.includes('set "location" on the BigQuery connection')) return msg
return `${msg} (queried region-${sanitizedRegion}; set "location" on the BigQuery connection to change this)`
}

/**
* Detect errors raised by BigQuery when the caller lacks the org-level
* permissions required by views such as `INFORMATION_SCHEMA.TABLE_STORAGE`
* (used by `finops_unused_resources`). Most project-scoped service accounts
* lack `bigquery.resourceAdmin` at the org and hit this path first.
*/
export function isBqPermissionError(error: unknown): boolean {
const msg = String(error).toLowerCase()
// Word-boundary regex on `403` so `4031`, `40322`, `port 4030`, and similar
// numeric prefixes don't false-positive into the permission-error branch.
// The two `accessdenied` / `access denied` checks are intentional and not
// redundant — Google's BigQuery SDKs emit `AccessDenied` (camelCase),
// which `.toLowerCase()` collapses to `accessdenied` (no space) and would
// not be caught by the `access denied` substring check.
return (
msg.includes("permission denied") ||
msg.includes("access denied") ||
msg.includes("accessdenied") ||
msg.includes("bigquery.resourceadmin") ||
msg.includes("iam permission") ||
/\b403\b/.test(msg)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 25, 2026

Choose a reason for hiding this comment

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

P2: isBqPermissionError is too broad: matching any 403 can misclassify non-permission BigQuery errors as permission issues.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/native/finops/bq-utils.ts, line 111:

<comment>`isBqPermissionError` is too broad: matching any `403` can misclassify non-permission BigQuery errors as permission issues.</comment>

<file context>
@@ -55,3 +55,59 @@ export function interpolateBqRegion(sqlTemplate: string, bqRegion?: unknown): st
+    msg.includes("accessdenied") ||
+    msg.includes("bigquery.resourceadmin") ||
+    msg.includes("iam permission") ||
+    /\b403\b/.test(msg)
+  )
+}
</file context>
Fix with Cubic

)
}
14 changes: 11 additions & 3 deletions packages/opencode/src/altimate/native/finops/credit-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import * as Registry from "../connections/registry"
import { bqRegionFor, interpolateBqRegion } from "./bq-utils"
import { augmentBqError, bqRegionFor, interpolateBqRegion, sanitizeBqRegion } from "./bq-utils"
import type {
CreditAnalysisParams,
CreditAnalysisResult,
Expand Down Expand Up @@ -306,6 +306,7 @@ export async function analyzeCredits(params: CreditAnalysisParams): Promise<Cred
const days = params.days ?? 30
const limit = params.limit ?? 50
const bqRegion = whType === "bigquery" ? bqRegionFor(params.warehouse) : undefined
const sanitisedBqRegion = whType === "bigquery" ? sanitizeBqRegion(bqRegion) : undefined

const dailyBuilt = buildCreditUsageSql(whType, days, limit, params.warehouse_filter, bqRegion)
const summaryBuilt = buildCreditSummarySql(whType, days, bqRegion)
Expand All @@ -319,6 +320,7 @@ export async function analyzeCredits(params: CreditAnalysisParams): Promise<Cred
days_analyzed: days,
recommendations: [],
error: `Credit analysis is not available for ${whType} warehouses.`,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}

Expand All @@ -339,6 +341,7 @@ export async function analyzeCredits(params: CreditAnalysisParams): Promise<Cred
total_credits: Math.round(totalCredits * 10000) / 10000,
days_analyzed: days,
recommendations,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
} catch (e) {
return {
Expand All @@ -348,7 +351,8 @@ export async function analyzeCredits(params: CreditAnalysisParams): Promise<Cred
total_credits: 0,
days_analyzed: days,
recommendations: [],
error: String(e),
error: sanitisedBqRegion ? augmentBqError(e, sanitisedBqRegion) : String(e),
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}
}
Expand All @@ -358,6 +362,7 @@ export async function getExpensiveQueries(params: ExpensiveQueriesParams): Promi
const days = params.days ?? 7
const limit = params.limit ?? 20
const bqRegion = whType === "bigquery" ? bqRegionFor(params.warehouse) : undefined
const sanitisedBqRegion = whType === "bigquery" ? sanitizeBqRegion(bqRegion) : undefined

const built = buildExpensiveSql(whType, days, limit, bqRegion)
if (!built) {
Expand All @@ -367,6 +372,7 @@ export async function getExpensiveQueries(params: ExpensiveQueriesParams): Promi
query_count: 0,
days_analyzed: days,
error: `Expensive query analysis is not available for ${whType} warehouses.`,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}

Expand All @@ -380,14 +386,16 @@ export async function getExpensiveQueries(params: ExpensiveQueriesParams): Promi
queries,
query_count: queries.length,
days_analyzed: days,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
} catch (e) {
return {
success: false,
queries: [],
query_count: 0,
days_analyzed: days,
error: String(e),
error: sanitisedBqRegion ? augmentBqError(e, sanitisedBqRegion) : String(e),
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions packages/opencode/src/altimate/native/finops/query-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import * as Registry from "../connections/registry"
import { bqRegionFor, interpolateBqRegion, sanitizeBqRegion } from "./bq-utils"
import { augmentBqError, bqRegionFor, interpolateBqRegion, sanitizeBqRegion } from "./bq-utils"
import type { QueryHistoryParams, QueryHistoryResult } from "../types"

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -214,6 +214,7 @@ export async function getQueryHistory(params: QueryHistoryParams): Promise<Query
const days = params.days ?? 7
const limit = params.limit ?? 100
const bqRegion = whType === "bigquery" ? bqRegionFor(params.warehouse) : undefined
const sanitisedBqRegion = whType === "bigquery" ? sanitizeBqRegion(bqRegion) : undefined

const built = buildHistoryQuery(whType, days, limit, params.user, params.warehouse_filter, bqRegion)
if (!built) {
Expand All @@ -222,6 +223,7 @@ export async function getQueryHistory(params: QueryHistoryParams): Promise<Query
queries: [],
summary: {},
error: `Query history is not available for ${whType} warehouses.`,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}

Expand Down Expand Up @@ -255,13 +257,15 @@ export async function getQueryHistory(params: QueryHistoryParams): Promise<Query
queries,
summary,
warehouse_type: whType,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
} catch (e) {
return {
success: false,
queries: [],
summary: {},
error: String(e),
error: sanitisedBqRegion ? augmentBqError(e, sanitisedBqRegion) : String(e),
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions packages/opencode/src/altimate/native/finops/role-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import * as Registry from "../connections/registry"
import { bqRegionFor, interpolateBqRegion } from "./bq-utils"
import { augmentBqError, bqRegionFor, interpolateBqRegion, sanitizeBqRegion } from "./bq-utils"
import type {
RoleGrantsParams,
RoleGrantsResult,
Expand Down Expand Up @@ -167,6 +167,7 @@ export async function queryGrants(params: RoleGrantsParams): Promise<RoleGrantsR
const whType = getWhType(params.warehouse)
const limit = params.limit ?? 100
const bqRegion = whType === "bigquery" ? bqRegionFor(params.warehouse) : undefined
const sanitisedBqRegion = whType === "bigquery" ? sanitizeBqRegion(bqRegion) : undefined

const built = buildGrantsSql(whType, params.role, params.object_name, limit, bqRegion)
if (!built) {
Expand All @@ -176,6 +177,7 @@ export async function queryGrants(params: RoleGrantsParams): Promise<RoleGrantsR
grant_count: 0,
privilege_summary: {},
error: `Role/access queries are not available for ${whType} warehouses.`,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}

Expand All @@ -195,14 +197,16 @@ export async function queryGrants(params: RoleGrantsParams): Promise<RoleGrantsR
grants,
grant_count: grants.length,
privilege_summary: privilegeSummary,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
} catch (e) {
return {
success: false,
grants: [],
grant_count: 0,
privilege_summary: {},
error: String(e),
error: sanitisedBqRegion ? augmentBqError(e, sanitisedBqRegion) : String(e),
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}
}
Expand Down
29 changes: 25 additions & 4 deletions packages/opencode/src/altimate/native/finops/unused-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
*/

import * as Registry from "../connections/registry"
import { bqRegionFor, interpolateBqRegion } from "./bq-utils"
import {
augmentBqError,
bqRegionFor,
interpolateBqRegion,
isBqPermissionError,
sanitizeBqRegion,
} from "./bq-utils"
import type {
UnusedResourcesParams,
UnusedResourcesResult,
Expand Down Expand Up @@ -145,6 +151,8 @@ export async function findUnusedResources(params: UnusedResourcesParams): Promis
const whType = getWhType(params.warehouse)
const days = params.days ?? 30
const limit = params.limit ?? 50
const bqRegion = whType === "bigquery" ? bqRegionFor(params.warehouse) : undefined
const sanitisedBqRegion = whType === "bigquery" ? sanitizeBqRegion(bqRegion) : undefined

if (!["snowflake", "bigquery", "databricks"].includes(whType)) {
return {
Expand Down Expand Up @@ -187,11 +195,22 @@ export async function findUnusedResources(params: UnusedResourcesParams): Promis
}
} else if (whType === "bigquery") {
try {
const sql = interpolateBqRegion(BIGQUERY_UNUSED_TABLES_SQL, bqRegionFor(params.warehouse))
const sql = interpolateBqRegion(BIGQUERY_UNUSED_TABLES_SQL, bqRegion)
const result = await connector.execute(sql, limit, [days, limit])
unusedTables = rowsToRecords(result)
} catch (e) {
errors.push(`Could not query unused tables: ${e}`)
// TABLE_STORAGE is an org-level view. Most project-scoped service
// accounts lack bigquery.resourceAdmin at the org and hit 403 here —
// point them at the specific permission rather than a raw SQL error.
if (isBqPermissionError(e)) {
errors.push(
`Could not query unused tables: BigQuery returned a permission error for INFORMATION_SCHEMA.TABLE_STORAGE. ` +
`This view is org-level and requires bigquery.resourceAdmin (or equivalent) at the organisation. ` +
`Project-scoped service accounts typically can't read it. Raw error: ${e}`,
)
} else {
errors.push(`Could not query unused tables: ${augmentBqError(e, sanitisedBqRegion!)}`)
}
}
} else if (whType === "databricks") {
try {
Expand Down Expand Up @@ -220,6 +239,7 @@ export async function findUnusedResources(params: UnusedResourcesParams): Promis
},
days_analyzed: days,
error: errors.length > 0 ? errors.join("; ") : undefined,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
} catch (e) {
return {
Expand All @@ -228,7 +248,8 @@ export async function findUnusedResources(params: UnusedResourcesParams): Promis
idle_warehouses: [],
summary: {},
days_analyzed: days,
error: String(e),
error: sanitisedBqRegion ? augmentBqError(e, sanitisedBqRegion) : String(e),
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import * as Registry from "../connections/registry"
import { bqRegionFor, interpolateBqRegion } from "./bq-utils"
import { augmentBqError, bqRegionFor, interpolateBqRegion, sanitizeBqRegion } from "./bq-utils"
import type {
WarehouseAdvisorParams,
WarehouseAdvisorResult,
Expand Down Expand Up @@ -223,6 +223,7 @@ export async function adviseWarehouse(params: WarehouseAdvisorParams): Promise<W
const whType = getWhType(params.warehouse)
const days = params.days ?? 14
const bqRegion = whType === "bigquery" ? bqRegionFor(params.warehouse) : undefined
const sanitisedBqRegion = whType === "bigquery" ? sanitizeBqRegion(bqRegion) : undefined

const loadSql = buildLoadSql(whType, days, bqRegion)
const sizingSql = buildSizingSql(whType, days, bqRegion)
Expand All @@ -235,6 +236,7 @@ export async function adviseWarehouse(params: WarehouseAdvisorParams): Promise<W
recommendations: [],
days_analyzed: days,
error: `Warehouse sizing advice is not available for ${whType} warehouses.`,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}

Expand Down Expand Up @@ -276,6 +278,7 @@ export async function adviseWarehouse(params: WarehouseAdvisorParams): Promise<W
warehouse_performance: sizingData,
recommendations,
days_analyzed: days,
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
} catch (e) {
return {
Expand All @@ -284,7 +287,8 @@ export async function adviseWarehouse(params: WarehouseAdvisorParams): Promise<W
warehouse_performance: [],
recommendations: [],
days_analyzed: days,
error: String(e),
error: sanitisedBqRegion ? augmentBqError(e, sanitisedBqRegion) : String(e),
...(sanitisedBqRegion && { bq_region: sanitisedBqRegion }),
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/altimate/native/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,8 @@ export interface QueryHistoryResult {
queries: Record<string, unknown>[]
summary: Record<string, unknown>
warehouse_type?: string
/** For BigQuery warehouses: the sanitised region the query was executed against. */
bq_region?: string
error?: string
}

Expand All @@ -553,6 +555,8 @@ export interface CreditAnalysisResult {
total_credits: number
days_analyzed: number
recommendations: Record<string, unknown>[]
/** For BigQuery warehouses: the sanitised region the query was executed against. */
bq_region?: string
error?: string
}

Expand All @@ -569,6 +573,8 @@ export interface ExpensiveQueriesResult {
queries: Record<string, unknown>[]
query_count: number
days_analyzed: number
/** For BigQuery warehouses: the sanitised region the query was executed against. */
bq_region?: string
error?: string
}

Expand All @@ -585,6 +591,8 @@ export interface WarehouseAdvisorResult {
warehouse_performance: Record<string, unknown>[]
recommendations: Record<string, unknown>[]
days_analyzed: number
/** For BigQuery warehouses: the sanitised region the query was executed against. */
bq_region?: string
error?: string
}

Expand All @@ -602,6 +610,8 @@ export interface UnusedResourcesResult {
idle_warehouses: Record<string, unknown>[]
summary: Record<string, unknown>
days_analyzed: number
/** For BigQuery warehouses: the sanitised region the query was executed against. */
bq_region?: string
error?: string
}

Expand All @@ -619,6 +629,8 @@ export interface RoleGrantsResult {
grants: Record<string, unknown>[]
grant_count: number
privilege_summary: Record<string, number>
/** For BigQuery warehouses: the sanitised region the query was executed against. */
bq_region?: string
error?: string
}

Expand Down
Loading
Loading