0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-28 09:16:49 +01:00

Migrate event-related logics to be project-based (#6566)

* Instrument legacy endpoint debugging

* Add `currentTeamId` to `teamLogic`

* Add logics utils

* Update annotations endpoint in the frontend

* Limit legacy endpoint logging to DEBUG

* Fix `annotationsTableLogic` usage

* Update test_annotation.py

* Add `test_deleting_annotation_of_other_team_prevented`

* Comment out debug code

* Migrate actions-related code to project-based approach

* Fix `infiniteTestLogic`

* Rework approach

* Remove redundant lines

* Fix mixup

* Fix non-logged-in scenes

* Fix Python cast

* Align approach to `teamLogic`-based

* Fix logic tests

* Clean up code

* Fix stupid omission

* Restore `props` in `connect`s

* Fix capitalization

* Fix action creation

* Fix `ActionEdit` props type

* Migrate events-related code to project-based approach

* Remove `MOCK_ORGANIZATION_ID`

* Update sessionsPlayLogic.test.ts

* Fix logic tests

* Fix duplicate imports

* Reduce duplication in URL test instrumentation

* Update API interception paths in tests

* Fix `Person.cy-spec.js`

* Add comments in `posthog/api/__init__.py`

* reduce test noise

* Update infiniteListLogic.ts

* Simplify `teamLogic` seeding

* Simplify `teamLogic` seeding better

* Fix `organizationLogic` tests

* Migrate feature flags-related logics to be project-based (#6603)

* Migrate feature flags-related logics to be project-based

* Add comment

* Migrate insight-related logics to be project-based (#6574)

* Migrate insight-related logics to be project-based

* Migrate over the rest and fix logic tests

* Update funnelLogic.ts

* Fix URL formatting in tests

* Update dashboardLogic.tsx

* Remove now redundant `initTeamLogic`

* Add tracking comments

Co-authored-by: Marius Andra <marius.andra@gmail.com>
This commit is contained in:
Michael Matloka 2021-10-22 18:32:58 +02:00 committed by GitHub
parent 1f143d109e
commit 165ffcaffb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 567 additions and 380 deletions

View File

@ -1,3 +1,5 @@
import json
from ee.api.test.base import APILicensedTest
from posthog.models import User
@ -6,7 +8,9 @@ class TestQueryMiddleware(APILicensedTest):
def test_query(self):
self.user.is_staff = True
self.user.save()
response = self.client.get('/api/insight/trend/?events=[{"id": "$pageview"}]')
response = self.client.get(
f'/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{"id": "$pageview"}])}'
)
self.assertEqual(response.status_code, 200)
response = self.client.get("/api/debug_ch_queries/").json()
self.assertIn("SELECT", response[0]["query"]) # type: ignore
@ -14,7 +18,7 @@ class TestQueryMiddleware(APILicensedTest):
#  Test saving queries if we're impersonating a user
user2 = User.objects.create_and_join(organization=self.organization, email="test", password="bla")
self.client.post("/admin/login/user/{}/".format(user2.pk))
self.client.get('/api/insight/trend/?events=[{"id": "$pageleave"}]')
self.client.get(f'/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{"id": "$pageleave"}])}')
response = self.client.get("/api/debug_ch_queries/").json()
self.assertIn("SELECT", response[0]["query"]) # type: ignore

View File

@ -166,7 +166,7 @@ class ClickhouseEventsViewSet(EventViewSet):
return Response({"result": SessionsListEvents().run(filter=filter, team=self.team)})
# ******************************************
# /event/session_recording
# /events/session_recording
# params:
# - session_recording_id: (string) id of the session recording
# - save_view: (boolean) save view of the recording

View File

@ -110,7 +110,7 @@ class ClickhouseInsightsViewSet(InsightViewSet):
return {"result": funnel_order_class(team=team, filter=filter).run()}
# ******************************************
# /insight/funnel/correlation
# /projects/:id/insights/funnel/correlation
#
# params:
# - params are the same as for funnel

View File

@ -37,10 +37,10 @@ class ClickhouseTestEventApi(
#  For ClickHouse we normally only query the last day,
# but if a user doesn't have many events we still want to return events that are older
patch_sync_execute.return_value = [("event", "d", "{}", timezone.now(), "d", "d", "d")]
response = self.client.get("/api/event/").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/").json()
self.assertEqual(len(response["results"]), 1)
self.assertEqual(patch_sync_execute.call_count, 2)
patch_sync_execute.return_value = [("event", "d", "{}", timezone.now(), "d", "d", "d") for _ in range(0, 100)]
response = self.client.get("/api/event/").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/").json()
self.assertEqual(patch_sync_execute.call_count, 3)

View File

@ -32,7 +32,9 @@ class ClickhouseTestInsights(
self.organization_membership.save()
self.team.access_control = False
self.team.save()
response = self.client.get("/api/insight/trend/?events={}".format(json.dumps([{"id": "$pageview"}])))
response = self.client.get(
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_insight_trends_forbidden_if_project_private_and_org_member(self):
@ -40,7 +42,9 @@ class ClickhouseTestInsights(
self.organization_membership.save()
self.team.access_control = True
self.team.save()
response = self.client.get("/api/insight/trend/?events={}".format(json.dumps([{"id": "$pageview"}])))
response = self.client.get(
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}"
)
self.assertDictEqual(self.permission_denied_response("You don't have access to the project."), response.json())
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@ -52,7 +56,9 @@ class ClickhouseTestInsights(
self_team_membership = ExplicitTeamMembership.objects.create(
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
)
response = self.client.get("/api/insight/trend/?events={}".format(json.dumps([{"id": "$pageview"}])))
response = self.client.get(
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -67,7 +73,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(team=self.team, event="step one", distinct_id="2")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -96,7 +102,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(team=self.team, event="step two", distinct_id="2")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -129,7 +135,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(event="step three", distinct_id="user_two", team=self.team, timestamp="2021-05-05 00:00:00")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -161,7 +167,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(event="step two", distinct_id="user_two", team=self.team, timestamp="2021-05-04 00:00:00")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -195,7 +201,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(event="step three", distinct_id="user_two", team=self.team, timestamp="2021-05-05 00:00:00")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -235,7 +241,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(event="step three", distinct_id="user_three", team=self.team, timestamp="2021-05-05 00:00:00")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -274,7 +280,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
# Converted from 0 to 1 in 82_800 s
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"insight": "funnels",
"funnel_viz_type": "time_to_convert",
@ -324,7 +330,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
# Converted from 0 to 1 in 82_800 s
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"insight": "funnels",
"funnel_viz_type": "time_to_convert",
@ -375,7 +381,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
# Converted from 0 to 1 in 82_800 s
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"insight": "funnels",
"funnel_viz_type": "time_to_convert",
@ -408,7 +414,10 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
)
def test_funnel_invalid_action_handled(self):
response = self.client.post("/api/insight/funnel/", {"actions": [{"id": 666, "type": "actions", "order": 0},]},)
response = self.client.post(
f"/api/projects/{self.team.id}/insights/funnel/",
{"actions": [{"id": 666, "type": "actions", "order": 0},]},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), self.validation_error_response("Action ID 666 does not exist!"))
@ -424,7 +433,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
_create_event(team=self.team, event="step two", distinct_id="2")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},
@ -461,7 +470,7 @@ class ClickhouseTestFunnelTypes(ClickhouseTestMixin, APIBaseTest):
("step three", 0, 1, False),
]:
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "step one", "type": "events", "order": 0},

View File

@ -59,7 +59,7 @@ class TestClickhousePaths(ClickhouseTestMixin, APIBaseTest):
properties={"$current_url": "/about"}, distinct_id="person_1", event="$pageview", team=self.team,
)
response = self.client.get("/api/insight/path",).json()
response = self.client.get(f"/api/projects/{self.team.id}/insights/path",).json()
self.assertEqual(len(response["result"]), 1)
def test_insight_paths_basic_exclusions(self):
@ -74,7 +74,9 @@ class TestClickhousePaths(ClickhouseTestMixin, APIBaseTest):
distinct_id="person_1", event="third event", team=self.team,
)
response = self.client.get("/api/insight/path", data={"exclude_events": '["second event"]'}).json()
response = self.client.get(
f"/api/projects/{self.team.id}/insights/path", data={"exclude_events": '["second event"]'}
).json()
self.assertEqual(len(response["result"]), 1)
def test_backwards_compatible_path_types(self):
@ -98,12 +100,18 @@ class TestClickhousePaths(ClickhouseTestMixin, APIBaseTest):
_create_event(
distinct_id="person_1", event="custom2", team=self.team,
)
response = self.client.get("/api/insight/path", data={"path_type": "$pageview", "insight": "PATHS",}).json()
response = self.client.get(
f"/api/projects/{self.team.id}/insights/path", data={"path_type": "$pageview", "insight": "PATHS",}
).json()
self.assertEqual(len(response["result"]), 2)
response = self.client.get("/api/insight/path", data={"path_type": "custom_event", "insight": "PATHS"}).json()
response = self.client.get(
f"/api/projects/{self.team.id}/insights/path", data={"path_type": "custom_event", "insight": "PATHS"}
).json()
self.assertEqual(len(response["result"]), 1)
response = self.client.get("/api/insight/path", data={"path_type": "$screen", "insight": "PATHS"}).json()
response = self.client.get(
f"/api/projects/{self.team.id}/insights/path", data={"path_type": "$screen", "insight": "PATHS"}
).json()
self.assertEqual(len(response["result"]), 0)
def test_backwards_compatible_start_point(self):
@ -131,16 +139,19 @@ class TestClickhousePaths(ClickhouseTestMixin, APIBaseTest):
distinct_id="person_1", event="custom2", team=self.team,
)
response = self.client.get(
"/api/insight/path", data={"path_type": "$pageview", "insight": "PATHS", "start_point": "/about",}
f"/api/projects/{self.team.id}/insights/path",
data={"path_type": "$pageview", "insight": "PATHS", "start_point": "/about",},
).json()
self.assertEqual(len(response["result"]), 1)
response = self.client.get(
"/api/insight/path", data={"path_type": "custom_event", "insight": "PATHS", "start_point": "custom2",}
f"/api/projects/{self.team.id}/insights/path",
data={"path_type": "custom_event", "insight": "PATHS", "start_point": "custom2",},
).json()
self.assertEqual(len(response["result"]), 0)
response = self.client.get(
"/api/insight/path", data={"path_type": "$screen", "insight": "PATHS", "start_point": "/screen1",}
f"/api/projects/{self.team.id}/insights/path",
data={"path_type": "$screen", "insight": "PATHS", "start_point": "/screen1",},
).json()
self.assertEqual(len(response["result"]), 1)
@ -180,12 +191,14 @@ class TestClickhousePaths(ClickhouseTestMixin, APIBaseTest):
)
response = self.client.get(
"/api/insight/path", data={"insight": "PATHS", "path_groupings": json.dumps(["/about*"])}
f"/api/projects/{self.team.id}/insights/path",
data={"insight": "PATHS", "path_groupings": json.dumps(["/about*"])},
).json()
self.assertEqual(len(response["result"]), 2)
response = self.client.get(
"/api/insight/path", data={"insight": "PATHS", "path_groupings": json.dumps(["/about_*"])}
f"/api/projects/{self.team.id}/insights/path",
data={"insight": "PATHS", "path_groupings": json.dumps(["/about_*"])},
).json()
self.assertEqual(len(response["result"]), 3)
@ -214,7 +227,9 @@ class TestClickhousePaths(ClickhouseTestMixin, APIBaseTest):
],
}
post_response = self.client.post("/api/insight/path/", data={**request_data, "funnel_filter": funnel_filter})
post_response = self.client.post(
f"/api/projects/{self.team.id}/insights/path/", data={**request_data, "funnel_filter": funnel_filter}
)
self.assertEqual(post_response.status_code, status.HTTP_200_OK)
post_j = post_response.json()
self.assertEqual(

View File

@ -1,6 +1,6 @@
import apiNoMock from 'lib/api'
import { combineUrl } from 'kea-router'
import { AvailableFeature, TeamType } from '~/types'
import { AvailableFeature, OrganizationType, TeamType } from '~/types'
type APIMockReturnType = {
[K in keyof typeof apiNoMock]: jest.Mock<ReturnType<typeof apiNoMock[K]>, Parameters<typeof apiNoMock[K]>>
@ -22,6 +22,7 @@ interface APIMockOptions {
}
export const MOCK_TEAM_ID: TeamType['id'] = 997
export const MOCK_ORGANIZATION_ID: OrganizationType['id'] = 'ABCD'
export const api = apiNoMock as any as APIMockReturnType
@ -55,14 +56,14 @@ export function defaultAPIMocks(
}
} else if (pathname === 'api/organizations/@current') {
return {
id: 'ABCD', // Should be a UUID but that doesn't matter here
id: MOCK_ORGANIZATION_ID,
}
} else if (
[
`api/projects/${MOCK_TEAM_ID}/actions/`,
`api/projects/${MOCK_TEAM_ID}/event_definitions/`,
'api/dashboard',
'api/organizations/@current',
'api/projects/@current/event_definitions/',
].includes(pathname)
) {
return { results: [] }

View File

@ -11,7 +11,7 @@ const defaultValue = 'https://'
export const appUrlsLogic = kea<appUrlsLogicType>({
connect: {
values: [teamLogic, ['currentTeam']],
values: [teamLogic, ['currentTeam', 'currentTeamId']],
},
actions: () => ({
setAppUrls: (appUrls: string[]) => ({ appUrls }),
@ -30,7 +30,9 @@ export const appUrlsLogic = kea<appUrlsLogicType>({
breakdown: '$current_url',
date_from: dayjs().subtract(3, 'days').toISOString(),
}
const result = (await api.get('api/insight/trend/?' + toParams(params))).result as TrendResult[]
const result = (
await api.get(`api/projects/${values.currentTeamId}/insights/trend/?${toParams(params)}`)
).result as TrendResult[]
if (result && result[0]?.count === 0) {
return []
}

View File

@ -50,7 +50,12 @@ export function SaveToDashboardModal({
async function save(event: MouseEvent | FormEvent): Promise<void> {
event.preventDefault()
if (newItem) {
const response = await api.create('api/insight', { filters, name, saved: true, dashboard: dashboardId })
const response = await api.create(`api/projects/${currentTeamId}/insights`, {
filters,
name,
saved: true,
dashboard: dashboardId,
})
if (annotations) {
for (const { content, date_marker, created_at, scope } of annotations) {
await api.create(`api/projects/${currentTeamId}/annotations`, {
@ -63,7 +68,7 @@ export function SaveToDashboardModal({
}
}
} else {
await api.update(`api/insight/${fromItem}`, { filters })
await api.update(`api/projects/${currentTeamId}/insights/${fromItem}`, { filters })
}
reportSavedInsightToDashboard()
toast(

View File

@ -130,7 +130,7 @@ export const taxonomicFilterLogic = kea<taxonomicFilterLogicType>({
{
name: 'Pageview URLs',
type: TaxonomicFilterGroupType.PageviewUrls,
endpoint: 'api/event/values/?key=$current_url',
endpoint: `api/projects/${teamId}/events/values/?key=$current_url`,
searchAlias: 'value',
getName: (option: SimpleOption): string => option.name,
getValue: (option: SimpleOption): TaxonomicFilterValue => option.name,
@ -138,7 +138,7 @@ export const taxonomicFilterLogic = kea<taxonomicFilterLogicType>({
{
name: 'Screens',
type: TaxonomicFilterGroupType.Screens,
endpoint: 'api/event/values/?key=$screen_name',
endpoint: `api/projects/${teamId}/events/values/?key=$screen_name`,
searchAlias: 'value',
getName: (option: SimpleOption): string => option.name,
getValue: (option: SimpleOption): TaxonomicFilterValue => option.name,

View File

@ -8,6 +8,7 @@ import { dashboardsModel } from './dashboardsModel'
import { Link } from 'lib/components/Link'
import { dashboardItemsModelType } from './dashboardItemsModelType'
import { urls } from 'scenes/urls'
import { teamLogic } from '../scenes/teamLogic'
export const dashboardItemsModel = kea<dashboardItemsModelType>({
actions: () => ({
@ -28,7 +29,9 @@ export const dashboardItemsModel = kea<dashboardItemsModelType>({
value: item.name,
error: 'You must enter name',
success: async (name: string) => {
item = await api.update(`api/insight/${item.id}`, { name })
item = await api.update(`api/projects/${teamLogic.values.currentTeamId}/insights/${item.id}`, {
name,
})
toast('Successfully renamed item')
actions.renameDashboardItemSuccess(item)
},
@ -46,14 +49,17 @@ export const dashboardItemsModel = kea<dashboardItemsModelType>({
const { id: _discard, ...rest } = item // eslint-disable-line
const newItem = dashboardId ? { ...rest, dashboard: dashboardId, layouts } : { ...rest, layouts }
const addedItem = await api.create('api/insight', newItem)
const addedItem = await api.create(`api/projects/${teamLogic.values.currentTeamId}/insights`, newItem)
const dashboard = dashboardId ? dashboardsModel.values.rawDashboards[dashboardId] : null
if (move && dashboard) {
const deletedItem = await api.update(`api/insight/${item.id}`, {
deleted: true,
})
const deletedItem = await api.update(
`api/projects/${teamLogic.values.currentTeamId}/insights/${item.id}`,
{
deleted: true,
}
)
dashboardsModel.actions.updateDashboardItem(deletedItem)
const toastId = toast(
@ -68,10 +74,15 @@ export const dashboardItemsModel = kea<dashboardItemsModelType>({
onClick={async () => {
toast.dismiss(toastId)
const [restoredItem, removedItem] = await Promise.all([
api.update(`api/insight/${item.id}`, { deleted: false }),
api.update(`api/insight/${addedItem.id}`, {
deleted: true,
api.update(`api/projects/${teamLogic.values.currentTeamId}/insights/${item.id}`, {
deleted: false,
}),
api.update(
`api/projects/${teamLogic.values.currentTeamId}/insights/${addedItem.id}`,
{
deleted: true,
}
),
])
toast(<div>Panel move reverted!</div>)
dashboardsModel.actions.updateDashboardItem(restoredItem)

View File

@ -4,6 +4,7 @@ import { toParams } from 'lib/utils'
import { SavedFunnel, ViewType } from '~/types'
import { insightHistoryLogic } from 'scenes/insights/InsightHistoryPanel/insightHistoryLogic'
import { funnelsModelType } from './funnelsModelType'
import { teamLogic } from '../scenes/teamLogic'
const parseSavedFunnel = (result: Record<string, any>): SavedFunnel => {
return {
@ -23,20 +24,19 @@ export const funnelsModel = kea<funnelsModelType>({
__default: [] as SavedFunnel[],
loadFunnels: async () => {
const response = await api.get(
'api/insight/?' +
toParams({
order: '-created_at',
saved: true,
limit: 5,
insight: ViewType.FUNNELS,
})
`api/projects/${teamLogic.values.currentTeamId}/insights/?${toParams({
order: '-created_at',
saved: true,
limit: 5,
insight: ViewType.FUNNELS,
})}`
)
const results = response.results.map((result: Record<string, any>) => parseSavedFunnel(result))
actions.setNext(response.next)
return results
},
deleteFunnel: async (funnelId: number) => {
await api.delete(`api/insight/${funnelId}`)
await api.delete(`api/projects/${teamLogic.values.currentTeamId}/insights/${funnelId}`)
return values.funnels.filter((funnel) => funnel.id !== funnelId)
},
},

View File

@ -45,6 +45,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { FEATURE_FLAGS } from 'lib/constants'
import { LinkButton } from 'lib/components/LinkButton'
import { DiveIcon } from 'lib/components/icons'
import { teamLogic } from '../teamLogic'
dayjs.extend(relativeTime)
@ -201,6 +202,7 @@ export function DashboardItem({
}: Props): JSX.Element {
const [initialLoaded, setInitialLoaded] = useState(false)
const [showSaveModal, setShowSaveModal] = useState(false)
const { currentTeamId } = useValues(teamLogic)
const { nameSortedDashboards } = useValues(dashboardsModel)
const { renameDashboardItem } = useActions(dashboardItemsModel)
const { featureFlags } = useValues(featureFlagLogic)
@ -588,7 +590,7 @@ export function DashboardItem({
id: item.id,
name: item.name,
},
endpoint: 'insight',
endpoint: `projects/${currentTeamId}/insights`,
callback: loadDashboardItems,
})
}

View File

@ -13,6 +13,7 @@ import { dashboardLogicType } from './dashboardLogicType'
import React from 'react'
import { Layout, Layouts } from 'react-grid-layout'
import { insightLogic } from 'scenes/insights/insightLogic'
import { teamLogic } from '../teamLogic'
export interface DashboardLogicProps {
id?: number
@ -23,7 +24,10 @@ export interface DashboardLogicProps {
export const AUTO_REFRESH_INITIAL_INTERVAL_SECONDS = 300
export const dashboardLogic = kea<dashboardLogicType<DashboardLogicProps>>({
connect: [dashboardsModel, dashboardItemsModel, eventUsageLogic],
connect: {
values: [teamLogic, ['currentTeamId']],
logic: [dashboardsModel, dashboardItemsModel, eventUsageLogic],
},
props: {} as DashboardLogicProps,
@ -490,10 +494,10 @@ export const dashboardLogic = kea<dashboardLogicType<DashboardLogicProps>>({
})
},
updateItemColor: ({ id, color }) => {
api.update(`api/insight/${id}`, { color })
api.update(`api/projects/${values.currentTeamId}/insights/${id}`, { color })
},
setDiveDashboard: ({ id, dive_dashboard }) => {
api.update(`api/insight/${id}`, { dive_dashboard })
api.update(`api/projects/${values.currentTeamId}/insights/${id}`, { dive_dashboard })
},
refreshAllDashboardItemsManual: () => {
// reset auto refresh interval

View File

@ -12152,7 +12152,7 @@
}
}
],
"hasNext": "http://localhost:8000/api/event/?properties=%7B%7D&orderBy=%5B%22-timestamp%22%5D&after=2021-08-24T10:14:50.240000Z",
"hasNext": "http://localhost:8000/api/projects/1/events/?properties=%7B%7D&orderBy=%5B%22-timestamp%22%5D&after=2021-08-24T10:14:50.240000Z",
"orderBy": "-timestamp",
"selectedEvent": null,
"newEvents": [

View File

@ -7,6 +7,7 @@ import { errorToast, toParams, uniqueBy } from 'lib/utils'
import { eventDefinitionsModel } from '~/models/eventDefinitionsModel'
import { keyMapping } from 'lib/components/PropertyKeyInfo'
import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel'
import { teamLogic } from '../../teamLogic'
export const definitionDrawerLogic = kea<definitionDrawerLogicType>({
actions: () => ({
@ -71,7 +72,9 @@ export const definitionDrawerLogic = kea<definitionDrawerLogicType>({
orderBy: ['-timestamp'],
limit: 5,
})
const events = await api.get(`api/event/?${eventsParams}`)
const events = await api.get(
`api/projects/${teamLogic.values.currentTeamId}/events/?${eventsParams}`
)
if (values.type === 'property') {
actions.loadEventsSnippetSuccess(events.results)
}

View File

@ -1,7 +1,7 @@
import { BuiltLogic } from 'kea'
import { eventsTableLogicType } from 'scenes/events/eventsTableLogicType'
import { ApiError, eventsTableLogic, EventsTableLogicProps, OnFetchEventsSuccess } from 'scenes/events/eventsTableLogic'
import { mockAPI } from 'lib/api.mock'
import { mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { router } from 'kea-router'
@ -52,7 +52,7 @@ describe('eventsTableLogic', () => {
})
it('sets a key', () => {
expect(logic.key).toEqual('all-events-test-key')
expect(logic.key).toEqual('all-test-key')
})
it('starts with known defaults', async () => {
@ -362,7 +362,7 @@ describe('eventsTableLogic', () => {
it('can build the export URL when there are no properties or filters', async () => {
await expectLogic(logic, () => {}).toMatchValues({
exportUrl: '/api/event.csv?properties=%5B%5D&orderBy=%5B%22-timestamp%22%5D',
exportUrl: `/api/projects/${MOCK_TEAM_ID}/events.csv?properties=%5B%5D&orderBy=%5B%22-timestamp%22%5D`,
})
})
@ -370,8 +370,7 @@ describe('eventsTableLogic', () => {
await expectLogic(logic, () => {
logic.actions.setProperties([makePropertyFilter('fixed value')])
}).toMatchValues({
exportUrl:
'/api/event.csv?properties=%5B%7B%22key%22%3A%22fixed%20value%22%2C%22operator%22%3Anull%2C%22type%22%3A%22t%22%2C%22value%22%3A%22v%22%7D%5D&orderBy=%5B%22-timestamp%22%5D',
exportUrl: `/api/projects/${MOCK_TEAM_ID}/events.csv?properties=%5B%7B%22key%22%3A%22fixed%20value%22%2C%22operator%22%3Anull%2C%22type%22%3A%22t%22%2C%22value%22%3A%22v%22%7D%5D&orderBy=%5B%22-timestamp%22%5D`,
})
})
@ -379,7 +378,7 @@ describe('eventsTableLogic', () => {
await expectLogic(logic, () => {
logic.actions.flipSort()
}).toMatchValues({
exportUrl: '/api/event.csv?properties=%5B%5D&orderBy=%5B%22timestamp%22%5D',
exportUrl: `/api/projects/${MOCK_TEAM_ID}/events.csv?properties=%5B%5D&orderBy=%5B%22timestamp%22%5D`,
})
})
})

View File

@ -3,11 +3,12 @@ import { errorToast, toParams } from 'lib/utils'
import { router } from 'kea-router'
import api from 'lib/api'
import dayjs from 'dayjs'
import { eventsTableLogicType } from './eventsTableLogicType'
import { FixedFilters } from 'scenes/events/EventsTable'
import { AnyPropertyFilter, EventsTableRowItem, EventType, PropertyFilter } from '~/types'
import { isValidPropertyFilter } from 'lib/components/PropertyFilters/utils'
import { teamLogic } from '../teamLogic'
const POLL_TIMEOUT = 5000
const formatEvents = (events: EventType[], newEvents: EventType[]): EventsTableRowItem[] => {
@ -35,7 +36,6 @@ const formatEvents = (events: EventType[], newEvents: EventType[]): EventsTableR
export interface EventsTableLogicProps {
fixedFilters?: FixedFilters
apiUrl?: string // = 'api/event/'
key?: string
}
@ -57,10 +57,12 @@ export const eventsTableLogic = kea<eventsTableLogicType<ApiError, EventsTableLo
// Set a unique key based on the fixed filters.
// This way if we move back/forward between /events and /person/ID, the logic is reloaded.
key: (props) =>
[props.fixedFilters ? JSON.stringify(props.fixedFilters) : 'all', props.apiUrl || 'events', props.key]
[props.fixedFilters ? JSON.stringify(props.fixedFilters) : 'all', props.key]
.filter((keyPart) => !!keyPart)
.join('-'),
connect: {
values: [teamLogic, ['currentTeamId']],
},
actions: {
setProperties: (properties: AnyPropertyFilter[] | AnyPropertyFilter): { properties: AnyPropertyFilter[] } => {
// there seem to be multiple representations of "empty" properties
@ -187,9 +189,9 @@ export const eventsTableLogic = kea<eventsTableLogicType<ApiError, EventsTableLo
(events, newEvents) => formatEvents(events, newEvents),
],
exportUrl: [
() => [selectors.eventFilter, selectors.orderBy, selectors.properties],
(eventFilter, orderBy, properties) =>
`/api/event.csv?${toParams({
() => [selectors.currentTeamId, selectors.eventFilter, selectors.orderBy, selectors.properties],
(teamId, eventFilter, orderBy, properties) =>
`/api/projects/${teamId}/events.csv?${toParams({
properties,
...(props.fixedFilters || {}),
...(eventFilter ? { event: eventFilter } : {}),
@ -306,7 +308,7 @@ export const eventsTableLogic = kea<eventsTableLogicType<ApiError, EventsTableLo
let apiResponse = null
try {
apiResponse = await api.get(`${props.apiUrl || 'api/event/'}?${urlParams}`)
apiResponse = await api.get(`api/projects/${values.currentTeamId}/events/?${urlParams}`)
} catch (error) {
actions.fetchOrPollFailure(error)
return
@ -347,7 +349,7 @@ export const eventsTableLogic = kea<eventsTableLogicType<ApiError, EventsTableLo
let apiResponse = null
try {
apiResponse = await api.get(`${props.apiUrl || 'api/event/'}?${urlParams}`)
apiResponse = await api.get(`api/projects/${values.currentTeamId}/events/?${urlParams}`)
} catch (e) {
// We don't call fetchOrPollFailure because we don't to generate an error alert for this
return

View File

@ -16,8 +16,10 @@ import { normalizeColumnTitle, useIsTableScrolling } from 'lib/components/Table/
import { urls } from 'scenes/urls'
import { Tooltip } from 'lib/components/Tooltip'
import stringWithWBR from 'lib/utils/stringWithWBR'
import { teamLogic } from '../teamLogic'
export function FeatureFlags(): JSX.Element {
const { currentTeamId } = useValues(teamLogic)
const { featureFlags, featureFlagsLoading } = useValues(featureFlagsLogic)
const { updateFeatureFlag, loadFeatureFlags } = useActions(featureFlagsLogic)
const { push } = useActions(router)
@ -145,7 +147,7 @@ export function FeatureFlags(): JSX.Element {
</Link>
{featureFlag.id && (
<DeleteWithUndo
endpoint="feature_flag"
endpoint={`projects/${currentTeamId}/feature_flags`}
object={{ name: featureFlag.name, id: featureFlag.id }}
className="text-danger"
style={{ marginLeft: 8 }}

View File

@ -7,6 +7,7 @@ import { toast } from 'react-toastify'
import { router } from 'kea-router'
import { deleteWithUndo } from 'lib/utils'
import { urls } from 'scenes/urls'
import { teamLogic } from '../teamLogic'
const NEW_FLAG = {
id: null,
key: '',
@ -34,6 +35,9 @@ const EMPTY_MULTIVARIATE_OPTIONS: MultivariateFlagOptions = {
}
export const featureFlagLogic = kea<featureFlagLogicType>({
connect: {
values: [teamLogic, ['currentTeamId']],
},
actions: {
setFeatureFlagId: (id: number | 'new') => ({ id }),
addMatchGroup: true,
@ -197,18 +201,18 @@ export const featureFlagLogic = kea<featureFlagLogicType>({
featureFlag: {
loadFeatureFlag: async () => {
if (values.featureFlagId && values.featureFlagId !== 'new') {
return await api.get(`api/feature_flag/${values.featureFlagId}`)
return await api.get(`api/projects/${values.currentTeamId}/feature_flags/${values.featureFlagId}`)
}
return NEW_FLAG
},
saveFeatureFlag: async (updatedFlag: Partial<FeatureFlagType>) => {
if (!updatedFlag.id) {
return await api.create('api/feature_flag', {
return await api.create(`api/projects/${values.currentTeamId}/feature_flags`, {
...updatedFlag,
id: undefined,
})
} else {
return await api.update(`api/feature_flag/${updatedFlag.id}`, {
return await api.update(`api/projects/${values.currentTeamId}/feature_flags/${updatedFlag.id}`, {
...updatedFlag,
id: undefined,
})
@ -216,7 +220,7 @@ export const featureFlagLogic = kea<featureFlagLogicType>({
},
},
}),
listeners: ({ actions }) => ({
listeners: ({ actions, values }) => ({
saveFeatureFlagSuccess: () => {
toast.success(
<div>
@ -233,7 +237,7 @@ export const featureFlagLogic = kea<featureFlagLogicType>({
},
deleteFeatureFlag: async ({ featureFlag }) => {
deleteWithUndo({
endpoint: 'feature_flag',
endpoint: `projects/${values.currentTeamId}/feature_flags`,
object: { name: featureFlag.name, id: featureFlag.id },
callback: () => {
router.actions.push(urls.featureFlags())

View File

@ -2,17 +2,21 @@ import { kea } from 'kea'
import api from 'lib/api'
import { featureFlagsLogicType } from './featureFlagsLogicType'
import { FeatureFlagType } from '~/types'
import { teamLogic } from '../teamLogic'
export const featureFlagsLogic = kea<featureFlagsLogicType>({
connect: {
values: [teamLogic, ['currentTeamId']],
},
loaders: ({ values }) => ({
featureFlags: {
__default: [] as FeatureFlagType[],
loadFeatureFlags: async () => {
const response = await api.get('api/feature_flag/')
const response = await api.get(`api/projects/${values.currentTeamId}/feature_flags/`)
return response.results as FeatureFlagType[]
},
updateFeatureFlag: async ({ id, payload }: { id: number; payload: Partial<FeatureFlagType> }) => {
const response = await api.update(`api/feature_flag/${id}`, payload)
const response = await api.update(`api/projects/${values.currentTeamId}/feature_flags/${id}`, payload)
return [...values.featureFlags].map((flag) => (flag.id === response.id ? response : flag))
},
},

View File

@ -1,5 +1,5 @@
import { funnelLogic } from './funnelLogic'
import { api, defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { api, defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
@ -15,14 +15,14 @@ describe('funnelLogic', () => {
let logic: ReturnType<typeof funnelLogic.build>
mockAPI(async (url) => {
if (url.pathname === 'api/insight/funnel/') {
if (url.pathname === `api/projects/${MOCK_TEAM_ID}/insights/funnel/`) {
return {
is_cached: true,
last_refresh: '2021-09-16T13:41:41.297295Z',
result: ['result from api'],
type: 'Funnel',
}
} else if (url.pathname.startsWith('api/insight')) {
} else if (url.pathname.startsWith(`api/projects/${MOCK_TEAM_ID}/insights`)) {
return { results: [], next: null }
}
return defaultAPIMocks(url)
@ -166,7 +166,7 @@ describe('funnelLogic', () => {
})
expect(api.create).toBeCalledWith(
'api/insight/funnel/?',
`api/projects/${MOCK_TEAM_ID}/insights/funnel/`,
expect.objectContaining({
actions: [],
events: [

View File

@ -49,6 +49,7 @@ import { dashboardsModel } from '~/models/dashboardsModel'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { cleanFilters } from 'scenes/insights/utils/cleanFilters'
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
import { teamLogic } from '../teamLogic'
const DEVIATION_SIGNIFICANCE_MULTIPLIER = 1.5
// Chosen via heuristics by eyeballing some values
@ -60,7 +61,7 @@ export const funnelLogic = kea<funnelLogicType>({
key: keyForInsightLogicProps('insight_funnel'),
connect: (props: InsightLogicProps) => ({
values: [insightLogic(props), ['filters', 'insight', 'insightLoading']],
values: [insightLogic(props), ['filters', 'insight', 'insightLoading'], teamLogic, ['currentTeamId']],
actions: [insightLogic(props), ['loadResults', 'loadResultsSuccess'], funnelsModel, ['loadFunnels']],
logic: [eventUsageLogic, dashboardsModel],
}),
@ -129,7 +130,7 @@ export const funnelLogic = kea<funnelLogicType>({
{
loadCorrelations: async () => {
return (
await api.create('api/insight/funnel/correlation', {
await api.create(`api/projects/${values.currentTeamId}/insights/funnel/correlation`, {
...values.apiParams,
funnel_correlation_type: 'events',
})
@ -144,7 +145,7 @@ export const funnelLogic = kea<funnelLogicType>({
{
loadPropertyCorrelations: async (propertyNames: string[]) => {
return (
await api.create('api/insight/funnel/correlation', {
await api.create(`api/projects/${values.currentTeamId}/insights/funnel/correlation`, {
...values.apiParams,
funnel_correlation_type: 'properties',
// Name is comma separated list of property names
@ -161,7 +162,7 @@ export const funnelLogic = kea<funnelLogicType>({
{
loadEventWithPropertyCorrelations: async (eventName: string) => {
const results = (
await api.create('api/insight/funnel/correlation', {
await api.create(`api/projects/${values.currentTeamId}/insights/funnel/correlation`, {
...values.apiParams,
funnel_correlation_type: 'event_with_properties',
funnel_correlation_event_names: [eventName],
@ -854,7 +855,7 @@ export const funnelLogic = kea<funnelLogicType>({
actions.setFilters({ new_entity: values.filters.new_entity }, false, true)
},
saveFunnelInsight: async ({ name }) => {
await api.create('api/insight', {
await api.create(`api/projects/${values.currentTeamId}/insights`, {
filters: values.filters,
name,
saved: true,

View File

@ -13,6 +13,7 @@ import {
FunnelsTimeConversionBins,
FunnelAPIResponse,
FunnelStepReference,
TeamType,
} from '~/types'
const PERCENTAGE_DISPLAY_PRECISION = 1 // Number of decimals to show in percentages
@ -226,15 +227,19 @@ export function getVisibilityIndex(step: FunnelStep, key?: number | string): str
export const SECONDS_TO_POLL = 3 * 60
export async function pollFunnel<T = FunnelStep[] | FunnelsTimeConversionBins>(
teamId: TeamType['id'],
apiParams: FunnelRequestParams
): Promise<FunnelResult<T>> {
// Tricky: This API endpoint has wildly different return types depending on parameters.
const { refresh, ...bodyParams } = apiParams
let result = await api.create('api/insight/funnel/?' + (refresh ? 'refresh=true' : ''), bodyParams)
let result = await api.create(
`api/projects/${teamId}/insights/funnel/${refresh ? '?refresh=true' : ''}`,
bodyParams
)
const start = window.performance.now()
while (result.result?.loading && (window.performance.now() - start) / 1000 < SECONDS_TO_POLL) {
await wait()
result = await api.create('api/insight/funnel', bodyParams)
result = await api.create(`api/projects/${teamId}/insights/funnel`, bodyParams)
}
// if endpoint is still loading after 3 minutes just return default
if (result.loading) {

View File

@ -6,6 +6,7 @@ import { toast } from 'react-toastify'
import { DashboardItemType } from '~/types'
import { insightHistoryLogicType } from './insightHistoryLogicType'
import { dashboardItemsModel } from '~/models/dashboardItemsModel'
import { teamLogic } from '../../teamLogic'
const updateInsightState = (
state: DashboardItemType[],
@ -39,17 +40,19 @@ const updateInsightState = (
/* insightHistoryLogic - Handles all logic for saved insights and recent history */
export const insightHistoryLogic = kea<insightHistoryLogicType>({
connect: {
values: [teamLogic, ['currentTeamId']],
},
loaders: ({ actions }) => ({
insights: {
__default: [] as DashboardItemType[],
loadInsights: async () => {
const response = await api.get(
'api/insight/?' +
toParams({
order: '-created_at',
limit: 25,
user: true,
})
`api/projects/${teamLogic.values.currentTeamId}/insights/?${toParams({
order: '-created_at',
limit: 25,
user: true,
})}`
)
actions.setInsightsNext(response.next)
return response.results
@ -59,13 +62,12 @@ export const insightHistoryLogic = kea<insightHistoryLogicType>({
__default: [] as DashboardItemType[],
loadSavedInsights: async () => {
const response = await api.get(
'api/insight/?' +
toParams({
order: '-created_at',
saved: true,
limit: 25,
user: true,
})
`api/projects/${teamLogic.values.currentTeamId}/insights/?${toParams({
order: '-created_at',
saved: true,
limit: 25,
user: true,
})}`
)
actions.setSavedInsightsNext(response.next)
return response.results
@ -75,12 +77,11 @@ export const insightHistoryLogic = kea<insightHistoryLogicType>({
__default: [] as DashboardItemType[],
loadTeamInsights: async () => {
const response = await api.get(
'api/insight/?' +
toParams({
order: '-created_at',
saved: true,
limit: 25,
})
`api/projects/${teamLogic.values.currentTeamId}/insights/?${toParams({
order: '-created_at',
saved: true,
limit: 25,
})}`
)
actions.setTeamInsightsNext(response.next)
return response.results
@ -159,13 +160,13 @@ export const insightHistoryLogic = kea<insightHistoryLogicType>({
},
listeners: ({ actions, values }) => ({
updateInsight: async ({ insight }) => {
await api.update(`api/insight/${insight.id}`, insight)
await api.update(`api/projects/${teamLogic.values.currentTeamId}/insights/${insight.id}`, insight)
toast('Saved Insight')
actions.updateInsightSuccess(insight)
},
deleteInsight: ({ insight }) => {
deleteWithUndo({
endpoint: 'insight',
endpoint: `api/projects/${values.currentTeamId}/insights`,
object: { name: insight.name, id: insight.id },
callback: () => actions.loadSavedInsights(),
})

View File

@ -4,7 +4,7 @@ import { insightMetadataLogic, InsightMetadataLogicProps } from 'scenes/insights
import { expectLogic, truth } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { insightLogic } from 'scenes/insights/insightLogic'
import { defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { userLogic } from 'scenes/userLogic'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { AvailableFeature } from '~/types'
@ -23,7 +23,7 @@ describe('insightMetadataLogic', () => {
mockAPI(async (url) => {
const { pathname } = url
if (pathname.startsWith('api/insight')) {
if (pathname.startsWith(`api/projects/${MOCK_TEAM_ID}/insight`)) {
return { results: [], next: null }
}
return defaultAPIMocks(url, { availableFeatures: [AvailableFeature.DASHBOARD_COLLABORATION] })

View File

@ -6,6 +6,7 @@ import { NotFound } from 'lib/components/NotFound'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import React from 'react'
import { DashboardItemType } from '~/types'
import { teamLogic } from '../teamLogic'
import { insightRouterLogicType } from './InsightRouterType'
const insightRouterLogic = kea<insightRouterLogicType>({
@ -23,7 +24,7 @@ const insightRouterLogic = kea<insightRouterLogicType>({
},
listeners: ({ actions }) => ({
loadInsight: async ({ id }) => {
const response = await api.get(`api/insight/?short_id=${id}`)
const response = await api.get(`api/projects/${teamLogic.values.currentTeamId}/insights/?short_id=${id}`)
if (response.results.length) {
const item = response.results[0] as DashboardItemType
eventUsageLogic.actions.reportInsightShortUrlVisited(true, item.filters.insight || null)

View File

@ -26,7 +26,7 @@ xdescribe('<Insights /> trends', () => {
cy.intercept('/api/action/', { fixture: 'api/action/actions' })
cy.intercept('/api/cohort/', { fixture: 'api/cohort/cohorts' })
cy.intercept('/api/person/properties/', { fixture: 'api/person/properties' })
cy.interceptLazy('/api/insight/', () => ({ fixture: 'api/insight/trends' })).as('api_insight')
cy.interceptLazy('/api/projects/2/insights/', () => ({ fixture: 'api/insight/trends' })).as('api_insight')
helpers.mockPosthog()
})
@ -79,7 +79,7 @@ xdescribe('<Insights /> trends', () => {
it('can render bar graphs', () => {
mountAndCheckAPI()
cy.overrideInterceptLazy('/api/insight/', () => ({ fixture: 'api/insight/trends/breakdown' }))
cy.overrideInterceptLazy('/api/projects/2/insights/', () => ({ fixture: 'api/insight/trends/breakdown' }))
cy.get('[data-attr=add-breakdown-button]').click()
cy.get('[data-attr=prop-breakdown-select]').click().type('Browser').type('{enter}')

View File

@ -3344,7 +3344,7 @@
}
],
"funnelsLoading": false,
"next": "http://localhost:8000/api/insight/?insight=FUNNELS&limit=5&offset=5&order=-created_at&saved=true",
"next": "http://localhost:8000/api/projects/${currentTeamId}/insights/?insight=FUNNELS&limit=5&offset=5&order=-created_at&saved=true",
"loadingMore": false
}
},

View File

@ -1,4 +1,4 @@
import { defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { insightLogic } from './insightLogic'
@ -16,20 +16,27 @@ describe('insightLogic', () => {
const throwAPIError = (): void => {
throw { status: 0, statusText: 'error from the API' }
}
if (['api/insight/42', 'api/insight/43'].includes(pathname)) {
if (
[`api/projects/${MOCK_TEAM_ID}/insights/42`, `api/projects/${MOCK_TEAM_ID}/insights/43`].includes(pathname)
) {
return {
result: pathname === 'api/insight/42' ? ['result from api'] : null,
id: pathname === 'api/insight/42' ? 42 : 43,
result: pathname.endsWith('42') ? ['result from api'] : null,
id: pathname.endsWith('42') ? 42 : 43,
filters: {
insight: ViewType.TRENDS,
events: [{ id: 3 }],
properties: [{ value: 'a', operator: PropertyOperator.Exact, key: 'a', type: 'a' }],
},
}
} else if (['api/insight/44'].includes(pathname)) {
} else if ([`api/projects/${MOCK_TEAM_ID}/insights/44`].includes(pathname)) {
throwAPIError()
} else if (
['api/insight', 'api/insight/session/', 'api/insight/trend/', 'api/insight/funnel/'].includes(pathname)
[
`api/projects/${MOCK_TEAM_ID}/insights`,
`api/projects/${MOCK_TEAM_ID}/insights/session/`,
`api/projects/${MOCK_TEAM_ID}/insights/trend/`,
`api/projects/${MOCK_TEAM_ID}/insights/funnel/`,
].includes(pathname)
) {
if (searchParams?.events?.[0]?.throw) {
throwAPIError()

View File

@ -20,6 +20,7 @@ import { pollFunnel } from 'scenes/funnels/funnelUtils'
import { preflightLogic } from 'scenes/PreflightCheck/logic'
import { extractObjectDiffKeys } from './utils'
import * as Sentry from '@sentry/browser'
import { teamLogic } from '../teamLogic'
const IS_TEST_MODE = process.env.NODE_ENV === 'test'
@ -38,6 +39,7 @@ export const insightLogic = kea<insightLogicType>({
key: keyForInsightLogicProps('new'),
connect: {
values: [teamLogic, ['currentTeamId']],
logic: [eventUsageLogic, dashboardsModel],
},
@ -94,13 +96,16 @@ export const insightLogic = kea<insightLogicType>({
} as Partial<DashboardItemType>,
{
loadInsight: async ({ id }) => {
return await api.get(`api/insight/${id}`)
return await api.get(`api/projects/${teamLogic.values.currentTeamId}/insights/${id}`)
},
updateInsight: async (payload: Partial<DashboardItemType>, breakpoint) => {
if (!Object.entries(payload).length) {
return
}
const response = await api.update(`api/insight/${values.insight.id}`, payload)
const response = await api.update(
`api/projects/${teamLogic.values.currentTeamId}/insights/${values.insight.id}`,
payload
)
breakpoint()
return { ...response, result: response.result || values.insight.result }
},
@ -127,6 +132,10 @@ export const insightLogic = kea<insightLogicType>({
}
let response
const { currentTeamId } = values
if (!currentTeamId) {
throw new Error("Can't load insight before current project is determined.")
}
try {
if (
insight === ViewType.TRENDS ||
@ -134,23 +143,27 @@ export const insightLogic = kea<insightLogicType>({
insight === ViewType.LIFECYCLE
) {
response = await api.get(
`api/insight/trend/?${toParams(filterTrendsClientSideParams(params))}`,
`api/projects/${currentTeamId}/insights/trend/?${toParams(
filterTrendsClientSideParams(params)
)}`,
cache.abortController.signal
)
} else if (insight === ViewType.SESSIONS || filters?.session) {
response = await api.get(
`api/insight/session/?${toParams(filterTrendsClientSideParams(params))}`,
`api/projects/${currentTeamId}/insights/session/?${toParams(
filterTrendsClientSideParams(params)
)}`,
cache.abortController.signal
)
} else if (insight === ViewType.RETENTION) {
response = await api.get(
`api/insight/retention/?${toParams(params)}`,
`api/projects/${currentTeamId}/insights/retention/?${toParams(params)}`,
cache.abortController.signal
)
} else if (insight === ViewType.FUNNELS) {
response = await pollFunnel(params)
response = await pollFunnel(currentTeamId, params)
} else if (insight === ViewType.PATHS) {
response = await api.create(`api/insight/path`, params)
response = await api.create(`api/projects/${currentTeamId}/insights/path`, params)
} else {
throw new Error(`Can not load insight of type ${insight}`)
}
@ -467,10 +480,13 @@ export const insightLogic = kea<insightLogicType>({
actions.setInsight({ ...values.insight, tags: values.insight.tags?.filter((_tag) => _tag !== tag) })
},
saveInsight: async () => {
const savedInsight = await api.update(`api/insight/${values.insight.id}`, {
...values.insight,
saved: true,
})
const savedInsight = await api.update(
`api/projects/${teamLogic.values.currentTeamId}/insights/${values.insight.id}`,
{
...values.insight,
saved: true,
}
)
actions.setInsight({ ...savedInsight, result: savedInsight.result || values.insight.result })
actions.setInsightMode(ItemMode.View, InsightEventSource.InsightHeader)
toast(
@ -481,7 +497,7 @@ export const insightLogic = kea<insightLogicType>({
)
},
loadInsightSuccess: async ({ payload, insight }) => {
// loaded `/api/insight`, but it didn't have `results`, so make another query
// loaded `/api/projects/:id/insights`, but it didn't have `results`, so make another query
if (!insight.result && values.filters && !payload?.doNotLoadResults) {
actions.loadResults()
}
@ -492,7 +508,7 @@ export const insightLogic = kea<insightLogicType>({
return
}
if (!insight.id) {
const createdInsight = await api.create('api/insight', {
const createdInsight = await api.create(`api/projects/${values.currentTeamId}/insights`, {
filters: insight.filters,
})
breakpoint()

View File

@ -8,7 +8,7 @@ import { organizationLogicType } from './organizationLogicType'
jest.mock('lib/api')
describe('entityFilterLogic', () => {
describe('organizationLogic', () => {
let logic: BuiltLogic<organizationLogicType<OrganizationUpdatePayload>>
mockAPI(async (url) => {
@ -19,9 +19,7 @@ describe('entityFilterLogic', () => {
beforeEach(() => {
window.POSTHOG_APP_CONTEXT = { current_user: { organization: { id: 'WXYZ' } } } as unknown as AppContext
})
afterEach(() => {
delete window.POSTHOG_APP_CONTEXT
})
initKeaTestLogic({
logic: organizationLogic,
onLogic: (l) => {

View File

@ -1,4 +1,4 @@
import { defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { pathsLogic } from 'scenes/paths/pathsLogic'
@ -11,7 +11,7 @@ describe('pathsLogic', () => {
mockAPI(async (url) => {
const { pathname } = url
if (['api/insight/paths/'].includes(pathname)) {
if (`api/projects/${MOCK_TEAM_ID}/insights/paths/` === pathname) {
return { result: ['result from api'] }
}
return defaultAPIMocks(url)
@ -28,6 +28,7 @@ describe('pathsLogic', () => {
it('setFilter calls insightLogic.setFilters', async () => {
await expectLogic(logic, () => {
logic.actions.setFilter({
insight: 'PATHS',
step_limit: 999,
})
})
@ -51,6 +52,7 @@ describe('pathsLogic', () => {
it('insightLogic.setFilters updates filter', async () => {
await expectLogic(logic, () => {
insightLogic(props).actions.setFilters({
insight: 'PATHS',
step_limit: 999,
})
})

View File

@ -7,9 +7,10 @@ describe('<Person /> ', () => {
beforeEach(() => {
cy.intercept('/_preflight/', { fixture: '_preflight' })
cy.intercept('/api/projects/@current/', { fixture: 'api/projects/@current' })
cy.intercept('/api/users/@me/', { fixture: 'api/users/@me' })
cy.intercept('/api/person/', { fixture: 'api/person' }).as('api_person')
cy.intercept('/api/event/?', { fixture: 'api/event/single_person_events' }).as('api_event')
cy.intercept('/api/projects/2/events/?', { fixture: 'api/event/single_person_events' }).as('api_event')
helpers.mockPosthog()
helpers.setLocation('/person/01779064-53be-000c-683f-23b1a8c8eb4c')
@ -38,10 +39,10 @@ describe('<Person /> ', () => {
cy.intercept('/api/dashboard/', { fixture: 'api/dashboard' })
cy.intercept('/api/personal_api_keys/', { fixture: 'api/personal_api_keys' })
cy.intercept('/api/projects/@current/', { fixture: 'api/projects/@current' })
cy.intercept('/api/event/sessions/', { fixture: 'api/event/sessions/session_with_recording' }).as(
'api_sessions'
)
cy.intercept('/api/event/session_recording', { fixture: 'api/event/session_recording' }).as(
cy.intercept('/api/projects/2/events/sessions/', {
fixture: 'api/event/sessions/session_with_recording',
}).as('api_sessions')
cy.intercept('/api/projects/2/events/session_recording', { fixture: 'api/event/session_recording' }).as(
'api_session_recording'
)
})

View File

@ -1,4 +1,4 @@
import { defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { retentionTableLogic } from 'scenes/retention/retentionTableLogic'
@ -11,9 +11,9 @@ describe('retentionTableLogic', () => {
mockAPI(async (url) => {
const { pathname } = url
if (['api/insight', 'api/projects/85/actions/'].includes(pathname)) {
if ([`api/projects/${MOCK_TEAM_ID}/insights/`, `api/projects/${MOCK_TEAM_ID}/actions/`].includes(pathname)) {
return { results: [] }
} else if (pathname === 'api/insight/retention/') {
} else if (pathname === `api/projects/${MOCK_TEAM_ID}/insights/retention/`) {
return { result: ['result from api'] }
}
return defaultAPIMocks(url)
@ -29,37 +29,37 @@ describe('retentionTableLogic', () => {
it('setFilters calls insightLogic.setFilters', async () => {
await expectLogic(logic, () => {
logic.actions.setFilters({ events: [{ id: 42 }] })
logic.actions.setFilters({ insight: 'RETENTION', period: 'Week' })
})
.toDispatchActions([
(action) =>
action.type === insightLogic(props).actionTypes.setFilters &&
action.payload.filters?.events?.[0]?.id === 42,
action.payload.filters?.period === 'Week',
])
.toMatchValues(logic, {
filters: expect.objectContaining({
events: [{ id: 42 }],
period: 'Week',
}),
})
.toMatchValues(insightLogic(props), {
filters: expect.objectContaining({
events: [{ id: 42 }],
period: 'Week',
}),
})
})
it('insightLogic.setFilters updates filters', async () => {
await expectLogic(logic, () => {
insightLogic(props).actions.setFilters({ events: [{ id: 42 }] })
insightLogic(props).actions.setFilters({ insight: 'RETENTION', period: 'Week' })
})
.toMatchValues(logic, {
filters: expect.objectContaining({
events: [{ id: 42 }],
period: 'Week',
}),
})
.toMatchValues(insightLogic(props), {
filters: expect.objectContaining({
events: [{ id: 42 }],
period: 'Week',
}),
})
})

View File

@ -35,6 +35,7 @@ import dayjs from 'dayjs'
import { PageHeader } from 'lib/components/PageHeader'
import { SavedInsightsEmptyState } from 'scenes/insights/EmptyStates'
import { teamLogic } from '../teamLogic'
const { TabPane } = Tabs
@ -64,6 +65,7 @@ export function SavedInsights(): JSX.Element {
const { nameSortedDashboards } = useValues(dashboardsModel)
const { hasDashboardCollaboration } = useValues(organizationLogic)
const { currentTeamId } = useValues(teamLogic)
const { members } = useValues(membersLogic)
const { tab, order, createdBy, layoutView, search, insightType, dateFrom, dateTo } = filters
const insightTypes: InsightType[] = [
@ -210,7 +212,7 @@ export function SavedInsights(): JSX.Element {
onClick={() =>
deleteWithUndo({
object: item,
endpoint: 'insight',
endpoint: `api/projects/${currentTeamId}/insights`,
callback: loadInsights,
})
}

View File

@ -8,6 +8,7 @@ import { prompt } from 'lib/logic/prompt'
import { toast } from 'react-toastify'
import { Dayjs } from 'dayjs'
import { dashboardItemsModel } from '~/models/dashboardItemsModel'
import { teamLogic } from '../teamLogic'
import { urls } from 'scenes/urls'
export interface InsightsResult {
@ -42,6 +43,9 @@ function cleanFilters(values: Partial<SavedInsightFilters>): SavedInsightFilters
}
export const savedInsightsLogic = kea<savedInsightsLogicType<InsightsResult, SavedInsightFilters>>({
connect: {
values: [teamLogic, ['currentTeamId']],
},
actions: {
setSavedInsightsFilters: (filters: Partial<SavedInsightFilters>, merge = true) => ({ filters, merge }),
addGraph: (type: string) => ({ type }),
@ -58,30 +62,31 @@ export const savedInsightsLogic = kea<savedInsightsLogicType<InsightsResult, Sav
await breakpoint(1)
const { filters } = values
const response = await api.get(
'api/insight/?' +
toParams({
order: filters.order,
limit: 15,
saved: true,
...(filters.tab === SavedInsightsTabs.Yours && { user: true }),
...(filters.tab === SavedInsightsTabs.Favorites && { favorited: true }),
...(filters.search && { search: filters.search }),
...(filters.insightType?.toLowerCase() !== 'all types' && {
insight: filters.insightType?.toUpperCase(),
`api/projects/${teamLogic.values.currentTeamId}/insights/?${toParams({
order: filters.order,
limit: 15,
saved: true,
...(filters.tab === SavedInsightsTabs.Yours && { user: true }),
...(filters.tab === SavedInsightsTabs.Favorites && { favorited: true }),
...(filters.search && { search: filters.search }),
...(filters.insightType?.toLowerCase() !== 'all types' && {
insight: filters.insightType?.toUpperCase(),
}),
...(filters.createdBy !== 'All users' && { created_by: filters.createdBy }),
...(filters.dateFrom &&
filters.dateFrom !== 'all' && {
date_from: filters.dateFrom,
date_to: filters.dateTo,
}),
...(filters.createdBy !== 'All users' && { created_by: filters.createdBy }),
...(filters.dateFrom &&
filters.dateFrom !== 'all' && {
date_from: filters.dateFrom,
date_to: filters.dateTo,
}),
})
})}`
)
return response
},
loadPaginatedInsights: async (url: string) => await api.get(url),
updateFavoritedInsight: async ({ id, favorited }) => {
const response = await api.update(`api/insight/${id}`, { favorited })
const response = await api.update(`api/projects/${teamLogic.values.currentTeamId}/insights/${id}`, {
favorited,
})
const updatedInsights = values.insights.results.map((insight) =>
insight.id === id ? response : insight
)
@ -149,14 +154,16 @@ export const savedInsightsLogic = kea<savedInsightsLogicType<InsightsResult, Sav
value: name,
error: 'You must enter name',
success: async (name: string) => {
const insight = await api.update(`api/insight/${id}`, { name })
const insight = await api.update(`api/projects/${teamLogic.values.currentTeamId}/insights/${id}`, {
name,
})
toast('Successfully renamed item')
actions.setInsight(insight)
},
})
},
duplicateInsight: async ({ insight }) => {
await api.create('api/insight', insight)
await api.create(`api/projects/${values.currentTeamId}/insights`, insight)
actions.loadInsights()
},
setDates: () => {

View File

@ -157,6 +157,9 @@ export const sceneConfigurations: Partial<Record<Scene, SceneConfig>> = {
[Scene.Sessions]: {
projectBased: true,
},
[Scene.SessionRecordings]: {
projectBased: true,
},
[Scene.Person]: {
projectBased: true,
},

View File

@ -7,7 +7,7 @@ import {
} from './sessionRecordingsTableLogic'
import { sessionRecordingsTableLogicType } from './sessionRecordingsTableLogicType'
import { BuiltLogic } from 'kea'
import { mockAPI, defaultAPIMocks } from 'lib/api.mock'
import { mockAPI, defaultAPIMocks, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { router } from 'kea-router'
@ -21,7 +21,7 @@ describe('sessionRecordingsTableLogic', () => {
mockAPI(async (url) => {
const { pathname, searchParams } = url
if (pathname === 'api/projects/@current/session_recordings') {
if (pathname === `api/projects/${MOCK_TEAM_ID}/session_recordings`) {
if (searchParams['events'].length > 0 && searchParams['events'][0]['id'] === '$autocapture') {
return {
results: ['List of recordings filtered by events'],

View File

@ -14,6 +14,7 @@ import { router } from 'kea-router'
import dayjs from 'dayjs'
import { RecordingWatchedSource } from 'lib/utils/eventUsageLogic'
import equal from 'fast-deep-equal'
import { teamLogic } from '../teamLogic'
export type SessionRecordingId = string
export type PersonUUID = string
@ -50,6 +51,9 @@ export const sessionRecordingsTableLogic = kea<sessionRecordingsTableLogicType<P
props: {} as {
personUUID?: PersonUUID
},
connect: {
values: [teamLogic, ['currentTeamId']],
},
actions: {
getSessionRecordings: true,
openSessionPlayer: (sessionRecordingId: SessionRecordingId | null, source: RecordingWatchedSource) => ({
@ -83,7 +87,7 @@ export const sessionRecordingsTableLogic = kea<sessionRecordingsTableLogicType<P
}
const params = toParams(paramsDict)
await breakpoint(100) // Debounce for lots of quick filter changes
const response = await api.get(`api/projects/@current/session_recordings?${params}`)
const response = await api.get(`api/projects/${values.currentTeamId}/session_recordings?${params}`)
breakpoint()
return response
},

View File

@ -12,7 +12,7 @@ xdescribe('<Sessions />', () => {
cy.intercept('/api/personal_api_keys/', { fixture: 'api/personal_api_keys' })
cy.intercept('/api/projects/@current/', { fixture: 'api/projects/@current' })
cy.intercept('/api/person/properties/', { fixture: 'api/person/properties' })
cy.interceptLazy('/api/event/sessions/', given.sessions).as('api_sessions')
cy.interceptLazy('/api/projects/2/events/sessions/', given.sessions).as('api_sessions')
helpers.mockPosthog()
helpers.setLocation('/sessions')
@ -108,7 +108,7 @@ xdescribe('<Sessions />', () => {
given('sessions', () => iterateResponses([{ fixture: 'api/event/sessions/session_with_recording' }]))
beforeEach(() => {
cy.intercept('/api/event/session_recording', { fixture: 'api/event/session_recording' }).as(
cy.intercept('/api/projects/2/events/session_recording', { fixture: 'api/event/session_recording' }).as(
'api_session_recording'
)
})

View File

@ -1,5 +1,5 @@
import { sessionsPlayLogic } from 'scenes/sessions/sessionsPlayLogic'
import { api, defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { api, defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { sessionsTableLogic } from 'scenes/sessions/sessionsTableLogic'
@ -10,13 +10,15 @@ import { combineUrl } from 'kea-router'
jest.mock('lib/api')
const EVENTS_SESSION_RECORDING_ENDPOINT = `api/projects/${MOCK_TEAM_ID}/events/session_recording`
describe('sessionsPlayLogic', () => {
let logic: ReturnType<typeof sessionsPlayLogic.build>
mockAPI(async (url) => {
if (
url.pathname === 'api/event/session_recording' || // Old api
url.pathname === 'api/projects/@current/session_recordings' // New api
url.pathname === EVENTS_SESSION_RECORDING_ENDPOINT || // Old api
url.pathname === `api/projects/${MOCK_TEAM_ID}/session_recordings` // New api
) {
return { result: recordingJson }
} else if (url.pathname === 'api/sessions_filter') {
@ -63,29 +65,29 @@ describe('sessionsPlayLogic', () => {
await expectLogic(logic).toMount([eventUsageLogic])
api.get.mockClear()
const firstNext = `api/event/session_recording?session_recording_id=1&offset=200&limit=200`
const secondNext = `api/event/session_recording?session_recording_id=1&offset=400&limit=200`
const thirdNext = `api/event/session_recording?session_recording_id=1&offset=600&limit=200`
const firstNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=200&limit=200`
const secondNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=400&limit=200`
const thirdNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=600&limit=200`
const snaps = recordingJson.snapshots
api.get
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: firstNext } }
}
})
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: secondNext } }
}
})
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: thirdNext } }
}
})
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: recordingJson }
}
})
@ -129,18 +131,18 @@ describe('sessionsPlayLogic', () => {
await expectLogic(logic).toMount([eventUsageLogic])
api.get.mockClear()
const firstNext = `api/event/session_recording?session_recording_id=1&offset=200&limit=200`
const secondNext = `api/event/session_recording?session_recording_id=1&offset=400&limit=200`
const firstNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=200&limit=200`
const secondNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=400&limit=200`
const snaps = recordingJson.snapshots
api.get
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: firstNext } }
}
})
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: secondNext } }
}
})
@ -193,22 +195,22 @@ describe('sessionsPlayLogic', () => {
await expectLogic(preflightLogic).toDispatchActions(['loadPreflightSuccess'])
await expectLogic(logic).toMount([eventUsageLogic])
const firstNext = `api/event/session_recording?session_recording_id=1&offset=200&limit=200`
const secondNext = `api/event/session_recording?session_recording_id=1&offset=400&limit=200`
const firstNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=200&limit=200`
const secondNext = `${EVENTS_SESSION_RECORDING_ENDPOINT}?session_recording_id=1&offset=400&limit=200`
api.get
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: firstNext } }
}
})
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: { ...recordingJson, next: secondNext } }
}
})
.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return { result: recordingJson }
}
})
@ -269,7 +271,7 @@ describe('sessionsPlayLogic', () => {
},
]
api.get.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return {
result: {
...recordingJson,
@ -299,7 +301,7 @@ describe('sessionsPlayLogic', () => {
},
]
api.get.mockImplementationOnce(async (url: string) => {
if (combineUrl(url).pathname === 'api/event/session_recording') {
if (combineUrl(url).pathname === EVENTS_SESSION_RECORDING_ENDPOINT) {
return {
result: {
...recordingJson,

View File

@ -8,13 +8,20 @@ import { EventIndex } from '@posthog/react-rrweb-player'
import { sessionsTableLogic } from 'scenes/sessions/sessionsTableLogic'
import { toast } from 'react-toastify'
import { eventUsageLogic, RecordingWatchedSource } from 'lib/utils/eventUsageLogic'
import { teamLogic } from '../teamLogic'
import { eventWithTime } from 'rrweb/typings/types'
const IS_TEST_MODE = process.env.NODE_ENV === 'test'
export const sessionsPlayLogic = kea<sessionsPlayLogicType>({
connect: {
logic: [eventUsageLogic],
values: [sessionsTableLogic, ['sessions', 'pagination', 'orderedSessionRecordingIds', 'loadedSessionEvents']],
values: [
sessionsTableLogic,
['sessions', 'pagination', 'orderedSessionRecordingIds', 'loadedSessionEvents'],
teamLogic,
['currentTeamId'],
],
actions: [
sessionsTableLogic,
['fetchNextSessions', 'appendNewSessions', 'closeSessionPlayer', 'loadSessionEvents'],
@ -153,7 +160,7 @@ export const sessionsPlayLogic = kea<sessionsPlayLogicType>({
} else {
// Very first call
const params = toParams({ session_recording_id: sessionRecordingId, save_view: true })
response = await api.get(`api/event/session_recording?${params}`)
response = await api.get(`api/projects/${values.currentTeamId}/events/session_recording?${params}`)
actions.reportUsage(response.result, performance.now() - startTime)
}
const currData = values.sessionPlayerData

View File

@ -10,6 +10,7 @@ import { sessionsFiltersLogic } from 'scenes/sessions/filters/sessionsFiltersLog
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { FEATURE_FLAGS } from 'lib/constants'
import { RecordingWatchedSource } from 'lib/utils/eventUsageLogic'
import { teamLogic } from '../teamLogic'
type SessionRecordingId = string
@ -49,7 +50,9 @@ export const sessionsTableLogic = kea<sessionsTableLogicType<SessionRecordingId>
properties: values.properties,
})
await breakpoint(10)
const response = await api.get(`api/event/sessions/?${params}`)
const response = await api.get(
`api/projects/${teamLogic.values.currentTeamId}/events/sessions/?${params}`
)
breakpoint()
actions.setPagination(response.pagination)
return response.result
@ -207,7 +210,7 @@ export const sessionsTableLogic = kea<sessionsTableLogicType<SessionRecordingId>
filters: values.filters,
properties: values.properties,
})
const response = await api.get(`api/event/sessions/?${params}`)
const response = await api.get(`api/projects/${teamLogic.values.currentTeamId}/events/sessions/?${params}`)
breakpoint()
actions.setPagination(response.pagination)
actions.appendNewSessions(response.result)
@ -237,7 +240,9 @@ export const sessionsTableLogic = kea<sessionsTableLogicType<SessionRecordingId>
await breakpoint(200)
const response = await api.get(`api/event/session_events?${toParams(params)}`)
const response = await api.get(
`api/projects/${teamLogic.values.currentTeamId}/events/session_events?${toParams(params)}`
)
actions.addSessionEvents(session, response.result)
}
},

View File

@ -1,5 +1,5 @@
import { BuiltLogic } from 'kea'
import { defaultAPIMocks, mockAPI } from 'lib/api.mock'
import { defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
import { expectLogic } from 'kea-test-utils'
import { initKeaTestLogic } from '~/test/init'
import { trendsLogic } from 'scenes/trends/trendsLogic'
@ -14,9 +14,15 @@ describe('trendsLogic', () => {
mockAPI(async (url) => {
const { pathname } = url
if (['api/insight'].includes(pathname)) {
if (pathname === `api/projects/${MOCK_TEAM_ID}/insights`) {
return { results: [] }
} else if (['api/insight/123', 'api/insight/session/', 'api/insight/trend/'].includes(pathname)) {
} else if (
[
`api/projects/${MOCK_TEAM_ID}/insights/123`,
`api/projects/${MOCK_TEAM_ID}/insights/session/`,
`api/projects/${MOCK_TEAM_ID}/insights/trend/`,
].includes(pathname)
) {
return { result: ['result from api'] }
}
return defaultAPIMocks(url)

View File

@ -3,6 +3,8 @@ import { initKea } from '~/initKea'
import { testUtilsPlugin, expectLogic } from 'kea-test-utils'
import { createMemoryHistory } from 'history'
import posthog from 'posthog-js'
import { AppContext } from '../types'
import { MOCK_TEAM_ID } from '../lib/api.mock'
export function initKeaTestLogic<L extends Logic = Logic>({
logic,
@ -17,6 +19,10 @@ export function initKeaTestLogic<L extends Logic = Logic>({
let unmount: () => void
beforeEach(async () => {
window.POSTHOG_APP_CONTEXT = {
current_team: { id: MOCK_TEAM_ID },
...window.POSTHOG_APP_CONTEXT,
} as unknown as AppContext
posthog.init('no token', {
api_host: 'borked',
test: true,
@ -45,5 +51,6 @@ export function initKeaTestLogic<L extends Logic = Logic>({
unmount()
await expectLogic(logic).toFinishAllListeners()
}
delete window.POSTHOG_APP_CONTEXT
})
}

View File

@ -189,7 +189,11 @@ export const actionsTabLogic = kea<actionsTabLogicType<ActionFormInstance>>({
Insights
</a>{' '}
-{' '}
<a href={`${apiURL}/action/${response.id}`} target="_blank" rel="noreferrer noopener">
<a
href={`${apiURL}/projects/@current/actions/${response.id}`}
target="_blank"
rel="noreferrer noopener"
>
Actions
</a>
</>

View File

@ -1,4 +1,4 @@
// /api/event/?event=$autocapture&properties[pathname]=/docs/introduction/what-is-kea
// /api/projects/@current/events/?event=$autocapture&properties[pathname]=/docs/introduction/what-is-kea
import { kea } from 'kea'
import { encodeParams } from 'kea-router'
import { currentPageLogic } from '~/toolbar/stats/currentPageLogic'

View File

@ -20,7 +20,7 @@ export const featureFlagsLogic = kea<featureFlagsLogicType>({
[] as CombinedFeatureFlagAndOverrideType[],
{
getUserFlags: async (_, breakpoint) => {
const response = await toolbarFetch('/api/feature_flag/my_flags')
const response = await toolbarFetch('/api/projects/@current/feature_flags/my_flags')
breakpoint()
if (!response.ok) {
return []

View File

@ -42,7 +42,7 @@ router = DefaultRouterPlusPlus()
# Legacy endpoints shared (to be removed eventually)
router.register(r"annotation", annotation.LegacyAnnotationsViewSet) # Should be completely unused now
router.register(r"feature_flag", feature_flag.LegacyFeatureFlagViewSet)
router.register(r"feature_flag", feature_flag.LegacyFeatureFlagViewSet) # Should be completely unused now
router.register(r"dashboard", dashboard.LegacyDashboardsViewSet)
router.register(r"dashboard_item", dashboard.LegacyDashboardItemsViewSet)
router.register(r"plugin_config", plugin.LegacyPluginConfigViewSet)
@ -112,8 +112,8 @@ if is_clickhouse_enabled():
# Legacy endpoints CH (to be removed eventually)
router.register(r"action", LegacyClickhouseActionsViewSet, basename="action") # Should be completely unused now
router.register(r"event", LegacyClickhouseEventsViewSet, basename="event")
router.register(r"insight", LegacyClickhouseInsightsViewSet, basename="insight")
router.register(r"event", LegacyClickhouseEventsViewSet, basename="event") # Should be completely unused now
router.register(r"insight", LegacyClickhouseInsightsViewSet, basename="insight") # Should be completely unused now
router.register(r"person", LegacyClickhousePersonViewSet, basename="person")
router.register(r"paths", LegacyClickhousePathsViewSet, basename="paths")
router.register(r"element", LegacyClickhouseElementViewSet, basename="element")
@ -131,10 +131,10 @@ if is_clickhouse_enabled():
)
else:
# Legacy endpoints PG (to be removed eventually)
router.register(r"insight", insight.LegacyInsightViewSet)
router.register(r"insight", insight.LegacyInsightViewSet) # Should be completely unused now
router.register(r"action", action.LegacyActionViewSet) # Should be completely unused now
router.register(r"person", person.LegacyPersonViewSet)
router.register(r"event", event.LegacyEventViewSet)
router.register(r"event", event.LegacyEventViewSet) # Should be completely unused now
router.register(r"paths", paths.LegacyPathsViewSet, basename="paths")
router.register(r"element", element.LegacyElementViewSet)
router.register(r"cohort", cohort.LegacyCohortViewSet)

View File

@ -280,7 +280,7 @@ class EventViewSet(StructuredViewSetMixin, mixins.RetrieveModelMixin, mixins.Lis
return [{"name": convert_property_value(value)} for value in flattened]
# ******************************************
# /event/sessions
# /events/sessions
#
# params:
# - pagination: (dict) Object containing information about pagination (offset, last page info)
@ -306,7 +306,7 @@ class EventViewSet(StructuredViewSetMixin, mixins.RetrieveModelMixin, mixins.Lis
return Response({"result": SessionsListEvents().run(filter=filter, team=self.team)})
# ******************************************
# /event/session_recording
# /events/session_recording
# params:
# - session_recording_id: (string) id of the session recording
# - save_view: (boolean) save view of the recording

View File

@ -188,17 +188,17 @@ class InsightViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
# ******************************************
# Calculated Insight Endpoints
# /insight/trend
# /insight/session
# /insight/funnel
# /insight/retention
# /insight/path
# /projects/:id/insights/trend
# /projects/:id/insights/session
# /projects/:id/insights/funnel
# /projects/:id/insights/retention
# /projects/:id/insights/path
#
# Request parameteres and caching are handled here and passed onto respective .queries classes
# ******************************************
# ******************************************
# /insight/trend
# /projects/:id/insights/trend
#
# params:
# - from_dashboard: (string) determines trend is being retrieved from dashboard item to update dashboard_item metadata
@ -230,7 +230,7 @@ class InsightViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
return {"result": result}
# ******************************************
# /insight/session
# /projects/:id/insights/session
#
# params:
# - session: (string: avg, dist) specifies session type
@ -246,7 +246,7 @@ class InsightViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
return {"result": result}
# ******************************************
# /insight/funnel
# /projects/:id/insights/funnel
# The funnel endpoint is asynchronously processed. When a request is received, the endpoint will
# call an async task with an id that can be continually polled for 3 minutes.
#
@ -291,7 +291,7 @@ class InsightViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
return {"result": result}
# ******************************************
# /insight/retention
# /projects/:id/insights/retention
# params:
# - start_entity: (dict) specifies id and type of the entity to focus retention on
# - **shared filter types
@ -312,7 +312,7 @@ class InsightViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
return {"result": result}
# ******************************************
# /insight/path
# /projects/:id/insights/path
# params:
# - start: (string) specifies the name of the starting property or element
# - request_type: (string: $pageview, $autocapture, $screen, custom_event) specifies the path type

View File

@ -125,7 +125,7 @@ class TestDashboard(APIBaseTest):
# cache results
response = self.client.get(
"/api/insight/trend/?events=%s&properties=%s"
f"/api/projects/{self.team.id}/insights/trend/?events=%s&properties=%s"
% (json.dumps(filter_dict["events"]), json.dumps(filter_dict["properties"]))
)
self.assertEqual(response.status_code, 200)
@ -404,7 +404,7 @@ class TestDashboard(APIBaseTest):
dashboard=dashboard, filters=filter.to_dict(), team=self.team,
)
self.client.get(
"/api/insight/trend/?events=%s&properties=%s&date_from=-7d"
f"/api/projects/{self.team.id}/insights/trend/?events=%s&properties=%s&date_from=-7d"
% (json.dumps(filter_dict["events"]), json.dumps(filter_dict["properties"]))
)
patch_response = self.client.patch(
@ -414,7 +414,7 @@ class TestDashboard(APIBaseTest):
# cache results
response = self.client.get(
"/api/insight/trend/?events=%s&properties=%s&date_from=-24h"
f"/api/projects/{self.team.id}/insights/trend/?events=%s&properties=%s&date_from=-24h"
% (json.dumps(filter_dict["events"]), json.dumps(filter_dict["properties"]))
)
self.assertEqual(response.status_code, 200)
@ -426,7 +426,7 @@ class TestDashboard(APIBaseTest):
def test_invalid_properties(self):
properties = "invalid_json"
response = self.client.get(f"/api/insight/trend/?properties={properties}")
response = self.client.get(f"/api/projects/{self.team.id}/insights/trend/?properties={properties}")
self.assertEqual(response.status_code, 400)
self.assertDictEqual(

View File

@ -59,7 +59,7 @@ def factory_test_event_api(event_factory, person_factory, _):
expected_queries += 7
with self.assertNumQueries(expected_queries):
response = self.client.get("/api/event/?distinct_id=2").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?distinct_id=2").json()
self.assertEqual(
response["results"][0]["person"],
{"distinct_ids": ["2"], "is_identified": True, "properties": {"email": "tim@posthog.com"}},
@ -84,7 +84,7 @@ def factory_test_event_api(event_factory, person_factory, _):
expected_queries += 4 # PostHog event, PostHog event, PostHog person, PostHog person distinct ID
with self.assertNumQueries(expected_queries):
response = self.client.get("/api/event/?event=event_name").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?event=event_name").json()
self.assertEqual(response["results"][0]["event"], "event_name")
def test_filter_events_by_properties(self):
@ -104,13 +104,14 @@ def factory_test_event_api(event_factory, person_factory, _):
with self.assertNumQueries(expected_queries):
response = self.client.get(
"/api/event/?properties=%s" % (json.dumps([{"key": "$browser", "value": "Safari"}]))
f"/api/projects/{self.team.id}/events/?properties=%s"
% (json.dumps([{"key": "$browser", "value": "Safari"}]))
).json()
self.assertEqual(response["results"][0]["id"], event2.pk)
properties = "invalid_json"
response = self.client.get(f"/api/event/?properties={properties}")
response = self.client.get(f"/api/projects/{self.team.id}/events/?properties={properties}")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(
@ -145,7 +146,7 @@ def factory_test_event_api(event_factory, person_factory, _):
with self.settings(USE_PRECALCULATED_CH_COHORT_PEOPLE=True): # Normally this is False in tests
with freeze_time("2020-01-04T13:01:01Z"):
response = self.client.get(
"/api/event/?properties=%s"
f"/api/projects/{self.team.id}/events/?properties=%s"
% (json.dumps([{"key": "id", "value": cohort1.id, "type": "cohort"}]))
).json()
@ -164,12 +165,12 @@ def factory_test_event_api(event_factory, person_factory, _):
event="random event", team=self.team, distinct_id="some-other-one", properties={"$ip": "8.8.8.8"}
)
response = self.client.get(f"/api/event/?person_id={person.pk}").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?person_id={person.pk}").json()
self.assertEqual(len(response["results"]), 2)
self.assertEqual(response["results"][0]["elements"], [])
def test_filter_by_nonexisting_person(self):
response = self.client.get(f"/api/event/?person_id=5555555555")
response = self.client.get(f"/api/projects/{self.team.id}/events/?person_id=5555555555")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()["results"]), 0)
@ -222,7 +223,7 @@ def factory_test_event_api(event_factory, person_factory, _):
team=self.team,
properties={"random_prop": "don't include", "some other prop": "with some text"},
)
response = self.client.get("/api/event/values/?key=custom_event").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/values/?key=custom_event").json()
self.assertListEqual(sorted(events), sorted([event["name"] for event in response]))
def test_event_property_values(self):
@ -274,7 +275,7 @@ def factory_test_event_api(event_factory, person_factory, _):
team2 = Organization.objects.bootstrap(None)[2]
event_factory(distinct_id="bla", event="random event", team=team2, properties={"random_prop": "abcd"})
response = self.client.get("/api/event/values/?key=random_prop").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/values/?key=random_prop").json()
keys = [resp["name"].replace(" ", "") for resp in response]
self.assertCountEqual(
@ -293,10 +294,14 @@ def factory_test_event_api(event_factory, person_factory, _):
)
self.assertEqual(len(response), 9)
response = self.client.get("/api/event/values/?key=random_prop&value=qw").json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/values/?key=random_prop&value=qw"
).json()
self.assertEqual(response[0]["name"], "qwerty")
response = self.client.get("/api/event/values/?key=random_prop&value=6").json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/values/?key=random_prop&value=6"
).json()
self.assertEqual(response[0]["name"], "565")
def test_before_and_after(self):
@ -317,20 +322,24 @@ def factory_test_event_api(event_factory, person_factory, _):
ActionStep.objects.create(action=action, event="sign up")
action.calculate_events()
response = self.client.get("/api/event/?after=2020-01-09T00:00:00.000Z&action_id=%s" % action.pk).json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/?after=2020-01-09T00:00:00.000Z&action_id=%s" % action.pk
).json()
self.assertEqual(len(response["results"]), 1)
self.assertEqual(response["results"][0]["id"], event1.pk)
response = self.client.get("/api/event/?before=2020-01-09T00:00:00.000Z&action_id=%s" % action.pk).json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/?before=2020-01-09T00:00:00.000Z&action_id=%s" % action.pk
).json()
self.assertEqual(len(response["results"]), 1)
self.assertEqual(response["results"][0]["id"], event2.pk)
# without action
response = self.client.get("/api/event/?after=2020-01-09T00:00:00.000Z").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?after=2020-01-09T00:00:00.000Z").json()
self.assertEqual(len(response["results"]), 1)
self.assertEqual(response["results"][0]["id"], event1.pk)
response = self.client.get("/api/event/?before=2020-01-09T00:00:00.000Z").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?before=2020-01-09T00:00:00.000Z").json()
self.assertEqual(len(response["results"]), 2)
self.assertEqual(response["results"][0]["id"], event2.pk)
self.assertEqual(response["results"][1]["id"], event3.pk)
@ -344,9 +353,11 @@ def factory_test_event_api(event_factory, person_factory, _):
distinct_id="1",
timestamp=timezone.now() - relativedelta(months=11) + relativedelta(days=idx, seconds=idx),
)
response = self.client.get("/api/event/?distinct_id=1").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?distinct_id=1").json()
self.assertEqual(len(response["results"]), 100)
self.assertIn("http://testserver/api/event/?distinct_id=1&before=", response["next"])
self.assertIn(
f"http://testserver/api/projects/{self.team.id}/events/?distinct_id=1&before=", response["next"]
)
page2 = self.client.get(response["next"]).json()
from posthog.utils import is_clickhouse_enabled
@ -367,13 +378,13 @@ def factory_test_event_api(event_factory, person_factory, _):
action = Action.objects.create(team=self.team)
action.calculate_events()
response = self.client.get("/api/event/?action_id=%s" % action.pk)
response = self.client.get(f"/api/projects/{self.team.id}/events/?action_id=%s" % action.pk)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()["results"]), 0)
def test_get_single_action(self):
event1 = event_factory(team=self.team, event="sign up", distinct_id="2", properties={"key": "test_val"})
response = self.client.get("/api/event/%s/" % event1.id)
response = self.client.get(f"/api/projects/{self.team.id}/events/%s/" % event1.id)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["event"], "sign up")
self.assertEqual(response.json()["properties"], {"key": "test_val"})
@ -394,11 +405,13 @@ def factory_test_event_api(event_factory, person_factory, _):
event_factory(team=self.team, event="4th action", distinct_id="2", properties={"$os": "Windows 95"})
with freeze_time("2012-01-15T04:01:34.000Z"):
response = self.client.get("/api/event/sessions/",).json()
response = self.client.get(f"/api/projects/{self.team.id}/events/sessions/",).json()
self.assertEqual(len(response["result"]), 2)
response = self.client.get("/api/event/sessions/?date_from=2012-01-14&date_to=2012-01-15",).json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/sessions/?date_from=2012-01-14&date_to=2012-01-15",
).json()
self.assertEqual(len(response["result"]), 4)
# 4 sessions were already created above
@ -406,7 +419,9 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time(relative_date_parse("2012-01-15T04:01:34.000Z") + relativedelta(hours=i)):
event_factory(team=self.team, event="action {}".format(i), distinct_id=str(i + 3))
response = self.client.get("/api/event/sessions/?date_from=2012-01-14&date_to=2012-01-17",).json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/sessions/?date_from=2012-01-14&date_to=2012-01-17",
).json()
self.assertEqual(len(response["result"]), SESSIONS_LIST_DEFAULT_LIMIT)
self.assertIsNone(response.get("pagination"))
@ -414,16 +429,18 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time(relative_date_parse("2012-01-15T04:01:34.000Z") + relativedelta(hours=i + 46)):
event_factory(team=self.team, event="action {}".format(i), distinct_id=str(i + 49))
response = self.client.get("/api/event/sessions/?date_from=2012-01-14&date_to=2012-01-17",).json()
response = self.client.get(
f"/api/projects/{self.team.id}/events/sessions/?date_from=2012-01-14&date_to=2012-01-17",
).json()
self.assertEqual(len(response["result"]), SESSIONS_LIST_DEFAULT_LIMIT)
self.assertIsNotNone(response["pagination"])
def test_events_nonexistent_cohort_handling(self):
response_nonexistent_property = self.client.get(
f"/api/event/sessions/?filters={json.dumps([{'type':'property','key':'abc','value':'xyz'}])}"
f"/api/projects/{self.team.id}/events/sessions/?filters={json.dumps([{'type':'property','key':'abc','value':'xyz'}])}"
).json()
response_nonexistent_cohort = self.client.get(
f"/api/event/sessions/?filters={json.dumps([{'type':'cohort','key':'id','value':2137}])}"
f"/api/projects/{self.team.id}/events/sessions/?filters={json.dumps([{'type':'cohort','key':'id','value':2137}])}"
).json()
self.assertEqual(response_nonexistent_property, response_nonexistent_cohort) # Both cases just empty
@ -447,7 +464,9 @@ def factory_test_event_api(event_factory, person_factory, _):
event_factory(team=self.team, event="4th action", distinct_id="2", properties={"$os": "Windows 95"})
with freeze_time("2012-01-15T04:01:34.000Z"):
response_person_1 = self.client.get("/api/event/sessions/?distinct_id=1",).json()
response_person_1 = self.client.get(
f"/api/projects/{self.team.id}/events/sessions/?distinct_id=1",
).json()
self.assertEqual(len(response_person_1["result"]), 1)
@ -458,7 +477,7 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time("2012-01-15T04:01:44.000Z"):
event_factory(team=self.team, event="5th action", distinct_id="2", properties={"$os": "Windows 95"})
with freeze_time("2012-01-15T04:01:34.000Z"):
response = self.client.get("/api/event/").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/").json()
self.assertEqual(len(response["results"]), 1)
def test_session_events(self):
@ -482,7 +501,7 @@ def factory_test_event_api(event_factory, person_factory, _):
event_factory(team=self.team, event="4th action", distinct_id="1", properties={"$os": "Mac OS X"})
response = self.client.get(
f"/api/event/session_events?distinct_id=1&date_from=2012-01-14T03:25:34&date_to=2012-01-15T04:00:00"
f"/api/projects/{self.team.id}/events/session_events?distinct_id=1&date_from=2012-01-14T03:25:34&date_to=2012-01-15T04:00:00"
).json()
self.assertEqual(len(response["result"]), 2)
self.assertEqual(response["result"][0]["event"], "2nd action")
@ -493,7 +512,7 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time("2012-01-15T04:01:34.000Z"):
for _ in range(12):
event_factory(team=self.team, event="5th action", distinct_id="2", properties={"$os": "Windows 95"})
response = self.client.get("/api/event.csv?limit=5")
response = self.client.get(f"/api/projects/{self.team.id}/events.csv?limit=5")
self.assertEqual(
len(response.content.splitlines()), 6, "CSV export should return up to limit=5 events (+ headers row)",
)
@ -503,7 +522,7 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time("2012-01-15T04:01:34.000Z"):
for _ in range(12):
event_factory(team=self.team, event="5th action", distinct_id="2", properties={"$os": "Windows 95"})
response = self.client.get("/api/event.csv")
response = self.client.get(f"/api/projects/{self.team.id}/events.csv")
self.assertEqual(
len(response.content.splitlines()),
11,
@ -515,7 +534,7 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time("2012-01-15T04:01:34.000Z"):
for _ in range(12):
event_factory(team=self.team, event="5th action", distinct_id="2", properties={"$os": "Windows 95"})
response = self.client.get("/api/event.csv")
response = self.client.get(f"/api/projects/{self.team.id}/events.csv")
self.assertEqual(
len(response.content.splitlines()),
11,
@ -527,7 +546,7 @@ def factory_test_event_api(event_factory, person_factory, _):
with freeze_time("2012-01-15T04:01:34.000Z"):
for _ in range(12):
event_factory(team=self.team, event="5th action", distinct_id="2", properties={"$os": "Windows 95"})
response = self.client.get("/api/event.csv?limit=100")
response = self.client.get(f"/api/projects/{self.team.id}/events.csv?limit=100")
self.assertEqual(
len(response.content.splitlines()),
11,
@ -553,15 +572,15 @@ def factory_test_event_api(event_factory, person_factory, _):
else:
event_factory(team=self.team, event="event", distinct_id="1", timestamp=timezone.now(), id=event_id)
response = self.client.get(f"/api/event/{event_id}",)
response = self.client.get(f"/api/projects/{self.team.id}/events/{event_id}",)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["event"], "event")
response = self.client.get(f"/api/event/123456",)
response = self.client.get(f"/api/projects/{self.team.id}/events/123456",)
# EE will inform the user the ID passed is not a valid UUID
self.assertIn(response.status_code, [status.HTTP_404_NOT_FOUND, status.HTTP_400_BAD_REQUEST])
response = self.client.get(f"/api/event/im_a_string_not_an_integer",)
response = self.client.get(f"/api/projects/{self.team.id}/events/im_a_string_not_an_integer",)
self.assertIn(response.status_code, [status.HTTP_404_NOT_FOUND, status.HTTP_400_BAD_REQUEST])
def test_limit(self):
@ -586,10 +605,10 @@ def factory_test_event_api(event_factory, person_factory, _):
event="$pageview", team=self.team, distinct_id="some-other-one", properties={"$ip": "8.8.8.8"}
)
response = self.client.get("/api/event/?limit=1").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?limit=1").json()
self.assertEqual(1, len(response["results"]))
response = self.client.get("/api/event/?limit=2").json()
response = self.client.get(f"/api/projects/{self.team.id}/events/?limit=2").json()
self.assertEqual(2, len(response["results"]))
def test_get_events_with_specified_token(self):
@ -602,16 +621,22 @@ def factory_test_event_api(event_factory, person_factory, _):
event1 = event_factory(team=self.team, event="sign up", distinct_id="2", properties={"key": "test_val"})
event2 = event_factory(team=user2.team, event="sign up", distinct_id="2", properties={"key": "test_val"})
response_team1 = self.client.get(f"/api/event/{event1.id}/")
response_team1_token = self.client.get(f"/api/event/{event1.id}/", data={"token": self.team.api_token})
response_team1 = self.client.get(f"/api/projects/{self.team.id}/events/{event1.id}/")
response_team1_token = self.client.get(
f"/api/projects/{self.team.id}/events/{event1.id}/", data={"token": self.team.api_token}
)
response_team2_event1 = self.client.get(f"/api/event/{event1.id}/", data={"token": user2.team.api_token})
response_team2_event1 = self.client.get(
f"/api/projects/{self.team.id}/events/{event1.id}/", data={"token": user2.team.api_token}
)
# The feature being tested here is usually used with personal API token auth,
# but logging in works the same way and is more to the point in the test
self.client.force_login(user2)
response_team2_event2 = self.client.get(f"/api/event/{event2.id}/", data={"token": user2.team.api_token})
response_team2_event2 = self.client.get(
f"/api/projects/{self.team.id}/events/{event2.id}/", data={"token": user2.team.api_token}
)
self.assertEqual(response_team1.status_code, status.HTTP_200_OK)
self.assertEqual(response_team1_token.status_code, status.HTTP_200_OK)
@ -620,7 +645,7 @@ def factory_test_event_api(event_factory, person_factory, _):
self.assertEqual(response_team2_event1.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response_team2_event2.status_code, status.HTTP_200_OK)
response_invalid_token = self.client.get(f"/api/event?token=invalid")
response_invalid_token = self.client.get(f"/api/projects/{self.team.id}/events?token=invalid")
self.assertEqual(response_invalid_token.status_code, 401)
return TestEvents

View File

@ -19,7 +19,9 @@ class TestFeatureFlag(APIBaseTest):
def test_cant_create_flag_with_duplicate_key(self):
count = FeatureFlag.objects.count()
# Make sure the endpoint works with and without the trailing slash
response = self.client.post("/api/feature_flag", {"name": "Beta feature", "key": "red_button"})
response = self.client.post(
f"/api/projects/{self.team.id}/feature_flags", {"name": "Beta feature", "key": "red_button"}
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
@ -37,7 +39,8 @@ class TestFeatureFlag(APIBaseTest):
team=self.team, rollout_percentage=50, name="some feature", key="some-feature", created_by=self.user,
)
response = self.client.patch(
f"/api/feature_flag/{another_feature_flag.pk}", {"name": "Beta feature", "key": "red_button"},
f"/api/projects/{self.team.id}/feature_flags/{another_feature_flag.pk}",
{"name": "Beta feature", "key": "red_button"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
@ -54,7 +57,8 @@ class TestFeatureFlag(APIBaseTest):
# Try updating the existing one
response = self.client.patch(
f"/api/feature_flag/{self.feature_flag.id}/", {"name": "Beta feature 3", "key": "red_button"},
f"/api/projects/{self.team.id}/feature_flags/{self.feature_flag.id}/",
{"name": "Beta feature 3", "key": "red_button"},
)
self.assertEqual(response.status_code, 200)
self.feature_flag.refresh_from_db()
@ -62,7 +66,7 @@ class TestFeatureFlag(APIBaseTest):
def test_is_simple_flag(self):
feature_flag = self.client.post(
"/api/feature_flag/",
f"/api/projects/{self.team.id}/feature_flags/",
data={
"name": "Beta feature",
"key": "beta-feature",
@ -86,7 +90,7 @@ class TestFeatureFlag(APIBaseTest):
def test_create_feature_flag(self, mock_capture):
response = self.client.post(
"/api/feature_flag/",
f"/api/projects/{self.team.id}/feature_flags/",
{"name": "Alpha feature", "key": "alpha-feature", "filters": {"groups": [{"rollout_percentage": 50}]}},
format="json",
)
@ -112,7 +116,9 @@ class TestFeatureFlag(APIBaseTest):
@patch("posthoganalytics.capture")
def test_create_minimal_feature_flag(self, mock_capture):
response = self.client.post("/api/feature_flag/", {"key": "omega-feature"}, format="json")
response = self.client.post(
f"/api/projects/{self.team.id}/feature_flags/", {"key": "omega-feature"}, format="json"
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["key"], "omega-feature")
self.assertEqual(response.json()["name"], "")
@ -139,7 +145,7 @@ class TestFeatureFlag(APIBaseTest):
def test_create_multivariate_feature_flag(self, mock_capture):
response = self.client.post(
"/api/feature_flag/",
f"/api/projects/{self.team.id}/feature_flags/",
{
"name": "Multivariate feature",
"key": "multivariate-feature",
@ -177,7 +183,7 @@ class TestFeatureFlag(APIBaseTest):
def test_cant_create_multivariate_feature_flag_with_variant_rollout_lt_100(self):
response = self.client.post(
"/api/feature_flag/",
f"/api/projects/{self.team.id}/feature_flags/",
{
"name": "Multivariate feature",
"key": "multivariate-feature",
@ -202,7 +208,7 @@ class TestFeatureFlag(APIBaseTest):
def test_cant_create_multivariate_feature_flag_with_variant_rollout_gt_100(self):
response = self.client.post(
"/api/feature_flag/",
f"/api/projects/{self.team.id}/feature_flags/",
{
"name": "Multivariate feature",
"key": "multivariate-feature",
@ -227,7 +233,7 @@ class TestFeatureFlag(APIBaseTest):
def test_cant_create_feature_flag_without_key(self):
count = FeatureFlag.objects.count()
response = self.client.post("/api/feature_flag/", format="json")
response = self.client.post(f"/api/projects/{self.team.id}/feature_flags/", format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
@ -240,7 +246,7 @@ class TestFeatureFlag(APIBaseTest):
instance = self.feature_flag
response = self.client.patch(
f"/api/feature_flag/{instance.pk}",
f"/api/projects/{self.team.id}/feature_flags/{instance.pk}",
{
"name": "Updated name",
"filters": {
@ -283,7 +289,7 @@ class TestFeatureFlag(APIBaseTest):
self.client.force_login(new_user)
with patch("posthoganalytics.capture") as mock_capture:
response = self.client.delete(f"/api/feature_flag/{instance.pk}/")
response = self.client.delete(f"/api/projects/{self.team.id}/feature_flags/{instance.pk}/")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(FeatureFlag.objects.filter(pk=instance.pk).exists())
@ -305,10 +311,10 @@ class TestFeatureFlag(APIBaseTest):
@patch("posthoganalytics.capture")
def test_cannot_delete_feature_flag_on_another_team(self, mock_capture):
_, _, user = User.objects.bootstrap("Test", "team2@posthog.com", None)
self.client.force_login(user)
_, other_team, other_user = User.objects.bootstrap("Test", "team2@posthog.com", None)
self.client.force_login(other_user)
response = self.client.delete(f"/api/feature_flag/{self.feature_flag.pk}/")
response = self.client.delete(f"/api/projects/{other_team.id}/feature_flags/{self.feature_flag.pk}/")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(FeatureFlag.objects.filter(pk=self.feature_flag.pk).exists())
@ -321,20 +327,22 @@ class TestFeatureFlag(APIBaseTest):
assert self.team is not None
self.assertNotEqual(user.team.id, self.team.id)
response_team_1 = self.client.get(f"/api/feature_flag")
response_team_1_token = self.client.get(f"/api/feature_flag?token={user.team.api_token}")
response_team_2 = self.client.get(f"/api/feature_flag?token={self.team.api_token}")
response_team_1 = self.client.get(f"/api/projects/@current/feature_flags")
response_team_1_token = self.client.get(f"/api/projects/@current/feature_flags?token={user.team.api_token}")
response_team_2 = self.client.get(f"/api/projects/@current/feature_flags?token={self.team.api_token}")
self.assertEqual(response_team_1.json(), response_team_1_token.json())
self.assertNotEqual(response_team_1.json(), response_team_2.json())
response_invalid_token = self.client.get(f"/api/feature_flag?token=invalid")
response_invalid_token = self.client.get(f"/api/projects/@current/feature_flags?token=invalid")
self.assertEqual(response_invalid_token.status_code, 401)
def test_creating_a_feature_flag_with_same_team_and_key_after_deleting(self):
FeatureFlag.objects.create(team=self.team, created_by=self.user, key="alpha-feature", deleted=True)
response = self.client.post("/api/feature_flag/", {"name": "Alpha feature", "key": "alpha-feature"})
response = self.client.post(
f"/api/projects/{self.team.id}/feature_flags/", {"name": "Alpha feature", "key": "alpha-feature"}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
instance = FeatureFlag.objects.get(id=response.json()["id"])
self.assertEqual(instance.key, "alpha-feature")
@ -344,7 +352,9 @@ class TestFeatureFlag(APIBaseTest):
instance = FeatureFlag.objects.create(team=self.team, created_by=self.user, key="beta-feature")
response = self.client.patch(f"/api/feature_flag/{instance.pk}", {"key": "alpha-feature",}, format="json",)
response = self.client.patch(
f"/api/projects/{self.team.id}/feature_flags/{instance.pk}", {"key": "alpha-feature",}, format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
instance.refresh_from_db()
self.assertEqual(instance.key, "alpha-feature")
@ -352,7 +362,7 @@ class TestFeatureFlag(APIBaseTest):
@patch("posthoganalytics.capture")
def test_my_flags(self, mock_capture):
self.client.post(
"/api/feature_flag/",
f"/api/projects/{self.team.id}/feature_flags/",
{
"name": "Alpha feature",
"key": "alpha-feature",
@ -375,7 +385,7 @@ class TestFeatureFlag(APIBaseTest):
distinct_id_user.distinct_id = "distinct_id"
distinct_id_user.save()
self.client.force_login(distinct_id_user)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(len(response_data), 2)
@ -395,7 +405,7 @@ class TestFeatureFlag(APIBaseTest):
distinct_id_0_user.distinct_id = "distinct_id_0"
distinct_id_0_user.save()
self.client.force_login(distinct_id_0_user)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(len(response_data), 2)
@ -432,7 +442,7 @@ class TestFeatureFlag(APIBaseTest):
)
)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
@ -456,7 +466,7 @@ class TestFeatureFlag(APIBaseTest):
{"feature_flag": feature_flag_instance.id, "override_value": "hey-hey"},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
first_flag = response_data[0]
@ -469,7 +479,7 @@ class TestFeatureFlag(APIBaseTest):
{"feature_flag": feature_flag_instance.id, "override_value": "new-override"},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
first_flag = response_data[0]
@ -489,7 +499,7 @@ class TestFeatureFlag(APIBaseTest):
{"feature_flag": feature_flag_instance.id, "override_value": "hey-hey"},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
first_flag = response_data[0]
@ -501,7 +511,7 @@ class TestFeatureFlag(APIBaseTest):
response = self.client.delete(f"/api/projects/@current/feature_flag_overrides/{existing_override_id}",)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self.client.get("/api/feature_flag/my_flags")
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
first_flag = response_data[0]

View File

@ -39,7 +39,7 @@ def insight_test_factory(event_factory, person_factory):
# create without user
DashboardItem.objects.create(filters=Filter(data=filter_dict).to_dict(), team=self.team)
response = self.client.get("/api/insight/", data={"user": "true"}).json()
response = self.client.get(f"/api/projects/{self.team.id}/insights/", data={"user": "true"}).json()
self.assertEqual(len(response["results"]), 1)
@ -61,7 +61,9 @@ def insight_test_factory(event_factory, person_factory):
# create without user
DashboardItem.objects.create(filters=Filter(data=filter_dict).to_dict(), team=self.team)
response = self.client.get("/api/insight/", data={"saved": "true", "user": "true"})
response = self.client.get(
f"/api/projects/{self.team.id}/insights/", data={"saved": "true", "user": "true"}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json()["results"]), 1)
@ -85,7 +87,7 @@ def insight_test_factory(event_factory, person_factory):
# create without user
DashboardItem.objects.create(filters=Filter(data=filter_dict).to_dict(), team=self.team)
response = self.client.get("/api/insight/?favorited=true&user=true")
response = self.client.get(f"/api/projects/{self.team.id}/insights/?favorited=true&user=true")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json()["results"]), 1)
@ -106,7 +108,7 @@ def insight_test_factory(event_factory, person_factory):
filters=Filter(data=filter_dict).to_dict(), team=new_team, short_id="12345678",
)
response = self.client.get("/api/insight/?short_id=12345678")
response = self.client.get(f"/api/projects/{self.team.id}/insights/?short_id=12345678")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json()["results"]), 1)
@ -129,7 +131,7 @@ def insight_test_factory(event_factory, person_factory):
filters=Filter(data=filter_dict).to_dict(), team=self.team, saved=True,
)
response = self.client.get("/api/insight/?basic=true")
response = self.client.get(f"/api/projects/{self.team.id}/insights/?basic=true")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json()["results"]), 2)
@ -153,7 +155,7 @@ def insight_test_factory(event_factory, person_factory):
def test_create_insight_items(self):
# Make sure the endpoint works with and without the trailing slash
response = self.client.post(
"/api/insight",
f"/api/projects/{self.team.id}/insights",
data={
"filters": {
"events": [{"id": "$pageview"}],
@ -175,7 +177,7 @@ def insight_test_factory(event_factory, person_factory):
def test_update_insight(self):
insight = DashboardItem.objects.create(team=self.team, name="special insight", created_by=self.user,)
response = self.client.patch(
f"/api/insight/{insight.id}",
f"/api/projects/{self.team.id}/insights/{insight.id}",
{
"name": "insight new name",
"tags": ["official", "engineering"],
@ -207,7 +209,7 @@ def insight_test_factory(event_factory, person_factory):
["Custom filter", 100, "", " ", None], ["Custom filter", "100", None, None, None]
):
response = self.client.patch(
f"/api/insight/{insight.id}",
f"/api/projects/{self.team.id}/insights/{insight.id}",
{"filters": {"events": [{"id": "$pageview", "custom_name": custom_name}]}},
)
@ -223,7 +225,7 @@ def insight_test_factory(event_factory, person_factory):
dashboard = Dashboard.objects.create(name="My Dashboard", team=self.team)
response = self.client.post(
"/api/insight",
f"/api/projects/{self.team.id}/insights",
data={
"filters": {
"insight": "FUNNELS",
@ -278,7 +280,7 @@ def insight_test_factory(event_factory, person_factory):
with freeze_time("2012-01-15T04:01:34.000Z"):
response = self.client.get(
"/api/insight/trend/?events={}".format(json.dumps([{"id": "$pageview"}]))
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}"
).json()
self.assertEqual(response["result"][0]["count"], 2)
@ -286,10 +288,10 @@ def insight_test_factory(event_factory, person_factory):
def test_nonexistent_cohort_is_handled(self):
response_nonexistent_property = self.client.get(
f"/api/insight/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'event','key':'foo','value':'barabarab'}])}"
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'event','key':'foo','value':'barabarab'}])}"
)
response_nonexistent_cohort = self.client.get(
f"/api/insight/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'cohort','key':'id','value':2137}])}"
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'cohort','key':'id','value':2137}])}"
) # This should not throw an error, just act like there's no event matches
response_nonexistent_property_data = response_nonexistent_property.json()
@ -304,10 +306,10 @@ def insight_test_factory(event_factory, person_factory):
whatever_cohort_without_match_groups = Cohort.objects.create(team=self.team)
response_nonexistent_property = self.client.get(
f"/api/insight/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'event','key':'foo','value':'barabarab'}])}"
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'event','key':'foo','value':'barabarab'}])}"
)
response_cohort_without_match_groups = self.client.get(
f"/api/insight/trend/?events={json.dumps([{'id':'$pageview'}])}&properties={json.dumps([{'type':'cohort','key':'id','value':whatever_cohort_without_match_groups.pk}])}"
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id':'$pageview'}])}&properties={json.dumps([{'type':'cohort','key':'id','value':whatever_cohort_without_match_groups.pk}])}"
) # This should not throw an error, just act like there's no event matches
self.assertEqual(response_nonexistent_property.status_code, 200)
@ -333,10 +335,10 @@ def insight_test_factory(event_factory, person_factory):
with self.settings(USE_PRECALCULATED_CH_COHORT_PEOPLE=True): # Normally this is False in tests
response_user_property = self.client.get(
f"/api/insight/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'person','key':'foo','value':'bar'}])}"
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id': '$pageview'}])}&properties={json.dumps([{'type':'person','key':'foo','value':'bar'}])}"
)
response_precalculated_cohort = self.client.get(
f"/api/insight/trend/?events={json.dumps([{'id':'$pageview'}])}&properties={json.dumps([{'type':'cohort','key':'id','value':113}])}"
f"/api/projects/{self.team.id}/insights/trend/?events={json.dumps([{'id':'$pageview'}])}&properties={json.dumps([{'type':'cohort','key':'id','value':113}])}"
)
self.assertEqual(response_precalculated_cohort.status_code, 200)
@ -356,7 +358,7 @@ def insight_test_factory(event_factory, person_factory):
with freeze_time("2012-01-15T04:01:34.000Z"):
response = self.client.get(
"/api/insight/trend/",
f"/api/projects/{self.team.id}/insights/trend/",
data={
"events": json.dumps([{"id": "$pageview"}]),
"breakdown": "$some_property",
@ -396,10 +398,11 @@ def insight_test_factory(event_factory, person_factory):
)
get_response = self.client.get(
"/api/insight/path", data={"properties": json.dumps([{"key": "test", "value": "val"}]),}
f"/api/projects/{self.team.id}/insights/path",
data={"properties": json.dumps([{"key": "test", "value": "val"}]),},
).json()
post_response = self.client.post(
"/api/insight/path", {"properties": [{"key": "test", "value": "val"}],}
f"/api/projects/{self.team.id}/insights/path", {"properties": [{"key": "test", "value": "val"}],}
).json()
self.assertEqual(len(get_response["result"]), 1)
self.assertEqual(len(post_response["result"]), 1)
@ -409,7 +412,7 @@ def insight_test_factory(event_factory, person_factory):
event_factory(team=self.team, event="user signed up", distinct_id="1")
event_factory(team=self.team, event="user did things", distinct_id="1")
response = self.client.post(
"/api/insight/funnel/",
f"/api/projects/{self.team.id}/insights/funnel/",
{
"events": [
{"id": "user signed up", "type": "events", "order": 0},
@ -434,14 +437,7 @@ def insight_test_factory(event_factory, person_factory):
event_factory(team=self.team, event="user signed up", distinct_id="1")
event_factory(team=self.team, event="user did things", distinct_id="1")
response = self.client.get(
"/api/insight/funnel/?funnel_window_days=14&events={}".format(
json.dumps(
[
{"id": "user signed up", "type": "events", "order": 0},
{"id": "user did things", "type": "events", "order": 1},
]
)
)
f"/api/projects/{self.team.id}/insights/funnel/?funnel_window_days=14&events={json.dumps([{'id': 'user signed up', 'type': 'events', 'order': 0},{'id': 'user did things', 'type': 'events', 'order': 1},])}"
).json()
# clickhouse funnels don't have a loading system
@ -461,7 +457,7 @@ def insight_test_factory(event_factory, person_factory):
event_factory(
team=self.team, event="$pageview", distinct_id="person1", timestamp=timezone.now() - timedelta(days=10),
)
response = self.client.get("/api/insight/retention/",).json()
response = self.client.get(f"/api/projects/{self.team.id}/insights/retention/",).json()
self.assertEqual(len(response["result"]), 11)
@ -486,14 +482,15 @@ def insight_test_factory(event_factory, person_factory):
events_filter = json.dumps([{"id": "$pageview"}])
response_team1 = self.client.get(f"/api/insight/trend/?events={events_filter}")
response_team1 = self.client.get(f"/api/projects/{self.team.id}/insights/trend/?events={events_filter}")
response_team1_token = self.client.get(
f"/api/insight/trend/?events={events_filter}&token={self.user.team.api_token}"
f"/api/projects/{self.team.id}/insights/trend/?events={events_filter}&token={self.user.team.api_token}"
)
self.client.force_login(user2)
response_team2 = self.client.get(
f"/api/insight/trend/?events={events_filter}", data={"token": user2.team.api_token}
f"/api/projects/{self.team.id}/insights/trend/?events={events_filter}",
data={"token": user2.team.api_token},
)
self.assertEqual(response_team1.status_code, 200)
@ -501,7 +498,7 @@ def insight_test_factory(event_factory, person_factory):
self.assertEqual(response_team1.json()["result"], response_team1_token.json()["result"])
self.assertNotEqual(len(response_team1.json()["result"]), len(response_team2.json()["result"]))
response_invalid_token = self.client.get(f"/api/insight/trend?token=invalid")
response_invalid_token = self.client.get(f"/api/projects/{self.team.id}/insights/trend?token=invalid")
self.assertEqual(response_invalid_token.status_code, 401)
return TestInsight

View File

@ -57,7 +57,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
self.create_snapshot("user2", "2", base_time + relativedelta(seconds=20))
self.create_snapshot("user", "1", base_time + relativedelta(seconds=30))
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(len(response_data["results"]), 2)
@ -86,7 +86,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
self.create_snapshot("user", "1", now() - relativedelta(days=1), team_id=another_team.pk)
self.create_snapshot("user", "2", now() - relativedelta(days=1))
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(len(response_data["results"]), 1)
@ -101,7 +101,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
)
self.create_snapshot("d1", "1", base_time)
self.create_snapshot("d2", "2", base_time + relativedelta(seconds=30))
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
response_data = response.json()
self.assertEqual(len(response_data["results"]), 2)
self.assertEqual(response_data["results"][0]["person"]["id"], p.pk)
@ -112,7 +112,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
SessionRecordingViewed.objects.create(team=self.team, user=self.user, session_id="1")
self.create_snapshot("u1", "1", base_time)
self.create_snapshot("u1", "2", base_time + relativedelta(seconds=30))
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
response_data = response.json()
self.assertEqual(len(response_data["results"]), 2)
self.assertEqual(response_data["results"][0]["id"], "2")
@ -128,7 +128,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
base_time = now() - relativedelta(days=1)
self.create_snapshot("d1", session_recording_id, base_time)
self.create_snapshot("d1", session_recording_id, base_time + relativedelta(seconds=30))
response = self.client.get(f"/api/projects/@current/session_recordings/{session_recording_id}")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/{session_recording_id}")
response_data = response.json()
self.assertEqual(
response_data["result"]["snapshots"][0], {"timestamp": base_time.timestamp() * 1000, "type": 2}
@ -147,7 +147,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
for s in range(num_snapshots):
self.create_snapshot("user", "1", base_time)
response = self.client.get("/api/projects/@current/session_recordings/1")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/1")
response_data = response.json()
self.assertEqual(len(response_data["result"]["snapshots"]), num_snapshots)
@ -168,7 +168,7 @@ def factory_test_session_recordings_api(session_recording_event_factory):
"user", chunked_session_id, start_time + relativedelta(seconds=s), s, chunk_size
)
next_url = f"/api/projects/@current/session_recordings/{chunked_session_id}"
next_url = f"/api/projects/{self.team.id}/session_recordings/{chunked_session_id}"
for i in range(expected_num_requests):
response = self.client.get(next_url)
@ -199,41 +199,41 @@ def factory_test_session_recordings_api(session_recording_event_factory):
"user", chunked_session_id, base_time + relativedelta(seconds=s), s, chunk_size
)
response = self.client.get(f"/api/projects/@current/session_recordings/{chunked_session_id}")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/{chunked_session_id}")
response_data = response.json()
self.assertEqual(len(response_data["result"]["snapshots"]), num_snapshots)
def test_single_session_recording_doesnt_leak_teams(self):
another_team = Team.objects.create(organization=self.organization)
self.create_snapshot("user", "id_no_team_leaking", now() - relativedelta(days=1), team_id=another_team.pk)
response = self.client.get("/api/projects/@current/session_recordings/id_no_team_leaking")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/id_no_team_leaking")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_session_recording_with_no_person(self):
self.create_snapshot("d1", "id_no_person", now() - relativedelta(days=1))
response = self.client.get("/api/projects/@current/session_recordings/id_no_person")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/id_no_person")
response_data = response.json()
self.assertEqual(response_data["result"]["person"], None)
def test_session_recording_doesnt_exist(self):
response = self.client.get("/api/projects/@current/session_recordings/non_existent_id")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/non_existent_id")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_setting_viewed_state_of_session_recording(self):
self.create_snapshot("u1", "1", now() - relativedelta(days=1))
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
response_data = response.json()
# Make sure it starts not viewed
self.assertEqual(response_data["results"][0]["viewed"], False)
response = self.client.get("/api/projects/@current/session_recordings/1")
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/1")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
response_data = response.json()
# Make sure it remains not viewed
self.assertEqual(response_data["results"][0]["viewed"], False)
response = self.client.get("/api/projects/@current/session_recordings/1?save_view=True")
response = self.client.get("/api/projects/@current/session_recordings")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings/1?save_view=True")
response = self.client.get(f"/api/projects/{self.team.id}/session_recordings")
response_data = response.json()
# Make sure the query param sets it to viewed
self.assertEqual(response_data["results"][0]["viewed"], True)

View File

@ -16,6 +16,7 @@ from posthog.api.test.test_event_definition import (
create_user,
)
from posthog.api.test.test_retention import identify
from posthog.models.team import Team
@pytest.mark.django_db
@ -83,6 +84,7 @@ def test_includes_only_intervals_within_range(client: Client):
}
],
),
team=team,
)
assert trends == {
"is_cached": False,
@ -124,9 +126,9 @@ class TrendsRequest:
events: List[Dict[str, Any]]
def get_trends(client, request: TrendsRequest):
def get_trends(client, request: TrendsRequest, team: Team):
return client.get(
"/api/insight/trend/",
f"/api/projects/{team.id}/insights/trend/",
data={
"date_from": request.date_from,
"date_to": request.date_to,
@ -140,7 +142,7 @@ def get_trends(client, request: TrendsRequest):
)
def get_trends_ok(client: Client, request: TrendsRequest):
response = get_trends(client=client, request=request)
def get_trends_ok(client: Client, request: TrendsRequest, team: Team):
response = get_trends(client=client, request=request, team=team)
assert response.status_code == 200, response.content
return response.json()

View File

@ -17,7 +17,9 @@ from posthog.test.base import BaseTest
def session_recording_test_factory(session_recording, filter_sessions, event_factory):
def create_recording_request_and_filter(session_recording_id, limit=None, offset=None) -> Tuple[Request, Filter]:
def create_recording_request_and_filter(
team_id, session_recording_id, limit=None, offset=None
) -> Tuple[Request, Filter]:
params = {}
if limit:
params["limit"] = limit
@ -27,7 +29,8 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
build_req.META = {"HTTP_HOST": "www.testserver"}
req = Request(
build_req, f"/api/event/session_recording?session_recording_id={session_recording_id}{urlencode(params)}"
build_req,
f"/api/projects/{team_id}/session_recordings?session_recording_id={session_recording_id}{urlencode(params)}",
)
return (req, Filter(request=req, data=params))
@ -43,7 +46,7 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
self.create_snapshot("user2", "2", now() + relativedelta(seconds=20))
self.create_snapshot("user", "1", now() + relativedelta(seconds=30))
req, filt = create_recording_request_and_filter("1")
req, filt = create_recording_request_and_filter(self.team.id, "1")
session = session_recording(team=self.team, session_recording_id="1", request=req, filter=filt).run()
self.assertEqual(
session["snapshots"],
@ -58,7 +61,7 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
def test_query_run_with_no_such_session(self):
req, filt = create_recording_request_and_filter("xxx")
req, filt = create_recording_request_and_filter(self.team.id, "xxx")
session = session_recording(team=self.team, session_recording_id="xxx", request=req, filter=filt).run()
self.assertEqual(
session, {"snapshots": [], "person": None, "start_time": None, "next": None, "duration": 0}
@ -174,7 +177,7 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
for s in range(num_snapshots + 1):
self.create_chunked_snapshot("user", chunked_session_id, now() + relativedelta(seconds=s), s)
req, filt = create_recording_request_and_filter(chunked_session_id)
req, filt = create_recording_request_and_filter(self.team.id, chunked_session_id)
session = session_recording(
team=self.team, session_recording_id=chunked_session_id, request=req, filter=filt
).run()
@ -195,7 +198,7 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
for s in range(200):
self.create_chunked_snapshot("user", chunked_session_id, now() + relativedelta(seconds=s), s)
req, filt = create_recording_request_and_filter(chunked_session_id, limit)
req, filt = create_recording_request_and_filter(self.team.id, chunked_session_id, limit)
session = session_recording(
team=self.team, session_recording_id=chunked_session_id, request=req, filter=filt
).run()
@ -223,7 +226,7 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
)
# A successful single session recording query will make {num_chunks} requests
base_req, base_filter = create_recording_request_and_filter(chunked_session_id)
base_req, base_filter = create_recording_request_and_filter(self.team.id, chunked_session_id)
session = None
for i in range(expected_num_requests):
@ -307,7 +310,7 @@ def session_recording_test_factory(session_recording, filter_sessions, event_fac
)
# Do the thing
req, filt = create_recording_request_and_filter(chunked_session_id)
req, filt = create_recording_request_and_filter(self.team.id, chunked_session_id)
session = session_recording(
team=self.team, session_recording_id=chunked_session_id, request=req, filter=filt
).run()

View File

@ -91,25 +91,27 @@ def paths_test_factory(paths, event_factory, person_factory):
with freeze_time("2012-01-15T03:21:34.000Z"):
date_from = now() - relativedelta(days=7)
response = self.client.get(
"/api/insight/path/?insight=PATHS&date_from=" + date_from.strftime("%Y-%m-%d")
f"/api/projects/{self.team.id}/insights/path/?insight=PATHS&date_from="
+ date_from.strftime("%Y-%m-%d")
).json()
self.assertEqual(len(response["result"]), 4)
date_to = now()
response = self.client.get(
"/api/insight/path/?insight=PATHS&date_to=" + date_to.strftime("%Y-%m-%d")
f"/api/projects/{self.team.id}/insights/path/?insight=PATHS&date_to=" + date_to.strftime("%Y-%m-%d")
).json()
self.assertEqual(len(response["result"]), 4)
date_from = now() + relativedelta(days=7)
response = self.client.get(
"/api/insight/path/?insight=PATHS&date_from=" + date_from.strftime("%Y-%m-%d")
f"/api/projects/{self.team.id}/insights/path/?insight=PATHS&date_from="
+ date_from.strftime("%Y-%m-%d")
).json()
self.assertEqual(len(response["result"]), 0)
date_to = now() - relativedelta(days=7)
response = self.client.get(
"/api/insight/path/?insight=PATHS&date_to=" + date_to.strftime("%Y-%m-%d")
f"/api/projects/{self.team.id}/insights/path/?insight=PATHS&date_to=" + date_to.strftime("%Y-%m-%d")
).json()
self.assertEqual(len(response["result"]), 0)
@ -348,7 +350,9 @@ def paths_test_factory(paths, event_factory, person_factory):
properties={"$current_url": "/help"}, distinct_id="person_5b", event="$pageview", team=self.team,
)
response = self.client.get("/api/insight/path/?type=%24pageview&start=%2Fpricing").json()
response = self.client.get(
f"/api/projects/{self.team.id}/insights/path/?type=%24pageview&start=%2Fpricing"
).json()
filter = PathFilter(data={"path_type": "$pageview", "start_point": "/pricing"})
response = paths(team=self.team, filter=filter).run(team=self.team, filter=filter,)