Reports.utils.tsx195 lines · main
1import dayjs from 'dayjs'
2
3import {
4 type BaseQueries,
5 type PresetConfig,
6 type ReportFilterItem,
7 type ReportQuery,
8} from './Reports.types'
9import {
10 isUnixMicro,
11 unixMicroToIsoTimestamp,
12} from '@/components/interfaces/Settings/Logs/Logs.utils'
13import { REPORT_STATUS_CODE_COLORS } from '@/data/reports/report.utils'
14import useDbQuery, { DbQueryHook } from '@/hooks/analytics/useDbQuery'
15import useLogsQuery, { LogsQueryHook } from '@/hooks/analytics/useLogsQuery'
16import { getHttpStatusCodeInfo } from '@/lib/http-status-codes'
17
18/**
19 * Converts a query params string to an object
20 */
21export const queryParamsToObject = (params: string) => {
22 return Object.fromEntries(new URLSearchParams(params))
23}
24
25export type PresetHookResult = LogsQueryHook | DbQueryHook
26type PresetHooks = Record<keyof PresetConfig['queries'], () => PresetHookResult>
27/**
28 * @deprecated
29 * Queries are hooks, avoid generating hooks dynamically
30 * Generate fetch functions instead, and pass it to a hook inside the component
31 */
32export const queriesFactory = <T extends string>(
33 queries: BaseQueries<T>,
34 projectRef: string
35): PresetHooks => {
36 const hooks: PresetHooks = Object.entries<ReportQuery>(queries).reduce((acc, [k, query]) => {
37 if (query.queryType === 'db') {
38 return {
39 ...acc,
40 [k]: () => useDbQuery({ sql: query.safeSql }),
41 }
42 } else {
43 return {
44 ...acc,
45 [k]: () => useLogsQuery(projectRef),
46 }
47 }
48 }, {})
49 return hooks
50}
51
52export function getLogsSql(query: ReportQuery, filters: ReportFilterItem[]): string {
53 if (query.queryType !== 'logs') {
54 throw new Error(`Expected logs query, got ${query.queryType}`)
55 }
56 return query.sql(filters)
57}
58
59/**
60 * Formats a timestamp to a human readable format in UTC
61 *
62 * @param timestamp - The timestamp to format
63 * @param returnUtc - Whether to return the timestamp in UTC
64 * @param format - The format to use for the timestamp
65 * @returns The formatted timestamp string
66 */
67export const formatTimestamp = (
68 timestamp: number | string,
69 { returnUtc = false, format = 'MMM D, h:mma' }: { returnUtc?: boolean; format?: string } = {}
70) => {
71 try {
72 const isSeconds = String(timestamp).length === 10
73 const isMicroseconds = String(timestamp).length === 16
74
75 const timestampInMs = isSeconds
76 ? Number(timestamp) * 1000
77 : isMicroseconds
78 ? Number(timestamp) / 1000
79 : Number(timestamp)
80
81 if (returnUtc) {
82 return dayjs.utc(timestampInMs).format(format)
83 } else {
84 return dayjs(timestampInMs).format(format)
85 }
86 } catch (error) {
87 console.error(error)
88 return 'Invalid Date'
89 }
90}
91
92/**
93 * Extracts distinct status codes from log data rows
94 */
95export function extractStatusCodesFromData(data: any[]): string[] {
96 const statusCodes = new Set<string>()
97
98 data.forEach((item: any) => {
99 if (item.status_code !== undefined && item.status_code !== null) {
100 statusCodes.add(String(item.status_code))
101 }
102 })
103
104 return Array.from(statusCodes).sort()
105}
106
107/**
108 * Generates chart attributes for status codes with labels and colors
109 */
110export function generateStatusCodeAttributes(statusCodes: string[]) {
111 return statusCodes.map((code) => ({
112 attribute: code,
113 label: `${code} ${getHttpStatusCodeInfo(parseInt(code, 10)).label}`,
114 color: REPORT_STATUS_CODE_COLORS[code] || REPORT_STATUS_CODE_COLORS.default,
115 }))
116}
117
118/**
119 * Pivots rows of { timestamp, status_code, count } into { timestamp, [status_code]: count }
120 * and normalizes timestamps to ISO strings (UTC), filling missing codes with 0 per timestamp
121 */
122export function transformStatusCodeData(data: any[], statusCodes: string[]) {
123 const pivotedData = data.reduce((acc: Record<string, any>, d: any) => {
124 const timestamp = isUnixMicro(d.timestamp)
125 ? unixMicroToIsoTimestamp(d.timestamp)
126 : dayjs.utc(d.timestamp).toISOString()
127 if (!acc[timestamp]) {
128 acc[timestamp] = { timestamp }
129 statusCodes.forEach((code) => {
130 acc[timestamp][code] = 0
131 })
132 }
133 const codeKey = String(d.status_code)
134 if (codeKey in acc[timestamp]) {
135 acc[timestamp][codeKey] = d.count
136 }
137 return acc
138 }, {})
139
140 return Object.values(pivotedData)
141}
142
143/**
144 * Extract distinct string values for a given field from data rows
145 */
146export function extractDistinctValuesFromData(data: any[], field: string): string[] {
147 const values = new Set<string>()
148 data.forEach((item: any) => {
149 if (item[field] !== undefined && item[field] !== null) {
150 values.add(String(item[field]))
151 }
152 })
153 return Array.from(values).sort()
154}
155
156/**
157 * Generates chart attributes from a list of category values
158 */
159export function generateCategoryAttributes(
160 values: string[],
161 labelResolver?: (v: string) => string
162) {
163 return values.map((v) => ({
164 attribute: v,
165 label: labelResolver ? labelResolver(v) : v,
166 }))
167}
168
169/**
170 * Pivot rows of { timestamp, [categoryField], count } into { timestamp, [category]: count }
171 */
172export function transformCategoricalCountData(
173 data: any[],
174 categoryField: string,
175 categories: string[]
176) {
177 const pivotedData = data.reduce((acc: Record<string, any>, d: any) => {
178 const timestamp = isUnixMicro(d.timestamp)
179 ? unixMicroToIsoTimestamp(d.timestamp)
180 : dayjs.utc(d.timestamp).toISOString()
181 if (!acc[timestamp]) {
182 acc[timestamp] = { timestamp }
183 categories.forEach((c) => {
184 acc[timestamp][c] = 0
185 })
186 }
187 const key = String(d[categoryField])
188 if (key in acc[timestamp]) {
189 acc[timestamp][key] = d.count
190 }
191 return acc
192 }, {})
193
194 return Object.values(pivotedData)
195}