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

feat(data-exploration): data exploration backend API with types #13933

Merged
merged 40 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a896737
feat(data-exploration): backend json schema validation
mariusandra Jan 26, 2023
1b55747
Update snapshots
github-actions[bot] Jan 26, 2023
2c68681
Update snapshots
github-actions[bot] Jan 26, 2023
4b5a595
Update snapshots
github-actions[bot] Jan 26, 2023
3e3b1ec
Update snapshots
github-actions[bot] Jan 26, 2023
3bf4a30
Update snapshots
github-actions[bot] Jan 26, 2023
1e91573
Update snapshots
github-actions[bot] Jan 26, 2023
5007caf
Update snapshots
github-actions[bot] Jan 26, 2023
df9fd12
Update snapshots
github-actions[bot] Jan 26, 2023
dd72cbe
refactor
mariusandra Jan 26, 2023
12ec5ef
switch to pydantic
mariusandra Jan 26, 2023
5546ca4
Merge remote-tracking branch 'origin/backend-queries' into backend-qu…
mariusandra Jan 26, 2023
dec00a0
split into v2 events list query
mariusandra Jan 26, 2023
1863738
add backend query endpoint
mariusandra Jan 26, 2023
350ca11
Update snapshots
github-actions[bot] Jan 26, 2023
334b3d7
make events query work
mariusandra Jan 26, 2023
d567a7c
Merge branch 'backend-queries' of github.com:PostHog/posthog into bac…
mariusandra Jan 26, 2023
f941eff
bring back HogQLExpression, inline all __root__ only classes in pytho…
mariusandra Jan 26, 2023
7c40322
Update snapshots
github-actions[bot] Jan 26, 2023
6e08e9e
Update snapshots
github-actions[bot] Jan 26, 2023
d02e4b7
Merge branch 'master' into backend-queries
mariusandra Jan 26, 2023
8a223b2
schema and mypy
mariusandra Jan 26, 2023
62f3d46
Update snapshots
github-actions[bot] Jan 26, 2023
efe5135
Update snapshots
github-actions[bot] Jan 26, 2023
9701325
restore /fake/ orderBy (only "timestamp" and "-timestamp" are supported)
mariusandra Jan 26, 2023
457bf4d
Merge branch 'backend-queries' of github.com:PostHog/posthog into bac…
mariusandra Jan 26, 2023
cccf24d
Update snapshots
github-actions[bot] Jan 26, 2023
b085362
Update snapshots
github-actions[bot] Jan 26, 2023
360f211
Update snapshots
github-actions[bot] Jan 26, 2023
a77ab10
Merge branch 'master' into backend-queries
mariusandra Jan 27, 2023
d8256b1
raise if invalid json
mariusandra Jan 27, 2023
6aa4064
Update snapshots
github-actions[bot] Jan 27, 2023
8aadcdd
remove __root__ hack
mariusandra Jan 27, 2023
2b86581
improve comments, in and out of quotes
mariusandra Jan 27, 2023
4f4a840
shrug
mariusandra Jan 27, 2023
2760a0c
future proofing
mariusandra Jan 27, 2023
3cce8b5
Merge remote-tracking branch 'origin/backend-queries' into backend-qu…
mariusandra Jan 27, 2023
e42bc9f
comment
mariusandra Jan 27, 2023
511f040
rename to "run_events_query"
mariusandra Jan 27, 2023
8756d17
simplify
mariusandra Jan 27, 2023
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
19 changes: 19 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/human
import { ActivityLogProps } from 'lib/components/ActivityLog/ActivityLog'
import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic'
import { dayjs } from 'lib/dayjs'
import { QuerySchema } from '~/queries/schema'

export const ACTIVITY_PAGE_SIZE = 20

Expand Down Expand Up @@ -424,6 +425,11 @@ class ApiRequest {
.withQueryString(toParams({ date_from: dateFrom, date_to: dateTo }))
}

// # Queries
public query(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('query')
}

// Request finalization

public async get(options?: ApiMethodOptions): Promise<any> {
Expand Down Expand Up @@ -1124,6 +1130,19 @@ const api = {
},
},

async query<T extends Record<string, any> = QuerySchema>(
query: T,
options?: ApiMethodOptions
): Promise<
T extends { [response: string]: any }
? T['response'] extends infer P | undefined
? P
: T['response']
: Record<string, any>
> {
return await new ApiRequest().query().create({ ...options, data: query })
},

