From d1c75ab747a2e714e23f1d114a14fe3be47b077e Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 16 Oct 2024 16:23:48 +0200 Subject: [PATCH] fix(environments): Rejig use of insight/dashboard endpoints (#25469) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .github/workflows/copy-clickhouse-udfs.yml | 31 +++--- .storybook/decorators/withKea/kea-story.tsx | 2 + cypress/e2e/dashboard-duplication.ts | 2 +- cypress/e2e/dashboard-shared.cy.ts | 6 +- cypress/e2e/dashboard.cy.ts | 8 +- cypress/e2e/insights-saved.cy.ts | 2 +- cypress/e2e/insights.cy.ts | 2 +- cypress/e2e/notebooks.cy.ts | 4 +- cypress/e2e/projectHomepage.cy.ts | 2 +- cypress/e2e/trends.cy.ts | 2 +- cypress/productAnalytics/index.ts | 8 +- cypress/support/e2e.ts | 2 +- ee/urls.py | 12 ++- .../scenes-app-max-ai--welcome--dark.png | Bin 22519 -> 18040 bytes .../scenes-app-max-ai--welcome--light.png | Bin 22689 -> 18219 bytes ...-ai--welcome-loading-suggestions--dark.png | Bin 19329 -> 18040 bytes ...ai--welcome-loading-suggestions--light.png | Bin 19701 -> 18222 bytes .../scenes-app-persons-modal--empty--dark.png | Bin 12449 -> 11286 bytes ...scenes-app-persons-modal--empty--light.png | Bin 12721 -> 11493 bytes ...-app-persons-modal--server-error--dark.png | Bin 15443 -> 14498 bytes ...app-persons-modal--server-error--light.png | Bin 15905 -> 14949 bytes .../navigation-3000/Navigation.stories.tsx | 10 +- .../components/Sidebar.stories.tsx | 2 +- .../sidepanel/SidePanel.stories.tsx | 2 +- frontend/src/lib/api.test.ts | 2 +- frontend/src/lib/api.ts | 95 +++++++++++------- .../ActivityLog/ActivityLog.stories.tsx | 2 +- .../activityLogLogic.insight.test.tsx | 2 +- .../activityLogLogic.person.test.tsx | 5 +- .../annotationsOverlayLogic.test.ts | 4 +- .../authorizedUrlListLogic.test.ts | 2 +- .../PropertiesTimeline.stories.tsx | 6 +- .../propertiesTimelineLogic.ts | 2 +- .../PropertySelect/PropertySelect.stories.tsx | 2 +- .../reverseProxyCheckerLogic.test.ts | 2 +- .../Sharing/SharingModal.stories.tsx | 14 +-- .../SubscriptionsModal.stories.tsx | 4 +- .../Subscriptions/subscriptionsLogic.test.ts | 8 +- .../taxonomicFilterMocksDecorator.ts | 2 +- .../TaxonomicFilter/infiniteListLogic.test.ts | 5 +- .../taxonomicFilterLogic.test.ts | 7 +- .../TaxonomicFilter/taxonomicFilterLogic.tsx | 40 ++++---- .../versionCheckerLogic.test.ts | 2 +- frontend/src/mocks/handlers.ts | 20 ++-- frontend/src/models/dashboardsModel.test.ts | 2 +- frontend/src/models/dashboardsModel.tsx | 30 +++--- .../nodes/DataNode/DataNode.stories.tsx | 4 +- .../dataNodeLogic.queryCancellation.test.ts | 8 +- .../queries/nodes/DataNode/dataNodeLogic.ts | 2 +- .../nodes/DataTable/DataTable.stories.tsx | 4 +- frontend/src/queries/query.test.ts | 2 +- .../activity/explore/Events.stories.tsx | 2 +- frontend/src/scenes/appContextLogic.ts | 6 +- .../DashboardInsightCardLegend.stories.tsx | 4 +- .../scenes/dashboard/Dashboards.stories.tsx | 10 +- .../scenes/dashboard/dashboardLogic.test.ts | 36 +++---- .../src/scenes/dashboard/dashboardLogic.tsx | 16 +-- .../dashboards/dashboardsLogic.test.ts | 2 +- .../src/scenes/dashboard/newDashboardLogic.ts | 4 +- .../DataManagementScene.stories.tsx | 2 +- .../events/eventDefinitionsTableLogic.test.ts | 6 +- .../error-tracking/ErrorTracking.stories.tsx | 2 +- .../feature-flags/FeatureFlags.stories.tsx | 2 +- .../scenes/feature-flags/featureFlagLogic.ts | 22 ++-- .../funnels/funnelPersonsModalLogic.test.ts | 2 +- .../funnelPropertyCorrelationLogic.test.ts | 8 +- .../heatmaps/HeatmapsBrowser.stories.tsx | 2 +- .../EmptyStates/EmptyStates.stories.tsx | 18 ++-- .../insights/EmptyStates/EmptyStates.tsx | 8 +- .../InsightNav/insightNavLogic.test.ts | 4 +- .../src/scenes/insights/Insights.stories.tsx | 4 +- .../insights/__mocks__/createInsightScene.tsx | 4 +- .../scenes/insights/insightDataLogic.test.ts | 2 +- .../src/scenes/insights/insightLogic.test.ts | 32 +++--- .../scenes/insights/insightSceneLogic.test.ts | 6 +- .../src/scenes/insights/insightUsageLogic.ts | 6 +- .../insights/insightVizDataLogic.test.ts | 4 +- frontend/src/scenes/insights/utils.tsx | 2 +- .../FunnelCorrelationTable.stories.tsx | 2 +- ...FunnelPropertyCorrelationTable.stories.tsx | 2 +- frontend/src/scenes/max/Max.stories.tsx | 4 +- .../notebooks/Notebook/Notebook.stories.tsx | 4 +- .../src/scenes/persons/personsLogic.test.ts | 7 +- .../src/scenes/pipeline/Pipeline.stories.tsx | 8 +- .../destinations/destinationsLogic.tsx | 2 +- .../ProjectHomepage.stories.tsx | 10 +- .../projectHomepageLogic.test.ts | 6 +- .../project-homepage/projectHomepageLogic.tsx | 5 +- frontend/src/scenes/projectLogic.ts | 12 ++- .../saved-insights/SavedInsights.stories.tsx | 4 +- .../saved-insights/savedInsightsLogic.test.ts | 12 +-- .../saved-insights/savedInsightsLogic.ts | 2 +- ...sionsRecordings-player-failure.stories.tsx | 8 +- ...sionsRecordings-player-success.stories.tsx | 8 +- ...onsRecordings-playlist-listing.stories.tsx | 4 +- .../inspector/playerInspectorLogic.test.ts | 2 +- .../modal/sessionPlayerModalLogic.test.ts | 2 +- .../player/playerMetaLogic.test.ts | 6 +- .../player/sessionRecordingDataLogic.test.ts | 14 +-- .../sessionRecordingPlayerLogic.test.ts | 18 ++-- ...ssionRecordingsListPropertiesLogic.test.ts | 2 +- .../sessionRecordingsPlaylistLogic.test.ts | 4 +- .../settings/environment/teamMembersLogic.tsx | 10 +- .../src/scenes/surveys/Surveys.stories.tsx | 2 +- .../src/scenes/surveys/surveyLogic.test.ts | 8 +- .../persons-modal/PersonsModal.stories.tsx | 2 +- .../persons-modal/peronsModalLogic.test.ts | 2 +- .../sessionAttributionExplorer.stories.tsx | 4 +- .../stories/How to mock requests.stories.mdx | 2 +- frontend/src/test/init.ts | 13 ++- posthog/api/__init__.py | 36 +++++-- posthog/api/dashboards/dashboard.py | 15 ++- posthog/api/event_definition.py | 8 +- posthog/api/feature_flag.py | 2 +- posthog/api/insight.py | 8 +- posthog/api/organization_feature_flag.py | 1 + posthog/api/sharing.py | 1 + .../api/test/__snapshots__/test_api_docs.ambr | 33 +++--- .../api/test/__snapshots__/test_insight.ambr | 8 +- .../test_organization_feature_flag.ambr | 3 +- posthog/api/test/dashboards/__init__.py | 4 +- .../__snapshots__/test_dashboard.ambr | 28 +++--- posthog/api/test/dashboards/test_dashboard.py | 32 +++++- posthog/api/test/test_feature_flag.py | 35 ++++--- posthog/api/test/test_insight.py | 40 ++++++++ posthog/api/utils.py | 2 +- posthog/caching/calculate_results.py | 7 +- posthog/tasks/alerts/checks.py | 1 + 128 files changed, 629 insertions(+), 417 deletions(-) diff --git a/.github/workflows/copy-clickhouse-udfs.yml b/.github/workflows/copy-clickhouse-udfs.yml index c6862b0345c67..3dc6fce3ade07 100644 --- a/.github/workflows/copy-clickhouse-udfs.yml +++ b/.github/workflows/copy-clickhouse-udfs.yml @@ -1,21 +1,20 @@ name: Trigger UDFs Workflow on: - push: - branches: - - master - paths: - - 'posthog/user_scripts/**' + push: + branches: + - master + paths: + - 'posthog/user_scripts/**' jobs: - trigger_udfs_workflow: - runs-on: ubuntu-latest - steps: - - name: Trigger UDFs Workflow - uses: benc-uk/workflow-dispatch@v1 - with: - workflow: .github/workflows/clickhouse-udfs.yml - repo: posthog/posthog-cloud-infra - token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} - ref: refs/heads/main - + trigger_udfs_workflow: + runs-on: ubuntu-latest + steps: + - name: Trigger UDFs Workflow + uses: benc-uk/workflow-dispatch@v1 + with: + workflow: .github/workflows/clickhouse-udfs.yml + repo: posthog/posthog-cloud-infra + token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} + ref: refs/heads/main diff --git a/.storybook/decorators/withKea/kea-story.tsx b/.storybook/decorators/withKea/kea-story.tsx index 3525a4befbe0d..0e04d991a82fb 100644 --- a/.storybook/decorators/withKea/kea-story.tsx +++ b/.storybook/decorators/withKea/kea-story.tsx @@ -8,6 +8,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { worker } from '~/mocks/browser' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' +import { projectLogic } from 'scenes/projectLogic' export function resetKeaStory(): void { worker.resetHandlers() @@ -18,6 +19,7 @@ export function resetKeaStory(): void { initKea({ routerLocation: history.location, routerHistory: history }) featureFlagLogic.mount() teamLogic.mount() + projectLogic.mount() userLogic.mount() router.mount() const { store } = getContext() diff --git a/cypress/e2e/dashboard-duplication.ts b/cypress/e2e/dashboard-duplication.ts index 75c1dea0c3998..e19bbd1ad046f 100644 --- a/cypress/e2e/dashboard-duplication.ts +++ b/cypress/e2e/dashboard-duplication.ts @@ -7,7 +7,7 @@ describe('duplicating dashboards', () => { let dashboardName, insightName, expectedCopiedDashboardName, expectedCopiedInsightName beforeEach(() => { - cy.intercept('POST', /\/api\/projects\/\d+\/dashboards/).as('createDashboard') + cy.intercept('POST', /\/api\/environments\/\d+\/dashboards/).as('createDashboard') dashboardName = randomString('dashboard-') expectedCopiedDashboardName = `${dashboardName} (Copy)` diff --git a/cypress/e2e/dashboard-shared.cy.ts b/cypress/e2e/dashboard-shared.cy.ts index 4e46554160424..755297f5c52dd 100644 --- a/cypress/e2e/dashboard-shared.cy.ts +++ b/cypress/e2e/dashboard-shared.cy.ts @@ -2,9 +2,9 @@ import { dashboards } from '../productAnalytics' describe('Shared dashboard', () => { beforeEach(() => { - cy.intercept('GET', /api\/projects\/\d+\/insights\/\?.*/).as('loadInsightList') - cy.intercept('PATCH', /api\/projects\/\d+\/insights\/\d+\/.*/).as('patchInsight') - cy.intercept('POST', /\/api\/projects\/\d+\/dashboards/).as('createDashboard') + cy.intercept('GET', /api\/environments\/\d+\/insights\/\?.*/).as('loadInsightList') + cy.intercept('PATCH', /api\/environments\/\d+\/insights\/\d+\/.*/).as('patchInsight') + cy.intercept('POST', /\/api\/environments\/\d+\/dashboards/).as('createDashboard') cy.useSubscriptionStatus('unsubscribed') cy.clickNavMenu('dashboards') diff --git a/cypress/e2e/dashboard.cy.ts b/cypress/e2e/dashboard.cy.ts index 954bc390759d3..b5f62097ebee1 100644 --- a/cypress/e2e/dashboard.cy.ts +++ b/cypress/e2e/dashboard.cy.ts @@ -3,9 +3,9 @@ import { randomString } from '../support/random' describe('Dashboard', () => { beforeEach(() => { - cy.intercept('GET', /api\/projects\/\d+\/insights\/\?.*/).as('loadInsightList') - cy.intercept('PATCH', /api\/projects\/\d+\/insights\/\d+\/.*/).as('patchInsight') - cy.intercept('POST', /\/api\/projects\/\d+\/dashboards/).as('createDashboard') + cy.intercept('GET', /api\/environments\/\d+\/insights\/\?.*/).as('loadInsightList') + cy.intercept('PATCH', /api\/environments\/\d+\/insights\/\d+\/.*/).as('patchInsight') + cy.intercept('POST', /\/api\/environments\/\d+\/dashboards/).as('createDashboard') cy.clickNavMenu('dashboards') cy.location('pathname').should('include', '/dashboard') @@ -306,7 +306,7 @@ describe('Dashboard', () => { }) it('Move dashboard item', () => { - cy.intercept('PATCH', /api\/projects\/\d+\/dashboards\/\d+\/move_tile.*/).as('moveTile') + cy.intercept('PATCH', /api\/environments\/\d+\/dashboards\/\d+\/move_tile.*/).as('moveTile') const sourceDashboard = randomString('source-dashboard') const targetDashboard = randomString('target-dashboard') diff --git a/cypress/e2e/insights-saved.cy.ts b/cypress/e2e/insights-saved.cy.ts index c5a498edf63af..748c0984543f3 100644 --- a/cypress/e2e/insights-saved.cy.ts +++ b/cypress/e2e/insights-saved.cy.ts @@ -30,7 +30,7 @@ describe('Insights - saved', () => { }) it('If cache empty, initiate async refresh', () => { - cy.intercept('GET', /\/api\/projects\/\d+\/insights\/?\?[^/]*?refresh=async/).as('getInsightsRefreshAsync') + cy.intercept('GET', /\/api\/environments\/\d+\/insights\/?\?[^/]*?refresh=async/).as('getInsightsRefreshAsync') let newInsightId: string createInsight('saved insight').then((insightId) => { newInsightId = insightId diff --git a/cypress/e2e/insights.cy.ts b/cypress/e2e/insights.cy.ts index e7e05fa4e0491..a7dc8924c6e09 100644 --- a/cypress/e2e/insights.cy.ts +++ b/cypress/e2e/insights.cy.ts @@ -53,7 +53,7 @@ describe('Insights', () => { }) it('Create new insight and save and continue editing', () => { - cy.intercept('PATCH', /\/api\/projects\/\d+\/insights\/\d+\/?/).as('patchInsight') + cy.intercept('PATCH', /\/api\/environments\/\d+\/insights\/\d+\/?/).as('patchInsight') const insightName = randomString('insight-name-') createInsight(insightName) diff --git a/cypress/e2e/notebooks.cy.ts b/cypress/e2e/notebooks.cy.ts index 36c81378d2667..3022d621ba63d 100644 --- a/cypress/e2e/notebooks.cy.ts +++ b/cypress/e2e/notebooks.cy.ts @@ -3,13 +3,13 @@ import { urls } from 'scenes/urls' describe('Notebooks', () => { beforeEach(() => { cy.fixture('api/session-recordings/recordings.json').then((recordings) => { - cy.intercept('GET', /api\/projects\/\d+\/session_recordings\/?\?.*/, { body: recordings }).as( + cy.intercept('GET', /api\/environments\/\d+\/session_recordings\/?\?.*/, { body: recordings }).as( 'loadSessionRecordingsList' ) }) cy.fixture('api/session-recordings/recording.json').then((recording) => { - cy.intercept('GET', /api\/projects\/\d+\/session_recordings\/.*\?.*/, { body: recording }).as( + cy.intercept('GET', /api\/environments\/\d+\/session_recordings\/.*\?.*/, { body: recording }).as( 'loadSessionRecording' ) }) diff --git a/cypress/e2e/projectHomepage.cy.ts b/cypress/e2e/projectHomepage.cy.ts index 069041d039435..6d88abfe24c44 100644 --- a/cypress/e2e/projectHomepage.cy.ts +++ b/cypress/e2e/projectHomepage.cy.ts @@ -1,6 +1,6 @@ describe('Project Homepage', () => { beforeEach(() => { - cy.intercept('GET', /\/api\/projects\/\d+\/dashboards\/\d+\//).as('getDashboard') + cy.intercept('GET', /\/api\/environments\/\d+\/dashboards\/\d+\//).as('getDashboard') cy.clickNavMenu('projecthomepage') }) diff --git a/cypress/e2e/trends.cy.ts b/cypress/e2e/trends.cy.ts index ce8a6e8574b30..9f6d45236520a 100644 --- a/cypress/e2e/trends.cy.ts +++ b/cypress/e2e/trends.cy.ts @@ -6,7 +6,7 @@ describe('Trends', () => { }) it('Can load a graph from a URL directly', () => { - cy.intercept('POST', /api\/projects\/\d+\/query\//).as('loadNewQueryInsight') + cy.intercept('POST', /api\/environments\/\d+\/query\//).as('loadNewQueryInsight') // regression test, the graph wouldn't load when going directly to a URL cy.visit( diff --git a/cypress/productAnalytics/index.ts b/cypress/productAnalytics/index.ts index 46344f78a627a..0fc9972014116 100644 --- a/cypress/productAnalytics/index.ts +++ b/cypress/productAnalytics/index.ts @@ -42,7 +42,7 @@ export const insight = { cy.url().should('not.include', '/new') }, clickTab: (tabName: string): void => { - cy.intercept('POST', /api\/projects\/\d+\/query\//).as('loadNewQueryInsight') + cy.intercept('POST', /api\/environments\/\d+\/query\//).as('loadNewQueryInsight') cy.get(`[data-attr="insight-${(tabName === 'PATHS' ? 'PATH' : tabName).toLowerCase()}-tab"]`).click() if (tabName !== 'FUNNELS') { @@ -51,7 +51,7 @@ export const insight = { } }, newInsight: (insightType: string = 'TRENDS'): void => { - cy.intercept('POST', /api\/projects\/\d+\/query\//).as('loadNewQueryInsight') + cy.intercept('POST', /api\/environments\/\d+\/query\//).as('loadNewQueryInsight') if (insightType === 'JSON') { cy.clickNavMenu('savedinsights') @@ -86,7 +86,7 @@ export const insight = { cy.url().should('not.include', '/new') // wait for insight to complete and update URL }, addInsightToDashboard: (dashboardName: string, options: { visitAfterAdding: boolean }): void => { - cy.intercept('PATCH', /api\/projects\/\d+\/insights\/\d+\/.*/).as('patchInsight') + cy.intercept('PATCH', /api\/environments\/\d+\/insights\/\d+\/.*/).as('patchInsight') cy.get('[data-attr="save-to-dashboard-button"]').click() cy.get('[data-attr="dashboard-searchfield"]').type(dashboardName) @@ -158,7 +158,7 @@ export const dashboards = { export const dashboard = { addInsightToEmptyDashboard: (insightName: string): void => { - cy.intercept('POST', /api\/projects\/\d+\/insights\//).as('postInsight') + cy.intercept('POST', /api\/environments\/\d+\/insights\//).as('postInsight') cy.get('[data-attr=dashboard-add-graph-header]').contains('Add insight').click() cy.get('[data-attr=toast-close-button]').click({ multiple: true }) diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 3b3d74a2287ea..fe164bf074b3a 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -101,7 +101,7 @@ beforeEach(() => { req.reply({ statusCode: 404, body: 'Cypress forced 404' }) ) - cy.intercept('GET', /\/api\/projects\/\d+\/insights\/?\?/).as('getInsights') + cy.intercept('GET', /\/api\/environments\/\d+\/insights\/?\?/).as('getInsights') cy.request('POST', '/api/login/', { email: 'test@posthog.com', diff --git a/ee/urls.py b/ee/urls.py index 633766add1439..f0cf168acffb0 100644 --- a/ee/urls.py +++ b/ee/urls.py @@ -30,7 +30,8 @@ def extend_api_router() -> None: projects_router, organizations_router, project_feature_flags_router, - project_dashboards_router, + environment_dashboards_router, + legacy_project_dashboards_router, ) root_router.register(r"billing", billing.BillingViewset, "billing") @@ -67,7 +68,14 @@ def extend_api_router() -> None: "environment_explicit_members", ["team_id"], ) - project_dashboards_router.register( + + environment_dashboards_router.register( + r"collaborators", + dashboard_collaborator.DashboardCollaboratorViewSet, + "environment_dashboard_collaborators", + ["project_id", "dashboard_id"], + ) + legacy_project_dashboards_router.register( r"collaborators", dashboard_collaborator.DashboardCollaboratorViewSet, "project_dashboard_collaborators", diff --git a/frontend/__snapshots__/scenes-app-max-ai--welcome--dark.png b/frontend/__snapshots__/scenes-app-max-ai--welcome--dark.png index a439dd9822a1933bb9c7eab17af0ef1c7ead5bbf..72976d20f697961191423c0ef509b84048e218ae 100644 GIT binary patch delta 443 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 jIR2kQkG(iRr#79pYdHm7?j;9XJ8NDaJ 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 diff --git a/frontend/__snapshots__/scenes-app-max-ai--welcome-loading-suggestions--dark.png b/frontend/__snapshots__/scenes-app-max-ai--welcome-loading-suggestions--dark.png index 8fd19ae835a64f83723f6dd6cf0ea88d372a5570..72976d20f697961191423c0ef509b84048e218ae 100644 GIT binary patch delta 443 zcmZpi&iJE;ae^2(!v_u~1_p);I&pFv71bS>*rYalIZS7%f8JL*o9z%o-*)kwM~gyQ z*C-r0;;h8x)^eoI|EThY5T1@D8ft!zpTt~S6~ePJ`t;8G?^)m7UbEls{!xi)JE4jq zvjYqaJ;%16e)+ol?(WLpo4hW^l^?jid-CepC%?NEzJF(a?8n2$%k9oHw<$Cv9trYN zY+3i&i=9Do=bu{t#dh=RZ-13`y0QFAiaYb(&)gD3F5Gs+!Njxea|KKkisr0MLlr=CCEd@|)lA;W_eZ-TS7zg@NNZQVl-jsy#~ z*J+!-g|aa;{4ZjcU|?v_;Ya`m?Iwi=Mh1oxO$=-d3<}&##7Rhjv>b~6kXdHW?<*Gp PG={;`)z4*}Q$iB}zvsdy delta 1742 zcmZ`(Z8)207Jl+kO~~wGv@$-n^))S{q@`rh)|JvWv_o{XVnov^ZH-8bOQ&p;d4o*b z!BS1c6vZOx)aRf>2@+B4jw&5h+8{p0rpEBnq@q>qo7w%lzs|X?=bZa_?(^LDS<6PH z6`*eIG(j2T&;S5s7-yuxZjV3O#Gxt3zW@utY_avhauVU_QE!U*Y7A@8<@9O#K%lho zp9k`*e!mjlDqu+)Z$eJFYxjJ!Vy0i?e$9(S7wTVF%{n|en_z3l{3(*YDFjb)JQN5# z@$X0EkH6ub-AE4KH1+nV*6_7IU-Rrbr%%$fsz<(jqji_Re18qRQvgDC#IPp?2i3L) zZwE|Dr{CiDv=Ggl?i0ZeMgL|&R{Sg6ZC1pFa@YJ{ciI9_{B!@jeyLQRFN&qDGA#Jr z`vJybwf(KtVD!04EC4kr2RG`@RHxc-N=n!3Occ{Zewvv|df)5ajQzaK6o4r0qHbLU zL(fhLrF69fMK0c28P;?{@gPxDU9G3A(ZvPfJ#P>aV@`t5u-=uLI9+%51@#;t8+&)5 zkHMRp=lACd9cg*S8D(%10Es~pp0PA`N$FqpCS*Q}nAqht(ER?a5i$jzuRb6l0Wi&p ze3T&(B76<;d#n-D5EYIjfH5Q=Lun|YAtB)j3Pa>mnZN)6PNFgY2Se~9j0O>jsdpZ& zMB%2_c?IbeAI-+qHhD5QCF-?Luv3+s%6w!5S_TC_G6in^i#b)~r25UY0 zUrX8V+_v`_D|eUlXrWYAc56q6N_GfEKInsP$y@H|)FXWdjF`;v!{g(ZC&nj^=oq-) zWu2X`dGhsvohb4iq>iC(*LO^A$k-6zJ|_l%<(zdG)hKjR*m@s1`x<6a19ZzqT4*n^g{u=KPwZ31%;~? z&%`HOYQav0BD{PV%7UzKlkKM|xE;JfMap}A9s&pnW2cNMgxN0FwlRBkzBYi}!>koo){J=9gkdti}lm0JhLxZp@X?+AW{0I;p zN~ zGcFf&!?QvO!wi^YKMl!^50;fX5N+1+qjgT>mp61V8bpy6-lZy|lQ-PC73b8I6$W#j zws4kwM!Zg0rj7c?)h@KT^*8SQx-@1?g*@BRHcd4tT7I&A7(cym7Y`*nPWQQ^fl&^M zNc5O>zW=gL?vnX@OAQfCoMT6G@roBu%^sc#it~X#*e79Stpu2NUP82%!o9gITgCEP zkYS5MZQyx#6xjnAchAaW*bn1Z)T61scx%<|f4B|D;??@wF)yy#k-u}K`VGbxKEIJ@ z%J)EItlGkJY3CV?U@%)mAzPAN=%UXc(cLJ{8T|Sy z+0nzV+Nv_`z1ze=2`#GTeRxIcklDk$f?cCAOMtW;LsMyX5O)_J;$D+$n<#K`AAcG|zbfY_O}^@o)>qra79E?!WDP z3TM)>NbA~;-Pr-AEU&={>ks)JWeDkhLH(#B_XZq^H2rtoBHOlxZZoK9D16-V zbo{wz+rFAAmTX{>dpivjTOfaSVl8^R4Wf@X?)D+hq$0jpXjq2Sz)u6}D9rT#0GUo2 A^8f$< diff --git a/frontend/__snapshots__/scenes-app-max-ai--welcome-loading-suggestions--light.png b/frontend/__snapshots__/scenes-app-max-ai--welcome-loading-suggestions--light.png index 96b45dc3059b4cb774138b5412946d2c4c3ae70a..64ba4b1cac7bbec0521eb9ff2a2e5f7f21d825da 100644 GIT binary patch literal 18222 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 n?t@#&mB7BGJH@;IjT5IBL$o2!6EXGf9iSKe4Cf literal 19701 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%0Atg6sT#{`1(2r^uc}q4H?$r5XjO z0CZ>qQ2)tCj@tgi5se?edim(XZ4hN!7#ym3znHwFxgmSe5^lz$6u#1b`a}Qu3dl#R ztG^V{p9~IaZmYY;6DQ zhfh>q9Xxo@NB=b7tLE+7)sCyY+&$iyC_am9->OH3QR;Rtt6n%yD#idHxIEaCrKSt| z3)ue09ck&Ly04d=+t4Aurlv9kER_G0r#cFl`Ze7!jL=XWDL?7058U|jtXj#_oW48J zZHzwp@@41vfvdVTvNcolTTrOlPON{w=1Y;0ezS<6OH_(>qh^PX`u`~htf%1-7QS0n z7UiQai@fM81H-4u!ze#$>SM=l&2zh-JP)fX-6rb?RR2=u-yTh--Nt$7vWZ!SxIGQI z(2EZ(E%8i3b~&;kzBm~GUn3ecSMB^@p+|-uLhKc?_%d2v}nGk1x-O zCH98VidPMzjss=lZyqowW@*UZzps*SP}K`hp)HqoD*l*TSj+{m`gWGP%oPDFNUVIn zLQS(a&puRERyOqM@pIqZWjQl5GtR45W%st%)c^zYyGKi>dNb8xB)p2v+v8@sI&8

ifyp(97Q;JrpF+_V-) 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 OF!z-1=G`%R_P+ojDC*My diff --git a/frontend/__snapshots__/scenes-app-persons-modal--server-error--dark.png b/frontend/__snapshots__/scenes-app-persons-modal--server-error--dark.png index be5d0970ca208d79ea711619d3e4515b91fd62c1..ee5eb6d97e9fdd606699453796e898216120842a 100644 GIT binary patch literal 14498 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 Ku97tm`~Lu2Jx1~X literal 15443 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 diff --git a/frontend/__snapshots__/scenes-app-persons-modal--server-error--light.png b/frontend/__snapshots__/scenes-app-persons-modal--server-error--light.png index 495ede9027f52b12345a36690c4b95529380d7a4..daa7e4e9adab17e91f8ec70a7c5a32108393483b 100644 GIT binary patch literal 14949 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 diff --git a/frontend/src/layout/navigation-3000/Navigation.stories.tsx b/frontend/src/layout/navigation-3000/Navigation.stories.tsx index 3b9671e50deec..a5cbadec01f73 100644 --- a/frontend/src/layout/navigation-3000/Navigation.stories.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.stories.tsx @@ -13,11 +13,11 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/dashboards/': require('../../scenes/dashboard/__mocks__/dashboards.json'), - '/api/projects/:team_id/dashboards/1/': require('../../scenes/dashboard/__mocks__/dashboard1.json'), - '/api/projects/:team_id/dashboards/1/collaborators/': [], - '/api/projects/:team_id/insights/my_last_viewed/': require('../../scenes/saved-insights/__mocks__/insightsMyLastViewed.json'), - '/api/projects/:team_id/session_recordings/': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/dashboards/': require('../../scenes/dashboard/__mocks__/dashboards.json'), + '/api/environments/:team_id/dashboards/1/': require('../../scenes/dashboard/__mocks__/dashboard1.json'), + '/api/environments/:team_id/dashboards/1/collaborators/': [], + '/api/environments/:team_id/insights/my_last_viewed/': require('../../scenes/saved-insights/__mocks__/insightsMyLastViewed.json'), + '/api/environments/:team_id/session_recordings/': EMPTY_PAGINATED_RESPONSE, }, }), ], diff --git a/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx b/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx index c1bb27c69182c..bdb07b697a5c2 100644 --- a/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx +++ b/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx @@ -37,7 +37,7 @@ const multipliedFeatureFlagsJson = { export function Dashboards(): JSX.Element { useStorybookMocks({ get: { - '/api/projects/:team_id/dashboards/': dashboardsJson, + '/api/environments/:team_id/dashboards/': dashboardsJson, }, }) const { showSidebar } = useActions(navigation3000Logic) diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx index 2cbd7574fa1fb..e83dde7f8f5a5 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx @@ -29,7 +29,7 @@ const meta: Meta = { '/api/projects/:id/integrations': { results: [] }, }, post: { - '/api/projects/:team_id/query': {}, + '/api/environments/:team_id/query': {}, }, }), ], diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts index bea56e12d350f..a7da16722813c 100644 --- a/frontend/src/lib/api.test.ts +++ b/frontend/src/lib/api.test.ts @@ -37,7 +37,7 @@ describe('API helper', () => { ) expect(fakeFetch).toHaveBeenCalledWith( - '/api/projects/2/events?properties=%5B%7B%22key%22%3A%22something%22%2C%22value%22%3A%22is_set%22%2C%22operator%22%3A%22is_set%22%2C%22type%22%3A%22event%22%7D%5D&limit=10&orderBy=%5B%22-timestamp%22%5D', + '/api/environments/2/events?properties=%5B%7B%22key%22%3A%22something%22%2C%22value%22%3A%22is_set%22%2C%22operator%22%3A%22is_set%22%2C%22type%22%3A%22event%22%7D%5D&limit=10&orderBy=%5B%22-timestamp%22%5D', { signal: undefined, headers: { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c078d8b91cb96..a8e6024171bee 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -83,6 +83,7 @@ import { PluginConfigTypeNew, PluginConfigWithPluginInfoNew, PluginLogEntry, + ProjectType, PropertyDefinition, PropertyDefinitionType, QueryBasedInsightModel, @@ -195,6 +196,7 @@ export async function getJSONOrNull(response: Response): Promise { export class ApiConfig { private static _currentOrganizationId: OrganizationType['id'] | null = null + private static _currentProjectId: ProjectType['id'] | null = null private static _currentTeamId: TeamType['id'] | null = null static getCurrentOrganizationId(): OrganizationType['id'] { @@ -218,6 +220,17 @@ export class ApiConfig { static setCurrentTeamId(id: TeamType['id']): void { this._currentTeamId = id } + + static getCurrentProjectId(): ProjectType['id'] { + if (!this._currentProjectId) { + throw new Error('Project ID is not known.') + } + return this._currentProjectId + } + + static setCurrentProjectId(id: ProjectType['id']): void { + this._currentProjectId = id + } } class ApiRequest { @@ -304,13 +317,22 @@ class ApiRequest { return this.addPathComponent('projects') } - public projectsDetail(id: TeamType['id'] = ApiConfig.getCurrentTeamId()): ApiRequest { + public projectsDetail(id: ProjectType['id'] = ApiConfig.getCurrentProjectId()): ApiRequest { return this.projects().addPathComponent(id) } + // # Projects + public environments(): ApiRequest { + return this.addPathComponent('environments') + } + + public environmentsDetail(id: TeamType['id'] = ApiConfig.getCurrentTeamId()): ApiRequest { + return this.environments().addPathComponent(id) + } + // # Insights public insights(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('insights') + return this.environmentsDetail(teamId).addPathComponent('insights') } public insight(id: QueryBasedInsightModel['id'], teamId?: TeamType['id']): ApiRequest { @@ -335,7 +357,7 @@ class ApiRequest { } public pluginConfigs(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('plugin_configs') + return this.environmentsDetail(teamId).addPathComponent('plugin_configs') } public pluginConfig(id: number, teamId?: TeamType['id']): ApiRequest { @@ -381,7 +403,7 @@ class ApiRequest { // # Exports public exports(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('exports') + return this.environmentsDetail(teamId).addPathComponent('exports') } public export(id: number, teamId?: TeamType['id']): ApiRequest { @@ -390,7 +412,7 @@ class ApiRequest { // # Events public events(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('events') + return this.environmentsDetail(teamId).addPathComponent('events') } public event(id: EventType['id'], teamId?: TeamType['id']): ApiRequest { @@ -402,16 +424,16 @@ class ApiRequest { } // # Data management - public eventDefinitions(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('event_definitions') + public eventDefinitions(projectId?: ProjectType['id']): ApiRequest { + return this.projectsDetail(projectId).addPathComponent('event_definitions') } - public eventDefinitionDetail(eventDefinitionId: EventDefinition['id'], teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('event_definitions').addPathComponent(eventDefinitionId) + public eventDefinitionDetail(eventDefinitionId: EventDefinition['id'], projectId?: ProjectType['id']): ApiRequest { + return this.projectsDetail(projectId).addPathComponent('event_definitions').addPathComponent(eventDefinitionId) } - public propertyDefinitions(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('property_definitions') + public propertyDefinitions(projectId?: ProjectType['id']): ApiRequest { + return this.projectsDetail(projectId).addPathComponent('property_definitions') } public propertyDefinitionDetail( @@ -458,13 +480,15 @@ class ApiRequest { // Recordings public recording(recordingId: SessionRecordingType['id'], teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('session_recordings').addPathComponent(recordingId) + return this.environmentsDetail(teamId).addPathComponent('session_recordings').addPathComponent(recordingId) } public recordings(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('session_recordings') + return this.environmentsDetail(teamId).addPathComponent('session_recordings') } public recordingMatchingEvents(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('session_recordings').addPathComponent('matching_events') + return this.environmentsDetail(teamId) + .addPathComponent('session_recordings') + .addPathComponent('matching_events') } public recordingPlaylists(teamId?: TeamType['id']): ApiRequest { return this.projectsDetail(teamId).addPathComponent('session_recording_playlists') @@ -484,7 +508,7 @@ class ApiRequest { // # Dashboards public dashboards(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('dashboards') + return this.environmentsDetail(teamId).addPathComponent('dashboards') } public dashboardsDetail(dashboardId: DashboardType['id'], teamId?: TeamType['id']): ApiRequest { @@ -564,7 +588,7 @@ class ApiRequest { // # Persons public persons(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('persons') + return this.environmentsDetail(teamId).addPathComponent('persons') } public person(id: string | number, teamId?: TeamType['id']): ApiRequest { @@ -580,7 +604,7 @@ class ApiRequest { // # Groups public groups(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('groups') + return this.environmentsDetail(teamId).addPathComponent('groups') } // # Search @@ -719,11 +743,11 @@ class ApiRequest { // # Subscriptions public subscriptions(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('subscriptions') + return this.environmentsDetail(teamId).addPathComponent('subscriptions') } public subscription(id: SubscriptionType['id'], teamId?: TeamType['id']): ApiRequest { - return this.subscriptions(teamId).addPathComponent(id) + return this.environmentsDetail(teamId).addPathComponent(id) } // # Integrations @@ -746,12 +770,15 @@ class ApiRequest { // # Alerts public alerts(alertId?: AlertType['id'], insightId?: InsightModel['id'], teamId?: TeamType['id']): ApiRequest { if (alertId) { - return this.projectsDetail(teamId).addPathComponent('alerts').addPathComponent(alertId).withQueryString({ - insight_id: insightId, - }) + return this.environmentsDetail(teamId) + .addPathComponent('alerts') + .addPathComponent(alertId) + .withQueryString({ + insight_id: insightId, + }) } - return this.projectsDetail(teamId).addPathComponent('alerts').withQueryString({ + return this.environmentsDetail(teamId).addPathComponent('alerts').withQueryString({ insight_id: insightId, }) } @@ -775,7 +802,7 @@ class ApiRequest { // # Queries public query(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('query') + return this.environmentsDetail(teamId).addPathComponent('query') } public queryStatus(queryId: string, showProgress: boolean, teamId?: TeamType['id']): ApiRequest { @@ -788,7 +815,7 @@ class ApiRequest { // Chat public chat(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('query').addPathComponent('chat') + return this.environmentsDetail(teamId).addPathComponent('query').addPathComponent('chat') } // Notebooks @@ -802,7 +829,7 @@ class ApiRequest { // Batch Exports public batchExports(teamId?: TeamType['id']): ApiRequest { - return this.projectsDetail(teamId).addPathComponent('batch_exports') + return this.environmentsDetail(teamId).addPathComponent('batch_exports') } public batchExport(id: BatchExportConfiguration['id'], teamId?: TeamType['id']): ApiRequest { @@ -907,14 +934,14 @@ const prepareUrl = (url: string): string => { return output } -const PROJECT_ID_REGEX = /\/api\/projects\/(\w+)(?:$|[/?#])/ +const PROJECT_ID_REGEX = /\/api\/(project|environment)s\/(\w+)(?:$|[/?#])/ const ensureProjectIdNotInvalid = (url: string): void => { const projectIdMatch = PROJECT_ID_REGEX.exec(url) if (projectIdMatch) { - const projectId = projectIdMatch[1].trim() + const projectId = projectIdMatch[2].trim() if (projectId === 'null' || projectId === 'undefined') { - throw { status: 0, detail: 'Cannot make request - project ID is unknown.' } + throw { status: 0, detail: `Cannot make request - ${projectIdMatch[1]} ID is unknown.` } } } } @@ -1249,7 +1276,7 @@ const api = { }, async list({ limit = EVENT_DEFINITIONS_PER_PAGE, - teamId = ApiConfig.getCurrentTeamId(), + teamId, ...params }: { limit?: number @@ -1265,7 +1292,7 @@ const api = { }, determineListEndpoint({ limit = EVENT_DEFINITIONS_PER_PAGE, - teamId = ApiConfig.getCurrentTeamId(), + teamId, ...params }: { limit?: number @@ -1314,7 +1341,7 @@ const api = { }, async list({ limit = EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - teamId = ApiConfig.getCurrentTeamId(), + teamId, ...params }: { event_names?: string[] @@ -1340,7 +1367,7 @@ const api = { }, determineListEndpoint({ limit = EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - teamId = ApiConfig.getCurrentTeamId(), + teamId, ...params }: { event_names?: string[] @@ -1368,7 +1395,7 @@ const api = { sessions: { async propertyDefinitions({ - teamId = ApiConfig.getCurrentTeamId(), + teamId, search, properties, }: { diff --git a/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx b/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx index 1a549691f9088..d850cd6258cdf 100644 --- a/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx +++ b/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx @@ -42,7 +42,7 @@ const meta: Meta = { ctx.status(200), ctx.json({ results: featureFlagsActivityResponseJson }), ], - '/api/projects/:team/insights/activity': (_, __, ctx) => [ + '/api/environments/:team_id/insights/activity': (_, __, ctx) => [ ctx.status(200), ctx.json({ results: insightsActivityResponseJson }), ], diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx index 2f290332b7e19..a302f831743a2 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx @@ -13,7 +13,7 @@ describe('the activity log logic', () => { describe('humanizing insights', () => { const insightTestSetup = makeTestSetup( ActivityScope.INSIGHT, - `/api/projects/${MOCK_TEAM_ID}/insights/activity/` + `/api/environments/${MOCK_TEAM_ID}/insights/activity/` ) it('can handle change of name', async () => { diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx index 8a34afd1c00a6..988b1b741f9c8 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx @@ -8,7 +8,10 @@ import { ActivityScope } from '~/types' describe('the activity log logic', () => { describe('humanizing persons', () => { - const personTestSetup = makeTestSetup(ActivityScope.PERSON, `/api/projects/${MOCK_TEAM_ID}/persons/7/activity/`) + const personTestSetup = makeTestSetup( + ActivityScope.PERSON, + `/api/environments/${MOCK_TEAM_ID}/persons/7/activity/` + ) it('can handle addition of a property', async () => { const logic = await personTestSetup('test person', 'updated', [ { diff --git a/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts b/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts index b6d2f1c5fc8eb..a8c984dc0e928 100644 --- a/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts +++ b/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts @@ -181,7 +181,7 @@ function useInsightMocks(interval: string = 'day', timezone: string = 'UTC'): vo } useMocks({ get: { - '/api/projects/:team_id/insights/': () => { + '/api/environments/:team_id/insights/': () => { return [ 200, { @@ -189,7 +189,7 @@ function useInsightMocks(interval: string = 'day', timezone: string = 'UTC'): vo }, ] }, - [`/api/projects/:team_id/insights/${MOCK_INSIGHT_NUMERIC_ID}`]: () => { + [`/api/environments/:team_id/insights/${MOCK_INSIGHT_NUMERIC_ID}`]: () => { return [200, insight] }, '/api/users/@me/': [200, {}], diff --git a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts index b8678715352ea..6e9897b9e2693 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts +++ b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts @@ -20,7 +20,7 @@ describe('the authorized urls list logic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/insights/trend/': (req) => { + '/api/environments/:team_id/insights/trend/': (req) => { if (JSON.parse(req.url.searchParams.get('events') || '[]')?.[0]?.throw) { return [500, { status: 0, detail: 'error from the API' }] } diff --git a/frontend/src/lib/components/PropertiesTimeline/PropertiesTimeline.stories.tsx b/frontend/src/lib/components/PropertiesTimeline/PropertiesTimeline.stories.tsx index 3d38ab578827d..cbfcbf10df59f 100644 --- a/frontend/src/lib/components/PropertiesTimeline/PropertiesTimeline.stories.tsx +++ b/frontend/src/lib/components/PropertiesTimeline/PropertiesTimeline.stories.tsx @@ -27,7 +27,7 @@ export function MultiplePointsForOnePersonProperty(): JSX.Element { const examplePerson: PersonActorType = { ...EXAMPLE_PERSON, id: 1, uuid: '012e89b5-4239-4319-8ae4-d3cae2f5deb1' } useStorybookMocks({ get: { - [`/api/projects/${MOCK_TEAM_ID}/persons/${examplePerson.uuid}/properties_timeline/`]: { + [`/api/environments/${MOCK_TEAM_ID}/persons/${examplePerson.uuid}/properties_timeline/`]: { points: [ { timestamp: '2021-01-01T00:00:00.000Z', @@ -88,7 +88,7 @@ export function OnePointForOnePersonProperty(): JSX.Element { const examplePerson: PersonActorType = { ...EXAMPLE_PERSON, id: 2, uuid: '012e89b5-4239-4319-8ae4-d3cae2f5deb2' } useStorybookMocks({ get: { - [`/api/projects/${MOCK_TEAM_ID}/persons/${examplePerson.uuid}/properties_timeline/`]: { + [`/api/environments/${MOCK_TEAM_ID}/persons/${examplePerson.uuid}/properties_timeline/`]: { points: [ { timestamp: '2021-05-01T00:00:00.000Z', @@ -125,7 +125,7 @@ export function NoPointsForNoPersonProperties(): JSX.Element { const examplePerson: PersonActorType = { ...EXAMPLE_PERSON, id: 3, uuid: '012e89b5-4239-4319-8ae4-d3cae2f5deb3' } useStorybookMocks({ get: { - [`/api/projects/${MOCK_TEAM_ID}/persons/${examplePerson.uuid}/properties_timeline/`]: { + [`/api/environments/${MOCK_TEAM_ID}/persons/${examplePerson.uuid}/properties_timeline/`]: { points: [ { timestamp: '2021-01-01T00:00:00.000Z', diff --git a/frontend/src/lib/components/PropertiesTimeline/propertiesTimelineLogic.ts b/frontend/src/lib/components/PropertiesTimeline/propertiesTimelineLogic.ts index adff5170c3cdf..b1f3c848cfd68 100644 --- a/frontend/src/lib/components/PropertiesTimeline/propertiesTimelineLogic.ts +++ b/frontend/src/lib/components/PropertiesTimeline/propertiesTimelineLogic.ts @@ -66,7 +66,7 @@ export const propertiesTimelineLogic = kea([ if (props.actor.type === 'person') { const queryId = uuid() const response = await apiGetWithTimeToSeeDataTracking( - `api/projects/${values.currentTeamId}/persons/${ + `api/environments/${values.currentTeamId}/persons/${ props.actor.uuid }/properties_timeline/?${toParams(props.filter)}`, values.currentTeamId, diff --git a/frontend/src/lib/components/PropertySelect/PropertySelect.stories.tsx b/frontend/src/lib/components/PropertySelect/PropertySelect.stories.tsx index 35021b789889b..b42bf7366157d 100644 --- a/frontend/src/lib/components/PropertySelect/PropertySelect.stories.tsx +++ b/frontend/src/lib/components/PropertySelect/PropertySelect.stories.tsx @@ -13,7 +13,7 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/persons/properties': [ + '/api/environments/:team_id/persons/properties': [ { name: 'Property A', count: 10 }, { name: 'Property B', count: 20 }, { name: 'Property C', count: 30 }, diff --git a/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts b/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts index 5ea635b7e4f90..8842402310efe 100644 --- a/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts +++ b/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts @@ -11,7 +11,7 @@ const doesNotHaveReverseProxyValues = [[null], [null]] const useMockedValues = (results: (string | null)[][]): void => { useMocks({ post: { - '/api/projects/:team/query': () => [ + '/api/environments/:team_id/query': () => [ 200, { results, diff --git a/frontend/src/lib/components/Sharing/SharingModal.stories.tsx b/frontend/src/lib/components/Sharing/SharingModal.stories.tsx index 8c088002c4cb7..75f527f56da73 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.stories.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.stories.tsx @@ -36,9 +36,9 @@ const Template = (args: Partial & { licensed?: boolean }): JS useStorybookMocks({ get: { ...[ - '/api/projects/:id/insights/:insight_id/sharing/', - '/api/projects/:id/dashboards/:dashboard_id/sharing/', - '/api/projects/:id/session_recordings/:recording_id/sharing/', + '/api/environments/:id/insights/:insight_id/sharing/', + '/api/environments/:id/dashboards/:dashboard_id/sharing/', + '/api/environments/:id/session_recordings/:recording_id/sharing/', ].reduce( (acc, url) => ({ ...acc, @@ -50,13 +50,13 @@ const Template = (args: Partial & { licensed?: boolean }): JS }), {} ), - '/api/projects/:id/insights/': { results: [fakeInsight] }, + '/api/environments/:id/insights/': { results: [fakeInsight] }, }, patch: { ...[ - '/api/projects/:id/insights/:insight_id/sharing/', - '/api/projects/:id/dashboards/:dashboard_id/sharing/', - '/api/projects/:id/session_recordings/:recording_id/sharing/', + '/api/environments/:id/insights/:insight_id/sharing/', + '/api/environments/:id/dashboards/:dashboard_id/sharing/', + '/api/environments/:id/session_recordings/:recording_id/sharing/', ].reduce( (acc, url) => ({ ...acc, diff --git a/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx b/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx index ce6bca5ee6903..0dcb793df4383 100644 --- a/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx +++ b/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx @@ -40,7 +40,7 @@ const Template = ( slack_service: noIntegrations ? { available: false } : { available: true, client_id: 'test-client-id' }, site_url: noIntegrations ? 'bad-value' : window.location.origin, }, - '/api/projects/:id/subscriptions': { + '/api/environments/:id/subscriptions': { results: insightShortIdRef.current === 'empty' ? [] @@ -61,7 +61,7 @@ const Template = ( }), ], }, - '/api/projects/:id/subscriptions/:subId': createMockSubscription(), + '/api/environments/:id/subscriptions/:subId': createMockSubscription(), '/api/projects/:id/integrations': { results: !noIntegrations ? [mockIntegration] : [] }, '/api/projects/:id/integrations/:intId/channels': { channels: mockSlackChannels }, }, diff --git a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts index 2cfbe7207bf52..092614abd0d67 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts @@ -54,15 +54,15 @@ describe('subscriptionsLogic', () => { subscriptions = [fixtureSubscriptionResponse(1), fixtureSubscriptionResponse(2)] useMocks({ get: { - '/api/projects/:team/insights/1': fixtureInsightResponse(1), - '/api/projects/:team/insights/2': fixtureInsightResponse(2), - '/api/projects/:team/insights': (req) => { + '/api/environments/:team_id/insights/1': fixtureInsightResponse(1), + '/api/environments/:team_id/insights/2': fixtureInsightResponse(2), + '/api/environments/:team_id/insights': (req) => { const insightShortId = req.url.searchParams.get('short_id') const res = insightShortId ? [fixtureInsightResponse(parseInt(insightShortId, 10))] : [] return [200, { results: res }] }, - '/api/projects/:team/subscriptions': (req) => { + '/api/environments/:team_id/subscriptions': (req) => { const insightId = req.url.searchParams.get('insight') let results: SubscriptionType[] = [] diff --git a/frontend/src/lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator.ts b/frontend/src/lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator.ts index 9c29ae50e755c..f5140db2985e0 100644 --- a/frontend/src/lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator.ts +++ b/frontend/src/lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator.ts @@ -4,7 +4,7 @@ import { mockActionDefinition } from '~/test/mocks' export const taxonomicFilterMocksDecorator = mswDecorator({ get: { '/api/projects/:team_id/actions': { results: [mockActionDefinition] }, - '/api/projects/:team_id/persons/properties': [ + '/api/environments/:team_id/persons/properties': [ { id: 1, name: 'location', count: 1 }, { id: 2, name: 'role', count: 2 }, { id: 3, name: 'height', count: 3 }, diff --git a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts index cf04e58e218ae..25e2866afc2c3 100644 --- a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts +++ b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts @@ -9,7 +9,10 @@ import { AppContext, PropertyDefinition } from '~/types' import { infiniteListLogic } from './infiniteListLogic' -window.POSTHOG_APP_CONTEXT = { current_team: { id: MOCK_TEAM_ID } } as unknown as AppContext +window.POSTHOG_APP_CONTEXT = { + current_team: { id: MOCK_TEAM_ID }, + current_project: { id: MOCK_TEAM_ID }, +} as unknown as AppContext describe('infiniteListLogic', () => { let logic: ReturnType diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts index 74278c510754e..90103bc8686ad 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts @@ -12,7 +12,10 @@ import { AppContext } from '~/types' import { infiniteListLogic } from './infiniteListLogic' -window.POSTHOG_APP_CONTEXT = { current_team: { id: MOCK_TEAM_ID } } as unknown as AppContext +window.POSTHOG_APP_CONTEXT = { + current_team: { id: MOCK_TEAM_ID }, + current_project: { id: MOCK_TEAM_ID }, +} as unknown as AppContext describe('taxonomicFilterLogic', () => { let logic: ReturnType @@ -33,7 +36,7 @@ describe('taxonomicFilterLogic', () => { }, ] }, - '/api/projects/:team/sessions/property_definitions': (res) => { + '/api/environments/:team/sessions/property_definitions': (res) => { const search = res.url.searchParams.get('search') const results = search ? mockSessionPropertyDefinitions.filter((e) => e.name.includes(search)) diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index a93ba76878fe0..4f78288aa2c75 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -20,6 +20,7 @@ import { dataWarehouseSceneLogic } from 'scenes/data-warehouse/settings/dataWare import { experimentsLogic } from 'scenes/experiments/experimentsLogic' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { projectLogic } from 'scenes/projectLogic' import { ReplayTaxonomicFilters } from 'scenes/session-recordings/filters/ReplayTaxonomicFilters' import { teamLogic } from 'scenes/teamLogic' @@ -79,6 +80,8 @@ export const taxonomicFilterLogic = kea([ values: [ teamLogic, ['currentTeamId'], + projectLogic, + ['currentProjectId'], groupsModel, ['groupTypes', 'aggregationLabel'], groupPropertiesModel, @@ -159,6 +162,7 @@ export const taxonomicFilterLogic = kea([ taxonomicGroups: [ (s) => [ s.currentTeamId, + s.currentProjectId, s.groupAnalyticsTaxonomicGroups, s.groupAnalyticsTaxonomicGroupNames, s.eventNames, @@ -169,6 +173,7 @@ export const taxonomicFilterLogic = kea([ ], ( teamId, + projectId, groupAnalyticsTaxonomicGroups, groupAnalyticsTaxonomicGroupNames, eventNames, @@ -185,7 +190,7 @@ export const taxonomicFilterLogic = kea([ options: [{ name: 'All events', value: null }].filter( (o) => !excludedProperties[TaxonomicFilterGroupType.Events]?.includes(o.value) ), - endpoint: combineUrl(`api/projects/${teamId}/event_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/event_definitions`, { event_type: EventDefinitionType.Event, }).url, getName: (eventDefinition: Record) => eventDefinition.name, @@ -261,7 +266,7 @@ export const taxonomicFilterLogic = kea([ name: 'Event properties', searchPlaceholder: 'event properties', type: TaxonomicFilterGroupType.EventProperties, - endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/property_definitions`, { is_feature_flag: false, ...(eventNames.length > 0 ? { event_names: eventNames } : {}), properties: propertyAllowList?.[TaxonomicFilterGroupType.EventProperties] @@ -270,7 +275,7 @@ export const taxonomicFilterLogic = kea([ }).url, scopedEndpoint: eventNames.length > 0 - ? combineUrl(`api/projects/${teamId}/property_definitions`, { + ? combineUrl(`api/projects/${projectId}/property_definitions`, { event_names: eventNames, is_feature_flag: false, filter_by_event_names: true, @@ -296,13 +301,13 @@ export const taxonomicFilterLogic = kea([ name: 'Feature flags', searchPlaceholder: 'feature flags', type: TaxonomicFilterGroupType.EventFeatureFlags, - endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/property_definitions`, { is_feature_flag: true, ...(eventNames.length > 0 ? { event_names: eventNames } : {}), }).url, scopedEndpoint: eventNames.length > 0 - ? combineUrl(`api/projects/${teamId}/property_definitions`, { + ? combineUrl(`api/projects/${projectId}/property_definitions`, { event_names: eventNames, is_feature_flag: true, filter_by_event_names: true, @@ -324,7 +329,7 @@ export const taxonomicFilterLogic = kea([ name: 'Numerical event properties', searchPlaceholder: 'numerical event properties', type: TaxonomicFilterGroupType.NumericalEventProperties, - endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/property_definitions`, { is_numerical: true, event_names: eventNames, }).url, @@ -336,7 +341,7 @@ export const taxonomicFilterLogic = kea([ name: 'Person properties', searchPlaceholder: 'person properties', type: TaxonomicFilterGroupType.PersonProperties, - endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/property_definitions`, { type: 'person', properties: propertyAllowList?.[TaxonomicFilterGroupType.PersonProperties] ? propertyAllowList[TaxonomicFilterGroupType.PersonProperties].join(',') @@ -377,7 +382,7 @@ export const taxonomicFilterLogic = kea([ name: 'Pageview URLs', searchPlaceholder: 'pageview URLs', type: TaxonomicFilterGroupType.PageviewUrls, - endpoint: `api/projects/${teamId}/events/values/?key=$current_url`, + endpoint: `api/environments/${teamId}/events/values/?key=$current_url`, searchAlias: 'value', getName: (option: SimpleOption) => option.name, getValue: (option: SimpleOption) => option.name, @@ -387,7 +392,7 @@ export const taxonomicFilterLogic = kea([ name: 'Screens', searchPlaceholder: 'screens', type: TaxonomicFilterGroupType.Screens, - endpoint: `api/projects/${teamId}/events/values/?key=$screen_name`, + endpoint: `api/environments/${teamId}/events/values/?key=$screen_name`, searchAlias: 'value', getName: (option: SimpleOption) => option.name, getValue: (option: SimpleOption) => option.name, @@ -397,7 +402,7 @@ export const taxonomicFilterLogic = kea([ name: 'Custom Events', searchPlaceholder: 'custom events', type: TaxonomicFilterGroupType.CustomEvents, - endpoint: combineUrl(`api/projects/${teamId}/event_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/event_definitions`, { event_type: EventDefinitionType.EventCustom, }).url, getName: (eventDefinition: EventDefinition) => eventDefinition.name, @@ -417,7 +422,7 @@ export const taxonomicFilterLogic = kea([ name: 'Persons', searchPlaceholder: 'persons', type: TaxonomicFilterGroupType.Persons, - endpoint: `api/projects/${teamId}/persons/`, + endpoint: `api/environments/${teamId}/persons/`, getName: (person: PersonType) => person.name || 'Anon user?', getValue: (person: PersonType) => person.distinct_ids[0], getPopoverHeader: () => `Person`, @@ -426,7 +431,7 @@ export const taxonomicFilterLogic = kea([ name: 'Insights', searchPlaceholder: 'insights', type: TaxonomicFilterGroupType.Insights, - endpoint: combineUrl(`api/projects/${teamId}/insights/`, { + endpoint: combineUrl(`api/environments/${teamId}/insights/`, { saved: true, }).url, getName: (insight: QueryBasedInsightModel) => insight.name, @@ -481,7 +486,7 @@ export const taxonomicFilterLogic = kea([ getName: (option: any) => option.name, getValue: (option) => option.name, getPopoverHeader: () => 'Session', - endpoint: `api/projects/${teamId}/sessions/property_definitions`, + endpoint: `api/environments/${teamId}/sessions/property_definitions`, getIcon: getPropertyDefinitionIcon, }, { @@ -500,6 +505,7 @@ export const taxonomicFilterLogic = kea([ valuesEndpoint: (key) => { if (key === 'visited_page') { return ( + `api/environments/${teamId}/events/values/?key=` + 'api/event/values/?key=' + encodeURIComponent('$current_url') + '&event_name=' + @@ -532,7 +538,7 @@ export const taxonomicFilterLogic = kea([ name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).plural)}`, searchPlaceholder: `${aggregationLabel(type.group_type_index).plural}`, type: `${TaxonomicFilterGroupType.GroupNamesPrefix}_${type.group_type_index}` as unknown as TaxonomicFilterGroupType, - endpoint: combineUrl(`api/projects/${teamId}/groups/`, { + endpoint: combineUrl(`api/environments/${teamId}/groups/`, { group_type_index: type.group_type_index, }).url, searchAlias: 'group_key', @@ -543,13 +549,13 @@ export const taxonomicFilterLogic = kea([ })), ], groupAnalyticsTaxonomicGroups: [ - (s) => [s.groupTypes, s.currentTeamId, s.aggregationLabel], - (groupTypes, teamId, aggregationLabel): TaxonomicFilterGroup[] => + (s) => [s.groupTypes, s.currentProjectId, s.currentTeamId, s.aggregationLabel], + (groupTypes, projectId, teamId, aggregationLabel): TaxonomicFilterGroup[] => Array.from(groupTypes.values()).map((type) => ({ name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).singular)} properties`, searchPlaceholder: `${aggregationLabel(type.group_type_index).singular} properties`, type: `${TaxonomicFilterGroupType.GroupsPrefix}_${type.group_type_index}` as unknown as TaxonomicFilterGroupType, - endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { + endpoint: combineUrl(`api/projects/${projectId}/property_definitions`, { type: 'group', group_type_index: type.group_type_index, }).url, diff --git a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts index 2107640885e26..103e89b2bfed9 100644 --- a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts +++ b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts @@ -24,7 +24,7 @@ const useMockedVersions = ( ], }, post: { - '/api/projects/:team/query': () => [ + '/api/environments/:team_id/query': () => [ 200, { results: usedVersions.map((x) => [x.version, x.timestamp]), diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 3b5898aa52e64..9d14c1b3c0acf 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -57,7 +57,8 @@ export const defaultMocks: Mocks = { '/api/projects/:team_id/annotations/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/event_definitions/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/cohorts/': toPaginatedResponse([MOCK_DEFAULT_COHORT]), - '/api/projects/:team_id/dashboards/': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/dashboards/': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/alerts/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/dashboard_templates': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/dashboard_templates/repository/': [], '/api/projects/:team_id/external_data_sources/': EMPTY_PAGINATED_RESPONSE, @@ -76,8 +77,8 @@ export const defaultMocks: Mocks = { }, '/api/projects/:team_id/groups/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/groups_types/': [], - '/api/projects/:team_id/insights/': EMPTY_PAGINATED_RESPONSE, - '/api/projects/:team_id/insights/:insight_id/sharing/': { + '/api/environments/:team_id/insights/': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/insights/:insight_id/sharing/': { enabled: false, access_token: 'foo', created_at: '2020-11-11T00:00:00Z', @@ -86,7 +87,7 @@ export const defaultMocks: Mocks = { '/api/projects/:team_id/feature_flags/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/feature_flags/:feature_flag_id/role_access': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/experiments/': EMPTY_PAGINATED_RESPONSE, - '/api/projects/:team_id/explicit_members/': [], + '/api/environments/:team_id/explicit_members/': [], '/api/projects/:team_id/warehouse_view_link/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/warehouse_saved_queries/': EMPTY_PAGINATED_RESPONSE, '/api/projects/:team_id/warehouse_tables/': EMPTY_PAGINATED_RESPONSE, @@ -104,9 +105,9 @@ export const defaultMocks: Mocks = { '/api/organizations/@current/plugins/repository/': [], '/api/organizations/@current/plugins/unused/': [], '/api/plugin_config/': toPaginatedResponse([MOCK_DEFAULT_PLUGIN_CONFIG]), - [`/api/projects/:team_id/plugin_configs/${MOCK_DEFAULT_PLUGIN_CONFIG.id}/`]: MOCK_DEFAULT_PLUGIN_CONFIG, - '/api/projects/:team_id/persons': EMPTY_PAGINATED_RESPONSE, - '/api/projects/:team_id/persons/properties/': toPaginatedResponse(MOCK_PERSON_PROPERTIES), + [`/api/environments/:team_id/plugin_configs/${MOCK_DEFAULT_PLUGIN_CONFIG.id}/`]: MOCK_DEFAULT_PLUGIN_CONFIG, + '/api/environments/:team_id/persons': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/persons/properties/': toPaginatedResponse(MOCK_PERSON_PROPERTIES), '/api/personal_api_keys/': [], '/api/users/@me/': (): MockSignature => [ 200, @@ -118,6 +119,7 @@ export const defaultMocks: Mocks = { }, }, ], + '/api/environments/@current/': MOCK_DEFAULT_TEAM, '/api/projects/@current/': MOCK_DEFAULT_TEAM, '/api/projects/:team_id/comments/count': { count: 0 }, '/api/projects/:team_id/comments': { results: [] }, @@ -157,8 +159,8 @@ export const defaultMocks: Mocks = { 'https://us.i.posthog.com/decide/': (req, res, ctx): MockSignature => posthogCORSResponse(req, res, ctx), '/decide/': (req, res, ctx): MockSignature => posthogCORSResponse(req, res, ctx), 'https://us.i.posthog.com/engage/': (req, res, ctx): MockSignature => posthogCORSResponse(req, res, ctx), - '/api/projects/:team_id/insights/:insight_id/viewed/': (): MockSignature => [201, null], - 'api/projects/:team_id/query': [200, { results: [] }], + '/api/environments/:team_id/insights/:insight_id/viewed/': (): MockSignature => [201, null], + 'api/environments/:team_id/query': [200, { results: [] }], }, patch: { '/api/projects/:team_id/session_recording_playlists/:playlist_id/': {}, diff --git a/frontend/src/models/dashboardsModel.test.ts b/frontend/src/models/dashboardsModel.test.ts index 8c021609e5ded..c3d42a78b83b7 100644 --- a/frontend/src/models/dashboardsModel.test.ts +++ b/frontend/src/models/dashboardsModel.test.ts @@ -62,7 +62,7 @@ describe('the dashboards model', () => { beforeEach(async () => { useMocks({ get: { - '/api/projects/:team_id/dashboards/': () => { + '/api/environments/:team_id/dashboards/': () => { return [ 200, { diff --git a/frontend/src/models/dashboardsModel.tsx b/frontend/src/models/dashboardsModel.tsx index f90650d8bb831..99c4cdf01ea96 100644 --- a/frontend/src/models/dashboardsModel.tsx +++ b/frontend/src/models/dashboardsModel.tsx @@ -92,7 +92,7 @@ export const dashboardsModel = kea([ return { count: 0, next: null, previous: null, results: [] } } const dashboards: PaginatedResponse = await api.get( - url || `api/projects/${teamLogic.values.currentTeamId}/dashboards/?limit=2000` + url || `api/environments/${teamLogic.values.currentTeamId}/dashboards/?limit=2000` ) return { @@ -115,7 +115,7 @@ export const dashboardsModel = kea([ const beforeChange = { ...values.rawDashboards[id] } const response = (await api.update( - `api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}`, + `api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}`, payload )) as DashboardType const updatedAttribute = Object.keys(payload)[0] @@ -135,7 +135,7 @@ export const dashboardsModel = kea([ label: 'Undo', action: async () => { const reverted = (await api.update( - `api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}`, + `api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}`, beforeChange )) as DashboardType actions.updateDashboardSuccess(getQueryBasedDashboard(reverted)) @@ -148,33 +148,39 @@ export const dashboardsModel = kea([ }, deleteDashboard: async ({ id, deleteInsights }) => getQueryBasedDashboard( - await api.update(`api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}`, { + await api.update(`api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}`, { deleted: true, delete_insights: deleteInsights, }) ) as DashboardType, restoreDashboard: async ({ id }) => getQueryBasedDashboard( - await api.update(`api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}`, { + await api.update(`api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}`, { deleted: false, }) ) as DashboardType, pinDashboard: async ({ id, source }) => { - const response = (await api.update(`api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}`, { - pinned: true, - })) as DashboardType + const response = (await api.update( + `api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}`, + { + pinned: true, + } + )) as DashboardType eventUsageLogic.actions.reportDashboardPinToggled(true, source) return getQueryBasedDashboard(response)! }, unpinDashboard: async ({ id, source }) => { - const response = (await api.update(`api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}`, { - pinned: false, - })) as DashboardType + const response = (await api.update( + `api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}`, + { + pinned: false, + } + )) as DashboardType eventUsageLogic.actions.reportDashboardPinToggled(false, source) return getQueryBasedDashboard(response)! }, duplicateDashboard: async ({ id, name, show, duplicateTiles }) => { - const result = (await api.create(`api/projects/${teamLogic.values.currentTeamId}/dashboards/`, { + const result = (await api.create(`api/environments/${teamLogic.values.currentTeamId}/dashboards/`, { use_dashboard: id, name: `${name} (Copy)`, duplicate_tiles: duplicateTiles, diff --git a/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx b/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx index 7b20caa2b8037..4868de5e4fe3e 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx @@ -19,8 +19,8 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/events': events, - '/api/projects/:team_id/persons': persons, + '/api/environments/:team_id/events': events, + '/api/environments/:team_id/persons': persons, }, }), ], diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts index be84e5ad61e14..9401a11605d7c 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts @@ -17,7 +17,7 @@ describe('dataNodeLogic - query cancellation', () => { featureFlagLogic.mount() useMocks({ get: { - '/api/projects/:team/insights/trend/': async (req) => { + '/api/environments/:team_id/insights/trend/': async (req) => { if (req.url.searchParams.get('date_from') === '-180d') { // delay for a second before response without pausing return new Promise((resolve) => @@ -30,8 +30,8 @@ describe('dataNodeLogic - query cancellation', () => { }, }, post: { - '/api/projects/997/insights/cancel/': [201], - '/api/projects/997/query/': async () => { + '/api/environments/997/insights/cancel/': [201], + '/api/environments/997/query/': async () => { return new Promise((resolve) => setTimeout(() => { resolve([200, { result: ['slow result from api'] }]) @@ -40,7 +40,7 @@ describe('dataNodeLogic - query cancellation', () => { }, }, delete: { - '/api/projects/:team_id/query/uuid-first': [200, {}], + '/api/environments/:team_id/query/uuid-first': [200, {}], }, }) }) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 997857b202f9c..6d1bdfae9ff6e 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -652,7 +652,7 @@ export const dataNodeLogic = kea([ abortQuery: async ({ queryId }) => { try { const { currentTeamId } = values - await api.delete(`api/projects/${currentTeamId}/query/${queryId}/`) + await api.delete(`api/environments/${currentTeamId}/query/${queryId}/`) } catch (e) { console.warn('Failed cancelling query', e) } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx index 64a978e45f95d..e909710177813 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx @@ -19,8 +19,8 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/events': events, - '/api/projects/:team_id/persons': persons, + '/api/environments/:team_id/events': events, + '/api/environments/:team_id/persons': persons, }, }), ], diff --git a/frontend/src/queries/query.test.ts b/frontend/src/queries/query.test.ts index f5783bebd2113..060333b75be19 100644 --- a/frontend/src/queries/query.test.ts +++ b/frontend/src/queries/query.test.ts @@ -11,7 +11,7 @@ describe('query', () => { beforeEach(() => { useMocks({ post: { - '/api/projects/:team/query': (req) => { + '/api/environments/:team_id/query': (req) => { const data = req.body as any if (data.query?.kind === 'HogQLQuery') { return [200, { results: [], clickhouse: 'clickhouse string', hogql: 'hogql string' }] diff --git a/frontend/src/scenes/activity/explore/Events.stories.tsx b/frontend/src/scenes/activity/explore/Events.stories.tsx index d38e1686b4ec9..0545ece9a5640 100644 --- a/frontend/src/scenes/activity/explore/Events.stories.tsx +++ b/frontend/src/scenes/activity/explore/Events.stories.tsx @@ -14,7 +14,7 @@ const meta: Meta = { decorators: [ mswDecorator({ post: { - '/api/projects/:team_id/query': eventsQuery, + '/api/environments/:team_id/query': eventsQuery, }, }), ], diff --git a/frontend/src/scenes/appContextLogic.ts b/frontend/src/scenes/appContextLogic.ts index ce3a6a11a317c..3e17e10876136 100644 --- a/frontend/src/scenes/appContextLogic.ts +++ b/frontend/src/scenes/appContextLogic.ts @@ -7,6 +7,7 @@ import { UserType } from '~/types' import type { appContextLogicType } from './appContextLogicType' import { organizationLogic } from './organizationLogic' +import { projectLogic } from './projectLogic' import { teamLogic } from './teamLogic' import { userLogic } from './userLogic' @@ -19,7 +20,9 @@ export const appContextLogic = kea([ organizationLogic, ['loadCurrentOrganizationSuccess'], teamLogic, - ['loadCurrentTeam', 'loadCurrentTeamSuccess'], + ['loadCurrentTeam'], + projectLogic, + ['loadCurrentProject'], ], }), afterMount(({ actions }) => { @@ -43,6 +46,7 @@ export const appContextLogic = kea([ // NOTE: This doesn't fix the issue but removes the confusion of seeing incorrect user info in the UI actions.loadUserSuccess(remoteUser) actions.loadCurrentOrganizationSuccess(remoteUser.organization) + actions.loadCurrentProject() actions.loadCurrentTeam() } }) diff --git a/frontend/src/scenes/dashboard/DashboardInsightCardLegend.stories.tsx b/frontend/src/scenes/dashboard/DashboardInsightCardLegend.stories.tsx index 5c9caef93e5d2..bf3eebbc439e1 100644 --- a/frontend/src/scenes/dashboard/DashboardInsightCardLegend.stories.tsx +++ b/frontend/src/scenes/dashboard/DashboardInsightCardLegend.stories.tsx @@ -11,8 +11,8 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/dashboards/1/': require('./__mocks__/dashboard_insight_card_legend_query.json'), - '/api/projects/:team_id/dashboards/2/': require('./__mocks__/dashboard_insight_card_legend_legacy.json'), + '/api/environments/:team_id/dashboards/1/': require('./__mocks__/dashboard_insight_card_legend_query.json'), + '/api/environments/:team_id/dashboards/2/': require('./__mocks__/dashboard_insight_card_legend_legacy.json'), }, }), ], diff --git a/frontend/src/scenes/dashboard/Dashboards.stories.tsx b/frontend/src/scenes/dashboard/Dashboards.stories.tsx index 34e6be5f20878..16d30235fa2a2 100644 --- a/frontend/src/scenes/dashboard/Dashboards.stories.tsx +++ b/frontend/src/scenes/dashboard/Dashboards.stories.tsx @@ -18,13 +18,13 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/dashboards/': require('./__mocks__/dashboards.json'), - '/api/projects/:team_id/dashboards/1/': require('./__mocks__/dashboard1.json'), - '/api/projects/:team_id/dashboards/1/collaborators/': [], - '/api/projects/:team_id/dashboards/2/': [500, { detail: 'Server error' }], + '/api/environments/:team_id/dashboards/': require('./__mocks__/dashboards.json'), + '/api/environments/:team_id/dashboards/1/': require('./__mocks__/dashboard1.json'), + '/api/environments/:team_id/dashboards/1/collaborators/': [], + '/api/environments/:team_id/dashboards/2/': [500, { detail: 'Server error' }], '/api/projects/:team_id/dashboard_templates/': require('./__mocks__/dashboard_templates.json'), '/api/projects/:team_id/dashboard_templates/json_schema/': require('./__mocks__/dashboard_template_schema.json'), - '/api/projects/:team_id/dashboards/:dash_id/sharing/': { + '/api/environments/:team_id/dashboards/:dash_id/sharing/': { created_at: '2023-02-25T13:28:20.454940Z', enabled: false, access_token: 'a-secret-token', diff --git a/frontend/src/scenes/dashboard/dashboardLogic.test.ts b/frontend/src/scenes/dashboard/dashboardLogic.test.ts index fea16483894fd..cb60271cae3bf 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.test.ts +++ b/frontend/src/scenes/dashboard/dashboardLogic.test.ts @@ -179,7 +179,7 @@ describe('dashboardLogic', () => { } useMocks({ get: { - '/api/projects/:team/query/123/': () => [ + '/api/environments/:team_id/query/123/': () => [ 200, { query_status: { @@ -187,14 +187,14 @@ describe('dashboardLogic', () => { }, }, ], - '/api/projects/:team/dashboards/5/': { ...dashboards['5'] }, - '/api/projects/:team/dashboards/6/': { ...dashboards['6'] }, - '/api/projects/:team/dashboards/7/': () => [500, '💣'], - '/api/projects/:team/dashboards/8/': { ...dashboards['8'] }, - '/api/projects/:team/dashboards/9/': { ...dashboards['9'] }, - '/api/projects/:team/dashboards/10/': { ...dashboards['10'] }, - '/api/projects/:team/dashboards/11/': { ...dashboards['11'] }, - '/api/projects/:team/dashboards/': { + '/api/environments/:team_id/dashboards/5/': { ...dashboards['5'] }, + '/api/environments/:team_id/dashboards/6/': { ...dashboards['6'] }, + '/api/environments/:team_id/dashboards/7/': () => [500, '💣'], + '/api/environments/:team_id/dashboards/8/': { ...dashboards['8'] }, + '/api/environments/:team_id/dashboards/9/': { ...dashboards['9'] }, + '/api/environments/:team_id/dashboards/10/': { ...dashboards['10'] }, + '/api/environments/:team_id/dashboards/11/': { ...dashboards['11'] }, + '/api/environments/:team_id/dashboards/': { count: 6, next: null, previous: null, @@ -206,9 +206,9 @@ describe('dashboardLogic', () => { { ...dashboards['10'] }, ], }, - '/api/projects/:team/insights/1001/': () => [500, '💣'], - '/api/projects/:team/insights/800/': () => [200, { ...insights['800'] }], - '/api/projects/:team/insights/:id/': (req) => { + '/api/environments/:team_id/insights/1001/': () => [500, '💣'], + '/api/environments/:team_id/insights/800/': () => [200, { ...insights['800'] }], + '/api/environments/:team_id/insights/:id/': (req) => { const dashboard = req.url.searchParams.get('from_dashboard') if (!dashboard) { throw new Error('the logic must always add this param') @@ -221,15 +221,15 @@ describe('dashboardLogic', () => { }, }, post: { - '/api/projects/:team/insights/cancel/': [201], + '/api/environments/:team_id/insights/cancel/': [201], }, patch: { - '/api/projects/:team/dashboards/:id/': async (req) => { + '/api/environments/:team_id/dashboards/:id/': async (req) => { const dashboardId = typeof req.params['id'] === 'string' ? req.params['id'] : req.params['id'][0] const payload = await req.json() return [200, { ...dashboards[dashboardId], ...payload }] }, - '/api/projects/:team/dashboards/:id/move_tile/': async (req) => { + '/api/environments/:team_id/dashboards/:id/move_tile/': async (req) => { // backend updates the two dashboards and the insight const jsonPayload = await req.json() const { toDashboard, tile: tileToUpdate } = jsonPayload @@ -256,7 +256,7 @@ describe('dashboardLogic', () => { return [200, { ...from }] }, - '/api/projects/:team/insights/:id/': async (req) => { + '/api/environments/:team_id/insights/:id/': async (req) => { try { const updates = await req.json() if (typeof updates !== 'object') { @@ -309,7 +309,7 @@ describe('dashboardLogic', () => { logic.actions.updateFiltersAndLayouts() }).toFinishAllListeners() - expect(api.update).toHaveBeenCalledWith(`api/projects/${MOCK_TEAM_ID}/dashboards/5`, { + expect(api.update).toHaveBeenCalledWith(`api/environments/${MOCK_TEAM_ID}/dashboards/5`, { tiles: [ { id: 0, @@ -393,7 +393,7 @@ describe('dashboardLogic', () => { await expectLogic(dashboardEightlogic).toFinishAllListeners() expect(api.update).toHaveBeenCalledWith( - `api/projects/${MOCK_TEAM_ID}/dashboards/${9}/move_tile`, + `api/environments/${MOCK_TEAM_ID}/dashboards/${9}/move_tile`, expect.objectContaining({ tile: sourceTile, toDashboard: 8 }) ) }) diff --git a/frontend/src/scenes/dashboard/dashboardLogic.tsx b/frontend/src/scenes/dashboard/dashboardLogic.tsx index 2cbb301df28d3..17b478a1338dc 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboardLogic.tsx @@ -141,7 +141,7 @@ async function getSingleInsight( methodOptions?: ApiMethodOptions, filtersOverride?: DashboardFilter ): Promise { - const apiUrl = `api/projects/${currentTeamId}/insights/${insight.id}/?${toParams({ + const apiUrl = `api/environments/${currentTeamId}/insights/${insight.id}/?${toParams({ refresh, from_dashboard: dashboardId, // needed to load insight in correct context client_query_id: queryId, @@ -294,7 +294,7 @@ export const dashboardLogic = kea([ breakpoint() const dashboard: DashboardType = await api.update( - `api/projects/${values.currentTeamId}/dashboards/${props.id}`, + `api/environments/${values.currentTeamId}/dashboards/${props.id}`, { filters: values.filters, tiles: layoutsToUpdate, @@ -307,7 +307,7 @@ export const dashboardLogic = kea([ } }, updateTileColor: async ({ tileId, color }) => { - await api.update(`api/projects/${values.currentTeamId}/dashboards/${props.id}`, { + await api.update(`api/environments/${values.currentTeamId}/dashboards/${props.id}`, { tiles: [{ id: tileId, color }], }) const matchingTile = values.tiles.find((tile) => tile.id === tileId) @@ -318,7 +318,7 @@ export const dashboardLogic = kea([ }, removeTile: async ({ tile }) => { try { - await api.update(`api/projects/${values.currentTeamId}/dashboards/${props.id}`, { + await api.update(`api/environments/${values.currentTeamId}/dashboards/${props.id}`, { tiles: [{ id: tile.id, deleted: true }], }) dashboardsModel.actions.tileRemovedFromDashboard({ @@ -361,7 +361,7 @@ export const dashboardLogic = kea([ } const dashboard: DashboardType = await api.update( - `api/projects/${values.currentTeamId}/dashboards/${props.id}`, + `api/environments/${values.currentTeamId}/dashboards/${props.id}`, { tiles: [newTile], } @@ -381,7 +381,7 @@ export const dashboardLogic = kea([ return values.dashboard } const dashboard: DashboardType = await api.update( - `api/projects/${teamLogic.values.currentTeamId}/dashboards/${props.id}/move_tile`, + `api/environments/${teamLogic.values.currentTeamId}/dashboards/${props.id}/move_tile`, { tile, toDashboard, @@ -732,7 +732,7 @@ export const dashboardLogic = kea([ () => [(_, props) => props.id], (id) => { return (refresh?: RefreshType, filtersOverride?: DashboardFilter) => - `api/projects/${teamLogic.values.currentTeamId}/dashboards/${id}/?${toParams({ + `api/environments/${teamLogic.values.currentTeamId}/dashboards/${id}/?${toParams({ refresh, filters_override: filtersOverride, })}` @@ -1282,7 +1282,7 @@ export const dashboardLogic = kea([ abortQuery: async ({ dashboardQueryId, queryId, queryStartTime }) => { const { currentTeamId } = values - await api.create(`api/projects/${currentTeamId}/insights/cancel`, { client_query_id: dashboardQueryId }) + await api.create(`api/environments/${currentTeamId}/insights/cancel`, { client_query_id: dashboardQueryId }) // TRICKY: we cancel just once using the dashboard query id. // we can record the queryId that happened to capture the AbortError exception diff --git a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts index 61d98a07a914f..d3723a18f7deb 100644 --- a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts +++ b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts @@ -39,7 +39,7 @@ describe('dashboardsLogic', () => { beforeEach(async () => { useMocks({ get: { - '/api/projects/:team/dashboards/': { + '/api/environments/:team_id/dashboards/': { count: 6, next: null, previous: null, diff --git a/frontend/src/scenes/dashboard/newDashboardLogic.ts b/frontend/src/scenes/dashboard/newDashboardLogic.ts index 44fa0252f5889..6749067872258 100644 --- a/frontend/src/scenes/dashboard/newDashboardLogic.ts +++ b/frontend/src/scenes/dashboard/newDashboardLogic.ts @@ -137,7 +137,7 @@ export const newDashboardLogic = kea([ actions.setIsLoading(true) try { const result: DashboardType = await api.create( - `api/projects/${teamLogic.values.currentTeamId}/dashboards/`, + `api/environments/${teamLogic.values.currentTeamId}/dashboards/`, { name: name, description: description, @@ -195,7 +195,7 @@ export const newDashboardLogic = kea([ try { const result: DashboardType = await api.create( - `api/projects/${teamLogic.values.currentTeamId}/dashboards/create_from_template_json`, + `api/environments/${teamLogic.values.currentTeamId}/dashboards/create_from_template_json`, { template: dashboardJSON, creation_context: creationContext } ) actions.hideNewDashboardModal() diff --git a/frontend/src/scenes/data-management/DataManagementScene.stories.tsx b/frontend/src/scenes/data-management/DataManagementScene.stories.tsx index 0df671d07f3bd..b8ce6c6ae967d 100644 --- a/frontend/src/scenes/data-management/DataManagementScene.stories.tsx +++ b/frontend/src/scenes/data-management/DataManagementScene.stories.tsx @@ -238,7 +238,7 @@ const meta: Meta = { }, }, post: { - '/api/projects/:team_id/query/': (req) => { + '/api/environments/:team_id/query/': (req) => { if ((req.body as any).query.kind === 'DatabaseSchemaQuery') { return [200, MOCK_DATABASE] } diff --git a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts index 2767047a54cdc..461b35d5a3acc 100644 --- a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts +++ b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts @@ -98,7 +98,7 @@ describe('eventDefinitionsTableLogic', () => { ] } }, - '/api/projects/:team/events': (req) => { + '/api/environments/:team_id/events': (req) => { if ( req.url.searchParams.get('limit') === '1' && req.url.searchParams.get('event') === 'event_with_example' @@ -259,13 +259,13 @@ describe('eventDefinitionsTableLogic', () => { [propertiesStartingUrl]: partial({ count: 5, }), - [`api/projects/${MOCK_TEAM_ID}/events?event=event1&limit=1`]: partial(mockEvent.properties), + [`api/environments/${MOCK_TEAM_ID}/events?event=event1&limit=1`]: partial(mockEvent.properties), }), }) expect(api.get).toHaveBeenCalledTimes(3) expect(api.get).toHaveBeenNthCalledWith(1, propertiesStartingUrl) - expect(api.get).toHaveBeenNthCalledWith(2, `api/projects/${MOCK_TEAM_ID}/events?event=event1&limit=1`) + expect(api.get).toHaveBeenNthCalledWith(2, `api/environments/${MOCK_TEAM_ID}/events?event=event1&limit=1`) expect(api.get).toHaveBeenNthCalledWith(3, startingUrl) await expectLogic(logic, () => { diff --git a/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx b/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx index 348dc537034fd..2a97ca624ab98 100644 --- a/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx @@ -20,7 +20,7 @@ const meta: Meta = { decorators: [ mswDecorator({ post: { - '/api/projects/:team_id/query': async (req, res, ctx) => { + '/api/environments/:team_id/query': async (req, res, ctx) => { const query = (await req.clone().json()).query if (query.kind === NodeKind.ErrorTrackingQuery) { return res(ctx.json(errorTrackingQueryResponse)) diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx index debe6ee739398..acf32b9788ed5 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx @@ -35,7 +35,7 @@ const meta: Meta = { ], }, post: { - '/api/projects/:team_id/query': {}, + '/api/environments/:team_id/query': {}, // flag targeting has loaders, make sure they don't keep loading '/api/projects/:team_id/feature_flags/user_blast_radius/': () => [ 200, diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index f01c917bd8a9a..db2811ef1f44b 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -16,6 +16,7 @@ import { experimentLogic } from 'scenes/experiments/experimentLogic' import { featureFlagsLogic, FeatureFlagsTab } from 'scenes/feature-flags/featureFlagsLogic' import { filterTrendsClientSideParams } from 'scenes/insights/sharedUtils' import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { projectLogic } from 'scenes/projectLogic' import { Scene } from 'scenes/sceneTypes' import { NEW_SURVEY, NewSurvey } from 'scenes/surveys/constants' import { urls } from 'scenes/urls' @@ -186,6 +187,8 @@ export const featureFlagLogic = kea([ values: [ teamLogic, ['currentTeamId'], + projectLogic, + ['currentProjectId'], groupsModel, ['aggregationLabel'], userLogic, @@ -472,13 +475,16 @@ export const featureFlagLogic = kea([ try { let savedFlag: FeatureFlagType if (!updatedFlag.id) { - savedFlag = await api.create(`api/projects/${values.currentTeamId}/feature_flags`, preparedFlag) + savedFlag = await api.create( + `api/projects/${values.currentProjectId}/feature_flags`, + preparedFlag + ) if (values.roleBasedAccessEnabled && savedFlag.id) { featureFlagPermissionsLogic({ flagId: null })?.actions.addAssociatedRoles(savedFlag.id) } } else { savedFlag = await api.update( - `api/projects/${values.currentTeamId}/feature_flags/${updatedFlag.id}`, + `api/projects/${values.currentProjectId}/feature_flags/${updatedFlag.id}`, preparedFlag ) } @@ -525,7 +531,7 @@ export const featureFlagLogic = kea([ loadRelatedInsights: async () => { if (props.id && props.id !== 'new' && values.featureFlag.key) { const response = await api.get>( - `api/projects/${values.currentTeamId}/insights/?feature_flag=${values.featureFlag.key}&order=-created_at` + `api/environments/${values.currentProjectId}/insights/?feature_flag=${values.featureFlag.key}&order=-created_at` ) return response.results.map((legacyInsight) => getQueryBasedInsightModel(legacyInsight)) } @@ -665,13 +671,13 @@ export const featureFlagLogic = kea([ })), listeners(({ actions, values, props }) => ({ submitNewDashboardSuccessWithResult: async ({ result }) => { - await api.update(`api/projects/${values.currentTeamId}/feature_flags/${values.featureFlag.id}`, { + await api.update(`api/projects/${values.currentProjectId}/feature_flags/${values.featureFlag.id}`, { analytics_dashboards: [result.id], }) }, generateUsageDashboard: async () => { if (props.id) { - await api.create(`api/projects/${values.currentTeamId}/feature_flags/${props.id}/dashboard`) + await api.create(`api/projects/${values.currentProjectId}/feature_flags/${props.id}/dashboard`) actions.loadFeatureFlag() } }, @@ -679,7 +685,7 @@ export const featureFlagLogic = kea([ if (props.id) { await breakpoint(1000) // in ms await api.create( - `api/projects/${values.currentTeamId}/feature_flags/${props.id}/enrich_usage_dashboard` + `api/projects/${values.currentProjectId}/feature_flags/${props.id}/enrich_usage_dashboard` ) } }, @@ -712,7 +718,7 @@ export const featureFlagLogic = kea([ }, deleteFeatureFlag: async ({ featureFlag }) => { await deleteWithUndo({ - endpoint: `projects/${values.currentTeamId}/feature_flags`, + endpoint: `projects/${values.currentProjectId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: () => { featureFlag.id && actions.deleteFlag(featureFlag.id) @@ -734,7 +740,7 @@ export const featureFlagLogic = kea([ loadInsightAtIndex: async ({ index, filters }) => { if (filters) { const response = await api.get( - `api/projects/${values.currentTeamId}/insights/trend/?${toParams( + `api/environments/${values.currentProjectId}/insights/trend/?${toParams( filterTrendsClientSideParams(filters) )}` ) diff --git a/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts b/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts index c649ccb59c42e..73d469b842e07 100644 --- a/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts @@ -22,7 +22,7 @@ describe('funnelPersonsModalLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/insights/': { + '/api/environments/:team_id/insights/': { results: [{}], }, }, diff --git a/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts b/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts index 0101a3c56e120..e2821b8499d92 100644 --- a/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts @@ -29,10 +29,10 @@ describe('funnelPropertyCorrelationLogic', () => { correlation_config: correlationConfig, }, ], - '/api/projects/:team/insights/': { results: [{}] }, - '/api/projects/:team/insights/:id/': {}, + '/api/environments/:team_id/insights/': { results: [{}] }, + '/api/environments/:team_id/insights/:id/': {}, '/api/projects/:team/groups_types/': [], - '/api/projects/:team/persons/properties': [ + '/api/environments/:team_id/persons/properties': [ { name: 'some property', count: 20 }, { name: 'another property', count: 10 }, { name: 'third property', count: 5 }, @@ -59,7 +59,7 @@ describe('funnelPropertyCorrelationLogic', () => { ], }, post: { - '/api/projects/:team/insights/funnel/correlation': (req) => { + '/api/environments/:team_id/insights/funnel/correlation': (req) => { const data = req.body as any const excludePropertyFromProjectNames = data?.funnel_correlation_exclude_names || [] const includePropertyNames = data?.funnel_correlation_names || [] diff --git a/frontend/src/scenes/heatmaps/HeatmapsBrowser.stories.tsx b/frontend/src/scenes/heatmaps/HeatmapsBrowser.stories.tsx index e043ae3025e24..b89a5f653e429 100644 --- a/frontend/src/scenes/heatmaps/HeatmapsBrowser.stories.tsx +++ b/frontend/src/scenes/heatmaps/HeatmapsBrowser.stories.tsx @@ -21,7 +21,7 @@ const meta: Meta = { '/api/projects/:team_id/integrations': {}, }, post: { - '/api/projects/:team_id/query': async (req, res, ctx) => { + '/api/environments/:team_id/query': async (req, res, ctx) => { const qry = (await req.clone().json()).query.query // top urls query if (qry.startsWith('SELECT properties.$current_url AS url, count()')) { diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx index 15e23154d2fef..8a0e5c91af302 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx @@ -27,7 +27,7 @@ export default meta export const Empty: StoryFn = () => { useStorybookMocks({ get: { - '/api/projects/:team_id/insights/': (_, __, ctx) => [ + '/api/environments/:team_id/insights/': (_, __, ctx) => [ ctx.delay(100), ctx.status(200), ctx.json({ count: 1, results: [{ ...insight, result: [] }] }), @@ -43,12 +43,12 @@ export const Empty: StoryFn = () => { export const ServerError: StoryFn = () => { useStorybookMocks({ get: { - '/api/projects/:team_id/insights/': (_, __, ctx) => [ + '/api/environments/:team_id/insights/': (_, __, ctx) => [ ctx.delay(100), ctx.status(200), ctx.json({ count: 1, results: [{ ...insight, result: null }] }), ], - '/api/projects/:team_id/insights/:id': (_, __, ctx) => [ + '/api/environments/:team_id/insights/:id': (_, __, ctx) => [ ctx.delay(100), ctx.status(500), ctx.json({ @@ -67,14 +67,14 @@ export const ServerError: StoryFn = () => { export const ValidationError: StoryFn = () => { useStorybookMocks({ get: { - '/api/projects/:team_id/insights/': (_, __, ctx) => [ + '/api/environments/:team_id/insights/': (_, __, ctx) => [ ctx.delay(100), ctx.status(200), ctx.json({ count: 1, results: [{ ...insight, result: null }] }), ], }, post: { - '/api/projects/:team_id/insights/:id': (_, __, ctx) => [ + '/api/environments/:team_id/insights/:id': (_, __, ctx) => [ ctx.delay(100), ctx.status(400), ctx.json({ @@ -93,13 +93,13 @@ export const ValidationError: StoryFn = () => { export const EstimatedQueryExecutionTimeTooLong: StoryFn = () => { useStorybookMocks({ get: { - '/api/projects/:team_id/insights/': (_, __, ctx) => [ + '/api/environments/:team_id/insights/': (_, __, ctx) => [ ctx.status(200), ctx.json({ count: 1, results: [{ ...insight, result: null }] }), ], }, post: { - '/api/projects/:team_id/query/': (_, __, ctx) => [ + '/api/environments/:team_id/query/': (_, __, ctx) => [ ctx.delay(100), ctx.status(512), ctx.json({ @@ -124,13 +124,13 @@ EstimatedQueryExecutionTimeTooLong.parameters = { export const LongLoading: StoryFn = () => { useStorybookMocks({ get: { - '/api/projects/:team_id/insights/': (_, __, ctx) => [ + '/api/environments/:team_id/insights/': (_, __, ctx) => [ ctx.status(200), ctx.json({ count: 1, results: [{ ...insight, result: null }] }), ], }, post: { - '/api/projects/:team_id/query/': (_, __, ctx) => [ctx.delay('infinite')], + '/api/environments/:team_id/query/': (_, __, ctx) => [ctx.delay('infinite')], }, }) useEffect(() => { diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx index 95283388cb974..06f0928dc54f3 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx @@ -273,9 +273,11 @@ export function InsightErrorState({ excludeDetail, title, query, queryId }: Insi )} -

- Query ID: {queryId} -
+ {queryId && ( +
+ Query ID: {queryId} +
+ )} {query && ( { beforeEach(async () => { useMocks({ get: { - '/api/projects/:team/insights/trend/': async () => { + '/api/environments/:team_id/insights/trend/': async () => { return [200, { result: ['result from api'] }] }, }, post: { - '/api/projects/:team/insights/funnel/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/funnel/': { result: ['result from api'] }, }, }) initKeaTests(true, { ...MOCK_DEFAULT_TEAM, test_account_filters_default_checked: true }) diff --git a/frontend/src/scenes/insights/Insights.stories.tsx b/frontend/src/scenes/insights/Insights.stories.tsx index c59e304f4be68..c97f692eb5bd0 100644 --- a/frontend/src/scenes/insights/Insights.stories.tsx +++ b/frontend/src/scenes/insights/Insights.stories.tsx @@ -24,8 +24,8 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/persons/retention': sampleRetentionPeopleResponse, - '/api/projects/:team_id/persons/properties': samplePersonProperties, + '/api/environments/:team_id/persons/retention': sampleRetentionPeopleResponse, + '/api/environments/:team_id/persons/properties': samplePersonProperties, '/api/projects/:team_id/groups_types': [], }, post: { diff --git a/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx b/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx index 0032cb2baafe6..819a35716abdd 100644 --- a/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx +++ b/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx @@ -16,7 +16,7 @@ export function createInsightStory( return function InsightStory() { useStorybookMocks({ get: { - '/api/projects/:team_id/insights/': (_, __, ctx) => [ + '/api/environments/:team_id/insights/': (_, __, ctx) => [ ctx.status(200), ctx.json({ count: 1, @@ -35,7 +35,7 @@ export function createInsightStory( ], }, post: { - '/api/projects/:team_id/query/': (req, __, ctx) => [ + '/api/environments/:team_id/query/': (req, __, ctx) => [ ctx.status(200), ctx.json({ cache_key: req.params.query, diff --git a/frontend/src/scenes/insights/insightDataLogic.test.ts b/frontend/src/scenes/insights/insightDataLogic.test.ts index 1d46ee553434d..11af2a186a279 100644 --- a/frontend/src/scenes/insights/insightDataLogic.test.ts +++ b/frontend/src/scenes/insights/insightDataLogic.test.ts @@ -20,7 +20,7 @@ describe('insightDataLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team_id/insights/trend': [], + '/api/environments/:team_id/insights/trend': [], }, }) initKeaTests() diff --git a/frontend/src/scenes/insights/insightLogic.test.ts b/frontend/src/scenes/insights/insightLogic.test.ts index 3eb7fa86bec3f..cdf3b53a54784 100644 --- a/frontend/src/scenes/insights/insightLogic.test.ts +++ b/frontend/src/scenes/insights/insightLogic.test.ts @@ -112,7 +112,7 @@ describe('insightLogic', () => { useMocks({ get: { '/api/projects/:team/tags': [], - '/api/projects/:team/insights/trend/': async (req) => { + '/api/environments/:team_id/insights/trend/': async (req) => { const clientQueryId = req.url.searchParams.get('client_query_id') if (clientQueryId !== null) { seenQueryIDs.push(clientQueryId) @@ -131,18 +131,18 @@ describe('insightLogic', () => { } return [200, { result: ['result from api'] }] }, - '/api/projects/:team/insights/path/': { result: ['result from api'] }, - '/api/projects/:team/insights/path': { result: ['result from api'] }, - '/api/projects/:team/insights/funnel/': { result: ['result from api'] }, - '/api/projects/:team/insights/retention/': { result: ['result from api'] }, - '/api/projects/:team/insights/43/': partialInsight43, - '/api/projects/:team/insights/44/': { + '/api/environments/:team_id/insights/path/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/path': { result: ['result from api'] }, + '/api/environments/:team_id/insights/funnel/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/retention/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/43/': partialInsight43, + '/api/environments/:team_id/insights/44/': { id: 44, short_id: Insight44, result: ['result 44'], filters: API_FILTERS, }, - '/api/projects/:team/insights/': (req) => { + '/api/environments/:team_id/insights/': (req) => { if (req.url.searchParams.get('saved')) { return [ 200, @@ -181,7 +181,7 @@ describe('insightLogic', () => { }, ] }, - '/api/projects/:team/dashboards/33/': { + '/api/environments/:team_id/dashboards/33/': { id: 33, filters: {}, tiles: [ @@ -198,7 +198,7 @@ describe('insightLogic', () => { }, ], }, - '/api/projects/:team/dashboards/34/': { + '/api/environments/:team_id/dashboards/34/': { id: 33, filters: {}, tiles: [ @@ -217,16 +217,16 @@ describe('insightLogic', () => { }, }, post: { - '/api/projects/:team/insights/funnel/': { result: ['result from api'] }, - '/api/projects/:team/insights/:id/viewed': [201], - '/api/projects/:team/insights/': (req) => [ + '/api/environments/:team_id/insights/funnel/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/:id/viewed': [201], + '/api/environments/:team_id/insights/': (req) => [ 200, { id: 12, short_id: Insight12, ...((req.body as any) || {}) }, ], - '/api/projects/997/insights/cancel/': [201], + '/api/environments/997/insights/cancel/': [201], }, patch: { - '/api/projects/:team/insights/:id': async (req) => { + '/api/environments/:team_id/insights/:id': async (req) => { const payload = await req.json() const response = patchResponseFor( payload, @@ -737,7 +737,7 @@ describe('insightLogic', () => { const mockCreateCalls = (api.create as jest.Mock).mock.calls expect(mockCreateCalls).toEqual([ [ - `api/projects/${MOCK_TEAM_ID}/insights`, + `api/environments/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ derived_name: 'DataTableNode query', query: { diff --git a/frontend/src/scenes/insights/insightSceneLogic.test.ts b/frontend/src/scenes/insights/insightSceneLogic.test.ts index 24f6db1e8bb64..d6a1b4bdcf2ca 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.test.ts +++ b/frontend/src/scenes/insights/insightSceneLogic.test.ts @@ -19,11 +19,11 @@ describe('insightSceneLogic', () => { beforeEach(async () => { useMocks({ get: { - '/api/projects/:team/insights/trend/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/trend/': { result: ['result from api'] }, }, post: { - '/api/projects/:team/insights/funnel/': { result: ['result from api'] }, - '/api/projects/:team/insights/': (req) => [ + '/api/environments/:team_id/insights/funnel/': { result: ['result from api'] }, + '/api/environments/:team_id/insights/': (req) => [ 200, { id: 12, short_id: Insight12, ...((req.body as any) || {}) }, ], diff --git a/frontend/src/scenes/insights/insightUsageLogic.ts b/frontend/src/scenes/insights/insightUsageLogic.ts index 1c1ae8140df08..2b2a2517c0969 100644 --- a/frontend/src/scenes/insights/insightUsageLogic.ts +++ b/frontend/src/scenes/insights/insightUsageLogic.ts @@ -3,7 +3,7 @@ import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' import { objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from 'scenes/teamLogic' +import { projectLogic } from 'scenes/projectLogic' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' @@ -23,6 +23,8 @@ export const insightUsageLogic = kea([ path((key) => ['scenes', 'insights', 'insightUsageLogic', key]), connect((props: InsightLogicProps) => ({ values: [ + projectLogic, + ['currentProjectId'], insightLogic(props), ['insight'], dataNodeLogic({ key: insightVizDataNodeKey(props) } as DataNodeLogicProps), @@ -58,7 +60,7 @@ export const insightUsageLogic = kea([ // Report the insight being viewed to our '/viewed' endpoint. Used for "recently viewed insights". if (values.insight.id) { - void api.create(`api/projects/${teamLogic.values.currentTeamId}/insights/${values.insight.id}/viewed`) + void api.create(`api/environments/${values.currentProjectId}/insights/${values.insight.id}/viewed`) } // Debounce to avoid noisy events from the query changing multiple times. diff --git a/frontend/src/scenes/insights/insightVizDataLogic.test.ts b/frontend/src/scenes/insights/insightVizDataLogic.test.ts index c3bc313e815a6..266fe56061263 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.test.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.test.ts @@ -22,8 +22,8 @@ describe('insightVizDataLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team_id/insights/trend': [], - '/api/projects/:team_id/insights/': { results: [{}] }, + '/api/environments/:team_id/insights/trend': [], + '/api/environments/:team_id/insights/': { results: [{}] }, }, }) initKeaTests() diff --git a/frontend/src/scenes/insights/utils.tsx b/frontend/src/scenes/insights/utils.tsx index e9dc44767d875..96d3129e47fa6 100644 --- a/frontend/src/scenes/insights/utils.tsx +++ b/frontend/src/scenes/insights/utils.tsx @@ -137,7 +137,7 @@ export async function getInsightId(shortId: InsightShortId): Promise = { decorators: [ mswDecorator({ post: { - 'api/projects/:team_id/insights/funnel/correlation/': funnelCorrelation, + 'api/environments/:team_id/insights/funnel/correlation/': funnelCorrelation, }, }), ], diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.stories.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.stories.tsx index 5f7275b4b8818..4bff8e9a36356 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.stories.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.stories.tsx @@ -21,7 +21,7 @@ const meta: Meta = { decorators: [ mswDecorator({ post: { - 'api/projects/:team_id/insights/funnel/correlation/': funnelCorrelation, + 'api/environments/:team_id/insights/funnel/correlation/': funnelCorrelation, }, }), taxonomicFilterMocksDecorator, diff --git a/frontend/src/scenes/max/Max.stories.tsx b/frontend/src/scenes/max/Max.stories.tsx index 1e9761f352370..65106d4ae4420 100644 --- a/frontend/src/scenes/max/Max.stories.tsx +++ b/frontend/src/scenes/max/Max.stories.tsx @@ -13,7 +13,7 @@ const meta: Meta = { decorators: [ mswDecorator({ post: { - '/api/projects/:team_id/query/chat/': chatResponse, + '/api/environments/:team_id/query/chat/': chatResponse, }, }), ], @@ -86,7 +86,7 @@ export const Thread: StoryFn = () => { export const EmptyThreadLoading: StoryFn = () => { useStorybookMocks({ post: { - '/api/projects/:team_id/query/chat/': (_req, _res, ctx) => [ctx.delay('infinite')], + '/api/environments/:team_id/query/chat/': (_req, _res, ctx) => [ctx.delay('infinite')], }, }) diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx index e67b51d7ad639..9b760fe72eb86 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx @@ -206,7 +206,7 @@ const meta: Meta = { decorators: [ mswDecorator({ post: { - 'api/projects/:team_id/query': { + 'api/environments/:team_id/query': { clickhouse: "SELECT nullIf(nullIf(events.`$session_id`, ''), 'null') AS session_id, any(events.properties) AS properties FROM events WHERE and(equals(events.team_id, 1), in(events.event, [%(hogql_val_0)s, %(hogql_val_1)s]), ifNull(in(session_id, [%(hogql_val_2)s]), 0), ifNull(greaterOrEquals(toTimeZone(events.timestamp, %(hogql_val_3)s), %(hogql_val_4)s), 0), ifNull(lessOrEquals(toTimeZone(events.timestamp, %(hogql_val_5)s), %(hogql_val_6)s), 0)) GROUP BY session_id LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=True", columns: ['session_id', 'properties'], @@ -275,7 +275,7 @@ const meta: Meta = { ], }, 'api/projects/:team_id/notebooks/12345': notebook12345Json, - 'api/projects/:team_id/session_recordings': { + 'api/environments/:team_id/session_recordings': { results: [ { id: '018a8a51-a39d-7b18-897f-94054eec5f61', diff --git a/frontend/src/scenes/persons/personsLogic.test.ts b/frontend/src/scenes/persons/personsLogic.test.ts index 6e8127842f135..602ff71a2992b 100644 --- a/frontend/src/scenes/persons/personsLogic.test.ts +++ b/frontend/src/scenes/persons/personsLogic.test.ts @@ -15,7 +15,7 @@ describe('personsLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team_id/persons/': (req) => { + '/api/environments/:team_id/persons/': (req) => { if (['+', 'abc', 'xyz'].includes(req.url.searchParams.get('distinct_id') ?? '')) { return [200, { results: ['person from api'] }] } @@ -103,7 +103,10 @@ describe('personsLogic', () => { await expectLogic(logic, () => { logic.actions.loadPerson('+') // has encoded from + in the action to %2B in the API call - expect(api.get).toHaveBeenCalledWith(`api/projects/${MOCK_TEAM_ID}/persons?distinct_id=%2B`, undefined) + expect(api.get).toHaveBeenCalledWith( + `api/environments/${MOCK_TEAM_ID}/persons?distinct_id=%2B`, + undefined + ) }) .toDispatchActions(['loadPerson', 'loadPersonSuccess']) .toMatchValues({ diff --git a/frontend/src/scenes/pipeline/Pipeline.stories.tsx b/frontend/src/scenes/pipeline/Pipeline.stories.tsx index ce45d28639c7c..ac01c5b62cd53 100644 --- a/frontend/src/scenes/pipeline/Pipeline.stories.tsx +++ b/frontend/src/scenes/pipeline/Pipeline.stories.tsx @@ -55,8 +55,8 @@ export default { '/api/organizations/:organization_id/plugins/repository': [], '/api/organizations/:organization_id/plugins/unused': [], '/api/organizations/:organization_id/plugins/:id': pluginRetrieveMock, - '/api/projects/:team_id/plugin_configs/': pluginConfigs, - '/api/projects/:team_id/plugin_configs/:id': pluginConfigRetrieveMock, + '/api/environments/:team_id/plugin_configs/': pluginConfigs, + '/api/environments/:team_id/plugin_configs/:id': pluginConfigRetrieveMock, // TODO: Differentiate between transformation and destination mocks for nicer mocks '/api/organizations/:organization_id/pipeline_transformations/': plugins, '/api/projects/:team_id/pipeline_transformation_configs/': pluginConfigs, @@ -278,7 +278,7 @@ export function PipelineNodeMetricsErrorModal(): JSX.Element { export function PipelineNodeLogs(): JSX.Element { useStorybookMocks({ get: { - '/api/projects/:team_id/plugin_configs/:plugin_config_id/logs': require('./__mocks__/pluginLogs.json'), + '/api/environments/:team_id/plugin_configs/:plugin_config_id/logs': require('./__mocks__/pluginLogs.json'), }, }) useEffect(() => { @@ -290,7 +290,7 @@ export function PipelineNodeLogs(): JSX.Element { export function PipelineNodeLogsBatchExport(): JSX.Element { useStorybookMocks({ get: { - '/api/projects/:team_id/batch_exports/:export_id/logs': require('./__mocks__/batchExportLogs.json'), + '/api/environments/:team_id/batch_exports/:export_id/logs': require('./__mocks__/batchExportLogs.json'), }, }) useEffect(() => { diff --git a/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx b/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx index 38fa9a80270a9..3d13151059182 100644 --- a/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx +++ b/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx @@ -109,7 +109,7 @@ export const pipelineDestinationsLogic = kea([ deleteNodeWebhook: async ({ destination }) => { await deleteWithUndo({ - endpoint: `projects/${teamLogic.values.currentTeamId}/plugin_configs`, + endpoint: `environments/${teamLogic.values.currentTeamId}/plugin_configs`, object: { id: destination.id, name: destination.name, diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx b/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx index a3857dc2c8c2b..922c3de9476be 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx @@ -12,11 +12,11 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/dashboards/': require('../dashboard/__mocks__/dashboards.json'), - '/api/projects/:team_id/dashboards/1/': require('../dashboard/__mocks__/dashboard1.json'), - '/api/projects/:team_id/dashboards/1/collaborators/': [], - '/api/projects/:team_id/session_recordings/': EMPTY_PAGINATED_RESPONSE, - '/api/projects/:team_id/insights/my_last_viewed/': [], + '/api/environments/:team_id/dashboards/': require('../dashboard/__mocks__/dashboards.json'), + '/api/environments/:team_id/dashboards/1/': require('../dashboard/__mocks__/dashboard1.json'), + '/api/environments/:team_id/dashboards/1/collaborators/': [], + '/api/environments/:team_id/session_recordings/': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/insights/my_last_viewed/': [], }, }), ], diff --git a/frontend/src/scenes/project-homepage/projectHomepageLogic.test.ts b/frontend/src/scenes/project-homepage/projectHomepageLogic.test.ts index 0818d735c1536..d778680ced854 100644 --- a/frontend/src/scenes/project-homepage/projectHomepageLogic.test.ts +++ b/frontend/src/scenes/project-homepage/projectHomepageLogic.test.ts @@ -15,9 +15,9 @@ describe('projectHomepageLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/dashboards/1/': dashboardJson, - '/api/projects/:team/insights/': { results: ['result from api'] }, - '/api/projects/:team/persons/': { results: ['result from api'] }, + '/api/environments/:team_id/dashboards/1/': dashboardJson, + '/api/environments/:team_id/insights/': { results: ['result from api'] }, + '/api/environments/:team_id/persons/': { results: ['result from api'] }, }, }) initKeaTests() diff --git a/frontend/src/scenes/project-homepage/projectHomepageLogic.tsx b/frontend/src/scenes/project-homepage/projectHomepageLogic.tsx index af349e3aa86bc..619e6765fcc14 100644 --- a/frontend/src/scenes/project-homepage/projectHomepageLogic.tsx +++ b/frontend/src/scenes/project-homepage/projectHomepageLogic.tsx @@ -2,6 +2,7 @@ import { afterMount, connect, kea, path, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' import { DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' +import { projectLogic } from 'scenes/projectLogic' import { teamLogic } from 'scenes/teamLogic' import { getQueryBasedInsightModel } from '~/queries/nodes/InsightViz/utils' @@ -12,7 +13,7 @@ import type { projectHomepageLogicType } from './projectHomepageLogicType' export const projectHomepageLogic = kea([ path(['scenes', 'project-homepage', 'projectHomepageLogic']), connect({ - values: [teamLogic, ['currentTeamId', 'currentTeam']], + values: [teamLogic, ['currentTeam'], projectLogic, ['currentProjectId']], }), selectors({ @@ -35,7 +36,7 @@ export const projectHomepageLogic = kea([ { loadRecentInsights: async () => { const insights = await api.get( - `api/projects/${values.currentTeamId}/insights/my_last_viewed` + `api/environments/${values.currentProjectId}/insights/my_last_viewed` ) return insights.map((legacyInsight) => getQueryBasedInsightModel(legacyInsight)) }, diff --git a/frontend/src/scenes/projectLogic.ts b/frontend/src/scenes/projectLogic.ts index fca5367075fb6..8712f9d4d8279 100644 --- a/frontend/src/scenes/projectLogic.ts +++ b/frontend/src/scenes/projectLogic.ts @@ -1,6 +1,6 @@ -import { actions, afterMount, connect, kea, listeners, path, reducers } from 'kea' +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import api from 'lib/api' +import api, { ApiConfig } from 'lib/api' import { lemonToast } from 'lib/lemon-ui/LemonToast' import { identifierToHuman, isUserLoggedIn } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' @@ -80,7 +80,15 @@ export const projectLogic = kea([ }, ], })), + selectors({ + currentProjectId: [(s) => [s.currentProject], (currentProject) => currentProject?.id || null], + }), listeners(({ actions }) => ({ + loadCurrentProjectSuccess: ({ currentProject }) => { + if (currentProject) { + ApiConfig.setCurrentProjectId(currentProject.id) + } + }, deleteProject: async ({ project }) => { try { await api.delete(`api/projects/${project.id}`) diff --git a/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx b/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx index 8f9ff797ee74b..673a3076fb654 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/insights': toPaginatedResponse( + '/api/environments/:team_id/insights': toPaginatedResponse( insightsJson.results.slice(0, 6).map((result, i) => ({ // Keep size of response in check ...result, @@ -56,7 +56,7 @@ CardView.parameters = { export const EmptyState: Story = () => { useStorybookMocks({ get: { - '/api/projects/:team_id/insights': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/insights': EMPTY_PAGINATED_RESPONSE, }, }) useEffect(() => { diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts index 308be468581cf..47729fab591fa 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts @@ -52,18 +52,18 @@ describe('savedInsightsLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/insights/': (req) => [ + '/api/environments/:team_id/insights/': (req) => [ 200, createSavedInsights( req.url.searchParams.get('search') ?? '', parseInt(req.url.searchParams.get('offset') ?? '0') ), ], - '/api/projects/:team/insights/42': createInsight(42), - '/api/projects/:team/insights/123': createInsight(123), + '/api/environments/:team_id/insights/42': createInsight(42), + '/api/environments/:team_id/insights/123': createInsight(123), }, post: { - '/api/projects/:team/insights/': () => [200, createInsight(42)], + '/api/environments/:team_id/insights/': () => [200, createInsight(42)], }, }) initKeaTests() @@ -192,7 +192,7 @@ describe('savedInsightsLogic', () => { sourceInsight.derived_name = 'should be copied' await logic.asyncActions.duplicateInsight(sourceInsight) expect(api.create).toHaveBeenCalledWith( - `api/projects/${MOCK_TEAM_ID}/insights`, + `api/environments/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ name: '' }), expect.objectContaining({}) ) @@ -204,7 +204,7 @@ describe('savedInsightsLogic', () => { sourceInsight.derived_name = '' await logic.asyncActions.duplicateInsight(sourceInsight) expect(api.create).toHaveBeenCalledWith( - `api/projects/${MOCK_TEAM_ID}/insights`, + `api/environments/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ name: 'should be copied (copy)' }), expect.objectContaining({}) ) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index 923b13bc2999f..1b958165f4f3b 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -104,7 +104,7 @@ export const savedInsightsLogic = kea([ } const legacyResponse: CountedPaginatedResponse = await api.get( - `api/projects/${teamLogic.values.currentTeamId}/insights/?${toParams(params)}` + `api/environments/${teamLogic.values.currentTeamId}/insights/?${toParams(params)}` ) const response = { ...legacyResponse, diff --git a/frontend/src/scenes/session-recordings/SessionsRecordings-player-failure.stories.tsx b/frontend/src/scenes/session-recordings/SessionsRecordings-player-failure.stories.tsx index 6b8f14a78cc20..bb9063ed6c933 100644 --- a/frontend/src/scenes/session-recordings/SessionsRecordings-player-failure.stories.tsx +++ b/frontend/src/scenes/session-recordings/SessionsRecordings-player-failure.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { // API is set up so that everything except the call to load session recording metadata succeeds mswDecorator({ get: { - '/api/projects/:team_id/session_recordings': (req) => { + '/api/environments/:team_id/session_recordings': (req) => { const version = req.url.searchParams.get('version') return [ 200, @@ -88,7 +88,7 @@ const meta: Meta = { const response = playlistId === '1234567' ? recordings : [] return [200, { has_next: false, results: response, version: 1 }] }, - '/api/projects/:team/session_recordings/:id/snapshots': (req, res, ctx) => { + '/api/environments/:team_id/session_recordings/:id/snapshots': (req, res, ctx) => { // with no sources, returns sources... if (req.url.searchParams.get('source') === 'blob') { return res(ctx.text(snapshotsAsJSONLines())) @@ -108,7 +108,7 @@ const meta: Meta = { }, ] }, - '/api/projects/:team/session_recordings/:id': () => { + '/api/environments/:team_id/session_recordings/:id': () => { return [404, {}] }, 'api/projects/:team/notebooks': { @@ -119,7 +119,7 @@ const meta: Meta = { }, }, post: { - '/api/projects/:team/query': recordingEventsJson, + '/api/environments/:team_id/query': recordingEventsJson, }, }), ], diff --git a/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx b/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx index 39b6bbd3d9980..c4f13b003c2c7 100644 --- a/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx +++ b/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/session_recordings': (req) => { + '/api/environments/:team_id/session_recordings': (req) => { const version = req.url.searchParams.get('version') return [ 200, @@ -88,7 +88,7 @@ const meta: Meta = { const response = playlistId === '1234567' ? recordings : [] return [200, { has_next: false, results: response, version: 1 }] }, - '/api/projects/:team/session_recordings/:id/snapshots': (req, res, ctx) => { + '/api/environments/:team_id/session_recordings/:id/snapshots': (req, res, ctx) => { // with no sources, returns sources... if (req.url.searchParams.get('source') === 'blob') { return res(ctx.text(snapshotsAsJSONLines())) @@ -108,7 +108,7 @@ const meta: Meta = { }, ] }, - '/api/projects/:team/session_recordings/:id': recordingMetaJson, + '/api/environments/:team_id/session_recordings/:id': recordingMetaJson, 'api/projects/:team/notebooks': { count: 0, next: null, @@ -117,7 +117,7 @@ const meta: Meta = { }, }, post: { - '/api/projects/:team/query': (req, res, ctx) => { + '/api/environments/:team_id/query': (req, res, ctx) => { const body = req.body as Record if ( diff --git a/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx b/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx index f90ee0ed1285a..86ef8078ac397 100644 --- a/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx +++ b/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx @@ -22,7 +22,7 @@ const meta: Meta = { mswDecorator({ get: { '/api/projects/:team_id/session_recording_playlists': recording_playlists, - '/api/projects/:team_id/session_recordings': (req) => { + '/api/environments/:team_id/session_recordings': (req) => { const version = req.url.searchParams.get('version') return [ 200, @@ -35,7 +35,7 @@ const meta: Meta = { }, }, post: { - '/api/projects/:team/query': recordingEventsJson, + '/api/environments/:team_id/query': recordingEventsJson, }, }), ], diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts index 5f9c7bb8ffa77..dafd05c304ff3 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts @@ -15,7 +15,7 @@ describe('playerInspectorLogic', () => { beforeEach(() => { useMocks({ get: { - 'api/projects/:team_id/session_recordings/1/': {}, + 'api/environments/:team_id/session_recordings/1/': {}, 'api/projects/:team/notebooks/recording_comments': { results: [ { diff --git a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts index 838f51c78e6f5..77fc0fde434e0 100644 --- a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts @@ -13,7 +13,7 @@ describe('sessionPlayerModalLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/session_recordings': [ + '/api/environments/:team_id/session_recordings': [ 200, { results: listOfSessionRecordings, diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts b/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts index f21c3f7189f6e..4fccafcfb856a 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts @@ -19,12 +19,12 @@ describe('playerMetaLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/session_recordings/:id': recordingMetaJson, - '/api/projects/:team/session_recordings/:id/snapshots/': (_, res, ctx) => + '/api/environments/:team_id/session_recordings/:id': recordingMetaJson, + '/api/environments/:team_id/session_recordings/:id/snapshots/': (_, res, ctx) => res(ctx.text(snapshotsAsJSONLines())), }, post: { - '/api/projects/:team/query': recordingEventsJson, + '/api/environments/:team_id/query': recordingEventsJson, }, }) initKeaTests() diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts index d42f36b416a99..353a2aa04236c 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts @@ -43,7 +43,7 @@ describe('sessionRecordingDataLogic', () => { useAvailableFeatures([AvailableFeature.RECORDINGS_PERFORMANCE]) useMocks({ get: { - '/api/projects/:team/session_recordings/:id/snapshots': async (req, res, ctx) => { + '/api/environments/:team_id/session_recordings/:id/snapshots': async (req, res, ctx) => { // with no sources, returns sources... if (req.url.searchParams.get('source') === 'blob') { return res(ctx.text(snapshotsAsJSONLines())) @@ -69,13 +69,13 @@ describe('sessionRecordingDataLogic', () => { }, ] }, - '/api/projects/:team/session_recordings/:id': recordingMetaJson, + '/api/environments/:team_id/session_recordings/:id': recordingMetaJson, }, post: { - '/api/projects/:team/query': recordingEventsJson, + '/api/environments/:team_id/query': recordingEventsJson, }, patch: { - '/api/projects/:team/session_recordings/:id': { success: true }, + '/api/environments/:team_id/session_recordings/:id': { success: true }, }, }) initKeaTests() @@ -139,7 +139,7 @@ describe('sessionRecordingDataLogic', () => { logic.unmount() useMocks({ get: { - '/api/projects/:team/session_recordings/:id': () => [500, { status: 0 }], + '/api/environments/:team_id/session_recordings/:id': () => [500, { status: 0 }], }, }) logic.mount() @@ -170,7 +170,7 @@ describe('sessionRecordingDataLogic', () => { logic.unmount() useMocks({ get: { - '/api/projects/:team/session_recordings/:id/snapshots': () => [500, { status: 0 }], + '/api/environments/:team_id/session_recordings/:id/snapshots': () => [500, { status: 0 }], }, }) logic.mount() @@ -224,7 +224,7 @@ describe('sessionRecordingDataLogic', () => { }).toDispatchActions(['loadEvents', 'loadEventsSuccess']) expect(api.create).toHaveBeenCalledWith( - `api/projects/${MOCK_TEAM_ID}/query`, + `api/environments/${MOCK_TEAM_ID}/query`, { client_query_id: undefined, query: { diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts index 4724e4369cdf2..3dde171f5c309 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts @@ -27,7 +27,7 @@ describe('sessionRecordingPlayerLogic', () => { useMocks({ get: { '/api/projects/:team_id/session_recordings/:id/comments/': { results: [] }, - '/api/projects/:team/session_recordings/:id/snapshots/': (req, res, ctx) => { + '/api/environments/:team_id/session_recordings/:id/snapshots/': (req, res, ctx) => { // with no sources, returns sources... if (req.url.searchParams.get('source') === 'blob') { return res(ctx.text(snapshotsAsJSONLines())) @@ -47,13 +47,13 @@ describe('sessionRecordingPlayerLogic', () => { }, ] }, - '/api/projects/:team/session_recordings/:id': recordingMetaJson, + '/api/environments/:team_id/session_recordings/:id': recordingMetaJson, }, delete: { - '/api/projects/:team/session_recordings/:id': { success: true }, + '/api/environments/:team_id/session_recordings/:id': { success: true }, }, post: { - '/api/projects/:team/query': recordingEventsJson, + '/api/environments/:team_id/query': recordingEventsJson, }, }) initKeaTests() @@ -128,7 +128,7 @@ describe('sessionRecordingPlayerLogic', () => { useMocks({ get: { - '/api/projects/:team/session_recordings/:id/snapshots': () => [500, { status: 0 }], + '/api/environments/:team_id/session_recordings/:id/snapshots': () => [500, { status: 0 }], }, }) logic.mount() @@ -194,7 +194,7 @@ describe('sessionRecordingPlayerLogic', () => { sessionRecordingsPlaylistLogic({ updateSearchParams: true }).actionTypes.loadAllRecordings, ]) - expect(api.delete).toHaveBeenCalledWith(`api/projects/${MOCK_TEAM_ID}/session_recordings/3`) + expect(api.delete).toHaveBeenCalledWith(`api/environments/${MOCK_TEAM_ID}/session_recordings/3`) resumeKeaLoadersErrors() }) @@ -218,7 +218,7 @@ describe('sessionRecordingPlayerLogic', () => { listLogic.actionCreators.setSelectedRecordingId(null), ]) - expect(api.delete).toHaveBeenCalledWith(`api/projects/${MOCK_TEAM_ID}/session_recordings/3`) + expect(api.delete).toHaveBeenCalledWith(`api/environments/${MOCK_TEAM_ID}/session_recordings/3`) resumeKeaLoadersErrors() }) @@ -240,7 +240,7 @@ describe('sessionRecordingPlayerLogic', () => { expect(router.values.location.pathname).toEqual(urls.replay()) - expect(api.delete).toHaveBeenCalledWith(`api/projects/${MOCK_TEAM_ID}/session_recordings/3`) + expect(api.delete).toHaveBeenCalledWith(`api/environments/${MOCK_TEAM_ID}/session_recordings/3`) resumeKeaLoadersErrors() }) @@ -261,7 +261,7 @@ describe('sessionRecordingPlayerLogic', () => { expect(router.values.location.pathname).toEqual('/') - expect(api.delete).toHaveBeenCalledWith(`api/projects/${MOCK_TEAM_ID}/session_recordings/3`) + expect(api.delete).toHaveBeenCalledWith(`api/environments/${MOCK_TEAM_ID}/session_recordings/3`) resumeKeaLoadersErrors() }) }) diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts index 0b032a2f5f1e0..881f1202d120c 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts @@ -39,7 +39,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { beforeEach(() => { useMocks({ post: { - '/api/projects/:team/query': { + '/api/environments/:team_id/query': { results: [ ['s1', JSON.stringify({ blah: 'blah1' })], ['s2', JSON.stringify({ blah: 'blah2' })], diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts index 309362176d427..876ef29c38af9 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts @@ -43,7 +43,7 @@ describe('sessionRecordingsPlaylistLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/session_recordings/properties': { + '/api/environments/:team_id/session_recordings/properties': { results: [ { id: 's1', properties: { blah: 'blah1' } }, { id: 's2', properties: { blah: 'blah2' } }, @@ -52,7 +52,7 @@ describe('sessionRecordingsPlaylistLogic', () => { 'api/projects/:team/property_definitions/seen_together': { $pageview: true }, - '/api/projects/:team/session_recordings': (req) => { + '/api/environments/:team_id/session_recordings': (req) => { const { searchParams } = req.url if ( (searchParams.get('events')?.length || 0) > 0 && diff --git a/frontend/src/scenes/settings/environment/teamMembersLogic.tsx b/frontend/src/scenes/settings/environment/teamMembersLogic.tsx index 650ea406daa05..23274ece6ae8d 100644 --- a/frontend/src/scenes/settings/environment/teamMembersLogic.tsx +++ b/frontend/src/scenes/settings/environment/teamMembersLogic.tsx @@ -32,12 +32,12 @@ export const teamMembersLogic = kea([ explicitMembers: { __default: [] as ExplicitTeamMemberType[], loadMembers: async () => { - return await api.get(`api/projects/${teamLogic.values.currentTeamId}/explicit_members/`) + return await api.get(`api/environments/${teamLogic.values.currentTeamId}/explicit_members/`) }, addMembers: async ({ userUuids, level }: AddMembersFields) => { const newMembers: ExplicitTeamMemberType[] = await Promise.all( userUuids.map((userUuid) => - api.create(`api/projects/${teamLogic.values.currentTeamId}/explicit_members/`, { + api.create(`api/environments/${teamLogic.values.currentTeamId}/explicit_members/`, { user_uuid: userUuid, level, }) @@ -49,7 +49,9 @@ export const teamMembersLogic = kea([ return [...values.explicitMembers, ...newMembers] }, removeMember: async ({ member }: { member: BaseMemberType }) => { - await api.delete(`api/projects/${teamLogic.values.currentTeamId}/explicit_members/${member.user.uuid}/`) + await api.delete( + `api/environments/${teamLogic.values.currentTeamId}/explicit_members/${member.user.uuid}/` + ) lemonToast.success( <> {member.user.uuid === userLogic.values.user?.uuid @@ -164,7 +166,7 @@ export const teamMembersLogic = kea([ })), listeners(({ actions }) => ({ changeUserAccessLevel: async ({ user, newLevel }) => { - await api.update(`api/projects/${teamLogic.values.currentTeamId}/explicit_members/${user.uuid}/`, { + await api.update(`api/environments/${teamLogic.values.currentTeamId}/explicit_members/${user.uuid}/`, { level: newLevel, }) lemonToast.success( diff --git a/frontend/src/scenes/surveys/Surveys.stories.tsx b/frontend/src/scenes/surveys/Surveys.stories.tsx index 8c88e69dcd3fa..624c993b3e559 100644 --- a/frontend/src/scenes/surveys/Surveys.stories.tsx +++ b/frontend/src/scenes/surveys/Surveys.stories.tsx @@ -221,7 +221,7 @@ const meta: Meta = { }`]: toPaginatedResponse([MOCK_SURVEY_WITH_RELEASE_CONS.targeting_flag]), }, post: { - '/api/projects/:team_id/query/': async (req, res, ctx) => { + '/api/environments/:team_id/query/': async (req, res, ctx) => { const body = await req.json() if (body.kind == 'EventsQuery') { return res(ctx.json(MOCK_SURVEY_RESULTS)) diff --git a/frontend/src/scenes/surveys/surveyLogic.test.ts b/frontend/src/scenes/surveys/surveyLogic.test.ts index 38223aa0c0fdc..9827f071ed65b 100644 --- a/frontend/src/scenes/surveys/surveyLogic.test.ts +++ b/frontend/src/scenes/surveys/surveyLogic.test.ts @@ -213,7 +213,7 @@ describe('multiple choice survey logic', () => { '/api/projects/:team/surveys/responses_count': () => [200, {}], }, post: { - '/api/projects/:team/query/': () => [ + '/api/environments/:team_id/query/': () => [ 200, { results: [ @@ -263,7 +263,7 @@ describe('single choice survey logic', () => { '/api/projects/:team/surveys/responses_count': () => [200, {}], }, post: { - '/api/projects/:team/query/': () => [ + '/api/environments/:team_id/query/': () => [ 200, { results: [ @@ -313,7 +313,7 @@ describe('multiple choice survey with open choice logic', () => { '/api/projects/:team/surveys/responses_count': () => [200, {}], }, post: { - '/api/projects/:team/query/': () => [ + '/api/environments/:team_id/query/': () => [ 200, { results: [ @@ -363,7 +363,7 @@ describe('single choice survey with open choice logic', () => { '/api/projects/:team/surveys/responses_count': () => [200, {}], }, post: { - '/api/projects/:team/query/': () => [ + '/api/environments/:team_id/query/': () => [ 200, { results: [ diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx index f56637999e527..5d981f6004fec 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx @@ -86,7 +86,7 @@ export const Empty: StoryFn = () => { return (
- +
) } diff --git a/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts b/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts index f2666ba43f58f..ba343a2ffe02d 100644 --- a/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts +++ b/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts @@ -11,7 +11,7 @@ describe('personsModalLogic', () => { beforeEach(() => { useMocks({ get: { - 'api/projects/:team_id/persons/trends': {}, + 'api/environments/:team_id/persons/trends': {}, }, }) initKeaTests() diff --git a/frontend/src/scenes/web-analytics/SessionAttributionExplorer/sessionAttributionExplorer.stories.tsx b/frontend/src/scenes/web-analytics/SessionAttributionExplorer/sessionAttributionExplorer.stories.tsx index b98489824fd5f..6ba2259fd99ef 100644 --- a/frontend/src/scenes/web-analytics/SessionAttributionExplorer/sessionAttributionExplorer.stories.tsx +++ b/frontend/src/scenes/web-analytics/SessionAttributionExplorer/sessionAttributionExplorer.stories.tsx @@ -16,13 +16,13 @@ const meta: Meta = { decorators: [ mswDecorator({ get: { - '/api/projects/:team_id/query/:id/': async (_, res, ctx) => { + '/api/environments/:team_id/query/:id/': async (_, res, ctx) => { // eslint-disable-next-line @typescript-eslint/no-var-requires return res(ctx.json(require('./__mocks__/sessionAttributionQueryStatus.json'))) }, }, post: { - '/api/projects/:team_id/query/': async (_, res, ctx) => { + '/api/environments/:team_id/query/': async (_, res, ctx) => { // eslint-disable-next-line @typescript-eslint/no-var-requires return res(ctx.json(require('./__mocks__/sessionAttributionQuery.json'))) }, diff --git a/frontend/src/stories/How to mock requests.stories.mdx b/frontend/src/stories/How to mock requests.stories.mdx index dc2562938cbe8..6dd638be8da66 100644 --- a/frontend/src/stories/How to mock requests.stories.mdx +++ b/frontend/src/stories/How to mock requests.stories.mdx @@ -65,7 +65,7 @@ useStorybookMocks({ '/api/status_shorthand': () => [500, { error: 'Error text' }] // complicated param handling - '/api/projects/:team/insights': (req, _, ctx) => { + '/api/environments/:team_id/insights': (req, _, ctx) => { const team = req.params['team'] const shortId = req.url.searchParams.get('short_id') if (shortId === 'my_insight') { diff --git a/frontend/src/test/init.ts b/frontend/src/test/init.ts index 13597c8eebb25..c39467f2953fe 100644 --- a/frontend/src/test/init.ts +++ b/frontend/src/test/init.ts @@ -1,24 +1,30 @@ import { createMemoryHistory } from 'history' import { testUtilsPlugin } from 'kea-test-utils' -import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' +import { MOCK_DEFAULT_PROJECT, MOCK_DEFAULT_TEAM } from 'lib/api.mock' import { dayjs } from 'lib/dayjs' import posthog from 'posthog-js' import { organizationLogic } from 'scenes/organizationLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { projectLogic } from 'scenes/projectLogic' import { teamLogic } from 'scenes/teamLogic' import { initKea } from '~/initKea' -import { AppContext, TeamType } from '~/types' +import { AppContext, ProjectType, TeamType } from '~/types' process.on('unhandledRejection', (err) => { console.warn(err) }) -export function initKeaTests(mountCommonLogic = true, teamForWindowContext: TeamType = MOCK_DEFAULT_TEAM): void { +export function initKeaTests( + mountCommonLogic = true, + teamForWindowContext: TeamType = MOCK_DEFAULT_TEAM, + projectForWindowContext: ProjectType = MOCK_DEFAULT_PROJECT +): void { dayjs.tz.setDefault('UTC') window.POSTHOG_APP_CONTEXT = { ...window.POSTHOG_APP_CONTEXT, current_team: teamForWindowContext, + current_project: projectForWindowContext, } as unknown as AppContext posthog.init('no token', { autocapture: false, @@ -37,6 +43,7 @@ export function initKeaTests(mountCommonLogic = true, teamForWindowContext: Team if (mountCommonLogic) { preflightLogic.mount() teamLogic.mount() + projectLogic.mount() organizationLogic.mount() } } diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index c79582ed8d726..173909d404df6 100644 --- a/posthog/api/__init__.py +++ b/posthog/api/__init__.py @@ -171,8 +171,8 @@ def register_grandfathered_environment_nested_viewset( "project_dashboard_templates", ["project_id"], ) -project_dashboards_router = projects_router.register( - r"dashboards", dashboard.DashboardsViewSet, "project_dashboards", ["project_id"] +environment_dashboards_router, legacy_project_dashboards_router = register_grandfathered_environment_nested_viewset( + r"dashboards", dashboard.DashboardsViewSet, "environment_dashboards", ["team_id"] ) register_grandfathered_environment_nested_viewset( @@ -418,34 +418,54 @@ def register_grandfathered_environment_nested_viewset( projects_router.register(r"experiments", EnterpriseExperimentsViewSet, "project_experiments", ["project_id"]) register_grandfathered_environment_nested_viewset(r"groups", GroupsViewSet, "environment_groups", ["team_id"]) projects_router.register(r"groups_types", GroupsTypesViewSet, "project_groups_types", ["project_id"]) - project_insights_router = projects_router.register( - r"insights", EnterpriseInsightsViewSet, "project_insights", ["project_id"] + environment_insights_router, legacy_project_insights_router = register_grandfathered_environment_nested_viewset( + r"insights", EnterpriseInsightsViewSet, "environment_insights", ["team_id"] ) register_grandfathered_environment_nested_viewset( r"persons", EnterprisePersonViewSet, "environment_persons", ["team_id"] ) router.register(r"person", LegacyEnterprisePersonViewSet, "persons") else: - project_insights_router = projects_router.register(r"insights", InsightViewSet, "project_insights", ["project_id"]) + environment_insights_router, legacy_project_insights_router = register_grandfathered_environment_nested_viewset( + r"insights", InsightViewSet, "environment_insights", ["team_id"] + ) register_grandfathered_environment_nested_viewset(r"persons", PersonViewSet, "environment_persons", ["team_id"]) router.register(r"person", LegacyPersonViewSet, "persons") -project_dashboards_router.register( +environment_dashboards_router.register( r"sharing", sharing.SharingConfigurationViewSet, "environment_dashboard_sharing", ["team_id", "dashboard_id"], ) +legacy_project_dashboards_router.register( + r"sharing", + sharing.SharingConfigurationViewSet, + "project_dashboard_sharing", + ["team_id", "dashboard_id"], +) -project_insights_router.register( +environment_insights_router.register( r"sharing", sharing.SharingConfigurationViewSet, "environment_insight_sharing", ["team_id", "insight_id"], ) +legacy_project_insights_router.register( + r"sharing", + sharing.SharingConfigurationViewSet, + "project_insight_sharing", + ["team_id", "insight_id"], +) -project_insights_router.register( +environment_insights_router.register( + "thresholds", + alert.ThresholdViewSet, + "environment_insight_thresholds", + ["team_id", "insight_id"], +) +legacy_project_insights_router.register( "thresholds", alert.ThresholdViewSet, "project_insight_thresholds", diff --git a/posthog/api/dashboards/dashboard.py b/posthog/api/dashboards/dashboard.py index 86c2c568e9340..7541b6f00803b 100644 --- a/posthog/api/dashboards/dashboard.py +++ b/posthog/api/dashboards/dashboard.py @@ -2,7 +2,7 @@ from typing import Any, Optional, cast import structlog -from django.db.models import Prefetch, QuerySet +from django.db.models import Prefetch from django.shortcuts import get_object_or_404 from django.utils.timezone import now from rest_framework import exceptions, serializers, viewsets @@ -437,7 +437,12 @@ class DashboardsViewSet( def get_serializer_class(self) -> type[BaseSerializer]: return DashboardBasicSerializer if self.action == "list" else DashboardSerializer - def safely_get_queryset(self, queryset) -> QuerySet: + def dangerously_get_queryset(self): + # Dashboards are retrieved under /environments/ because they include team-specific query results, + # but they are in fact project-level, rather than environment-level + assert self.team.project_id is not None + queryset = self.queryset.filter(team__project_id=self.team.project_id) + include_deleted = ( self.action == "partial_update" and "deleted" in self.request.data @@ -488,7 +493,7 @@ def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Response: dashboard = get_object_or_404(queryset, pk=pk) dashboard.last_accessed_at = now() dashboard.save(update_fields=["last_accessed_at"]) - serializer = DashboardSerializer(dashboard, context={"view": self, "request": request}) + serializer = DashboardSerializer(dashboard, context=self.get_serializer_context()) return Response(serializer.data) @action(methods=["PATCH"], detail=True) @@ -504,7 +509,7 @@ def move_tile(self, request: Request, *args: Any, **kwargs: Any) -> Response: serializer = DashboardSerializer( Dashboard.objects.get(id=from_dashboard), - context={"view": self, "request": request}, + context=self.get_serializer_context(), ) return Response(serializer.data) @@ -544,7 +549,7 @@ def create_from_template_json(self, request: Request, *args: Any, **kwargs: Any) dashboard.delete() raise - return Response(DashboardSerializer(dashboard, context={"view": self, "request": request}).data) + return Response(DashboardSerializer(dashboard, context=self.get_serializer_context()).data) class LegacyDashboardsViewSet(DashboardsViewSet): diff --git a/posthog/api/event_definition.py b/posthog/api/event_definition.py index d8d1584c9c61e..4156d10f82793 100644 --- a/posthog/api/event_definition.py +++ b/posthog/api/event_definition.py @@ -88,7 +88,7 @@ def dangerously_get_queryset(self): search = self.request.GET.get("search", None) search_query, search_kwargs = term_search_filter_sql(self.search_fields, search) - params = {"team_id": self.team.project_id, "is_posthog_event": "$%", **search_kwargs} + params = {"project_id": self.project_id, "is_posthog_event": "$%", **search_kwargs} order_expressions = [self._ordering_params_from_request()] ingestion_taxonomy_is_available = self.organization.is_feature_available(AvailableFeature.INGESTION_TAXONOMY) @@ -136,11 +136,11 @@ def dangerously_get_object(self): ): from ee.models.event_definition import EnterpriseEventDefinition - enterprise_event = EnterpriseEventDefinition.objects.filter(id=id, team_id=self.team_id).first() + enterprise_event = EnterpriseEventDefinition.objects.filter(id=id, team__project_id=self.project_id).first() if enterprise_event: return enterprise_event - non_enterprise_event = EventDefinition.objects.get(id=id, team_id=self.team_id) + non_enterprise_event = EventDefinition.objects.get(id=id, team__project_id=self.project_id) new_enterprise_event = EnterpriseEventDefinition( eventdefinition_ptr_id=non_enterprise_event.id, description="" ) @@ -148,7 +148,7 @@ def dangerously_get_object(self): new_enterprise_event.save() return new_enterprise_event - return EventDefinition.objects.get(id=id, team_id=self.team_id) + return EventDefinition.objects.get(id=id, team__project_id=self.project_id) def get_serializer_class(self) -> type[serializers.ModelSerializer]: serializer_class = self.serializer_class diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py index aa9aa8222b9b2..324eb87765441 100644 --- a/posthog/api/feature_flag.py +++ b/posthog/api/feature_flag.py @@ -175,7 +175,7 @@ def validate_key(self, value): exclude_kwargs = {"pk": cast(FeatureFlag, self.instance).pk} if ( - FeatureFlag.objects.filter(key=value, team_id=self.context["team_id"], deleted=False) + FeatureFlag.objects.filter(key=value, team__project_id=self.context["project_id"], deleted=False) .exclude(**exclude_kwargs) .exists() ): diff --git a/posthog/api/insight.py b/posthog/api/insight.py index f27a1f41e559f..a039b7cb1929b 100644 --- a/posthog/api/insight.py +++ b/posthog/api/insight.py @@ -645,6 +645,7 @@ def insight_result(self, insight: Insight) -> InsightResult: return calculate_for_query_based_insight( insight, + team=self.context["get_team"](), dashboard=dashboard, execution_mode=execution_mode, user=None if self.context["request"].user.is_anonymous else self.context["request"].user, @@ -726,7 +727,12 @@ def get_serializer_context(self) -> dict[str, Any]: context["is_shared"] = isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication) return context - def safely_get_queryset(self, queryset) -> QuerySet: + def dangerously_get_queryset(self): + # Insights are retrieved under /environments/ because they include team-specific query results, + # but they are in fact project-level, rather than environment-level + assert self.team.project_id is not None + queryset = self.queryset.filter(team__project_id=self.team.project_id) + include_deleted = False if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication): diff --git a/posthog/api/organization_feature_flag.py b/posthog/api/organization_feature_flag.py index d91ec15ba1c41..3585f5f149849 100644 --- a/posthog/api/organization_feature_flag.py +++ b/posthog/api/organization_feature_flag.py @@ -179,6 +179,7 @@ def copy_flags(self, request, *args, **kwargs): context = { "request": request, "team_id": target_project_id, + "project_id": target_project_id, } existing_flag = FeatureFlag.objects.filter( diff --git a/posthog/api/sharing.py b/posthog/api/sharing.py index ef85e143152c2..d0cf5af56bafd 100644 --- a/posthog/api/sharing.py +++ b/posthog/api/sharing.py @@ -246,6 +246,7 @@ def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Any: "request": request, "user_permissions": UserPermissions(cast(User, request.user), resource.team), "is_shared": True, + "get_team": lambda: resource.team, } exported_data: dict[str, Any] = {"type": "embed" if embedded else "scene"} diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 59745fc27236d..5f9fbda86651a 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -11,6 +11,9 @@ '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportViewSet]: could not derive type of path parameter "project_id" because model "posthog.batch_exports.models.BatchExport" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportViewSet > BatchExportSerializer]: could not resolve serializer field "HogQLSelectQueryField(required=False)". Defaulting to "string"', '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportRunViewSet]: could not derive type of path parameter "project_id" because model "posthog.batch_exports.models.BatchExportRun" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard.py: Warning [DashboardsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard.Dashboard" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/api/dashboard_collaborator.py: Warning [DashboardCollaboratorViewSet]: could not derive type of path parameter "project_id" because model "ee.models.dashboard_privilege.DashboardPrivilege" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/sharing.py: Warning [SharingConfigurationViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.sharing_configuration.SharingConfiguration" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/event.py: Warning [EventViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/models/event/util.py: Warning [EventViewSet > ClickhouseEventSerializer]: unable to resolve type hint for function "get_id". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/models/event/util.py: Warning [EventViewSet > ClickhouseEventSerializer]: unable to resolve type hint for function "get_distinct_id". Consider using a type hint or @extend_schema_field. Defaulting to string.', @@ -26,6 +29,18 @@ '/home/runner/work/posthog/posthog/posthog/api/exports.py: Warning [ExportedAssetViewSet > ExportedAssetSerializer]: unable to resolve type hint for function "has_content". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/exports.py: Warning [ExportedAssetViewSet > ExportedAssetSerializer]: unable to resolve type hint for function "filename". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [GroupsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.group.group.Group" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/clickhouse/views/insights.py: Warning [EnterpriseInsightsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.insight.Insight" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_last_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_cache_target_age". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_next_allowed_client_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_result". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hasMore". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_columns". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_timezone". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_is_cached". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_query_status". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hogql". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_types". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/ee/clickhouse/views/person.py: Warning [EnterprisePersonViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.person.person.Person" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineDestinationsConfigsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.plugin.PluginConfig" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineDestinationsConfigsViewSet > PluginConfigSerializer]: unable to resolve type hint for function "get_config". Consider using a type hint or @extend_schema_field. Defaulting to string.', @@ -43,7 +58,6 @@ '/home/runner/work/posthog/posthog/posthog/session_recordings/session_recording_api.py: Warning [SessionRecordingViewSet]: could not derive type of path parameter "project_id" because model "posthog.session_recordings.models.session_recording.SessionRecording" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/person.py: Warning [SessionRecordingViewSet > SessionRecordingSerializer > MinimalPersonSerializer]: unable to resolve type hint for function "get_distinct_ids". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/session_recordings/session_recording_api.py: Warning [SessionRecordingViewSet > SessionRecordingSerializer]: unable to resolve type hint for function "storage". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/sharing.py: Warning [SharingConfigurationViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.sharing_configuration.SharingConfiguration" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/session.py: Warning [SessionViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/api/subscription.py: Warning [SubscriptionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.subscription.Subscription" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/api/subscription.py: Warning [SubscriptionViewSet > SubscriptionSerializer]: unable to resolve type hint for function "summary". Consider using a type hint or @extend_schema_field. Defaulting to string.', @@ -65,8 +79,6 @@ '/home/runner/work/posthog/posthog/posthog/api/annotation.py: Warning [AnnotationsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.annotation.Annotation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/cohort.py: Warning [CohortViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.cohort.cohort.Cohort" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard_templates.py: Warning [DashboardTemplateViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard_templates.DashboardTemplate" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard.py: Warning [DashboardsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard.Dashboard" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/api/dashboard_collaborator.py: Warning [DashboardCollaboratorViewSet]: could not derive type of path parameter "project_id" because model "ee.models.dashboard_privilege.DashboardPrivilege" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/early_access_feature.py: Warning [EarlyAccessFeatureViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.early_access_feature.EarlyAccessFeature" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/team.py: Warning [TeamViewSet > TeamSerializer]: unable to resolve type hint for function "get_product_intents". Consider using a type hint or @extend_schema_field. Defaulting to string.', "/home/runner/work/posthog/posthog/posthog/api/event_definition.py: Error [EventDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", @@ -75,18 +87,6 @@ '/home/runner/work/posthog/posthog/posthog/api/feature_flag.py: Warning [FeatureFlagViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.feature_flag.feature_flag.FeatureFlag" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/api/feature_flag_role_access.py: Warning [FeatureFlagRoleAccessViewSet]: could not derive type of path parameter "project_id" because model "ee.models.feature_flag_role_access.FeatureFlagRoleAccess" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [GroupsTypesViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.group_type_mapping.GroupTypeMapping" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/clickhouse/views/insights.py: Warning [EnterpriseInsightsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.insight.Insight" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_last_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_cache_target_age". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_next_allowed_client_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_result". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hasMore". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_columns". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_timezone". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_is_cached". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_query_status". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hogql". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_types". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/notebook.py: Warning [NotebookViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.notebook.notebook.Notebook" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', "/home/runner/work/posthog/posthog/posthog/api/property_definition.py: Error [PropertyDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", '/home/runner/work/posthog/posthog/posthog/api/property_definition.py: Warning [PropertyDefinitionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.property_definition.PropertyDefinition" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', @@ -99,6 +99,9 @@ 'Warning: encountered multiple names for the same choice set (EffectivePrivilegeLevelEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: encountered multiple names for the same choice set (MembershipLevelEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: operationId "environments_app_metrics_historical_exports_retrieve" has collisions [(\'/api/environments/{project_id}/app_metrics/{plugin_config_id}/historical_exports/\', \'get\'), (\'/api/environments/{project_id}/app_metrics/{plugin_config_id}/historical_exports/{id}/\', \'get\')]. resolving with numeral suffixes.', + 'Warning: operationId "environments_insights_activity_retrieve" has collisions [(\'/api/environments/{project_id}/insights/{id}/activity/\', \'get\'), (\'/api/environments/{project_id}/insights/activity/\', \'get\')]. resolving with numeral suffixes.', + 'Warning: operationId "Funnels" has collisions [(\'/api/environments/{project_id}/insights/funnel/\', \'post\'), (\'/api/projects/{project_id}/insights/funnel/\', \'post\')]. resolving with numeral suffixes.', + 'Warning: operationId "Trends" has collisions [(\'/api/environments/{project_id}/insights/trend/\', \'post\'), (\'/api/projects/{project_id}/insights/trend/\', \'post\')]. resolving with numeral suffixes.', 'Warning: operationId "environments_persons_activity_retrieve" has collisions [(\'/api/environments/{project_id}/persons/{id}/activity/\', \'get\'), (\'/api/environments/{project_id}/persons/activity/\', \'get\')]. resolving with numeral suffixes.', 'Warning: operationId "list" has collisions [(\'/api/organizations/\', \'get\'), (\'/api/organizations/{organization_id}/projects/\', \'get\')]. resolving with numeral suffixes.', 'Warning: operationId "create" has collisions [(\'/api/organizations/\', \'post\'), (\'/api/organizations/{organization_id}/projects/\', \'post\')]. resolving with numeral suffixes.', diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index dce82cffcf694..6f8487c876f95 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -1228,8 +1228,8 @@ SELECT COUNT(*) AS "__count" FROM "posthog_dashboarditem" INNER JOIN "posthog_team" ON ("posthog_dashboarditem"."team_id" = "posthog_team"."id") - WHERE (NOT ("posthog_dashboarditem"."deleted") - AND "posthog_team"."project_id" = 2) + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboarditem"."deleted")) ''' # --- # name: TestInsight.test_listing_insights_does_not_nplus1.26 @@ -1376,8 +1376,8 @@ INNER JOIN "posthog_team" ON ("posthog_dashboarditem"."team_id" = "posthog_team"."id") LEFT OUTER JOIN "posthog_user" ON ("posthog_dashboarditem"."created_by_id" = "posthog_user"."id") LEFT OUTER JOIN "posthog_user" T5 ON ("posthog_dashboarditem"."last_modified_by_id" = T5."id") - WHERE (NOT ("posthog_dashboarditem"."deleted") - AND "posthog_team"."project_id" = 2) + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboarditem"."deleted")) ORDER BY "posthog_dashboarditem"."order" ASC LIMIT 100 ''' diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr index f16a07a370c06..06e36c9b1ce79 100644 --- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr @@ -1914,9 +1914,10 @@ ''' SELECT 1 AS "a" FROM "posthog_featureflag" + INNER JOIN "posthog_team" ON ("posthog_featureflag"."team_id" = "posthog_team"."id") WHERE (NOT "posthog_featureflag"."deleted" AND "posthog_featureflag"."key" = 'copied-flag-key' - AND "posthog_featureflag"."team_id" = 2) + AND "posthog_team"."project_id" = 2) LIMIT 1 ''' # --- diff --git a/posthog/api/test/dashboards/__init__.py b/posthog/api/test/dashboards/__init__.py index ad6505b5a61a7..dff375dcae21d 100644 --- a/posthog/api/test/dashboards/__init__.py +++ b/posthog/api/test/dashboards/__init__.py @@ -83,6 +83,8 @@ def list_dashboards( team_id: Optional[int] = None, expected_status: int = status.HTTP_200_OK, query_params: Optional[dict] = None, + *, + parent: Literal["project", "environment"] = "project", ) -> dict: if team_id is None: team_id = self.team.id @@ -90,7 +92,7 @@ def list_dashboards( if query_params is None: query_params = {} - response = self.client.get(f"/api/projects/{team_id}/dashboards/", query_params) + response = self.client.get(f"/api/{parent}s/{team_id}/dashboards/", query_params) self.assertEqual(response.status_code, expected_status) response_json = response.json() diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index e0a2eaef4dd4a..da2c2cec45a27 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -304,8 +304,8 @@ FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") LEFT OUTER JOIN "posthog_user" ON ("posthog_dashboard"."created_by_id" = "posthog_user"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2 + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted") AND "posthog_dashboard"."id" = 2) LIMIT 21 ''' @@ -1941,8 +1941,8 @@ SELECT COUNT(*) AS "__count" FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2) + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted")) ''' # --- # name: TestDashboard.test_listing_dashboards_is_not_nplus1.27 @@ -1992,8 +1992,8 @@ FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") LEFT OUTER JOIN "posthog_user" ON ("posthog_dashboard"."created_by_id" = "posthog_user"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2) + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted")) ORDER BY "posthog_dashboard"."pinned" DESC, "posthog_dashboard"."name" ASC LIMIT 300 @@ -3692,8 +3692,8 @@ FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") LEFT OUTER JOIN "posthog_user" ON ("posthog_dashboard"."created_by_id" = "posthog_user"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2 + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted") AND "posthog_dashboard"."id" = 2) LIMIT 21 ''' @@ -8033,8 +8033,8 @@ FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") LEFT OUTER JOIN "posthog_user" ON ("posthog_dashboard"."created_by_id" = "posthog_user"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2 + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted") AND "posthog_dashboard"."id" = 2) LIMIT 21 ''' @@ -9368,8 +9368,8 @@ SELECT COUNT(*) AS "__count" FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2) + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted")) ''' # --- # name: TestDashboard.test_retrieve_dashboard_list.29 @@ -9419,8 +9419,8 @@ FROM "posthog_dashboard" INNER JOIN "posthog_team" ON ("posthog_dashboard"."team_id" = "posthog_team"."id") LEFT OUTER JOIN "posthog_user" ON ("posthog_dashboard"."created_by_id" = "posthog_user"."id") - WHERE (NOT ("posthog_dashboard"."deleted") - AND "posthog_team"."project_id" = 2) + WHERE ("posthog_team"."project_id" = 2 + AND NOT ("posthog_dashboard"."deleted")) ORDER BY "posthog_dashboard"."pinned" DESC, "posthog_dashboard"."name" ASC LIMIT 100 diff --git a/posthog/api/test/dashboards/test_dashboard.py b/posthog/api/test/dashboards/test_dashboard.py index d3e7e43d7f200..1b9c98b029f01 100644 --- a/posthog/api/test/dashboards/test_dashboard.py +++ b/posthog/api/test/dashboards/test_dashboard.py @@ -14,6 +14,7 @@ from posthog.hogql_queries.legacy_compatibility.filter_to_query import filter_to_query from posthog.models import Dashboard, DashboardTile, Filter, Insight, Team, User from posthog.models.organization import Organization +from posthog.models.project import Project from posthog.models.sharing_configuration import SharingConfiguration from posthog.models.signals import mute_selected_signals from posthog.test.base import ( @@ -82,6 +83,33 @@ def test_retrieve_dashboard_list(self): dashboard_names, ) + def test_retrieve_dashboard_list_includes_other_environments(self): + other_team_in_project = Team.objects.create(organization=self.organization, project=self.project) + _, team_in_other_project = Project.objects.create_with_team( + organization=self.organization, initiating_user=self.user + ) + + dashboard_a_id, _ = self.dashboard_api.create_dashboard({"name": "A"}, team_id=self.team.id) + dashboard_b_id, _ = self.dashboard_api.create_dashboard({"name": "B"}, team_id=other_team_in_project.id) + self.dashboard_api.create_dashboard({"name": "C"}, team_id=team_in_other_project.id) + + response_project_data = self.dashboard_api.list_dashboards(self.project.id) + response_env_current_data = self.dashboard_api.list_dashboards(self.team.id, parent="environment") + response_env_other_data = self.dashboard_api.list_dashboards(other_team_in_project.id, parent="environment") + + self.assertEqual( + {dashboard["id"] for dashboard in response_project_data["results"]}, + {dashboard_a_id, dashboard_b_id}, + ) + self.assertEqual( + {dashboard["id"] for dashboard in response_env_current_data["results"]}, + {dashboard_a_id, dashboard_b_id}, + ) + self.assertEqual( + {dashboard["id"] for dashboard in response_env_other_data["results"]}, + {dashboard_a_id, dashboard_b_id}, + ) + @snapshot_postgres_queries def test_retrieve_dashboard(self): dashboard = Dashboard.objects.create(team=self.team, name="private dashboard", created_by=self.user) @@ -555,7 +583,9 @@ def test_dashboard_insights_out_of_synch_with_tiles_are_not_shown(self): mock_view.action = "retrieve" mock_request = MagicMock() mock_request.query_params.get.return_value = None - dashboard_data = DashboardSerializer(dashboard, context={"view": mock_view, "request": mock_request}).data + dashboard_data = DashboardSerializer( + dashboard, context={"view": mock_view, "request": mock_request, "get_team": lambda: self.team} + ).data assert len(dashboard_data["tiles"]) == 1 def test_dashboard_insight_tiles_can_be_loaded_correct_context(self): diff --git a/posthog/api/test/test_feature_flag.py b/posthog/api/test/test_feature_flag.py index 7667de035550b..f43e49aaee91c 100644 --- a/posthog/api/test/test_feature_flag.py +++ b/posthog/api/test/test_feature_flag.py @@ -5151,6 +5151,7 @@ def test_feature_flags_v3_with_group_properties(self, *args): self.user = User.objects.create_and_join(self.organization, "random@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5185,7 +5186,7 @@ def test_feature_flags_v3_with_group_properties(self, *args): ], }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5200,7 +5201,7 @@ def test_feature_flags_v3_with_group_properties(self, *args): "groups": [{"properties": [], "rollout_percentage": None}], }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5256,6 +5257,7 @@ def test_feature_flags_v3_with_person_properties(self, mock_counter, *args): self.user = User.objects.create_and_join(self.organization, "random@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5286,7 +5288,7 @@ def test_feature_flags_v3_with_person_properties(self, mock_counter, *args): ] }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5298,7 +5300,7 @@ def test_feature_flags_v3_with_person_properties(self, mock_counter, *args): "key": "default-flag", "filters": {"groups": [{"properties": [], "rollout_percentage": None}]}, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5354,6 +5356,7 @@ def test_feature_flags_v3_with_a_working_slow_db(self, mock_postgres_check): self.user = User.objects.create_and_join(self.organization, "random@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5384,7 +5387,7 @@ def test_feature_flags_v3_with_a_working_slow_db(self, mock_postgres_check): ] }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5396,7 +5399,7 @@ def test_feature_flags_v3_with_a_working_slow_db(self, mock_postgres_check): "key": "default-flag", "filters": {"groups": [{"properties": [], "rollout_percentage": None}]}, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5454,6 +5457,7 @@ def test_feature_flags_v3_with_skip_database_setting(self, mock_postgres_check): self.user = User.objects.create_and_join(self.organization, "random@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5484,7 +5488,7 @@ def test_feature_flags_v3_with_skip_database_setting(self, mock_postgres_check): ] }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5496,7 +5500,7 @@ def test_feature_flags_v3_with_skip_database_setting(self, mock_postgres_check): "key": "default-flag", "filters": {"groups": [{"properties": [], "rollout_percentage": None}]}, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5661,6 +5665,7 @@ def test_feature_flags_v3_with_group_properties_and_slow_db(self, mock_counter, self.user = User.objects.create_and_join(self.organization, "randomXYZ@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5695,7 +5700,7 @@ def test_feature_flags_v3_with_group_properties_and_slow_db(self, mock_counter, ], }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5710,7 +5715,7 @@ def test_feature_flags_v3_with_group_properties_and_slow_db(self, mock_counter, "groups": [{"properties": [], "rollout_percentage": None}], }, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5782,6 +5787,7 @@ def test_feature_flags_v3_with_experience_continuity_working_slow_db(self, mock_ self.user = User.objects.create_and_join(self.organization, "random12@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5813,7 +5819,7 @@ def test_feature_flags_v3_with_experience_continuity_working_slow_db(self, mock_ }, "ensure_experience_continuity": True, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5825,7 +5831,7 @@ def test_feature_flags_v3_with_experience_continuity_working_slow_db(self, mock_ "key": "default-flag", "filters": {"groups": [{"properties": [], "rollout_percentage": None}]}, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5878,6 +5884,7 @@ def test_feature_flags_v3_with_experience_continuity_and_incident_mode(self, moc self.user = User.objects.create_and_join(self.organization, "random12@test.com", "password", "first_name") team_id = self.team.pk + project_id = self.team.project_id rf = RequestFactory() create_request = rf.post(f"api/projects/{self.team.pk}/feature_flags/", {"name": "xyz"}) create_request.user = self.user @@ -5909,7 +5916,7 @@ def test_feature_flags_v3_with_experience_continuity_and_incident_mode(self, moc }, "ensure_experience_continuity": True, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() @@ -5921,7 +5928,7 @@ def test_feature_flags_v3_with_experience_continuity_and_incident_mode(self, moc "key": "default-flag", "filters": {"groups": [{"properties": [], "rollout_percentage": None}]}, }, - context={"team_id": team_id, "request": create_request}, + context={"team_id": team_id, "project_id": project_id, "request": create_request}, ) self.assertTrue(serialized_data.is_valid()) serialized_data.save() diff --git a/posthog/api/test/test_insight.py b/posthog/api/test/test_insight.py index 6834ae2d6c40f..3aa7723fb9557 100644 --- a/posthog/api/test/test_insight.py +++ b/posthog/api/test/test_insight.py @@ -33,6 +33,7 @@ User, ) from posthog.models.insight_caching_state import InsightCachingState +from posthog.models.project import Project from posthog.schema import ( DataTableNode, DataVisualizationNode, @@ -91,6 +92,43 @@ def test_get_insight_items(self) -> None: self.assertEqual(len(response["results"]), 1) + def test_get_insight_items_all_environments_included(self) -> None: + filter_dict = { + "events": [{"id": "$pageview"}], + "properties": [{"key": "$browser", "value": "Mac OS X"}], + } + + other_team_in_project = Team.objects.create(organization=self.organization, project=self.project) + _, team_in_other_project = Project.objects.create_with_team( + organization=self.organization, initiating_user=self.user + ) + + insight_a = Insight.objects.create( + filters=Filter(data=filter_dict).to_dict(), + team=self.team, + created_by=self.user, + ) + insight_b = Insight.objects.create( + filters=Filter(data=filter_dict).to_dict(), + team=other_team_in_project, + created_by=self.user, + ) + Insight.objects.create( + filters=Filter(data=filter_dict).to_dict(), + team=team_in_other_project, + created_by=self.user, + ) + + # All of these three ways should return the same set of insights, + # i.e. all insights in the test project regardless of environment + response_project = self.client.get(f"/api/projects/{self.project.id}/insights/").json() + response_env_current = self.client.get(f"/api/environments/{self.team.id}/insights/").json() + response_env_other = self.client.get(f"/api/environments/{other_team_in_project.id}/insights/").json() + + self.assertEqual({insight["id"] for insight in response_project["results"]}, {insight_a.id, insight_b.id}) + self.assertEqual({insight["id"] for insight in response_env_current["results"]}, {insight_a.id, insight_b.id}) + self.assertEqual({insight["id"] for insight in response_env_other["results"]}, {insight_a.id, insight_b.id}) + @patch("posthoganalytics.capture") def test_created_updated_and_last_modified(self, mock_capture: mock.Mock) -> None: alt_user = User.objects.create_and_join(self.organization, "team2@posthog.com", None) @@ -339,6 +377,7 @@ def test_get_insight_in_shared_context(self) -> None: mock.ANY, dashboard=mock.ANY, execution_mode=ExecutionMode.EXTENDED_CACHE_CALCULATE_ASYNC_IF_STALE, + team=self.team, user=mock.ANY, filters_override=None, ) @@ -351,6 +390,7 @@ def test_get_insight_in_shared_context(self) -> None: mock.ANY, dashboard=mock.ANY, execution_mode=ExecutionMode.RECENT_CACHE_CALCULATE_BLOCKING_IF_STALE, + team=self.team, user=mock.ANY, filters_override=None, ) diff --git a/posthog/api/utils.py b/posthog/api/utils.py index 69abed44fd27f..514534990a8f0 100644 --- a/posthog/api/utils.py +++ b/posthog/api/utils.py @@ -312,7 +312,7 @@ def create_event_definitions_sql( SELECT {",".join(event_definition_fields)} FROM posthog_eventdefinition {enterprise_join} - WHERE team_id = %(team_id)s {conditions} + WHERE team_id = %(project_id)s {conditions} ORDER BY {additional_ordering}name ASC """ diff --git a/posthog/caching/calculate_results.py b/posthog/caching/calculate_results.py index 7da32bb9e88cd..985332c3c7206 100644 --- a/posthog/caching/calculate_results.py +++ b/posthog/caching/calculate_results.py @@ -125,6 +125,7 @@ def get_cache_type(cacheable: Optional[FilterType] | Optional[dict]) -> CacheTyp def calculate_for_query_based_insight( insight: Insight, *, + team: Team, dashboard: Optional[Dashboard] = None, execution_mode: ExecutionMode, user: Optional[User], @@ -133,12 +134,12 @@ def calculate_for_query_based_insight( from posthog.caching.fetch_from_cache import InsightResult, NothingInCacheResult from posthog.caching.insight_cache import update_cached_state - tag_queries(team_id=insight.team_id, insight_id=insight.pk) + tag_queries(team_id=team.id, insight_id=insight.pk) if dashboard: tag_queries(dashboard_id=dashboard.pk) response = process_response = process_query_dict( - insight.team, + team, insight.query, dashboard_filters_json=( filters_override if filters_override is not None else dashboard.filters if dashboard is not None else None @@ -161,7 +162,7 @@ def calculate_for_query_based_insight( last_refresh = response.get("last_refresh") if isinstance(cache_key, str) and isinstance(last_refresh, datetime): update_cached_state( # Updating the relevant InsightCachingState - insight.team_id, + team.id, cache_key, last_refresh, result=None, # Not caching the result here, since in HogQL this is the query runner's responsibility diff --git a/posthog/tasks/alerts/checks.py b/posthog/tasks/alerts/checks.py index 7af02e97a2ccd..7c66c1158c12b 100644 --- a/posthog/tasks/alerts/checks.py +++ b/posthog/tasks/alerts/checks.py @@ -274,6 +274,7 @@ def check_alert_atomically(alert: AlertConfiguration) -> None: calculation_result = calculate_for_query_based_insight( insight, + team=alert.team, execution_mode=ExecutionMode.RECENT_CACHE_CALCULATE_BLOCKING_IF_STALE, user=None, filters_override=filters_override,