diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 4e09f52eda7b6..a346ec59367a7 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -60,7 +60,9 @@ jobs: # NOTE: we are at risk of missing a dependency here. We could make # the dependencies more clear if we separated the backend/frontend # code completely + # really we should ignore ee/frontend/** but dorny doesn't support that - 'ee/**/*' + - '!ee/frontend/**' - 'posthog/**/*' - 'bin/*.py' - requirements.txt diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 2ea70218bd608..c586598152fd6 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -35,6 +35,7 @@ jobs: # NOTE: we are at risk of missing a dependency here. - 'bin/**' - 'frontend/**' + - 'ee/frontend/**' # Make sure we run if someone is explicitly change the workflow - .github/workflows/ci-frontend.yml # various JS config files @@ -117,18 +118,23 @@ jobs: jest: runs-on: ubuntu-latest needs: changes - name: Jest test (${{ matrix.chunk }}) + name: Jest test (${{ matrix.segment }} - ${{ matrix.chunk }}) strategy: # If one test fails, still run the others fail-fast: false matrix: + segment: ['FOSS', 'EE'] chunk: [1, 2, 3] steps: # we need at least one thing to run to make sure we include everything for required jobs - uses: actions/checkout@v3 + - name: Remove ee + if: needs.changes.outputs.frontend == 'true' && matrix.segment == 'FOSS' + run: rm -rf ee + - name: Install pnpm if: needs.changes.outputs.frontend == 'true' uses: pnpm/action-setup@v2 diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 32e74f8e62fe5..605e79c6b1066 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -1,5 +1,5 @@ import { toMatchImageSnapshot } from 'jest-image-snapshot' -import { getStoryContext, TestRunnerConfig, TestContext } from '@storybook/test-runner' +import { getStoryContext, TestRunnerConfig, TestContext, waitForPageReady } from '@storybook/test-runner' import type { Locator, Page, LocatorScreenshotOptions } from '@playwright/test' import type { Mocks } from '~/mocks/utils' import { StoryContext } from '@storybook/csf' @@ -119,8 +119,7 @@ async function expectStoryToMatchSnapshot( check = expectStoryToMatchComponentSnapshot } - // Wait for story to load - await page.waitForSelector('.sb-show-preparing-story', { state: 'detached' }) + await waitForPageReady(page) await page.evaluate(() => { // Stop all animations for consistent snapshots document.body.classList.add('storybook-test-runner') @@ -132,7 +131,7 @@ async function expectStoryToMatchSnapshot( await page.waitForSelector(waitForSelector) } - await page.waitForTimeout(400) // Wait for animations to finish + await page.waitForTimeout(400) // Wait for effects to finish // Wait for all images to load await page.waitForFunction(() => diff --git a/ee/frontend/exports.ts b/ee/frontend/exports.ts new file mode 100644 index 0000000000000..29d80016d730c --- /dev/null +++ b/ee/frontend/exports.ts @@ -0,0 +1,13 @@ +import { PostHogEE } from '@posthog/ee/types' + +const myTestCode = (): void => { + // eslint-disable-next-line no-console + console.log('it works!') +} + +const postHogEE: PostHogEE = { + enabled: true, + myTestCode, +} + +export default postHogEE diff --git a/frontend/@posthog/ee/exports.ts b/frontend/@posthog/ee/exports.ts new file mode 100644 index 0000000000000..96d7dc5a5e93d --- /dev/null +++ b/frontend/@posthog/ee/exports.ts @@ -0,0 +1,7 @@ +import { PostHogEE } from './types' + +const posthogEE: PostHogEE = { + enabled: false, +} + +export default posthogEE diff --git a/frontend/@posthog/ee/types.ts b/frontend/@posthog/ee/types.ts new file mode 100644 index 0000000000000..7270631c7c78f --- /dev/null +++ b/frontend/@posthog/ee/types.ts @@ -0,0 +1,5 @@ +// NOTE: All exported items from the EE module _must_ be optionally defined to ensure we work well with FOSS +export type PostHogEE = { + enabled: boolean + myTestCode?: () => void +} diff --git a/frontend/__snapshots__/components-cards-insight-card--insight-card.png b/frontend/__snapshots__/components-cards-insight-card--insight-card.png index febbc6faf33b6..0a38b26952afc 100644 Binary files a/frontend/__snapshots__/components-cards-insight-card--insight-card.png and b/frontend/__snapshots__/components-cards-insight-card--insight-card.png differ diff --git a/frontend/__snapshots__/components-compact-list--compact-list.png b/frontend/__snapshots__/components-compact-list--compact-list.png index 49cbfb2048571..1ab5cccecf7f1 100644 Binary files a/frontend/__snapshots__/components-compact-list--compact-list.png and b/frontend/__snapshots__/components-compact-list--compact-list.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png index e7268e9e53d57..310cd26b65415 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png index dd65298c2ff8f..d4c096567c39b 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png differ diff --git a/frontend/__snapshots__/filters-cohort-filters-fields-number--basic.png b/frontend/__snapshots__/filters-cohort-filters-fields-number--basic.png index f8cb1193d87e2..d9d0a7dd0ada5 100644 Binary files a/frontend/__snapshots__/filters-cohort-filters-fields-number--basic.png and b/frontend/__snapshots__/filters-cohort-filters-fields-number--basic.png differ diff --git a/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters.png b/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters.png index 4bf204ad1e606..d90e64ea54ca3 100644 Binary files a/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters.png and b/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters.png differ diff --git a/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png b/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png index d6139bbdf9627..511e5bcc051b4 100644 Binary files a/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png and b/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--custom-styles.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--custom-styles.png index bc7634e3b5f1e..92882cab397a8 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--custom-styles.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--custom-styles.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--default.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--default.png index 3ac0dedd31030..9e1bea26011c1 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--default.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--default.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--friday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--friday-first.png index 8cc57d7815d4b..751a6708b994f 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--friday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--friday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--monday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--monday-first.png index 0e49faf8d2454..c2af14074c91a 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--monday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--monday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--multiple-months.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--multiple-months.png index f17a82b5c17f5..a79d0f0a89777 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--multiple-months.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--multiple-months.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--saturday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--saturday-first.png index d22c3d4b650b3..5a0879568f37e 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--saturday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--saturday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--sunday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--sunday-first.png index 3ac0dedd31030..9e1bea26011c1 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--sunday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--sunday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--thursday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--thursday-first.png index 2daa09266b125..854876aec0706 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--thursday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--thursday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--tuesday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--tuesday-first.png index 5035ace9f02d5..34d43dec75bca 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--tuesday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--tuesday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--wednesday-first.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--wednesday-first.png index 2677654470c84..d1ca9e9a54ff5 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--wednesday-first.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar--wednesday-first.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png index 4c4220ef11640..b5e2113ee71af 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range-inline--lemon-calendar-range-inline.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range-inline--lemon-calendar-range-inline.png index 4d72144b7b205..6b9f4b17649a5 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range-inline--lemon-calendar-range-inline.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range-inline--lemon-calendar-range-inline.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png index 1c7c6fa99c2ed..8ea8124973d82 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png b/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png index f9d8d709b0563..01c803c849fb2 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png and b/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png differ diff --git a/frontend/__snapshots__/lemon-ui-utilities--overview.png b/frontend/__snapshots__/lemon-ui-utilities--overview.png index 646796a14a8f2..57866050bab70 100644 Binary files a/frontend/__snapshots__/lemon-ui-utilities--overview.png and b/frontend/__snapshots__/lemon-ui-utilities--overview.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png index cb3513e97394d..9c4e289199783 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png index d42c385e3c023..a0e2068387f91 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--edit.png b/frontend/__snapshots__/scenes-app-dashboards--edit.png index b7fe0d4edb8c0..89cb4fa56f224 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--edit.png and b/frontend/__snapshots__/scenes-app-dashboards--edit.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--show.png b/frontend/__snapshots__/scenes-app-dashboards--show.png index 432321bdd4a81..11b82f1a6edce 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--show.png and b/frontend/__snapshots__/scenes-app-dashboards--show.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png index 5f7c428ad21dd..3831be858dd60 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png b/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png index 9fe8b83975eb5..af83f781707c8 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png and b/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png b/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png index 4638e3c252629..e2d2e1bbd2679 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png and b/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png index 613e89cdbc0d8..5ee7a8708613f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png index f62fd32d947ea..336a737d983af 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png index 9dd59f171f782..b22467c95b46c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png index a290aabaf2c89..8bcadaeddb713 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png index 61758c0e5908a..48c95635cae74 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png index f0f18dad2910e..f7f0a786c5d30 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png index 0e7909e11f451..9a4387639a76b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png index 9e9adda1263cd..4cf0295e84e27 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png index 74c7c60a50e11..456b5fd067465 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png index 8760764ed3a04..1a4722fdb1a6f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png differ diff --git a/frontend/__snapshots__/scenes-app-persons-groups--cohorts.png b/frontend/__snapshots__/scenes-app-persons-groups--cohorts.png new file mode 100644 index 0000000000000..e1b37e1f46562 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-persons-groups--cohorts.png differ diff --git a/frontend/__snapshots__/scenes-app-persons-groups--groups.png b/frontend/__snapshots__/scenes-app-persons-groups--groups.png new file mode 100644 index 0000000000000..92ce45fd149ad Binary files /dev/null and b/frontend/__snapshots__/scenes-app-persons-groups--groups.png differ diff --git a/frontend/__snapshots__/scenes-app-persons-groups--persons.png b/frontend/__snapshots__/scenes-app-persons-groups--persons.png new file mode 100644 index 0000000000000..b2ff5023a1c61 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-persons-groups--persons.png differ diff --git a/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png b/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png index 774aa3c97a3e1..b7a45b2b53c70 100644 Binary files a/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png and b/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar--heatmap-dark.png b/frontend/__snapshots__/scenes-other-toolbar--heatmap-dark.png index d00fa2fb6139e..fcee0abbf1035 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar--heatmap-dark.png and b/frontend/__snapshots__/scenes-other-toolbar--heatmap-dark.png differ diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx index e080f49c67f26..e193083feef00 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx @@ -1,5 +1,5 @@ import { IconExternal } from '@posthog/icons' -import { LemonButton, LemonSkeleton } from '@posthog/lemon-ui' +import { LemonButton, LemonSelect, LemonSkeleton } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { useEffect, useRef, useState } from 'react' @@ -8,6 +8,11 @@ import { themeLogic } from '../../themeLogic' import { SidePanelPaneHeader } from '../components/SidePanelPane' import { POSTHOG_WEBSITE_ORIGIN, sidePanelDocsLogic } from './sidePanelDocsLogic' +type Menu = { + name: string + url?: string +} + function SidePanelDocsSkeleton(): JSX.Element { return (
@@ -23,12 +28,50 @@ function SidePanelDocsSkeleton(): JSX.Element { ) } +function Menu({ + menu, + activeMenuName, + onChange, +}: { + menu: Menu[] + activeMenuName: string | null + onChange: (newValue: string | null) => void +}): JSX.Element { + return ( +
+ ({ label: name, value: name }))} + /> +
+ ) +} + export const SidePanelDocs = (): JSX.Element => { const { iframeSrc, currentUrl } = useValues(sidePanelDocsLogic) const { updatePath, unmountIframe, closeSidePanel, handleExternalUrl } = useActions(sidePanelDocsLogic) const ref = useRef(null) const [ready, setReady] = useState(false) const { isDarkModeOn } = useValues(themeLogic) + const [menu, setMenu] = useState(null) + const [activeMenuName, setActiveMenuName] = useState(null) + + const handleMenuChange = (newValue: string | null): void => { + const url = menu?.find(({ name }: Menu) => name === newValue)?.url + if (url) { + ref.current?.contentWindow?.postMessage( + { + type: 'navigate', + url, + }, + '*' + ) + } + } useEffect(() => { ref.current?.contentWindow?.postMessage( @@ -57,6 +100,15 @@ export const SidePanelDocs = (): JSX.Element => { handleExternalUrl(event.data.url) return } + if (event.data.type === 'docs-menu') { + setMenu(event.data.menu) + return + } + + if (event.data.type === 'docs-active-menu') { + setActiveMenuName(event.data.activeMenuName) + return + } console.warn('Unhandled iframe message from Docs:', event.data) } @@ -79,6 +131,7 @@ export const SidePanelDocs = (): JSX.Element => { return ( <> + {menu && } } diff --git a/frontend/src/lib/components/CompactList/CompactList.scss b/frontend/src/lib/components/CompactList/CompactList.scss index 5c0e76c6093a3..6d8877a087898 100644 --- a/frontend/src/lib/components/CompactList/CompactList.scss +++ b/frontend/src/lib/components/CompactList/CompactList.scss @@ -1,15 +1,15 @@ -.compact-list { +.CompactList { border-radius: var(--radius); border: 1px solid var(--border); - height: calc(19.25rem); background: var(--bg-light); box-sizing: content-box; display: flex; flex-direction: column; flex: 1; + overflow: hidden; - .compact-list-header { - padding: 0.5rem 1rem 0; + .CompactList__header { + padding: 0.5rem; display: flex; align-items: center; justify-content: space-between; @@ -17,15 +17,15 @@ h3 { margin-bottom: 0; font-weight: 600; - font-size: 1rem; + font-size: 0.9rem; line-height: 1.4; } } - .scrollable-list { - flex: 1; - overflow: auto; - padding: 0 0.5rem; + .CompactList__content { + overflow-y: auto; + height: 16rem; + padding: 0.5rem; } .LemonButton { diff --git a/frontend/src/lib/components/CompactList/CompactList.tsx b/frontend/src/lib/components/CompactList/CompactList.tsx index eaa3f392c1013..826ef799b1038 100644 --- a/frontend/src/lib/components/CompactList/CompactList.tsx +++ b/frontend/src/lib/components/CompactList/CompactList.tsx @@ -24,15 +24,15 @@ export function CompactList({ renderRow, }: CompactListProps): JSX.Element { return ( -
-
-

{title}

+
+
+

+ {title} +

{viewAllURL && View all}
-
- -
-
+ +
{loading ? (
{Array.from({ length: 6 }, (_, index) => ( diff --git a/frontend/src/lib/ee.test.ts b/frontend/src/lib/ee.test.ts new file mode 100644 index 0000000000000..e5cddf9ec2436 --- /dev/null +++ b/frontend/src/lib/ee.test.ts @@ -0,0 +1,17 @@ +import fs from 'fs' + +const eeFolderExists = fs.existsSync('ee/frontend/exports.ts') +export const ifEeIt = eeFolderExists ? it : it.skip +export const ifFossIt = !eeFolderExists ? it : it.skip + +import posthogEE from '@posthog/ee/exports' + +describe('ee importing', () => { + ifEeIt('should import actual ee code', () => { + expect(posthogEE.enabled).toBe(true) + }) + + ifFossIt('should import actual ee code', () => { + expect(posthogEE.enabled).toBe(false) + }) +}) diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index 10be42e8fd679..c37cf5ecceb6f 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -661,6 +661,11 @@ export const KEY_MAPPING: KeyMappingInterface = { label: 'GeoIP Disabled', description: `Whether to skip GeoIP processing for the event.`, }, + $el_text: { + label: 'Element Text', + description: `The text of the element that was clicked. Only sent with Autocapture events.`, + examples: ['Click here!'], + }, // NOTE: This is a hack. $session_duration is a session property, not an event property // but we don't do a good job of tracking property types, so making it a session property // would require a large refactor, and this works (because all properties are treated as diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index b37a126d607e8..302f81c2d0657 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -391,8 +391,16 @@ export function idToKey(array: Record[], keyField: string = 'id'): return object } -export function delay(ms: number): Promise { - return new Promise((resolve) => window.setTimeout(resolve, ms)) +export function delay(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(resolve, ms) + if (signal) { + signal.addEventListener('abort', () => { + clearTimeout(timeoutId) + reject(new DOMException('Aborted', 'AbortError')) + }) + } + }) } export function clearDOMTextSelection(): void { diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 29d08863a62c0..ded22d89a9979 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -35,7 +35,7 @@ import { isTimeToSeeDataSessionsQuery, } from './utils' -const QUERY_ASYNC_MAX_INTERVAL_SECONDS = 10 +const QUERY_ASYNC_MAX_INTERVAL_SECONDS = 5 const QUERY_ASYNC_TOTAL_POLL_SECONDS = 300 //get export context for a given query @@ -115,15 +115,9 @@ async function executeQuery( let currentDelay = 300 // start low, because all queries will take at minimum this while (performance.now() - pollStart < QUERY_ASYNC_TOTAL_POLL_SECONDS * 1000) { - await delay(currentDelay) + await delay(currentDelay, methodOptions?.signal) currentDelay = Math.min(currentDelay * 2, QUERY_ASYNC_MAX_INTERVAL_SECONDS * 1000) - if (methodOptions?.signal?.aborted) { - const customAbortError = new Error('Query aborted') - customAbortError.name = 'AbortError' - throw customAbortError - } - const statusResponse = await api.queryStatus.get(response.id) if (statusResponse.complete || statusResponse.error) { diff --git a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss index b4a3090c12a25..6b1aea45485cc 100644 --- a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss +++ b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss @@ -10,6 +10,8 @@ table { --bar-width: 0.5rem; // This should be overriden from React --bar-row-height: 18rem; + --bar-padding-top: 1rem; + --bar-padding-bottom: 1.5rem; width: 100%; height: 100%; @@ -20,8 +22,8 @@ border-bottom: 1px solid var(--border); > td { - padding: 1.5rem 0; - padding-top: 1rem; + padding-top: var(--bar-padding-top); + padding-bottom: var(--bar-padding-bottom); } } @@ -40,7 +42,7 @@ } .StepBarLabels { - height: calc(var(--bar-row-height) - 3rem); + height: calc(var(--bar-row-height) - var(--bar-padding-top) - var(--bar-padding-bottom)); display: flex; flex-direction: column-reverse; align-items: flex-end; @@ -68,7 +70,7 @@ align-items: flex-end; gap: 0.125rem; border-bottom: 1px solid var(--border); - height: calc(var(--bar-row-height) - 3rem); + height: calc(var(--bar-row-height) - var(--bar-padding-top) - var(--bar-padding-bottom)); padding: 0 1rem; &:not(.StepBars--first) { diff --git a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx index db04937511c85..bd18eb7fdcbad 100644 --- a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx +++ b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx' import { useValues } from 'kea' import { useResizeObserver } from 'lib/hooks/useResizeObserver' import { useScrollable } from 'lib/hooks/useScrollable' -import { useMemo } from 'react' +import { useLayoutEffect, useState } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' import { ChartParams } from '~/types' @@ -29,7 +29,8 @@ export function FunnelBarChart({ showPersonsModal: showPersonsModalProp = true } const vizRef = useFunnelTooltip(showPersonsModal) const [scrollRef, [isScrollableLeft, isScrollableRight]] = useScrollable() - const { height } = useResizeObserver({ ref: vizRef }) + const { height: availableHeight } = useResizeObserver({ ref: vizRef }) + const [scrollbarHeightPx, setScrollbarHeightPx] = useState(0) const seriesCount = visibleStepsWithConversionMetrics[0]?.nested_breakdown?.length ?? 0 const barWidthPx = @@ -55,57 +56,24 @@ export function FunnelBarChart({ showPersonsModal: showPersonsModalProp = true } ? 96 : 192 - const table = useMemo(() => { - /** Average conversion time is only shown if it's known for at least one step. */ - // != is intentional to catch undefined too - const showTime = visibleStepsWithConversionMetrics.some((step) => step.average_conversion_time != null) - const barRowHeight = `calc(${height}px - 17px - 3rem - (1.75rem * ${showTime ? 3 : 2}) - 1px)` + useLayoutEffect(() => { + if (scrollRef.current) { + setScrollbarHeightPx(scrollRef.current.offsetHeight - scrollRef.current.clientHeight) + } + }, [availableHeight]) - return ( - - - {visibleStepsWithConversionMetrics.map((_, i) => ( - - ))} - - {/* The last column is meant to fill up leftover space. */} - - - - - {visibleStepsWithConversionMetrics.map((step, stepIndex) => ( - - ))} - - - - ))} - - -
- - - -
- {visibleStepsWithConversionMetrics.map((step, stepIndex) => ( - - -
- ) - }, [visibleStepsWithConversionMetrics, height]) + /** Average conversion time is only shown if it's known for at least one step. */ + // != is intentional to catch undefined too + const showTime = visibleStepsWithConversionMetrics.some((step) => step.average_conversion_time != null) + + const stepLegendRows = showTime ? 4 : 3 + + // rows * (row height + gap between rows) - no gap for first row + padding top and bottom + const stepLegendHeightRem = stepLegendRows * (1.5 + 0.25) - 0.25 + 2 * 0.75 + const borderHeightPx = 1 + + // available height - border - legend - (maybe) scrollbar + const barRowHeight = `calc(${availableHeight}px - ${borderHeightPx}px - ${stepLegendHeightRem}rem - ${scrollbarHeightPx}px)` return (
- {table} + + + {visibleStepsWithConversionMetrics.map((_, i) => ( + + ))} + + {/* The last column is meant to fill up leftover space. */} + + + + + {visibleStepsWithConversionMetrics.map((step, stepIndex) => ( + + ))} + + + + ))} + + +
+ + + +
+ {visibleStepsWithConversionMetrics.map((step, stepIndex) => ( + + +
) diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx b/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx index 5b77c77556392..2569743a57205 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx +++ b/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx @@ -25,6 +25,7 @@ interface BarProps { wrapperWidth: number } type LabelPosition = 'inside' | 'outside' + export function Bar({ percentage: conversionPercentage, name, diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx index 10b44978f1cf8..77f589785120a 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx +++ b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx @@ -98,7 +98,7 @@ export function FunnelBarGraph({ ) : null}
-
+
{!width ? null : isBreakdown ? ( <> {step?.nested_breakdown?.map((breakdown, index) => { diff --git a/frontend/src/scenes/persons-management/PersonsManagementScene.stories.tsx b/frontend/src/scenes/persons-management/PersonsManagementScene.stories.tsx new file mode 100644 index 0000000000000..98172922fa975 --- /dev/null +++ b/frontend/src/scenes/persons-management/PersonsManagementScene.stories.tsx @@ -0,0 +1,37 @@ +import { Meta, StoryFn } from '@storybook/react' +import { router } from 'kea-router' +import { useEffect } from 'react' +import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + +const meta: Meta = { + title: 'Scenes-App/Persons & Groups', + parameters: { + layout: 'fullscreen', + viewMode: 'story', + mockDate: '2023-07-04', // To stabilize relative dates + }, + decorators: [], +} +export default meta + +export const Persons: StoryFn = () => { + useEffect(() => { + router.actions.push(urls.persons()) + }, []) + return +} + +export const Cohorts: StoryFn = () => { + useEffect(() => { + router.actions.push(urls.cohorts()) + }, []) + return +} + +export const Groups: StoryFn = () => { + useEffect(() => { + router.actions.push(urls.groups(0)) + }, []) + return +} diff --git a/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx b/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx index 7734a37566197..36c93aee71e34 100644 --- a/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx +++ b/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx @@ -95,7 +95,7 @@ export const personsManagementSceneLogic = kea( ...(showGroupsIntroductionPage ? [ { - key: 'groups-intro', + key: 'groups-0', label: 'Groups', url: urls.groups(0), content: , diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.scss b/frontend/src/scenes/project-homepage/ProjectHomepage.scss index f905c0241e4a4..33df45806bc54 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.scss +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.scss @@ -1,11 +1,13 @@ -.project-homepage { - .HomepageDashboardHeader { +.ProjectHomepage { + container-type: inline-size; + + .ProjectHomepage__dashboardheader { margin-top: 1rem; display: flex; justify-content: space-between; align-items: center; - .HomepageDashboardHeader__title { + .ProjectHomepage__dashboardheader__title { display: flex; flex-direction: row; align-items: center; @@ -30,29 +32,12 @@ } } - .top-list-container-horizontal { + .ProjectHomepage__lists { display: flex; + gap: 1rem; - .top-list { - margin-bottom: 1.5rem; - - .posthog-3000 & { - margin-bottom: 0; - } - - width: 33%; - } - - .spacer { - width: 1rem; - } - } - - .top-list-container-vertical { - margin-bottom: 1.5rem; - - .spacer { - height: 1rem; + @container (max-width: 800px) { + flex-direction: column; } } diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.tsx b/frontend/src/scenes/project-homepage/ProjectHomepage.tsx index ef77e7b65ef86..383d73fae8125 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.tsx @@ -2,7 +2,6 @@ import './ProjectHomepage.scss' import { IconHome } from '@posthog/icons' import { Link } from '@posthog/lemon-ui' -import useSize from '@react-hook/size' import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' import { SceneDashboardChoiceModal } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceModal' @@ -13,7 +12,6 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { useRef } from 'react' import { Dashboard } from 'scenes/dashboard/Dashboard' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { projectHomepageLogic } from 'scenes/project-homepage/projectHomepageLogic' @@ -25,8 +23,8 @@ import { urls } from 'scenes/urls' import { DashboardPlacement } from '~/types' -import { NewlySeenPersons } from './NewlySeenPersons' import { RecentInsights } from './RecentInsights' +import { RecentPersons } from './RecentPersons' import { RecentRecordings } from './RecentRecordings' export function ProjectHomepage(): JSX.Element { @@ -39,9 +37,6 @@ export function ProjectHomepage(): JSX.Element { ) const { featureFlags } = useValues(featureFlagLogic) - const topListContainerRef = useRef(null) - const [topListContainerWidth] = useSize(topListContainerRef) - const has3000 = featureFlags[FEATURE_FLAGS.POSTHOG_3000] const headerButtons = ( @@ -60,32 +55,17 @@ export function ProjectHomepage(): JSX.Element { ) return ( -
+
-
-
- -
-
-
- -
-
-
- -
+
+ + +
{currentTeam?.primary_dashboard ? ( <> -
-
+
+
{!dashboard && } {dashboard?.name && ( <> diff --git a/frontend/src/scenes/project-homepage/NewlySeenPersons.tsx b/frontend/src/scenes/project-homepage/RecentPersons.tsx similarity index 97% rename from frontend/src/scenes/project-homepage/NewlySeenPersons.tsx rename to frontend/src/scenes/project-homepage/RecentPersons.tsx index 8df69f2bc4697..d66987da1f25a 100644 --- a/frontend/src/scenes/project-homepage/NewlySeenPersons.tsx +++ b/frontend/src/scenes/project-homepage/RecentPersons.tsx @@ -29,7 +29,7 @@ function PersonRow({ person }: { person: PersonType }): JSX.Element { ) } -export function NewlySeenPersons(): JSX.Element { +export function RecentPersons(): JSX.Element { const { persons, personsLoading } = useValues(projectHomepageLogic) return ( diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss index 379fe80fd42cc..05cd2af4360e3 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss @@ -58,6 +58,10 @@ // NOTE: Somewhat random way to offset the various headers and tabs above the playlist height: calc(100vh - 15rem); min-height: 41rem; + + .posthog-3000 & { + height: calc(100vh - 9rem); + } } .SessionRecordingPreview { diff --git a/jest.config.ts b/jest.config.ts index 54aa39875ab00..3ff4eeff1d470 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -89,6 +89,7 @@ const config: Config = { '^~/(.*)$': '/$1', '^@posthog/lemon-ui(|/.*)$': '/../@posthog/lemon-ui/src/$1', '^@posthog/apps-common(|/.*)$': '/../@posthog/apps-common/src/$1', + '^@posthog/ee/exports': ['/../../ee/frontend/exports', '/../@posthog/ee/exports'], '^lib/(.*)$': '/lib/$1', '^scenes/(.*)$': '/scenes/$1', '^antd/es/(.*)$': 'antd/lib/$1', diff --git a/package.json b/package.json index 229bb35d6e5aa..01da1beb31545 100644 --- a/package.json +++ b/package.json @@ -295,6 +295,11 @@ "optionalDependencies": { "fsevents": "^2.3.2" }, + "pnpm": { + "overrides": { + "playwright": "1.32.2" + } + }, "lint-staged": { "*.{json,yaml,yml}": "prettier --write", "*.{css,scss}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef1cc24fe378a..b95eef04bf7e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + playwright: 1.32.2 + dependencies: '@ant-design/icons': specifier: ^4.7.0 @@ -5311,7 +5314,7 @@ packages: jest-serializer-html: 7.1.0 jest-watch-typeahead: 2.2.2(jest@29.7.0) node-fetch: 2.6.7 - playwright: 1.29.2 + playwright: 1.32.2 read-pkg-up: 7.0.1 tempy: 1.0.1 ts-dedent: 2.2.0 @@ -15354,25 +15357,19 @@ packages: find-up: 6.3.0 dev: true - /playwright-core@1.29.2: - resolution: {integrity: sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA==} - engines: {node: '>=14'} - hasBin: true - dev: true - /playwright-core@1.32.2: resolution: {integrity: sha512-zD7aonO+07kOTthsrCR3YCVnDcqSHIJpdFUtZEMOb6//1Rc7/6mZDRdw+nlzcQiQltOOsiqI3rrSyn/SlyjnJQ==} engines: {node: '>=14'} hasBin: true dev: true - /playwright@1.29.2: - resolution: {integrity: sha512-hKBYJUtdmYzcjdhYDkP9WGtORwwZBBKAW8+Lz7sr0ZMxtJr04ASXVzH5eBWtDkdb0c3LLFsehfPBTRfvlfKJOA==} + /playwright@1.32.2: + resolution: {integrity: sha512-jHVnXJke0PXpuPszKtk9y1zZSlzO5+2a+aockT/AND0oeXx46FiJEFrafthurglLygVZA+1gEbtUM1C7qtTV+Q==} engines: {node: '>=14'} hasBin: true requiresBuild: true dependencies: - playwright-core: 1.29.2 + playwright-core: 1.32.2 dev: true /please-upgrade-node@3.2.0: diff --git a/posthog/admin.py b/posthog/admin.py index 2c0d26a1b01fb..7161cbaf27081 100644 --- a/posthog/admin.py +++ b/posthog/admin.py @@ -33,8 +33,6 @@ ) from posthog.warehouse.models import DataWarehouseTable -admin.site.register(DataWarehouseTable) - class DashboardTileInline(admin.TabularInline): extra = 0 @@ -81,6 +79,39 @@ def organization_link(self, dashboard: Dashboard): ) +@admin.register(DataWarehouseTable) +class DataWarehouseTableAdmin(admin.ModelAdmin): + list_display = ( + "id", + "name", + "format", + "url_pattern", + "team_link", + "organization_link", + "created_at", + "created_by", + ) + list_display_links = ("id", "name") + list_select_related = ("team", "team__organization") + search_fields = ("id", "name", "team__name", "team__organization__name") + autocomplete_fields = ("team", "created_by") + ordering = ("-created_at",) + + def team_link(self, dashboard: Dashboard): + return format_html( + '{}', + dashboard.team.pk, + dashboard.team.name, + ) + + def organization_link(self, dashboard: Dashboard): + return format_html( + '{}', + dashboard.team.organization.pk, + dashboard.team.organization.name, + ) + + @admin.register(Text) class TextAdmin(admin.ModelAdmin): autocomplete_fields = ("created_by", "last_modified_by", "team") diff --git a/posthog/api/cohort.py b/posthog/api/cohort.py index 90324a48bfda0..c023d7ffe7ab2 100644 --- a/posthog/api/cohort.py +++ b/posthog/api/cohort.py @@ -2,6 +2,7 @@ import json from django.db import DatabaseError +from sentry_sdk import start_span import structlog from posthog.models.feature_flag.flag_matching import ( @@ -10,7 +11,8 @@ get_feature_flag_hash_key_overrides, ) from posthog.models.person.person import PersonDistinctId -from posthog.models.property.property import Property +from posthog.models.property.property import Property, PropertyGroup +from posthog.queries.base import property_group_to_Q from posthog.queries.insight import insight_sync_execute import posthoganalytics from posthog.metrics import LABEL_TEAM_ID @@ -47,6 +49,7 @@ INSIGHT_TRENDS, LIMIT, OFFSET, + PropertyOperatorType, ) from posthog.event_usage import report_user_action from posthog.hogql.context import HogQLContext @@ -584,11 +587,20 @@ def get_cohort_actors_for_feature_flag(cohort_id: int, flag: str, team_id: int, for property in property_list: default_person_properties.update(get_default_person_property(property, cohorts_cache)) + flag_property_conditions = [Filter(data=condition).property_groups for condition in feature_flag.conditions] + flag_property_group = PropertyGroup(type=PropertyOperatorType.OR, values=flag_property_conditions) + try: # QuerySet.Iterator() doesn't work with pgbouncer, it will load everything into memory and then stream # which doesn't work for us, so need a manual chunking here. # Because of this pgbouncer transaction pooling mode, we can't use server-side cursors. - queryset = Person.objects.filter(team_id=team_id).order_by("id") + # We pre-filter all persons to be ones that will match the feature flag, so that we don't have to + # iterate through all persons + queryset = ( + Person.objects.filter(team_id=team_id) + .filter(property_group_to_Q(flag_property_group, cohorts_cache=cohorts_cache)) + .order_by("id") + ) # get batchsize number of people at a time start = 0 batch_of_persons = queryset[start : start + batchsize] @@ -614,48 +626,49 @@ def get_cohort_actors_for_feature_flag(cohort_id: int, flag: str, team_id: int, if len(all_persons) == 0: break - for person in all_persons: - # ignore almost-deleted persons / persons with no distinct ids - if len(person.distinct_ids) == 0: - continue - - distinct_id = person.distinct_ids[0] - person_overrides = {} - if feature_flag.ensure_experience_continuity: - # :TRICKY: This is inefficient because it tries to get the hashkey overrides one by one. - # But reusing functions is better for maintainability. Revisit optimising if this becomes a bottleneck. - person_overrides = get_feature_flag_hash_key_overrides( - team_id, [distinct_id], person_id_to_distinct_id_mapping={person.id: distinct_id} - ) + with start_span(op="batch_flag_matching_with_overrides"): + for person in all_persons: + # ignore almost-deleted persons / persons with no distinct ids + if len(person.distinct_ids) == 0: + continue + + distinct_id = person.distinct_ids[0] + person_overrides = {} + if feature_flag.ensure_experience_continuity: + # :TRICKY: This is inefficient because it tries to get the hashkey overrides one by one. + # But reusing functions is better for maintainability. Revisit optimising if this becomes a bottleneck. + person_overrides = get_feature_flag_hash_key_overrides( + team_id, [distinct_id], person_id_to_distinct_id_mapping={person.id: distinct_id} + ) - try: - match = FeatureFlagMatcher( - [feature_flag], - distinct_id, - groups={}, - cache=matcher_cache, - hash_key_overrides=person_overrides, - property_value_overrides={**default_person_properties, **person.properties}, - group_property_value_overrides={}, - cohorts_cache=cohorts_cache, - ).get_match(feature_flag) - if match.match: - uuids_to_add_to_cohort.append(str(person.uuid)) - except (DatabaseError, ValueError, ValidationError): - logger.exception( - "Error evaluating feature flag for person", person_uuid=str(person.uuid), team_id=team_id - ) - except Exception as err: - # matching errors are not fatal, so we just log them and move on. - # Capturing in sentry for now just in case there are some unexpected errors - # we did not account for. - capture_exception(err) - - if len(uuids_to_add_to_cohort) >= batchsize - 1: - cohort.insert_users_list_by_uuid( - uuids_to_add_to_cohort, insert_in_clickhouse=True, batchsize=batchsize - ) - uuids_to_add_to_cohort = [] + try: + match = FeatureFlagMatcher( + [feature_flag], + distinct_id, + groups={}, + cache=matcher_cache, + hash_key_overrides=person_overrides, + property_value_overrides={**default_person_properties, **person.properties}, + group_property_value_overrides={}, + cohorts_cache=cohorts_cache, + ).get_match(feature_flag) + if match.match: + uuids_to_add_to_cohort.append(str(person.uuid)) + except (DatabaseError, ValueError, ValidationError): + logger.exception( + "Error evaluating feature flag for person", person_uuid=str(person.uuid), team_id=team_id + ) + except Exception as err: + # matching errors are not fatal, so we just log them and move on. + # Capturing in sentry for now just in case there are some unexpected errors + # we did not account for. + capture_exception(err) + + if len(uuids_to_add_to_cohort) >= batchsize: + cohort.insert_users_list_by_uuid( + uuids_to_add_to_cohort, insert_in_clickhouse=True, batchsize=batchsize + ) + uuids_to_add_to_cohort = [] start += batchsize batch_of_persons = queryset[start : start + batchsize] diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py index 92add84a0bcab..5022fd21676ea 100644 --- a/posthog/api/feature_flag.py +++ b/posthog/api/feature_flag.py @@ -636,6 +636,7 @@ def create_static_cohort_for_flag(self, request: request.Request, **kwargs): "is_static": True, "key": feature_flag_key, "name": f'Users with feature flag {feature_flag_key} enabled at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', + "is_calculating": True, }, context={ "request": request, diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index ffe583b425eac..e58824ff62f94 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -380,7 +380,7 @@ LIMIT 21 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.10 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.2 ' SELECT "posthog_person"."id", "posthog_person"."created_at", @@ -393,77 +393,15 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 2 - OFFSET 4 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.11 - ' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."key" = 'some-feature2' - AND "posthog_featureflag"."team_id" = 2) - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.12 - ' - SELECT "posthog_cohort"."id", - "posthog_cohort"."name", - "posthog_cohort"."description", - "posthog_cohort"."team_id", - "posthog_cohort"."deleted", - "posthog_cohort"."filters", - "posthog_cohort"."version", - "posthog_cohort"."pending_version", - "posthog_cohort"."count", - "posthog_cohort"."created_by_id", - "posthog_cohort"."created_at", - "posthog_cohort"."is_calculating", - "posthog_cohort"."last_calculation", - "posthog_cohort"."errors_calculating", - "posthog_cohort"."is_static", - "posthog_cohort"."groups" - FROM "posthog_cohort" - WHERE "posthog_cohort"."id" = 2 - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.13 - ' - SELECT "posthog_person"."id", - "posthog_person"."created_at", - "posthog_person"."properties_last_updated_at", - "posthog_person"."properties_last_operation", - "posthog_person"."team_id", - "posthog_person"."properties", - "posthog_person"."is_user_id", - "posthog_person"."is_identified", - "posthog_person"."uuid", - "posthog_person"."version" - FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 - ORDER BY "posthog_person"."id" ASC - LIMIT 10 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.14 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.3 ' SELECT "posthog_persondistinctid"."id", "posthog_persondistinctid"."team_id", @@ -475,7 +413,7 @@ (SELECT U0."id" FROM "posthog_persondistinctid" U0 WHERE U0."person_id" = "posthog_persondistinctid"."person_id" - LIMIT 1) + LIMIT 3) AND "posthog_persondistinctid"."person_id" IN (1, 2, 3, @@ -483,26 +421,7 @@ 5 /* ... */)) ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.15 - ' - SELECT "posthog_person"."id", - "posthog_person"."created_at", - "posthog_person"."properties_last_updated_at", - "posthog_person"."properties_last_operation", - "posthog_person"."team_id", - "posthog_person"."properties", - "posthog_person"."is_user_id", - "posthog_person"."is_identified", - "posthog_person"."uuid", - "posthog_person"."version" - FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 - ORDER BY "posthog_person"."id" ASC - LIMIT 10 - OFFSET 10 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.16 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.4 ' SELECT "posthog_person"."uuid" FROM "posthog_person" @@ -517,7 +436,7 @@ LIMIT 1))) ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.17 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.5 ' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -568,13 +487,14 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.2 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.6 ' SELECT "posthog_person"."id", "posthog_person"."created_at", @@ -587,12 +507,16 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 2 + OFFSET 2 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.3 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.7 ' SELECT "posthog_persondistinctid"."id", "posthog_persondistinctid"."team_id", @@ -612,95 +536,7 @@ 5 /* ... */)) ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.4 - ' - SELECT "posthog_person"."uuid" - FROM "posthog_person" - WHERE ("posthog_person"."team_id" = 2 - AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, - '00000000-0000-0000-0000-000000000001'::uuid /* ... */) - AND NOT (EXISTS - (SELECT (1) AS "a" - FROM "posthog_cohortpeople" U1 - WHERE (U1."cohort_id" = 2 - AND U1."person_id" = "posthog_person"."id") - LIMIT 1))) - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.5 - ' - SELECT "posthog_team"."id", - "posthog_team"."uuid", - "posthog_team"."organization_id", - "posthog_team"."api_token", - "posthog_team"."app_urls", - "posthog_team"."name", - "posthog_team"."slack_incoming_webhook", - "posthog_team"."created_at", - "posthog_team"."updated_at", - "posthog_team"."anonymize_ips", - "posthog_team"."completed_snippet_onboarding", - "posthog_team"."has_completed_onboarding_for", - "posthog_team"."ingested_event", - "posthog_team"."autocapture_opt_out", - "posthog_team"."autocapture_exceptions_opt_in", - "posthog_team"."autocapture_exceptions_errors_to_ignore", - "posthog_team"."session_recording_opt_in", - "posthog_team"."session_recording_sample_rate", - "posthog_team"."session_recording_minimum_duration_milliseconds", - "posthog_team"."session_recording_linked_flag", - "posthog_team"."session_recording_network_payload_capture_config", - "posthog_team"."capture_console_log_opt_in", - "posthog_team"."capture_performance_opt_in", - "posthog_team"."surveys_opt_in", - "posthog_team"."session_recording_version", - "posthog_team"."signup_token", - "posthog_team"."is_demo", - "posthog_team"."access_control", - "posthog_team"."week_start_day", - "posthog_team"."inject_web_apps", - "posthog_team"."test_account_filters", - "posthog_team"."test_account_filters_default_checked", - "posthog_team"."path_cleaning_filters", - "posthog_team"."timezone", - "posthog_team"."data_attributes", - "posthog_team"."person_display_name_properties", - "posthog_team"."live_events_columns", - "posthog_team"."recording_domains", - "posthog_team"."primary_dashboard_id", - "posthog_team"."extra_settings", - "posthog_team"."correlation_config", - "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id", - "posthog_team"."external_data_workspace_last_synced_at" - FROM "posthog_team" - WHERE "posthog_team"."id" = 2 - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.6 - ' - SELECT "posthog_person"."uuid" - FROM "posthog_person" - WHERE ("posthog_person"."team_id" = 2 - AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, - '00000000-0000-0000-0000-000000000001'::uuid /* ... */) - AND NOT (EXISTS - (SELECT (1) AS "a" - FROM "posthog_cohortpeople" U1 - WHERE (U1."cohort_id" = 2 - AND U1."person_id" = "posthog_person"."id") - LIMIT 1))) - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.7 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.8 ' SELECT "posthog_person"."id", "posthog_person"."created_at", @@ -713,30 +549,13 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 2 - OFFSET 2 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.8 - ' - SELECT "posthog_persondistinctid"."id", - "posthog_persondistinctid"."team_id", - "posthog_persondistinctid"."person_id", - "posthog_persondistinctid"."distinct_id", - "posthog_persondistinctid"."version" - FROM "posthog_persondistinctid" - WHERE ("posthog_persondistinctid"."id" IN - (SELECT U0."id" - FROM "posthog_persondistinctid" U0 - WHERE U0."person_id" = "posthog_persondistinctid"."person_id" - LIMIT 3) - AND "posthog_persondistinctid"."person_id" IN (1, - 2, - 3, - 4, - 5 /* ... */)) + OFFSET 4 ' --- # name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.9 @@ -909,7 +728,28 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ((("posthog_person"."properties" -> 'group') = '"none"' + AND "posthog_person"."properties" ? 'group' + AND NOT (("posthog_person"."properties" -> 'group') = 'null')) + OR (("posthog_person"."properties" -> 'group2') IN ('1', + '2', + '3') + AND "posthog_person"."properties" ? 'group2' + AND NOT (("posthog_person"."properties" -> 'group2') = 'null')) + OR EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U0 + WHERE (U0."cohort_id" = 2 + AND U0."cohort_id" = 2 + AND U0."person_id" = "posthog_person"."id") + LIMIT 1) + OR (("posthog_person"."properties" -> 'does-not-exist') = '"none"' + AND "posthog_person"."properties" ? 'does-not-exist' + AND NOT (("posthog_person"."properties" -> 'does-not-exist') = 'null')) + OR (("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')))) ORDER BY "posthog_person"."id" ASC LIMIT 1000 ' @@ -1023,7 +863,28 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ((("posthog_person"."properties" -> 'group') = '"none"' + AND "posthog_person"."properties" ? 'group' + AND NOT (("posthog_person"."properties" -> 'group') = 'null')) + OR (("posthog_person"."properties" -> 'group2') IN ('1', + '2', + '3') + AND "posthog_person"."properties" ? 'group2' + AND NOT (("posthog_person"."properties" -> 'group2') = 'null')) + OR EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U0 + WHERE (U0."cohort_id" = 2 + AND U0."cohort_id" = 2 + AND U0."person_id" = "posthog_person"."id") + LIMIT 1) + OR (("posthog_person"."properties" -> 'does-not-exist') = '"none"' + AND "posthog_person"."properties" ? 'does-not-exist' + AND NOT (("posthog_person"."properties" -> 'does-not-exist') = 'null')) + OR (("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')))) ORDER BY "posthog_person"."id" ASC LIMIT 1000 OFFSET 1000 @@ -1096,16 +957,6 @@ ' --- # name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.11 - ' - SELECT ("posthog_person"."properties" -> 'key') IS NOT NULL AS "flag_X_condition_0" - FROM "posthog_person" - INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") - WHERE ("posthog_persondistinctid"."distinct_id" = 'person3' - AND "posthog_persondistinctid"."team_id" = 2 - AND "posthog_person"."team_id" = 2) - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.12 ' SELECT "posthog_person"."id", "posthog_person"."created_at", @@ -1118,13 +969,14 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') IS NOT NULL) ORDER BY "posthog_person"."id" ASC LIMIT 1000 OFFSET 1000 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.13 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.12 ' SELECT "posthog_person"."uuid" FROM "posthog_person" @@ -1139,7 +991,7 @@ LIMIT 1))) ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.14 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.13 ' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -1210,7 +1062,10 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND UPPER(("posthog_person"."properties" ->> 'key')::text) LIKE UPPER('%value%') + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 1000 ' @@ -1248,7 +1103,10 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND UPPER(("posthog_person"."properties" ->> 'key')::text) LIKE UPPER('%value%') + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 1000 OFFSET 1000 @@ -1386,34 +1244,12 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') IS NOT NULL) ORDER BY "posthog_person"."id" ASC LIMIT 1000 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_deleted_flag - ' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."key" = 'some-feature' - AND "posthog_featureflag"."team_id" = 2) - LIMIT 21 - ' ---- # name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag ' SELECT "posthog_featureflag"."id", @@ -1460,83 +1296,7 @@ LIMIT 21 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.10 - ' - SELECT "posthog_team"."id", - "posthog_team"."uuid", - "posthog_team"."organization_id", - "posthog_team"."api_token", - "posthog_team"."app_urls", - "posthog_team"."name", - "posthog_team"."slack_incoming_webhook", - "posthog_team"."created_at", - "posthog_team"."updated_at", - "posthog_team"."anonymize_ips", - "posthog_team"."completed_snippet_onboarding", - "posthog_team"."has_completed_onboarding_for", - "posthog_team"."ingested_event", - "posthog_team"."autocapture_opt_out", - "posthog_team"."autocapture_exceptions_opt_in", - "posthog_team"."autocapture_exceptions_errors_to_ignore", - "posthog_team"."session_recording_opt_in", - "posthog_team"."session_recording_sample_rate", - "posthog_team"."session_recording_minimum_duration_milliseconds", - "posthog_team"."session_recording_linked_flag", - "posthog_team"."session_recording_network_payload_capture_config", - "posthog_team"."capture_console_log_opt_in", - "posthog_team"."capture_performance_opt_in", - "posthog_team"."surveys_opt_in", - "posthog_team"."session_recording_version", - "posthog_team"."signup_token", - "posthog_team"."is_demo", - "posthog_team"."access_control", - "posthog_team"."week_start_day", - "posthog_team"."inject_web_apps", - "posthog_team"."test_account_filters", - "posthog_team"."test_account_filters_default_checked", - "posthog_team"."path_cleaning_filters", - "posthog_team"."timezone", - "posthog_team"."data_attributes", - "posthog_team"."person_display_name_properties", - "posthog_team"."live_events_columns", - "posthog_team"."recording_domains", - "posthog_team"."primary_dashboard_id", - "posthog_team"."extra_settings", - "posthog_team"."correlation_config", - "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" - FROM "posthog_team" - WHERE "posthog_team"."id" = 2 - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.11 - ' - SELECT "posthog_person"."id", - "posthog_person"."created_at", - "posthog_person"."properties_last_updated_at", - "posthog_person"."properties_last_operation", - "posthog_person"."team_id", - "posthog_person"."properties", - "posthog_person"."is_user_id", - "posthog_person"."is_identified", - "posthog_person"."uuid", - "posthog_person"."version" - FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 - ORDER BY "posthog_person"."id" ASC - LIMIT 21 - OFFSET 5000 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.12 +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.2 ' SELECT "posthog_person"."id", "posthog_person"."created_at", @@ -1549,98 +1309,10 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 - ORDER BY "posthog_person"."id" ASC - LIMIT 5000 - OFFSET 5000 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.13 - ' - SELECT "posthog_person"."uuid" - FROM "posthog_person" WHERE ("posthog_person"."team_id" = 2 - AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, - '00000000-0000-0000-0000-000000000001'::uuid /* ... */) - AND NOT (EXISTS - (SELECT (1) AS "a" - FROM "posthog_cohortpeople" U1 - WHERE (U1."cohort_id" = 2 - AND U1."person_id" = "posthog_person"."id") - LIMIT 1))) - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.14 - ' - SELECT "posthog_team"."id", - "posthog_team"."uuid", - "posthog_team"."organization_id", - "posthog_team"."api_token", - "posthog_team"."app_urls", - "posthog_team"."name", - "posthog_team"."slack_incoming_webhook", - "posthog_team"."created_at", - "posthog_team"."updated_at", - "posthog_team"."anonymize_ips", - "posthog_team"."completed_snippet_onboarding", - "posthog_team"."has_completed_onboarding_for", - "posthog_team"."ingested_event", - "posthog_team"."autocapture_opt_out", - "posthog_team"."autocapture_exceptions_opt_in", - "posthog_team"."autocapture_exceptions_errors_to_ignore", - "posthog_team"."session_recording_opt_in", - "posthog_team"."session_recording_sample_rate", - "posthog_team"."session_recording_minimum_duration_milliseconds", - "posthog_team"."session_recording_linked_flag", - "posthog_team"."session_recording_network_payload_capture_config", - "posthog_team"."capture_console_log_opt_in", - "posthog_team"."capture_performance_opt_in", - "posthog_team"."surveys_opt_in", - "posthog_team"."session_recording_version", - "posthog_team"."signup_token", - "posthog_team"."is_demo", - "posthog_team"."access_control", - "posthog_team"."week_start_day", - "posthog_team"."inject_web_apps", - "posthog_team"."test_account_filters", - "posthog_team"."test_account_filters_default_checked", - "posthog_team"."path_cleaning_filters", - "posthog_team"."timezone", - "posthog_team"."data_attributes", - "posthog_team"."person_display_name_properties", - "posthog_team"."live_events_columns", - "posthog_team"."recording_domains", - "posthog_team"."primary_dashboard_id", - "posthog_team"."extra_settings", - "posthog_team"."correlation_config", - "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" - FROM "posthog_team" - WHERE "posthog_team"."id" = 2 - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.2 - ' - SELECT "posthog_person"."id", - "posthog_person"."created_at", - "posthog_person"."properties_last_updated_at", - "posthog_person"."properties_last_operation", - "posthog_person"."team_id", - "posthog_person"."properties", - "posthog_person"."is_user_id", - "posthog_person"."is_identified", - "posthog_person"."uuid", - "posthog_person"."version" - FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 1000 ' @@ -1720,7 +1392,10 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC LIMIT 1000 OFFSET 1000 @@ -1799,161 +1474,6 @@ LIMIT 21 ' --- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag - ' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."key" = 'some-feature3' - AND "posthog_featureflag"."team_id" = 2) - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.1 - ' - SELECT "posthog_cohort"."id", - "posthog_cohort"."name", - "posthog_cohort"."description", - "posthog_cohort"."team_id", - "posthog_cohort"."deleted", - "posthog_cohort"."filters", - "posthog_cohort"."version", - "posthog_cohort"."pending_version", - "posthog_cohort"."count", - "posthog_cohort"."created_by_id", - "posthog_cohort"."created_at", - "posthog_cohort"."is_calculating", - "posthog_cohort"."last_calculation", - "posthog_cohort"."errors_calculating", - "posthog_cohort"."is_static", - "posthog_cohort"."groups" - FROM "posthog_cohort" - WHERE "posthog_cohort"."id" = 2 - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.2 - ' - DECLARE "_django_curs_X" NO SCROLL - CURSOR WITHOUT HOLD - FOR - SELECT "posthog_person"."id", - "posthog_person"."created_at", - "posthog_person"."properties_last_updated_at", - "posthog_person"."properties_last_operation", - "posthog_person"."team_id", - "posthog_person"."properties", - "posthog_person"."is_user_id", - "posthog_person"."is_identified", - "posthog_person"."uuid", - "posthog_person"."version" - FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.3 - ' - SELECT "posthog_persondistinctid"."distinct_id" - FROM "posthog_persondistinctid" - WHERE ("posthog_persondistinctid"."person_id" = 2 - AND "posthog_persondistinctid"."team_id" = 2) - LIMIT 1 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.4 - ' - SELECT "posthog_grouptypemapping"."id", - "posthog_grouptypemapping"."team_id", - "posthog_grouptypemapping"."group_type", - "posthog_grouptypemapping"."group_type_index", - "posthog_grouptypemapping"."name_singular", - "posthog_grouptypemapping"."name_plural" - FROM "posthog_grouptypemapping" - WHERE "posthog_grouptypemapping"."team_id" = 2 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_inactive_flag - ' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."key" = 'some-feature2' - AND "posthog_featureflag"."team_id" = 2) - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_invalid_flags - ' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."key" = 'some-feature' - AND "posthog_featureflag"."team_id" = 2) - LIMIT 21 - ' ---- -# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_non_existing_flag - ' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."key" = 'some-feature2' - AND "posthog_featureflag"."team_id" = 2) - LIMIT 21 - ' ---- # name: TestFeatureFlag.test_creating_static_cohort ' SELECT "posthog_user"."id", @@ -2067,10 +1587,13 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC - LIMIT 1000 - OFFSET 1000 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + LIMIT 10000 + OFFSET 10000 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ ' --- # name: TestFeatureFlag.test_creating_static_cohort.12 @@ -2519,9 +2042,12 @@ "posthog_person"."uuid", "posthog_person"."version" FROM "posthog_person" - WHERE "posthog_person"."team_id" = 2 + WHERE ("posthog_person"."team_id" = 2 + AND ("posthog_person"."properties" -> 'key') = '"value"' + AND "posthog_person"."properties" ? 'key' + AND NOT (("posthog_person"."properties" -> 'key') = 'null')) ORDER BY "posthog_person"."id" ASC - LIMIT 1000 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + LIMIT 10000 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ ' --- # name: TestResiliency.test_feature_flags_v3_with_experience_continuity_working_slow_db diff --git a/posthog/api/test/test_feature_flag.py b/posthog/api/test/test_feature_flag.py index 31f46aabff9a0..9e33e55ca1a51 100644 --- a/posthog/api/test/test_feature_flag.py +++ b/posthog/api/test/test_feature_flag.py @@ -3883,7 +3883,7 @@ def test_creating_static_cohort_iterator(self): ) # Extra queries because each batch adds its own queries - with snapshot_postgres_queries_context(self), self.assertNumQueries(17): + with snapshot_postgres_queries_context(self), self.assertNumQueries(14): get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk, batchsize=2) cohort.refresh_from_db() @@ -3974,8 +3974,8 @@ def test_creating_static_cohort_with_default_person_properties_adjustment(self): name="some cohort2", ) - with snapshot_postgres_queries_context(self), self.assertNumQueries(13): - # need to evaluate flags for person3 using db, because is_set operator can't have defaults added. + with snapshot_postgres_queries_context(self), self.assertNumQueries(9): + # person3 doesn't match filter conditions so is pre-filtered out get_cohort_actors_for_feature_flag(cohort2.pk, "some-feature-new", self.team.pk) cohort2.refresh_from_db() diff --git a/posthog/settings/sentry.py b/posthog/settings/sentry.py index d38d73300f792..208c862c7fa7e 100644 --- a/posthog/settings/sentry.py +++ b/posthog/settings/sentry.py @@ -114,6 +114,14 @@ def traces_sampler(sampling_context: dict) -> float: else: # Default sample rate for Celery tasks return 0.001 # 0.1% + elif op == "queue.task.celery": + task = sampling_context.get("celery_job", {}).get("task") + if task == "posthog.tasks.calculate_cohort.insert_cohort_from_feature_flag": + # sample all cohort calculations via feature flag + return 1 + # Default sample rate + return 0.01 + else: # Default sample rate for everything else return 0.01 # 1% diff --git a/posthog/tasks/calculate_cohort.py b/posthog/tasks/calculate_cohort.py index 066469636dc37..b4ff3a9aff390 100644 --- a/posthog/tasks/calculate_cohort.py +++ b/posthog/tasks/calculate_cohort.py @@ -77,4 +77,4 @@ def insert_cohort_from_insight_filter(cohort_id: int, filter_data: Dict[str, Any def insert_cohort_from_feature_flag(cohort_id: int, flag_key: str, team_id: int) -> None: from posthog.api.cohort import get_cohort_actors_for_feature_flag - get_cohort_actors_for_feature_flag(cohort_id, flag_key, team_id) + get_cohort_actors_for_feature_flag(cohort_id, flag_key, team_id, batchsize=10_000) diff --git a/posthog/warehouse/models/table.py b/posthog/warehouse/models/table.py index 2acf4fb1faf9b..93e6a20e890f2 100644 --- a/posthog/warehouse/models/table.py +++ b/posthog/warehouse/models/table.py @@ -43,6 +43,7 @@ "Array": StringArrayDatabaseField, "Map": StringJSONDatabaseField, "Bool": BooleanDatabaseField, + "Decimal": IntegerDatabaseField, } ExtractErrors = { diff --git a/tsconfig.json b/tsconfig.json index d00e7af6a118b..ef6815e5be0d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "@posthog/lemon-ui": ["@posthog/lemon-ui/src/index"], "@posthog/lemon-ui/*": ["@posthog/lemon-ui/src/*"], "storybook/*": ["../.storybook/*"], + "@posthog/ee/exports": ["../ee/exports", "@posthog/ee/exports"], "~/*": ["src/*"], "public/*": ["public/*"] }, @@ -33,7 +34,7 @@ "suppressImplicitAnyIndexErrors": true, // Index objects by number "lib": ["dom", "es2019"] }, - "include": ["frontend/**/*", ".storybook/**/*"], + "include": ["frontend/**/*", ".storybook/**/*", "ee/frontend/**/*"], "exclude": ["frontend/dist/**/*"], "ts-node": { "compilerOptions": { diff --git a/webpack.config.js b/webpack.config.js index afa086a96c11c..aef12b7ab0c4b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -56,6 +56,10 @@ function createEntry(entry) { scenes: path.resolve(__dirname, 'frontend', 'src', 'scenes'), '@posthog/apps-common': path.resolve(__dirname, 'frontend', '@posthog', 'apps-common', 'src'), '@posthog/lemon-ui': path.resolve(__dirname, 'frontend', '@posthog', 'lemon-ui', 'src'), + '@posthog/ee/exports': [ + path.resolve(__dirname, 'ee', 'frontend', 'exports'), + path.resolve(__dirname, 'frontend', '@posthog', 'ee', 'exports'), + ], storybook: path.resolve(__dirname, '.storybook'), types: path.resolve(__dirname, 'frontend', 'types'), public: path.resolve(__dirname, 'frontend', 'public'),