/** Fetch data from specified URL. The result already is JSON-parsed. */
async get(url: string, options?: ApiMethodOptions): Promise<any> {
const res = await api.getResponse(url, options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { FEATURE_FLAGS } from 'lib/constants'
import { useState } from 'react'
import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnConfiguratorLogic'
import { defaultDataTableColumns, extractExpressionComment } from '../utils'
import { defaultDataTableColumns, extractExpressionComment, removeExpressionComment } from '../utils'
import { DataTableNode, NodeKind } from '~/queries/schema'
import { LemonModal } from 'lib/components/LemonModal'
import { isEventsQuery, taxonomicFilterToHogQl } from '~/queries/utils'
Expand All @@ -42,10 +42,16 @@ export function ColumnConfigurator({ query, setQuery }: ColumnConfiguratorProps)
columns: columnsInQuery,
setColumns: (columns: string[]) => {
if (isEventsQuery(query.source)) {
const oldOrderBy = query.source.orderBy
const firstOldOrderBy = oldOrderBy?.length == 1 ? removeExpressionComment(oldOrderBy?.[0]) : undefined
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
setQuery?.({
...query,
source: {
...query.source,
orderBy:
firstOldOrderBy && columns.find((c) => removeExpressionComment(c) === firstOldOrderBy)
? oldOrderBy
: undefined,
select: columns,
},
})
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/queries/nodes/DataTable/dataTableLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { actions, connect, kea, key, path, props, propsChanged, reducers, selectors } from 'kea'
import type { dataTableLogicType } from './dataTableLogicType'
import { AnyDataNode, DataTableNode, EventsQuery, HogQLExpression, NodeKind } from '~/queries/schema'
import { AnyDataNode, DataTableNode, EventsQuery, NodeKind } from '~/queries/schema'
import { getColumnsForQuery, removeExpressionComment } from './utils'
import { objectsEqual, sortedKeys } from 'lib/utils'
import { isDataTableNode, isEventsQuery } from '~/queries/utils'
Expand Down Expand Up @@ -37,7 +37,7 @@ export const dataTableLogic = kea<dataTableLogicType>([
return props.key
}),
path(['queries', 'nodes', 'DataTable', 'dataTableLogic']),
actions({ setColumnsInQuery: (columns: HogQLExpression[]) => ({ columns }) }),
actions({ setColumnsInQuery: (columns: string[]) => ({ columns }) }),
reducers(({ props }) => ({
columnsInQuery: [getColumnsForQuery(props.query), { setColumnsInQuery: (_, { columns }) => columns }],
})),
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/queries/nodes/DataTable/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DataNode, HogQLExpression, DataTableNode, NodeKind } from '~/queries/schema'
import { DataNode, DataTableNode, NodeKind } from '~/queries/schema'
import { isEventsQuery } from '~/queries/utils'

export const defaultDataTableEventColumns: HogQLExpression[] = [
export const defaultDataTableEventColumns: string[] = [
'*',
'event',
'person',
Expand All @@ -10,9 +10,9 @@ export const defaultDataTableEventColumns: HogQLExpression[] = [
'timestamp',
]

export const defaultDataTablePersonColumns: HogQLExpression[] = ['person', 'id', 'created_at', 'person.$delete']
export const defaultDataTablePersonColumns: string[] = ['person', 'id', 'created_at', 'person.$delete']

export function defaultDataTableColumns(kind: NodeKind): HogQLExpression[] {
export function defaultDataTableColumns(kind: NodeKind): string[] {
return kind === NodeKind.PersonsNode
? defaultDataTablePersonColumns
: kind === NodeKind.EventsQuery
Expand All @@ -22,14 +22,14 @@ export function defaultDataTableColumns(kind: NodeKind): HogQLExpression[] {
: []
}

export function getDataNodeDefaultColumns(source: DataNode): HogQLExpression[] {
export function getDataNodeDefaultColumns(source: DataNode): string[] {
return (
(isEventsQuery(source) && Array.isArray(source.select) && source.select.length > 0 ? source.select : null) ??
defaultDataTableColumns(source.kind)
)
}

export function getColumnsForQuery(query: DataTableNode): HogQLExpression[] {
export function getColumnsForQuery(query: DataTableNode): string[] {
return query.columns ?? getDataNodeDefaultColumns(query.source)
}

Expand Down
9 changes: 3 additions & 6 deletions frontend/src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,15 @@ export async function query<N extends DataNode = DataNode>(
): Promise<N['response']> {
if (isEventsQuery(query)) {
if (!query.before && !query.after) {
const earlyResults = await api.get(
getEventsEndpoint({ ...query, after: now().subtract(EVENTS_DAYS_FIRST_FETCH, 'day').toISOString() }),
const earlyResults = await api.query(
{ ...query, after: now().subtract(EVENTS_DAYS_FIRST_FETCH, 'day').toISOString() },
methodOptions
)
if (earlyResults.results.length > 0) {
return earlyResults
}
}
return await api.get(
getEventsEndpoint({ after: now().subtract(1, 'year').toISOString(), ...query }),
methodOptions
)
return await api.query({ after: now().subtract(1, 'year').toISOString(), ...query }, methodOptions)
} else if (isPersonsNode(query)) {
return await api.get(getPersonsEndpoint(query), methodOptions)
} else if (isInsightQueryNode(query)) {
Expand Down
17 changes: 7 additions & 10 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,7 @@
"columns": {
"description": "Columns shown in the table, unless the `source` provides them.",
"items": {
"$ref": "#/definitions/HogQLExpression"
"type": "string"
},
"type": "array"
},
Expand All @@ -1354,7 +1354,7 @@
"hiddenColumns": {
"description": "Columns that aren't shown in the table, even if in columns or returned data",
"items": {
"$ref": "#/definitions/HogQLExpression"
"type": "string"
},
"type": "array"
},
Expand Down Expand Up @@ -1630,7 +1630,7 @@
"properties": {
"actionId": {
"description": "Show events matching a given action",
"type": "number"
"type": "integer"
},
"after": {
"description": "Only fetch events that happened after this timestamp",
Expand All @@ -1657,11 +1657,11 @@
},
"limit": {
"description": "Number of rows to return",
"type": "number"
"type": "integer"
},
"offset": {
"description": "Number of rows to skip before returning rows",
"type": "number"
"type": "integer"
},
"orderBy": {
"description": "Columns to order by",
Expand Down Expand Up @@ -1714,14 +1714,14 @@
"select": {
"description": "Return a limited set of data. Required.",
"items": {
"$ref": "#/definitions/HogQLExpression"
"type": "string"
},
"type": "array"
},
"where": {
"description": "HogQL filters to apply on returned data",
"items": {
"$ref": "#/definitions/HogQLExpression"
"type": "string"
},
"type": "array"
}
Expand Down Expand Up @@ -1930,9 +1930,6 @@
"const": "unique_group",
"type": "string"
},
"HogQLExpression": {
"type": "string"
},
"InsightQueryNode": {
"anyOf": [
{
Expand Down
37 changes: 28 additions & 9 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ import {
LifecycleToggle,
} from '~/types'

/**
* PostHog Query Schema definition.
*
* This file acts as the source of truth for:
*
* - frontend/src/queries/schema.json
* - generated from typescript via "pnpm run generate:schema:json"
Copy link
Member

Choose a reason for hiding this comment

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

If schema.json and schema.py are only generated from this file, do we need to commit them? I can see this being a bit easier, but I'm a bit worried about them getting of out of sync as it's possible to commit changes to the wrong file. Plus they're really big. Could be compiled in package.json's prepare

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Says the guy who just committed gigabytes of PNGs to the repo :D.

Just like snapshots, I'm afraid if we don't commit them to git, we won't see any unexpected changes in the schema. Since this is a very important schema that'll be used to generate public facing docs, tooling for users, etc, I'd prefer to not take any chances.

It also makes development easier, if the files are already there. Unrelated, I've also been playing around with a Kea typegen inline types mode that would also commit the types into the repo... for clean, fast and reproducible builds.

Also, I still want to add a github action that throws a wrench when the schema is out of sync.

Copy link
Contributor

Choose a reason for hiding this comment

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

Having the kea types ship with git would help a lot for reviews etc. where switching branches takes a bit of time at the moment, so +1 for a solution there.

Copy link
Member

Choose a reason for hiding this comment

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

Isn't schema.ts the snapshot in this case? The derived .json and .py versions ones are just distractions in a code review.
But fair enough, should be okay IF we have a wrench-throwing action.

*
* - posthog/schema.py
* - generated from json the above json via "pnpm run generate:schema:python"
* */

export enum NodeKind {
// Data nodes
EventsNode = 'EventsNode',
Expand Down Expand Up @@ -128,20 +140,29 @@ export interface NewEntityNode extends EntityNode {
export interface EventsQuery extends DataNode {
kind: NodeKind.EventsQuery
/** Return a limited set of data. Required. */
select: HogQLExpression[]
select: string[]
/** HogQL filters to apply on returned data */
where?: HogQLExpression[]
where?: string[]
/** Properties configurable in the interface */
properties?: AnyPropertyFilter[]
/** Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person) */
fixedProperties?: AnyPropertyFilter[]
/** Limit to events matching this string */
event?: string
/** Number of rows to return */
/**
* Number of rows to return
* @asType integer
*/
limit?: number
/** Number of rows to skip before returning rows */
/**
* Number of rows to skip before returning rows
* @asType integer
*/
offset?: number
/** Show events matching a given action */
/**
* Show events matching a given action
* @asType integer
*/
actionId?: number
/** Show events for a given person */
personId?: string
Expand Down Expand Up @@ -181,9 +202,9 @@ export interface DataTableNode extends Node {
/** Source of the events */
source: EventsNode | EventsQuery | PersonsNode | RecentPerformancePageViewNode
/** Columns shown in the table, unless the `source` provides them. */
columns?: HogQLExpression[]
columns?: string[]
/** Columns that aren't shown in the table, even if in columns or returned data */
hiddenColumns?: HogQLExpression[]
hiddenColumns?: string[]
/** Show with most visual options enabled. Used in scenes. */
full?: boolean
/** Include an event filter above the table (EventsNode only) */
Expand Down Expand Up @@ -357,8 +378,6 @@ export interface RecentPerformancePageViewNode extends DataNode {
numberOfDays?: number // defaults to 7
}

export type HogQLExpression = string
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved

// Legacy queries

export interface LegacyQuery extends Node {
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"clean": "rm -rf frontend/dist && mkdir frontend/dist",
"build": "pnpm copy-scripts && pnpm build:esbuild",
"build:esbuild": "node frontend/build.mjs",
"schema:build": "ts-json-schema-generator -f tsconfig.json --path 'frontend/src/*.ts' --type 'QuerySchema' --no-type-check > frontend/src/queries/schema.json && prettier --write frontend/src/queries/schema.json",
"schema:build": "pnpm run schema:build:json && pnpm run schema:build:python",
"schema:build:json": "ts-json-schema-generator -f tsconfig.json --path 'frontend/src/*.ts' --type 'QuerySchema' --no-type-check > frontend/src/queries/schema.json && prettier --write frontend/src/queries/schema.json",
"schema:build:python": "datamodel-codegen --input frontend/src/queries/schema.json --input-file-type jsonschema --output posthog/schema.py && black posthog/schema.py",
"packages:build": "pnpm packages:build:apps-common && pnpm packages:build:lemon-ui",
"packages:build:apps-common": "cd frontend/@posthog/apps-common && pnpm i && pnpm build",
"packages:build:lemon-ui": "cd frontend/@posthog/lemon-ui && pnpm i && pnpm build",
Expand Down
3 changes: 3 additions & 0 deletions posthog/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
plugin_log_entry,
prompt,
property_definition,
query,
sharing,
site_app,
tagged_item,
Expand Down Expand Up @@ -65,6 +66,7 @@ def api_not_found(request):
"project_plugins_config_logs",
["team_id", "plugin_config_id"],
)

projects_router.register(r"annotations", annotation.AnnotationsViewSet, "project_annotations", ["team_id"])
projects_router.register(
r"activity_log",
Expand Down Expand Up @@ -148,6 +150,7 @@ def api_not_found(request):
projects_router.register(r"uploaded_media", uploaded_media.MediaViewSet, "project_media", ["team_id"])

projects_router.register(r"tags", tagged_item.TaggedItemViewSet, "project_tags", ["team_id"])
projects_router.register(r"query", query.QueryViewSet, "project_query", ["team_id"])

# General endpoints (shared across CH & PG)
router.register(r"login", authentication.LoginViewSet)
Expand Down
Loading