Files
zeekay 40d120dbf7 refactor(api): say /v1/ in the Python callers
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.
2026-08-08 18:38:44 -07:00

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],
})),
])