Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dashboards): Make sure loading dashboard items does not POST /query/ #24808

Merged
merged 6 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was surprised that EventsQuery (aka activity view/data table) is sync-only! Please QA that async is good here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything works well. I tested various nodes of 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 @@ -200,6 +203,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
Loading