Skip to content

Commit

Permalink
feat(hogql): query debugger (#17939)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra authored Oct 12, 2023
1 parent 85451aa commit f66850d
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 81 deletions.
29 changes: 18 additions & 11 deletions frontend/src/queries/nodes/DataNode/ElapsedTime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,13 @@ import { Popover } from 'lib/lemon-ui/Popover'
import clsx from 'clsx'
import { QueryTiming } from '~/queries/schema'

function ElapsedTimeWithTimings({
elapsedTime,
hasError,
timings,
}: {
elapsedTime: number
hasError: boolean
export interface TimingsProps {
timings: QueryTiming[]
}): JSX.Element | null {
const [popoverVisible, setPopoverVisible] = useState(false)
elapsedTime: number
}

const overlay = (
export function Timings({ timings, elapsedTime }: TimingsProps): JSX.Element | null {
return (
<div className="space-y-2 p-2">
{timings.map(({ k: key, t: time }) => (
<div
Expand All @@ -38,12 +33,24 @@ function ElapsedTimeWithTimings({
) : null}
</div>
)
}

function ElapsedTimeWithTimings({
elapsedTime,
hasError,
timings,
}: {
elapsedTime: number
hasError: boolean
timings: QueryTiming[]
}): JSX.Element | null {
const [popoverVisible, setPopoverVisible] = useState(false)
return (
<Popover
onClickOutside={() => setPopoverVisible(false)}
visible={popoverVisible}
placement="bottom"
overlay={overlay}
overlay={<Timings timings={timings} elapsedTime={elapsedTime} />}
>
<div
onClick={() => setPopoverVisible((visible) => !visible)}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/appScenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const appScenes: Record<Scene, () => any> = {
[Scene.Unsubscribe]: () => import('./Unsubscribe/Unsubscribe'),
[Scene.IntegrationsRedirect]: () => import('./IntegrationsRedirect/IntegrationsRedirect'),
[Scene.IngestionWarnings]: () => import('./data-management/ingestion-warnings/IngestionWarningsView'),
[Scene.DebugQuery]: () => import('./query/QueryScene'),
[Scene.DebugQuery]: () => import('./debug/DebugScene'),
[Scene.VerifyEmail]: () => import('./authentication/signup/verify-email/VerifyEmail'),
[Scene.Feedback]: () => import('./feedback/Feedback'),
[Scene.Notebook]: () => import('./notebooks/NotebookScene'),
Expand Down
94 changes: 94 additions & 0 deletions frontend/src/scenes/debug/DebugScene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { debugSceneLogic } from './debugSceneLogic'
import { SceneExport } from 'scenes/sceneTypes'
import { PageHeader } from 'lib/components/PageHeader'
import { Query } from '~/queries/Query/Query'
import { useActions, useValues } from 'kea'
import { stringifiedExamples } from '~/queries/examples'
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { HogQLQuery } from '~/queries/schema'
import { HogQLDebug } from 'scenes/debug/HogQLDebug'

export function DebugScene(): JSX.Element {
const { query } = useValues(debugSceneLogic)
const { setQuery } = useActions(debugSceneLogic)

let parsed: Record<string, any> | undefined
try {
parsed = JSON.parse(query)
} catch (e) {
// do nothing
}

const showQueryEditor = !(
parsed &&
parsed.kind == 'DataTableNode' &&
parsed.source.kind == 'HogQLQuery' &&
(parsed.full || parsed.showHogQLEditor)
)

return (
<div className="QueryScene">
<PageHeader
title="Query Debugger"
buttons={
<>
<LemonButton
active={query === stringifiedExamples.HogQLRaw}
onClick={() => setQuery(stringifiedExamples.HogQLRaw)}
>
HogQL Debug
</LemonButton>
<LemonButton
active={query === stringifiedExamples.HogQLTable}
onClick={() => setQuery(stringifiedExamples.HogQLTable)}
>
HogQL Table
</LemonButton>
<LemonButton
active={query === stringifiedExamples.Events}
onClick={() => setQuery(stringifiedExamples.Events)}
>
Any Query
</LemonButton>
<LemonLabel>
<LemonSelect
placeholder={'More sample queries'}
options={Object.entries(stringifiedExamples)
.filter(([k]) => k !== 'HogQLTable' && k !== 'HogQLRaw')
.map(([k, v]) => {
return { label: k, value: v }
})}
onChange={(v) => {
if (v) {
setQuery(v)
}
}}
/>
</LemonLabel>
</>
}
/>
{parsed && parsed?.kind === 'HogQLQuery' ? (
<HogQLDebug
query={parsed as HogQLQuery}
setQuery={(query) => setQuery(JSON.stringify(query, null, 2))}
/>
) : (
<Query
query={query}
setQuery={(query) => setQuery(JSON.stringify(query, null, 2))}
context={{
showQueryEditor: showQueryEditor,
}}
/>
)}
</div>
)
}

export const scene: SceneExport = {
component: DebugScene,
logic: debugSceneLogic,
}
69 changes: 69 additions & 0 deletions frontend/src/scenes/debug/HogQLDebug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor'
import { DataNode, HogQLQuery } from '~/queries/schema'
import { DateRange } from '~/queries/nodes/DataNode/DateRange'
import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters'
import { BindLogic, useValues } from 'kea'
import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic'
import { ElapsedTime, Timings } from '~/queries/nodes/DataNode/ElapsedTime'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
import { CodeEditor } from 'lib/components/CodeEditors'

interface HogQLDebugProps {
query: HogQLQuery
setQuery: (query: DataNode) => void
}
export function HogQLDebug({ query, setQuery }: HogQLDebugProps): JSX.Element {
const dataNodeLogicProps: DataNodeLogicProps = { query, key: 'debug-scene' }
const { dataLoading, response, responseErrorObject, elapsedTime } = useValues(dataNodeLogic(dataNodeLogicProps))
return (
<BindLogic logic={dataNodeLogic} props={dataNodeLogicProps}>
<div className="space-y-2">
<HogQLQueryEditor query={query} setQuery={setQuery} />
<div className="flex gap-2">
<DateRange key="date-range" query={query} setQuery={setQuery} />
<EventPropertyFilters key="event-property" query={query} setQuery={setQuery} />
</div>
{dataLoading ? (
<>
<h2>Running query...</h2>
<div className="flex">
Time elapsed: <ElapsedTime />
</div>
</>
) : (
<>
{response?.hogql ? (
<>
<h2>Executed HogQL</h2>
<CodeSnippet language={Language.SQL} wrap>
{response.hogql}
</CodeSnippet>
</>
) : null}
{response?.clickhouse ? (
<>
<h2>Executed ClickHouse SQL</h2>
<CodeSnippet language={Language.SQL} wrap>
{response.clickhouse}
</CodeSnippet>
</>
) : null}
{response?.timings && elapsedTime !== null ? (
<>
<h2>Time spent</h2>
<Timings timings={response.timings} elapsedTime={elapsedTime} />
</>
) : null}
<h2>Raw response</h2>
<CodeEditor
className="border"
language={'json'}
value={JSON.stringify(response ?? responseErrorObject, null, 2)}
height={800}
/>
</>
)}
</div>
</BindLogic>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { actions, kea, path, reducers } from 'kea'

import type { querySceneLogicType } from './querySceneLogicType'
import type { debugSceneLogicType } from './debugSceneLogicType'
import { actionToUrl, urlToAction } from 'kea-router'
import { urls } from 'scenes/urls'
import { stringifiedExamples } from '~/queries/examples'

const DEFAULT_QUERY: string = stringifiedExamples['Events']
const DEFAULT_QUERY: string = stringifiedExamples['HogQLRaw']

export const querySceneLogic = kea<querySceneLogicType>([
path(['scenes', 'query', 'querySceneLogic']),
export const debugSceneLogic = kea<debugSceneLogicType>([
path(['scenes', 'query', 'debugSceneLogic']),
actions({
setQuery: (query: string) => ({ query: query }),
}),
Expand Down
65 changes: 0 additions & 65 deletions frontend/src/scenes/query/QueryScene.tsx

This file was deleted.

0 comments on commit f66850d

Please sign in to comment.