Files
Joshen Lim 944c5862f3 Chore/small refactors (#47740)
## Context

Just extracting the fixes which I think are applicable from this
[PR](https://github.com/supabase/supabase/pull/47695)

Main files are
- `apps/studio/hooks/analytics/useLogsQuery.tsx`
- `packages/common/auth.tsx`
- `packages/common/feature-flags.tsx`

## Changes involved
- Adjust `useLogsQuery` to accept an object as prop, rather than 4
individual params
- This one doesn't address any Sentry issues, but is just a improvement
to the function's API imo, more readable
- Adjust how user email is retrieved in `feature-flags`
- Related Sentry issue
[here](https://supabase.sentry.io/issues/7592718607/?project=5459134)
- The error is a bit vague, but Claude's attempt to fix looks alright in
general IMO
  - Minimally verified that feature flags are loading as expected still

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved log-related screens and queries for more reliable loading and
filtering across the app.
* Fixed profile and account data handling so identity details are
retrieved more consistently.
* Improved authentication handling to better recognize missing user data
and keep the app stable.
* Updated feature flag personalization to use more accurate account
information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-08 23:21:48 +08:00

212 lines
6.0 KiB
TypeScript

import dayjs from 'dayjs'
import {
type BaseQueries,
type PresetConfig,
type ReportFilterItem,
type ReportQuery,
} from './Reports.types'
import {
isUnixMicro,
unixMicroToIsoTimestamp,
} from '@/components/interfaces/Settings/Logs/Logs.utils'
import type { SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql'
import { REPORT_STATUS_CODE_COLORS } from '@/data/reports/report.utils'
import useDbQuery, { DbQueryHook } from '@/hooks/analytics/useDbQuery'
import { useLogsQuery, type LogsQueryHook } from '@/hooks/analytics/useLogsQuery'
import { getHttpStatusCodeInfo } from '@/lib/http-status-codes'
/**
* Converts a query params string to an object
*/
export const queryParamsToObject = (params: string) => {
return Object.fromEntries(new URLSearchParams(params))
}
/**
* Decodes a URI component, returning the original string if it is malformed.
*
* `decodeURIComponent` throws a `URIError` on invalid percent-encoding (e.g. a
* literal `%` such as `?discount=100%`). Request paths are user-controlled, so
* decoding inline during render can crash the page.
*/
export const safeDecodeURIComponent = (value: string) => {
try {
return decodeURIComponent(value)
} catch {
return value
}
}
export type PresetHookResult = LogsQueryHook | DbQueryHook
type PresetHooks = Record<keyof PresetConfig['queries'], () => PresetHookResult>
/**
* @deprecated
* Queries are hooks, avoid generating hooks dynamically
* Generate fetch functions instead, and pass it to a hook inside the component
*/
export const queriesFactory = <T extends string>(
queries: BaseQueries<T>,
projectRef: string
): PresetHooks => {
const hooks: PresetHooks = Object.entries<ReportQuery>(queries).reduce((acc, [k, query]) => {
if (query.queryType === 'db') {
return {
...acc,
[k]: () => useDbQuery({ sql: query.safeSql }),
}
} else {
return {
...acc,
[k]: () => useLogsQuery({ projectRef }),
}
}
}, {})
return hooks
}
export function getLogsSql(query: ReportQuery, filters: ReportFilterItem[]): SafeLogSqlFragment {
if (query.queryType !== 'logs') {
throw new Error(`Expected logs query, got ${query.queryType}`)
}
return query.safeSql(filters)
}
/**
* Formats a timestamp to a human readable format in UTC
*
* @param timestamp - The timestamp to format
* @param returnUtc - Whether to return the timestamp in UTC
* @param format - The format to use for the timestamp
* @returns The formatted timestamp string
*/
export const formatTimestamp = (
timestamp: number | string,
{ returnUtc = false, format = 'MMM D, h:mma' }: { returnUtc?: boolean; format?: string } = {}
) => {
try {
const isSeconds = String(timestamp).length === 10
const isMicroseconds = String(timestamp).length === 16
const timestampInMs = isSeconds
? Number(timestamp) * 1000
: isMicroseconds
? Number(timestamp) / 1000
: Number(timestamp)
if (returnUtc) {
return dayjs.utc(timestampInMs).format(format)
} else {
return dayjs(timestampInMs).format(format)
}
} catch (error) {
console.error(error)
return 'Invalid Date'
}
}
/**
* Extracts distinct status codes from log data rows
*/
export function extractStatusCodesFromData(data: any[]): string[] {
const statusCodes = new Set<string>()
data.forEach((item: any) => {
if (item.status_code !== undefined && item.status_code !== null) {
statusCodes.add(String(item.status_code))
}
})
return Array.from(statusCodes).sort()
}
/**
* Generates chart attributes for status codes with labels and colors
*/
export function generateStatusCodeAttributes(statusCodes: string[]) {
return statusCodes.map((code) => ({
attribute: code,
label: `${code} ${getHttpStatusCodeInfo(parseInt(code, 10)).label}`,
color: REPORT_STATUS_CODE_COLORS[code] || REPORT_STATUS_CODE_COLORS.default,
}))
}
/**
* Pivots rows of { timestamp, status_code, count } into { timestamp, [status_code]: count }
* and normalizes timestamps to ISO strings (UTC), filling missing codes with 0 per timestamp
*/
export function transformStatusCodeData(data: any[], statusCodes: string[]) {
const pivotedData = data.reduce((acc: Record<string, any>, d: any) => {
const timestamp = isUnixMicro(d.timestamp)
? unixMicroToIsoTimestamp(d.timestamp)
: dayjs.utc(d.timestamp).toISOString()
if (!acc[timestamp]) {
acc[timestamp] = { timestamp }
statusCodes.forEach((code) => {
acc[timestamp][code] = 0
})
}
const codeKey = String(d.status_code)
if (codeKey in acc[timestamp]) {
acc[timestamp][codeKey] = d.count
}
return acc
}, {})
return Object.values(pivotedData)
}
/**
* Extract distinct string values for a given field from data rows
*/
export function extractDistinctValuesFromData(data: any[], field: string): string[] {
const values = new Set<string>()
data.forEach((item: any) => {
if (item[field] !== undefined && item[field] !== null) {
values.add(String(item[field]))
}
})
return Array.from(values).sort()
}
/**
* Generates chart attributes from a list of category values
*/
export function generateCategoryAttributes(
values: string[],
labelResolver?: (v: string) => string
) {
return values.map((v) => ({
attribute: v,
label: labelResolver ? labelResolver(v) : v,
}))
}
/**
* Pivot rows of { timestamp, [categoryField], count } into { timestamp, [category]: count }
*/
export function transformCategoricalCountData(
data: any[],
categoryField: string,
categories: string[]
) {
const pivotedData = data.reduce((acc: Record<string, any>, d: any) => {
const timestamp = isUnixMicro(d.timestamp)
? unixMicroToIsoTimestamp(d.timestamp)
: dayjs.utc(d.timestamp).toISOString()
if (!acc[timestamp]) {
acc[timestamp] = { timestamp }
categories.forEach((c) => {
acc[timestamp][c] = 0
})
}
const key = String(d[categoryField])
if (key in acc[timestamp]) {
acc[timestamp][key] = d.count
}
return acc
}, {})
return Object.values(pivotedData)
}