mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-03 00:52:32 +00:00
The five days of upstream evolution since the 2026-08-01 import land through bin/debrand: new query schema, billing counters, ducklake compaction, the desktop sandbox default, capture evolution. Ours' invariants hold everywhere they met it: IAM-native auth only, no ee, no upsell surfaces, one sessions model, org-is-the-tenant, v1-first serving, fail-closed capture. Crates and workflows the fork retired stay retired; the Lemon prefix finishes dissolving into elements.
455 lines
18 KiB
TypeScript
455 lines
18 KiB
TypeScript
import { deepEqual as equal } from 'fast-equals'
|
|
import { MakeLogicType, actions, connect, kea, listeners, path, reducers } from 'kea'
|
|
import { router, urlToAction } from 'kea-router'
|
|
|
|
import { syncSearchParams, updateSearchParams } from '@hanzo/products-error-tracking/frontend/utils'
|
|
|
|
import { DEFAULT_UNIVERSAL_GROUP_FILTER } from 'lib/components/UniversalFilters/constants'
|
|
import { trackedActionToUrl } from 'lib/logic/scenes/trackedActionToUrl'
|
|
import { parseTagsFilter } from 'lib/utils/url'
|
|
import { sqlEditorLogic } from 'scenes/data-warehouse/editor/sqlEditorLogic'
|
|
import { SQLEditorMode } from 'scenes/data-warehouse/editor/sqlEditorModes'
|
|
import { Params } from 'scenes/sceneTypes'
|
|
|
|
import {
|
|
DEFAULT_ORDER_BY,
|
|
DEFAULT_VIEW_MODE,
|
|
logsViewerConfigLogic,
|
|
LogsViewerViewMode,
|
|
} from 'products/logs/frontend/components/LogsViewer/config/logsViewerConfigLogic'
|
|
import { LogsViewerFilters } from 'products/logs/frontend/components/LogsViewer/config/types'
|
|
import {
|
|
DEFAULT_INITIAL_LOGS_LIMIT,
|
|
logsViewerDataLogic,
|
|
} from 'products/logs/frontend/components/LogsViewer/data/logsViewerDataLogic'
|
|
import { facetRailLogic } from 'products/logs/frontend/components/LogsViewer/FacetRail/facetRailLogic'
|
|
import { logsFilterHistoryLogic } from 'products/logs/frontend/components/LogsViewer/Filters/logsFilterHistoryLogic'
|
|
import {
|
|
DEFAULT_DATE_RANGE,
|
|
DEFAULT_SERVICE_NAMES,
|
|
DEFAULT_SEVERITY_LEVELS,
|
|
isValidSeverityLevel,
|
|
logsViewerFiltersLogic,
|
|
} from 'products/logs/frontend/components/LogsViewer/Filters/logsViewerFiltersLogic'
|
|
import { logDetailsModalLogic } from 'products/logs/frontend/components/LogsViewer/LogDetailsModal/logDetailsModalLogic'
|
|
import { logsViewerLogic } from 'products/logs/frontend/components/LogsViewer/logsViewerLogic'
|
|
|
|
import type { LogMessage } from '../../../frontend/src/queries/schema/schema-general'
|
|
import type { LogsOrderBy } from './types'
|
|
|
|
export const getLogsSqlEditorTabId = (id: string): string => `logs-sql-editor-${id}`
|
|
|
|
// Scope the viewer id (and so its persisted state: pinned logs, filters, config) per project.
|
|
// A static id would persist across projects in the same browser, leaking one project's pinned log payloads into another.
|
|
export const LOGS_SCENE_VIEWER_ID = `logs-scene-${window.INSIGHTS_APP_CONTEXT?.current_team?.id ?? 'unknown'}`
|
|
|
|
const VALID_VIEW_MODES: LogsViewerViewMode[] = ['logs', 'patterns', 'group']
|
|
|
|
export type LogsSceneActiveTab =
|
|
| 'viewer'
|
|
| 'services'
|
|
| 'alerts'
|
|
| 'anomalies'
|
|
| 'sql'
|
|
| 'transformations'
|
|
| 'configuration'
|
|
const VALID_ACTIVE_TABS: LogsSceneActiveTab[] = [
|
|
'viewer',
|
|
'services',
|
|
'alerts',
|
|
'anomalies',
|
|
'sql',
|
|
'transformations',
|
|
'configuration',
|
|
]
|
|
export const DEFAULT_ACTIVE_TAB: LogsSceneActiveTab = 'viewer'
|
|
|
|
const resolveActiveTabFromParams = (params: Params): LogsSceneActiveTab | null => {
|
|
if (typeof params.alertId === 'string' && params.alertId.length > 0) {
|
|
return 'alerts'
|
|
}
|
|
if (typeof params.activeTab === 'string' && VALID_ACTIVE_TABS.includes(params.activeTab as LogsSceneActiveTab)) {
|
|
return params.activeTab as LogsSceneActiveTab
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface logsSceneLogicValues {
|
|
facetNameSearch: string // facetRailLogic
|
|
orderBy: LogsOrderBy // logsViewerConfigLogic
|
|
viewMode: LogsViewerViewMode // logsViewerConfigLogic
|
|
initialLogsLimit: number | null // logsViewerDataLogic
|
|
filters: LogsViewerFilters // logsViewerFiltersLogic
|
|
utcDateRange: {
|
|
date_from: string | null | undefined
|
|
date_to: string | null | undefined
|
|
explicitDate: boolean | null | undefined
|
|
} // logsViewerFiltersLogic
|
|
linkToLogId: string | null // logsViewerLogic
|
|
activeTab: LogsSceneActiveTab
|
|
expandedAttributeBreaksdowns: string[]
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface logsSceneLogicActions {
|
|
setFacetNameSearch: (search: string) => {
|
|
search: string
|
|
} // facetRailLogic
|
|
closeLogDetails: () => {
|
|
value: true
|
|
} // logDetailsModalLogic
|
|
pushToFilterHistory: (filters: LogsViewerFilters) => {
|
|
filters: LogsViewerFilters
|
|
} // logsFilterHistoryLogic
|
|
setOrderBy: (
|
|
orderBy: LogsOrderBy,
|
|
source?: 'header' | 'toolbar' | undefined
|
|
) => {
|
|
orderBy: LogsOrderBy
|
|
source: 'header' | 'toolbar'
|
|
} // logsViewerConfigLogic
|
|
setViewMode: (viewMode: LogsViewerViewMode) => {
|
|
viewMode: LogsViewerViewMode
|
|
} // logsViewerConfigLogic
|
|
fetchLogsSuccess: (
|
|
logs: LogMessage[],
|
|
payload?: any
|
|
) => {
|
|
logs: LogMessage[]
|
|
payload?: any
|
|
} // logsViewerDataLogic
|
|
handleQueryChange: (
|
|
filterType: string,
|
|
extraProps?: Record<string, unknown> | undefined
|
|
) => {
|
|
extraProps: Record<string, unknown> | undefined
|
|
filterType: string
|
|
} // logsViewerDataLogic
|
|
setInitialLogsLimit: (initialLogsLimit: number | null) => {
|
|
initialLogsLimit: number | null
|
|
} // logsViewerDataLogic
|
|
setFilters: (
|
|
filters: Partial<LogsViewerFilters>,
|
|
pushToHistory?: boolean | undefined
|
|
) => {
|
|
filters: Partial<LogsViewerFilters>
|
|
pushToHistory: boolean
|
|
} // logsViewerFiltersLogic
|
|
clearLinkToLogId: () => {
|
|
value: true
|
|
} // logsViewerLogic
|
|
setLinkToLogId: (linkToLogId: string | null) => {
|
|
linkToLogId: string | null
|
|
} // logsViewerLogic
|
|
keepSqlEditorMounted: (editorTabId: string) => {
|
|
editorTabId: string
|
|
}
|
|
setActiveTab: (activeTab: LogsSceneActiveTab) => {
|
|
activeTab: LogsSceneActiveTab
|
|
}
|
|
setExpandedAttributeBreaksdowns: (expandedAttributeBreaksdowns: string[]) => {
|
|
expandedAttributeBreaksdowns: string[]
|
|
}
|
|
syncUrl: () => {
|
|
value: true
|
|
}
|
|
toggleAttributeBreakdown: (key: string) => {
|
|
key: string
|
|
}
|
|
}
|
|
|
|
export type logsSceneLogicType = MakeLogicType<logsSceneLogicValues, logsSceneLogicActions>
|
|
|
|
export const logsSceneLogic = kea<logsSceneLogicType>([
|
|
path(['products', 'logs', 'frontend', 'logsSceneLogic']),
|
|
connect(() => ({
|
|
actions: [
|
|
logsViewerFiltersLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['setFilters'],
|
|
logsFilterHistoryLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['pushToFilterHistory'],
|
|
logsViewerConfigLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['setOrderBy', 'setViewMode'],
|
|
logsViewerDataLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['setInitialLogsLimit', 'fetchLogsSuccess', 'handleQueryChange'],
|
|
logsViewerLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['setLinkToLogId', 'clearLinkToLogId'],
|
|
logDetailsModalLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['closeLogDetails'],
|
|
facetRailLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['setFacetNameSearch'],
|
|
],
|
|
values: [
|
|
logsViewerFiltersLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['filters', 'utcDateRange'],
|
|
logsViewerConfigLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['orderBy', 'viewMode'],
|
|
logsViewerDataLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['initialLogsLimit'],
|
|
logsViewerLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['linkToLogId'],
|
|
facetRailLogic({ id: LOGS_SCENE_VIEWER_ID }),
|
|
['facetNameSearch'],
|
|
],
|
|
})),
|
|
urlToAction(({ actions, values, cache }) => {
|
|
const urlToAction = (_: any, params: Params): void => {
|
|
if (cache.isSyncingUrl) {
|
|
return
|
|
}
|
|
const requestedTab = resolveActiveTabFromParams(params)
|
|
if (requestedTab && requestedTab !== values.activeTab) {
|
|
actions.setActiveTab(requestedTab)
|
|
}
|
|
|
|
const filtersFromUrl: Partial<LogsViewerFilters> = {}
|
|
let hasFilterChanges = false
|
|
|
|
if (params.dateRange) {
|
|
try {
|
|
const dateRange =
|
|
typeof params.dateRange === 'string' ? JSON.parse(params.dateRange) : params.dateRange
|
|
if (!equal(dateRange, values.filters.dateRange)) {
|
|
filtersFromUrl.dateRange = dateRange
|
|
hasFilterChanges = true
|
|
}
|
|
} catch {
|
|
// Ignore malformed dateRange JSON in URL
|
|
}
|
|
}
|
|
if (params.filterGroup) {
|
|
try {
|
|
const filterGroup =
|
|
typeof params.filterGroup === 'string' ? JSON.parse(params.filterGroup) : params.filterGroup
|
|
if (!equal(filterGroup, values.filters.filterGroup)) {
|
|
filtersFromUrl.filterGroup = filterGroup
|
|
hasFilterChanges = true
|
|
}
|
|
} catch {
|
|
// Ignore malformed filterGroup JSON in URL
|
|
}
|
|
} else if (!equal(DEFAULT_UNIVERSAL_GROUP_FILTER, values.filters.filterGroup)) {
|
|
filtersFromUrl.filterGroup = DEFAULT_UNIVERSAL_GROUP_FILTER
|
|
hasFilterChanges = true
|
|
}
|
|
if (params.searchTerm) {
|
|
if (!equal(params.searchTerm, values.filters.searchTerm)) {
|
|
filtersFromUrl.searchTerm = params.searchTerm
|
|
hasFilterChanges = true
|
|
}
|
|
} else if (values.filters.searchTerm !== '') {
|
|
filtersFromUrl.searchTerm = ''
|
|
hasFilterChanges = true
|
|
}
|
|
if (params.severityLevels) {
|
|
const parsed = parseTagsFilter(params.severityLevels)
|
|
if (parsed) {
|
|
const levels = parsed.filter(isValidSeverityLevel)
|
|
if (levels.length > 0 && !equal(levels, values.filters.severityLevels)) {
|
|
filtersFromUrl.severityLevels = levels
|
|
hasFilterChanges = true
|
|
}
|
|
}
|
|
} else if (!equal(DEFAULT_SEVERITY_LEVELS, values.filters.severityLevels)) {
|
|
filtersFromUrl.severityLevels = DEFAULT_SEVERITY_LEVELS
|
|
hasFilterChanges = true
|
|
}
|
|
if (params.serviceNames) {
|
|
const names = parseTagsFilter(params.serviceNames)
|
|
if (names && !equal(names, values.filters.serviceNames)) {
|
|
filtersFromUrl.serviceNames = names
|
|
hasFilterChanges = true
|
|
}
|
|
} else if (!equal(DEFAULT_SERVICE_NAMES, values.filters.serviceNames)) {
|
|
filtersFromUrl.serviceNames = DEFAULT_SERVICE_NAMES
|
|
hasFilterChanges = true
|
|
}
|
|
|
|
if (hasFilterChanges) {
|
|
actions.setFilters(filtersFromUrl, false)
|
|
}
|
|
|
|
// Non-filter params handled separately
|
|
if (params.orderBy && !equal(params.orderBy, values.orderBy)) {
|
|
actions.setOrderBy(params.orderBy)
|
|
}
|
|
// The Logs⇄Patterns⇄Group lens. Absent or unrecognised param means the default —
|
|
// a shared link with a flag-gated lens stays safe because the viewer double-gates
|
|
// rendering on the flag, so an unreachable mode just shows the Logs table.
|
|
const viewMode: LogsViewerViewMode = VALID_VIEW_MODES.includes(params.viewMode as LogsViewerViewMode)
|
|
? (params.viewMode as LogsViewerViewMode)
|
|
: DEFAULT_VIEW_MODE
|
|
if (viewMode !== values.viewMode) {
|
|
actions.setViewMode(viewMode)
|
|
}
|
|
if (params.initialLogsLimit != null && +params.initialLogsLimit !== values.initialLogsLimit) {
|
|
actions.setInitialLogsLimit(+params.initialLogsLimit)
|
|
}
|
|
|
|
const linkToLogId = params.linkToLogId as string | undefined
|
|
if (linkToLogId && linkToLogId !== values.linkToLogId) {
|
|
actions.setLinkToLogId(linkToLogId)
|
|
}
|
|
|
|
// Facet-name search: a plain string param. Absent param resets the field to empty.
|
|
const facetNameSearch = typeof params.facetNameSearch === 'string' ? params.facetNameSearch : ''
|
|
if (facetNameSearch !== values.facetNameSearch) {
|
|
actions.setFacetNameSearch(facetNameSearch)
|
|
}
|
|
}
|
|
return {
|
|
'*': urlToAction,
|
|
}
|
|
}),
|
|
|
|
trackedActionToUrl(({ values, cache }) => {
|
|
// Guard to prevent infinite loops between actionToUrl and urlToAction.
|
|
// Uses setTimeout (macrotask) so the flag stays set until the router has
|
|
// fully processed the URL change, even in test environments with
|
|
// synchronously-resolving mocks.
|
|
const withUrlSyncGuard = <T,>(fn: () => T): T => {
|
|
cache.isSyncingUrl = true
|
|
const result = fn()
|
|
setTimeout(() => {
|
|
cache.isSyncingUrl = false
|
|
}, 0)
|
|
return result
|
|
}
|
|
|
|
const syncUrl = (): [
|
|
string,
|
|
Params,
|
|
Record<string, any>,
|
|
{
|
|
replace: boolean
|
|
},
|
|
] => {
|
|
return withUrlSyncGuard(() =>
|
|
syncSearchParams(router, (params: Params) => {
|
|
updateSearchParams(params, 'searchTerm', values.filters.searchTerm, '')
|
|
updateSearchParams(
|
|
params,
|
|
'filterGroup',
|
|
values.filters.filterGroup,
|
|
DEFAULT_UNIVERSAL_GROUP_FILTER
|
|
)
|
|
updateSearchParams(params, 'dateRange', values.filters.dateRange, DEFAULT_DATE_RANGE)
|
|
updateSearchParams(params, 'severityLevels', values.filters.severityLevels, DEFAULT_SEVERITY_LEVELS)
|
|
updateSearchParams(params, 'serviceNames', values.filters.serviceNames, DEFAULT_SERVICE_NAMES)
|
|
updateSearchParams(params, 'orderBy', values.orderBy, DEFAULT_ORDER_BY)
|
|
updateSearchParams(params, 'facetNameSearch', values.facetNameSearch, '')
|
|
return params
|
|
})
|
|
)
|
|
}
|
|
|
|
const clearLinkToLogId = (): ReturnType<typeof syncSearchParams> => {
|
|
return withUrlSyncGuard(() =>
|
|
syncSearchParams(router, (params: Params) => {
|
|
delete params.linkToLogId
|
|
return params
|
|
})
|
|
)
|
|
}
|
|
|
|
const clearInitialLogsLimit = (): [
|
|
string,
|
|
Params,
|
|
Record<string, any>,
|
|
{
|
|
replace: boolean
|
|
},
|
|
] => {
|
|
return withUrlSyncGuard(() =>
|
|
syncSearchParams(router, (params: Params) => {
|
|
updateSearchParams(params, 'initialLogsLimit', null, DEFAULT_INITIAL_LOGS_LIMIT)
|
|
return params
|
|
})
|
|
)
|
|
}
|
|
|
|
const syncActiveTab = (): ReturnType<typeof syncSearchParams> => {
|
|
return withUrlSyncGuard(() =>
|
|
syncSearchParams(router, (params: Params) => {
|
|
updateSearchParams(params, 'activeTab', values.activeTab, DEFAULT_ACTIVE_TAB)
|
|
return params
|
|
})
|
|
)
|
|
}
|
|
|
|
const syncViewMode = (): ReturnType<typeof syncSearchParams> => {
|
|
return withUrlSyncGuard(() =>
|
|
syncSearchParams(router, (params: Params) => {
|
|
updateSearchParams(params, 'viewMode', values.viewMode, DEFAULT_VIEW_MODE)
|
|
return params
|
|
})
|
|
)
|
|
}
|
|
|
|
return {
|
|
// initialLogsLimit is a one-shot override from "copy link to log" URLs.
|
|
// It ensures the first fetch loads enough logs to include the linked log,
|
|
// then resets to null so subsequent queries use the default page size.
|
|
fetchLogsSuccess: () => clearInitialLogsLimit(),
|
|
closeLogDetails: () => clearLinkToLogId(),
|
|
clearLinkToLogId: () => clearLinkToLogId(),
|
|
syncUrl: () => syncUrl(),
|
|
setActiveTab: () => syncActiveTab(),
|
|
setViewMode: () => syncViewMode(),
|
|
}
|
|
}),
|
|
|
|
actions({
|
|
setActiveTab: (activeTab: LogsSceneActiveTab) => ({ activeTab }),
|
|
syncUrl: true,
|
|
toggleAttributeBreakdown: (key: string) => ({ key }),
|
|
setExpandedAttributeBreaksdowns: (expandedAttributeBreaksdowns: string[]) => ({ expandedAttributeBreaksdowns }),
|
|
keepSqlEditorMounted: (editorTabId: string) => ({ editorTabId }),
|
|
}),
|
|
|
|
reducers({
|
|
activeTab: [
|
|
DEFAULT_ACTIVE_TAB as LogsSceneActiveTab,
|
|
{
|
|
setActiveTab: (_, { activeTab }) => activeTab,
|
|
},
|
|
],
|
|
expandedAttributeBreaksdowns: [
|
|
[] as string[],
|
|
{
|
|
setExpandedAttributeBreaksdowns: (_, { expandedAttributeBreaksdowns }) => expandedAttributeBreaksdowns,
|
|
},
|
|
],
|
|
}),
|
|
|
|
listeners(({ values, actions, cache }) => ({
|
|
toggleAttributeBreakdown: ({ key }) => {
|
|
const breakdowns = [...values.expandedAttributeBreaksdowns]
|
|
const index = breakdowns.indexOf(key)
|
|
index >= 0 ? breakdowns.splice(index, 1) : breakdowns.push(key)
|
|
actions.setExpandedAttributeBreaksdowns(breakdowns)
|
|
},
|
|
handleQueryChange: () => {
|
|
actions.pushToFilterHistory(values.filters)
|
|
actions.syncUrl()
|
|
},
|
|
setOrderBy: () => {
|
|
actions.syncUrl()
|
|
},
|
|
setFacetNameSearch: () => {
|
|
actions.syncUrl()
|
|
},
|
|
keepSqlEditorMounted: ({ editorTabId }) => {
|
|
if (cache.sqlEditorTabId === editorTabId) {
|
|
return
|
|
}
|
|
cache.unmountSqlEditor?.()
|
|
cache.sqlEditorTabId = editorTabId
|
|
// Intentionally not cleaned up in beforeUnmount: keeps the embedded sqlEditorLogic
|
|
// alive across navigation so the user's query survives leaving and re-entering /logs.
|
|
cache.unmountSqlEditor = sqlEditorLogic({ tabId: editorTabId, mode: SQLEditorMode.Embedded }).mount()
|
|
},
|
|
})),
|
|
])
|