Skip to content

Commit

Permalink
fix(dashboards): Make sure loading dashboard items does not `POST /qu…
Browse files Browse the repository at this point in the history
…ery/` (#24808)
  • Loading branch information
Twixes authored and timgl committed Sep 10, 2024
1 parent 5cd21ea commit 7777500
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 28 deletions.
3 changes: 1 addition & 2 deletions cypress/e2e/dashboard-shared.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ describe('Shared dashboard', () => {

cy.get('.InsightCard').should('have.length', 6)
// Make sure no element with text "There are no matching events for this query" exists
// TODO this was failing, it shouldn't be but YOLO
// cy.get('.insight-empty-state').should('not.exist')
cy.get('.insight-empty-state').should('not.exist')
})
})
11 changes: 8 additions & 3 deletions frontend/src/queries/nodes/DataNode/dataNodeLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ export const dataNodeLogic = kea<dataNodeLogicType>([
}
}),
actions({
loadData: (refresh = false, queryId?: string) => ({ refresh, queryId: queryId || uuid() }),
loadData: (refresh = false, alreadyRunningQueryId?: string) => ({
refresh,
queryId: alreadyRunningQueryId || uuid(),
pollOnly: !!alreadyRunningQueryId,
}),
abortAnyRunningQuery: true,
abortQuery: (payload: { queryId: string }) => payload,
cancelQuery: true,
Expand All @@ -142,7 +146,7 @@ export const dataNodeLogic = kea<dataNodeLogicType>([
{
setResponse: (response) => response,
clearResponse: () => null,
loadData: async ({ refresh: refreshArg, queryId }, breakpoint) => {
loadData: async ({ refresh: refreshArg, queryId, pollOnly }, breakpoint) => {
const refresh = props.alwaysRefresh || refreshArg
if (props.doNotLoad) {
return props.cachedResults
Expand Down Expand Up @@ -200,7 +204,8 @@ export const dataNodeLogic = kea<dataNodeLogicType>([
methodOptions,
refresh,
queryId,
actions.setPollResponse
actions.setPollResponse,
pollOnly
)) ?? null
const duration = performance.now() - now
return { data, duration }
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/queries/nodes/DataTable/dataTableLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ describe('dataTableLogic', () => {
expect.anything(),
false,
expect.any(String),
expect.any(Function)
expect.any(Function),
false
)
expect(performQuery).toHaveBeenCalledTimes(1)
})
Expand Down
61 changes: 41 additions & 20 deletions frontend/src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import posthog from 'posthog-js'
import { OnlineExportContext, QueryExportContext } from '~/types'

import { DataNode, HogQLQuery, HogQLQueryResponse, NodeKind, PersonsNode, QueryStatus } from './schema'
import { isDataTableNode, isDataVisualizationNode, isHogQLQuery, isInsightVizNode, isPersonsNode } from './utils'
import {
isAsyncResponse,
isDataTableNode,
isDataVisualizationNode,
isHogQLQuery,
isInsightVizNode,
isPersonsNode,
} from './utils'

const QUERY_ASYNC_MAX_INTERVAL_SECONDS = 3
const QUERY_ASYNC_TOTAL_POLL_SECONDS = 10 * 60 + 6 // keep in sync with backend-side timeout (currently 10min) + a small buffer
Expand All @@ -29,7 +36,6 @@ export function queryExportContext<N extends DataNode>(
const SYNC_ONLY_QUERY_KINDS = [
'HogQuery',
'HogQLMetadata',
'EventsQuery',
'HogQLAutocomplete',
'DatabaseSchemaQuery',
'ErrorTrackingQuery',
Expand All @@ -39,7 +45,7 @@ export async function pollForResults(
queryId: string,
showProgress: boolean,
methodOptions?: ApiMethodOptions,
callback?: (response: QueryStatus) => void
onPoll?: (response: QueryStatus) => void
): Promise<QueryStatus> {
const pollStart = performance.now()
let currentDelay = 300 // start low, because all queries will take at minimum this
Expand All @@ -50,12 +56,11 @@ export async function pollForResults(

try {
const statusResponse = (await api.queryStatus.get(queryId, showProgress)).query_status

if (statusResponse.complete) {
return statusResponse
}
if (callback) {
callback(statusResponse)
if (onPoll) {
onPoll(statusResponse)
}
} catch (e: any) {
e.detail = e.data?.query_status?.error_message
Expand All @@ -73,27 +78,42 @@ async function executeQuery<N extends DataNode>(
methodOptions?: ApiMethodOptions,
refresh?: boolean,
queryId?: string,
setPollResponse?: (response: QueryStatus) => void
setPollResponse?: (response: QueryStatus) => void,
/**
* Whether to limit the function to just polling the provided query ID.
* This is important in shared contexts, where we cannot create arbitrary queries via POST – we can only GET.
*/
pollOnly = false
): Promise<NonNullable<N['response']>> {
const isAsyncQuery =
methodOptions?.async !== false &&
!SYNC_ONLY_QUERY_KINDS.includes(queryNode.kind) &&
!!featureFlagLogic.findMounted()?.values.featureFlags?.[FEATURE_FLAGS.QUERY_ASYNC]

const showProgress = !!featureFlagLogic.findMounted()?.values.featureFlags?.[FEATURE_FLAGS.INSIGHT_LOADING_BAR]

const response = await api.query(queryNode, methodOptions, queryId, refresh, isAsyncQuery)
if (!pollOnly) {
const response = await api.query(queryNode, methodOptions, queryId, refresh, isAsyncQuery)

if (!response.query_status?.query_async) {
// Executed query synchronously or from cache
return response
}
if (response.query_status?.complete) {
// Async query returned immediately
return response.results
}
if (!isAsyncResponse(response)) {
// Executed query synchronously or from cache
return response
}

const statusResponse = await pollForResults(response.query_status.id, showProgress, methodOptions, setPollResponse)
if (response.query_status.complete) {
// Async query returned immediately
return response.results
}

queryId = response.query_status.id
} else {
if (!isAsyncQuery) {
throw new Error('pollOnly is only supported for async queries')
}
if (!queryId) {
throw new Error('pollOnly requires a queryId')
}
}
const statusResponse = await pollForResults(queryId, showProgress, methodOptions, setPollResponse)
return statusResponse.results
}

Expand All @@ -103,7 +123,8 @@ export async function performQuery<N extends DataNode>(
methodOptions?: ApiMethodOptions,
refresh?: boolean,
queryId?: string,
setPollResponse?: (status: QueryStatus) => void
setPollResponse?: (status: QueryStatus) => void,
pollOnly = false
): Promise<NonNullable<N['response']>> {
let response: NonNullable<N['response']>
const logParams: Record<string, any> = {}
Expand All @@ -113,7 +134,7 @@ export async function performQuery<N extends DataNode>(
if (isPersonsNode(queryNode)) {
response = await api.get(getPersonsEndpoint(queryNode), methodOptions)
} else {
response = await executeQuery(queryNode, methodOptions, refresh, queryId, setPollResponse)
response = await executeQuery(queryNode, methodOptions, refresh, queryId, setPollResponse, pollOnly)
if (isHogQLQuery(queryNode) && response && typeof response === 'object') {
logParams.clickhouse_sql = (response as HogQLQueryResponse)?.clickhouse
}
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/queries/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
NodeKind,
PathsQuery,
PersonsNode,
QuerySchema,
QueryStatusResponse,
RetentionQuery,
SavedInsightNode,
SessionAttributionExplorerQuery,
Expand Down Expand Up @@ -78,6 +80,7 @@ export function isDataWarehouseNode(node?: Record<string, any> | null): node is
return node?.kind === NodeKind.DataWarehouseNode
}

/** @deprecated `ActorsQuery` is now used instead of `PersonsNode`. */
export function isPersonsNode(node?: Record<string, any> | null): node is PersonsNode {
return node?.kind === NodeKind.PersonsNode
}
Expand Down Expand Up @@ -204,6 +207,10 @@ export function isQueryForGroup(query: PersonsNode | ActorsQuery): boolean {
)
}

export function isAsyncResponse(response: NonNullable<QuerySchema['response']>): response is QueryStatusResponse {
return 'query_status' in response && response.query_status
}

export function isInsightQueryWithSeries(
node?: Node
): node is TrendsQuery | FunnelsQuery | StickinessQuery | LifecycleQuery {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/funnels/funnelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export function flattenedStepsByBreakdown(
breakdown_value: 'Baseline',
})),
conversionRates: {
total: (lastStep?.count ?? 0) / (baseStep?.count ?? 1),
total: (lastStep?.count || 0) / (baseStep?.count || 1),
},
})
}
Expand Down
2 changes: 1 addition & 1 deletion posthog/models/person_overrides/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
ENGINE = ReplicatedReplacingMergeTree(
-- NOTE: for testing we use a uuid to ensure that we don't get conflicts
-- when the tests tear down and recreate the table.
'/clickhouse/tables/{'{uuid}' if settings.TEST else ''}noshard/{CLICKHOUSE_DATABASE}.person_overrides',
'/clickhouse/tables/{'{uuid}' if settings.TEST or settings.E2E_TESTING else ''}noshard/{CLICKHOUSE_DATABASE}.person_overrides',
'{{replica}}-{{shard}}',
version
)
Expand Down

0 comments on commit 7777500

Please sign in to comment.