mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
Every first-party Python caller and test now names the mount the API actually answers on. Mechanical: URL literals only, and only where the /api/ was ours -- a preceding word character means it was a module path like insights/api/ or a third-party host like slack.com/api/, and those are untouched. Left saying /api/, each for a reason rather than for safety: - products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints - services/llm-gateway deploys separately and is under a code freeze, so moving its two calls would break it during the skew - the mcp_store and Slack/Teams redirect_uris are registered with third parties and must byte-match what was registered - the Wizard and Raycast client-metadata paths are OAuth client_ids - /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the ingress routes to the Rust fleets, held by released SDK versions Test URLconfs moved with the requests that call them, and the Datastore user-selection fixtures now carry the leading slash that request.path has.
487 lines
18 KiB
TypeScript
487 lines
18 KiB
TypeScript
import { MakeLogicType, actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea'
|
|
import { loaders } from 'kea-loaders'
|
|
|
|
import api from 'lib/api'
|
|
import { teamLogic } from 'scenes/teamLogic'
|
|
|
|
export interface Breakpoint {
|
|
id: string
|
|
repository: string | null
|
|
filename: string
|
|
line_number: number
|
|
enabled: boolean
|
|
condition?: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface BreakpointInstance {
|
|
id: string
|
|
lineNumber: number
|
|
functionName?: string
|
|
timestamp: string
|
|
variables: Record<string, any>
|
|
stackTrace?: any[]
|
|
breakpoint_id?: string
|
|
filename: string
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface liveDebuggerLogicValues {
|
|
currentProjectId: number | string // teamLogic
|
|
breakpointInstances: BreakpointInstance[]
|
|
breakpointInstancesLoading: boolean
|
|
breakpointLines: number[]
|
|
breakpoints: Breakpoint[]
|
|
breakpointsByLine: Record<number, Breakpoint>
|
|
breakpointsLoading: boolean
|
|
currentRepository: string
|
|
hitCountsByLine: Record<number, number>
|
|
hitsForSelectedLine: BreakpointInstance[]
|
|
instancesByLine: Record<number, BreakpointInstance[]>
|
|
newHitsByLine: Set<number>
|
|
newInstanceIds: Set<string>
|
|
previousInstanceIds: Set<string>
|
|
seenInstanceIds: Set<string>
|
|
selectedFilePath: string
|
|
selectedInstance: BreakpointInstance | null
|
|
selectedInstanceId: string | null
|
|
selectedLineForHits: number | null
|
|
visibleBreakpointIds: string[]
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface liveDebuggerLogicActions {
|
|
clearAllBreakpoints: () => {
|
|
value: true
|
|
}
|
|
loadBreakpointInstances: () => any
|
|
loadBreakpointInstancesFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
loadBreakpointInstancesSuccess: (
|
|
breakpointInstances: BreakpointInstance[],
|
|
payload?: any
|
|
) => {
|
|
breakpointInstances: BreakpointInstance[]
|
|
payload?: any
|
|
}
|
|
loadBreakpoints: () => any
|
|
loadBreakpointsFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
loadBreakpointsSuccess: (
|
|
breakpoints: Breakpoint[],
|
|
payload?: any
|
|
) => {
|
|
breakpoints: Breakpoint[]
|
|
payload?: any
|
|
}
|
|
markInstanceAsOld: (instanceId: string) => {
|
|
instanceId: string
|
|
}
|
|
selectInstance: (instanceId: string | null) => {
|
|
instanceId: string | null
|
|
}
|
|
setCurrentRepository: (repository: string) => {
|
|
repository: string
|
|
}
|
|
setHoveredLine: (lineNumber: number | null) => {
|
|
lineNumber: number | null
|
|
}
|
|
setSelectedFilePath: (filePath: string) => {
|
|
filePath: string
|
|
}
|
|
showHitsForLine: (lineNumber: number | null) => {
|
|
lineNumber: number | null
|
|
}
|
|
startPollingBreakpoints: () => {
|
|
value: true
|
|
}
|
|
stopPollingBreakpoints: () => {
|
|
value: true
|
|
}
|
|
toggleBreakpoint: (
|
|
filename: string,
|
|
lineNumber: number,
|
|
repository: string
|
|
) => {
|
|
filename: string
|
|
lineNumber: number
|
|
repository: string
|
|
}
|
|
toggleBreakpointForFile: (
|
|
filename: string,
|
|
lineNumber: number,
|
|
repository: string
|
|
) => {
|
|
filename: string
|
|
lineNumber: number
|
|
repository: string
|
|
}
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface liveDebuggerLogicMeta {
|
|
__keaTypeGenInternalSelectorTypes: {
|
|
selectedInstance: (
|
|
selectedInstanceId: string | null,
|
|
breakpointInstances: BreakpointInstance[]
|
|
) => BreakpointInstance | null
|
|
visibleBreakpointIds: (breakpoints: Breakpoint[]) => string[]
|
|
breakpointLines: (breakpoints: Breakpoint[], selectedFilePath: string) => number[]
|
|
breakpointsByLine: (breakpoints: Breakpoint[], selectedFilePath: string) => Record<number, Breakpoint>
|
|
instancesByLine: (
|
|
breakpointInstances: BreakpointInstance[],
|
|
selectedFilePath: string
|
|
) => Record<number, BreakpointInstance[]>
|
|
newInstanceIds: (breakpointInstances: BreakpointInstance[], seenInstanceIds: Set<string>) => Set<string>
|
|
hitCountsByLine: (breakpointInstances: BreakpointInstance[], selectedFilePath: string) => Record<number, number>
|
|
newHitsByLine: (
|
|
breakpointInstances: BreakpointInstance[],
|
|
seenInstanceIds: Set<string>,
|
|
selectedFilePath: string
|
|
) => Set<number>
|
|
hitsForSelectedLine: (
|
|
selectedLineForHits: number | null,
|
|
instancesByLine: Record<number, BreakpointInstance[]>
|
|
) => BreakpointInstance[]
|
|
}
|
|
}
|
|
|
|
export type liveDebuggerLogicType = MakeLogicType<
|
|
liveDebuggerLogicValues,
|
|
liveDebuggerLogicActions,
|
|
Record<string, any>,
|
|
liveDebuggerLogicMeta
|
|
>
|
|
|
|
export const liveDebuggerLogic = kea<liveDebuggerLogicType>([
|
|
path(['products', 'live_debugger', 'frontend', 'liveDebuggerLogic']),
|
|
connect(() => ({ values: [teamLogic, ['currentProjectId']] })),
|
|
|
|
actions({
|
|
toggleBreakpoint: (filename: string, lineNumber: number, repository: string) => ({
|
|
filename,
|
|
lineNumber,
|
|
repository,
|
|
}),
|
|
toggleBreakpointForFile: (filename: string, lineNumber: number, repository: string) => ({
|
|
filename,
|
|
lineNumber,
|
|
repository,
|
|
}),
|
|
setHoveredLine: (lineNumber: number | null) => ({ lineNumber }),
|
|
selectInstance: (instanceId: string | null) => ({ instanceId }),
|
|
clearAllBreakpoints: true,
|
|
markInstanceAsOld: (instanceId: string) => ({ instanceId }),
|
|
showHitsForLine: (lineNumber: number | null) => ({ lineNumber }),
|
|
startPollingBreakpoints: true,
|
|
stopPollingBreakpoints: true,
|
|
setSelectedFilePath: (filePath: string) => ({ filePath }),
|
|
setCurrentRepository: (repository: string) => ({ repository }),
|
|
}),
|
|
|
|
loaders(({ values }) => ({
|
|
breakpoints: [
|
|
[] as Breakpoint[],
|
|
{
|
|
loadBreakpoints: async () => {
|
|
// Only load when both repository and file are selected
|
|
if (!values.currentRepository || !values.selectedFilePath) {
|
|
return []
|
|
}
|
|
|
|
const params = new URLSearchParams()
|
|
params.append('repository', values.currentRepository)
|
|
params.append('filename', values.selectedFilePath)
|
|
|
|
const queryString = params.toString()
|
|
const url = `v1/projects/${values.currentProjectId}/live_debugger_breakpoints/?${queryString}`
|
|
// nosemgrep: prefer-codegen-api
|
|
const response = await api.get(url)
|
|
return response.results || []
|
|
},
|
|
},
|
|
],
|
|
breakpointInstances: [
|
|
[] as BreakpointInstance[],
|
|
{
|
|
loadBreakpointInstances: async () => {
|
|
// Filter hits by currently loaded breakpoints (for efficiency)
|
|
const breakpointIds = values.breakpoints.map((bp) => bp.id)
|
|
|
|
const params = new URLSearchParams()
|
|
breakpointIds.forEach((id: string) => params.append('breakpoint_ids', id))
|
|
|
|
const queryString = params.toString()
|
|
const url = `v1/projects/${values.currentProjectId}/live_debugger_breakpoints/breakpoint_hits/${queryString ? `?${queryString}` : ''}`
|
|
// nosemgrep: prefer-codegen-api
|
|
const response = await api.get(url)
|
|
return response.results || []
|
|
},
|
|
},
|
|
],
|
|
})),
|
|
|
|
reducers({
|
|
currentRepository: [
|
|
'Insights/insights' as string,
|
|
{
|
|
setCurrentRepository: (_, { repository }) => repository,
|
|
},
|
|
],
|
|
selectedFilePath: [
|
|
'',
|
|
{
|
|
setSelectedFilePath: (_, { filePath }) => filePath,
|
|
},
|
|
],
|
|
selectedInstanceId: [
|
|
null as string | null,
|
|
{
|
|
selectInstance: (_, { instanceId }) => instanceId,
|
|
},
|
|
],
|
|
seenInstanceIds: [
|
|
new Set<string>(),
|
|
{
|
|
markInstanceAsOld: (state, { instanceId }) => {
|
|
return new Set([...state, instanceId])
|
|
},
|
|
loadBreakpointInstancesSuccess: (state) => {
|
|
const newState = new Set(state)
|
|
return newState
|
|
},
|
|
},
|
|
],
|
|
previousInstanceIds: [
|
|
new Set<string>(),
|
|
{
|
|
loadBreakpointInstancesSuccess: (_, { breakpointInstances }) => {
|
|
return new Set(breakpointInstances.map((instance) => instance.id))
|
|
},
|
|
},
|
|
],
|
|
selectedLineForHits: [
|
|
null as number | null,
|
|
{
|
|
showHitsForLine: (_, { lineNumber }) => lineNumber,
|
|
},
|
|
],
|
|
}),
|
|
|
|
listeners(({ actions, values, cache }) => ({
|
|
loadBreakpointInstancesSuccess: ({ breakpointInstances }) => {
|
|
const newIds = breakpointInstances
|
|
.map((instance) => instance.id)
|
|
.filter((id) => !values.seenInstanceIds.has(id))
|
|
|
|
if (newIds.length > 0) {
|
|
setTimeout(() => {
|
|
newIds.forEach((id) => actions.markInstanceAsOld(id))
|
|
}, 2000)
|
|
}
|
|
},
|
|
toggleBreakpoint: async ({ filename, lineNumber, repository }) => {
|
|
const existingBreakpoint = Array.isArray(values.breakpoints)
|
|
? values.breakpoints.find(
|
|
(bp) => bp.line_number === lineNumber && bp.filename === filename && bp.repository === repository
|
|
)
|
|
: undefined
|
|
|
|
if (existingBreakpoint) {
|
|
// nosemgrep: prefer-codegen-api
|
|
await api.delete(
|
|
`v1/projects/${values.currentProjectId}/live_debugger_breakpoints/${existingBreakpoint.id}/`
|
|
)
|
|
} else {
|
|
// nosemgrep: prefer-codegen-api
|
|
await api.create(`v1/projects/${values.currentProjectId}/live_debugger_breakpoints/`, {
|
|
repository,
|
|
filename,
|
|
line_number: lineNumber,
|
|
enabled: true,
|
|
})
|
|
}
|
|
|
|
actions.loadBreakpoints()
|
|
actions.loadBreakpointInstances()
|
|
},
|
|
toggleBreakpointForFile: async ({ filename, lineNumber, repository }) => {
|
|
const existingBreakpoint = Array.isArray(values.breakpoints)
|
|
? values.breakpoints.find(
|
|
(bp) => bp.line_number === lineNumber && bp.filename === filename && bp.repository === repository
|
|
)
|
|
: undefined
|
|
|
|
if (existingBreakpoint) {
|
|
// nosemgrep: prefer-codegen-api
|
|
await api.delete(
|
|
`v1/projects/${values.currentProjectId}/live_debugger_breakpoints/${existingBreakpoint.id}/`
|
|
)
|
|
} else {
|
|
// nosemgrep: prefer-codegen-api
|
|
await api.create(`v1/projects/${values.currentProjectId}/live_debugger_breakpoints/`, {
|
|
repository,
|
|
filename,
|
|
line_number: lineNumber,
|
|
enabled: true,
|
|
})
|
|
}
|
|
|
|
actions.loadBreakpoints()
|
|
actions.loadBreakpointInstances()
|
|
},
|
|
clearAllBreakpoints: async () => {
|
|
if (Array.isArray(values.breakpoints)) {
|
|
await Promise.all(
|
|
values.breakpoints.map((bp) =>
|
|
// nosemgrep: prefer-codegen-api
|
|
api.delete(`v1/projects/${values.currentProjectId}/live_debugger_breakpoints/${bp.id}/`)
|
|
)
|
|
)
|
|
}
|
|
|
|
actions.loadBreakpoints()
|
|
actions.loadBreakpointInstances()
|
|
},
|
|
startPollingBreakpoints: () => {
|
|
actions.loadBreakpoints()
|
|
actions.loadBreakpointInstances()
|
|
|
|
cache.disposables.add(() => {
|
|
const interval = setInterval(() => {
|
|
actions.loadBreakpoints()
|
|
actions.loadBreakpointInstances()
|
|
}, 15000)
|
|
return () => clearInterval(interval)
|
|
}, 'breakpointPolling')
|
|
},
|
|
stopPollingBreakpoints: () => {
|
|
cache.disposables.dispose('breakpointPolling')
|
|
},
|
|
setCurrentRepository: () => {
|
|
// Reload breakpoints when repository changes (only if file is selected)
|
|
if (values.selectedFilePath) {
|
|
actions.loadBreakpoints()
|
|
}
|
|
},
|
|
setSelectedFilePath: () => {
|
|
// Reload breakpoints when file path changes (only if repository is set)
|
|
if (values.currentRepository) {
|
|
actions.loadBreakpoints()
|
|
}
|
|
},
|
|
loadBreakpointsSuccess: () => {
|
|
// Reload instances when breakpoints change (the list of IDs has changed)
|
|
actions.loadBreakpointInstances()
|
|
},
|
|
})),
|
|
|
|
selectors({
|
|
selectedInstance: [
|
|
(s) => [s.selectedInstanceId, s.breakpointInstances],
|
|
(selectedId: string | null, instances: BreakpointInstance[]): BreakpointInstance | null =>
|
|
instances.find((i) => i.id === selectedId) || null,
|
|
],
|
|
visibleBreakpointIds: [
|
|
(s) => [s.breakpoints],
|
|
(breakpoints: Breakpoint[]): string[] => breakpoints.map((bp) => bp.id),
|
|
],
|
|
breakpointLines: [
|
|
(s) => [s.breakpoints, s.selectedFilePath],
|
|
(breakpoints: Breakpoint[], selectedFilePath: string): number[] => {
|
|
if (!Array.isArray(breakpoints) || !selectedFilePath) {
|
|
return []
|
|
}
|
|
return breakpoints
|
|
.filter((bp) => bp.filename === selectedFilePath)
|
|
.map((bp) => bp.line_number)
|
|
.sort((a, b) => a - b)
|
|
},
|
|
],
|
|
breakpointsByLine: [
|
|
(s) => [s.breakpoints, s.selectedFilePath],
|
|
(breakpoints: Breakpoint[], selectedFilePath: string): Record<number, Breakpoint> => {
|
|
const byLine: Record<number, Breakpoint> = {}
|
|
if (Array.isArray(breakpoints) && selectedFilePath) {
|
|
breakpoints
|
|
.filter((bp) => bp.filename === selectedFilePath)
|
|
.forEach((bp) => {
|
|
byLine[bp.line_number] = bp
|
|
})
|
|
}
|
|
return byLine
|
|
},
|
|
],
|
|
instancesByLine: [
|
|
(s) => [s.breakpointInstances, s.selectedFilePath],
|
|
(instances: BreakpointInstance[], selectedFilePath: string): Record<number, BreakpointInstance[]> => {
|
|
const grouped: Record<number, BreakpointInstance[]> = {}
|
|
instances
|
|
.filter((instance) => !selectedFilePath || instance.filename === selectedFilePath)
|
|
.forEach((instance) => {
|
|
if (!grouped[instance.lineNumber]) {
|
|
grouped[instance.lineNumber] = []
|
|
}
|
|
grouped[instance.lineNumber].push(instance)
|
|
})
|
|
return grouped
|
|
},
|
|
],
|
|
newInstanceIds: [
|
|
(s) => [s.breakpointInstances, s.seenInstanceIds],
|
|
(instances: BreakpointInstance[], seenIds: Set<string>): Set<string> => {
|
|
return new Set(instances.filter((instance) => !seenIds.has(instance.id)).map((instance) => instance.id))
|
|
},
|
|
],
|
|
hitCountsByLine: [
|
|
(s) => [s.breakpointInstances, s.selectedFilePath],
|
|
(instances: BreakpointInstance[], selectedFilePath: string): Record<number, number> => {
|
|
const counts: Record<number, number> = {}
|
|
instances
|
|
.filter((instance) => !selectedFilePath || instance.filename === selectedFilePath)
|
|
.forEach((instance) => {
|
|
counts[instance.lineNumber] = (counts[instance.lineNumber] || 0) + 1
|
|
})
|
|
return counts
|
|
},
|
|
],
|
|
newHitsByLine: [
|
|
(s) => [s.breakpointInstances, s.seenInstanceIds, s.selectedFilePath],
|
|
(instances: BreakpointInstance[], seenIds: Set<string>, selectedFilePath: string): Set<number> => {
|
|
const newInstances = instances
|
|
.filter((instance) => !seenIds.has(instance.id))
|
|
.filter((instance) => !selectedFilePath || instance.filename === selectedFilePath)
|
|
return new Set(newInstances.map((instance) => instance.lineNumber))
|
|
},
|
|
],
|
|
hitsForSelectedLine: [
|
|
(s) => [s.selectedLineForHits, s.instancesByLine],
|
|
(
|
|
selectedLine: number | null,
|
|
instancesByLine: Record<number, BreakpointInstance[]>
|
|
): BreakpointInstance[] => {
|
|
if (!selectedLine) {
|
|
return []
|
|
}
|
|
return instancesByLine[selectedLine] || []
|
|
},
|
|
],
|
|
}),
|
|
|
|
events(({ actions }) => ({
|
|
afterMount: [actions.startPollingBreakpoints],
|
|
beforeUnmount: [actions.stopPollingBreakpoints],
|
|
})),
|
|
])
|